@keyfunc/keybricks-ds 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DEPLOY.md CHANGED
@@ -55,7 +55,7 @@ deploys the documentation site. Watch it under *Build → Pipelines*.
55
55
  **5. Cut the first release.**
56
56
 
57
57
  ```bash
58
- git tag v2.4.2 && git push --tags
58
+ git tag v2.4.4 && git push --tags
59
59
  ```
60
60
 
61
61
  That tag runs `generated:check` → `version:check` → `npm publish`. Confirm under *Deploy →
@@ -130,6 +130,28 @@ git push && git push --tags
130
130
  `.gitlab-ci.yml` then runs: `generated:check` → `version:check` (tag must equal
131
131
  `package.json` version) → `npm publish` → `pages`.
132
132
 
133
+ **Two sources of change.** The design-system project (where releases are packed) and this
134
+ repo can both move: app developers push components straight to `main` (2.4.0 → 2.4.2 carried
135
+ `RelativeTimePicker` that way). So a release is always two steps, never one:
136
+
137
+ ```bash
138
+ # step 1 — look before you overwrite: what does GitLab have that the package was not built from?
139
+ git fetch origin && git log --oneline --stat HEAD..origin/main
140
+ # anything listed → bring those files into the design-system project first (onto the system
141
+ # rules, into the docs site, tests and changelog), bump the version past origin/main's
142
+ # package.json, and pack again.
143
+
144
+ # step 2 — release from a clean checkout of origin/main, so the package is the whole tree
145
+ git fetch origin && git reset --hard origin/main
146
+ # copy the unpacked package over the working tree, then:
147
+ npm run build && npm run check && git add -A && git commit -m "<version>: …" && git push && git tag v<version> && git push --tags
148
+ ```
149
+
150
+ A rejected push (`fetch first`) means step 1 was skipped. Do not resolve it with `git pull`: a
151
+ half-merged package.json is worse than starting step 1 over. Teammate commits are welcome, but a
152
+ component only counts as part of the system once it has an example page, a test and a changelog
153
+ line — that is what step 1 is for.
154
+
133
155
  **Changelog.** Every release gets an entry at the top of `changelog.js` (version, ISO date, and
134
156
  items tagged `new` / `improved` / `fixed` / `breaking` / `removed`, each in English, Chinese and
135
157
  Japanese). The documentation site renders it as the *Changelog* section under Overview. While a
@@ -162,7 +184,7 @@ GitLab injects `CI_JOB_TOKEN` — use that instead and no secret is stored anywh
162
184
  **Install — straight from git** (no registry, no publish; works today)
163
185
 
164
186
  ```json
165
- "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.4.2"
187
+ "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.4.4"
166
188
  ```
167
189
 
168
190
  Because the package has no build step, a git dependency needs no `prepare` script — npm
@@ -212,7 +234,7 @@ semantic layer. That is what makes dark mode cost nothing.
212
234
  ## Runtime checks
213
235
 
214
236
  `_tests/` holds browser pages that exercise the real components: `react-runtime.html` (the compiled
215
- bundle), `table-selection.html` (Table loaded from source: checkbox spacing at every width, code chips, two-line rows, sorting), `smart-table-density.html` (SmartTable from source: default / compact rows, the Row height menu, controlled density), `relative-time-picker.html` (RelativeTimePicker from source: helpers, tiers, dialog, keyboard), `vue-runtime.html` (the `.vue` files, loaded with vue3-sfc-loader), `cards-interaction*.html`
237
+ bundle), `table-selection.html` (Table loaded from source: checkbox spacing at every width, code chips, two-line rows, sorting), `smart-table-density.html` (SmartTable from source: default / compact rows, the Row height menu, controlled density), `relative-time-picker.html` (RelativeTimePicker from source: helpers, tiers, dialog, keyboard), `tree-reorder.html` (Tree from source: insert-between on leaves and folder edges, self/subtree rejection), `vue-runtime.html` (the `.vue` files, loaded with vue3-sfc-loader), `cards-interaction*.html`
216
238
  (clicks through the documentation demos) and `icon-rules.html` (the Keybricks glyphs against the drawing rules). Serve the repo root (`npx serve .`), open a page and read the
