@kerfjs/ui 5.0.0-beta.105 → 5.0.0-beta.107

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 (227) hide show
  1. package/README.md +2 -1
  2. package/ai/application-ui-diagnostic-ids-v1.json +1 -0
  3. package/ai/application-ui-profile.defaults.json +6 -1
  4. package/ai/component-catalog.json +160 -27
  5. package/ai/component-composition-rules.json +20 -2
  6. package/ai/component-composition.d.ts +11 -0
  7. package/ai/component-composition.json +118 -19
  8. package/ai/component-composition.schema.json +32 -0
  9. package/ai/components/README.md +3 -2
  10. package/ai/components/button.md +4 -1
  11. package/ai/components/checkbox-group.md +89 -0
  12. package/ai/components/checkbox.md +3 -2
  13. package/ai/components/color-picker.md +10 -7
  14. package/ai/components/disclosure.md +6 -1
  15. package/ai/components/foundation.md +1 -1
  16. package/ai/components/list-detail-dialog.md +1 -1
  17. package/ai/components/message-composer.md +2 -1
  18. package/ai/components/radio-group.md +4 -3
  19. package/ai/components/split-button.md +5 -2
  20. package/ai/components/surface-scaffold.md +2 -1
  21. package/ai/components/switch.md +1 -1
  22. package/ai/components/text.md +2 -0
  23. package/ai/components/wa-checkbox-group.md +5 -5
  24. package/ai/components/workbench-dialog.md +1 -1
  25. package/ai/public-api-signatures-v1.md +198 -26
  26. package/ai/skill.md +73 -62
  27. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  28. package/dist/browser/checkbox-group.js +5 -0
  29. package/dist/button.d.ts +15 -2
  30. package/dist/button.js +1 -1
  31. package/dist/catalog.js +9 -8
  32. package/dist/catalog.js.map +1 -1
  33. package/dist/checkbox-group.d.ts +70 -0
  34. package/dist/checkbox-group.js +7 -0
  35. package/dist/checkbox-group.js.map +1 -0
  36. package/dist/{chunk-SQ2Q3OQI.js → chunk-2LM3PYHA.js} +2 -2
  37. package/dist/{chunk-SQ2Q3OQI.js.map → chunk-2LM3PYHA.js.map} +1 -1
  38. package/dist/{chunk-HYORKKYP.js → chunk-3VZ6SFD4.js} +1 -1
  39. package/dist/chunk-3VZ6SFD4.js.map +1 -0
  40. package/dist/{chunk-EKBMUXFV.js → chunk-3Z7N2NAZ.js} +141 -54
  41. package/dist/chunk-3Z7N2NAZ.js.map +1 -0
  42. package/dist/{chunk-FKKJHGN5.js → chunk-AZ3WT77F.js} +2 -2
  43. package/dist/{chunk-FKKJHGN5.js.map → chunk-AZ3WT77F.js.map} +1 -1
  44. package/dist/{chunk-FG6U7KQQ.js → chunk-DNWWF6UN.js} +2 -2
  45. package/dist/{chunk-FG6U7KQQ.js.map → chunk-DNWWF6UN.js.map} +1 -1
  46. package/dist/{chunk-C3RLKKFQ.js → chunk-EYDXFOSH.js} +13 -8
  47. package/dist/chunk-EYDXFOSH.js.map +1 -0
  48. package/dist/{chunk-TAHR457X.js → chunk-G4DRH4A5.js} +3 -1
  49. package/dist/chunk-G4DRH4A5.js.map +1 -0
  50. package/dist/{chunk-PE2KPZDW.js → chunk-HDXUHLTQ.js} +7 -2
  51. package/dist/chunk-HDXUHLTQ.js.map +1 -0
  52. package/dist/chunk-HU5IHXCI.js +73 -0
  53. package/dist/chunk-HU5IHXCI.js.map +1 -0
  54. package/dist/{chunk-DF2YKGIX.js → chunk-JCCN3U6X.js} +2 -2
  55. package/dist/{chunk-DF2YKGIX.js.map → chunk-JCCN3U6X.js.map} +1 -1
  56. package/dist/{chunk-GCAK23PU.js → chunk-MVKHSAKE.js} +2 -2
  57. package/dist/{chunk-GCAK23PU.js.map → chunk-MVKHSAKE.js.map} +1 -1
  58. package/dist/{chunk-DDSCVEO5.js → chunk-NLMEDZRM.js} +5 -5
  59. package/dist/{chunk-DDSCVEO5.js.map → chunk-NLMEDZRM.js.map} +1 -1
  60. package/dist/{chunk-ECFCAKWE.js → chunk-NR6O74ST.js} +5 -1
  61. package/dist/chunk-NR6O74ST.js.map +1 -0
  62. package/dist/{chunk-7W3GKMN4.js → chunk-OQP3PFKQ.js} +7 -4
  63. package/dist/chunk-OQP3PFKQ.js.map +1 -0
  64. package/dist/{chunk-WJKHLOP5.js → chunk-TI2GV36S.js} +8 -2
  65. package/dist/chunk-TI2GV36S.js.map +1 -0
  66. package/dist/{chunk-UTCAOOXU.js → chunk-TXRKCGT3.js} +8 -2
  67. package/dist/chunk-TXRKCGT3.js.map +1 -0
  68. package/dist/{chunk-ACUKTVS3.js → chunk-XWNXGFX3.js} +52 -42
  69. package/dist/chunk-XWNXGFX3.js.map +1 -0
  70. package/dist/{chunk-ZXZAECHZ.js → chunk-Z4DS77U2.js} +2 -2
  71. package/dist/{chunk-ZXZAECHZ.js.map → chunk-Z4DS77U2.js.map} +1 -1
  72. package/dist/collapsible-panel.js +2 -2
  73. package/dist/color-picker.d.ts +30 -5
  74. package/dist/color-picker.js +1 -1
  75. package/dist/disclosure.d.ts +13 -4
  76. package/dist/disclosure.js +1 -1
  77. package/dist/disclosure.js.map +1 -1
  78. package/dist/help-tags-register.js +1 -1
  79. package/dist/index.d.ts +2 -1
  80. package/dist/index.js +20 -19
  81. package/dist/list-action-row.js +1 -1
  82. package/dist/list-detail-dialog.d.ts +7 -1
  83. package/dist/list-detail-dialog.js +6 -4
  84. package/dist/list-detail-dialog.js.map +1 -1
  85. package/dist/list-header.d.ts +8 -1
  86. package/dist/list-header.js +1 -1
  87. package/dist/nav-stack.js +1 -1
  88. package/dist/popup-menu-register.js +2 -2
  89. package/dist/popup-menu.d.ts +15 -5
  90. package/dist/popup-menu.js +1 -1
  91. package/dist/select-register.js +3 -3
  92. package/dist/split-button.d.ts +14 -2
  93. package/dist/split-button.js +1 -1
  94. package/dist/split-view.js +1 -1
  95. package/dist/styles/anchored-surface.css +3 -0
  96. package/dist/styles/button.css +29 -0
  97. package/dist/styles/catalog/components/catalog-example.css +55 -18
  98. package/dist/styles/checkbox-group.css +63 -0
  99. package/dist/styles/checkbox.css +7 -6
  100. package/dist/styles/color-picker.css +69 -34
  101. package/dist/styles/components/actions/button/button.css +29 -0
  102. package/dist/styles/components/actions/split-button/split-button.css +51 -5
  103. package/dist/styles/components/collections/framed-list/framed-list.css +4 -0
  104. package/dist/styles/components/collections/list/list.css +6 -0
  105. package/dist/styles/components/collections/list-header/list-header.css +7 -0
  106. package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +3 -0
  107. package/dist/styles/components/forms/checkbox/checkbox.css +7 -6
  108. package/dist/styles/components/forms/checkbox-group/checkbox-group.css +63 -0
  109. package/dist/styles/components/forms/color-picker/color-picker.css +69 -34
  110. package/dist/styles/components/forms/radio-group/radio-group.css +7 -5
  111. package/dist/styles/components/forms/select/select.css +6 -2
  112. package/dist/styles/components/forms/switch/switch.css +3 -3
  113. package/dist/styles/components/layout/pane/pane.css +9 -0
  114. package/dist/styles/components/layout/workbench/workbench.css +4 -0
  115. package/dist/styles/components/navigation/nav-stack/nav-stack.css +64 -1
  116. package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +1 -0
  117. package/dist/styles/components/surfaces/content-item/content-item.css +30 -0
  118. package/dist/styles/components/surfaces/disclosure/disclosure.css +38 -0
  119. package/dist/styles/components/surfaces/sunken-panel/sunken-panel.css +3 -0
  120. package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +6 -0
  121. package/dist/styles/components/typography/text/text.css +10 -0
  122. package/dist/styles/content-item.css +30 -0
  123. package/dist/styles/disclosure.css +38 -0
  124. package/dist/styles/foundation.css +21 -0
  125. package/dist/styles/framed-list.css +4 -0
  126. package/dist/styles/list-header.css +7 -0
  127. package/dist/styles/list.css +6 -0
  128. package/dist/styles/nav-stack.css +64 -1
  129. package/dist/styles/pane.css +9 -0
  130. package/dist/styles/radio-group.css +7 -5
  131. package/dist/styles/select.css +6 -2
  132. package/dist/styles/split-button.css +51 -5
  133. package/dist/styles/styles.css +1 -0
  134. package/dist/styles/sunken-panel.css +3 -0
  135. package/dist/styles/surface-scaffold.css +6 -0
  136. package/dist/styles/switch.css +3 -3
  137. package/dist/styles/tab-navigator.css +1 -0
  138. package/dist/styles/text.css +10 -0
  139. package/dist/styles/webawesome.css +45 -9
  140. package/dist/styles/workbench.css +4 -0
  141. package/dist/surface-scaffold.d.ts +7 -1
  142. package/dist/surface-scaffold.js +1 -1
  143. package/dist/wire-color-fields.js +2 -2
  144. package/dist/wire-color-pickers.d.ts +4 -3
  145. package/dist/wire-color-pickers.js +1 -1
  146. package/dist/wire-comboboxes.js +1 -1
  147. package/dist/workbench-dialog.d.ts +11 -2
  148. package/dist/workbench-dialog.js +5 -2
  149. package/dist/workbench-dialog.js.map +1 -1
  150. package/dist/workbench.js +1 -1
  151. package/docs/accessibility.md +40 -1
  152. package/docs/anchored-surface.md +6 -1
  153. package/docs/catalog.md +26 -1
  154. package/docs/color-picker.md +39 -8
  155. package/docs/component-contract.md +33 -2
  156. package/docs/component-selection.md +20 -9
  157. package/docs/design/templates/button/full-width-dark.svg +1 -0
  158. package/docs/design/templates/button/full-width.svg +1 -0
  159. package/docs/design/templates/button-dark.svg +1 -1
  160. package/docs/design/templates/button.svg +1 -1
  161. package/docs/design/templates/checkbox/error-dark.svg +1 -1
  162. package/docs/design/templates/checkbox/error.svg +1 -1
  163. package/docs/design/templates/checkbox/states-dark.svg +1 -1
  164. package/docs/design/templates/checkbox/states.svg +1 -1
  165. package/docs/design/templates/checkbox-dark.svg +1 -1
  166. package/docs/design/templates/checkbox-group/horizontal-dark.svg +1 -0
  167. package/docs/design/templates/checkbox-group/horizontal.svg +1 -0
  168. package/docs/design/templates/checkbox-group/vertical-dark.svg +1 -0
  169. package/docs/design/templates/checkbox-group/vertical.svg +1 -0
  170. package/docs/design/templates/checkbox-group-dark.svg +1 -0
  171. package/docs/design/templates/checkbox-group.svg +1 -0
  172. package/docs/design/templates/checkbox.svg +1 -1
  173. package/docs/design/templates/color-picker/gray-dark.svg +1 -1
  174. package/docs/design/templates/color-picker/gray.svg +1 -1
  175. package/docs/design/templates/color-picker/hsb-dark.svg +1 -0
  176. package/docs/design/templates/color-picker/hsb.svg +1 -0
  177. package/docs/design/templates/color-picker/rgb-dark.svg +1 -1
  178. package/docs/design/templates/color-picker/rgb.svg +1 -1
  179. package/docs/design/templates/color-picker-dark.svg +1 -1
  180. package/docs/design/templates/color-picker.svg +1 -1
  181. package/docs/design/templates/disclosure/embedded-dark.svg +1 -0
  182. package/docs/design/templates/disclosure/embedded.svg +1 -0
  183. package/docs/design/templates/disclosure-dark.svg +1 -1
  184. package/docs/design/templates/disclosure.svg +1 -1
  185. package/docs/design/templates/radio-group/horizontal-dark.svg +1 -1
  186. package/docs/design/templates/radio-group/horizontal.svg +1 -1
  187. package/docs/design/templates/radio-group/vertical-dark.svg +1 -1
  188. package/docs/design/templates/radio-group/vertical.svg +1 -1
  189. package/docs/design/templates/radio-group-dark.svg +1 -1
  190. package/docs/design/templates/radio-group.svg +1 -1
  191. package/docs/design/templates/split-button/full-width-dark.svg +1 -0
  192. package/docs/design/templates/split-button/full-width.svg +1 -0
  193. package/docs/design/templates/split-button/pill-dark.svg +1 -0
  194. package/docs/design/templates/split-button/pill.svg +1 -0
  195. package/docs/design/templates/split-button/states-dark.svg +1 -0
  196. package/docs/design/templates/split-button/states.svg +1 -0
  197. package/docs/design/templates/split-button/tones-dark.svg +1 -0
  198. package/docs/design/templates/split-button/tones.svg +1 -0
  199. package/docs/design/templates/split-button-dark.svg +1 -0
  200. package/docs/design/templates/split-button.svg +1 -0
  201. package/docs/design/templates/switch/states-dark.svg +1 -1
  202. package/docs/design/templates/switch/states.svg +1 -1
  203. package/docs/design/templates/switch-dark.svg +1 -1
  204. package/docs/design/templates/switch.svg +1 -1
  205. package/docs/design/templates.md +10 -3
  206. package/docs/disclosure.md +34 -0
  207. package/docs/list-detail-dialog.md +4 -0
  208. package/docs/nav-stack.md +6 -2
  209. package/docs/surface-scaffold.md +6 -2
  210. package/docs/ui-doctor.md +16 -4
  211. package/docs/webawesome-migration.md +1 -1
  212. package/docs/webawesome-theme.md +48 -16
  213. package/docs/workbench-dialog.md +4 -1
  214. package/docs/writing.md +3 -3
  215. package/doctor/copy-capitalization-checks.mjs +10 -7
  216. package/llms.txt +50 -20
  217. package/package.json +7 -1
  218. package/dist/chunk-7W3GKMN4.js.map +0 -1
  219. package/dist/chunk-ACUKTVS3.js.map +0 -1
  220. package/dist/chunk-C3RLKKFQ.js.map +0 -1
  221. package/dist/chunk-ECFCAKWE.js.map +0 -1
  222. package/dist/chunk-EKBMUXFV.js.map +0 -1
  223. package/dist/chunk-HYORKKYP.js.map +0 -1
  224. package/dist/chunk-PE2KPZDW.js.map +0 -1
  225. package/dist/chunk-TAHR457X.js.map +0 -1
  226. package/dist/chunk-UTCAOOXU.js.map +0 -1
  227. package/dist/chunk-WJKHLOP5.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/surfaces/workbench-dialog/workbench-dialog.tsx"],"names":[],"mappings":"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","file":"workbench-dialog.js","sourcesContent":["import { X } from 'lucide';\n\nimport type { KerfUiContent } from '../../../shared/content/semantic-content.js';\nimport type { CssLength } from '../../../shared/styles/css-values.js';\nimport { remify } from '../../../shared/styles/css-values.js';\nimport { ToolbarControlGroup } from '../../actions/toolbar-control-group/toolbar-control-group.js';\nimport { ToolbarText } from '../../actions/toolbar-text/toolbar-text.js';\nimport {\n Workbench,\n type WorkbenchMainBottomToolbar,\n type WorkbenchPanel,\n} from '../../layout/workbench/workbench.js';\nimport { LucideIcon } from '../../media/lucide-icon/lucide-icon.js';\nimport {\n DialogSurface,\n type DialogSurfaceHeight,\n type DialogSurfaceMaxHeight,\n type DialogSurfaceRootAttributes,\n type DialogSurfaceSize,\n} from '../surface-scaffold/surface-scaffold.js';\n\n// Open and close it like any DialogSurface, from the same import.\nexport {\n closeDialog,\n type DialogCloseSource,\n type DialogRequestCloseDetail,\n openDialog,\n} from '../surface-scaffold/surface-scaffold.js';\n\n/**\n * Application `data-*` identity on the dialog root, such as `data-key`; the\n * dialog's own configuration attributes stay protected.\n */\nexport type WorkbenchDialogRootAttributes = DialogSurfaceRootAttributes;\n\nexport interface WorkbenchDialogProps {\n /**\n * The dialog's id, for `openDialog(document.getElementById(id))`. The\n * Workbench inside is `<id>-workbench`, so its rails are\n * `<id>-workbench-left-rail` and `<id>-workbench-right-rail`.\n */\n id: string;\n /** The dialog's accessible name. */\n label: string;\n /** The dialog's primary title: an xl heading in the work area's toolbar. */\n title: string;\n /** A short supporting line under the title. */\n description?: string;\n /** Groups after the title in the work area's toolbar. */\n leading?: KerfUiContent;\n /** Header controls before the close control, such as a `ToolbarControlGroup`. */\n actions?: KerfUiContent;\n /** The work area. */\n main: KerfUiContent;\n /** Fixed content over the work area's bottom toolbar, such as a status line. */\n mainFooter?: KerfUiContent;\n /** The work area's bottom toolbar, usually the dialog's buttons. */\n mainBottomToolbar?: WorkbenchMainBottomToolbar;\n /**\n * Side rails with their own toolbars. On compact devices each presents as\n * an overlay over the work area; the app starts it collapsed and its\n * `toolbar.toggle` shows it.\n */\n leftRail?: WorkbenchPanel;\n rightRail?: WorkbenchPanel;\n /** A bottom drawer under the work area. */\n bottomDrawer?: WorkbenchPanel;\n /**\n * Fill the screen with the rails as overlays: derive from\n * `deviceClass().value.compact`.\n */\n compact?: boolean;\n /** The close control's accessible name (default `Close`). */\n closeLabel?: string;\n /** Close on a press on the backdrop; otherwise the panel pulses. */\n lightDismiss?: boolean;\n /** Roomy preferred width preset (default `large`). */\n size?: DialogSurfaceSize;\n /** Roomy preferred width; takes precedence over `size`. */\n preferredWidth?: CssLength;\n /** Roomy definite height (default 560px), bounded by the viewport. */\n height?: DialogSurfaceHeight;\n /** Roomy height cap in place of a definite height. */\n maxHeight?: DialogSurfaceMaxHeight;\n /** Roomy edge clearance from the viewport. */\n viewportGutter?: CssLength;\n /** Minimum work-area width beside inline rails, in px (Workbench default 320). */\n mainMinSize?: number;\n rootAttributes?: WorkbenchDialogRootAttributes;\n className?: string;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\n/** A rail presents as an overlay in a compact dialog, as the app left it otherwise. */\nfunction compactRail(\n panel: WorkbenchPanel | undefined,\n compact: boolean,\n): WorkbenchPanel | undefined {\n return panel && compact ? { ...panel, presentation: 'overlay' } : panel;\n}\n\n/**\n * A workbench dialog: a modal whose body is a `Workbench`. The work area's\n * toolbar carries the dialog's primary title, its actions, and the close\n * control; side rails keep their own toolbars and run the dialog's full\n * height. On compact devices it fills the screen and its rails become\n * overlays over the work area, shown by their toolbar toggles, so the work\n * area keeps its width. Open it with `openDialog()`; the app owns each\n * rail's `collapsed` state, and `wireWorkbench` closes an overlay rail on\n * Escape or a press outside it before the dialog sees the Escape. Built from\n * `DialogSurface` and `Workbench`.\n */\nexport function WorkbenchDialog({\n id,\n label,\n title,\n description,\n leading,\n actions,\n main,\n mainFooter,\n mainBottomToolbar,\n leftRail,\n rightRail,\n bottomDrawer,\n compact = false,\n closeLabel = 'Close',\n lightDismiss = false,\n size = 'large',\n preferredWidth,\n height,\n maxHeight,\n viewportGutter,\n mainMinSize,\n rootAttributes,\n className = '',\n slot,\n}: WorkbenchDialogProps) {\n return (\n <DialogSurface\n id={id}\n label={label}\n withoutHeader\n lightDismiss={lightDismiss}\n closeLabel={closeLabel}\n size={size}\n presentation={compact ? 'fullscreen' : 'modal'}\n preferredWidth={preferredWidth}\n height={\n compact\n ? undefined\n : maxHeight === undefined\n ? (height ?? remify(560))\n : height\n }\n maxHeight={compact ? undefined : maxHeight}\n viewportGutter={compact ? undefined : viewportGutter}\n bodyInset=\"none\"\n bodyLayout=\"fill\"\n rootAttributes={{\n ...rootAttributes,\n 'data-dialog-layout': 'workbench',\n }}\n className={className}\n slot={slot}\n >\n <Workbench\n id={`${id}-workbench`}\n label={label}\n mainToolbar={{\n label: title,\n title: (\n <ToolbarText\n text={title}\n description={description}\n size=\"xlarge\"\n id={`${id}-title`}\n headingLevel={2}\n />\n ),\n leading,\n trailing: actions,\n // After any hidden right rail's toggle, so Close stays outermost.\n end: (\n <ToolbarControlGroup appearance=\"borderless\" single>\n <button\n type=\"button\"\n aria-label={closeLabel}\n data-dialog-close=\"\"\n >\n <LucideIcon icon={X} name=\"x\" />\n </button>\n </ToolbarControlGroup>\n ),\n }}\n main={main}\n mainFooter={mainFooter}\n mainBottomToolbar={mainBottomToolbar}\n leftRail={compactRail(leftRail, compact)}\n rightRail={compactRail(rightRail, compact)}\n bottomDrawer={bottomDrawer}\n mainMinSize={mainMinSize}\n />\n </DialogSurface>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/components/surfaces/workbench-dialog/workbench-dialog.tsx"],"names":[],"mappings":"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","file":"workbench-dialog.js","sourcesContent":["import { X } from 'lucide';\n\nimport type { KerfUiContent } from '../../../shared/content/semantic-content.js';\nimport type { CssLength } from '../../../shared/styles/css-values.js';\nimport { remify } from '../../../shared/styles/css-values.js';\nimport { ToolbarControlGroup } from '../../actions/toolbar-control-group/toolbar-control-group.js';\nimport { ToolbarText } from '../../actions/toolbar-text/toolbar-text.js';\nimport {\n Workbench,\n type WorkbenchMainBottomToolbar,\n type WorkbenchPanel,\n} from '../../layout/workbench/workbench.js';\nimport { LucideIcon } from '../../media/lucide-icon/lucide-icon.js';\nimport {\n DialogSurface,\n type DialogSurfaceHeight,\n type DialogSurfaceMaxHeight,\n type DialogSurfaceRootAttributes,\n type DialogSurfaceSize,\n} from '../surface-scaffold/surface-scaffold.js';\n\n// Open and close it like any DialogSurface, from the same import.\nexport {\n closeDialog,\n type DialogCloseSource,\n type DialogRequestCloseDetail,\n openDialog,\n} from '../surface-scaffold/surface-scaffold.js';\n\n/**\n * Application `data-*` identity on the dialog root, such as `data-key`; the\n * dialog's own configuration attributes stay protected.\n */\nexport type WorkbenchDialogRootAttributes = DialogSurfaceRootAttributes;\n\nexport interface WorkbenchDialogProps {\n /**\n * The dialog's id, for `openDialog(document.getElementById(id))`. The\n * Workbench inside is `<id>-workbench`, so its rails are\n * `<id>-workbench-left-rail` and `<id>-workbench-right-rail`.\n */\n id: string;\n /** The dialog's accessible name. */\n label: string;\n /** The dialog's primary title: an xl heading in the work area's toolbar. */\n title: string;\n /**\n * A short supporting line under the title. It is the dialog's accessible\n * description (`aria-describedby` to its `<id>-description`) unless\n * `describedBy` names other content.\n */\n description?: string;\n /**\n * Id of the element whose text is the dialog's accessible description, in\n * place of `description`, such as app-owned summary copy in `main`.\n */\n describedBy?: string;\n /** Groups after the title in the work area's toolbar. */\n leading?: KerfUiContent;\n /** Header controls before the close control, such as a `ToolbarControlGroup`. */\n actions?: KerfUiContent;\n /** The work area. */\n main: KerfUiContent;\n /** Fixed content over the work area's bottom toolbar, such as a status line. */\n mainFooter?: KerfUiContent;\n /** The work area's bottom toolbar, usually the dialog's buttons. */\n mainBottomToolbar?: WorkbenchMainBottomToolbar;\n /**\n * Side rails with their own toolbars. On compact devices each presents as\n * an overlay over the work area; the app starts it collapsed and its\n * `toolbar.toggle` shows it.\n */\n leftRail?: WorkbenchPanel;\n rightRail?: WorkbenchPanel;\n /** A bottom drawer under the work area. */\n bottomDrawer?: WorkbenchPanel;\n /**\n * Fill the screen with the rails as overlays: derive from\n * `deviceClass().value.compact`.\n */\n compact?: boolean;\n /** The close control's accessible name (default `Close`). */\n closeLabel?: string;\n /** Close on a press on the backdrop; otherwise the panel pulses. */\n lightDismiss?: boolean;\n /** Roomy preferred width preset (default `large`). */\n size?: DialogSurfaceSize;\n /** Roomy preferred width; takes precedence over `size`. */\n preferredWidth?: CssLength;\n /** Roomy definite height (default 560px), bounded by the viewport. */\n height?: DialogSurfaceHeight;\n /** Roomy height cap in place of a definite height. */\n maxHeight?: DialogSurfaceMaxHeight;\n /** Roomy edge clearance from the viewport. */\n viewportGutter?: CssLength;\n /** Minimum work-area width beside inline rails, in px (Workbench default 320). */\n mainMinSize?: number;\n rootAttributes?: WorkbenchDialogRootAttributes;\n className?: string;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\n/** A rail presents as an overlay in a compact dialog, as the app left it otherwise. */\nfunction compactRail(\n panel: WorkbenchPanel | undefined,\n compact: boolean,\n): WorkbenchPanel | undefined {\n return panel && compact ? { ...panel, presentation: 'overlay' } : panel;\n}\n\n/**\n * A workbench dialog: a modal whose body is a `Workbench`. The work area's\n * toolbar carries the dialog's primary title, its actions, and the close\n * control; side rails keep their own toolbars and run the dialog's full\n * height. On compact devices it fills the screen and its rails become\n * overlays over the work area, shown by their toolbar toggles, so the work\n * area keeps its width. Open it with `openDialog()`; the app owns each\n * rail's `collapsed` state, and `wireWorkbench` closes an overlay rail on\n * Escape or a press outside it before the dialog sees the Escape. Built from\n * `DialogSurface` and `Workbench`.\n */\nexport function WorkbenchDialog({\n id,\n label,\n title,\n description,\n describedBy,\n leading,\n actions,\n main,\n mainFooter,\n mainBottomToolbar,\n leftRail,\n rightRail,\n bottomDrawer,\n compact = false,\n closeLabel = 'Close',\n lightDismiss = false,\n size = 'large',\n preferredWidth,\n height,\n maxHeight,\n viewportGutter,\n mainMinSize,\n rootAttributes,\n className = '',\n slot,\n}: WorkbenchDialogProps) {\n return (\n <DialogSurface\n id={id}\n label={label}\n describedBy={\n describedBy ??\n (description === undefined ? undefined : `${id}-description`)\n }\n withoutHeader\n lightDismiss={lightDismiss}\n closeLabel={closeLabel}\n size={size}\n presentation={compact ? 'fullscreen' : 'modal'}\n preferredWidth={preferredWidth}\n height={\n compact\n ? undefined\n : maxHeight === undefined\n ? (height ?? remify(560))\n : height\n }\n maxHeight={compact ? undefined : maxHeight}\n viewportGutter={compact ? undefined : viewportGutter}\n bodyInset=\"none\"\n bodyLayout=\"fill\"\n rootAttributes={{\n ...rootAttributes,\n 'data-dialog-layout': 'workbench',\n }}\n className={className}\n slot={slot}\n >\n <Workbench\n id={`${id}-workbench`}\n label={label}\n mainToolbar={{\n label: title,\n title: (\n <ToolbarText\n text={title}\n description={description}\n descriptionId={\n description === undefined ? undefined : `${id}-description`\n }\n size=\"xlarge\"\n id={`${id}-title`}\n headingLevel={2}\n />\n ),\n leading,\n trailing: actions,\n // After any hidden right rail's toggle, so Close stays outermost.\n end: (\n <ToolbarControlGroup appearance=\"borderless\" single>\n <button\n type=\"button\"\n aria-label={closeLabel}\n data-dialog-close=\"\"\n >\n <LucideIcon icon={X} name=\"x\" />\n </button>\n </ToolbarControlGroup>\n ),\n }}\n main={main}\n mainFooter={mainFooter}\n mainBottomToolbar={mainBottomToolbar}\n leftRail={compactRail(leftRail, compact)}\n rightRail={compactRail(rightRail, compact)}\n bottomDrawer={bottomDrawer}\n mainMinSize={mainMinSize}\n />\n </DialogSurface>\n );\n}\n"]}
