@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/dist/styles.css
ADDED
|
@@ -0,0 +1,3474 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @lutra-ui-system/react stylesheet.
|
|
3
|
+
*
|
|
4
|
+
* Plain, standards-based CSS shipped as a single file. Consumers import it once:
|
|
5
|
+
*
|
|
6
|
+
* import '@lutra-ui-system/react/styles.css';
|
|
7
|
+
*
|
|
8
|
+
* This file is the source-side entry point and is composed with plain `@import`.
|
|
9
|
+
* The build inlines those imports, so what ships in `dist/styles.css` is one flat
|
|
10
|
+
* stylesheet with no `@import` left in it. That matters: a distributed stylesheet
|
|
11
|
+
* that still contained `@import` would either need the consumer's bundler to
|
|
12
|
+
* resolve it or would cost a second network request, and neither is something a
|
|
13
|
+
* library should require. See `scripts/build-styles.mjs`.
|
|
14
|
+
*
|
|
15
|
+
* The file has two layers, and the order matters:
|
|
16
|
+
*
|
|
17
|
+
* 1. The generated token layer, which declares every Lutra design token as a CSS
|
|
18
|
+
* custom property on `:root`, plus the dark values on `[data-theme='dark']`.
|
|
19
|
+
* It is produced from `tokens/source.json` and is not hand-edited.
|
|
20
|
+
* 2. One file per component, which reads those tokens and never a raw value.
|
|
21
|
+
*
|
|
22
|
+
* Rules for anything added here:
|
|
23
|
+
* - Every selector is prefixed with `lutra-`, so the library cannot collide with
|
|
24
|
+
* a consuming application's styles.
|
|
25
|
+
* - The only global selectors permitted are the token layer's `:root` and
|
|
26
|
+
* `[data-theme]` blocks, and they declare custom properties and nothing else.
|
|
27
|
+
* A declaration that paints — a colour, a font, a margin on `body` — must never
|
|
28
|
+
* appear on a global selector: a library stylesheet does not get to restyle a
|
|
29
|
+
* page it was merely imported into. See `docs/decisions/0002-design-tokens.md`.
|
|
30
|
+
* - No component rule may reach for `:root`, an element selector, or `*`.
|
|
31
|
+
* - Sizes use `rem` and unitless line heights, so the library respects the user's
|
|
32
|
+
* browser font size and keeps working under zoom. Border and outline widths are
|
|
33
|
+
* the exception and stay in `px`; see the unit policy in `docs/design-tokens.md`.
|
|
34
|
+
* - No rule may convey state through colour alone.
|
|
35
|
+
* - Text must be allowed to wrap rather than overflow or clip.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* GENERATED FILE — DO NOT EDIT.
|
|
40
|
+
*
|
|
41
|
+
* Produced by `scripts/generate-tokens.mjs` from `src/tokens/source.json`.
|
|
42
|
+
* Change a value in `source.json` and regenerate; an edit here is overwritten by the next build
|
|
43
|
+
* and is caught by the drift check in `src/tokens/tokens.test.ts`.
|
|
44
|
+
*
|
|
45
|
+
* See `docs/design-tokens.md` for the naming rule, the unit policy, and the theme contract.
|
|
46
|
+
*/
|
|
47
|
+
:root {
|
|
48
|
+
/* Lutra Theme */
|
|
49
|
+
--lutra-color-background-canvas: #f5f7fa;
|
|
50
|
+
--lutra-color-background-disabled: #e5e7eb;
|
|
51
|
+
--lutra-color-background-elevated: #ffffff;
|
|
52
|
+
--lutra-color-background-subtle: #f5f7fa;
|
|
53
|
+
--lutra-color-background-subtle-active: #dfe5ed;
|
|
54
|
+
--lutra-color-background-subtle-hover: #eaeef4;
|
|
55
|
+
--lutra-color-background-surface: #ffffff;
|
|
56
|
+
--lutra-color-background-transparent: #ffffff00;
|
|
57
|
+
--lutra-color-border-default: #d0d7de;
|
|
58
|
+
--lutra-color-border-disabled: #d1d5db;
|
|
59
|
+
--lutra-color-border-strong: #7f8896;
|
|
60
|
+
--lutra-color-border-subtle: #e8ecf2;
|
|
61
|
+
--lutra-color-border-transparent: #ffffff00;
|
|
62
|
+
--lutra-color-brand-primary: #3265d3;
|
|
63
|
+
--lutra-color-brand-primary-active: #1f4087;
|
|
64
|
+
--lutra-color-brand-primary-hover: #2852ac;
|
|
65
|
+
--lutra-color-feedback-danger: #d14343;
|
|
66
|
+
--lutra-color-feedback-danger-active: #942c2c;
|
|
67
|
+
--lutra-color-feedback-danger-hover: #b33636;
|
|
68
|
+
--lutra-color-feedback-danger-text: #c73f3f;
|
|
69
|
+
--lutra-color-focus-ring: #3f7cff;
|
|
70
|
+
--lutra-color-icon-default: var(--lutra-color-text-primary);
|
|
71
|
+
--lutra-color-link-text: var(--lutra-color-brand-primary);
|
|
72
|
+
--lutra-color-text-disabled: #9ca3af;
|
|
73
|
+
--lutra-color-text-inverse: #ffffff;
|
|
74
|
+
--lutra-color-text-muted: #6b7280;
|
|
75
|
+
--lutra-color-text-on-brand: #ffffff;
|
|
76
|
+
--lutra-color-text-on-danger: #ffffff;
|
|
77
|
+
--lutra-color-text-primary: #111827;
|
|
78
|
+
--lutra-color-text-secondary: #6b7280;
|
|
79
|
+
--lutra-component-button-danger-border: var(--lutra-color-feedback-danger);
|
|
80
|
+
--lutra-component-button-danger-surface: var(--lutra-color-feedback-danger);
|
|
81
|
+
--lutra-component-button-danger-surface-active: var(--lutra-color-feedback-danger-active);
|
|
82
|
+
--lutra-component-button-danger-surface-hover: var(--lutra-color-feedback-danger-hover);
|
|
83
|
+
--lutra-component-button-danger-text: var(--lutra-color-text-on-danger);
|
|
84
|
+
--lutra-component-button-disabled-border: var(--lutra-color-border-disabled);
|
|
85
|
+
--lutra-component-button-disabled-surface: var(--lutra-color-background-disabled);
|
|
86
|
+
--lutra-component-button-disabled-text: var(--lutra-color-text-disabled);
|
|
87
|
+
--lutra-component-button-focus: var(--lutra-color-focus-ring);
|
|
88
|
+
--lutra-component-button-primary-border: var(--lutra-color-brand-primary);
|
|
89
|
+
--lutra-component-button-primary-surface: var(--lutra-color-brand-primary);
|
|
90
|
+
--lutra-component-button-primary-surface-active: var(--lutra-color-brand-primary-active);
|
|
91
|
+
--lutra-component-button-primary-surface-hover: var(--lutra-color-brand-primary-hover);
|
|
92
|
+
--lutra-component-button-primary-text: var(--lutra-color-text-on-brand);
|
|
93
|
+
--lutra-component-button-secondary-border: var(--lutra-color-border-strong);
|
|
94
|
+
--lutra-component-button-secondary-surface: var(--lutra-color-background-subtle);
|
|
95
|
+
--lutra-component-button-secondary-surface-active: var(--lutra-color-background-subtle-active);
|
|
96
|
+
--lutra-component-button-secondary-surface-hover: var(--lutra-color-background-subtle-hover);
|
|
97
|
+
--lutra-component-button-secondary-text: var(--lutra-color-text-primary);
|
|
98
|
+
--lutra-component-checkbox-border-default: var(--lutra-component-input-border-default);
|
|
99
|
+
--lutra-component-checkbox-border-disabled: var(--lutra-color-border-disabled);
|
|
100
|
+
--lutra-component-checkbox-border-hover: var(--lutra-component-input-border-hover);
|
|
101
|
+
--lutra-component-checkbox-border-invalid: var(--lutra-component-input-border-invalid);
|
|
102
|
+
--lutra-component-checkbox-description: var(--lutra-color-text-secondary);
|
|
103
|
+
--lutra-component-checkbox-error-text: var(--lutra-component-field-error-text);
|
|
104
|
+
--lutra-component-checkbox-fill-disabled: var(--lutra-color-background-disabled);
|
|
105
|
+
--lutra-component-checkbox-fill-invalid: var(--lutra-color-feedback-danger);
|
|
106
|
+
--lutra-component-checkbox-fill-selected: var(--lutra-color-brand-primary);
|
|
107
|
+
--lutra-component-checkbox-fill-selected-hover: var(--lutra-color-brand-primary-hover);
|
|
108
|
+
--lutra-component-checkbox-label: var(--lutra-color-text-primary);
|
|
109
|
+
--lutra-component-checkbox-mark: var(--lutra-color-text-on-brand);
|
|
110
|
+
--lutra-component-checkbox-mark-on-danger: var(--lutra-color-text-on-danger);
|
|
111
|
+
--lutra-component-checkbox-surface: var(--lutra-color-background-surface);
|
|
112
|
+
--lutra-component-checkbox-text-disabled: var(--lutra-color-text-disabled);
|
|
113
|
+
--lutra-component-content-accent: var(--lutra-color-brand-primary);
|
|
114
|
+
--lutra-component-content-body: var(--lutra-color-text-secondary);
|
|
115
|
+
--lutra-component-content-border: var(--lutra-color-border-default);
|
|
116
|
+
--lutra-component-content-icon: var(--lutra-color-icon-default);
|
|
117
|
+
--lutra-component-content-surface: var(--lutra-color-background-surface);
|
|
118
|
+
--lutra-component-content-surface-subtle: var(--lutra-color-background-subtle);
|
|
119
|
+
--lutra-component-content-title: var(--lutra-color-text-primary);
|
|
120
|
+
--lutra-component-field-error-text: var(--lutra-color-feedback-danger-text);
|
|
121
|
+
--lutra-component-input-border-default: var(--lutra-color-border-strong);
|
|
122
|
+
--lutra-component-input-border-focus: var(--lutra-color-focus-ring);
|
|
123
|
+
--lutra-component-input-border-hover: var(--lutra-color-brand-primary-hover);
|
|
124
|
+
--lutra-component-input-border-invalid: var(--lutra-color-feedback-danger);
|
|
125
|
+
--lutra-component-input-placeholder: var(--lutra-color-text-secondary);
|
|
126
|
+
--lutra-component-input-surface: var(--lutra-color-background-surface);
|
|
127
|
+
--lutra-component-input-text: var(--lutra-color-text-primary);
|
|
128
|
+
--lutra-component-link-text-default: var(--lutra-color-link-text);
|
|
129
|
+
--lutra-component-link-text-focus: var(--lutra-color-link-text);
|
|
130
|
+
--lutra-component-link-text-hover: var(--lutra-color-link-text);
|
|
131
|
+
--lutra-component-link-text-visited: var(--lutra-color-text-secondary);
|
|
132
|
+
--lutra-component-menu-border: var(--lutra-component-input-border-default);
|
|
133
|
+
--lutra-component-menu-divider: var(--lutra-component-input-border-default);
|
|
134
|
+
--lutra-component-menu-item-danger: var(--lutra-component-field-error-text);
|
|
135
|
+
--lutra-component-menu-item-hover: var(--lutra-color-background-subtle);
|
|
136
|
+
--lutra-component-menu-item-text: var(--lutra-color-text-primary);
|
|
137
|
+
--lutra-component-menu-surface: var(--lutra-color-background-elevated);
|
|
138
|
+
--lutra-component-modal-body: var(--lutra-color-text-secondary);
|
|
139
|
+
--lutra-component-modal-border: var(--lutra-component-input-border-default);
|
|
140
|
+
--lutra-component-modal-divider: var(--lutra-component-input-border-default);
|
|
141
|
+
--lutra-component-modal-scrim: #0f172aa3;
|
|
142
|
+
--lutra-component-modal-surface: var(--lutra-color-background-elevated);
|
|
143
|
+
--lutra-component-modal-title: var(--lutra-color-text-primary);
|
|
144
|
+
--lutra-component-navigation-border: var(--lutra-color-border-default);
|
|
145
|
+
--lutra-component-navigation-focus: var(--lutra-color-focus-ring);
|
|
146
|
+
--lutra-component-navigation-item-indicator: var(--lutra-color-brand-primary);
|
|
147
|
+
--lutra-component-navigation-item-text: var(--lutra-color-text-primary);
|
|
148
|
+
--lutra-component-navigation-surface: var(--lutra-color-background-surface);
|
|
149
|
+
--lutra-component-publishing-accent: var(--lutra-color-brand-primary);
|
|
150
|
+
--lutra-component-publishing-body: var(--lutra-color-text-secondary);
|
|
151
|
+
--lutra-component-publishing-border: var(--lutra-color-border-default);
|
|
152
|
+
--lutra-component-publishing-code-surface: var(--lutra-color-background-elevated);
|
|
153
|
+
--lutra-component-publishing-metadata: var(--lutra-color-text-muted);
|
|
154
|
+
--lutra-component-publishing-surface: var(--lutra-color-background-surface);
|
|
155
|
+
--lutra-component-publishing-surface-subtle: var(--lutra-color-background-subtle);
|
|
156
|
+
--lutra-component-publishing-title: var(--lutra-color-text-primary);
|
|
157
|
+
--lutra-component-radio-border-default: var(--lutra-component-checkbox-border-default);
|
|
158
|
+
--lutra-component-radio-border-disabled: var(--lutra-component-checkbox-border-disabled);
|
|
159
|
+
--lutra-component-radio-border-hover: var(--lutra-component-checkbox-border-hover);
|
|
160
|
+
--lutra-component-radio-border-invalid: var(--lutra-component-checkbox-border-invalid);
|
|
161
|
+
--lutra-component-radio-description: var(--lutra-component-checkbox-description);
|
|
162
|
+
--lutra-component-radio-error-text: var(--lutra-component-checkbox-error-text);
|
|
163
|
+
--lutra-component-radio-fill-disabled: var(--lutra-component-checkbox-fill-disabled);
|
|
164
|
+
--lutra-component-radio-fill-invalid: var(--lutra-component-checkbox-fill-invalid);
|
|
165
|
+
--lutra-component-radio-fill-selected: var(--lutra-component-checkbox-fill-selected);
|
|
166
|
+
--lutra-component-radio-fill-selected-hover: var(--lutra-component-checkbox-fill-selected-hover);
|
|
167
|
+
--lutra-component-radio-label: var(--lutra-component-checkbox-label);
|
|
168
|
+
--lutra-component-radio-mark: var(--lutra-component-checkbox-mark);
|
|
169
|
+
--lutra-component-radio-mark-on-danger: var(--lutra-component-checkbox-mark-on-danger);
|
|
170
|
+
--lutra-component-radio-surface: var(--lutra-component-checkbox-surface);
|
|
171
|
+
--lutra-component-radio-text-disabled: var(--lutra-component-checkbox-text-disabled);
|
|
172
|
+
--lutra-component-toggle-border-default: var(--lutra-component-input-border-default);
|
|
173
|
+
--lutra-component-toggle-border-hover: var(--lutra-component-input-border-hover);
|
|
174
|
+
--lutra-component-toggle-border-invalid: var(--lutra-component-input-border-invalid);
|
|
175
|
+
--lutra-component-toggle-description: var(--lutra-color-text-secondary);
|
|
176
|
+
--lutra-component-toggle-error-text: var(--lutra-component-field-error-text);
|
|
177
|
+
--lutra-component-toggle-focus: var(--lutra-color-focus-ring);
|
|
178
|
+
--lutra-component-toggle-label: var(--lutra-color-text-primary);
|
|
179
|
+
--lutra-component-toggle-thumb-disabled: var(--lutra-color-text-disabled);
|
|
180
|
+
--lutra-component-toggle-thumb-off: var(--lutra-color-text-secondary);
|
|
181
|
+
--lutra-component-toggle-thumb-on: var(--lutra-color-text-on-brand);
|
|
182
|
+
--lutra-component-toggle-track-disabled: var(--lutra-color-background-disabled);
|
|
183
|
+
--lutra-component-toggle-track-off: var(--lutra-component-checkbox-surface);
|
|
184
|
+
--lutra-component-toggle-track-on: var(--lutra-component-checkbox-fill-selected);
|
|
185
|
+
--lutra-component-toggle-track-on-hover: var(--lutra-component-checkbox-fill-selected-hover);
|
|
186
|
+
--lutra-component-tooltip-border: var(--lutra-color-text-primary);
|
|
187
|
+
--lutra-component-tooltip-surface: var(--lutra-color-text-primary);
|
|
188
|
+
--lutra-component-tooltip-text: var(--lutra-color-background-canvas);
|
|
189
|
+
|
|
190
|
+
/* Lutra Typography */
|
|
191
|
+
--lutra-text-body: 1rem;
|
|
192
|
+
--lutra-text-body-small: 0.875rem;
|
|
193
|
+
--lutra-text-button-lg: 1rem;
|
|
194
|
+
--lutra-text-button-md: 0.875rem;
|
|
195
|
+
--lutra-text-button-sm: 0.8125rem;
|
|
196
|
+
--lutra-text-caption: 0.75rem;
|
|
197
|
+
--lutra-text-code: 0.875rem;
|
|
198
|
+
--lutra-text-display: 2rem;
|
|
199
|
+
--lutra-text-h1: 1.5rem;
|
|
200
|
+
--lutra-text-h2: 1.25rem;
|
|
201
|
+
--lutra-text-h3: 1.125rem;
|
|
202
|
+
--lutra-text-label: 0.8125rem;
|
|
203
|
+
--lutra-type-family-mono: 'Atkinson Hyperlegible Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
204
|
+
--lutra-type-family-sans: 'IBM Plex Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
205
|
+
--lutra-type-line-height-body: 1.5;
|
|
206
|
+
--lutra-type-line-height-body-small: 1.4286;
|
|
207
|
+
--lutra-type-line-height-button-lg: 1.5;
|
|
208
|
+
--lutra-type-line-height-button-md: 1.4286;
|
|
209
|
+
--lutra-type-line-height-button-sm: 1.2308;
|
|
210
|
+
--lutra-type-line-height-caption: 1.3333;
|
|
211
|
+
--lutra-type-line-height-code: 1.5714;
|
|
212
|
+
--lutra-type-line-height-display: 1.25;
|
|
213
|
+
--lutra-type-line-height-h1: 1.3333;
|
|
214
|
+
--lutra-type-line-height-h2: 1.4;
|
|
215
|
+
--lutra-type-line-height-h3: 1.3333;
|
|
216
|
+
--lutra-type-line-height-label: 1.2308;
|
|
217
|
+
--lutra-type-weight-bold: 700;
|
|
218
|
+
--lutra-type-weight-medium: 500;
|
|
219
|
+
--lutra-type-weight-regular: 400;
|
|
220
|
+
--lutra-type-weight-semibold: 600;
|
|
221
|
+
|
|
222
|
+
/* Lutra Spacing */
|
|
223
|
+
--lutra-border-width-thick: 2px;
|
|
224
|
+
--lutra-border-width-thin: 1px;
|
|
225
|
+
--lutra-radius-full: 999px;
|
|
226
|
+
--lutra-radius-lg: 0.75rem;
|
|
227
|
+
--lutra-radius-md: 0.5rem;
|
|
228
|
+
--lutra-radius-none: 0;
|
|
229
|
+
--lutra-radius-sm: 0.25rem;
|
|
230
|
+
--lutra-space-2xl: 1.5rem;
|
|
231
|
+
--lutra-space-2xs: 0.125rem;
|
|
232
|
+
--lutra-space-3xl: 2rem;
|
|
233
|
+
--lutra-space-lg: 1rem;
|
|
234
|
+
--lutra-space-md: 0.75rem;
|
|
235
|
+
--lutra-space-sm: 0.5rem;
|
|
236
|
+
--lutra-space-xl: 1.25rem;
|
|
237
|
+
--lutra-space-xs: 0.25rem;
|
|
238
|
+
|
|
239
|
+
/* Lutra Layout */
|
|
240
|
+
--lutra-layout-article-max: 45rem;
|
|
241
|
+
--lutra-layout-container-max: 75rem;
|
|
242
|
+
--lutra-layout-gutter-desktop: 2rem;
|
|
243
|
+
--lutra-layout-gutter-mobile: 1rem;
|
|
244
|
+
--lutra-layout-gutter-tablet: 1.5rem;
|
|
245
|
+
--lutra-layout-section-lg: 6rem;
|
|
246
|
+
--lutra-layout-section-sm: 4rem;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Dark mode. Every token whose value differs in dark, plus every alias that reaches one — an
|
|
250
|
+
* alias is substituted where it is declared, so it has to be re-declared here to resolve
|
|
251
|
+
* against dark values rather than inheriting the light result from :root. */
|
|
252
|
+
[data-theme='dark'] {
|
|
253
|
+
--lutra-color-background-canvas: #0f172a;
|
|
254
|
+
--lutra-color-background-disabled: #1f2937;
|
|
255
|
+
--lutra-color-background-elevated: #1f2937;
|
|
256
|
+
--lutra-color-background-subtle: #1f2937;
|
|
257
|
+
--lutra-color-background-subtle-active: #354256;
|
|
258
|
+
--lutra-color-background-subtle-hover: #2a3646;
|
|
259
|
+
--lutra-color-background-surface: #111827;
|
|
260
|
+
--lutra-color-border-default: #374151;
|
|
261
|
+
--lutra-color-border-disabled: #374151;
|
|
262
|
+
--lutra-color-border-strong: #6b7688;
|
|
263
|
+
--lutra-color-border-subtle: #2d3748;
|
|
264
|
+
--lutra-color-brand-primary: #3a6ee0;
|
|
265
|
+
--lutra-color-feedback-danger: #ff5c8a;
|
|
266
|
+
--lutra-color-feedback-danger-active: #ff9ab8;
|
|
267
|
+
--lutra-color-feedback-danger-hover: #ff7ba1;
|
|
268
|
+
--lutra-color-feedback-danger-text: #ff8fab;
|
|
269
|
+
--lutra-color-icon-default: var(--lutra-color-text-primary);
|
|
270
|
+
--lutra-color-link-text: #7aa2ff;
|
|
271
|
+
--lutra-color-text-disabled: #6b7280;
|
|
272
|
+
--lutra-color-text-inverse: #111827;
|
|
273
|
+
--lutra-color-text-muted: #9ca3af;
|
|
274
|
+
--lutra-color-text-on-danger: #2b0713;
|
|
275
|
+
--lutra-color-text-primary: #f9fafb;
|
|
276
|
+
--lutra-color-text-secondary: #d1d5db;
|
|
277
|
+
--lutra-component-button-danger-border: var(--lutra-color-feedback-danger);
|
|
278
|
+
--lutra-component-button-danger-surface: var(--lutra-color-feedback-danger);
|
|
279
|
+
--lutra-component-button-danger-surface-active: var(--lutra-color-feedback-danger-active);
|
|
280
|
+
--lutra-component-button-danger-surface-hover: var(--lutra-color-feedback-danger-hover);
|
|
281
|
+
--lutra-component-button-danger-text: var(--lutra-color-text-on-danger);
|
|
282
|
+
--lutra-component-button-disabled-border: var(--lutra-color-border-disabled);
|
|
283
|
+
--lutra-component-button-disabled-surface: var(--lutra-color-background-disabled);
|
|
284
|
+
--lutra-component-button-disabled-text: var(--lutra-color-text-disabled);
|
|
285
|
+
--lutra-component-button-primary-border: var(--lutra-color-brand-primary);
|
|
286
|
+
--lutra-component-button-primary-surface: var(--lutra-color-brand-primary);
|
|
287
|
+
--lutra-component-button-secondary-border: var(--lutra-color-border-strong);
|
|
288
|
+
--lutra-component-button-secondary-surface: var(--lutra-color-background-subtle);
|
|
289
|
+
--lutra-component-button-secondary-surface-active: var(--lutra-color-background-subtle-active);
|
|
290
|
+
--lutra-component-button-secondary-surface-hover: var(--lutra-color-background-subtle-hover);
|
|
291
|
+
--lutra-component-button-secondary-text: var(--lutra-color-text-primary);
|
|
292
|
+
--lutra-component-checkbox-border-default: var(--lutra-component-input-border-default);
|
|
293
|
+
--lutra-component-checkbox-border-disabled: var(--lutra-color-border-disabled);
|
|
294
|
+
--lutra-component-checkbox-border-invalid: var(--lutra-component-input-border-invalid);
|
|
295
|
+
--lutra-component-checkbox-description: var(--lutra-color-text-secondary);
|
|
296
|
+
--lutra-component-checkbox-error-text: var(--lutra-component-field-error-text);
|
|
297
|
+
--lutra-component-checkbox-fill-disabled: var(--lutra-color-background-disabled);
|
|
298
|
+
--lutra-component-checkbox-fill-invalid: var(--lutra-color-feedback-danger);
|
|
299
|
+
--lutra-component-checkbox-fill-selected: var(--lutra-color-brand-primary);
|
|
300
|
+
--lutra-component-checkbox-label: var(--lutra-color-text-primary);
|
|
301
|
+
--lutra-component-checkbox-mark-on-danger: var(--lutra-color-text-on-danger);
|
|
302
|
+
--lutra-component-checkbox-surface: var(--lutra-color-background-surface);
|
|
303
|
+
--lutra-component-checkbox-text-disabled: var(--lutra-color-text-disabled);
|
|
304
|
+
--lutra-component-content-accent: var(--lutra-color-brand-primary);
|
|
305
|
+
--lutra-component-content-body: var(--lutra-color-text-secondary);
|
|
306
|
+
--lutra-component-content-border: var(--lutra-color-border-default);
|
|
307
|
+
--lutra-component-content-icon: var(--lutra-color-icon-default);
|
|
308
|
+
--lutra-component-content-surface: var(--lutra-color-background-surface);
|
|
309
|
+
--lutra-component-content-surface-subtle: var(--lutra-color-background-subtle);
|
|
310
|
+
--lutra-component-content-title: var(--lutra-color-text-primary);
|
|
311
|
+
--lutra-component-field-error-text: var(--lutra-color-feedback-danger-text);
|
|
312
|
+
--lutra-component-input-border-default: var(--lutra-color-border-strong);
|
|
313
|
+
--lutra-component-input-border-invalid: var(--lutra-color-feedback-danger);
|
|
314
|
+
--lutra-component-input-placeholder: var(--lutra-color-text-secondary);
|
|
315
|
+
--lutra-component-input-surface: var(--lutra-color-background-surface);
|
|
316
|
+
--lutra-component-input-text: var(--lutra-color-text-primary);
|
|
317
|
+
--lutra-component-link-text-default: var(--lutra-color-link-text);
|
|
318
|
+
--lutra-component-link-text-focus: var(--lutra-color-link-text);
|
|
319
|
+
--lutra-component-link-text-hover: var(--lutra-color-link-text);
|
|
320
|
+
--lutra-component-link-text-visited: var(--lutra-color-text-secondary);
|
|
321
|
+
--lutra-component-menu-border: var(--lutra-component-input-border-default);
|
|
322
|
+
--lutra-component-menu-divider: var(--lutra-component-input-border-default);
|
|
323
|
+
--lutra-component-menu-item-danger: var(--lutra-component-field-error-text);
|
|
324
|
+
--lutra-component-menu-item-hover: var(--lutra-color-background-subtle);
|
|
325
|
+
--lutra-component-menu-item-text: var(--lutra-color-text-primary);
|
|
326
|
+
--lutra-component-menu-surface: var(--lutra-color-background-elevated);
|
|
327
|
+
--lutra-component-modal-body: var(--lutra-color-text-secondary);
|
|
328
|
+
--lutra-component-modal-border: var(--lutra-component-input-border-default);
|
|
329
|
+
--lutra-component-modal-divider: var(--lutra-component-input-border-default);
|
|
330
|
+
--lutra-component-modal-scrim: #0f172ac2;
|
|
331
|
+
--lutra-component-modal-surface: var(--lutra-color-background-elevated);
|
|
332
|
+
--lutra-component-modal-title: var(--lutra-color-text-primary);
|
|
333
|
+
--lutra-component-navigation-border: var(--lutra-color-border-default);
|
|
334
|
+
--lutra-component-navigation-item-indicator: var(--lutra-color-focus-ring);
|
|
335
|
+
--lutra-component-navigation-item-text: var(--lutra-color-text-primary);
|
|
336
|
+
--lutra-component-navigation-surface: var(--lutra-color-background-surface);
|
|
337
|
+
--lutra-component-publishing-accent: var(--lutra-color-brand-primary);
|
|
338
|
+
--lutra-component-publishing-body: var(--lutra-color-text-secondary);
|
|
339
|
+
--lutra-component-publishing-border: var(--lutra-color-border-default);
|
|
340
|
+
--lutra-component-publishing-code-surface: var(--lutra-color-background-elevated);
|
|
341
|
+
--lutra-component-publishing-metadata: var(--lutra-color-text-muted);
|
|
342
|
+
--lutra-component-publishing-surface: var(--lutra-color-background-surface);
|
|
343
|
+
--lutra-component-publishing-surface-subtle: var(--lutra-color-background-subtle);
|
|
344
|
+
--lutra-component-publishing-title: var(--lutra-color-text-primary);
|
|
345
|
+
--lutra-component-radio-border-default: var(--lutra-component-checkbox-border-default);
|
|
346
|
+
--lutra-component-radio-border-disabled: var(--lutra-component-checkbox-border-disabled);
|
|
347
|
+
--lutra-component-radio-border-invalid: var(--lutra-component-checkbox-border-invalid);
|
|
348
|
+
--lutra-component-radio-description: var(--lutra-component-checkbox-description);
|
|
349
|
+
--lutra-component-radio-error-text: var(--lutra-component-checkbox-error-text);
|
|
350
|
+
--lutra-component-radio-fill-disabled: var(--lutra-component-checkbox-fill-disabled);
|
|
351
|
+
--lutra-component-radio-fill-invalid: var(--lutra-component-checkbox-fill-invalid);
|
|
352
|
+
--lutra-component-radio-fill-selected: var(--lutra-component-checkbox-fill-selected);
|
|
353
|
+
--lutra-component-radio-label: var(--lutra-component-checkbox-label);
|
|
354
|
+
--lutra-component-radio-mark-on-danger: var(--lutra-component-checkbox-mark-on-danger);
|
|
355
|
+
--lutra-component-radio-surface: var(--lutra-component-checkbox-surface);
|
|
356
|
+
--lutra-component-radio-text-disabled: var(--lutra-component-checkbox-text-disabled);
|
|
357
|
+
--lutra-component-toggle-border-default: var(--lutra-component-input-border-default);
|
|
358
|
+
--lutra-component-toggle-border-invalid: var(--lutra-component-input-border-invalid);
|
|
359
|
+
--lutra-component-toggle-description: var(--lutra-color-text-secondary);
|
|
360
|
+
--lutra-component-toggle-error-text: var(--lutra-component-field-error-text);
|
|
361
|
+
--lutra-component-toggle-label: var(--lutra-color-text-primary);
|
|
362
|
+
--lutra-component-toggle-thumb-disabled: var(--lutra-color-text-disabled);
|
|
363
|
+
--lutra-component-toggle-thumb-off: var(--lutra-color-text-secondary);
|
|
364
|
+
--lutra-component-toggle-track-disabled: var(--lutra-color-background-disabled);
|
|
365
|
+
--lutra-component-toggle-track-off: var(--lutra-component-checkbox-surface);
|
|
366
|
+
--lutra-component-toggle-track-on: var(--lutra-component-checkbox-fill-selected);
|
|
367
|
+
--lutra-component-tooltip-border: var(--lutra-color-text-primary);
|
|
368
|
+
--lutra-component-tooltip-surface: var(--lutra-color-text-primary);
|
|
369
|
+
--lutra-component-tooltip-text: var(--lutra-color-background-canvas);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* An explicit light region, so a light island inside a dark one is possible. Same set as above,
|
|
373
|
+
* back at their light values. */
|
|
374
|
+
[data-theme='light'] {
|
|
375
|
+
--lutra-color-background-canvas: #f5f7fa;
|
|
376
|
+
--lutra-color-background-disabled: #e5e7eb;
|
|
377
|
+
--lutra-color-background-elevated: #ffffff;
|
|
378
|
+
--lutra-color-background-subtle: #f5f7fa;
|
|
379
|
+
--lutra-color-background-subtle-active: #dfe5ed;
|
|
380
|
+
--lutra-color-background-subtle-hover: #eaeef4;
|
|
381
|
+
--lutra-color-background-surface: #ffffff;
|
|
382
|
+
--lutra-color-border-default: #d0d7de;
|
|
383
|
+
--lutra-color-border-disabled: #d1d5db;
|
|
384
|
+
--lutra-color-border-strong: #7f8896;
|
|
385
|
+
--lutra-color-border-subtle: #e8ecf2;
|
|
386
|
+
--lutra-color-brand-primary: #3265d3;
|
|
387
|
+
--lutra-color-feedback-danger: #d14343;
|
|
388
|
+
--lutra-color-feedback-danger-active: #942c2c;
|
|
389
|
+
--lutra-color-feedback-danger-hover: #b33636;
|
|
390
|
+
--lutra-color-feedback-danger-text: #c73f3f;
|
|
391
|
+
--lutra-color-icon-default: var(--lutra-color-text-primary);
|
|
392
|
+
--lutra-color-link-text: var(--lutra-color-brand-primary);
|
|
393
|
+
--lutra-color-text-disabled: #9ca3af;
|
|
394
|
+
--lutra-color-text-inverse: #ffffff;
|
|
395
|
+
--lutra-color-text-muted: #6b7280;
|
|
396
|
+
--lutra-color-text-on-danger: #ffffff;
|
|
397
|
+
--lutra-color-text-primary: #111827;
|
|
398
|
+
--lutra-color-text-secondary: #6b7280;
|
|
399
|
+
--lutra-component-button-danger-border: var(--lutra-color-feedback-danger);
|
|
400
|
+
--lutra-component-button-danger-surface: var(--lutra-color-feedback-danger);
|
|
401
|
+
--lutra-component-button-danger-surface-active: var(--lutra-color-feedback-danger-active);
|
|
402
|
+
--lutra-component-button-danger-surface-hover: var(--lutra-color-feedback-danger-hover);
|
|
403
|
+
--lutra-component-button-danger-text: var(--lutra-color-text-on-danger);
|
|
404
|
+
--lutra-component-button-disabled-border: var(--lutra-color-border-disabled);
|
|
405
|
+
--lutra-component-button-disabled-surface: var(--lutra-color-background-disabled);
|
|
406
|
+
--lutra-component-button-disabled-text: var(--lutra-color-text-disabled);
|
|
407
|
+
--lutra-component-button-primary-border: var(--lutra-color-brand-primary);
|
|
408
|
+
--lutra-component-button-primary-surface: var(--lutra-color-brand-primary);
|
|
409
|
+
--lutra-component-button-secondary-border: var(--lutra-color-border-strong);
|
|
410
|
+
--lutra-component-button-secondary-surface: var(--lutra-color-background-subtle);
|
|
411
|
+
--lutra-component-button-secondary-surface-active: var(--lutra-color-background-subtle-active);
|
|
412
|
+
--lutra-component-button-secondary-surface-hover: var(--lutra-color-background-subtle-hover);
|
|
413
|
+
--lutra-component-button-secondary-text: var(--lutra-color-text-primary);
|
|
414
|
+
--lutra-component-checkbox-border-default: var(--lutra-component-input-border-default);
|
|
415
|
+
--lutra-component-checkbox-border-disabled: var(--lutra-color-border-disabled);
|
|
416
|
+
--lutra-component-checkbox-border-invalid: var(--lutra-component-input-border-invalid);
|
|
417
|
+
--lutra-component-checkbox-description: var(--lutra-color-text-secondary);
|
|
418
|
+
--lutra-component-checkbox-error-text: var(--lutra-component-field-error-text);
|
|
419
|
+
--lutra-component-checkbox-fill-disabled: var(--lutra-color-background-disabled);
|
|
420
|
+
--lutra-component-checkbox-fill-invalid: var(--lutra-color-feedback-danger);
|
|
421
|
+
--lutra-component-checkbox-fill-selected: var(--lutra-color-brand-primary);
|
|
422
|
+
--lutra-component-checkbox-label: var(--lutra-color-text-primary);
|
|
423
|
+
--lutra-component-checkbox-mark-on-danger: var(--lutra-color-text-on-danger);
|
|
424
|
+
--lutra-component-checkbox-surface: var(--lutra-color-background-surface);
|
|
425
|
+
--lutra-component-checkbox-text-disabled: var(--lutra-color-text-disabled);
|
|
426
|
+
--lutra-component-content-accent: var(--lutra-color-brand-primary);
|
|
427
|
+
--lutra-component-content-body: var(--lutra-color-text-secondary);
|
|
428
|
+
--lutra-component-content-border: var(--lutra-color-border-default);
|
|
429
|
+
--lutra-component-content-icon: var(--lutra-color-icon-default);
|
|
430
|
+
--lutra-component-content-surface: var(--lutra-color-background-surface);
|
|
431
|
+
--lutra-component-content-surface-subtle: var(--lutra-color-background-subtle);
|
|
432
|
+
--lutra-component-content-title: var(--lutra-color-text-primary);
|
|
433
|
+
--lutra-component-field-error-text: var(--lutra-color-feedback-danger-text);
|
|
434
|
+
--lutra-component-input-border-default: var(--lutra-color-border-strong);
|
|
435
|
+
--lutra-component-input-border-invalid: var(--lutra-color-feedback-danger);
|
|
436
|
+
--lutra-component-input-placeholder: var(--lutra-color-text-secondary);
|
|
437
|
+
--lutra-component-input-surface: var(--lutra-color-background-surface);
|
|
438
|
+
--lutra-component-input-text: var(--lutra-color-text-primary);
|
|
439
|
+
--lutra-component-link-text-default: var(--lutra-color-link-text);
|
|
440
|
+
--lutra-component-link-text-focus: var(--lutra-color-link-text);
|
|
441
|
+
--lutra-component-link-text-hover: var(--lutra-color-link-text);
|
|
442
|
+
--lutra-component-link-text-visited: var(--lutra-color-text-secondary);
|
|
443
|
+
--lutra-component-menu-border: var(--lutra-component-input-border-default);
|
|
444
|
+
--lutra-component-menu-divider: var(--lutra-component-input-border-default);
|
|
445
|
+
--lutra-component-menu-item-danger: var(--lutra-component-field-error-text);
|
|
446
|
+
--lutra-component-menu-item-hover: var(--lutra-color-background-subtle);
|
|
447
|
+
--lutra-component-menu-item-text: var(--lutra-color-text-primary);
|
|
448
|
+
--lutra-component-menu-surface: var(--lutra-color-background-elevated);
|
|
449
|
+
--lutra-component-modal-body: var(--lutra-color-text-secondary);
|
|
450
|
+
--lutra-component-modal-border: var(--lutra-component-input-border-default);
|
|
451
|
+
--lutra-component-modal-divider: var(--lutra-component-input-border-default);
|
|
452
|
+
--lutra-component-modal-scrim: #0f172aa3;
|
|
453
|
+
--lutra-component-modal-surface: var(--lutra-color-background-elevated);
|
|
454
|
+
--lutra-component-modal-title: var(--lutra-color-text-primary);
|
|
455
|
+
--lutra-component-navigation-border: var(--lutra-color-border-default);
|
|
456
|
+
--lutra-component-navigation-item-indicator: var(--lutra-color-brand-primary);
|
|
457
|
+
--lutra-component-navigation-item-text: var(--lutra-color-text-primary);
|
|
458
|
+
--lutra-component-navigation-surface: var(--lutra-color-background-surface);
|
|
459
|
+
--lutra-component-publishing-accent: var(--lutra-color-brand-primary);
|
|
460
|
+
--lutra-component-publishing-body: var(--lutra-color-text-secondary);
|
|
461
|
+
--lutra-component-publishing-border: var(--lutra-color-border-default);
|
|
462
|
+
--lutra-component-publishing-code-surface: var(--lutra-color-background-elevated);
|
|
463
|
+
--lutra-component-publishing-metadata: var(--lutra-color-text-muted);
|
|
464
|
+
--lutra-component-publishing-surface: var(--lutra-color-background-surface);
|
|
465
|
+
--lutra-component-publishing-surface-subtle: var(--lutra-color-background-subtle);
|
|
466
|
+
--lutra-component-publishing-title: var(--lutra-color-text-primary);
|
|
467
|
+
--lutra-component-radio-border-default: var(--lutra-component-checkbox-border-default);
|
|
468
|
+
--lutra-component-radio-border-disabled: var(--lutra-component-checkbox-border-disabled);
|
|
469
|
+
--lutra-component-radio-border-invalid: var(--lutra-component-checkbox-border-invalid);
|
|
470
|
+
--lutra-component-radio-description: var(--lutra-component-checkbox-description);
|
|
471
|
+
--lutra-component-radio-error-text: var(--lutra-component-checkbox-error-text);
|
|
472
|
+
--lutra-component-radio-fill-disabled: var(--lutra-component-checkbox-fill-disabled);
|
|
473
|
+
--lutra-component-radio-fill-invalid: var(--lutra-component-checkbox-fill-invalid);
|
|
474
|
+
--lutra-component-radio-fill-selected: var(--lutra-component-checkbox-fill-selected);
|
|
475
|
+
--lutra-component-radio-label: var(--lutra-component-checkbox-label);
|
|
476
|
+
--lutra-component-radio-mark-on-danger: var(--lutra-component-checkbox-mark-on-danger);
|
|
477
|
+
--lutra-component-radio-surface: var(--lutra-component-checkbox-surface);
|
|
478
|
+
--lutra-component-radio-text-disabled: var(--lutra-component-checkbox-text-disabled);
|
|
479
|
+
--lutra-component-toggle-border-default: var(--lutra-component-input-border-default);
|
|
480
|
+
--lutra-component-toggle-border-invalid: var(--lutra-component-input-border-invalid);
|
|
481
|
+
--lutra-component-toggle-description: var(--lutra-color-text-secondary);
|
|
482
|
+
--lutra-component-toggle-error-text: var(--lutra-component-field-error-text);
|
|
483
|
+
--lutra-component-toggle-label: var(--lutra-color-text-primary);
|
|
484
|
+
--lutra-component-toggle-thumb-disabled: var(--lutra-color-text-disabled);
|
|
485
|
+
--lutra-component-toggle-thumb-off: var(--lutra-color-text-secondary);
|
|
486
|
+
--lutra-component-toggle-track-disabled: var(--lutra-color-background-disabled);
|
|
487
|
+
--lutra-component-toggle-track-off: var(--lutra-component-checkbox-surface);
|
|
488
|
+
--lutra-component-toggle-track-on: var(--lutra-component-checkbox-fill-selected);
|
|
489
|
+
--lutra-component-tooltip-border: var(--lutra-color-text-primary);
|
|
490
|
+
--lutra-component-tooltip-surface: var(--lutra-color-text-primary);
|
|
491
|
+
--lutra-component-tooltip-text: var(--lutra-color-background-canvas);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/*
|
|
495
|
+
* Button.
|
|
496
|
+
*
|
|
497
|
+
* The public styling contract is the `lutra-button` class plus the `data-variant`,
|
|
498
|
+
* `data-size`, `data-content-type` and `data-full-width` attributes. Interaction
|
|
499
|
+
* states are selected from the platform — `:hover`, `:active`, `:focus-visible`,
|
|
500
|
+
* `:disabled` — never from a prop.
|
|
501
|
+
*
|
|
502
|
+
* Two token tiers are in play:
|
|
503
|
+
*
|
|
504
|
+
* 1. **Design tokens** — generated from the Figma library into
|
|
505
|
+
* `src/tokens/generated.css` and declared on `:root`. Button never reads a raw
|
|
506
|
+
* value, and never hard-codes a Figma colour as a `var()` fallback: the token
|
|
507
|
+
* layer is always present, because it ships in the same stylesheet.
|
|
508
|
+
*
|
|
509
|
+
* Every colour comes from Figma's own `component/button/*` alias layer, so the
|
|
510
|
+
* design system decides what "the primary surface" is and this file only
|
|
511
|
+
* consumes it. Sizes, spacing and type come from the core scales
|
|
512
|
+
* (`--lutra-space-*`, `--lutra-text-*`, `--lutra-radius-*`,
|
|
513
|
+
* `--lutra-border-width-*`, `--lutra-type-*`), which Figma does not mirror per
|
|
514
|
+
* component.
|
|
515
|
+
* 2. **Component tokens** — `--lutra-button-*`, declared below on the component
|
|
516
|
+
* root. Each one names which design token it takes its value from, so the
|
|
517
|
+
* mapping from the design system to this component is one readable list.
|
|
518
|
+
*
|
|
519
|
+
* Overriding a `--lutra-button-*` property on a Button, or a `--lutra-*` design
|
|
520
|
+
* token at any level above it, retunes the component without touching this file.
|
|
521
|
+
*
|
|
522
|
+
* The handful of values with no Figma variable behind them are marked. They are
|
|
523
|
+
* accessibility floors and drawing details Figma has no way to express, not colours
|
|
524
|
+
* or type someone forgot to tokenise.
|
|
525
|
+
*/
|
|
526
|
+
|
|
527
|
+
.lutra-button {
|
|
528
|
+
/* Brand, used by `variant="primary"`. */
|
|
529
|
+
--lutra-button-brand: var(--lutra-component-button-primary-surface);
|
|
530
|
+
--lutra-button-brand-hover: var(--lutra-component-button-primary-surface-hover);
|
|
531
|
+
--lutra-button-brand-active: var(--lutra-component-button-primary-surface-active);
|
|
532
|
+
--lutra-button-brand-border: var(--lutra-component-button-primary-border);
|
|
533
|
+
--lutra-button-on-brand: var(--lutra-component-button-primary-text);
|
|
534
|
+
|
|
535
|
+
/* Danger, used by `variant="danger"`. */
|
|
536
|
+
--lutra-button-danger: var(--lutra-component-button-danger-surface);
|
|
537
|
+
--lutra-button-danger-hover: var(--lutra-component-button-danger-surface-hover);
|
|
538
|
+
--lutra-button-danger-active: var(--lutra-component-button-danger-surface-active);
|
|
539
|
+
--lutra-button-danger-border: var(--lutra-component-button-danger-border);
|
|
540
|
+
--lutra-button-on-danger: var(--lutra-component-button-danger-text);
|
|
541
|
+
|
|
542
|
+
/* Neutral, used by `variant="secondary"`. */
|
|
543
|
+
--lutra-button-neutral: var(--lutra-component-button-secondary-surface);
|
|
544
|
+
--lutra-button-neutral-hover: var(--lutra-component-button-secondary-surface-hover);
|
|
545
|
+
--lutra-button-neutral-active: var(--lutra-component-button-secondary-surface-active);
|
|
546
|
+
--lutra-button-border: var(--lutra-component-button-secondary-border);
|
|
547
|
+
--lutra-button-text: var(--lutra-component-button-secondary-text);
|
|
548
|
+
|
|
549
|
+
/* Disabled. Deliberate colours rather than an opacity wash, so the state is a
|
|
550
|
+
* real visual change and not just a faded version of the enabled button. */
|
|
551
|
+
--lutra-button-disabled-surface: var(--lutra-component-button-disabled-surface);
|
|
552
|
+
--lutra-button-disabled-border: var(--lutra-component-button-disabled-border);
|
|
553
|
+
--lutra-button-disabled-text: var(--lutra-component-button-disabled-text);
|
|
554
|
+
|
|
555
|
+
/* Type. Line heights are unitless ratios, so they follow the font size. */
|
|
556
|
+
--lutra-button-font-family: var(--lutra-type-family-sans);
|
|
557
|
+
--lutra-button-font-weight: var(--lutra-type-weight-medium);
|
|
558
|
+
--lutra-button-font-size-sm: var(--lutra-text-button-sm);
|
|
559
|
+
--lutra-button-font-size-md: var(--lutra-text-button-md);
|
|
560
|
+
--lutra-button-font-size-lg: var(--lutra-text-button-lg);
|
|
561
|
+
--lutra-button-line-height-sm: var(--lutra-type-line-height-button-sm);
|
|
562
|
+
--lutra-button-line-height-md: var(--lutra-type-line-height-button-md);
|
|
563
|
+
--lutra-button-line-height-lg: var(--lutra-type-line-height-button-lg);
|
|
564
|
+
|
|
565
|
+
/* Spacing. */
|
|
566
|
+
--lutra-button-padding-inline-sm: var(--lutra-space-sm);
|
|
567
|
+
--lutra-button-padding-inline-md: var(--lutra-space-lg);
|
|
568
|
+
--lutra-button-padding-inline-lg: var(--lutra-space-xl);
|
|
569
|
+
--lutra-button-gap-sm: var(--lutra-space-xs);
|
|
570
|
+
--lutra-button-gap-md: var(--lutra-space-sm);
|
|
571
|
+
--lutra-button-gap-lg: var(--lutra-space-md);
|
|
572
|
+
|
|
573
|
+
/* Shape. */
|
|
574
|
+
--lutra-button-radius: var(--lutra-radius-md);
|
|
575
|
+
--lutra-button-border-width: var(--lutra-border-width-thin);
|
|
576
|
+
|
|
577
|
+
/* Focus. The ring colour is the one canonical `color/focus/ring`; the width is
|
|
578
|
+
* Figma's thick border. The offset has no Figma variable — Figma documents the
|
|
579
|
+
* ring as a drawn rectangle rather than a value — and it is not cosmetic: it is
|
|
580
|
+
* what puts the ring on the surface *behind* a filled button, where it clears
|
|
581
|
+
* WCAG 1.4.11's 3:1, instead of on the button's own fill where it would be about
|
|
582
|
+
* 1.4:1 and invisible. See `button.md`. */
|
|
583
|
+
--lutra-button-focus-ring-color: var(--lutra-component-button-focus);
|
|
584
|
+
--lutra-button-focus-ring-width: var(--lutra-border-width-thick);
|
|
585
|
+
--lutra-button-focus-ring-offset: 2px;
|
|
586
|
+
|
|
587
|
+
/* NO FIGMA VARIABLE. Figma draws buttons at a fixed height; these become
|
|
588
|
+
* `min-block-size` instead, because a fixed height on a text-bearing container
|
|
589
|
+
* clips wrapped labels and breaks at 200% zoom. `rem` so they grow with the
|
|
590
|
+
* reader's font size. 36px, 40px, 44px. */
|
|
591
|
+
--lutra-button-min-height-sm: 2.25rem;
|
|
592
|
+
--lutra-button-min-height-md: 2.5rem;
|
|
593
|
+
--lutra-button-min-height-lg: 2.75rem;
|
|
594
|
+
|
|
595
|
+
/* NO FIGMA VARIABLE. Vertical breathing room a wrapped label needs once the
|
|
596
|
+
* button is free to grow; small enough that a single-line button is still
|
|
597
|
+
* exactly its minimum height. */
|
|
598
|
+
--lutra-button-padding-block: var(--lutra-space-xs);
|
|
599
|
+
|
|
600
|
+
/* NO FIGMA VARIABLE. The stable 16px icon slot from the Figma documentation, so
|
|
601
|
+
* a button's height does not depend on the icon a consumer passes. */
|
|
602
|
+
--lutra-button-icon-size: 1rem;
|
|
603
|
+
|
|
604
|
+
/* NO FIGMA VARIABLE. The 44px minimum pointer target. An accessibility floor
|
|
605
|
+
* rather than a design value, and it must not be tuned down. */
|
|
606
|
+
--lutra-button-min-target-size: 2.75rem;
|
|
607
|
+
|
|
608
|
+
box-sizing: border-box;
|
|
609
|
+
display: inline-flex;
|
|
610
|
+
align-items: center;
|
|
611
|
+
justify-content: center;
|
|
612
|
+
margin: 0;
|
|
613
|
+
padding-block: var(--lutra-button-padding-block);
|
|
614
|
+
border-style: solid;
|
|
615
|
+
border-width: var(--lutra-button-border-width);
|
|
616
|
+
border-radius: var(--lutra-button-radius);
|
|
617
|
+
font-family: var(--lutra-button-font-family);
|
|
618
|
+
font-weight: var(--lutra-button-font-weight);
|
|
619
|
+
text-align: center;
|
|
620
|
+
cursor: pointer;
|
|
621
|
+
|
|
622
|
+
/* Strips the platform's own button chrome so the token colours are what shows,
|
|
623
|
+
* notably on iOS Safari. */
|
|
624
|
+
appearance: none;
|
|
625
|
+
|
|
626
|
+
/* Long labels and long words wrap rather than overflow the button. Nothing here
|
|
627
|
+
* sets a fixed height, so the button grows to hold them. */
|
|
628
|
+
overflow-wrap: break-word;
|
|
629
|
+
text-wrap: pretty;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* Variants.
|
|
633
|
+
*
|
|
634
|
+
* Each carries a border in its own fill colour, so swapping variants never
|
|
635
|
+
* changes a button's size. */
|
|
636
|
+
|
|
637
|
+
.lutra-button[data-variant='primary'] {
|
|
638
|
+
border-color: var(--lutra-button-brand-border);
|
|
639
|
+
background-color: var(--lutra-button-brand);
|
|
640
|
+
color: var(--lutra-button-on-brand);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* Only the fill moves on hover and active. The border stays at the base brand
|
|
644
|
+
* colour, which is what Figma draws and why `component/button/primary/border` is a
|
|
645
|
+
* separate token from the surface. */
|
|
646
|
+
|
|
647
|
+
.lutra-button[data-variant='primary']:not(:disabled):hover {
|
|
648
|
+
background-color: var(--lutra-button-brand-hover);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.lutra-button[data-variant='primary']:not(:disabled):active {
|
|
652
|
+
background-color: var(--lutra-button-brand-active);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.lutra-button[data-variant='secondary'] {
|
|
656
|
+
border-color: var(--lutra-button-border);
|
|
657
|
+
background-color: var(--lutra-button-neutral);
|
|
658
|
+
color: var(--lutra-button-text);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.lutra-button[data-variant='secondary']:not(:disabled):hover {
|
|
662
|
+
background-color: var(--lutra-button-neutral-hover);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.lutra-button[data-variant='secondary']:not(:disabled):active {
|
|
666
|
+
background-color: var(--lutra-button-neutral-active);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.lutra-button[data-variant='danger'] {
|
|
670
|
+
border-color: var(--lutra-button-danger-border);
|
|
671
|
+
background-color: var(--lutra-button-danger);
|
|
672
|
+
color: var(--lutra-button-on-danger);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.lutra-button[data-variant='danger']:not(:disabled):hover {
|
|
676
|
+
background-color: var(--lutra-button-danger-hover);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.lutra-button[data-variant='danger']:not(:disabled):active {
|
|
680
|
+
background-color: var(--lutra-button-danger-active);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/* Sizes.
|
|
684
|
+
*
|
|
685
|
+
* `min-block-size` rather than `height`: it reproduces the Figma height for the
|
|
686
|
+
* single-line case the design draws, and lets the button grow when the label
|
|
687
|
+
* wraps or the user raises their font size. */
|
|
688
|
+
|
|
689
|
+
.lutra-button[data-size='sm'] {
|
|
690
|
+
min-block-size: var(--lutra-button-min-height-sm);
|
|
691
|
+
padding-inline: var(--lutra-button-padding-inline-sm);
|
|
692
|
+
gap: var(--lutra-button-gap-sm);
|
|
693
|
+
font-size: var(--lutra-button-font-size-sm);
|
|
694
|
+
line-height: var(--lutra-button-line-height-sm);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.lutra-button[data-size='md'] {
|
|
698
|
+
min-block-size: var(--lutra-button-min-height-md);
|
|
699
|
+
padding-inline: var(--lutra-button-padding-inline-md);
|
|
700
|
+
gap: var(--lutra-button-gap-md);
|
|
701
|
+
font-size: var(--lutra-button-font-size-md);
|
|
702
|
+
line-height: var(--lutra-button-line-height-md);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.lutra-button[data-size='lg'] {
|
|
706
|
+
min-block-size: var(--lutra-button-min-height-lg);
|
|
707
|
+
padding-inline: var(--lutra-button-padding-inline-lg);
|
|
708
|
+
gap: var(--lutra-button-gap-lg);
|
|
709
|
+
font-size: var(--lutra-button-font-size-lg);
|
|
710
|
+
line-height: var(--lutra-button-line-height-lg);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/* Disabled, which is also how loading renders.
|
|
714
|
+
*
|
|
715
|
+
* The colour change is a real one — a different surface, border and text, not the
|
|
716
|
+
* enabled button at a lower opacity — and it is never the only signal: the button
|
|
717
|
+
* is natively disabled, so it is skipped by Tab, announced as unavailable, and
|
|
718
|
+
* shows the platform's own cursor. Hover and active are excluded above. */
|
|
719
|
+
|
|
720
|
+
.lutra-button:disabled {
|
|
721
|
+
border-color: var(--lutra-button-disabled-border);
|
|
722
|
+
background-color: var(--lutra-button-disabled-surface);
|
|
723
|
+
color: var(--lutra-button-disabled-text);
|
|
724
|
+
cursor: default;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* Focus.
|
|
728
|
+
*
|
|
729
|
+
* The browser outline is replaced, not removed: a 2px ring at a 2px offset, in a
|
|
730
|
+
* colour with enough contrast against the surface behind the button. The offset
|
|
731
|
+
* is what makes the ring visible against a filled button — see `tokens/focus.css`. */
|
|
732
|
+
|
|
733
|
+
.lutra-button:focus-visible {
|
|
734
|
+
outline: var(--lutra-button-focus-ring-width) solid var(--lutra-button-focus-ring-color);
|
|
735
|
+
outline-offset: var(--lutra-button-focus-ring-offset);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* Full width. Layout only. */
|
|
739
|
+
|
|
740
|
+
.lutra-button[data-full-width] {
|
|
741
|
+
inline-size: 100%;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* Icon slot.
|
|
745
|
+
*
|
|
746
|
+
* A fixed 16px box, so a button's height does not depend on the icon a consumer
|
|
747
|
+
* happens to pass. The SVG rule sizes an arbitrary consumer icon to the slot; it
|
|
748
|
+
* is scoped under the icon class, so it cannot restyle anything else on the page. */
|
|
749
|
+
|
|
750
|
+
.lutra-button__icon {
|
|
751
|
+
display: inline-flex;
|
|
752
|
+
flex: 0 0 auto;
|
|
753
|
+
align-items: center;
|
|
754
|
+
justify-content: center;
|
|
755
|
+
inline-size: var(--lutra-button-icon-size);
|
|
756
|
+
block-size: var(--lutra-button-icon-size);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.lutra-button__icon > svg {
|
|
760
|
+
inline-size: 100%;
|
|
761
|
+
block-size: 100%;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* Label.
|
|
765
|
+
*
|
|
766
|
+
* When a loading label replaces the ordinary one, both are stacked in the same
|
|
767
|
+
* grid cell so the button keeps the wider of the two widths and the swap causes no
|
|
768
|
+
* layout shift. The measuring copy is hidden from sight and from assistive
|
|
769
|
+
* technology, so it contributes width and nothing else. */
|
|
770
|
+
|
|
771
|
+
.lutra-button__label {
|
|
772
|
+
min-inline-size: 0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.lutra-button__label--swapped {
|
|
776
|
+
display: grid;
|
|
777
|
+
grid-template-areas: 'label';
|
|
778
|
+
align-items: center;
|
|
779
|
+
justify-items: center;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.lutra-button__label--swapped > * {
|
|
783
|
+
grid-area: label;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.lutra-button__label-measure {
|
|
787
|
+
visibility: hidden;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/* Icon-only.
|
|
791
|
+
*
|
|
792
|
+
* The drawn button keeps its size at every scale — an `sm` icon-only button is
|
|
793
|
+
* still 36px — and reaches the 44px minimum pointer target through a transparent
|
|
794
|
+
* hit area centred on it. Growing the button instead would make `sm`, `md` and
|
|
795
|
+
* `lg` icon-only buttons indistinguishable. */
|
|
796
|
+
|
|
797
|
+
.lutra-button[data-content-type='icon-only'] {
|
|
798
|
+
position: relative;
|
|
799
|
+
padding-inline: 0;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.lutra-button[data-content-type='icon-only'][data-size='sm'] {
|
|
803
|
+
inline-size: var(--lutra-button-min-height-sm);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.lutra-button[data-content-type='icon-only'][data-size='md'] {
|
|
807
|
+
inline-size: var(--lutra-button-min-height-md);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.lutra-button[data-content-type='icon-only'][data-size='lg'] {
|
|
811
|
+
inline-size: var(--lutra-button-min-height-lg);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.lutra-button[data-content-type='icon-only']::after {
|
|
815
|
+
content: '';
|
|
816
|
+
position: absolute;
|
|
817
|
+
inset-block-start: 50%;
|
|
818
|
+
inset-inline-start: 50%;
|
|
819
|
+
transform: translate(-50%, -50%);
|
|
820
|
+
inline-size: 100%;
|
|
821
|
+
block-size: 100%;
|
|
822
|
+
min-inline-size: var(--lutra-button-min-target-size);
|
|
823
|
+
min-block-size: var(--lutra-button-min-target-size);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/* Spinner.
|
|
827
|
+
*
|
|
828
|
+
* Built from borders rather than an imported asset: three sides in the current
|
|
829
|
+
* text colour, one transparent, rotating. `currentcolor` means it is legible on
|
|
830
|
+
* every variant and in the disabled palette without a token of its own. */
|
|
831
|
+
|
|
832
|
+
.lutra-button__spinner {
|
|
833
|
+
flex: 0 0 auto;
|
|
834
|
+
box-sizing: border-box;
|
|
835
|
+
inline-size: var(--lutra-button-icon-size);
|
|
836
|
+
block-size: var(--lutra-button-icon-size);
|
|
837
|
+
border: var(--lutra-button-focus-ring-width) solid currentcolor;
|
|
838
|
+
border-block-start-color: transparent;
|
|
839
|
+
border-radius: 50%;
|
|
840
|
+
animation: lutra-button-spin 0.7s linear infinite;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
@keyframes lutra-button-spin {
|
|
844
|
+
to {
|
|
845
|
+
transform: rotate(1turn);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* Reduced motion: the rotation is slowed to a crawl rather than removed. A static
|
|
850
|
+
* ring does not read as "working"; a slow one does, without the fast spin that
|
|
851
|
+
* the preference exists to avoid. */
|
|
852
|
+
|
|
853
|
+
@media (prefers-reduced-motion: reduce) {
|
|
854
|
+
.lutra-button__spinner {
|
|
855
|
+
animation-duration: 2.4s;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* Windows High Contrast Mode and other forced-colours modes.
|
|
860
|
+
*
|
|
861
|
+
* The system replaces the token colours, which is the point of the mode. What it
|
|
862
|
+
* does not do reliably is keep a custom focus ring visible, so the ring is pinned
|
|
863
|
+
* to the system highlight colour. Disabled buttons already pick up `GrayText`
|
|
864
|
+
* from the native `disabled` attribute. */
|
|
865
|
+
|
|
866
|
+
@media (forced-colors: active) {
|
|
867
|
+
.lutra-button:focus-visible {
|
|
868
|
+
outline-color: Highlight;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/*
|
|
873
|
+
* Link.
|
|
874
|
+
*
|
|
875
|
+
* The public styling contract is the `lutra-link` class plus `data-variant`. Hover, focus, active
|
|
876
|
+
* and visited are selected from the platform — `:hover`, `:focus-visible`, `:active`, `:visited` —
|
|
877
|
+
* never from a prop.
|
|
878
|
+
*
|
|
879
|
+
* Colour is never the only thing separating an inline link from the prose around it: inline links
|
|
880
|
+
* keep their underline at rest, which is what the annotation asks for.
|
|
881
|
+
*/
|
|
882
|
+
|
|
883
|
+
.lutra-link {
|
|
884
|
+
--lutra-link-text: var(--lutra-component-link-text-default);
|
|
885
|
+
--lutra-link-text-hover: var(--lutra-component-link-text-hover);
|
|
886
|
+
--lutra-link-text-focus: var(--lutra-component-link-text-focus);
|
|
887
|
+
--lutra-link-text-visited: var(--lutra-component-link-text-visited);
|
|
888
|
+
|
|
889
|
+
--lutra-link-focus-ring-color: var(--lutra-component-navigation-focus);
|
|
890
|
+
--lutra-link-focus-ring-width: var(--lutra-border-width-thick);
|
|
891
|
+
|
|
892
|
+
/* NO FIGMA VARIABLE. The same 2px offset every other Lutra control uses, so a focused link in a
|
|
893
|
+
* row of controls shows one treatment. */
|
|
894
|
+
--lutra-link-focus-ring-offset: 2px;
|
|
895
|
+
|
|
896
|
+
/* NO FIGMA VARIABLE. Figma keeps a 44px target on standalone links. */
|
|
897
|
+
--lutra-link-standalone-min-target: 2.75rem;
|
|
898
|
+
|
|
899
|
+
color: var(--lutra-link-text);
|
|
900
|
+
border-radius: var(--lutra-radius-sm);
|
|
901
|
+
|
|
902
|
+
/* Underline thickness follows the font, and the offset keeps the line clear of descenders so the
|
|
903
|
+
* text stays readable rather than struck through. */
|
|
904
|
+
text-decoration-thickness: from-font;
|
|
905
|
+
text-underline-offset: 0.2em;
|
|
906
|
+
|
|
907
|
+
/* A long URL or an unbroken word wraps rather than pushing its container wider. */
|
|
908
|
+
overflow-wrap: break-word;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* Inline links live in prose and keep their underline at rest — colour alone is not enough to find
|
|
912
|
+
* a link inside a paragraph. */
|
|
913
|
+
|
|
914
|
+
.lutra-link[data-variant='inline'] {
|
|
915
|
+
text-decoration-line: underline;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/* Standalone links sit on their own, where position and spacing already separate them, so the
|
|
919
|
+
* underline is reserved for hover. The minimum target is met without changing the drawn text: an
|
|
920
|
+
* inline-flex box with a minimum block size grows the hit area, not the type. */
|
|
921
|
+
|
|
922
|
+
.lutra-link[data-variant='standalone'] {
|
|
923
|
+
display: inline-flex;
|
|
924
|
+
align-items: center;
|
|
925
|
+
min-block-size: var(--lutra-link-standalone-min-target);
|
|
926
|
+
text-decoration-line: none;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.lutra-link:hover {
|
|
930
|
+
color: var(--lutra-link-text-hover);
|
|
931
|
+
text-decoration-line: underline;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/* `:visited` is deliberately after `:hover` for the colour, and the browser restricts what may be
|
|
935
|
+
* styled here anyway — colour is one of the few properties allowed, which is exactly what Figma
|
|
936
|
+
* varies. */
|
|
937
|
+
|
|
938
|
+
.lutra-link:visited {
|
|
939
|
+
color: var(--lutra-link-text-visited);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.lutra-link:visited:hover {
|
|
943
|
+
color: var(--lutra-link-text-hover);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.lutra-link:focus-visible {
|
|
947
|
+
color: var(--lutra-link-text-focus);
|
|
948
|
+
outline: var(--lutra-link-focus-ring-width) solid var(--lutra-link-focus-ring-color);
|
|
949
|
+
outline-offset: var(--lutra-link-focus-ring-offset);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* Forced-colours modes: the system supplies its own link colour, and what it does not reliably keep
|
|
953
|
+
* is a custom focus ring, so that is pinned to the system highlight. */
|
|
954
|
+
|
|
955
|
+
@media (forced-colors: active) {
|
|
956
|
+
.lutra-link:focus-visible {
|
|
957
|
+
outline-color: Highlight;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/*
|
|
962
|
+
* Navigation Item.
|
|
963
|
+
*
|
|
964
|
+
* A destination inside a navigation landmark. The styling contract is the `lutra-navigation-item`
|
|
965
|
+
* class; the current state is selected from `[aria-current='page']` rather than from a class, so the
|
|
966
|
+
* look and what assistive technology is told cannot drift apart.
|
|
967
|
+
*
|
|
968
|
+
* Figma draws four states — default, hover, focus, current. Three of them are platform states and are
|
|
969
|
+
* selected as such; only `current` is a property, because only `current` is a fact about the page
|
|
970
|
+
* rather than about the pointer.
|
|
971
|
+
*/
|
|
972
|
+
|
|
973
|
+
.lutra-navigation-item {
|
|
974
|
+
--lutra-navigation-item-text: var(--lutra-component-navigation-item-text);
|
|
975
|
+
--lutra-navigation-item-indicator: var(--lutra-component-navigation-item-indicator);
|
|
976
|
+
--lutra-navigation-item-focus-ring-color: var(--lutra-component-navigation-focus);
|
|
977
|
+
--lutra-navigation-item-focus-ring-width: var(--lutra-border-width-thick);
|
|
978
|
+
|
|
979
|
+
/* NO FIGMA VARIABLE. Figma draws a 44px tall item with 8/8/6/8 padding, a 4px gap above a 32×2
|
|
980
|
+
* indicator, and a 4px corner on the focus ring. The 44px target is the annotation's minimum. */
|
|
981
|
+
--lutra-navigation-item-min-target: 2.75rem;
|
|
982
|
+
--lutra-navigation-item-indicator-inline-size: 2rem;
|
|
983
|
+
--lutra-navigation-item-indicator-block-size: 2px;
|
|
984
|
+
|
|
985
|
+
display: flex;
|
|
986
|
+
flex-direction: column;
|
|
987
|
+
align-items: center;
|
|
988
|
+
justify-content: center;
|
|
989
|
+
gap: var(--lutra-space-xs);
|
|
990
|
+
box-sizing: border-box;
|
|
991
|
+
min-block-size: var(--lutra-navigation-item-min-target);
|
|
992
|
+
padding: var(--lutra-space-sm) var(--lutra-space-sm) calc(var(--lutra-space-sm) - 2px);
|
|
993
|
+
border-radius: var(--lutra-radius-sm);
|
|
994
|
+
|
|
995
|
+
color: var(--lutra-navigation-item-text);
|
|
996
|
+
font-family: var(--lutra-type-family-sans);
|
|
997
|
+
font-size: var(--lutra-text-body);
|
|
998
|
+
line-height: var(--lutra-type-line-height-body);
|
|
999
|
+
font-weight: var(--lutra-type-weight-medium);
|
|
1000
|
+
text-decoration-line: none;
|
|
1001
|
+
|
|
1002
|
+
/* A long destination name wraps rather than pushing the navigation wider. */
|
|
1003
|
+
overflow-wrap: break-word;
|
|
1004
|
+
text-wrap: pretty;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/* Hover is the only place the underline appears at rest-adjacent state, matching Figma. */
|
|
1008
|
+
|
|
1009
|
+
.lutra-navigation-item:hover {
|
|
1010
|
+
text-decoration-line: underline;
|
|
1011
|
+
text-decoration-thickness: from-font;
|
|
1012
|
+
text-underline-offset: 0.2em;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.lutra-navigation-item:focus-visible {
|
|
1016
|
+
outline: var(--lutra-navigation-item-focus-ring-width) solid
|
|
1017
|
+
var(--lutra-navigation-item-focus-ring-color);
|
|
1018
|
+
|
|
1019
|
+
/* No offset, because Figma draws the ring on the item's own 4px corner rather than around it, and
|
|
1020
|
+
* items sit 4px apart — an offset ring would touch its neighbour. */
|
|
1021
|
+
outline-offset: 0;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/* The current destination: a weight change, an indicator bar, and colour — three signals, so no one
|
|
1025
|
+
* of them has to carry it alone. Selected from the ARIA attribute, so it cannot be styled as current
|
|
1026
|
+
* without also being announced as current. */
|
|
1027
|
+
|
|
1028
|
+
.lutra-navigation-item[aria-current='page'] {
|
|
1029
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.lutra-navigation-item__indicator {
|
|
1033
|
+
inline-size: var(--lutra-navigation-item-indicator-inline-size);
|
|
1034
|
+
block-size: var(--lutra-navigation-item-indicator-block-size);
|
|
1035
|
+
border-radius: var(--lutra-radius-full);
|
|
1036
|
+
background-color: var(--lutra-navigation-item-indicator);
|
|
1037
|
+
|
|
1038
|
+
/* Holds its size when the item is in a flex column that would otherwise squash it. */
|
|
1039
|
+
flex: 0 0 auto;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* Forced-colours modes: the indicator's background is not painted, so it is pinned to the system
|
|
1043
|
+
* highlight, and the focus ring to the same — otherwise the current item and the focused item both
|
|
1044
|
+
* lose their only remaining distinction. */
|
|
1045
|
+
|
|
1046
|
+
@media (forced-colors: active) {
|
|
1047
|
+
.lutra-navigation-item__indicator {
|
|
1048
|
+
background-color: Highlight;
|
|
1049
|
+
forced-color-adjust: none;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
.lutra-navigation-item:focus-visible {
|
|
1053
|
+
outline-color: Highlight;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/*
|
|
1058
|
+
* Mobile Navigation.
|
|
1059
|
+
*
|
|
1060
|
+
* A disclosure: a trigger button and a panel of destinations. Nothing here is modal, so nothing here
|
|
1061
|
+
* needs a scrim or the top layer — the panel is an ordinary positioned element.
|
|
1062
|
+
*
|
|
1063
|
+
* Figma draws a 390px panel below the trigger with a 16px inline gutter and a 24px bottom pad. The
|
|
1064
|
+
* width is not fixed here: the panel stretches to its container and is capped, so it works at 400%
|
|
1065
|
+
* zoom, where a fixed 390px panel would simply overflow.
|
|
1066
|
+
*/
|
|
1067
|
+
|
|
1068
|
+
.lutra-mobile-navigation {
|
|
1069
|
+
--lutra-mobile-navigation-surface: var(--lutra-component-navigation-surface);
|
|
1070
|
+
--lutra-mobile-navigation-border: var(--lutra-component-navigation-border);
|
|
1071
|
+
--lutra-mobile-navigation-border-width: var(--lutra-border-width-thin);
|
|
1072
|
+
|
|
1073
|
+
/* NO FIGMA VARIABLE. Figma draws a 390px panel. */
|
|
1074
|
+
--lutra-mobile-navigation-max-inline-size: 24.375rem;
|
|
1075
|
+
|
|
1076
|
+
/* **No shadow, deliberately.** The Figma component binds no effect style and carries no effect of
|
|
1077
|
+
* its own — unlike Tooltip, Dropdown and Dialog, which each bind one. The panel is separated by its
|
|
1078
|
+
* border and surface alone, which is consistent with the Elevation page's instruction to use
|
|
1079
|
+
* elevation "sparingly to clarify layer relationships—not as decoration". The shadow that used to
|
|
1080
|
+
* be here was invented, and is gone.
|
|
1081
|
+
*
|
|
1082
|
+
* NO FIGMA VARIABLE: the layer token does not exist, so the z-index keeps a local fallback. There
|
|
1083
|
+
* is no layer scale in Figma — elevation is expressed as effect styles, not as a z-index ramp. */
|
|
1084
|
+
--lutra-mobile-navigation-layer: var(--lutra-layer-popover, 900);
|
|
1085
|
+
|
|
1086
|
+
position: relative;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.lutra-mobile-navigation__panel {
|
|
1090
|
+
position: absolute;
|
|
1091
|
+
inset-block-start: calc(100% + var(--lutra-space-xs));
|
|
1092
|
+
|
|
1093
|
+
box-sizing: border-box;
|
|
1094
|
+
inline-size: max-content;
|
|
1095
|
+
min-inline-size: 100%;
|
|
1096
|
+
max-inline-size: min(var(--lutra-mobile-navigation-max-inline-size), 100vw);
|
|
1097
|
+
|
|
1098
|
+
display: flex;
|
|
1099
|
+
flex-direction: column;
|
|
1100
|
+
gap: var(--lutra-space-sm);
|
|
1101
|
+
padding: var(--lutra-space-md) var(--lutra-space-lg) var(--lutra-space-2xl);
|
|
1102
|
+
|
|
1103
|
+
border: var(--lutra-mobile-navigation-border-width) solid var(--lutra-mobile-navigation-border);
|
|
1104
|
+
border-radius: var(--lutra-radius-md);
|
|
1105
|
+
background-color: var(--lutra-mobile-navigation-surface);
|
|
1106
|
+
z-index: var(--lutra-mobile-navigation-layer);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/*
|
|
1110
|
+
* Which edge the panel is anchored to, following the same `data-align` convention as Tooltip.
|
|
1111
|
+
*
|
|
1112
|
+
* The panel used to be anchored to the inline start unconditionally. That is correct only when the
|
|
1113
|
+
* trigger sits at the start of its row — and the Figma `Site Header` puts it at the **end**, with the
|
|
1114
|
+
* site name at the start. A start-anchored panel hanging off a trigger that is itself a gutter's
|
|
1115
|
+
* width from the edge extends past the viewport, which forces the horizontal scrolling that WCAG
|
|
1116
|
+
* 1.4.10 exists to prevent. No story exercised an end-aligned trigger, so nothing caught it until a
|
|
1117
|
+
* real site header did.
|
|
1118
|
+
*
|
|
1119
|
+
* `start` remains the default, so this changes no existing composition.
|
|
1120
|
+
*/
|
|
1121
|
+
|
|
1122
|
+
.lutra-mobile-navigation__panel[data-align='start'] {
|
|
1123
|
+
inset-inline-start: 0;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.lutra-mobile-navigation__panel[data-align='end'] {
|
|
1127
|
+
inset-inline-end: 0;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/* The panel's own controls row. The close button sits at the end, where a thumb reaches it. */
|
|
1131
|
+
|
|
1132
|
+
.lutra-mobile-navigation__controls {
|
|
1133
|
+
display: flex;
|
|
1134
|
+
justify-content: flex-end;
|
|
1135
|
+
flex: 0 0 auto;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/* Destinations stack full width, so the whole row is the target rather than just the text. */
|
|
1139
|
+
|
|
1140
|
+
.lutra-mobile-navigation__nav {
|
|
1141
|
+
display: flex;
|
|
1142
|
+
flex-direction: column;
|
|
1143
|
+
gap: var(--lutra-space-xs);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
.lutra-mobile-navigation__nav .lutra-navigation-item {
|
|
1147
|
+
align-items: flex-start;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/* Forced-colours modes: the border is what separates the panel from the page behind it — which is
|
|
1151
|
+
* true in every mode here, since this panel has no shadow. */
|
|
1152
|
+
|
|
1153
|
+
@media (forced-colors: active) {
|
|
1154
|
+
.lutra-mobile-navigation__panel {
|
|
1155
|
+
border-color: CanvasText;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/*
|
|
1160
|
+
* Tooltip.
|
|
1161
|
+
*
|
|
1162
|
+
* Positioned with ordinary absolute positioning against an inline-block wrapper. `placement` and
|
|
1163
|
+
* `align` are data attributes; the component flips to the opposite side when the preferred one does
|
|
1164
|
+
* not fit, and that flip is the whole of the positioning logic. No floating-element engine, and no
|
|
1165
|
+
* dependency.
|
|
1166
|
+
*
|
|
1167
|
+
* The tooltip is only in the DOM while open, so there is nothing to hide and no stale
|
|
1168
|
+
* `aria-describedby` target left behind.
|
|
1169
|
+
*
|
|
1170
|
+
* There is no transition. Motion tokens are still deferred in the design system, and the 120ms open
|
|
1171
|
+
* delay in the component comes from the Figma API contract rather than from an invented scale.
|
|
1172
|
+
*/
|
|
1173
|
+
|
|
1174
|
+
.lutra-tooltip-wrapper {
|
|
1175
|
+
position: relative;
|
|
1176
|
+
display: inline-block;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.lutra-tooltip {
|
|
1180
|
+
--lutra-tooltip-surface: var(--lutra-component-tooltip-surface);
|
|
1181
|
+
--lutra-tooltip-text: var(--lutra-component-tooltip-text);
|
|
1182
|
+
--lutra-tooltip-border: var(--lutra-component-tooltip-border);
|
|
1183
|
+
--lutra-tooltip-radius: var(--lutra-radius-md);
|
|
1184
|
+
--lutra-tooltip-border-width: var(--lutra-border-width-thin);
|
|
1185
|
+
--lutra-tooltip-padding-block: var(--lutra-space-xs);
|
|
1186
|
+
--lutra-tooltip-padding-inline: var(--lutra-space-sm);
|
|
1187
|
+
|
|
1188
|
+
/* NO FIGMA VARIABLE. The 8px offset from the Figma API contract, and a maximum width so a long
|
|
1189
|
+
* tooltip wraps into a readable column instead of stretching across the viewport. */
|
|
1190
|
+
--lutra-tooltip-offset: var(--lutra-space-sm);
|
|
1191
|
+
--lutra-tooltip-max-inline-size: 20rem;
|
|
1192
|
+
|
|
1193
|
+
/* The canonical `Elevation/Medium` effect style, reproduced verbatim: 0 6px 16px black at 20%.
|
|
1194
|
+
*
|
|
1195
|
+
* NO FIGMA VARIABLE, and not an invented value either — the Elevation foundations page states that
|
|
1196
|
+
* elevation "is expressed as reusable effect styles, not numeric tokens", so there is nothing for
|
|
1197
|
+
* the token pipeline to read. Every one of the twelve Tooltip variants binds this style; the
|
|
1198
|
+
* stylesheet had no shadow at all, which is the drift this corrects. */
|
|
1199
|
+
--lutra-tooltip-shadow: 0 6px 16px rgb(0 0 0 / 20%);
|
|
1200
|
+
|
|
1201
|
+
position: absolute;
|
|
1202
|
+
z-index: var(--lutra-layer-tooltip, 950);
|
|
1203
|
+
box-sizing: border-box;
|
|
1204
|
+
inline-size: max-content;
|
|
1205
|
+
max-inline-size: var(--lutra-tooltip-max-inline-size);
|
|
1206
|
+
padding: var(--lutra-tooltip-padding-block) var(--lutra-tooltip-padding-inline);
|
|
1207
|
+
border: var(--lutra-tooltip-border-width) solid var(--lutra-tooltip-border);
|
|
1208
|
+
border-radius: var(--lutra-tooltip-radius);
|
|
1209
|
+
background-color: var(--lutra-tooltip-surface);
|
|
1210
|
+
box-shadow: var(--lutra-tooltip-shadow);
|
|
1211
|
+
color: var(--lutra-tooltip-text);
|
|
1212
|
+
font-family: var(--lutra-type-family-sans);
|
|
1213
|
+
font-size: var(--lutra-text-body-small);
|
|
1214
|
+
line-height: var(--lutra-type-line-height-body-small);
|
|
1215
|
+
|
|
1216
|
+
/* The pointer *is* allowed here, and that is deliberate. WCAG 2.2 SC 1.4.13 requires content shown
|
|
1217
|
+
* on hover to stay visible while the pointer is on it, which a surface with `pointer-events: none`
|
|
1218
|
+
* can never satisfy — the pointer would pass straight through and the tooltip would close as soon
|
|
1219
|
+
* as it left the trigger.
|
|
1220
|
+
*
|
|
1221
|
+
* Nothing is swallowed by this, because the tooltip holds nothing to interact with: it sits
|
|
1222
|
+
* outside the trigger rather than over it, and interactive content inside a tooltip warns in
|
|
1223
|
+
* development. */
|
|
1224
|
+
|
|
1225
|
+
overflow-wrap: break-word;
|
|
1226
|
+
text-wrap: pretty;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/* Placement. */
|
|
1230
|
+
|
|
1231
|
+
.lutra-tooltip[data-placement='top'] {
|
|
1232
|
+
inset-block-end: calc(100% + var(--lutra-tooltip-offset));
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.lutra-tooltip[data-placement='bottom'] {
|
|
1236
|
+
inset-block-start: calc(100% + var(--lutra-tooltip-offset));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.lutra-tooltip[data-placement='left'] {
|
|
1240
|
+
inset-inline-end: calc(100% + var(--lutra-tooltip-offset));
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.lutra-tooltip[data-placement='right'] {
|
|
1244
|
+
inset-inline-start: calc(100% + var(--lutra-tooltip-offset));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/* Alignment along the chosen side. Vertical placements align horizontally and the reverse, so the
|
|
1248
|
+
* two axes are handled separately. */
|
|
1249
|
+
|
|
1250
|
+
.lutra-tooltip[data-placement='top'][data-align='start'],
|
|
1251
|
+
.lutra-tooltip[data-placement='bottom'][data-align='start'] {
|
|
1252
|
+
inset-inline-start: 0;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
.lutra-tooltip[data-placement='top'][data-align='center'],
|
|
1256
|
+
.lutra-tooltip[data-placement='bottom'][data-align='center'] {
|
|
1257
|
+
inset-inline-start: 50%;
|
|
1258
|
+
transform: translateX(-50%);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.lutra-tooltip[data-placement='top'][data-align='end'],
|
|
1262
|
+
.lutra-tooltip[data-placement='bottom'][data-align='end'] {
|
|
1263
|
+
inset-inline-end: 0;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.lutra-tooltip[data-placement='left'][data-align='start'],
|
|
1267
|
+
.lutra-tooltip[data-placement='right'][data-align='start'] {
|
|
1268
|
+
inset-block-start: 0;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.lutra-tooltip[data-placement='left'][data-align='center'],
|
|
1272
|
+
.lutra-tooltip[data-placement='right'][data-align='center'] {
|
|
1273
|
+
inset-block-start: 50%;
|
|
1274
|
+
transform: translateY(-50%);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.lutra-tooltip[data-placement='left'][data-align='end'],
|
|
1278
|
+
.lutra-tooltip[data-placement='right'][data-align='end'] {
|
|
1279
|
+
inset-block-end: 0;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/* Forced-colours modes: the system replaces the surface and text, and the border is what keeps the
|
|
1283
|
+
* tooltip distinguishable from the page behind it. */
|
|
1284
|
+
|
|
1285
|
+
@media (forced-colors: active) {
|
|
1286
|
+
.lutra-tooltip {
|
|
1287
|
+
border-color: CanvasText;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/*
|
|
1292
|
+
* Dropdown.
|
|
1293
|
+
*
|
|
1294
|
+
* A disclosure: a button and a panel of ordinary buttons and links. Nothing here implies a menu
|
|
1295
|
+
* role, and the items are styled by class rather than by element, so a `<button>` item and an `<a>`
|
|
1296
|
+
* item are indistinguishable to look at while staying distinct to assistive technology.
|
|
1297
|
+
*
|
|
1298
|
+
* The panel is positioned absolutely against the wrapper. It needs to sit above the page, and while
|
|
1299
|
+
* Figma does define elevation, it defines it as effect styles rather than as a z-index scale — so
|
|
1300
|
+
* the shadow is canonical and the z-index falls back to a local value —
|
|
1301
|
+
* see `dropdown.md`.
|
|
1302
|
+
*/
|
|
1303
|
+
|
|
1304
|
+
.lutra-dropdown {
|
|
1305
|
+
position: relative;
|
|
1306
|
+
display: inline-block;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.lutra-dropdown__panel {
|
|
1310
|
+
--lutra-dropdown-surface: var(--lutra-component-menu-surface);
|
|
1311
|
+
--lutra-dropdown-border: var(--lutra-component-menu-border);
|
|
1312
|
+
--lutra-dropdown-radius: var(--lutra-radius-md);
|
|
1313
|
+
--lutra-dropdown-border-width: var(--lutra-border-width-thin);
|
|
1314
|
+
--lutra-dropdown-padding-block: var(--lutra-space-xs);
|
|
1315
|
+
|
|
1316
|
+
/* NO FIGMA VARIABLE. Figma draws a 240px panel. */
|
|
1317
|
+
--lutra-dropdown-min-inline-size: 15rem;
|
|
1318
|
+
|
|
1319
|
+
/* The canonical `Elevation/Medium` effect style, reproduced verbatim: 0 6px 16px black at 20%.
|
|
1320
|
+
*
|
|
1321
|
+
* NO FIGMA VARIABLE, and not an invented value either — the Elevation foundations page states that
|
|
1322
|
+
* elevation "is expressed as reusable effect styles, not numeric tokens", so there is nothing for
|
|
1323
|
+
* the token pipeline to read. The style is named here so the value stays traceable to it. */
|
|
1324
|
+
--lutra-dropdown-shadow: 0 6px 16px rgb(0 0 0 / 20%);
|
|
1325
|
+
|
|
1326
|
+
position: absolute;
|
|
1327
|
+
z-index: var(--lutra-layer-popover, 900);
|
|
1328
|
+
inset-block-start: calc(100% + var(--lutra-space-xs));
|
|
1329
|
+
inset-inline-start: 0;
|
|
1330
|
+
box-sizing: border-box;
|
|
1331
|
+
min-inline-size: var(--lutra-dropdown-min-inline-size);
|
|
1332
|
+
max-inline-size: min(20rem, calc(100vw - var(--lutra-space-2xl)));
|
|
1333
|
+
padding-block: var(--lutra-dropdown-padding-block);
|
|
1334
|
+
border: var(--lutra-dropdown-border-width) solid var(--lutra-dropdown-border);
|
|
1335
|
+
border-radius: var(--lutra-dropdown-radius);
|
|
1336
|
+
background-color: var(--lutra-dropdown-surface);
|
|
1337
|
+
box-shadow: var(--lutra-dropdown-shadow);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/* Items.
|
|
1341
|
+
*
|
|
1342
|
+
* Styled by class, not element, so a button item and a link item look identical while keeping the
|
|
1343
|
+
* semantics that tell them apart. */
|
|
1344
|
+
|
|
1345
|
+
.lutra-dropdown__item {
|
|
1346
|
+
--lutra-dropdown-item-text: var(--lutra-component-menu-item-text);
|
|
1347
|
+
--lutra-dropdown-item-hover: var(--lutra-component-menu-item-hover);
|
|
1348
|
+
--lutra-dropdown-item-danger: var(--lutra-component-menu-item-danger);
|
|
1349
|
+
--lutra-dropdown-item-disabled: var(--lutra-color-text-disabled);
|
|
1350
|
+
|
|
1351
|
+
/* NO FIGMA VARIABLE. Figma draws a 44px row, which is also the minimum pointer target. */
|
|
1352
|
+
--lutra-dropdown-item-min-height: 2.75rem;
|
|
1353
|
+
|
|
1354
|
+
box-sizing: border-box;
|
|
1355
|
+
display: flex;
|
|
1356
|
+
align-items: center;
|
|
1357
|
+
gap: var(--lutra-space-sm);
|
|
1358
|
+
inline-size: 100%;
|
|
1359
|
+
min-block-size: var(--lutra-dropdown-item-min-height);
|
|
1360
|
+
margin: 0;
|
|
1361
|
+
padding: var(--lutra-space-xs) var(--lutra-space-md);
|
|
1362
|
+
border: 0;
|
|
1363
|
+
border-radius: var(--lutra-radius-sm);
|
|
1364
|
+
background-color: transparent;
|
|
1365
|
+
color: var(--lutra-dropdown-item-text);
|
|
1366
|
+
font-family: var(--lutra-type-family-sans);
|
|
1367
|
+
font-size: var(--lutra-text-body-small);
|
|
1368
|
+
line-height: var(--lutra-type-line-height-body-small);
|
|
1369
|
+
text-align: start;
|
|
1370
|
+
text-decoration: none;
|
|
1371
|
+
cursor: pointer;
|
|
1372
|
+
appearance: none;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.lutra-dropdown__item[data-tone='danger'] {
|
|
1376
|
+
color: var(--lutra-dropdown-item-danger);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.lutra-dropdown__item:hover:not(:disabled):not([aria-disabled='true']) {
|
|
1380
|
+
background-color: var(--lutra-dropdown-item-hover);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/* Focus is a ring at the same width and offset every other Lutra control uses, drawn inside the
|
|
1384
|
+
* panel so it is not clipped by the panel's own rounded corner. */
|
|
1385
|
+
|
|
1386
|
+
.lutra-dropdown__item:focus-visible {
|
|
1387
|
+
background-color: var(--lutra-dropdown-item-hover);
|
|
1388
|
+
outline: var(--lutra-border-width-thick) solid var(--lutra-color-focus-ring);
|
|
1389
|
+
outline-offset: -2px;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.lutra-dropdown__item:disabled,
|
|
1393
|
+
.lutra-dropdown__item[aria-disabled='true'] {
|
|
1394
|
+
color: var(--lutra-dropdown-item-disabled);
|
|
1395
|
+
cursor: not-allowed;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/* Long labels wrap inside the panel rather than escaping it. */
|
|
1399
|
+
|
|
1400
|
+
.lutra-dropdown__item-label {
|
|
1401
|
+
min-inline-size: 0;
|
|
1402
|
+
overflow-wrap: break-word;
|
|
1403
|
+
text-wrap: pretty;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
.lutra-dropdown__item-icon {
|
|
1407
|
+
flex: 0 0 auto;
|
|
1408
|
+
display: inline-flex;
|
|
1409
|
+
inline-size: 1rem;
|
|
1410
|
+
block-size: 1rem;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.lutra-dropdown__item-icon > svg {
|
|
1414
|
+
inline-size: 100%;
|
|
1415
|
+
block-size: 100%;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.lutra-dropdown__separator {
|
|
1419
|
+
margin: var(--lutra-space-xs) 0;
|
|
1420
|
+
border: 0;
|
|
1421
|
+
border-block-start: var(--lutra-border-width-thin) solid var(--lutra-component-menu-divider);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/* Forced-colours modes: the system replaces the surface and the shadow is not painted, so the
|
|
1425
|
+
* border is what keeps the panel distinguishable from the page behind it. */
|
|
1426
|
+
|
|
1427
|
+
@media (forced-colors: active) {
|
|
1428
|
+
.lutra-dropdown__panel {
|
|
1429
|
+
border-color: CanvasText;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/*
|
|
1434
|
+
* Dialog.
|
|
1435
|
+
*
|
|
1436
|
+
* A native <dialog> opened with showModal(), so the top layer, the inert background, focus
|
|
1437
|
+
* containment and Escape all come from the browser. The stylesheet only has to draw it.
|
|
1438
|
+
*
|
|
1439
|
+
* The scrim is ::backdrop — the element's own pseudo-element — which is why no separate overlay node
|
|
1440
|
+
* exists and why nothing has to manage z-index against the rest of the page. The top layer sits
|
|
1441
|
+
* above everything regardless of stacking context, which is also why Dialog needs no entry in the
|
|
1442
|
+
* layer scale the design system is still missing.
|
|
1443
|
+
*/
|
|
1444
|
+
|
|
1445
|
+
.lutra-dialog {
|
|
1446
|
+
--lutra-dialog-surface: var(--lutra-component-modal-surface);
|
|
1447
|
+
--lutra-dialog-border: var(--lutra-component-modal-border);
|
|
1448
|
+
--lutra-dialog-title: var(--lutra-component-modal-title);
|
|
1449
|
+
--lutra-dialog-body: var(--lutra-component-modal-body);
|
|
1450
|
+
--lutra-dialog-divider: var(--lutra-component-modal-divider);
|
|
1451
|
+
--lutra-dialog-scrim: var(--lutra-component-modal-scrim);
|
|
1452
|
+
--lutra-dialog-radius: var(--lutra-radius-lg);
|
|
1453
|
+
--lutra-dialog-border-width: var(--lutra-border-width-thin);
|
|
1454
|
+
|
|
1455
|
+
/* NO FIGMA VARIABLE. Figma draws 400/560/720px panels and a 16px viewport gutter. */
|
|
1456
|
+
--lutra-dialog-max-inline-size-sm: 25rem;
|
|
1457
|
+
--lutra-dialog-max-inline-size-md: 35rem;
|
|
1458
|
+
--lutra-dialog-max-inline-size-lg: 45rem;
|
|
1459
|
+
--lutra-dialog-gutter: var(--lutra-space-lg);
|
|
1460
|
+
|
|
1461
|
+
/* The canonical `Elevation/Large` effect style, reproduced verbatim: 0 8px 24px black at 18%.
|
|
1462
|
+
*
|
|
1463
|
+
* NO FIGMA VARIABLE, and not an invented value either — the Elevation foundations page states that
|
|
1464
|
+
* elevation "is expressed as reusable effect styles, not numeric tokens", so there is nothing for
|
|
1465
|
+
* the token pipeline to read. The style is named here so the value stays traceable to it. */
|
|
1466
|
+
--lutra-dialog-shadow: 0 8px 24px rgb(0 0 0 / 18%);
|
|
1467
|
+
|
|
1468
|
+
box-sizing: border-box;
|
|
1469
|
+
padding: 0;
|
|
1470
|
+
border: var(--lutra-dialog-border-width) solid var(--lutra-dialog-border);
|
|
1471
|
+
border-radius: var(--lutra-dialog-radius);
|
|
1472
|
+
background-color: var(--lutra-dialog-surface);
|
|
1473
|
+
color: var(--lutra-dialog-body);
|
|
1474
|
+
font-family: var(--lutra-type-family-sans);
|
|
1475
|
+
box-shadow: var(--lutra-dialog-shadow);
|
|
1476
|
+
|
|
1477
|
+
/* Stretches to the available width on a narrow screen and never touches the viewport edge.
|
|
1478
|
+
* `dvh` so a mobile browser's collapsing toolbar does not hide the footer. */
|
|
1479
|
+
inline-size: calc(100vw - var(--lutra-dialog-gutter) * 2);
|
|
1480
|
+
max-block-size: calc(100dvh - var(--lutra-dialog-gutter) * 2);
|
|
1481
|
+
|
|
1482
|
+
/* The dialog element itself never scrolls; the body inside it does. Scrolling here as well would
|
|
1483
|
+
* let the header and footer drift out of reach. */
|
|
1484
|
+
overflow: hidden;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.lutra-dialog[data-size='sm'] {
|
|
1488
|
+
max-inline-size: var(--lutra-dialog-max-inline-size-sm);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
.lutra-dialog[data-size='md'] {
|
|
1492
|
+
max-inline-size: var(--lutra-dialog-max-inline-size-md);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.lutra-dialog[data-size='lg'] {
|
|
1496
|
+
max-inline-size: var(--lutra-dialog-max-inline-size-lg);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.lutra-dialog::backdrop {
|
|
1500
|
+
background-color: var(--lutra-dialog-scrim);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/* The dialog is focusable only as a target for `initialFocus="dialog"`. It is not a tab stop, and
|
|
1504
|
+
* the ring would be noise around the whole panel, so it is suppressed here specifically — every
|
|
1505
|
+
* control inside keeps its own. */
|
|
1506
|
+
.lutra-dialog:focus {
|
|
1507
|
+
outline: none;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/* A column: header and footer hold their size, the body takes what is left and scrolls. */
|
|
1511
|
+
|
|
1512
|
+
.lutra-dialog__panel {
|
|
1513
|
+
display: flex;
|
|
1514
|
+
flex-direction: column;
|
|
1515
|
+
max-block-size: inherit;
|
|
1516
|
+
block-size: 100%;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
.lutra-dialog__header {
|
|
1520
|
+
display: flex;
|
|
1521
|
+
align-items: flex-start;
|
|
1522
|
+
gap: var(--lutra-space-lg);
|
|
1523
|
+
flex: 0 0 auto;
|
|
1524
|
+
padding: var(--lutra-space-2xl) var(--lutra-space-2xl) var(--lutra-space-lg);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.lutra-dialog__heading-group {
|
|
1528
|
+
display: flex;
|
|
1529
|
+
flex-direction: column;
|
|
1530
|
+
gap: var(--lutra-space-sm);
|
|
1531
|
+
min-inline-size: 0;
|
|
1532
|
+
flex: 1 1 auto;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.lutra-dialog__title {
|
|
1536
|
+
margin: 0;
|
|
1537
|
+
font-size: var(--lutra-text-h2);
|
|
1538
|
+
line-height: var(--lutra-type-line-height-h2);
|
|
1539
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
1540
|
+
color: var(--lutra-dialog-title);
|
|
1541
|
+
|
|
1542
|
+
/* A long title wraps rather than pushing the close button off the panel. */
|
|
1543
|
+
overflow-wrap: break-word;
|
|
1544
|
+
text-wrap: pretty;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
.lutra-dialog__description {
|
|
1548
|
+
margin: 0;
|
|
1549
|
+
font-size: var(--lutra-text-body);
|
|
1550
|
+
line-height: var(--lutra-type-line-height-body);
|
|
1551
|
+
color: var(--lutra-dialog-body);
|
|
1552
|
+
overflow-wrap: break-word;
|
|
1553
|
+
text-wrap: pretty;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/* The only scrolling region, so the header and the actions can never drift out of reach. The
|
|
1557
|
+
* footer's border doubles as the line the content scrolls beneath. */
|
|
1558
|
+
|
|
1559
|
+
.lutra-dialog__body {
|
|
1560
|
+
flex: 1 1 auto;
|
|
1561
|
+
min-block-size: 0;
|
|
1562
|
+
overflow-y: auto;
|
|
1563
|
+
padding: 0 var(--lutra-space-2xl) var(--lutra-space-2xl);
|
|
1564
|
+
color: var(--lutra-dialog-body);
|
|
1565
|
+
font-size: var(--lutra-text-body);
|
|
1566
|
+
line-height: var(--lutra-type-line-height-body);
|
|
1567
|
+
overscroll-behavior: contain;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
.lutra-dialog__footer {
|
|
1571
|
+
display: flex;
|
|
1572
|
+
flex-wrap: wrap;
|
|
1573
|
+
justify-content: flex-end;
|
|
1574
|
+
gap: var(--lutra-space-md);
|
|
1575
|
+
flex: 0 0 auto;
|
|
1576
|
+
padding: var(--lutra-space-lg) var(--lutra-space-2xl);
|
|
1577
|
+
border-block-start: var(--lutra-border-width-thin) solid var(--lutra-dialog-divider);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/* Forced-colours modes: the scrim is not painted and the shadow disappears, so the border is what
|
|
1581
|
+
* separates the dialog from the page behind it. */
|
|
1582
|
+
|
|
1583
|
+
@media (forced-colors: active) {
|
|
1584
|
+
.lutra-dialog {
|
|
1585
|
+
border-color: CanvasText;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/*
|
|
1590
|
+
* Card and Card Footer.
|
|
1591
|
+
*
|
|
1592
|
+
* A bounded surface and its action row. Everything drawn here comes from foundations the rest of the
|
|
1593
|
+
* library already uses — a card is not a second place where type, spacing or colour get decided.
|
|
1594
|
+
*
|
|
1595
|
+
* The border is on both elevations deliberately. In dark mode `color/background/surface` and
|
|
1596
|
+
* `color/border/default` carry the whole separation from the page, and the shadow contributes almost
|
|
1597
|
+
* nothing, which is why a flat card still reads as bounded there.
|
|
1598
|
+
*/
|
|
1599
|
+
|
|
1600
|
+
.lutra-card {
|
|
1601
|
+
--lutra-card-surface: var(--lutra-color-background-surface);
|
|
1602
|
+
--lutra-card-border: var(--lutra-color-border-default);
|
|
1603
|
+
--lutra-card-border-width: var(--lutra-border-width-thin);
|
|
1604
|
+
--lutra-card-radius: var(--lutra-radius-lg);
|
|
1605
|
+
--lutra-card-padding: var(--lutra-space-2xl);
|
|
1606
|
+
--lutra-card-gap: var(--lutra-space-lg);
|
|
1607
|
+
--lutra-card-header-gap: var(--lutra-space-xs);
|
|
1608
|
+
--lutra-card-title: var(--lutra-color-text-primary);
|
|
1609
|
+
--lutra-card-text: var(--lutra-color-text-secondary);
|
|
1610
|
+
|
|
1611
|
+
/* The canonical `Elevation/Small` effect style, reproduced verbatim: 0 1px 2px black at 10%.
|
|
1612
|
+
*
|
|
1613
|
+
* NO FIGMA VARIABLE, and not an invented value either — the Elevation foundations page states that
|
|
1614
|
+
* elevation "is expressed as reusable effect styles, not numeric tokens", so there is nothing for
|
|
1615
|
+
* the token pipeline to read. The style is named here so the value stays traceable to it. */
|
|
1616
|
+
--lutra-card-shadow-raised: 0 1px 2px rgb(0 0 0 / 10%);
|
|
1617
|
+
|
|
1618
|
+
box-sizing: border-box;
|
|
1619
|
+
display: flex;
|
|
1620
|
+
flex-direction: column;
|
|
1621
|
+
gap: var(--lutra-card-gap);
|
|
1622
|
+
padding: var(--lutra-card-padding);
|
|
1623
|
+
|
|
1624
|
+
border: var(--lutra-card-border-width) solid var(--lutra-card-border);
|
|
1625
|
+
border-radius: var(--lutra-card-radius);
|
|
1626
|
+
background-color: var(--lutra-card-surface);
|
|
1627
|
+
color: var(--lutra-card-text);
|
|
1628
|
+
font-family: var(--lutra-type-family-sans);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
.lutra-card[data-elevation='raised'] {
|
|
1632
|
+
box-shadow: var(--lutra-card-shadow-raised);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/* The heading and its metadata read as one unit, so they share a block with a tighter gap than the
|
|
1636
|
+
* one between blocks. */
|
|
1637
|
+
|
|
1638
|
+
.lutra-card__header {
|
|
1639
|
+
display: flex;
|
|
1640
|
+
flex-direction: column;
|
|
1641
|
+
gap: var(--lutra-card-header-gap);
|
|
1642
|
+
min-inline-size: 0;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
.lutra-card__title {
|
|
1646
|
+
margin: 0;
|
|
1647
|
+
font-size: var(--lutra-text-h3);
|
|
1648
|
+
line-height: var(--lutra-type-line-height-h3);
|
|
1649
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
1650
|
+
color: var(--lutra-card-title);
|
|
1651
|
+
|
|
1652
|
+
/* Headings wrap rather than truncate. Truncation hides information from everyone equally,
|
|
1653
|
+
* including from assistive technology, which reads the full string while the reader cannot. */
|
|
1654
|
+
overflow-wrap: break-word;
|
|
1655
|
+
text-wrap: pretty;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
.lutra-card__supporting-text {
|
|
1659
|
+
margin: 0;
|
|
1660
|
+
font-size: var(--lutra-text-body-small);
|
|
1661
|
+
line-height: var(--lutra-type-line-height-body-small);
|
|
1662
|
+
color: var(--lutra-card-text);
|
|
1663
|
+
overflow-wrap: break-word;
|
|
1664
|
+
text-wrap: pretty;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.lutra-card__body {
|
|
1668
|
+
font-size: var(--lutra-text-body);
|
|
1669
|
+
line-height: var(--lutra-type-line-height-body);
|
|
1670
|
+
color: var(--lutra-card-text);
|
|
1671
|
+
overflow-wrap: break-word;
|
|
1672
|
+
text-wrap: pretty;
|
|
1673
|
+
|
|
1674
|
+
/* Grows with its content. Nothing here depends on a particular string length, and cards in a row
|
|
1675
|
+
* are equal width with independent height. */
|
|
1676
|
+
min-inline-size: 0;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/* The action row.
|
|
1680
|
+
*
|
|
1681
|
+
* `align` sets `justify-content` and nothing else. There is no `row-reverse`: it would put focus
|
|
1682
|
+
* order out of step with reading order, which is a defect that looks perfectly correct on screen. */
|
|
1683
|
+
|
|
1684
|
+
.lutra-card-footer {
|
|
1685
|
+
--lutra-card-footer-gap: var(--lutra-space-md);
|
|
1686
|
+
|
|
1687
|
+
display: flex;
|
|
1688
|
+
flex-wrap: wrap;
|
|
1689
|
+
align-items: center;
|
|
1690
|
+
gap: var(--lutra-card-footer-gap);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.lutra-card-footer[data-align='start'] {
|
|
1694
|
+
justify-content: flex-start;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.lutra-card-footer[data-align='end'] {
|
|
1698
|
+
justify-content: flex-end;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/* Forced-colours modes: the shadow is not painted, so the border is the whole of the boundary. */
|
|
1702
|
+
|
|
1703
|
+
@media (forced-colors: active) {
|
|
1704
|
+
.lutra-card {
|
|
1705
|
+
border-color: CanvasText;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/*
|
|
1710
|
+
* Link Card.
|
|
1711
|
+
*
|
|
1712
|
+
* The same surface as Card, with one anchor whose pseudo-element covers it. The card is the
|
|
1713
|
+
* positioning context; the anchor is the only interactive thing in it.
|
|
1714
|
+
*
|
|
1715
|
+
* Hover and focus are selected from the platform — `:hover` and `:has(:focus-visible)` — never from a
|
|
1716
|
+
* prop, and both change more than colour so the state survives greyscale.
|
|
1717
|
+
*/
|
|
1718
|
+
|
|
1719
|
+
.lutra-link-card {
|
|
1720
|
+
--lutra-link-card-surface: var(--lutra-color-background-surface);
|
|
1721
|
+
--lutra-link-card-border: var(--lutra-color-border-default);
|
|
1722
|
+
--lutra-link-card-border-hover: var(--lutra-color-border-strong);
|
|
1723
|
+
--lutra-link-card-border-width: var(--lutra-border-width-thin);
|
|
1724
|
+
--lutra-link-card-radius: var(--lutra-radius-lg);
|
|
1725
|
+
--lutra-link-card-padding: var(--lutra-space-2xl);
|
|
1726
|
+
--lutra-link-card-gap: var(--lutra-space-lg);
|
|
1727
|
+
--lutra-link-card-row-gap: var(--lutra-space-sm);
|
|
1728
|
+
--lutra-link-card-text: var(--lutra-component-link-text-default);
|
|
1729
|
+
--lutra-link-card-text-hover: var(--lutra-component-link-text-hover);
|
|
1730
|
+
--lutra-link-card-supporting-text: var(--lutra-color-text-secondary);
|
|
1731
|
+
--lutra-link-card-focus-ring-color: var(--lutra-color-focus-ring);
|
|
1732
|
+
--lutra-link-card-focus-ring-width: var(--lutra-border-width-thick);
|
|
1733
|
+
|
|
1734
|
+
/* The canonical `Elevation/Small` effect style, reproduced verbatim: 0 1px 2px black at 10%. Same
|
|
1735
|
+
* value and same reasoning as Card's raised elevation.
|
|
1736
|
+
*
|
|
1737
|
+
* NO FIGMA VARIABLE, and not an invented value either — the Elevation foundations page states that
|
|
1738
|
+
* elevation "is expressed as reusable effect styles, not numeric tokens", so there is nothing for
|
|
1739
|
+
* the token pipeline to read. The style is named here so the value stays traceable to it. */
|
|
1740
|
+
--lutra-link-card-shadow-hover: 0 1px 2px rgb(0 0 0 / 10%);
|
|
1741
|
+
|
|
1742
|
+
/* The positioning context the stretched pseudo-element resolves against. */
|
|
1743
|
+
position: relative;
|
|
1744
|
+
|
|
1745
|
+
box-sizing: border-box;
|
|
1746
|
+
display: flex;
|
|
1747
|
+
flex-direction: column;
|
|
1748
|
+
gap: var(--lutra-link-card-gap);
|
|
1749
|
+
padding: var(--lutra-link-card-padding);
|
|
1750
|
+
|
|
1751
|
+
border: var(--lutra-link-card-border-width) solid var(--lutra-link-card-border);
|
|
1752
|
+
border-radius: var(--lutra-link-card-radius);
|
|
1753
|
+
background-color: var(--lutra-link-card-surface);
|
|
1754
|
+
font-family: var(--lutra-type-family-sans);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/* The heading holds the link and the chevron on one row. It is a heading, so it keeps its margin
|
|
1758
|
+
* reset and its own type — the level it renders at is a prop, and never implied by this size. */
|
|
1759
|
+
|
|
1760
|
+
.lutra-link-card__heading {
|
|
1761
|
+
display: flex;
|
|
1762
|
+
align-items: flex-start;
|
|
1763
|
+
gap: var(--lutra-link-card-row-gap);
|
|
1764
|
+
margin: 0;
|
|
1765
|
+
font-size: var(--lutra-text-h3);
|
|
1766
|
+
line-height: var(--lutra-type-line-height-h3);
|
|
1767
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
1768
|
+
min-inline-size: 0;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
.lutra-link-card__link {
|
|
1772
|
+
color: var(--lutra-link-card-text);
|
|
1773
|
+
text-decoration-line: none;
|
|
1774
|
+
min-inline-size: 0;
|
|
1775
|
+
|
|
1776
|
+
/* Headings wrap rather than truncate, here as everywhere. */
|
|
1777
|
+
overflow-wrap: break-word;
|
|
1778
|
+
text-wrap: pretty;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/* What makes the whole surface clickable without putting the whole surface inside the anchor. The
|
|
1782
|
+
* anchor keeps its own small box — and therefore its own accessible name — while this covers the
|
|
1783
|
+
* card. */
|
|
1784
|
+
.lutra-link-card__link::after {
|
|
1785
|
+
content: '';
|
|
1786
|
+
position: absolute;
|
|
1787
|
+
inset: 0;
|
|
1788
|
+
border-radius: inherit;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
.lutra-link-card__chevron {
|
|
1792
|
+
/* Holds its size beside a wrapping heading, and aligns with the first line rather than centring
|
|
1793
|
+
* against a heading that may be three lines tall. */
|
|
1794
|
+
flex: 0 0 auto;
|
|
1795
|
+
inline-size: 1.5rem;
|
|
1796
|
+
block-size: 1.5rem;
|
|
1797
|
+
margin-block-start: 0.125rem;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
.lutra-link-card__supporting-text {
|
|
1801
|
+
margin: 0;
|
|
1802
|
+
font-size: var(--lutra-text-body-small);
|
|
1803
|
+
line-height: var(--lutra-type-line-height-body-small);
|
|
1804
|
+
font-weight: var(--lutra-type-weight-regular);
|
|
1805
|
+
color: var(--lutra-link-card-supporting-text);
|
|
1806
|
+
overflow-wrap: break-word;
|
|
1807
|
+
text-wrap: pretty;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/* Hover raises the card, strengthens the border and underlines the link text. Selected on the card
|
|
1811
|
+
* rather than the anchor, because the pseudo-element means the pointer is over the card whenever it
|
|
1812
|
+
* is over the link. */
|
|
1813
|
+
|
|
1814
|
+
.lutra-link-card:hover {
|
|
1815
|
+
border-color: var(--lutra-link-card-border-hover);
|
|
1816
|
+
box-shadow: var(--lutra-link-card-shadow-hover);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.lutra-link-card:hover .lutra-link-card__link {
|
|
1820
|
+
color: var(--lutra-link-card-text-hover);
|
|
1821
|
+
text-decoration-line: underline;
|
|
1822
|
+
text-decoration-thickness: from-font;
|
|
1823
|
+
text-underline-offset: 0.2em;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/* Focus. Drawn on the card, because a ring around the anchor's own small box would not describe what
|
|
1827
|
+
* activating it does.
|
|
1828
|
+
*
|
|
1829
|
+
* Figma thickens the 1px border to a 2px stroke. An outline is used instead so the ring can appear
|
|
1830
|
+
* without a 1px layout shift that would nudge every card in a grid; it reads identically and keeps
|
|
1831
|
+
* the weight change the annotation asks for. `:focus-visible` so it answers the keyboard rather than
|
|
1832
|
+
* the pointer. */
|
|
1833
|
+
|
|
1834
|
+
.lutra-link-card:has(.lutra-link-card__link:focus-visible) {
|
|
1835
|
+
outline: var(--lutra-link-card-focus-ring-width) solid var(--lutra-link-card-focus-ring-color);
|
|
1836
|
+
outline-offset: 0;
|
|
1837
|
+
border-color: var(--lutra-link-card-focus-ring-color);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
.lutra-link-card:has(.lutra-link-card__link:focus-visible) .lutra-link-card__link {
|
|
1841
|
+
color: var(--lutra-link-card-text-hover);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/* Forced-colours modes: the shadow is not painted and system colours replace the rest, so the border
|
|
1845
|
+
* carries the boundary and the ring is pinned to the system highlight. */
|
|
1846
|
+
|
|
1847
|
+
@media (forced-colors: active) {
|
|
1848
|
+
.lutra-link-card {
|
|
1849
|
+
border-color: CanvasText;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
.lutra-link-card:has(.lutra-link-card__link:focus-visible) {
|
|
1853
|
+
outline-color: Highlight;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/*
|
|
1858
|
+
* Table.
|
|
1859
|
+
*
|
|
1860
|
+
* Native table semantics throughout; this stylesheet only draws them. Every part carries its own
|
|
1861
|
+
* class rather than being reached through an element selector, which is why `TableRow` exists as a
|
|
1862
|
+
* component at all — the divider, the hover tint and the selected tint have to hang off something
|
|
1863
|
+
* prefixed with `lutra-`.
|
|
1864
|
+
*
|
|
1865
|
+
* Row dividers use `color/border/default` rather than `color/border/subtle`. Default carries more of
|
|
1866
|
+
* the separation in dark mode, where the surface tints sit close together, and the Table pattern says
|
|
1867
|
+
* so explicitly.
|
|
1868
|
+
*/
|
|
1869
|
+
|
|
1870
|
+
.lutra-table-wrapper {
|
|
1871
|
+
--lutra-table-surface: var(--lutra-color-background-surface);
|
|
1872
|
+
--lutra-table-header-surface: var(--lutra-color-background-subtle);
|
|
1873
|
+
--lutra-table-row-hover: var(--lutra-color-background-subtle);
|
|
1874
|
+
--lutra-table-row-selected: var(--lutra-color-background-subtle);
|
|
1875
|
+
--lutra-table-border: var(--lutra-color-border-default);
|
|
1876
|
+
--lutra-table-border-width: var(--lutra-border-width-thin);
|
|
1877
|
+
--lutra-table-radius: var(--lutra-radius-md);
|
|
1878
|
+
--lutra-table-text: var(--lutra-color-text-primary);
|
|
1879
|
+
--lutra-table-text-muted: var(--lutra-color-text-secondary);
|
|
1880
|
+
--lutra-table-icon-muted: var(--lutra-color-text-muted);
|
|
1881
|
+
--lutra-table-padding-block: var(--lutra-space-md);
|
|
1882
|
+
--lutra-table-padding-inline: var(--lutra-space-lg);
|
|
1883
|
+
--lutra-table-focus-ring-color: var(--lutra-color-focus-ring);
|
|
1884
|
+
--lutra-table-focus-ring-width: var(--lutra-border-width-thick);
|
|
1885
|
+
|
|
1886
|
+
/* NO FIGMA VARIABLE. The 44px pointer target the whole library holds to, expressed here as a hit
|
|
1887
|
+
* area that grows into the cell padding rather than as a taller row — Figma draws the dashed 44px
|
|
1888
|
+
* outlines overlapping a 52px row for exactly this reason. */
|
|
1889
|
+
--lutra-table-hit-target: 2.75rem;
|
|
1890
|
+
|
|
1891
|
+
display: flex;
|
|
1892
|
+
flex-direction: column;
|
|
1893
|
+
font-family: var(--lutra-type-family-sans);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/* The result count after a sort, filter or page change. Visible as well as announced — a count is
|
|
1897
|
+
* useful to everyone, and a live region nobody can see is easy to forget to maintain.
|
|
1898
|
+
*
|
|
1899
|
+
* The node is always in the DOM, because a live region only announces changes to a region that was
|
|
1900
|
+
* already there. Empty, it has no text and therefore no line box, so it draws nothing; the spacing
|
|
1901
|
+
* below it is applied only when it has content, which is why the wrapper carries no gap. It is never
|
|
1902
|
+
* hidden with `display: none` — that would remove it from the accessibility tree and silence it. */
|
|
1903
|
+
|
|
1904
|
+
.lutra-table__status {
|
|
1905
|
+
font-size: var(--lutra-text-body-small);
|
|
1906
|
+
line-height: var(--lutra-type-line-height-body-small);
|
|
1907
|
+
color: var(--lutra-table-text-muted);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
.lutra-table__status:not(:empty) {
|
|
1911
|
+
margin-block-end: var(--lutra-space-sm);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/* The scroll region. Focusable only while it overflows, which is when there is something to reach. */
|
|
1915
|
+
|
|
1916
|
+
.lutra-table__scroll {
|
|
1917
|
+
overflow-x: auto;
|
|
1918
|
+
border: var(--lutra-table-border-width) solid var(--lutra-table-border);
|
|
1919
|
+
border-radius: var(--lutra-table-radius);
|
|
1920
|
+
background-color: var(--lutra-table-surface);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
.lutra-table__scroll:focus-visible {
|
|
1924
|
+
outline: var(--lutra-table-focus-ring-width) solid var(--lutra-table-focus-ring-color);
|
|
1925
|
+
outline-offset: 2px;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
.lutra-table {
|
|
1929
|
+
inline-size: 100%;
|
|
1930
|
+
|
|
1931
|
+
/* Collapsed, so a row's divider is one line rather than two touching borders. */
|
|
1932
|
+
border-collapse: collapse;
|
|
1933
|
+
color: var(--lutra-table-text);
|
|
1934
|
+
font-size: var(--lutra-text-body-small);
|
|
1935
|
+
line-height: var(--lutra-type-line-height-body-small);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
.lutra-table__caption {
|
|
1939
|
+
padding: var(--lutra-table-padding-block) var(--lutra-table-padding-inline);
|
|
1940
|
+
color: var(--lutra-table-text-muted);
|
|
1941
|
+
text-align: start;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/* Visually hidden, but present for assistive technology. Scoped to the table rather than shipped as
|
|
1945
|
+
* a global utility — the stylesheet does not get to add global classes to a consuming page. */
|
|
1946
|
+
|
|
1947
|
+
.lutra-table__visually-hidden {
|
|
1948
|
+
position: absolute;
|
|
1949
|
+
inline-size: 1px;
|
|
1950
|
+
block-size: 1px;
|
|
1951
|
+
margin: -1px;
|
|
1952
|
+
padding: 0;
|
|
1953
|
+
overflow: hidden;
|
|
1954
|
+
white-space: nowrap;
|
|
1955
|
+
clip-path: inset(50%);
|
|
1956
|
+
border: 0;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/* Rows. The divider sits on the row, so it spans the full width regardless of how many cells there
|
|
1960
|
+
* are. */
|
|
1961
|
+
|
|
1962
|
+
.lutra-table-row {
|
|
1963
|
+
border-block-start: var(--lutra-table-border-width) solid var(--lutra-table-border);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
.lutra-table-row:hover {
|
|
1967
|
+
background-color: var(--lutra-table-row-hover);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/* The tint is supplemental. The checked box in the selection cell is the primary cue, which is what
|
|
1971
|
+
* keeps selection legible in greyscale, in high contrast and on paper. */
|
|
1972
|
+
|
|
1973
|
+
.lutra-table-row[data-selected='true'] {
|
|
1974
|
+
background-color: var(--lutra-table-row-selected);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/* Header cells. */
|
|
1978
|
+
|
|
1979
|
+
.lutra-table-header-cell {
|
|
1980
|
+
padding: var(--lutra-table-padding-block) var(--lutra-table-padding-inline);
|
|
1981
|
+
background-color: var(--lutra-table-header-surface);
|
|
1982
|
+
color: var(--lutra-table-text);
|
|
1983
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
1984
|
+
text-align: start;
|
|
1985
|
+
vertical-align: middle;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
.lutra-table-header-cell[data-align='right'] {
|
|
1989
|
+
text-align: end;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/* A sortable header holds a button. The button fills the cell so the whole header is the target,
|
|
1993
|
+
* while remaining a real button rather than a clickable `th`. */
|
|
1994
|
+
|
|
1995
|
+
.lutra-table-header-cell__button {
|
|
1996
|
+
display: inline-flex;
|
|
1997
|
+
align-items: center;
|
|
1998
|
+
gap: var(--lutra-space-sm);
|
|
1999
|
+
inline-size: 100%;
|
|
2000
|
+
margin: 0;
|
|
2001
|
+
padding: 0;
|
|
2002
|
+
border: 0;
|
|
2003
|
+
border-radius: var(--lutra-radius-sm);
|
|
2004
|
+
background: none;
|
|
2005
|
+
color: inherit;
|
|
2006
|
+
font: inherit;
|
|
2007
|
+
text-align: inherit;
|
|
2008
|
+
cursor: pointer;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
.lutra-table-header-cell[data-align='right'] .lutra-table-header-cell__button {
|
|
2012
|
+
justify-content: flex-end;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
.lutra-table-header-cell__button:focus-visible {
|
|
2016
|
+
outline: var(--lutra-table-focus-ring-width) solid var(--lutra-table-focus-ring-color);
|
|
2017
|
+
outline-offset: 2px;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
.lutra-table-header-cell__sort-icon {
|
|
2021
|
+
flex: 0 0 auto;
|
|
2022
|
+
inline-size: 1rem;
|
|
2023
|
+
block-size: 1rem;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/* Unsorted is quieter than a live sort direction, but the glyph differs in all three states, so the
|
|
2027
|
+
* colour is never what carries the direction. */
|
|
2028
|
+
|
|
2029
|
+
.lutra-table-header-cell__sort-icon[data-state='unsorted'] {
|
|
2030
|
+
color: var(--lutra-table-icon-muted);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/* Value cells. */
|
|
2034
|
+
|
|
2035
|
+
.lutra-table-cell {
|
|
2036
|
+
padding: var(--lutra-table-padding-block) var(--lutra-table-padding-inline);
|
|
2037
|
+
text-align: start;
|
|
2038
|
+
vertical-align: top;
|
|
2039
|
+
overflow-wrap: break-word;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
.lutra-table-cell[data-emphasis='strong'] {
|
|
2043
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
2044
|
+
color: var(--lutra-table-text);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
.lutra-table-cell[data-emphasis='muted'] {
|
|
2048
|
+
color: var(--lutra-table-text-muted);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/* Right-aligned cells are numeric by convention, and numbers only line up column-wise in tabular
|
|
2052
|
+
* figures. Figma cannot express the OpenType feature, so the design depends on this rule existing. */
|
|
2053
|
+
|
|
2054
|
+
.lutra-table-cell[data-align='right'] {
|
|
2055
|
+
text-align: end;
|
|
2056
|
+
font-variant-numeric: tabular-nums;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/* Selection cells. */
|
|
2060
|
+
|
|
2061
|
+
.lutra-table-selection-cell {
|
|
2062
|
+
inline-size: 1px;
|
|
2063
|
+
padding: var(--lutra-table-padding-block) var(--lutra-table-padding-inline);
|
|
2064
|
+
vertical-align: top;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
.lutra-table-selection-cell__label {
|
|
2068
|
+
display: inline-flex;
|
|
2069
|
+
align-items: center;
|
|
2070
|
+
|
|
2071
|
+
/* The label is the 44px target for the checkbox inside it, without changing the drawn control. */
|
|
2072
|
+
min-inline-size: var(--lutra-table-hit-target);
|
|
2073
|
+
min-block-size: var(--lutra-table-hit-target);
|
|
2074
|
+
margin-block: calc((var(--lutra-table-hit-target) - 1.5rem) / -2);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/* Row actions.
|
|
2078
|
+
*
|
|
2079
|
+
* The buttons stay compact so rows stay dense; the pointer target grows into the cell padding
|
|
2080
|
+
* instead of adding to the row height. */
|
|
2081
|
+
|
|
2082
|
+
.lutra-table-row-actions {
|
|
2083
|
+
padding: var(--lutra-space-sm) var(--lutra-table-padding-inline);
|
|
2084
|
+
text-align: end;
|
|
2085
|
+
vertical-align: middle;
|
|
2086
|
+
white-space: nowrap;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
.lutra-table-row-actions__row {
|
|
2090
|
+
display: flex;
|
|
2091
|
+
align-items: center;
|
|
2092
|
+
justify-content: flex-end;
|
|
2093
|
+
gap: var(--lutra-space-sm);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
.lutra-table-row-actions__row > * {
|
|
2097
|
+
position: relative;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/* The invisible 44 × 44px target, centred on the control and overlapping the row padding. The row
|
|
2101
|
+
* below stays its own height; only the hit area grows. */
|
|
2102
|
+
|
|
2103
|
+
.lutra-table-row-actions__row > *::before {
|
|
2104
|
+
content: '';
|
|
2105
|
+
position: absolute;
|
|
2106
|
+
inset-block-start: 50%;
|
|
2107
|
+
inset-inline-start: 50%;
|
|
2108
|
+
translate: -50% -50%;
|
|
2109
|
+
min-inline-size: var(--lutra-table-hit-target);
|
|
2110
|
+
min-block-size: var(--lutra-table-hit-target);
|
|
2111
|
+
inline-size: 100%;
|
|
2112
|
+
block-size: 100%;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/* Empty state. The structure around it stays; only the rows are gone. */
|
|
2116
|
+
|
|
2117
|
+
.lutra-table-empty-state__cell {
|
|
2118
|
+
padding: var(--lutra-space-3xl) var(--lutra-table-padding-inline);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
.lutra-table-empty-state__content {
|
|
2122
|
+
display: flex;
|
|
2123
|
+
flex-direction: column;
|
|
2124
|
+
align-items: center;
|
|
2125
|
+
gap: var(--lutra-space-md);
|
|
2126
|
+
text-align: center;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
.lutra-table-empty-state__icon {
|
|
2130
|
+
inline-size: 1.5rem;
|
|
2131
|
+
block-size: 1.5rem;
|
|
2132
|
+
color: var(--lutra-table-icon-muted);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
.lutra-table-empty-state__title {
|
|
2136
|
+
margin: 0;
|
|
2137
|
+
font-size: var(--lutra-text-body);
|
|
2138
|
+
line-height: var(--lutra-type-line-height-body);
|
|
2139
|
+
color: var(--lutra-table-text);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
.lutra-table-empty-state__body {
|
|
2143
|
+
margin: 0;
|
|
2144
|
+
color: var(--lutra-table-text-muted);
|
|
2145
|
+
text-wrap: pretty;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/* Forced-colours modes: tints are not painted, so the dividers and the border carry the structure,
|
|
2149
|
+
* and the focus ring is pinned to the system highlight. */
|
|
2150
|
+
|
|
2151
|
+
@media (forced-colors: active) {
|
|
2152
|
+
.lutra-table-row,
|
|
2153
|
+
.lutra-table__scroll {
|
|
2154
|
+
border-color: CanvasText;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
.lutra-table__scroll:focus-visible,
|
|
2158
|
+
.lutra-table-header-cell__button:focus-visible {
|
|
2159
|
+
outline-color: Highlight;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/*
|
|
2164
|
+
* Input.
|
|
2165
|
+
*
|
|
2166
|
+
* The public styling contract is the `lutra-input` class plus the `data-size` and
|
|
2167
|
+
* `data-full-width` attributes, and `aria-invalid` for the error state. Hover,
|
|
2168
|
+
* focus and disabled are selected from the platform — `:hover`, `:focus-visible`,
|
|
2169
|
+
* `:disabled` — never from a prop.
|
|
2170
|
+
*
|
|
2171
|
+
* Colour comes from Figma's `component/input/*` alias layer, so the design system
|
|
2172
|
+
* decides what an input surface, its text, its placeholder and each of its border
|
|
2173
|
+
* states are. Sizes, spacing and type come from the core scales. The values with
|
|
2174
|
+
* no Figma variable behind them are marked.
|
|
2175
|
+
*
|
|
2176
|
+
* The error state is styled from `aria-invalid` rather than a `data-*` attribute
|
|
2177
|
+
* of its own. There is then no way for the field to look wrong while reporting
|
|
2178
|
+
* itself valid, because the attribute that assistive technology reads is the same
|
|
2179
|
+
* one the stylesheet matches on.
|
|
2180
|
+
*/
|
|
2181
|
+
|
|
2182
|
+
.lutra-input {
|
|
2183
|
+
/* Surface and text. */
|
|
2184
|
+
--lutra-input-surface: var(--lutra-component-input-surface);
|
|
2185
|
+
--lutra-input-text: var(--lutra-component-input-text);
|
|
2186
|
+
--lutra-input-placeholder: var(--lutra-component-input-placeholder);
|
|
2187
|
+
|
|
2188
|
+
/* Border, one token per state. */
|
|
2189
|
+
--lutra-input-border: var(--lutra-component-input-border-default);
|
|
2190
|
+
--lutra-input-border-hover: var(--lutra-component-input-border-hover);
|
|
2191
|
+
--lutra-input-border-focus: var(--lutra-component-input-border-focus);
|
|
2192
|
+
--lutra-input-border-invalid: var(--lutra-component-input-border-invalid);
|
|
2193
|
+
|
|
2194
|
+
/* Disabled.
|
|
2195
|
+
*
|
|
2196
|
+
* NO `component/input/*` ALIAS. Figma binds the core disabled tokens directly
|
|
2197
|
+
* here — the Input alias layer has surface, text, placeholder and four border
|
|
2198
|
+
* states, but no disabled trio. These are the tokens the Figma component
|
|
2199
|
+
* actually uses, so they are what this reproduces; the gap is recorded in
|
|
2200
|
+
* `input.md` as something for the design system to close. */
|
|
2201
|
+
--lutra-input-disabled-surface: var(--lutra-color-background-disabled);
|
|
2202
|
+
--lutra-input-disabled-border: var(--lutra-color-border-disabled);
|
|
2203
|
+
--lutra-input-disabled-text: var(--lutra-color-text-disabled);
|
|
2204
|
+
|
|
2205
|
+
/* Shape and type. */
|
|
2206
|
+
--lutra-input-radius: var(--lutra-radius-md);
|
|
2207
|
+
--lutra-input-border-width: var(--lutra-border-width-thin);
|
|
2208
|
+
--lutra-input-border-width-focus: var(--lutra-border-width-thick);
|
|
2209
|
+
--lutra-input-font-family: var(--lutra-type-family-sans);
|
|
2210
|
+
--lutra-input-font-weight: var(--lutra-type-weight-medium);
|
|
2211
|
+
|
|
2212
|
+
/* Per size: 8px, 12px, 16px of inline padding. */
|
|
2213
|
+
--lutra-input-padding-inline-sm: var(--lutra-space-sm);
|
|
2214
|
+
--lutra-input-padding-inline-md: var(--lutra-space-md);
|
|
2215
|
+
--lutra-input-padding-inline-lg: var(--lutra-space-lg);
|
|
2216
|
+
|
|
2217
|
+
/* 14px, 16px, 18px. */
|
|
2218
|
+
--lutra-input-font-size-sm: var(--lutra-text-body-small);
|
|
2219
|
+
--lutra-input-font-size-md: var(--lutra-text-body);
|
|
2220
|
+
--lutra-input-font-size-lg: var(--lutra-text-h3);
|
|
2221
|
+
|
|
2222
|
+
/* The leading paired with each size in the type ramp. Figma draws a flat 120%
|
|
2223
|
+
* on this component with no variable behind it; inside a fixed-height control
|
|
2224
|
+
* the two are visually identical, and using the ramp keeps the field consistent
|
|
2225
|
+
* with every other piece of text at the same size. */
|
|
2226
|
+
--lutra-input-line-height-sm: var(--lutra-type-line-height-body-small);
|
|
2227
|
+
--lutra-input-line-height-md: var(--lutra-type-line-height-body);
|
|
2228
|
+
--lutra-input-line-height-lg: var(--lutra-type-line-height-h3);
|
|
2229
|
+
|
|
2230
|
+
/* NO FIGMA VARIABLE. Figma draws inputs at a fixed 36/40/44px; these become
|
|
2231
|
+
* `min-block-size` instead, because a fixed height on a text-bearing control
|
|
2232
|
+
* breaks at 200% zoom. `rem` so they grow with the reader's font size. */
|
|
2233
|
+
--lutra-input-min-height-sm: 2.25rem;
|
|
2234
|
+
--lutra-input-min-height-md: 2.5rem;
|
|
2235
|
+
--lutra-input-min-height-lg: 2.75rem;
|
|
2236
|
+
|
|
2237
|
+
/* NO FIGMA VARIABLE. Figma centres the value inside a fixed height; this is the
|
|
2238
|
+
* vertical breathing room the text needs once the field is free to grow. */
|
|
2239
|
+
--lutra-input-padding-block: var(--lutra-space-2xs);
|
|
2240
|
+
|
|
2241
|
+
box-sizing: border-box;
|
|
2242
|
+
inline-size: 100%;
|
|
2243
|
+
max-inline-size: 100%;
|
|
2244
|
+
margin: 0;
|
|
2245
|
+
padding-block: var(--lutra-input-padding-block);
|
|
2246
|
+
border-style: solid;
|
|
2247
|
+
border-width: var(--lutra-input-border-width);
|
|
2248
|
+
border-color: var(--lutra-input-border);
|
|
2249
|
+
border-radius: var(--lutra-input-radius);
|
|
2250
|
+
background-color: var(--lutra-input-surface);
|
|
2251
|
+
color: var(--lutra-input-text);
|
|
2252
|
+
font-family: var(--lutra-input-font-family);
|
|
2253
|
+
font-weight: var(--lutra-input-font-weight);
|
|
2254
|
+
|
|
2255
|
+
/* Strips the platform's own field chrome — notably iOS Safari's inner shadow
|
|
2256
|
+
* and rounded corners — so the token values are what shows. */
|
|
2257
|
+
appearance: none;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/* Sizes.
|
|
2261
|
+
*
|
|
2262
|
+
* `min-block-size` rather than `height`: it reproduces the Figma height at the
|
|
2263
|
+
* default font size and lets the field grow when the reader raises theirs. */
|
|
2264
|
+
|
|
2265
|
+
.lutra-input[data-size='sm'] {
|
|
2266
|
+
min-block-size: var(--lutra-input-min-height-sm);
|
|
2267
|
+
padding-inline: var(--lutra-input-padding-inline-sm);
|
|
2268
|
+
font-size: var(--lutra-input-font-size-sm);
|
|
2269
|
+
line-height: var(--lutra-input-line-height-sm);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
.lutra-input[data-size='md'] {
|
|
2273
|
+
min-block-size: var(--lutra-input-min-height-md);
|
|
2274
|
+
padding-inline: var(--lutra-input-padding-inline-md);
|
|
2275
|
+
font-size: var(--lutra-input-font-size-md);
|
|
2276
|
+
line-height: var(--lutra-input-line-height-md);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
.lutra-input[data-size='lg'] {
|
|
2280
|
+
min-block-size: var(--lutra-input-min-height-lg);
|
|
2281
|
+
padding-inline: var(--lutra-input-padding-inline-lg);
|
|
2282
|
+
font-size: var(--lutra-input-font-size-lg);
|
|
2283
|
+
line-height: var(--lutra-input-line-height-lg);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
/* Placeholder.
|
|
2287
|
+
*
|
|
2288
|
+
* A different token from the value, so an empty field and a filled one are
|
|
2289
|
+
* distinguishable at a glance. Before this distinction existed both resolved to
|
|
2290
|
+
* the same colour. It is an emphasis difference only — the placeholder still
|
|
2291
|
+
* measures above 4.5:1 on both surfaces, because a placeholder is text and
|
|
2292
|
+
* dimming it below AA to sharpen the contrast between the two is not a trade this
|
|
2293
|
+
* library makes. `opacity: 1` overrides the faded default Firefox applies. */
|
|
2294
|
+
|
|
2295
|
+
.lutra-input::placeholder {
|
|
2296
|
+
color: var(--lutra-input-placeholder);
|
|
2297
|
+
opacity: 1;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/* Interaction states. */
|
|
2301
|
+
|
|
2302
|
+
.lutra-input:hover:not(:disabled) {
|
|
2303
|
+
border-color: var(--lutra-input-border-hover);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
/* Focus.
|
|
2307
|
+
*
|
|
2308
|
+
* Input draws focus by thickening its own border rather than adding a separate
|
|
2309
|
+
* ring — a deliberate difference from Button, using the same `color/focus/ring`
|
|
2310
|
+
* source. The thickening is not decoration: the focus colour against the resting
|
|
2311
|
+
* border is only about 1.1:1, so a colour change alone would not be a perceivable
|
|
2312
|
+
* indicator. The added pixel replaces surface, where the focus colour reaches
|
|
2313
|
+
* roughly 3.8:1 on light and clears the 3:1 that WCAG 2.4.11 asks of the changed
|
|
2314
|
+
* area. Removing the thickening would break the indicator.
|
|
2315
|
+
*
|
|
2316
|
+
* That pixel is added as an inset shadow rather than by raising `border-width`,
|
|
2317
|
+
* so the value does not shift sideways when the field takes focus.
|
|
2318
|
+
*
|
|
2319
|
+
* The native outline is replaced, not removed — and `forced-colors` below covers
|
|
2320
|
+
* the mode where a shadow is not painted at all. */
|
|
2321
|
+
|
|
2322
|
+
.lutra-input:focus-visible {
|
|
2323
|
+
border-color: var(--lutra-input-border-focus);
|
|
2324
|
+
box-shadow: inset 0 0 0
|
|
2325
|
+
calc(var(--lutra-input-border-width-focus) - var(--lutra-input-border-width))
|
|
2326
|
+
var(--lutra-input-border-focus);
|
|
2327
|
+
outline: none;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/* Invalid.
|
|
2331
|
+
*
|
|
2332
|
+
* Matched on `aria-invalid` so the look and the announced state are the same
|
|
2333
|
+
* fact. Focus is ordered after this on purpose: a focused invalid field shows the
|
|
2334
|
+
* focus indicator, because losing track of focus is the more serious failure and
|
|
2335
|
+
* the error is still announced by `aria-invalid` and carried by the message the
|
|
2336
|
+
* field wrapper renders. Figma does not draw the focused-and-invalid combination;
|
|
2337
|
+
* this is recorded in `input.md`. */
|
|
2338
|
+
|
|
2339
|
+
.lutra-input[aria-invalid='true'] {
|
|
2340
|
+
border-color: var(--lutra-input-border-invalid);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
.lutra-input[aria-invalid='true']:focus-visible {
|
|
2344
|
+
border-color: var(--lutra-input-border-focus);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/* Disabled.
|
|
2348
|
+
*
|
|
2349
|
+
* Deliberate colours rather than an opacity wash, so the state is a real visual
|
|
2350
|
+
* change. It is never the only signal: the field is natively disabled, so it is
|
|
2351
|
+
* skipped by Tab, announced as unavailable, and rejects input.
|
|
2352
|
+
*
|
|
2353
|
+
* `-webkit-text-fill-color` is not redundant. iOS Safari renders disabled field
|
|
2354
|
+
* text at reduced opacity and ignores `color`, which would wash out the chosen
|
|
2355
|
+
* disabled token and undo the point of picking one. */
|
|
2356
|
+
|
|
2357
|
+
.lutra-input:disabled {
|
|
2358
|
+
border-color: var(--lutra-input-disabled-border);
|
|
2359
|
+
background-color: var(--lutra-input-disabled-surface);
|
|
2360
|
+
color: var(--lutra-input-disabled-text);
|
|
2361
|
+
-webkit-text-fill-color: var(--lutra-input-disabled-text);
|
|
2362
|
+
cursor: not-allowed;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
.lutra-input:disabled::placeholder {
|
|
2366
|
+
color: var(--lutra-input-disabled-text);
|
|
2367
|
+
-webkit-text-fill-color: var(--lutra-input-disabled-text);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/* Read-only keeps the normal visual treatment. It stays focusable and its value
|
|
2371
|
+
* stays selectable and copyable, which is the behaviour that distinguishes it
|
|
2372
|
+
* from disabled, so it must not be dressed up to look unavailable. */
|
|
2373
|
+
|
|
2374
|
+
/* Full width. Layout only.
|
|
2375
|
+
*
|
|
2376
|
+
* The base rule already fills the inline axis, which is what a field in a form
|
|
2377
|
+
* almost always wants. `data-full-width` additionally opts out of any width a
|
|
2378
|
+
* layout has constrained it to. */
|
|
2379
|
+
|
|
2380
|
+
.lutra-input[data-full-width] {
|
|
2381
|
+
inline-size: 100%;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/* Forced-colours modes.
|
|
2385
|
+
*
|
|
2386
|
+
* The system replaces the token colours, which is the point of the mode. What it
|
|
2387
|
+
* does not do is paint the inset shadow the focus indicator is built from, so
|
|
2388
|
+
* focus is restored here as a real outline in the system highlight colour.
|
|
2389
|
+
* Disabled fields already pick up `GrayText` from the native attribute. */
|
|
2390
|
+
|
|
2391
|
+
@media (forced-colors: active) {
|
|
2392
|
+
.lutra-input:focus-visible {
|
|
2393
|
+
outline: var(--lutra-input-border-width-focus) solid Highlight;
|
|
2394
|
+
outline-offset: 1px;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/*
|
|
2399
|
+
* Select.
|
|
2400
|
+
*
|
|
2401
|
+
* The public styling contract is the `lutra-select` class plus `data-size` and `data-full-width`,
|
|
2402
|
+
* and `aria-invalid` for the error state — the same contract Input uses, and for the same reason:
|
|
2403
|
+
* matching on the ARIA attribute means the look and the announced state cannot drift apart.
|
|
2404
|
+
*
|
|
2405
|
+
* Figma binds Select to the `component/input/*` alias layer rather than one of its own, so this
|
|
2406
|
+
* consumes exactly the same tokens Input does. That is the design system's decision, not a
|
|
2407
|
+
* shortcut: a select and a text field are the same surface with the same border states.
|
|
2408
|
+
*
|
|
2409
|
+
* ## Why there is a wrapper
|
|
2410
|
+
*
|
|
2411
|
+
* A `<select>` cannot render a pseudo-element, and a `background-image` chevron would have to be a
|
|
2412
|
+
* fixed-colour asset — wrong in the other theme and wrong when disabled. The wrapper exists solely
|
|
2413
|
+
* so the chevron can be a mask filled with a token colour. It is the smallest thing that keeps the
|
|
2414
|
+
* chevron on the token system, and it is why this component has one when Input does not.
|
|
2415
|
+
*/
|
|
2416
|
+
|
|
2417
|
+
.lutra-select-wrapper {
|
|
2418
|
+
--lutra-select-chevron-size: 1rem;
|
|
2419
|
+
--lutra-select-chevron-color: var(--lutra-component-input-text);
|
|
2420
|
+
|
|
2421
|
+
/* NO FIGMA VARIABLE. Figma lays the chevron out with auto-layout; in CSS the trailing space has
|
|
2422
|
+
* to be reserved explicitly, so it is the leading padding plus the icon plus a gap. */
|
|
2423
|
+
--lutra-select-chevron-gap: var(--lutra-space-sm);
|
|
2424
|
+
|
|
2425
|
+
position: relative;
|
|
2426
|
+
display: block;
|
|
2427
|
+
inline-size: 100%;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/* The chevron. A mask filled with `background-color`, so the glyph takes a token colour and follows
|
|
2431
|
+
* the theme and the disabled state, rather than being a fixed-colour image. `pointer-events: none`
|
|
2432
|
+
* keeps clicks going to the control underneath. */
|
|
2433
|
+
|
|
2434
|
+
.lutra-select-wrapper::after {
|
|
2435
|
+
content: '';
|
|
2436
|
+
position: absolute;
|
|
2437
|
+
inset-inline-end: var(--lutra-select-chevron-inset, var(--lutra-space-md));
|
|
2438
|
+
inset-block-start: 50%;
|
|
2439
|
+
transform: translateY(-50%);
|
|
2440
|
+
inline-size: var(--lutra-select-chevron-size);
|
|
2441
|
+
block-size: var(--lutra-select-chevron-size);
|
|
2442
|
+
background-color: var(--lutra-select-chevron-color);
|
|
2443
|
+
pointer-events: none;
|
|
2444
|
+
|
|
2445
|
+
--lutra-select-chevron-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
|
|
2446
|
+
-webkit-mask-image: var(--lutra-select-chevron-glyph);
|
|
2447
|
+
mask-image: var(--lutra-select-chevron-glyph);
|
|
2448
|
+
-webkit-mask-repeat: no-repeat;
|
|
2449
|
+
mask-repeat: no-repeat;
|
|
2450
|
+
-webkit-mask-size: 100% 100%;
|
|
2451
|
+
mask-size: 100% 100%;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/* The chevron greys out with the control, because it is part of the control. */
|
|
2455
|
+
.lutra-select-wrapper:has(> .lutra-select:disabled)::after {
|
|
2456
|
+
--lutra-select-chevron-color: var(--lutra-color-text-disabled);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
.lutra-select {
|
|
2460
|
+
/* Shared with Input, by way of Figma's own bindings. */
|
|
2461
|
+
--lutra-select-surface: var(--lutra-component-input-surface);
|
|
2462
|
+
--lutra-select-text: var(--lutra-component-input-text);
|
|
2463
|
+
--lutra-select-border: var(--lutra-component-input-border-default);
|
|
2464
|
+
--lutra-select-border-hover: var(--lutra-component-input-border-hover);
|
|
2465
|
+
--lutra-select-border-focus: var(--lutra-component-input-border-focus);
|
|
2466
|
+
--lutra-select-border-invalid: var(--lutra-component-input-border-invalid);
|
|
2467
|
+
|
|
2468
|
+
/* NO `component/input/*` ALIAS. Figma binds the core disabled tokens directly, exactly as it does
|
|
2469
|
+
* on Input; the alias layer has no disabled trio. See `select.md`. */
|
|
2470
|
+
--lutra-select-disabled-surface: var(--lutra-color-background-disabled);
|
|
2471
|
+
--lutra-select-disabled-border: var(--lutra-color-border-disabled);
|
|
2472
|
+
--lutra-select-disabled-text: var(--lutra-color-text-disabled);
|
|
2473
|
+
|
|
2474
|
+
--lutra-select-radius: var(--lutra-radius-md);
|
|
2475
|
+
--lutra-select-border-width: var(--lutra-border-width-thin);
|
|
2476
|
+
--lutra-select-border-width-focus: var(--lutra-border-width-thick);
|
|
2477
|
+
--lutra-select-font-family: var(--lutra-type-family-sans);
|
|
2478
|
+
--lutra-select-font-weight: var(--lutra-type-weight-medium);
|
|
2479
|
+
|
|
2480
|
+
--lutra-select-padding-inline-sm: var(--lutra-space-sm);
|
|
2481
|
+
--lutra-select-padding-inline-md: var(--lutra-space-md);
|
|
2482
|
+
--lutra-select-padding-inline-lg: var(--lutra-space-lg);
|
|
2483
|
+
|
|
2484
|
+
--lutra-select-font-size-sm: var(--lutra-text-body-small);
|
|
2485
|
+
--lutra-select-font-size-md: var(--lutra-text-body);
|
|
2486
|
+
--lutra-select-font-size-lg: var(--lutra-text-h3);
|
|
2487
|
+
|
|
2488
|
+
--lutra-select-line-height-sm: var(--lutra-type-line-height-body-small);
|
|
2489
|
+
--lutra-select-line-height-md: var(--lutra-type-line-height-body);
|
|
2490
|
+
--lutra-select-line-height-lg: var(--lutra-type-line-height-h3);
|
|
2491
|
+
|
|
2492
|
+
/* NO FIGMA VARIABLE. Figma draws 36/40/44px fixed heights; these become `min-block-size` so the
|
|
2493
|
+
* control grows with the reader's font size instead of clipping at 200% zoom. */
|
|
2494
|
+
--lutra-select-min-height-sm: 2.25rem;
|
|
2495
|
+
--lutra-select-min-height-md: 2.5rem;
|
|
2496
|
+
--lutra-select-min-height-lg: 2.75rem;
|
|
2497
|
+
--lutra-select-padding-block: var(--lutra-space-2xs);
|
|
2498
|
+
|
|
2499
|
+
box-sizing: border-box;
|
|
2500
|
+
inline-size: 100%;
|
|
2501
|
+
max-inline-size: 100%;
|
|
2502
|
+
margin: 0;
|
|
2503
|
+
padding-block: var(--lutra-select-padding-block);
|
|
2504
|
+
border-style: solid;
|
|
2505
|
+
border-width: var(--lutra-select-border-width);
|
|
2506
|
+
border-color: var(--lutra-select-border);
|
|
2507
|
+
border-radius: var(--lutra-select-radius);
|
|
2508
|
+
background-color: var(--lutra-select-surface);
|
|
2509
|
+
color: var(--lutra-select-text);
|
|
2510
|
+
font-family: var(--lutra-select-font-family);
|
|
2511
|
+
font-weight: var(--lutra-select-font-weight);
|
|
2512
|
+
cursor: pointer;
|
|
2513
|
+
|
|
2514
|
+
/* Removes the platform's own dropdown arrow, so the Figma chevron is the only one drawn. */
|
|
2515
|
+
appearance: none;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/* Sizes. `min-block-size` rather than `height`, so the control grows with the reader's font size.
|
|
2519
|
+
* Trailing padding reserves room for the chevron. */
|
|
2520
|
+
|
|
2521
|
+
.lutra-select[data-size='sm'] {
|
|
2522
|
+
min-block-size: var(--lutra-select-min-height-sm);
|
|
2523
|
+
padding-inline: var(--lutra-select-padding-inline-sm)
|
|
2524
|
+
calc(
|
|
2525
|
+
var(--lutra-select-padding-inline-sm) + var(--lutra-select-chevron-size) +
|
|
2526
|
+
var(--lutra-select-chevron-gap)
|
|
2527
|
+
);
|
|
2528
|
+
font-size: var(--lutra-select-font-size-sm);
|
|
2529
|
+
line-height: var(--lutra-select-line-height-sm);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
.lutra-select[data-size='md'] {
|
|
2533
|
+
min-block-size: var(--lutra-select-min-height-md);
|
|
2534
|
+
padding-inline: var(--lutra-select-padding-inline-md)
|
|
2535
|
+
calc(
|
|
2536
|
+
var(--lutra-select-padding-inline-md) + var(--lutra-select-chevron-size) +
|
|
2537
|
+
var(--lutra-select-chevron-gap)
|
|
2538
|
+
);
|
|
2539
|
+
font-size: var(--lutra-select-font-size-md);
|
|
2540
|
+
line-height: var(--lutra-select-line-height-md);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
.lutra-select[data-size='lg'] {
|
|
2544
|
+
min-block-size: var(--lutra-select-min-height-lg);
|
|
2545
|
+
padding-inline: var(--lutra-select-padding-inline-lg)
|
|
2546
|
+
calc(
|
|
2547
|
+
var(--lutra-select-padding-inline-lg) + var(--lutra-select-chevron-size) +
|
|
2548
|
+
var(--lutra-select-chevron-gap)
|
|
2549
|
+
);
|
|
2550
|
+
font-size: var(--lutra-select-font-size-lg);
|
|
2551
|
+
line-height: var(--lutra-select-line-height-lg);
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/* The chevron sits at the same inset as the control's own padding, per size. */
|
|
2555
|
+
|
|
2556
|
+
.lutra-select-wrapper:has(> .lutra-select[data-size='sm']) {
|
|
2557
|
+
--lutra-select-chevron-inset: var(--lutra-space-sm);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
.lutra-select-wrapper:has(> .lutra-select[data-size='md']) {
|
|
2561
|
+
--lutra-select-chevron-inset: var(--lutra-space-md);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
.lutra-select-wrapper:has(> .lutra-select[data-size='lg']) {
|
|
2565
|
+
--lutra-select-chevron-inset: var(--lutra-space-lg);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
/* Interaction states, matching Input. */
|
|
2569
|
+
|
|
2570
|
+
.lutra-select:hover:not(:disabled) {
|
|
2571
|
+
border-color: var(--lutra-select-border-hover);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/* Focus thickens the control's own border, as Input does. The extra pixel is an inset shadow rather
|
|
2575
|
+
* than a wider border, so the value does not shift sideways on focus — and it is what makes the
|
|
2576
|
+
* indicator perceivable: the focus colour against the resting border is only about 1.1:1, while the
|
|
2577
|
+
* added pixel replaces surface, where it clears 3:1. */
|
|
2578
|
+
|
|
2579
|
+
.lutra-select:focus-visible {
|
|
2580
|
+
border-color: var(--lutra-select-border-focus);
|
|
2581
|
+
box-shadow: inset 0 0 0
|
|
2582
|
+
calc(var(--lutra-select-border-width-focus) - var(--lutra-select-border-width))
|
|
2583
|
+
var(--lutra-select-border-focus);
|
|
2584
|
+
outline: none;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
.lutra-select[aria-invalid='true'] {
|
|
2588
|
+
border-color: var(--lutra-select-border-invalid);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
.lutra-select[aria-invalid='true']:focus-visible {
|
|
2592
|
+
border-color: var(--lutra-select-border-focus);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
.lutra-select:disabled {
|
|
2596
|
+
border-color: var(--lutra-select-disabled-border);
|
|
2597
|
+
background-color: var(--lutra-select-disabled-surface);
|
|
2598
|
+
color: var(--lutra-select-disabled-text);
|
|
2599
|
+
-webkit-text-fill-color: var(--lutra-select-disabled-text);
|
|
2600
|
+
cursor: not-allowed;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/* The options popup is drawn by the operating system, not by this stylesheet. Setting a colour on
|
|
2604
|
+
* the options is the one thing that reliably keeps it readable in dark mode on the browsers that
|
|
2605
|
+
* respect it, without pretending the popup is ours to style. */
|
|
2606
|
+
|
|
2607
|
+
.lutra-select option {
|
|
2608
|
+
background-color: var(--lutra-select-surface);
|
|
2609
|
+
color: var(--lutra-select-text);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
/* Forced-colours modes. The mask is painted with `background-color`, which the system replaces, so
|
|
2613
|
+
* the chevron survives. The inset focus shadow is not painted, so focus is restored as an outline. */
|
|
2614
|
+
|
|
2615
|
+
@media (forced-colors: active) {
|
|
2616
|
+
.lutra-select:focus-visible {
|
|
2617
|
+
outline: var(--lutra-select-border-width-focus) solid Highlight;
|
|
2618
|
+
outline-offset: 1px;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
/*
|
|
2623
|
+
* Checkbox, and the choice-item layout shared with Radio and Toggle.
|
|
2624
|
+
*
|
|
2625
|
+
* `.lutra-choice*` lives here because Checkbox is the first component to need it, the same way the
|
|
2626
|
+
* `.lutra-field*` shell lives in `field.css` and serves Select Field too. Radio Item and Toggle Item
|
|
2627
|
+
* reuse these classes rather than each defining their own — one label treatment, one description
|
|
2628
|
+
* treatment, one gap.
|
|
2629
|
+
*
|
|
2630
|
+
* The control is a native `<input type="checkbox">` with `appearance: none`, drawn with its own
|
|
2631
|
+
* pseudo-element. Once appearance is none the input is an ordinary box that accepts `::after`,
|
|
2632
|
+
* which is what lets the mark be a mask filled with a token colour rather than a fixed-colour
|
|
2633
|
+
* image that would be wrong in dark mode.
|
|
2634
|
+
*
|
|
2635
|
+
* The mixed state is selected with the native `:indeterminate` pseudo-class, which matches the DOM
|
|
2636
|
+
* property assistive technology reads. There is no mirrored data attribute to fall out of step.
|
|
2637
|
+
*/
|
|
2638
|
+
|
|
2639
|
+
.lutra-checkbox {
|
|
2640
|
+
--lutra-checkbox-size: 1.25rem;
|
|
2641
|
+
--lutra-checkbox-surface: var(--lutra-component-checkbox-surface);
|
|
2642
|
+
--lutra-checkbox-border: var(--lutra-component-checkbox-border-default);
|
|
2643
|
+
--lutra-checkbox-border-hover: var(--lutra-component-checkbox-border-hover);
|
|
2644
|
+
--lutra-checkbox-border-invalid: var(--lutra-component-checkbox-border-invalid);
|
|
2645
|
+
--lutra-checkbox-border-disabled: var(--lutra-component-checkbox-border-disabled);
|
|
2646
|
+
--lutra-checkbox-fill-selected: var(--lutra-component-checkbox-fill-selected);
|
|
2647
|
+
--lutra-checkbox-fill-selected-hover: var(--lutra-component-checkbox-fill-selected-hover);
|
|
2648
|
+
--lutra-checkbox-fill-invalid: var(--lutra-component-checkbox-fill-invalid);
|
|
2649
|
+
--lutra-checkbox-fill-disabled: var(--lutra-component-checkbox-fill-disabled);
|
|
2650
|
+
--lutra-checkbox-mark: var(--lutra-component-checkbox-mark);
|
|
2651
|
+
--lutra-checkbox-mark-on-danger: var(--lutra-component-checkbox-mark-on-danger);
|
|
2652
|
+
--lutra-checkbox-mark-disabled: var(--lutra-component-checkbox-text-disabled);
|
|
2653
|
+
|
|
2654
|
+
--lutra-checkbox-radius: var(--lutra-radius-sm);
|
|
2655
|
+
--lutra-checkbox-border-width: var(--lutra-border-width-thick);
|
|
2656
|
+
--lutra-checkbox-focus-ring-color: var(--lutra-color-focus-ring);
|
|
2657
|
+
--lutra-checkbox-focus-ring-width: var(--lutra-border-width-thick);
|
|
2658
|
+
|
|
2659
|
+
/* NO FIGMA VARIABLE. Figma draws the focus state as a 28px ring around the 20px box, which is a
|
|
2660
|
+
* 2px ring at a 2px offset — the same geometry Button uses. */
|
|
2661
|
+
--lutra-checkbox-focus-ring-offset: 2px;
|
|
2662
|
+
|
|
2663
|
+
box-sizing: border-box;
|
|
2664
|
+
flex: 0 0 auto;
|
|
2665
|
+
display: grid;
|
|
2666
|
+
place-content: center;
|
|
2667
|
+
inline-size: var(--lutra-checkbox-size);
|
|
2668
|
+
block-size: var(--lutra-checkbox-size);
|
|
2669
|
+
margin: 0;
|
|
2670
|
+
border: var(--lutra-checkbox-border-width) solid var(--lutra-checkbox-border);
|
|
2671
|
+
border-radius: var(--lutra-checkbox-radius);
|
|
2672
|
+
background-color: var(--lutra-checkbox-surface);
|
|
2673
|
+
cursor: pointer;
|
|
2674
|
+
appearance: none;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
/* The mark. A mask filled with a token colour, so it follows the theme and the disabled state. */
|
|
2678
|
+
|
|
2679
|
+
.lutra-checkbox::after {
|
|
2680
|
+
content: '';
|
|
2681
|
+
inline-size: 0.75rem;
|
|
2682
|
+
block-size: 0.75rem;
|
|
2683
|
+
background-color: var(--lutra-checkbox-mark);
|
|
2684
|
+
|
|
2685
|
+
/* Nothing to show until the box is checked or mixed. */
|
|
2686
|
+
-webkit-mask-image: none;
|
|
2687
|
+
mask-image: none;
|
|
2688
|
+
-webkit-mask-repeat: no-repeat;
|
|
2689
|
+
mask-repeat: no-repeat;
|
|
2690
|
+
-webkit-mask-position: center;
|
|
2691
|
+
mask-position: center;
|
|
2692
|
+
-webkit-mask-size: 100% 100%;
|
|
2693
|
+
mask-size: 100% 100%;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
.lutra-checkbox:checked:not(:indeterminate) {
|
|
2697
|
+
--lutra-checkbox-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 6.5l2.5 2.5L10 3.5'/%3E%3C/svg%3E");
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/* The mixed state is a dash, and it wins over the tick: a box can be checked in the DOM and mixed
|
|
2701
|
+
* at the same time, and mixed is what is being communicated. */
|
|
2702
|
+
.lutra-checkbox:indeterminate {
|
|
2703
|
+
--lutra-checkbox-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M3 6h6'/%3E%3C/svg%3E");
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
.lutra-checkbox:checked::after,
|
|
2707
|
+
.lutra-checkbox:indeterminate::after {
|
|
2708
|
+
-webkit-mask-image: var(--lutra-checkbox-check);
|
|
2709
|
+
mask-image: var(--lutra-checkbox-check);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
.lutra-checkbox:checked,
|
|
2713
|
+
.lutra-checkbox:indeterminate {
|
|
2714
|
+
border-color: var(--lutra-checkbox-fill-selected);
|
|
2715
|
+
background-color: var(--lutra-checkbox-fill-selected);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
/* Interaction states. */
|
|
2719
|
+
|
|
2720
|
+
.lutra-checkbox:hover:not(:disabled) {
|
|
2721
|
+
border-color: var(--lutra-checkbox-border-hover);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
.lutra-checkbox:checked:hover:not(:disabled),
|
|
2725
|
+
.lutra-checkbox:indeterminate:hover:not(:disabled) {
|
|
2726
|
+
border-color: var(--lutra-checkbox-fill-selected-hover);
|
|
2727
|
+
background-color: var(--lutra-checkbox-fill-selected-hover);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/* Focus is a ring outside the box, not a border change: the box's own border already carries the
|
|
2731
|
+
* checked state, so changing it would make two states fight for the same pixel. */
|
|
2732
|
+
|
|
2733
|
+
.lutra-checkbox:focus-visible {
|
|
2734
|
+
outline: var(--lutra-checkbox-focus-ring-width) solid var(--lutra-checkbox-focus-ring-color);
|
|
2735
|
+
outline-offset: var(--lutra-checkbox-focus-ring-offset);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/* Invalid, matched on the ARIA attribute so the look and the announced state are one fact. */
|
|
2739
|
+
|
|
2740
|
+
.lutra-checkbox[aria-invalid='true'] {
|
|
2741
|
+
border-color: var(--lutra-checkbox-border-invalid);
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
.lutra-checkbox[aria-invalid='true']:checked,
|
|
2745
|
+
.lutra-checkbox[aria-invalid='true']:indeterminate {
|
|
2746
|
+
border-color: var(--lutra-checkbox-border-invalid);
|
|
2747
|
+
background-color: var(--lutra-checkbox-fill-invalid);
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
.lutra-checkbox[aria-invalid='true']:checked::after,
|
|
2751
|
+
.lutra-checkbox[aria-invalid='true']:indeterminate::after {
|
|
2752
|
+
background-color: var(--lutra-checkbox-mark-on-danger);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
/* Disabled. Deliberate colours rather than an opacity wash; the native attribute is what carries
|
|
2756
|
+
* the state to assistive technology. */
|
|
2757
|
+
|
|
2758
|
+
.lutra-checkbox:disabled {
|
|
2759
|
+
border-color: var(--lutra-checkbox-border-disabled);
|
|
2760
|
+
background-color: var(--lutra-checkbox-fill-disabled);
|
|
2761
|
+
cursor: not-allowed;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
.lutra-checkbox:disabled::after {
|
|
2765
|
+
background-color: var(--lutra-checkbox-mark-disabled);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
/* Forced-colours modes: the mask survives because it is painted with `background-color`, but the
|
|
2769
|
+
* checked fill and the border are replaced by the system, so the mark needs to follow the system
|
|
2770
|
+
* text colour to stay visible. */
|
|
2771
|
+
|
|
2772
|
+
@media (forced-colors: active) {
|
|
2773
|
+
.lutra-checkbox::after {
|
|
2774
|
+
background-color: CanvasText;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
.lutra-checkbox:disabled::after {
|
|
2778
|
+
background-color: GrayText;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
/*
|
|
2783
|
+
* The choice item: a control beside a label and an optional description.
|
|
2784
|
+
*
|
|
2785
|
+
* Shared by Checkbox Item, Radio Item and Toggle Item. Toggle Item puts its control after the
|
|
2786
|
+
* content, which is the only structural difference and is handled by a modifier.
|
|
2787
|
+
*/
|
|
2788
|
+
|
|
2789
|
+
.lutra-choice {
|
|
2790
|
+
--lutra-choice-gap: var(--lutra-space-md);
|
|
2791
|
+
--lutra-choice-content-gap: var(--lutra-space-2xs);
|
|
2792
|
+
--lutra-choice-label-color: var(--lutra-component-checkbox-label);
|
|
2793
|
+
--lutra-choice-description-color: var(--lutra-component-checkbox-description);
|
|
2794
|
+
--lutra-choice-label-size: var(--lutra-text-body);
|
|
2795
|
+
--lutra-choice-label-line-height: var(--lutra-type-line-height-body);
|
|
2796
|
+
--lutra-choice-description-size: var(--lutra-text-body-small);
|
|
2797
|
+
--lutra-choice-description-line-height: var(--lutra-type-line-height-body-small);
|
|
2798
|
+
|
|
2799
|
+
display: flex;
|
|
2800
|
+
align-items: flex-start;
|
|
2801
|
+
gap: var(--lutra-choice-gap);
|
|
2802
|
+
font-family: var(--lutra-type-family-sans);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.lutra-choice__content {
|
|
2806
|
+
display: flex;
|
|
2807
|
+
flex-direction: column;
|
|
2808
|
+
gap: var(--lutra-choice-content-gap);
|
|
2809
|
+
min-inline-size: 0;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
/* The label is the click target as well as the name, so it gets the pointer cursor the control has.
|
|
2813
|
+
* `display: block` and no height: a long label wraps and the row grows with it. */
|
|
2814
|
+
|
|
2815
|
+
.lutra-choice__label {
|
|
2816
|
+
display: block;
|
|
2817
|
+
font-size: var(--lutra-choice-label-size);
|
|
2818
|
+
line-height: var(--lutra-choice-label-line-height);
|
|
2819
|
+
color: var(--lutra-choice-label-color);
|
|
2820
|
+
cursor: pointer;
|
|
2821
|
+
overflow-wrap: break-word;
|
|
2822
|
+
text-wrap: pretty;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
.lutra-choice__description {
|
|
2826
|
+
margin: 0;
|
|
2827
|
+
font-size: var(--lutra-choice-description-size);
|
|
2828
|
+
line-height: var(--lutra-choice-description-line-height);
|
|
2829
|
+
color: var(--lutra-choice-description-color);
|
|
2830
|
+
overflow-wrap: break-word;
|
|
2831
|
+
text-wrap: pretty;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
.lutra-choice[data-disabled='true'] .lutra-choice__label {
|
|
2835
|
+
color: var(--lutra-component-checkbox-text-disabled);
|
|
2836
|
+
cursor: not-allowed;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
.lutra-choice[data-disabled='true'] .lutra-choice__description {
|
|
2840
|
+
color: var(--lutra-component-checkbox-text-disabled);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/*
|
|
2844
|
+
* The control keeps its 20px drawn size, and the row reaches a comfortable pointer target through
|
|
2845
|
+
* the label rather than by growing the box: the label is associated with `for`/`id`, so the whole
|
|
2846
|
+
* of it is clickable. That is why nothing here sets a 44px height — a fixed height would clip a
|
|
2847
|
+
* wrapped label, and the target is already larger than the control.
|
|
2848
|
+
*
|
|
2849
|
+
* The control is nudged down so it sits on the label's first line rather than its box top.
|
|
2850
|
+
*/
|
|
2851
|
+
|
|
2852
|
+
.lutra-choice > .lutra-checkbox,
|
|
2853
|
+
.lutra-choice > .lutra-radio {
|
|
2854
|
+
margin-block-start: calc(
|
|
2855
|
+
(var(--lutra-choice-label-line-height) * 1em - var(--lutra-checkbox-size, 1.25rem)) / 2
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
/*
|
|
2860
|
+
* Radio.
|
|
2861
|
+
*
|
|
2862
|
+
* The same 20px control as Checkbox, drawn round instead of square, with a dot instead of a tick.
|
|
2863
|
+
* It reuses the `.lutra-choice*` layout defined in `checkbox.css`, so a radio row and a checkbox row
|
|
2864
|
+
* are the same shape — same gap, same label treatment, same description treatment.
|
|
2865
|
+
*
|
|
2866
|
+
* The control is a native `<input type="radio">` with `appearance: none`, drawn with its own
|
|
2867
|
+
* pseudo-element. Grouping, arrow-key navigation and single-selection all come from the element.
|
|
2868
|
+
*/
|
|
2869
|
+
|
|
2870
|
+
.lutra-radio {
|
|
2871
|
+
--lutra-radio-size: 1.25rem;
|
|
2872
|
+
--lutra-radio-surface: var(--lutra-component-radio-surface);
|
|
2873
|
+
--lutra-radio-border: var(--lutra-component-radio-border-default);
|
|
2874
|
+
--lutra-radio-border-hover: var(--lutra-component-radio-border-hover);
|
|
2875
|
+
--lutra-radio-border-invalid: var(--lutra-component-radio-border-invalid);
|
|
2876
|
+
--lutra-radio-border-disabled: var(--lutra-component-radio-border-disabled);
|
|
2877
|
+
--lutra-radio-fill-selected: var(--lutra-component-radio-fill-selected);
|
|
2878
|
+
--lutra-radio-fill-selected-hover: var(--lutra-component-radio-fill-selected-hover);
|
|
2879
|
+
--lutra-radio-fill-invalid: var(--lutra-component-radio-fill-invalid);
|
|
2880
|
+
--lutra-radio-fill-disabled: var(--lutra-component-radio-fill-disabled);
|
|
2881
|
+
--lutra-radio-mark: var(--lutra-component-radio-mark);
|
|
2882
|
+
--lutra-radio-mark-on-danger: var(--lutra-component-radio-mark-on-danger);
|
|
2883
|
+
--lutra-radio-mark-disabled: var(--lutra-component-radio-text-disabled);
|
|
2884
|
+
|
|
2885
|
+
--lutra-radio-border-width: var(--lutra-border-width-thick);
|
|
2886
|
+
--lutra-radio-focus-ring-color: var(--lutra-color-focus-ring);
|
|
2887
|
+
--lutra-radio-focus-ring-width: var(--lutra-border-width-thick);
|
|
2888
|
+
|
|
2889
|
+
/* NO FIGMA VARIABLE. Figma draws focus as a 28px ring around the 20px control — a 2px ring at a
|
|
2890
|
+
* 2px offset, the same geometry Checkbox and Button use. */
|
|
2891
|
+
--lutra-radio-focus-ring-offset: 2px;
|
|
2892
|
+
|
|
2893
|
+
/* NO FIGMA VARIABLE. Figma draws the selected dot at 8px inside the 20px control. */
|
|
2894
|
+
--lutra-radio-mark-size: 0.5rem;
|
|
2895
|
+
|
|
2896
|
+
box-sizing: border-box;
|
|
2897
|
+
flex: 0 0 auto;
|
|
2898
|
+
display: grid;
|
|
2899
|
+
place-content: center;
|
|
2900
|
+
inline-size: var(--lutra-radio-size);
|
|
2901
|
+
block-size: var(--lutra-radio-size);
|
|
2902
|
+
margin: 0;
|
|
2903
|
+
border: var(--lutra-radio-border-width) solid var(--lutra-radio-border);
|
|
2904
|
+
border-radius: var(--lutra-radius-full);
|
|
2905
|
+
background-color: var(--lutra-radio-surface);
|
|
2906
|
+
cursor: pointer;
|
|
2907
|
+
appearance: none;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
/* The dot. A solid circle rather than a mask — a filled shape needs no glyph. */
|
|
2911
|
+
|
|
2912
|
+
.lutra-radio::after {
|
|
2913
|
+
content: '';
|
|
2914
|
+
inline-size: var(--lutra-radio-mark-size);
|
|
2915
|
+
block-size: var(--lutra-radio-mark-size);
|
|
2916
|
+
border-radius: var(--lutra-radius-full);
|
|
2917
|
+
background-color: transparent;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
.lutra-radio:checked {
|
|
2921
|
+
border-color: var(--lutra-radio-fill-selected);
|
|
2922
|
+
background-color: var(--lutra-radio-fill-selected);
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
.lutra-radio:checked::after {
|
|
2926
|
+
background-color: var(--lutra-radio-mark);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
/* Interaction states. */
|
|
2930
|
+
|
|
2931
|
+
.lutra-radio:hover:not(:disabled) {
|
|
2932
|
+
border-color: var(--lutra-radio-border-hover);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
.lutra-radio:checked:hover:not(:disabled) {
|
|
2936
|
+
border-color: var(--lutra-radio-fill-selected-hover);
|
|
2937
|
+
background-color: var(--lutra-radio-fill-selected-hover);
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
/* Focus is a ring outside the control, not a border change: the border already carries the selected
|
|
2941
|
+
* state. */
|
|
2942
|
+
|
|
2943
|
+
.lutra-radio:focus-visible {
|
|
2944
|
+
outline: var(--lutra-radio-focus-ring-width) solid var(--lutra-radio-focus-ring-color);
|
|
2945
|
+
outline-offset: var(--lutra-radio-focus-ring-offset);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
/* Invalid.
|
|
2949
|
+
*
|
|
2950
|
+
* Matched on a data attribute rather than `aria-invalid`, which ARIA 1.2 deprecates on the radio
|
|
2951
|
+
* role — a single option cannot usefully be invalid on its own. The semantic state lives at group
|
|
2952
|
+
* level, which Field Group owns; this only draws it. */
|
|
2953
|
+
|
|
2954
|
+
.lutra-radio[data-invalid] {
|
|
2955
|
+
border-color: var(--lutra-radio-border-invalid);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
.lutra-radio[data-invalid]:checked {
|
|
2959
|
+
border-color: var(--lutra-radio-border-invalid);
|
|
2960
|
+
background-color: var(--lutra-radio-fill-invalid);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
.lutra-radio[data-invalid]:checked::after {
|
|
2964
|
+
background-color: var(--lutra-radio-mark-on-danger);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
/* Disabled. */
|
|
2968
|
+
|
|
2969
|
+
.lutra-radio:disabled {
|
|
2970
|
+
border-color: var(--lutra-radio-border-disabled);
|
|
2971
|
+
background-color: var(--lutra-radio-fill-disabled);
|
|
2972
|
+
cursor: not-allowed;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
.lutra-radio:disabled:checked::after {
|
|
2976
|
+
background-color: var(--lutra-radio-mark-disabled);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
/* Forced-colours modes: the system replaces the fill, so the dot follows the system text colour to
|
|
2980
|
+
* stay visible against it. */
|
|
2981
|
+
|
|
2982
|
+
@media (forced-colors: active) {
|
|
2983
|
+
.lutra-radio:checked::after {
|
|
2984
|
+
background-color: CanvasText;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
.lutra-radio:disabled:checked::after {
|
|
2988
|
+
background-color: GrayText;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/*
|
|
2993
|
+
* Toggle.
|
|
2994
|
+
*
|
|
2995
|
+
* A native `<input type="checkbox" role="switch">` with `appearance: none`, drawn as a track with a
|
|
2996
|
+
* thumb on its own pseudo-element.
|
|
2997
|
+
*
|
|
2998
|
+
* The state is carried by the thumb's position as well as its colour, and `role="switch"` puts
|
|
2999
|
+
* on/off in the accessibility tree — so nothing about this control depends on colour alone.
|
|
3000
|
+
*
|
|
3001
|
+
* There is deliberately **no transition** on the thumb. Motion tokens are still deferred in the
|
|
3002
|
+
* design system, so a duration here would be an invented value rather than a system one. When the
|
|
3003
|
+
* motion scale lands, the movement belongs on `--lutra-motion-*` and gets a
|
|
3004
|
+
* `prefers-reduced-motion` guard with it.
|
|
3005
|
+
*/
|
|
3006
|
+
|
|
3007
|
+
.lutra-toggle {
|
|
3008
|
+
/* NO FIGMA VARIABLE. Figma draws the track at 40×24 with a 16px thumb, and no size scale exists
|
|
3009
|
+
* for this component. */
|
|
3010
|
+
--lutra-toggle-track-inline-size: 2.5rem;
|
|
3011
|
+
--lutra-toggle-track-block-size: 1.5rem;
|
|
3012
|
+
--lutra-toggle-thumb-size: 1rem;
|
|
3013
|
+
|
|
3014
|
+
--lutra-toggle-track-off: var(--lutra-component-toggle-track-off);
|
|
3015
|
+
--lutra-toggle-track-on: var(--lutra-component-toggle-track-on);
|
|
3016
|
+
--lutra-toggle-track-on-hover: var(--lutra-component-toggle-track-on-hover);
|
|
3017
|
+
--lutra-toggle-track-disabled: var(--lutra-component-toggle-track-disabled);
|
|
3018
|
+
--lutra-toggle-border: var(--lutra-component-toggle-border-default);
|
|
3019
|
+
--lutra-toggle-border-hover: var(--lutra-component-toggle-border-hover);
|
|
3020
|
+
--lutra-toggle-border-invalid: var(--lutra-component-toggle-border-invalid);
|
|
3021
|
+
--lutra-toggle-thumb-off: var(--lutra-component-toggle-thumb-off);
|
|
3022
|
+
--lutra-toggle-thumb-on: var(--lutra-component-toggle-thumb-on);
|
|
3023
|
+
--lutra-toggle-thumb-disabled: var(--lutra-component-toggle-thumb-disabled);
|
|
3024
|
+
--lutra-toggle-focus: var(--lutra-component-toggle-focus);
|
|
3025
|
+
|
|
3026
|
+
--lutra-toggle-border-width: var(--lutra-border-width-thin);
|
|
3027
|
+
--lutra-toggle-focus-ring-width: var(--lutra-border-width-thick);
|
|
3028
|
+
|
|
3029
|
+
/* NO FIGMA VARIABLE. Figma draws the focus state as a thicker track border in the focus colour;
|
|
3030
|
+
* this offsets a ring instead, matching every other control in the library, so a row of mixed
|
|
3031
|
+
* controls shows one focus treatment. */
|
|
3032
|
+
--lutra-toggle-focus-ring-offset: 2px;
|
|
3033
|
+
|
|
3034
|
+
box-sizing: border-box;
|
|
3035
|
+
position: relative;
|
|
3036
|
+
flex: 0 0 auto;
|
|
3037
|
+
display: block;
|
|
3038
|
+
inline-size: var(--lutra-toggle-track-inline-size);
|
|
3039
|
+
block-size: var(--lutra-toggle-track-block-size);
|
|
3040
|
+
margin: 0;
|
|
3041
|
+
border: var(--lutra-toggle-border-width) solid var(--lutra-toggle-border);
|
|
3042
|
+
border-radius: var(--lutra-radius-full);
|
|
3043
|
+
background-color: var(--lutra-toggle-track-off);
|
|
3044
|
+
cursor: pointer;
|
|
3045
|
+
appearance: none;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
/* The thumb. Its position is the primary signal — colour reinforces it. */
|
|
3049
|
+
|
|
3050
|
+
.lutra-toggle::after {
|
|
3051
|
+
content: '';
|
|
3052
|
+
position: absolute;
|
|
3053
|
+
inset-block-start: 50%;
|
|
3054
|
+
inset-inline-start: calc(
|
|
3055
|
+
(var(--lutra-toggle-track-block-size) - var(--lutra-toggle-thumb-size)) / 2 -
|
|
3056
|
+
var(--lutra-toggle-border-width)
|
|
3057
|
+
);
|
|
3058
|
+
transform: translateY(-50%);
|
|
3059
|
+
inline-size: var(--lutra-toggle-thumb-size);
|
|
3060
|
+
block-size: var(--lutra-toggle-thumb-size);
|
|
3061
|
+
border-radius: var(--lutra-radius-full);
|
|
3062
|
+
background-color: var(--lutra-toggle-thumb-off);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
.lutra-toggle:checked {
|
|
3066
|
+
border-color: var(--lutra-toggle-track-on);
|
|
3067
|
+
background-color: var(--lutra-toggle-track-on);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
.lutra-toggle:checked::after {
|
|
3071
|
+
inset-inline-start: auto;
|
|
3072
|
+
inset-inline-end: calc(
|
|
3073
|
+
(var(--lutra-toggle-track-block-size) - var(--lutra-toggle-thumb-size)) / 2 -
|
|
3074
|
+
var(--lutra-toggle-border-width)
|
|
3075
|
+
);
|
|
3076
|
+
background-color: var(--lutra-toggle-thumb-on);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
/* Interaction states. */
|
|
3080
|
+
|
|
3081
|
+
.lutra-toggle:hover:not(:disabled) {
|
|
3082
|
+
border-color: var(--lutra-toggle-border-hover);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
.lutra-toggle:checked:hover:not(:disabled) {
|
|
3086
|
+
border-color: var(--lutra-toggle-track-on-hover);
|
|
3087
|
+
background-color: var(--lutra-toggle-track-on-hover);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
.lutra-toggle:focus-visible {
|
|
3091
|
+
outline: var(--lutra-toggle-focus-ring-width) solid var(--lutra-toggle-focus);
|
|
3092
|
+
outline-offset: var(--lutra-toggle-focus-ring-offset);
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
/* Invalid — visual only; see `toggle.md`. */
|
|
3096
|
+
|
|
3097
|
+
.lutra-toggle[data-invalid] {
|
|
3098
|
+
border-color: var(--lutra-toggle-border-invalid);
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
/* Disabled. */
|
|
3102
|
+
|
|
3103
|
+
.lutra-toggle:disabled {
|
|
3104
|
+
background-color: var(--lutra-toggle-track-disabled);
|
|
3105
|
+
cursor: not-allowed;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
.lutra-toggle:disabled::after {
|
|
3109
|
+
background-color: var(--lutra-toggle-thumb-disabled);
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
/* Forced-colours modes: the system replaces the track fill, so the thumb takes the system text
|
|
3113
|
+
* colour to stay visible, and the border carries the on/off difference. */
|
|
3114
|
+
|
|
3115
|
+
@media (forced-colors: active) {
|
|
3116
|
+
.lutra-toggle::after {
|
|
3117
|
+
background-color: CanvasText;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
.lutra-toggle:disabled::after {
|
|
3121
|
+
background-color: GrayText;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
/*
|
|
3126
|
+
* The trailing-control variant of the choice row.
|
|
3127
|
+
*
|
|
3128
|
+
* Toggle Item puts the label first and the switch at the end — a settings-list convention, where the
|
|
3129
|
+
* name of the setting reads first and the switches line up down the right edge. Checkbox Item and
|
|
3130
|
+
* Radio Item put the control first. Both use `.lutra-choice`; only the order and the gap differ.
|
|
3131
|
+
*
|
|
3132
|
+
* The DOM order matches the visual order, so reading order and Tab order agree with what is drawn.
|
|
3133
|
+
*/
|
|
3134
|
+
|
|
3135
|
+
.lutra-choice--trailing {
|
|
3136
|
+
--lutra-choice-gap: var(--lutra-space-lg);
|
|
3137
|
+
|
|
3138
|
+
justify-content: space-between;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
.lutra-choice--trailing .lutra-choice__content {
|
|
3142
|
+
flex: 1 1 auto;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
/* Aligned to the first line of the label rather than the top of the row, so a two-line label does
|
|
3146
|
+
* not leave the switch floating. */
|
|
3147
|
+
.lutra-choice--trailing > .lutra-toggle {
|
|
3148
|
+
margin-block-start: calc(
|
|
3149
|
+
(var(--lutra-choice-label-line-height) * 1em - var(--lutra-toggle-track-block-size)) / 2
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
.lutra-choice[data-disabled='true'] > .lutra-toggle {
|
|
3154
|
+
cursor: not-allowed;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
/*
|
|
3158
|
+
* Field.
|
|
3159
|
+
*
|
|
3160
|
+
* These classes are shared: `Field` and `SelectField` render the same shell around different
|
|
3161
|
+
* controls, so both use `.lutra-field*`. The control inside brings its own styling.
|
|
3162
|
+
*
|
|
3163
|
+
* Order is label → hint → control → error, expressed as source order in a vertical stack rather
|
|
3164
|
+
* than by any reordering property, so the visual order and the DOM order — which is the order a
|
|
3165
|
+
* screen reader and the Tab key follow — are the same thing.
|
|
3166
|
+
*
|
|
3167
|
+
* Figma composes the field with 4px auto-layout spacing and draws its supporting text below the
|
|
3168
|
+
* control, because the component models one supporting slot that becomes the error when invalid.
|
|
3169
|
+
* The accessibility annotation is the behavioural contract and puts the error below the control
|
|
3170
|
+
* while a hint is guidance read before typing, so the implementation splits them. See `field.md`.
|
|
3171
|
+
*/
|
|
3172
|
+
|
|
3173
|
+
.lutra-field {
|
|
3174
|
+
--lutra-field-gap: var(--lutra-space-xs);
|
|
3175
|
+
--lutra-field-label-color: var(--lutra-color-text-primary);
|
|
3176
|
+
--lutra-field-label-size: var(--lutra-text-body-small);
|
|
3177
|
+
--lutra-field-label-line-height: var(--lutra-type-line-height-body-small);
|
|
3178
|
+
|
|
3179
|
+
/* NO FIGMA VARIABLE. The label and hint are the same size, so weight is what separates them.
|
|
3180
|
+
* Figma binds no weight variable on these text nodes. */
|
|
3181
|
+
--lutra-field-label-weight: var(--lutra-type-weight-medium);
|
|
3182
|
+
--lutra-field-hint-weight: var(--lutra-type-weight-regular);
|
|
3183
|
+
|
|
3184
|
+
/* Figma draws the Field hint in text/primary. Field Group draws its hint in text/secondary.
|
|
3185
|
+
* Both are reproduced as drawn; the divergence is recorded in `field.md` for the design system
|
|
3186
|
+
* to reconcile rather than being resolved unilaterally here. */
|
|
3187
|
+
--lutra-field-hint-color: var(--lutra-color-text-primary);
|
|
3188
|
+
--lutra-field-error-color: var(--lutra-component-field-error-text);
|
|
3189
|
+
--lutra-field-indicator-color: var(--lutra-color-text-primary);
|
|
3190
|
+
--lutra-field-indicator-required-color: var(--lutra-component-field-error-text);
|
|
3191
|
+
|
|
3192
|
+
display: flex;
|
|
3193
|
+
flex-direction: column;
|
|
3194
|
+
gap: var(--lutra-field-gap);
|
|
3195
|
+
font-family: var(--lutra-type-family-sans);
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
/* Normal inline flow rather than flex, so the indicator wraps inline with the label text the way
|
|
3199
|
+
* the annotation describes, and so the space between them is a real text space that the accessible
|
|
3200
|
+
* name includes. A flex `gap` would look right and read as "Email addressRequired". */
|
|
3201
|
+
|
|
3202
|
+
.lutra-field__label {
|
|
3203
|
+
display: block;
|
|
3204
|
+
font-size: var(--lutra-field-label-size);
|
|
3205
|
+
line-height: var(--lutra-field-label-line-height);
|
|
3206
|
+
font-weight: var(--lutra-field-label-weight);
|
|
3207
|
+
color: var(--lutra-field-label-color);
|
|
3208
|
+
|
|
3209
|
+
/* A long label wraps and grows the field. Nothing here sets a height. */
|
|
3210
|
+
overflow-wrap: break-word;
|
|
3211
|
+
text-wrap: pretty;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
/* The indicator wraps inline with the label text rather than sitting in its own column, so a long
|
|
3215
|
+
* label pushes it onto the next line instead of squeezing the label. It is inside the <label>, so
|
|
3216
|
+
* it is part of the accessible name. */
|
|
3217
|
+
|
|
3218
|
+
.lutra-field__indicator {
|
|
3219
|
+
font-weight: var(--lutra-field-hint-weight);
|
|
3220
|
+
color: var(--lutra-field-indicator-color);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
.lutra-field__indicator[data-indicator='required'] {
|
|
3224
|
+
color: var(--lutra-field-indicator-required-color);
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
.lutra-field__hint,
|
|
3228
|
+
.lutra-field__error {
|
|
3229
|
+
margin: 0;
|
|
3230
|
+
font-size: var(--lutra-field-label-size);
|
|
3231
|
+
line-height: var(--lutra-field-label-line-height);
|
|
3232
|
+
font-weight: var(--lutra-field-hint-weight);
|
|
3233
|
+
overflow-wrap: break-word;
|
|
3234
|
+
text-wrap: pretty;
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
.lutra-field__hint {
|
|
3238
|
+
color: var(--lutra-field-hint-color);
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
.lutra-field__error {
|
|
3242
|
+
color: var(--lutra-field-error-color);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
/* Invalid.
|
|
3246
|
+
*
|
|
3247
|
+
* The label takes the error colour, which is what Figma draws. Colour is never the only signal: the
|
|
3248
|
+
* control carries `aria-invalid`, and the error message is text rather than a coloured decoration.
|
|
3249
|
+
*/
|
|
3250
|
+
|
|
3251
|
+
.lutra-field[data-invalid='true'] .lutra-field__label {
|
|
3252
|
+
color: var(--lutra-field-error-color);
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
/*
|
|
3256
|
+
* Field Group.
|
|
3257
|
+
*
|
|
3258
|
+
* A native `<fieldset>` and `<legend>`. Both come with default browser styling that has to be
|
|
3259
|
+
* cleared before they can be laid out — a fieldset has a border and padding, and a legend sits
|
|
3260
|
+
* inside that border with its own inline behaviour. Everything below the reset is the design.
|
|
3261
|
+
*
|
|
3262
|
+
* Order is legend → hint → controls → error, matching Field, expressed as source order so the drawn
|
|
3263
|
+
* order and the reading order are the same.
|
|
3264
|
+
*/
|
|
3265
|
+
|
|
3266
|
+
.lutra-field-group {
|
|
3267
|
+
--lutra-field-group-gap: var(--lutra-space-sm);
|
|
3268
|
+
--lutra-field-group-item-gap: var(--lutra-space-md);
|
|
3269
|
+
--lutra-field-group-legend-color: var(--lutra-color-text-primary);
|
|
3270
|
+
--lutra-field-group-legend-size: var(--lutra-text-body);
|
|
3271
|
+
--lutra-field-group-legend-line-height: var(--lutra-type-line-height-body);
|
|
3272
|
+
--lutra-field-group-hint-color: var(--lutra-color-text-secondary);
|
|
3273
|
+
--lutra-field-group-hint-size: var(--lutra-text-body-small);
|
|
3274
|
+
--lutra-field-group-hint-line-height: var(--lutra-type-line-height-body-small);
|
|
3275
|
+
--lutra-field-group-error-color: var(--lutra-component-field-error-text);
|
|
3276
|
+
--lutra-field-group-error-gap: var(--lutra-space-xs);
|
|
3277
|
+
|
|
3278
|
+
/* NO FIGMA VARIABLE. Figma binds no weight on the legend; it is heavier than the options so the
|
|
3279
|
+
* question reads before the answers. */
|
|
3280
|
+
--lutra-field-group-legend-weight: var(--lutra-type-weight-medium);
|
|
3281
|
+
|
|
3282
|
+
/* Clearing the browser's own fieldset chrome. */
|
|
3283
|
+
min-inline-size: 0;
|
|
3284
|
+
margin: 0;
|
|
3285
|
+
padding: 0;
|
|
3286
|
+
border: 0;
|
|
3287
|
+
|
|
3288
|
+
display: flex;
|
|
3289
|
+
flex-direction: column;
|
|
3290
|
+
gap: var(--lutra-field-group-gap);
|
|
3291
|
+
font-family: var(--lutra-type-family-sans);
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
/* A legend is not an ordinary flex item — browsers give it special placement inside the fieldset
|
|
3295
|
+
* border. `float: left` with a full-width clear is the reliable way to make it behave as a normal
|
|
3296
|
+
* block at the top of the stack without losing its semantics. */
|
|
3297
|
+
|
|
3298
|
+
.lutra-field-group__legend {
|
|
3299
|
+
float: left;
|
|
3300
|
+
inline-size: 100%;
|
|
3301
|
+
padding: 0;
|
|
3302
|
+
font-size: var(--lutra-field-group-legend-size);
|
|
3303
|
+
line-height: var(--lutra-field-group-legend-line-height);
|
|
3304
|
+
font-weight: var(--lutra-field-group-legend-weight);
|
|
3305
|
+
color: var(--lutra-field-group-legend-color);
|
|
3306
|
+
overflow-wrap: break-word;
|
|
3307
|
+
text-wrap: pretty;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
.lutra-field-group__legend + * {
|
|
3311
|
+
clear: both;
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
.lutra-field-group__hint {
|
|
3315
|
+
margin: 0;
|
|
3316
|
+
font-size: var(--lutra-field-group-hint-size);
|
|
3317
|
+
line-height: var(--lutra-field-group-hint-line-height);
|
|
3318
|
+
color: var(--lutra-field-group-hint-color);
|
|
3319
|
+
overflow-wrap: break-word;
|
|
3320
|
+
text-wrap: pretty;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
.lutra-field-group__items {
|
|
3324
|
+
display: flex;
|
|
3325
|
+
flex-direction: column;
|
|
3326
|
+
gap: var(--lutra-field-group-item-gap);
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/* The error.
|
|
3330
|
+
*
|
|
3331
|
+
* An icon beside the message, which is what Figma draws here — and what Field's error does not have.
|
|
3332
|
+
* The difference is reproduced rather than reconciled; see `field-group.md`. The icon is decorative,
|
|
3333
|
+
* so the signal is the message text and the shape, never the colour alone.
|
|
3334
|
+
*/
|
|
3335
|
+
|
|
3336
|
+
.lutra-field-group__error {
|
|
3337
|
+
display: flex;
|
|
3338
|
+
align-items: flex-start;
|
|
3339
|
+
gap: var(--lutra-field-group-error-gap);
|
|
3340
|
+
margin: 0;
|
|
3341
|
+
font-size: var(--lutra-field-group-hint-size);
|
|
3342
|
+
line-height: var(--lutra-field-group-hint-line-height);
|
|
3343
|
+
color: var(--lutra-field-group-error-color);
|
|
3344
|
+
overflow-wrap: break-word;
|
|
3345
|
+
text-wrap: pretty;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
.lutra-field-group__error-icon {
|
|
3349
|
+
flex: 0 0 auto;
|
|
3350
|
+
inline-size: 1rem;
|
|
3351
|
+
block-size: 1rem;
|
|
3352
|
+
|
|
3353
|
+
/* Nudged onto the first line of the message rather than the top of its box. */
|
|
3354
|
+
margin-block-start: calc((var(--lutra-field-group-hint-line-height) * 1em - 1rem) / 2);
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
/*
|
|
3358
|
+
* Error Summary.
|
|
3359
|
+
*
|
|
3360
|
+
* Shown above the form after a failed submit, and only then. The red border and the alert icon both
|
|
3361
|
+
* carry the meaning, and the heading says it in words, so nothing here depends on colour alone.
|
|
3362
|
+
*/
|
|
3363
|
+
|
|
3364
|
+
.lutra-error-summary {
|
|
3365
|
+
--lutra-error-summary-surface: var(--lutra-color-background-surface);
|
|
3366
|
+
--lutra-error-summary-border: var(--lutra-color-feedback-danger);
|
|
3367
|
+
--lutra-error-summary-border-width: var(--lutra-border-width-thick);
|
|
3368
|
+
--lutra-error-summary-radius: var(--lutra-radius-md);
|
|
3369
|
+
--lutra-error-summary-padding: var(--lutra-space-xl);
|
|
3370
|
+
--lutra-error-summary-gap: var(--lutra-space-md);
|
|
3371
|
+
--lutra-error-summary-row-gap: var(--lutra-space-sm);
|
|
3372
|
+
--lutra-error-summary-title: var(--lutra-color-text-primary);
|
|
3373
|
+
--lutra-error-summary-link: var(--lutra-component-field-error-text);
|
|
3374
|
+
--lutra-error-summary-focus-ring-color: var(--lutra-color-focus-ring);
|
|
3375
|
+
--lutra-error-summary-focus-ring-width: var(--lutra-border-width-thick);
|
|
3376
|
+
|
|
3377
|
+
/* NO FIGMA VARIABLE. The same 2px offset every other Lutra control uses for its focus ring. */
|
|
3378
|
+
--lutra-error-summary-focus-ring-offset: 2px;
|
|
3379
|
+
|
|
3380
|
+
box-sizing: border-box;
|
|
3381
|
+
display: flex;
|
|
3382
|
+
flex-direction: column;
|
|
3383
|
+
gap: var(--lutra-error-summary-gap);
|
|
3384
|
+
padding: var(--lutra-error-summary-padding);
|
|
3385
|
+
|
|
3386
|
+
border: var(--lutra-error-summary-border-width) solid var(--lutra-error-summary-border);
|
|
3387
|
+
border-radius: var(--lutra-error-summary-radius);
|
|
3388
|
+
background-color: var(--lutra-error-summary-surface);
|
|
3389
|
+
font-family: var(--lutra-type-family-sans);
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
/* The container takes focus programmatically, so its ring has to be visible when it does — this is
|
|
3393
|
+
* one of the few places `:focus` rather than `:focus-visible` is right, because the focus was moved
|
|
3394
|
+
* by the application and there is no keystroke for the browser to judge. */
|
|
3395
|
+
|
|
3396
|
+
.lutra-error-summary:focus {
|
|
3397
|
+
outline: var(--lutra-error-summary-focus-ring-width) solid
|
|
3398
|
+
var(--lutra-error-summary-focus-ring-color);
|
|
3399
|
+
outline-offset: var(--lutra-error-summary-focus-ring-offset);
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
.lutra-error-summary__heading-row {
|
|
3403
|
+
display: flex;
|
|
3404
|
+
align-items: flex-start;
|
|
3405
|
+
gap: var(--lutra-error-summary-row-gap);
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
.lutra-error-summary__icon {
|
|
3409
|
+
flex: 0 0 auto;
|
|
3410
|
+
inline-size: 1.5rem;
|
|
3411
|
+
block-size: 1.5rem;
|
|
3412
|
+
color: var(--lutra-error-summary-border);
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
.lutra-error-summary__title {
|
|
3416
|
+
margin: 0;
|
|
3417
|
+
font-size: var(--lutra-text-h3);
|
|
3418
|
+
line-height: var(--lutra-type-line-height-h3);
|
|
3419
|
+
font-weight: var(--lutra-type-weight-semibold);
|
|
3420
|
+
color: var(--lutra-error-summary-title);
|
|
3421
|
+
overflow-wrap: break-word;
|
|
3422
|
+
text-wrap: pretty;
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
.lutra-error-summary__list {
|
|
3426
|
+
display: flex;
|
|
3427
|
+
flex-direction: column;
|
|
3428
|
+
gap: var(--lutra-error-summary-row-gap);
|
|
3429
|
+
margin: 0;
|
|
3430
|
+
padding: 0;
|
|
3431
|
+
|
|
3432
|
+
/* A real list for the count, without the marker — the number of problems is the point, and the
|
|
3433
|
+
* bullets would compete with the link underlines. */
|
|
3434
|
+
list-style: none;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
.lutra-error-summary__link {
|
|
3438
|
+
color: var(--lutra-error-summary-link);
|
|
3439
|
+
font-size: var(--lutra-text-body);
|
|
3440
|
+
line-height: var(--lutra-type-line-height-body);
|
|
3441
|
+
|
|
3442
|
+
/* Underlined at rest: these are links inside a block of red text, and colour alone would not
|
|
3443
|
+
* separate them from the message they sit in. */
|
|
3444
|
+
text-decoration-line: underline;
|
|
3445
|
+
text-decoration-thickness: from-font;
|
|
3446
|
+
text-underline-offset: 0.2em;
|
|
3447
|
+
overflow-wrap: break-word;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
.lutra-error-summary__link:hover {
|
|
3451
|
+
text-decoration-thickness: 0.125em;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
.lutra-error-summary__link:focus-visible {
|
|
3455
|
+
outline: var(--lutra-error-summary-focus-ring-width) solid
|
|
3456
|
+
var(--lutra-error-summary-focus-ring-color);
|
|
3457
|
+
outline-offset: var(--lutra-error-summary-focus-ring-offset);
|
|
3458
|
+
border-radius: var(--lutra-radius-sm);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
/* Forced-colours modes: the red border is not painted, so the border is pinned to the system's own
|
|
3462
|
+
* boundary colour and the focus ring to its highlight. */
|
|
3463
|
+
|
|
3464
|
+
@media (forced-colors: active) {
|
|
3465
|
+
.lutra-error-summary {
|
|
3466
|
+
border-color: CanvasText;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
.lutra-error-summary:focus,
|
|
3470
|
+
.lutra-error-summary__link:focus-visible {
|
|
3471
|
+
outline-color: Highlight;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
|