217
239
  console — every line starts with PASS or FAIL. Run them after changing a component in either framework.
218
240
  `measure-cards.html` is a tool, not a test: it measures each documentation page at its card width so the
package/DESIGN.md CHANGED
@@ -322,7 +322,11 @@ Where each tier belongs:
322
322
  `SmartTable` adds a **row density** on top of the tier: `default` rows are the tier's `--bar-h`
323
323
  (36 / 40 / 48) with `--cell-px`; `compact` rows are the tier's `--control-h` (28 / 32 / 40 — the
324
324
  same as a Spreadsheet cell) with the Spreadsheet's 8px cell padding — `--cell-px` is already 8 at
325
- sm, so there only the height changes. Header and footer rows follow; the toolbar does not. Users switch it from the toolbar's *Row height* menu (a single-choice menu); `density`
325
+ sm, so there only the height changes. Header and footer rows follow; the toolbar does not.
326
+ Both grids freeze their edges the same way: the header row is `position: sticky; top: 0` and the
327
+ row-number column `position: sticky; left: 0` inside the scroll area, the corner cell on both axes,
328
+ stacked corner > header > row numbers > body. Frozen cells keep the `--bg-inset` fill so body
329
+ cells never show through, and the frozen edge is a `--border-subtle` hairline — at every tier and density. Users switch it from the toolbar's *Row height* menu (a single-choice menu); `density`
326
330
  controls it, `defaultDensity` seeds it. Pills (20), checkboxes (16) and avatars (20) all centre in
327
331
  a 28px row. There is no taller option: long text truncates in every density.
328
332
  - **md** — everything else: app toolbars, filter bars, tables, long forms, settings pages.
@@ -424,10 +428,18 @@ An indicator appears **only where a drop is legal**, and its shape names the ope
424
428
 
425
429
  - **Move into a container** — the whole target row fills `--soft-bg` with a 2px
426
430
  `--action-primary` outline. Never a half-height box: the target is the row, not an edge.
