@burojs/ui-shadcn 0.3.0 → 0.5.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 +26 -7
- package/dist/bulk-summary-CwSd6Yjb.cjs +108 -0
- package/dist/bulk-summary-CwSd6Yjb.cjs.map +1 -0
- package/dist/bulk-summary-GxMDP4bw.js +108 -0
- package/dist/bulk-summary-GxMDP4bw.js.map +1 -0
- package/dist/button-BxnnE28H.cjs +55 -0
- package/dist/button-BxnnE28H.cjs.map +1 -0
- package/dist/button-MWVeZaqj.js +44 -0
- package/dist/button-MWVeZaqj.js.map +1 -0
- package/dist/candidates-gate-CX2PBu6l.js +63 -0
- package/dist/candidates-gate-CX2PBu6l.js.map +1 -0
- package/dist/candidates-gate-DiFI52r7.cjs +70 -0
- package/dist/candidates-gate-DiFI52r7.cjs.map +1 -0
- package/dist/candidates-relation-picker-CBsVcnPR.js +82 -0
- package/dist/candidates-relation-picker-CBsVcnPR.js.map +1 -0
- package/dist/candidates-relation-picker-Dd_GVLlu.cjs +84 -0
- package/dist/candidates-relation-picker-Dd_GVLlu.cjs.map +1 -0
- package/dist/check-confirm-dialog-BRByTFpg.cjs +1534 -0
- package/dist/check-confirm-dialog-BRByTFpg.cjs.map +1 -0
- package/dist/check-confirm-dialog-CkaVTPT9.js +1232 -0
- package/dist/check-confirm-dialog-CkaVTPT9.js.map +1 -0
- package/dist/dialog-B_rt9S1c.js +63 -0
- package/dist/dialog-B_rt9S1c.js.map +1 -0
- package/dist/dialog-DRNUq9EA.cjs +112 -0
- package/dist/dialog-DRNUq9EA.cjs.map +1 -0
- package/dist/filter-builder-B3mZFL-K.cjs +1761 -0
- package/dist/filter-builder-B3mZFL-K.cjs.map +1 -0
- package/dist/filter-builder-DOnaXNfP.js +1605 -0
- package/dist/filter-builder-DOnaXNfP.js.map +1 -0
- package/dist/flow-runtime-B6zOHYqI.cjs +255 -0
- package/dist/flow-runtime-B6zOHYqI.cjs.map +1 -0
- package/dist/flow-runtime-D8h99FSg.js +252 -0
- package/dist/flow-runtime-D8h99FSg.js.map +1 -0
- package/dist/flow-screen-CX05b3G9.cjs +705 -0
- package/dist/flow-screen-CX05b3G9.cjs.map +1 -0
- package/dist/flow-screen-CmbARSqg.js +701 -0
- package/dist/flow-screen-CmbARSqg.js.map +1 -0
- package/dist/index.cjs +6453 -17494
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3754 -3621
- package/dist/index.d.ts +3754 -3621
- package/dist/index.js +6217 -17219
- package/dist/index.js.map +1 -1
- package/dist/inline-flow-host-B7U1OJUe.cjs +112 -0
- package/dist/inline-flow-host-B7U1OJUe.cjs.map +1 -0
- package/dist/inline-flow-host-C3H2UpSA.js +112 -0
- package/dist/inline-flow-host-C3H2UpSA.js.map +1 -0
- package/dist/input-BI0czGyI.cjs +23 -0
- package/dist/input-BI0czGyI.cjs.map +1 -0
- package/dist/input-CUPStzZO.js +17 -0
- package/dist/input-CUPStzZO.js.map +1 -0
- package/dist/label-66W1qbke.js +29 -0
- package/dist/label-66W1qbke.js.map +1 -0
- package/dist/label-w6JYf2cF.cjs +36 -0
- package/dist/label-w6JYf2cF.cjs.map +1 -0
- package/dist/layout/theme-storage-key.cjs +5 -10
- package/dist/layout/theme-storage-key.cjs.map +1 -1
- package/dist/layout/theme-storage-key.d.cts +2 -23
- package/dist/layout/theme-storage-key.d.ts +2 -23
- package/dist/layout/theme-storage-key.js +5 -2
- package/dist/layout/theme-storage-key.js.map +1 -1
- package/dist/lazy-relation-picker-dialog-2oxnA6IR.js +14 -0
- package/dist/lazy-relation-picker-dialog-2oxnA6IR.js.map +1 -0
- package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs +19 -0
- package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs.map +1 -0
- package/dist/markdown/index.cjs +424 -511
- package/dist/markdown/index.cjs.map +1 -1
- package/dist/markdown/index.d.cts +1072 -34
- package/dist/markdown/index.d.ts +1072 -34
- package/dist/markdown/index.js +411 -459
- package/dist/markdown/index.js.map +1 -1
- package/dist/page-flow-route-CQ7HsbBm.cjs +277 -0
- package/dist/page-flow-route-CQ7HsbBm.cjs.map +1 -0
- package/dist/page-flow-route-CyXSvXSE.js +277 -0
- package/dist/page-flow-route-CyXSvXSE.js.map +1 -0
- package/dist/peek-filter-CnqHwTMU.cjs +51 -0
- package/dist/peek-filter-CnqHwTMU.cjs.map +1 -0
- package/dist/peek-filter-ErLfO153.js +28 -0
- package/dist/peek-filter-ErLfO153.js.map +1 -0
- package/dist/place-node--GdsP688.js +7315 -0
- package/dist/place-node--GdsP688.js.map +1 -0
- package/dist/place-node-SIp4XYQH.cjs +7833 -0
- package/dist/place-node-SIp4XYQH.cjs.map +1 -0
- package/dist/related-list-cards-B6x9HQQp.js +42 -0
- package/dist/related-list-cards-B6x9HQQp.js.map +1 -0
- package/dist/related-list-cards-nxuODlIA.cjs +42 -0
- package/dist/related-list-cards-nxuODlIA.cjs.map +1 -0
- package/dist/related-list-timeline-BYJKEbmA.cjs +142 -0
- package/dist/related-list-timeline-BYJKEbmA.cjs.map +1 -0
- package/dist/related-list-timeline-DEdfd-rr.js +142 -0
- package/dist/related-list-timeline-DEdfd-rr.js.map +1 -0
- package/dist/related-rows-B4levHcR.js +186 -0
- package/dist/related-rows-B4levHcR.js.map +1 -0
- package/dist/related-rows-zh5f_4QR.cjs +209 -0
- package/dist/related-rows-zh5f_4QR.cjs.map +1 -0
- package/dist/relation-create-BOaUe12G.cjs +362 -0
- package/dist/relation-create-BOaUe12G.cjs.map +1 -0
- package/dist/relation-create-Bg-eEJLs.js +337 -0
- package/dist/relation-create-Bg-eEJLs.js.map +1 -0
- package/dist/relation-many-picker-B-NCEUgX.cjs +465 -0
- package/dist/relation-many-picker-B-NCEUgX.cjs.map +1 -0
- package/dist/relation-many-picker-gdAOD-qK.js +464 -0
- package/dist/relation-many-picker-gdAOD-qK.js.map +1 -0
- package/dist/relation-picker-BAY77ivO.js +462 -0
- package/dist/relation-picker-BAY77ivO.js.map +1 -0
- package/dist/relation-picker-Da_uhFvA.cjs +479 -0
- package/dist/relation-picker-Da_uhFvA.cjs.map +1 -0
- package/dist/relation-picker-dialog-M0pQaWlp.cjs +300 -0
- package/dist/relation-picker-dialog-M0pQaWlp.cjs.map +1 -0
- package/dist/relation-picker-dialog-hA9rYrZB.js +299 -0
- package/dist/relation-picker-dialog-hA9rYrZB.js.map +1 -0
- package/dist/resource-page-router-OSQyRA5b.cjs +1779 -0
- package/dist/resource-page-router-OSQyRA5b.cjs.map +1 -0
- package/dist/resource-page-router-T4vS5gbJ.js +1582 -0
- package/dist/resource-page-router-T4vS5gbJ.js.map +1 -0
- package/dist/rich-text-DFerywgz.js +457 -0
- package/dist/rich-text-DFerywgz.js.map +1 -0
- package/dist/rich-text-vTyVjDoa.cjs +460 -0
- package/dist/rich-text-vTyVjDoa.cjs.map +1 -0
- package/dist/search-access-B7G5oR8n.cjs +812 -0
- package/dist/search-access-B7G5oR8n.cjs.map +1 -0
- package/dist/search-access-CURsWn-U.js +669 -0
- package/dist/search-access-CURsWn-U.js.map +1 -0
- package/dist/shell-BPlk8hEa.js +205 -0
- package/dist/shell-BPlk8hEa.js.map +1 -0
- package/dist/shell-DxhlsPwm.cjs +271 -0
- package/dist/shell-DxhlsPwm.cjs.map +1 -0
- package/dist/skeleton-U2noefj5.js +13 -0
- package/dist/skeleton-U2noefj5.js.map +1 -0
- package/dist/skeleton-_1gP7wan.cjs +18 -0
- package/dist/skeleton-_1gP7wan.cjs.map +1 -0
- package/dist/styles.css +255 -17
- package/dist/theme/theme-css.cjs +95 -96
- package/dist/theme/theme-css.cjs.map +1 -1
- package/dist/theme/theme-css.d.cts +10 -8
- package/dist/theme/theme-css.d.ts +10 -8
- package/dist/theme/theme-css.js +94 -94
- package/dist/theme/theme-css.js.map +1 -1
- package/dist/theme/theme-storage.cjs +34 -39
- package/dist/theme/theme-storage.cjs.map +1 -1
- package/dist/theme/theme-storage.d.cts +3 -1
- package/dist/theme/theme-storage.d.ts +3 -1
- package/dist/theme/theme-storage.js +33 -37
- package/dist/theme/theme-storage.js.map +1 -1
- package/dist/theme-storage-key-pYlp4aDS.d.cts +31 -0
- package/dist/theme-storage-key-pYlp4aDS.d.ts +31 -0
- package/dist/toaster-C7CiYA4Z.cjs +248 -0
- package/dist/toaster-C7CiYA4Z.cjs.map +1 -0
- package/dist/toaster-CfCRLgR-.js +164 -0
- package/dist/toaster-CfCRLgR-.js.map +1 -0
- package/dist/use-dialog-filter-state-BBCC0OEK.cjs +92 -0
- package/dist/use-dialog-filter-state-BBCC0OEK.cjs.map +1 -0
- package/dist/use-dialog-filter-state-BDcgS5Oz.js +87 -0
- package/dist/use-dialog-filter-state-BDcgS5Oz.js.map +1 -0
- package/dist/use-theme-DFlpXils.cjs +257 -0
- package/dist/use-theme-DFlpXils.cjs.map +1 -0
- package/dist/use-theme-dnE3Od3I.js +175 -0
- package/dist/use-theme-dnE3Od3I.js.map +1 -0
- package/package.json +18 -17
- package/dist/chunk-ABUQPOOE.js +0 -11
- package/dist/chunk-ABUQPOOE.js.map +0 -1
- package/dist/chunk-DBGGWBFG.cjs +0 -199
- package/dist/chunk-DBGGWBFG.cjs.map +0 -1
- package/dist/chunk-FMX4GLCQ.js +0 -6
- package/dist/chunk-FMX4GLCQ.js.map +0 -1
- package/dist/chunk-UC57Z243.js +0 -143
- package/dist/chunk-UC57Z243.js.map +0 -1
- package/dist/chunk-UPGPO75B.js +0 -164
- package/dist/chunk-UPGPO75B.js.map +0 -1
- package/dist/chunk-VXJAXJ3U.cjs +0 -8
- package/dist/chunk-VXJAXJ3U.cjs.map +0 -1
- package/dist/chunk-W7AEYE7A.cjs +0 -13
- package/dist/chunk-W7AEYE7A.cjs.map +0 -1
- package/dist/chunk-YF5IW525.cjs +0 -155
- package/dist/chunk-YF5IW525.cjs.map +0 -1
- package/dist/rich-text-6V7WZ3WV.cjs +0 -518
- package/dist/rich-text-6V7WZ3WV.cjs.map +0 -1
- package/dist/rich-text-N2FZEJJV.js +0 -491
- package/dist/rich-text-N2FZEJJV.js.map +0 -1
package/dist/styles.css
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* scans, or every `prose*` class compiles to nothing and the docs render
|
|
21
21
|
* unstyled. Opposite of the two imports above: this is a real Tailwind
|
|
22
22
|
* plugin, not a CSS library, so it takes `@plugin`, not `@import`. */
|
|
23
|
-
@plugin
|
|
23
|
+
@plugin "@tailwindcss/typography";
|
|
24
24
|
|
|
25
25
|
/* Resolves relative to THIS file. Published consumers scan compiled JS;
|
|
26
26
|
* source-aliased development hosts scan TS/TSX. Both are required because
|
|
27
27
|
* node_modules is excluded from Tailwind's automatic detection. */
|
|
28
|
-
@source
|
|
28
|
+
@source "./**/*.{js,ts,tsx}";
|
|
29
29
|
|
|
30
30
|
/* The documentation panel's opening.
|
|
31
31
|
*
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
* exactly this reason and read as two data points; it wasn't.)
|
|
90
90
|
* - red text on a 10%-red tint over --background
|
|
91
91
|
* (`bg-destructive/10 text-destructive` — badges, banners, inline
|
|
92
|
-
* errors; see forms/declarative-form.tsx,
|
|
92
|
+
* errors; see forms/declarative-form.tsx, record/record-screen.tsx,
|
|
93
93
|
* auth/login-form.tsx, resource/list-error-state.tsx,
|
|
94
94
|
* resource/tab-body.tsx, document-show-banner.tsx and others):
|
|
95
95
|
* 4.74:1
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
* `warning` would visibly change what's on screen once tasks 4-5 wire
|
|
130
130
|
* these up, which is exactly the kind of silent drift this contract
|
|
131
131
|
* exists to prevent. The one deliberate MERGE going the other way:
|
|
132
|
-
* `
|
|
132
|
+
* `record/record-screen.tsx`'s yellow-500 draft-banner collapses into `warning`
|
|
133
133
|
* (amber) — Tailwind's own OKLCH hue distance from amber-500 is 16°
|
|
134
134
|
* (amber↔yellow) vs 22.5° for both amber↔orange and sky↔blue, and L/C are
|
|
135
135
|
* nearly identical (76.9%/0.188 vs 79.5%/0.184) — this reads as accidental
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
* badge was the only context that existed when task 3 measured them. Task
|
|
146
146
|
* 4 initially reused the name `-foreground` for that tinted-text value
|
|
147
147
|
* too, and it broke at the one real filled-surface site that exists
|
|
148
|
-
*
|
|
148
|
+
* then: the old create wizard's completed-step circle
|
|
149
149
|
* (`bg-success text-success-foreground`) rendered dark-green-on-green at
|
|
150
150
|
* 2.19:1 — *worse* than the `text-white` it replaced. Split into two
|
|
151
151
|
* names so the shape (`bg-<x> text-<x>-foreground`) means the same thing
|
|
@@ -153,11 +153,11 @@
|
|
|
153
153
|
* `--<severity>-foreground` — shadcn's meaning, restored: text on a
|
|
154
154
|
* SOLID `bg-<severity>` fill. Near-white, reproducing what the
|
|
155
155
|
* wizard's literal `text-white` actually rendered (`rgb(255, 255,
|
|
156
|
-
* 255)`) — the only filled-surface severity site that
|
|
156
|
+
* 255)`) — the only filled-surface severity site that existed then
|
|
157
|
+
* (the wizard was removed in P1b+P1c; none remains).
|
|
157
158
|
* Not repeated in `.dark`: the fill itself (`--success` etc.) doesn't
|
|
158
159
|
* vary by mode either (see below), so neither does its contrasting
|
|
159
|
-
* text.
|
|
160
|
-
* the other six carry the same value for symmetry within the family,
|
|
160
|
+
* text. All seven carry the same value for symmetry within the family,
|
|
161
161
|
* unverified against a real filled site of their own — see task 4's
|
|
162
162
|
* fix-round report.
|
|
163
163
|
* `--<severity>-emphasis` — task 3's original measurement, renamed,
|
|
@@ -227,8 +227,12 @@
|
|
|
227
227
|
--neutral-foreground: hsl(0 0% 100%); /* text on a SOLID bg-neutral fill (not a palette colour) */
|
|
228
228
|
--neutral-emphasis: oklch(37% 0.013 285.805); /* zinc-700, text on a TINTED bg-neutral/15 */
|
|
229
229
|
--highlight: oklch(60.6% 0.25 292.717); /* violet-500 */
|
|
230
|
-
--highlight-foreground: hsl(
|
|
231
|
-
|
|
230
|
+
--highlight-foreground: hsl(
|
|
231
|
+
0 0% 100%
|
|
232
|
+
); /* text on a SOLID bg-highlight fill (not a palette colour) */
|
|
233
|
+
--highlight-emphasis: oklch(
|
|
234
|
+
49.1% 0.27 292.581
|
|
235
|
+
); /* violet-700, text on a TINTED bg-highlight/15 */
|
|
232
236
|
--active: oklch(62.3% 0.214 259.815); /* blue-500 */
|
|
233
237
|
--active-foreground: hsl(0 0% 100%); /* text on a SOLID bg-active fill (not a palette colour) */
|
|
234
238
|
--active-emphasis: oklch(48.8% 0.243 264.376); /* blue-700, text on a TINTED bg-active/15 */
|
|
@@ -316,8 +320,9 @@
|
|
|
316
320
|
* `--font-mono` needed no such pin: v3's and v4's default mono stacks are
|
|
317
321
|
* byte-identical (same two files), confirmed by the gate showing zero
|
|
318
322
|
* font-family drift on any `font-mono` element. */
|
|
319
|
-
--font-sans:
|
|
320
|
-
"Segoe UI
|
|
323
|
+
--font-sans:
|
|
324
|
+
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
|
325
|
+
"Noto Color Emoji";
|
|
321
326
|
|
|
322
327
|
/* v4 redefines every named font-size's paired line-height as a UNITLESS
|
|
323
328
|
* ratio (e.g. `--text-xs--line-height: calc(1 / 0.75)`); v3 used a FIXED
|
|
@@ -330,12 +335,13 @@
|
|
|
330
335
|
* line-height than v3 did. Task 7's computed-style gate caught exactly
|
|
331
336
|
* one instance of this (the command palette's `text-[10px]` kbd chips,
|
|
332
337
|
* nested in `text-xs`, went 16px -> 13.3333px) but the mechanism is
|
|
333
|
-
* general, not particular to kbd — `
|
|
334
|
-
* `
|
|
338
|
+
* general, not particular to kbd — `record/record-screen.tsx`,
|
|
339
|
+
* `record/record-screen.tsx`, `file.tsx` and others carry `text-[10px]`/
|
|
335
340
|
* `text-[11px]` on routes this gate doesn't cover, and would have drifted
|
|
336
341
|
* silently. Pinning every named size actually used in this kit
|
|
337
|
-
* (`grep -rE 'text-(xs|sm|base|lg|xl|2xl|3xl)\b' packages/ui-shadcn/src
|
|
338
|
-
* apps/playground/src`) to v3's fixed value closes
|
|
342
|
+
* (`grep -rE 'text-(xs|sm|base|lg|xl|2xl|3xl)\b' packages/ui-shadcn/src`,
|
|
343
|
+
* plus the since-deleted `apps/playground/src`) to v3's fixed value closes
|
|
344
|
+
* the whole class of bug
|
|
339
345
|
* at the token level, not just the four kbd elements that happened to be
|
|
340
346
|
* gated — those per-element `leading-[1rem]` pins have been removed as
|
|
341
347
|
* redundant. Values read verbatim from the installed
|
|
@@ -694,7 +700,7 @@
|
|
|
694
700
|
* inherits whatever cursor its ancestor set instead of being pinned —
|
|
695
701
|
* real nestings exist (clickable rows in `resource-list.tsx`,
|
|
696
702
|
* `relation-picker-dialog.tsx`, `<summary className="cursor-pointer">`
|
|
697
|
-
* in `
|
|
703
|
+
* in `record/record-screen.tsx` and `list-error-state.tsx`), and any disabled
|
|
698
704
|
* input without an explicit `disabled:cursor-not-allowed` loses v3's
|
|
699
705
|
* behaviour too. */
|
|
700
706
|
:disabled {
|
|
@@ -792,3 +798,235 @@ html {
|
|
|
792
798
|
background: transparent;
|
|
793
799
|
}
|
|
794
800
|
}
|
|
801
|
+
|
|
802
|
+
/*
|
|
803
|
+
* The record-screen styling contract (P1b+P1c section 9). Every rule keys off
|
|
804
|
+
* the `data-buro-*` attributes the renderer writes, never a utility class, so
|
|
805
|
+
* an application restyles a screen without a custom frame. Container queries
|
|
806
|
+
* only: a 380px sheet on a 1280px page lays out like a phone.
|
|
807
|
+
*/
|
|
808
|
+
@layer components {
|
|
809
|
+
:root {
|
|
810
|
+
--buro-grid-min: 16rem;
|
|
811
|
+
--buro-grid-max-columns: 3;
|
|
812
|
+
--buro-node-gap: 0.75rem;
|
|
813
|
+
--buro-node-min: 12rem;
|
|
814
|
+
--buro-size-sm: 10rem;
|
|
815
|
+
--buro-size-md: 16rem;
|
|
816
|
+
--buro-size-lg: 24rem;
|
|
817
|
+
/* The second level of rhythm: between blocks and field groups, never between two fields. */
|
|
818
|
+
--buro-section-gap: 1.5rem;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* The header too: the fields of its `header` slot lay out by its width, as the body's do. A
|
|
822
|
+
form's and a page's body likewise, or a form would take its layout from whatever encloses it
|
|
823
|
+
(beside-labels in a panel, stacked in a dialog). */
|
|
824
|
+
[data-buro-region="record-header"],
|
|
825
|
+
[data-buro-region="form-body"],
|
|
826
|
+
[data-buro-region="page-body"],
|
|
827
|
+
[data-buro-region="record-body"],
|
|
828
|
+
[data-buro-region="panel-body"],
|
|
829
|
+
[data-buro-region="tab-panel"],
|
|
830
|
+
[data-buro-region="step-panel"],
|
|
831
|
+
[data-buro-node="grid"] {
|
|
832
|
+
container-type: inline-size;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/*
|
|
836
|
+
* In a form a panel's or a step's body is a fieldset (P6-17): the user-agent border, padding,
|
|
837
|
+
* margin and min-content inline size would let a wide grid or table overflow its panel.
|
|
838
|
+
*/
|
|
839
|
+
[data-buro-region="panel-body"]:where(fieldset),
|
|
840
|
+
[data-buro-region="step-panel"]:where(fieldset) {
|
|
841
|
+
min-inline-size: 0;
|
|
842
|
+
border: 0;
|
|
843
|
+
padding: 0;
|
|
844
|
+
margin: 0;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/* An author `display` rule outranks the user-agent `[hidden]` rule. */
|
|
848
|
+
[data-buro-node][hidden],
|
|
849
|
+
[data-buro-region][hidden] {
|
|
850
|
+
/* biome-ignore lint/complexity/noImportantStyles: the spec's rule — an author display on a node must not outrank [hidden] */
|
|
851
|
+
display: none !important;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/* A pending node keeps its box; only its skeleton shows. */
|
|
855
|
+
[data-buro-state="pending"] {
|
|
856
|
+
visibility: hidden;
|
|
857
|
+
}
|
|
858
|
+
[data-buro-state="pending"] [data-buro-skeleton] {
|
|
859
|
+
visibility: visible;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
[data-buro-node="stack"],
|
|
863
|
+
[data-buro-region="panel-body"],
|
|
864
|
+
[data-buro-region="tab-panel"],
|
|
865
|
+
[data-buro-region="step-panel"] {
|
|
866
|
+
display: flex;
|
|
867
|
+
flex-direction: column;
|
|
868
|
+
gap: var(--buro-node-gap);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/*
|
|
872
|
+
* A section container's children are field groups (`data-buro-group="fields"`, drawn by the
|
|
873
|
+
* renderer around each run of consecutive fields) and blocks, so a block stands apart from the
|
|
874
|
+
* fields around it while two fields stay at `--buro-node-gap`. Authored stacks, rows and grids
|
|
875
|
+
* keep `--buro-node-gap`.
|
|
876
|
+
*/
|
|
877
|
+
[data-buro-node="stack"][data-buro-slot],
|
|
878
|
+
[data-buro-region="record-body"] > [data-buro-node="stack"],
|
|
879
|
+
[data-buro-region="form-body"] > [data-buro-node="stack"],
|
|
880
|
+
[data-buro-region="page-body"] > [data-buro-node="stack"],
|
|
881
|
+
[data-buro-region="panel-body"],
|
|
882
|
+
[data-buro-region="tab-panel"],
|
|
883
|
+
[data-buro-region="step-panel"] {
|
|
884
|
+
gap: var(--buro-section-gap);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/* The record header is set off from the body by a rule... */
|
|
888
|
+
[data-buro-region="record-header"] {
|
|
889
|
+
padding-block-end: 1rem;
|
|
890
|
+
border-block-end: 1px solid var(--border);
|
|
891
|
+
}
|
|
892
|
+
/* ...unless the body opens with tabs: the tab strip already separates, a second line would not. */
|
|
893
|
+
[data-buro-region="record-header"]:has(
|
|
894
|
+
~ [data-buro-region="record-body"]
|
|
895
|
+
> [data-buro-node="stack"]
|
|
896
|
+
> [data-buro-node="tabs"]:first-child,
|
|
897
|
+
~ *
|
|
898
|
+
[data-buro-region="record-body"]
|
|
899
|
+
> [data-buro-node="stack"]
|
|
900
|
+
> [data-buro-node="tabs"]:first-child
|
|
901
|
+
) {
|
|
902
|
+
padding-block-end: 0;
|
|
903
|
+
border-block-end: 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
[data-buro-node="grid"] {
|
|
907
|
+
display: grid;
|
|
908
|
+
gap: var(--buro-node-gap);
|
|
909
|
+
grid-template-columns: repeat(
|
|
910
|
+
auto-fill,
|
|
911
|
+
minmax(
|
|
912
|
+
max(
|
|
913
|
+
min(var(--buro-grid-min), 100%),
|
|
914
|
+
calc(100% / var(--buro-grid-max-columns) - var(--buro-node-gap))
|
|
915
|
+
),
|
|
916
|
+
1fr
|
|
917
|
+
)
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
[data-buro-node="grid"] > [data-buro-size="full"] {
|
|
921
|
+
grid-column: 1 / -1;
|
|
922
|
+
}
|
|
923
|
+
@container (min-width: 29rem) {
|
|
924
|
+
[data-buro-node="grid"] > [data-buro-size="lg"] {
|
|
925
|
+
grid-column: span 2;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
[data-buro-node="row"] {
|
|
930
|
+
display: flex;
|
|
931
|
+
flex-wrap: wrap;
|
|
932
|
+
gap: var(--buro-node-gap);
|
|
933
|
+
}
|
|
934
|
+
[data-buro-node="row"] > [data-buro-node] {
|
|
935
|
+
flex: 1 1 var(--buro-size-md);
|
|
936
|
+
min-width: min(var(--buro-node-min), 100%);
|
|
937
|
+
}
|
|
938
|
+
[data-buro-node="row"] > [data-buro-size="sm"] {
|
|
939
|
+
flex-basis: var(--buro-size-sm);
|
|
940
|
+
min-width: min(var(--buro-size-sm), 100%);
|
|
941
|
+
}
|
|
942
|
+
[data-buro-node="row"] > [data-buro-size="lg"] {
|
|
943
|
+
flex-basis: var(--buro-size-lg);
|
|
944
|
+
min-width: min(var(--buro-size-lg), 100%);
|
|
945
|
+
}
|
|
946
|
+
[data-buro-node="row"] > [data-buro-size="full"] {
|
|
947
|
+
flex-basis: 100%;
|
|
948
|
+
}
|
|
949
|
+
[data-buro-node="row"] > [data-buro-weight="2"] {
|
|
950
|
+
flex-grow: 2;
|
|
951
|
+
}
|
|
952
|
+
[data-buro-node="row"] > [data-buro-weight="3"] {
|
|
953
|
+
flex-grow: 3;
|
|
954
|
+
}
|
|
955
|
+
[data-buro-node="row"] > [data-buro-weight="4"] {
|
|
956
|
+
flex-grow: 4;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/* `buro:facts` in its grid view: as many tiles a row as fit at `--buro-size-sm` each. */
|
|
960
|
+
[data-buro-facts="grid"] {
|
|
961
|
+
display: grid;
|
|
962
|
+
gap: var(--buro-node-gap);
|
|
963
|
+
grid-template-columns: repeat(auto-fill, minmax(min(var(--buro-size-sm), 100%), 1fr));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
[data-buro-node="panel"] {
|
|
967
|
+
display: flex;
|
|
968
|
+
flex-direction: column;
|
|
969
|
+
gap: 0.75rem;
|
|
970
|
+
border: 1px solid var(--border);
|
|
971
|
+
border-radius: var(--radius);
|
|
972
|
+
padding: 1rem;
|
|
973
|
+
}
|
|
974
|
+
[data-buro-region="panel-title"] {
|
|
975
|
+
display: flex;
|
|
976
|
+
align-items: center;
|
|
977
|
+
gap: 0.25rem;
|
|
978
|
+
}
|
|
979
|
+
[data-buro-node="panel"][data-buro-tone="info"] {
|
|
980
|
+
border-color: var(--info, var(--border));
|
|
981
|
+
}
|
|
982
|
+
[data-buro-node="panel"][data-buro-tone="success"] {
|
|
983
|
+
border-color: var(--success, var(--border));
|
|
984
|
+
}
|
|
985
|
+
[data-buro-node="panel"][data-buro-tone="warning"] {
|
|
986
|
+
border-color: var(--warning, var(--border));
|
|
987
|
+
}
|
|
988
|
+
[data-buro-node="panel"][data-buro-tone="destructive"] {
|
|
989
|
+
border-color: var(--destructive);
|
|
990
|
+
}
|
|
991
|
+
/* A panel inside steps or tabs is a section of its parent, not a card of its own. */
|
|
992
|
+
[data-buro-node="steps"] > [data-buro-node="panel"] {
|
|
993
|
+
border-width: 0 0 1px;
|
|
994
|
+
border-radius: 0;
|
|
995
|
+
padding: 0 0 1rem;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/*
|
|
999
|
+
* One field geometry in show, in a grid and in a form (visual-quality spec, finding 2): a leaf
|
|
1000
|
+
* is its label, its control, then any message line (`field-message`). Narrow, the label sits
|
|
1001
|
+
* above the value; from 36rem a single-column leaf puts it in a label column no wider than
|
|
1002
|
+
* `--buro-size-sm`, and messages stay under the control.
|
|
1003
|
+
*/
|
|
1004
|
+
[data-buro-node="field"] {
|
|
1005
|
+
display: grid;
|
|
1006
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1007
|
+
gap: 0.375rem 1rem;
|
|
1008
|
+
align-items: baseline;
|
|
1009
|
+
}
|
|
1010
|
+
@container (min-width: 36rem) {
|
|
1011
|
+
[data-buro-node="field"]:not([data-buro-size="full"]) {
|
|
1012
|
+
/* The label column is never narrower than its content: a one-word label and its 24px
|
|
1013
|
+
help button did not fit in a narrow label column, and the button covered the label's
|
|
1014
|
+
last letters (REF-41). A longer label still wraps at its spaces. */
|
|
1015
|
+
grid-template-columns: minmax(min-content, var(--buro-size-sm)) minmax(0, 1fr);
|
|
1016
|
+
}
|
|
1017
|
+
[data-buro-node="field"]:not([data-buro-size="full"]) > :nth-child(n + 3) {
|
|
1018
|
+
grid-column: 2;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/* In a grid cell or a row the label is always above the value: the cell is the column. */
|
|
1022
|
+
/* (Outranks the container rules above by one attribute: the `:is()` parent.) */
|
|
1023
|
+
:is([data-buro-node="grid"], [data-buro-node="row"])
|
|
1024
|
+
> [data-buro-node="field"]:not([data-buro-size="full"]) {
|
|
1025
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1026
|
+
}
|
|
1027
|
+
:is([data-buro-node="grid"], [data-buro-node="row"])
|
|
1028
|
+
> [data-buro-node="field"]:not([data-buro-size="full"])
|
|
1029
|
+
> :nth-child(n + 3) {
|
|
1030
|
+
grid-column: auto;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
package/dist/theme/theme-css.cjs
CHANGED
|
@@ -1,105 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// src/theme/theme-css.ts
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/theme/theme-css.ts
|
|
4
3
|
function parseTopLevelBlocks(src) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
4
|
+
const n = src.length;
|
|
5
|
+
const blocks = [];
|
|
6
|
+
let i = 0;
|
|
7
|
+
const skipComment = (pos) => {
|
|
8
|
+
const end = src.indexOf("*/", pos + 2);
|
|
9
|
+
return end === -1 ? n : end + 2;
|
|
10
|
+
};
|
|
11
|
+
const skipString = (pos) => {
|
|
12
|
+
const quote = src[pos];
|
|
13
|
+
let j = pos + 1;
|
|
14
|
+
while (j < n && src[j] !== quote) {
|
|
15
|
+
if (src[j] === "\\") j++;
|
|
16
|
+
j++;
|
|
17
|
+
}
|
|
18
|
+
return j < n ? j + 1 : n;
|
|
19
|
+
};
|
|
20
|
+
while (i < n) {
|
|
21
|
+
while (i < n) {
|
|
22
|
+
const ch = src[i];
|
|
23
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (src.startsWith("/*", i)) {
|
|
28
|
+
i = skipComment(i);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (i >= n) break;
|
|
34
|
+
const statementStart = i;
|
|
35
|
+
let head = "";
|
|
36
|
+
while (i < n && src[i] !== "{" && src[i] !== ";") {
|
|
37
|
+
if (src.startsWith("/*", i)) {
|
|
38
|
+
i = skipComment(i);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (src[i] === "\"" || src[i] === "'") {
|
|
42
|
+
const strStart = i;
|
|
43
|
+
i = skipString(i);
|
|
44
|
+
head += src.slice(strStart, i);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
head += src[i];
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
if (i >= n) break;
|
|
51
|
+
if (src[i] === ";") {
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let depth = 1;
|
|
56
|
+
let j = i + 1;
|
|
57
|
+
while (j < n && depth > 0) {
|
|
58
|
+
if (src.startsWith("/*", j)) {
|
|
59
|
+
j = skipComment(j);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (src[j] === "\"" || src[j] === "'") {
|
|
63
|
+
j = skipString(j);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (src[j] === "{") depth++;
|
|
67
|
+
else if (src[j] === "}") depth--;
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
blocks.push({
|
|
71
|
+
selector: head.trim(),
|
|
72
|
+
raw: src.slice(statementStart, j)
|
|
73
|
+
});
|
|
74
|
+
i = j;
|
|
75
|
+
}
|
|
76
|
+
return blocks;
|
|
75
77
|
}
|
|
76
78
|
function stripThemeCss(raw) {
|
|
77
|
-
|
|
78
|
-
const blocks = parseTopLevelBlocks(normalized);
|
|
79
|
-
const kept = blocks.filter((b) => b.selector === ":root" || b.selector === ".dark");
|
|
80
|
-
return kept.map((b) => b.raw).join("\n\n");
|
|
79
|
+
return parseTopLevelBlocks(raw.replace(/\r\n/g, "\n")).filter((b) => b.selector === ":root" || b.selector === ".dark").map((b) => b.raw).join("\n\n");
|
|
81
80
|
}
|
|
82
81
|
function validateThemeCss(css) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
const normalized = css.replace(/\r\n/g, "\n");
|
|
83
|
+
if (normalized.trim().length === 0) return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: "Theme CSS is empty."
|
|
86
|
+
};
|
|
87
|
+
const blocks = parseTopLevelBlocks(normalized);
|
|
88
|
+
const hasDark = blocks.some((b) => b.selector === ".dark");
|
|
89
|
+
const hasRoot = blocks.some((b) => b.selector === ":root");
|
|
90
|
+
if (!hasDark) return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: "Theme has no \".dark\" block. Without it, the theme's \":root\" values override the app's \".dark\" styles at equal CSS specificity, and dark mode silently stops working."
|
|
93
|
+
};
|
|
94
|
+
if (!hasRoot) return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "Theme has no \":root\" block — light-mode values are missing."
|
|
97
|
+
};
|
|
98
|
+
return { ok: true };
|
|
100
99
|
}
|
|
101
|
-
|
|
100
|
+
//#endregion
|
|
102
101
|
exports.stripThemeCss = stripThemeCss;
|
|
103
102
|
exports.validateThemeCss = validateThemeCss;
|
|
104
|
-
|
|
103
|
+
|
|
105
104
|
//# sourceMappingURL=theme-css.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/theme-css.ts"],"names":[],"mappings":"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","file":"theme-css.cjs","sourcesContent":["/**\n * Pure functions for handling a pasted tweakcn theme.\n *\n * tweakcn's \"Copy code\" button emits six constructs. Only two of them mean\n * anything once dropped into a browser at runtime (not build time, since\n * there is no Tailwind build step for pasted CSS):\n *\n * @import \"tailwindcss\"; -> 404s if kept (no such route at runtime)\n * @custom-variant dark (...); -> meaningless outside a Tailwind build\n * :root { ... } -> survives, sets light-mode custom properties\n * .dark { ... } -> survives, sets dark-mode custom properties\n * @theme inline { ... } -> meaningless outside a Tailwind build\n * @layer base { ...; @apply ...; } -> @apply is not real CSS, dead weight\n *\n * `stripThemeCss` throws away everything except the `:root` and `.dark`\n * blocks. `validateThemeCss` catches the one failure mode that is otherwise\n * invisible: a theme with no `.dark` block. `.dark` and `:root` share equal\n * specificity, so injecting a `:root`-only theme *after* the app's own\n * stylesheet makes its `:root` win over `html.dark`'s rules too — dark mode\n * silently stops working, with no error anywhere.\n */\n\nexport type ThemeRecord = { id: string; name: string; css: string };\n\nexport type ThemeValidationResult = { ok: true } | { ok: false; reason: string };\n\ninterface TopLevelBlock {\n /** The selector/at-rule head, trimmed (e.g. \":root\", \".dark\", \"@theme inline\"). */\n selector: string;\n /** The full source slice for this block, selector through closing brace. */\n raw: string;\n}\n\n/**\n * Walks the source once, splitting it into top-level statements. Any\n * statement terminated by `;` (e.g. `@import \"tailwindcss\";`,\n * `@custom-variant dark (&:is(.dark *));`) is dropped outright — it never\n * produces a block. Any statement with a `{ ... }` body is returned as a\n * block, with brace depth tracked through the body so a nested block (e.g.\n * `@layer base { * { ... } body { ... } }`) is consumed as a single unit\n * instead of stopping at its first inner `}`.\n *\n * Comments and quoted strings are skipped transparently everywhere braces\n * are counted, so a brace inside `/* ... *\\/` or inside a string value never\n * perturbs the depth count.\n *\n * Unclosed input (an opened `{`, `/*`, or quote that never finds its\n * closer) is NOT dropped: the depth/comment/string scan simply runs off\n * the end of the string, and the block gets pushed with its `raw` spanning\n * from the opening selector all the way to EOF — swallowing whatever\n * well-formed blocks would otherwise have followed it. Concretely: an\n * unclosed `:root {` earlier in the source will absorb a perfectly valid\n * `.dark { ... }` that comes after it, so `.dark` never becomes its own\n * top-level block. This is a deliberately-left gap, not a bug: the\n * consequence is caught downstream by `validateThemeCss`'s \"no .dark\"\n * check, so truncated/corrupt input fails closed rather than corrupting\n * silently. The one sharp edge is the *reason string* it fails with — a\n * clipboard read that came back truncated will be reported as \"no .dark\n * block\", not \"your paste was cut off\", which is worth remembering when\n * debugging a fetch step that feeds this function.\n */\nfunction parseTopLevelBlocks(src: string): TopLevelBlock[] {\n const n = src.length;\n const blocks: TopLevelBlock[] = [];\n let i = 0;\n\n const skipComment = (pos: number): number => {\n const end = src.indexOf('*/', pos + 2);\n return end === -1 ? n : end + 2;\n };\n\n const skipString = (pos: number): number => {\n const quote = src[pos];\n let j = pos + 1;\n while (j < n && src[j] !== quote) {\n if (src[j] === '\\\\') j++;\n j++;\n }\n return j < n ? j + 1 : n;\n };\n\n while (i < n) {\n // Skip whitespace and comments between statements.\n while (i < n) {\n const ch = src[i];\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++;\n continue;\n }\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n break;\n }\n if (i >= n) break;\n\n const statementStart = i;\n let head = '';\n\n // Scan the head (selector / at-rule prelude) up to a top-level `{` or `;`.\n while (i < n && src[i] !== '{' && src[i] !== ';') {\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n if (src[i] === '\"' || src[i] === \"'\") {\n const strStart = i;\n i = skipString(i);\n head += src.slice(strStart, i);\n continue;\n }\n head += src[i];\n i++;\n }\n\n // Ran off the end while scanning the head itself (no `{` or `;` ever\n // showed up) — this fragment genuinely IS dropped, since there is no\n // `{` to even start a block from. Contrast with an opened-but-unclosed\n // `{ ... }` body below, which is NOT dropped — see the block comment\n // on this function.\n if (i >= n) break;\n\n if (src[i] === ';') {\n i++; // e.g. `@import \"tailwindcss\";` — drop, no block produced.\n continue;\n }\n\n // src[i] === '{' — consume the whole block, tracking nested brace depth.\n let depth = 1;\n let j = i + 1;\n while (j < n && depth > 0) {\n if (src.startsWith('/*', j)) {\n j = skipComment(j);\n continue;\n }\n if (src[j] === '\"' || src[j] === \"'\") {\n j = skipString(j);\n continue;\n }\n if (src[j] === '{') depth++;\n else if (src[j] === '}') depth--;\n j++;\n }\n\n // Pushed unconditionally — if `depth` never reached 0 the loop above\n // only stopped because `j` hit `n` (EOF). That's intentional: see the\n // \"Unclosed input\" paragraph on this function's doc comment.\n blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });\n i = j;\n }\n\n return blocks;\n}\n\n/**\n * Reduces a pasted tweakcn theme to just its `:root { ... }` and\n * `.dark { ... }` blocks, in source order. Everything else (imports,\n * `@custom-variant`, `@theme inline`, `@layer base`) is dropped. Idempotent:\n * running it again on its own output is a no-op.\n */\nexport function stripThemeCss(raw: string): string {\n const normalized = raw.replace(/\\r\\n/g, '\\n');\n const blocks = parseTopLevelBlocks(normalized);\n const kept = blocks.filter((b) => b.selector === ':root' || b.selector === '.dark');\n return kept.map((b) => b.raw).join('\\n\\n');\n}\n\n/**\n * Validates that a (normally already-stripped) theme is safe to inject.\n * The load-bearing check is `.dark`: without it, a theme's `:root` values\n * override the app's `.dark` rules at equal specificity, and dark mode\n * breaks silently. Also rejects empty input and (defensively) a theme with\n * no `:root` block at all.\n */\nexport function validateThemeCss(css: string): ThemeValidationResult {\n const normalized = css.replace(/\\r\\n/g, '\\n');\n if (normalized.trim().length === 0) {\n return { ok: false, reason: 'Theme CSS is empty.' };\n }\n\n const blocks = parseTopLevelBlocks(normalized);\n const hasDark = blocks.some((b) => b.selector === '.dark');\n const hasRoot = blocks.some((b) => b.selector === ':root');\n\n if (!hasDark) {\n return {\n ok: false,\n reason:\n 'Theme has no \".dark\" block. Without it, the theme\\'s \":root\" values ' +\n 'override the app\\'s \".dark\" styles at equal CSS specificity, and dark ' +\n 'mode silently stops working.',\n };\n }\n\n if (!hasRoot) {\n return { ok: false, reason: 'Theme has no \":root\" block — light-mode values are missing.' };\n }\n\n return { ok: true };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"theme-css.cjs","names":[],"sources":["../../src/theme/theme-css.ts"],"sourcesContent":["/**\n * Pure functions for handling a pasted tweakcn theme.\n *\n * tweakcn's \"Copy code\" button emits six constructs. Only two of them mean\n * anything once dropped into a browser at runtime (not build time, since\n * there is no Tailwind build step for pasted CSS):\n *\n * @import \"tailwindcss\"; -> 404s if kept (no such route at runtime)\n * @custom-variant dark (...); -> meaningless outside a Tailwind build\n * :root { ... } -> survives, sets light-mode custom properties\n * .dark { ... } -> survives, sets dark-mode custom properties\n * @theme inline { ... } -> meaningless outside a Tailwind build\n * @layer base { ...; @apply ...; } -> @apply is not real CSS, dead weight\n *\n * `stripThemeCss` throws away everything except the `:root` and `.dark`\n * blocks. `validateThemeCss` catches the one failure mode that is otherwise\n * invisible: a theme with no `.dark` block. `.dark` and `:root` share equal\n * specificity, so injecting a `:root`-only theme *after* the app's own\n * stylesheet makes its `:root` win over `html.dark`'s rules too — dark mode\n * silently stops working, with no error anywhere.\n */\n\nexport type ThemeRecord = { id: string; name: string; css: string };\n\nexport type ThemeValidationResult = { ok: true } | { ok: false; reason: string };\n\ninterface TopLevelBlock {\n /** The selector/at-rule head, trimmed (e.g. \":root\", \".dark\", \"@theme inline\"). */\n selector: string;\n /** The full source slice for this block, selector through closing brace. */\n raw: string;\n}\n\n/**\n * Walks the source once, splitting it into top-level statements. Any\n * statement terminated by `;` (e.g. `@import \"tailwindcss\";`,\n * `@custom-variant dark (&:is(.dark *));`) is dropped outright — it never\n * produces a block. Any statement with a `{ ... }` body is returned as a\n * block, with brace depth tracked through the body so a nested block (e.g.\n * `@layer base { * { ... } body { ... } }`) is consumed as a single unit\n * instead of stopping at its first inner `}`.\n *\n * Comments and quoted strings are skipped transparently everywhere braces\n * are counted, so a brace inside `/* ... *\\/` or inside a string value never\n * perturbs the depth count.\n *\n * Unclosed input (an opened `{`, `/*`, or quote that never finds its\n * closer) is NOT dropped: the depth/comment/string scan simply runs off\n * the end of the string, and the block gets pushed with its `raw` spanning\n * from the opening selector all the way to EOF — swallowing whatever\n * well-formed blocks would otherwise have followed it. Concretely: an\n * unclosed `:root {` earlier in the source will absorb a perfectly valid\n * `.dark { ... }` that comes after it, so `.dark` never becomes its own\n * top-level block. This is a deliberately-left gap, not a bug: the\n * consequence is caught downstream by `validateThemeCss`'s \"no .dark\"\n * check, so truncated/corrupt input fails closed rather than corrupting\n * silently. The one sharp edge is the *reason string* it fails with — a\n * clipboard read that came back truncated will be reported as \"no .dark\n * block\", not \"your paste was cut off\", which is worth remembering when\n * debugging a fetch step that feeds this function.\n */\nfunction parseTopLevelBlocks(src: string): TopLevelBlock[] {\n const n = src.length;\n const blocks: TopLevelBlock[] = [];\n let i = 0;\n\n const skipComment = (pos: number): number => {\n const end = src.indexOf('*/', pos + 2);\n return end === -1 ? n : end + 2;\n };\n\n const skipString = (pos: number): number => {\n const quote = src[pos];\n let j = pos + 1;\n while (j < n && src[j] !== quote) {\n if (src[j] === '\\\\') j++;\n j++;\n }\n return j < n ? j + 1 : n;\n };\n\n while (i < n) {\n // Skip whitespace and comments between statements.\n while (i < n) {\n const ch = src[i];\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++;\n continue;\n }\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n break;\n }\n if (i >= n) break;\n\n const statementStart = i;\n let head = '';\n\n // Scan the head (selector / at-rule prelude) up to a top-level `{` or `;`.\n while (i < n && src[i] !== '{' && src[i] !== ';') {\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n if (src[i] === '\"' || src[i] === \"'\") {\n const strStart = i;\n i = skipString(i);\n head += src.slice(strStart, i);\n continue;\n }\n head += src[i];\n i++;\n }\n\n // Ran off the end while scanning the head itself (no `{` or `;` ever\n // showed up) — this fragment genuinely IS dropped, since there is no\n // `{` to even start a block from. Contrast with an opened-but-unclosed\n // `{ ... }` body below, which is NOT dropped — see the block comment\n // on this function.\n if (i >= n) break;\n\n if (src[i] === ';') {\n i++; // e.g. `@import \"tailwindcss\";` — drop, no block produced.\n continue;\n }\n\n // src[i] === '{' — consume the whole block, tracking nested brace depth.\n let depth = 1;\n let j = i + 1;\n while (j < n && depth > 0) {\n if (src.startsWith('/*', j)) {\n j = skipComment(j);\n continue;\n }\n if (src[j] === '\"' || src[j] === \"'\") {\n j = skipString(j);\n continue;\n }\n if (src[j] === '{') depth++;\n else if (src[j] === '}') depth--;\n j++;\n }\n\n // Pushed unconditionally — if `depth` never reached 0 the loop above\n // only stopped because `j` hit `n` (EOF). That's intentional: see the\n // \"Unclosed input\" paragraph on this function's doc comment.\n blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });\n i = j;\n }\n\n return blocks;\n}\n\n/**\n * Reduces a pasted tweakcn theme to just its `:root { ... }` and\n * `.dark { ... }` blocks, in source order. Everything else (imports,\n * `@custom-variant`, `@theme inline`, `@layer base`) is dropped. Idempotent:\n * running it again on its own output is a no-op.\n */\nexport function stripThemeCss(raw: string): string {\n const normalized = raw.replace(/\\r\\n/g, '\\n');\n const blocks = parseTopLevelBlocks(normalized);\n const kept = blocks.filter((b) => b.selector === ':root' || b.selector === '.dark');\n return kept.map((b) => b.raw).join('\\n\\n');\n}\n\n/**\n * Validates that a (normally already-stripped) theme is safe to inject.\n * The load-bearing check is `.dark`: without it, a theme's `:root` values\n * override the app's `.dark` rules at equal specificity, and dark mode\n * breaks silently. Also rejects empty input and (defensively) a theme with\n * no `:root` block at all.\n */\nexport function validateThemeCss(css: string): ThemeValidationResult {\n const normalized = css.replace(/\\r\\n/g, '\\n');\n if (normalized.trim().length === 0) {\n return { ok: false, reason: 'Theme CSS is empty.' };\n }\n\n const blocks = parseTopLevelBlocks(normalized);\n const hasDark = blocks.some((b) => b.selector === '.dark');\n const hasRoot = blocks.some((b) => b.selector === ':root');\n\n if (!hasDark) {\n return {\n ok: false,\n reason:\n 'Theme has no \".dark\" block. Without it, the theme\\'s \":root\" values ' +\n 'override the app\\'s \".dark\" styles at equal CSS specificity, and dark ' +\n 'mode silently stops working.',\n };\n }\n\n if (!hasRoot) {\n return { ok: false, reason: 'Theme has no \":root\" block — light-mode values are missing.' };\n }\n\n return { ok: true };\n}\n"],"mappings":";;AA6DA,SAAS,oBAAoB,KAA8B;CACzD,MAAM,IAAI,IAAI;CACd,MAAM,SAA0B,CAAC;CACjC,IAAI,IAAI;CAER,MAAM,eAAe,QAAwB;EAC3C,MAAM,MAAM,IAAI,QAAQ,MAAM,MAAM,CAAC;EACrC,OAAO,QAAQ,KAAK,IAAI,MAAM;CAChC;CAEA,MAAM,cAAc,QAAwB;EAC1C,MAAM,QAAQ,IAAI;EAClB,IAAI,IAAI,MAAM;EACd,OAAO,IAAI,KAAK,IAAI,OAAO,OAAO;GAChC,IAAI,IAAI,OAAO,MAAM;GACrB;EACF;EACA,OAAO,IAAI,IAAI,IAAI,IAAI;CACzB;CAEA,OAAO,IAAI,GAAG;EAEZ,OAAO,IAAI,GAAG;GACZ,MAAM,KAAK,IAAI;GACf,IAAI,OAAO,OAAO,OAAO,OAAQ,OAAO,QAAQ,OAAO,MAAM;IAC3D;IACA;GACF;GACA,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA;EACF;EACA,IAAI,KAAK,GAAG;EAEZ,MAAM,iBAAiB;EACvB,IAAI,OAAO;EAGX,OAAO,IAAI,KAAK,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK;GAChD,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA,IAAI,IAAI,OAAO,QAAO,IAAI,OAAO,KAAK;IACpC,MAAM,WAAW;IACjB,IAAI,WAAW,CAAC;IAChB,QAAQ,IAAI,MAAM,UAAU,CAAC;IAC7B;GACF;GACA,QAAQ,IAAI;GACZ;EACF;EAOA,IAAI,KAAK,GAAG;EAEZ,IAAI,IAAI,OAAO,KAAK;GAClB;GACA;EACF;EAGA,IAAI,QAAQ;EACZ,IAAI,IAAI,IAAI;EACZ,OAAO,IAAI,KAAK,QAAQ,GAAG;GACzB,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA,IAAI,IAAI,OAAO,QAAO,IAAI,OAAO,KAAK;IACpC,IAAI,WAAW,CAAC;IAChB;GACF;GACA,IAAI,IAAI,OAAO,KAAK;QACf,IAAI,IAAI,OAAO,KAAK;GACzB;EACF;EAKA,OAAO,KAAK;GAAE,UAAU,KAAK,KAAK;GAAG,KAAK,IAAI,MAAM,gBAAgB,CAAC;EAAE,CAAC;EACxE,IAAI;CACN;CAEA,OAAO;AACT;AAQA,SAAgB,cAAc,KAAqB;CAIjD,OAFe,oBADI,IAAI,QAAQ,SAAS,IACI,CAC1B,CAAC,CAAC,QAAQ,MAAM,EAAE,aAAa,WAAW,EAAE,aAAa,OACjE,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,MAAM;AAC3C;AASA,SAAgB,iBAAiB,KAAoC;CACnE,MAAM,aAAa,IAAI,QAAQ,SAAS,IAAI;CAC5C,IAAI,WAAW,KAAK,CAAC,CAAC,WAAW,GAC/B,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAsB;CAGpD,MAAM,SAAS,oBAAoB,UAAU;CAC7C,MAAM,UAAU,OAAO,MAAM,MAAM,EAAE,aAAa,OAAO;CACzD,MAAM,UAAU,OAAO,MAAM,MAAM,EAAE,aAAa,OAAO;CAEzD,IAAI,CAAC,SACH,OAAO;EACL,IAAI;EACJ,QACE;CAGJ;CAGF,IAAI,CAAC,SACH,OAAO;EAAE,IAAI;EAAO,QAAQ;CAA8D;CAG5F,OAAO,EAAE,IAAI,KAAK;AACpB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/theme/theme-css.d.ts
|
|
1
2
|
/**
|
|
2
3
|
* Pure functions for handling a pasted tweakcn theme.
|
|
3
4
|
*
|
|
@@ -20,15 +21,15 @@
|
|
|
20
21
|
* silently stops working, with no error anywhere.
|
|
21
22
|
*/
|
|
22
23
|
type ThemeRecord = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
css: string;
|
|
26
27
|
};
|
|
27
28
|
type ThemeValidationResult = {
|
|
28
|
-
|
|
29
|
+
ok: true;
|
|
29
30
|
} | {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Reduces a pasted tweakcn theme to just its `:root { ... }` and
|
|
@@ -45,5 +46,6 @@ declare function stripThemeCss(raw: string): string;
|
|
|
45
46
|
* no `:root` block at all.
|
|
46
47
|
*/
|
|
47
48
|
declare function validateThemeCss(css: string): ThemeValidationResult;
|
|
48
|
-
|
|
49
|
-
export {
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ThemeRecord, ThemeValidationResult, stripThemeCss, validateThemeCss };
|
|
51
|
+
//# sourceMappingURL=theme-css.d.cts.map
|