@orianatech/pire 0.2.1 → 0.4.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.
Files changed (34) hide show
  1. package/README.md +43 -5
  2. package/dist/components/core/Icon.d.cts +22 -7
  3. package/dist/components/core/Icon.d.ts +22 -7
  4. package/dist/components/core/Icon.d.ts.map +1 -1
  5. package/dist/components/core/icon-data.generated.d.cts +1 -0
  6. package/dist/components/core/icon-data.generated.d.ts +2 -0
  7. package/dist/components/core/icon-data.generated.d.ts.map +1 -0
  8. package/dist/components/core/icon-map.d.cts +13 -0
  9. package/dist/components/core/icon-map.d.ts +14 -0
  10. package/dist/components/core/icon-map.d.ts.map +1 -0
  11. package/dist/components/feedback/Skeleton.d.cts +43 -0
  12. package/dist/components/feedback/Skeleton.d.ts +44 -0
  13. package/dist/components/feedback/Skeleton.d.ts.map +1 -0
  14. package/dist/components/forms/MultiComboBox.d.cts +58 -0
  15. package/dist/components/forms/MultiComboBox.d.ts +59 -0
  16. package/dist/components/forms/MultiComboBox.d.ts.map +1 -0
  17. package/dist/components/forms/TextArea.d.cts +42 -0
  18. package/dist/components/forms/TextArea.d.ts +43 -0
  19. package/dist/components/forms/TextArea.d.ts.map +1 -0
  20. package/dist/components/navigation/ShellBar.d.cts +8 -1
  21. package/dist/components/navigation/ShellBar.d.ts +8 -1
  22. package/dist/components/navigation/ShellBar.d.ts.map +1 -1
  23. package/dist/components/navigation/SideNav.d.cts +56 -4
  24. package/dist/components/navigation/SideNav.d.ts +56 -4
  25. package/dist/components/navigation/SideNav.d.ts.map +1 -1
  26. package/dist/components.css +73 -1
  27. package/dist/index.cjs +806 -353
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +4 -1
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +809 -347
  33. package/dist/index.js.map +1 -1
  34. package/package.json +4 -2
@@ -1,9 +1,24 @@
1
- import type * as React from 'react';
1
+ import * as React from 'react';
2
+ /** A second-level destination, nested under a first-level entry.
3
+ *
4
+ * Deliberately has no `icon`: only the first level carries one, so the second level reads as an
5
+ * indented list of labels under a single glyph rather than a wall of decoration. Leaving `icon` off
6
+ * the *type* is what stops that drifting back in one call site at a time. */
7
+ export interface SideNavChildItem {
8
+ id: string;
9
+ label: string;
10
+ count?: number;
11
+ }
2
12
  export interface SideNavItem {
3
13
  id: string;
4
14
  label: string;
5
15
  icon?: string;
6
16
  count?: number;
17
+ /** Second-level destinations. Supplying this turns the entry into a collapsible group: it stops
18
+ * being a destination itself (pressing it expands, never navigates) and `id` becomes the
19
+ * expansion key used by `expandedIds` / `defaultExpandedIds`, not something `onChange` can ever
20
+ * emit. Omit it and the entry behaves exactly as it always has. */
21
+ items?: SideNavChildItem[];
7
22
  }