package/dist/workbench.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Workbench } from './chunk-DDSCVEO5.js';
1
+ export { Workbench } from './chunk-NLMEDZRM.js';
2
2
 
3
3
 
4
4
 
@@ -364,7 +364,13 @@ holds an optional icon `ToolbarControlGroup` and a direct extra-large
364
364
  `ToolbarText`; controls belong in a trailing `ToolbarControlGroup`. By default
365
365
  the title carries no native heading role, so for a **dialog or panel** the
366
366
  application connects the title id and optional supporting-copy id to the owning
367
- host through `aria-labelledby` and `aria-describedby`. For a **page or view**
367
+ host through `aria-labelledby` and `aria-describedby`. `DialogSurface` and
368
+ `PopoverDialogSurface` take these as `labelledBy` and `describedBy`, so the
369
+ supporting line (or app-owned summary copy in the body) is the dialog's
370
+ accessible description; `WorkbenchDialog` wires its title `description`
371
+ automatically (`<id>-description`), and `ListDetailDialog` takes an explicit
372
+ `describedBy`, since its detail description describes the selection, not the
373
+ dialog. For a **page or view**
368
374
  title, pass `headingLevel` (usually `1`) to `ToolbarText`: it then exposes
369
375
  `role="heading"` with a matching `aria-level`, giving the view a heading landmark
