@orianatech/pire 0.2.0 → 0.3.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.
@@ -0,0 +1,43 @@
1
+ import type * as React from 'react';
2
+ export type SkeletonShape = 'text' | 'block' | 'circle';
3
+ export interface SkeletonProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
4
+ /** text — a line of copy; block — a card, chart or image; circle — an avatar. */
5
+ shape?: SkeletonShape;
6
+ /** Overrides the shape's default. A percentage reads better than a pixel count in a
7
+ * fluid layout, and keeps the placeholder honest when the real value is unknown. */
8
+ width?: number | string;
9
+ height?: number | string;
10
+ }
11
+ /** Loading placeholder for content whose shape is known before its value is.
12
+ *
13
+ * Deliberately `aria-hidden`: a screen reader gains nothing from "graphic, graphic, graphic",
14
+ * and announcing placeholders as content is worse than silence. Announce the wait once, on the
15
+ * region that is loading — `aria-busy="true"` on the container, or a live-region message — and
16
+ * let the skeleton be purely visual. Nothing inside is focusable, so keyboard order is
17
+ * unaffected while the data is in flight.
18
+ *
19
+ * The shimmer is suppressed under `prefers-reduced-motion`; the placeholder still reads as one
20
+ * because the shape and the surface contrast carry it, not the animation. */
21
+ export declare function Skeleton({ shape, width, height, className, style, ...rest }: SkeletonProps): React.JSX.Element;
22
+ export interface SkeletonTableColumn {
23
+ align?: 'left' | 'right' | 'center';
24
+ /** Fixed column width. Omit to let the column share the free space, as DataTable does. */
25
+ width?: number | string;
26
+ /** Right-aligns the bar. Mirrors DataTable's column flag so a loading table and a loaded one
27
+ * put their numbers in the same place. */
28
+ numeric?: boolean;
29
+ }
30
+ export interface SkeletonTableRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
31
+ /** A column count, or one descriptor per column when alignment or width matters. */
32
+ columns: number | SkeletonTableColumn[];
33
+ /** Must match the DataTable it stands in for, or the rows jump on load. */
34
+ density?: 'condensed' | 'regular' | 'comfortable';
35
+ }
36
+ /** One placeholder row, sized to line up with `DataTable` — same row height per density, same
37
+ * cell padding, same bottom rule. Render a handful inside `.pire-table-wrap` (or above a real
38
+ * table's body) while the first page loads, so the grid does not resize when it arrives.
39
+ *
40
+ * It is not a real `<tr>` and cannot be handed to `DataTable` as a row: DataTable builds its
41
+ * collection from `rows`, and React Aria's table owns everything under it. Use it in place of
42
+ * the table, not inside one. */
43
+ export declare function SkeletonTableRow({ columns, density, className, style, ...rest }: SkeletonTableRowProps): React.JSX.Element;
@@ -0,0 +1,44 @@
1
+ import type * as React from 'react';
2
+ export type SkeletonShape = 'text' | 'block' | 'circle';
3
+ export interface SkeletonProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
4
+ /** text — a line of copy; block — a card, chart or image; circle — an avatar. */
5
+ shape?: SkeletonShape;
6
+ /** Overrides the shape's default. A percentage reads better than a pixel count in a
7
+ * fluid layout, and keeps the placeholder honest when the real value is unknown. */
8
+ width?: number | string;
9
+ height?: number | string;
10
+ }
11
+ /** Loading placeholder for content whose shape is known before its value is.
12
+ *
13
+ * Deliberately `aria-hidden`: a screen reader gains nothing from "graphic, graphic, graphic",
14
+ * and announcing placeholders as content is worse than silence. Announce the wait once, on the
15
+ * region that is loading — `aria-busy="true"` on the container, or a live-region message — and
16
+ * let the skeleton be purely visual. Nothing inside is focusable, so keyboard order is
17
+ * unaffected while the data is in flight.
18
+ *
19
+ * The shimmer is suppressed under `prefers-reduced-motion`; the placeholder still reads as one
20
+ * because the shape and the surface contrast carry it, not the animation. */
21
+ export declare function Skeleton({ shape, width, height, className, style, ...rest }: SkeletonProps): React.JSX.Element;
22
+ export interface SkeletonTableColumn {
23
+ align?: 'left' | 'right' | 'center';
24
+ /** Fixed column width. Omit to let the column share the free space, as DataTable does. */
25
+ width?: number | string;
26
+ /** Right-aligns the bar. Mirrors DataTable's column flag so a loading table and a loaded one
27
+ * put their numbers in the same place. */
28
+ numeric?: boolean;
29
+ }
30
+ export interface SkeletonTableRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
31
+ /** A column count, or one descriptor per column when alignment or width matters. */
32
+ columns: number | SkeletonTableColumn[];
33
+ /** Must match the DataTable it stands in for, or the rows jump on load. */
34
+ density?: 'condensed' | 'regular' | 'comfortable';
35
+ }
36
+ /** One placeholder row, sized to line up with `DataTable` — same row height per density, same
37
+ * cell padding, same bottom rule. Render a handful inside `.pire-table-wrap` (or above a real
38
+ * table's body) while the first page loads, so the grid does not resize when it arrives.
39
+ *
40
+ * It is not a real `<tr>` and cannot be handed to `DataTable` as a row: DataTable builds its
41
+ * collection from `rows`, and React Aria's table owns everything under it. Use it in place of
42
+ * the table, not inside one. */
43
+ export declare function SkeletonTableRow({ columns, density, className, style, ...rest }: SkeletonTableRowProps): React.JSX.Element;
44
+ //# sourceMappingURL=Skeleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Skeleton.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/Skeleton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAGpC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IAC3F,iFAAiF;IACjF,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;yFACqF;IACrF,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED;;;;;;;;;8EAS8E;AAC9E,wBAAgB,QAAQ,CAAC,EAAE,KAAc,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,aAAa,qBAKnG;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpC,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;+CAC2C;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC;IACnG,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAAC;IACxC,2EAA2E;IAC3E,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,GAAG,aAAa,CAAC;CACnD;AAOD;;;;;;iCAMiC;AACjC,wBAAgB,gBAAgB,CAAC,EAAE,OAAO,EAAE,OAAmB,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,qBAAqB,qBAmBlH"}
@@ -0,0 +1,58 @@
1
+ import * as React from 'react';
2
+ import type { ComboBoxOption } from './ComboBox.cjs';
3
+ export interface MultiComboBoxProps {
4
+ label?: React.ReactNode;
5
+ /** Selected values, in the order they were picked. */
6
+ value?: string[];
7
+ defaultValue?: string[];
8
+ onChange?: (values: string[]) => void;
9
+ /**
10
+ * Free-text as typed, on every keystroke.
11
+ *
12
+ * Server-side search is only partly supported, exactly as on ComboBox. `options` is passed
13
+ * to React Aria as `defaultItems`, so React Aria applies its own local "contains" filter on
14
+ * top of whatever you supply: any result whose `label` does not literally contain the typed
15
+ * text is hidden client-side, and when the local filter matches nothing the menu closes.
16
+ * Fuzzy, synonym, code-to-description and server-ranked matches are therefore dropped. Use
17
+ * this to observe input (analytics, a debounced prefetch), or for server search only where
18
+ * the returned labels are guaranteed to contain the query as a substring.
19
+ */
20
+ onInputChange?: (text: string) => void;
21
+ options?: Array<string | ComboBoxOption>;
22
+ placeholder?: string;
23
+ description?: React.ReactNode;
24
+ errorMessage?: React.ReactNode;
25
+ isInvalid?: boolean;
26
+ isDisabled?: boolean;
27
+ isRequired?: boolean;
28
+ isReadOnly?: boolean;
29
+ /** Let Enter commit text that is not in `options` — how product tags get coined. */
30
+ allowsCustomValue?: boolean;
31
+ size?: 'sm' | 'md' | 'lg';
32
+ fullWidth?: boolean;
33
+ name?: string;
34
+ className?: string;
35
+ style?: React.CSSProperties;
36
+ }
37
+ /** Multi-value type-ahead: pick several options, each shown as a removable chip, and — with
38
+ * `allowsCustomValue` — coin new ones by typing. Product tags, cost-centre sets, watchers.
39
+ *
40
+ * A separate component rather than a mode on ComboBox: ComboBox's `value` is a `string | null`
41
+ * and its `onChange` emits one key, while multi-select needs an array on both. Folding the two
42
+ * together would put a discriminated union in front of every existing consumer of
43
+ * `ComboBoxProps` — a breaking change in exchange for a purely additive feature.
44
+ *
45
+ * Three things worth knowing before you use it:
46
+ *
47
+ * - **Free text commits on Enter, never on blur.** React Aria treats the input text as
48
+ * independent of the selection in multi-select mode, so leaving the field discards whatever
49
+ * was typed but not committed. The chips are the only record of what was entered.
50
+ * - **Backspace on an empty input removes the last chip.** The handler runs in the capture
51
+ * phase because React Aria's own key handling sits on the same input and would otherwise act
52
+ * on the same key.
53
+ * - **The chips are plain `Tag`s, not a React Aria TagGroup.** React Aria's ComboBox builds its
54
+ * listbox collection by shallow-rendering its entire subtree, which swallows any collection
55
+ * component nested inside it — a TagGroup there registers its tags as listbox items and then
56
+ * crashes looking for grid state. Each chip's remove button is an ordinary Tab stop instead,
57
+ * and removals are announced through the live region below. */
58
+ export declare function MultiComboBox({ label, value, defaultValue, onChange, onInputChange, options, description, errorMessage, allowsCustomValue, size, fullWidth, className, style, ...rest }: MultiComboBoxProps): React.JSX.Element;
@@ -0,0 +1,59 @@
1
+ import * as React from 'react';
2
+ import type { ComboBoxOption } from './ComboBox.js';
3
+ export interface MultiComboBoxProps {
4
+ label?: React.ReactNode;
5
+ /** Selected values, in the order they were picked. */
6
+ value?: string[];
7
+ defaultValue?: string[];
8
+ onChange?: (values: string[]) => void;
9
+ /**
10
+ * Free-text as typed, on every keystroke.
11
+ *
12
+ * Server-side search is only partly supported, exactly as on ComboBox. `options` is passed
13
+ * to React Aria as `defaultItems`, so React Aria applies its own local "contains" filter on
14
+ * top of whatever you supply: any result whose `label` does not literally contain the typed
15
+ * text is hidden client-side, and when the local filter matches nothing the menu closes.
16
+ * Fuzzy, synonym, code-to-description and server-ranked matches are therefore dropped. Use
17
+ * this to observe input (analytics, a debounced prefetch), or for server search only where
18
+ * the returned labels are guaranteed to contain the query as a substring.
19
+ */
20
+ onInputChange?: (text: string) => void;
21
+ options?: Array<string | ComboBoxOption>;
22
+ placeholder?: string;
23
+ description?: React.ReactNode;
24
+ errorMessage?: React.ReactNode;
25
+ isInvalid?: boolean;
26
+ isDisabled?: boolean;
27
+ isRequired?: boolean;
28
+ isReadOnly?: boolean;
29
+ /** Let Enter commit text that is not in `options` — how product tags get coined. */
30
+ allowsCustomValue?: boolean;
31
+ size?: 'sm' | 'md' | 'lg';
32
+ fullWidth?: boolean;
33
+ name?: string;
34
+ className?: string;
35
+ style?: React.CSSProperties;
36
+ }
37
+ /** Multi-value type-ahead: pick several options, each shown as a removable chip, and — with
38
+ * `allowsCustomValue` — coin new ones by typing. Product tags, cost-centre sets, watchers.
39
+ *
40
+ * A separate component rather than a mode on ComboBox: ComboBox's `value` is a `string | null`
41
+ * and its `onChange` emits one key, while multi-select needs an array on both. Folding the two
42
+ * together would put a discriminated union in front of every existing consumer of
43
+ * `ComboBoxProps` — a breaking change in exchange for a purely additive feature.
44
+ *
45
+ * Three things worth knowing before you use it:
46
+ *
47
+ * - **Free text commits on Enter, never on blur.** React Aria treats the input text as
48
+ * independent of the selection in multi-select mode, so leaving the field discards whatever
49
+ * was typed but not committed. The chips are the only record of what was entered.
50
+ * - **Backspace on an empty input removes the last chip.** The handler runs in the capture
51
+ * phase because React Aria's own key handling sits on the same input and would otherwise act
52
+ * on the same key.
53
+ * - **The chips are plain `Tag`s, not a React Aria TagGroup.** React Aria's ComboBox builds its
54
+ * listbox collection by shallow-rendering its entire subtree, which swallows any collection
55
+ * component nested inside it — a TagGroup there registers its tags as listbox items and then
56
+ * crashes looking for grid state. Each chip's remove button is an ordinary Tab stop instead,
57
+ * and removals are announced through the live region below. */
58
+ export declare function MultiComboBox({ label, value, defaultValue, onChange, onInputChange, options, description, errorMessage, allowsCustomValue, size, fullWidth, className, style, ...rest }: MultiComboBoxProps): React.JSX.Element;
59
+ //# sourceMappingURL=MultiComboBox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiComboBox.d.ts","sourceRoot":"","sources":["../../../src/components/forms/MultiComboBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACtC;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B;AAKD;;;;;;;;;;;;;;;;;;;;kEAoBkE;AAClE,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAY,EAC/F,WAAW,EAAE,YAAY,EAAE,iBAAiB,EAAE,IAAW,EACzD,SAAgB,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,kBAAkB,qBAiHlE"}
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ export interface TextAreaProps {
3
+ label?: React.ReactNode;
4
+ value?: string;
5
+ defaultValue?: string;
6
+ /** Receives the raw string value. */
7
+ onChange?: (value: string) => void;
8
+ placeholder?: string;
9
+ /** Help text below the field. Replaced by errorMessage when invalid. */
10
+ description?: React.ReactNode;
11
+ errorMessage?: React.ReactNode;
12
+ isInvalid?: boolean;
13
+ isDisabled?: boolean;
14
+ isReadOnly?: boolean;
15
+ isRequired?: boolean;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ /** Visible lines before scrolling. Three is the ERP default: an address or a short note. */
18
+ rows?: number;
19
+ /** Grow with the content instead of scrolling. Off by default — see the component note. */
20
+ autoGrow?: boolean;
21
+ /** Ceiling for `autoGrow`, in lines. Beyond it the field scrolls, so a pasted essay
22
+ * cannot push the form's buttons off screen. */
23
+ maxRows?: number;
24
+ fullWidth?: boolean;
25
+ name?: string;
26
+ autoFocus?: boolean;
27
+ className?: string;
28
+ style?: React.CSSProperties;
29
+ textAreaRef?: React.Ref<HTMLTextAreaElement>;
30
+ }
31
+ /** Multi-line text entry — notes, addresses, rejection reasons. The prop API is Input's, so a
32
+ * field can be swapped between the two without relearning; labels, descriptions and errors are
33
+ * wired up by React Aria, so no manual aria-describedby is ever needed.
34
+ *
35
+ * `autoGrow` is opt-in, not always-on, for two reasons. It costs a synchronous measure-and-write
36
+ * on every keystroke, and in a dense form it makes every field below the one being typed in jump
37
+ * — the wrong trade in a screen the user is tabbing through. Turn it on where the content length
38
+ * is genuinely unknown (a long free-text comment) and leave it off everywhere else. When on, the
39
+ * height is only written when it actually changed, so a keystroke that does not wrap costs no
40
+ * layout; and the resize grip is removed, since a manual height would be overwritten on the next
41
+ * keystroke anyway. */
42
+ export declare function TextArea({ label, description, errorMessage, size, rows, autoGrow, maxRows, fullWidth, className, style, textAreaRef, onChange, ...rest }: TextAreaProps): React.JSX.Element;
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ export interface TextAreaProps {
3
+ label?: React.ReactNode;
4
+ value?: string;
5
+ defaultValue?: string;
6
+ /** Receives the raw string value. */
7
+ onChange?: (value: string) => void;
8
+ placeholder?: string;
9
+ /** Help text below the field. Replaced by errorMessage when invalid. */
10
+ description?: React.ReactNode;
11
+ errorMessage?: React.ReactNode;
12
+ isInvalid?: boolean;
13
+ isDisabled?: boolean;
14
+ isReadOnly?: boolean;
15
+ isRequired?: boolean;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ /** Visible lines before scrolling. Three is the ERP default: an address or a short note. */
18
+ rows?: number;
19
+ /** Grow with the content instead of scrolling. Off by default — see the component note. */
20
+ autoGrow?: boolean;
21
+ /** Ceiling for `autoGrow`, in lines. Beyond it the field scrolls, so a pasted essay
22
+ * cannot push the form's buttons off screen. */
23
+ maxRows?: number;
24
+ fullWidth?: boolean;
25
+ name?: string;
26
+ autoFocus?: boolean;
27
+ className?: string;
28
+ style?: React.CSSProperties;
29
+ textAreaRef?: React.Ref<HTMLTextAreaElement>;
30
+ }
31
+ /** Multi-line text entry — notes, addresses, rejection reasons. The prop API is Input's, so a
32
+ * field can be swapped between the two without relearning; labels, descriptions and errors are
33
+ * wired up by React Aria, so no manual aria-describedby is ever needed.
34
+ *
35
+ * `autoGrow` is opt-in, not always-on, for two reasons. It costs a synchronous measure-and-write
36
+ * on every keystroke, and in a dense form it makes every field below the one being typed in jump
37
+ * — the wrong trade in a screen the user is tabbing through. Turn it on where the content length
38
+ * is genuinely unknown (a long free-text comment) and leave it off everywhere else. When on, the
39
+ * height is only written when it actually changed, so a keystroke that does not wrap costs no
40
+ * layout; and the resize grip is removed, since a manual height would be overwritten on the next
41
+ * keystroke anyway. */
42
+ export declare function TextArea({ label, description, errorMessage, size, rows, autoGrow, maxRows, fullWidth, className, style, textAreaRef, onChange, ...rest }: TextAreaProps): React.JSX.Element;
43
+ //# sourceMappingURL=TextArea.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextArea.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextArea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,4FAA4F;IAC5F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;qDACiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,WAAW,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;CAC9C;AAED;;;;;;;;;;wBAUwB;AACxB,wBAAgB,QAAQ,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,IAAW,EAAE,IAAQ,EAAE,QAAQ,EAC1F,OAAO,EAAE,SAAgB,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,aAAa,qBAsD7F"}
@@ -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)}
@@ -121,8 +145,10 @@
121
145
  .pire-choice[data-focus-visible] .pire-choice-box{outline:var(--focus-w) solid var(--focus-color);outline-offset:var(--focus-offset)}
