@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.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/components/button/button.d.ts +105 -0
- package/dist/components/button/button.d.ts.map +1 -0
- package/dist/components/button/button.js +91 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/button/index.d.ts +3 -0
- package/dist/components/button/index.d.ts.map +1 -0
- package/dist/components/button/index.js +2 -0
- package/dist/components/button/index.js.map +1 -0
- package/dist/components/card/card.d.ts +100 -0
- package/dist/components/card/card.d.ts.map +1 -0
- package/dist/components/card/card.js +51 -0
- package/dist/components/card/card.js.map +1 -0
- package/dist/components/card/index.d.ts +3 -0
- package/dist/components/card/index.d.ts.map +1 -0
- package/dist/components/card/index.js +2 -0
- package/dist/components/card/index.js.map +1 -0
- package/dist/components/checkbox/checkbox.d.ts +67 -0
- package/dist/components/checkbox/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox/checkbox.js +63 -0
- package/dist/components/checkbox/checkbox.js.map +1 -0
- package/dist/components/checkbox/index.d.ts +3 -0
- package/dist/components/checkbox/index.d.ts.map +1 -0
- package/dist/components/checkbox/index.js +2 -0
- package/dist/components/checkbox/index.js.map +1 -0
- package/dist/components/dialog/dialog.d.ts +88 -0
- package/dist/components/dialog/dialog.d.ts.map +1 -0
- package/dist/components/dialog/dialog.js +156 -0
- package/dist/components/dialog/dialog.js.map +1 -0
- package/dist/components/dialog/index.d.ts +3 -0
- package/dist/components/dialog/index.d.ts.map +1 -0
- package/dist/components/dialog/index.js +2 -0
- package/dist/components/dialog/index.js.map +1 -0
- package/dist/components/dropdown/dropdown.d.ts +89 -0
- package/dist/components/dropdown/dropdown.d.ts.map +1 -0
- package/dist/components/dropdown/dropdown.js +173 -0
- package/dist/components/dropdown/dropdown.js.map +1 -0
- package/dist/components/dropdown/index.d.ts +3 -0
- package/dist/components/dropdown/index.d.ts.map +1 -0
- package/dist/components/dropdown/index.js +2 -0
- package/dist/components/dropdown/index.js.map +1 -0
- package/dist/components/error-summary/error-summary.d.ts +81 -0
- package/dist/components/error-summary/error-summary.d.ts.map +1 -0
- package/dist/components/error-summary/error-summary.js +85 -0
- package/dist/components/error-summary/error-summary.js.map +1 -0
- package/dist/components/error-summary/index.d.ts +3 -0
- package/dist/components/error-summary/index.d.ts.map +1 -0
- package/dist/components/error-summary/index.js +2 -0
- package/dist/components/error-summary/index.js.map +1 -0
- package/dist/components/field/field.d.ts +55 -0
- package/dist/components/field/field.d.ts.map +1 -0
- package/dist/components/field/field.js +26 -0
- package/dist/components/field/field.js.map +1 -0
- package/dist/components/field/index.d.ts +3 -0
- package/dist/components/field/index.d.ts.map +1 -0
- package/dist/components/field/index.js +2 -0
- package/dist/components/field/index.js.map +1 -0
- package/dist/components/field-group/field-group.d.ts +67 -0
- package/dist/components/field-group/field-group.d.ts.map +1 -0
- package/dist/components/field-group/field-group.js +38 -0
- package/dist/components/field-group/field-group.js.map +1 -0
- package/dist/components/field-group/index.d.ts +3 -0
- package/dist/components/field-group/index.d.ts.map +1 -0
- package/dist/components/field-group/index.js +2 -0
- package/dist/components/field-group/index.js.map +1 -0
- package/dist/components/input/index.d.ts +3 -0
- package/dist/components/input/index.d.ts.map +1 -0
- package/dist/components/input/index.js +2 -0
- package/dist/components/input/index.js.map +1 -0
- package/dist/components/input/input.d.ts +96 -0
- package/dist/components/input/input.d.ts.map +1 -0
- package/dist/components/input/input.js +26 -0
- package/dist/components/input/input.js.map +1 -0
- package/dist/components/link/index.d.ts +3 -0
- package/dist/components/link/index.d.ts.map +1 -0
- package/dist/components/link/index.js +2 -0
- package/dist/components/link/index.js.map +1 -0
- package/dist/components/link/link.d.ts +44 -0
- package/dist/components/link/link.d.ts.map +1 -0
- package/dist/components/link/link.js +26 -0
- package/dist/components/link/link.js.map +1 -0
- package/dist/components/link-card/index.d.ts +3 -0
- package/dist/components/link-card/index.d.ts.map +1 -0
- package/dist/components/link-card/index.js +2 -0
- package/dist/components/link-card/index.js.map +1 -0
- package/dist/components/link-card/link-card.d.ts +62 -0
- package/dist/components/link-card/link-card.d.ts.map +1 -0
- package/dist/components/link-card/link-card.js +43 -0
- package/dist/components/link-card/link-card.js.map +1 -0
- package/dist/components/mobile-navigation/index.d.ts +3 -0
- package/dist/components/mobile-navigation/index.d.ts.map +1 -0
- package/dist/components/mobile-navigation/index.js +2 -0
- package/dist/components/mobile-navigation/index.js.map +1 -0
- package/dist/components/mobile-navigation/mobile-navigation.d.ts +88 -0
- package/dist/components/mobile-navigation/mobile-navigation.d.ts.map +1 -0
- package/dist/components/mobile-navigation/mobile-navigation.js +153 -0
- package/dist/components/mobile-navigation/mobile-navigation.js.map +1 -0
- package/dist/components/navigation-item/index.d.ts +3 -0
- package/dist/components/navigation-item/index.d.ts.map +1 -0
- package/dist/components/navigation-item/index.js +2 -0
- package/dist/components/navigation-item/index.js.map +1 -0
- package/dist/components/navigation-item/navigation-item.d.ts +38 -0
- package/dist/components/navigation-item/navigation-item.d.ts.map +1 -0
- package/dist/components/navigation-item/navigation-item.js +22 -0
- package/dist/components/navigation-item/navigation-item.js.map +1 -0
- package/dist/components/radio/index.d.ts +3 -0
- package/dist/components/radio/index.d.ts.map +1 -0
- package/dist/components/radio/index.js +2 -0
- package/dist/components/radio/index.js.map +1 -0
- package/dist/components/radio/radio.d.ts +58 -0
- package/dist/components/radio/radio.d.ts.map +1 -0
- package/dist/components/radio/radio.js +37 -0
- package/dist/components/radio/radio.js.map +1 -0
- package/dist/components/select/index.d.ts +3 -0
- package/dist/components/select/index.d.ts.map +1 -0
- package/dist/components/select/index.js +2 -0
- package/dist/components/select/index.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.d.ts.map +1 -0
- package/dist/components/select/select.js +25 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/select-field/index.d.ts +3 -0
- package/dist/components/select-field/index.d.ts.map +1 -0
- package/dist/components/select-field/index.js +2 -0
- package/dist/components/select-field/index.js.map +1 -0
- package/dist/components/select-field/select-field.d.ts +43 -0
- package/dist/components/select-field/select-field.d.ts.map +1 -0
- package/dist/components/select-field/select-field.js +24 -0
- package/dist/components/select-field/select-field.js.map +1 -0
- package/dist/components/table/index.d.ts +3 -0
- package/dist/components/table/index.d.ts.map +1 -0
- package/dist/components/table/index.js +2 -0
- package/dist/components/table/index.js.map +1 -0
- package/dist/components/table/table.d.ts +251 -0
- package/dist/components/table/table.d.ts.map +1 -0
- package/dist/components/table/table.js +209 -0
- package/dist/components/table/table.js.map +1 -0
- package/dist/components/toggle/index.d.ts +3 -0
- package/dist/components/toggle/index.d.ts.map +1 -0
- package/dist/components/toggle/index.js +2 -0
- package/dist/components/toggle/index.js.map +1 -0
- package/dist/components/toggle/toggle.d.ts +66 -0
- package/dist/components/toggle/toggle.d.ts.map +1 -0
- package/dist/components/toggle/toggle.js +49 -0
- package/dist/components/toggle/toggle.js.map +1 -0
- package/dist/components/tooltip/index.d.ts +3 -0
- package/dist/components/tooltip/index.d.ts.map +1 -0
- package/dist/components/tooltip/index.js +2 -0
- package/dist/components/tooltip/index.js.map +1 -0
- package/dist/components/tooltip/tooltip.d.ts +74 -0
- package/dist/components/tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip/tooltip.js +180 -0
- package/dist/components/tooltip/tooltip.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/development.d.ts +2 -0
- package/dist/internal/development.d.ts.map +1 -0
- package/dist/internal/development.js +2 -0
- package/dist/internal/development.js.map +1 -0
- package/dist/internal/field-support.d.ts +68 -0
- package/dist/internal/field-support.d.ts.map +1 -0
- package/dist/internal/field-support.js +42 -0
- package/dist/internal/field-support.js.map +1 -0
- package/dist/internal/isomorphic-layout-effect.d.ts +26 -0
- package/dist/internal/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/internal/isomorphic-layout-effect.js +26 -0
- package/dist/internal/isomorphic-layout-effect.js.map +1 -0
- package/dist/styles.css +3474 -0
- package/dist/tokens.json +995 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|