370
376
  so screen-reader heading navigation and "main heading" semantics work — the same
@@ -542,6 +548,32 @@ disabled or read-only field's clear button is disabled. `showCount` (with
542
548
  `wireTextFields` keeps current. Compact text grows to 16px on coarse-pointer
543
549
  touch screens so focusing a field never zooms the page in iOS.
544
550
 
551
+ ## Button and SplitButton focus
552
+
553
+ `Button` and each `SplitButton` half draw the 3px focus ring 1px outside the
554
+ box, against the surrounding surface, because a loud fill can share the ring's
555
+ hue (the brand fill matches the focus color in both schemes). An ancestor that
556
+ clips overflow would crop that ring, so `focusRingInset` draws it inside the
557
+ box instead, the name and meaning `Select` uses. An inset ring alone would
558
+ touch the fill (1.0:1 on brand, at most 2.3:1 on any loud fill), so the inset
559
+ ring is two-tone: the ring color on the outer 3px and a 3px surface-colored
560
+ line inside it. The ring then meets a surface color on both edges and clears
561
+ 3:1 against each on every tone (about 4.0:1 in light, 9.9:1 in dark against
562
+ the line), whatever the fill. Forced-colors mode drops the line and keeps the
563
+ system ring.
564
+
565
+ The line paints the surface the control sits on, not always the base surface:
566
+ a surface that changes the background behind its children publishes the
567
+ surface context (`--kui-color-surface-current`, `-region`, and `-tint`; see
568
+ `docs/component-contract.md`), and the line layers it the same way the page
569
+ does. On a sunken panel, a quiet `ContentItem`, a lowered `FramedList`, a
570
+ raised `List`, popup, or dialog, or a sunken Pane, Workbench area, or
571
+ TabNavigator scene, the line matches the pixels outside the box, so it reads
572
+ as a cut through the fill instead of a light halo. On a doubly sunken surface
573
+ the brand ring still clears 3:1 against the line (about 3.2:1 in light, 6.9:1
574
+ in dark). A sunken surface's grain is not part of the line, and a translucent
575
+ glass material, which has no single color, falls back to the base surface.
576
+
545
577
  ## Checkbox, RadioGroup, and Switch
