defuss-shadcn 0.8.1 → 0.8.3

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 (351) hide show
  1. package/AGENTS.md +10 -3
  2. package/Makefile +4 -1
  3. package/README.md +3 -2
  4. package/dist/SKILL.md +1 -1
  5. package/dist/SKILL_tpl.md +1 -1
  6. package/dist/architecture_tpl.html +1 -2
  7. package/dist/components/accordion/accordion.css +12 -5
  8. package/dist/components/accordion/accordion.js +16 -0
  9. package/dist/components/accordion/component-skill.md +1 -0
  10. package/dist/components/alert-dialog/alert-dialog.css +8 -0
  11. package/dist/components/alert-dialog/component-skill.md +1 -0
  12. package/dist/components/button-group/button-group.css +14 -15
  13. package/dist/components/collapsible/collapsible.css +11 -3
  14. package/dist/components/combobox/combobox.css +40 -0
  15. package/dist/components/combobox/combobox.js +24 -0
  16. package/dist/components/combobox/component-skill.md +2 -0
  17. package/dist/components/command/command.css +11 -0
  18. package/dist/components/command/component-skill.md +1 -0
  19. package/dist/components/dialog/component-skill.md +1 -0
  20. package/dist/components/dialog/dialog.css +12 -0
  21. package/dist/components/image/component-skill.md +1 -0
  22. package/dist/components/image/image.css +8 -0
  23. package/dist/components/label/component-skill.md +6 -1
  24. package/dist/components/select/component-skill.md +11 -2
  25. package/dist/components/select/select.css +10 -0
  26. package/dist/components/sheet/component-skill.md +1 -0
  27. package/dist/components/sheet/sheet.css +8 -0
  28. package/dist/components/sidebar/component-skill.md +1 -1
  29. package/dist/components/sidebar/sidebar.css +8 -0
  30. package/dist/components/toggle-group/toggle-group.css +18 -14
  31. package/dist/components/tree-view/tree-view.css +11 -3
  32. package/dist/components/typography/component-skill.md +2 -2
  33. package/dist/components/typography/typography.css +3 -3
  34. package/dist/documentation/accessibility.html +1 -2
  35. package/dist/documentation/accordion.html +28 -163
  36. package/dist/documentation/alert-dialog.html +11 -48
  37. package/dist/documentation/alert.html +1 -59
  38. package/dist/documentation/animations.html +1 -2
  39. package/dist/documentation/architecture.html +18 -24
  40. package/dist/documentation/avatar.html +1 -62
  41. package/dist/documentation/badge.html +1 -43
  42. package/dist/documentation/blog.html +1 -87
  43. package/dist/documentation/brand-logos.html +1 -60
  44. package/dist/documentation/breadcrumb.html +1 -114
  45. package/dist/documentation/button-group.html +15 -85
  46. package/dist/documentation/button.html +2 -86
  47. package/dist/documentation/calendar.html +1 -63
  48. package/dist/documentation/card.html +1 -124
  49. package/dist/documentation/carousel.html +1 -156
  50. package/dist/documentation/cascade-layers.html +1 -2
  51. package/dist/documentation/changelog.html +34 -2
  52. package/dist/documentation/checkbox.html +1 -103
  53. package/dist/documentation/collapsible.html +12 -38
  54. package/dist/documentation/color-picker.html +1 -32
  55. package/dist/documentation/combobox.html +69 -158
  56. package/dist/documentation/command.html +16 -95
  57. package/dist/documentation/component-skills.html +1 -2
  58. package/dist/documentation/context-menu.html +1 -24
  59. package/dist/documentation/css/layout.css +23 -33
  60. package/dist/documentation/dark-mode.html +1 -2
  61. package/dist/documentation/data-attribute-api.html +1 -2
  62. package/dist/documentation/date-picker.html +1 -43
  63. package/dist/documentation/dialog.html +14 -102
  64. package/dist/documentation/dropdown.html +1 -153
  65. package/dist/documentation/es-modules.html +1 -2
  66. package/dist/documentation/faq.html +2 -70
  67. package/dist/documentation/feature-details.html +1 -85
  68. package/dist/documentation/file-input.html +1 -40
  69. package/dist/documentation/form.html +1 -137
  70. package/dist/documentation/get-in-touch.html +1 -65
  71. package/dist/documentation/hero.html +1 -57
  72. package/dist/documentation/icon.html +1 -162
  73. package/dist/documentation/image.html +13 -175
  74. package/dist/documentation/index.html +2 -3
  75. package/dist/documentation/input.html +1 -140
  76. package/dist/documentation/installation.html +1 -2
  77. package/dist/documentation/js/layout.js +46 -3
  78. package/dist/documentation/js/search-index.js +1 -1
  79. package/dist/documentation/js/site.js +74 -72
  80. package/dist/documentation/label.html +6 -104
  81. package/dist/documentation/native-web-apis.html +1 -2
  82. package/dist/documentation/navigation-menu.html +1 -79
  83. package/dist/documentation/newsletter.html +1 -64
  84. package/dist/documentation/number-input.html +1 -30
  85. package/dist/documentation/pagination.html +1 -112
  86. package/dist/documentation/popover.html +1 -81
  87. package/dist/documentation/pricing.html +1 -109
  88. package/dist/documentation/product-showcase.html +1 -91
  89. package/dist/documentation/progress.html +1 -31
  90. package/dist/documentation/radio.html +1 -143
  91. package/dist/documentation/scroll-area.html +1 -47
  92. package/dist/documentation/select.html +19 -65
  93. package/dist/documentation/separator.html +1 -96
  94. package/dist/documentation/sheet.html +10 -112
  95. package/dist/documentation/sidebar.html +12 -177
  96. package/dist/documentation/site-footer.html +1 -82
  97. package/dist/documentation/site-header.html +1 -98
  98. package/dist/documentation/skeleton.html +1 -37
  99. package/dist/documentation/slider.html +1 -112
  100. package/dist/documentation/sortable.html +1 -126
  101. package/dist/documentation/spinner.html +1 -37
  102. package/dist/documentation/statistic.html +1 -48
  103. package/dist/documentation/stats.html +1 -70
  104. package/dist/documentation/steps.html +1 -186
  105. package/dist/documentation/switch.html +1 -80
  106. package/dist/documentation/table.html +1 -54
  107. package/dist/documentation/tabs.html +1 -143
  108. package/dist/documentation/testimonials.html +1 -81
  109. package/dist/documentation/textarea.html +1 -44
  110. package/dist/documentation/theming.html +1 -2
  111. package/dist/documentation/timeline.html +1 -48
  112. package/dist/documentation/toast.html +1 -114
  113. package/dist/documentation/toggle-group.html +19 -168
  114. package/dist/documentation/toggle.html +1 -90
  115. package/dist/documentation/toolbar.html +1 -71
  116. package/dist/documentation/tooltip.html +1 -99
  117. package/dist/documentation/tree-view.html +12 -55
  118. package/dist/documentation/typography.html +7 -120
  119. package/docs/404.html +340 -0
  120. package/docs/accessibility.html +1 -2
  121. package/docs/accordion.html +28 -163
  122. package/docs/alert-dialog.html +11 -48
  123. package/docs/alert.html +1 -59
  124. package/docs/animations.html +1 -2
  125. package/docs/architecture.html +18 -24
  126. package/docs/avatar.html +1 -62
  127. package/docs/badge.html +1 -43
  128. package/docs/blog.html +1 -87
  129. package/docs/brand-logos.html +1 -60
  130. package/docs/breadcrumb.html +1 -114
  131. package/docs/button-group.html +15 -85
  132. package/docs/button.html +2 -86
  133. package/docs/calendar.html +1 -63
  134. package/docs/card.html +1 -124
  135. package/docs/carousel.html +1 -156
  136. package/docs/cascade-layers.html +1 -2
  137. package/docs/changelog.html +34 -2
  138. package/docs/checkbox.html +1 -103
  139. package/docs/collapsible.html +12 -38
  140. package/docs/color-picker.html +1 -32
  141. package/docs/combobox.html +69 -158
  142. package/docs/command.html +16 -95
  143. package/docs/component-skills.html +1 -2
  144. package/docs/context-menu.html +1 -24
  145. package/docs/css/layout.css +23 -33
  146. package/docs/dark-mode.html +1 -2
  147. package/docs/data-attribute-api.html +1 -2
  148. package/docs/date-picker.html +1 -43
  149. package/docs/dialog.html +14 -102
  150. package/docs/dropdown.html +1 -153
  151. package/docs/es-modules.html +1 -2
  152. package/docs/faq.html +2 -70
  153. package/docs/feature-details.html +1 -85
  154. package/docs/file-input.html +1 -40
  155. package/docs/form.html +1 -137
  156. package/docs/get-in-touch.html +1 -65
  157. package/docs/hero.html +1 -57
  158. package/docs/icon.html +1 -162
  159. package/docs/image.html +13 -175
  160. package/docs/index.html +2 -3
  161. package/docs/input.html +1 -140
  162. package/docs/installation.html +1 -2
  163. package/docs/js/layout.js +46 -3
  164. package/docs/js/search-index.js +1 -1
  165. package/docs/js/site.js +74 -72
  166. package/docs/label.html +6 -104
  167. package/docs/native-web-apis.html +1 -2
  168. package/docs/navigation-menu.html +1 -79
  169. package/docs/newsletter.html +1 -64
  170. package/docs/number-input.html +1 -30
  171. package/docs/pagination.html +1 -112
  172. package/docs/popover.html +1 -81
  173. package/docs/pricing.html +1 -109
  174. package/docs/product-showcase.html +1 -91
  175. package/docs/progress.html +1 -31
  176. package/docs/radio.html +1 -143
  177. package/docs/scroll-area.html +1 -47
  178. package/docs/select.html +19 -65
  179. package/docs/separator.html +1 -96
  180. package/docs/sheet.html +10 -112
  181. package/docs/sidebar.html +12 -177
  182. package/docs/site-footer.html +1 -82
  183. package/docs/site-header.html +1 -98
  184. package/docs/skeleton.html +1 -37
  185. package/docs/slider.html +1 -112
  186. package/docs/sortable.html +1 -126
  187. package/docs/spinner.html +1 -37
  188. package/docs/statistic.html +1 -48
  189. package/docs/stats.html +1 -70
  190. package/docs/steps.html +1 -186
  191. package/docs/switch.html +1 -80
  192. package/docs/table.html +1 -54
  193. package/docs/tabs.html +1 -143
  194. package/docs/testimonials.html +1 -81
  195. package/docs/textarea.html +1 -44
  196. package/docs/theming.html +1 -2
  197. package/docs/timeline.html +1 -48
  198. package/docs/toast.html +1 -114
  199. package/docs/toggle-group.html +19 -168
  200. package/docs/toggle.html +1 -90
  201. package/docs/toolbar.html +1 -71
  202. package/docs/tooltip.html +1 -99
  203. package/docs/tree-view.html +12 -55
  204. package/docs/typography.html +7 -120
  205. package/package.json +2 -1
  206. package/scripts/lib/arch-page.ts +32 -9
  207. package/scripts/lib/mirror.ts +8 -0
  208. package/scripts/purge-cdn.ts +76 -0
  209. package/scripts/sync-docs.ts +1 -2
  210. package/scripts/verify.ts +4 -3
  211. package/src/SKILL.md +1 -1
  212. package/src/SKILL_tpl.md +1 -1
  213. package/src/architecture_tpl.html +1 -2
  214. package/src/components/accordion/accordion.css +12 -5
  215. package/src/components/accordion/accordion.ts +13 -0
  216. package/src/components/accordion/component-skill.md +1 -0
  217. package/src/components/alert-dialog/alert-dialog.css +8 -0
  218. package/src/components/alert-dialog/component-skill.md +1 -0
  219. package/src/components/button-group/button-group.css +14 -15
  220. package/src/components/collapsible/collapsible.css +11 -3
  221. package/src/components/combobox/combobox.css +40 -0
  222. package/src/components/combobox/combobox.ts +25 -0
  223. package/src/components/combobox/component-skill.md +2 -0
  224. package/src/components/command/command.css +11 -0
  225. package/src/components/command/component-skill.md +1 -0
  226. package/src/components/dialog/component-skill.md +1 -0
  227. package/src/components/dialog/dialog.css +12 -0
  228. package/src/components/image/component-skill.md +1 -0
  229. package/src/components/image/image.css +8 -0
  230. package/src/components/label/component-skill.md +6 -1
  231. package/src/components/select/component-skill.md +11 -2
  232. package/src/components/select/select.css +10 -0
  233. package/src/components/sheet/component-skill.md +1 -0
  234. package/src/components/sheet/sheet.css +8 -0
  235. package/src/components/sidebar/component-skill.md +1 -1
  236. package/src/components/sidebar/sidebar.css +8 -0
  237. package/src/components/toggle-group/toggle-group.css +18 -14
  238. package/src/components/tree-view/tree-view.css +11 -3
  239. package/src/components/typography/component-skill.md +2 -2
  240. package/src/components/typography/typography.css +3 -3
  241. package/src/documentation/accessibility.html +1 -2
  242. package/src/documentation/accordion.html +28 -163
  243. package/src/documentation/alert-dialog.html +11 -48
  244. package/src/documentation/alert.html +1 -59
  245. package/src/documentation/animations.html +1 -2
  246. package/src/documentation/architecture.html +18 -24
  247. package/src/documentation/avatar.html +1 -62
  248. package/src/documentation/badge.html +1 -43
  249. package/src/documentation/blog.html +1 -87
  250. package/src/documentation/brand-logos.html +1 -60
  251. package/src/documentation/breadcrumb.html +1 -114
  252. package/src/documentation/button-group.html +15 -85
  253. package/src/documentation/button.html +2 -86
  254. package/src/documentation/calendar.html +1 -63
  255. package/src/documentation/card.html +1 -124
  256. package/src/documentation/carousel.html +1 -156
  257. package/src/documentation/cascade-layers.html +1 -2
  258. package/src/documentation/changelog.html +34 -2
  259. package/src/documentation/checkbox.html +1 -103
  260. package/src/documentation/collapsible.html +12 -38
  261. package/src/documentation/color-picker.html +1 -32
  262. package/src/documentation/combobox.html +69 -158
  263. package/src/documentation/command.html +16 -95
  264. package/src/documentation/component-skills.html +1 -2
  265. package/src/documentation/context-menu.html +1 -24
  266. package/src/documentation/css/layout.css +23 -33
  267. package/src/documentation/dark-mode.html +1 -2
  268. package/src/documentation/data-attribute-api.html +1 -2
  269. package/src/documentation/date-picker.html +1 -43
  270. package/src/documentation/dialog.html +14 -102
  271. package/src/documentation/dropdown.html +1 -153
  272. package/src/documentation/es-modules.html +1 -2
  273. package/src/documentation/faq.html +2 -70
  274. package/src/documentation/feature-details.html +1 -85
  275. package/src/documentation/file-input.html +1 -40
  276. package/src/documentation/form.html +1 -137
  277. package/src/documentation/get-in-touch.html +1 -65
  278. package/src/documentation/hero.html +1 -57
  279. package/src/documentation/icon.html +1 -162
  280. package/src/documentation/image.html +13 -175
  281. package/src/documentation/index.html +2 -3
  282. package/src/documentation/input.html +1 -140
  283. package/src/documentation/installation.html +1 -2
  284. package/src/documentation/js/layout.ts +48 -4
  285. package/src/documentation/js/search-index.js +1 -1
  286. package/src/documentation/js/site.ts +70 -75
  287. package/src/documentation/label.html +6 -104
  288. package/src/documentation/native-web-apis.html +1 -2
  289. package/src/documentation/navigation-menu.html +1 -79
  290. package/src/documentation/newsletter.html +1 -64
  291. package/src/documentation/number-input.html +1 -30
  292. package/src/documentation/pagination.html +1 -112
  293. package/src/documentation/popover.html +1 -81
  294. package/src/documentation/pricing.html +1 -109
  295. package/src/documentation/product-showcase.html +1 -91
  296. package/src/documentation/progress.html +1 -31
  297. package/src/documentation/radio.html +1 -143
  298. package/src/documentation/scroll-area.html +1 -47
  299. package/src/documentation/select.html +19 -65
  300. package/src/documentation/separator.html +1 -96
  301. package/src/documentation/sheet.html +10 -112
  302. package/src/documentation/sidebar.html +12 -177
  303. package/src/documentation/site-footer.html +1 -82
  304. package/src/documentation/site-header.html +1 -98
  305. package/src/documentation/skeleton.html +1 -37
  306. package/src/documentation/slider.html +1 -112
  307. package/src/documentation/sortable.html +1 -126
  308. package/src/documentation/spinner.html +1 -37
  309. package/src/documentation/statistic.html +1 -48
  310. package/src/documentation/stats.html +1 -70
  311. package/src/documentation/steps.html +1 -186
  312. package/src/documentation/switch.html +1 -80
  313. package/src/documentation/table.html +1 -54
  314. package/src/documentation/tabs.html +1 -143
  315. package/src/documentation/testimonials.html +1 -81
  316. package/src/documentation/textarea.html +1 -44
  317. package/src/documentation/theming.html +1 -2
  318. package/src/documentation/timeline.html +1 -48
  319. package/src/documentation/toast.html +1 -114
  320. package/src/documentation/toggle-group.html +19 -168
  321. package/src/documentation/toggle.html +1 -90
  322. package/src/documentation/toolbar.html +1 -71
  323. package/src/documentation/tooltip.html +1 -99
  324. package/src/documentation/tree-view.html +12 -55
  325. package/src/documentation/typography.html +7 -120
  326. package/tests/e2e/accordion.e2e.ts +47 -3
  327. package/tests/e2e/alert-dialog.e2e-fixture.html +6 -1
  328. package/tests/e2e/alert-dialog.e2e.ts +28 -0
  329. package/tests/e2e/button-group.e2e-fixture.html +2 -1
  330. package/tests/e2e/button-group.e2e.ts +22 -0
  331. package/tests/e2e/combobox.e2e.ts +54 -0
  332. package/tests/e2e/command.e2e-fixture.html +3 -1
  333. package/tests/e2e/command.e2e.ts +24 -0
  334. package/tests/e2e/dialog.e2e-fixture.html +6 -1
  335. package/tests/e2e/dialog.e2e.ts +25 -0
  336. package/tests/e2e/image.e2e-fixture.html +3 -1
  337. package/tests/e2e/image.e2e.ts +27 -0
  338. package/tests/e2e/label.e2e.ts +10 -0
  339. package/tests/e2e/select.e2e-fixture.html +5 -3
  340. package/tests/e2e/select.e2e.ts +24 -0
  341. package/tests/e2e/sheet.e2e-fixture.html +6 -1
  342. package/tests/e2e/sheet.e2e.ts +34 -0
  343. package/tests/e2e/sidebar.e2e-fixture.html +3 -0
  344. package/tests/e2e/sidebar.e2e.ts +31 -0
  345. package/tests/e2e/spinner.e2e.ts +5 -1
  346. package/tests/e2e/tabs.e2e.ts +5 -0
  347. package/tests/e2e/toggle-group.e2e-fixture.html +10 -0
  348. package/tests/e2e/toggle-group.e2e.ts +21 -0
  349. package/tests/e2e/typography.e2e-fixture.html +1 -1
  350. package/tests/e2e/typography.e2e.ts +1 -1
  351. package/tests/ui.test.ts +84 -0
