@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.
Files changed (179) hide show
  1. package/README.md +26 -7
  2. package/dist/bulk-summary-CwSd6Yjb.cjs +108 -0
  3. package/dist/bulk-summary-CwSd6Yjb.cjs.map +1 -0
  4. package/dist/bulk-summary-GxMDP4bw.js +108 -0
  5. package/dist/bulk-summary-GxMDP4bw.js.map +1 -0
  6. package/dist/button-BxnnE28H.cjs +55 -0
  7. package/dist/button-BxnnE28H.cjs.map +1 -0
  8. package/dist/button-MWVeZaqj.js +44 -0
  9. package/dist/button-MWVeZaqj.js.map +1 -0
  10. package/dist/candidates-gate-CX2PBu6l.js +63 -0
  11. package/dist/candidates-gate-CX2PBu6l.js.map +1 -0
  12. package/dist/candidates-gate-DiFI52r7.cjs +70 -0
  13. package/dist/candidates-gate-DiFI52r7.cjs.map +1 -0
  14. package/dist/candidates-relation-picker-CBsVcnPR.js +82 -0
  15. package/dist/candidates-relation-picker-CBsVcnPR.js.map +1 -0
  16. package/dist/candidates-relation-picker-Dd_GVLlu.cjs +84 -0
  17. package/dist/candidates-relation-picker-Dd_GVLlu.cjs.map +1 -0
  18. package/dist/check-confirm-dialog-BRByTFpg.cjs +1534 -0
  19. package/dist/check-confirm-dialog-BRByTFpg.cjs.map +1 -0
  20. package/dist/check-confirm-dialog-CkaVTPT9.js +1232 -0
  21. package/dist/check-confirm-dialog-CkaVTPT9.js.map +1 -0
  22. package/dist/dialog-B_rt9S1c.js +63 -0
  23. package/dist/dialog-B_rt9S1c.js.map +1 -0
  24. package/dist/dialog-DRNUq9EA.cjs +112 -0
  25. package/dist/dialog-DRNUq9EA.cjs.map +1 -0
  26. package/dist/filter-builder-B3mZFL-K.cjs +1761 -0
  27. package/dist/filter-builder-B3mZFL-K.cjs.map +1 -0
  28. package/dist/filter-builder-DOnaXNfP.js +1605 -0
  29. package/dist/filter-builder-DOnaXNfP.js.map +1 -0
  30. package/dist/flow-runtime-B6zOHYqI.cjs +255 -0
  31. package/dist/flow-runtime-B6zOHYqI.cjs.map +1 -0
  32. package/dist/flow-runtime-D8h99FSg.js +252 -0
  33. package/dist/flow-runtime-D8h99FSg.js.map +1 -0
  34. package/dist/flow-screen-CX05b3G9.cjs +705 -0
  35. package/dist/flow-screen-CX05b3G9.cjs.map +1 -0
  36. package/dist/flow-screen-CmbARSqg.js +701 -0
  37. package/dist/flow-screen-CmbARSqg.js.map +1 -0
  38. package/dist/index.cjs +6453 -17494
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +3754 -3621
  41. package/dist/index.d.ts +3754 -3621
  42. package/dist/index.js +6217 -17219
  43. package/dist/index.js.map +1 -1
  44. package/dist/inline-flow-host-B7U1OJUe.cjs +112 -0
  45. package/dist/inline-flow-host-B7U1OJUe.cjs.map +1 -0
  46. package/dist/inline-flow-host-C3H2UpSA.js +112 -0
  47. package/dist/inline-flow-host-C3H2UpSA.js.map +1 -0
  48. package/dist/input-BI0czGyI.cjs +23 -0
  49. package/dist/input-BI0czGyI.cjs.map +1 -0
  50. package/dist/input-CUPStzZO.js +17 -0
  51. package/dist/input-CUPStzZO.js.map +1 -0
  52. package/dist/label-66W1qbke.js +29 -0
  53. package/dist/label-66W1qbke.js.map +1 -0
  54. package/dist/label-w6JYf2cF.cjs +36 -0
  55. package/dist/label-w6JYf2cF.cjs.map +1 -0
  56. package/dist/layout/theme-storage-key.cjs +5 -10
  57. package/dist/layout/theme-storage-key.cjs.map +1 -1
  58. package/dist/layout/theme-storage-key.d.cts +2 -23
  59. package/dist/layout/theme-storage-key.d.ts +2 -23
  60. package/dist/layout/theme-storage-key.js +5 -2
  61. package/dist/layout/theme-storage-key.js.map +1 -1
  62. package/dist/lazy-relation-picker-dialog-2oxnA6IR.js +14 -0
  63. package/dist/lazy-relation-picker-dialog-2oxnA6IR.js.map +1 -0
  64. package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs +19 -0
  65. package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs.map +1 -0
  66. package/dist/markdown/index.cjs +424 -511
  67. package/dist/markdown/index.cjs.map +1 -1
  68. package/dist/markdown/index.d.cts +1072 -34
  69. package/dist/markdown/index.d.ts +1072 -34
  70. package/dist/markdown/index.js +411 -459
  71. package/dist/markdown/index.js.map +1 -1
  72. package/dist/page-flow-route-CQ7HsbBm.cjs +277 -0
  73. package/dist/page-flow-route-CQ7HsbBm.cjs.map +1 -0
  74. package/dist/page-flow-route-CyXSvXSE.js +277 -0
  75. package/dist/page-flow-route-CyXSvXSE.js.map +1 -0
  76. package/dist/peek-filter-CnqHwTMU.cjs +51 -0
  77. package/dist/peek-filter-CnqHwTMU.cjs.map +1 -0
  78. package/dist/peek-filter-ErLfO153.js +28 -0
  79. package/dist/peek-filter-ErLfO153.js.map +1 -0
  80. package/dist/place-node--GdsP688.js +7315 -0
  81. package/dist/place-node--GdsP688.js.map +1 -0
  82. package/dist/place-node-SIp4XYQH.cjs +7833 -0
  83. package/dist/place-node-SIp4XYQH.cjs.map +1 -0
  84. package/dist/related-list-cards-B6x9HQQp.js +42 -0
  85. package/dist/related-list-cards-B6x9HQQp.js.map +1 -0
  86. package/dist/related-list-cards-nxuODlIA.cjs +42 -0
  87. package/dist/related-list-cards-nxuODlIA.cjs.map +1 -0
  88. package/dist/related-list-timeline-BYJKEbmA.cjs +142 -0
  89. package/dist/related-list-timeline-BYJKEbmA.cjs.map +1 -0
  90. package/dist/related-list-timeline-DEdfd-rr.js +142 -0
  91. package/dist/related-list-timeline-DEdfd-rr.js.map +1 -0
  92. package/dist/related-rows-B4levHcR.js +186 -0
  93. package/dist/related-rows-B4levHcR.js.map +1 -0
  94. package/dist/related-rows-zh5f_4QR.cjs +209 -0
  95. package/dist/related-rows-zh5f_4QR.cjs.map +1 -0
  96. package/dist/relation-create-BOaUe12G.cjs +362 -0
  97. package/dist/relation-create-BOaUe12G.cjs.map +1 -0
  98. package/dist/relation-create-Bg-eEJLs.js +337 -0
  99. package/dist/relation-create-Bg-eEJLs.js.map +1 -0
  100. package/dist/relation-many-picker-B-NCEUgX.cjs +465 -0
  101. package/dist/relation-many-picker-B-NCEUgX.cjs.map +1 -0
  102. package/dist/relation-many-picker-gdAOD-qK.js +464 -0
  103. package/dist/relation-many-picker-gdAOD-qK.js.map +1 -0
  104. package/dist/relation-picker-BAY77ivO.js +462 -0
  105. package/dist/relation-picker-BAY77ivO.js.map +1 -0
  106. package/dist/relation-picker-Da_uhFvA.cjs +479 -0
  107. package/dist/relation-picker-Da_uhFvA.cjs.map +1 -0
  108. package/dist/relation-picker-dialog-M0pQaWlp.cjs +300 -0
  109. package/dist/relation-picker-dialog-M0pQaWlp.cjs.map +1 -0
  110. package/dist/relation-picker-dialog-hA9rYrZB.js +299 -0
  111. package/dist/relation-picker-dialog-hA9rYrZB.js.map +1 -0
  112. package/dist/resource-page-router-OSQyRA5b.cjs +1779 -0
  113. package/dist/resource-page-router-OSQyRA5b.cjs.map +1 -0
  114. package/dist/resource-page-router-T4vS5gbJ.js +1582 -0
  115. package/dist/resource-page-router-T4vS5gbJ.js.map +1 -0
  116. package/dist/rich-text-DFerywgz.js +457 -0
  117. package/dist/rich-text-DFerywgz.js.map +1 -0
  118. package/dist/rich-text-vTyVjDoa.cjs +460 -0
  119. package/dist/rich-text-vTyVjDoa.cjs.map +1 -0
  120. package/dist/search-access-B7G5oR8n.cjs +812 -0
  121. package/dist/search-access-B7G5oR8n.cjs.map +1 -0
  122. package/dist/search-access-CURsWn-U.js +669 -0
  123. package/dist/search-access-CURsWn-U.js.map +1 -0
  124. package/dist/shell-BPlk8hEa.js +205 -0
  125. package/dist/shell-BPlk8hEa.js.map +1 -0
  126. package/dist/shell-DxhlsPwm.cjs +271 -0
  127. package/dist/shell-DxhlsPwm.cjs.map +1 -0
  128. package/dist/skeleton-U2noefj5.js +13 -0
  129. package/dist/skeleton-U2noefj5.js.map +1 -0
  130. package/dist/skeleton-_1gP7wan.cjs +18 -0
  131. package/dist/skeleton-_1gP7wan.cjs.map +1 -0
  132. package/dist/styles.css +255 -17
  133. package/dist/theme/theme-css.cjs +95 -96
  134. package/dist/theme/theme-css.cjs.map +1 -1
  135. package/dist/theme/theme-css.d.cts +10 -8
  136. package/dist/theme/theme-css.d.ts +10 -8
  137. package/dist/theme/theme-css.js +94 -94
  138. package/dist/theme/theme-css.js.map +1 -1
  139. package/dist/theme/theme-storage.cjs +34 -39
  140. package/dist/theme/theme-storage.cjs.map +1 -1
  141. package/dist/theme/theme-storage.d.cts +3 -1
  142. package/dist/theme/theme-storage.d.ts +3 -1
  143. package/dist/theme/theme-storage.js +33 -37
  144. package/dist/theme/theme-storage.js.map +1 -1
  145. package/dist/theme-storage-key-pYlp4aDS.d.cts +31 -0
  146. package/dist/theme-storage-key-pYlp4aDS.d.ts +31 -0
  147. package/dist/toaster-C7CiYA4Z.cjs +248 -0
  148. package/dist/toaster-C7CiYA4Z.cjs.map +1 -0
  149. package/dist/toaster-CfCRLgR-.js +164 -0
  150. package/dist/toaster-CfCRLgR-.js.map +1 -0
  151. package/dist/use-dialog-filter-state-BBCC0OEK.cjs +92 -0
  152. package/dist/use-dialog-filter-state-BBCC0OEK.cjs.map +1 -0
  153. package/dist/use-dialog-filter-state-BDcgS5Oz.js +87 -0
  154. package/dist/use-dialog-filter-state-BDcgS5Oz.js.map +1 -0
  155. package/dist/use-theme-DFlpXils.cjs +257 -0
  156. package/dist/use-theme-DFlpXils.cjs.map +1 -0
  157. package/dist/use-theme-dnE3Od3I.js +175 -0
  158. package/dist/use-theme-dnE3Od3I.js.map +1 -0
  159. package/package.json +18 -17
  160. package/dist/chunk-ABUQPOOE.js +0 -11
  161. package/dist/chunk-ABUQPOOE.js.map +0 -1
  162. package/dist/chunk-DBGGWBFG.cjs +0 -199
  163. package/dist/chunk-DBGGWBFG.cjs.map +0 -1
  164. package/dist/chunk-FMX4GLCQ.js +0 -6
  165. package/dist/chunk-FMX4GLCQ.js.map +0 -1
  166. package/dist/chunk-UC57Z243.js +0 -143
  167. package/dist/chunk-UC57Z243.js.map +0 -1
  168. package/dist/chunk-UPGPO75B.js +0 -164
  169. package/dist/chunk-UPGPO75B.js.map +0 -1
  170. package/dist/chunk-VXJAXJ3U.cjs +0 -8
  171. package/dist/chunk-VXJAXJ3U.cjs.map +0 -1
  172. package/dist/chunk-W7AEYE7A.cjs +0 -13
  173. package/dist/chunk-W7AEYE7A.cjs.map +0 -1
  174. package/dist/chunk-YF5IW525.cjs +0 -155
  175. package/dist/chunk-YF5IW525.cjs.map +0 -1
  176. package/dist/rich-text-6V7WZ3WV.cjs +0 -518
  177. package/dist/rich-text-6V7WZ3WV.cjs.map +0 -1
  178. package/dist/rich-text-N2FZEJJV.js +0 -491
  179. 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 '@tailwindcss/typography';
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 './**/*.{js,ts,tsx}';
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, resource/resource-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
- * `resource-form.tsx`'s yellow-500 draft-banner collapses into `warning`
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
- * today: `resource-wizard.tsx`'s completed-step circle
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 exists today.
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. Only `--success-foreground` has a live consumer right now;
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(0 0% 100%); /* text on a SOLID bg-highlight fill (not a palette colour) */
231
- --highlight-emphasis: oklch(49.1% 0.27 292.581); /* violet-700, text on a TINTED bg-highlight/15 */
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: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
320
- "Segoe UI Symbol", "Noto Color Emoji";
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 — `resource-form.tsx`,
334
- * `resource-show.tsx`, `file.tsx` and others carry `text-[10px]`/
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 the whole class of bug
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 `resource-form.tsx` and `list-error-state.tsx`), and any disabled
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
+ }
@@ -1,105 +1,104 @@
1
- 'use strict';
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
- const n = src.length;
6
- const blocks = [];
7
- let i = 0;
8
- const skipComment = (pos) => {
9
- const end = src.indexOf("*/", pos + 2);
10
- return end === -1 ? n : end + 2;
11
- };
12
- const skipString = (pos) => {
13
- const quote = src[pos];
14
- let j = pos + 1;
15
- while (j < n && src[j] !== quote) {
16
- if (src[j] === "\\") j++;
17
- j++;
18
- }
19
- return j < n ? j + 1 : n;
20
- };
21
- while (i < n) {
22
- while (i < n) {
23
- const ch = src[i];
24
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
25
- i++;
26
- continue;
27
- }
28
- if (src.startsWith("/*", i)) {
29
- i = skipComment(i);
30
- continue;
31
- }
32
- break;
33
- }
34
- if (i >= n) break;
35
- const statementStart = i;
36
- let head = "";
37
- while (i < n && src[i] !== "{" && src[i] !== ";") {
38
- if (src.startsWith("/*", i)) {
39
- i = skipComment(i);
40
- continue;
41
- }
42
- if (src[i] === '"' || src[i] === "'") {
43
- const strStart = i;
44
- i = skipString(i);
45
- head += src.slice(strStart, i);
46
- continue;
47
- }
48
- head += src[i];
49
- i++;
50
- }
51
- if (i >= n) break;
52
- if (src[i] === ";") {
53
- i++;
54
- continue;
55
- }
56
- let depth = 1;
57
- let j = i + 1;
58
- while (j < n && depth > 0) {
59
- if (src.startsWith("/*", j)) {
60
- j = skipComment(j);
61
- continue;
62
- }
63
- if (src[j] === '"' || src[j] === "'") {
64
- j = skipString(j);
65
- continue;
66
- }
67
- if (src[j] === "{") depth++;
68
- else if (src[j] === "}") depth--;
69
- j++;
70
- }
71
- blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });
72
- i = j;
73
- }
74
- return blocks;
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
- const normalized = raw.replace(/\r\n/g, "\n");
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
- const normalized = css.replace(/\r\n/g, "\n");
84
- if (normalized.trim().length === 0) {
85
- return { ok: false, 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) {
91
- return {
92
- ok: false,
93
- 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.`
94
- };
95
- }
96
- if (!hasRoot) {
97
- return { ok: false, reason: 'Theme has no ":root" block \u2014 light-mode values are missing.' };
98
- }
99
- return { ok: true };
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
- //# sourceMappingURL=theme-css.cjs.map
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
- id: string;
24
- name: string;
25
- css: string;
24
+ id: string;
25
+ name: string;
26
+ css: string;
26
27
  };
27
28
  type ThemeValidationResult = {
28
- ok: true;
29
+ ok: true;
29
30
  } | {
30
- ok: false;
31
- reason: string;
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 { type ThemeRecord, type ThemeValidationResult, stripThemeCss, validateThemeCss };
49
+ //#endregion
50
+ export { ThemeRecord, ThemeValidationResult, stripThemeCss, validateThemeCss };
51
+ //# sourceMappingURL=theme-css.d.cts.map