8
23
  export interface SideNavGroup {
9
24
  label?: string;
@@ -13,10 +28,47 @@ export interface SideNavProps extends Omit<React.HTMLAttributes<HTMLElement>, 'o
13
28
  /** Spaces-and-pages hierarchy: one group per space. */
14
29
  groups: SideNavGroup[];
15
30
  value?: string;
16
- /** Fires with the selected item id — not a DOM change event. */
31
+ /** Fires with the selected item id — not a DOM change event. Only ever a destination id: a
32
+ * first-level entry that owns `items` is a disclosure, and toggling it does not fire this. */
17
33
  onChange?: (id: string) => void;
18
- /** 48px icon rail. Labels move into tooltips. */
34
+ /** Expanded first-level entry ids the controlled half of the pair. Pass it and you own expansion
35
+ * outright, including expanding the group that holds `value`. */
36
+ expandedIds?: string[];
37
+ /** Initial expanded ids for the uncontrolled case. Omit it and the group holding `value` opens
38
+ * itself; supply it and it is taken literally, so `value`'s group can start closed if that is
39
+ * genuinely what you want. */
40
+ defaultExpandedIds?: string[];
41
+ /** Fires with every expanded id after a toggle, controlled or not. */
42
+ onExpandedChange?: (ids: string[]) => void;
43
+ /** 48px icon rail. Labels move into tooltips, and second-level items are not rendered at all —
44
+ * see the note on {@link SideNav}. */
19
45
  collapsed?: boolean;
20
46
  footer?: React.ReactNode;
21
47
  }
22
- export declare function SideNav({ groups, value, onChange, collapsed, footer, className, ...rest }: SideNavProps): React.JSX.Element;
48
+ /** Primary navigation. A first-level entry is either a destination (icon + label, selectable) or a
49
+ * collapsible group (icon + label + chevron) when it carries `items` — the two coexist in one list,
50
+ * which is how a 35-destination ERP menu fits without showing all 35 at once.
51
+ *
52
+ * Both levels are plain `Button`s and the disclosure is React Aria's `Disclosure` /
53
+ * `DisclosureGroup`, so `aria-expanded`, `aria-controls`, the panel's `role="group"` and
54
+ * `aria-labelledby`, and Enter/Space on the trigger all come from React Aria rather than from
55
+ * hand-rolled ARIA here.
56
+ *
57
+ * Three things worth knowing before you use it:
58
+ *
59
+ * - **The panel is always in the DOM.** React Aria hides a collapsed panel with
60
+ * `hidden="until-found"`, applied imperatively in a layout effect, so find-in-page can reveal it.
61
+ * A test asserting a collapsed child is *absent* will fail; assert `aria-expanded` on the trigger,
62
+ * or visibility, instead. The corollary in CSS: any `display` we set on the panel would beat the
63
+ * UA's `[hidden] { display: none }`, leaving collapsed children focusable under
64
+ * `aria-hidden="true"`. `.pire-sidenav-subnav[hidden]` in `components.css` is that guard.
65
+ * - **`collapsed` and expansion are different axes, and the rail wins.** With `collapsed` set there
66
+ * are no labels, and a second level is nothing *but* labels, so children are not rendered in the
67
+ * rail. A group's row becomes an ordinary icon button that records the expansion (so the group is
68
+ * already open when the rail expands) and lights up while it holds `value`, which is the one thing
69
+ * a rail must still communicate. It follows that a nav with collapsible groups needs a way back
70
+ * out of the rail — `ShellBar`'s `onMenu` — or 25 of those 35 destinations are unreachable, for
71
+ * everyone, exactly as the labels already are.
72
+ * - **Several groups can be open at once**, and there is no accordion mode. Closing the group you
73
+ * are in to open another is the wrong trade in a nav whose whole job is showing where you are. */
74
+ export declare function SideNav({ groups, value, onChange, expandedIds, defaultExpandedIds, onExpandedChange, collapsed, footer, className, ...rest }: SideNavProps): React.JSX.Element;
@@ -1,9 +1,24 @@
1
- import type * as React from 'react';
1
+ import * as React from 'react';
2
+ /** A second-level destination, nested under a first-level entry.
3
+ *
4
+ * Deliberately has no `icon`: only the first level carries one, so the second level reads as an
5
+ * indented list of labels under a single glyph rather than a wall of decoration. Leaving `icon` off
6
+ * the *type* is what stops that drifting back in one call site at a time. */
7
+ export interface SideNavChildItem {
8
+ id: string;
9
+ label: string;
10
+ count?: number;
11
+ }
2
12
  export interface SideNavItem {
3
13
  id: string;
4
14
  label: string;
5
15
  icon?: string;
6
16
  count?: number;
17
+ /** Second-level destinations. Supplying this turns the entry into a collapsible group: it stops
18
+ * being a destination itself (pressing it expands, never navigates) and `id` becomes the
19
+ * expansion key used by `expandedIds` / `defaultExpandedIds`, not something `onChange` can ever
20
+ * emit. Omit it and the entry behaves exactly as it always has. */
21
+ items?: SideNavChildItem[];
7
22
  }
