@threelight/ui 0.1.0-alpha.0 → 0.2.0-alpha.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/README.md CHANGED
@@ -2,19 +2,169 @@
2
2
 
3
3
  Framework-neutral governed UI primitives for readable HTML/CSS interfaces.
4
4
 
5
- This package is an early alpha for the ThreeLight UI grammar. It currently
6
- publishes the initial package surface for CSS primitives, examples, and typed
7
- package metadata.
5
+ This package is an early alpha for the ThreeLight UI grammar. It publishes CSS
6
+ primitives, safe default theme tokens, examples, and typed registry metadata.
8
7
 
9
8
  ```bash
10
9
  pnpm add @threelight/ui@alpha
11
10
  ```
12
11
 
13
12
  ```ts
14
- import "@threelight/ui/css/base.css"
13
+ import "@threelight/ui/base.css"
15
14
  ```
16
15
 
16
+ Import this once from the application entrypoint or global stylesheet entry
17
+ before project-specific override CSS:
18
+
19
+ ```ts
20
+ import "@threelight/ui/base.css"
21
+ import "./app.css"
22
+ ```
23
+
24
+ ThreeLight uses low-specificity selectors and cascade layers, so application CSS
25
+ loaded later can intentionally override project-specific details.
26
+
27
+ ## Applying The Styles
28
+
29
+ Importing the CSS loads ThreeLight styles globally, but primitives only apply
30
+ inside an explicit `data-tl-root` scope. This keeps existing project CSS from
31
+ being changed by import alone.
32
+
33
+ Apply ThreeLight to the whole app by putting the root attributes on `body`:
34
+
35
+ ```html
36
+ <body data-tl-root data-tl-theme="default" data-tl-mode="light">
37
+ <main class="tl-section tl-stack">
38
+ <h1 class="tl-display">Project archive</h1>
39
+ <p class="tl-body">Readable governed UI primitives.</p>
40
+ </main>
41
+ </body>
42
+ ```
43
+
44
+ Apply ThreeLight to only one area by putting the root attributes on a subtree:
45
+
46
+ ```html
47
+ <body>
48
+ <main>
49
+ <section data-tl-root data-tl-theme="default" data-tl-mode="dark">
50
+ <article class="tl-card tl-stack" data-tl-tone="info">
51
+ <h2 class="tl-heading">Scoped preview</h2>
52
+ <p class="tl-body">Only this subtree uses ThreeLight primitives.</p>
53
+ </article>
54
+ </section>
55
+ </main>
56
+ </body>
57
+ ```
58
+
59
+ `tl-*` classes outside `data-tl-root` are intentionally not styled.
60
+
61
+ ## Overriding Styles
62
+
63
+ Prefer overriding `--tl-*` tokens when customizing theme color, border, focus,
64
+ or component role values. Token overrides keep primitive structure, spacing, and
65
+ readability defaults intact:
66
+
67
+ ```css
68
+ [data-tl-root][data-tl-theme="studio"][data-tl-mode="light"] {
69
+ --tl-primary-fill: #55ffff;
70
+ --tl-primary-on-fill: #061010;
71
+ --tl-primary-border: #27757c;
72
+ }
73
+ ```
74
+
75
+ Directly overriding `.tl-*` classes is allowed, but it can intentionally replace
76
+ primitive structure. For example, later app CSS such as `.my-page .tl-button {
77
+ padding: 0; border: 0; }` will change how that button renders.
78
+
79
+ ## Primitive Contract
80
+
81
+ ThreeLight UI exposes opt-in, framework-neutral HTML/CSS primitives. Compose
82
+ with `tl-*` classes and namespaced `data-tl-*` attributes.
83
+
84
+ - Layout: `tl-section`, `tl-stack`, `tl-cluster`, `tl-grid`
85
+ - Surface: `tl-surface`, `tl-card`, `tl-panel`
86
+ - Text: `tl-display`, `tl-heading`, `tl-body`, `tl-caption`, `tl-meta`, `tl-label`, `tl-action-text`, `tl-metric`, `tl-code`
87
+ - Action/status: `tl-button`, `tl-badge`, `tl-alert`
88
+ - Form: `tl-field`, `tl-input`, `tl-help`
89
+
90
+ Supported tones are `neutral`, `primary`, `info`, `success`, `warning`, and
91
+ `danger`. Omit `data-tl-tone` for neutral behavior.
92
+
93
+ Primitives keep body and input text at 16px or larger, keep helper and caption
94
+ text at the 14px readable floor, and expose spacing overrides through CSS custom
95
+ properties such as `--tl-gap`, `--tl-grid-min`, and `--tl-surface-padding`.
96
+
97
+ ## Theme Contract
98
+
99
+ ThreeLight UI separates theme family, mode, and semantic tone.
100
+
101
+ ```html
102
+ <body data-tl-root data-tl-theme="default" data-tl-mode="light">
103
+ <article class="tl-card" data-tl-tone="warning">
104
+ ...
105
+ </article>
106
+ </body>
107
+ ```
108
+
109
+ - `data-tl-root` opts an app or subtree into ThreeLight theme variables.
110
+ - `data-tl-theme` selects palette family, initially `default`.
111
+ - `data-tl-mode` selects lightness mode, initially `light` or `dark`.
112
+ - `data-tl-tone` expresses component meaning and stays stable across themes.
113
+
114
+ Theme tokens are role-centric. Prefer `content` roles over text-only roles:
115
+
116
+ ```text
117
+ --tl-canvas
118
+ --tl-surface
119
+ --tl-layer
120
+ --tl-content-primary
121
+ --tl-content-secondary
122
+ --tl-content-subtle
123
+ --tl-border-subtle
124
+ --tl-border-strong
125
+ --tl-focus
126
+ ```
127
+
128
+ Theme and mode changes swap token values behind each role. Consumers should not
129
+ rewrite a component from `warning` to another tone unless the component meaning
130
+ changes.
131
+
132
+ ## Registry Metadata
133
+
134
+ The package exports readonly registry metadata for adapters and migration tools:
135
+
136
+ ```ts
137
+ import {
138
+ primitiveClasses,
139
+ semanticTokenNames,
140
+ themeAttributes,
141
+ themeFamilies,
142
+ themeModes,
143
+ toneNames,
144
+ } from "@threelight/ui"
145
+ ```
146
+
147
+ Use these exports to avoid hard-coding the public `tl-*`, `data-tl-*`, tone, and
148
+ token contract in downstream packages.
149
+
17
150
  ## Status
