@typecad/ui 1.0.0-alpha.12 → 1.0.0-alpha.13

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.
Files changed (113) hide show
  1. package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
  2. package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  3. package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
  4. package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
  5. package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
  6. package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
  7. package/assets/fonts/dejavu/LICENSE +187 -0
  8. package/assets/fonts/dejavu/README.md +49 -0
  9. package/dist/engine-index.js +2 -0
  10. package/dist/preview/build-program.d.ts +7 -1
  11. package/dist/preview/build-program.js +94 -15
  12. package/dist/preview/host-gfx.d.ts +3 -0
  13. package/dist/preview/host-gfx.js +30 -3
  14. package/dist/preview/host-ui-runtime.d.ts +115 -0
  15. package/dist/preview/host-ui-runtime.js +928 -46
  16. package/dist/ui-engine/block-layout.js +25 -11
  17. package/dist/ui-engine/color.d.ts +6 -0
  18. package/dist/ui-engine/color.js +80 -2
  19. package/dist/ui-engine/compat-report.d.ts +8 -0
  20. package/dist/ui-engine/compat-report.js +124 -0
  21. package/dist/ui-engine/css-imports.d.ts +4 -0
  22. package/dist/ui-engine/css-imports.js +70 -0
  23. package/dist/ui-engine/css-parser.js +58 -14
  24. package/dist/ui-engine/default-font.d.ts +25 -0
  25. package/dist/ui-engine/default-font.js +80 -0
  26. package/dist/ui-engine/font-assets.d.ts +2 -2
  27. package/dist/ui-engine/font-assets.js +22 -5
  28. package/dist/ui-engine/html-parser.d.ts +4 -0
  29. package/dist/ui-engine/html-parser.js +241 -26
  30. package/dist/ui-engine/image-assets.d.ts +8 -1
  31. package/dist/ui-engine/image-assets.js +35 -2
  32. package/dist/ui-engine/image-decode.d.ts +22 -0
  33. package/dist/ui-engine/image-decode.js +194 -0
  34. package/dist/ui-engine/inline-parser.js +1 -1
  35. package/dist/ui-engine/layout-engine.d.ts +19 -0
  36. package/dist/ui-engine/layout-engine.js +59 -3
  37. package/dist/ui-engine/model.d.ts +18 -0
  38. package/dist/ui-engine/model.js +75 -7
  39. package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
  40. package/dist/ui-engine/runtime-header/canvas-helpers.js +3 -0
  41. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
  42. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
  43. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
  44. package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
  45. package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
  46. package/dist/ui-engine/runtime-header/keyboard.js +682 -380
  47. package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1386
  48. package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
  49. package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
  50. package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
  51. package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
  52. package/dist/ui-engine/runtime-header/structs.js +228 -216
  53. package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
  54. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
  55. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
  56. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
  57. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +78 -23
  58. package/dist/ui-engine/runtime-header/types-defines.js +110 -93
  59. package/dist/ui-engine/shadcn-kit.d.ts +1 -0
  60. package/dist/ui-engine/shadcn-kit.js +502 -0
  61. package/dist/ui-engine/style-resolver.d.ts +4 -0
  62. package/dist/ui-engine/style-resolver.js +58 -6
  63. package/dist/ui-engine/transpile-ui.js +12 -5
  64. package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
  65. package/dist/ui-engine/ua-stylesheet.js +119 -32
  66. package/dist/ui-engine/ui-lowering.js +37 -3
  67. package/dist/ui-engine/ui-registry.js +32 -4
  68. package/dist/ui-engine/yoga-layout.js +57 -52
  69. package/package.json +9 -3
  70. package/src/engine-index.ts +53 -51
  71. package/src/preview/build-program.ts +810 -734
  72. package/src/preview/host-gfx.ts +30 -3
  73. package/src/preview/host-ui-runtime.ts +4133 -3289
  74. package/src/ui-engine/block-layout.ts +24 -11
  75. package/src/ui-engine/color.ts +77 -2
  76. package/src/ui-engine/compat-report.ts +139 -0
  77. package/src/ui-engine/css-imports.ts +69 -0
  78. package/src/ui-engine/css-parser.ts +64 -15
  79. package/src/ui-engine/default-font.ts +95 -0
  80. package/src/ui-engine/font-assets.ts +545 -525
  81. package/src/ui-engine/html-parser.ts +615 -397
  82. package/src/ui-engine/image-assets.ts +37 -2
  83. package/src/ui-engine/image-decode.ts +244 -0
  84. package/src/ui-engine/inline-parser.ts +1 -1
  85. package/src/ui-engine/layout-engine.ts +61 -3
  86. package/src/ui-engine/model.ts +1317 -1230
  87. package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
  88. package/src/ui-engine/runtime-header/canvas-helpers.ts +3 -0
  89. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
  90. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
  91. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
  92. package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
  93. package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
  94. package/src/ui-engine/runtime-header/keyboard.ts +689 -386
  95. package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1411
  96. package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
  97. package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
  98. package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
  99. package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
  100. package/src/ui-engine/runtime-header/structs.ts +234 -222
  101. package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
  102. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
  103. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
  104. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
  105. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +78 -23
  106. package/src/ui-engine/runtime-header/types-defines.ts +116 -99
  107. package/src/ui-engine/shadcn-kit.ts +503 -0
  108. package/src/ui-engine/style-resolver.ts +471 -416
  109. package/src/ui-engine/transpile-ui.ts +12 -5
  110. package/src/ui-engine/ua-stylesheet.ts +137 -31
  111. package/src/ui-engine/ui-lowering.ts +486 -452
  112. package/src/ui-engine/ui-registry.ts +556 -524
  113. package/src/ui-engine/yoga-layout.ts +307 -309
