@poodle64/ui 2026.8.7

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 (387) hide show
  1. package/README.md +624 -0
  2. package/dist/components/ui/alert/alert-description.svelte +23 -0
  3. package/dist/components/ui/alert/alert-description.svelte.d.ts +9 -0
  4. package/dist/components/ui/alert/alert-title.svelte +23 -0
  5. package/dist/components/ui/alert/alert-title.svelte.d.ts +9 -0
  6. package/dist/components/ui/alert/alert.svelte +46 -0
  7. package/dist/components/ui/alert/alert.svelte.d.ts +28 -0
  8. package/dist/components/ui/alert/index.d.ts +4 -0
  9. package/dist/components/ui/alert/index.js +4 -0
  10. package/dist/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  11. package/dist/components/ui/alert-dialog/alert-dialog-action.svelte.d.ts +9 -0
  12. package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  13. package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte.d.ts +9 -0
  14. package/dist/components/ui/alert-dialog/alert-dialog-content.svelte +39 -0
  15. package/dist/components/ui/alert-dialog/alert-dialog-content.svelte.d.ts +11 -0
  16. package/dist/components/ui/alert-dialog/alert-dialog-description.svelte +20 -0
  17. package/dist/components/ui/alert-dialog/alert-dialog-description.svelte.d.ts +4 -0
  18. package/dist/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  19. package/dist/components/ui/alert-dialog/alert-dialog-footer.svelte.d.ts +5 -0
  20. package/dist/components/ui/alert-dialog/alert-dialog-header.svelte +23 -0
  21. package/dist/components/ui/alert-dialog/alert-dialog-header.svelte.d.ts +5 -0
  22. package/dist/components/ui/alert-dialog/alert-dialog-media.svelte +23 -0
  23. package/dist/components/ui/alert-dialog/alert-dialog-media.svelte.d.ts +5 -0
  24. package/dist/components/ui/alert-dialog/alert-dialog-overlay.svelte +20 -0
  25. package/dist/components/ui/alert-dialog/alert-dialog-overlay.svelte.d.ts +4 -0
  26. package/dist/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  27. package/dist/components/ui/alert-dialog/alert-dialog-portal.svelte.d.ts +3 -0
  28. package/dist/components/ui/alert-dialog/alert-dialog-title.svelte +20 -0
  29. package/dist/components/ui/alert-dialog/alert-dialog-title.svelte.d.ts +4 -0
  30. package/dist/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  31. package/dist/components/ui/alert-dialog/alert-dialog-trigger.svelte.d.ts +4 -0
  32. package/dist/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  33. package/dist/components/ui/alert-dialog/alert-dialog.svelte.d.ts +3 -0
  34. package/dist/components/ui/alert-dialog/index.d.ts +13 -0
  35. package/dist/components/ui/alert-dialog/index.js +15 -0
  36. package/dist/components/ui/app-dialog/app-dialog.svelte +111 -0
  37. package/dist/components/ui/app-dialog/app-dialog.svelte.d.ts +41 -0
  38. package/dist/components/ui/app-dialog/index.d.ts +2 -0
  39. package/dist/components/ui/app-dialog/index.js +2 -0
  40. package/dist/components/ui/app-shell/app-nav.svelte +299 -0
  41. package/dist/components/ui/app-shell/app-nav.svelte.d.ts +22 -0
  42. package/dist/components/ui/app-shell/app-shell.svelte +675 -0
  43. package/dist/components/ui/app-shell/app-shell.svelte.d.ts +157 -0
  44. package/dist/components/ui/app-shell/index.d.ts +6 -0
  45. package/dist/components/ui/app-shell/index.js +6 -0
  46. package/dist/components/ui/app-shell/measure.d.ts +28 -0
  47. package/dist/components/ui/app-shell/measure.js +21 -0
  48. package/dist/components/ui/app-shell/texture.d.ts +32 -0
  49. package/dist/components/ui/app-shell/texture.js +27 -0
  50. package/dist/components/ui/app-shell/types.d.ts +134 -0
  51. package/dist/components/ui/app-shell/types.js +88 -0
  52. package/dist/components/ui/arc-gauge/arc-gauge.svelte +112 -0
  53. package/dist/components/ui/arc-gauge/arc-gauge.svelte.d.ts +15 -0
  54. package/dist/components/ui/arc-gauge/index.d.ts +3 -0
  55. package/dist/components/ui/arc-gauge/index.js +2 -0
  56. package/dist/components/ui/avatar/avatar-fallback.svelte +26 -0
  57. package/dist/components/ui/avatar/avatar-fallback.svelte.d.ts +4 -0
  58. package/dist/components/ui/avatar/avatar-image.svelte +17 -0
  59. package/dist/components/ui/avatar/avatar-image.svelte.d.ts +4 -0
  60. package/dist/components/ui/avatar/avatar.svelte +31 -0
  61. package/dist/components/ui/avatar/avatar.svelte.d.ts +4 -0
  62. package/dist/components/ui/avatar/index.d.ts +4 -0
  63. package/dist/components/ui/avatar/index.js +6 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +38 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/bar-row/bar-row.svelte +42 -0
  69. package/dist/components/ui/bar-row/bar-row.svelte.d.ts +12 -0
  70. package/dist/components/ui/bar-row/index.d.ts +2 -0
  71. package/dist/components/ui/bar-row/index.js +2 -0
  72. package/dist/components/ui/button/button.svelte +89 -0
  73. package/dist/components/ui/button/button.svelte.d.ts +70 -0
  74. package/dist/components/ui/button/index.d.ts +2 -0
  75. package/dist/components/ui/button/index.js +4 -0
  76. package/dist/components/ui/card/card-action.svelte +23 -0
  77. package/dist/components/ui/card/card-action.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-content.svelte +20 -0
  79. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-description.svelte +20 -0
  81. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  82. package/dist/components/ui/card/card-footer.svelte +23 -0
  83. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/card-header.svelte +23 -0
  85. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  86. package/dist/components/ui/card/card-title.svelte +52 -0
  87. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  88. package/dist/components/ui/card/card.svelte +25 -0
  89. package/dist/components/ui/card/card.svelte.d.ts +8 -0
  90. package/dist/components/ui/card/index.d.ts +8 -0
  91. package/dist/components/ui/card/index.js +10 -0
  92. package/dist/components/ui/checkbox/checkbox.svelte +36 -0
  93. package/dist/components/ui/checkbox/checkbox.svelte.d.ts +8 -0
  94. package/dist/components/ui/checkbox/index.d.ts +2 -0
  95. package/dist/components/ui/checkbox/index.js +2 -0
  96. package/dist/components/ui/command/command-dialog.svelte +42 -0
  97. package/dist/components/ui/command/command-dialog.svelte.d.ts +14 -0
  98. package/dist/components/ui/command/command-empty.svelte +17 -0
  99. package/dist/components/ui/command/command-empty.svelte.d.ts +4 -0
  100. package/dist/components/ui/command/command-group.svelte +33 -0
  101. package/dist/components/ui/command/command-group.svelte.d.ts +7 -0
  102. package/dist/components/ui/command/command-input.svelte +36 -0
  103. package/dist/components/ui/command/command-input.svelte.d.ts +4 -0
  104. package/dist/components/ui/command/command-item.svelte +27 -0
  105. package/dist/components/ui/command/command-item.svelte.d.ts +4 -0
  106. package/dist/components/ui/command/command-link-item.svelte +20 -0
  107. package/dist/components/ui/command/command-link-item.svelte.d.ts +4 -0
  108. package/dist/components/ui/command/command-list.svelte +20 -0
  109. package/dist/components/ui/command/command-list.svelte.d.ts +4 -0
  110. package/dist/components/ui/command/command-loading.svelte +7 -0
  111. package/dist/components/ui/command/command-loading.svelte.d.ts +4 -0
  112. package/dist/components/ui/command/command-separator.svelte +17 -0
  113. package/dist/components/ui/command/command-separator.svelte.d.ts +4 -0
  114. package/dist/components/ui/command/command-shortcut.svelte +23 -0
  115. package/dist/components/ui/command/command-shortcut.svelte.d.ts +5 -0
  116. package/dist/components/ui/command/command.svelte +28 -0
  117. package/dist/components/ui/command/command.svelte.d.ts +8 -0
  118. package/dist/components/ui/command/index.d.ts +12 -0
  119. package/dist/components/ui/command/index.js +14 -0
  120. package/dist/components/ui/command-palette/command-palette.svelte +94 -0
  121. package/dist/components/ui/command-palette/command-palette.svelte.d.ts +40 -0
  122. package/dist/components/ui/command-palette/index.d.ts +3 -0
  123. package/dist/components/ui/command-palette/index.js +2 -0
  124. package/dist/components/ui/context-column/context-column.svelte +56 -0
  125. package/dist/components/ui/context-column/context-column.svelte.d.ts +14 -0
  126. package/dist/components/ui/context-column/index.d.ts +2 -0
  127. package/dist/components/ui/context-column/index.js +2 -0
  128. package/dist/components/ui/data-table/data-table.svelte.d.ts +40 -0
  129. package/dist/components/ui/data-table/data-table.svelte.js +110 -0
  130. package/dist/components/ui/data-table/flex-render.svelte +40 -0
  131. package/dist/components/ui/data-table/flex-render.svelte.d.ts +33 -0
  132. package/dist/components/ui/data-table/index.d.ts +3 -0
  133. package/dist/components/ui/data-table/index.js +3 -0
  134. package/dist/components/ui/data-table/render-helpers.d.ts +90 -0
  135. package/dist/components/ui/data-table/render-helpers.js +99 -0
  136. package/dist/components/ui/data-table-tanstack/data-table-tanstack.svelte +247 -0
  137. package/dist/components/ui/data-table-tanstack/data-table-tanstack.svelte.d.ts +79 -0
  138. package/dist/components/ui/data-table-tanstack/index.d.ts +3 -0
  139. package/dist/components/ui/data-table-tanstack/index.js +2 -0
  140. package/dist/components/ui/data-table-toolbar/data-table-toolbar.svelte +102 -0
  141. package/dist/components/ui/data-table-toolbar/data-table-toolbar.svelte.d.ts +32 -0
  142. package/dist/components/ui/data-table-toolbar/index.d.ts +3 -0
  143. package/dist/components/ui/data-table-toolbar/index.js +2 -0
  144. package/dist/components/ui/detail-panel/detail-panel.svelte +95 -0
  145. package/dist/components/ui/detail-panel/detail-panel.svelte.d.ts +26 -0
  146. package/dist/components/ui/detail-panel/index.d.ts +2 -0
  147. package/dist/components/ui/detail-panel/index.js +2 -0
  148. package/dist/components/ui/dialog/dialog-close.svelte +11 -0
  149. package/dist/components/ui/dialog/dialog-close.svelte.d.ts +4 -0
  150. package/dist/components/ui/dialog/dialog-content.svelte +48 -0
  151. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +13 -0
  152. package/dist/components/ui/dialog/dialog-description.svelte +20 -0
  153. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  154. package/dist/components/ui/dialog/dialog-footer.svelte +35 -0
  155. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +8 -0
  156. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  157. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/dialog/dialog-overlay.svelte +20 -0
  159. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  160. package/dist/components/ui/dialog/dialog-portal.svelte +7 -0
  161. package/dist/components/ui/dialog/dialog-portal.svelte.d.ts +3 -0
  162. package/dist/components/ui/dialog/dialog-title.svelte +17 -0
  163. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  164. package/dist/components/ui/dialog/dialog-trigger.svelte +11 -0
  165. package/dist/components/ui/dialog/dialog-trigger.svelte.d.ts +4 -0
  166. package/dist/components/ui/dialog/dialog.svelte +7 -0
  167. package/dist/components/ui/dialog/dialog.svelte.d.ts +3 -0
  168. package/dist/components/ui/dialog/index.d.ts +11 -0
  169. package/dist/components/ui/dialog/index.js +13 -0
  170. package/dist/components/ui/dialog-section/dialog-section.svelte +20 -0
  171. package/dist/components/ui/dialog-section/dialog-section.svelte.d.ts +8 -0
  172. package/dist/components/ui/dialog-section/index.d.ts +2 -0
  173. package/dist/components/ui/dialog-section/index.js +2 -0
  174. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  175. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +4 -0
  176. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
  177. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +9 -0
  178. package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  179. package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +10 -0
  180. package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  181. package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +8 -0
  182. package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  183. package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte.d.ts +4 -0
  184. package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  185. package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte.d.ts +8 -0
  186. package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +27 -0
  187. package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte.d.ts +8 -0
  188. package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  189. package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte.d.ts +3 -0
  190. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
  191. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +4 -0
  192. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
  193. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
  194. package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  195. package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
  196. package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +23 -0
  197. package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte.d.ts +5 -0
  198. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
  199. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +4 -0
  200. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  201. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +7 -0
  202. package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  203. package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte.d.ts +3 -0
  204. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  205. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +4 -0
  206. package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  207. package/dist/components/ui/dropdown-menu/dropdown-menu.svelte.d.ts +3 -0
  208. package/dist/components/ui/dropdown-menu/index.d.ts +18 -0
  209. package/dist/components/ui/dropdown-menu/index.js +18 -0
  210. package/dist/components/ui/empty-state/empty-state.svelte +44 -0
  211. package/dist/components/ui/empty-state/empty-state.svelte.d.ts +14 -0
  212. package/dist/components/ui/empty-state/index.d.ts +2 -0
  213. package/dist/components/ui/empty-state/index.js +2 -0
  214. package/dist/components/ui/error-state/error-state.svelte +52 -0
  215. package/dist/components/ui/error-state/error-state.svelte.d.ts +10 -0
  216. package/dist/components/ui/error-state/index.d.ts +2 -0
  217. package/dist/components/ui/error-state/index.js +2 -0
  218. package/dist/components/ui/info-tip/index.d.ts +2 -0
  219. package/dist/components/ui/info-tip/index.js +2 -0
  220. package/dist/components/ui/info-tip/info-tip.svelte +37 -0
  221. package/dist/components/ui/info-tip/info-tip.svelte.d.ts +10 -0
  222. package/dist/components/ui/input/index.d.ts +2 -0
  223. package/dist/components/ui/input/index.js +4 -0
  224. package/dist/components/ui/input/input.svelte +48 -0
  225. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  226. package/dist/components/ui/input-group/index.d.ts +7 -0
  227. package/dist/components/ui/input-group/index.js +9 -0
  228. package/dist/components/ui/input-group/input-group-addon.svelte +53 -0
  229. package/dist/components/ui/input-group/input-group-addon.svelte.d.ts +32 -0
  230. package/dist/components/ui/input-group/input-group-button.svelte +49 -0
  231. package/dist/components/ui/input-group/input-group-button.svelte.d.ts +32 -0
  232. package/dist/components/ui/input-group/input-group-input.svelte +23 -0
  233. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +11 -0
  234. package/dist/components/ui/input-group/input-group-text.svelte +22 -0
  235. package/dist/components/ui/input-group/input-group-text.svelte.d.ts +5 -0
  236. package/dist/components/ui/input-group/input-group-textarea.svelte +23 -0
  237. package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +3 -0
  238. package/dist/components/ui/input-group/input-group.svelte +24 -0
  239. package/dist/components/ui/input-group/input-group.svelte.d.ts +5 -0
  240. package/dist/components/ui/label/index.d.ts +2 -0
  241. package/dist/components/ui/label/index.js +4 -0
  242. package/dist/components/ui/label/label.svelte +20 -0
  243. package/dist/components/ui/label/label.svelte.d.ts +4 -0
  244. package/dist/components/ui/loading-state/index.d.ts +2 -0
  245. package/dist/components/ui/loading-state/index.js +2 -0
  246. package/dist/components/ui/loading-state/loading-state.svelte +28 -0
  247. package/dist/components/ui/loading-state/loading-state.svelte.d.ts +8 -0
  248. package/dist/components/ui/page-header/index.d.ts +2 -0
  249. package/dist/components/ui/page-header/index.js +2 -0
  250. package/dist/components/ui/page-header/page-header.svelte +100 -0
  251. package/dist/components/ui/page-header/page-header.svelte.d.ts +17 -0
  252. package/dist/components/ui/panel/index.d.ts +2 -0
  253. package/dist/components/ui/panel/index.js +2 -0
  254. package/dist/components/ui/panel/panel.svelte +54 -0
  255. package/dist/components/ui/panel/panel.svelte.d.ts +16 -0
  256. package/dist/components/ui/password-input/index.d.ts +2 -0
  257. package/dist/components/ui/password-input/index.js +4 -0
  258. package/dist/components/ui/password-input/password-input.svelte +48 -0
  259. package/dist/components/ui/password-input/password-input.svelte.d.ts +8 -0
  260. package/dist/components/ui/popover/index.d.ts +4 -0
  261. package/dist/components/ui/popover/index.js +6 -0
  262. package/dist/components/ui/popover/popover-content.svelte +45 -0
  263. package/dist/components/ui/popover/popover-content.svelte.d.ts +7 -0
  264. package/dist/components/ui/popover/popover-trigger.svelte +7 -0
  265. package/dist/components/ui/popover/popover-trigger.svelte.d.ts +4 -0
  266. package/dist/components/ui/popover/popover.svelte +7 -0
  267. package/dist/components/ui/popover/popover.svelte.d.ts +3 -0
  268. package/dist/components/ui/progress/index.d.ts +2 -0
  269. package/dist/components/ui/progress/index.js +4 -0
  270. package/dist/components/ui/progress/progress.svelte +30 -0
  271. package/dist/components/ui/progress/progress.svelte.d.ts +4 -0
  272. package/dist/components/ui/scorecard/index.d.ts +2 -0
  273. package/dist/components/ui/scorecard/index.js +2 -0
  274. package/dist/components/ui/scorecard/scorecard.svelte +35 -0
  275. package/dist/components/ui/scorecard/scorecard.svelte.d.ts +8 -0
  276. package/dist/components/ui/select/index.d.ts +12 -0
  277. package/dist/components/ui/select/index.js +14 -0
  278. package/dist/components/ui/select/select-content.svelte +45 -0
  279. package/dist/components/ui/select/select-content.svelte.d.ts +11 -0
  280. package/dist/components/ui/select/select-group-heading.svelte +21 -0
  281. package/dist/components/ui/select/select-group-heading.svelte.d.ts +10 -0
  282. package/dist/components/ui/select/select-group.svelte +17 -0
  283. package/dist/components/ui/select/select-group.svelte.d.ts +4 -0
  284. package/dist/components/ui/select/select-item.svelte +38 -0
  285. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  286. package/dist/components/ui/select/select-label.svelte +20 -0
  287. package/dist/components/ui/select/select-label.svelte.d.ts +6 -0
  288. package/dist/components/ui/select/select-portal.svelte +7 -0
  289. package/dist/components/ui/select/select-portal.svelte.d.ts +3 -0
  290. package/dist/components/ui/select/select-scroll-down-button.svelte +23 -0
  291. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  292. package/dist/components/ui/select/select-scroll-up-button.svelte +23 -0
  293. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  294. package/dist/components/ui/select/select-separator.svelte +18 -0
  295. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  296. package/dist/components/ui/select/select-trigger.svelte +29 -0
  297. package/dist/components/ui/select/select-trigger.svelte.d.ts +8 -0
  298. package/dist/components/ui/select/select.svelte +11 -0
  299. package/dist/components/ui/select/select.svelte.d.ts +3 -0
  300. package/dist/components/ui/separator/index.d.ts +2 -0
  301. package/dist/components/ui/separator/index.js +4 -0
  302. package/dist/components/ui/separator/separator.svelte +23 -0
  303. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  304. package/dist/components/ui/skeleton/index.d.ts +2 -0
  305. package/dist/components/ui/skeleton/index.js +4 -0
  306. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  307. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  308. package/dist/components/ui/sonner/index.d.ts +1 -0
  309. package/dist/components/ui/sonner/index.js +1 -0
  310. package/dist/components/ui/sonner/sonner.svelte +34 -0
  311. package/dist/components/ui/sonner/sonner.svelte.d.ts +4 -0
  312. package/dist/components/ui/sparkline/index.d.ts +3 -0
  313. package/dist/components/ui/sparkline/index.js +2 -0
  314. package/dist/components/ui/sparkline/sparkline.svelte +90 -0
  315. package/dist/components/ui/sparkline/sparkline.svelte.d.ts +16 -0
  316. package/dist/components/ui/stat-card/index.d.ts +2 -0
  317. package/dist/components/ui/stat-card/index.js +2 -0
  318. package/dist/components/ui/stat-card/stat-card.svelte +77 -0
  319. package/dist/components/ui/stat-card/stat-card.svelte.d.ts +34 -0
  320. package/dist/components/ui/stat-list/index.d.ts +3 -0
  321. package/dist/components/ui/stat-list/index.js +2 -0
  322. package/dist/components/ui/stat-list/stat-list.svelte +80 -0
  323. package/dist/components/ui/stat-list/stat-list.svelte.d.ts +23 -0
  324. package/dist/components/ui/status/index.d.ts +1 -0
  325. package/dist/components/ui/status/index.js +1 -0
  326. package/dist/components/ui/status-badge/index.d.ts +3 -0
  327. package/dist/components/ui/status-badge/index.js +2 -0
  328. package/dist/components/ui/status-badge/status-badge.svelte +53 -0
  329. package/dist/components/ui/status-badge/status-badge.svelte.d.ts +39 -0
  330. package/dist/components/ui/switch/index.d.ts +2 -0
  331. package/dist/components/ui/switch/index.js +4 -0
  332. package/dist/components/ui/switch/switch.svelte +39 -0
  333. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  334. package/dist/components/ui/table/index.d.ts +10 -0
  335. package/dist/components/ui/table/index.js +12 -0
  336. package/dist/components/ui/table/table-body.svelte +20 -0
  337. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  338. package/dist/components/ui/table/table-caption.svelte +20 -0
  339. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  340. package/dist/components/ui/table/table-cell.svelte +20 -0
  341. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  342. package/dist/components/ui/table/table-classes.d.ts +15 -0
  343. package/dist/components/ui/table/table-classes.js +15 -0
  344. package/dist/components/ui/table/table-footer.svelte +20 -0
  345. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  346. package/dist/components/ui/table/table-head.svelte +23 -0
  347. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  348. package/dist/components/ui/table/table-header.svelte +20 -0
  349. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  350. package/dist/components/ui/table/table-row.svelte +23 -0
  351. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  352. package/dist/components/ui/table/table.svelte +37 -0
  353. package/dist/components/ui/table/table.svelte.d.ts +20 -0
  354. package/dist/components/ui/tabs/index.d.ts +5 -0
  355. package/dist/components/ui/tabs/index.js +7 -0
  356. package/dist/components/ui/tabs/tabs-content.svelte +13 -0
  357. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  358. package/dist/components/ui/tabs/tabs-list.svelte +16 -0
  359. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  360. package/dist/components/ui/tabs/tabs-trigger.svelte +16 -0
  361. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  362. package/dist/components/ui/tabs/tabs.svelte +19 -0
  363. package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
  364. package/dist/components/ui/textarea/index.d.ts +2 -0
  365. package/dist/components/ui/textarea/index.js +4 -0
  366. package/dist/components/ui/textarea/textarea.svelte +22 -0
  367. package/dist/components/ui/textarea/textarea.svelte.d.ts +5 -0
  368. package/dist/components/ui/tile-grid/index.d.ts +2 -0
  369. package/dist/components/ui/tile-grid/index.js +2 -0
  370. package/dist/components/ui/tile-grid/tile-grid.svelte +66 -0
  371. package/dist/components/ui/tile-grid/tile-grid.svelte.d.ts +43 -0
  372. package/dist/components/ui/tooltip/index.d.ts +6 -0
  373. package/dist/components/ui/tooltip/index.js +8 -0
  374. package/dist/components/ui/tooltip/tooltip-content.svelte +52 -0
  375. package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +11 -0
  376. package/dist/components/ui/tooltip/tooltip-portal.svelte +7 -0
  377. package/dist/components/ui/tooltip/tooltip-portal.svelte.d.ts +4 -0
  378. package/dist/components/ui/tooltip/tooltip-provider.svelte +7 -0
  379. package/dist/components/ui/tooltip/tooltip-provider.svelte.d.ts +4 -0
  380. package/dist/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  381. package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
  382. package/dist/components/ui/tooltip/tooltip.svelte +7 -0
  383. package/dist/components/ui/tooltip/tooltip.svelte.d.ts +4 -0
  384. package/dist/styles.css +961 -0
  385. package/dist/utils.d.ts +12 -0
  386. package/dist/utils.js +5 -0
  387. package/package.json +81 -0
