@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.
- package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
- package/assets/fonts/dejavu/LICENSE +187 -0
- package/assets/fonts/dejavu/README.md +49 -0
- package/dist/engine-index.js +2 -0
- package/dist/preview/build-program.d.ts +7 -1
- package/dist/preview/build-program.js +94 -15
- package/dist/preview/host-gfx.d.ts +3 -0
- package/dist/preview/host-gfx.js +30 -3
- package/dist/preview/host-ui-runtime.d.ts +115 -0
- package/dist/preview/host-ui-runtime.js +928 -46
- package/dist/ui-engine/block-layout.js +25 -11
- package/dist/ui-engine/color.d.ts +6 -0
- package/dist/ui-engine/color.js +80 -2
- package/dist/ui-engine/compat-report.d.ts +8 -0
- package/dist/ui-engine/compat-report.js +124 -0
- package/dist/ui-engine/css-imports.d.ts +4 -0
- package/dist/ui-engine/css-imports.js +70 -0
- package/dist/ui-engine/css-parser.js +58 -14
- package/dist/ui-engine/default-font.d.ts +25 -0
- package/dist/ui-engine/default-font.js +80 -0
- package/dist/ui-engine/font-assets.d.ts +2 -2
- package/dist/ui-engine/font-assets.js +22 -5
- package/dist/ui-engine/html-parser.d.ts +4 -0
- package/dist/ui-engine/html-parser.js +241 -26
- package/dist/ui-engine/image-assets.d.ts +8 -1
- package/dist/ui-engine/image-assets.js +35 -2
- package/dist/ui-engine/image-decode.d.ts +22 -0
- package/dist/ui-engine/image-decode.js +194 -0
- package/dist/ui-engine/inline-parser.js +1 -1
- package/dist/ui-engine/layout-engine.d.ts +19 -0
- package/dist/ui-engine/layout-engine.js +59 -3
- package/dist/ui-engine/model.d.ts +18 -0
- package/dist/ui-engine/model.js +75 -7
- package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
- package/dist/ui-engine/runtime-header/canvas-helpers.js +3 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
- package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
- package/dist/ui-engine/runtime-header/keyboard.js +682 -380
- package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1386
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
- package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
- package/dist/ui-engine/runtime-header/structs.js +228 -216
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +78 -23
- package/dist/ui-engine/runtime-header/types-defines.js +110 -93
- package/dist/ui-engine/shadcn-kit.d.ts +1 -0
- package/dist/ui-engine/shadcn-kit.js +502 -0
- package/dist/ui-engine/style-resolver.d.ts +4 -0
- package/dist/ui-engine/style-resolver.js +58 -6
- package/dist/ui-engine/transpile-ui.js +12 -5
- package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
- package/dist/ui-engine/ua-stylesheet.js +119 -32
- package/dist/ui-engine/ui-lowering.js +37 -3
- package/dist/ui-engine/ui-registry.js +32 -4
- package/dist/ui-engine/yoga-layout.js +57 -52
- package/package.json +9 -3
- package/src/engine-index.ts +53 -51
- package/src/preview/build-program.ts +810 -734
- package/src/preview/host-gfx.ts +30 -3
- package/src/preview/host-ui-runtime.ts +4133 -3289
- package/src/ui-engine/block-layout.ts +24 -11
- package/src/ui-engine/color.ts +77 -2
- package/src/ui-engine/compat-report.ts +139 -0
- package/src/ui-engine/css-imports.ts +69 -0
- package/src/ui-engine/css-parser.ts +64 -15
- package/src/ui-engine/default-font.ts +95 -0
- package/src/ui-engine/font-assets.ts +545 -525
- package/src/ui-engine/html-parser.ts +615 -397
- package/src/ui-engine/image-assets.ts +37 -2
- package/src/ui-engine/image-decode.ts +244 -0
- package/src/ui-engine/inline-parser.ts +1 -1
- package/src/ui-engine/layout-engine.ts +61 -3
- package/src/ui-engine/model.ts +1317 -1230
- package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
- package/src/ui-engine/runtime-header/canvas-helpers.ts +3 -0
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
- package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
- package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
- package/src/ui-engine/runtime-header/keyboard.ts +689 -386
- package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1411
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
- package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
- package/src/ui-engine/runtime-header/structs.ts +234 -222
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +78 -23
- package/src/ui-engine/runtime-header/types-defines.ts +116 -99
- package/src/ui-engine/shadcn-kit.ts +503 -0
- package/src/ui-engine/style-resolver.ts +471 -416
- package/src/ui-engine/transpile-ui.ts +12 -5
- package/src/ui-engine/ua-stylesheet.ts +137 -31
- package/src/ui-engine/ui-lowering.ts +486 -452
- package/src/ui-engine/ui-registry.ts +556 -524
- 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
|
|
6
|
-
// (
|
|
7
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
23
|
-
const
|
|
24
|
-
const
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
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[];
|