cursedbelt 5.78.0 → 5.79.1

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 (143) hide show
  1. package/dist/react/canvas-hud/HudControls.d.ts +8 -2
  2. package/dist/react/canvas-hud/HudControls.d.ts.map +1 -1
  3. package/dist/react/canvas-hud/HudControls.js +6 -3
  4. package/dist/react/canvas-hud/HudControls.js.map +1 -1
  5. package/dist/react/components/AppShell.d.ts +14 -0
  6. package/dist/react/components/AppShell.d.ts.map +1 -1
  7. package/dist/react/components/AppShell.js +7 -31
  8. package/dist/react/components/AppShell.js.map +1 -1
  9. package/dist/react/components/ChipFilterStrip.d.ts.map +1 -1
  10. package/dist/react/components/ChipFilterStrip.js +2 -18
  11. package/dist/react/components/ChipFilterStrip.js.map +1 -1
  12. package/dist/react/components/FileDropZone.d.ts.map +1 -1
  13. package/dist/react/components/FileDropZone.js +4 -1
  14. package/dist/react/components/FileDropZone.js.map +1 -1
  15. package/dist/react/components/KanbanBoard.d.ts +55 -0
  16. package/dist/react/components/KanbanBoard.d.ts.map +1 -0
  17. package/dist/react/components/KanbanBoard.js +89 -0
  18. package/dist/react/components/KanbanBoard.js.map +1 -0
  19. package/dist/react/components/ResponsiveWorkbench.d.ts +14 -1
  20. package/dist/react/components/ResponsiveWorkbench.d.ts.map +1 -1
  21. package/dist/react/components/ResponsiveWorkbench.js +33 -4
  22. package/dist/react/components/ResponsiveWorkbench.js.map +1 -1
  23. package/dist/react/components/SelectField.d.ts +18 -48
  24. package/dist/react/components/SelectField.d.ts.map +1 -1
  25. package/dist/react/components/SelectField.js +88 -11
  26. package/dist/react/components/SelectField.js.map +1 -1
  27. package/dist/react/components/SelectFieldImpl.d.ts +7 -0
  28. package/dist/react/components/SelectFieldImpl.d.ts.map +1 -0
  29. package/dist/react/components/SelectFieldImpl.js +26 -0
  30. package/dist/react/components/SelectFieldImpl.js.map +1 -0
  31. package/dist/react/components/filterChip.d.ts +8 -0
  32. package/dist/react/components/filterChip.d.ts.map +1 -0
  33. package/dist/react/components/filterChip.js +20 -0
  34. package/dist/react/components/filterChip.js.map +1 -0
  35. package/dist/react/hooks/pointerDnd.d.ts +37 -3
  36. package/dist/react/hooks/pointerDnd.d.ts.map +1 -1
  37. package/dist/react/hooks/pointerDnd.js +101 -9
  38. package/dist/react/hooks/pointerDnd.js.map +1 -1
  39. package/dist/react/hooks/pragmaticDnd.d.ts +4 -1
  40. package/dist/react/hooks/pragmaticDnd.d.ts.map +1 -1
  41. package/dist/react/hooks/pragmaticDnd.js +9 -3
  42. package/dist/react/hooks/pragmaticDnd.js.map +1 -1
  43. package/dist/react/hooks/usePublishedBottomChrome.d.ts +12 -0
  44. package/dist/react/hooks/usePublishedBottomChrome.d.ts.map +1 -0
  45. package/dist/react/hooks/usePublishedBottomChrome.js +31 -0
  46. package/dist/react/hooks/usePublishedBottomChrome.js.map +1 -0
  47. package/dist/react/index.d.ts +3 -0
  48. package/dist/react/index.d.ts.map +1 -1
  49. package/dist/react/index.js +3 -0
  50. package/dist/react/index.js.map +1 -1
  51. package/dist/react/lib/selectField.d.ts +72 -0
  52. package/dist/react/lib/selectField.d.ts.map +1 -0
  53. package/dist/react/lib/selectField.js +23 -0
  54. package/dist/react/lib/selectField.js.map +1 -0
  55. package/dist/react/media/VideoChapterEditor.d.ts.map +1 -1
  56. package/dist/react/media/VideoChapterEditor.js +2 -1
  57. package/dist/react/media/VideoChapterEditor.js.map +1 -1
  58. package/dist/react/media/VideoCutEditor.d.ts.map +1 -1
  59. package/dist/react/media/VideoCutEditor.js +2 -1
  60. package/dist/react/media/VideoCutEditor.js.map +1 -1
  61. package/dist/react/media/denseGallery.css +2 -0
  62. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  63. package/dist/react/media-gallery/MediaGallery.js +6 -7
  64. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  65. package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -1
  66. package/dist/react/media-gallery/MediaMetaEditor.js +2 -1
  67. package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -1
  68. package/dist/react/nav/CommandBar.d.ts.map +1 -1
  69. package/dist/react/nav/CommandBar.js +2 -1
  70. package/dist/react/nav/CommandBar.js.map +1 -1
  71. package/dist/react/palette/CommandPalette.d.ts +15 -1
  72. package/dist/react/palette/CommandPalette.d.ts.map +1 -1
  73. package/dist/react/palette/CommandPalette.js +58 -8
  74. package/dist/react/palette/CommandPalette.js.map +1 -1
  75. package/dist/react/palette/Kbd.d.ts +15 -0
  76. package/dist/react/palette/Kbd.d.ts.map +1 -0
  77. package/dist/react/palette/Kbd.js +17 -0
  78. package/dist/react/palette/Kbd.js.map +1 -0
  79. package/dist/react/palette/ShortcutSheet.d.ts +30 -0
  80. package/dist/react/palette/ShortcutSheet.d.ts.map +1 -0
  81. package/dist/react/palette/ShortcutSheet.js +15 -0
  82. package/dist/react/palette/ShortcutSheet.js.map +1 -0
  83. package/dist/react/palette/commandSources.d.ts +14 -0
  84. package/dist/react/palette/commandSources.d.ts.map +1 -1
  85. package/dist/react/palette/commandSources.js +11 -1
  86. package/dist/react/palette/commandSources.js.map +1 -1
  87. package/dist/react/palette/index.d.ts +4 -2
  88. package/dist/react/palette/index.d.ts.map +1 -1
  89. package/dist/react/palette/index.js +2 -0
  90. package/dist/react/palette/index.js.map +1 -1
  91. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  92. package/dist/styles-areas/canvas-hud.css +2 -2
  93. package/dist/styles-areas/kanban.css +31 -0
  94. package/dist/styles-areas/palette.css +3 -3
  95. package/dist/styles-areas/workbench.css +15 -13
  96. package/dist/styles-utilities.css +42 -0
  97. package/package.json +4 -3
  98. package/scripts/guardrails-baseline.json +2 -1
  99. package/scripts/guardrailsEnforce.spec.ts +62 -0
  100. package/scripts/guardrailsEnforce.ts +77 -6
  101. package/scripts/styleAreas.ts +2 -0
  102. package/src/react/canvas-hud/HudControls.spec.tsx +11 -0
  103. package/src/react/canvas-hud/HudControls.tsx +33 -9
  104. package/src/react/components/AppShell.spec.tsx +40 -0
  105. package/src/react/components/AppShell.tsx +25 -27
  106. package/src/react/components/ChipFilterStrip.tsx +2 -38
  107. package/src/react/components/FileDropZone.spec.tsx +16 -0
  108. package/src/react/components/FileDropZone.tsx +3 -1
  109. package/src/react/components/KanbanBoard.spec.tsx +89 -0
  110. package/src/react/components/KanbanBoard.tsx +270 -0
  111. package/src/react/components/ResponsiveWorkbench.spec.tsx +57 -0
  112. package/src/react/components/ResponsiveWorkbench.tsx +69 -4
  113. package/src/react/components/SelectField.spec.tsx +117 -0
  114. package/src/react/components/SelectField.tsx +131 -166
  115. package/src/react/components/SelectFieldImpl.tsx +140 -0
  116. package/src/react/components/filterChip.tsx +43 -0
  117. package/src/react/hooks/pointerDnd.spec.ts +89 -3
  118. package/src/react/hooks/pointerDnd.ts +103 -8
  119. package/src/react/hooks/pragmaticDnd.ts +13 -2
  120. package/src/react/hooks/usePublishedBottomChrome.ts +30 -0
  121. package/src/react/hooks/useScrollMemory.spec.tsx +7 -5
  122. package/src/react/index.ts +3 -0
  123. package/src/react/lib/floatingStyles.spec.ts +1 -1
  124. package/src/react/lib/selectField.ts +96 -0
  125. package/src/react/media/VideoChapterEditor.tsx +3 -2
  126. package/src/react/media/VideoCutEditor.tsx +3 -2
  127. package/src/react/media/denseGallery.css +2 -0
  128. package/src/react/media-gallery/MediaGallery.spec.tsx +15 -11
  129. package/src/react/media-gallery/MediaGallery.tsx +16 -18
  130. package/src/react/media-gallery/MediaMetaEditor.tsx +2 -1
  131. package/src/react/nav/CommandBar.tsx +2 -3
  132. package/src/react/palette/CommandPalette.spec.tsx +73 -0
  133. package/src/react/palette/CommandPalette.tsx +153 -13
  134. package/src/react/palette/Kbd.tsx +46 -0
  135. package/src/react/palette/ShortcutSheet.spec.tsx +58 -0
  136. package/src/react/palette/ShortcutSheet.tsx +110 -0
  137. package/src/react/palette/commandSources.tsx +30 -1
  138. package/src/react/palette/index.ts +9 -0
  139. package/src/styles-areas/canvas-hud.css +2 -2
  140. package/src/styles-areas/kanban.css +31 -0
  141. package/src/styles-areas/palette.css +3 -3
  142. package/src/styles-areas/workbench.css +15 -13
  143. package/src/styles-utilities.css +42 -0
