@lutra-ui-system/react 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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/dist/components/button/button.d.ts +105 -0
  4. package/dist/components/button/button.d.ts.map +1 -0
  5. package/dist/components/button/button.js +91 -0
  6. package/dist/components/button/button.js.map +1 -0
  7. package/dist/components/button/index.d.ts +3 -0
  8. package/dist/components/button/index.d.ts.map +1 -0
  9. package/dist/components/button/index.js +2 -0
  10. package/dist/components/button/index.js.map +1 -0
  11. package/dist/components/card/card.d.ts +100 -0
  12. package/dist/components/card/card.d.ts.map +1 -0
  13. package/dist/components/card/card.js +51 -0
  14. package/dist/components/card/card.js.map +1 -0
  15. package/dist/components/card/index.d.ts +3 -0
  16. package/dist/components/card/index.d.ts.map +1 -0
  17. package/dist/components/card/index.js +2 -0
  18. package/dist/components/card/index.js.map +1 -0
  19. package/dist/components/checkbox/checkbox.d.ts +67 -0
  20. package/dist/components/checkbox/checkbox.d.ts.map +1 -0
  21. package/dist/components/checkbox/checkbox.js +63 -0
  22. package/dist/components/checkbox/checkbox.js.map +1 -0
  23. package/dist/components/checkbox/index.d.ts +3 -0
  24. package/dist/components/checkbox/index.d.ts.map +1 -0
  25. package/dist/components/checkbox/index.js +2 -0
  26. package/dist/components/checkbox/index.js.map +1 -0
  27. package/dist/components/dialog/dialog.d.ts +88 -0
  28. package/dist/components/dialog/dialog.d.ts.map +1 -0
  29. package/dist/components/dialog/dialog.js +156 -0
  30. package/dist/components/dialog/dialog.js.map +1 -0
  31. package/dist/components/dialog/index.d.ts +3 -0
  32. package/dist/components/dialog/index.d.ts.map +1 -0
  33. package/dist/components/dialog/index.js +2 -0
  34. package/dist/components/dialog/index.js.map +1 -0
  35. package/dist/components/dropdown/dropdown.d.ts +89 -0
  36. package/dist/components/dropdown/dropdown.d.ts.map +1 -0
  37. package/dist/components/dropdown/dropdown.js +173 -0
  38. package/dist/components/dropdown/dropdown.js.map +1 -0
  39. package/dist/components/dropdown/index.d.ts +3 -0
  40. package/dist/components/dropdown/index.d.ts.map +1 -0
  41. package/dist/components/dropdown/index.js +2 -0
  42. package/dist/components/dropdown/index.js.map +1 -0
  43. package/dist/components/error-summary/error-summary.d.ts +81 -0
  44. package/dist/components/error-summary/error-summary.d.ts.map +1 -0
  45. package/dist/components/error-summary/error-summary.js +85 -0
  46. package/dist/components/error-summary/error-summary.js.map +1 -0
  47. package/dist/components/error-summary/index.d.ts +3 -0
  48. package/dist/components/error-summary/index.d.ts.map +1 -0
  49. package/dist/components/error-summary/index.js +2 -0
  50. package/dist/components/error-summary/index.js.map +1 -0
  51. package/dist/components/field/field.d.ts +55 -0
  52. package/dist/components/field/field.d.ts.map +1 -0
  53. package/dist/components/field/field.js +26 -0
  54. package/dist/components/field/field.js.map +1 -0
  55. package/dist/components/field/index.d.ts +3 -0
  56. package/dist/components/field/index.d.ts.map +1 -0
  57. package/dist/components/field/index.js +2 -0
  58. package/dist/components/field/index.js.map +1 -0
  59. package/dist/components/field-group/field-group.d.ts +67 -0
  60. package/dist/components/field-group/field-group.d.ts.map +1 -0
  61. package/dist/components/field-group/field-group.js +38 -0
  62. package/dist/components/field-group/field-group.js.map +1 -0
  63. package/dist/components/field-group/index.d.ts +3 -0
  64. package/dist/components/field-group/index.d.ts.map +1 -0
  65. package/dist/components/field-group/index.js +2 -0
  66. package/dist/components/field-group/index.js.map +1 -0
  67. package/dist/components/input/index.d.ts +3 -0
  68. package/dist/components/input/index.d.ts.map +1 -0
  69. package/dist/components/input/index.js +2 -0
  70. package/dist/components/input/index.js.map +1 -0
  71. package/dist/components/input/input.d.ts +96 -0
  72. package/dist/components/input/input.d.ts.map +1 -0
  73. package/dist/components/input/input.js +26 -0
  74. package/dist/components/input/input.js.map +1 -0
  75. package/dist/components/link/index.d.ts +3 -0
  76. package/dist/components/link/index.d.ts.map +1 -0
  77. package/dist/components/link/index.js +2 -0
  78. package/dist/components/link/index.js.map +1 -0
  79. package/dist/components/link/link.d.ts +44 -0
  80. package/dist/components/link/link.d.ts.map +1 -0
  81. package/dist/components/link/link.js +26 -0
  82. package/dist/components/link/link.js.map +1 -0
  83. package/dist/components/link-card/index.d.ts +3 -0
  84. package/dist/components/link-card/index.d.ts.map +1 -0
  85. package/dist/components/link-card/index.js +2 -0
  86. package/dist/components/link-card/index.js.map +1 -0
  87. package/dist/components/link-card/link-card.d.ts +62 -0
  88. package/dist/components/link-card/link-card.d.ts.map +1 -0
  89. package/dist/components/link-card/link-card.js +43 -0
  90. package/dist/components/link-card/link-card.js.map +1 -0
  91. package/dist/components/mobile-navigation/index.d.ts +3 -0
  92. package/dist/components/mobile-navigation/index.d.ts.map +1 -0
  93. package/dist/components/mobile-navigation/index.js +2 -0
  94. package/dist/components/mobile-navigation/index.js.map +1 -0
  95. package/dist/components/mobile-navigation/mobile-navigation.d.ts +88 -0
  96. package/dist/components/mobile-navigation/mobile-navigation.d.ts.map +1 -0
  97. package/dist/components/mobile-navigation/mobile-navigation.js +153 -0
  98. package/dist/components/mobile-navigation/mobile-navigation.js.map +1 -0
  99. package/dist/components/navigation-item/index.d.ts +3 -0
  100. package/dist/components/navigation-item/index.d.ts.map +1 -0
  101. package/dist/components/navigation-item/index.js +2 -0
  102. package/dist/components/navigation-item/index.js.map +1 -0
  103. package/dist/components/navigation-item/navigation-item.d.ts +38 -0
  104. package/dist/components/navigation-item/navigation-item.d.ts.map +1 -0
  105. package/dist/components/navigation-item/navigation-item.js +22 -0
  106. package/dist/components/navigation-item/navigation-item.js.map +1 -0
  107. package/dist/components/radio/index.d.ts +3 -0
  108. package/dist/components/radio/index.d.ts.map +1 -0
  109. package/dist/components/radio/index.js +2 -0
  110. package/dist/components/radio/index.js.map +1 -0
  111. package/dist/components/radio/radio.d.ts +58 -0
  112. package/dist/components/radio/radio.d.ts.map +1 -0
  113. package/dist/components/radio/radio.js +37 -0
  114. package/dist/components/radio/radio.js.map +1 -0
  115. package/dist/components/select/index.d.ts +3 -0
  116. package/dist/components/select/index.d.ts.map +1 -0
  117. package/dist/components/select/index.js +2 -0
  118. package/dist/components/select/index.js.map +1 -0
  119. package/dist/components/select/select.d.ts +67 -0
  120. package/dist/components/select/select.d.ts.map +1 -0
  121. package/dist/components/select/select.js +25 -0
  122. package/dist/components/select/select.js.map +1 -0
  123. package/dist/components/select-field/index.d.ts +3 -0
  124. package/dist/components/select-field/index.d.ts.map +1 -0
  125. package/dist/components/select-field/index.js +2 -0
  126. package/dist/components/select-field/index.js.map +1 -0
  127. package/dist/components/select-field/select-field.d.ts +43 -0
  128. package/dist/components/select-field/select-field.d.ts.map +1 -0
  129. package/dist/components/select-field/select-field.js +24 -0
  130. package/dist/components/select-field/select-field.js.map +1 -0
  131. package/dist/components/table/index.d.ts +3 -0
  132. package/dist/components/table/index.d.ts.map +1 -0
  133. package/dist/components/table/index.js +2 -0
  134. package/dist/components/table/index.js.map +1 -0
  135. package/dist/components/table/table.d.ts +251 -0
  136. package/dist/components/table/table.d.ts.map +1 -0
  137. package/dist/components/table/table.js +209 -0
  138. package/dist/components/table/table.js.map +1 -0
  139. package/dist/components/toggle/index.d.ts +3 -0
  140. package/dist/components/toggle/index.d.ts.map +1 -0
  141. package/dist/components/toggle/index.js +2 -0
  142. package/dist/components/toggle/index.js.map +1 -0
  143. package/dist/components/toggle/toggle.d.ts +66 -0
  144. package/dist/components/toggle/toggle.d.ts.map +1 -0
  145. package/dist/components/toggle/toggle.js +49 -0
  146. package/dist/components/toggle/toggle.js.map +1 -0
  147. package/dist/components/tooltip/index.d.ts +3 -0
  148. package/dist/components/tooltip/index.d.ts.map +1 -0
  149. package/dist/components/tooltip/index.js +2 -0
  150. package/dist/components/tooltip/index.js.map +1 -0
  151. package/dist/components/tooltip/tooltip.d.ts +74 -0
  152. package/dist/components/tooltip/tooltip.d.ts.map +1 -0
  153. package/dist/components/tooltip/tooltip.js +180 -0
  154. package/dist/components/tooltip/tooltip.js.map +1 -0
  155. package/dist/index.d.ts +57 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +38 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/internal/development.d.ts +2 -0
  160. package/dist/internal/development.d.ts.map +1 -0
  161. package/dist/internal/development.js +2 -0
  162. package/dist/internal/development.js.map +1 -0
  163. package/dist/internal/field-support.d.ts +68 -0
  164. package/dist/internal/field-support.d.ts.map +1 -0
  165. package/dist/internal/field-support.js +42 -0
  166. package/dist/internal/field-support.js.map +1 -0
  167. package/dist/internal/isomorphic-layout-effect.d.ts +26 -0
  168. package/dist/internal/isomorphic-layout-effect.d.ts.map +1 -0
  169. package/dist/internal/isomorphic-layout-effect.js +26 -0
  170. package/dist/internal/isomorphic-layout-effect.js.map +1 -0
  171. package/dist/styles.css +3474 -0
  172. package/dist/tokens.json +995 -0
  173. package/package.json +76 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 My Duong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # @lutra-ui-system/react