546
578
 
547
579
  Each control is a native input inside its `<label>`, positioned invisibly over
@@ -563,6 +595,13 @@ forced-colors mode paints checked state with `Highlight`.
563
595
  its `aria-description`; `error` sets `aria-invalid` on the group, `required`
564
596
  sets native `required` on each radio plus `aria-required` on the group, and a
565
597
  choice's `hint` is that radio's `aria-description`.
598
+ - `CheckboxGroup` is a `<fieldset>` (natively a group) of `Checkbox`es, named
599
+ by its `<legend>` (`label`), `ariaLabel`, or `labelledBy`. Each checkbox
600
+ is its own Tab stop and Space toggles it; every checked value submits under
601
+ `name`. The group's `hint` and `error` form its `aria-description`; `error`
602
+ sets `aria-invalid` on the group and the danger border on each box, and a
603
+ choice's `hint` is that checkbox's `aria-description`. `disabled` disables
604
+ the fieldset and so, natively, every checkbox in it.
566
605
  - `Switch` is an `<input type="checkbox" role="switch">`, so assistive
567
606
  technology announces on and off; Space toggles it. Its `hint` is its
568
607
  `aria-description`. The thumb's slide is skipped when reduced motion is
@@ -26,7 +26,12 @@ help that leaves focus on its trigger. Escape and an outside press dismiss it;
26
26
  `placement` (`bottom` or `top`), `align` (`start` or `end`), `gap`, and
27
27
  `viewportMargin` feed the shared `kerfjs/overlay` positioning core: it flips
28
28
  vertically when the other side fits, keeps the surface `viewportMargin` (default
29
- 8px) inside the viewport edges, and follows scroll and resize. The surface fits
29
+ 8px) inside the viewport edges, and follows scroll, resize, and changes to the
30
+ anchor's own size. Anchor help for a field to the whole field rather than to a
31
+ button inside it: the surface then clears every line of a field that wraps, and
32
+ moves as the field grows (a SearchField's trailing help action opens beside
33
+ `[data-component="search-field"]`, which also keeps typing in the field from
34
+ dismissing it). The surface fits
30
35
  its height to the room beside the anchor: when neither side holds it, it opens
31
36
  on the roomier side, caps its height to that room, and scrolls its content.
32
37
  The room is exposed to the content as `--kui-anchored-surface-available-height`
package/docs/catalog.md CHANGED
@@ -219,7 +219,32 @@ for a control whose own inline padding contributes about 8px, and
219
219
  8px inset (such as a plain `PopupMenu` trigger), so its text lines up with the
220
220
  example label past the trigger's own padding and border. Use
221
221
  `align="none"` (the default) for a content item or composition that owns its
222
- geometry.
222
+ geometry. Every form field specimen (TextField, Select, ColorField, TagField,
223
+ Slider, Switch, Checkbox, RadioGroup, CheckboxGroup, ColorSwatches) uses
224
+ `inline-control`, so field borders and label text share one column across the
225
+ catalog; a field that already insets itself by 8px (Combobox) stays at `none`.
226
+ A field is a control a person fills in or picks a value with inline, swatch
227
+ pickers included. A field composition (fields in a `Grid` or a form `List`)
228
+ uses `inline-control` too. A width-sized content specimen — one that fills a
229
+ sized `viewport` or its container rather than its content, such as a
230
+ ProgressBar, a group-title `Text`, or Web Awesome's Progress Bar and Skeleton —
231
+ joins the same column with `inline-control`, so a bar's label lines up with the
232
+ example label and note text. Web Awesome integration specimens follow their
233
+ Kerf counterpart's demo: controls and control compositions (Button, Button
234
+ Group, Dropdown, Dropdown Item, the Tab family, Badge, Tag, Avatar, Progress
235
+ Ring, Toast and Toast Item, the dialog, drawer, popover, and tooltip triggers,
236
+ Animation, and the intersection, mutation, and resize observer compositions)
237
+ use `inline-control`, as the Kerf Button demo does, so the control box sits on
238
+ the 8px column; a specimen led by bare text or a bare glyph (Copy Button,
239
+ Breadcrumb and Breadcrumb Item, Divider, Markdown, Relative Time, Pagination's
240
+ summary, Stepper and Step markers, Spinner, Icon) uses `glyph`, as Kerf's flush
241
+ `Text`, `LoadingSpinner`, and `LucideIcon` demos do, so its text lands on the
242
+ text column instead of flush at the example's edge. A panel — a multi-part picker or
243
+ a drop zone such as ColorPicker and FilePicker, or a framed specimen such as
244
+ Web Awesome's Zoomable Frame — owns its geometry and stays flush at `none`, as
245
+ does an edge-to-edge composition (a toolbar, tab bar, or pane). A sized
246
+ `viewport` takes the alignment offset out of its width, so it still ends at the
247
+ example's inline-end edge.
223
248
 