8
23
  export interface SideNavGroup {
9
24
  label?: string;
@@ -13,11 +28,48 @@ export interface SideNavProps extends Omit<React.HTMLAttributes<HTMLElement>, 'o
13
28
  /** Spaces-and-pages hierarchy: one group per space. */
14
29
  groups: SideNavGroup[];
15
30
  value?: string;
16
- /** Fires with the selected item id — not a DOM change event. */
31
+ /** Fires with the selected item id — not a DOM change event. Only ever a destination id: a
32
+ * first-level entry that owns `items` is a disclosure, and toggling it does not fire this. */
17
33
  onChange?: (id: string) => void;
18
- /** 48px icon rail. Labels move into tooltips. */
34
+ /** Expanded first-level entry ids the controlled half of the pair. Pass it and you own expansion
35
+ * outright, including expanding the group that holds `value`. */
36
+ expandedIds?: string[];
37
+ /** Initial expanded ids for the uncontrolled case. Omit it and the group holding `value` opens
38
+ * itself; supply it and it is taken literally, so `value`'s group can start closed if that is
39
+ * genuinely what you want. */
40
+ defaultExpandedIds?: string[];
41
+ /** Fires with every expanded id after a toggle, controlled or not. */
42
+ onExpandedChange?: (ids: string[]) => void;
43
+ /** 48px icon rail. Labels move into tooltips, and second-level items are not rendered at all —
44
+ * see the note on {@link SideNav}. */
19
45
  collapsed?: boolean;
20
46
  footer?: React.ReactNode;
21
47
  }
22
- export declare function SideNav({ groups, value, onChange, collapsed, footer, className, ...rest }: SideNavProps): React.JSX.Element;
48
+ /** Primary navigation. A first-level entry is either a destination (icon + label, selectable) or a
49
+ * collapsible group (icon + label + chevron) when it carries `items` — the two coexist in one list,
50
+ * which is how a 35-destination ERP menu fits without showing all 35 at once.
51
+ *
52
+ * Both levels are plain `Button`s and the disclosure is React Aria's `Disclosure` /
53
+ * `DisclosureGroup`, so `aria-expanded`, `aria-controls`, the panel's `role="group"` and
54
+ * `aria-labelledby`, and Enter/Space on the trigger all come from React Aria rather than from
55
+ * hand-rolled ARIA here.
56
+ *
57
+ * Three things worth knowing before you use it:
58
+ *
59
+ * - **The panel is always in the DOM.** React Aria hides a collapsed panel with
60
+ * `hidden="until-found"`, applied imperatively in a layout effect, so find-in-page can reveal it.
61
+ * A test asserting a collapsed child is *absent* will fail; assert `aria-expanded` on the trigger,
62
+ * or visibility, instead. The corollary in CSS: any `display` we set on the panel would beat the
63
+ * UA's `[hidden] { display: none }`, leaving collapsed children focusable under
64
+ * `aria-hidden="true"`. `.pire-sidenav-subnav[hidden]` in `components.css` is that guard.
65
+ * - **`collapsed` and expansion are different axes, and the rail wins.** With `collapsed` set there
66
+ * are no labels, and a second level is nothing *but* labels, so children are not rendered in the
67
+ * rail. A group's row becomes an ordinary icon button that records the expansion (so the group is
68
+ * already open when the rail expands) and lights up while it holds `value`, which is the one thing
69
+ * a rail must still communicate. It follows that a nav with collapsible groups needs a way back
70
+ * out of the rail — `ShellBar`'s `onMenu` — or 25 of those 35 destinations are unreachable, for
71
+ * everyone, exactly as the labels already are.
72
+ * - **Several groups can be open at once**, and there is no accordion mode. Closing the group you
73
+ * are in to open another is the wrong trade in a nav whose whole job is showing where you are. */
74
+ export declare function SideNav({ groups, value, onChange, expandedIds, defaultExpandedIds, onExpandedChange, collapsed, footer, className, ...rest }: SideNavProps): React.JSX.Element;
23
75
  //# sourceMappingURL=SideNav.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SideNav.d.ts","sourceRoot":"","sources":["../../../src/components/navigation/SideNav.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAMpC,MAAM,WAAW,WAAW;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AACzF,MAAM,WAAW,YAAY;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE;AAEtE,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;IACvF,uDAAuD;IACvD,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,YAAY,qBAwBvG"}