package/AGENTS.md CHANGED
@@ -17,12 +17,18 @@ You are working on the **defuss-shadcn** design system repo.
17
17
  The consumer-facing system lives in `dist/` — **it is generated**: edit sources in
18
18
  `src/` (`bun run build` compiles `.ts` → `.js` and copies everything else 1:1).
19
19
  `docs/` is also generated: **only the documentation site** (`dist/documentation/*` +
20
- `robots.txt`/`sitemap.xml`) published by GitHub Pages — its pages' `../components/…` and
20
+ `robots.txt`/`sitemap.xml` + a `404.html` copy of `index.html` so GitHub Pages never
21
+ serves an empty page for dead links) published by GitHub Pages — its pages' `../components/…` and
21
22
  `../theme/…` references are rewritten to the jsDelivr GitHub CDN (shared transform in
22
23
  `scripts/lib/mirror.ts`), so the mirror carries no copies of the component assets.
23
24
  Refresh it with `bun run docs` (`make build` does this automatically and `verify`
24
25
  fails if the mirror drifts). Never edit `docs/` directly — like `dist/`, it is
25
26
  deleted and rebuilt on every `bun run docs`.
27
+ Because the docs pages load their assets from jsDelivr `@latest` (newest git tag),
28
+ a fresh release keeps serving the **previous** release's CSS/JS until jsDelivr's
29
+ cache expires (12h edge, 7d browser). After `bun run deploy`, run
30
+ `bun run purge-cdn` (`make purge-cdn`) to force `@latest` to re-resolve to the
31
+ new tag immediately.
26
32
  Never edit `dist/` directly; it is deleted and rebuilt on every build.