122
146
  .pire-choice[data-disabled] .pire-choice-box{background:var(--surface-disabled);border-color:var(--border-default)}
123
147
  .pire-choice-hint{display:block;font:var(--type-caption);color:var(--text-secondary)}
124
- .pire-switch-track{flex:0 0 auto;width:36px;height:20px;border-radius:var(--radius-pill);background:var(--gray-40);padding:2px;transition:background-color var(--dur-fast) var(--ease-standard)}
125
- .pire-switch-thumb{width:16px;height:16px;border-radius:var(--radius-pill);background:var(--gray-0);box-shadow:var(--shadow-1);transition:transform var(--dur-fast) var(--ease-standard)}
148
+ /* The track must establish a flex context: the thumb is a <span>, and an inline box
149
+ ignores width/height it rendered at 0x0 until this was added. */
150
+ .pire-switch-track{flex:0 0 auto;display:flex;align-items:center;width:36px;height:20px;border-radius:var(--radius-pill);background:var(--gray-40);padding:2px;transition:background-color var(--dur-fast) var(--ease-standard)}
151
+ .pire-switch-thumb{flex:0 0 auto;width:16px;height:16px;border-radius:var(--radius-pill);background:var(--gray-0);box-shadow:var(--shadow-1);transition:transform var(--dur-fast) var(--ease-standard)}
126
152
  .pire-choice[data-selected] .pire-switch-track{background:var(--accent-bg)}