@@ -6,6 +6,8 @@ export * from 'cursedbelt-core/palette';
6
6
  export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from './Command.js';
7
7
  export { CommandSourceProvider, useCommandSource, useCommandSources, } from './commandSources.js';
8
8
  export { CommandPalette, } from './CommandPalette.js';
9
+ export { Kbd } from './Kbd.js';
10
+ export { ShortcutSheet, } from './ShortcutSheet.js';
9
11
  export { useCommandPalette, } from './useCommandPalette.js';
10
12
  export { useHotkeys } from './useHotkeys.js';
11
13
  export { useShortcutRegistry } from './useShortcutRegistry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/palette/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,yFAAyF;AACzF,6FAA6F;AAC7F,oFAAoF;AAEpF,cAAc,yBAAyB,CAAC;AACxC,OAAO,EACL,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,YAAY,EAEZ,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,GAEf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAA0B,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/palette/index.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,yFAAyF;AACzF,6FAA6F;AAC7F,oFAAoF;AAEpF,cAAc,yBAAyB,CAAC;AACxC,OAAO,EACL,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,YAAY,EAEZ,WAAW,EACX,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAIL,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,GAGf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,GAAG,EAAiB,MAAM,UAAU,CAAC;AAC9C,OAAO,EACL,aAAa,GAId,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGL,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAA0B,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AA6PA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AAgwCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AAm4BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AA0FD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAiCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAmGhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
