リサイザブル
stimeo--resizable
WAI-ARIA APG の Splitter パターンに準拠し、PointerCapture を用いた頑強なドラッグ操作とキーボード調節が可能なペイン分割システム。
stimeo--resizable コントローラは、画面領域を伸縮可能なペインに分割するスプリッターの振る舞いを提供します。 PointerCapture API(setPointerCapture)を採用することで、境界スプリッターからマウスが外れたり、画面外までドラッグしてもドラッグ状態を失わない堅牢なリサイズ挙動を誇ります。min/max 値による自動クランプ、境界ダブルクリック時の折りたたみ(トグル)動作、キーボード(矢印キー、Home/End)によるアクセシブルな比率変更、および伸縮結果を CSS 変数(--stimeo--resizable-fraction)として即座に同期・伝播させることで、マークアップやレイアウトデザインの完全な自由度を実現します。
左ペイン
メインのエディタ領域
右ペイン
プレビュー作業領域
キーボード操作
| キー | 動作 |
|---|---|
| ← / ↑ | スプリッターの比率を 10 段階(あるいは 1% 単位)減らし、左(上)側ペインを縮小する。 |
| → / ↓ | スプリッターの比率を 10 段階(あるいは 1% 単位)増やし、左(上)側ペインを拡大する。 |
| Home | スプリッターの比率を許容される最小比率(minValue)に即座に変更する。 |
| End | スプリッターの比率を許容される最大比率(maxValue)に即座に変更する。 |
| Double Click | スプリッターをダブルクリックすると、ペインを折りたたみ(しきい値以下に縮小)、再ダブルクリックで元の比率に復元する。 |
<%# Markup for the resizable (splitter / pane-split) demo.
stimeo--resizable provides PointerCapture dragging, a double-click collapse
toggle, and keyboard adjustment, and exposes the ratio (0..1) on the root element
as the CSS variable --stimeo--resizable-fraction. %>
<div class="resizable-demo-container">
<div
class="resizable-demo"
id="resizable-splitter"
data-controller="stimeo--resizable"
data-stimeo--resizable-min-value="20"
data-stimeo--resizable-max-value="80"
data-stimeo--resizable-value-value="50"
data-stimeo--resizable-step-value="5"
style="--stimeo--resizable-fraction: 0.5;"
>
<div
class="resizable-demo__pane resizable-demo__pane--left"
data-stimeo--resizable-target="primary"
>
<div class="resizable-demo__pane-content">
<h3><%= t("components.resizable.demo.pane_left") %></h3>
<p><%= t("components.resizable.demo.pane_left_body") %></p>
</div>
</div>
<div
class="resizable-demo__handle"
role="separator"
tabindex="0"
aria-label="<%= t("components.resizable.demo.splitter_label") %>"
aria-orientation="vertical"
data-stimeo--resizable-target="separator"
data-action="
pointerdown->stimeo--resizable#onPointerDown
keydown->stimeo--resizable#onKeydown
dblclick->stimeo--resizable#toggle
"
>
<div class="resizable-demo__handle-bar" aria-hidden="true"></div>
</div>
<div
class="resizable-demo__pane resizable-demo__pane--right"
data-stimeo--resizable-target="secondary"
>
<div class="resizable-demo__pane-content">
<h3><%= t("components.resizable.demo.pane_right") %></h3>
<p><%= t("components.resizable.demo.pane_right_body") %></p>
</div>
</div>
</div>
<div class="resizable-demo__controls">
<%# demo.js (a module) wires this up with addEventListener. Inline on*
attributes can't reference a module-scoped function, so they aren't used. %>
<button class="resizable-demo__control-btn" type="button" data-resizable-toggle-direction>
<%= t("components.resizable.demo.toggle_direction") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the resizable demo.
* The split ratio is expressed by the Playground as CSS Grid column/row widths using
* the --stimeo--resizable-fraction variable (set on the root by the library).
*/
.resizable-demo-container {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.resizable-demo {
display: grid;
/* Bind the ratio variable to the Grid column. */
grid-template-columns: calc(var(--stimeo--resizable-fraction, 0.5) * 100%) 8px 1fr;
grid-template-rows: 1fr;
width: 100%;
height: 250px;
background: var(--surface-subtle);
border: 1px solid var(--border-default);
border-radius: 12px;
overflow: hidden;
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
}
/* Vertical-split class toggle. */
.resizable-demo--vertical {
grid-template-columns: 1fr;
/* Bind the ratio variable to the Grid row. */
grid-template-rows: calc(var(--stimeo--resizable-fraction, 0.5) * 100%) 8px 1fr;
}
.resizable-demo__pane {
background: var(--surface-card);
overflow: hidden;
}
.resizable-demo__pane--left {
background: var(--surface-card);
}
.resizable-demo__pane--right {
background: var(--surface-subtle);
}
.resizable-demo__pane-content {
padding: 1.25rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.resizable-demo__pane-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
color: var(--fg);
}
.resizable-demo__pane-content p {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
/* Splitter handle. */
.resizable-demo__handle {
display: flex;
align-items: center;
justify-content: center;
background: var(--border-strong);
cursor: col-resize;
position: relative;
transition: background 0.15s ease;
outline: none;
}
.resizable-demo--vertical .resizable-demo__handle {
cursor: row-resize;
}
.resizable-demo__handle:hover,
.resizable-demo__handle:focus {
background: var(--accent);
}
.resizable-demo__handle-bar {
width: 2px;
height: 24px;
background: var(--surface-card);
border-radius: 1px;
}
.resizable-demo--vertical .resizable-demo__handle-bar {
width: 24px;
height: 2px;
}
.resizable-demo__controls {
display: flex;
justify-content: flex-end;
}
.resizable-demo__control-btn {
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--surface-card);
color: var(--fg);
cursor: pointer;
transition: all 0.15s ease;
}
.resizable-demo__control-btn:hover {
background: var(--surface-subtle);
border-color: var(--border-interactive);
}
// Demo script that toggles the split layout direction (horizontal / vertical).
// This file loads as a module (type="module"), so top-level functions are not
// global — wire interactions with addEventListener, not an inline onclick.
const toggleButton = document.querySelector("[data-resizable-toggle-direction]");
toggleButton?.addEventListener("click", () => {
const el = document.getElementById("resizable-splitter");
if (!el) return;
const isVertical = el.classList.toggle("resizable-demo--vertical");
const separator = el.querySelector('[role="separator"]');
if (separator) {
// Horizontal split (column layout): the divider is "vertical".
// Vertical split (row layout): the divider is "horizontal".
separator.setAttribute("aria-orientation", isVertical ? "horizontal" : "vertical");
}
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--resizable"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
primary
必須
|
分割比でサイズが決まる主ペイン。 | data-stimeo--resizable-target="primary" |
secondary
必須
|
残り領域を占める副ペイン。 | data-stimeo--resizable-target="secondary" |
separator
必須
|
ドラッグ可能な分割ハンドル(role=separator)。aria-value*状態を持つ。 |
data-stimeo--resizable-target="separator" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
min
|
分割割合の最小値(%、既定0)。 | data-stimeo--resizable-min-value |
max
|
分割割合の最大値(%、既定100)。 | data-stimeo--resizable-max-value |
step
|
矢印キー調整の刻み幅(%、既定1)。 | data-stimeo--resizable-step-value |
value
|
現在の分割割合。min/maxで丸められる(%、既定50)。 | data-stimeo--resizable-value-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
onKeydown
|
矢印キー(向きに応じて)で分割を調整、Home/Endでmin/max、Enterでトグルする。 | stimeo--resizable#onKeydown |
onPointerDown
|
セパレーターのポインタードラッグを開始し、capture取得とフォーカスを行う。 | stimeo--resizable#onPointerDown |
toggle
|
ペインをminまで折りたたむ、または前回値/maxへ復元する。 | stimeo--resizable#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
分割値が変わると発火する。detailにvalueとfractionを含む。 | stimeo--resizable:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-valuenow |
スプリッター要素 (handle) | 現在の左(上)側ペインのパーセンテージ比率 (0 - 100) がリアルタイムに格納される。 |
aria-valuemin |
スプリッター要素 (handle) | スプリッターの許容最小比率(minValue)を示す。 |
aria-valuemax |
スプリッター要素 (handle) | スプリッターの許容最大比率(maxValue)を示す。 |
--stimeo--resizable-fraction |
ルート要素 (resizable) | 左(上)側ペインが全体に占める比率が "0.0" から "1.0" の小数値として同期される。CSS の grid-template-columns 等で直接ペイン幅としてバインドされる。 |