1
+ {"version":3,"file":"SideNav.d.ts","sourceRoot":"","sources":["../../../src/components/navigation/SideNav.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAO/B;;;;8EAI8E;AAC9E,MAAM,WAAW,gBAAgB;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAE/E,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;wEAGoE;IACpE,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,EAAE,CAAA;CAAE;AAEtE,MAAM,WAAW,YAAa,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;IACvF,uDAAuD;IACvD,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;mGAC+F;IAC/F,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC;sEACkE;IAClE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;mCAE+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3C;2CACuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;qGAyBqG;AACrG,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,EAClG,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,YAAY,qBAwGtD"}
@@ -59,7 +59,7 @@
59
59
  .pire-card-body{padding:var(--sp-4);flex:1}
60
60
  .pire-card-foot{display:flex;justify-content:flex-end;gap:var(--sp-2);padding:var(--sp-3) var(--sp-4);border-top:1px solid var(--border-subtle)}
61
61
 
62
- /* ---- Field shell (Input, Select, ComboBox) --------------------------- */
62
+ /* ---- Field shell (Input, Select, ComboBox, TextArea, MultiComboBox) --- */
63
63
  .pire-field{display:flex;flex-direction:column;gap:var(--sp-1)}
64
64
  .pire-field-label{font:var(--type-label);color:var(--text-label)}
65
65
  .pire-field-req{color:var(--status-negative-fg);margin-left:var(--sp-0h)}
@@ -90,6 +90,30 @@
90
90
  .pire-input::placeholder{color:var(--text-tertiary)}
91
91
  .pire-affix{font:var(--type-caption);color:var(--text-secondary);flex:0 0 auto}
92
92
  .pire-leading-icon{color:var(--text-tertiary);flex:0 0 auto}
93
+ /* The shell is fixed-height by default because every single-line control is. Controls whose
94
+ content sets the height (TextArea, MultiComboBox's chips) opt out with data-multiline and
95
+ keep everything else — focus ring, invalid, disabled, readonly, sizing — from the rules
96
+ above. This block sits after the [data-size] heights on purpose: equal specificity, so the
97
+ later `height:auto` wins, and the per-size floor is restored as a min-height. */
98
+ .pire-control[data-multiline="true"]{height:auto;min-height:var(--control-h-md);align-items:stretch;padding-top:var(--sp-2);padding-bottom:var(--sp-2)}
99
+ .pire-control[data-multiline="true"][data-size="sm"]{min-height:var(--control-h-sm)}
100
+ .pire-control[data-multiline="true"][data-size="lg"]{min-height:var(--control-h-lg)}
101
+
102
+ /* ---- TextArea -------------------------------------------------------- */
103
+ .pire-textarea{display:block;width:100%;resize:vertical;overflow:auto}
104
+ /* autoGrow writes an inline height on every keystroke, so a height dragged by hand would be
105
+ thrown away on the next one — the grip is removed rather than left lying about it. */
106
+ .pire-textarea[data-auto-grow="true"]{resize:none}
107
+
108
+ /* ---- MultiComboBox --------------------------------------------------- */
109
+ .pire-chipfield{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-1)}
110
+ /* Never narrower than a few characters, so the caret stays reachable however many chips are in. */
111
+ .pire-chipinput{flex:1 1 6ch;min-width:6ch}
112
+ /* Each chip's remove button is a Tab stop of its own inside the shell, and the shell's blanket
113
+ `:focus-visible{outline:none}` (see above) would otherwise leave it unmarked. */
114
+ .pire-chipfield .pire-tag-remove[data-focus-visible]{outline:var(--focus-w) solid var(--focus-color);outline-offset:var(--focus-offset)}
115
+ /* One ring at a time: while a chip owns focus it, not the shell, is what moved. */
116
+ .pire-control:has(.pire-tag-remove[data-focus-visible])[data-focus-within]{outline:none}
93
117
 