1
+ {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AAiQA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AA2yCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AAs4BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AA0FD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAkCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAgHhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
@@ -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
- * 39 candidate(s).
21
+ * 40 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,4 +29,4 @@
29
29
  @source inline("[scrollbar-width:none] aria-pressed:bg-muted aria-pressed:text-foreground basis-0 basis-full");
30
30
  @source inline("bg-background/40 bg-background/70 bottom-3 bottom-4 focus-visible:border-transparent");
31
31
  @source inline("focus-visible:ring-0 left-3 max-w-48 min-w-10 min-w-26 min-w-40 ml-2 opacity-100 order-1 order-2");
32
- @source inline("order-3 pb-1.5 pl-0 pl-1 place-items-center pr-1 pt-1 px-0.5 right-4 w-40 w-64");
32
+ @source inline("order-3 pb-1.5 pl-0 pl-1 place-items-center pr-1 pt-1 px-0.5 right-3 right-4 w-40 w-64");
@@ -0,0 +1,31 @@
1
+ /*
2
+ * cursedbelt — the `kanban` area's utility candidates. GENERATED BUILD ARTIFACT. Never hand-edit.
3
+ *
4
+ * the Kanban board — columns of dragged cards; an app with no board renders none of it.
5
+ *
6
+ * Every Tailwind utility the modules in this area's import closure use, MINUS the
7
+ * ones `styles-areas/core.css` already ships. 🔴 Import ALL of these, in
8
+ * this order — this file alone is incomplete:
9
+ *
10
+ * @import "tailwindcss";
11
+ * @import "cursedbelt/styles-static.css";
12
+ * @import "cursedbelt/styles-areas/core.css";
13
+ * @import "cursedbelt/styles-areas/kanban.css";
14
+ * @import "cursedbelt/theme.css";
15
+ *
16
+ * These are CANDIDATES (`@source inline`), not CSS: the app's own Tailwind pass generates
17
+ * them, sorted together with the app's utilities, so an area can never put a base utility
18
+ * after an app's responsive variant (task 2124). An app that imports every area gets exactly
19
+ * what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
20
+ * src/stylesAreas.spec.ts, which is also what fails when this file is stale.
21
+ * 26 candidate(s).
22
+ *
23
+ * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
24
+ * `bun run build` runs. The map of what is in which area, and why the areas are
25
+ * import closures rather than a hand-written list, is scripts/styleAreas.ts.
26
+ */
27
+ @source inline("[&::-webkit-scrollbar]:hidden [scrollbar-width:none] active:cursor-grabbing bg-accent/15");
28
+ @source inline("border-accent/70 border-dashed cursor-grab focus-visible:border-accent hover:bg-muted/80");
29
+ @source inline("max-h-full max-lg:snap-mandatory max-lg:snap-x max-md:w-full max-w-64 md:hidden min-h min-h-8");
30
+ @source inline("overscroll-x-auto ring-1 ring-accent ring-accent/30 ring-accent/50 ring-inset shadow-2xl");
31
+ @source inline("snap-start w-72");
@@ -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
- * 29 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
@@ -31,5 +31,5 @@
31
31
  @source inline("border-dashed data-[disabled=true]:opacity-50 data-[disabled=true]:pointer-events-none");
32
32
  @source inline("data-[selected=true]:bg-muted data-[selected=true]:text-foreground h-12 max-h-72");
33
33
  @source inline("max-h-[var(--palette-max-h)] max-w-64 mx-1 opacity-100 place-items-center");
34
- @source inline("placeholder:text-muted-foreground py-8 shadow-popover sm:inline-block top-[var(--palette-top)]");
35
- @source inline("w-72 w-[calc(100%-2rem)]");
34
+ @source inline("placeholder:text-muted-foreground py-8 shadow-popover sm:inline-block sm:inline-flex");
35
+ @source inline("top-[var(--palette-top)] w-72 w-[calc(100%-2rem)]");
@@ -18,25 +18,27 @@
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
- * 92 candidate(s).
21
+ * 98 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
25
25
  * import closures rather than a hand-written list, is scripts/styleAreas.ts.
26
26
  */
27
27
  @source inline("-my-0.5 [&::-webkit-scrollbar]:hidden [&_svg]:size-3.5 [scrollbar-width:none] bg-accent/15");
28
- @source inline("bg-accent/40 bg-accent/8 bg-card-overlay/85 bg-muted/30 bg-muted/60 bg-success/8 border-2");
29
- @source inline("border-8 border-accent-solid border-accent/70 border-border-strong/60 border-border/50");
30
- @source inline("border-dashed border-solid bottom-6 disabled:opacity-0 duration-150 first:pt-1 flex-row");
31
- @source inline("group-hover:disabled:opacity-50 h-105 h-40 hover:bg-card-overlay hover:bg-muted-foreground/10");
32
- @source inline("hover:bg-muted/80 hover:border-field-ring hover:shadow-popover italic left-2.5 max-h-full min-h");
33
- @source inline("min-h-20 min-h-8 min-h-9 min-h-dvh min-h-full min-w-20 min-w-40 opacity-100 pb-0.5");
34
- @source inline("pb-[max(env(safe-area-inset-bottom,0px),0.5rem)] peer-focus peer-focus:outline-none");
35
- @source inline("peer-focus:ring-2 peer-focus:ring-field-ring/40 peer-focus:rounded-lg pl-1 pl-3");
36
- @source inline("pointer-coarse:disabled:opacity-50 pointer-coarse:h-12 pointer-coarse:w-12 pr-16 pr-9");
37
- @source inline("resize-none right-4 ring-1 ring-4 ring-accent/20 ring-accent/30 ring-accent/40 ring-border");
38
- @source inline("ring-inset ring-ring rounded-[var(--radius-device)] scale-[1.01] self-stretch shrink-[2] size-7");
39
- @source inline("sm:flex-1 sm:flex-nowrap sm:shrink-0 sm:w-56 text-muted-foreground/60 text-tone-amber-ink");
28
+ @source inline("bg-accent/40 bg-accent/8 bg-background/85 bg-card-overlay/85 bg-muted/30 bg-muted/60");
29
+ @source inline("bg-success/8 border-2 border-8 border-accent-solid border-accent/70 border-border-strong/60");
30
+ @source inline("border-border/50 border-dashed border-solid bottom-6");
31
+ @source inline("bottom-[max(env(safe-area-inset-bottom,0px),0.75rem)] disabled:opacity-0 duration-150 first:pt-1");
32
+ @source inline("flex-row group-hover:disabled:opacity-50 h-105 h-40 hover:bg-card-overlay");
33
+ @source inline("hover:bg-muted-foreground/10 hover:bg-muted/80 hover:border-field-ring hover:shadow-popover");
34
+ @source inline("inset-x-3 italic items-stretch left-2.5 max-h-full min-h min-h-20 min-h-8 min-h-9 min-h-dvh");
35
+ @source inline("min-h-full min-w-20 min-w-40 opacity-100 pb-0.5 pb-[max(env(safe-area-inset-bottom,0px),0.5rem)]");
36
+ @source inline("peer-focus peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-field-ring/40");
37
+ @source inline("peer-focus:rounded-lg pl-1 pl-3 pointer-coarse:disabled:opacity-50 pointer-coarse:h-12");
38
+ @source inline("pointer-coarse:w-12 pr-16 pr-9 pt-1.5 resize-none right-4 ring-1 ring-4 ring-accent/20");
39
+ @source inline("ring-accent/30 ring-accent/40 ring-border ring-inset ring-ring rounded-2xl");
40
+ @source inline("rounded-[var(--radius-device)] scale-[1.01] self-stretch shrink-[2] size-7 sm:flex-1");
41
+ @source inline("sm:flex-nowrap sm:shrink-0 sm:w-56 text-muted-foreground/60 text-tone-amber-ink");
40
42
  @source inline("text-tone-cyan-ink text-tone-emerald-ink text-tone-orange-ink text-tone-purple-ink");
41
43
  @source inline("text-tone-rose-ink text-tone-sky-ink text-tone-slate-ink text-xl transition-[opacity,transform]");
42
44
  @source inline("translate-y-0 translate-y-2 w-0 w-88");
@@ -297,6 +297,9 @@
297
297
  .bottom-6 {
298
298
  bottom: calc(var(--spacing) * 6);
299
299
  }
300
+ .bottom-\[max\(env\(safe-area-inset-bottom\,0px\)\,0\.75rem\)\] {
301
+ bottom: max(env(safe-area-inset-bottom,0px), 0.75rem);
302
+ }
300
303
  .-left-1\.5 {
301
304
  left: calc(var(--spacing) * -1.5);
302
305
  }
@@ -1323,6 +1326,9 @@
1323
1326
  .resize-y {
1324
1327
  resize: vertical;
1325
1328
  }
1329
+ .snap-start {
1330
+ scroll-snap-align: start;
1331
+ }
1326
1332
  .\[scrollbar-width\:none\] {
1327
1333
  scrollbar-width: none;
1328
1334
  }
@@ -1576,6 +1582,9 @@
1576
1582
  .overscroll-contain {
1577
1583
  overscroll-behavior: contain;
1578
1584
  }
1585
+ .overscroll-x-auto {
1586
+ overscroll-behavior-x: auto;
1587
+ }
1579
1588
  .rounded {
1580
1589
  border-radius: 0.25rem;
1581
1590
  }
@@ -2648,6 +2657,9 @@
2648
2657
  .pt-1 {
2649
2658
  padding-top: var(--spacing);
2650
2659
  }
2660
+ .pt-1\.5 {
2661
+ padding-top: calc(var(--spacing) * 1.5);
2662
+ }
2651
2663
  .pt-2 {
2652
2664
  padding-top: calc(var(--spacing) * 2);
2653
2665
  }
@@ -3201,6 +3213,10 @@
3201
3213
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
3202
3214
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
3203
3215
  }