127
153
  .pire-choice[data-selected] .pire-switch-thumb{transform:translateX(16px)}
128
154
  .pire-choice[data-focus-visible] .pire-switch-track{outline:var(--focus-w) solid var(--focus-color);outline-offset:var(--focus-offset)}
@@ -201,6 +227,28 @@
201
227
  .pire-progress[data-status="negative"] .pire-progress-fill{background:var(--status-negative-fg)}
202
228
  .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)}
203
229
 
230
+ /* ---- Skeleton ---------------------------------------------------------- */
231
+ /* The sweep is a lightness *difference* between two surface aliases, not a fixed direction:
232
+ --surface-hover is lighter than --surface-active in the light theme and darker in the dark
233
+ one, and either reads as a sweep. That keeps the placeholder on the shared alias layer
234
+ instead of introducing a skeleton-only token pair for both themes to maintain.
235
+ The duration is derived, not literal: a shimmer at --dur-slow (400ms) is agitated rather
236
+ than patient, and 3x is the slowest step the motion scale can express. */
237
+ .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}
238
+ .pire-skeleton[data-shape="text"]{height:var(--fs-body)}
239
+ .pire-skeleton[data-shape="block"]{height:var(--sp-16);border-radius:var(--radius-2)}
240
+ .pire-skeleton[data-shape="circle"]{width:var(--sp-8);height:var(--sp-8);border-radius:var(--radius-pill);flex:0 0 auto}
241
+ @keyframes pire-skeleton-sweep{from{background-position:150% 0}to{background-position:-50% 0}}
242
+ /* Geometry copied from .pire-tr / .pire-td, not approximated, so a loading table and a loaded
243
+ one are the same height and the rows do not jump when the data lands. */
244
+ .pire-skeleton-row{display:flex;align-items:center;height:var(--row-h-regular);border-bottom:1px solid var(--border-subtle)}
245
+ .pire-skeleton-row[data-density="condensed"]{height:var(--row-h-condensed)}
246
+ .pire-skeleton-row[data-density="comfortable"]{height:var(--row-h-comfortable)}
247
+ .pire-skeleton-cell{flex:1 1 0;min-width:0;display:flex;padding:0 var(--sp-3)}
248
+ .pire-skeleton-row[data-density="condensed"] .pire-skeleton-cell{padding:0 var(--sp-2)}
249
+ .pire-skeleton-cell[data-align="right"]{justify-content:flex-end}
250
+ .pire-skeleton-cell[data-align="center"]{justify-content:center}
251
+
204
252
  /* ---- Data ------------------------------------------------------------ */
205
253
  .pire-table-wrap{overflow:auto;background:var(--surface-card);border:1px solid var(--border-subtle);border-radius:var(--radius-2)}
206
254
  .pire-table{width:100%;border-collapse:collapse;font:var(--type-body)}
@@ -346,6 +394,10 @@
346
394
 
347
395
  /* ---- Reduced motion --------------------------------------------------- */
348
396
  @media (prefers-reduced-motion: reduce){.pire-modal,.pire-toast,.pire-popover,.pire-tooltip,.pire-sidepanel{animation:none}
397
+ /* The skeleton also drops its background sweep, not just the animation: an infinite shimmer is
398
+ exactly the "large, repeated motion" the preference is asking us to stop, and the placeholder
399
+ still reads from its shape and surface contrast alone. */
400
+ .pire-skeleton{animation:none;background-image:none}
349
401
  /* !important is the point here: this must beat every component transition, including any a
350
402
  consuming app has layered on top. Honouring the OS motion preference outranks the style rule. */
351
403
  /* biome-ignore lint/complexity/noImportantStyles: accessibility override must win unconditionally */