94
118
  /* ---- Select / ListBox popover ---------------------------------------- */
95
119
  .pire-select-value{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:var(--type-body)}
@@ -164,6 +188,28 @@
164
188
  .pire-sidenav-item[data-hovered]{background:var(--surface-hover)}
165
189
  .pire-sidenav-item[data-selected="true"]{background:var(--surface-selected);border-left-color:var(--accent-bg);color:var(--accent-subtle-fg);font:var(--type-body-strong)}
166
190
  .pire-sidenav-count{margin-left:auto;font:var(--type-code);color:var(--text-tertiary)}
191
+ /* Second level. `-tree` is React Aria's DisclosureGroup, `-section` one Disclosure; both exist only
192
+ to keep the rows in a column, since the wrappers are divs the component cannot avoid. */
193
+ .pire-sidenav-tree,.pire-sidenav-section{display:flex;flex-direction:column}
194
+ /* The trailing chevron. Its own auto margin pushes it to the edge on a group row with no count; when
195
+ there is a count, the count's auto margin has already claimed the free space and a second auto here
196
+ would split it and leave the count floating mid-row — hence the reset. */
197
+ .pire-sidenav-chevron{margin-left:auto;color:var(--text-tertiary);transition:transform var(--dur-fast) var(--ease-standard)}
198
+ .pire-sidenav-count + .pire-sidenav-chevron{margin-left:0}
199
+ .pire-sidenav-section:not([data-expanded]) .pire-sidenav-chevron{transform:rotate(-90deg)}
200
+ /* Marks a group row whose child is the current destination: the accent tick and label weight of a
201
+ selection without its fill, so the trail to `aria-current` reads at a glance and still loses to it.
202
+ Also the only cue left in the icon rail, where the children are not rendered. */
203
+ .pire-sidenav-item[data-active-child="true"]{border-left-color:var(--accent-bg);color:var(--accent-subtle-fg);font:var(--type-body-strong)}
204
+ /* React Aria animates the panel through --disclosure-panel-height, and applies `hidden` only once the
205
+ animation settles. The `display` above therefore has to be undone for the collapsed state: it beats
206
+ the UA's `[hidden]{display:none}`, and a visible-to-the-layout panel under `aria-hidden="true"`
207
+ leaves every child in the tab order — an aria-hidden-focus defect, not just a cosmetic one. */
208
+ .pire-sidenav-subnav{display:flex;flex-direction:column;overflow:hidden;height:var(--disclosure-panel-height,auto);transition:height var(--dur-fast) var(--ease-standard)}
209
+ .pire-sidenav-subnav[hidden]{display:none}
210
+ /* Indented to the parent's *label*, not its icon: page padding + the 16px glyph box + the row gap.
211
+ Condensed height because a two-level menu is 35 rows, not 10. */
212
+ .pire-sidenav-subitem{height:var(--row-h-condensed);padding-left:calc(var(--sp-4) + var(--sp-4) + var(--sp-3))}
167
213
  .pire-sidenav-footer{margin-top:auto;padding:var(--sp-3) var(--sp-4);border-top:1px solid var(--border-subtle)}
168
214
 
169
215
  /* ---- Feedback -------------------------------------------------------- */
@@ -203,6 +249,28 @@
203
249
  .pire-progress[data-status="negative"] .pire-progress-fill{background:var(--status-negative-fg)}
