cssints 0.24.0 → 0.25.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/docs/API.md +120 -23
- package/package.json +2 -2
package/docs/API.md
CHANGED
|
@@ -1223,27 +1223,27 @@ export const typo = mdi("clos"); // error: not an icon of mdi
|
|
|
1223
1223
|
|
|
1224
1224
|
More plugins on `scope()`, each a package of its own with its rules and limits in its README. Import them with the attribute, like `cssints`.
|
|
1225
1225
|
|
|
1226
|
-
| Package | Members
|
|
1227
|
-
| ----------------------------------------------------------- |
|
|
1228
|
-
| [`@cssints/fluid`](../packages/fluid/README.md) | `fluid(min, max, { from?, to?, zoom? })`, `fluid.container(...)`
|
|
1229
|
-
| [`@cssints/layout`](../packages/layout/README.md) | `stack()`, `cluster()`, `sidebar()`, `switcher()`, `cover()`, `autoGrid()`
|
|
1230
|
-
| [`@cssints/fonts`](../packages/fonts/README.md) | `font(family, { provider, ... })` → `.family()`, `.trim()`
|
|
1231
|
-
| [`@cssints/reset`](../packages/reset/README.md) | `reset({ media?, prose? })`
|
|
1232
|
-
| [`@cssints/tokens`](../packages/tokens/README.md) | `/open-props`, `/radix`, `/flexoki`, `/reasonable-colors`, `/madoromi`
|
|
1233
|
-
| [`@cssints/gradient`](../packages/gradient/README.md) | `gradient(kind, direction, from, to, easing?, stops?)`
|
|
1234
|
-
| [`@cssints/gradient-2d`](../packages/gradient-2d/README.md) | `meshGradient(mesh)`, `freeformGradient(points)`
|
|
1235
|
-
| [`@cssints/color-names`](../packages/color-names/README.md) | `named(name)`, `colorNames.<sector>(name)`, `/nearest`
|
|
1236
|
-
| [`@cssints/motion`](../packages/motion/README.md) | `motion().preset().duration().delay().easing()`
|
|
1237
|
-
| [`@cssints/squircle`](../packages/squircle/README.md) | `squircle().radius()`
|
|
1238
|
-
| [`@cssints/reveal`](../packages/reveal/README.md) | `reveal().preset().range().duration().easing()`, `appear().preset().range().duration().easing().inOut()`
|
|
1239
|
-
| [`@cssints/stuck`](../packages/stuck/README.md) | `sticky().top()`, `stuck(styles)`, `stuck("bottom")(styles)`
|
|
1240
|
-
| [`@cssints/balance`](../packages/balance/README.md) | `balance()`, `pretty()`
|
|
1241
|
-
| [`@cssints/masonry`](../packages/masonry/README.md) | `masonry().columns().gap()`
|
|
1242
|
-
| [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })`
|
|
1243
|
-
| [`@cssints/ramp`](../packages/ramp/README.md) | `ramp(base, { mode, steps \| ratios, background })`, `overrides(ramp)`
|
|
1244
|
-
| [`@cssints/rybitten`](../packages/rybitten/README.md) | `color(hue, { cube })`, `wheel(steps, { cube })`, `shades(hue, lightness)`, `overrides(group)`
|
|
1245
|
-
| [`@cssints/skeleton`](../packages/skeleton/README.md) | `skeleton().radius().color().highlight()`
|
|
1246
|
-
| [`@cssints/reactive`](../packages/reactive/README.md) | `state()`, `toggle()`, `set()`, `value()`, `checked()`, `persist()`, `on()`, `bind()`, `when()`, `inside()` | State as custom properties, events as data on the element: writes only in events, conditions and DOM output in plain CSS, wired by a 1.
|
|
1226
|
+
| Package | Members | What it does |
|
|
1227
|
+
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1228
|
+
| [`@cssints/fluid`](../packages/fluid/README.md) | `fluid(min, max, { from?, to?, zoom? })`, `fluid.container(...)` | A `clamp()` that grows between two widths, computed at build time; a range that fails WCAG 1.4.4 is a build error |
|
|
1229
|
+
| [`@cssints/layout`](../packages/layout/README.md) | `stack()`, `cluster()`, `sidebar()`, `switcher()`, `cover()`, `autoGrid()` | Every Layout primitives as chain members |
|
|
1230
|
+
| [`@cssints/fonts`](../packages/fonts/README.md) | `font(family, { provider, ... })` → `.family()`, `.trim()` | `@font-face` and a metric-matched fallback face; cap-height trimming by `text-box`, else capsize's margins |
|
|
1231
|
+
| [`@cssints/reset`](../packages/reset/README.md) | `reset({ media?, prose? })` | A modern reset in a top-level `@layer reset` declared before the engine's, below every style; `@cssints/reset/<baseline group>` keeps only the declarations of that group |
|
|
1232
|
+
| [`@cssints/tokens`](../packages/tokens/README.md) | `/open-props`, `/radix`, `/flexoki`, `/reasonable-colors`, `/madoromi` | DTCG data for `createTokens` |
|
|
1233
|
+
| [`@cssints/gradient`](../packages/gradient/README.md) | `gradient(kind, direction, from, to, easing?, stops?)` | Eased colour stops mixed in oklab at build time |
|
|
1234
|
+
| [`@cssints/gradient-2d`](../packages/gradient-2d/README.md) | `meshGradient(mesh)`, `freeformGradient(points)` | `mesh-gradient()` / `freeform-gradient()` where the browser has them, else a runtime's canvas (this package's algorithms; lines and shapes not yet) |
|
|
1235
|
+
| [`@cssints/color-names`](../packages/color-names/README.md) | `named(name)`, `colorNames.<sector>(name)`, `/nearest` | The Color Name List (31,939 names) as hex colours; the ten sector members keep an editor's completion short, `nearest()` is the build-time reverse lookup |
|
|
1236
|
+
| [`@cssints/motion`](../packages/motion/README.md) | `motion().preset().duration().delay().easing()` | Animation presets over `keyframes()`, with reduced motion |
|
|
1237
|
+
| [`@cssints/squircle`](../packages/squircle/README.md) | `squircle().radius()` | `corner-shape: squircle`, else a CSS `mask` of the same shape |
|
|
1238
|
+
| [`@cssints/reveal`](../packages/reveal/README.md) | `reveal().preset().range().duration().easing()`, `appear().preset().range().duration().easing().inOut()` | motion's entrance presets on `animation-timeline: view()` (reveal) or on `timeline-trigger` + `animation-trigger` (appear); for other targets a script plays it once on entry (delivered by the runtime) |
|
|
1239
|
+
| [`@cssints/stuck`](../packages/stuck/README.md) | `sticky().top()`, `stuck(styles)`, `stuck("bottom")(styles)` | Styles inside a sticky element while it is stuck, by `@container scroll-state()`; for other targets an `IntersectionObserver` (delivered by the runtime) |
|
|
1240
|
+
| [`@cssints/balance`](../packages/balance/README.md) | `balance()`, `pretty()` | `text-wrap: balance` / `pretty`; for targets without balance a script balances the lines (delivered by the runtime) |
|
|
1241
|
+
| [`@cssints/masonry`](../packages/masonry/README.md) | `masonry().columns().gap()` | `display: grid-lanes`, else CSS `columns` (top to bottom per column) |
|
|
1242
|
+
| [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })` | An `<image>` from a JPEG or PNG read at build time: a gradient of its colours laid out as the image, or a tiny thumbhash PNG |
|
|
1243
|
+
| [`@cssints/ramp`](../packages/ramp/README.md) | `ramp(base, { mode, steps \| ratios, background })`, `overrides(ramp)` | An OKLCH palette as DTCG data for `createTokens`; the contrast mode meets each ratio against its background |
|
|
1244
|
+
| [`@cssints/rybitten`](../packages/rybitten/README.md) | `color(hue, { cube })`, `wheel(steps, { cube })`, `shades(hue, lightness)`, `overrides(group)` | RYBitten's red-yellow-blue colours (36 historical cubes) as DTCG data for `createTokens`: a hue, a wheel of hues, a hue at several lightnesses |
|
|
1245
|
+
| [`@cssints/skeleton`](../packages/skeleton/README.md) | `skeleton().radius().color().highlight()` | Loading placeholders whose shimmer is in phase across the page (one clock on `:root`), static under reduced motion |
|
|
1246
|
+
| [`@cssints/reactive`](../packages/reactive/README.md) | `state()`, `toggle()`, `set()`, `value()`, `checked()`, `persist()`, `equals()`, `either()`, `on()`, `bind()`, `when()`, `inside()` | State as custom properties, events as data on the element: writes only in events, conditions and DOM output in plain CSS, wired by a 1.3 kB runtime shipped to every target; `value()`/`checked()` take the field's value, calls of `on()`, `bind()` or `state()` in one `cn()` append, `persist(value, key)` keeps a state across a reload, and `bind()` takes a source, a comparison (`equals()`), a choice of two literals (`either()`) or a literal — the one expression the runtime evaluates, because an attribute on the element cannot be computed in CSS (2026-10-31, cssints-ac3o) |
|
|
1247
1247
|
|
|
1248
1248
|
### Headless: `createField` and `createConfirm` (`@cssints/headless`)
|
|
1249
1249
|
|
|
@@ -1592,7 +1592,104 @@ createCommandPalette(options: {
|
|
|
1592
1592
|
};
|
|
1593
1593
|
```
|
|
1594
1594
|
|
|
1595
|
-
Focus stays in the field and `aria-activedescendant` names the highlighted option: ↓ on the last and ↑ on the first return the highlight to the field, and ↓/↑ from there re-enter at the first/last row. Home/End move the caret while the field has text and are list keys only while it is empty. Enter runs the highlight, Escape closes through the dialog, Tab is not intercepted. Filtering is a plain case-insensitive substring match over `label` and `keywords` — no fuzzy scoring, because a palette is short and ranking (recents, scopes) is the host's policy. `results()` keeps host order; `group` is what the host groups by; `emptyProps.hidden` and `labels.empty` are the empty state; the count is written to `statusProps` once per query, not on every arrow. The query and highlight reset on close, focus returns to the element that had it, and `runActive()` closes before `run()` so a command may open its own UI. Host's: rendering, grouping and CSS; async commands and their errors; every visible string via `labels`; any close other than Escape or a command. The
|
|
1595
|
+
Focus stays in the field and `aria-activedescendant` names the highlighted option: ↓ on the last and ↑ on the first return the highlight to the field, and ↓/↑ from there re-enter at the first/last row. Home/End move the caret while the field has text and are list keys only while it is empty. Enter runs the highlight, Escape closes through the dialog, Tab is not intercepted. Filtering is a plain case-insensitive substring match over `label` and `keywords` — no fuzzy scoring, because a palette is short and ranking (recents, scopes) is the host's policy. `results()` keeps host order; `group` is what the host groups by; `emptyProps.hidden` and `labels.empty` are the empty state; the count is written to `statusProps` once per query, not on every arrow. The query and highlight reset on close, focus returns to the element that had it, and `runActive()` closes before `run()` so a command may open its own UI. Host's: rendering, grouping and CSS; async commands and their errors; every visible string via `labels`; any close other than Escape or a command. The date picker is a later composition on the same research; the filterable menu is the next section.
|
|
1596
|
+
|
|
1597
|
+
### Headless: `createFilterableMenu` (`@cssints/headless`)
|
|
1598
|
+
|
|
1599
|
+
The second **composition**, not a new primitive and not a new spec: `createDialogModal` + `createTextInput` + `createStatus` with one filtered `role=menu`, per Base UI's [Building a filterable menu that works for everyone](https://base-ui.com/blog/building-a-filterable-menu) (settled in `docs/research/compositions-2026-10.md`). Its oracle is `packages/headless/test/filterable-menu.browser.test.tsx`. The structure is a role promise: choosing a row runs a command, so the popup is a `role=dialog` whose trigger says `aria-haspopup="dialog"`, the field is `input[type=text][role=searchbox]` with no `aria-autocomplete`, `aria-haspopup` or `aria-expanded`, and the list is a `role=menu`.
|
|
1600
|
+
|
|
1601
|
+
```ts skip
|
|
1602
|
+
createFilterableMenu(options: {
|
|
1603
|
+
items:
|
|
1604
|
+
| ReadonlyArray<{
|
|
1605
|
+
id: string;
|
|
1606
|
+
label: string;
|
|
1607
|
+
group?: string | undefined; // the heading the host renders
|
|
1608
|
+
submenu?: ReadonlyArray<{ id: string; label: string; run?(): void | Promise<void> }>;
|
|
1609
|
+
run(): void | Promise<void>;
|
|
1610
|
+
}>
|
|
1611
|
+
| (() => ReadonlyArray<FilterableMenuItem>);
|
|
1612
|
+
open?: boolean | undefined; // controlled: the host owns the open state
|
|
1613
|
+
onOpenChange?(open: boolean): void;
|
|
1614
|
+
labels?: Partial<{
|
|
1615
|
+
placeholder: string; // "Filter actions"
|
|
1616
|
+
empty: string; // "No actions found"; the host renders it
|
|
1617
|
+
count(count: number): string; // "5 actions"
|
|
1618
|
+
clear: string; // "Clear"; the clear button's name
|
|
1619
|
+
}> | undefined;
|
|
1620
|
+
id?: string | undefined;
|
|
1621
|
+
}): {
|
|
1622
|
+
open(): boolean;
|
|
1623
|
+
openMenu(): void;
|
|
1624
|
+
closeMenu(): void; // closes every level; Escape pops one first
|
|
1625
|
+
query(): string;
|
|
1626
|
+
setQuery(text: string): void;
|
|
1627
|
+
active(): string | null; // the highlighted row, or null while the field holds it
|
|
1628
|
+
setActive(id: string | null): void;
|
|
1629
|
+
results(submenuId?: string): ReadonlyArray<{
|
|
1630
|
+
id: string;
|
|
1631
|
+
label: string;
|
|
1632
|
+
group: string | undefined;
|
|
1633
|
+
hasSubmenu: boolean;
|
|
1634
|
+
}>;
|
|
1635
|
+
labels: FilterableMenuLabels;
|
|
1636
|
+
status(): string | undefined;
|
|
1637
|
+
// triggerProps (aria-haspopup="dialog", needs nothing from the field),
|
|
1638
|
+
// inputProps, menuProps, groupProps(name), groupLabelProps(name),
|
|
1639
|
+
// itemProps(id), submenuTriggerProps(id), submenuProps(id), emptyProps
|
|
1640
|
+
// ({ hidden, text }), statusProps, clearProps (tabindex -1), dialogProps,
|
|
1641
|
+
// backdropProps and titleProps: the attribute objects.
|
|
1642
|
+
};
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1645
|
+
Focus stays in the field and `aria-activedescendant` names the highlighted row; `data-highlighted` sits on the field while the highlight is there. ↓ on the last row and ↑ on the first return it to the field, and ↓/↑ from the field enter at the first/last row. Home/End are menu keys only while the query is empty **and** a row is highlighted; otherwise they move the caret. Enter runs the row or enters its submenu. **Tab closes and leaves** (not prevented: the dialog is hidden on the next task, after focus moved, so the menu never traps; a native `showModal()` wrapper keeps its own trap and moves first). **Escape pops one submenu level** and closes at the top level; Backspace on an empty submenu field goes back too.
|
|
1646
|
+
|
|
1647
|
+
A submenu is a level: each level keeps its query and highlight and there is one field, so Enter on a trigger moves the field to that level at once, hover shows the level without moving focus, and coming back never loses the caret (`submenuProps` is the pointer-into-the-submenu hook; `results(triggerId)` gives a submenu's rows for a panel the host draws). Filtering is a case-insensitive substring on the label; typing inside an IME composition updates the text but filters only on `compositionend` (`event.isComposing` is honoured on the keyboard too). `groupProps` / `groupLabelProps` (`aria-hidden`: the group is already named through `aria-labelledby`) are the groups; `emptyProps.hidden` + `emptyProps.text` are the empty state; the count is one polite write per query change, not per arrow; `clearProps` has `tabindex: -1`, so Clear is a pointer affordance and not a tab stop. The host mounts the dialog and field only while `open()`; `triggerProps` names `aria-haspopup="dialog"` and needs nothing from the field, so the trigger works before it exists. Host's: rendering the groups and the submenu geometry, CSS, async commands and their errors and closing, and every visible string through `labels`.
|
|
1648
|
+
Focus stays in the field and `aria-activedescendant` names the highlighted option: ↓ on the last and ↑ on the first return the highlight to the field, and ↓/↑ from there re-enter at the first/last row. Home/End move the caret while the field has text and are list keys only while it is empty. Enter runs the highlight, Escape closes through the dialog, Tab is not intercepted. Filtering is a plain case-insensitive substring match over `label` and `keywords` — no fuzzy scoring, because a palette is short and ranking (recents, scopes) is the host's policy. `results()` keeps host order; `group` is what the host groups by; `emptyProps.hidden` and `labels.empty` are the empty state; the count is written to `statusProps` once per query, not on every arrow. The query and highlight reset on close, focus returns to the element that had it, and `runActive()` closes before `run()` so a command may open its own UI. Host's: rendering, grouping and CSS; async commands and their errors; every visible string via `labels`; any close other than Escape or a command. The filterable menu is a later composition on the same research.
|
|
1649
|
+
|
|
1650
|
+
### Headless: `createDatePicker` (`@cssints/headless`)
|
|
1651
|
+
|
|
1652
|
+
The second **composition**, not a new primitive and not a new spec: `createPopover` (a non-modal trigger and popup) + `createGrid` (a 6x7 month) + `createStatus` (the selected day). Its oracle is `packages/headless/test/date-picker.browser.test.tsx`. The popup is not modal on purpose — Tab leaves it, nothing behind it is `inert` — and the calendar maths is integer day numbers (`Date.UTC(y, m, d) / 86_400_000`), not a date library.
|
|
1653
|
+
|
|
1654
|
+
```ts skip
|
|
1655
|
+
createDatePicker(options: {
|
|
1656
|
+
value?: string | undefined; // controlled, ISO YYYY-MM-DD
|
|
1657
|
+
defaultValue?: string | undefined;
|
|
1658
|
+
onValueChange?(iso: string): void; // a request; a controlled host may ignore it
|
|
1659
|
+
min?: string | undefined; // inclusive
|
|
1660
|
+
max?: string | undefined;
|
|
1661
|
+
today?: string | undefined; // injected; the local date by default
|
|
1662
|
+
open?: boolean | undefined; // controlled
|
|
1663
|
+
onOpenChange?(open: boolean): void;
|
|
1664
|
+
labels?: Partial<{
|
|
1665
|
+
trigger: string; // "Choose date"
|
|
1666
|
+
previousMonth: string; // "Previous month"
|
|
1667
|
+
nextMonth: string; // "Next month"
|
|
1668
|
+
month(year: number, month: number): string; // 0-11; "October 2026"
|
|
1669
|
+
weekdays: ReadonlyArray<string>; // 7, Sunday first (getUTCDay order)
|
|
1670
|
+
day(iso: string): string; // the announcement; the ISO string by default
|
|
1671
|
+
}> | undefined;
|
|
1672
|
+
id?: string | undefined;
|
|
1673
|
+
firstDayOfWeek?: number | undefined; // 0 = Sunday .. 6 = Saturday; 1
|
|
1674
|
+
}): {
|
|
1675
|
+
value(): string | undefined;
|
|
1676
|
+
open(): boolean;
|
|
1677
|
+
openPicker(): void;
|
|
1678
|
+
closePicker(): void;
|
|
1679
|
+
month(): { year: number; month: number }; // 0-11
|
|
1680
|
+
setMonth(year: number, month: number): void; // the roving day at the value, today or the 1st
|
|
1681
|
+
today(): string;
|
|
1682
|
+
select(iso: string): void; // a no-op outside min/max; the host closes
|
|
1683
|
+
monthDays(): ReadonlyArray<ReadonlyArray<string>>; // 6 rows of 7 ISO dates, neighbours included
|
|
1684
|
+
labels: DatePickerLabels;
|
|
1685
|
+
status(): string | undefined;
|
|
1686
|
+
// statusProps, triggerProps, popoverProps, headerProps, previousProps,
|
|
1687
|
+
// nextProps, gridProps, weekdayProps(index) and cellProps(iso): the attribute objects.
|
|
1688
|
+
attach(trigger: HTMLElement, content: HTMLElement): () => void; // the popover wiring; returns the teardown
|
|
1689
|
+
};
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
Every date is an ISO `YYYY-MM-DD`, read as a UTC midnight; `month()` and `labels.month` use 0-11. `monthDays()` is **six weeks around the shown month**, so a row can start in the previous month and end in the next one: every cell is a real focusable date (a blank cannot hold the roving focus), and a cursor that leaves the month pages the grid to where it landed. `cellProps(iso)` marks the chosen day `aria-selected`, `today` `aria-current="date"`, a day outside `min`/`max` `aria-disabled`, and gives `tabindex=0` to the roving day only: the selected day, else today when it is in the shown month, else the first day the bounds allow. Keys on the focused day: ←/→ a day, ↑/↓ a week, Home/End the week's first/last day, PageUp/PageDown a month, Shift+PageUp/PageDown a year (the day clamps, 29 February included), Enter/Space selects and closes, Escape closes without changing the value, Tab leaves. The month arrows move real DOM focus. `attach()` is the popover's positioning, light dismissal and focus return, plus focus into the roving day on open. The host renders the cells, the CSS and the trigger's visible text; a segmented text field is a host composition of `createField` + `createSpinbutton` over the same day numbers, not part of this API.
|
|
1596
1693
|
|
|
1597
1694
|
### Headless: `createPanZoom` (`@cssints/headless`)
|
|
1598
1695
|
|
|
@@ -1642,7 +1739,7 @@ The host owns the box and the look: the viewport's size and `overflow: hidden`,
|
|
|
1642
1739
|
|
|
1643
1740
|
### Headless: `createMenu` (`@cssints/headless`)
|
|
1644
1741
|
|
|
1645
|
-
The APG menu family (`createMenu` for the menu button and the checkable menus, `createMenubar` for the roving bar) as state and attribute objects. Its behaviour is `specs/apg/menu-button.qnt` and `menu-
|
|
1742
|
+
The APG menu family (`createMenu` for the menu button and the checkable menus, `createMenubar` for the roving bar) as state and attribute objects. Its behaviour is `specs/apg/menu-button.qnt`, `menu-checkable.qnt` and `menu-menubar.qnt`, and `packages/headless/test/menu-button.browser.test.tsx` and `packages/headless/test/menu.browser.test.tsx` replay the generated models, ported scenarios and seeded differential walks against it.
|
|
1646
1743
|
|
|
1647
1744
|
```ts skip
|
|
1648
1745
|
createMenu(options: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cssints",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.25.0",
|
|
76
76
|
"culori": "^4.0.2",
|
|
77
77
|
"effect": "4.0.6",
|
|
78
78
|
"happy-dom": "^20.14.5",
|