3216
+ .shadow-2xl {
3217
+ --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
3218
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
3219
+ }
3204
3220
  .shadow-\[0_0_0_1px_rgba\(0\,0\,0\,0\.55\)\] {
3205
3221
  --tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgba(0,0,0,0.55));
3206
3222
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -3295,6 +3311,12 @@
3295
3311
  --tw-ring-color: color-mix(in oklab, var(--accent) 40%, transparent);
3296
3312
  }
3297
3313
  }
3314
+ .ring-accent\/50 {
3315
+ --tw-ring-color: var(--accent);
3316
+ @supports (color: color-mix(in lab, red, red)) {
3317
+ --tw-ring-color: color-mix(in oklab, var(--accent) 50%, transparent);
3318
+ }
3319
+ }
3298
3320
  .ring-background {
3299
3321
  --tw-ring-color: var(--background);
3300
3322
  }
@@ -3979,6 +4001,9 @@
3979
4001
  --tw-outline-style: none;
3980
4002
  outline-style: none;
3981
4003
  }
4004
+ .focus-visible\:border-accent:focus-visible {
4005
+ border-color: var(--accent);
4006
+ }
3982
4007
  .focus-visible\:border-border-strong:focus-visible {
3983
4008
  border-color: var(--border-strong);
3984
4009
  }
@@ -4482,6 +4507,14 @@
4482
4507
  scale: var(--tw-scale-x) var(--tw-scale-y);
4483
4508
  }
4484
4509
  }
