cursedbelt 5.55.2 → 5.58.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 (174) hide show
  1. package/dist/react/charts/tokens.js +4 -4
  2. package/dist/react/code-editor/CodeEditor.d.ts.map +1 -1
  3. package/dist/react/code-editor/CodeEditor.js +2 -1
  4. package/dist/react/code-editor/CodeEditor.js.map +1 -1
  5. package/dist/react/code-editor/lineAbove.d.ts +17 -0
  6. package/dist/react/code-editor/lineAbove.d.ts.map +1 -0
  7. package/dist/react/code-editor/lineAbove.js +34 -0
  8. package/dist/react/code-editor/lineAbove.js.map +1 -0
  9. package/dist/react/companion-link/CompanionLinkStatus.d.ts.map +1 -1
  10. package/dist/react/companion-link/CompanionLinkStatus.js +2 -1
  11. package/dist/react/companion-link/CompanionLinkStatus.js.map +1 -1
  12. package/dist/react/components/Accordion.d.ts.map +1 -1
  13. package/dist/react/components/Accordion.js +2 -2
  14. package/dist/react/components/Accordion.js.map +1 -1
  15. package/dist/react/components/Badge.d.ts +20 -1
  16. package/dist/react/components/Badge.d.ts.map +1 -1
  17. package/dist/react/components/Badge.js +30 -3
  18. package/dist/react/components/Badge.js.map +1 -1
  19. package/dist/react/components/Button.d.ts.map +1 -1
  20. package/dist/react/components/Button.js +8 -2
  21. package/dist/react/components/Button.js.map +1 -1
  22. package/dist/react/components/Collapsible.d.ts.map +1 -1
  23. package/dist/react/components/Collapsible.js +2 -2
  24. package/dist/react/components/Collapsible.js.map +1 -1
  25. package/dist/react/components/DisclosureButton.d.ts +2 -2
  26. package/dist/react/components/DisclosureButton.d.ts.map +1 -1
  27. package/dist/react/components/DisclosureButton.js +9 -6
  28. package/dist/react/components/DisclosureButton.js.map +1 -1
  29. package/dist/react/components/EditorPage.d.ts +78 -0
  30. package/dist/react/components/EditorPage.d.ts.map +1 -0
  31. package/dist/react/components/EditorPage.js +133 -0
  32. package/dist/react/components/EditorPage.js.map +1 -0
  33. package/dist/react/components/IconPicker.d.ts +70 -0
  34. package/dist/react/components/IconPicker.d.ts.map +1 -0
  35. package/dist/react/components/IconPicker.js +175 -0
  36. package/dist/react/components/IconPicker.js.map +1 -0
  37. package/dist/react/components/PropertyList.d.ts +45 -0
  38. package/dist/react/components/PropertyList.d.ts.map +1 -0
  39. package/dist/react/components/PropertyList.js +12 -0
  40. package/dist/react/components/PropertyList.js.map +1 -0
  41. package/dist/react/components/Wizard/ReviewStep.d.ts +1 -1
  42. package/dist/react/components/Wizard/ReviewStep.d.ts.map +1 -1
  43. package/dist/react/components/Wizard/ReviewStep.js +3 -3
  44. package/dist/react/components/Wizard/ReviewStep.js.map +1 -1
  45. package/dist/react/components/editorPage.css +421 -0
  46. package/dist/react/components/formKit.d.ts +3 -1
  47. package/dist/react/components/formKit.d.ts.map +1 -1
  48. package/dist/react/components/formKit.js +3 -1
  49. package/dist/react/components/formKit.js.map +1 -1
  50. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  51. package/dist/react/file-tree/FileTree.js +7 -0
  52. package/dist/react/file-tree/FileTree.js.map +1 -1
  53. package/dist/react/file-tree/fileTree.css +14 -0
  54. package/dist/react/filter-rail/FilterRail.js +1 -1
  55. package/dist/react/filter-rail/FilterRail.js.map +1 -1
  56. package/dist/react/filter-rail/RailSection.js +2 -2
  57. package/dist/react/filter-rail/RailSection.js.map +1 -1
  58. package/dist/react/filter-rail/headerContextMenu.d.ts.map +1 -1
  59. package/dist/react/filter-rail/headerContextMenu.js +20 -1
  60. package/dist/react/filter-rail/headerContextMenu.js.map +1 -1
  61. package/dist/react/hooks/longPressContextMenu.d.ts +35 -0
  62. package/dist/react/hooks/longPressContextMenu.d.ts.map +1 -0
  63. package/dist/react/hooks/longPressContextMenu.js +136 -0
  64. package/dist/react/hooks/longPressContextMenu.js.map +1 -0
  65. package/dist/react/hooks/useLongPressContextMenu.d.ts +4 -0
  66. package/dist/react/hooks/useLongPressContextMenu.d.ts.map +1 -0
  67. package/dist/react/hooks/useLongPressContextMenu.js +12 -0
  68. package/dist/react/hooks/useLongPressContextMenu.js.map +1 -0
  69. package/dist/react/index.d.ts +4 -1
  70. package/dist/react/index.d.ts.map +1 -1
  71. package/dist/react/index.js +3 -0
  72. package/dist/react/index.js.map +1 -1
  73. package/dist/react/line-keys/index.d.ts +2 -0
  74. package/dist/react/line-keys/index.d.ts.map +1 -0
  75. package/dist/react/line-keys/index.js +2 -0
  76. package/dist/react/line-keys/index.js.map +1 -0
  77. package/dist/react/line-keys/lineKeys.d.ts +62 -0
  78. package/dist/react/line-keys/lineKeys.d.ts.map +1 -0
  79. package/dist/react/line-keys/lineKeys.js +159 -0
  80. package/dist/react/line-keys/lineKeys.js.map +1 -0
  81. package/dist/react/master-detail/DetailPane.d.ts +3 -0
  82. package/dist/react/master-detail/DetailPane.d.ts.map +1 -1
  83. package/dist/react/master-detail/DetailPane.js +5 -2
  84. package/dist/react/master-detail/DetailPane.js.map +1 -1
  85. package/dist/react/nav/SideNav.js +1 -1
  86. package/dist/react/nav/SideNav.js.map +1 -1
  87. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  88. package/dist/styles-areas/analytics.css +14 -14
  89. package/dist/styles-areas/charts.css +3 -3
  90. package/dist/styles-areas/companion-link.css +2 -2
  91. package/dist/styles-areas/core.css +50 -45
  92. package/dist/styles-areas/data-table.css +15 -16
  93. package/dist/styles-areas/disk-usage.css +4 -4
  94. package/dist/styles-areas/fields.css +11 -11
  95. package/dist/styles-areas/folder-tree.css +3 -3
  96. package/dist/styles-areas/layout-engine.css +2 -2
  97. package/dist/styles-areas/master-detail.css +4 -5
  98. package/dist/styles-areas/media-gallery.css +9 -9
  99. package/dist/styles-areas/media.css +3 -3
  100. package/dist/styles-areas/nav.css +5 -5
  101. package/dist/styles-areas/notifications.css +3 -3
  102. package/dist/styles-areas/problem-detail.css +3 -3
  103. package/dist/styles-areas/sharing.css +2 -3
  104. package/dist/styles-areas/stats.css +3 -3
  105. package/dist/styles-areas/wizard.css +7 -8
  106. package/dist/styles-areas/workbench.css +4 -4
  107. package/dist/styles-utilities.css +49 -10
  108. package/dist/theme.css +38 -22
  109. package/package.json +14 -2
  110. package/scripts/guardrails-baseline.json +2 -1
  111. package/scripts/guardrailsEnforce.spec.ts +32 -0
  112. package/scripts/guardrailsEnforce.ts +67 -1
  113. package/scripts/styleAreas.ts +3 -0
  114. package/src/react/charts/tokens.ts +4 -4
  115. package/src/react/code-editor/CodeEditor.tsx +2 -1
  116. package/src/react/code-editor/lineAbove.spec.ts +36 -0
  117. package/src/react/code-editor/lineAbove.ts +35 -0
  118. package/src/react/companion-link/CompanionLinkStatus.tsx +10 -12
  119. package/src/react/components/Accordion.tsx +5 -4
  120. package/src/react/components/Badge.capsule.spec.ts +1 -1
  121. package/src/react/components/Badge.spec.tsx +22 -1
  122. package/src/react/components/Badge.tsx +66 -11
  123. package/src/react/components/Button.spec.tsx +5 -4
  124. package/src/react/components/Button.tsx +8 -2
  125. package/src/react/components/Collapsible.tsx +5 -4
  126. package/src/react/components/DisclosureButton.spec.tsx +47 -0
  127. package/src/react/components/DisclosureButton.tsx +11 -7
  128. package/src/react/components/EditorPage.spec.tsx +106 -0
  129. package/src/react/components/EditorPage.tsx +281 -0
  130. package/src/react/components/IconPicker.spec.tsx +84 -0
  131. package/src/react/components/IconPicker.tsx +289 -0
  132. package/src/react/components/PropertyList.spec.tsx +110 -0
  133. package/src/react/components/PropertyList.tsx +92 -0
  134. package/src/react/components/Wizard/ReviewStep.tsx +7 -7
  135. package/src/react/components/editorPage.css +421 -0
  136. package/src/react/components/formKit.ts +10 -1
  137. package/src/react/components/formKitStylesheets.spec.ts +47 -0
  138. package/src/react/file-tree/FileTree.spec.tsx +19 -0
  139. package/src/react/file-tree/FileTree.tsx +8 -0
  140. package/src/react/file-tree/fileTree.css +14 -0
  141. package/src/react/filter-rail/FilterRail.tsx +1 -1
  142. package/src/react/filter-rail/RailSection.tsx +2 -2
  143. package/src/react/filter-rail/headerContextMenu.spec.tsx +52 -0
  144. package/src/react/filter-rail/headerContextMenu.ts +19 -1
  145. package/src/react/hooks/longPressContextMenu.spec.ts +102 -0
  146. package/src/react/hooks/longPressContextMenu.ts +135 -0
  147. package/src/react/hooks/useLongPressContextMenu.ts +15 -0
  148. package/src/react/index.ts +11 -1
  149. package/src/react/line-keys/index.ts +1 -0
  150. package/src/react/line-keys/lineKeys.spec.tsx +176 -0
  151. package/src/react/line-keys/lineKeys.ts +176 -0
  152. package/src/react/master-detail/DetailPane.tsx +14 -18
  153. package/src/react/nav/SideNav.tsx +1 -1
  154. package/src/styles-areas/analytics.css +14 -14
  155. package/src/styles-areas/charts.css +3 -3
  156. package/src/styles-areas/companion-link.css +2 -2
  157. package/src/styles-areas/core.css +50 -45
  158. package/src/styles-areas/data-table.css +15 -16
  159. package/src/styles-areas/disk-usage.css +4 -4
  160. package/src/styles-areas/fields.css +11 -11
  161. package/src/styles-areas/folder-tree.css +3 -3
  162. package/src/styles-areas/layout-engine.css +2 -2
  163. package/src/styles-areas/master-detail.css +4 -5
  164. package/src/styles-areas/media-gallery.css +9 -9
  165. package/src/styles-areas/media.css +3 -3
  166. package/src/styles-areas/nav.css +5 -5
  167. package/src/styles-areas/notifications.css +3 -3
  168. package/src/styles-areas/problem-detail.css +3 -3
  169. package/src/styles-areas/sharing.css +2 -3
  170. package/src/styles-areas/stats.css +3 -3
  171. package/src/styles-areas/wizard.css +7 -8
  172. package/src/styles-areas/workbench.css +4 -4
  173. package/src/styles-utilities.css +49 -10
  174. package/src/theme.css +38 -22
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 31 candidate(s).
21
+ * 30 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -29,5 +29,4 @@
29
29
  @source inline("data-[state=open]:slide-in-from-bottom data-[state=open]:slide-in-from-left");