204
250
  .pire-tooltip{max-width:240px;padding:var(--sp-1) var(--sp-2);border-radius:var(--radius-1);background:var(--gray-90);color:var(--text-inverse);font:var(--type-caption);box-shadow:var(--shadow-2);animation:pire-fade-in var(--dur-fast) var(--ease-entrance)}
205
251
 
252
+ /* ---- Skeleton ---------------------------------------------------------- */
253
+ /* The sweep is a lightness *difference* between two surface aliases, not a fixed direction:
254
+ --surface-hover is lighter than --surface-active in the light theme and darker in the dark
255
+ one, and either reads as a sweep. That keeps the placeholder on the shared alias layer
256
+ instead of introducing a skeleton-only token pair for both themes to maintain.
257
+ The duration is derived, not literal: a shimmer at --dur-slow (400ms) is agitated rather
258
+ than patient, and 3x is the slowest step the motion scale can express. */
259
+ .pire-skeleton{display:block;width:100%;background-color:var(--surface-active);background-image:linear-gradient(90deg,transparent,var(--surface-hover),transparent);background-size:200% 100%;background-repeat:no-repeat;border-radius:var(--radius-1);animation:pire-skeleton-sweep calc(var(--dur-slow) * 3) var(--ease-standard) infinite}
260
+ .pire-skeleton[data-shape="text"]{height:var(--fs-body)}
261
+ .pire-skeleton[data-shape="block"]{height:var(--sp-16);border-radius:var(--radius-2)}
262
+ .pire-skeleton[data-shape="circle"]{width:var(--sp-8);height:var(--sp-8);border-radius:var(--radius-pill);flex:0 0 auto}
263
+ @keyframes pire-skeleton-sweep{from{background-position:150% 0}to{background-position:-50% 0}}
264
+ /* Geometry copied from .pire-tr / .pire-td, not approximated, so a loading table and a loaded
265
+ one are the same height and the rows do not jump when the data lands. */
266
+ .pire-skeleton-row{display:flex;align-items:center;height:var(--row-h-regular);border-bottom:1px solid var(--border-subtle)}
267
+ .pire-skeleton-row[data-density="condensed"]{height:var(--row-h-condensed)}
268
+ .pire-skeleton-row[data-density="comfortable"]{height:var(--row-h-comfortable)}
269
+ .pire-skeleton-cell{flex:1 1 0;min-width:0;display:flex;padding:0 var(--sp-3)}
270
+ .pire-skeleton-row[data-density="condensed"] .pire-skeleton-cell{padding:0 var(--sp-2)}
271
+ .pire-skeleton-cell[data-align="right"]{justify-content:flex-end}
272
+ .pire-skeleton-cell[data-align="center"]{justify-content:center}
273
+
206
274
  /* ---- Data ------------------------------------------------------------ */
207
275
  .pire-table-wrap{overflow:auto;background:var(--surface-card);border:1px solid var(--border-subtle);border-radius:var(--radius-2)}
208
276
  .pire-table{width:100%;border-collapse:collapse;font:var(--type-body)}
@@ -348,6 +416,10 @@
348
416
 
349
417
  /* ---- Reduced motion --------------------------------------------------- */
350
418
  @media (prefers-reduced-motion: reduce){.pire-modal,.pire-toast,.pire-popover,.pire-tooltip,.pire-sidepanel{animation:none}
419
+ /* The skeleton also drops its background sweep, not just the animation: an infinite shimmer is
420
+ exactly the "large, repeated motion" the preference is asking us to stop, and the placeholder
421
+ still reads from its shape and surface contrast alone. */
422
+ .pire-skeleton{animation:none;background-image:none}
351
423
  /* !important is the point here: this must beat every component transition, including any a
352
424
  consuming app has layered on top. Honouring the OS motion preference outranks the style rule. */
353
425
  /* biome-ignore lint/complexity/noImportantStyles: accessibility override must win unconditionally */