@@ -0,0 +1,502 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The built-in shadcn-style kit stylesheet (tokens + class recipes). Always
3
+ // prepended to the CSS chain by BOTH pipelines (ui-registry for device
4
+ // builds, buildPreviewSnapshot for the preview) ahead of the user's CSS, so
5
+ // user rules and theme token blocks override it by cascade order. Themes are
6
+ // plain CSS files the user @imports — no registry, no splicing.
7
+ //
8
+ // Source of truth: this string. Edit here; the demos rely on it verbatim.
9
+ // ---------------------------------------------------------------------------
10
+ export const SHADCN_KIT_CSS = `/* ---------------------------------------------------------------------------
11
+ * shadcn-style component kit — BUILT IN and always included.
12
+ *
13
+ * These tokens + class recipes are prepended to every build and preview
14
+ * automatically, BEFORE your stylesheets, so anything you write overrides
15
+ * them by normal cascade order. No scaffolding, no imports needed: put the
16
+ * classes on native elements and they work.
17
+ *
18
+ * THEMES: a theme is any CSS file. Pre-packaged ones ship with @typecad/ui —
19
+ * import by bare specifier from a <style> block (or the sidecar .ui.css):
20
+ *
21
+ * @import "@typecad/ui/themes/blue.css"; (zinc slate stone gray neutral
22
+ * blue green red)
23
+ *
24
+ * Your own: save a ui.shadcn.com / tweakcn export into the project and
25
+ * @import it by path — its :root/.dark token blocks override the kit
26
+ * defaults (later definitions win). Both dialects parse — classic HSL channel triplets and
27
+ * Tailwind-v4 oklch(); alpha in colors is ignored (no blending on bare
28
+ * metal). Dark mode activates with themeClass: 'dark' in the config's
29
+ * display block.
30
+ *
31
+ * Overriding recipes: redefine any class in your own stylesheet; your
32
+ * definition wins.
33
+ *
34
+ * Components (classes over native elements):
35
+ * Button <button class="btn btn-primary">Save</button>
36
+ * variants: -secondary -outline -ghost -destructive
37
+ * sizes: .btn-sm .btn-lg full width: .btn-block
38
+ * Badge <text class="badge badge-secondary">new</text>
39
+ * variants: -default(omit) -destructive -outline
40
+ * Card <view class="card">
41
+ * <view class="card-header">
42
+ * <text class="card-title">Title</text>
43
+ * <text class="card-description">Subtitle</text>
44
+ * </view>
45
+ * <view class="card-content">...</view>
46
+ * <view class="card-footer">...buttons...</view>
47
+ * </view>
48
+ * Input <input class="input" placeholder="..."/>
49
+ * Label <label class="form-label">Name</label>
50
+ * Row (kit) <view class="row"> ... </view> (flex row + gap, for composing)
51
+ * Validation .input-error (destructive border) + .field-error hint;
52
+ * runtime-driven: bind borderColor/visible to a signal
53
+ * Separator <hr class="separator"/> (horizontal) or
54
+ * <hr class="vseparator"/> (vertical; stretches to the row height)
55
+ * Alert <view class="alert alert-destructive">
56
+ * <text class="alert-title">...</text>
57
+ * <text class="alert-description">...</text>
58
+ * </view>
59
+ * Skeleton <view class="skeleton"/> (pulse while loading)
60
+ * Spinner <view class="spinner"><view class="spinner-dot"/></view>
61
+ * (indeterminate loading: a dot orbiting a ring via pure
62
+ * transform keyframes — translate lerps smoothly, unlike
63
+ * rotate which only renders exact quarter turns)
64
+ * Dialog <dialog id="d"><view class="dialog-scrim" on:click={...}/>
65
+ * <view class="dialog-card"> ... <view class="dialog-footer">
66
+ * </view></dialog>
67
+ * (centered modal: programmatic ui.dialog.open(id), scrim tap
68
+ * closes; same slot machinery as the drawer)
69
+ * Toast <toast id="t" side="bottom" duration="2500" class="toast">
70
+ * ...</toast> (ui.toast(id) shows it; auto-closes after the
71
+ * duration; stacks as authored siblings)
72
+ * Progress <progress class="progress" value="40"/>
73
+ * Avatar <img class="avatar" src="face.bmp"/>
74
+ * Switch <check class="switch"/> (pill container; the
75
+ * 16px indicator is runtime-drawn at the top-left — best-effort)
76
+ * Tabs <view class="tabs-list"><button class="tabs-trigger">..</button>..
77
+ * <view class="tabs-content-area"><view class="tabs-content">..
78
+ * (panes toggle via ui.bind(x, 'visible', ...) on a signal)
79
+ * Accordion <view class="accordion"><view class="accordion-item">..
80
+ * (content toggles via ui.bind visible; chevron via text bind)
81
+ * Table <table> ... </table> (UA styles apply; see the
82
+ * html-table-approximation build note)
83
+ * ------------------------------------------------------------------------- */
84
+
85
+ :root {
86
+ --background: #ffffff;
87
+ --foreground: #09090b;
88
+ --card: #ffffff;
89
+ --card-foreground: #09090b;
90
+ --primary: #18181b;
91
+ --primary-foreground: #fafafa;
92
+ --secondary: #f4f4f5;
93
+ --secondary-foreground: #18181b;
94
+ --muted: #f4f4f5;
95
+ --muted-foreground: #71717a;
96
+ --accent: #f4f4f5;
97
+ --accent-foreground: #18181b;
98
+ --destructive: #dc2626;
99
+ --destructive-foreground: #fafafa;
100
+ --destructive-background: #fee2e2;
101
+ --border: #e4e4e7;
102
+ --input: #e4e4e7;
103
+ --radius: 8px;
104
+ --shadow-sm: 0 1px 3px rgb(0 0 0 / 0.1);
105
+ /* Solid scrim for dialogs — alpha blending needs a canvas underneath, so
106
+ the kit uses an opaque near-black that reads as a dimmed backdrop. */
107
+ --scrim: #101014;
108
+ }
109
+
110
+ .dark {
111
+ --background: #09090b;
112
+ --foreground: #fafafa;
113
+ --card: #18181b;
114
+ --card-foreground: #fafafa;
115
+ --primary: #fafafa;
116
+ --primary-foreground: #18181b;
117
+ --secondary: #27272a;
118
+ --secondary-foreground: #fafafa;
119
+ --muted: #27272a;
120
+ --muted-foreground: #a1a1aa;
121
+ --accent: #27272a;
122
+ --accent-foreground: #fafafa;
123
+ --destructive: #ef4444;
124
+ --destructive-foreground: #09090b;
125
+ --destructive-background: #451a1a;
126
+ --border: #27272a;
127
+ --input: #3f3f46;
128
+ --radius: 8px;
129
+ }
130
+
131
+ /* ---- Button ---------------------------------------------------------------- */
132
+
133
+ .btn {
134
+ border-radius: var(--radius);
135
+ padding: 10px 16px;
136
+ font-weight: bold;
137
+ transition: background 80ms;
138
+ }
139
+ .btn:pressed { transform: translateY(1px); }
140
+ .btn-primary { background: var(--primary); color: var(--primary-foreground); }
141
+ .btn-secondary { background: var(--secondary); color: var(--secondary-foreground); }
142
+ .btn-outline { background: var(--background); color: var(--foreground); border: 1px solid var(--border); }
143
+ .btn-ghost { color: var(--foreground); }
144
+ .btn-destructive { background: var(--destructive); color: var(--destructive-foreground); }
145
+ .btn-sm { padding: 6px 10px; min-height: 32px; font-size: 12px; }
146
+ .btn-lg { padding: 14px 22px; min-height: 52px; font-size: 18px; }
147
+ .btn-block { align-self: stretch; }
148
+
149
+ /* ---- Badge ------------------------------------------------------------------ */
150
+
151
+ .badge {
152
+ background: var(--primary);
153
+ color: var(--primary-foreground);
154
+ padding: 2px 8px;
155
+ border-radius: 999px;
156
+ font-size: 12px;
157
+ text-align: center;
158
+ }
159
+ .badge-secondary { background: var(--secondary); color: var(--secondary-foreground); }
160
+ .badge-destructive { background: var(--destructive); color: var(--destructive-foreground); }
161
+ .badge-outline { background: var(--background); color: var(--foreground); border: 1px solid var(--border); }
162
+
163
+ /* ---- Layout utility -------------------------------------------------------------
164
+ Not a shadcn component — a kit convenience for composing recipes. */
165
+
166
+ .row { flex-direction: row; align-items: center; gap: 8px; }
167
+
168
+ /* ---- Card ------------------------------------------------------------------- */
169
+
170
+ .card {
171
+ background: var(--card);
172
+ color: var(--card-foreground);
173
+ border: 1px solid var(--border);
174
+ border-radius: var(--radius);
175
+ box-shadow: var(--shadow-sm);
176
+ padding: 16px;
177
+ gap: 8px;
178
+ align-self: stretch;
179
+ }
180
+ .card-header { gap: 4px; }
181
+ .card-title { font-size: 18px; font-weight: bold; }
182
+ .card-description { font-size: 14px; color: var(--muted-foreground); }
183
+ .card-content { gap: 8px; }
184
+ .card-footer { flex-direction: row; gap: 8px; }
185
+
186
+ /* ---- Tabs ---------------------------------------------------------------- */
187
+
188
+ /* Segmented trigger row (shadcn TabsList). Active-trigger styling is driven
189
+ at runtime by ui.bind background/color bindings in author code — runtime
190
+ color swaps are literal hex (compile-time tokens), so pin them to the
191
+ active theme the way native_demo accents do. */
192
+ .tabs-list {
193
+ flex-direction: row;
194
+ gap: 4px;
195
+ align-self: stretch;
196
+ background: var(--muted);
197
+ border-radius: var(--radius);
198
+ padding: 4px;
199
+ }
200
+ .tabs-trigger {
201
+ flex-grow: 1;
202
+ border-radius: calc(var(--radius) - 2px);
203
+ padding: 6px 10px;
204
+ min-height: 32px;
205
+ font-size: 13px;
206
+ text-align: center;
207
+ }
208
+ /* Fixed-height content region: panes are absolutely stacked inside it and
209
+ toggled via ui.bind(x, 'visible', ...) — layout keeps every pane's box,
210
+ so switching never re-flows. */
211
+ .tabs-content-area {
212
+ position: relative;
213
+ align-self: stretch;
214
+ height: 150px;
215
+ }
216
+ .tabs-content {
217
+ position: absolute;
218
+ left: 0;
219
+ top: 0;
220
+ right: 0;
221
+ bottom: 0;
222
+ gap: 8px;
223
+ }
224
+
225
+ /* ---- Accordion (single-open) --------------------------------------------- */
226
+
227
+ /* Stacked collapsible sections. The trigger is a button + chevron row; the
228
+ content pane toggles via ui.bind(x, 'visible', () => signal === i) — the
229
+ same signal pattern as Tabs. The chevron swaps v/^ via a text binding. */
230
+ .accordion {
231
+ gap: 8px;
232
+ align-self: stretch;
233
+ }
234
+ .accordion-item {
235
+ background: var(--card);
236
+ border: 1px solid var(--border);
237
+ border-radius: var(--radius);
238
+ }
239
+ .accordion-trigger-row {
240
+ flex-direction: row;
241
+ align-items: center;
242
+ gap: 8px;
243
+ padding: 10px 12px;
244
+ }
245
+ .accordion-trigger {
246
+ flex-grow: 1;
247
+ text-align: left;
248
+ font-weight: bold;
249
+ padding: 0;
250
+ min-height: 20px;
251
+ /* The UA sheet gives every button a 1px border (border: 1px solid, colored
252
+ by the foreground token). A trigger is a plain text row on the card —
253
+ cancel it or each one renders with a bright outline. */
254
+ border: none;
255
+ }
256
+ .accordion-chevron {
257
+ color: var(--muted-foreground);
258
+ font-size: 14px;
259
+ /* Wide enough for the '^' glyph (advance 12px at 14px size) — a single
260
+ char wider than the box wraps to nothing. */
261
+ width: 16px;
262
+ text-align: center;
263
+ }
264
+ .accordion-content {
265
+ padding: 0 12px 10px 12px;
266
+ gap: 8px;
267
+ }
268
+
269
+ /* ---- Form controls ----------------------------------------------------------- */
270
+
271
+ .input {
272
+ background: var(--background);
273
+ color: var(--foreground);
274
+ border: 1px solid var(--input);
275
+ border-radius: var(--radius);
276
+ }
277
+ .form-label { color: var(--foreground); font-size: 14px; }
278
+
279
+ /* Switch: pill container. The runtime draws a 16px circular knob that
280
+ slides with state — hollow at the left when off, solid at the right when
281
+ on (a static jump, no travel animation). min-height overrides the UA
282
+ touch target so the pill stays 26px tall. */
283
+ .switch {
284
+ width: 44px;
285
+ height: 26px;
286
+ /* Override the UA's touch-target min-height (42px on small panels) — the
287
+ pill must stay 26px tall to read as a switch, not a checkbox. */
288
+ min-height: 26px;
289
+ border-radius: 999px;
290
+ border: 1px solid var(--border);
291
+ /* --card, not --muted: on a bare (near-black) page background a --muted
292
+ track reads as a stray gray box; --card keeps the pill affordance while
293
+ staying quiet. Swap back to var(--muted) inside lighter containers. */
294
+ background: var(--card);
295
+ }
296
+
297
+ /* ---- Checked-state pairs (shadcn data-[state=checked]) ------------------------
298
+ The ON control carries the primary pair: the switch's TRACK turns primary
299
+ with a primary-foreground knob, the checkbox face and the radio's selected
300
+ ring do the same. The select's pair themes the OPTION LIST's selected row
301
+ with the accent pair (shadcn's SelectItem selected state). These bake at
302
+ build time from :checked rules (a separate style bucket, like :pressed);
303
+ the device runtime swaps the pair onto the indicator when the value flips,
304
+ and the preview draws through the same fields. */
305
+ .switch:checked {
306
+ background: var(--primary);
307
+ border-color: var(--primary);
308
+ color: var(--primary-foreground);
309
+ }
310
+ check:checked {
311
+ background: var(--primary);
312
+ color: var(--primary-foreground);
313
+ }
314
+ radio:checked {
315
+ background: var(--primary);
316
+ color: var(--primary-foreground);
317
+ }
318
+ .select:checked {
319
+ background: var(--accent);
320
+ color: var(--accent-foreground);
321
+ }
322
+
323
+ /* ---- Form validation states -------------------------------------------------- */
324
+
325
+ /* Static hook: always-invalid styling for hardcoded markup. Runtime-driven
326
+ validation (recommended) binds the input's borderColor + a .field-error
327
+ hint's visible/text to a signal — see the demo's Forms screen. */
328
+ .input-error {
329
+ border-color: var(--destructive);
330
+ }
331
+ .field-error {
332
+ color: var(--destructive);
333
+ font-size: 12px;
334
+ }
335
+ .field-success {
336
+ color: var(--muted-foreground);
337
+ font-size: 12px;
338
+ }
339
+
340
+ /* ---- Separator ---------------------------------------------------------------- */
341
+
342
+ .separator {
343
+ height: 1px;
344
+ background: var(--border);
345
+ margin: 8px 0;
346
+ }
347
+ /* Vertical: 1px wide, stretches to the row's cross height (shadcn's
348
+ <Separator orientation="vertical" /> — flex column + align-self: stretch). */
349
+ .vseparator {
350
+ width: 1px;
351
+ height: 100%;
352
+ align-self: stretch;
353
+ background: var(--border);
354
+ margin: 0 8px;
355
+ }
356
+
357
+ /* ---- Alert -------------------------------------------------------------------- */
358
+
359
+ .alert {
360
+ background: var(--background);
361
+ color: var(--foreground);
362
+ border: 1px solid var(--border);
363
+ border-radius: var(--radius);
364
+ padding: 12px;
365
+ gap: 4px;
366
+ align-self: stretch;
367
+ }
368
+ .alert-destructive {
369
+ background: var(--destructive-background);
370
+ border: 1px solid var(--destructive);
371
+ color: var(--destructive);
372
+ }
373
+ .alert-title { font-weight: bold; }
374
+ .alert-description { font-size: 14px; color: var(--muted-foreground); }
375
+
376
+ /* ---- Skeleton (loading placeholder) --------------------------------------------- */
377
+
378
+ /* Opacity pulse. If the target display's keyframe support skips opacity,
379
+ the block still renders as a static muted placeholder. */
380
+ .skeleton {
381
+ background: var(--muted);
382
+ border-radius: var(--radius);
383
+ height: 16px;
384
+ align-self: stretch;
385
+ animation: ui-skeleton-pulse 1.2s ease-in-out infinite;
386
+ }
387
+ @keyframes ui-skeleton-pulse {
388
+ 0% { opacity: 1; }
389
+ 50% { opacity: 0.55; }
390
+ 100% { opacity: 1; }
391
+ }
392
+
393
+ /* ---- Spinner ---------------------------------------------------------------- */
394
+
395
+ /* Indeterminate loading indicator: a dot orbiting inside a ring, pure
396
+ transform keyframes. translate() lerps CONTINUOUSLY between stops (unlike
397
+ rotate(), which only renders exact quarter turns), so the orbit is smooth.
398
+ The dot is absolute + out of flow; the ring is a fixed square so border
399
+ clipping stays symmetric. Sizes: change .spinner's width/height and keep
400
+ the dot inset consistent (orbit travel = inner - dot). */
401
+ .spinner {
402
+ position: relative;
403
+ width: 22px;
404
+ height: 22px;
405
+ border: 2px solid var(--muted);
406
+ border-radius: 999px;
407
+ }
408
+ .spinner-dot {
409
+ position: absolute;
410
+ /* The dot's BASE position is the orbit's TOP-LEFT corner, not the ring
411
+ center: the keyframes translate 0..8px from here, and the path only
412
+ centers when base + travel/2 == ring center (with the 2px border,
413
+ top/left 2px + border lands the 6px dot at 4,4; its center travels
414
+ 7..15 around the 22px ring's center at 11,11). */
415
+ top: 2px;
416
+ left: 2px;
417
+ width: 6px;
418
+ height: 6px;
419
+ background: var(--primary);
420
+ border-radius: 999px;
421
+ animation: ui-spinner-orbit 1000ms linear infinite;
422
+ }
423
+ @keyframes ui-spinner-orbit {
424
+ 0% { transform: translate(0px, 0px); }
425
+ 25% { transform: translate(8px, 0px); }
426
+ 50% { transform: translate(8px, 8px); }
427
+ 75% { transform: translate(0px, 8px); }
428
+ 100% { transform: translate(0px, 0px); }
429
+ }
430
+ /* ---- Dialog ---------------------------------------------------------------- */
431
+
432
+ /* Centered modal: <dialog> is a centered drawer (side-free) — programmatic
433
+ open/close via ui.dialog.open(id)/close(id?), hidden while closed by the
434
+ runtime (visibility gate for centered panels; offsets can't hide them).
435
+ The scrim is part of the markup: an absolute full-area view whose tap
436
+ closes the dialog; the card sits above it. z-index keeps both over page
437
+ content. */
438
+ .dialog-scrim {
439
+ position: absolute;
440
+ left: 0;
441
+ right: 0;
442
+ top: 0;
443
+ bottom: 0;
444
+ background: var(--scrim);
445
+ z-index: 30;
446
+ }
447
+ .dialog-card {
448
+ position: absolute;
449
+ left: 24px;
450
+ right: 24px;
451
+ top: 64px;
452
+ background: var(--card);
453
+ color: var(--card-foreground);
454
+ border: 1px solid var(--border);
455
+ border-radius: var(--radius);
456
+ box-shadow: var(--shadow-lg);
457
+ padding: 16px;
458
+ gap: 8px;
459
+ z-index: 31;
460
+ }
461
+ .dialog-footer {
462
+ flex-direction: row;
463
+ justify-content: flex-end;
464
+ gap: 8px;
465
+ }
466
+
467
+ /* ---- Toast ----------------------------------------------------------------- */
468
+
469
+ /* Transient notification: <toast side="bottom" duration="2500"> slides from
470
+ the bottom edge (author-positioned, flush to the edge so the slide fully
471
+ hides it) and auto-closes after duration ms. Show with ui.toast(id).
472
+ Sibling toasts stack by author layout; each closes on its own timer. */
473
+ .toast {
474
+ position: absolute;
475
+ left: 12px;
476
+ right: 12px;
477
+ bottom: 0;
478
+ background: var(--foreground);
479
+ color: var(--background);
480
+ border-radius: var(--radius);
481
+ padding: 12px;
482
+ gap: 4px;
483
+ z-index: 40;
484
+ }
485
+ .toast-title { font-weight: bold; font-size: 14px; }
486
+ .toast-description { font-size: 12px; color: var(--muted-foreground); }
487
+
488
+
489
+
490
+ /* ---- Progress / Avatar ----------------------------------------------------------- */
491
+
492
+ .progress {
493
+ height: 12px;
494
+ border-radius: 999px;
495
+ background: var(--secondary);
496
+ border: 1px solid var(--border);
497
+ color: var(--primary);
498
+ }
499
+ /* Avatar: plain image element. The runtime draws images rectangular — no
500
+ rounded clipping — so no border/radius here (a border would just draw
501
+ over the image). Wrap in a sized view if you want a frame. */
502
+ `;
@@ -52,7 +52,11 @@ export interface StyledNode {
52
52
  canvasW?: number;
53
53
  /** Canvas buffer height in pixels (for <canvas>). */
54
54
  canvasH?: number;
55
+ /** HTML presentational attribute: blocks taps/keyboard, dims the draw. */
55
56
  disabled?: boolean;
57
+ /** <drawer side="..."> — the edge the panel slides from (bottom default). */
58
+ drawerSide?: string;
59
+ toastDuration?: number;
56
60
  /** True when text contains a `{expr}` interpolation; auto-wire synthesizes an
57
61
  * implicit text binding. Propagated from UIElementNode for the auto-wire walk. */
58
62
  hasInterpolation?: boolean;
@@ -2,9 +2,12 @@
2
2
  // Style resolver — match CSS rules to element nodes and produce a computed
3
3
  // style per node (base state + :pressed state).
4
4
  //
5
- // Walks the tree; for each node, applies every matching rule in source order
6
- // (cascade: later rules win). :pressed rules populate the separate
7
- // `style.pressed` object so the runtime can lerp to it on press.
5
+ // Walks the tree; for each node, applies every matching rule in cascade order
6
+ // (UA sheet first, then author rules by specificity, then source order — the
7
+ // rule applied last wins each property, matching browser cascade rules; the
8
+ // inline style attribute always wins over all stylesheet rules).
9
+ // :pressed rules populate the separate `style.pressed` object so the runtime
10
+ // can lerp to it on press.
8
11
  // ---------------------------------------------------------------------------
9
12
  import { parseInlineStyle } from "./css-parser.js";
10
13
  import { getUARules } from "./ua-stylesheet.js";
@@ -222,13 +225,52 @@ function flattenInline(items, parentStyle, rules, ancestors, diagnostics) {
222
225
  walk(items, parentStyle, undefined);
223
226
  return runs;
224
227
  }
228
+ /** CSS specificity as (a, b, c): a = id selectors, b = class/attribute/
229
+ * pseudo-class selectors, c = element selectors. :not()'s argument counts
230
+ * its own simples (the :not itself adds nothing), per the CSS spec. */
231
+ function specificityOf(sel) {
232
+ let a = 0, b = 0, c = 0;
233
+ const count = (compounds) => {
234
+ for (const compound of compounds) {
235
+ for (const s of compound) {
236
+ if (s.kind === "id")
237
+ a++;
238
+ else if (s.kind === "class" || s.kind === "attribute")
239
+ b++;
240
+ else if (s.kind === "element")
241
+ c++;
242
+ }
243
+ }
244
+ };
245
+ count(sel.compounds);
246
+ if (sel.not)
247
+ count(sel.not);
248
+ if (sel.pseudo)
249
+ b++;
250
+ return [a, b, c];
251
+ }
252
+ /** Sort rules into cascade application order: ascending specificity (a, b, c),
253
+ * then source order (Array.prototype.sort is stable, so equal-specificity
254
+ * rules keep source order and the later one wins). */
255
+ function cascadeOrder(rules) {
256
+ return rules
257
+ .map((rule, order) => ({ rule, order, spec: specificityOf(rule.selector) }))
258
+ .sort((x, y) => x.spec[0] - y.spec[0] ||
259
+ x.spec[1] - y.spec[1] ||
260
+ x.spec[2] - y.spec[2] ||
261
+ x.order - y.order)
262
+ .map((e) => e.rule);
263
+ }
225
264
  export function resolveStyles(root, rules, diagnostics) {
226
- const allRules = [...getUARules(), ...rules];
265
+ // Two cascade layers: the UA sheet loses to any author rule regardless of
266
+ // specificity; author rules cascade by specificity then source order.
267
+ const allRules = [...cascadeOrder(getUARules()), ...cascadeOrder(rules)];
227
268
  return resolveNode(root, allRules, [], diagnostics, undefined);
228
269
  }
229
270
  function resolveNode(node, rules, ancestors, diagnostics, parentInherited) {
230
271
  const base = {};
231
272
  const pressed = {};
273
+ const checked = {};
232
274
  // Preceding siblings of this node (most-recent-first), for + and ~ combinators.
233
275
  // The parent is the last ancestor; its children before this node are siblings.
234
276
  const parent = ancestors.length > 0 ? ancestors[ancestors.length - 1] : undefined;
@@ -249,8 +291,11 @@ function resolveNode(node, rules, ancestors, diagnostics, parentInherited) {
249
291
  Object.assign(pressed, rule.properties);
250
292
  }
251
293
  else if (pseudo === "checked") {
252
- if (node.checked)
253
- Object.assign(base, rule.properties);
294
+ // Baked as a separate checked-state pair (like :pressed), consumed by
295
+ // the check/radio/select draw paths — the runtime swaps the pair when
296
+ // the value flips. Merging into base only ever worked for statically
297
+ // checked markup and would repaint the node's whole box.
298
+ Object.assign(checked, rule.properties);
254
299
  }
255
300
  else if (pseudo === "disabled") {
256
301
  if (node.disabled)
@@ -277,6 +322,11 @@ function resolveNode(node, rules, ancestors, diagnostics, parentInherited) {
277
322
  // Attach pressed overrides; the transition driver reads these on press.
278
323
  style.pressed = pressed;
279
324
  }
325
+ if (Object.keys(checked).length > 0) {
326
+ // Attach checked-state overrides; the lowering bakes the resolved pair
327
+ // (checkedBg/checkedFg) into the node for runtime state swaps.
328
+ style.checked = checked;
329
+ }
280
330
  // Apply CSS inheritance: for each inherited key the node didn't set itself
281
331
  // (no rule, no inline, no hidden override), take the parent's resolved value.
282
332
  // The node's own value always wins; only the gaps are filled.
@@ -323,6 +373,8 @@ function resolveNode(node, rules, ancestors, diagnostics, parentInherited) {
323
373
  type: node.type,
324
374
  placeholder: node.placeholder,
325
375
  maxlen: node.maxlength,
376
+ drawerSide: node.drawerSide,
377
+ toastDuration: node.toastDuration,
326
378
  keyboard: node.keyboard,
327
379
  hidden: node.hidden,
328
380
  href: node.href,
@@ -17,17 +17,24 @@ import { measureWithFonts } from "./layout-engine.js";
17
17
  import { lowerUIToCpp } from "./ui-lowering.js";
18
18
  import { buildUIFontAssets } from "./font-assets.js";
19
19
  import { buildKeyframeSets } from "./keyframes.js";
20
+ import { injectDefaultFontFaces } from "./default-font.js";
21
+ import { cssCompatDiagnostics } from "./compat-report.js";
22
+ import { expandCssImports } from "./css-imports.js";
20
23
  export function transpileUI(html, css, opts) {
21
24
  const tree = parseHtml(html);
22
- const rules = parseCss(css);
23
- const fontFaces = parseFontFaces(css);
24
- const rawKeyframes = parseKeyframes(css);
25
+ const cssBaseDir = path.resolve(opts.assetBaseDir ?? process.cwd());
26
+ const expandedCss = expandCssImports(css, cssBaseDir);
27
+ const rules = parseCss(expandedCss);
28
+ const fontFaces = injectDefaultFontFaces(parseFontFaces(expandedCss), opts.colorFormat);
29
+ const rawKeyframes = parseKeyframes(expandedCss);
25
30
  const styled = resolveStyles(tree, rules);
26
- const fontAssets = buildUIFontAssets(styled, fontFaces, path.resolve(opts.assetBaseDir ?? process.cwd()));
31
+ const fontAssets = buildUIFontAssets(styled, fontFaces, cssBaseDir);
27
32
  // Select layout engine: Yoga for flexbox, BlockLayout as fallback.
28
33
  const engine = selectEngine(styled);
29
34
  const viewport = { x: 0, y: 0, w: opts.viewport.width, h: opts.viewport.height };
30
35
  const boxes = engine.arrange(styled, viewport, measureWithFonts(fontAssets));
31
36
  const keyframeSets = buildKeyframeSets(rawKeyframes, opts.colorFormat);
32
- return lowerUIToCpp(styled, boxes, opts.colorFormat, opts.storage, [], rules, undefined, fontAssets, [], new Map(), keyframeSets);
37
+ const result = lowerUIToCpp(styled, boxes, opts.colorFormat, opts.storage, [], rules, undefined, fontAssets, [], new Map(), keyframeSets);
38
+ result.diagnostics.push(...cssCompatDiagnostics([styled], fontAssets, opts.viewport, opts.colorFormat, "inline"));
39
+ return result;
33
40
  }
@@ -1,3 +1,24 @@
1
1
  import type { CSSRule } from "./css-parser.js";
2
- /** Get the parsed UA default rules. Lazily computed, cached. */
2
+ /** The derived UA scale for one display profile. */
3
+ export interface UAScale {
4
+ /** Root (body text) size in px — the display-anchored "16px". */
5
+ root: number;
6
+ /** Monochrome display: stock bitmap font, quantized text sizes. */
7
+ mono: boolean;
8
+ /** Heading sizes h1..h6 in px. */
9
+ h: [number, number, number, number, number, number];
10
+ /** Vertical margin for headings and paragraphs (browser-like spacing). */
11
+ blockMargin: number;
12
+ /** Minimum touch-target height for buttons/selects/checks/radios. */
13
+ controlMinHeight: number;
14
+ /** Button/select vertical + horizontal padding. */
15
+ buttonPadV: number;
16
+ buttonPadH: number;
17
+ }
18
+ /** Derive the UA type scale from the display class. Height is the portrait
19
+ * dimension the profile reports; an unbound profile gets the color/large
20
+ * default (root 16 — the classic browser default). */
21
+ export declare function uaScaleFor(height: number | undefined, mono: boolean): UAScale;
22
+ /** Get the parsed UA default rules for the active display profile. Lazily
23
+ * computed, cached per scale (profile changes pick up a new scale key). */
3
24
  export declare function getUARules(): CSSRule[];