224
249
  ```tsx
225
250
  import { CatalogExample, CatalogExampleStack } from "@kerfjs/ui/catalog";
@@ -3,8 +3,15 @@
3
3
  `@kerfjs/ui/color-picker` is a full inline color picker panel:
4
4
 
5
5
  - a saturation and brightness area;
6
- - a hue slider and, optionally, an opacity slider on a checkerboard;
7
- - a color-model menu (RGB, HSL, HSB, Grayscale) with its channel fields;
6
+ - a slider for each channel of the color model — red, green, and blue (RGB);
7
+ hue, saturation, and lightness (HSL); hue, saturation, and brightness
8
+ (HSB); or one gray level (Grayscale) — each track painting the colors that
9
+ channel produces while the others hold, except the hue track, which always
10
+ shows the full-saturation, full-brightness rainbow so a dark or gray color
11
+ still shows every hue; and, optionally, an opacity slider on a
12
+ checkerboard;
13
+ - a color-model menu (RGB, HSL, HSB, Grayscale) that switches the channel
14
+ sliders and fields together;
8
15
  - a hex field (8 digits below full opacity) and an opacity field;
9
16
  - a screen sampler where the browser offers the EyeDropper API;
10
17
  - optional preset and sample swatches.
@@ -28,7 +35,8 @@ const stop = wireColorPickers(root, {
28
35
  - `id` and `label` (required): identity and the picker's accessible name.
29
36
  - `value` (required): CSS color text (`#rrggbb[aa]`, `rgb()`, `hsl()`);
30
37
  invalid text shows black.
31
- - `mode`: `rgb` (default), `hsl`, `hsb`, or `gray` — which channel fields show.
38
+ - `mode`: `rgb` (default), `hsl`, `hsb`, or `gray` — which channel sliders
39
+ and fields show.
32
40
  - `alpha` (default true): the opacity slider and field. Without it the value
33
41
  is always opaque.
34
42
  - `name`, `form`: a hidden input submits the hex value.
@@ -49,7 +57,9 @@ picker with a new `value` (or `mode`), the picker adopts it.
49
57
 
50
58
  - **The area** takes pointer drags and the arrow keys (Shift for 10% steps);
51
59
  Page Up and Down change brightness, Home and End saturation.
52
- - **Hue and opacity** are native range inputs.
60
+ - **Channel and opacity sliders** are native range inputs (arrow keys, Page
61
+ Up and Down, Home and End). Each changes only its own channel; a slider and
62
+ the field of the same channel always agree.
53
63
  - **Fields** commit on Enter or blur; Escape restores the shown value; invalid
54
64
  text restores it too.
55
65
  - **Swatches** (presets and samples) apply their color.
@@ -59,13 +69,34 @@ Each change dispatches a bubbling `kui-color-input` (live) or
59
69
  `onInput` or `onChange`. The value is `#rrggbb`, or `#rrggbbaa` below full
60
70
  opacity. The picker keeps hue, saturation, and brightness as its own state,
61
71
  so a hue survives while the color is gray, and conversions round-trip without
62
- drift.
72
+ drift: switching models never changes the color, and a channel edit keeps
73
+ every other channel's exact value (an HSL saturation survives lightness
74
+ reaching black or white). The 2D area stays in every model as the primary
75
+ picking surface; the sliders are the precise, per-channel controls.
63
76
 
64
77
  ## Accessibility and geometry
65
78
 
66
79
  The picker is a named group. The area is a slider named "Saturation and
67
- Brightness" whose spoken value gives both; every field is named (Red, Hue,
68
- Hex Color, Opacity). The panel takes the shared block-content inset, fills
69
- its container up to 320px, and spaces its rows 12px apart.
80
+ Brightness" whose spoken value gives both. Each channel slider is named for
81
+ its channel (Red, Hue, Lightness, Gray Level) and speaks its unit (`210°`,
82
+ `40%`, or a plain 0–255 byte); the opacity slider speaks a percentage. Every
83
+ field is named the same way (Red, Hue, Hex Color, Opacity). The panel takes
84
+ the shared block-content inset, fills its container up to 320px, and spaces
85
+ its rows 12px apart; the sliders form one group 8px apart.
86
+
87
+ The saturation and brightness area and every slider track draw a 1px inner
88
+ edge halfway between the text color and the color under it, so an area or a
89
+ track as dark as a dark surface (the area's black bottom, a hue at low
90
+ brightness, a faint opacity) or as light as a light one keeps a boundary of at
91
+ least 3:1 against the surface around it. Increased contrast draws the edge in
92
+ the full text color and forced colors in `CanvasText`.
93
+
94
+ Every thumb draws a two-tone ring: a white border inside a dark outline. The
95
+ white holds at least 3:1 against a dark color and the outline against a light
96
+ one, so a thumb stays findable wherever it sits.
97
+
98
+ Every track runs in the writing direction, as the native range does: in
99
+ right-to-left text the gradient starts at the right, so the thumb at a
100
+ channel's minimum (or at no opacity) sits over the color it sets.
70
101
 
71
102
  Use `ColorSwatches` for a small fixed palette.
@@ -65,6 +65,14 @@ diagnostic even when both the wrapper and a rendered root reject that parent.
65
65
  When one catalog entry exports another JSX component with different placement,
66
66
  `jsxExports` gives that export its own parent contract. The extra export does
67
67
  not stand for the entry's root when a parent zone is checked.
68
+ An entry whose own geometry an owner already provides in some configuration
69
+ declares `insetFreeChildren`: `{ prop, equals }` conditions under which its
70
+ direct JSX children must bring no content-item inset (`Disclosure` with
71
+ `appearance="embedded"`). A child brings that inset when its
72
+ `layout.geometry` owns its margin, border, and padding (`self` on all three),
73
+ unless one of its own `insetFreeWhen` conditions holds (`Text` with `flush` or
74
+ `variant="span"`). `ui-composition` reports a child that statically brings the
75
+ inset as `KUI-L204` and names the child's inset-free form when it has one.
68
76
  Generated `boundaries.rootClass` prefers the public `kui-<entry-id>` class when
69
77
  present, so a sibling export's class cannot become the entry's root class.
70
78
 
@@ -411,6 +419,24 @@ a `Pane` overlay header, through `wireScrollDividers`, on its own Pane root).
411
419
  A scroll owner reads it only through its own `scrollClearance` prop, or, for an
412
420
  overlay-header Pane's content, through its own `headerPresentation`.
413
421
 
422
+ The surface context needs no exception either. A component that changes the
423
+ background behind its children publishes, on its own painted element, the
424
+ foundation tokens `--kui-color-surface-current` (the opaque surface beneath:
425
+ a raised `DialogSurface`, popover dialog, `AnchoredSurface`, or `List`
426
+ `surface`, and a sunken Pane's opaque auto chrome, set it and clear both
427
+ layers), `--kui-color-surface-current-region` (a layout region's translucent
428
+ fill: a sunken `Pane`, `Workbench` area, or `TabNavigator` scene), and
429
+ `--kui-color-surface-current-tint` (a content surface's translucent fill: a
430
+ `SunkenPanel`, a sunken `Disclosure`, a lowered `FramedList`, a quiet,
431
+ hovered, pressed, or selected `ContentItem`). They are undeclared at `:root`
432
+ and a scoped `[data-kui-tint]` resets them. A control that must match what
433
+ lies behind it (the inset focus ring's inner line on `Button` and
434
+ `SplitButton`) layers tint over region over the opaque color, falling back to
435
+ `--kui-color-surface`. Custom properties cannot fold an inherited value into a
436
+ new one, so a content surface inside a sunken region composes exactly while a
437
+ third translucent level keeps only the innermost content tint. An application
438
+ surface that paints its own background sets the same tokens on its own root.
439
+
414
440
  Applications and component packages built on the package follow the same
415
441
  rules, and the downstream tools enforce them against the composition catalog
416
442
  instead of package source: `kerf-ui-analyze` / `kerf-ui-doctor` report a rule
@@ -624,14 +650,19 @@ compact segment never becomes an oval. An icon-only `Select` joins a group as
624
650
  one more segment with its siblings' inset, radius, hover, and focus ring; only a
625
651
  lone Select grows the group to its icon-and-caret pill.
626
652
 
627
- Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. Keep popup sizing and position repairs out of consumer CSS.
653
+ Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. A listbox (like a PopupMenu menu) shifts only along its trigger's edge, never over the trigger: when neither side fits, it opens on the side with more room and scrolls inside its capped height, so a second press on the trigger still closes it. Keep popup sizing and position repairs out of consumer CSS.
628
654
 
629
655
  For a rail view heading, use `Select presentation="title"`: its intrinsic
630
656
  36px borderless trigger has a bold large label flush with its leading edge and
631
657
  a caret beside that label. Set `focusRingInset` when a NavStack or panel clips
632
658
  outside painting; the Select keeps its focus outline 3px inside the trigger
633
659
  and drops it while the listbox is open. Both settings belong to Select, not an