4510
+ @media (width < 64rem) {
4511
+ .max-lg\:snap-x {
4512
+ scroll-snap-type: x var(--tw-scroll-snap-strictness);
4513
+ }
4514
+ .max-lg\:snap-mandatory {
4515
+ --tw-scroll-snap-strictness: mandatory;
4516
+ }
4517
+ }
4485
4518
  @media (width < 48rem) {
4486
4519
  .max-md\:absolute {
4487
4520
  position: absolute;
@@ -4495,6 +4528,9 @@
4495
4528
  .max-md\:hidden {
4496
4529
  display: none;
4497
4530
  }
4531
+ .max-md\:w-full {
4532
+ width: 100%;
4533
+ }
4498
4534
  }
4499
4535
  @media (width < 40rem) {
4500
4536
  .max-sm\:inset-x-0 {
@@ -5785,6 +5821,11 @@
5785
5821
  inherits: false;
5786
5822
  initial-value: 1;
5787
5823
  }
5824
+ @property --tw-scroll-snap-strictness {
5825
+ syntax: "*";
5826
+ inherits: false;
5827
+ initial-value: proximity;
5828
+ }
5788
5829
  @property --tw-content {
5789
5830
  syntax: "*";
5790
5831
  initial-value: "";
@@ -5915,6 +5956,7 @@
5915
5956
  --tw-scale-x: 1;
5916
5957
  --tw-scale-y: 1;
5917
5958
  --tw-scale-z: 1;
5959
+ --tw-scroll-snap-strictness: proximity;
5918
5960
  --tw-content: "";
5919
5961
  }
5920
5962
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.78.0",
3
+ "version": "5.79.1",
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.",
@@ -75,7 +75,7 @@
75
75
  "demo:check": "tsc --noEmit --project demo/tsconfig.json",
76
76
  "demo:paths": "bun run demo/generatePaths.ts --write",
77
77
  "demo:coverage": "bun run scripts/demoCoverage.ts",
78
- "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-select,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",
78
+ "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-raw-kbd,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-select,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",
79
79
  "lint": "biome lint --error-on-warnings src e2e demo scripts",
80
80
  "styles": "bun run styles:static && bun run styles:utilities && bun run styles:areas",
81
81
  "styles:static": "bun run scripts/generateStaticStyles.ts",
@@ -657,6 +657,7 @@
657
657
  "./react/file-tree/geometry.json": "./src/react/file-tree/fileTreeGeometry.fixture.json",
658
658
  "./styles-areas/filter-rail.css": "./src/styles-areas/filter-rail.css",
659
659
  "./styles-areas/folder-tree.css": "./src/styles-areas/folder-tree.css",
660
+ "./styles-areas/kanban.css": "./src/styles-areas/kanban.css",
660
661
  "./styles-areas/keep-awake.css": "./src/styles-areas/keep-awake.css",
661
662
  "./styles-areas/layout-engine.css": "./src/styles-areas/layout-engine.css",
662
663
  "./styles-areas/markdown.css": "./src/styles-areas/markdown.css",
@@ -972,7 +973,7 @@
972
973
  "class-variance-authority": "^0.7.1",
973
974
  "clsx": "^2.1.1",
974
975
  "cmdk": "^1.1.1",
975
- "cursedbelt-core": "^2.10.0",
976
+ "cursedbelt-core": "^2.13.1",
976
977
  "cwip": "^4.12.0",
977
978
  "lucide-react": "^1.21.0",
978
979
  "react-hook-form": "^7.80.0",
@@ -11,6 +11,7 @@
11
11
  "no-unguarded-editing-modal": 0,
12
12
  "no-raw-data-table": 0,
13
13
  "no-hand-rolled-property-list": 0,
14
+ "no-raw-kbd": 0,
14
15
  "no-tailwind-palette": 0,
15
16
  "no-raw-title-attr": 0,
16
17
  "no-local-reimplementation": 0,
@@ -22,7 +23,7 @@
22
23
  "no-hand-rolled-surface": 0,
23
24
  "no-inline-style": 73,
24
25
  "no-arbitrary-length": 0,
25
- "opt-out-count": 272,
26
+ "opt-out-count": 273,
26
27
  "opt-out-needs-cause": 255
27
28
  },
28
29
  "cursedshell": {
@@ -1054,6 +1054,15 @@ describe('no-native-select — the OS draws its list (owner, 2026-10-10)', () =>
1054
1054
  expect(check('no-native-select', source)).toHaveLength(2);
1055
1055
  });
1056
1056
 
1057
+ test('a raw <select> is the same OS list, and counts too (forge task 3097)', () => {
1058
+ const source = `<select className="cs-select" value={rule.field} onChange={(e) => change(e.target.value)}>
1059
+ {FIELDS.map((f) => <option key={f}>{f}</option>)}
1060
+ </select>`;
1061
+ expect(check('no-native-select', source)).toHaveLength(1);
1062
+ expect(check('no-native-select', `{/* a \`<select>\` here would be an OS list */}\n<SelectField options={o} />`)).toHaveLength(0);
1063
+ expect(check('no-native-select', `<selection-bar />`)).toHaveLength(0);
1064
+ });
1065
+
1057
1066
  test('the styled controls, and a comment that names it, are not one', () => {
1058
1067
  expect(check('no-native-select', `<SelectField value={sort} options={SORTS} />`)).toHaveLength(0);
1059
1068
  expect(check('no-native-select', `// was a <NativeSelect> until 2026-10-10\n<PropertyPicker name="Type" />`)).toHaveLength(0);
@@ -1442,6 +1451,36 @@ describe('--baseline (a consuming repo\'s ratchet)', () => {
1442
1451
  );
1443
1452
  });
1444
1453
 
1454
+ test('🔴 a row for a rule this belt does not have yet only warns — the app is behind a bump, not drifting (task 3098)', async () => {
1455
+ // 2026-10-13: a new rule's row went into every app's baseline with the belt still
1456
+ // unpublished, and the six apps on the LOCKED belt went red at once.
1457
+ await withFixture(
1458
+ { ...FIXTURE, 'scripts/baseline.json': JSON.stringify({ 'no-hardcoded-colors': 2, 'no-rule-from-the-future': 0 }) },
1459
+ async (dir) => {
1460
+ const { code, report } = await runCli(dir, ['no-hardcoded-colors', '--baseline', 'scripts/baseline.json', 'src']);
1461
+ expect(report).toContain('this cursedbelt does not have yet: no-rule-from-the-future');
1462
+ expect(report).not.toContain('nobody scans');
1463
+ expect(report).toContain('✓ no-hardcoded-colors: 2 violation(s)');
1464
+ expect(code).toBe(0);
1465
+ },
1466
+ );
1467
+ });
1468
+
1469
+ test('a row ahead of the belt does not excuse a row the belt knows and does not scan', async () => {
1470
+ await withFixture(
1471
+ {
1472
+ ...FIXTURE,
1473
+ 'scripts/baseline.json': JSON.stringify({ 'no-hardcoded-colors': 2, 'no-raw-zindex': 0, 'no-rule-from-the-future': 0 }),
1474
+ },
1475
+ async (dir) => {
1476
+ const { code, report } = await runCli(dir, ['no-hardcoded-colors', '--baseline', 'scripts/baseline.json', 'src']);
1477
+ expect(code).toBe(2);
1478
+ expect(report).toContain('row(s) for rules nobody scans: no-raw-zindex');
1479
+ expect(report).not.toContain('nobody scans: no-raw-zindex, no-rule-from-the-future');
1480
+ },
1481
+ );
1482
+ });
1483
+
1445
1484
  test('a scanned rule with no row is refused — a silent 0 demands the whole pile at once', async () => {
1446
1485
  await withFixture({ ...FIXTURE, 'scripts/baseline.json': JSON.stringify({ 'no-hardcoded-colors': 2 }) }, async (dir) => {
1447
1486
  const { code, report } = await runCli(dir, [
@@ -1651,6 +1690,29 @@ describe('no-hand-rolled-property-list', () => {
1651
1690
  });
1652
1691
  });
1653
1692
 
1693
+ describe('no-raw-kbd', () => {
1694
+ const rule = RULES['no-raw-kbd'];
1695
+ test('a raw <kbd> in app source is a violation — desk\'s switcher foot drew its own', () => {
1696
+ const found = rule.check('src/ui/QuickSwitcher.tsx', `<footer>\n <kbd>↑</kbd> move\n</footer>`);
1697
+ expect(found).toHaveLength(1);
1698
+ expect(found[0]?.line).toBe(2);
1699
+ });
1700
+ test('the belt\'s Kbd is the fix, and is not flagged', () => {
1701
+ expect(rule.check('src/ui/QuickSwitcher.tsx', `<Kbd keys="$mod+k" /> <Kbd>↑</Kbd>`)).toHaveLength(0);
1702
+ });
1703
+ test('an opt-out with a reason on the block above is honoured; a comment mentioning <kbd> is not a violation', () => {
1704
+ expect(
1705
+ rule.check('src/ui/Menu.tsx', `// guardrails-ignore no-raw-kbd: a menu accelerator is bare text,\n// not a cap\n<kbd>{shortcut}</kbd>`),
1706
+ ).toHaveLength(0);
1707
+ expect(rule.check('src/ui/Menu.tsx', `// the old <kbd> here was hand-drawn`)).toHaveLength(0);
1708
+ });
1709
+ test('exempt by construction: Kbd itself, fixtures and stylesheets', () => {
1710
+ for (const file of ['src/react/palette/Kbd.tsx', 'src/ui/Foot.spec.tsx', 'src/ui/foot.css']) {
1711
+ expect(rule.check(file, `<kbd>K</kbd>`)).toHaveLength(0);
1712
+ }
1713
+ });
1714
+ });
1715
+
1654
1716
  describe('no-raw-nav', () => {
1655
1717
  test('🔴 flags a hand-rolled <nav> — the header music shipped on 2026-09-25', () => {
1656
1718
  expect(
@@ -76,6 +76,10 @@
76
76
  // `Property`. On the ELEMENT, like `no-raw-data-table`. The belt's own PropertyList, the
77
77
  // markdown/rich-text renderers and `*.spec|test.*` fixtures are exempt by construction.
78
78
  //
79
+ // no-raw-kbd — forge task 3087 (2026-10-13): no hand-drawn `<kbd>` — a key is cursedbelt's
80
+ // `Kbd` (`<Kbd keys="$mod+k" />` → ⌘K / Ctrl K), and an app's `?` list is `ShortcutSheet`.
81
+ // On the ELEMENT. The belt's own `Kbd`, stylesheets and `*.spec|test.*` fixtures are exempt.
82
+ //
79
83
  // no-raw-data-table — DESIGN.md §3 "Visual Formatting & Data Presentation" (owner, 2026-08-14): no hand-rolled `<table>` —
80
84
  // use `cursedbelt/react/data-table`'s `DataTable`, which carries sorting, a global
81
85
  // filter, column visibility, pagination and the phone card tier. The rule is on the
@@ -113,7 +117,7 @@
113
117
  // Semantic classes are never matched — `text-xs` IS 12px. A non-text glyph takes
114
118
  // the per-line opt-out.
115
119
  //
116
- // no-native-select — owner, 2026-10-10: a `NativeSelect`'s list is drawn by the OS, where no
120
+ // no-native-select — owner, 2026-10-10: a `NativeSelect`'s (or raw `<select>`'s) list is drawn by the OS, where no
117
121
  // stylesheet reaches it, so it breaks the app's cohesion. `SelectField` / `ComboboxField` /
118
122
  // `PropertyPicker` / `ChoiceMenu` instead; a use where the OS picker is genuinely better says
119
123
  // why with `// guardrails-ignore no-native-select: <why>` on the line above. Ratcheted per app.
@@ -1057,6 +1061,7 @@ const CONTROL_HEIGHT_FILES = new Set([
1057
1061
  "IconButton.tsx",
1058
1062
  "Input.tsx",
1059
1063
  "SelectField.tsx",
1064
+ "SelectFieldImpl.tsx",
1060
1065
  "ComboboxField.tsx",
1061
1066
  "Autocomplete.tsx",
1062
1067
  "TagsInput.tsx",
@@ -1296,6 +1301,48 @@ const noHandRolledPropertyList: Rule = {
1296
1301
  },
1297
1302
  };
1298
1303
 
1304
+ // ── no-raw-kbd (forge task 3087, 2026-10-13) ───────────────────────────────────
1305
+ // By 2026-10-13 four apps drew the same key four ways: vault's cap was mono with a 2px bottom
1306
+ // border, desk's a bare browser `<kbd>`, family's its own grey box, music's another — and ⌘ showed
1307
+ // to a Windows reader wherever a glyph had been typed by hand. cursedbelt 5.79.0's `Kbd` is the
1308
+ // one cap, formatted per platform from a binding (`<Kbd keys="$mod+k" />`), or drawn as written
1309
+ // from children (`<Kbd>↑</Kbd>`); `ShortcutSheet` is the `?` list built from it.
1310
+ //
1311
+ // So the rule is on the ELEMENT, like `no-hand-rolled-property-list`: a `<kbd` in app source is
1312
+ // a violation. Exempt by construction: `Kbd` itself, `DropdownMenu`'s accelerator column (bare
1313
+ // text in the platform's notation, as macOS's and Linear's menus draw it — not a cap), and
1314
+ // `*.spec|test.*` fixtures. A key named in
1315
+ // prose is still a key — it is `<Kbd>`; a BUTTON named in prose is `<strong>`, never a cap.
1316
+ const RAW_KBD_RE = /<kbd\b/g;
1317
+ const RAW_KBD_IGNORE_RE = /guardrails-ignore\s+no-raw-kbd\s*:/;
1318
+ const RAW_KBD_EXEMPT_FRAGMENTS = ["react/palette/Kbd.tsx", "react/components/DropdownMenu.tsx"];
1319
+
1320
+ const noRawKbd: Rule = {
1321
+ name: "no-raw-kbd",
1322
+ description:
1323
+ "forge task 3087: no hand-drawn <kbd> — a key cap is cursedbelt's Kbd (cursedbelt/react: <Kbd keys=\"$mod+k\" /> draws ⌘K on a Mac and Ctrl K elsewhere; <Kbd>↑</Kbd> draws a literal), and an app's ? list is ShortcutSheet. A raw <kbd> is how every app picks its own cap, and how ⌘ reaches a Windows reader.",
1324
+ check(filePath, source) {
1325
+ const normalized = filePath.replace(/\\/g, "/");
1326
+ if (extname(normalized) === ".css" || isTestSource(normalized)) return [];
1327
+ if (RAW_KBD_EXEMPT_FRAGMENTS.some((fragment) => normalized.includes(fragment))) return [];
1328
+ const violations: Violation[] = [];
1329
+ const scannable = blankComments(source);
1330
+ const lines = source.split("\n");
1331
+ for (const match of scannable.matchAll(RAW_KBD_RE)) {
1332
+ const line = lineOf(source, match.index ?? 0);
1333
+ const block: string[] = [];
1334
+ for (let i = line - 2; i >= 0 && block.length < 12; i--) {
1335
+ const raw = lines[i] ?? "";
1336
+ if (raw.trim() === "") break;
1337
+ block.push(raw);
1338
+ }
1339
+ if (RAW_KBD_IGNORE_RE.test(block.join("\n"))) continue;
1340
+ violations.push({ file: filePath, line, snippet: lines[line - 1]?.trim() ?? "" });
1341
+ }
1342
+ return violations;
1343
+ },
1344
+ };
1345
+
1299
1346
  // ── no-unguarded-editing-modal ──────────────────────────────────────────────
1300
1347
  // The owner lost a half-typed password by clicking outside an editing modal,
1301
1348
  // which dismissed it and threw the form away with no confirmation. `Modal`/
@@ -2041,7 +2088,9 @@ const noSelectAsActionMenu: Rule = {
2041
2088
  // macOS Chrome a grey system menu no stylesheet reaches — so every app that used one had a control
2042
2089
  // that was not the kit's. `no-select-as-action-menu` caught only the action-menu shape of it.
2043
2090
  //
2044
- // Every app now counts its `<NativeSelect` uses in its two-sided ratchet, so the number only falls:
2091
+ // Every app now counts its `<NativeSelect` uses — and its raw `<select>`s, the same OS list without
2092
+ // even the kit's frame (forge task 3097: desk had eight under `no-raw-inputs`, which no count of
2093
+ // NativeSelect could see) — in its two-sided ratchet, so the number only falls:
2045
2094
  // `SelectField` / `ComboboxField` for a field, `PropertyPicker` for a record's property pill,
2046
2095
  // `ChoiceMenu` to choose-and-act. A use that is genuinely right — a form on a phone where the OS
2047
2096
  // wheel IS the better control — says so on the line above:
@@ -2051,18 +2100,19 @@ const NATIVE_SELECT_IGNORE_RE = /guardrails-ignore\s+no-native-select\s*:/;
2051
2100
  const noNativeSelect: Rule = {
2052
2101
  name: "no-native-select",
2053
2102
  description:
2054
- "Owner, 2026-10-10: unstyled dropdowns break the app's cohesiveness. A `NativeSelect`'s list is drawn by the OS, outside the page, where no stylesheet reaches it. Use `SelectField` / `ComboboxField` for a form field, `PropertyPicker` for a record's property (Type, Folder, Tags), or `ChoiceMenu` to choose-and-act. Where the OS picker is the better control, say why on the line above: `// guardrails-ignore no-native-select: <why>`.",
2103
+ "Owner, 2026-10-10: unstyled dropdowns break the app's cohesiveness. A `NativeSelect`'s (or a raw `<select>`'s) list is drawn by the OS, outside the page, where no stylesheet reaches it. Use `SelectField` / `ComboboxField` for a form field, `PropertyPicker` for a record's property (Type, Folder, Tags), or `ChoiceMenu` to choose-and-act. Where the OS picker is the better control, say why on the line above: `// guardrails-ignore no-native-select: <why>`.",
2055
2104
  check(filePath, source) {
2056
2105
  const unix = filePath.replace(/\\/g, "/");
2057
2106
  // The kit is held to its own law (forge task 3063: its DataTable filter builder and
2058
2107
  // FocusViewer drew OS menus into every app whose count read 0). Only the control itself
2059
2108
  // may render one.
2060
2109
  if (/(^|\/)(?:packages|libs)\/cursedbelt\/src\/react\/components\/NativeSelect\.tsx$/.test(unix)) return [];
2110
+ if (isBeltOwnSource(filePath) && /(^|\/)src\/react\/components\/NativeSelect\.tsx$/.test(unix)) return [];
2061
2111
  if (/\.(?:spec|test)\.[tj]sx?$/.test(unix)) return [];
2062
2112
  const scannable = blankComments(source);
2063
2113
  const lines = source.split("\n");
2064
2114
  const out: Violation[] = [];
2065
- for (const match of scannable.matchAll(/<NativeSelect\b/g)) {
2115
+ for (const match of scannable.matchAll(/<(?:NativeSelect|select)\b/g)) {
2066
2116
  const line = lineOf(source, match.index ?? 0);
2067
2117
  if (NATIVE_SELECT_IGNORE_RE.test(lines[line - 1] ?? "") || NATIVE_SELECT_IGNORE_RE.test(blockAbove(lines, line))) continue;
2068
2118
  out.push({ file: filePath, line, snippet: lines[line - 1]?.trim() ?? "" });
@@ -3022,6 +3072,7 @@ export const RULES: Record<string, Rule> = {
3022
3072
  [noRawZindex.name]: noRawZindex,
3023
3073
  [noRawDataTable.name]: noRawDataTable,
3024
3074
  [noHandRolledPropertyList.name]: noHandRolledPropertyList,
3075
+ [noRawKbd.name]: noRawKbd,
3025
3076
  [noTailwindPalette.name]: noTailwindPalette,
3026
3077
  [themeColoursResolve.name]: themeColoursResolve,
3027
3078
  [noRawTitleAttr.name]: noRawTitleAttr,
@@ -3206,12 +3257,25 @@ function parseFlatBaseline(json: string, label: string): FlatBaseline {
3206
3257
  function baselineDisagreements(rows: FlatBaseline, ruleNames: readonly string[]): string[] {
3207
3258
  const complaints: string[] = [];
3208
3259
  const missing = ruleNames.filter((name) => !(name in rows));
3209
- const orphans = Object.keys(rows).filter((name) => !ruleNames.includes(name));
3260
+ const orphans = Object.keys(rows).filter((name) => !ruleNames.includes(name) && name in RULES);
3210
3261
  if (missing.length > 0) complaints.push(`no row for rule(s) being scanned: ${missing.join(", ")}`);
3211
3262
  if (orphans.length > 0) complaints.push(`row(s) for rules nobody scans: ${orphans.join(", ")}`);
3212
3263
  return complaints;
3213
3264
  }
3214
3265
 
3266
+ /**
3267
+ * Rows naming a rule this belt has never heard of — forge task 3098. A new rule lands
3268
+ * in the belt's tree and every consumer's baseline gets its row in the same pass, but a
3269
+ * consumer still on the LOCKED belt then carries a row ahead of its library: not drift,
3270
+ * a bump not made yet. Refusing it red every app off the tree at once (2026-10-13, six
3271
+ * fast-lane deploys). It is a warning; a row this belt KNOWS and does not scan is still
3272
+ * refused above, and a mistyped row cannot hide anything — the real rule's row is then
3273
+ * missing, which is red.
3274
+ */
3275
+ function rowsAheadOfThisBelt(rows: FlatBaseline, ruleNames: readonly string[]): string[] {
3276
+ return Object.keys(rows).filter((name) => !ruleNames.includes(name) && !(name in RULES));
3277
+ }
3278
+
3215
3279
  // ── `--reimplements-from-belt`: the name list, off this package's own exports ───
3216
3280
  // `no-local-reimplementation` matches app-declared names against the ones cursedbelt
3217
3281
  // exports (384 of them as this lands). A hand-kept copy of a dependency's export list
@@ -3322,6 +3386,13 @@ function main() {
3322
3386
  console.error((error as Error).message);
3323
3387
  process.exit(2);
3324
3388
  }
3389
+ const ahead = rowsAheadOfThisBelt(flatBaseline, ruleNames);
3390
+ if (ahead.length > 0) {
3391
+ console.error(
3392
+ `⚠ ${baselinePath} has row(s) for rule(s) this cursedbelt does not have yet: ${ahead.join(", ")} — ` +
3393
+ "not scanned; they apply once the app is on a cursedbelt that ships them.",
3394
+ );
3395
+ }
3325
3396
  const complaints = baselineDisagreements(flatBaseline, ruleNames);
3326
3397
  if (complaints.length > 0) {
3327
3398
  console.error(`${baselinePath} and the rules being scanned disagree:`);
@@ -3329,7 +3400,7 @@ function main() {
3329
3400
  // A row with no rule stops here. A rule with no row — what a belt bump that adds a
3330
3401
  // rule produces — is still scanned, so the run prints the exact row to record
3331
3402
  // instead of making the bumping agent find the count by hand; it exits 2 at the end.
3332
- if (Object.keys(flatBaseline).some((name) => !ruleNames.includes(name))) process.exit(2);
3403
+ if (Object.keys(flatBaseline).some((name) => !ruleNames.includes(name) && name in RULES)) process.exit(2);
3333
3404
  for (const name of ruleNames) if (!(name in flatBaseline)) missingRows.add(name);
3334
3405
  }
3335
3406
  }
@@ -195,6 +195,7 @@ export const AREAS: readonly Area[] = [
195
195
  'components/Section.tsx',
196
196
  'components/SegmentedControl.tsx',
197
197
  'components/SelectField.tsx',
198
+ 'components/SelectFieldImpl.tsx',
198
199
  'components/Skeleton.tsx',
199
200
  'components/Slider.tsx',
200
201
  'components/Spinner.tsx',
@@ -291,6 +292,7 @@ export const AREAS: readonly Area[] = [
291
292
  // The breadcrumb's menus and pivot are `import()`ed by `nav/BreadcrumbTrail.tsx` (lazyImportFault).
292
293
  roots: ['folder-tree', 'nav/BreadcrumbMenus.tsx', 'nav/BreadcrumbPivot.tsx'],
293
294
  },
295
+ { name: 'kanban', why: 'the Kanban board — columns of dragged cards; an app with no board renders none of it', roots: ['components/KanbanBoard.tsx'] },
294
296
  { name: 'keep-awake', why: 'the keep-awake toggle and its badge', roots: ['keep-awake'] },
295
297
  { name: 'layout-engine', why: 'the persisted pane/grid layout engine — apps with a fixed layout render none of it', roots: ['layoutEngine'] },
296
298
  { name: 'markdown', why: 'the rendered-markdown document', roots: ['markdown', 'markdown/MarkdownDoc.tsx'] },
@@ -26,6 +26,17 @@ describe('HudZoomControls (CNV-3)', () => {
26
26
  expect(calls).toEqual(['out', 'in', 'fit', 'center']);
27
27
  });
28
28
 
29
+ test('vertical: a column with + on top and − under the percentage, announced as one', () => {
30
+ const { getByRole } = render(
31
+ <HudZoomControls orientation='vertical' scale={0.5} onZoomIn={() => {}} onZoomOut={() => {}} onFit={() => {}} />,
32
+ );
33
+ const group = getByRole('group', { name: 'View' });
34
+ expect(group.getAttribute('aria-orientation')).toBe('vertical');
35
+ expect(group.className).toContain('flex-col');
36
+ const names = Array.from(group.querySelectorAll('button')).map((b) => b.getAttribute('aria-label'));
37
+ expect(names).toEqual(['Zoom in', 'Zoom out', 'Fit to screen']);
38
+ });
39
+
29
40
  test('no Center key when the canvas has nothing to centre on', () => {
30
41
  const { queryByRole } = render(
31
42
  <HudZoomControls scale={1} onZoomIn={() => {}} onZoomOut={() => {}} onFit={() => {}} />,