30
30
  @source inline("data-[state=open]:slide-in-from-right disabled:opacity-100 hover:opacity-90 left-0 left-3");
31
31
  @source inline("max-h-56 max-h-[var(--sheet-h-lg)] max-h-[var(--sheet-h-md)] max-h-[var(--sheet-h-sm)] max-w-sm");
32
- @source inline("min-w-48 ml-2 mt-4 pl-9 right-3 rounded-l-xl rounded-r-xl rounded-t-xl size-9 space-y-4");
33
- @source inline("text-[var(--muted-foreground)] w-32");
32
+ @source inline("min-w-48 ml-2 mt-4 pl-9 right-3 rounded-l-xl rounded-r-xl rounded-t-xl size-9 space-y-4 w-32");
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 60 candidate(s).
21
+ * 59 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -26,8 +26,8 @@
26
26
  */
27
27
  @source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3.5 basis-40 bg-accent/15 bg-accent/40");
28
28
  @source inline("bg-background/70 border-l-2 border-l-border-strong border-l-destructive border-l-warning");
29
- @source inline("bottom-3 col-span-2 col-start-2 collapse cursor-help divide-x first:pl-1 gap-x-2 gap-x-4");
30
- @source inline("gap-y-0.5 grid-cols-[minmax(0,1fr)_auto] group-focus-visible/meter:opacity-100");
29
+ @source inline("bottom-3 col-span-2 col-start-2 collapse cursor-help divide-x first:pl-1 gap-x-2 gap-y-0.5");
30
+ @source inline("grid-cols-[minmax(0,1fr)_auto] group-focus-visible/meter:opacity-100");
31
31
  @source inline("group-focus-visible/stat:opacity-100 group-focus-visible/tile:opacity-100");