427
- - **Insert between** — a 2px `--action-primary` rule inset at the row's top or bottom edge.
428
- Only where ordering is a real operation (`Tree` opts in with `reorder`).
429
- - **Nothing** — hovering an illegal target (a file, a disabled row, a node's own subtree)
430
- shows no indicator at all. An indicator for a drop that would be rejected is a lie.
431
+ - **Insert between** — an insert marker: a 2px `--action-primary` line with a 6px dot of the
432
+ same colour at its left end (the Finder / VS Code shape). It is its own element, not a shadow
433
+ on the row: it sits centred on the gap between rows, starts at the target row's content indent
434
+ (the chevron column, `8 + depth × 18`, which is also the guide-line elbow under `showLine`) and
435
+ ends at the row's right padding — so the line says which level the item lands on. The row keeps
436
+ its own hover or selected fill underneath. Only where ordering is a real operation (`Tree` opts
437
+ in with `reorder`). On a folder the
438
+ outer 20% of the row's height means before / after and the middle 60% means inside; a leaf
439
+ has no inside, so its whole height splits in half — top half before, bottom half after — and
440
+ draws the same rule. So under `reorder` files can be reordered among themselves.
441
+ - **Nothing** — hovering an illegal target (a file without `reorder`, a disabled row, a node's
442
+ own subtree) shows no indicator at all. An indicator for a drop that would be rejected is a lie.
431
443
  - **Connecting two things** (pipeline ports) — the source handle stays lit, a dashed
432
444
  `--action-primary` bezier follows the cursor, and the reachable end point lights and swaps
433
445
  its glyph for a crosshair. Snapping is by distance, not by hover, so the wire lands even
package/INTEGRATION.md CHANGED
@@ -31,7 +31,7 @@ Not using a registry yet? A git dependency works with no publishing at all — t
31
31
  has no build step, so npm can use the checkout as-is:
32
32
 
33
33
  ```json
34
- "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.4.2"
34
+ "@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v2.4.4"
35
35
  ```
36
36
 
37
37
  **Source-only package.** `.jsx` and `.vue` files are published as authored, so your
@@ -226,7 +226,7 @@ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
226
226
  | Component | What it's for |
227
227
  | --- | --- |
228
228
  | `Table` | The standard table (renamed from `DataTable` in 1.0.6; the old name still works as a deprecated alias from the package entry until 3.0, but file imports must point at `data/Table.jsx` / `Table.vue`): sortable headers, per-column render functions, alignment, selection. It is a size region — rows are 32 / 40 / 44px and cells share the tier's text; `size="sm"` (or the 1.x `dense`) for preview grids. Put it in a rounded box with `bordered` (it draws its own frame), or `framed` when it sits flush inside a `Card pad={false}` — never wrap it in your own bordered div, the header rule doubles against the frame. `empty` (Vue: `#empty` slot) renders inside the table when there are no rows. `resizable` adds a short divider between header cells — hover darkens it, drag sets the width; pass `columnWidths` + `onColumnResize` to persist. |
229
- | `SmartTable` | Notion/Airtable-style table — typed field columns, inline editing, row hover actions. |
229
+ | `SmartTable` | Notion/Airtable-style table — typed field columns, inline editing, row hover actions, default/compact row density, frozen header row and row-number column (like `Spreadsheet`). |
230
230
  | `Spreadsheet` | Excel-style grid: formula bar, frozen headers, range selection, `=SUM()` evaluation, sheet tabs. |
231
231
  | `FilterBar` | List-report filter bar with variant title, wrapping filter fields, and Adapt Filters / Go. |
232
232
  | `ListRow` | One row of a list view — icon, title, meta, trailing actions. |
@@ -245,7 +245,7 @@ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
245
245
  | `PageHeading` | Page title with optional subtitle and right-aligned actions. |
246
246
  | `PageToolbar` | Secondary bar under the heading: filters, view toggles, bulk actions. |
247
247
  | `Breadcrumb` | Path trail; collapses to an ellipsis dropdown past `maxItems`. |
248
- | `Tree` | Generic nested tree: cascading checkboxes with indeterminate parents (`checkStrictly` opts out), disabled nodes vs. locked checkboxes, `showLine` guides, async `loadData`, search highlight, drag to move into a folder, full keyboard nav. |
248
+ | `Tree` | Generic nested tree: cascading checkboxes with indeterminate parents (`checkStrictly` opts out), disabled nodes vs. locked checkboxes, `showLine` guides, async `loadData`, search highlight, drag to move into a folder (`reorder` adds insert-between on folder edges and between leaves), full keyboard nav. |
249
249
  | `DocTree` | Document-oriented tree with file-type glyph hues. |
250
250
  | `ThemeToggle` | Light/dark/auto switch. `applyStoredTheme()` restores the choice on load. |
251
251
 
@@ -520,6 +520,15 @@ Pin a minor range and update deliberately:
520
520
  - **Tokens outside CSS.** `tokens.json` / `tokens.js` / `tokens.d.ts` gained the tier tokens
521
521
  (`--control-*`, `--bar-h`, `--group-gap`, …) — 352 light tokens in all (2.0.2 adds `--danger-soft-hover`).
522
522
 
523
+ ### 2.4.4
524
+
525
+ - **`.npmrc`** (added on `main` in `817e86d`) now reads the npmjs.org token from the `NPM_TOKEN` environment variable (`${NPM_TOKEN?}` — empty when unset, so builds never fail) instead of storing it in the repository. Set `NPM_TOKEN` locally if you need it; the GitLab release uses `CI_JOB_TOKEN` and does not.
526
+ - **`Tree` reorder between leaves** — with `reorder`, a leaf row is now an insert target over its whole height (top half → `before`, bottom half → `after`) and shows the same insert marker as a folder edge; folders keep the 20 / 60 / 20 before / inside / after split. `onDrop({ dragKey, dropKey, position, treeData })` is unchanged; drops onto the dragged node or into its own subtree are still ignored, and `inside` on a leaf is rejected. The before / after indicator is now an insert marker — a separate 2px `--action-primary` line with a 6px dot at its left end, centred on the gap between rows, from the target row's indent (`8 + depth × 18`) to its right padding — instead of an inset shadow on the row; the row keeps its hover / selected fill, and only `inside` fills with `--soft-bg` + outline. Rows are now `position: relative`. React and Vue.
527
+
528
+ ### 2.4.3
529
+
530
+ - **`SmartTable` frozen edges** — the field header row is `position: sticky; top: 0` and the 40px row-number column `position: sticky; left: 0` inside the scroll area (`height` sets it), the corner cell on both axes; stacked corner > header > row numbers > body, `--bg-inset` fill kept on sticky cells, `--border-subtle` hairline on the frozen edge. Works at every tier and both densities. No API change; give the grid a `height` to get vertical scrolling.
531
+
523
532
  ### 2.4.2
524
533
 
525
534
  - **New `RelativeTimePicker`** (forms) — the relative-time filter from the product, contributed by the app team and brought onto the system rules: inline token styling (the separate `relative-time.css` is gone), `Field` labels in the dialog, `data-field` hover, `.5` disabled opacity like every other field. Unit labels may be `{ one, other }` and follow `Intl.PluralRules` of `locale` ("1 hour ago" / "2 hours ago"; a CJK label stays one string); the amount field is a plain numeric text field (`inputmode="numeric"`, no native spinners). Props unchanged: `value` / `onChange(value, date)` (Vue `v-model` + `@change`), `allowUnlimited`, `labels`, `locale`, `referenceDate`, `formatDate`, `formatValue`, `size`. Helpers in `forms/relative-time.js`. Also brought in from the 2.4.1 merge: the app team's original files (`relative-time.js`, types, prompt) are kept; only the styling moved inline.
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Components" viewport="700x900" name="Smart Table (Database Grid)" subtitle="Default and compact row density · Notion/Airtable-style grid — typed fields · hue pills · aggregates · + New" -->
1
+ <!-- @dsCard group="Components" viewport="700x1040" name="Smart Table (Database Grid)" subtitle="Frozen header and row numbers · default and compact density · typed fields · hue pills · aggregates · + New" -->
2
2
  <!DOCTYPE html>
3
3
  <html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
4
4
  <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
@@ -19,21 +19,31 @@ const fields=[
19
19
  {key:"done",label:"Done",type:"checkbox",width:58,aggregate:"count"},
20
20
  {key:"effort",label:"Effort",type:"number",width:68,aggregate:"sum"},
21
21
  {key:"progress",label:"Progress",type:"progress",width:118,aggregate:"avg"},
22
+ {key:"entity",label:"Entity",type:"select",width:96,options:{"HK":"blue","SG":"teal","CN":"purple","JP":"pink"}},
23
+ {key:"notes",label:"Notes",type:"longText",width:220},
22
24
  ];
23
- const rows=[
24
- {task:"Migrate journal import",status:"Live",tags:["Data","Q3"],owner:"Felix Zhang",due:"Aug 18",done:true,effort:8,progress:100},
25
- {task:"FX rounding rule review",status:"In progress",tags:["Finance","Urgent"],owner:"Maria Gomez",due:"Aug 20",done:false,effort:5,progress:60},
26
- {task:"Vendor master dedupe",status:"In progress",tags:["Data"],owner:"Ken Watanabe",due:"Aug 22",done:false,effort:3,progress:45},
27
- {task:"Consolidation dry run",status:"Blocked",tags:["Finance"],owner:"Priya Nair",due:"Aug 25",done:false,effort:13,progress:20},
28
- {task:"Archive 2025 workpapers",status:"Backlog",tags:["Q3"],owner:"Felix Zhang",due:"Sep 02",done:false,effort:2,progress:0},
29
- {task:"Reconciliation alerts",status:"Live",tags:["Data","Urgent"],owner:"Maria Gomez",due:"Aug 15",done:true,effort:5,progress:100},
25
+ const owners=["Felix Zhang","Maria Gomez","Ken Watanabe","Priya Nair"], statuses=["Live","In progress","Blocked","Backlog"], ents=["HK","SG","CN","JP"];
26
+ const base=[
27
+ {task:"Migrate journal import",tags:["Data","Q3"],notes:"Cut over after the August close"},
28
+ {task:"FX rounding rule review",tags:["Finance","Urgent"],notes:"Treasury sign-off pending"},
29
+ {task:"Vendor master dedupe",tags:["Data"],notes:"Fuzzy match on tax ID first"},
30
+ {task:"Consolidation dry run",tags:["Finance"],notes:"Blocked on the IC matching rules"},
31
+ {task:"Archive 2025 workpapers",tags:["Q3"],notes:"Retention policy: 7 years"},
32
+ {task:"Reconciliation alerts",tags:["Data","Urgent"],notes:"Slack channel wired up"},
33
+ {task:"Lease schedule rebuild",tags:["Finance"],notes:"IFRS 16 remeasurement"},
34
+ {task:"Budget template refresh",tags:["Q3"],notes:"Drivers moved to the Assumptions sheet"},
35
+ {task:"Audit evidence export",tags:["Finance","Data"],notes:"PBC list v3"},
36
+ {task:"Cash forecast automation",tags:["Data"],notes:"13-week rolling"},
37
+ {task:"Intercompany netting",tags:["Finance"],notes:"Monthly cycle, day 3"},
38
+ {task:"Chart of accounts cleanup",tags:["Data","Q3"],notes:"Retire 41 unused accounts"},
30
39
  ];
40
+ const rows=base.map((b,i)=>({...b,status:statuses[i%4],owner:owners[i%4],due:"Aug "+(12+i),done:i%3===0,effort:[8,5,3,13,2,5][i%6],progress:[100,60,45,20,0,100][i%6],entity:ents[i%4]}));
31
41
  function Sheet(){return <div>
32
- <SmartTable fields={fields} rows={rows} title="Launch tracker"/>
33
- <div className="hint" style={{marginBottom:20}}>Default density — rows are the tier's bar height (36px at sm). The toolbar's <b>Row height</b> menu switches between Default and Compact.</div>
34
- <SmartTable fields={fields} rows={rows.slice(0,4)} title="Launch tracker — compact" defaultDensity="compact"/>
35
- <div className="hint" style={{marginBottom:20}}>Compact density — rows are the tier's control height (28px at sm, the same as a Spreadsheet cell) with the Spreadsheet's 8px cell padding (the same as sm's <code>--cell-px</code>, so here only the height changes); header and footer follow. <code>density="compact"</code> controls it, <code>defaultDensity</code> just seeds it.</div>
36
- <div className="hint">Typed fields with per-type header icons; select/multi-select render theme-aware hue pills; hover a row number for the expand affordance; double-click text/number/date cells to edit; “+ New” appends a row; the footer aggregates per column (COUNT / SUM / AVG). Scrolls horizontally like Airtable when fields overflow.</div>
42
+ <SmartTable fields={fields} rows={rows} title="Launch tracker" height={300}/>
43
+ <div className="hint" style={{marginBottom:20}}>Frozen edges, like a Spreadsheet: scroll down and the field header stays; scroll right and the row-number column stays; the corner cell holds on both axes. Sticky cells keep the <code>--bg-inset</code> fill and a <code>--border-subtle</code> hairline on the frozen edge. Default density — rows are the tier's bar height (36px at sm). The toolbar's <b>Row height</b> menu switches between Default and Compact.</div>
44
+ <SmartTable fields={fields} rows={rows} title="Launch tracker — compact" defaultDensity="compact" height={240}/>
45
+ <div className="hint" style={{marginBottom:20}}>Compact density — rows are the tier's control height (28px at sm, the same as a Spreadsheet cell) with the Spreadsheet's 8px cell padding (the same as sm's <code>--cell-px</code>, so here only the height changes); header and footer follow, and so do the frozen edges. <code>density="compact"</code> controls it, <code>defaultDensity</code> just seeds it.</div>
46
+ <div className="hint">Typed fields with per-type header icons; select/multi-select render theme-aware hue pills; hover a row number for the expand affordance; double-click text/number/date cells to edit; “+ New” appends a row; the footer aggregates per column (COUNT / SUM / AVG). Without <code>height</code> the grid grows with its rows and only the row-number column freezes.</div>
37
47
  </div>;}
38
48
  ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);
39
49
  if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"SmartTable.jsx","path":"../data/SmartTable.jsx"}]); }, 0);
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Components" viewport="700x2110" name="Tree" subtitle="Cascading checkboxes · disabled nodes · lines · async load · search · drag · keyboard" -->
1
+ <!-- @dsCard group="Components" viewport="700x2820" name="Tree" subtitle="Cascading checkboxes · disabled nodes · lines · async load · search · drag · reorder · keyboard" -->
2
2
  <!DOCTYPE html>
