@financedistrict/apps-ui 0.1.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 (136) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +119 -0
  3. package/dist/Radio-BYWz1n7p.d.cts +32 -0
  4. package/dist/Radio-BYWz1n7p.d.ts +32 -0
  5. package/dist/SelectTrigger-By0iPBI3.d.cts +78 -0
  6. package/dist/SelectTrigger-By0iPBI3.d.ts +78 -0
  7. package/dist/accordion.cjs +322 -0
  8. package/dist/accordion.d.cts +156 -0
  9. package/dist/accordion.d.ts +156 -0
  10. package/dist/accordion.js +2 -0
  11. package/dist/apps-ui.umd.js +1 -0
  12. package/dist/button.cjs +366 -0
  13. package/dist/button.d.cts +52 -0
  14. package/dist/button.d.ts +52 -0
  15. package/dist/button.js +3 -0
  16. package/dist/checkbox.cjs +74 -0
  17. package/dist/checkbox.d.cts +63 -0
  18. package/dist/checkbox.d.ts +63 -0
  19. package/dist/checkbox.js +2 -0
  20. package/dist/chunk-3RU7JMFE.js +27 -0
  21. package/dist/chunk-5TWRK4NW.js +783 -0
  22. package/dist/chunk-7EOSDFS3.js +9 -0
  23. package/dist/chunk-AIQ4UOGK.js +142 -0
  24. package/dist/chunk-B3MMGTPV.js +200 -0
  25. package/dist/chunk-DAHQYLUI.js +146 -0
  26. package/dist/chunk-DGBXADSS.js +1 -0
  27. package/dist/chunk-E6KPNTWW.js +295 -0
  28. package/dist/chunk-EOAB6G2D.js +149 -0
  29. package/dist/chunk-FAOVKDVM.js +32 -0
  30. package/dist/chunk-FV76Z4Z2.js +78 -0
  31. package/dist/chunk-GLTLBUZ7.js +285 -0
  32. package/dist/chunk-GTZFV43T.js +154 -0
  33. package/dist/chunk-HRGZNL4A.js +88 -0
  34. package/dist/chunk-IEVN3J2F.js +98 -0
  35. package/dist/chunk-IRS3T6LF.js +352 -0
  36. package/dist/chunk-IYYHCQXG.js +146 -0
  37. package/dist/chunk-JIXW3455.js +89 -0
  38. package/dist/chunk-N4VAP2GB.js +77 -0
  39. package/dist/chunk-SJ2ZRYVV.js +167 -0
  40. package/dist/chunk-SNU4YXOR.js +66 -0
  41. package/dist/chunk-TT6JQATV.js +99 -0
  42. package/dist/chunk-UPQ3ZPCO.js +66 -0
  43. package/dist/chunk-UVUSVFOH.js +264 -0
  44. package/dist/chunk-WJFPGGJB.js +255 -0
  45. package/dist/chunk-WXEBQA7A.js +371 -0
  46. package/dist/chunk-Y2O4YYZQ.js +184 -0
  47. package/dist/cut-corner.cjs +111 -0
  48. package/dist/cut-corner.d.cts +49 -0
  49. package/dist/cut-corner.d.ts +49 -0
  50. package/dist/cut-corner.js +1 -0
  51. package/dist/dialog.cjs +441 -0
  52. package/dist/dialog.d.cts +240 -0
  53. package/dist/dialog.d.ts +240 -0
  54. package/dist/dialog.js +3 -0
  55. package/dist/feature-card.cjs +182 -0
  56. package/dist/feature-card.d.cts +54 -0
  57. package/dist/feature-card.d.ts +54 -0
  58. package/dist/feature-card.js +2 -0
  59. package/dist/filter.cjs +873 -0
  60. package/dist/filter.d.cts +129 -0
  61. package/dist/filter.d.ts +129 -0
  62. package/dist/filter.js +6 -0
  63. package/dist/index.cjs +4583 -0
  64. package/dist/index.d.cts +29 -0
  65. package/dist/index.d.ts +29 -0
  66. package/dist/index.js +27 -0
  67. package/dist/inline-select.cjs +856 -0
  68. package/dist/inline-select.d.cts +92 -0
  69. package/dist/inline-select.d.ts +92 -0
  70. package/dist/inline-select.js +6 -0
  71. package/dist/input.cjs +263 -0
  72. package/dist/input.d.cts +157 -0
  73. package/dist/input.d.ts +157 -0
  74. package/dist/input.js +4 -0
  75. package/dist/panel.cjs +185 -0
  76. package/dist/panel.d.cts +103 -0
  77. package/dist/panel.d.ts +103 -0
  78. package/dist/panel.js +2 -0
  79. package/dist/property-grid.cjs +110 -0
  80. package/dist/property-grid.d.cts +56 -0
  81. package/dist/property-grid.d.ts +56 -0
  82. package/dist/property-grid.js +2 -0
  83. package/dist/radio.cjs +74 -0
  84. package/dist/radio.d.cts +39 -0
  85. package/dist/radio.d.ts +39 -0
  86. package/dist/radio.js +2 -0
  87. package/dist/scrollbar.cjs +34 -0
  88. package/dist/scrollbar.d.cts +35 -0
  89. package/dist/scrollbar.d.ts +35 -0
  90. package/dist/scrollbar.js +1 -0
  91. package/dist/search-bar.cjs +408 -0
  92. package/dist/search-bar.d.cts +42 -0
  93. package/dist/search-bar.d.ts +42 -0
  94. package/dist/search-bar.js +4 -0
  95. package/dist/select.cjs +998 -0
  96. package/dist/select.d.cts +357 -0
  97. package/dist/select.d.ts +357 -0
  98. package/dist/select.js +5 -0
  99. package/dist/selectable-feature-card.cjs +159 -0
  100. package/dist/selectable-feature-card.d.cts +74 -0
  101. package/dist/selectable-feature-card.d.ts +74 -0
  102. package/dist/selectable-feature-card.js +3 -0
  103. package/dist/sidebar-item.cjs +175 -0
  104. package/dist/sidebar-item.d.cts +75 -0
  105. package/dist/sidebar-item.d.ts +75 -0
  106. package/dist/sidebar-item.js +2 -0
  107. package/dist/switch.cjs +86 -0
  108. package/dist/switch.d.cts +72 -0
  109. package/dist/switch.d.ts +72 -0
  110. package/dist/switch.js +2 -0
  111. package/dist/table-cell.cjs +196 -0
  112. package/dist/table-cell.d.cts +76 -0
  113. package/dist/table-cell.d.ts +76 -0
  114. package/dist/table-cell.js +2 -0
  115. package/dist/table-title.cjs +34 -0
  116. package/dist/table-title.d.cts +16 -0
  117. package/dist/table-title.d.ts +16 -0
  118. package/dist/table-title.js +2 -0
  119. package/dist/tabs.cjs +299 -0
  120. package/dist/tabs.d.cts +116 -0
  121. package/dist/tabs.d.ts +116 -0
  122. package/dist/tabs.js +2 -0
  123. package/dist/toast.cjs +87 -0
  124. package/dist/toast.d.cts +48 -0
  125. package/dist/toast.d.ts +48 -0
  126. package/dist/toast.js +2 -0
  127. package/dist/token-input.cjs +415 -0
  128. package/dist/token-input.d.cts +90 -0
  129. package/dist/token-input.d.ts +90 -0
  130. package/dist/token-input.js +3 -0
  131. package/dist/tooltip.cjs +384 -0
  132. package/dist/tooltip.d.cts +142 -0
  133. package/dist/tooltip.d.ts +142 -0
  134. package/dist/tooltip.js +2 -0
  135. package/package.json +200 -0
  136. package/src/fonts.css +33 -0
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 First Digital Trust Limited ("FD"). All rights reserved.
2
+
3
+ This software and the accompanying files (the "Software") are proprietary and
4
+ confidential to FD. The Software is published to a public package registry
5
+ solely to allow installation and use by FD's own applications and by prototypes
6
+ authorized in writing by FD. Publication to that registry does NOT place the
7
+ Software in the public domain and does NOT grant any license under FD's
8
+ copyright or other rights.
9
+
10
+ Except as expressly permitted by a separate written agreement with FD, no
11
+ person or entity may use, copy, reproduce, modify, adapt, merge, publish,
12
+ redistribute, sublicense, or sell the Software or any portion of it, or create
13
+ derivative works from it.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL FD BE LIABLE
18
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN
19
+ CONNECTION WITH THE SOFTWARE OR ITS USE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @financedistrict/apps-ui
2
+
3
+ The **single shared base-component library** for all FD apps (Button, Input, Modal, DataTable, etc.). App-specific _semantic_ components do NOT live here — they live in each app.
4
+
5
+ **React + Tailwind v4** (ADR-0001), styled via Tailwind utility classes — semantic tokens from `@financedistrict/apps-tokens` where a value is meant to be shared, plain Tailwind utilities and arbitrary values for everything component-specific (see Authoring rules below). Components are authored RSC-safe **unless they measure their own geometry** — anything with a cut corner (`clip-path` measured via `useLayoutEffect`/`ResizeObserver`) is `"use client"` (a standard RSC opt-out, not a framework-specific API). See ADR-0009.
6
+
7
+ ## Components
8
+
9
+ ### Button (`@financedistrict/apps-ui/button`)
10
+
11
+ `cva` variant table + `cn`, bound to the `--button-*` semantic tokens (ADR-0009). Cut corners via `@financedistrict/apps-ui/cut-corner`.
12
+
13
+ ```tsx
14
+ import { Button } from "@financedistrict/apps-ui/button";
15
+ import { PlusIcon } from "@phosphor-icons/react";
16
+
17
+ <Button variation="primary" size="lg" onClick={...}>Save</Button>
18
+ <Button variation="secondary" leftSlot={<PlusIcon />}>Add</Button>
19
+ <Button variation="brand" loading>Submitting</Button>
20
+ <Button variation="ghost" iconOnly aria-label="More"><PlusIcon /></Button>
21
+ <Button asChild variation="primary"><a href="/x">Link</a></Button>
22
+ ```
23
+
24
+ - **`variation`**: `primary` (black) · `secondary` (outline) · `brand` (purple) · `ghost` · `destructive`. **`size`**: `sm` · `lg`. Plus `iconOnly`, `loading`, `asChild`, `leftSlot`/`rightSlot`.
25
+ - Cut corners on every variation except `ghost`; the `secondary` border follows the chamfer as an SVG stroke.
26
+
27
+ ## Storybook
28
+
29
+ Local authoring environment (never a CI gate):
30
+
31
+ ```bash
32
+ npm run storybook # from the repo root (or this package) — http://localhost:6006
33
+ ```
34
+
35
+ - **Framework: `@storybook/nextjs-vite`** (Vite under the hood). **Preview = an app's CSS entry:** `.storybook/preview.css` runs Tailwind v4 and imports `@financedistrict/apps-ui/fonts.css` + `@financedistrict/apps-tokens/theme.css`, so Archivo / Chakra Petch resolve through the portable `@fontsource` default (ADR-0008) exactly as in an app.
36
+ - **Theme toolbar** toggles the `light`/`dark` ancestor class — the same mechanism apps use, so stories need no `dark:` variants.
37
+ - Stories are colocated: `src/<component>/<Component>.stories.tsx`.
38
+
39
+ ## Fonts
40
+
41
+ This package doesn't own font _loading_ — only `@financedistrict/apps-tokens`
42
+ `theme.css`'s CSS variable contract (`--font-archivo`, `--font-chakra-petch`), which
43
+ is all it actually reads (see [ADR-0008](../../docs/adr/0008-framework-agnostic-fonts.md)).
44
+ `fonts.css` is one _optional_ way to satisfy that contract, using
45
+ [`@fontsource`](https://fontsource.org/) — self-hosted `@font-face` rules, no
46
+ compiler-specific transform, so it works identically under Vite, Next, a UMD build,
47
+ or a plain static HTML file with no build step.
48
+
49
+ ```css
50
+ /* app's main CSS entry */
51
+ @import "@financedistrict/apps-ui/fonts.css";
52
+ @import "@financedistrict/apps-tokens/theme.css";
53
+ ```
54
+
55
+ That's it — no JS wrapper, no root-element `className` to apply. Importing the CSS
56
+ registers the `@font-face` rules and defines the two variables globally.
57
+
58
+ Any app is free to load these fonts a different way instead — a plain Google Fonts
59
+ `<link>` tag (what zero-build CDN prototypes already do), `next/font/google` in a
60
+ Next app's own root layout, whatever fits — as long as it ends up defining the same
61
+ two variable names.
62
+
63
+ ## Icons
64
+
65
+ Phosphor only — see `.claude/rules/icons.md` (regular + fill weights, no inline SVG).
66
+
67
+ ## Authoring rules
68
+
69
+ This package ships two builds from one source (see
70
+ [`docs/guides/standalone-prototype-consumption.md`](../../docs/guides/standalone-prototype-consumption.md))
71
+ — a normal ESM/CJS build for the four apps, and a UMD bundle for zero-build CDN
72
+ prototypes. A few rules exist specifically so both builds stay possible from the same
73
+ component code:
74
+
75
+ - **Style entirely through Tailwind utility classes in `className` — not through a
76
+ separate CSS file or CSS-in-JS.** This is _not_ "everything must be a shared
77
+ token." Three tiers, all fine, all just utility classes:
78
+ 1. Shared semantic tokens for values that repeat across components/apps and need
79
+ to change consistently (`bg-card-background`, `text-focus`) — these come from
80
+ `@financedistrict/apps-tokens`.
81
+ 2. Plain Tailwind utilities that aren't tokens at all (`flex`, `gap-2`,
82
+ `items-center`, `rounded-md`) — completely normal for a component's own layout,
83
+ nothing to promote anywhere.
84
+ 3. A genuine one-off value that isn't worth sharing — use Tailwind's arbitrary-value
85
+ syntax (`top-[3px]`, `mt-[7px]`) directly in the component. Still just a utility
86
+ class, still lives entirely in that one component.
87
+ Never primitives (`--purple-500`) or raw inline hex/pixel `style` props — anything
88
+ expressible as a class should be one, from whichever of the three tiers actually
89
+ fits.
90
+ - **What's actually disallowed: a standalone CSS file or CSS-in-JS
91
+ (`Button.module.css`, styled-components, emotion, `<style jsx>`).** Not because
92
+ components can't have their own styling (they can, extensively — see above) but
93
+ because the CDN path has exactly one CSS delivery mechanism: `dist/tokens.css`,
94
+ produced by scanning every component's utility classes (see the content-scan
95
+ requirement in the consumption guide). A separate stylesheet or runtime-injected
96
+ styles has no way to reach a prototype that only ever loads that one file.
97
+ - **Never `import` `@financedistrict/apps-tokens`'s CSS from inside component
98
+ source**, for the same reason — the CSS gets loaded separately by whoever consumes
99
+ the component (an app importing `theme.css` itself, or a prototype linking
100
+ `dist/tokens.css` itself). A component should never assume it's responsible for
101
+ loading its own tokens.
102
+ - **No framework-specific APIs.** Nothing that only runs under Next.js, nothing that
103
+ assumes RSC. Components must render correctly in a plain client-rendered
104
+ environment, since the CDN path is exactly that and the four real apps are Vite,
105
+ not Next. This is the rule `next/font/google` violated (see
106
+ [ADR-0008](../../docs/adr/0008-framework-agnostic-fonts.md)) — fixed by not
107
+ repeating the mistake with a different framework-specific API later.
108
+ - **Don't assume a specific React renderer wiring beyond the `react`/`react-dom` peer
109
+ dependencies.** The UMD build declares both external — it will not bundle its own
110
+ copy of React. Relying on anything Next- or Vite-specific breaks that assumption.
111
+
112
+ ## Consuming from a zero-build prototype (CDN)
113
+
114
+ This README covers the normal `npm install` + `import` path, used by the four real
115
+ apps. Standalone HTML/JSX prototypes (Metacarbon's screen deliveries) consume a
116
+ different build of this same package — a UMD bundle loaded via `<script src>`, no
117
+ bundler involved. See
118
+ [`docs/guides/standalone-prototype-consumption.md`](../../docs/guides/standalone-prototype-consumption.md)
119
+ for the exact `index.html` shape and the global variable name (`window.FDAppsUI`).
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+
3
+ interface RadioProps extends Omit<React.ComponentProps<"input">, "type" | "size"> {
4
+ /**
5
+ * Convenience change callback with the next checked state already
6
+ * extracted. Fires alongside (not instead of) the native `onChange`.
7
+ * Radio semantics: the platform fires `change` only when this radio
8
+ * BECOMES checked (never on deselection by a sibling), so the payload is
9
+ * `true` in practice — the boolean type keeps the ADR-0010 `on<Value>Change`
10
+ * contract shared with Checkbox. Controlled/uncontrolled usage is the
11
+ * standard input contract: `defaultChecked` (uncontrolled, the default) or
12
+ * `checked` + a change handler (controlled).
13
+ */
14
+ onCheckedChange?: (checked: boolean) => void;
15
+ /** Class names for the outer wrapper span (positions the ● indicator). */
16
+ wrapperClassName?: string;
17
+ }
18
+ /**
19
+ * Radio control — the Figma `radio-control` set (node 5117:5677), rendered as
20
+ * a real `<input type="radio">` so grouping (`name`), keyboard behavior
21
+ * (arrow keys move selection within a group; only the checked radio is in the
22
+ * tab order), forms, and label association all come from the platform. There
23
+ * is no RadioGroup wrapper: give radios the same `name` and wrap the set in a
24
+ * `<fieldset>` with a `<legend>` for a group label.
25
+ *
26
+ * Ships without a label (the Figma set is the bare 20×20 control): associate
27
+ * one by wrapping in a `<label>` or via `htmlFor`/`id`; icon-style usages
28
+ * need an `aria-label`.
29
+ */
30
+ declare function Radio({ className, wrapperClassName, onChange, onCheckedChange, ...props }: RadioProps): React.JSX.Element;
31
+
32
+ export { Radio as R, type RadioProps as a };
@@ -0,0 +1,32 @@
1
+ import * as React from 'react';
2
+
3
+ interface RadioProps extends Omit<React.ComponentProps<"input">, "type" | "size"> {
4
+ /**
5
+ * Convenience change callback with the next checked state already
6
+ * extracted. Fires alongside (not instead of) the native `onChange`.
7
+ * Radio semantics: the platform fires `change` only when this radio
8
+ * BECOMES checked (never on deselection by a sibling), so the payload is
9
+ * `true` in practice — the boolean type keeps the ADR-0010 `on<Value>Change`
10
+ * contract shared with Checkbox. Controlled/uncontrolled usage is the
11
+ * standard input contract: `defaultChecked` (uncontrolled, the default) or
12
+ * `checked` + a change handler (controlled).
13
+ */
14
+ onCheckedChange?: (checked: boolean) => void;
15
+ /** Class names for the outer wrapper span (positions the ● indicator). */
16
+ wrapperClassName?: string;
17
+ }
18
+ /**
19
+ * Radio control — the Figma `radio-control` set (node 5117:5677), rendered as
20
+ * a real `<input type="radio">` so grouping (`name`), keyboard behavior
21
+ * (arrow keys move selection within a group; only the checked radio is in the
22
+ * tab order), forms, and label association all come from the platform. There
23
+ * is no RadioGroup wrapper: give radios the same `name` and wrap the set in a
24
+ * `<fieldset>` with a `<legend>` for a group label.
25
+ *
26
+ * Ships without a label (the Figma set is the bare 20×20 control): associate
27
+ * one by wrapping in a `<label>` or via `htmlFor`/`id`; icon-style usages
28
+ * need an `aria-label`.
29
+ */
30
+ declare function Radio({ className, wrapperClassName, onChange, onCheckedChange, ...props }: RadioProps): React.JSX.Element;
31
+
32
+ export { Radio as R, type RadioProps as a };
@@ -0,0 +1,78 @@
1
+ import * as React from 'react';
2
+
3
+ /** A selectable option. `disabled` rows render but are skipped by keyboard nav. */
4
+ type SelectOption = {
5
+ value: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ /**
9
+ * Optional adornment before the label (usually a 16px Phosphor icon). In
10
+ * multi mode it sits after the checkbox (checkbox → leading → label). Purely
11
+ * decorative — rendered `aria-hidden`, and it inherits the row's text colour
12
+ * (so it recolours to the brand foreground on the single-select selected row).
13
+ */
14
+ leadingSlot?: React.ReactNode;
15
+ /**
16
+ * Optional adornment after the label (usually a 16px Phosphor icon), pinned to
17
+ * the trailing edge of the row. Decorative + colour-inheriting, like
18
+ * `leadingSlot`.
19
+ */
20
+ trailingSlot?: React.ReactNode;
21
+ };
22
+ /**
23
+ * Pure value-display helper — decides what the trigger shows for a given
24
+ * selection. Kept free of React so it is unit-testable in isolation:
25
+ * - `[]` → the placeholder (`isPlaceholder: true`)
26
+ * - one → that option's label
27
+ * - many → `"${n} selected"` (never overflows the single lg line)
28
+ *
29
+ * A selected value with no matching option is ignored for the single-label case
30
+ * (it can't be labelled); the count path still counts it.
31
+ */
32
+ declare function selectTriggerLabel(options: SelectOption[], selected: string[], placeholder: string): {
33
+ text: string;
34
+ isPlaceholder: boolean;
35
+ };
36
+ interface SelectTriggerProps extends Omit<React.ComponentProps<"button">, "value"> {
37
+ /** `field` (bordered box) | `line` (bottom border only). */
38
+ variation?: "field" | "line";
39
+ /** Error state — destructive border/label/hint (out-ranks the focus border). */
40
+ error?: boolean;
41
+ /** LINE-only: hide the resting bottom line (still shows on focus/error). */
42
+ showBottomLine?: boolean;
43
+ /** Whether the listbox is currently open (flips the caret + `aria-expanded`). */
44
+ open?: boolean;
45
+ /** Resolved display text (from `selectTriggerLabel`). */
46
+ displayText: string;
47
+ /** Whether `displayText` is the placeholder (muted) vs a real value (accent). */
48
+ isPlaceholder: boolean;
49
+ /** Uppercase label above the field; omitted → no label row. */
50
+ label?: React.ReactNode;
51
+ /** Helper text below the field; destructive when `error`. */
52
+ hint?: React.ReactNode;
53
+ /** Leading adornment (decorative — kept out of the a11y tree via `aria-hidden`). */
54
+ leadingSlot?: React.ReactNode;
55
+ /** id of the field `<button>` (label association + `aria-labelledby` target). */
56
+ id: string;
57
+ /** id of the hint node, linked via `aria-describedby` when a hint is present. */
58
+ hintId?: string;
59
+ /** Class names for the outer wrapper (the label/field/hint stack). */
60
+ wrapperClassName?: string;
61
+ /** Class names for the field row (the trigger button), not the wrapper. */
62
+ fieldClassName?: string;
63
+ }
64
+ /**
65
+ * The Select trigger — a `<button type="button">` styled as an input field
66
+ * (reusing `inputVariants`), with an optional uppercase label above and a hint
67
+ * below (both owned by the component, mirroring `Input`). It renders a leading
68
+ * slot, the current value (or placeholder), and a trailing caret that flips
69
+ * when `open`.
70
+ *
71
+ * This is a presentational leaf: all open/selection/keyboard orchestration and
72
+ * the ARIA popup wiring (`aria-haspopup`/`aria-expanded`/`aria-controls`/
73
+ * `aria-activedescendant`) live in `Select`, which spreads them through
74
+ * `...props`. `ref` is a normal React 19 prop, forwarded to the button.
75
+ */
76
+ declare function SelectTrigger({ variation, error, showBottomLine, open, displayText, isPlaceholder, label, hint, leadingSlot, id, hintId, disabled, className, wrapperClassName, fieldClassName, ref, ...props }: SelectTriggerProps): React.JSX.Element;
77
+
78
+ export { type SelectOption as S, SelectTrigger as a, type SelectTriggerProps as b, selectTriggerLabel as s };
@@ -0,0 +1,78 @@
1
+ import * as React from 'react';
2
+
3
+ /** A selectable option. `disabled` rows render but are skipped by keyboard nav. */
4
+ type SelectOption = {
5
+ value: string;
6
+ label: string;
7
+ disabled?: boolean;
8
+ /**
9
+ * Optional adornment before the label (usually a 16px Phosphor icon). In
10
+ * multi mode it sits after the checkbox (checkbox → leading → label). Purely
11
+ * decorative — rendered `aria-hidden`, and it inherits the row's text colour
12
+ * (so it recolours to the brand foreground on the single-select selected row).
13
+ */
14
+ leadingSlot?: React.ReactNode;
15
+ /**
16
+ * Optional adornment after the label (usually a 16px Phosphor icon), pinned to
17
+ * the trailing edge of the row. Decorative + colour-inheriting, like
18
+ * `leadingSlot`.
19
+ */
20
+ trailingSlot?: React.ReactNode;
21
+ };
22
+ /**
23
+ * Pure value-display helper — decides what the trigger shows for a given
24
+ * selection. Kept free of React so it is unit-testable in isolation:
25
+ * - `[]` → the placeholder (`isPlaceholder: true`)
26
+ * - one → that option's label
27
+ * - many → `"${n} selected"` (never overflows the single lg line)
28
+ *
29
+ * A selected value with no matching option is ignored for the single-label case
30
+ * (it can't be labelled); the count path still counts it.
31
+ */
32
+ declare function selectTriggerLabel(options: SelectOption[], selected: string[], placeholder: string): {
33
+ text: string;
34
+ isPlaceholder: boolean;
35
+ };
36
+ interface SelectTriggerProps extends Omit<React.ComponentProps<"button">, "value"> {
37
+ /** `field` (bordered box) | `line` (bottom border only). */
38
+ variation?: "field" | "line";
39
+ /** Error state — destructive border/label/hint (out-ranks the focus border). */
40
+ error?: boolean;
41
+ /** LINE-only: hide the resting bottom line (still shows on focus/error). */
42
+ showBottomLine?: boolean;
43
+ /** Whether the listbox is currently open (flips the caret + `aria-expanded`). */
44
+ open?: boolean;
45
+ /** Resolved display text (from `selectTriggerLabel`). */
46
+ displayText: string;
47
+ /** Whether `displayText` is the placeholder (muted) vs a real value (accent). */
48
+ isPlaceholder: boolean;
49
+ /** Uppercase label above the field; omitted → no label row. */
50
+ label?: React.ReactNode;
51
+ /** Helper text below the field; destructive when `error`. */
52
+ hint?: React.ReactNode;
53
+ /** Leading adornment (decorative — kept out of the a11y tree via `aria-hidden`). */
54
+ leadingSlot?: React.ReactNode;
55
+ /** id of the field `<button>` (label association + `aria-labelledby` target). */
56
+ id: string;
57
+ /** id of the hint node, linked via `aria-describedby` when a hint is present. */
58
+ hintId?: string;
59
+ /** Class names for the outer wrapper (the label/field/hint stack). */
60
+ wrapperClassName?: string;
61
+ /** Class names for the field row (the trigger button), not the wrapper. */
62
+ fieldClassName?: string;
63
+ }
64
+ /**
65
+ * The Select trigger — a `<button type="button">` styled as an input field
66
+ * (reusing `inputVariants`), with an optional uppercase label above and a hint
67
+ * below (both owned by the component, mirroring `Input`). It renders a leading
68
+ * slot, the current value (or placeholder), and a trailing caret that flips
69
+ * when `open`.
70
+ *
71
+ * This is a presentational leaf: all open/selection/keyboard orchestration and
72
+ * the ARIA popup wiring (`aria-haspopup`/`aria-expanded`/`aria-controls`/
73
+ * `aria-activedescendant`) live in `Select`, which spreads them through
74
+ * `...props`. `ref` is a normal React 19 prop, forwarded to the button.
75
+ */
76
+ declare function SelectTrigger({ variation, error, showBottomLine, open, displayText, isPlaceholder, label, hint, leadingSlot, id, hintId, disabled, className, wrapperClassName, fieldClassName, ref, ...props }: SelectTriggerProps): React.JSX.Element;
77
+
78
+ export { type SelectOption as S, SelectTrigger as a, type SelectTriggerProps as b, selectTriggerLabel as s };
@@ -0,0 +1,322 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var react = require('@phosphor-icons/react');
5
+ var clsx = require('clsx');
6
+ var tailwindMerge = require('tailwind-merge');
7
+ var classVarianceAuthority = require('class-variance-authority');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+
10
+ function _interopNamespace(e) {
11
+ if (e && e.__esModule) return e;
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
29
+
30
+ // src/accordion/Accordion.tsx
31
+ function cn(...inputs) {
32
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
33
+ }
34
+ var accordionRootVariants = classVarianceAuthority.cva(["flex flex-col"]);
35
+ var accordionItemVariants = classVarianceAuthority.cva(["border-b border-card-border"], {
36
+ variants: {
37
+ disabled: {
38
+ true: "opacity-60",
39
+ false: ""
40
+ }
41
+ },
42
+ defaultVariants: {
43
+ disabled: false
44
+ }
45
+ });
46
+ var accordionTriggerVariants = classVarianceAuthority.cva(
47
+ [
48
+ "group flex w-full cursor-pointer items-start gap-4 pt-3 text-left",
49
+ "outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
50
+ "disabled:cursor-not-allowed"
51
+ ],
52
+ {
53
+ variants: {
54
+ open: {
55
+ true: "pb-2",
56
+ false: "pb-3"
57
+ }
58
+ },
59
+ defaultVariants: {
60
+ open: false
61
+ }
62
+ }
63
+ );
64
+ var accordionTriggerTitleVariants = classVarianceAuthority.cva([
65
+ "body-02-medium min-w-0 flex-1 break-words text-card-foreground"
66
+ ]);
67
+ var accordionTriggerIconVariants = classVarianceAuthority.cva([
68
+ "flex shrink-0 items-center text-card-foreground-muted transition-colors",
69
+ "group-hover:text-card-foreground",
70
+ "[&_svg]:size-4 [&_svg]:shrink-0"
71
+ ]);
72
+ var accordionContentVariants = classVarianceAuthority.cva([
73
+ "body-03 pb-3 text-card-foreground-muted"
74
+ ]);
75
+ var AccordionContext = React__namespace.createContext(
76
+ null
77
+ );
78
+ function useAccordionContext(part) {
79
+ const context = React__namespace.useContext(AccordionContext);
80
+ if (context === null) {
81
+ throw new Error(`${part} must be used within <Accordion.Root>`);
82
+ }
83
+ return context;
84
+ }
85
+ var AccordionItemContext = React__namespace.createContext(null);
86
+ function useAccordionItemContext(part) {
87
+ const context = React__namespace.useContext(AccordionItemContext);
88
+ if (context === null) {
89
+ throw new Error(`${part} must be used within <Accordion.Item>`);
90
+ }
91
+ return context;
92
+ }
93
+ function setRef(ref, node) {
94
+ if (typeof ref === "function") ref(node);
95
+ else if (ref) ref.current = node;
96
+ }
97
+ function AccordionRoot({
98
+ type = "single",
99
+ value: valueProp,
100
+ defaultValue,
101
+ onValueChange,
102
+ collapsible = true,
103
+ className,
104
+ children,
105
+ ref,
106
+ ...props
107
+ }) {
108
+ const isMultiple = type === "multiple";
109
+ const isControlled = valueProp !== void 0;
110
+ const [uncontrolledValue, setUncontrolledValue] = React__namespace.useState(
111
+ () => defaultValue !== void 0 ? defaultValue : isMultiple ? [] : null
112
+ );
113
+ const openModel = isControlled ? valueProp : uncontrolledValue;
114
+ const commit = (next) => {
115
+ if (!isControlled) setUncontrolledValue(next);
116
+ onValueChange?.(next);
117
+ };
118
+ const isOpen = (itemValue) => isMultiple ? Array.isArray(openModel) && openModel.includes(itemValue) : openModel === itemValue;
119
+ const toggle = (itemValue) => {
120
+ if (isMultiple) {
121
+ const current2 = Array.isArray(openModel) ? openModel : [];
122
+ commit(
123
+ current2.includes(itemValue) ? current2.filter((v) => v !== itemValue) : [...current2, itemValue]
124
+ );
125
+ return;
126
+ }
127
+ const current = Array.isArray(openModel) ? null : openModel;
128
+ if (current === itemValue) {
129
+ commit(collapsible ? null : itemValue);
130
+ } else {
131
+ commit(itemValue);
132
+ }
133
+ };
134
+ const triggersRef = React__namespace.useRef(/* @__PURE__ */ new Set());
135
+ const registerTrigger = React__namespace.useCallback((node) => {
136
+ triggersRef.current.add(node);
137
+ }, []);
138
+ const unregisterTrigger = React__namespace.useCallback((node) => {
139
+ triggersRef.current.delete(node);
140
+ }, []);
141
+ const onTriggerKeyDown = (event) => {
142
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
143
+ const triggers = Array.from(triggersRef.current).filter((el) => !el.disabled).sort(
144
+ (a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
145
+ );
146
+ if (triggers.length === 0) return;
147
+ const currentIndex = triggers.indexOf(event.currentTarget);
148
+ if (currentIndex === -1) return;
149
+ event.preventDefault();
150
+ let nextIndex = currentIndex;
151
+ switch (event.key) {
152
+ case "ArrowDown":
153
+ nextIndex = (currentIndex + 1) % triggers.length;
154
+ break;
155
+ case "ArrowUp":
156
+ nextIndex = (currentIndex - 1 + triggers.length) % triggers.length;
157
+ break;
158
+ case "Home":
159
+ nextIndex = 0;
160
+ break;
161
+ case "End":
162
+ nextIndex = triggers.length - 1;
163
+ break;
164
+ }
165
+ triggers[nextIndex]?.focus();
166
+ };
167
+ const contextValue = {
168
+ type,
169
+ isOpen,
170
+ toggle,
171
+ registerTrigger,
172
+ unregisterTrigger,
173
+ onTriggerKeyDown
174
+ };
175
+ return /* @__PURE__ */ jsxRuntime.jsx(AccordionContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
176
+ "div",
177
+ {
178
+ ref,
179
+ "data-slot": "accordion",
180
+ className: cn(accordionRootVariants(), className),
181
+ ...props,
182
+ children
183
+ }
184
+ ) });
185
+ }
186
+ function AccordionItem({
187
+ value,
188
+ disabled = false,
189
+ className,
190
+ children,
191
+ ref,
192
+ ...props
193
+ }) {
194
+ const { isOpen } = useAccordionContext("Accordion.Item");
195
+ const open = isOpen(value);
196
+ const reactId = React__namespace.useId();
197
+ const triggerId = `${reactId}-trigger`;
198
+ const contentId = `${reactId}-content`;
199
+ const itemContext = {
200
+ value,
201
+ open,
202
+ disabled,
203
+ triggerId,
204
+ contentId
205
+ };
206
+ return /* @__PURE__ */ jsxRuntime.jsx(AccordionItemContext.Provider, { value: itemContext, children: /* @__PURE__ */ jsxRuntime.jsx(
207
+ "div",
208
+ {
209
+ ref,
210
+ "data-slot": "accordion-item",
211
+ "data-state": open ? "open" : "closed",
212
+ "data-disabled": disabled || void 0,
213
+ className: cn(accordionItemVariants({ disabled }), className),
214
+ ...props,
215
+ children
216
+ }
217
+ ) });
218
+ }
219
+ function AccordionTrigger({
220
+ className,
221
+ children,
222
+ onClick,
223
+ onKeyDown,
224
+ ref,
225
+ ...props
226
+ }) {
227
+ const { toggle, registerTrigger, unregisterTrigger, onTriggerKeyDown } = useAccordionContext("Accordion.Trigger");
228
+ const { value, open, disabled, triggerId, contentId } = useAccordionItemContext("Accordion.Trigger");
229
+ const buttonRef = React__namespace.useRef(null);
230
+ const setButtonRef = (node) => {
231
+ buttonRef.current = node;
232
+ setRef(ref, node);
233
+ };
234
+ React__namespace.useEffect(() => {
235
+ const node = buttonRef.current;
236
+ if (!node) return;
237
+ registerTrigger(node);
238
+ return () => unregisterTrigger(node);
239
+ }, [registerTrigger, unregisterTrigger]);
240
+ return /* @__PURE__ */ jsxRuntime.jsxs(
241
+ "button",
242
+ {
243
+ ...props,
244
+ ref: setButtonRef,
245
+ type: "button",
246
+ id: triggerId,
247
+ "data-slot": "accordion-trigger",
248
+ "aria-expanded": open,
249
+ "aria-controls": open ? contentId : void 0,
250
+ disabled,
251
+ className: cn(accordionTriggerVariants({ open }), className),
252
+ onClick: (event) => {
253
+ onClick?.(event);
254
+ if (!event.defaultPrevented) toggle(value);
255
+ },
256
+ onKeyDown: (event) => {
257
+ onKeyDown?.(event);
258
+ if (!event.defaultPrevented) onTriggerKeyDown(event);
259
+ },
260
+ children: [
261
+ /* @__PURE__ */ jsxRuntime.jsx(
262
+ "span",
263
+ {
264
+ "data-slot": "accordion-trigger-title",
265
+ className: accordionTriggerTitleVariants(),
266
+ children
267
+ }
268
+ ),
269
+ /* @__PURE__ */ jsxRuntime.jsx(
270
+ "span",
271
+ {
272
+ "data-slot": "accordion-trigger-icon",
273
+ "aria-hidden": "true",
274
+ className: accordionTriggerIconVariants(),
275
+ children: open ? /* @__PURE__ */ jsxRuntime.jsx(react.MinusIcon, {}) : /* @__PURE__ */ jsxRuntime.jsx(react.PlusIcon, {})
276
+ }
277
+ )
278
+ ]
279
+ }
280
+ );
281
+ }
282
+ function AccordionContent({
283
+ className,
284
+ children,
285
+ ref,
286
+ ...props
287
+ }) {
288
+ const { open, triggerId, contentId } = useAccordionItemContext("Accordion.Content");
289
+ if (!open) return null;
290
+ return /* @__PURE__ */ jsxRuntime.jsx(
291
+ "div",
292
+ {
293
+ ref,
294
+ role: "region",
295
+ id: contentId,
296
+ "aria-labelledby": triggerId,
297
+ "data-slot": "accordion-content",
298
+ "data-state": "open",
299
+ className: cn(accordionContentVariants(), className),
300
+ ...props,
301
+ children
302
+ }
303
+ );
304
+ }
305
+ var Accordion = {
306
+ Root: AccordionRoot,
307
+ Item: AccordionItem,
308
+ Trigger: AccordionTrigger,
309
+ Content: AccordionContent
310
+ };
311
+
312
+ exports.Accordion = Accordion;
313
+ exports.AccordionContent = AccordionContent;
314
+ exports.AccordionItem = AccordionItem;
315
+ exports.AccordionRoot = AccordionRoot;
316
+ exports.AccordionTrigger = AccordionTrigger;
317
+ exports.accordionContentVariants = accordionContentVariants;
318
+ exports.accordionItemVariants = accordionItemVariants;
319
+ exports.accordionRootVariants = accordionRootVariants;
320
+ exports.accordionTriggerIconVariants = accordionTriggerIconVariants;
321
+ exports.accordionTriggerTitleVariants = accordionTriggerTitleVariants;
322
+ exports.accordionTriggerVariants = accordionTriggerVariants;