634
- application `::part()` override.
660
+ application `::part()` override. `Button` and `SplitButton` take the same
661
+ `focusRingInset` for a clipping ancestor: the ring moves inside the box (each
662
+ half's, for a SplitButton) with a line inside it in the color of the surface
663
+ it sits on (the surface context), so it stays visible on a loud fill of its
664
+ own hue without a halo on a sunken or tinted surface. Never re-offset their outline from
665
+ application CSS.
635
666
 
636
667
  Set `menuInset` to configure the menu surface
637
668
  without application `::part(menu)` CSS; the default `standard` keeps each menu's
@@ -259,7 +259,7 @@ beside a `ContentItem` value, and connect its id to the preview group's
259
259
  | Modal dialog, dialog or popup geometry — `DialogSurface`, `openDialog`, `closeDialog`, `PopoverDialogSurface`, `PopupSurface` | A task must block the page in a modal dialog with standard sizes/presentations/body/footer insets, typed modal viewport bounds, a definite height, or a body one child fills; a dialog must stay non-modal and light-dismiss; or a raw dropdown menu needs list-compatible inset ownership. | Do not use these surfaces as generic layout. Do not toggle a `DialogSurface`'s `open` attribute (that opens it non-modally), avoid consumer `::part()` or descendant overrides, and do not stack a dialog body inset around list-owned child geometry. | Render `DialogSurface` with a `label` (or `labelledBy` plus a `heading`), the body as children, and its buttons as `footer`; open it with `openDialog()` and close it with `closeDialog()`, its close button, or a `data-dialog-close` element, and cancel `kui-request-close` to keep it open. Compose a dialog body as a `List` by default, use `bodyInset="none"` when its children own list geometry, and wrap bare prose in `ListInsetText`. Set `height` with `bodyLayout="fill"` when one `SplitView` or `Pane` should fill the body. For a non-modal, light-dismiss dialog render `PopoverDialogSurface` (native `popover="auto"`, opened by a `popoverTarget` control or `showPopover()`), never a hand-built popover with app chrome CSS; `presentation="embedded"` shows it in flow. Wrap a raw `wa-dropdown` in `PopupSurface` for its menu inset. | Open state, whether a close may proceed, dismissal policy, names, content, and outer placement. | `@kerfjs/ui/surface-scaffold` | [Surface scaffolds](./surface-scaffold.md) |
260
260
  | Modal list-detail — `ListDetailDialog` | A modal task picks one record from a list and shows its detail beside it, with the dialog's title, actions, and close control belonging to the detail. | Do not use it for a single-pane dialog; use `DialogSurface`. Do not wrap a `SplitView` in a `DialogSurface` by hand for this layout: the surface header spans both columns and stops the divider below it. | Pass `list` (`title`, `toolbar`, `content`, `footer`, `width`) and `detail` (`title`, `description`, `actions`, `content`, `footer`); open it with `openDialog()`; pass `compact` from `deviceClass()` and `detailActive` for the compact drill-down, and pop on `nav-back`. | Open state, selection, the pushed detail, device policy, content, and record actions. | `@kerfjs/ui/list-detail-dialog` | [ListDetailDialog](./list-detail-dialog.md) |
261
261
  | Modal workbench — `WorkbenchDialog` | A modal editing task needs a work area with side panels (layers, an inspector) whose rails overlay the work area on compact devices. |
262
- | Show and hide content — `Disclosure` | Optional or advanced content should stay one press away under a summary (advanced settings, a build log), or several sections should open one at a time. | Do not use it to switch peer views (use `SegmentedControl` or `TabBar`) or for a collapsible rail (use `CollapsiblePanel`). Do not hand-build a details element with its own marker CSS. | Render `Disclosure` with a `title`, an optional `detail`, and content that draws no inset of its own; give Disclosures the same `name` for an accordion; pick `appearance` `plain`, `framed`, or `sunken`. The open state is native: close it from code with `element.open = false`, and listen for the captured `toggle` event through `toggleAction`. | Title, content, initial open state, group name, and toggle handling. | `@kerfjs/ui/disclosure` | [Disclosure](./disclosure.md) |
262
+ | Show and hide content — `Disclosure` | Optional or advanced content should stay one press away under a summary (advanced settings, a build log), or several sections should open one at a time. | Do not use it to switch peer views (use `SegmentedControl` or `TabBar`) or for a collapsible rail (use `CollapsiblePanel`). Do not hand-build a details element with its own marker CSS. | Render `Disclosure` with a `title`, an optional `detail`, and content that draws no inset of its own; give Disclosures the same `name` for an accordion; pick `appearance` `plain`, `framed`, or `sunken`, or `embedded` inside a `ContentItem` that already owns the inset (no margin, border, or padding of its own, a compact summary row, and `flush` `Text` content). The open state is native: close it from code with `element.open = false`, and listen for the captured `toggle` event through `toggleAction`. | Title, content, initial open state, group name, and toggle handling. | `@kerfjs/ui/disclosure` | [Disclosure](./disclosure.md) |
263
263
  | Core overlay bottom sheet — `@kerfjs/ui/overlay-sheet.css`, `wireOverlaySheets` | A `kerfjs/overlay` `overlay()`, `confirm()`, `prompt()`, `form()`, or `choice()` opened with `presentation: 'bottom-sheet'` should look like a `DialogSurface` sheet, and optionally be resizable by the person. | Do not hand-style `.kerf-overlay` or build a grabber; prefer a `DialogSurface` `presentation="bottom-sheet"` when the dialog is a Kerf `DialogSurface` rather than a core overlay. | Import `@kerfjs/ui/overlay-sheet.css` once. Pass `exitClass: 'kui-overlay-sheet-exit'` for the slide out. For a resizable sheet, add the `kui-overlay-sheet-resizable` class (`className: 'kerf-overlay kui-overlay-sheet-resizable'`) and call `wireOverlaySheets()` once per container sheets open in (`document.body` by default). | Opening, results, content, and which sheets are resizable. | `@kerfjs/ui/overlay-sheet.css`, `@kerfjs/ui/wire-overlay-sheets` | [Core overlay bottom sheets](./surface-scaffold.md#core-dialogs-as-bottom-sheets) |
264
264
  | Key/value facts — `ValueTable`, `ValueTableRow` | Read-only labels and values form a semantic definition list, optionally with a leading icon. | Do not use it for editable form fields or a row/column data grid; use native form or table semantics. | Compose typed `ValueTableRow` entries; pass `icon` when a 24px leading icon adds useful context. Set `labelWidth` for a minimum label column, `presentation="stacked"` to put each value under its label, or `stackBelow` (`compact` 512px, `narrow` 576px) to stack only while the table is that narrow; never override `--kui-value-table-row-columns` for these. | Values, formatting, icon meaning, and empty/loading policy. | `@kerfjs/ui/value-table` | [Component ownership](./component-contract.md#ownership-boundaries) |
265
265
  | Framed rows of arbitrary content — `FramedList`, `FramedListRow` | Settings rows, account or source cards, or a shortcut list group several rows of copy and controls in one rounded frame with dividers between them. | Do not use it for read-only label/value facts; use `ValueTable`. Do not use it for selectable navigation rows or a bounded result picker; use `List` with `ListItem` (a picker adds `frame`, `scrollable`, and `maxHeight`). Do not pad or border rows from application CSS. | Compose any row content (a `Row` of copy, a keycap, icon-only `Button`s) in each `FramedListRow`; mark the row being edited `highlighted`. Name the list with `label` when no heading does. | Row content, controls, which row is highlighted, and the list's name. | `@kerfjs/ui/framed-list` | [Feedback accessibility](./accessibility.md#shared-rules) |
@@ -273,15 +273,16 @@ beside a `ContentItem` value, and connect its id to the preview group's
273
273
  | Indeterminate activity — `LoadingSpinner` | A Kerf surface needs compact, labeled or decorative indeterminate progress. | Do not use it for known progress; use a native `<progress>` element, or `wa-progress-bar` / `wa-progress-ring` in an application using the Web Awesome integration. Do not mix spinner systems within one surface. | Pass a label when the spinner conveys status; use size for a named icon step or positive pixel size. | Loading lifecycle and adjacent status copy. | `@kerfjs/ui/loading-spinner` | [Accessibility](./accessibility.md#shared-rules) |
274
274
  | Loading placeholder — `Skeleton` + a component's `placeholder` prop | A value or a whole component is still loading and should hold its space as a subtle, unanimated block, keeping the layout stable — an inspector or detail view rendering its real chrome with per-record values absent. | Do not use it for known progress (use `LoadingSpinner`), do not animate it, and do not hand-rebuild a component's empty state — set `placeholder` on the component instead. Prefer it over `wa-skeleton`. | Set `placeholder` on a value-bearing component (`Select`, `ListHeader`, `ListItem`, `ValueTableRow`, `SegmentedControl`, `StateBanner`, `TabBarItem`, `ToolbarText`, `ListActionRow`) to render skeletons in its value slots with interactivity disabled; use the standalone `Skeleton` for a custom slot. | Loading lifecycle, which slots are unknown, and announcing the loading region. | `@kerfjs/ui/skeleton` | [Accessibility](./accessibility.md#shared-rules) |
275
275
  | Command menu — `PopupMenu` | A short list of commands sits behind one trigger, such as a toolbar sort or More menu, optionally grouped under headings and dividers with item icons. | Do not use it to choose a persistent value; use `Select`. Do not hand-write `wa-dropdown`/`wa-dropdown-item` markup for an action menu. | Import `@kerfjs/ui/popup-menu/register` once. Place it in a `single` `ToolbarControlGroup` with `nestedDropdown` and a `menuInset`, or wrap it in `PopupSurface` outside a toolbar. Handle item `action`s by delegation. | Commands, enabled state, and reactions to opening or closing. | `@kerfjs/ui/popup-menu` | [Toolbar composition](#toolbar-composition) |
276
- | Standalone action — `Button` | A dialog footer, a form, or content needs an action button with visible text: `tone="brand"` for the main action, `tone="danger"` for a destructive one, and `emphasis="secondary"` or `"plain"` for the quieter actions beside it. A link that looks like a button takes `href`. | Do not use it inside a `ToolbarControlGroup`; give the group plain `<button>`s. For an icon-only action give it an `icon` and `ariaLabel` and no label, and for an on/off toggle set `pressed`; a menu trigger is a `PopupMenu`. | Pass the label as children and delegate its `action`; a form button sets `type="submit"` or `"reset"` (the default `button` never submits) with `name`, `value`, and `form`. `loading` shows a spinner and `aria-busy`; the app decides what a press does meanwhile. `size="compact"` is 33px. In a `Toolbar` trailing zone or a `TabBar` action zone it keeps the control band. | The label, delegated action, and form ownership. | `@kerfjs/ui/button` | [Feedback accessibility](./accessibility.md#shared-rules) |
276
+ | Standalone action — `Button` | A dialog footer, a form, or content needs an action button with visible text: `tone="brand"` for the main action, `tone="danger"` for a destructive one, and `emphasis="secondary"` or `"plain"` for the quieter actions beside it. A link that looks like a button takes `href`. | Do not use it inside a `ToolbarControlGroup`; give the group plain `<button>`s. For an icon-only action give it an `icon` and `ariaLabel` and no label, and for an on/off toggle set `pressed`; a menu trigger is a `PopupMenu`. | Pass the label as children and delegate its `action`; a form button sets `type="submit"` or `"reset"` (the default `button` never submits) with `name`, `value`, and `form`. `loading` shows a spinner and `aria-busy`; the app decides what a press does meanwhile. `size="compact"` is 33px. In a `Toolbar` trailing zone or a `TabBar` action zone it keeps the control band. Set `focusRingInset` when an ancestor clips overflow: the focus ring moves inside the box, with a surface-colored line inside it so it stays visible on a loud fill. `fill` spans a full-width primary action across its parent's inline width, label centered, as on `SplitButton`. | The label, delegated action, and form ownership. | `@kerfjs/ui/button` | [Feedback accessibility](./accessibility.md#shared-rules) |
277
277
  | Icon-only action — `Button` with `icon` and `ariaLabel` | One action is clear from its icon alone and stands by itself or beside a form field, such as Browse next to a path input: give `Button` an `icon` and an `ariaLabel` and no label. |
278
278
  | Standalone toggle — `Button` with `pressed` | An on/off control (show a panel or a conversation), icon-only or labeled, sits in content or a pane footer outside any Toolbar: `pressed` reports `aria-pressed` and draws the pressed look on top of the tone and emphasis; `shape="pill"` rounds it. |
279
- | Primary action with options — `SplitButton` | A prominent primary action has a closely related set of options chosen from an attached trigger, such as Drive with a provider menu. | Do not use it for a plain action; use `Button`. Do not use it for a toolbar command menu; use `PopupMenu`. Do not assemble one from `wa-button-group`, whose hosts do not forward names or `aria-expanded`. | Pass the primary `label` and `action`, the options trigger's `optionsLabel`, `optionsAction`, and `optionsExpanded`; pick a loud `tone`, `fill` for full width, `busy` while work runs, and `disabled`/`optionsDisabled`. | Both actions, the options menu and its open state, the running state and tone switch, and copy. | `@kerfjs/ui/split-button` | [Feedback accessibility](./accessibility.md#shared-rules) |
279
+ | Primary action with options — `SplitButton` | A prominent primary action has a closely related set of options chosen from an attached trigger, such as Drive with a provider menu; `shape="pill"` rounds both outer ends, as on `Button`. | Do not use it for a plain action; use `Button`. Do not use it for a toolbar command menu; use `PopupMenu`. Do not assemble one from `wa-button-group`, whose hosts do not forward names or `aria-expanded`. | Pass the primary `label` and `action`, the options trigger's `optionsLabel`, `optionsAction`, and `optionsExpanded`; pick a loud `tone`, `fill` for full width, `busy` while work runs, and `disabled`/`optionsDisabled`. Set `focusRingInset` when an ancestor clips overflow, so each half's ring is drawn inside its box. | Both actions, the options menu and its open state, the running state and tone switch, and copy. | `@kerfjs/ui/split-button` | [Feedback accessibility](./accessibility.md#shared-rules) |
280
280
  | Choosing or dropping files — `FilePickerTrigger`, `FileDropZone`, `wireFilePickers` | People attach or import files by browsing or by dropping them onto a target. | Do not style a native file input or its label from app CSS. Do not make the drop zone the only way in; it also opens the chooser. | Use `FilePickerTrigger` for a compact Add control and `FileDropZone` for a dashed target; set `accept`, `multiple`, `name`, and `pickerId`. Call `wireFilePickers(root, { onFiles })` once: it reports browsed and dropped files (drops filtered by `accept`, the same check `fileMatchesAccept` exposes) and marks the zone while dragging. | What happens to the files, the chosen-file list, copy, and accepted types. | `@kerfjs/ui/file-picker`, `@kerfjs/ui/wire-file-pickers` | [Feedback accessibility](./accessibility.md#shared-rules) |
281
281
  | Value selection — `Select` | A controlled form, toolbar, or compact navigation value comes from a moderate or long choice list, possibly grouped, icon-bearing, icon-only when closed, or truncating. Set `multiple` with an array `value` when the person may choose any number of them; add `selectedPresentation="icon-only"` and a fixed `triggerIcon` for a toolbar filter menu. | Do not use it for commands; use `PopupMenu`. Do not use it for a small visible choice set; use `SegmentedControl`. Prefer it over direct `wa-select`, `wa-option`, or value-like `wa-dropdown`/`wa-dropdown-item` composition. | Import `@kerfjs/ui/select/register` once for registration and current-request animation ownership; listen for standard input/change events. Do not add consumer popup timing or positioning repairs. Use typed presentation props rather than consumer `::part()` overrides; let the parent own placement. | Controlled value, validation, choices, domain mapping, and outer placement. | `@kerfjs/ui/select` | [Web Awesome integration](../README.md#web-awesome-theme) |
282
282
  | Free text — `TextField`, `wireTextFields` | A form or dialog collects a line of free text (a name, an address, a title) or a longer note: `label` names it (or `ariaLabel` when nearby content already does), `hint` explains it, and `error` reports a validation problem. `multiline` with `rows` (and `maxRows` to grow) takes a longer note; `maxLength` with `showCount` shows the remaining budget; `icon` and `clearable` suit a filter field. | Do not use it when only existing values are allowed; use `Select`. Do not use it to pick among suggestions or create tags; use `Combobox`. Do not use it for search text with removable filter tokens (`SearchField`) or a message with a send action (`MessageComposer`). Prefer it over `wa-input` and `wa-textarea`. | Call `wireTextFields(root)` once when a field is `clearable` or `showCount`. Read the value from the native `input` or `change` event. | Value, validation and error copy, hint copy, and what a cleared field means. | `@kerfjs/ui/text-field` | [TextField contract](./accessibility.md#textfield) |
283
- | On/off option — `Checkbox`, `wireCheckboxes`, `Switch` | A form or settings list has an independent yes/no option: `Checkbox` when a form submits it (with `indeterminate` for a parent of partly checked children), `Switch` when the setting applies the moment it changes. | Do not use a `Switch` for an option a form submits later, or a `Checkbox` for an immediate setting. Do not use either for one choice among several (`RadioGroup`). Prefer them over `wa-checkbox` and `wa-switch`. | Call `wireCheckboxes(root)` once when a `Checkbox` is `indeterminate`. Read `checked` from the native `change` event. | Checked and mixed state, validation copy, and what a change does. | `@kerfjs/ui/checkbox`, `@kerfjs/ui/switch` | [Checkbox, RadioGroup, and Switch contract](./accessibility.md#checkbox-radiogroup-and-switch) |
283
+ | On/off option — `Checkbox`, `wireCheckboxes`, `Switch` | A form or settings list has an independent yes/no option: `Checkbox` when a form submits it (with `indeterminate` for a parent of partly checked children), `Switch` when the setting applies the moment it changes. | Do not use a `Switch` for an option a form submits later, or a `Checkbox` for an immediate setting. Do not use either for one choice among several (`RadioGroup`); a labeled set of related checkboxes is a `CheckboxGroup`. Prefer them over `wa-checkbox` and `wa-switch`. | Call `wireCheckboxes(root)` once when a `Checkbox` is `indeterminate`. Read `checked` from the native `change` event. Stack several standalone Checkboxes in a `List gap="xs"`: 8px apart, the same 28px pitch as `RadioGroup` choices; a labeled set of options is a `CheckboxGroup`. | Checked and mixed state, validation copy, and what a change does. | `@kerfjs/ui/checkbox`, `@kerfjs/ui/switch` | [Checkbox, RadioGroup, and Switch contract](./accessibility.md#checkbox-radiogroup-and-switch) |
284
284
  | One of a few labeled options — `RadioGroup` | A form asks for one of two to about six options that people should compare at a glance, each with a readable label and an optional hint. | Do not use it for a long list (`Select`), a view switch (`SegmentedControl`), or pill or color choices (`ChoiceChips`). Prefer it over `wa-radio-group`. | Read the value from the native `change` event and render `value`. | Value, choices and their copy, and validation. | `@kerfjs/ui/radio-group` | [Checkbox, RadioGroup, and Switch contract](./accessibility.md#checkbox-radiogroup-and-switch) |
285
+ | Several of a few labeled options — `CheckboxGroup` | A form asks for any number of two to about eight related options under one label, each with a readable label and an optional hint, and every checked value submits under one name. | Do not use it for one choice among several (`RadioGroup`), one independent option or a mixed parent (`Checkbox`), an immediate setting (`Switch`), or wrapping pill choices (`ChoiceChips` `multiple`). Prefer it over `wa-checkbox-group`. | Pass the checked values as `value` and read each change from the native `change` event. Its choices stack like `RadioGroup` choices: 8px apart, the same 28px pitch. Validate a minimum or maximum in the app and pass `error`. | Checked values, choices and their copy, and validation. | `@kerfjs/ui/checkbox-group` | [Checkbox, RadioGroup, and Switch contract](./accessibility.md#checkbox-radiogroup-and-switch) |
285
286
  | Pick or create a value by typing — `Combobox`, `wireComboboxes` | People type to find an existing value among many or create a new one (tags, labels, recipients). | Do not use it when only existing values are allowed; use `Select`. Do not use it for search text with removable filter tokens; use `SearchField`. Do not style a native input with a datalist. | Pass `id`, `label`, and `suggestions` (strings or `{ value, label }`), plus `placeholder`, `hint`, and `createLabel` (or `false` for suggestions only). Call `wireComboboxes(root, { onCommit })` once; it filters, moves the active option with the arrows, and reports `{ id, value, created, input }` on Enter, a comma, or a click. | Suggestions, committed values, normalization, and clearing the field. | `@kerfjs/ui/combobox`, `@kerfjs/ui/wire-comboboxes` | [Feedback accessibility](./accessibility.md#shared-rules) |
286
287
  | Small exclusive choice — `SegmentedControl` | A few visible choices switch a compact view or setting, with toolbar, rounded, or pill presentation. | Do not use it for tabpanel semantics; use `TabBar`. Do not use it for many choices; use `ChoiceChips` when they stay visible and wrap, or `Select` for a long list. Prefer it over `wa-button-group` when the controls select one value. | Delegate its action, read `data-segment-value`, update `value`, and rerender. | Controlled value, labels, action, and persistence. | `@kerfjs/ui/segmented-control` | [SegmentedControl contract](./accessibility.md#segmentedcontrol) |
287
288
  | Wrapping visible choice — `ChoiceChips` | Several visible options, too many for one segmented track, must wrap onto more rows — a labeled color palette with swatches, a tag color, a category. Set `multiple` with an array `value` when any number may be on, such as label filters or annotation intents. | Do not use it for a few compact choices on one track; use `SegmentedControl`. Do not use it for a long list or tight space; use `Select`. Do not use it for removable tags (`Chip`). Prefer it over `wa-radio-group` with `appearance="button"` radios, which wrap as a ragged joined group. | Pass a form `name`, a `label` or `labelledBy`, and the controlled `value`; delegate the `action` on `change`, read the radio `value` (or, with `multiple`, the checked boxes under `name`), and rerender. Place it in a dark appearance scope for inverse chrome rather than restyling it. Lead choices with a `swatch` (`CssColor` or `'transparent'`) or an `icon`. | Controlled value, choices, swatch colors, change handling, and form submission. | `@kerfjs/ui/choice-chips` | [ChoiceChips contract](./accessibility.md#choicechips) |
@@ -765,15 +766,22 @@ belongs to — never inside an app wrapper with `position: fixed`, a 1px size,
765
766
  or a `z-index` (the open menu is in the browser top layer). In the app's
766
767
  `contextmenu` handler, prevent the browser menu, store
767
768
  `{ x: event.clientX, y: event.clientY }` in the signal the menu renders as
768
- `anchor`, and call `openPopupMenu` with the menu. Because the point is part of the
769
+ `anchor`, and call `openPopupMenu` with the menu. Its `placement` applies at
770
+ the point: the default `bottom-start` opens below it, and `top-start` or
771
+ `top-end` opens above it (for a menu anchored to a footer or composer), still
772
+ flipping when that side has no room. Because the point is part of the
769
773
  menu's own template, it survives re-renders and a rebuilt menu, so the app
770
774
  never re-applies it. `openPopupMenuAt` still opens a menu rendered without `anchor` at a given
771
- point. Call `closePopupMenu` for programmatic dismissal. A pointer-anchored menu
775
+ point, at its `placement` unless the call names one for that opening
776
+ (`openPopupMenuAt(menu, x, y, { placement: 'top-start' })`). Call `closePopupMenu` for programmatic dismissal. A pointer-anchored menu
772
777
  keeps the same collision placement, focus management, submenu keyboard
773
778
  navigation, and outside/Escape dismissal as a trigger-anchored one.
774
779
 
775
780
  The menu opens in the browser top layer, flips and shifts at viewport edges,
776
- and limits its height to the available space, scrolling past it. The app
781
+ and limits its height to the available space, scrolling past it. It shifts
782
+ only along its trigger's (or point's) edge, never across it: when neither
783
+ side fits, it opens on the side with more room and scrolls, so a tall menu
784
+ never covers its trigger and a second press on the trigger closes it. The app
777
785
  supplies pointer coordinates and command policy; it does not need to calculate
778
786
  menu offsets. For other anchored content, use `AnchoredSurface` for help or
779
787
  popup content, a native `title` for a plain-text hint, or `kerfjs/overlay`'s
@@ -813,7 +821,10 @@ right-click, delegate `contextmenu` on the row's primary button, prevent the
813
821
  browser menu, and call `openPopupMenuAt` with the row's
814
822
  `[data-component="popup-menu"]` child and the pointer's `clientX` /
815
823
  `clientY`: a button-triggered menu opens `bottom-start` at the pointer for that
816
- opening and returns to its trigger once it closes.
824
+ opening and returns to its trigger once it closes. Pass a placement for that
825
+ opening as a fourth argument, `openPopupMenuAt(menu, x, y, { placement:
826
+ 'top-start' })`, to open it above the point instead (for a row near a footer
827
+ or composer); the viewport flip, shift, and height cap still apply.
817
828
 
818
829
  ```tsx
819
830
  <ListActionRow
@@ -1111,7 +1122,7 @@ component below, and file a component gap rather than dropping to the raw tag.
1111
1122
  | `wa-button`, `wa-dropdown`, `wa-dropdown-item` | Use `Button` (icon-only with `icon` and `ariaLabel`, or a toggle with `pressed`) for actions and `PopupMenu` for a command menu. Use `ListItem` for a navigation row and `Select` for a value. |
1112
1123
  | `wa-button-group` | Use `ToolbarControlGroup` or `SplitButton` for grouped actions and `SegmentedControl` for one-of-many selection. |
1113
1124
  | `wa-input`, `wa-textarea`, `wa-tag` | Use `TextField` for free text (`multiline` for a textarea) and `Chip` for removable or disabled tags; use `SearchField` only when text and ordered filter tokens form one editor. |
1114
- | `wa-checkbox`, `wa-checkbox-group`, `wa-switch` | Use `Checkbox` for a submitted option (a group of them in a `List`, or `ChoiceChips` `multiple: 'toggle'` for wrapping pills) and `Switch` for a setting that applies immediately. |
1125
+ | `wa-checkbox`, `wa-checkbox-group`, `wa-switch` | Use `Checkbox` for a submitted option (`CheckboxGroup` for a labeled set of them, or `ChoiceChips` `multiple: 'toggle'` for wrapping pills) and `Switch` for a setting that applies immediately. |
1115
1126
  | `wa-radio-group`, `wa-radio` | Use `RadioGroup` for a few labeled options, `ChoiceChips` for wrapping pills, and `SegmentedControl` for a view switch. |
1116
1127
  | `wa-select`, `wa-option` | Use `Select`, which owns Kerf spacing, controlled rendering, and form participation. |
1117
1128
  | `wa-dialog`, `wa-drawer` | Use `DialogSurface` with `openDialog` / `closeDialog` (`presentation="side-sheet"` or `"bottom-sheet"` for a drawer), or a `kerfjs/overlay` dialog. Use `CollapsiblePanel` for a rail that overlays content on compact devices. |
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 84" width="520" height="84"><defs><clipPath id="cp0"><rect x="174.9" y="34" width="16.1" height="16"/></clipPath><clipPath id="cp2"><rect x="198.9" y="33" width="106.3" height="18"/></clipPath><clipPath id="ct3"><rect x="198.9" y="33" width="106.3" height="18"/></clipPath></defs><rect x="20" y="20" width="440" height="44" rx="12" fill="rgb(100,210,255)"/><rect x="20.5" y="20.5" width="439" height="43" rx="11.5" fill="none" stroke="rgba(0,0,0,0)" stroke-width="1"/><g clip-path="url(#cp0)"><g color="rgb(17, 17, 19)"><clipPath id="svgic1"><rect x="174.9" y="34" width="16" height="16"/></clipPath><g clip-path="url(#svgic1)"><g transform="translate(174.86 34)scale(.66667)"><path d="M5 12h14" style="marker:none;vector-effect:none;color-interpolation:srgb" fill="none" stroke="rgb(17, 17, 19)" stroke-width="2px" stroke-dasharray="none" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="1" fill-opacity="1" opacity="1"/><path d="M12 5v14" style="marker:none;vector-effect:none;color-interpolation:srgb" fill="none" stroke="rgb(17, 17, 19)" stroke-width="2px" stroke-dasharray="none" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="1" fill-opacity="1" opacity="1"/></g></g></g></g><g clip-path="url(#cp2)"><g clip-path="url(#ct3)"><text x="198.86" y="48" font-family="system-ui, &quot;-apple-system&quot;, sans-serif" font-size="16" font-weight="500" fill="rgb(17,17,19)">Create Project</text></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 84" width="520" height="84"><defs><clipPath id="cp0"><rect x="174.9" y="34" width="16.1" height="16"/></clipPath><clipPath id="cp2"><rect x="198.9" y="33" width="106.3" height="18"/></clipPath><clipPath id="ct3"><rect x="198.9" y="33" width="106.3" height="18"/></clipPath></defs><rect x="20" y="20" width="440" height="44" rx="12" fill="rgb(0,117,219)"/><rect x="20.5" y="20.5" width="439" height="43" rx="11.5" fill="none" stroke="rgba(0,0,0,0)" stroke-width="1"/><g clip-path="url(#cp0)"><g color="rgb(255, 255, 255)"><clipPath id="svgic1"><rect x="174.9" y="34" width="16" height="16"/></clipPath><g clip-path="url(#svgic1)"><g transform="translate(174.86 34)scale(.66667)"><path d="M5 12h14" style="marker:none;vector-effect:none;color-interpolation:srgb" fill="none" stroke="rgb(255, 255, 255)" stroke-width="2px" stroke-dasharray="none" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="1" fill-opacity="1" opacity="1"/><path d="M12 5v14" style="marker:none;vector-effect:none;color-interpolation:srgb" fill="none" stroke="rgb(255, 255, 255)" stroke-width="2px" stroke-dasharray="none" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="1" fill-opacity="1" opacity="1"/></g></g></g></g><g clip-path="url(#cp2)"><g clip-path="url(#ct3)"><text x="198.86" y="48" font-family="system-ui, &quot;-apple-system&quot;, sans-serif" font-size="16" font-weight="500" fill="rgb(255,255,255)">Create Project</text></g></g></svg>