2
+
3
+ Accessibility-first React components for Lutra UI.
4
+
5
+ > **Prepared for public release, not yet published.** The intended first public package is
6
+ > `@lutra-ui-system/react@0.1.0`. Release automation, npm Trusted Publishing, and AWS-backed
7
+ > Storybook deployment are prepared in this repository, but no npm package has been published yet.
8
+ > See [`docs/releasing.md`](../../docs/releasing.md).
9
+
10
+ ## Usage
11
+
12
+ After the first public release is published, consumers will install the package and its peer
13
+ dependencies:
14
+
15
+ ```bash
16
+ pnpm add @lutra-ui-system/react react react-dom
17
+ ```
18
+
19
+ Import the stylesheet once, at the root of the application:
20
+
21
+ ```tsx
22
+ // Next.js App Router: app/layout.tsx — Vite: src/main.tsx
23
+ import '@lutra-ui-system/react/styles.css';
24
+ ```
25
+
26
+ Then import components by name:
27
+
28
+ ```tsx
29
+ import { Button } from '@lutra-ui-system/react';
30
+
31
+ export function Example() {
32
+ return <Button onClick={save}>Save</Button>;
33
+ }
34
+ ```
35
+
36
+ ### Fonts are the consumer's responsibility
37
+
38
+ The library asks for IBM Plex Sans first and falls back through the system stack. **It does not load
39
+ the font** — it adds no font package and makes no remote request, because how an application loads
40
+ its typefaces is the application's decision. Load IBM Plex Sans yourself, or override
41
+ `--lutra-font-family-sans`. Doing neither is fine: you get the system font.
42
+
43
+ ## Components
44
+
45
+ The current public surface includes:
46
+
47
+ - `Button`
48
+ - `Card`, `CardFooter`, `LinkCard`
49
+ - `Checkbox`, `Radio`, `Toggle`
50
+ - `Dialog`, `Dropdown`, `Tooltip`
51
+ - `Field`, `FieldGroup`, `Input`, `Select`, `SelectField`, `ErrorSummary`
52
+ - `Link`, `NavigationItem`, `MobileNavigation`
53
+ - `Table`, `TableRow`, `TableHeaderCell`, `TableCell`, `TableSelectionCell`, `TableRowActions`,
54
+ `TableEmptyState`
55
+
56
+ Component contracts live in their colocated Markdown files under `src/components/<component-name>/`,
57
+ and Storybook is the reviewable runtime reference for states, themes, and representative layouts.
58
+
59
+ ## Design tokens
60
+
61
+ 192 design tokens, generated from the Lutra UI Figma library and declared as CSS custom properties by
62
+ the stylesheet you already imported. Components read them; you can override them.
63
+
64
+ ```css
65
+ --lutra-color-text-primary: #111827;
66
+ --lutra-space-sm: 0.5rem;
67
+ --lutra-radius-md: 0.5rem;
68
+ ```
69
+
70
+ Full reference — naming, units, the sync process, the editing rule — is in
71
+ [`docs/design-tokens.md`](../../docs/design-tokens.md).
72
+
73
+ ### Themes
74
+
75
+ Light is the default. Dark is one attribute, on any element:
76
+
77
+ ```html
78
+ <html data-theme="dark"></html>
79
+ ```
80
+
81
+ The library does **not** read `prefers-color-scheme`. Deciding when an application is dark belongs to
82
+ the application, and plenty have no dark design at all — so set `data-theme` from your own theme
83
+ logic, where a user's explicit choice can still win over the system preference.
84
+
85
+ ### Overriding a token
86
+
87
+ Override on any element below the root and it wins outright, because a declaration on an element
88
+ beats one inherited into it:
89
+
90
+ ```css
91
+ .my-brand-section {
92
+ --lutra-color-brand-primary: #005ea2;
93
+ }
94
+ ```
95
+
96
+ Overriding at `:root` also works, but your stylesheet must load **after** `@lutra-ui-system/react/styles.css`
97
+ — `:root` and `[data-theme]` have equal specificity, so source order decides.
98
+
99
+ ### Tokens as data
100
+
101
+ ```ts
102
+ import tokens from '@lutra-ui-system/react/tokens.json' with { type: 'json' };
103
+ ```
104
+
105
+ Each entry carries its Figma name, whether it is themed, and its value or its light/dark pair. Useful
106
+ for tooling; components should read the CSS custom properties.
107
+
108
+ ## Public API
109
+
110
+ `src/index.ts` is the only supported entry point, and every export in it is listed explicitly. There
111
+ are no deep imports: the `exports` map allows `@lutra-ui-system/react`,
112
+ `@lutra-ui-system/react/styles.css`, and `@lutra-ui-system/react/tokens.json`, and nothing else.
113
+
114
+ ## Source layout
115
+
116
+ Components live one per directory under `src/components/`, in lowercase kebab-case, with the
117
+ implementation filename matching the directory:
118
+
119
+ ```
120
+ src/
121
+ ├── index.ts the package's public API
122
+ ├── styles.css the stylesheet entry point; composes the files below
123
+ ├── tokens/ the design-token layer
124
+ │ ├── source.json CANONICAL — the only hand-edited token file
125
+ │ ├── generated.css GENERATED — do not edit
126
+ │ ├── generated.json GENERATED — do not edit
127
+ │ └── tokens.test.ts token contract tests
128
+ ├── internal/ shared internals; never exported from the package root
129
+ │ └── development.ts
130
+ └── components/
131
+ └── button/
132
+ ├── button.tsx implementation
133
+ ├── button.test.tsx colocated tests
134
+ ├── button.css component styles and component tokens
135
+ ├── button.md component documentation
136
+ └── index.ts local barrel
137
+ ```
138
+
139
+ Development-only directories sit outside `src/`, where `tsconfig.build.json`'s `rootDir` cannot
140
+ compile them into `dist`:
141
+
142
+ ```
143
+ .storybook/ Storybook configuration
144
+ stories/foundations/ Foundations documentation and token verification
145
+ scripts/ build scripts, including the token generator
146
+ ```
147
+
148
+ A component is exported from its own `index.ts`; making it public is the separate, deliberate act of
149
+ adding it to `src/index.ts`. See [`docs/engineering-guidelines.md`](../../docs/engineering-guidelines.md)
150
+ sections 2 and 4.
151
+
152
+ ## How it is built
153
+
154
+ The package is compiled with `tsc` — there is no bundler. The build runs four steps:
155
+
156
+ ```
157
+ node scripts/clean-dist.mjs remove dist/ so no stale output survives
158
+ node scripts/generate-tokens.mjs regenerate the token layer from source.json
159
+ tsc -p tsconfig.build.json emit JavaScript, declarations, and source maps
160
+ node scripts/build-assets.mjs assemble dist/styles.css and dist/tokens.json, and verify both
161
+ ```
162
+
163
+ ```
164
+ src/index.ts -> dist/index.js + dist/index.d.ts
165
+ src/components/**/*.ts(x) -> dist/components/**/*.js + .d.ts
166
+ src/styles.css + tokens + component CSS -> dist/styles.css (one flat file)
167
+ src/tokens/generated.json -> dist/tokens.json
168
+ ```
169
+
170
+ Storybook is not part of this. It has its own command, it is never invoked by `pnpm build`, and
171
+ nothing under `.storybook/` or `stories/` can reach `dist`. The AWS deployment publishes that
172
+ separate Storybook build; it does not publish the npm package.
173
+
174
+ - **Every build starts from a clean `dist/`.** `tsc` only overwrites the files it emits, so without
175
+ the clean step the output of a renamed or deleted source file would survive and keep being
176
+ published — a stale `.d.ts` in particular would have consumers type-checking against an export
177
+ that no longer exists. The clean step uses `node:fs/promises` rather than a deletion dependency,
178
+ and removes only this package's own `dist/`.
179
+ - **ESM only**, one output file per source file, so consumer bundlers can drop unused exports.
180
+ `sideEffects` is set to `["*.css"]` so the JavaScript is known to be side-effect free.
181
+ - **Declarations and source maps** are emitted alongside the JavaScript.
182
+ - **Relative imports carry explicit `.js` extensions**, so the output resolves correctly under Node
183
+ ESM as well as under bundlers.
184
+ - **`react` and `react-dom` are peer dependencies.** They appear in `peerDependencies`, and in
185
+ `devDependencies` only so the package can be tested. Because nothing bundles, every React import
186
+ survives into the output as a bare specifier and resolves to the consumer's own copy. There is no
187
+ way for a second React to be included.
188
+
189
+ ## Styling
190
+
191
+ Styles are a plain CSS file, shipped separately and imported by the consumer. The JavaScript never
192
+ imports CSS, so no bundler configuration is required and the stylesheet cannot be dragged into a
193
+ server bundle.
194
+
195
+ On the source side the stylesheet is split across the generated token layer and one file per
196
+ component, and composed with plain `@import`. The build inlines those imports, so `dist/styles.css`
197
+ is one flat file with no `@import` left in it — the consumer's bundler is never asked to resolve
198
+ relative CSS imports out of `node_modules`, and the browser never pays for extra render-blocking
199
+ requests. The build fails if an import survives.
200
+
201
+ Rules for anything added to the stylesheet:
202
+
203
+ - prefix every selector with `lutra-`
204
+ - the only global selectors permitted are the token layer's `:root` and `[data-theme]` blocks, and
205
+ they declare custom properties and nothing else — a stylesheet must not restyle a page it was
206
+ merely imported into, and a custom property declaration paints nothing
207
+ - take every value from a design token; no raw colours, sizes, or type
208
+ - size in `rem` with unitless line heights, so the library respects user font size and zoom; border
209
+ and outline widths stay in `px`
210
+ - never convey state through colour alone
211
+ - let text wrap rather than overflow or clip
212
+
213
+ ## Server rendering and static export
214
+
215
+ The package is safe for Next.js server rendering and static export:
216
+
217
+ - no browser global is touched while a module is being evaluated — no `window`, `document`,
218
+ `navigator`, or `matchMedia` at module scope
219
+ - no CSS is imported from JavaScript
220
+ - components render correctly in a Node process with no DOM present
221
+
222
+ Any component that genuinely needs a browser API must reach for it inside an effect or an event
223
+ handler, never during module initialisation or the first render.
224
+
225
+ ## Development
226
+
227
+ From the repository root:
228
+
229
+ ```bash
230
+ pnpm exec nx run @lutra-ui-system/react:build
231
+ pnpm exec nx run @lutra-ui-system/react:test
232
+ pnpm exec nx run @lutra-ui-system/react:typecheck
233
+ pnpm exec nx run @lutra-ui-system/react:lint
234
+ ```
235
+
236
+ Tests use Vitest with jsdom and React Testing Library, and query by role and accessible name. See
237
+ [`docs/engineering-guidelines.md`](../../docs/engineering-guidelines.md) for the authoritative
238
+ conventions, [`docs/accessibility.md`](../../docs/accessibility.md) for the rules every component
239
+ must meet, [`docs/package-distribution.md`](../../docs/package-distribution.md) for packed-artifact
240
+ verification, and [`CONTRIBUTING.md`](../../CONTRIBUTING.md) for review expectations.
@@ -0,0 +1,105 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ /** Visual prominence. Maps to the `variant` axis of the Figma component set. */
3
+ export type ButtonVariant = 'primary' | 'secondary' | 'danger';
4
+ /** Drawn size. Maps to the `size` axis of the Figma component set. */
5
+ export type ButtonSize = 'sm' | 'md' | 'lg';
6
+ /**
7
+ * What the button is made of. `'text'` buttons are named by their visible label;
8
+ * `'icon-only'` buttons show one icon and must be named explicitly.
9
+ */
10
+ export type ButtonContentType = 'text' | 'icon-only';
11
+ /**
12
+ * Native `<button>` attributes, minus `children`, which each content type
13
+ * redefines. Keeping the rest means `name`, `value`, `form`, `formAction`,
14
+ * `onClick`, every `aria-*`, and every `data-*` continue to work exactly as they
15
+ * do on a plain `<button>`.
16
+ */
17
+ type NativeButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'>;
18
+ interface ButtonSharedProps extends NativeButtonProps {
19
+ /** @default 'primary' */
20
+ variant?: ButtonVariant;
21
+ /** @default 'md' */
22
+ size?: ButtonSize;
23
+ /**
24
+ * Marks the action as in progress. The button becomes natively disabled and
25
+ * exposes `aria-busy="true"`, and a decorative spinner replaces the icon slot.
26
+ *
27
+ * @default false
28
+ */
29
+ loading?: boolean;
30
+ /**
31
+ * Stretches the button to the width of its container. Layout only — it is not
32
+ * a variant and changes nothing else about the button.
33
+ *
34
+ * @default false
35
+ */
36
+ fullWidth?: boolean;
37
+ }
38
+ /**
39
+ * A button labelled by its own visible text, optionally with decorative icons on
40
+ * either side.
41
+ */
42
+ export interface ButtonTextProps extends ButtonSharedProps {
43
+ /** @default 'text' */
44
+ contentType?: 'text';
45
+ /** The visible label. It is also the button's accessible name. */
46
+ children: ReactNode;
47
+ /** Decorative icon before the label. Never a replacement for the label. */
48
+ leftIcon?: ReactNode;
49
+ /** Decorative icon after the label. Never a replacement for the label. */
50
+ rightIcon?: ReactNode;
51
+ /**
52
+ * Replaces the visible label while `loading` is true — "Saving…" in place of
53
+ * "Save". Omit it to keep the original label. Either way the button keeps the
54
+ * greater of the two widths, so swapping the text does not shift the layout.
55
+ */
56
+ loadingLabel?: string;
57
+ }
58
+ /**
59
+ * An icon-only button has no visible text, so it cannot name itself. Exactly one
60
+ * of `aria-label` or `aria-labelledby` is required, and TypeScript enforces it:
61
+ * an icon-only button with no accessible name is unusable with a screen reader
62
+ * and is not a state this component allows a consumer to reach by accident.
63
+ */
64
+ type ButtonAccessibleName = {
65
+ 'aria-label': string;
66
+ 'aria-labelledby'?: never;
67
+ } | {
68
+ 'aria-labelledby': string;
69
+ 'aria-label'?: never;
70
+ };
71
+ /** A button whose entire content is one icon. */
72
+ export type ButtonIconOnlyProps = ButtonSharedProps & {
73
+ contentType: 'icon-only';
74
+ /** Exactly one icon. Not text — an icon-only button shows no visible label. */
75
+ children: ReactNode;
76
+ /** Not available: an icon-only button's single icon is its `children`. */
77
+ leftIcon?: never;
78
+ /** Not available: an icon-only button's single icon is its `children`. */
79
+ rightIcon?: never;
80
+ /** Not available: an icon-only button has no visible label to replace. */
81
+ loadingLabel?: never;
82
+ } & ButtonAccessibleName;
83
+ /**
84
+ * The two shapes a Button can take. A union rather than one wide interface,
85
+ * because the icon-only contract genuinely differs: it forbids the icon slots,
86
+ * forbids a loading label, and requires an accessible name.
87
+ */
88
+ export type ButtonProps = ButtonTextProps | ButtonIconOnlyProps;
89
+ /**
90
+ * A button that triggers an immediate action.
91
+ *
92
+ * Renders a native `<button>`, so keyboard activation with Space and Enter, focus
93
+ * behaviour, form participation, and disabled semantics come from the platform
94
+ * rather than from this component. `type` defaults to `"button"` so a button
95
+ * inside a form cannot submit it by accident.
96
+ *
97
+ * Hover, active and focus are browser states expressed in CSS, not props. The
98
+ * only states Button models are the ones a consumer owns: `disabled` and
99
+ * `loading`.
100
+ *
101
+ * Consumers are responsible for loading IBM Plex Sans; see `button.md`.
102
+ */
103
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
104
+ export {};
105
+ //# sourceMappingURL=button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/button/button.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI7D,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D,sEAAsE;AACtE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,WAAW,CAAC;AAErD;;;;;GAKG;AACH,KAAK,iBAAiB,GAAG,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC,CAAC;AAEnF,UAAU,iBAAkB,SAAQ,iBAAiB;IACnD,yBAAyB;IACzB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,oBAAoB;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,KAAK,oBAAoB,GACrB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,KAAK,CAAA;CAAE,GACnD;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAExD,iDAAiD;AACjD,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG;IACpD,WAAW,EAAE,WAAW,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,EAAE,SAAS,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,KAAK,CAAC;IAClB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB,GAAG,oBAAoB,CAAC;AAEzB;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAuEhE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM,2GAwFjB,CAAC"}
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, forwardRef } from 'react';
3
+ import { isDevelopment } from '../../internal/development.js';
4
+ /**
5
+ * Wraps consumer icon content in the stable 16px slot from the Figma
6
+ * documentation, and hides it from assistive technology.
7
+ *
8
+ * The icon is hidden rather than described because Button treats icons as
9
+ * decoration: a text button is named by its label, and an icon-only button by its
10
+ * `aria-label`. Announcing the icon as well would only duplicate that name. The
11
+ * consumer's element is rendered as given — Button never clones it to force ARIA
12
+ * onto it, which would silently overwrite attributes the consumer set on purpose.
13
+ */
14
+ function ButtonIcon({ children }) {
15
+ return (_jsx("span", { className: "lutra-button__icon", "aria-hidden": "true", children: children }));
16
+ }
17
+ /**
18
+ * The loading indicator: a CSS ring, not an image.
19
+ *
20
+ * `aria-hidden` keeps it silent, because `aria-busy` on the button already
21
+ * announces the state — a screen reader that also read the spinner would be
22
+ * describing the same thing twice. Its animation is slowed right down under
23
+ * `prefers-reduced-motion: reduce`; see `button.css`.
24
+ */
25
+ function ButtonSpinner() {
26
+ return _jsx("span", { className: "lutra-button__spinner", "aria-hidden": "true" });
27
+ }
28
+ /**
29
+ * Reports the invalid content combinations that the type union cannot express.
30
+ *
31
+ * TypeScript already makes most invalid states unrepresentable. What is left is
32
+ * everything that depends on the *value* of `children` — how many there are, and
33
+ * whether they are text — plus the accessible name, which a JavaScript consumer
34
+ * can still omit. Those are warned about rather than thrown, because a broken
35
+ * label should not take an application down, and rather than papered over,
36
+ * because guessing a name from an icon produces a label no user asked for.
37
+ */
38
+ function warnAboutInvalidIconOnlyContent(children, accessibleName, accessibleNameReference) {
39
+ if (accessibleName === undefined && accessibleNameReference === undefined) {
40
+ console.warn('Button: an icon-only button has no accessible name. Pass `aria-label` or ' +
41
+ '`aria-labelledby` — an icon alone cannot name a control.');
42
+ }
43
+ if (typeof children === 'string' || typeof children === 'number') {
44
+ console.warn('Button: an icon-only button was given text content. Use the default ' +
45
+ '`contentType="text"` for a button with a visible label.');
46
+ return;
47
+ }
48
+ const iconCount = Children.count(children);
49
+ if (iconCount !== 1) {
50
+ console.warn(`Button: an icon-only button expects exactly one icon, but received ${String(iconCount)}.`);
51
+ }
52
+ }
53
+ /**
54
+ * A button that triggers an immediate action.
55
+ *
56
+ * Renders a native `<button>`, so keyboard activation with Space and Enter, focus
57
+ * behaviour, form participation, and disabled semantics come from the platform
58
+ * rather than from this component. `type` defaults to `"button"` so a button
59
+ * inside a form cannot submit it by accident.
60
+ *
61
+ * Hover, active and focus are browser states expressed in CSS, not props. The
62
+ * only states Button models are the ones a consumer owns: `disabled` and
63
+ * `loading`.
64
+ *
65
+ * Consumers are responsible for loading IBM Plex Sans; see `button.md`.
66
+ */
67
+ export const Button = forwardRef(function Button({ variant = 'primary', size = 'md', contentType = 'text', loading = false, fullWidth = false, type = 'button', disabled = false, className, children, leftIcon, rightIcon, loadingLabel, ...nativeProps }, ref) {
68
+ const isIconOnly = contentType === 'icon-only';
69
+ // Loading is a kind of unavailable, so it disables natively rather than
70
+ // intercepting clicks. A disabled button fires no click event at all, which is
71
+ // stronger than a guard in a handler the consumer could route around, and it
72
+ // also removes the button from the tab order the way the platform intends.
73
+ const isDisabled = disabled || loading;
74
+ if (isDevelopment && isIconOnly) {
75
+ warnAboutInvalidIconOnlyContent(children, nativeProps['aria-label'], nativeProps['aria-labelledby']);
76
+ }
77
+ // The visible label is swapped only when there is something to swap it for.
78
+ // Both strings stay in the DOM so the button keeps the wider of the two widths;
79
+ // the one that is not current is hidden from sight and from assistive
80
+ // technology, so the accessible name is always exactly the visible label.
81
+ const isLabelSwapped = loading && loadingLabel !== undefined;
82
+ return (_jsx("button", { ...nativeProps, ref: ref, type: type, className: className === undefined ? 'lutra-button' : `lutra-button ${className}`, disabled: isDisabled, "aria-busy": loading || undefined, "data-variant": variant, "data-size": size, "data-content-type": contentType, "data-full-width": fullWidth ? 'true' : undefined, children: isIconOnly ? (
83
+ // The spinner takes the icon's place rather than joining it, so the
84
+ // button neither grows nor shows two things at once. Its accessible name
85
+ // comes from `aria-label`, which loading does not touch.
86
+ loading ? (_jsx(ButtonSpinner, {})) : (_jsx(ButtonIcon, { children: children }))) : (_jsxs(_Fragment, { children: [loading ? _jsx(ButtonSpinner, {}) : leftIcon ? _jsx(ButtonIcon, { children: leftIcon }) : null, _jsx("span", { className: isLabelSwapped
87
+ ? 'lutra-button__label lutra-button__label--swapped'
88
+ : 'lutra-button__label', children: isLabelSwapped ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "lutra-button__label-measure", "aria-hidden": "true", children: children }), _jsx("span", { children: loadingLabel })] })) : (children) }), rightIcon ? _jsx(ButtonIcon, { children: rightIcon }) : null] })) }));
89
+ });
90
+ Button.displayName = 'Button';
91
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../src/components/button/button.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { Button } from './button.js';
2
+ export type { ButtonProps, ButtonVariant, ButtonSize, ButtonContentType } from './button.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/button/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { Button } from './button.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/button/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,100 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ /**
3
+ * How the card separates itself from what is behind it. Maps to the `elevation` axis of the Figma
4
+ * component set.
5
+ *
6
+ * `'flat'` is the default and is right on a plain background. `'raised'` adds the `Elevation/Small`
7
+ * shadow and is for a card sitting over busy or scrolling content.
8
+ *
9
+ * Both keep the border, and that matters: in dark mode the border does most of the separating, which
10
+ * is why a flat card still reads as a bounded surface there.
11
+ */
12
+ export type CardElevation = 'flat' | 'raised';
13
+ /**
14
+ * The heading level the card's title renders at.
15
+ *
16
+ * There is no default, and that is deliberate. The annotation is explicit that the title is a real
17
+ * heading at the level its surrounding section requires — the next level down from whatever precedes
18
+ * it — and **never fixed at `h3` because the visual size happens to be 18px**. A default would put
19
+ * every card in an application at the same level regardless of where it sits, which is exactly the
20
+ * broken outline the rule exists to prevent. A heading outline built from visual sizes is not an
21
+ * outline.
22
+ *
23
+ * `h1` is absent because a card is never the page title.
24
+ */
25
+ export type HeadingLevel = 2 | 3 | 4 | 5 | 6;
26
+ /**
27
+ * Native `<div>` attributes, minus `title`.
28
+ *
29
+ * `title` here is the card's own visible heading, which is a `ReactNode`, and it takes the name the
30
+ * Figma property uses and that Dialog already uses for the same thing. The native `title` attribute
31
+ * is not forwarded as a result — no loss, since a browser tooltip on a container is unreachable by
32
+ * keyboard and invisible on touch.
33
+ */
34
+ type NativeDivProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'>;
35
+ export interface CardProps extends NativeDivProps {
36
+ /** The visible heading. */
37
+ title: ReactNode;
38
+ /** The level the heading renders at. Required — see {@link HeadingLevel}. */
39
+ headingLevel: HeadingLevel;
40
+ /**
41
+ * Metadata directly under the heading, in the same block, reading as one unit with it.
42
+ *
43
+ * It is metadata, not a second heading. **It must never be the only place a piece of information
44
+ * appears.**
45
+ */
46
+ supportingText?: ReactNode;
47
+ /** @default 'flat' */
48
+ elevation?: CardElevation;
49
+ /** The action row. A `CardFooter`, holding at most two buttons. */
50
+ footer?: ReactNode;
51
+ /** The body. */
52
+ children?: ReactNode;
53
+ }
54
+ /**
55
+ * A bounded surface grouping related content and its actions.
56
+ *
57
+ * **A card is a container, not a control.** A border and a shadow do not make something interactive:
58
+ * it has no click behaviour, adds nothing to the tab order, and is not a landmark. The buttons and
59
+ * links inside it are the tab stops. If the whole surface should be one destination, that is
60
+ * `LinkCard` — a different component, because it is a different thing.
61
+ *
62
+ * It is also not a landmark by default. Pass `role="region"` only when the card genuinely needs to be
63
+ * reachable as one; when you do, the card names it from its own heading automatically, because an
64
+ * unnamed region is worse than no region. Your own `aria-labelledby` wins if you pass one.
65
+ *
66
+ * That automatic naming is for `role="region"` and nothing else. Any other role passes straight
67
+ * through — naming it is yours to do, and a name invented here would be one you never asked for.
68
+ *
69
+ * Nothing here introduces type, spacing or colour of its own — everything is composed from existing
70
+ * Lutra UI foundations, so a card never becomes a second place where those decisions are made.
71
+ */
72
+ export declare function Card({ title, headingLevel, supportingText, elevation, footer, children, className, role, ...nativeProps }: CardProps): import("react").JSX.Element;
73
+ /** Where the actions sit. Maps to the `align` axis of the Figma component set. */
74
+ export type CardFooterAlign = 'start' | 'end';
75
+ export interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
76
+ /**
77
+ * `'end'` for a bounded card in a grid, matching the dialog convention of primary last.
78
+ * `'start'` for a full-width or form-like card, where primary first reads more naturally.
79
+ *
80
+ * @default 'end'
81
+ */
82
+ align?: CardFooterAlign;
83
+ /** At most two buttons. A third belongs in a menu. */
84
+ children: ReactNode;
85
+ }
86
+ /**
87
+ * The action row at the bottom of a card.
88
+ *
89
+ * **`align` moves the row, never the buttons.** It sets `justify-content` and nothing else — there is
90
+ * no `row-reverse` here, because DOM order has to match visual order so focus order matches reading
91
+ * order. Which button comes first is therefore something you write, not something this component
92
+ * rearranges: put the primary action last for `align="end"`, first for `align="start"`.
93
+ *
94
+ * The buttons keep Button's own contract and are the only focusable things in the card. A
95
+ * destructive action still needs its own confirmation step — danger styling communicates a
96
+ * consequence, it does not prevent one.
97
+ */
98
+ export declare function CardFooter({ align, children, className, ...nativeProps }: CardFooterProps): import("react").JSX.Element;
99
+ export {};
100
+ //# sourceMappingURL=card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../src/components/card/card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE7C;;;;;;;GAOG;AACH,KAAK,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC;AAEpE,MAAM,WAAW,SAAU,SAAQ,cAAc;IAC/C,2BAA2B;IAC3B,KAAK,EAAE,SAAS,CAAC;IACjB,6EAA6E;IAC7E,YAAY,EAAE,YAAY,CAAC;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,sBAAsB;IACtB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,mEAAmE;IACnE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,IAAI,CAAC,EACnB,KAAK,EACL,YAAY,EACZ,cAAc,EACd,SAAkB,EAClB,MAAM,EACN,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,GAAG,WAAW,EACf,EAAE,SAAS,+BAqCX;AAED,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,eAAgB,SAAQ,cAAc,CAAC,cAAc,CAAC;IACrE;;;;;OAKG;IACH,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,sDAAsD;IACtD,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAa,EACb,QAAQ,EACR,SAAS,EACT,GAAG,WAAW,EACf,EAAE,eAAe,+BAUjB"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ /**
4
+ * A bounded surface grouping related content and its actions.
5
+ *
6
+ * **A card is a container, not a control.** A border and a shadow do not make something interactive:
7
+ * it has no click behaviour, adds nothing to the tab order, and is not a landmark. The buttons and
8
+ * links inside it are the tab stops. If the whole surface should be one destination, that is
9
+ * `LinkCard` — a different component, because it is a different thing.
10
+ *
11
+ * It is also not a landmark by default. Pass `role="region"` only when the card genuinely needs to be
12
+ * reachable as one; when you do, the card names it from its own heading automatically, because an
13
+ * unnamed region is worse than no region. Your own `aria-labelledby` wins if you pass one.
14
+ *
15
+ * That automatic naming is for `role="region"` and nothing else. Any other role passes straight
16
+ * through — naming it is yours to do, and a name invented here would be one you never asked for.
17
+ *
18
+ * Nothing here introduces type, spacing or colour of its own — everything is composed from existing
19
+ * Lutra UI foundations, so a card never becomes a second place where those decisions are made.
20
+ */
21
+ export function Card({ title, headingLevel, supportingText, elevation = 'flat', footer, children, className, role, ...nativeProps }) {
22
+ const titleId = `lutra-card-title-${useId()}`;
23
+ const Heading = `h${headingLevel}`;
24
+ const hasSupportingText = supportingText !== undefined && supportingText !== null && supportingText !== false;
25
+ const hasBody = children !== undefined && children !== null && children !== false;
26
+ const hasFooter = footer !== undefined && footer !== null && footer !== false;
27
+ // A region with no name is a worse landmark than none at all, and the heading is the name the
28
+ // annotation asks for — so `role="region"`, and only that, gets the relationship filled in.
29
+ //
30
+ // Deliberately not every role. Another role a consumer reaches for is theirs to name, and quietly
31
+ // pointing it at the card's own heading would either duplicate a name they meant to supply
32
+ // elsewhere or invent one they never asked for. A consumer's own `aria-labelledby` always wins.
33
+ const labelledBy = role === 'region' && nativeProps['aria-labelledby'] === undefined ? titleId : undefined;
34
+ return (_jsxs("div", { ...nativeProps, ...(role === undefined ? {} : { role }), ...(labelledBy === undefined ? {} : { 'aria-labelledby': labelledBy }), className: className === undefined ? 'lutra-card' : `lutra-card ${className}`, "data-elevation": elevation, children: [_jsxs("div", { className: "lutra-card__header", children: [_jsx(Heading, { className: "lutra-card__title", id: titleId, children: title }), hasSupportingText && _jsx("p", { className: "lutra-card__supporting-text", children: supportingText })] }), hasBody && _jsx("div", { className: "lutra-card__body", children: children }), hasFooter && footer] }));
35
+ }
36
+ /**
37
+ * The action row at the bottom of a card.
38
+ *
39
+ * **`align` moves the row, never the buttons.** It sets `justify-content` and nothing else — there is
40
+ * no `row-reverse` here, because DOM order has to match visual order so focus order matches reading
41
+ * order. Which button comes first is therefore something you write, not something this component
42
+ * rearranges: put the primary action last for `align="end"`, first for `align="start"`.
43
+ *
44
+ * The buttons keep Button's own contract and are the only focusable things in the card. A
45
+ * destructive action still needs its own confirmation step — danger styling communicates a
46
+ * consequence, it does not prevent one.
47
+ */
48
+ export function CardFooter({ align = 'end', children, className, ...nativeProps }) {
49
+ return (_jsx("div", { ...nativeProps, className: className === undefined ? 'lutra-card-footer' : `lutra-card-footer ${className}`, "data-align": align, children: children }));
50
+ }
51
+ //# sourceMappingURL=card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.js","sourceRoot":"","sources":["../../../src/components/card/card.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AA2D9B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,IAAI,CAAC,EACnB,KAAK,EACL,YAAY,EACZ,cAAc,EACd,SAAS,GAAG,MAAM,EAClB,MAAM,EACN,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,GAAG,WAAW,EACJ;IACV,MAAM,OAAO,GAAG,oBAAoB,KAAK,EAAE,EAAE,CAAC;IAC9C,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAC;IAE5C,MAAM,iBAAiB,GACrB,cAAc,KAAK,SAAS,IAAI,cAAc,KAAK,IAAI,IAAI,cAAc,KAAK,KAAK,CAAC;IACtF,MAAM,OAAO,GAAG,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,KAAK,CAAC;IAClF,MAAM,SAAS,GAAG,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,KAAK,CAAC;IAE9E,8FAA8F;IAC9F,4FAA4F;IAC5F,EAAE;IACF,kGAAkG;IAClG,2FAA2F;IAC3F,gGAAgG;IAChG,MAAM,UAAU,GACd,IAAI,KAAK,QAAQ,IAAI,WAAW,CAAC,iBAAiB,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1F,OAAO,CACL,kBACM,WAAW,KACX,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KACpC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,UAAU,EAAE,CAAC,EACvE,SAAS,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,SAAS,EAAE,oBAC7D,SAAS,aAEzB,eAAK,SAAS,EAAC,oBAAoB,aACjC,KAAC,OAAO,IAAC,SAAS,EAAC,mBAAmB,EAAC,EAAE,EAAE,OAAO,YAC/C,KAAK,GACE,EACT,iBAAiB,IAAI,YAAG,SAAS,EAAC,6BAA6B,YAAE,cAAc,GAAK,IACjF,EAEL,OAAO,IAAI,cAAK,SAAS,EAAC,kBAAkB,YAAE,QAAQ,GAAO,EAC7D,SAAS,IAAI,MAAM,IAChB,CACP,CAAC;AACJ,CAAC;AAiBD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,EACzB,KAAK,GAAG,KAAK,EACb,QAAQ,EACR,SAAS,EACT,GAAG,WAAW,EACE;IAChB,OAAO,CACL,iBACM,WAAW,EACf,SAAS,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB,SAAS,EAAE,gBAC/E,KAAK,YAEhB,QAAQ,GACL,CACP,CAAC;AACJ,CAAC"}