18
151
 
19
152
  `@threelight/ui` is experimental. Public APIs may change before the first stable
20
153
  release.
154
+
155
+ ## Publishing
156
+
157
+ Publish alpha releases from the repository root with the scripted command. The
158
+ script runs package publish with an explicit `alpha` dist-tag.
159
+
160
+ ```bash
161
+ pnpm release:ui:alpha
162
+ ```
163
+
164
+ Check the published dist-tags with:
165
+
166
+ ```bash
167
+ pnpm release:ui:tags
168
+ ```
169
+
170
+ The tag check contacts the npm registry, so it requires network access.
package/css/base.css CHANGED
@@ -1 +1,535 @@
1
1
  @layer threelight.theme, threelight.primitives, threelight.utilities;
2
+
3
+ @layer threelight.theme {
4
+ [data-tl-root][data-tl-theme="default"][data-tl-mode="light"] {
5
+ color-scheme: light;
6
+ --tl-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
7
+ "Segoe UI", sans-serif;
8
+ --tl-font-mono: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono",
9
+ monospace;
10
+
11
+ --tl-canvas: #f6f7f9;
12
+ --tl-surface: #ffffff;
13
+ --tl-layer: #fbfcfe;
14
+ --tl-content-primary: #1d232b;
15
+ --tl-content-secondary: #566170;
16
+ --tl-content-subtle: #697586;
17
+ --tl-border-subtle: #d8dee7;
18
+ --tl-border-strong: #aeb8c6;
19
+ --tl-focus: #2563eb;
20
+
21
+ --tl-primary-fill: #234c9f;
22
+ --tl-primary-on-fill: #ffffff;
23
+ --tl-primary-soft: #eaf1ff;
24
+ --tl-primary-content: #173978;
25
+ --tl-primary-border: #b8cbf6;
26
+ --tl-info-fill: #075985;
27
+ --tl-info-on-fill: #ffffff;
28
+ --tl-info-soft: #e6f6fc;
29
+ --tl-info-content: #164e63;
30
+ --tl-info-border: #a8dff0;
31
+ --tl-success-fill: #166534;
32
+ --tl-success-on-fill: #ffffff;
33
+ --tl-success-soft: #e8f7ee;
34
+ --tl-success-content: #14532d;
35
+ --tl-success-border: #a9ddb9;
36
+ --tl-warning-fill: #92400e;
37
+ --tl-warning-on-fill: #ffffff;
38
+ --tl-warning-soft: #fff4d8;
39
+ --tl-warning-content: #78350f;
40
+ --tl-warning-border: #f1d18a;
41
+ --tl-danger-fill: #b42318;
42
+ --tl-danger-on-fill: #ffffff;
43
+ --tl-danger-soft: #ffebe7;
44
+ --tl-danger-content: #8a1f16;
45
+ --tl-danger-border: #f3b0a8;
46
+
47
+ --tl-radius-sm: 4px;
48
+ --tl-radius-md: 6px;
49
+ --tl-radius-lg: 8px;
50
+ --tl-shadow-sm: 0 1px 2px rgb(29 35 43 / 8%);
51
+ }
52
+
53
+ [data-tl-root][data-tl-theme="default"][data-tl-mode="dark"] {
54
+ color-scheme: dark;
55
+
56
+ --tl-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
57
+ "Segoe UI", sans-serif;
58
+ --tl-font-mono: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono",
59
+ monospace;
60
+
61
+ --tl-canvas: #10141b;
62
+ --tl-surface: #171d26;
63
+ --tl-layer: #1f2732;
64
+ --tl-content-primary: #eef2f7;
65
+ --tl-content-secondary: #c2cad6;
66
+ --tl-content-subtle: #a7b1bf;
67
+ --tl-border-subtle: #374151;
68
+ --tl-border-strong: #536071;
69
+ --tl-focus: #8ab4ff;
70
+
71
+ --tl-primary-fill: #8ab4ff;
72
+ --tl-primary-on-fill: #10141b;
73
+ --tl-primary-soft: #1b315d;
74
+ --tl-primary-content: #dce9ff;
75
+ --tl-primary-border: #456ba8;
76
+ --tl-info-fill: #67d5ff;
77
+ --tl-info-on-fill: #10141b;
78
+ --tl-info-soft: #15394a;
79
+ --tl-info-content: #d7f4ff;
80
+ --tl-info-border: #3c7d95;
81
+ --tl-success-fill: #74d99f;
82
+ --tl-success-on-fill: #10141b;
83
+ --tl-success-soft: #173b27;
84
+ --tl-success-content: #d9f8e4;
85
+ --tl-success-border: #3e8059;
86
+ --tl-warning-fill: #f3c663;
87
+ --tl-warning-on-fill: #10141b;
88
+ --tl-warning-soft: #3d2e12;
89
+ --tl-warning-content: #ffe8ad;
90
+ --tl-warning-border: #92722c;
91
+ --tl-danger-fill: #ff9a8f;
92
+ --tl-danger-on-fill: #10141b;
93
+ --tl-danger-soft: #4a1f1b;
94
+ --tl-danger-content: #ffe0dc;
95
+ --tl-danger-border: #9a5048;
96
+
97
+ --tl-radius-sm: 4px;
98
+ --tl-radius-md: 6px;
99
+ --tl-radius-lg: 8px;
100
+ --tl-shadow-sm: 0 1px 2px rgb(0 0 0 / 24%);
101
+ }
102
+ }
103
+
104
+ @layer threelight.primitives {
105
+ :where([data-tl-root]) :where(
106
+ .tl-section,
107
+ .tl-stack,
108
+ .tl-cluster,
109
+ .tl-grid,
110
+ .tl-surface,
111
+ .tl-card,
112
+ .tl-panel,
113
+ .tl-display,
114
+ .tl-heading,
115
+ .tl-body,
116
+ .tl-caption,
117
+ .tl-meta,
118
+ .tl-label,
119
+ .tl-action-text,
120
+ .tl-metric,
121
+ .tl-code,
122
+ .tl-button,
123
+ .tl-badge,
124
+ .tl-alert,
125
+ .tl-field,
126
+ .tl-input,
127
+ .tl-help
128
+ ) {
129
+ box-sizing: border-box;
130
+ }
131
+
132
+ :where([data-tl-root]) :where(
133
+ .tl-display,
134
+ .tl-heading,
135
+ .tl-body,
136
+ .tl-caption,
137
+ .tl-meta,
138
+ .tl-label,
139
+ .tl-action-text,
140
+ .tl-metric,
141
+ .tl-code,
142
+ .tl-help
143
+ ) {
144
+ margin: 0;
145
+ color: var(--tl-component-content, var(--tl-content-primary));
146
+ font-family: var(--tl-font-sans);
147
+ overflow-wrap: anywhere;
148
+ text-wrap: pretty;
149
+ }
150
+
151
+ :where([data-tl-root]) :where(.tl-section) {
152
+ inline-size: min(100% - 2rem, var(--tl-section-max, 72rem));
153
+ margin-inline: auto;
154
+ padding-block: var(--tl-section-padding, 3rem);
155
+ }
156
+
157
+ :where([data-tl-root]) :where(.tl-stack) {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: var(--tl-gap, 1rem);
161
+ min-inline-size: 0;
162
+ }
163
+
164
+ :where([data-tl-root]) :where(.tl-cluster) {
165
+ display: flex;
166
+ flex-wrap: wrap;
167
+ align-items: var(--tl-align, center);
168
+ justify-content: var(--tl-justify, flex-start);
169
+ gap: var(--tl-gap, 0.75rem);
170
+ min-inline-size: 0;
171
+ }
172
+
173
+ :where([data-tl-root]) :where(.tl-grid) {
174
+ display: grid;
175
+ grid-template-columns: repeat(
176
+ auto-fit,
177
+ minmax(min(100%, var(--tl-grid-min, 16rem)), 1fr)
178
+ );
179
+ gap: var(--tl-gap, 1rem);
180
+ min-inline-size: 0;
181
+ }
182
+
183
+ :where([data-tl-root]) :where(.tl-surface, .tl-card, .tl-panel, .tl-alert) {
184
+ --tl-component-background: var(--tl-surface);
185
+ --tl-component-content: var(--tl-content-primary);
186
+ --tl-component-content-muted: var(--tl-content-secondary);
187
+ --tl-component-border: var(--tl-border-subtle);
188
+
189
+ min-inline-size: 0;
190
+ color: var(--tl-component-content);
191
+ border: 1px solid var(--tl-component-border);
192
+ font-family: var(--tl-font-sans);
193
+ }
194
+
195
+ :where([data-tl-root]) :where(.tl-surface) {
196
+ background: var(--tl-component-background);
197
+ border-radius: var(--tl-radius-surface, var(--tl-radius-lg));
198
+ padding: var(--tl-surface-padding, 1.5rem);
199
+ }
200
+
201
+ :where([data-tl-root]) :where(.tl-card) {
202
+ background: var(--tl-component-background);
203
+ border-radius: var(--tl-radius-card, var(--tl-radius-lg));
204
+ box-shadow: var(--tl-card-shadow, var(--tl-shadow-sm));
205
+ padding: var(--tl-card-padding, 1.25rem);
206
+ }
207
+
208
+ :where([data-tl-root]) :where(.tl-panel) {
209
+ --tl-component-background: var(--tl-layer);
210
+
211
+ background: var(--tl-component-background);
212
+ border-radius: var(--tl-radius-panel, var(--tl-radius-md));
213
+ padding: var(--tl-panel-padding, 1rem);
214
+ }
215
+
216
+ :where([data-tl-root]) :where(
217
+ .tl-surface[data-tl-tone="primary"],
218
+ .tl-card[data-tl-tone="primary"],
219
+ .tl-panel[data-tl-tone="primary"]
220
+ ) {
221
+ --tl-component-background: var(--tl-primary-soft);
222
+ --tl-component-content: var(--tl-primary-content);
223
+ --tl-component-content-muted: var(--tl-primary-content);
224
+ --tl-component-border: var(--tl-primary-border);
225
+ }
226
+
227
+ :where([data-tl-root]) :where(
228
+ .tl-surface[data-tl-tone="info"],
229
+ .tl-card[data-tl-tone="info"],
230
+ .tl-panel[data-tl-tone="info"]
231
+ ) {
232
+ --tl-component-background: var(--tl-info-soft);
233
+ --tl-component-content: var(--tl-info-content);
234
+ --tl-component-content-muted: var(--tl-info-content);
235
+ --tl-component-border: var(--tl-info-border);
236
+ }
237
+
238
+ :where([data-tl-root]) :where(
239
+ .tl-surface[data-tl-tone="success"],
240
+ .tl-card[data-tl-tone="success"],
241
+ .tl-panel[data-tl-tone="success"]
242
+ ) {
243
+ --tl-component-background: var(--tl-success-soft);
244
+ --tl-component-content: var(--tl-success-content);
245
+ --tl-component-content-muted: var(--tl-success-content);
246
+ --tl-component-border: var(--tl-success-border);
247
+ }
248
+
249
+ :where([data-tl-root]) :where(
250
+ .tl-surface[data-tl-tone="warning"],
251
+ .tl-card[data-tl-tone="warning"],
252
+ .tl-panel[data-tl-tone="warning"]
253
+ ) {
254
+ --tl-component-background: var(--tl-warning-soft);
255
+ --tl-component-content: var(--tl-warning-content);
256
+ --tl-component-content-muted: var(--tl-warning-content);
257
+ --tl-component-border: var(--tl-warning-border);
258
+ }
259
+
260
+ :where([data-tl-root]) :where(
261
+ .tl-surface[data-tl-tone="danger"],
262
+ .tl-card[data-tl-tone="danger"],
263
+ .tl-panel[data-tl-tone="danger"]
264
+ ) {
265
+ --tl-component-background: var(--tl-danger-soft);
266
+ --tl-component-content: var(--tl-danger-content);
267
+ --tl-component-content-muted: var(--tl-danger-content);
268
+ --tl-component-border: var(--tl-danger-border);
269
+ }
270
+
271
+ :where([data-tl-root]) :where(.tl-display) {
272
+ color: var(--tl-component-content, var(--tl-content-primary));
273
+ font-size: clamp(2rem, 6vw, 4rem);
274
+ font-weight: 700;
275
+ letter-spacing: 0;
276
+ line-height: 1.05;
277
+ }
278
+
279
+ :where([data-tl-root]) :where(.tl-heading) {
280
+ color: var(--tl-component-content, var(--tl-content-primary));
281
+ font-size: clamp(1.25rem, 3vw, 2rem);
282
+ font-weight: 700;
283
+ letter-spacing: 0;
284
+ line-height: 1.2;
285
+ }
286
+
287
+ :where([data-tl-root]) :where(.tl-body) {
288
+ color: var(--tl-component-content-muted, var(--tl-content-secondary));
289
+ font-size: 1rem;
290
+ line-height: 1.6;
291
+ }
292
+
293
+ :where([data-tl-root]) :where(.tl-caption, .tl-help) {
294
+ color: var(--tl-component-content-muted, var(--tl-content-subtle));
295
+ font-size: 0.875rem;
296
+ line-height: 1.45;
297
+ }
298
+
299
+ :where([data-tl-root]) :where(.tl-meta, .tl-label) {
300
+ color: var(--tl-component-content, var(--tl-content-primary));
301
+ font-size: 0.875rem;
302
+ font-weight: 700;
303
+ letter-spacing: 0;
304
+ line-height: 1.4;
305
+ }
306
+
307
+ :where([data-tl-root]) :where(.tl-meta) {
308
+ font-family: var(--tl-font-mono);
309
+ }
310
+
311
+ :where([data-tl-root]) :where(.tl-action-text) {
312
+ color: var(--tl-component-content, var(--tl-primary-content));
313
+ font-size: 0.875rem;
314
+ font-weight: 700;
315
+ letter-spacing: 0;
316
+ line-height: 1.4;
317
+ }
318
+
319
+ :where([data-tl-root]) :where(.tl-metric) {
320
+ color: var(--tl-component-content, var(--tl-content-primary));
321
+ font-family: var(--tl-font-mono);
322
+ font-size: clamp(1.5rem, 4vw, 2.5rem);
323
+ font-variant-numeric: tabular-nums;
324
+ font-weight: 700;
325
+ letter-spacing: 0;
326
+ line-height: 1.1;
327
+ }
328
+
329
+ :where([data-tl-root]) :where(.tl-code) {
330
+ color: var(--tl-component-content, var(--tl-content-primary));
331
+ border: 1px solid var(--tl-component-border, var(--tl-border-subtle));
332
+ border-radius: var(--tl-radius-sm);
333
+ font-family: var(--tl-font-mono);
334
+ font-size: 0.875rem;
335
+ line-height: 1.5;
336
+ padding: 0.125rem 0.25rem;
337
+ }
338
+
339
+ :where([data-tl-root]) :where(.tl-button) {
340
+ --tl-button-background: var(--tl-surface);
341
+ --tl-button-content: var(--tl-content-primary);
342
+ --tl-button-border: var(--tl-border-strong);
343
+
344
+ display: inline-flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ min-block-size: 2.75rem;
348
+ max-inline-size: 100%;
349
+ gap: 0.5rem;
350
+ color: var(--tl-button-content);
351
+ background: var(--tl-button-background);
352
+ border: 1px solid var(--tl-button-border);
353
+ border-radius: var(--tl-radius-button, var(--tl-radius-md));
354
+ cursor: pointer;
355
+ font-family: var(--tl-font-sans);
356
+ font-size: 1rem;
357
+ font-weight: 700;
358
+ line-height: 1.2;
359
+ padding: 0.7rem 1rem;
360
+ text-align: center;
361
+ text-decoration: none;
362
+ overflow-wrap: anywhere;
363
+ }
364
+
365
+ :where([data-tl-root]) :where(.tl-button:hover) {
366
+ filter: brightness(0.98);
367
+ }
368
+
369
+ :where([data-tl-root]) :where(.tl-button:focus-visible, .tl-input:focus-visible) {
370
+ outline: 3px solid var(--tl-focus);
371
+ outline-offset: 2px;
372
+ }
373
+
374
+ :where([data-tl-root]) :where(.tl-button:disabled, .tl-button[aria-disabled="true"], .tl-input:disabled) {
375
+ cursor: not-allowed;
376
+ opacity: 0.68;
377
+ }
378
+
379
+ :where([data-tl-root]) :where(.tl-button[data-tl-tone="primary"]) {
380
+ --tl-button-background: var(--tl-primary-fill);
381
+ --tl-button-content: var(--tl-primary-on-fill);
382
+ --tl-button-border: var(--tl-primary-fill);
383
+ }
384
+
385
+ :where([data-tl-root]) :where(.tl-button[data-tl-tone="info"]) {
386
+ --tl-button-background: var(--tl-info-fill);
387
+ --tl-button-content: var(--tl-info-on-fill);
388
+ --tl-button-border: var(--tl-info-fill);
389
+ }
390
+
391
+ :where([data-tl-root]) :where(.tl-button[data-tl-tone="success"]) {
392
+ --tl-button-background: var(--tl-success-fill);
393
+ --tl-button-content: var(--tl-success-on-fill);
394
+ --tl-button-border: var(--tl-success-fill);
395
+ }
396
+
397
+ :where([data-tl-root]) :where(.tl-button[data-tl-tone="warning"]) {
398
+ --tl-button-background: var(--tl-warning-fill);
399
+ --tl-button-content: var(--tl-warning-on-fill);
400
+ --tl-button-border: var(--tl-warning-fill);
401
+ }
402
+
403
+ :where([data-tl-root]) :where(.tl-button[data-tl-tone="danger"]) {
404
+ --tl-button-background: var(--tl-danger-fill);
405
+ --tl-button-content: var(--tl-danger-on-fill);
406
+ --tl-button-border: var(--tl-danger-fill);
407
+ }
408
+
409
+ :where([data-tl-root]) :where(.tl-badge) {
410
+ --tl-badge-background: var(--tl-layer);
411
+ --tl-badge-content: var(--tl-content-primary);
412
+ --tl-badge-border: var(--tl-border-subtle);
413
+
414
+ display: inline-flex;
415
+ align-items: center;
416
+ max-inline-size: 100%;
417
+ color: var(--tl-badge-content);
418
+ background: var(--tl-badge-background);
419
+ border: 1px solid var(--tl-badge-border);
420
+ border-radius: 999px;
421
+ font-family: var(--tl-font-sans);
422
+ font-size: 0.875rem;
423
+ font-weight: 700;
424
+ line-height: 1.3;
425
+ padding: 0.25rem 0.625rem;
426
+ overflow-wrap: anywhere;
427
+ }
428
+
429
+ :where([data-tl-root]) :where(.tl-badge[data-tl-tone="primary"]) {
430
+ --tl-badge-background: var(--tl-primary-soft);
431
+ --tl-badge-content: var(--tl-primary-content);
432
+ --tl-badge-border: var(--tl-primary-border);
433
+ }
434
+
435
+ :where([data-tl-root]) :where(.tl-badge[data-tl-tone="info"]) {
436
+ --tl-badge-background: var(--tl-info-soft);
437
+ --tl-badge-content: var(--tl-info-content);
438
+ --tl-badge-border: var(--tl-info-border);
439
+ }
440
+
441
+ :where([data-tl-root]) :where(.tl-badge[data-tl-tone="success"]) {
442
+ --tl-badge-background: var(--tl-success-soft);
443
+ --tl-badge-content: var(--tl-success-content);
444
+ --tl-badge-border: var(--tl-success-border);
445
+ }
446
+
447
+ :where([data-tl-root]) :where(.tl-badge[data-tl-tone="warning"]) {
448
+ --tl-badge-background: var(--tl-warning-soft);
449
+ --tl-badge-content: var(--tl-warning-content);
450
+ --tl-badge-border: var(--tl-warning-border);
451
+ }
452
+
453
+ :where([data-tl-root]) :where(.tl-badge[data-tl-tone="danger"]) {
454
+ --tl-badge-background: var(--tl-danger-soft);
455
+ --tl-badge-content: var(--tl-danger-content);
456
+ --tl-badge-border: var(--tl-danger-border);
457
+ }
458
+
459
+ :where([data-tl-root]) :where(.tl-alert) {
460
+ --tl-component-background: var(--tl-layer);
461
+ --tl-component-content: var(--tl-content-primary);
462
+ --tl-component-content-muted: var(--tl-content-secondary);
463
+ --tl-component-border: var(--tl-border-strong);
464
+
465
+ background: var(--tl-component-background);
466
+ border-radius: var(--tl-radius-md);
467
+ padding: var(--tl-alert-padding, 1rem);
468
+ }
469
+
470
+ :where([data-tl-root]) :where(.tl-alert[data-tl-tone="primary"]) {
471
+ --tl-component-background: var(--tl-primary-soft);
472
+ --tl-component-content: var(--tl-primary-content);
473
+ --tl-component-content-muted: var(--tl-primary-content);
474
+ --tl-component-border: var(--tl-primary-border);
475
+ }
476
+
477
+ :where([data-tl-root]) :where(.tl-alert[data-tl-tone="info"]) {
478
+ --tl-component-background: var(--tl-info-soft);
479
+ --tl-component-content: var(--tl-info-content);
480
+ --tl-component-content-muted: var(--tl-info-content);
481
+ --tl-component-border: var(--tl-info-border);
482
+ }
483
+
484
+ :where([data-tl-root]) :where(.tl-alert[data-tl-tone="success"]) {
485
+ --tl-component-background: var(--tl-success-soft);
486
+ --tl-component-content: var(--tl-success-content);
487
+ --tl-component-content-muted: var(--tl-success-content);
488
+ --tl-component-border: var(--tl-success-border);
489
+ }
490
+
491
+ :where([data-tl-root]) :where(.tl-alert[data-tl-tone="warning"]) {
492
+ --tl-component-background: var(--tl-warning-soft);
493
+ --tl-component-content: var(--tl-warning-content);
494
+ --tl-component-content-muted: var(--tl-warning-content);
495
+ --tl-component-border: var(--tl-warning-border);
496
+ }
497
+
498
+ :where([data-tl-root]) :where(.tl-alert[data-tl-tone="danger"]) {
499
+ --tl-component-background: var(--tl-danger-soft);
500
+ --tl-component-content: var(--tl-danger-content);
501
+ --tl-component-content-muted: var(--tl-danger-content);
502
+ --tl-component-border: var(--tl-danger-border);
503
+ }
504
+
505
+ :where([data-tl-root]) :where(.tl-field) {
506
+ display: grid;
507
+ gap: 0.375rem;
508
+ min-inline-size: 0;
509
+ }
510
+
511
+ :where([data-tl-root]) :where(.tl-input) {
512
+ inline-size: 100%;
513
+ min-block-size: 2.75rem;
514
+ color: var(--tl-content-primary);
515
+ background: var(--tl-surface);
516
+ border: 1px solid var(--tl-border-strong);
517
+ border-radius: var(--tl-radius-input, var(--tl-radius-md));
518
+ font-family: var(--tl-font-sans);
519
+ font-size: 1rem;
520
+ line-height: 1.4;
521
+ padding: 0.65rem 0.75rem;
522
+ }
523
+
524
+ :where([data-tl-root]) :where(.tl-input::placeholder) {
525
+ color: var(--tl-content-subtle);
526
+ }
527
+ }
528
+
529
+ @layer threelight.utilities {
530
+ :where([data-tl-root]) {
531
+ background: var(--tl-canvas);
532
+ color: var(--tl-content-primary);
533
+ font-family: var(--tl-font-sans);
534
+ }
535
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,32 @@ export declare const threelightUiPackage: {
2
2
  readonly name: "@threelight/ui";
3
3
  readonly purpose: "framework-neutral governed HTML/CSS UI primitives";
4
4
  };
5
+ export declare const themeFamilies: readonly ["default"];
6
+ export type ThemeFamily = (typeof themeFamilies)[number];
7
+ export declare const themeModes: readonly ["light", "dark"];
8
+ export type ThemeMode = (typeof themeModes)[number];
9
+ export declare const toneNames: readonly ["neutral", "primary", "info", "success", "warning", "danger"];
10
+ export type ToneName = (typeof toneNames)[number];
11
+ export declare const themeAttributes: {
12
+ readonly root: "data-tl-root";
13
+ readonly theme: "data-tl-theme";
14
+ readonly mode: "data-tl-mode";
15
+ readonly tone: "data-tl-tone";
16
+ };
17
+ export declare const primitiveClasses: {
18
+ readonly layout: readonly ["tl-section", "tl-stack", "tl-cluster", "tl-grid"];
19
+ readonly surface: readonly ["tl-surface", "tl-card", "tl-panel"];
20
+ readonly text: readonly ["tl-display", "tl-heading", "tl-body", "tl-caption", "tl-meta", "tl-label", "tl-action-text", "tl-metric", "tl-code", "tl-help"];
21
+ readonly action: readonly ["tl-button"];
22
+ readonly status: readonly ["tl-badge", "tl-alert"];
23
+ readonly form: readonly ["tl-field", "tl-input"];
24
+ };
25
+ export type PrimitiveGroup = keyof typeof primitiveClasses;
26
+ export type PrimitiveClass = (typeof primitiveClasses)[PrimitiveGroup][number];
27
+ export declare const semanticTokenNames: readonly ["--tl-canvas", "--tl-surface", "--tl-layer", "--tl-content-primary", "--tl-content-secondary", "--tl-content-subtle", "--tl-border-subtle", "--tl-border-strong", "--tl-focus"];
28
+ export type SemanticTokenName = (typeof semanticTokenNames)[number];
29
+ export declare const toneTokenNames: ("--tl-primary-fill" | "--tl-info-fill" | "--tl-success-fill" | "--tl-warning-fill" | "--tl-danger-fill" | "--tl-primary-on-fill" | "--tl-info-on-fill" | "--tl-success-on-fill" | "--tl-warning-on-fill" | "--tl-danger-on-fill" | "--tl-primary-soft" | "--tl-info-soft" | "--tl-success-soft" | "--tl-warning-soft" | "--tl-danger-soft" | "--tl-primary-content" | "--tl-info-content" | "--tl-success-content" | "--tl-warning-content" | "--tl-danger-content" | "--tl-primary-border" | "--tl-info-border" | "--tl-success-border" | "--tl-warning-border" | "--tl-danger-border")[];
30
+ export type ToneTokenName = (typeof toneTokenNames)[number];
31
+ export declare const componentTokenNames: readonly ["--tl-component-background", "--tl-component-content", "--tl-component-content-muted", "--tl-component-border", "--tl-button-background", "--tl-button-content", "--tl-button-border", "--tl-badge-background", "--tl-badge-content", "--tl-badge-border"];
32
+ export type ComponentTokenName = (typeof componentTokenNames)[number];
5
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB;;;CAGtB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB;;;CAGtB,CAAA;AAEV,eAAO,MAAM,aAAa,sBAAuB,CAAA;AACjD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD,eAAO,MAAM,UAAU,4BAA6B,CAAA;AACpD,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnD,eAAO,MAAM,SAAS,yEAOZ,CAAA;AACV,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAA;AAEjD,eAAO,MAAM,eAAe;;;;;CAKlB,CAAA;AAEV,eAAO,MAAM,gBAAgB;;;;;;;CAkBnB,CAAA;AAEV,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,CAAA;AAC1D,MAAM,MAAM,cAAc,GACxB,CAAC,OAAO,gBAAgB,CAAC,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnD,eAAO,MAAM,kBAAkB,2LAUrB,CAAA;AACV,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,eAAO,MAAM,cAAc,6jBAUzB,CAAA;AACF,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAE3D,eAAO,MAAM,mBAAmB,sQAWtB,CAAA;AACV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA"}
package/dist/index.js CHANGED
@@ -2,4 +2,73 @@ export const threelightUiPackage = {
2
2
  name: '@threelight/ui',
3
3
  purpose: 'framework-neutral governed HTML/CSS UI primitives',
4
4
  };
5
+ export const themeFamilies = ['default'];
6
+ export const themeModes = ['light', 'dark'];
7
+ export const toneNames = [
8
+ 'neutral',
9
+ 'primary',
10
+ 'info',
11
+ 'success',
12
+ 'warning',
13
+ 'danger',
14
+ ];
15
+ export const themeAttributes = {
16
+ root: 'data-tl-root',
17
+ theme: 'data-tl-theme',
18
+ mode: 'data-tl-mode',
19
+ tone: 'data-tl-tone',
20
+ };
21
+ export const primitiveClasses = {
22
+ layout: ['tl-section', 'tl-stack', 'tl-cluster', 'tl-grid'],
23
+ surface: ['tl-surface', 'tl-card', 'tl-panel'],
24
+ text: [
25
+ 'tl-display',
26
+ 'tl-heading',
27
+ 'tl-body',
28
+ 'tl-caption',
29
+ 'tl-meta',
30
+ 'tl-label',
31
+ 'tl-action-text',
32
+ 'tl-metric',
33
+ 'tl-code',
34
+ 'tl-help',
35
+ ],
36
+ action: ['tl-button'],
37
+ status: ['tl-badge', 'tl-alert'],
38
+ form: ['tl-field', 'tl-input'],
39
+ };
40
+ export const semanticTokenNames = [
41
+ '--tl-canvas',
42
+ '--tl-surface',
43
+ '--tl-layer',
44
+ '--tl-content-primary',
45
+ '--tl-content-secondary',
46
+ '--tl-content-subtle',
47
+ '--tl-border-subtle',
48
+ '--tl-border-strong',
49
+ '--tl-focus',
50
+ ];
51
+ export const toneTokenNames = toneNames.flatMap((tone) => {
52
+ if (tone === 'neutral')
53
+ return [];
54
+ return [
55
+ `--tl-${tone}-fill`,
56
+ `--tl-${tone}-on-fill`,
57
+ `--tl-${tone}-soft`,
58
+ `--tl-${tone}-content`,
59
+ `--tl-${tone}-border`,
60
+ ];
61
+ });
62
+ export const componentTokenNames = [
63
+ '--tl-component-background',
64
+ '--tl-component-content',
65
+ '--tl-component-content-muted',
66
+ '--tl-component-border',
67
+ '--tl-button-background',
68
+ '--tl-button-content',
69
+ '--tl-button-border',
70
+ '--tl-badge-background',
71
+ '--tl-badge-content',
72
+ '--tl-badge-border',
73
+ ];
5
74
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,gBAAgB;IACtB,OAAO,EAAE,mDAAmD;CACpD,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,gBAAgB;IACtB,OAAO,EAAE,mDAAmD;CACpD,CAAA;AAEV,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAAS,CAAU,CAAA;AAGjD,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,CAAU,CAAA;AAGpD,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,SAAS;IACT,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;IACT,QAAQ;CACA,CAAA;AAGV,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,cAAc;IACpB,KAAK,EAAE,eAAe;IACtB,IAAI,EAAE,cAAc;IACpB,IAAI,EAAE,cAAc;CACZ,CAAA;AAEV,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,CAAC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,CAAC;IAC3D,OAAO,EAAE,CAAC,YAAY,EAAE,SAAS,EAAE,UAAU,CAAC;IAC9C,IAAI,EAAE;QACJ,YAAY;QACZ,YAAY;QACZ,SAAS;QACT,YAAY;QACZ,SAAS;QACT,UAAU;QACV,gBAAgB;QAChB,WAAW;QACX,SAAS;QACT,SAAS;KACV;IACD,MAAM,EAAE,CAAC,WAAW,CAAC;IACrB,MAAM,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,IAAI,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC;CACtB,CAAA;AAMV,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,aAAa;IACb,cAAc;IACd,YAAY;IACZ,sBAAsB;IACtB,wBAAwB;IACxB,qBAAqB;IACrB,oBAAoB;IACpB,oBAAoB;IACpB,YAAY;CACJ,CAAA;AAGV,MAAM,CAAC,MAAM,cAAc,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;IACvD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IAEjC,OAAO;QACL,QAAQ,IAAI,OAAO;QACnB,QAAQ,IAAI,UAAU;QACtB,QAAQ,IAAI,OAAO;QACnB,QAAQ,IAAI,UAAU;QACtB,QAAQ,IAAI,SAAS;KACb,CAAA;AACZ,CAAC,CAAC,CAAA;AAGF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,2BAA2B;IAC3B,wBAAwB;IACxB,8BAA8B;IAC9B,uBAAuB;IACvB,wBAAwB;IACxB,qBAAqB;IACrB,oBAAoB;IACpB,uBAAuB;IACvB,oBAAoB;IACpB,mBAAmB;CACX,CAAA"}
@@ -1,15 +1,68 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>ThreeLight UI Examples</title>
7
- <link rel="stylesheet" href="../css/base.css">
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>ThreeLight UI Example</title>
7
+ <link rel="stylesheet" href="../css/base.css" />
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ }
12
+
13
+ .example-shell {
14
+ min-block-size: 100vh;
15
+ }
16
+ </style>
8
17
  </head>