@@ -0,0 +1,675 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The application shell: navigation, top bar and page frame, once.
4
+ *
5
+ * Primitives and page chrome are not what makes an app feel like an app — the
6
+ * shell is. Five household frontends were surveyed before this API was
7
+ * settled, and between them they had built five shells: a rail-plus-drawer,
8
+ * an eleven-file collapsible sidebar tree under its own top bar, and three
9
+ * header-only bars that each solved the mobile question differently. They
10
+ * agreed on almost nothing structurally while looking like they were trying
11
+ * to be the same thing.
12
+ *
13
+ * There is now exactly one composition, no variant prop to choose it: a
14
+ * permanent side rail on md+ (an overlay drawer below it) carrying primary
15
+ * navigation and a brand row that also carries the collapse toggle, PLUS a
16
+ * real top navbar (search, leading/trailing slots, theme toggle, identity) —
17
+ * always both together. Operator ruling, 31/07/2026: every household app
18
+ * renders one shell shape; apps do not choose variants.
19
+ *
20
+ * The collapse toggle sits at the rail's own top edge, on the brand row,
21
+ * where every comparable shell puts a control that acts on the rail — icon
22
+ * only, right of the wordmark, stacking under the mark when the rail is
23
+ * collapsed. The foot is left to the app.
24
+ *
25
+ * Search can sit in either of two places, because it reads as a different
26
+ * thing depending on what is beside it. `searchPlacement` defaults to
27
+ * `leading` — its historical spot just after the `context` slot, so a shell
28
+ * that never names the prop is unchanged — and `trailing` moves it into the
29
+ * right-hand group with the theme toggle and identity, so that when `context`
30
+ * carries route content the global affordance reads as global rather than as
31
+ * part of the switcher next to it.
32
+ *
33
+ * Content WIDTH is the one dimension that came back, as `measure`, and it
34
+ * came back on a measurement rather than on taste. "Always full-width" did
35
+ * not remove the decision, it pushed it down into every page: a consumer was
36
+ * surveyed at 2560px and had six distinct caps across nine routes, every one
37
+ * of them a hand-written `mx-auto max-w-*` at the top of a `+page.svelte`,
38
+ * between them using 15% to 79% of the width actually available. That is
39
+ * fifteen independent guesses, not a shell shape.
40
+ *
41
+ * `measure` is a small named scale, chosen once in the layout, and it is not
42
+ * the old `content` prop returning. It defaults to `full`, so a shell that
43
+ * never mentions it renders byte-identically; its tiers are CSS custom
44
+ * properties an app retunes without a release; and it exists to REPLACE
45
+ * per-page width decisions rather than to offer an app a look.
46
+ *
47
+ * Content TEXTURE came back the same way, on the same kind of evidence. Two
48
+ * apps had separately built one picture — a faint dot-grid floor with a soft
49
+ * accent vignette — under two names in two app.css files, and one of them
50
+ * applied it as a helper class per route, so it reached four routes out of
51
+ * about fifteen. Which pages wore the house atmosphere was decided by which
52
+ * pages someone had happened to touch. `texture` is one named picture the
53
+ * SHELL paints once, off by default, its inks tuned by custom properties.
54
+ *
55
+ * Everything else the surveyed apps differed on turned out to be a slot, not
56
+ * a variant: the brand, the identity surface, a context switcher, a banner, a
57
+ * secondary column. Those are snippets, so this package imports no app store,
58
+ * no app route and no app brand — the coupling that made the best shell in
59
+ * the estate unliftable in the first place.
60
+ *
61
+ * The minimum useful call is two props:
62
+ *
63
+ * <AppShell nav={navGroups} brandTitle="Console" currentPath={page.url.pathname}>
64
+ * {@render children()}
65
+ * </AppShell>
66
+ */
67
+ import type { Snippet } from 'svelte';
68
+ import type { ShellMeasure } from './measure.js';
69
+ import type { ShellTexture } from './texture.js';
70
+ import { toggleMode } from 'mode-watcher';
71
+ import Menu from '@lucide/svelte/icons/menu';
72
+ import X from '@lucide/svelte/icons/x';
73
+ import Search from '@lucide/svelte/icons/search';
74
+ import Sun from '@lucide/svelte/icons/sun';
75
+ import Moon from '@lucide/svelte/icons/moon';
76
+ import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
77
+ import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
78
+ import AppNav from './app-nav.svelte';
79
+ import { toItems, type NavSource } from './types.js';
80
+ import { cn } from '../../../utils.js';
81
+
82
+ let {
83
+ nav,
84
+ navLabel = 'Primary',
85
+ currentPath,
86
+ collapsible = false,
87
+ collapsed = $bindable(false),
88
+ brand,
89
+ brandTitle,
90
+ brandMark,
91
+ homeHref = '/',
92
+ banner,
93
+ context,
94
+ actions,
95
+ identity,
96
+ sidebar,
97
+ onSearch,
98
+ searchLabel = 'Search…',
99
+ searchShortcut = '⌘K',
100
+ searchPlacement = 'leading',
101
+ themeToggle = true,
102
+ onToggleTheme,
103
+ padded = true,
104
+ measure = 'full',
105
+ texture = 'none',
106
+ mainClass,
107
+ children
108
+ }: {
109
+ /** Navigation: bare items, groups, or a mix. Filter for permissions first. */
110
+ nav?: NavSource;
111
+ /** The primary nav landmark's accessible name — forwarded to the rail/drawer
112
+ AppNav. Distinguish it from a second shell on the same page, or from a
113
+ route-scoped secondary AppNav. */
114
+ navLabel?: string;
115
+ /**
116
+ * The current path, for active state and for closing the mobile nav on
117
+ * navigation. Apps pass `page.url.pathname`.
118
+ *
119
+ * It is a prop rather than a `$app/state` import on purpose: this package
120
+ * is built with svelte-package and has no SvelteKit runtime of its own, so
121
+ * importing `$app/state` would make SvelteKit a hard peer AND make the
122
+ * shell untestable outside a running app. One line at the call site buys
123
+ * a component that is framework-agnostic and genuinely drivable.
124
+ */
125
+ currentPath?: string;
126
+ /** Offer an icon-only collapse toggle at the rail head, on the brand row. */
127
+ collapsible?: boolean;
128
+ /** Rail collapse state. Bind it to persist the choice across sessions. */
129
+ collapsed?: boolean;
130
+ /** Full control of the brand lockup. Overrides brandTitle/brandMark. */
131
+ brand?: Snippet;
132
+ /** Wordmark text, rendered in the display face. */
133
+ brandTitle?: string;
134
+ /** The mark beside the wordmark — an app's own logo component. */
135
+ brandMark?: Snippet;
136
+ /** Where the brand links. */
137
+ homeHref?: string;
138
+ /** Full-width region directly under the top bar: reconnect notices, trials. */
139
+ banner?: Snippet;
140
+ /** Leading top-bar slot, before the search affordance: a context switcher. */
141
+ context?: Snippet;
142
+ /** Trailing top-bar slot, before the theme toggle: app-level action buttons. */
143
+ actions?: Snippet;
144
+ /** The signed-in user surface. Rendered once, at the end of the top bar. */
145
+ identity?: Snippet;
146
+ /** A secondary, route-scoped column between the nav and the page body. */
147
+ sidebar?: Snippet;
148
+ /** Provide to render the search affordance. Usually opens a CommandPalette. */
149
+ onSearch?: () => void;
150
+ searchLabel?: string;
151
+ /** The shortcut hint rendered in the search affordance; '' hides the kbd. */
152
+ searchShortcut?: string;
153
+ /**
154
+ * Where the search affordance sits in the top bar. `leading` (default)
155
+ * keeps its historical position just after the `context` slot; `trailing`
156
+ * moves it into the right-hand group with the theme toggle and identity —
157
+ * for when `context` carries route content and search should read as a
158
+ * global control rather than as part of it.
159
+ */
160
+ searchPlacement?: 'leading' | 'trailing';
161
+ themeToggle?: boolean;
162
+ /** Override the theme action. Defaults to mode-watcher's toggleMode. */
163
+ onToggleTheme?: () => void;
164
+ padded?: boolean;
165
+ /**
166
+ * How wide the page body may get, from a named scale: `prose` (72ch, for
167
+ * long-form running text), `page` (80rem — forms, detail views,
168
+ * settings), `wide` (120rem — indexes, grids, tables, dashboards), or
169
+ * `full` (no cap).
170
+ *
171
+ * Defaults to `full`, so a shell that does not mention it is unchanged.
172
+ * Set it in the layout, once: the point is that pages stop each deciding
173
+ * their own width. The cap is the content box's border box, so `padded`
174
+ * spends its padding inside the measure.
175
+ */
176
+ measure?: ShellMeasure;
177
+ /**
178
+ * The house atmosphere on the content region: `grid` (a dot-grid floor
179
+ * plus a corner vignette) or `none`.
180
+ *
181
+ * Defaults to `none`, so a shell that does not mention it is unchanged.
182
+ * Set it in the layout, once — that the SHELL paints it is the whole
183
+ * point, because a helper class apps apply per route is how one consumer
184
+ * ended up wearing the house texture on four routes out of fifteen. The
185
+ * inks and the pitch are `--ds-shell-texture-*` custom properties an app
186
+ * retunes in one declaration.
187
+ */
188
+ texture?: ShellTexture;
189
+ /** Extra classes on the scrolling content container. */
190
+ mainClass?: string;
191
+ children: Snippet;
192
+ } = $props();
193
+
194
+ const hasNav = $derived(toItems(nav).length > 0);
195
+
196
+ // The measure is resolved ONCE, and the class and the attribute both read
197
+ // that one answer. Two independently-worded conditionals looked symmetric
198
+ // and were not: Svelte omits an attribute whose value is `null`, while a
199
+ // class token under the same test survives, so `measure={null}` — which the
200
+ // type forbids but a loosely-typed prop bag or a nullable route field can
201
+ // still deliver — produced the class with no attribute and therefore no
202
+ // matching rule. Harmless to look at, but it breaks the DOM contract the
203
+ // additivity gate states, and a guarantee with a hole in it is not one.
204
+ const capped = $derived(measure != null && measure !== 'full' ? measure : null);
205
+
206
+ // The texture resolves the same way, and for the same reason: one answer
207
+ // feeding both the class and the attribute, so `texture={null}` from a
208
+ // loosely-typed prop bag cannot produce a class with no matching rule.
209
+ const textured = $derived(texture != null && texture !== 'none' ? texture : null);
210
+
211
+ // The disclosure toggle and the region it opens have to be programmatically
212
+ // related, not merely adjacent: `aria-expanded` says the control is open,
213
+ // `aria-controls` says WHAT it opened, and assistive technology uses the
214
+ // pair to jump straight to the region instead of trusting DOM order (#12).
215
+ //
216
+ // Generated rather than a literal, because the id is a real one in the
217
+ // consumer's document. A fixed `ds-shell-nav` would collide with an app's
218
+ // own element of that name, and would collide with ITSELF the moment a page
219
+ // mounts two shells — and `document.getElementById` would answer with a coin
220
+ // toss, which is the same failure the rail deliberately avoids by being one
221
+ // element rather than two.
222
+ const shellId = $props.id();
223
+ const navRegionId = `${shellId}-nav`;
224
+
225
+ // The mobile nav is an overlay below `md`; on `md`+ the rail is a permanent
226
+ // column and this stays false. It closes on any navigation so a tap-through
227
+ // never leaves it covering the page it just opened.
228
+ let mobileNavOpen = $state(false);
229
+ $effect(() => {
230
+ // Reading `currentPath` registers this effect to re-run on every
231
+ // navigation — link tap, palette jump, or back button — which is exactly
232
+ // when the mobile nav should close.
233
+ void currentPath;
234
+ mobileNavOpen = false;
235
+ });
236
+
237
+ // The DRAWER is never collapsed, and that `&& !mobileNavOpen` is a fix rather
238
+ // than a flourish. The rail and the drawer are one element carrying one
239
+ // `collapsed` state, so a user who collapsed the rail on a desktop and later
240
+ // opened the menu on a phone was handed a 3.5rem icon-only drawer with no way
241
+ // out — the Collapse control is `md:flex` and does not render at that width.
242
+ // Nesting did not cause that, it raised the stakes: a collapsed rail renders
243
+ // no children, so the phone would have lost the sub-navigation entirely.
244
+ // `mobileNavOpen` implies narrow (the matchMedia effect below is what earns
245
+ // that), so this can never suppress a legitimate desktop collapse.
246
+ const railCollapsed = $derived(collapsible && collapsed && !mobileNavOpen);
247
+
248
+ // The collapse toggle's accessible name states what pressing it will DO, so it
249
+ // flips with the state: collapsed → "Expand", expanded → "Collapse".
250
+ const collapseLabel = $derived(collapsed ? 'Expand sidebar' : 'Collapse sidebar');
251
+
252
+ // Focus follows the overlay, in both directions. Opening a drawer and leaving
253
+ // the caret behind it strands a keyboard user in a page they can no longer
254
+ // see; closing it without returning focus drops them at the top of the
255
+ // document. Neither is visible in a screenshot and neither is something the
256
+ // consuming app can fix from the outside, so the shell owns it.
257
+ let menuButton = $state<HTMLButtonElement | null>(null);
258
+ let overlayFirstFocus = $state<HTMLElement | null>(null);
259
+ $effect(() => {
260
+ if (mobileNavOpen) overlayFirstFocus?.focus();
261
+ });
262
+
263
+ function closeMobileNav() {
264
+ if (!mobileNavOpen) return;
265
+ mobileNavOpen = false;
266
+ menuButton?.focus();
267
+ }
268
+
269
+ // Crossing up into md closes the overlay. Without this, opening the drawer on
270
+ // a phone and then widening (rotate, split-view, a resized window) leaves
271
+ // `mobileNavOpen` true while the rail is back in flow — a stale state that
272
+ // also makes the modal semantics below wrong. Closing it here is what earns
273
+ // the invariant the dialog role depends on: open implies narrow.
274
+ $effect(() => {
275
+ if (typeof window === 'undefined' || !window.matchMedia) return;
276
+ const wide = window.matchMedia('(min-width: 48rem)');
277
+ const sync = () => {
278
+ if (wide.matches) mobileNavOpen = false;
279
+ };
280
+ sync();
281
+ wide.addEventListener('change', sync);
282
+ return () => wide.removeEventListener('change', sync);
283
+ });
284
+
285
+ // Tab must not walk out of an overlay into the page it is covering. A full
286
+ // `inert` on the rest of the shell would be the heavier answer; wrapping at
287
+ // the ends is enough here because the overlay's content is a short, flat list.
288
+ function trapTab(event: KeyboardEvent) {
289
+ if (event.key !== 'Tab' || !mobileNavOpen) return;
290
+ const overlay = event.currentTarget as HTMLElement;
291
+ const candidates = [
292
+ ...overlay.querySelectorAll<HTMLElement>('a[href], button:not([disabled])')
293
+ ].filter((el) => el.tabIndex !== -1);
294
+ // Drop what CSS has hidden — the collapse control is display:none below md
295
+ // and must not be a tab stop. `getClientRects()` is the check that works
296
+ // against a real layout; where there is no layout engine at all it reports
297
+ // EVERYTHING as hidden, so an empty result means "cannot tell", not "none",
298
+ // and the trap falls back to the unfiltered set rather than silently
299
+ // switching itself off.
300
+ const visible = candidates.filter((el) => el.getClientRects().length > 0);
301
+ const focusable = visible.length > 0 ? visible : candidates;
302
+ if (focusable.length === 0) return;
303
+ const first = focusable[0];
304
+ const last = focusable[focusable.length - 1];
305
+ if (event.shiftKey && document.activeElement === first) {
306
+ event.preventDefault();
307
+ last.focus();
308
+ } else if (!event.shiftKey && document.activeElement === last) {
309
+ event.preventDefault();
310
+ first.focus();
311
+ }
312
+ }
313
+
314
+ function toggleTheme() {
315
+ if (onToggleTheme) onToggleTheme();
316
+ else toggleMode();
317
+ }
318
+
319
+ // `[` toggles the rail — but only when it is not a character the user is
320
+ // trying to TYPE. A bare printable key bound globally would otherwise eat every
321
+ // `[` typed into a search field, a JSON editor or any input the consumer
322
+ // renders, so the shortcut yields whenever focus is on an editable element.
323
+ // This rides the window keydown the shell already owns for Escape rather than
324
+ // adding a second document-level listener.
325
+ function isTypingTarget(target: EventTarget | null): boolean {
326
+ const el = target as HTMLElement | null;
327
+ if (!el || typeof el.tagName !== 'string') return false;
328
+ return (
329
+ el.tagName === 'INPUT' ||
330
+ el.tagName === 'TEXTAREA' ||
331
+ el.tagName === 'SELECT' ||
332
+ el.isContentEditable
333
+ );
334
+ }
335
+ </script>
336
+
337
+ <svelte:window
338
+ onkeydown={(e) => {
339
+ if (e.key === 'Escape') {
340
+ closeMobileNav();
341
+ } else if (
342
+ collapsible &&
343
+ e.key === '[' &&
344
+ !e.metaKey &&
345
+ !e.ctrlKey &&
346
+ !e.altKey &&
347
+ !isTypingTarget(e.target)
348
+ ) {
349
+ collapsed = !collapsed;
350
+ }
351
+ }}
352
+ />
353
+
354
+ {#snippet brandLockup(compact = false)}
355
+ <a
356
+ href={homeHref}
357
+ class={cn(
358
+ 'flex flex-none items-center gap-2.5',
359
+ compact ? '' : 'h-14 px-4',
360
+ railCollapsed && !compact && 'justify-center px-0'
361
+ )}
362
+ aria-label={brandTitle ?? 'Home'}
363
+ >
364
+ {#if brand}
365
+ {@render brand()}
366
+ {:else}
367
+ {#if brandMark}
368
+ <span class="border-border bg-background grid size-8 flex-none place-items-center rounded-md border">
369
+ {@render brandMark()}
370
+ </span>
371
+ {/if}
372
+ {#if brandTitle && !railCollapsed}
373
+ <span class="font-display text-body font-semibold tracking-tight">{brandTitle}</span>
374
+ {/if}
375
+ {/if}
376
+ </a>
377
+ {/snippet}
378
+
379
+ <!--
380
+ One search button, rendered in one of two places by `searchPlacement`. It is a
381
+ snippet rather than two literals so the two positions cannot drift apart — same
382
+ markup, same `data-testid`, same behaviour; only the sizing classes differ.
383
+ `sizing` is where the difference lives: leading fills the row (`flex-1`),
384
+ trailing sizes to its clamp without stretching the right-hand group.
385
+ -->
386
+ {#snippet searchButton(sizing: string)}
387
+ <button
388
+ onclick={onSearch}
389
+ class={cn(
390
+ 'border-border text-shell-muted-foreground hover:text-shell-foreground flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm transition-colors',
391
+ sizing
392
+ )}
393
+ data-testid="ds-shell-search"
394
+ >
395
+ <Search class="size-4 flex-none" />
396
+ <span class="truncate">{searchLabel}</span>
397
+ {#if searchShortcut}
398
+ <kbd
399
+ class="border-border bg-background text-shell-muted-foreground text-2xs ml-auto hidden rounded border px-1.5 py-0.5 font-mono sm:block"
400
+ >{searchShortcut}</kbd
401
+ >
402
+ {/if}
403
+ </button>
404
+ {/snippet}
405
+
406
+ <div class="ds-shell bg-background text-foreground flex h-dvh overflow-hidden">
407
+ <!-- WCAG 2.4.1: every app gets the bypass link, rather than one app having it. -->
408
+ <a href="#ds-main" class="ds-skip-link">Skip to content</a>
409
+
410
+ {#if hasNav}
411
+ {#if mobileNavOpen}
412
+ <button
413
+ class="bg-background/70 fixed inset-0 z-40 backdrop-blur-sm md:hidden"
414
+ aria-label="Dismiss menu"
415
+ tabindex="-1"
416
+ onclick={closeMobileNav}
417
+ ></button>
418
+ {/if}
419
+
420
+ <!--
421
+ ONE element, not two. The desktop rail and the mobile drawer are the
422
+ same aside wearing a different `data-drawer` state, because rendering
423
+ the rail's contents twice duplicates whatever the CONSUMER put in the
424
+ `identity` snippet — a second copy of their menu, their test hooks, and
425
+ any `id` they wrote, live in the DOM at the same time. Which of the two
426
+ `document.getElementById` then answers with is a coin toss the consumer
427
+ never agreed to. The reference shell rendered it twice; that is the one
428
+ piece of its behaviour deliberately not carried over.
429
+
430
+ The rail/drawer swap is therefore CSS on this single element (see
431
+ `.ds-shell-rail[data-drawer]` in styles.css) rather than two branches.
432
+ -->
433
+ <aside
434
+ id={navRegionId}
435
+ class="ds-shell-rail bg-shell text-shell-foreground border-border flex-none flex-col border-r"
436
+ data-collapsed={railCollapsed ? 'true' : undefined}
437
+ data-drawer={mobileNavOpen ? 'true' : undefined}
438
+ data-testid={mobileNavOpen ? 'ds-shell-drawer' : undefined}
439
+ role={mobileNavOpen ? 'dialog' : undefined}
440
+ aria-modal={mobileNavOpen ? 'true' : undefined}
441
+ aria-label={mobileNavOpen ? 'Navigation' : undefined}
442
+ onkeydown={trapTab}
443
+ >
444
+ {#if mobileNavOpen}
445
+ <button
446
+ bind:this={overlayFirstFocus}
447
+ onclick={closeMobileNav}
448
+ class="border-border text-shell-muted-foreground hover:text-shell-foreground absolute top-3 right-3 z-10 grid size-8 place-items-center rounded-md border md:hidden"
449
+ aria-label="Close menu"
450
+ >
451
+ <X class="size-4" />
452
+ </button>
453
+ {/if}
454
+
455
+ <!--
456
+ The collapse toggle rides the brand row, at the rail's own top edge —
457
+ where a control that acts on the rail belongs (Notion, Linear, GitHub's
458
+ newer nav all put it there), rather than a full-width labelled row at
459
+ the foot that read as a navigation destination. Expanded, it sits right
460
+ of the wordmark (`ml-auto`); collapsed, the head has no room beside the
461
+ mark, so the row becomes a column and the toggle stacks under it,
462
+ centred. `md:grid` keeps it off the mobile drawer, where collapse does
463
+ not apply — the same reason the foot control carried `md:flex`.
464
+ -->
465
+ {#if collapsible}
466
+ <div class={cn('flex items-center', railCollapsed ? 'flex-col gap-1 py-2' : 'pr-2')}>
467
+ {@render brandLockup()}
468
+ <button
469
+ type="button"
470
+ onclick={() => (collapsed = !collapsed)}
471
+ class={cn(
472
+ 'text-shell-muted-foreground hover:text-shell-foreground hidden size-8 flex-none place-items-center rounded-md transition-colors md:grid',
473
+ !railCollapsed && 'ml-auto'
474
+ )}
475
+ aria-label={collapseLabel}
476
+ aria-pressed={collapsed}
477
+ title={`${collapseLabel} (press [)`}
478
+ data-testid="ds-rail-collapse"
479
+ >
480
+ {#if collapsed}
481
+ <PanelLeftOpen class="size-4" />
482
+ {:else}
483
+ <PanelLeftClose class="size-4" />
484
+ {/if}
485
+ </button>
486
+ </div>
487
+ {:else}
488
+ {@render brandLockup()}
489
+ {/if}
490
+ <AppNav
491
+ {nav}
492
+ {currentPath}
493
+ collapsed={railCollapsed}
494
+ label={navLabel}
495
+ onNavigate={() => (mobileNavOpen = false)}
496
+ />
497
+ </aside>
498
+ {/if}
499
+
500
+ <div class="flex min-h-0 min-w-0 flex-1 flex-col">
501
+ <header
502
+ class="ds-shell-bar bg-shell/80 text-shell-foreground border-border sticky top-0 z-20 flex h-14 flex-none items-center gap-2 border-b px-3 backdrop-blur sm:gap-3 sm:px-5"
503
+ >
504
+ {#if hasNav}
505
+ <!-- `aria-controls` names the region this toggle opens. It is present
506
+ whenever that region is in the document, which is always — the
507
+ rail and the drawer are ONE element, so the reference is live from
508
+ first render and never dangles. -->
509
+ <button
510
+ bind:this={menuButton}
511
+ onclick={() => (mobileNavOpen ? closeMobileNav() : (mobileNavOpen = true))}
512
+ class="border-border text-shell-muted-foreground hover:text-shell-foreground grid size-9 flex-none place-items-center rounded-md border md:hidden"
513
+ aria-label="Menu"
514
+ aria-expanded={mobileNavOpen}
515
+ aria-controls={navRegionId}
516
+ data-testid="ds-shell-menu"
517
+ >
518
+ {#if mobileNavOpen}
519
+ <X class="size-4" />
520
+ {:else}
521
+ <Menu class="size-4" />
522
+ {/if}
523
+ </button>
524
+ {/if}
525
+
526
+ <!-- The rail carries the brand on md+; below md the rail is gone, so the
527
+ bar carries it or the app loses its identity on a phone entirely. -->
528
+ <span class={hasNav ? 'flex md:hidden' : 'flex'}>
529
+ {@render brandLockup(true)}
530
+ </span>
531
+
532
+ {#if context}{@render context()}{/if}
533
+
534
+ {#if onSearch && searchPlacement === 'leading'}
535
+ {@render searchButton('min-w-0 flex-1 sm:w-[clamp(200px,32vw,560px)] sm:flex-none')}
536
+ {/if}
537
+
538
+ <div class="ml-auto flex flex-none items-center gap-2 sm:gap-3">
539
+ {#if onSearch && searchPlacement === 'trailing'}
540
+ {@render searchButton('min-w-0 flex-none sm:w-[clamp(200px,32vw,560px)]')}
541
+ {/if}
542
+
543
+ {#if actions}{@render actions()}{/if}
544
+
545
+ {#if themeToggle}
546
+ <button
547
+ onclick={toggleTheme}
548
+ class="border-border text-shell-muted-foreground hover:text-shell-foreground grid size-9 flex-none place-items-center rounded-md border transition-colors"
549
+ aria-label="Toggle theme"
550
+ data-testid="ds-shell-theme"
551
+ >
552
+ <Sun class="size-4 dark:hidden" />
553
+ <Moon class="hidden size-4 dark:block" />
554
+ </button>
555
+ {/if}
556
+
557
+ {#if identity}{@render identity()}{/if}
558
+ </div>
559
+ </header>
560
+
561
+ {#if banner}{@render banner()}{/if}
562
+
563
+ <div class="flex min-h-0 min-w-0 flex-1">
564
+ {#if sidebar}
565
+ <div class="border-border hidden min-h-0 flex-none border-r md:flex">
566
+ {@render sidebar()}
567
+ </div>
568
+ {/if}
569
+ <!--
570
+ The scrolling content region, and the element a consumer's
571
+ "no horizontal overflow" test must measure.
572
+
573
+ `overflow-y: auto` makes `overflow-x` compute to `auto` too, so this
574
+ box — not the document — is where a wide child's excess ends up. Every
575
+ app's overflow check measures `document.documentElement.scrollWidth`,
576
+ which is why one app carried 39px of sideways scroll at 375px with its
577
+ suite green throughout: the number it was reading could not move.
578
+
579
+ `data-slot` rather than the `id` for that: `ds-main` is the skip link's
580
+ target and an app is free to have its own `#ds-main` ambitions, whereas
581
+ the slot marker is the same stable hook every other component here
582
+ exposes. `harness/drive.mjs` measures this element at 375px and 320px
583
+ and prints the document-level number beside it, blind, for contrast.
584
+ -->
585
+ <!--
586
+ `texture` paints the house atmosphere HERE, on the scroller itself,
587
+ through `.ds-shell-texture` in styles.css. It is this element rather
588
+ than the content box below because the texture is the FLOOR the page
589
+ sits on: capped by `measure`, that box is narrower than the region, and
590
+ a floor that stopped at the measure would be a stripe.
591
+
592
+ It is a background rather than a layer, and that is the whole design
593
+ rather than an implementation detail. An absolutely positioned child
594
+ inside a scroller is the shape both surveyed apps reached for first and
595
+ it is subtly wrong three ways: at `z-index: auto` a positioned box
596
+ paints ABOVE non-positioned content, so the "atmosphere" tints the page
597
+ (invisible only because it is faint); `z-index: -1` fixes that by
598
+ sinking it behind an ancestor's background instead, since neither this
599
+ element nor `.ds-shell` opens a stacking context; and it needs
600
+ `pointer-events: none` to stop eating clicks. A background layer has
601
+ none of those problems by construction — it cannot be hit-tested, it
602
+ always paints beneath every descendant, it adds no box to the flex
603
+ column, and it opens no stacking context. The one app that had already
604
+ moved to a background did so after hitting exactly the first failure.
605
+
606
+ `background-attachment: local` is what makes it the page's floor rather
607
+ than a backdrop: the default (`scroll`) pins a scroll container's
608
+ background to its border box, so the dots would sit frozen while the
609
+ content slid over them. `harness/drive.mjs` proves the difference by
610
+ scrolling half a grid pitch and comparing pixels, with `scroll` forced
611
+ on as the control.
612
+ -->
613
+ <main
614
+ id="ds-main"
615
+ data-slot="app-shell-content"
616
+ tabindex="-1"
617
+ class={cn(
618
+ 'relative flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto',
619
+ textured && 'ds-shell-texture',
620
+ mainClass
621
+ )}
622
+ data-texture={textured ?? undefined}
623
+ >
624
+ <!--
625
+ `min-w-0` on a flex child, for the same reason `<main>` and both
626
+ boxes above it carry it: a flex item's default `min-width: auto` can
627
+ size it by its own min-content rather than by its container.
628
+
629
+ Measured rather than assumed, and the measurement is worth writing
630
+ down because it contradicts the obvious reading: in THIS structure
631
+ the declaration is currently INERT. The automatic minimum size
632
+ applies to a flex item's MAIN axis, and `<main>` is a column, so this
633
+ box's width is already cross-axis stretch and cannot grow past its
634
+ container. Driving the harness at 375px and 320px, with and without
635
+ the declaration, against a plain page wrapper and an `mx-auto` one,
636
+ moved nothing — `harness/drive.mjs` pins that.
637
+
638
+ It stays because it is the correct declaration for the box, and
639
+ because the day this container becomes a row item the omission would
640
+ be invisible all over again. What it is NOT is the cause of the
641
+ sideways scroll in #5: that comes from a child with no scroller of
642
+ its own, which no sizing rule here can prevent — only the child
643
+ carrying its own scroller can, as this package's Table does. The half
644
+ of that defect the shell genuinely owns is on `<main>` above.
645
+ -->
646
+ <!--
647
+ `measure` caps and centres this box, through `.ds-shell-measure`
648
+ in styles.css rather than a utility here — an arbitrary
649
+ `max-w-[var(…)]` only exists if the consumer's Tailwind build
650
+ scanned the file spelling it, and a cap that silently does not
651
+ compile is the failure mode this package gates against everywhere
652
+ else.
653
+
654
+ At `full` the class and the attribute are both ABSENT, not set to
655
+ a no-op value. That is what makes the prop additive: a consumer
656
+ who never names it gets the same element, the same class string
657
+ and the same declarations as before it existed, so there is
658
+ nothing for the cascade to resolve differently. Both read the
659
+ single `capped` answer above, so they cannot disagree.
660
+ -->
661
+ <div
662
+ class={cn(
663
+ 'flex min-h-0 min-w-0 flex-1 flex-col',
664
+ 'w-full',
665
+ capped && 'ds-shell-measure',
666
+ padded && 'px-4 py-5 sm:px-6 md:px-8 md:py-7 2xl:px-12'
667
+ )}
668
+ data-measure={capped ?? undefined}
669
+ >
670
+ {@render children()}
671
+ </div>
672
+ </main>
673
+ </div>
674
+ </div>
675
+ </div>