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/css/layout.css
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
MTUI (MoreThanUI) — layout.css
|
|
3
|
+
Tier: Basic (zero JS). The five screen presets + the three constrained
|
|
4
|
+
primitives + the outline diagnostic (DESIGN.md §2.15).
|
|
5
|
+
Requires tokens.css. App code never writes layout CSS or media queries —
|
|
6
|
+
the media queries in THIS file are the only sanctioned ones.
|
|
7
|
+
Breakpoints: 768px (tab bar → rail, 4 → 8 columns), 900px (split panes),
|
|
8
|
+
1200px (8 → 12 columns).
|
|
9
|
+
============================================================================ */
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/* ================================ shell ====================================
|
|
13
|
+
App frame: header + content + navigation. One per app.
|
|
14
|
+
<body class="shell">
|
|
15
|
+
<header class="shell-header">…</header>
|
|
16
|
+
<nav class="shell-nav">…</nav>
|
|
17
|
+
<main class="shell-content">…</main>
|
|
18
|
+
</body>
|
|
19
|
+
Nav is a bottom tab bar below 768px, an inline-start rail at ≥768px.
|
|
20
|
+
The frame never scrolls; .shell-content is the app's only scroller. */
|
|
21
|
+
.shell {
|
|
22
|
+
display: grid;
|
|
23
|
+
block-size: 100dvh;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
grid-template-areas:
|
|
26
|
+
"header"
|
|
27
|
+
"content"
|
|
28
|
+
"nav";
|
|
29
|
+
grid-template-rows: auto 1fr auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.shell-header {
|
|
33
|
+
grid-area: header;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: var(--gap);
|
|
37
|
+
min-block-size: var(--ctl-h);
|
|
38
|
+
padding-block: var(--sp-8);
|
|
39
|
+
padding-inline: var(--pad);
|
|
40
|
+
padding-block-start: calc(var(--sp-8) + env(safe-area-inset-top, 0px));
|
|
41
|
+
background: var(--bg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.shell-content {
|
|
45
|
+
grid-area: content;
|
|
46
|
+
overflow-y: auto;
|
|
47
|
+
overscroll-behavior: contain;
|
|
48
|
+
-webkit-overflow-scrolling: touch;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.shell-nav {
|
|
52
|
+
grid-area: nav;
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: space-evenly;
|
|
55
|
+
align-items: stretch;
|
|
56
|
+
gap: var(--sp-4);
|
|
57
|
+
background: var(--surface);
|
|
58
|
+
padding-block-end: env(safe-area-inset-bottom, 0px);
|
|
59
|
+
-webkit-user-select: none;
|
|
60
|
+
user-select: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.shell-nav > * {
|
|
64
|
+
min-block-size: var(--tap-min);
|
|
65
|
+
min-inline-size: var(--tap-min);
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: var(--sp-4);
|
|
71
|
+
/* Reset native <a>/<button> chrome so tabs read as tabs, not links. */
|
|
72
|
+
border: 0;
|
|
73
|
+
background: none;
|
|
74
|
+
color: var(--text-muted);
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
border-radius: var(--r-ctl);
|
|
77
|
+
font: inherit;
|
|
78
|
+
font-size: var(--fs-label);
|
|
79
|
+
font-weight: var(--fw-label);
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
-webkit-tap-highlight-color: transparent;
|
|
82
|
+
transition:
|
|
83
|
+
color var(--t-micro) var(--ease),
|
|
84
|
+
background-color var(--t-micro) var(--ease);
|
|
85
|
+
}
|
|
86
|
+
.shell-nav > *:hover { color: var(--text); }
|
|
87
|
+
/* Active tab: mark the current item with aria-current (icon + label follow
|
|
88
|
+
currentColor, so the whole tab lights up in the accent). */
|
|
89
|
+
.shell-nav > [aria-current]:not([aria-current="false"]) { color: var(--accent); }
|
|
90
|
+
|
|
91
|
+
@media (min-width: 768px) {
|
|
92
|
+
.shell {
|
|
93
|
+
grid-template-areas:
|
|
94
|
+
"nav header"
|
|
95
|
+
"nav content";
|
|
96
|
+
grid-template-rows: auto 1fr;
|
|
97
|
+
grid-template-columns: auto 1fr;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.shell-nav {
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
justify-content: flex-start;
|
|
103
|
+
padding-block: var(--pad);
|
|
104
|
+
padding-inline: var(--sp-8);
|
|
105
|
+
padding-block-end: var(--pad);
|
|
106
|
+
inline-size: 5rem;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
/* ============================ screen presets ================================
|
|
112
|
+
One preset per screen, the outermost element inside .shell-content (or
|
|
113
|
+
<body> in shell-less pages). Presets own responsive behavior; screens
|
|
114
|
+
never add their own. */
|
|
115
|
+
|
|
116
|
+
/* screen-stack — settings, forms, articles, detail pages.
|
|
117
|
+
Single scrolling column; max-width 40rem centered ≥768px, full-bleed
|
|
118
|
+
below; --pad inline padding; --gap rhythm. */
|
|
119
|
+
.screen-stack {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
gap: var(--gap);
|
|
123
|
+
padding-block: var(--pad);
|
|
124
|
+
padding-inline: var(--pad);
|
|
125
|
+
inline-size: 100%;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@media (min-width: 768px) {
|
|
129
|
+
.screen-stack {
|
|
130
|
+
max-inline-size: calc(40rem + 2 * var(--pad));
|
|
131
|
+
margin-inline: auto;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* screen-list — feeds, chats, search results.
|
|
136
|
+
Full-bleed scroll region + sticky header slot; rows manage their own
|
|
137
|
+
inline padding. Pull-to-refresh (phase 6) mounts here. */
|
|
138
|
+
.screen-list {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
min-block-size: 100%;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.screen-list > header {
|
|
145
|
+
position: sticky;
|
|
146
|
+
inset-block-start: 0;
|
|
147
|
+
z-index: 1;
|
|
148
|
+
background: var(--bg);
|
|
149
|
+
padding-block: var(--sp-8);
|
|
150
|
+
padding-inline: var(--pad);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* screen-split — list + detail (mail, chat apps).
|
|
154
|
+
Two panes at ≥900px: side pane clamped 20–24rem, detail flexes; each
|
|
155
|
+
pane scrolls independently. Below 900px exactly one pane shows —
|
|
156
|
+
data-show="detail" on the preset switches from the side pane to the
|
|
157
|
+
detail pane (app toggles it; the push animation is a later enhancement). */
|
|
158
|
+
.screen-split {
|
|
159
|
+
display: grid;
|
|
160
|
+
block-size: 100%;
|
|
161
|
+
overflow: hidden;
|
|
162
|
+
grid-template-columns: 1fr;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.screen-split > * {
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
overscroll-behavior: contain;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (max-width: 899.98px) {
|
|
171
|
+
.screen-split > :last-child { display: none; }
|
|
172
|
+
.screen-split[data-show="detail"] > :first-child { display: none; }
|
|
173
|
+
.screen-split[data-show="detail"] > :last-child { display: block; }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@media (min-width: 900px) {
|
|
177
|
+
.screen-split {
|
|
178
|
+
grid-template-columns: clamp(20rem, 30%, 24rem) 1fr;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* screen-center — auth, onboarding, empty and error states.
|
|
183
|
+
Content block centered both axes, max-width 24rem, safe-area aware. */
|
|
184
|
+
.screen-center {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
gap: var(--gap);
|
|
190
|
+
min-block-size: 100%;
|
|
191
|
+
padding-block: var(--pad);
|
|
192
|
+
padding-inline: max(var(--pad), env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.screen-center > * {
|
|
196
|
+
inline-size: 100%;
|
|
197
|
+
max-inline-size: 24rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
/* ============================== primitives =================================
|
|
202
|
+
stack / row / grid — every option is an enum or a scale step. There is
|
|
203
|
+
deliberately no way to express an arbitrary gap, template, or width. */
|
|
204
|
+
|
|
205
|
+
/* stack — vertical flow. gap: scale step (default --gap);
|
|
206
|
+
align: start | center | stretch (default stretch). */
|
|
207
|
+
.stack {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
gap: var(--gap);
|
|
211
|
+
}
|
|
212
|
+
.stack[data-align="start"] { align-items: flex-start; }
|
|
213
|
+
.stack[data-align="center"] { align-items: center; }
|
|
214
|
+
|
|
215
|
+
/* row — horizontal flow. gap: scale step (default --gap);
|
|
216
|
+
align: start | center | end | between (default center); wrap: on. */
|
|
217
|
+
.row {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: var(--gap);
|
|
221
|
+
}
|
|
222
|
+
.row[data-align="start"] { align-items: flex-start; }
|
|
223
|
+
.row[data-align="end"] { align-items: flex-end; }
|
|
224
|
+
.row[data-align="between"] { justify-content: space-between; }
|
|
225
|
+
.row[data-wrap="on"] { flex-wrap: wrap; }
|
|
226
|
+
|
|
227
|
+
/* gap enum — shared by stack and row. Grid gutter is fixed and not here. */
|
|
228
|
+
.stack[data-gap="4"], .row[data-gap="4"] { gap: var(--sp-4); }
|
|
229
|
+
.stack[data-gap="8"], .row[data-gap="8"] { gap: var(--sp-8); }
|
|
230
|
+
.stack[data-gap="12"], .row[data-gap="12"] { gap: var(--sp-12); }
|
|
231
|
+
.stack[data-gap="16"], .row[data-gap="16"] { gap: var(--sp-16); }
|
|
232
|
+
.stack[data-gap="20"], .row[data-gap="20"] { gap: var(--sp-20); }
|
|
233
|
+
.stack[data-gap="24"], .row[data-gap="24"] { gap: var(--sp-24); }
|
|
234
|
+
.stack[data-gap="32"], .row[data-gap="32"] { gap: var(--sp-32); }
|
|
235
|
+
.stack[data-gap="40"], .row[data-gap="40"] { gap: var(--sp-40); }
|
|
236
|
+
.stack[data-gap="48"], .row[data-gap="48"] { gap: var(--sp-48); }
|
|
237
|
+
|
|
238
|
+
/* grid — fixed column system: 4 columns <768px, 8 at 768–1199px, 12 at
|
|
239
|
+
≥1200px; gutter fixed 12px. Children take integer spans per breakpoint
|
|
240
|
+
(data-span, data-span-md, data-span-lg); spans clamp to the column
|
|
241
|
+
count — a span ≥ the column count becomes a full row, nothing overflows.
|
|
242
|
+
Later breakpoints fall back: base → md → lg. */
|
|
243
|
+
.grid {
|
|
244
|
+
display: grid;
|
|
245
|
+
gap: var(--sp-12);
|
|
246
|
+
grid-template-columns: repeat(4, 1fr);
|
|
247
|
+
}
|
|
248
|
+
.grid > * { grid-column: span 1; }
|
|
249
|
+
|
|
250
|
+
@media (max-width: 767.98px) {
|
|
251
|
+
.grid > [data-span="2"] { grid-column: span 2; }
|
|
252
|
+
.grid > [data-span="3"] { grid-column: span 3; }
|
|
253
|
+
.grid > [data-span="4"], .grid > [data-span="5"], .grid > [data-span="6"],
|
|
254
|
+
.grid > [data-span="7"], .grid > [data-span="8"], .grid > [data-span="9"],
|
|
255
|
+
.grid > [data-span="10"], .grid > [data-span="11"], .grid > [data-span="12"] {
|
|
256
|
+
grid-column: 1 / -1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@media (min-width: 768px) and (max-width: 1199.98px) {
|
|
261
|
+
.grid { grid-template-columns: repeat(8, 1fr); }
|
|
262
|
+
.grid > [data-span="2"] { grid-column: span 2; }
|
|
263
|
+
.grid > [data-span="3"] { grid-column: span 3; }
|
|
264
|
+
.grid > [data-span="4"] { grid-column: span 4; }
|
|
265
|
+
.grid > [data-span="5"] { grid-column: span 5; }
|
|
266
|
+
.grid > [data-span="6"] { grid-column: span 6; }
|
|
267
|
+
.grid > [data-span="7"] { grid-column: span 7; }
|
|
268
|
+
.grid > [data-span="8"], .grid > [data-span="9"], .grid > [data-span="10"],
|
|
269
|
+
.grid > [data-span="11"], .grid > [data-span="12"] { grid-column: 1 / -1; }
|
|
270
|
+
/* md overrides base */
|
|
271
|
+
.grid > [data-span-md="1"] { grid-column: span 1; }
|
|
272
|
+
.grid > [data-span-md="2"] { grid-column: span 2; }
|
|
273
|
+
.grid > [data-span-md="3"] { grid-column: span 3; }
|
|
274
|
+
.grid > [data-span-md="4"] { grid-column: span 4; }
|
|
275
|
+
.grid > [data-span-md="5"] { grid-column: span 5; }
|
|
276
|
+
.grid > [data-span-md="6"] { grid-column: span 6; }
|
|
277
|
+
.grid > [data-span-md="7"] { grid-column: span 7; }
|
|
278
|
+
.grid > [data-span-md="8"], .grid > [data-span-md="9"], .grid > [data-span-md="10"],
|
|
279
|
+
.grid > [data-span-md="11"], .grid > [data-span-md="12"] { grid-column: 1 / -1; }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (min-width: 1200px) {
|
|
283
|
+
.grid { grid-template-columns: repeat(12, 1fr); }
|
|
284
|
+
.grid > [data-span="2"] { grid-column: span 2; }
|
|
285
|
+
.grid > [data-span="3"] { grid-column: span 3; }
|
|
286
|
+
.grid > [data-span="4"] { grid-column: span 4; }
|
|
287
|
+
.grid > [data-span="5"] { grid-column: span 5; }
|
|
288
|
+
.grid > [data-span="6"] { grid-column: span 6; }
|
|
289
|
+
.grid > [data-span="7"] { grid-column: span 7; }
|
|
290
|
+
.grid > [data-span="8"] { grid-column: span 8; }
|
|
291
|
+
.grid > [data-span="9"] { grid-column: span 9; }
|
|
292
|
+
.grid > [data-span="10"] { grid-column: span 10; }
|
|
293
|
+
.grid > [data-span="11"] { grid-column: span 11; }
|
|
294
|
+
.grid > [data-span="12"] { grid-column: 1 / -1; }
|
|
295
|
+
/* md persists at lg unless lg overrides */
|
|
296
|
+
.grid > [data-span-md="1"] { grid-column: span 1; }
|
|
297
|
+
.grid > [data-span-md="2"] { grid-column: span 2; }
|
|
298
|
+
.grid > [data-span-md="3"] { grid-column: span 3; }
|
|
299
|
+
.grid > [data-span-md="4"] { grid-column: span 4; }
|
|
300
|
+
.grid > [data-span-md="5"] { grid-column: span 5; }
|
|
301
|
+
.grid > [data-span-md="6"] { grid-column: span 6; }
|
|
302
|
+
.grid > [data-span-md="7"] { grid-column: span 7; }
|
|
303
|
+
.grid > [data-span-md="8"] { grid-column: span 8; }
|
|
304
|
+
.grid > [data-span-md="9"], .grid > [data-span-md="10"],
|
|
305
|
+
.grid > [data-span-md="11"], .grid > [data-span-md="12"] { grid-column: 1 / -1; }
|
|
306
|
+
/* lg overrides md and base */
|
|
307
|
+
.grid > [data-span-lg="1"] { grid-column: span 1; }
|
|
308
|
+
.grid > [data-span-lg="2"] { grid-column: span 2; }
|
|
309
|
+
.grid > [data-span-lg="3"] { grid-column: span 3; }
|
|
310
|
+
.grid > [data-span-lg="4"] { grid-column: span 4; }
|
|
311
|
+
.grid > [data-span-lg="5"] { grid-column: span 5; }
|
|
312
|
+
.grid > [data-span-lg="6"] { grid-column: span 6; }
|
|
313
|
+
.grid > [data-span-lg="7"] { grid-column: span 7; }
|
|
314
|
+
.grid > [data-span-lg="8"] { grid-column: span 8; }
|
|
315
|
+
.grid > [data-span-lg="9"] { grid-column: span 9; }
|
|
316
|
+
.grid > [data-span-lg="10"] { grid-column: span 10; }
|
|
317
|
+
.grid > [data-span-lg="11"] { grid-column: span 11; }
|
|
318
|
+
.grid > [data-span-lg="12"] { grid-column: 1 / -1; }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/* ========================= outline diagnostic ==============================
|
|
323
|
+
data-outline on any preset or primitive draws dashed overlays on itself
|
|
324
|
+
and every layout box inside it. Build-time diagnostic only — exempt from
|
|
325
|
+
the no-borders rule (DESIGN.md §2.15), never ships on. Outlines don't
|
|
326
|
+
affect layout metrics. */
|
|
327
|
+
[data-outline] :is(.stack, .row, .grid, .screen-stack, .screen-list,
|
|
328
|
+
.screen-split, .screen-center, .shell-header, .shell-nav, .shell-content),
|
|
329
|
+
:is(.stack, .row, .grid, .screen-stack, .screen-list,
|
|
330
|
+
.screen-split, .screen-center, .shell)[data-outline] {
|
|
331
|
+
outline: 1px dashed var(--text-muted);
|
|
332
|
+
outline-offset: -1px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
[data-outline] .grid > *,
|
|
336
|
+
.grid[data-outline] > * {
|
|
337
|
+
outline: 1px dashed var(--accent);
|
|
338
|
+
outline-offset: -1px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* Live column count on outlined grids. */
|
|
342
|
+
.grid[data-outline]::before,
|
|
343
|
+
[data-outline] .grid::before {
|
|
344
|
+
grid-column: 1 / -1;
|
|
345
|
+
font-size: var(--fs-label);
|
|
346
|
+
font-weight: var(--fw-label);
|
|
347
|
+
color: var(--accent);
|
|
348
|
+
content: "4 columns";
|
|
349
|
+
}
|
|
350
|
+
@media (min-width: 768px) {
|
|
351
|
+
.grid[data-outline]::before,
|
|
352
|
+
[data-outline] .grid::before { content: "8 columns"; }
|
|
353
|
+
}
|
|
354
|
+
@media (min-width: 1200px) {
|
|
355
|
+
.grid[data-outline]::before,
|
|
356
|
+
[data-outline] .grid::before { content: "12 columns"; }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/* ========================== forced-colors floor ============================
|
|
361
|
+
Tonal region separation disappears; the frame draws real borders. */
|
|
362
|
+
@media (forced-colors: active) {
|
|
363
|
+
.shell-header { border-block-end: 1px solid CanvasText; }
|
|
364
|
+
.shell-nav { border-block-start: 1px solid CanvasText; }
|
|
365
|
+
.screen-list > header { border-block-end: 1px solid CanvasText; }
|
|
366
|
+
/* Active tab is color-only otherwise; give it a system-color cue that
|
|
367
|
+
survives forced colors (accent and muted both collapse to CanvasText). */
|
|
368
|
+
.shell-nav > [aria-current]:not([aria-current="false"]) {
|
|
369
|
+
color: Highlight;
|
|
370
|
+
text-decoration: underline;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@media (min-width: 768px) {
|
|
374
|
+
.shell-header { border-block-end: none; }
|
|
375
|
+
.shell-nav {
|
|
376
|
+
border-block-start: none;
|
|
377
|
+
border-inline-end: 1px solid CanvasText;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
package/css/tokens.css
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
MTUI (MoreThanUI) — tokens.css
|
|
3
|
+
Transcribed from DESIGN.md v1.2 §2. Section banners match spec sections
|
|
4
|
+
so this file can be reviewed table-by-table against the spec.
|
|
5
|
+
----------------------------------------------------------------------------
|
|
6
|
+
Knobs (root-only, set on <html>, enum values only — DESIGN.md §3 Knob scope):
|
|
7
|
+
data-theme = light | dark (default light)
|
|
8
|
+
data-accent = clay | cobalt | fern | plum (default clay)
|
|
9
|
+
data-radius = sharp | soft | round (default round)
|
|
10
|
+
data-density = comfortable | compact (default comfortable)
|
|
11
|
+
data-tint = accent | warm (default accent — neutrals take
|
|
12
|
+
the accent hue; "warm" keeps the
|
|
13
|
+
fixed warm neutral; see §2.14)
|
|
14
|
+
Components reference ONLY the tokens in this file. No other color, size,
|
|
15
|
+
radius, or timing values exist in MTUI.
|
|
16
|
+
============================================================================ */
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/* ========================== §2.1 Neutrals — light ========================== */
|
|
20
|
+
:root {
|
|
21
|
+
color-scheme: light;
|
|
22
|
+
--bg: #F3F0EA; /* app background (warm cream) */
|
|
23
|
+
--surface: #FFFFFF; /* cards, sheets, menus */
|
|
24
|
+
--surface-2: #EAE6DD; /* wells, fields, secondary buttons */
|
|
25
|
+
--surface-3: #DFD9CD; /* hover of wells, switch tracks */
|
|
26
|
+
--text: #1C1A17;
|
|
27
|
+
--text-muted: #67625A;
|
|
28
|
+
--text-faint: #A8A297; /* decorative only — never information */
|
|
29
|
+
--inverse: #1C1A17; /* toasts, tooltips */
|
|
30
|
+
--on-inverse: #F3F0EA;
|
|
31
|
+
--scrim: rgba(28, 26, 23, .44);
|
|
32
|
+
/* --border intentionally does not exist. MTUI separates by tone, not lines.
|
|
33
|
+
The only sanctioned borders are the forced-colors block at the end of
|
|
34
|
+
this file and the layout `outline` diagnostic. */
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ========================== §2.2 Neutrals — dark =========================== */
|
|
38
|
+
[data-theme="dark"] {
|
|
39
|
+
color-scheme: dark;
|
|
40
|
+
--bg: #161513;
|
|
41
|
+
--surface: #211F1C;
|
|
42
|
+
--surface-2: #2B2925;
|
|
43
|
+
--surface-3: #38352F;
|
|
44
|
+
--text: #F0EDE7;
|
|
45
|
+
--text-muted: #A39D92;
|
|
46
|
+
--text-faint: #6B665D;
|
|
47
|
+
--inverse: #F0EDE7;
|
|
48
|
+
--on-inverse: #1C1A17;
|
|
49
|
+
--scrim: rgba(0, 0, 0, .55);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ==================== §2.3 Accent options (knob: accent) ===================
|
|
53
|
+
Solids identical in both themes; tint + tint-text swap per theme.
|
|
54
|
+
Dark tint/text values equal the matching identity tint (§2.5):
|
|
55
|
+
clay→rose, cobalt→sky, fern→mint, plum→lavender.
|
|
56
|
+
--on-accent is always white. --focus is always the current accent. */
|
|
57
|
+
:root {
|
|
58
|
+
--on-accent: #FFFFFF;
|
|
59
|
+
--focus: var(--accent);
|
|
60
|
+
|
|
61
|
+
/* Secondary brand color (§2.3b). Mirrors --accent by default, so the four
|
|
62
|
+
presets stay monochrome; the custom-theme generator overrides these to a
|
|
63
|
+
second hue. Used by data-variant="accent" and available to apps. */
|
|
64
|
+
--accent-2: var(--accent);
|
|
65
|
+
--accent-2-hover: var(--accent-hover);
|
|
66
|
+
--accent-2-text: var(--accent-text);
|
|
67
|
+
--accent-2-tint: var(--accent-tint);
|
|
68
|
+
--on-accent-2: var(--on-accent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* clay (default) */
|
|
72
|
+
:root, [data-accent="clay"] {
|
|
73
|
+
--accent: #BC4B2A;
|
|
74
|
+
--accent-hover: #A93F20;
|
|
75
|
+
--accent-text: #8F3016;
|
|
76
|
+
--accent-tint: #F3DCD6;
|
|
77
|
+
}
|
|
78
|
+
[data-theme="dark"],
|
|
79
|
+
[data-theme="dark"][data-accent="clay"] {
|
|
80
|
+
--accent-text: #F0A896;
|
|
81
|
+
--accent-tint: #3A2723;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* cobalt */
|
|
85
|
+
[data-accent="cobalt"] {
|
|
86
|
+
--accent: #3556C9;
|
|
87
|
+
--accent-hover: #2B49B2;
|
|
88
|
+
--accent-text: #2B44A6;
|
|
89
|
+
--accent-tint: #DCE3F7;
|
|
90
|
+
}
|
|
91
|
+
[data-theme="dark"][data-accent="cobalt"] {
|
|
92
|
+
--accent-text: #A9C3EC;
|
|
93
|
+
--accent-tint: #232B45;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* fern */
|
|
97
|
+
[data-accent="fern"] {
|
|
98
|
+
--accent: #2F7A50;
|
|
99
|
+
--accent-hover: #276A44;
|
|
100
|
+
--accent-text: #245D3D;
|
|
101
|
+
--accent-tint: #DCEEE2;
|
|
102
|
+
}
|
|
103
|
+
[data-theme="dark"][data-accent="fern"] {
|
|
104
|
+
--accent-text: #8ED1AB;
|
|
105
|
+
--accent-tint: #22352A;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* plum */
|
|
109
|
+
[data-accent="plum"] {
|
|
110
|
+
--accent: #6E46BD;
|
|
111
|
+
--accent-hover: #5F3AA8;
|
|
112
|
+
--accent-text: #56349B;
|
|
113
|
+
--accent-tint: #E6E0F2;
|
|
114
|
+
}
|
|
115
|
+
[data-theme="dark"][data-accent="plum"] {
|
|
116
|
+
--accent-text: #C9BCF0;
|
|
117
|
+
--accent-tint: #2E2A3C;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ============ §2.14 Neutral tinting (knob: tint, default "accent") =========
|
|
121
|
+
The §2.1/§2.2 neutrals above are the warm baseline AND the data-tint="warm"
|
|
122
|
+
opt-out AND the fallback for engines without relative-color syntax. By
|
|
123
|
+
default the neutral HUE follows the accent (blue accent -> blue-grey UI,
|
|
124
|
+
like Telegram/Material). Only the hue moves: each token keeps the exact
|
|
125
|
+
lightness + chroma measured from the warm ramp, so the tonal ladder and
|
|
126
|
+
every §2.6 contrast pair hold at any accent. --scrim stays translucent. */
|
|
127
|
+
@supports (color: oklch(from white l c h)) {
|
|
128
|
+
:root:not([data-theme="dark"]):not([data-tint="warm"]) {
|
|
129
|
+
--bg: oklch(from var(--accent) 95.6% 0.010 h);
|
|
130
|
+
--surface: oklch(from var(--accent) 99% 0.006 h); /* pure white -> whisper of hue */
|
|
131
|
+
--surface-2: oklch(from var(--accent) 92.6% 0.014 h);
|
|
132
|
+
--surface-3: oklch(from var(--accent) 88.7% 0.018 h);
|
|
133
|
+
--text: oklch(from var(--accent) 21.9% 0.008 h);
|
|
134
|
+
--text-muted: oklch(from var(--accent) 49.8% 0.014 h);
|
|
135
|
+
--text-faint: oklch(from var(--accent) 71.4% 0.017 h);
|
|
136
|
+
--inverse: oklch(from var(--accent) 21.9% 0.008 h);
|
|
137
|
+
--on-inverse: oklch(from var(--accent) 95.6% 0.010 h);
|
|
138
|
+
}
|
|
139
|
+
[data-theme="dark"]:not([data-tint="warm"]) {
|
|
140
|
+
--bg: oklch(from var(--accent) 19.6% 0.010 h);
|
|
141
|
+
--surface: oklch(from var(--accent) 24% 0.014 h);
|
|
142
|
+
--surface-2: oklch(from var(--accent) 28.2% 0.018 h);
|
|
143
|
+
--surface-3: oklch(from var(--accent) 33% 0.022 h);
|
|
144
|
+
--text: oklch(from var(--accent) 94.7% 0.008 h);
|
|
145
|
+
--text-muted: oklch(from var(--accent) 69.8% 0.016 h);
|
|
146
|
+
--text-faint: oklch(from var(--accent) 51.2% 0.015 h);
|
|
147
|
+
--inverse: oklch(from var(--accent) 94.7% 0.008 h);
|
|
148
|
+
--on-inverse: oklch(from var(--accent) 21.9% 0.008 h);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ========================= §2.4 Status colors ============================== */
|
|
153
|
+
:root {
|
|
154
|
+
--danger: #B3402F; /* solid, white text — same value in dark:
|
|
155
|
+
the lighter #C9553F fails AA with white */
|
|
156
|
+
--danger-text: #8F3016;
|
|
157
|
+
--danger-tint: #F3DCD6;
|
|
158
|
+
--success-text: #245D3D;
|
|
159
|
+
--success-tint: #DCEEE2;
|
|
160
|
+
--warning-text: #7A5600;
|
|
161
|
+
--warning-tint: #F2E8D0;
|
|
162
|
+
}
|
|
163
|
+
[data-theme="dark"] {
|
|
164
|
+
--danger-text: #F0A896;
|
|
165
|
+
--danger-tint: #3A2723;
|
|
166
|
+
--success-text: #8ED1AB;
|
|
167
|
+
--success-tint: #22352A;
|
|
168
|
+
--warning-text: #E4C27E;
|
|
169
|
+
--warning-tint: #383021;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* =========== §2.5 Identity tints (avatars, chips, alerts — never layout) === */
|
|
173
|
+
:root {
|
|
174
|
+
--tint-rose-bg: #F3DCD6; --tint-rose-text: #8F3016;
|
|
175
|
+
--tint-sky-bg: #DCE3F7; --tint-sky-text: #2B44A6;
|
|
176
|
+
--tint-mint-bg: #DCEEE2; --tint-mint-text: #245D3D;
|
|
177
|
+
--tint-butter-bg: #F2E8D0; --tint-butter-text: #7A5600;
|
|
178
|
+
--tint-lavender-bg: #E6E0F2; --tint-lavender-text: #56349B;
|
|
179
|
+
}
|
|
180
|
+
[data-theme="dark"] {
|
|
181
|
+
--tint-rose-bg: #3A2723; --tint-rose-text: #F0A896;
|
|
182
|
+
--tint-sky-bg: #232B45; --tint-sky-text: #A9C3EC;
|
|
183
|
+
--tint-mint-bg: #22352A; --tint-mint-text: #8ED1AB;
|
|
184
|
+
--tint-butter-bg: #383021; --tint-butter-text: #E4C27E;
|
|
185
|
+
--tint-lavender-bg: #2E2A3C; --tint-lavender-text: #C9BCF0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ============================== §2.7 Type ==================================
|
|
189
|
+
One family. Weight does hierarchy — never letter-spacing, never uppercase.
|
|
190
|
+
Font files: self-hosted Noto Sans (+ Noto Sans Arabic for ar, Noto Sans
|
|
191
|
+
Mono for code only). @font-face lives in base.css; system-ui is fallback. */
|
|
192
|
+
:root {
|
|
193
|
+
--font: "Noto Sans", system-ui, sans-serif;
|
|
194
|
+
--font-mono: "Noto Sans Mono", ui-monospace, monospace;
|
|
195
|
+
|
|
196
|
+
/* style: size / line-height / weight / tracking */
|
|
197
|
+
--fs-page: 38px; --lh-page: 1.12; --fw-page: 800; --ls-page: -0.02em;
|
|
198
|
+
--fs-section: 26px; --lh-section: 1.2; --fw-section: 700; --ls-section: -0.01em;
|
|
199
|
+
--fs-card: 19px; --lh-card: 1.3; --fw-card: 700;
|
|
200
|
+
--fs-control: 15px; --lh-control: 1.4; --fw-control: 600; /* buttons, row titles (spec: 15–16) */
|
|
201
|
+
--fs-body: 16px; --lh-body: 1.6; --fw-body: 400; /* inputs use this: 16px floor stops iOS zoom */
|
|
202
|
+
--fs-secondary: 14px; --lh-secondary: 1.5; /* color: --text-muted */
|
|
203
|
+
--fs-label: 13px; --fw-label: 600; /* color: --text-muted */
|
|
204
|
+
--fs-code: 13px; --lh-code: 1.7; /* mono, in --surface-2 block */
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ============================ §2.8 Spacing =================================
|
|
208
|
+
4px base scale. Value-named so the token says what it is. Gaps and padding
|
|
209
|
+
come exclusively from this scale — there is no token for anything else. */
|
|
210
|
+
:root {
|
|
211
|
+
--sp-4: 4px; --sp-8: 8px; --sp-12: 12px;
|
|
212
|
+
--sp-16: 16px; --sp-20: 20px; --sp-24: 24px;
|
|
213
|
+
--sp-32: 32px; --sp-40: 40px; --sp-48: 48px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* =================== §2.9 Radius presets (knob: radius) ==================== */
|
|
217
|
+
:root, [data-radius="round"] { /* default */
|
|
218
|
+
--r-ctl: 999px; /* buttons, switches, segmented */
|
|
219
|
+
--r-card: 24px;
|
|
220
|
+
--r-input: 16px;
|
|
221
|
+
--r-chip: 999px;
|
|
222
|
+
}
|
|
223
|
+
[data-radius="soft"] {
|
|
224
|
+
--r-ctl: 14px;
|
|
225
|
+
--r-card: 20px;
|
|
226
|
+
--r-input: 12px;
|
|
227
|
+
--r-chip: 10px;
|
|
228
|
+
}
|
|
229
|
+
[data-radius="sharp"] {
|
|
230
|
+
--r-ctl: 8px;
|
|
231
|
+
--r-card: 12px;
|
|
232
|
+
--r-input: 8px;
|
|
233
|
+
--r-chip: 6px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ==================== §2.10 Density (knob: density) ========================
|
|
237
|
+
44px is the floor — compact never goes below it for anything tappable. */
|
|
238
|
+
:root, [data-density="comfortable"] {
|
|
239
|
+
--ctl-h: 48px; /* buttons */
|
|
240
|
+
--field-h: 48px; /* inputs, selects */
|
|
241
|
+
--row-h: 64px; /* list rows */
|
|
242
|
+
--pad: 20px; /* card padding */
|
|
243
|
+
--gap: 16px; /* default stack/row gap */
|
|
244
|
+
}
|
|
245
|
+
[data-density="compact"] {
|
|
246
|
+
--ctl-h: 44px;
|
|
247
|
+
--field-h: 44px;
|
|
248
|
+
--row-h: 52px;
|
|
249
|
+
--pad: 14px;
|
|
250
|
+
--gap: 10px;
|
|
251
|
+
}
|
|
252
|
+
:root { --tap-min: 44px; } /* absolute floor, never overridden */
|
|
253
|
+
|
|
254
|
+
/* ======================= §2.11 Elevation (tonal) ===========================
|
|
255
|
+
Levels ARE the neutral ladder: 0 = --bg, 1 = --surface, 2 = --surface-2,
|
|
256
|
+
overlay = --scrim + level 1. No box-shadow for depth, ever.
|
|
257
|
+
The three sanctioned non-depth rings: */
|
|
258
|
+
:root {
|
|
259
|
+
--ring-focus: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
|
|
260
|
+
--ring-avatar: 0 0 0 3px var(--surface); /* stacked avatars */
|
|
261
|
+
--ring-popover: 0 0 0 6px var(--bg); /* popover separation, not shadow */
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ============================ §2.12 Motion =================================
|
|
265
|
+
Animate transform and opacity only (60fps rule). Color/background
|
|
266
|
+
transitions allowed for state tints (no reflow). */
|
|
267
|
+
:root {
|
|
268
|
+
--ease: cubic-bezier(.22, .9, .28, 1);
|
|
269
|
+
--spring: cubic-bezier(.34, 1.2, .4, 1);
|
|
270
|
+
|
|
271
|
+
--t-micro: 220ms; /* hover/press color: 200–250ms --ease */
|
|
272
|
+
--t-lift: 280ms; /* card lift, --ease */
|
|
273
|
+
--t-knob: 300ms; /* switch/checkbox/radio knobs, --spring */
|
|
274
|
+
--t-popover: 300ms; /* transform --spring; opacity 220ms --ease */
|
|
275
|
+
--t-tab: 350ms; /* tab pill slide, --spring */
|
|
276
|
+
--t-delete: 350ms; /* opacity + scale(.96) — fade reserved for this */
|
|
277
|
+
--t-dialog: 400ms; /* --spring; scrim color 300ms */
|
|
278
|
+
--t-toast: 500ms; /* --spring */
|
|
279
|
+
--t-skeleton: 1.5s; /* sheen loop: blurred bar translating across */
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (prefers-reduced-motion: reduce) {
|
|
283
|
+
:root {
|
|
284
|
+
--t-micro: 0ms; --t-lift: 0ms; --t-knob: 0ms; --t-popover: 0ms;
|
|
285
|
+
--t-tab: 0ms; --t-delete: 0ms; --t-dialog: 0ms; --t-toast: 0ms;
|
|
286
|
+
}
|
|
287
|
+
/* --t-skeleton intentionally kept: a static skeleton reads as broken.
|
|
288
|
+
base.css swaps the sheen for a gentle non-translating variant instead. */
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* =================== §2.14 Theme generator guardrails ======================
|
|
292
|
+
Brands may generate a custom accent. Hue is free; lightness and chroma are
|
|
293
|
+
clamped so contrast and calm survive. The generator (a build-time script
|
|
294
|
+
or the docs page, never runtime app code) must emit exactly:
|
|
295
|
+
|
|
296
|
+
--accent: oklch(52% C H) C clamped to 0.06–0.17
|
|
297
|
+
--accent-hover: oklch(47% C H)
|
|
298
|
+
--accent-tint: light oklch(92% min(0.32*C, 0.05) H)
|
|
299
|
+
dark oklch(30% 0.35*C H)
|
|
300
|
+
--accent-text: light oklch(41% 0.85*C H)
|
|
301
|
+
dark oklch(84% 0.45*C H)
|
|
302
|
+
|
|
303
|
+
52% lightness at any hue keeps >=4.5:1 with white text.
|
|
304
|
+
Neutrals, type, spacing, and motion are NOT brand-tunable. */
|
|
305
|
+
|
|
306
|
+
/* ================== §3 High contrast (forced-colors) =======================
|
|
307
|
+
The one sanctioned exception to zero borders. Tonal separation disappears
|
|
308
|
+
under forced colors, so components draw real borders with system colors.
|
|
309
|
+
This base block covers global concerns; every component adds its own
|
|
310
|
+
forced-colors rules (see CLAUDE.md checklist). */
|
|
311
|
+
@media (forced-colors: active) {
|
|
312
|
+
:root {
|
|
313
|
+
--scrim: Canvas;
|
|
314
|
+
--ring-focus: none; /* focus becomes a real outline per component */
|
|
315
|
+
--ring-avatar: none;
|
|
316
|
+
--ring-popover: none;
|
|
317
|
+
}
|
|
318
|
+
}
|