9
- <body>
10
- <main>
11
- <h1>ThreeLight UI</h1>
12
- <p>Component examples will be added through tracked implementation issues.</p>
18
+ <body data-tl-root data-tl-theme="default" data-tl-mode="light">
19
+ <main class="example-shell tl-section tl-stack">
20
+ <header class="tl-stack">
21
+ <span class="tl-badge" data-tl-tone="primary">Alpha contract</span>
22
+ <h1 class="tl-display">Deployment readiness</h1>
23
+ <p class="tl-body">
24
+ Review release health, approvals, and generated copy before the next
25
+ publish window.
26
+ </p>
27
+ </header>
28
+
29
+ <section class="tl-grid" style="--tl-grid-min: 14rem">
30
+ <article class="tl-card tl-stack" data-tl-tone="success">
31
+ <span class="tl-badge" data-tl-tone="success">Passing</span>
32
+ <p class="tl-metric">98%</p>
33
+ <p class="tl-body">Generated labels meet the readable text floor.</p>
34
+ </article>
35
+
36
+ <article class="tl-card tl-stack" data-tl-tone="warning">
37
+ <span class="tl-badge" data-tl-tone="warning">Pending</span>
38
+ <p class="tl-metric">3</p>
39
+ <p class="tl-body">Surface tone changes need a second review.</p>
40
+ </article>
41
+
42
+ <article class="tl-card tl-stack" data-tl-tone="info">
43
+ <span class="tl-badge" data-tl-tone="info">Quiet</span>
44
+ <p class="tl-metric">0</p>
45
+ <p class="tl-body">No active regressions in the release window.</p>
46
+ </article>
47
+ </section>
48
+
49
+ <section class="tl-alert tl-stack" data-tl-tone="warning" role="status">
50
+ <h2 class="tl-heading">Long content check</h2>
51
+ <p class="tl-body">
52
+ The generated release note contains a long URL:
53
+ <code class="tl-code"
54
+ >https://example.com/releases/2026/06/readability-window-review</code
55
+ >
56
+ </p>
57
+ <div class="tl-cluster">
58
+ <button class="tl-button" type="button" data-tl-tone="primary">
59
+ Approve
60
+ </button>
61
+ <button class="tl-button" type="button" data-tl-tone="danger">
62
+ Request changes
63
+ </button>
64
+ </div>
65
+ </section>
13
66
  </main>
14
67
  </body>
15
68
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threelight/ui",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.2.0-alpha.0",
4
4
  "description": "Framework-neutral governed UI primitives for readable HTML/CSS interfaces.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
- "./css/base.css": "./css/base.css",
15
+ "./base.css": "./css/base.css",
16
16
  "./examples": "./examples/index.html"
17
17
  },
18
18
  "files": [
@@ -28,6 +28,10 @@
28
28
  },
29
29
  "scripts": {
30
30
  "build": "tsc -p tsconfig.json",
31
- "check": "tsc -p tsconfig.json --noEmit"
31
+ "check": "tsc -p tsconfig.json --noEmit",
32
+ "pack:dry-run": "env -u npm_config_recursive npm --cache ${TMPDIR:-/tmp}/threelight-npm-cache pack --dry-run",
33
+ "prepublishOnly": "pnpm run check && pnpm run build",
34
+ "release:alpha": "env -u npm_config_recursive npm --cache ${TMPDIR:-/tmp}/threelight-npm-cache publish --access public --tag alpha",
35
+ "release:tags": "env -u npm_config_recursive npm --cache ${TMPDIR:-/tmp}/threelight-npm-cache dist-tag ls @threelight/ui"
32
36
  }
33
- }
37
+ }