27
33
 
28
34
  ---
@@ -45,7 +51,7 @@ defuss-shadcn/
45
51
  │ ├── css/docs-theme.css ← doc-site font overrides (not part of the system)
46
52
  │ ├── css/layout.css ← doc-site layout (not part of the system)
47
53
  │ ├── js/layout.js ← SPA router, <site-header>/<site-nav> web components
48
- │ ├── js/site.js ← doc-site-only JS (tabs, copy buttons, skill modal, code collapse)
54
+ │ ├── js/site.js ← doc-site-only JS (tabs, copy buttons, code collapse)
49
55
  │ ├── js/shiki-highlight.js ← Shiki-based syntax highlighting (ES module, CDN)
50
56
  │ ├── js/themes.js ← tweakcn color theme presets (global THEMES array)
51
57
  │ └── js/theme-switcher.js ← applies theme overrides to CSS custom properties
@@ -73,7 +79,8 @@ defuss-shadcn/
73
79
  │ ├── sync-css-snippets.ts ← re-embed component CSS into doc pages after edits
74
80
  │ ├── sync-js-snippets.ts ← re-embed component JS into doc pages after edits
75
81
  │ ├── push.sh ← commit + push dev → main (non-release)
76
- │ └── deploy.sh ← release: version bump, changelog, tag, GitHub release
82
+ │ ├── deploy.sh ← release: version bump, changelog, tag, GitHub release
83
+ │ └── purge-cdn.ts ← purge jsDelivr @latest cache for all dist assets (run after deploy)
77
84
  ├── tests/ ← UI tests (Vitest browser mode + Playwright)