32
32
  @source inline("group-hover/meter:opacity-100 group-hover/stat:opacity-100 group-hover/tile:opacity-100");
33
33
  @source inline("group/meter group/stat group/tile hover:bg-muted/50 last:pr-1 left-3");
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 62 candidate(s).
21
+ * 58 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -32,10 +32,9 @@
32
32
  @source inline("data-[state=open]:slide-in-from-bottom data-[state=open]:slide-in-from-left");
33
33
  @source inline("data-[state=open]:slide-in-from-right disabled:bg-transparent disabled:opacity-100 duration-100");
34
34
  @source inline("focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
35
- @source inline("focus-within:ring-field-ring/30 gap-x-4 grid-cols-[auto_1fr] h-105");
36
- @source inline("has-[[aria-invalid=true]]:border-field-error hover:bg-border hover:border-field-ring");
37
- @source inline("hover:text-warning left-0 max-h-64 max-h-[var(--sheet-h-lg)] max-h-[var(--sheet-h-md)]");
38
- @source inline("max-h-[var(--sheet-h-sm)] max-w-sm min-h-20 min-w-24 min-w-[var(--anchor-width)] mr-1.5 mr-2");
39
- @source inline("pl-1.5 pl-2 pr-1 pr-16 pr-3 resize-none resize-y ring-field-error/30 rounded-l-xl rounded-r-xl");
40
- @source inline("rounded-t-xl self-stretch size-2 size-7 sm:inline text-field-placeholder text-xl");
41
- @source inline("transition-[transform,opacity] w-36 w-88");
35
+ @source inline("focus-within:ring-field-ring/30 h-105 has-[[aria-invalid=true]]:border-field-error");
36
+ @source inline("hover:border-field-ring hover:text-warning left-0 max-h-64 max-h-[var(--sheet-h-lg)]");
37
+ @source inline("max-h-[var(--sheet-h-md)] max-h-[var(--sheet-h-sm)] max-w-sm min-h-20 min-w-24");
38
+ @source inline("min-w-[var(--anchor-width)] mr-1.5 mr-2 pl-1.5 pl-2 pr-1 pr-16 pr-3 resize-none resize-y");
39
+ @source inline("ring-field-error/30 rounded-l-xl rounded-r-xl rounded-t-xl self-stretch size-7 sm:inline");
40
+ @source inline("text-field-placeholder text-xl transition-[transform,opacity] w-36 w-88");
@@ -18,7 +18,7 @@
18
18
  * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
19
  * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
20
  * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
- * 109 candidate(s).
21
+ * 108 candidate(s).
22
22
  *
23
23
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
24
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -39,8 +39,8 @@
39
39
  @source inline("peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-field-ring/40 peer-focus:rounded-lg");
40
40
  @source inline("pl-1 pl-3 pointer-coarse:disabled:opacity-50 pointer-coarse:h-12 pointer-coarse:w-12 pr-16 pr-9");
41
41
  @source inline("resize-none right-4 ring-1 ring-4 ring-accent/20 ring-accent/30 ring-accent/40 ring-border");
42
- @source inline("ring-inset ring-ring rotate-90 rounded-[var(--radius-device)] rounded-l-xl rounded-r-xl");
43
- @source inline("rounded-t-xl scale-[1.01] self-stretch shrink-[2] size-7 sm:flex-1 sm:flex-nowrap sm:shrink-0");
44
- @source inline("sm:w-56 text-muted-foreground/60 text-tone-amber-ink text-tone-cyan-ink text-tone-emerald-ink");
42
+ @source inline("ring-inset ring-ring rounded-[var(--radius-device)] rounded-l-xl rounded-r-xl rounded-t-xl");
43
+ @source inline("scale-[1.01] self-stretch shrink-[2] size-7 sm:flex-1 sm:flex-nowrap sm:shrink-0 sm:w-56");
44
+ @source inline("text-muted-foreground/60 text-tone-amber-ink text-tone-cyan-ink text-tone-emerald-ink");
45
45
  @source inline("text-tone-orange-ink text-tone-purple-ink text-tone-rose-ink text-tone-sky-ink");
46
46
  @source inline("text-tone-slate-ink text-xl transition-[opacity,transform] translate-y-0 translate-y-2 w-0 w-88");
@@ -1325,14 +1325,11 @@
1325
1325
  .grid-cols-12 {
1326
1326
  grid-template-columns: repeat(12, minmax(0, 1fr));
1327
1327
  }
1328
- .grid-cols-\[auto_1fr\] {
1329
- grid-template-columns: auto 1fr;
1330
- }
1331
1328
  .grid-cols-\[minmax\(0\,1fr\)_auto\] {
1332
1329
  grid-template-columns: minmax(0,1fr) auto;
1333
1330
  }
1334
- .grid-cols-\[minmax\(5rem\,9rem\)_1fr\] {
1335
- grid-template-columns: minmax(5rem,9rem) 1fr;
1331
+ .grid-cols-\[minmax\(0\,var\(--property-label-w\)\)_minmax\(0\,1fr\)\] {
1332
+ grid-template-columns: minmax(0,var(--property-label-w)) minmax(0,1fr);
1336
1333
  }
1337
1334
  .grid-cols-\[repeat\(7\,minmax\(0\,1fr\)\)\] {
1338
1335
  grid-template-columns: repeat(7,minmax(0,1fr));
@@ -1469,15 +1466,15 @@
1469
1466
  margin-inline-start: calc(calc(var(--spacing) * -2) * var(--tw-space-x-reverse));
1470
1467
  margin-inline-end: calc(calc(var(--spacing) * -2) * calc(1 - var(--tw-space-x-reverse)));
1471
1468
  }
1469
+ .gap-y-0 {
1470
+ row-gap: 0px;
1471
+ }
1472
1472
  .gap-y-0\.5 {
1473
1473
  row-gap: calc(var(--spacing) * 0.5);
1474
1474
  }
1475
1475
  .gap-y-1 {
1476
1476
  row-gap: var(--spacing);
1477
1477
  }
1478
- .gap-y-1\.5 {
1479
- row-gap: calc(var(--spacing) * 1.5);
1480
- }
1481
1478
  .gap-y-2 {
1482
1479
  row-gap: calc(var(--spacing) * 2);
1483
1480
  }
@@ -2269,6 +2266,9 @@
2269
2266
  background-color: color-mix(in oklab, var(--tone-amber-bg) 50%, transparent);
2270
2267
  }
2271
2268
  }
