morethanui 1.0.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/DESIGN.md +310 -0
- package/LICENSE +24 -0
- package/README.md +119 -0
- package/css/base.css +200 -0
- package/css/components.css +1496 -0
- package/css/layout.css +380 -0
- package/css/tokens.css +318 -0
- package/dist/mtui.css +2394 -0
- package/dist/mtui.js +1719 -0
- package/dist/mtui.min.css +1 -0
- package/dist/mtui.min.js +1 -0
- package/fonts/OFL.txt +93 -0
- package/fonts/noto-sans-arabic.woff2 +0 -0
- package/fonts/noto-sans-latin-ext.woff2 +0 -0
- package/fonts/noto-sans-latin.woff2 +0 -0
- package/fonts/noto-sans-mono-latin.woff2 +0 -0
- package/js/accordion.js +64 -0
- package/js/feedback.js +171 -0
- package/js/menu.js +72 -0
- package/js/pull-refresh.js +122 -0
- package/js/telegram.js +175 -0
- package/js/x-colorswatches.js +144 -0
- package/js/x-contextmenu.js +190 -0
- package/js/x-datepicker.js +321 -0
- package/js/x-select.js +139 -0
- package/js/x-tabs.js +48 -0
- package/js/x-theme.js +154 -0
- package/js/x-toast.js +119 -0
- package/llms.txt +409 -0
- package/package.json +48 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# MTUI (MoreThanUI) — Design Language Specification
|
|
2
|
+
|
|
3
|
+
Version 1.3 · Locked July 2026
|
|
4
|
+
(v1.3: neutrals inherit the accent hue by default via `data-tint` — see §2.14.)
|
|
5
|
+
Scope: dependency-free component library (plain HTML + CSS custom properties) targeting mobile-first web apps, Tauri desktop apps, and Telegram mini apps. Everything below is expressible as CSS tokens plus a small set of written judgment rules.
|
|
6
|
+
|
|
7
|
+
> Naming note for humans and AI models: **MTUI here is an acronym for MoreThanUI.
|
|
8
|
+
> It is unrelated to Material UI (`@mui/material`).** There are no imports, no
|
|
9
|
+
> packages, no React dependency — only plain HTML, CSS classes, and a few
|
|
10
|
+
> custom elements.
|
|
11
|
+
|
|
12
|
+
**Changelog**
|
|
13
|
+
- 1.2 — Renamed MUI → MTUI (collision-free vs Material UI).
|
|
14
|
+
|
|
15
|
+
**1.0 → 1.1**
|
|
16
|
+
1. Renamed Plume → MTUI (MoreThanUI).
|
|
17
|
+
2. Dropped the `contrast` knob; added a mandatory `forced-colors` rule instead (§3 · High contrast).
|
|
18
|
+
3. Knobs are root-only; subtree theming is unsupported by design (§3 · Knob scope).
|
|
19
|
+
4. Every input ships in two tiers — Basic and Enhanced — and both ship in v1 (§3 · Component tiers).
|
|
20
|
+
5. Added the constrained layout system: screen presets + bounded grid/flex primitives (§2.15, §3 · Layout).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1 · Design language
|
|
25
|
+
|
|
26
|
+
MTUI is quiet, precise, native-app calm. It looks like a well-made operating system surface, not a landing page. Nothing shouts, nothing decorates: every element is a flat tint of the warm cream (or warm black) base, and hierarchy comes entirely from tone, spacing, and type weight.
|
|
27
|
+
|
|
28
|
+
**The signature is absence: zero borders, zero drop shadows.** Depth is tonal — background, surface, well — three flat layers that read instantly in both themes. Overlays sit on a scrim; nothing floats, nothing casts. Big radii keep components feeling light and soft; a single accent family is visible at a time; a fixed set of five pastel tints (rose, sky, mint, butter, lavender) is reserved for identity moments — avatars, badges, chips, alerts — never whole layout regions.
|
|
29
|
+
|
|
30
|
+
Screens are not laid out freehand. Every screen starts from a layout preset, and anything custom composes through constrained grid/flex primitives — responsive behavior is baked into the system, not reinvented per screen.
|
|
31
|
+
|
|
32
|
+
MTUI always: reacts to the pointer with movement or scale, shows its own loading state in place, contains its own errors, mirrors perfectly in RTL, and speaks in plain sentence-case verbs.
|
|
33
|
+
|
|
34
|
+
MTUI never: draws a hairline to separate things, fades content in and out (fade belongs to deletion and popovers only), uses uppercase letter-spaced labels, uses more than one accent hue in a view, or lets one broken component take the page down with it.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2 · Token specification
|
|
39
|
+
|
|
40
|
+
### 2.1 Neutrals — light theme
|
|
41
|
+
|
|
42
|
+
| Token | Value | Role |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `--bg` | `#F3F0EA` | App background (warm cream) |
|
|
45
|
+
| `--surface` | `#FFFFFF` | Cards, sheets, menus |
|
|
46
|
+
| `--surface-2` | `#EAE6DD` | Wells, fields, secondary buttons, skeletons |
|
|
47
|
+
| `--surface-3` | `#DFD9CD` | Hover state of wells, switch tracks |
|
|
48
|
+
| `--text` | `#1C1A17` | Primary text |
|
|
49
|
+
| `--text-muted` | `#67625A` | Secondary text, hints |
|
|
50
|
+
| `--text-faint` | `#A8A297` | Placeholders, disabled text (decorative only) |
|
|
51
|
+
| `--border` | *unused by design* | Reserved; MTUI separates by tone, not lines (exception: forced-colors mode, §3) |
|
|
52
|
+
| `--inverse` / `--on-inverse` | `#1C1A17` / `#F3F0EA` | Toasts, tooltips |
|
|
53
|
+
| `--scrim` | `rgba(28,26,23,.44)` | Behind dialogs |
|
|
54
|
+
|
|
55
|
+
### 2.2 Neutrals — dark theme
|
|
56
|
+
|
|
57
|
+
| Token | Value |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `--bg` | `#161513` |
|
|
60
|
+
| `--surface` | `#211F1C` |
|
|
61
|
+
| `--surface-2` | `#2B2925` |
|
|
62
|
+
| `--surface-3` | `#38352F` |
|
|
63
|
+
| `--text` | `#F0EDE7` |
|
|
64
|
+
| `--text-muted` | `#A39D92` |
|
|
65
|
+
| `--text-faint` | `#6B665D` |
|
|
66
|
+
| `--inverse` / `--on-inverse` | `#F0EDE7` / `#1C1A17` |
|
|
67
|
+
| `--scrim` | `rgba(0,0,0,.55)` |
|
|
68
|
+
|
|
69
|
+
### 2.3 Accent options (knob: `accent`)
|
|
70
|
+
|
|
71
|
+
One accent visible at a time. `--on-accent` is always `#FFFFFF`. Same solid values in both themes; only the tint swaps.
|
|
72
|
+
|
|
73
|
+
| Accent | `--accent` | `--accent-hover` | `--accent-text` | tint (light) | tint (dark) |
|
|
74
|
+
|---|---|---|---|---|---|
|
|
75
|
+
| clay *(default)* | `#BC4B2A` | `#A93F20` | `#8F3016` | `#F3DCD6` | `#3A2723` |
|
|
76
|
+
| cobalt | `#3556C9` | `#2B49B2` | `#2B44A6` | `#DCE3F7` | `#232B45` |
|
|
77
|
+
| fern | `#2F7A50` | `#276A44` | `#245D3D` | `#DCEEE2` | `#22352A` |
|
|
78
|
+
| plum | `#6E46BD` | `#5F3AA8` | `#56349B` | `#E6E0F2` | `#2E2A3C` |
|
|
79
|
+
|
|
80
|
+
`--focus` = current `--accent`.
|
|
81
|
+
|
|
82
|
+
**Secondary brand color (§2.3b, v1.3).** `--accent` is the *primary* (buttons, switch-on, focus, selected states, and the surface tint). A second family — `--accent-2`, `--accent-2-hover`, `--accent-2-text`, `--accent-2-tint`, `--on-accent-2` — is the *accent* for a second highlight (e.g. `data-variant="accent"` buttons). By default it **mirrors `--accent`** (`--accent-2: var(--accent)` etc.), so the four presets stay monochrome; the custom-theme generator sets it to a different hue when an app wants a distinct primary + accent. Both families use the identical §2.14 derivation, so `--accent-2` carries the same contrast guarantees. The surface tint always follows the **primary**.
|
|
83
|
+
|
|
84
|
+
### 2.4 Status colors
|
|
85
|
+
|
|
86
|
+
| Token | Light | Dark |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `--danger` (solid, white text) | `#B3402F` | `#B3402F` *(keep light value — the lighter `#C9553F` fails AA with white)* |
|
|
89
|
+
| `--danger-text` / `--danger-tint` | `#8F3016` / `#F3DCD6` | `#F0A896` / `#3A2723` |
|
|
90
|
+
| `--success-text` / `--success-tint` | `#245D3D` / `#DCEEE2` | `#8ED1AB` / `#22352A` |
|
|
91
|
+
| `--warning-text` / `--warning-tint` | `#7A5600` / `#F2E8D0` | `#E4C27E` / `#383021` |
|
|
92
|
+
|
|
93
|
+
### 2.5 Identity tints (avatars, chips, alerts — never layout)
|
|
94
|
+
|
|
95
|
+
| Tint | Light bg / text | Dark bg / text |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| rose | `#F3DCD6` / `#8F3016` | `#3A2723` / `#F0A896` |
|
|
98
|
+
| sky | `#DCE3F7` / `#2B44A6` | `#232B45` / `#A9C3EC` |
|
|
99
|
+
| mint | `#DCEEE2` / `#245D3D` | `#22352A` / `#8ED1AB` |
|
|
100
|
+
| butter | `#F2E8D0` / `#7A5600` | `#383021` / `#E4C27E` |
|
|
101
|
+
| lavender | `#E6E0F2` / `#56349B` | `#2E2A3C` / `#C9BCF0` |
|
|
102
|
+
|
|
103
|
+
### 2.6 Contrast ratios (WCAG)
|
|
104
|
+
|
|
105
|
+
All pairs independently re-verified July 2026 (computed, not estimated).
|
|
106
|
+
|
|
107
|
+
| Pair | Ratio | Verdict |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| text / bg (light) | 15.3:1 | AAA |
|
|
110
|
+
| text / surface (light) | 17.4:1 | AAA |
|
|
111
|
+
| text-muted / surface (light) | 6.1:1 | AA |
|
|
112
|
+
| text-muted / bg (light) | 5.3:1 | AA |
|
|
113
|
+
| text-muted / surface-2 (light) | 4.9:1 | AA *(minimal headroom — muted text never goes inside wells at small sizes)* |
|
|
114
|
+
| text / bg (dark) | 15.6:1 | AAA |
|
|
115
|
+
| text / surface (dark) | 14.1:1 | AAA |
|
|
116
|
+
| text-muted / surface (dark) | 6.1:1 | AA |
|
|
117
|
+
| white / clay `#BC4B2A` | 5.0:1 | AA |
|
|
118
|
+
| white / cobalt `#3556C9` | 6.3:1 | AA |
|
|
119
|
+
| white / fern `#2F7A50` | 5.2:1 | AA |
|
|
120
|
+
| white / plum `#6E46BD` | 6.4:1 | AA |
|
|
121
|
+
| white / danger `#B3402F` | 5.7:1 | AA |
|
|
122
|
+
| accent-text / accent-tint (clay, light) | 6.2:1 | AA |
|
|
123
|
+
| tint text / tint bg (all five, both themes) | 5.4–7.9:1 | AA |
|
|
124
|
+
| text-faint | — | decorative only; never carry information |
|
|
125
|
+
|
|
126
|
+
### 2.7 Type
|
|
127
|
+
|
|
128
|
+
One family: **Noto Sans** (+ **Noto Sans Arabic** for `ar`, **Noto Sans Mono** for code blocks only). Weight does the hierarchy work — never letter-spacing, never uppercase.
|
|
129
|
+
|
|
130
|
+
| Style | Spec |
|
|
131
|
+
|---|---|
|
|
132
|
+
| Page title | 800 · 38px / 1.12 · tracking −0.02em |
|
|
133
|
+
| Section title | 700 · 26px / 1.2 · −0.01em |
|
|
134
|
+
| Card title | 700 · 19px / 1.3 |
|
|
135
|
+
| Row title / buttons | 600 · 15–16px / 1.4 |
|
|
136
|
+
| Body | 400 · 16px / 1.6 |
|
|
137
|
+
| Secondary | 400 · 14px / 1.5 · `--text-muted` |
|
|
138
|
+
| Label | 600 · 13px · `--text-muted` |
|
|
139
|
+
| Code | 400 · 13px / 1.7 · Noto Sans Mono, in `--surface-2` block |
|
|
140
|
+
|
|
141
|
+
### 2.8 Spacing
|
|
142
|
+
|
|
143
|
+
4px base: **4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48**. Layout is always flex/grid with `gap`. Gap values come exclusively from this scale — arbitrary values are not part of the system (see §2.15).
|
|
144
|
+
|
|
145
|
+
### 2.9 Radius presets (knob: `radius`)
|
|
146
|
+
|
|
147
|
+
| Token | sharp | soft | round *(default)* |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `--r-ctl` (buttons, switches, segmented) | 8px | 14px | 999px |
|
|
150
|
+
| `--r-card` | 12px | 20px | 24px |
|
|
151
|
+
| `--r-input` | 8px | 12px | 16px |
|
|
152
|
+
| `--r-chip` | 6px | 10px | 999px |
|
|
153
|
+
|
|
154
|
+
### 2.10 Density (knob: `density`)
|
|
155
|
+
|
|
156
|
+
| Token | comfortable | compact |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `--ctl-h` / `--field-h` | 48px | 44px |
|
|
159
|
+
| `--row-h` | 64px | 52px |
|
|
160
|
+
| `--pad` (card padding) | 20px | 14px |
|
|
161
|
+
| `--gap` | 16px | 10px |
|
|
162
|
+
|
|
163
|
+
44px is the floor — compact never goes below it for anything tappable.
|
|
164
|
+
|
|
165
|
+
### 2.11 Elevation (tonal levels)
|
|
166
|
+
|
|
167
|
+
| Level | Means | Used for |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| 0 | `--bg` | Page |
|
|
170
|
+
| 1 | `--surface` | Cards, menus, popovers |
|
|
171
|
+
| 2 | `--surface-2` | Wells, fields, skeletons, secondary buttons |
|
|
172
|
+
| overlay | `--scrim` + level 1 | Dialogs |
|
|
173
|
+
|
|
174
|
+
No `box-shadow` for depth, ever. The two sanctioned box-shadow uses are non-depth: the focus ring (`0 0 0 2px surface, 0 0 0 4px accent`) and the 3px surface-colored ring around stacked avatars. Popovers separate from content with a 6px `--bg`-colored ring, not a shadow.
|
|
175
|
+
|
|
176
|
+
### 2.12 Motion
|
|
177
|
+
|
|
178
|
+
| Token | Value |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `--ease` | `cubic-bezier(.22,.9,.28,1)` |
|
|
181
|
+
| `--spring` | `cubic-bezier(.34,1.2,.4,1)` |
|
|
182
|
+
| Micro feedback (hover/press, color) | 200–250ms `--ease` |
|
|
183
|
+
| Card lift | 280ms `--ease` |
|
|
184
|
+
| Switch / checkbox / radio knobs | 300ms `--spring` |
|
|
185
|
+
| Popover in/out | transform 300ms `--spring` + opacity 220ms `--ease` |
|
|
186
|
+
| Tab pill slide | 350ms `--spring` |
|
|
187
|
+
| Dialog | 400ms `--spring` (+ scrim color 300ms) |
|
|
188
|
+
| Toast | 500ms `--spring` |
|
|
189
|
+
| Delete fade | 350ms opacity + scale(.96) |
|
|
190
|
+
| Skeleton sheen | 1.5s loop, blurred solid bar translating across |
|
|
191
|
+
|
|
192
|
+
Animate **transform and opacity only** (60fps rule). Color/background transitions are allowed for state tints since they don't reflow.
|
|
193
|
+
|
|
194
|
+
### 2.13 Feedback (haptics + sound)
|
|
195
|
+
|
|
196
|
+
Synthetic sine waves via WebAudio — no audio files. Global mute control required.
|
|
197
|
+
tick 880Hz·50ms · toggle-on 520→784Hz · toggle-off 784→520Hz · success 660→880Hz · error triangle 233Hz·160ms. Gain ≤ 0.06.
|
|
198
|
+
Haptics: `navigator.vibrate(8)` on toggles/confirmations, `[20,40,20]` on errors. Only where it means something: toggles, confirmations, arrivals, failures — never on hover or scroll.
|
|
199
|
+
|
|
200
|
+
### 2.14 Theme generator guardrails
|
|
201
|
+
|
|
202
|
+
Brands may generate a custom accent; hue is free, lightness and chroma are clamped so contrast and calm survive:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
--accent: oklch(52% C H) /* C clamped 0.06–0.17 */
|
|
206
|
+
--accent-hover: oklch(47% C H)
|
|
207
|
+
--accent-tint: light oklch(92% min(0.32·C, 0.05) H)
|
|
208
|
+
dark oklch(30% 0.35·C H)
|
|
209
|
+
--tint-text: light oklch(41% 0.85·C H)
|
|
210
|
+
dark oklch(84% 0.45·C H)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
52% lightness at any hue keeps ≥4.5:1 with white text. Type, spacing, and motion are not brand-tunable.
|
|
214
|
+
|
|
215
|
+
**Neutral tinting (`data-tint`, v1.3).** MTUI neutrals were never flat grey — they always carried a low-chroma tint (a fixed warm ~85° hue). As of v1.3 that tint **inherits the accent hue by default** (`data-tint="accent"`), so the whole UI shifts to the brand: a blue accent gives blue-grey surfaces, green gives green-grey — the cohesive behaviour of Telegram/Material, not a warm shell around a foreign accent. `data-tint="warm"` opts back into the fixed warm neutral (the ≤v1.2 look). Only the **hue** follows the accent; every neutral's **lightness and chroma stay fixed**, so the tonal ladder and all §2.6 contrast pairs are unchanged at any accent. Derived with relative-color syntax (`oklch(from var(--accent) L C h)`); webviews without it fall back to the warm neutral, so tinting is a progressive enhancement, never a hard dependency.
|
|
216
|
+
|
|
217
|
+
Ramp — L and C fixed, H = accent hue:
|
|
218
|
+
|
|
219
|
+
| token | light (L · C) | dark (L · C) |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `--bg` | 95.6% · .010 | 19.6% · .010 |
|
|
222
|
+
| `--surface` | 99% · .006 | 24% · .014 |
|
|
223
|
+
| `--surface-2` | 92.6% · .014 | 28.2% · .018 |
|
|
224
|
+
| `--surface-3` | 88.7% · .018 | 33% · .022 |
|
|
225
|
+
| `--text` | 21.9% · .008 | 94.7% · .008 |
|
|
226
|
+
| `--text-muted` | 49.8% · .014 | 69.8% · .016 |
|
|
227
|
+
| `--text-faint` | 71.4% · .017 | 51.2% · .015 |
|
|
228
|
+
|
|
229
|
+
### 2.15 Layout system
|
|
230
|
+
|
|
231
|
+
Layout is a component, not a habit. No screen is laid out with hand-written page CSS; screens start from a **preset**, and custom regions compose through **constrained primitives**. Every value a layout accepts is an enum or a scale step — arbitrary numbers are rejected by omission (there is simply no token for them).
|
|
232
|
+
|
|
233
|
+
**Screen presets** (one per screen, the outermost element after the shell):
|
|
234
|
+
|
|
235
|
+
| Preset | Purpose | Baked-in responsive behavior |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| `shell` | App frame: header + content + navigation | Navigation renders as a bottom tab bar below 768px, a side rail at ≥768px; header is sticky; safe-area insets handled; content region scrolls, frame doesn't |
|
|
238
|
+
| `screen-stack` | Settings, forms, articles, detail pages | Single scrolling column; max-width 40rem centered at ≥768px, full-bleed below; standard `--pad` inline padding; `--gap` stack rhythm |
|
|
239
|
+
| `screen-list` | Feeds, chats, search results | Full-bleed scrolling list region + sticky header slot; list rows manage their own inline padding; pull-to-refresh mounts here |
|
|
240
|
+
| `screen-split` | List + detail (mail, chat apps) | Two panes at ≥900px (side pane 20–24rem, detail flexes); below 900px collapses to a navigation stack (list screen pushes detail screen) |
|
|
241
|
+
| `screen-center` | Auth, onboarding, empty and error states | Content block centered both axes, max-width 24rem, safe-area aware |
|
|
242
|
+
|
|
243
|
+
**Constrained primitives** (inside presets, for custom composition):
|
|
244
|
+
|
|
245
|
+
| Primitive | Options (enums only) |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `stack` | gap: scale step (default `--gap`) · align: start / center / stretch |
|
|
248
|
+
| `row` | gap: scale step · align: start / center / end / between · wrap: on/off |
|
|
249
|
+
| `grid` | Fixed column system: 4 columns (<768px) / 8 (768–1199px) / 12 (≥1200px) · gutter fixed 12px · children take integer spans per breakpoint (e.g. span 4, span-md 4, span-lg 6) · spans clamp to the column count — nothing overflows |
|
|
250
|
+
|
|
251
|
+
Constraints, by design: no arbitrary gap values, no custom grid templates, no absolute positioning in app code (only components position absolutely, internally), no fixed pixel widths on content regions. If a layout can't be expressed with presets + primitives, that's a signal to add a preset to the system — not to hand-roll the screen.
|
|
252
|
+
|
|
253
|
+
**Diagnostics:** every layout preset and primitive accepts an `outline` option that renders dashed region/column/span overlays (the dashed lines are a build-time diagnostic, not UI, and are exempt from the no-borders rule).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 3 · Look-and-feel rules
|
|
258
|
+
|
|
259
|
+
**Borders vs shadows.** Neither, on-page. Separation is a change of surface tone. If two areas need dividing, give them different surfaces or more gap — never a line. Inputs are wells (`--surface-2` on `--surface`); when a card sits on `--bg` it is white and needs nothing else.
|
|
260
|
+
|
|
261
|
+
**Overlay exception (v1.3).** Floating overlay surfaces — the shared `.menu` popover foundation (popover menu, and the `x-select` / `x-datepicker` / `x-colorswatches` / `x-contextmenu` panels) — may carry a single 1px `--surface-3` tonal border. They open over an unpredictable backdrop (another surface, a card, an arbitrary pointer position), where a tone-only edge or the `--ring-popover` bg-ring reads as a stray line; a subtle tonal border reads as intentional against anything. This is the ONLY on-screen border exception — it applies to floating overlays only; every on-page component stays strictly borderless. (`--ring-popover` remains for raised panels that sit on the page background, where the ring works.)
|
|
262
|
+
|
|
263
|
+
**High contrast (forced-colors).** A sanctioned exception to zero borders. Under `@media (forced-colors: active)`, tonal separation disappears, so every component draws real borders using system colors (`CanvasText`, `ButtonBorder`), the focus ring becomes a standard `outline`, and skeleton sheens/scrims fall back to system defaults. This is a mandatory per-component CSS block, not a knob and not part of the visible design language.
|
|
264
|
+
|
|
265
|
+
**Knob scope.** All knobs (`theme`, `radius`, `density`, `accent`) are set once, on `<html>`, as data attributes. Subtree theming is unsupported and undocumented by design — this is what structurally enforces "one accent per view" and "never mix radius presets within one screen" instead of relying on prose rules.
|
|
266
|
+
|
|
267
|
+
**Component tiers.** Every component declares its tier, and inputs ship in two variants — both included in v1:
|
|
268
|
+
- **Basic** — the styled native element (or CSS-only construction). Works with zero JS. This is the fallback that always exists underneath.
|
|
269
|
+
- **Enhanced** — the MTUI-drawn version (custom select panel, date picker, color swatches, context menu, pull-to-refresh, toasts, sound/haptics). Enhanced is the *default face* of the library: apps show MTUI-drawn inputs, never browser chrome. Each Enhanced component progressively upgrades its Basic markup, so removing JS degrades every screen to Basic, functional and on-language.
|
|
270
|
+
|
|
271
|
+
**Elevation.** Three tonal levels, used in order, never skipped (no well directly on bg without a surface between, except full-screen wells like segmented controls). Anything above the page (menu, select panel, tooltip, toast, dialog) is level 1 on a scrim or bg-colored ring.
|
|
272
|
+
|
|
273
|
+
**Hover / press / focus.** Everything interactive responds: hover = background tone shift and/or −2px translateY (cards −3px); press = scale(.96–.98); focus-visible = 2px accent ring with a 2px surface gap. Rows may also nudge 3px inline-start. Movement and scale, not fade.
|
|
274
|
+
|
|
275
|
+
**Loading.** Any action that isn't instant swaps its label for a spinner in place (button keeps its size). Any layout waiting on data shows sheen skeletons matching the final shape — a moving blurred bar, never an opacity pulse. Pull-to-refresh ships with every scrolling list: indicator rotates with the pull, springs back, resolves in a toast.
|
|
276
|
+
|
|
277
|
+
**Errors.** Every component is its own boundary. Failure renders inside the component's own footprint — tinted rose panel, plain sentence, Retry button that shows a skeleton while retrying. The page around it never re-renders or breaks.
|
|
278
|
+
|
|
279
|
+
**Toasts.** Drop from the top center like a dynamic island: spring down + scale up, stack vertically (max 3), each leaves by sliding up. Inverse surface, pill radius, one short sentence, optional single action.
|
|
280
|
+
|
|
281
|
+
**Deletion.** Always confirm in a dialog first; on confirm the element fades + scales to .96 and collapses. Fade is reserved for exactly this (and popover exit).
|
|
282
|
+
|
|
283
|
+
**Inputs.** Every picker (select, date, color, context menu) is an MTUI-drawn Enhanced component, not browser chrome; the Basic native element remains underneath as the no-JS fallback. Right-click gets the MTUI context menu. Color pickers are curated swatches, never a free wheel.
|
|
284
|
+
|
|
285
|
+
**Layout.** Screens are composed, not drawn: pick a screen preset, fill its slots with components, use stack/row/grid for custom regions. Responsive behavior lives in the presets — app code never writes media queries for layout. The `outline` diagnostic is the first tool for debugging a layout, and it never ships on.
|
|
286
|
+
|
|
287
|
+
**RTL.** Only logical properties (`inline-start/end`, `text-align:start`); direction comes from the document. Numerals, emails, and code stay LTR inside RTL rows. Directional chevrons flip with direction.
|
|
288
|
+
|
|
289
|
+
**Dark mode beyond colors.** Same tonal ladder — surfaces get *lighter* as they rise. The neutral hue follows the accent under `data-tint` (§2.14): warm by default only under `data-tint="warm"`, otherwise blue-grey, green-grey, etc. to match the brand. Tints drop to low-luminance versions with light same-hue text; solid accents stay identical; scrim deepens. Nothing else changes: no dark-only borders, no added glow.
|
|
290
|
+
|
|
291
|
+
**Density.** Compact tightens padding, rows and gaps but never type sizes or the 44px touch floor. It's a workspace/desktop affordance, not a phone one.
|
|
292
|
+
|
|
293
|
+
**Icons.** Lucide, 2px stroke, `currentColor`, 15–20px inline with text. No emoji, no filled icon styles, no icon fonts.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 4 · Do / don't
|
|
298
|
+
|
|
299
|
+
1. **Do** separate regions with tone and gap; **never** draw a border, hairline divider, or outline (exceptions: the floating-overlay border per §2.9 above, forced-colors mode, and the layout `outline` diagnostic — which never ships on).
|
|
300
|
+
2. **Do** move or scale on interaction; **never** fade content in/out except deletion and popover exit.
|
|
301
|
+
3. **Do** show a spinner inside the control and skeletons in the layout while waiting; **never** block the page or show a bare white gap.
|
|
302
|
+
4. **Do** contain failures in the failing component with a Retry; **never** let one error blank or crash the screen.
|
|
303
|
+
5. **Do** use one accent family per app, with tints for identity moments; **never** show two accent hues in one view or tint a whole layout region pastel.
|
|
304
|
+
6. **Do** build hierarchy with Noto Sans weight and size; **never** use uppercase tracking, a second display face, or mono outside code blocks.
|
|
305
|
+
7. **Do** use logical properties everywhere; **never** write `left`/`right`/`margin-left` in component CSS.
|
|
306
|
+
8. **Do** keep every tap target ≥44px and every text/background pair ≥4.5:1; **never** put information in `--text-faint`.
|
|
307
|
+
9. **Do** use pill/round radii by default; **never** mix radius presets within one screen — knobs live on `<html>` only, never on subtrees.
|
|
308
|
+
10. **Do** confirm before destroying, then fade; **never** delete instantly on a single tap, and never play sound or haptics without a matching visible event.
|
|
309
|
+
11. **Do** start every screen from a layout preset and compose custom regions with stack/row/grid; **never** hand-write page layout CSS, media queries, absolute positioning, or arbitrary gaps in app code.
|
|
310
|
+
12. **Do** show MTUI-drawn (Enhanced) inputs as the default face of every app; **never** expose raw browser chrome for pickers — and every Enhanced input keeps its Basic native fallback underneath.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MoreThanUI (MTUI) contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
The bundled fonts in fonts/ are licensed separately under the SIL Open Font
|
|
24
|
+
License; see fonts/OFL.txt.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# MTUI (MoreThanUI)
|
|
2
|
+
|
|
3
|
+
A dependency-free, framework-agnostic UI component library: plain HTML + CSS
|
|
4
|
+
custom properties + a few light-DOM **Web Components**. No framework, no
|
|
5
|
+
runtime packages, **no build required to use it**. Targets mobile-first web
|
|
6
|
+
apps, Tauri desktop apps, and Telegram mini apps.
|
|
7
|
+
|
|
8
|
+
*(MTUI = MoreThanUI. Unrelated to Material UI — there is nothing to import to
|
|
9
|
+
write markup.)*
|
|
10
|
+
|
|
11
|
+
The whole thing is **~19 KB gzipped** (every component + all CSS), minified;
|
|
12
|
+
the self-hosted fonts are the only larger asset and are optional.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
**Copy the files** (most on-brand — zero tooling): drop `css/`, the `js/`
|
|
17
|
+
files you use, and `fonts/` into your project and link them.
|
|
18
|
+
|
|
19
|
+
**npm:**
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install morethanui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**CDN** (no install; jsDelivr mirrors npm):
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/morethanui/dist/mtui.min.css">
|
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/morethanui/dist/mtui.min.js" defer></script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<!doctype html>
|
|
36
|
+
<html data-theme="light" data-accent="clay" data-radius="round" data-density="comfortable">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="utf-8">
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
40
|
+
<link rel="stylesheet" href="node_modules/morethanui/dist/mtui.min.css">
|
|
41
|
+
</head>
|
|
42
|
+
<body class="shell">
|
|
43
|
+
<main class="shell-content">
|
|
44
|
+
<div class="screen-stack">
|
|
45
|
+
<h1 class="t-page">Settings</h1>
|
|
46
|
+
<button class="btn" data-variant="primary">Save changes</button>
|
|
47
|
+
</div>
|
|
48
|
+
</main>
|
|
49
|
+
<script src="node_modules/morethanui/dist/mtui.min.js" defer></script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The four `data-*` attributes on `<html>` are the theme knobs. Copy any
|
|
55
|
+
component from **`llms.txt`** (the complete API — one canonical example each).
|
|
56
|
+
|
|
57
|
+
## Two ways to include it
|
|
58
|
+
|
|
59
|
+
- **Bundle (drop-in / CDN):** `dist/mtui.min.css` + `dist/mtui.min.js` — every
|
|
60
|
+
component, one file each. `import 'morethanui'` (JS) / `import 'morethanui/style.css'`.
|
|
61
|
+
- **À la carte (only what you use):** each component is its own file with **no
|
|
62
|
+
imports between them**, so you pull in exactly what you use:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import 'morethanui/css/tokens.css';
|
|
66
|
+
import 'morethanui/css/base.css';
|
|
67
|
+
import 'morethanui/css/components.css'; // one stylesheet, all component classes
|
|
68
|
+
import 'morethanui/js/x-select.js'; // self-registers <x-select>; import only these
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
(JS is tree-shakeable by file; CSS ships as one `components.css`.)
|
|
72
|
+
|
|
73
|
+
## Frameworks
|
|
74
|
+
|
|
75
|
+
The components are standard Custom Elements (light DOM, no Shadow DOM), so:
|
|
76
|
+
|
|
77
|
+
- **Svelte / Vue / Solid / Angular / vanilla:** work out of the box — attributes
|
|
78
|
+
and slots pass straight through; CSS is global.
|
|
79
|
+
- **React 19+:** first-class (properties and custom events supported).
|
|
80
|
+
- **React ≤18:** the markup + CSS work; for a component's `change` / `select`
|
|
81
|
+
CustomEvent, attach a `ref` + `addEventListener` (React ≤18 doesn't auto-bind
|
|
82
|
+
custom events). Everything else is plain attributes.
|
|
83
|
+
|
|
84
|
+
## Theming
|
|
85
|
+
|
|
86
|
+
Root-only knobs on `<html>`:
|
|
87
|
+
|
|
88
|
+
| Knob | Values |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `data-theme` | `light` · `dark` |
|
|
91
|
+
| `data-accent` | `clay` · `cobalt` · `fern` · `plum` · `custom` |
|
|
92
|
+
| `data-radius` | `round` · `soft` · `sharp` |
|
|
93
|
+
| `data-density` | `comfortable` · `compact` |
|
|
94
|
+
| `data-tint` | `accent` (default — neutrals follow the accent hue) · `warm` |
|
|
95
|
+
|
|
96
|
+
Drop in `<x-theme></x-theme>` for a live in-app appearance panel, or generate a
|
|
97
|
+
custom two-color brand in the showcase.
|
|
98
|
+
|
|
99
|
+
## Docs
|
|
100
|
+
|
|
101
|
+
- **`llms.txt`** — the complete, copy-pasteable API (optimized for AI models too).
|
|
102
|
+
- **`demo/index.html`** — the interactive showcase: every component, live,
|
|
103
|
+
driven by the theme controller (`make dev`).
|
|
104
|
+
- **`DESIGN.md`** — the design language (source of truth).
|
|
105
|
+
|
|
106
|
+
## Develop
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
make dev # serve the showcase
|
|
110
|
+
make test # contrast checks + Playwright component suite
|
|
111
|
+
make bundle # rebuild dist/ (optional; the authored files need no build)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Dev tooling (vite, esbuild, Playwright) is fetched on demand and never becomes
|
|
115
|
+
a runtime dependency — the shipped library stays dependency-free.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
Code: MIT. Bundled fonts (`fonts/`): SIL Open Font License (see `fonts/OFL.txt`).
|
package/css/base.css
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
MTUI (MoreThanUI) — base.css
|
|
3
|
+
Tier: Basic (zero JS). Reset, self-hosted fonts, typography classes
|
|
4
|
+
(DESIGN.md §2.7), and the accessibility floor every component inherits.
|
|
5
|
+
Requires tokens.css loaded first.
|
|
6
|
+
============================================================================ */
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/* ============================== @font-face =================================
|
|
10
|
+
Self-hosted variable fonts (fonts/, OFL licensed). Noto Sans Arabic is
|
|
11
|
+
registered under the SAME family name "Noto Sans" with an Arabic
|
|
12
|
+
unicode-range, so the frozen --font token covers Arabic with no extra
|
|
13
|
+
rules: Latin glyphs resolve from the Latin files, Arabic falls through. */
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: "Noto Sans";
|
|
16
|
+
font-style: normal;
|
|
17
|
+
font-weight: 400 800;
|
|
18
|
+
font-display: swap;
|
|
19
|
+
src: url("../fonts/noto-sans-latin.woff2") format("woff2");
|
|
20
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
|
21
|
+
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
|
22
|
+
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
23
|
+
}
|
|
24
|
+
@font-face {
|
|
25
|
+
font-family: "Noto Sans";
|
|
26
|
+
font-style: normal;
|
|
27
|
+
font-weight: 400 800;
|
|
28
|
+
font-display: swap;
|
|
29
|
+
src: url("../fonts/noto-sans-latin-ext.woff2") format("woff2");
|
|
30
|
+
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
|
|
31
|
+
U+02DD-02FF, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
|
|
32
|
+
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
33
|
+
}
|
|
34
|
+
@font-face {
|
|
35
|
+
font-family: "Noto Sans";
|
|
36
|
+
font-style: normal;
|
|
37
|
+
font-weight: 400 800;
|
|
38
|
+
font-display: swap;
|
|
39
|
+
src: url("../fonts/noto-sans-arabic.woff2") format("woff2");
|
|
40
|
+
unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891,
|
|
41
|
+
U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41,
|
|
42
|
+
U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
|
|
43
|
+
}
|
|
44
|
+
@font-face {
|
|
45
|
+
font-family: "Noto Sans Mono";
|
|
46
|
+
font-style: normal;
|
|
47
|
+
font-weight: 400;
|
|
48
|
+
font-display: swap;
|
|
49
|
+
src: url("../fonts/noto-sans-mono-latin.woff2") format("woff2");
|
|
50
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
|
51
|
+
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
|
52
|
+
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/* ================================ Reset ==================================== */
|
|
57
|
+
*,
|
|
58
|
+
*::before,
|
|
59
|
+
*::after {
|
|
60
|
+
box-sizing: border-box;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
* {
|
|
64
|
+
margin: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
html {
|
|
68
|
+
-webkit-text-size-adjust: 100%;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
body {
|
|
72
|
+
min-height: 100dvh; /* shell presets (layout.css) own scroll; frame never does */
|
|
73
|
+
background: var(--bg);
|
|
74
|
+
color: var(--text);
|
|
75
|
+
font-family: var(--font);
|
|
76
|
+
font-size: var(--fs-body);
|
|
77
|
+
font-weight: var(--fw-body);
|
|
78
|
+
line-height: var(--lh-body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
img,
|
|
82
|
+
svg,
|
|
83
|
+
video,
|
|
84
|
+
canvas {
|
|
85
|
+
display: block;
|
|
86
|
+
max-inline-size: 100%;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
button,
|
|
90
|
+
input,
|
|
91
|
+
select,
|
|
92
|
+
textarea {
|
|
93
|
+
font: inherit;
|
|
94
|
+
color: inherit;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Plain lists in components (menus, rows) carry their own semantics/styles. */
|
|
98
|
+
ul[role="list"],
|
|
99
|
+
ol[role="list"] {
|
|
100
|
+
list-style: none;
|
|
101
|
+
padding: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/* ===================== Typography classes (§2.7) ===========================
|
|
106
|
+
One class per spec row. Hierarchy is weight and size — never uppercase,
|
|
107
|
+
never letter-spacing beyond the two negative headline trackings.
|
|
108
|
+
These classes ARE the type API; headings carry whichever class fits. */
|
|
109
|
+
.t-page {
|
|
110
|
+
font-size: var(--fs-page);
|
|
111
|
+
line-height: var(--lh-page);
|
|
112
|
+
font-weight: var(--fw-page);
|
|
113
|
+
letter-spacing: var(--ls-page);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.t-section {
|
|
117
|
+
font-size: var(--fs-section);
|
|
118
|
+
line-height: var(--lh-section);
|
|
119
|
+
font-weight: var(--fw-section);
|
|
120
|
+
letter-spacing: var(--ls-section);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.t-card {
|
|
124
|
+
font-size: var(--fs-card);
|
|
125
|
+
line-height: var(--lh-card);
|
|
126
|
+
font-weight: var(--fw-card);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.t-row {
|
|
130
|
+
font-size: var(--fs-control);
|
|
131
|
+
line-height: var(--lh-control);
|
|
132
|
+
font-weight: var(--fw-control);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.t-body {
|
|
136
|
+
font-size: var(--fs-body);
|
|
137
|
+
line-height: var(--lh-body);
|
|
138
|
+
font-weight: var(--fw-body);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.t-secondary {
|
|
142
|
+
font-size: var(--fs-secondary);
|
|
143
|
+
line-height: var(--lh-secondary);
|
|
144
|
+
font-weight: var(--fw-body);
|
|
145
|
+
color: var(--text-muted);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.t-label {
|
|
149
|
+
font-size: var(--fs-label);
|
|
150
|
+
font-weight: var(--fw-label);
|
|
151
|
+
color: var(--text-muted);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Type style only — the well-styled block is the phase-4 `.code` component.
|
|
155
|
+
Code, numerals, emails stay LTR inside RTL text (DESIGN.md §3 RTL). */
|
|
156
|
+
.t-code {
|
|
157
|
+
font-family: var(--font-mono);
|
|
158
|
+
font-size: var(--fs-code);
|
|
159
|
+
line-height: var(--lh-code);
|
|
160
|
+
direction: ltr;
|
|
161
|
+
unicode-bidi: isolate;
|
|
162
|
+
text-align: start;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Utility for non-code LTR islands in RTL rows: numbers, emails, URLs. */
|
|
166
|
+
.t-ltr {
|
|
167
|
+
direction: ltr;
|
|
168
|
+
unicode-bidi: isolate;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
/* ========================= Accessibility floor ============================= */
|
|
173
|
+
|
|
174
|
+
/* Focus: the token ring, on everything focusable, keyboard-only. */
|
|
175
|
+
:focus-visible {
|
|
176
|
+
outline: none;
|
|
177
|
+
box-shadow: var(--ring-focus);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Reduced motion: durations already collapse via the --t-* tokens
|
|
181
|
+
(tokens.css). --t-skeleton intentionally survives; the skeleton component
|
|
182
|
+
(components.css) swaps its translating sheen for a non-moving variant
|
|
183
|
+
under this same query. No component declares its own durations, so no
|
|
184
|
+
blanket animation kill-switch is needed here. */
|
|
185
|
+
|
|
186
|
+
/* Forced colors: tonal separation disappears, so the floor draws real
|
|
187
|
+
structure with system colors. Components add their own border blocks
|
|
188
|
+
(CLAUDE.md checklist); this covers the globals. */
|
|
189
|
+
@media (forced-colors: active) {
|
|
190
|
+
:focus-visible {
|
|
191
|
+
outline: 2px solid CanvasText;
|
|
192
|
+
outline-offset: 2px;
|
|
193
|
+
box-shadow: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.t-secondary,
|
|
197
|
+
.t-label {
|
|
198
|
+
color: CanvasText;
|
|
199
|
+
}
|
|
200
|
+
}
|