78
85
  │ ├── helpers.ts ← loads real doc pages in a same-origin iframe
79
86
  │ ├── ui.test.ts ← end-to-end tests of the actual site UI
package/Makefile CHANGED
@@ -2,7 +2,7 @@
2
2
  # KISS: every target delegates to package.json so there is one source of truth.
3
3
 
4
4
  .DEFAULT_GOAL := help
5
- .PHONY: help setup dev test test-run coverage e2e lint verify screenshots build docs
5
+ .PHONY: help setup dev test test-run coverage e2e lint verify screenshots build docs purge-cdn
6
6
 
7
7
  help: ## Show available targets
8
8
  @grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
@@ -35,6 +35,9 @@ typecheck: ## Strict tsc type-check of tests/ and scripts/ (no emit)
35
35
  docs: ## Build, then mirror dist/ → docs/ for GitHub Pages
36
36
  bun run docs
37
37
 
38
+ purge-cdn: ## Purge jsDelivr @latest cache for all dist assets (run after deploy)
39
+ bun run purge-cdn
40
+
38
41
  # Full pipeline: fast checks first, compile, refresh screenshots (the verify
39
42
  # gate requires them fresh vs. the new dist/), then gate + tests. Calls
40
43
  # scripts/build.ts directly because `bun run build` would run verify BEFORE
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![TypeScript definitions](https://img.shields.io/npm/types/defuss-shadcn.svg)](https://www.npmjs.com/package/defuss-shadcn)
10
10
  [![Socket Badge](https://badge.socket.dev/npm/package/defuss-shadcn/latest)](https://socket.dev/npm/package/defuss-shadcn)
11
11
 
12
- **A UI component system that scales with AI.** Themeable components built on semantic HTML, modern CSS, and vanilla JavaScript. No framework. No build step for consumers — `dist/` is committed and ready to use as-is. The simplest possible foundation for AI-driven prototyping.
12
+ **A UI component system that scales with _local_ AI.** Themeable components built on semantic HTML, modern CSS, and vanilla JavaScript. No framework. No build step for consumers — `dist/` is committed and ready to use as-is. The simplest possible foundation for AI-driven prototyping.
13
13
 
14
14
  41 of 68 components need no JavaScript — native HTML and modern CSS cover them entirely.
15
15
 
@@ -141,7 +141,8 @@ bun run test:run # run the UI test suite (headless Chromium)
141
141
 
142
142
  `src/` is the authoring tree (`.ts` + html/css/md/fonts); `dist/` is its compiled, 1:1
143
143
  mirror, committed and the only thing that ships. `docs/` is the generated **documentation
144
- site** (only `dist/documentation/` + the SEO files) that GitHub Pages publishes — the
144
+ site** (only `dist/documentation/` + the SEO files + a `404.html` copy of `index.html` so
145
+ GitHub Pages never serves an empty page for dead links) that GitHub Pages publishes — the
145
146
  pages' `../components/…` / `../theme/…` references are rewritten to the jsDelivr GitHub
146
147
  CDN by the mirror ([scripts/lib/mirror.ts](scripts/lib/mirror.ts)), so docs/ carries no
147
148
  copies of the component assets. Refresh with `bun run docs`, never edit it directly.
package/dist/SKILL.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # defuss-shadcn — Agent Skill
6
6
 
7
- **A UI component system that scales with AI.** Themeable components built on semantic HTML, modern CSS, and vanilla JavaScript. No framework, zero dependencies — `dist/` is committed and ready to use as-is. This file is the entry point for an AI agent integrating the library: read it once, then read only the component skills you need.
7
+ **A UI component system that scales with _local_ AI.** Themeable components built on semantic HTML, modern CSS, and vanilla JavaScript. No framework, zero dependencies — `dist/` is committed and ready to use as-is. This file is the entry point for an AI agent integrating the library: read it once, then read only the component skills you need.
8
8
 
9
9
  ## Integrate
10
10
 
package/dist/SKILL_tpl.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # defuss-shadcn — Agent Skill
6
6
 
7
- **A UI component system that scales with AI.** Themeable components built on semantic HTML, modern CSS, and vanilla JavaScript. No framework, zero dependencies — `dist/` is committed and ready to use as-is. This file is the entry point for an AI agent integrating the library: read it once, then read only the component skills you need.
7
+ **A UI component system that scales with _local_ AI.** Themeable components built on semantic HTML, modern CSS, and vanilla JavaScript. No framework, zero dependencies — `dist/` is committed and ready to use as-is. This file is the entry point for an AI agent integrating the library: read it once, then read only the component skills you need.
8
8
 
9
9
  ## Integrate
10
10
 
@@ -23,8 +23,7 @@
23
23
  <link rel="stylesheet" href="css/docs-theme.css">
24
24
  <link rel="stylesheet" href="css/docs-utilities.css">
25
25
  <link rel="stylesheet" href="css/layout.css">
26
- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js" defer integrity="sha384-H+hy9ULve6xfxRkWIh/YOtvDdpXgV2fmAGQkIDTxIgZwNoaoBal14Di2YTMR6MzR" crossorigin="anonymous"></script>
27
- <link rel="stylesheet" href="../components/button/button.css">
26
+ <link rel="stylesheet" href="../components/button/button.css">
28
27
  <link rel="stylesheet" href="../components/card/card.css">
29
28
  <link rel="stylesheet" href="../components/dialog/dialog.css">
30
29
  <link rel="stylesheet" href="../components/badge/badge.css">
@@ -11,20 +11,27 @@
11
11
 
12
12
  &:last-child { border-bottom: none; }
13
13
 
14
- /* -- Content animation (CSS-only) ------------------------- */
15
- & ::details-content {
14
+ /* -- Content animation (CSS-only) -------------------------
15
+ Two gotchas, both required for the panel to glide:
16
+ 1. `::details-content` must attach to the compound (`&::details-content`,
17
+ NOT `& ::details-content` — the descendant form never matches, the
18
+ pseudo's originating element is the subject itself).
19
+ 2. `block-size: auto` is a keyword — without interpolate-size the
20
+ 0→auto pair is non-interpolable and the transition silently snaps. */
21
+ &::details-content {
16
22
  block-size: 0;
17
23
  overflow-y: clip;
24
+ interpolate-size: allow-keywords;
18
25
  transition: block-size 200ms ease, content-visibility 200ms allow-discrete;
19
26
  }
20
27
 
21
- &[open] ::details-content {
28
+ &[open]::details-content {
22
29
  block-size: auto;
23
30
  }
24
31
  }
25
32
 
26
33
  @starting-style {
27
- .accordion-item[open] ::details-content {
34
+ .accordion-item[open]::details-content {
28
35
  block-size: 0;
29
36
  }
30
37
  }
@@ -76,7 +83,7 @@
76
83
  (REQUIRED for all components — AGENTS.md "Accessibility CSS"). */
77
84
  @media (prefers-reduced-motion: reduce) {
78
85
  @layer components {
79
- .accordion-item ::details-content { transition: none; }
86
+ .accordion-item::details-content { transition: none; }
80
87
  .accordion-trigger { transition: none; }
81
88
  }
82
89
  }
@@ -116,6 +116,18 @@ function init() {
116
116
  getState: () => accordionApi.getState(accordion),
117
117
  };
118
118
  items.forEach((item) => {
119
+ // Cancellable pre-event: closing the LAST open item of a non-collapsible
120
+ // single accordion is denied here, before the DOM changes. Reopening it
121
+ // in the `toggle` handler instead would visibly flicker close→open now
122
+ // that the ::details-content height transition animates.
123
+ item.addEventListener('beforetoggle', (e) => {
124
+ if (accordion._applying)
125
+ return; // programmatic state change in progress
126
+ if (e.newState !== 'closed' || collapsible)
127
+ return;
128
+ if (!Array.from(items).some((i) => i !== item && i.open))
129
+ e.preventDefault();
130
+ });
119
131
  item.addEventListener('toggle', () => {
120
132
  if (accordion._applying)
121
133
  return; // programmatic state change in progress
@@ -126,6 +138,10 @@ function init() {
126
138
  });
127
139
  }
128
140
  else if (!collapsible) {
141
+ // fallback for browsers without beforetoggle (which the deny above
142
+ // needs): reopen, accepting the flicker — better than losing the
143
+ // single-open guarantee. Modern browsers never reach this branch
144
+ // because a denied beforetoggle fires no toggle event at all.
129
145
  const anyOpen = Array.from(items).some((i) => i.open);
130
146
  if (!anyOpen)
131
147
  item.open = true;
@@ -171,3 +171,4 @@ These are all browser-native — no JS needed.
171
171
  - Content height animation uses `::details-content` pseudo-element with `block-size` transition and `@starting-style` for the enter animation — fully CSS-only, no JS measurement needed
172
172
  - Set `data-type="single"` for accordion behavior (only one open); omit for disclosure list (any number open)
173
173
  - Set `data-collapsible` alongside `data-type="single"` to allow all items to be closed
174
+ - A non-collapsible single accordion never closes its last open item: the attempt is denied in the cancellable `beforetoggle` event (`preventDefault()`), so the click is a silent no-op. Reopening in `toggle` instead would visibly flicker close-then-open
@@ -50,6 +50,14 @@
50
50
  }
51
51
  }
52
52
 
53
+ /* -- Scroll lock ------------------------------------------- */
54
+ /* Page behind stays put while the alert is modal (see dialog.css):
55
+ `:modal` + overflow:hidden freezes the viewport at its current offset. */
56
+ html:has(dialog.alert-dialog:modal) {
57
+ overflow: hidden;
58
+ scrollbar-gutter: stable;
59
+ }
60
+
53
61
  .alert-dialog-content {
54
62
  padding: 1.5rem;
55
63
  }
@@ -62,3 +62,4 @@ The api is bound per dialog; the registry global is
62
62
  - No backdrop click dismiss — user must make an explicit choice
63
63
  - Escape key is disabled — user must use the action buttons
64
64
  - Focus is trapped inside the dialog via native `showModal()`
65
+ - While the alert is modal, `html:has(dialog.alert-dialog:modal)` sets `overflow: hidden` + `scrollbar-gutter: stable` — the page behind cannot scroll and its position is preserved for when the alert closes (no JS scroll-lock).
@@ -10,13 +10,17 @@
10
10
  margin-inline-start: -1px;
11
11
  }
12
12
 
13
- /* Remove internal radii for connected appearance */
14
- & > .btn:not(:first-child):not(:has(+ [role="separator"])) {
13
+ /* Remove internal radii for connected appearance. The orientation gate is
14
+ :where()-wrapped so these INLINE-corner rules only ever apply to
15
+ horizontal groups; ungated they also squared vertical buttons' corners
16
+ at a higher specificity than the vertical block below, leaving "Top"
17
+ with a rounded top-left but a square top-right (issue #13). */
18
+ &:where(:not([data-orientation="vertical"])) > .btn:not(:first-child):not(:has(+ [role="separator"])) {
15
19
  border-start-start-radius: 0;
16
20
  border-end-start-radius: 0;
17
21
  }
18
22
 
19
- & > .btn:not(:last-child):not([role="separator"] + .btn) {
23
+ &:where(:not([data-orientation="vertical"])) > .btn:not(:last-child):not([role="separator"] + .btn) {
20
24
  border-start-end-radius: 0;
21
25
  border-end-end-radius: 0;
22
26
  }
@@ -61,26 +65,21 @@
61
65
  margin-top: -1px;
62
66
  }
63
67
 
68
+ /* Square exactly the BLOCK corners that touch a neighbour — a
69
+ non-first button squares its top edge, a non-last its bottom
70
+ edge; the base .btn radius keeps every outer corner rounded on
71
+ both sides. No per-corner "restore" overrides: the last block's
72
+ restores re-rounded a corner the previous block had just
73
+ squared, which (together with the ungated horizontal rules)
74
+ produced the asymmetric Top/Middle corners of issue #13. */
64
75
  & > .btn:not(:first-child) {
65
76
  border-start-start-radius: 0;
66
77
  border-start-end-radius: 0;
67
- border-end-start-radius: var(--radius-md);
68
78
  }
69
79
 
70
80
  & > .btn:not(:last-child) {
71
81
  border-end-start-radius: 0;
72
82
  border-end-end-radius: 0;
73
- border-start-end-radius: var(--radius-md);
74
- }
75
-
76
- & > .btn:first-child {
77
- border-start-start-radius: var(--radius-md);
78
- border-start-end-radius: var(--radius-md);
79
- }
80
-
81
- & > .btn:last-child {
82
- border-end-start-radius: var(--radius-md);
83
- border-end-end-radius: var(--radius-md);
84
83
  }
85
84
 
86
85
  & > [role="separator"] {
@@ -4,19 +4,26 @@
4
4
  border-radius: var(--radius-lg);
5
5
  overflow: hidden;
6
6
 
7
- & ::details-content {
7
+ /* Two gotchas, both required for the panel to glide:
8
+ 1. `::details-content` must attach to the compound (`&::details-content`,
9
+ NOT `& ::details-content` — the descendant form never matches, the
10
+ pseudo's originating element is the subject itself).
11
+ 2. `block-size: auto` is a keyword — without interpolate-size the
12
+ 0→auto pair is non-interpolable and the transition silently snaps. */
13
+ &::details-content {
8
14
  block-size: 0;
9
15
  overflow-y: clip;
16
+ interpolate-size: allow-keywords;
10
17
  transition: block-size 200ms ease, content-visibility 200ms allow-discrete;
11
18
  }
12
19
 
13
- &[open] ::details-content {
20
+ &[open]::details-content {
14
21
  block-size: auto;
15
22
  }
16
23
  }
17
24
 
18
25
  @starting-style {
19
- .collapsible[open] ::details-content {
26
+ .collapsible[open]::details-content {
20
27
  block-size: 0;
21
28
  }
22
29
  }
@@ -80,6 +87,7 @@
80
87
  .collapsible *::before,
81
88
  .collapsible *::after,
82
89
  .collapsible::backdrop,
90
+ .collapsible::details-content,
83
91
  .collapsible-chevron,
84
92
  .collapsible-chevron *,
85
93
  .collapsible-chevron::before,
@@ -29,6 +29,39 @@
29
29
  opacity: 0.5;
30
30
  }
31
31
 
32
+ /* Clear button (injected by combobox.js after the trigger). Anchor-positioned
33
+ over the trigger's chevron slot; anchored via the same per-instance anchor
34
+ name the popover uses. `data-placeholder` presence is the single
35
+ selection marker — the :has() rules below flip chevron and clear in sync,
36
+ so no JS toggles visibility. */
37
+ .combobox-clear {
38
+ position: fixed;
39
+ inset: auto;
40
+ margin: 0;
41
+ top: anchor(top);
42
+ bottom: anchor(bottom);
43
+ right: anchor(right);
44
+ /* center the 20px box exactly over the 16px chevron (1rem trigger padding) */
45
+ margin-inline-end: 0.875rem;
46
+ width: 1.25rem;
47
+ display: none;
48
+ place-items: center;
49
+ padding: 0;
50
+ border: none;
51
+ border-radius: var(--radius-sm);
52
+ background: transparent;
53
+ color: var(--muted-foreground);
54
+ cursor: pointer;
55
+
56
+ &:hover { color: var(--foreground); }
57
+ &:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
58
+ }
59
+
60
+ .combobox:has(.combobox-value:not([data-placeholder])) {
61
+ & .combobox-clear { display: grid; }
62
+ & .combobox-chevron { display: none; }
63
+ }
64
+
32
65
  .combobox-content {
33
66
  position: fixed;
34
67
  inset: auto;
@@ -142,6 +175,13 @@
142
175
  pointer-events: none;
143
176
  opacity: 0.5;
144
177
  }
178
+
179
+ /* Author `display: flex` overrides the UA's [hidden] { display: none }
180
+ (author origin always wins), so filter()'s item.hidden = true left
181
+ non-matching options rendered — and clickable — in the list. */
182
+ &[hidden] {
183
+ display: none;
184
+ }
145
185
  }
146
186
 
147
187
  .combobox-empty {
@@ -108,6 +108,30 @@ function init() {
108
108
  const anchorId = `--combobox-${popover.id}`;
109
109
  trigger.style.anchorName = anchorId;
110
110
  popover.style.positionAnchor = anchorId;
111
+ // Clear button - injected so consumer markup stays minimal (and the
112
+ // button can't be nested in the trigger's <button>). Visibility is pure
113
+ // CSS: .combobox-clear shows exactly while data-placeholder is absent
114
+ // (combobox.css :has() rule); JS only wires the click and focus.
115
+ const placeholder = valueEl?.dataset.placeholder ?? '';
116
+ const clearBtn = document.createElement('button');
117
+ clearBtn.type = 'button';
118
+ clearBtn.className = 'combobox-clear';
119
+ clearBtn.setAttribute('aria-label', 'Clear selection');
120
+ clearBtn.innerHTML =
121
+ '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>';
122
+ clearBtn.style.positionAnchor = anchorId;
123
+ trigger.after(clearBtn);
124
+ clearBtn.addEventListener('click', () => {
125
+ allItems.forEach((i) => { i.setAttribute('aria-selected', 'false'); });
126
+ if (valueEl) {
127
+ valueEl.textContent = placeholder;
128
+ // re-declare data-placeholder: selectItem removed it, and it is the
129
+ // very marker the CSS :has() rule keys off to hide this button again
130
+ valueEl.setAttribute('data-placeholder', placeholder);
131
+ }
132
+ // the button goes display:none with the selection - keep focus usable
133
+ trigger.focus();
134
+ });
111
135
  const getVisibleItems = () => allItems.filter((item) => !item.hidden && item.getAttribute('aria-disabled') !== 'true');
112
136
  const open = () => {
113
137
  // deferred show (safeShowPopover): showPopover() mid-exit crashes the
@@ -22,6 +22,7 @@ the search input when the popover opens.
22
22
 
23
23
  ## Native Web APIs
24
24
  - [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) — top-layer rendering and light-dismiss for the dropdown list
25
+ - [`:has()`](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) — reveals the injected clear button exactly while a selection exists (`data-placeholder` absent)
25
26
  - [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) — positions the popover relative to the trigger without JS
26
27
  - [`@starting-style`](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style) — entry animation starting values for popover appearance
27
28
  - [`overscroll-behavior`](https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior) — prevents scroll chaining from the listbox to the page
@@ -169,6 +170,7 @@ The registry global is `_defussShadcn.comboboxApi` / `_defussShadcn.comboboxStat
169
170
  - The popover animates in via `@starting-style` + `transition-behavior: allow-discrete`
170
171
  - The check icon for selected items uses a CSS `::before` pseudo-element
171
172
  - Filter matching is case-insensitive and supports substring matching
173
+ - The clear button (`.combobox-clear`, ✕) is **injected by the JS after the trigger** — consumer markup never contains it. It replaces the chevron whenever a value is selected (pure CSS via `:has()`), and clicking it restores the placeholder and deselects every option
172
174
  - The empty state element is shown when no items match the filter query
173
175
  - Group labels and separators auto-hide when their group has no visible items
174
176
  - `overscroll-behavior: contain` prevents scroll chaining from the listbox
@@ -18,6 +18,14 @@
18
18
  dialog.command[open]::backdrop { background: oklch(0 0 0 / 0); }
19
19
  }
20
20
 
21
+ /* -- Scroll lock ------------------------------------------- */
22
+ /* Page behind stays put while the palette is modal (see dialog.css):
23
+ `:modal` + overflow:hidden freezes the viewport at its current offset. */
24
+ html:has(dialog.command:modal) {
25
+ overflow: hidden;
26
+ scrollbar-gutter: stable;
27
+ }
28
+
21
29
  .command-input-wrapper {
22
30
  display: flex; align-items: center; gap: 0.5rem;
23
31
  padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
@@ -43,6 +51,9 @@
43
51
  &:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
44
52
  &[aria-disabled="true"] { pointer-events: none; opacity: 0.5; }
45
53
  & svg { width: 1rem; height: 1rem; color: var(--muted-foreground); flex-shrink: 0; }
54
+ /* Same as combobox-item: `display: flex` beats the UA [hidden] rule, and
55
+ command.ts filter() hides non-matches via the hidden attribute. */
56
+ &[hidden] { display: none; }
46
57
  }
47
58
 
48
59
  .command-separator { height: 1px; background-color: var(--border); margin: 0.25rem -0.25rem; }
@@ -109,3 +109,4 @@ The api is bound per palette dialog; the registry global is
109
109
  - The dialog uses `showModal()` — focus is trapped inside and Escape closes it natively.
110
110
  - On close, the search input is cleared and all items are restored.
111
111
  - No JavaScript positioning is needed — the dialog uses CSS `position: fixed` with `top: 15%`.
112
+ - While the palette is modal, `html:has(dialog.command:modal)` sets `overflow: hidden` + `scrollbar-gutter: stable` — the page behind cannot scroll and its position is preserved for when the palette closes (no JS scroll-lock).
@@ -114,5 +114,6 @@ Requires minimal JavaScript — only for trigger wiring and backdrop-click-to-cl
114
114
  ## Notes
115
115
 
116
116
  - Animation uses CSS-only enter via `@starting-style` and exit via `transition` + `allow-discrete`.
117
+ - While the dialog is modal, `html:has(dialog.dialog:modal)` sets `overflow: hidden` + `scrollbar-gutter: stable` — the page behind cannot scroll and its position is preserved for when the dialog closes (no JS scroll-lock).
117
118
  - The selector is `dialog.dialog` (element + class) to avoid styling native `<dialog>` elements used elsewhere.
118
119
  - For forms inside dialogs, use the `dialog-body` wrapper for the form content.
@@ -54,6 +54,18 @@
54
54
  }
55
55
  }
56
56
 
57
+ /* -- Scroll lock ------------------------------------------- */
58
+ /* While the dialog is modal the page behind must stay put — closing
59
+ returns you to exactly the scroll offset you had. `:modal` matches only
60
+ while opened via showModal(); overflow:hidden freezes the viewport
61
+ without losing the scroll position (no JS position:fixed hack), and
62
+ scrollbar-gutter:stable keeps the scrollbar's space reserved so its
63
+ removal can't shift the layout underneath. */
64
+ html:has(dialog.dialog:modal) {
65
+ overflow: hidden;
66
+ scrollbar-gutter: stable;
67
+ }
68
+
57
69
  /* -- Content sections ------------------------------------- */
58
70
  .dialog-content { padding: 1.5rem; }
59
71
  .dialog-header { margin-bottom: 1rem; }
@@ -144,6 +144,7 @@ The registry global is `_defussShadcn.imageApi` / `_defussShadcn.imageStates`.
144
144
  - Use `loading="lazy"` on images below the fold for performance.
145
145
  - Lightbox supports keyboard: Escape closes, Tab navigates controls.
146
146
  - Multiple images with `data-preview` share a single dialog instance.
147
+ - While the lightbox is modal, `html:has(dialog.image-lightbox:modal)` sets `overflow: hidden` + `scrollbar-gutter: stable` — the page behind cannot scroll and its position is preserved for when the lightbox closes (no JS scroll-lock).
147
148
  # Image
148
149
 
149
150
  ## Native basis
@@ -108,6 +108,14 @@
108
108
  }
109
109
  }
110
110
 
111
+ /* -- Scroll lock ------------------------------------------- */
112
+ /* Page behind stays put while the lightbox is modal (see dialog.css):
113
+ `:modal` + overflow:hidden freezes the viewport at its current offset. */
114
+ html:has(dialog.image-lightbox:modal) {
115
+ overflow: hidden;
116
+ scrollbar-gutter: stable;
117
+ }
118
+
111
119
  .image-lightbox-content {
112
120
  display: flex;
113
121
  align-items: center;
@@ -10,6 +10,9 @@ supportedStates: default
10
10
 
11
11
  ## Native basis
12
12
  `<label>` element. Browser provides built-in click-to-focus association with form controls.
13
+ The association is the `for`↔`id` pairing: the label's `for` and the control's `id`
14
+ must be the exact same string — click-to-focus and the accessible name both exist
15
+ only because of it.
13
16
 
14
17
  ---
15
18
 
@@ -25,6 +28,7 @@ supportedStates: default
25
28
 
26
29
  ### Basic
27
30
  ```html
31
+ <!-- for and id are one value in two places — keep them identical -->
28
32
  <label class="label" for="email">Email</label>
29
33
  <input class="input" id="email" type="email">
30
34
  ```
@@ -91,7 +95,8 @@ The label auto-dims when the adjacent control is disabled — no `data-disabled`
91
95
  |-----------|------|-------|
92
96
  | `for` | Always | Matches the `id` of the associated form control |
93
97
 
94
- - Clicking the label focuses the associated input — this is native `<label>` behavior.
98
+ - **One value, written twice:** the label's `for` and the control's `id` must be the exact same string. A mismatch fails silently — the label renders and styles normally but no longer focuses the field or names it for assistive technology. When renaming a control's `id`, update every `for` that pointed at it.
99
+ - Clicking the label focuses the associated input — native `<label>` behavior, and the practical proof the pairing is intact.
95
100
  - The required indicator `*` uses `aria-hidden="true"` since the `required` attribute on the input already conveys the requirement to assistive technology.
96
101
  - Do not use `<label>` without a `for` attribute or a nested input.
97
102
  - In `forced-colors` mode, label text maps to system `LinkText` color.
@@ -22,10 +22,12 @@ supportedStates: default
22
22
  ## Structure
23
23
 
24
24
  ### Basic
25
+ The empty-value placeholder stays selectable (no `disabled`) so it doubles as
26
+ the clear/reset entry — re-choosing it empties the box again.
25
27
  ```html
26
28
  <label class="label" for="fruit">Fruit</label>
27
29
  <select class="select" id="fruit">
28
- <option value="" disabled selected>Select a fruit</option>
30
+ <option value="" selected>Select a fruit</option>
29
31
  <option value="apple">Apple</option>
30
32
  <option value="banana">Banana</option>
31
33
  <option value="cherry">Cherry</option>
@@ -52,7 +54,11 @@ supportedStates: default
52
54
 
53
55
  - Native `<select>` provides full keyboard navigation (arrow keys, type-ahead).
54
56
  - Use `<label>` with `for` for description.
55
- - Use `disabled` on `<option>` elements for placeholder text.
57
+ - The placeholder is `<option value="" selected>` **without** `disabled`: a
58
+ disabled placeholder can't be re-selected, so a made selection could never be
59
+ cleared. Keep it selectable and add `required` to the `<select>` when an empty
60
+ value must not submit — native validation then blocks submission while the
61
+ placeholder is still the selection.
56
62
 
57
63
  ---
58
64
 
@@ -73,5 +79,8 @@ Set `data-size` on the `.select` trigger button.
73
79
  ## Notes
74
80
 
75
81
  - Uses `appearance: none` with a custom chevron via `background-image` SVG.
82
+ - While the empty option is selected, the closed control renders in
83
+ `--muted-foreground` (`:has(> option[value=""]:checked)`) so it reads as a
84
+ placeholder; a real value restores `--foreground`.
76
85
  - The dropdown list is rendered by the browser — it cannot be styled.
77
86
  - For a fully custom dropdown, use the Combobox component instead.
@@ -23,6 +23,16 @@
23
23
  background-position: right 0.5rem center;
24
24
  background-size: 1rem;
25
25
 
26
+ /* Placeholder = the empty-value option. It stays selectable (no
27
+ `disabled`) so it doubles as the clear/reset entry — choosing it
28
+ again empties the box (issue #32). While it is the selection the
29
+ closed control reads as a placeholder; a real value restores
30
+ --foreground because the base rule's color wins over this one.
31
+ option:checked matches the *selected* option in every engine. */
32
+ &:has(> option[value=""]:checked) {
33
+ color: var(--muted-foreground);
34
+ }
35
+
26
36
  &:focus {
27
37
  border-color: var(--ring);
28
38
  box-shadow: 0 0 0 2px oklch(from var(--ring) l c h / 0.2);
@@ -123,6 +123,7 @@ The api is bound per sheet element; the registry global is
123
123
 
124
124
  ## Notes
125
125
 
126
+ - While a sheet is modal, `html:has(dialog.sheet:modal)` sets `overflow: hidden` + `scrollbar-gutter: stable` — the page behind cannot scroll and its position is preserved for when the sheet closes (no JS scroll-lock).
126
127
  - Right/left sheets have a fixed width of `24rem` with `max-width: 100vw` for small screens.
127
128
  - Top/bottom sheets are full width with `height: auto` — they size to their content. Their `.sheet-content` column caps at `48rem` and centers, so content doesn't stretch to the viewport edges.
128
129
  - The selector is `dialog.sheet` (element + class) to avoid conflicts with `dialog.dialog`.
@@ -122,6 +122,14 @@
122
122
  }
123
123
  }
124
124
 
125
+ /* -- Scroll lock ------------------------------------------- */
126
+ /* Page behind stays put while a sheet is modal (see dialog.css):
127
+ `:modal` + overflow:hidden freezes the viewport at its current offset. */
128
+ html:has(dialog.sheet:modal) {
129
+ overflow: hidden;
130
+ scrollbar-gutter: stable;
131
+ }
132
+
125
133
  /* -- Content sections -------------------------------------- */
126
134
  .sheet-content { padding: 1.5rem; position: relative; }
127
135
 
@@ -126,7 +126,7 @@ The registry global is `_defussShadcn.sidebarApi` / `_defussShadcn.sidebarStates
126
126
 
127
127
  ## Notes
128
128
 
129
- - **Mobile**: Desktop sidebar hidden below 768px. Use `<dialog class="sidebar-mobile">` for slide-in sheet.
129
+ - **Mobile**: Desktop sidebar hidden below 768px. Use `<dialog class="sidebar-mobile">` for slide-in sheet. While it is modal, `html:has(.sidebar-mobile:modal)` sets `overflow: hidden` + `scrollbar-gutter: stable` — the page behind cannot scroll and keeps its position (no JS scroll-lock).
130
130
  - **Collapsible groups**: `<details class="sidebar-group">` — native toggle, no JS.
131
131
  - **Submenus**: `<details class="sidebar-submenu">` for nested nav with left border.
132
132
  - **Badges**: `<span class="sidebar-badge">` for notification counts.
@@ -356,6 +356,14 @@
356
356
  .sidebar-mobile[data-side="right"][open] { opacity: 0; translate: 100% 0; }
357
357
  }
358
358
 
359
+ /* ── Scroll lock ──────────────────────────────────────────── */
360
+ /* Page behind stays put while the mobile sheet is modal (see dialog.css):
361
+ `:modal` + overflow:hidden freezes the viewport at its current offset. */
362
+ html:has(.sidebar-mobile:modal) {
363
+ overflow: hidden;
364
+ scrollbar-gutter: stable;
365
+ }
366
+
359
367
  /* ── Reduced motion ───────────────────────────────────────── */
360
368
  @media (prefers-reduced-motion: reduce) {
361
369
  .app-sidebar { transition: none; }