2269
+ .bg-tone-amber-fg {
2270
+ background-color: var(--tone-amber-fg);
2271
+ }
2272
2272
  .bg-tone-cyan-bg {
2273
2273
  background-color: var(--tone-cyan-bg);
2274
2274
  }
@@ -2278,6 +2278,9 @@
2278
2278
  background-color: color-mix(in oklab, var(--tone-cyan-bg) 50%, transparent);
2279
2279
  }
2280
2280
  }
2281
+ .bg-tone-cyan-fg {
2282
+ background-color: var(--tone-cyan-fg);
2283
+ }
2281
2284
  .bg-tone-emerald-bg {
2282
2285
  background-color: var(--tone-emerald-bg);
2283
2286
  }
@@ -2287,15 +2290,27 @@
2287
2290
  background-color: color-mix(in oklab, var(--tone-emerald-bg) 50%, transparent);
2288
2291
  }
2289
2292
  }
2293
+ .bg-tone-emerald-fg {
2294
+ background-color: var(--tone-emerald-fg);
2295
+ }
2290
2296
  .bg-tone-fuchsia-bg {
2291
2297
  background-color: var(--tone-fuchsia-bg);
2292
2298
  }
2299
+ .bg-tone-fuchsia-fg {
2300
+ background-color: var(--tone-fuchsia-fg);
2301
+ }
2293
2302
  .bg-tone-lime-bg {
2294
2303
  background-color: var(--tone-lime-bg);
2295
2304
  }
2305
+ .bg-tone-lime-fg {
2306
+ background-color: var(--tone-lime-fg);
2307
+ }
2296
2308
  .bg-tone-orange-bg {
2297
2309
  background-color: var(--tone-orange-bg);
2298
2310
  }
2311
+ .bg-tone-orange-fg {
2312
+ background-color: var(--tone-orange-fg);
2313
+ }
2299
2314
  .bg-tone-purple-bg {
2300
2315
  background-color: var(--tone-purple-bg);
2301
2316
  }
@@ -2305,6 +2320,9 @@
2305
2320
  background-color: color-mix(in oklab, var(--tone-purple-bg) 50%, transparent);
2306
2321
  }
2307
2322
  }
2323
+ .bg-tone-purple-fg {
2324
+ background-color: var(--tone-purple-fg);
2325
+ }
2308
2326
  .bg-tone-rose-bg {
2309
2327
  background-color: var(--tone-rose-bg);
2310
2328
  }
@@ -2314,6 +2332,9 @@
2314
2332
  background-color: color-mix(in oklab, var(--tone-rose-bg) 50%, transparent);
2315
2333
  }
2316
2334
  }
2335
+ .bg-tone-rose-fg {
2336
+ background-color: var(--tone-rose-fg);
2337
+ }
2317
2338
  .bg-tone-sky-bg {
2318
2339
  background-color: var(--tone-sky-bg);
2319
2340
  }
@@ -2323,6 +2344,9 @@
2323
2344
  background-color: color-mix(in oklab, var(--tone-sky-bg) 50%, transparent);
2324
2345
  }
2325
2346
  }
2347
+ .bg-tone-sky-fg {
2348
+ background-color: var(--tone-sky-fg);
2349
+ }
2326
2350
  .bg-tone-slate-bg {
2327
2351
  background-color: var(--tone-slate-bg);
2328
2352
  }
@@ -2332,12 +2356,21 @@
2332
2356
  background-color: color-mix(in oklab, var(--tone-slate-bg) 50%, transparent);
2333
2357
  }
2334
2358
  }
2359
+ .bg-tone-slate-fg {
2360
+ background-color: var(--tone-slate-fg);
2361
+ }
2335
2362
  .bg-tone-teal-bg {
2336
2363
  background-color: var(--tone-teal-bg);
2337
2364
  }
2365
+ .bg-tone-teal-fg {
2366
+ background-color: var(--tone-teal-fg);
2367
+ }
2338
2368
  .bg-tone-violet-bg {
2339
2369
  background-color: var(--tone-violet-bg);
2340
2370
  }
2371
+ .bg-tone-violet-fg {
2372
+ background-color: var(--tone-violet-fg);
2373
+ }
2341
2374
  .bg-transparent {
2342
2375
  background-color: transparent;
2343
2376
  }
@@ -3488,8 +3521,14 @@
3488
3521
  .group-active\/sep\:opacity-100:is(:where(.group\/sep):active *) {
3489
3522
  opacity: 100%;
3490
3523
  }
