cssints 0.21.0 → 0.23.0
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/dist/vite.mjs +1 -1
- package/docs/API.md +77 -17
- package/package.json +2 -2
package/dist/vite.mjs
CHANGED
|
@@ -804,7 +804,7 @@ function cssints({ inner = false, exclude, devtools = false, opencode = false, e
|
|
|
804
804
|
if (!FILE_RE.test(id) || excluded(id)) return void 0;
|
|
805
805
|
if (!marked(code, id)) {
|
|
806
806
|
if (ctx.environment.name === "client") yield* drop(id);
|
|
807
|
-
return ctx.environment.name === "ssr" && (inner || !build) && /[jt]sx$/.test(id)
|
|
807
|
+
return ctx.environment.name === "ssr" && (inner || !build) && /[jt]sx$/.test(id) ? yield* inertJsx(code, id) : void 0;
|
|
808
808
|
}
|
|
809
809
|
if (ctx.environment.name === "ssr" && (inner || !build)) {
|
|
810
810
|
const out = yield* instrument(code, id, false);
|
package/docs/API.md
CHANGED
|
@@ -1298,8 +1298,8 @@ createConfirm(options?: { id?: string | undefined; onOpenChange?(open: boolean):
|
|
|
1298
1298
|
Two live regions with no APG pattern and no Quint spec: `createStatus` is one polite message whose text is replaced, `createToast` queues toasts whose countdown pauses. Their oracle is `packages/headless/test/toast.browser.test.tsx`.
|
|
1299
1299
|
|
|
1300
1300
|
```ts skip
|
|
1301
|
-
createStatus(options?: { id?: string | undefined }): {
|
|
1302
|
-
message(): string | undefined;
|
|
1301
|
+
createStatus(options?: { id?: string | undefined; from?: (() => string | undefined) | undefined }): {
|
|
1302
|
+
message(): string | undefined; // from() when the host controls the text, else the last show
|
|
1303
1303
|
show(text: string): void; // replaces the text; nothing is queued
|
|
1304
1304
|
clear(): void;
|
|
1305
1305
|
statusProps: { id; role: "status"; "aria-live": "polite"; "aria-atomic": "true" };
|
|
@@ -1332,7 +1332,7 @@ createToast(options?: {
|
|
|
1332
1332
|
};
|
|
1333
1333
|
```
|
|
1334
1334
|
|
|
1335
|
-
`createStatus` renders one `role=status` region that the host keeps mounted; `show()` replaces its text, so a burst of "Link copied" / "Saved" reads as the last state, not as a queue. A status is never for errors: an error is `createAlert` (`role=alert`), and a titled message with a timeout is a toast.
|
|
1335
|
+
`createStatus` renders one `role=status` region that the host keeps mounted; `show()` replaces its text, so a burst of "Link copied" / "Saved" reads as the last state, not as a queue. A status is never for errors: an error is `createAlert` (`role=alert`), and a titled message with a timeout is a toast. Both take `from: () => text`, which makes the text the host's own signal (`createAlert({ from: () => error() })`): no effect mirrors it into `show()`, and clearing the signal unmounts the alert.
|
|
1336
1336
|
|
|
1337
1337
|
A toast is its own live region inside the `role=region` landmark: `role=alert` for `tone: "error"` (assertive) and `role=status` otherwise (polite). The host renders the `title` and `description` handed to `show` inside that element, so the toast's own text is the announcement — `aria-atomic` reads all of it, a close button's label included, so keep a toast to its title and description and give a close button no more than an icon with a label. The role sits on the toast rather than on one container per tone: the container is the `region` landmark, so it cannot also be the live region, and a role per toast needs no second region from the host. One timer per toast honours `timeout`; hover pauses it when `pauseOnHover` is true and focus always pauses, and both resume with the time that was left rather than restarting the deadline. `max` drops the oldest toast past the ceiling, and `onDismiss` fires for every toast that leaves. F6 into the region is the host's handler (hotkeys are a separate bean, cssints-h0d5): give `regionProps` an element with `tabindex="-1"` and focus it, remembering where focus was.
|
|
1338
1338
|
|
|
@@ -1432,7 +1432,7 @@ A data table on a **native `<table>`**: sortable column headers and row selectio
|
|
|
1432
1432
|
```ts skip
|
|
1433
1433
|
createTable(options: {
|
|
1434
1434
|
columns: ReadonlyArray<{ key: string; sortable?: boolean; numeric?: boolean; abbr?: string | undefined }>; // sortable defaults true
|
|
1435
|
-
rows: ReadonlyArray<string
|
|
1435
|
+
rows: ReadonlyArray<string> | (() => ReadonlyArray<string>); // row ids in render order; a getter is read lazily
|
|
1436
1436
|
selection?: "none" | "single" | "multiple" | undefined; // "none"
|
|
1437
1437
|
sortCycle?: "tri-state" | "toggle" | undefined; // "tri-state": ascending -> descending -> unsorted
|
|
1438
1438
|
sort?: TableSort | null | undefined; // controlled; null = unsorted, undefined = uncontrolled
|
|
@@ -1465,7 +1465,7 @@ createTable(options: {
|
|
|
1465
1465
|
|
|
1466
1466
|
`aria-sort` is carried by the sorted column only; a sortable header that is not sorted has none (ARIA's "not sorted" default), and a `sortable: false` column gets no button from `headerButtonProps` at all, so it is not a tab stop. The header button cycles `ascending` -> `descending` -> unsorted by default (so the table can be put back in its own order) and `ascending` <-> `descending` with `sortCycle: "toggle"` (what the APG grid example does); a new column always starts `ascending`, and only one is sorted. Sorting is the host's data: the primitive carries the descriptor and announces it, it never reorders a row.
|
|
1467
1467
|
|
|
1468
|
-
Selection is over ids, so it survives the host's re-sort. `rowProps` carries `aria-selected` and a click that selects; `toggleRow` / `toggleAll` are the programmatic path, and the row's checkbox stops its click from reaching the row so the two controls never toggle the same row twice. `selectAllProps` and `rowCheckboxProps` are `undefined` unless `selection` is `"multiple"` (one control over one selection is meaningless), and the select-all is a `role="checkbox"` rather than a native `input` because the mixed state a screen reader must hear is `aria-checked="mixed"`, not the DOM-only `indeterminate` property. It toggles the rows the table renders: a windowed table toggles its window and keeps the rest of the selection. A table that renders all of its rows and columns carries no `aria-rowcount` / `aria-colcount` (the markup already says it); pass them, and every cell gets its `aria-colindex`, only when the host windows. `numeric` is a layout hint for the host's skin: HTML has no attribute for it.
|
|
1468
|
+
`rows` may be a getter (`() => rows()`): it is read at attribute-read time, so a table whose rows arrive later keeps its sort and its selection instead of being rebuilt, and a selection made before the rows landed applies once they do. Selection is over ids, so it survives the host's re-sort. `rowProps` carries `aria-selected` and a click that selects; `toggleRow` / `toggleAll` are the programmatic path, and the row's checkbox stops its click from reaching the row so the two controls never toggle the same row twice. `selectAllProps` and `rowCheckboxProps` are `undefined` unless `selection` is `"multiple"` (one control over one selection is meaningless), and the select-all is a `role="checkbox"` rather than a native `input` because the mixed state a screen reader must hear is `aria-checked="mixed"`, not the DOM-only `indeterminate` property. It toggles the rows the table renders: a windowed table toggles its window and keeps the rest of the selection. A table that renders all of its rows and columns carries no `aria-rowcount` / `aria-colcount` (the markup already says it); pass them, and every cell gets its `aria-colindex`, only when the host windows. `numeric` is a layout hint for the host's skin: HTML has no attribute for it.
|
|
1469
1469
|
|
|
1470
1470
|
### Headless: `createTabs` (`@cssints/headless`)
|
|
1471
1471
|
|
|
@@ -1477,7 +1477,7 @@ Close and rename are an **extension** (bean `cssints-gdv2`, the docs playground'
|
|
|
1477
1477
|
|
|
1478
1478
|
```ts skip
|
|
1479
1479
|
createTabs(options?: {
|
|
1480
|
-
...; // the plain tabs: items, value, defaultSelected, orientation, activation, disabled, focusablePanels, id, onValueChange
|
|
1480
|
+
...; // the plain tabs: items, value, defaultSelected, orientation, activation, disabled, focusablePanels, id, tabId, panelId, onValueChange
|
|
1481
1481
|
closable?: boolean; // false: closeProps and the Delete key; the host removes from onClose
|
|
1482
1482
|
renamable?: boolean; // false: a double click edits the label inline; the host renames from onRename
|
|
1483
1483
|
label?(index: number): string | undefined; // the tab's name: the buttons' labels and the rename draft
|
|
@@ -1501,6 +1501,7 @@ createTabs(options?: {
|
|
|
1501
1501
|
|
|
1502
1502
|
- **Close.** `closeProps(index)` is the tab's close button: `aria-label="Close <label>"` and `tabindex=-1`, because a tablist's tab stops are its tabs; the keyboard path is Delete on the focused tab. Focus after a close goes to the tab that slid into the closed one's place, or to the previous tab when the last one closed: `focusAfterClose(index)` names that index in the shortened list (call it before the host removes the tab). Closing the last remaining tab is refused, as the spec refuses an empty tablist; a closed tab that was selected asks for the tab that takes the focus.
|
|
1503
1503
|
- **Rename.** A double click begins; the tab renders `inputProps` instead of its label. Enter commits, Escape reverts, blur commits, and a label that trims to nothing cancels. While editing, the input owns the keys: the tablist's roving does not move. `begin(index)` is the programmatic start, for a tab the host has just added (append it, then `focusTab(index)` and `begin(index)`).
|
|
1504
|
+
- **Ids.** `tabId(index)` and `panelId(index)` replace the generated `${id}-tab-<index>` / `${id}-tabpanel-<index>`, and `aria-controls` / `aria-labelledby` follow them. Use them when the host's markup or a deep link already names a panel (`#logs`): the alternative is a second id on the same element, which no attribute can carry.
|
|
1504
1505
|
- **The host's list.** `items` is read from the option on every read, so the bounds, the roving tab stop and `firstEnabled` follow a host that shrinks its list. `label` is read lazily too, so a renamed tab renames its buttons with it.
|
|
1505
1506
|
- The spec models a `deletable` flavour (Delete, focus to the next tab, the last refused) but neither a rename nor the callbacks, so the browser test, not the model, holds the extension.
|
|
1506
1507
|
|
|
@@ -1649,9 +1650,12 @@ createMenu(options: {
|
|
|
1649
1650
|
open?: boolean | undefined; // controlled: the host owns the open state
|
|
1650
1651
|
defaultOpen?: boolean; // uncontrolled starting state
|
|
1651
1652
|
onOpenChange?(open: boolean): void; // a request the host may ignore
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1653
|
+
checked?: ReadonlyArray<number> | undefined; // controlled, readable: the checked checkbox item indices
|
|
1654
|
+
choice?: number | undefined; // controlled, readable: the chosen radio item, -1 for none
|
|
1655
|
+
value?: number | undefined; // controlled, packed view of the same state (checkboxItem0 * 5 + radioChoice, 1-based)
|
|
1656
|
+
defaultValue?: number; // the uncontrolled packed state at mount
|
|
1657
|
+
onCheckedValueChange?(value: number): void; // a packed request the host may ignore
|
|
1658
|
+
onCheckedValuesChange?(checked: ReadonlyArray<number>, choice: number): void; // the readable request
|
|
1655
1659
|
triggerMode?: "outside" | "held" | undefined; // where the focus sits while the menu is closed
|
|
1656
1660
|
disabled?: ReadonlyArray<number>; // focusable, never activated
|
|
1657
1661
|
id?: string | undefined;
|
|
@@ -1660,6 +1664,9 @@ createMenu(options: {
|
|
|
1660
1664
|
open(): boolean;
|
|
1661
1665
|
focusLevel(): 0 | 1 | 2; // 0 outside, 1 on the trigger, 2 on an item
|
|
1662
1666
|
focusIndex(): number;
|
|
1667
|
+
checked(): ReadonlyArray<number>; // the readable view of the checked state
|
|
1668
|
+
choice(): number; // the readable view of the radio choice
|
|
1669
|
+
value(): number; // the packed view of the same state
|
|
1663
1670
|
openMenu(): void; // a request, like every intent
|
|
1664
1671
|
closeMenu(): void;
|
|
1665
1672
|
keyDown(key: string): void;
|
|
@@ -1667,6 +1674,8 @@ createMenu(options: {
|
|
|
1667
1674
|
};
|
|
1668
1675
|
```
|
|
1669
1676
|
|
|
1677
|
+
**The checked state has one store and two views.** The readable pair (`checked` + `choice`) is the store; `value` is derived from it as `menu-checkable.qnt`'s `checked(wrap, choice)` (one checkbox bit * 5 + the radio). The pack has one checkbox bit, so only the readable pair can name a checkbox beyond item 0. A host may own either view; each has its request (`onCheckedValuesChange(checked, choice)` and `onCheckedValueChange(value)`), and where both views are given the readable pair wins. `defaultChecked` / `defaultChoice` start the pair and `defaultValue` the packed side.
|
|
1678
|
+
|
|
1670
1679
|
**The open state is a request/commit pair**, like every other controlled primitive: `open` (+ `onOpenChange`) makes the host own it, `defaultOpen` starts it. Every intent that opens or closes — the trigger's click and its keys, Escape, an item's activation, Tab, an outside click in a kit, the programmatic `openMenu` / `closeMenu` — is a request, so a host that holds `open` keeps the menu open and the focus where it was; `menu-button.qnt` reverts the whole transition of a rejected request, focus included. A write the host makes on its own is not a request: it takes the focus to the first item when it opens and to the trigger when it closes, and a write of the value it already has moves nothing (no widget can observe it).
|
|
1671
1680
|
|
|
1672
1681
|
### Headless: `createCombobox` (`@cssints/headless`)
|
|
@@ -1759,7 +1768,7 @@ createSlider(options?: {
|
|
|
1759
1768
|
focus(): void;
|
|
1760
1769
|
blur(): void;
|
|
1761
1770
|
keyDown(key: string): boolean; // true when the key was consumed (preventDefault it)
|
|
1762
|
-
pointerDown(position: number): void; // a rail press at a 0..1 fraction of the
|
|
1771
|
+
pointerDown(position: number): void; // a rail press or a drag move at a 0..1 fraction of the rail
|
|
1763
1772
|
thumbProps: {
|
|
1764
1773
|
id;
|
|
1765
1774
|
role: "slider";
|
|
@@ -1773,8 +1782,9 @@ createSlider(options?: {
|
|
|
1773
1782
|
onFocus;
|
|
1774
1783
|
onBlur;
|
|
1775
1784
|
onKeyDown;
|
|
1785
|
+
onPointerDown;
|
|
1776
1786
|
};
|
|
1777
|
-
trackProps: { onPointerDown };
|
|
1787
|
+
trackProps: { onPointerDown; onPointerMove; onPointerUp; onPointerCancel; onLostPointerCapture };
|
|
1778
1788
|
};
|
|
1779
1789
|
```
|
|
1780
1790
|
|
|
@@ -1830,10 +1840,56 @@ createSpinbutton(options?: {
|
|
|
1830
1840
|
|
|
1831
1841
|
`disabled` is **a widget-level extension of the APG patterns**, and the specs say so: `slider.qnt` lists it under its deliberate omissions, and `spinbutton.qnt` models the steppers' at-bound state only (`DisabledOnlyAtBound`). Neither machine has a `disabled` var, so the DOM tests, not the model, hold the primitive to these rules:
|
|
1832
1842
|
|
|
1833
|
-
- a disabled slider **keeps the thumb as its tab stop** (`tabindex="0"`, the same choice `createButton` makes with `aria-disabled`) and announces `aria-disabled="true"` on the thumb; every key
|
|
1843
|
+
- a disabled slider **keeps the thumb as its tab stop** (`tabindex="0"`, the same choice `createButton` makes with `aria-disabled`) and announces `aria-disabled="true"` on the thumb; every key, rail press and drag is dropped before it becomes a request, so `onValueChange` never fires and `keyDown` returns `false` (nothing to preventDefault);
|
|
1834
1844
|
- a disabled spinbutton's field announces `aria-disabled` **and carries the native `disabled`** (the field is an `<input>`, so the platform's own form-control rule applies); the spin keys, the typed draft (Enter and blur included) and both steppers change nothing, and **both** steppers announce `aria-disabled="true"` — the at-bound rule of the spec stays, and a disabled widget adds the other side;
|
|
1835
1845
|
- both read the option on every attribute read, so a host may flip `disabled` on a mounted widget and the attributes and the guards follow in the same tick.
|
|
1836
1846
|
|
|
1847
|
+
**Dragging** is the rail press repeated: `pointerdown` on the rail jumps the value to where it landed and starts a drag, and a press on the thumb starts the drag where the value already is (no jump, the thumb is there). While the pointer is held, every `pointermove` asks for the value of the pointer's position on the rail, rounded to `step` and clamped to `min`/`max`, in all four combinations of `orientation` and `direction`: up is more on a vertical rail, and a horizontal rail under `rtl` counts from its right edge, the same reflection the arrow keys use. `pointerup`, `pointercancel` or a lost capture ends the drag. The rail owns the gesture whichever element took the press, so the moves sit on `trackProps` and pointer capture keeps the drag alive off the rail: no document listener, and nothing to clean up on disposal. The thumb must be rendered inside the rail, because that is how its press reaches it. A drag is still a request: it fires `onValueChange` once per distinct value, and a controlled host that ignores the request keeps its value. `trackProps` carries no CSS, so a host that wants a drag instead of a text selection or a page scroll sets `touch-action: none` and `user-select: none` on the rail.
|
|
1848
|
+
|
|
1849
|
+
`specs/apg/slider.qnt` fixes a discrete rail press only, so the drag is a second deliberate extension and `slider.browser.test.tsx` is its oracle. The invariant the machine does not carry is **DragFollowsPointer**: while the pointer is held, `aria-valuenow` is the value of the pointer's current position (not an accumulation of moves, so returning the pointer to the press position returns the value), and no move after the release changes anything. The step grid and the clamp the drag shares with the press are the model's; the geometry is the DOM's, and the bridge hands the model a 0..1 fraction.
|
|
1850
|
+
|
|
1851
|
+
### Headless: `createSliderMultiThumb` (`@cssints/headless`)
|
|
1852
|
+
|
|
1853
|
+
The APG Slider (Multi-Thumb) pattern: two `role="slider"` thumbs on one rail, each bounded by its neighbour, so they cannot pass one another; equal values are allowed. Its behaviour is `packages/apg-specs/specs/apg/slider-multithumb.qnt`, checked against the real DOM by the differential walk in `packages/headless/test/slider-multithumb.browser.test.tsx`. The spec fixes two thumbs and one linked pair: the independent-thumbs variant, N thumbs, dragging, a nearest-thumb rail press and `disabled` are deliberate omissions (a press names its thumb). Options are plain and read lazily; `value` + `onValueChange` make the widget controlled.
|
|
1854
|
+
|
|
1855
|
+
```ts skip
|
|
1856
|
+
createSliderMultiThumb(options?: {
|
|
1857
|
+
min?: number; // 0
|
|
1858
|
+
max?: number; // 10
|
|
1859
|
+
step?: number; // 1
|
|
1860
|
+
largeStep?: number; // 3: the jump of PageUp / PageDown
|
|
1861
|
+
value?: readonly [number, number] | undefined; // controlled; undefined = uncontrolled
|
|
1862
|
+
defaultValue?: readonly [number, number]; // [3, 7], clamped into the range and ordered
|
|
1863
|
+
orientation?: "horizontal" | "vertical"; // "horizontal"
|
|
1864
|
+
direction?: "ltr" | "rtl"; // "ltr": under rtl the horizontal arrows flip
|
|
1865
|
+
id?: string; // base of the thumb ids
|
|
1866
|
+
onValueChange?(values: readonly [number, number]): void; // a request the host may ignore
|
|
1867
|
+
}): {
|
|
1868
|
+
values(): readonly [number, number];
|
|
1869
|
+
focused(): 0 | 1 | null;
|
|
1870
|
+
setValues(values: readonly [number, number]): void;
|
|
1871
|
+
pointerDown(index: 0 | 1, position: number): void; // a press at a 0..1 fraction of the track
|
|
1872
|
+
focusThumb(index: 0 | 1): void;
|
|
1873
|
+
blur(): void;
|
|
1874
|
+
keyDown(key: string): boolean; // true when the key was consumed (preventDefault it)
|
|
1875
|
+
thumbProps(index: 0 | 1): {
|
|
1876
|
+
id;
|
|
1877
|
+
role: "slider";
|
|
1878
|
+
"aria-valuenow"; // the thumb's value
|
|
1879
|
+
"aria-valuemin"; // the lower thumb: the range's minimum; the upper thumb: the lower value
|
|
1880
|
+
"aria-valuemax"; // the lower thumb: the upper value; the upper thumb: the range's maximum
|
|
1881
|
+
"aria-valuetext";
|
|
1882
|
+
"aria-orientation";
|
|
1883
|
+
tabindex: 0;
|
|
1884
|
+
onFocus;
|
|
1885
|
+
onBlur;
|
|
1886
|
+
onKeyDown;
|
|
1887
|
+
};
|
|
1888
|
+
};
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
Both thumbs are tab stops in a fixed order (lower, then upper), whatever the values are. There is no `trackProps`: the primitive has no rail press, because a press has to name a thumb.
|
|
1892
|
+
|
|
1837
1893
|
### Headless: `createKnob` (`@cssints/headless`)
|
|
1838
1894
|
|
|
1839
1895
|
A virtual knob after GarageBand's ([How Apple Designs a Virtual Knob](https://jherrm.github.io/knobs/)): spin around the centre, or slide vertically / horizontally, with one gesture locked per drag. **No APG pattern and no ARIA role exist for a dial**, so there is no Quint spec and no ported scenarios: `packages/headless/test/knob.browser.test.tsx` (real Chromium) is the oracle. The knob is a `role=slider` — the closest role WAI-ARIA has — with a polite live value; a numeric field must sit next to it, because a dial is never the only control. Options are plain and read lazily; `value` + `onValueChange` make the widget controlled.
|
|
@@ -1890,8 +1946,10 @@ createDropZone(options: {
|
|
|
1890
1946
|
maxSize?: number | undefined; // bytes; a larger file is rejected with "size"
|
|
1891
1947
|
disabled?: boolean | undefined; // zone aria-disabled, native disabled input and button, drops ignored
|
|
1892
1948
|
onFiles(files: File[]): void; // the accepted files of one batch; not called when none passed
|
|
1893
|
-
|
|
1894
|
-
|
|
1949
|
+
maxFiles?: number | undefined; // files taken from one batch; the rest are rejected with "count"
|
|
1950
|
+
onReject?({ file, reason }: { file: File; reason: "type" | "size" | "count" }): void; // once per refused file
|
|
1951
|
+
labels?: { zone?: string | undefined; labelledby?: string | undefined; browse?: string | undefined };
|
|
1952
|
+
formatMessage?(message: { total: number; accepted: number; rejected: ReadonlyArray<"type" | "size" | "count"> }): string | undefined; // the announcement in the host's language
|
|
1895
1953
|
}): {
|
|
1896
1954
|
dragging(): boolean; // a drag is over the zone; a child's dragleave does not clear it
|
|
1897
1955
|
message(): string | undefined; // "2 files, 1 rejected: too large"; undefined before the first batch
|
|
@@ -1907,6 +1965,7 @@ createDropZone(options: {
|
|
|
1907
1965
|
zoneProps: {
|
|
1908
1966
|
role: "group";
|
|
1909
1967
|
"aria-label": string | undefined;
|
|
1968
|
+
"aria-labelledby": string | undefined; // labels.labelledby: the host's own heading wins over aria-label
|
|
1910
1969
|
"aria-disabled": "true" | undefined;
|
|
1911
1970
|
onDragEnter;
|
|
1912
1971
|
onDragOver;
|
|
@@ -1921,10 +1980,10 @@ createDropZone(options: {
|
|
|
1921
1980
|
Principles:
|
|
1922
1981
|
|
|
1923
1982
|
- **The zone is never drag-only.** `browseProps` spreads onto a real `<button type="button">` that clicks the hidden input, so the picker is the keyboard path (Enter and Space are the platform's) and a pointer user can click too. The drop handlers add nothing the button cannot do.
|
|
1924
|
-
- **Validation happens once per file**, in the order type then size, with a reason the host can show: `onReject` gets the file and `"type" | "size"
|
|
1983
|
+
- **Validation happens once per file**, in the order type then size then count, with a reason the host can show: `onReject` gets the file and `"type" | "size" | "count"` (`maxFiles` limits what one batch takes), and `onFiles` gets the rest in one batch. A batch with nothing accepted calls `onFiles` not at all — not with an empty array.
|
|
1925
1984
|
- **A folder or an empty `DataTransfer` is a no-op** (Chromium marks a dropped directory as a `file` item whose `webkitGetAsEntry().isDirectory` is true): no callback, no rejection, and `message()` keeps the previous sentence.
|
|
1926
1985
|
- **`dragleave` counts.** A child's `dragleave` bubbles to the zone, so `dragging()` is a depth counter: it clears when the drag leaves the zone, or on a drop. It is the styling hook; the primitive writes no `data-dragging`.
|
|
1927
|
-
- **`message()` is a default sentence for the host's status region** ("2 files, 1 rejected: too large", `undefined` before the first batch): the primitive tracks the counts of the last batch, the host decides the words. Render `statusProps` once and keep it mounted, or a change of its text is not announced.
|
|
1986
|
+
- **`message()` is a default sentence for the host's status region** ("2 files, 1 rejected: too large", `undefined` before the first batch): the primitive tracks the counts of the last batch, the host decides the words. An app with a locale passes `formatMessage`, which gets the same counts (`total`, `accepted`, `rejected`) and returns the sentence to announce. Render `statusProps` once and keep it mounted, or a change of its text is not announced.
|
|
1928
1987
|
- Picking through the input is the same path as a drop and clears `input.value` afterwards, so choosing the same file twice fires `change` again.
|
|
1929
1988
|
|
|
1930
1989
|
### Headless: `createTimeline` (`@cssints/headless`)
|
|
@@ -1933,7 +1992,7 @@ Two modes over one axis. Without `rows` it is the package's demo tree of five fi
|
|
|
1933
1992
|
|
|
1934
1993
|
```ts skip
|
|
1935
1994
|
createTimeline(options: {
|
|
1936
|
-
rows: ReadonlyArray<{ id: string; label: string; start: number; end: number; depth?: number; status?: "ok" | "error" | "unset" }
|
|
1995
|
+
rows: ReadonlyArray<{ id: string; label: string; start: number; end: number; depth?: number; status?: "ok" | "error" | "unset" }> | (() => ...); // in display order; a getter lets a trace arrive after mount
|
|
1937
1996
|
axis?: { unit?: "ms" | "us" | "s" } | undefined; // "ms": the ruler's labels
|
|
1938
1997
|
selected?: string | undefined; // controlled; undefined = uncontrolled
|
|
1939
1998
|
onSelect?(id: string): void; // a request the host may ignore
|
|
@@ -1947,6 +2006,7 @@ createTimeline(options: {
|
|
|
1947
2006
|
rulerProps: { role: "presentation"; "aria-hidden": "true"; ticks: ReadonlyArray<{ value: number; left: number; label: string }> };
|
|
1948
2007
|
select(id: string): void;
|
|
1949
2008
|
selectedId(): string | undefined;
|
|
2009
|
+
focusedId(): string | undefined; // the row with the roving tab stop: the last one focused, else the first
|
|
1950
2010
|
};
|
|
1951
2011
|
```
|
|
1952
2012
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cssints",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "CSS in TypeScript: typed css.* calls compiled to atomic CSS at build time (a Vite plugin)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@cssints/css-grammar": "^0.
|
|
75
|
+
"@cssints/css-grammar": "^0.23.0",
|
|
76
76
|
"culori": "^4.0.2",
|
|
77
77
|
"effect": "4.0.6",
|
|
78
78
|
"happy-dom": "^20.14.5",
|