3
3
  <html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
4
4
  <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
@@ -94,6 +94,52 @@ function Draggable(){
94
94
  </div>;
95
95
  }
96
96
 
97
+ const REORDER=[
98
+ {key:"recon",title:"Reconciliation",isLeaf:false,children:[
99
+ {key:"bank",title:"Bank statements.xlsx",hue:"green"},
100
+ {key:"ic",title:"Intercompany balances.xlsx",hue:"green"},
101
+ {key:"ar",title:"AR aging.csv",hue:"blue"},
102
+ {key:"ap",title:"AP aging.csv",hue:"blue"}]},
103
+ {key:"adj",title:"Adjustments.docx",hue:"purple"},
104
+ {key:"memo",title:"Close memo.docx",hue:"purple"},
105
+ ];
106
+ function Reorderable(){
107
+ const [last,setLast]=React.useState(null);
108
+ return <div>
109
+ <span className="lbl">Reorder — leaf rows are insert targets too</span>
110
+ <Tree items={REORDER} draggable reorder defaultExpanded={["recon"]} onDrop={(i)=>setLast(i.dragKey+" → "+i.position+" "+i.dropKey)}/>
111
+ <div className="hint">{last ? <>Moved <code>{last}</code></> : <>With <code>reorder</code>, drag <em>Bank statements</em> below <em>AR aging</em>: a file’s top half means before, its bottom half after. The insert marker — a 2px line with a dot — sits on the gap between rows and starts at the target’s indent. Folders keep their 20% / 60% / 20% split (before / inside / after).</>}</div>
112
+ </div>;
113
+ }
114
+
115
+ /* Frozen drop states for reference: each tree gets a synthetic dragstart + dragover after mount, so the real component draws its own indicator. */
116
+ const DS_ITEMS=[{key:"recon",title:"Reconciliation",isLeaf:false,children:[{key:"bank",title:"Bank statements.xlsx",hue:"green"},{key:"ic",title:"Intercompany.xlsx",hue:"green"},{key:"ar",title:"AR aging.csv",hue:"blue"}]},{key:"memo",title:"Close memo.docx",hue:"purple"}];
117
+ function FrozenDrop({from,to,ratio,label,note}){
118
+ const ref=React.useRef(null);
119
+ React.useEffect(()=>{
120
+ const row=(t)=>[...ref.current.querySelectorAll("[role=treeitem]")].find(r=>r.textContent.trim().startsWith(t));
121
+ const fire=(el,type,y)=>{const r=el.getBoundingClientRect();const e=new Event(type,{bubbles:true,cancelable:true});Object.defineProperty(e,"dataTransfer",{value:{effectAllowed:"move",dropEffect:"move",setData(){},getData(){return "";}}});Object.defineProperty(e,"clientY",{value:r.top+r.height*y});el.dispatchEvent(e);};
122
+ const t1=setTimeout(()=>{fire(row(from),"dragstart",.5);},80);
123
+ const t2=setTimeout(()=>{fire(row(to),"dragover",ratio);},200);
124
+ return ()=>{clearTimeout(t1);clearTimeout(t2);};
125
+ },[]);
126
+ return <div ref={ref}>
127
+ <span className="lbl">{label}</span>
128
+ <Tree items={DS_ITEMS} draggable reorder defaultExpanded={["recon"]} defaultSelected={["ic"]}/>
129
+ <div className="hint">{note}</div>
130
+ </div>;
131
+ }
132
+ function DropStates(){
133
+ return <div style={{gridColumn:"1/-1"}}>
134
+ <span className="lbl" style={{color:"var(--text-heading)",fontWeight:"var(--weight-medium)"}}>Drop states — dragging <em>Close memo</em> (frozen for reference)</span>
135
+ <div style={{display:"grid",gridTemplateColumns:"repeat(3,minmax(0,1fr))",gap:20}}>
136
+ <FrozenDrop from="Close memo" to="Intercompany" ratio={.25} label="Before — top half of a file" note="2px line with a 6px dot, between Bank statements above and this row, starting at the file's indent. The selected row keeps its fill."/>
137
+ <FrozenDrop from="Close memo" to="Intercompany" ratio={.75} label="After — bottom half of a file" note="The same marker on the gap below. Its left end at the child indent says the item lands inside Reconciliation."/>
138
+ <FrozenDrop from="Close memo" to="Reconciliation" ratio={.5} label="Inside — middle 60% of a folder" note="Only “inside” fills the row: --soft-bg with a 2px outline. A folder’s outer 20% draws the before / after marker at the folder’s own indent."/>
139
+ </div>
140
+ </div>;
141
+ }
142
+
97
143
  function AsyncLoad(){
98
144
  const [log,setLog]=React.useState(0);
99
145
  const load=(node)=>new Promise(res=>setTimeout(()=>{
@@ -136,13 +182,13 @@ function Sheet(){return <div className="g">
136
182
  <Basic/><Checkable/>
137
183
  <Strict/><DisabledCase/>
138
184
  <Lines/><Multi/>
139
- <Draggable/><AsyncLoad/>
185
+ <Draggable/><Reorderable/><DropStates/><AsyncLoad/>
140
186
  <Searchable/><WithActions/>
141
187
  <div className="note">
142
188
  <strong>Keyboard.</strong> The tree is a single tab stop. <code>↑ ↓</code> move between visible rows, <code>→</code> opens a branch (or steps into it), <code>←</code> closes it (or jumps to the parent), <code>Enter</code> selects, <code>Space</code> toggles the checkbox. Focus draws the global 2px <code>--focus-ring</code>, never a new colour.
143
189
  <br/><br/><strong>Checkbox semantics.</strong> A parent is checked only when every <em>enabled</em> descendant is checked, and indeterminate when some are — so <code>disabled</code> and <code>disableCheckbox</code> rows never block a parent from completing. Use <code>checkStrictly</code> when a branch is a real selectable value rather than a roll-up of its children.
144
190
  <br/><br/><strong>Selection vs. expansion.</strong> With <code>selectable={"{false}"}</code> a row click only expands — the right default for a checkbox picker. Otherwise a click both selects and expands, matching the file-tree pattern used in the product sidebar.
145
- <br/><br/><strong>Dragging moves a node into a folder.</strong> Files are not drop targets — hovering one shows nothing rather than an indicator for a move that can’t happen — and neither is a folder inside the dragged node’s own subtree. Pass <code>reorder</code> when order matters: a folder’s outer 20% then becomes a thin insert rule above or below it.
191
+ <br/><br/><strong>Dragging moves a node into a folder.</strong> Files are not drop targets — hovering one shows nothing rather than an indicator for a move that can’t happen — and neither is a folder inside the dragged node’s own subtree. Pass <code>reorder</code> when order matters: a folder’s outer 20% and either half of a file then draw an insert marker — a 2px line with a 6px dot on the gap between rows, starting at the target’s indent so the landing level is visible.
146
192
  </div>
147
193
  </div>;}
148
194
  ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);