3491
- .group-data-\[state\=open\]\:rotate-180:is(:where(.group)[data-state="open"] *) {
3492
- rotate: 180deg;
3524
+ .group-data-\[density\=compact\]\/properties\:min-h-6:is(:where(.group\/properties)[data-density="compact"] *) {
3525
+ min-height: calc(var(--spacing) * 6);
3526
+ }
3527
+ .group-data-\[density\=compact\]\/properties\:items-start:is(:where(.group\/properties)[data-density="compact"] *) {
3528
+ align-items: flex-start;
3529
+ }
3530
+ .group-data-\[state\=open\]\:rotate-90:is(:where(.group)[data-state="open"] *) {
3531
+ rotate: 90deg;
3493
3532
  }
3494
3533
  .group-data-\[state\=open\]\/rail-section\:rotate-90:is(:where(.group\/rail-section)[data-state="open"] *) {
3495
3534
  rotate: 90deg;
package/dist/theme.css CHANGED
@@ -261,6 +261,13 @@
261
261
  --control-h-md: 36px;
262
262
  --control-h-lg: 40px;
263
263
 
264
+ /* ---- Property list label column ----------------------------------------- */
265
+ /* The ONE label column a `PropertyList` shares (FRM-1, FRM-2): wide enough for
266
+ "Target date" and "The done box" at 14px, narrow enough that the value column
267
+ starts where Linear's does. An app with longer labels answers this token on
268
+ its own scope; it never sizes a row's label to its own text. */
269
+ --property-label-w: 8rem;
270
+
264
271
  /* ---- Panel/card body default padding (task cbux-41) --------------------- */
265
272
  /* The default inner padding for a card/panel BODY (Surface's `md` pad, a
266
273
  Modal's content area, a Field group). Not theme-dependent. */
@@ -509,34 +516,43 @@
509
516
  clears 7:1+ on its own tints in dark, so re-bind to that instead. */
510
517
  --accent-strong: var(--accent);
511
518
 
512
- --background: #030712; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
513
- --foreground: #f9fafb; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
514
- --surface: #0b1220; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
515
- --card: #0b1220; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
516
- --card-foreground: #f9fafb; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
517
- /* Elevation reads through the bg here: flat (--surface #0b1220) < raised
519
+ /* 🔴 NEUTRAL since 5.58.0 — Linear's ramp, not a navy one. The owner put desk under Linear and
520
+ asked a judge which had the better UI (2026-10-05); the verdict praised Linear's "very
521
+ disciplined dark palette with minimal, purpose-driven pops of color". The grounds here were
522
+ slate-blue (#030712 / #0b1220 / #1f2937, hue 220 at 30–50% saturation), so every surface was
523
+ already a colour before a single status hue was drawn on it, and an accent or a warning had
524
+ to shout to be seen. These are the same lightness steps at under 8% saturation: the chrome
525
+ recedes and the hues that mean something are the only colour on the page (FND-10). Every
526
+ ratio the old values were tuned to is re-measured beside its token below. */
527
+ --background: #08090a; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
528
+ --foreground: #f7f8f8; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
529
+ --surface: #101113; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
530
+ --card: #101113; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
531
+ --card-foreground: #f7f8f8; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
532
+ /* Elevation reads through the bg here: flat (--surface #101113) < raised
518
533
  (--card-raised) < overlay (--card-overlay), each a step lighter than the last
519
- above the near-black --background (#030712). Shadows are near-invisible on
534
+ above the near-black --background (#08090a). Shadows are near-invisible on
520
535
  black, so the lighter bg + stronger border carry the depth. */
521
- --card-raised: #0f1929; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
522
- --card-overlay: #16213a; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
523
- --popover: #111827; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
524
- --popover-foreground: #f9fafb; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
525
- --muted: #1f2937; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
526
- /* 7.37:1 on --card (#0b1220) — the audited dark secondary-text token; the field
536
+ --card-raised: #16171a; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
537
+ --card-overlay: #1d1e22; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
538
+ --popover: #17181b; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
539
+ --popover-foreground: #f7f8f8; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
540
+ --muted: #202126; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
541
+ /* 7.06:1 on --card (#101113), 6.0:1 on --muted — the audited dark secondary-text token; the field
527
542
  placeholder re-binds to this (no lighter dark step exists worth the risk). */
528
- --muted-foreground: #9ca3af; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
543
+ --muted-foreground: #9b9ea7; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
529
544
  --field-placeholder: var(--muted-foreground);
530
545
  /* Hairlines: the old #1f2937 vanished against --card (1.28:1) — dark-mode rows
531
- read as one unbroken wall. Lifted a step; still quiet. */
532
- --border: #2c3850; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
533
- /* Interactive edges: 3.22:1 vs --card — clears the WCAG 3:1 non-text floor. */
534
- --border-strong: #5b6678; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
546
+ read as one unbroken wall. Lifted a step; still quiet.
547
+ 1.5:1 on --card since the neutral ramp (5.58.0). */
548
+ --border: #31333a; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
549
+ /* Interactive edges: 3.28:1 vs --card — clears the WCAG 3:1 non-text floor. */
550
+ --border-strong: #62666f; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
535
551
  /* Re-derived against THIS scope's --border — see the note at :root. */
536
552
  --rule: color-mix(in srgb, var(--border) 55%, transparent);
537
- /* dark: sits between --background (#030712) and --card (#0b1220) — recessed
553
+ /* dark: sits between --background (#08090a) and --card (#101113) — recessed
538
554
  relative to the card it's usually nested inside. */
539
- --surface-sunken: #070d18; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
555
+ --surface-sunken: #0b0c0d; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
540
556
 
541
557
  --destructive: #ef4444; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
542
558
  --destructive-foreground: #ffffff; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
@@ -555,9 +571,9 @@
555
571
  /* The neon the star always had — correct against a near-black canvas at 10.76:1,
556
572
  and the half of the pair that never needed changing. See the note at :root. */
557
573
  --star: #f5c518; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
558
- /* Same rule in dark — see the note at :root. Measured 3.49:1 on --card (#0b1220)
574
+ /* Same rule in dark — see the note at :root. Measured 3.49:1 on --card (#101113)
559
575
  and 3.66:1 on the dark aurora canvas. */
560
- --star-empty: #5f6b82; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
576
+ --star-empty: #666a74; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
561
577
  /* Dark badge text: the base green/amber already clear 6:1 on the dark tints;
562
578
  red needs the lighter step (#ef4444 was 3.9:1 on its tint). */
563
579
  --success-strong: #22c55e; /* guardrails-ignore no-hardcoded-colors: this file IS the theme token source of truth — every value here is necessarily a literal color definition */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.55.2",
3
+ "version": "5.58.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -72,7 +72,7 @@
72
72
  "demo:check": "tsc --noEmit --project demo/tsconfig.json",
73
73
  "demo:paths": "bun run demo/generatePaths.ts --write",
74
74
  "demo:coverage": "bun run scripts/demoCoverage.ts",
75
- "guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,raw-button-resets-face,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface,no-inline-style,no-arbitrary-length,opt-out-count,opt-out-needs-cause --scope cursedbelt src/react src/styles.css",
75
+ "guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,raw-button-resets-face,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-hand-rolled-property-list,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password,no-raw-lazy,no-raw-popover,no-hand-rolled-scroll-memory,no-hand-rolled-surface,no-inline-style,no-arbitrary-length,opt-out-count,opt-out-needs-cause --scope cursedbelt src/react src/styles.css",
76
76
  "lint": "biome lint --error-on-warnings src e2e demo scripts",
77
77
  "styles": "bun run styles:static && bun run styles:utilities && bun run styles:areas",
78
78
  "styles:static": "bun run scripts/generateStaticStyles.ts",
@@ -312,6 +312,12 @@
312
312
  "source": "./src/react/keep-awake/index.ts",
313
313
  "import": "./dist/react/keep-awake/index.js"
314
314
  },
315
+ "./react/line-keys": {
316
+ "types": "./dist/react/line-keys/index.d.ts",
317
+ "bun": "./src/react/line-keys/index.ts",
318
+ "source": "./src/react/line-keys/index.ts",
319
+ "import": "./dist/react/line-keys/index.js"
320
+ },
315
321
  "./react/markdown-doc": {
316
322
  "types": "./dist/react/markdown/MarkdownDoc.d.ts",
317
323
  "bun": "./src/react/markdown/MarkdownDoc.tsx",
@@ -396,6 +402,12 @@
396
402
  "source": "./src/react/components/Popover.tsx",
397
403
  "import": "./dist/react/components/Popover.js"
398
404
  },
405
+ "./react/property-list": {
406
+ "types": "./dist/react/components/PropertyList.d.ts",
407
+ "bun": "./src/react/components/PropertyList.tsx",
408
+ "source": "./src/react/components/PropertyList.tsx",
409
+ "import": "./dist/react/components/PropertyList.js"
410
+ },
399
411
  "./react/prompt": {
400
412
  "types": "./dist/react/prompt.d.ts",
401
413
  "bun": "./src/react/prompt.tsx",
@@ -10,6 +10,7 @@
10
10
  "no-raw-zindex": 0,
11
11
  "no-unguarded-editing-modal": 0,
12
12
  "no-raw-data-table": 0,
13
+ "no-hand-rolled-property-list": 0,
13
14
  "no-tailwind-palette": 0,
14
15
  "no-raw-title-attr": 0,
15
16
  "no-local-reimplementation": 0,
@@ -21,7 +22,7 @@
21
22
  "no-hand-rolled-surface": 0,
22
23
  "no-inline-style": 73,
23
24
  "no-arbitrary-length": 0,
24
- "opt-out-count": 272,
25
+ "opt-out-count": 273,
25
26
  "opt-out-needs-cause": 256
26
27
  },
27
28
  "cursedshell": {
@@ -552,6 +552,15 @@ describe('no-unguarded-editing-modal', () => {
552
552
  expect(appFile('<Drawer open onClose={close}><input value={v} /></Drawer>')).toHaveLength(1);
553
553
  });
554
554
 
555
+ test('flags an EditorPage the same way — Back and Escape leave it as a dismiss leaves a Modal', () => {
556
+ expect(
557
+ appFile('<EditorPage title="New" onBack={close} preview={card}><Input value={v} /></EditorPage>'),
558
+ ).toHaveLength(1);
559
+ expect(
560
+ appFile('<EditorPage title="New" onBack={close} isDirty={dirty} preview={card}><Input value={v} /></EditorPage>'),
561
+ ).toHaveLength(0);
562
+ });
563
+
555
564
  test('accepts the same Modal once it passes isDirty', () => {
556
565
  expect(
557
566
  appFile('<Modal open onClose={close} isDirty={isDirty}><Input value={v} /></Modal>'),
@@ -1508,6 +1517,29 @@ const html = \`<input type="password">\`;`,
1508
1517
  });
1509
1518
  });
1510
1519
 
1520
+ describe('no-hand-rolled-property-list', () => {
1521
+ const rule = RULES['no-hand-rolled-property-list'];
1522
+ test('a raw <dl> in app source is a violation — the pane the owner lost to Linear on', () => {
1523
+ const found = rule.check('src/ui/Panel.tsx', `<dl className="cs-cmppanel__facts">\n <div><dt>Opens as</dt><dd>Checklist</dd></div>\n</dl>`);
1524
+ expect(found).toHaveLength(1);
1525
+ expect(found[0]?.line).toBe(1);
1526
+ });
1527
+ test('the belt\'s PropertyList is the fix, and is not flagged', () => {
1528
+ expect(rule.check('src/ui/Panel.tsx', `<PropertyList><Property label="Opens as">Checklist</Property></PropertyList>`)).toHaveLength(0);
1529
+ });
1530
+ test('an opt-out with a reason on the block above is honoured; a comment mentioning <dl> is not a violation', () => {
1531
+ expect(
1532
+ rule.check('src/ui/Doc.tsx', `{/* guardrails-ignore no-hand-rolled-property-list: this renders the\n document's own definition list, which is content, not chrome */}\n<dl>{terms}</dl>`),
1533
+ ).toHaveLength(0);
1534
+ expect(rule.check('src/ui/Doc.tsx', `// replacing the <dl> here would lose the terms`)).toHaveLength(0);
1535
+ });
1536
+ test('exempt by construction: PropertyList itself, DataTable, renderers, fixtures and stylesheets', () => {
1537
+ for (const file of ['src/react/components/PropertyList.tsx', 'src/react/components/DataTable/DataTable.tsx', 'src/react/markdown/Markdown.tsx', 'src/ui/Panel.spec.tsx', 'src/ui/panel.css']) {
1538
+ expect(rule.check(file, `<dl><dt>a</dt><dd>b</dd></dl>`)).toHaveLength(0);
1539
+ }
1540
+ });
1541
+ });
1542
+
1511
1543
  describe('no-raw-nav', () => {
1512
1544
  test('🔴 flags a hand-rolled <nav> — the header music shipped on 2026-09-25', () => {
1513
1545
  expect(
@@ -71,6 +71,11 @@
71
71
  // className="h-8">`, a Progress bar's `h-1.5` track) in some other file never
72
72
  // false-positives.
73
73
  //
74
+ // no-hand-rolled-property-list — FRM-1/FRM-2/LAY-5 (owner, 2026-10-05): no hand-rolled `<dl>` — a
75
+ // record's label/value facts are `cursedbelt/react/property-list`'s `PropertyList` +
76
+ // `Property`. On the ELEMENT, like `no-raw-data-table`. The belt's own PropertyList, the
77
+ // markdown/rich-text renderers and `*.spec|test.*` fixtures are exempt by construction.
78
+ //
74
79
  // no-raw-data-table — DESIGN.md §3 "Visual Formatting & Data Presentation" (owner, 2026-08-14): no hand-rolled `<table>` —
75
80
  // use `cursedbelt/react/data-table`'s `DataTable`, which carries sorting, a global
76
81
  // filter, column visibility, pagination and the phone card tier. The rule is on the
@@ -1187,6 +1192,64 @@ const noRawDataTable: Rule = {
1187
1192
  },
1188
1193
  };
1189
1194
 
1195
+ // ── no-hand-rolled-property-list (FRM-1 / FRM-2 / LAY-5, 2026-10-05) ─────────────
1196
+ // The owner put desk's Components page under Linear's project page and asked a judge which
1197
+ // had the better UI. desk lost, and the facts of a record were a named reason: five
1198
+ // label/value rows, each in its own filled box with a bold label ("tightly boxed container
1199
+ // shapes, stark block borders"), where Linear's Properties panel is a muted label column, a
1200
+ // value column and nothing else. His instruction that day:
1201
+ //
1202
+ // *"be relentless in forcing our apps to no longer hand roll forms and layouts. They may
1203
+ // customize them somewhat but they must build off of standardized components."*
1204
+ //
1205
+ // desk's rows were a `<dl>` with a class vocabulary of its own, and a source comment beside it
1206
+ // records that an earlier version of the same pane shipped with NO rules behind its classes at
1207
+ // all — a browser-default definition list. That is what the raw element costs: every `<dl>` is
1208
+ // a fresh decision about the label column's width, the label's weight, the row's height and
1209
+ // whether a row gets a box, and each app decides differently.
1210
+ //
1211
+ // So the rule is on the ELEMENT, for the reason `no-raw-data-table` gives: "did this
1212
+ // hand-rolled list keep one label column and no boxes?" is not decidable by a scanner. A `<dl>`
1213
+ // in app source is a violation; the fix is `PropertyList` + `Property`
1214
+ // (`cursedbelt/react/property-list`), which takes a `className` for the customising an app is
1215
+ // allowed. `<dt>`/`<dd>` are not counted separately: they cannot appear without the `<dl>`.
1216
+ //
1217
+ // Exempt by construction: the belt's own `PropertyList`, `DataTable` (its phone card tier redraws
1218
+ // a ROW as pairs, which is the table's job), the markdown and rich-text renderers (a document
1219
+ // may CONTAIN a definition list), and `*.spec|test.*` fixtures.
1220
+ const RAW_DL_RE = /<dl\b/g;
1221
+ const RAW_DL_IGNORE_RE = /guardrails-ignore\s+no-hand-rolled-property-list\s*:/;
1222
+ // DataTable's two are the table's own phone card tier and footer summary: a row redrawn as pairs.
1223
+ const RAW_DL_EXEMPT_FRAGMENTS = ["react/components/PropertyList.tsx", "react/components/DataTable/", "react/markdown/", "react/rich-text/"];
1224
+
1225
+ const noHandRolledPropertyList: Rule = {
1226
+ name: "no-hand-rolled-property-list",
1227
+ description:
1228
+ "docs/design/forms-and-controls.md FRM-1/FRM-2, page-layout.md LAY-5: no hand-rolled <dl> — a record's label/value facts are cursedbelt's PropertyList + Property (cursedbelt/react/property-list): one shared label column, muted labels, no row boxes. A raw <dl> is how every app picks its own label width, weight and row fill.",
1229
+ check(filePath, source) {
1230
+ const normalized = filePath.replace(/\\/g, "/");
1231
+ if (extname(normalized) === ".css" || isTestSource(normalized)) return [];
1232
+ if (RAW_DL_EXEMPT_FRAGMENTS.some((fragment) => normalized.includes(fragment))) return [];
1233
+ const violations: Violation[] = [];
1234
+ const scannable = blankComments(source);
1235
+ const lines = source.split("\n");
1236
+ for (const match of scannable.matchAll(RAW_DL_RE)) {
1237
+ const line = lineOf(source, match.index ?? 0);
1238
+ // The contiguous comment block above, same as no-raw-data-table: a real reason runs
1239
+ // to several lines and its marker sits on the first.
1240
+ const block: string[] = [];
1241
+ for (let i = line - 2; i >= 0 && block.length < 12; i--) {
1242
+ const raw = lines[i] ?? "";
1243
+ if (raw.trim() === "") break;
1244
+ block.push(raw);
1245
+ }
1246
+ if (RAW_DL_IGNORE_RE.test(block.join("\n"))) continue;
1247
+ violations.push({ file: filePath, line, snippet: lines[line - 1]?.trim() ?? "" });
1248
+ }
1249
+ return violations;
1250
+ },
1251
+ };
1252
+
1190
1253
  // ── no-unguarded-editing-modal ──────────────────────────────────────────────
1191
1254
  // The owner lost a half-typed password by clicking outside an editing modal,
1192
1255
  // which dismissed it and threw the form away with no confirmation. `Modal`/
@@ -1211,7 +1274,9 @@ const noRawDataTable: Rule = {
1211
1274
  //
1212
1275
  // Opt-out, same shape as the other rules — on or within 3 lines above the offense:
1213
1276
  // // guardrails-ignore no-unguarded-editing-modal: read-only preview, no form state
1214
- const MODAL_TAG_OPEN_RE = /<(Modal|Drawer)\b/g;
1277
+ // `EditorPage` is the same dismissible surface drawn as a page — Escape and Back leave it, so a
1278
+ // half-made template is lost exactly as a half-typed modal was (desk-new-template-no-modal).
1279
+ const MODAL_TAG_OPEN_RE = /<(Modal|Drawer|EditorPage)\b/g;
1215
1280
  /** Editing controls. A dialog containing one of these can hold unsaved input. */
1216
1281
  const EDITING_CONTROL_RE =
1217
1282
  /<(?:Field|Input|TextInput|PasswordInput|NumberInput|Textarea|TextArea|Select|MultiSelect|Combobox|Checkbox|Switch|Toggle|RadioGroup|Slider|DatePicker|ColorPicker|RichTextEditor|CodeEditor|TagInput|input|textarea|select)\b/;
@@ -2816,6 +2881,7 @@ export const RULES: Record<string, Rule> = {
2816
2881
  [controlHeightTokens.name]: controlHeightTokens,
2817
2882
  [noRawZindex.name]: noRawZindex,
2818
2883
  [noRawDataTable.name]: noRawDataTable,
2884
+ [noHandRolledPropertyList.name]: noHandRolledPropertyList,
2819
2885
  [noTailwindPalette.name]: noTailwindPalette,
2820
2886
  [themeColoursResolve.name]: themeColoursResolve,
2821
2887
  [noRawTitleAttr.name]: noRawTitleAttr,
@@ -132,6 +132,8 @@ export const AREAS: readonly Area[] = [
132
132
  'layout',
133
133
  // The engagement beacon draws nothing; it is here because every app beacons (5.2.0).
134
134
  'engagement',
135
+ // The line keys draw nothing either; every app installs them at boot.
136
+ 'line-keys',
135
137
  'namespace',
136
138
  'portal',
137
139
  'slots',
@@ -184,6 +186,7 @@ export const AREAS: readonly Area[] = [
184
186
  'components/PathText.tsx',
185
187
  'components/Popover.tsx',
186
188
  'components/Progress.tsx',
189
+ 'components/PropertyList.tsx',
187
190
  'components/QueryBoundary.tsx',
188
191
  'components/ScrollArea.tsx',
189
192
  'components/Section.tsx',
@@ -50,10 +50,10 @@ export const FALLBACK_TOKENS: ChartTokens = {
50
50
  series: FALLBACK_SERIES,
51
51
  accent: '#34d399',
52
52
  foreground: '#f9fafb',
53
- foregroundMuted: '#9ca3af',
54
- background: '#030712',
55
- surface: '#111827',
56
- border: '#1f2937',
53
+ foregroundMuted: '#9b9ea7',
54
+ background: '#08090a',
55
+ surface: '#17181b',
56
+ border: '#31333a',
57
57
  success: '#22c55e',
58
58
  warning: '#f59e0b',
59
59
  danger: '#ef4444',
@@ -25,6 +25,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
25
25
  import { cn } from '../lib/cn.js';
26
26
  import type { CodeLanguage } from './languages.js';
27
27
  import { loadLanguageExtension } from './languages.js';
28
+ import { lineKeyBindings } from './lineAbove.js';
28
29
  import { cbEditorTheme, cbSyntaxHighlighting } from './theme.js';
29
30
 
30
31
  export interface CodeEditorProps {
@@ -124,7 +125,7 @@ export function CodeEditor({
124
125
  const dynExtensions = useMemo<Extension[]>(() => {
125
126
  const exts: Extension[] = [
126
127
  indentUnit.of(' '.repeat(tabSize)),
127
- keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
128
+ keymap.of([...lineKeyBindings, ...defaultKeymap, ...historyKeymap, indentWithTab]),
128
129
  ];
129
130
  if (showLineNumbers) exts.push(lineNumbersExt());
130
131
  if (showFoldGutter) exts.push(foldGutterExt());
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { EditorSelection, EditorState, type Transaction } from '@codemirror/state';
3
+ import { insertBlankLineAbove, lineKeyBindings } from './lineAbove.js';
4
+
5
+ function run(doc: string, at: number): { doc: string; caret: number } {
6
+ let state = EditorState.create({ doc, selection: EditorSelection.cursor(at) });
7
+ insertBlankLineAbove({ state, dispatch: (tr: Transaction) => { state = tr.state; } });
8
+ return { doc: state.doc.toString(), caret: state.selection.main.head };
9
+ }
10
+
11
+ describe('insertBlankLineAbove — ⇧⌘↵ in CodeEditor', () => {
12
+ test('opens a line above from anywhere on the line, with its indentation', () => {
13
+ expect(run('a\n bcd', 5)).toEqual({ doc: 'a\n \n bcd', caret: 4 });
14
+ expect(run('first', 3)).toEqual({ doc: '\nfirst', caret: 0 });
15
+ });
16
+
17
+ test('a read-only editor is left alone', () => {
18
+ const state = EditorState.create({ doc: 'x', extensions: EditorState.readOnly.of(true) });
19
+ expect(insertBlankLineAbove({ state, dispatch: () => {} })).toBe(false);
20
+ });
21
+ });
22
+
23
+ describe('lineKeyBindings — ⌘↑ / ⌘↓ in CodeEditor', () => {
24
+ function press(key: string, doc: string, at: number): { doc: string; caret: number } {
25
+ let state = EditorState.create({ doc, selection: EditorSelection.cursor(at) });
26
+ const binding = lineKeyBindings.find((b) => b.key === key);
27
+ // moveLineUp/Down are StateCommands at heart; they read only `state` and `dispatch`.
28
+ binding?.run?.({ state, dispatch: (tr: Transaction) => { state = tr.state; } } as never);
29
+ return { doc: state.doc.toString(), caret: state.selection.main.head };
30
+ }
31
+
32
+ test('⌘↑ moves the line up and ⌘↓ moves it down, the caret riding with it', () => {
33
+ expect(press('Mod-ArrowUp', 'one\ntwo', 5)).toEqual({ doc: 'two\none', caret: 1 });
34
+ expect(press('Mod-ArrowDown', 'one\ntwo', 1)).toEqual({ doc: 'two\none', caret: 5 });
35
+ });
36
+ });