@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.
- package/README.md +624 -0
- package/dist/components/ui/alert/alert-description.svelte +23 -0
- package/dist/components/ui/alert/alert-description.svelte.d.ts +9 -0
- package/dist/components/ui/alert/alert-title.svelte +23 -0
- package/dist/components/ui/alert/alert-title.svelte.d.ts +9 -0
- package/dist/components/ui/alert/alert.svelte +46 -0
- package/dist/components/ui/alert/alert.svelte.d.ts +28 -0
- package/dist/components/ui/alert/index.d.ts +4 -0
- package/dist/components/ui/alert/index.js +4 -0
- package/dist/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
- package/dist/components/ui/alert-dialog/alert-dialog-action.svelte.d.ts +9 -0
- package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
- package/dist/components/ui/alert-dialog/alert-dialog-cancel.svelte.d.ts +9 -0
- package/dist/components/ui/alert-dialog/alert-dialog-content.svelte +39 -0
- package/dist/components/ui/alert-dialog/alert-dialog-content.svelte.d.ts +11 -0
- package/dist/components/ui/alert-dialog/alert-dialog-description.svelte +20 -0
- package/dist/components/ui/alert-dialog/alert-dialog-description.svelte.d.ts +4 -0
- package/dist/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
- package/dist/components/ui/alert-dialog/alert-dialog-footer.svelte.d.ts +5 -0
- package/dist/components/ui/alert-dialog/alert-dialog-header.svelte +23 -0
- package/dist/components/ui/alert-dialog/alert-dialog-header.svelte.d.ts +5 -0
- package/dist/components/ui/alert-dialog/alert-dialog-media.svelte +23 -0
- package/dist/components/ui/alert-dialog/alert-dialog-media.svelte.d.ts +5 -0
- package/dist/components/ui/alert-dialog/alert-dialog-overlay.svelte +20 -0
- package/dist/components/ui/alert-dialog/alert-dialog-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/dist/components/ui/alert-dialog/alert-dialog-portal.svelte.d.ts +3 -0
- package/dist/components/ui/alert-dialog/alert-dialog-title.svelte +20 -0
- package/dist/components/ui/alert-dialog/alert-dialog-title.svelte.d.ts +4 -0
- package/dist/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/dist/components/ui/alert-dialog/alert-dialog-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/alert-dialog/alert-dialog.svelte +7 -0
- package/dist/components/ui/alert-dialog/alert-dialog.svelte.d.ts +3 -0
- package/dist/components/ui/alert-dialog/index.d.ts +13 -0
- package/dist/components/ui/alert-dialog/index.js +15 -0
- package/dist/components/ui/app-dialog/app-dialog.svelte +111 -0
- package/dist/components/ui/app-dialog/app-dialog.svelte.d.ts +41 -0
- package/dist/components/ui/app-dialog/index.d.ts +2 -0
- package/dist/components/ui/app-dialog/index.js +2 -0
- package/dist/components/ui/app-shell/app-nav.svelte +299 -0
- package/dist/components/ui/app-shell/app-nav.svelte.d.ts +22 -0
- package/dist/components/ui/app-shell/app-shell.svelte +675 -0
- package/dist/components/ui/app-shell/app-shell.svelte.d.ts +157 -0
- package/dist/components/ui/app-shell/index.d.ts +6 -0
- package/dist/components/ui/app-shell/index.js +6 -0
- package/dist/components/ui/app-shell/measure.d.ts +28 -0
- package/dist/components/ui/app-shell/measure.js +21 -0
- package/dist/components/ui/app-shell/texture.d.ts +32 -0
- package/dist/components/ui/app-shell/texture.js +27 -0
- package/dist/components/ui/app-shell/types.d.ts +134 -0
- package/dist/components/ui/app-shell/types.js +88 -0
- package/dist/components/ui/arc-gauge/arc-gauge.svelte +112 -0
- package/dist/components/ui/arc-gauge/arc-gauge.svelte.d.ts +15 -0
- package/dist/components/ui/arc-gauge/index.d.ts +3 -0
- package/dist/components/ui/arc-gauge/index.js +2 -0
- package/dist/components/ui/avatar/avatar-fallback.svelte +26 -0
- package/dist/components/ui/avatar/avatar-fallback.svelte.d.ts +4 -0
- package/dist/components/ui/avatar/avatar-image.svelte +17 -0
- package/dist/components/ui/avatar/avatar-image.svelte.d.ts +4 -0
- package/dist/components/ui/avatar/avatar.svelte +31 -0
- package/dist/components/ui/avatar/avatar.svelte.d.ts +4 -0
- package/dist/components/ui/avatar/index.d.ts +4 -0
- package/dist/components/ui/avatar/index.js +6 -0
- package/dist/components/ui/badge/badge.svelte +50 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +38 -0
- package/dist/components/ui/badge/index.d.ts +2 -0
- package/dist/components/ui/badge/index.js +2 -0
- package/dist/components/ui/bar-row/bar-row.svelte +42 -0
- package/dist/components/ui/bar-row/bar-row.svelte.d.ts +12 -0
- package/dist/components/ui/bar-row/index.d.ts +2 -0
- package/dist/components/ui/bar-row/index.js +2 -0
- package/dist/components/ui/button/button.svelte +89 -0
- package/dist/components/ui/button/button.svelte.d.ts +70 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +4 -0
- package/dist/components/ui/card/card-action.svelte +23 -0
- package/dist/components/ui/card/card-action.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-content.svelte +20 -0
- package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-description.svelte +20 -0
- package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-footer.svelte +23 -0
- package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-header.svelte +23 -0
- package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
- package/dist/components/ui/card/card-title.svelte +52 -0
- package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
- package/dist/components/ui/card/card.svelte +25 -0
- package/dist/components/ui/card/card.svelte.d.ts +8 -0
- package/dist/components/ui/card/index.d.ts +8 -0
- package/dist/components/ui/card/index.js +10 -0
- package/dist/components/ui/checkbox/checkbox.svelte +36 -0
- package/dist/components/ui/checkbox/checkbox.svelte.d.ts +8 -0
- package/dist/components/ui/checkbox/index.d.ts +2 -0
- package/dist/components/ui/checkbox/index.js +2 -0
- package/dist/components/ui/command/command-dialog.svelte +42 -0
- package/dist/components/ui/command/command-dialog.svelte.d.ts +14 -0
- package/dist/components/ui/command/command-empty.svelte +17 -0
- package/dist/components/ui/command/command-empty.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-group.svelte +33 -0
- package/dist/components/ui/command/command-group.svelte.d.ts +7 -0
- package/dist/components/ui/command/command-input.svelte +36 -0
- package/dist/components/ui/command/command-input.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-item.svelte +27 -0
- package/dist/components/ui/command/command-item.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-link-item.svelte +20 -0
- package/dist/components/ui/command/command-link-item.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-list.svelte +20 -0
- package/dist/components/ui/command/command-list.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-loading.svelte +7 -0
- package/dist/components/ui/command/command-loading.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-separator.svelte +17 -0
- package/dist/components/ui/command/command-separator.svelte.d.ts +4 -0
- package/dist/components/ui/command/command-shortcut.svelte +23 -0
- package/dist/components/ui/command/command-shortcut.svelte.d.ts +5 -0
- package/dist/components/ui/command/command.svelte +28 -0
- package/dist/components/ui/command/command.svelte.d.ts +8 -0
- package/dist/components/ui/command/index.d.ts +12 -0
- package/dist/components/ui/command/index.js +14 -0
- package/dist/components/ui/command-palette/command-palette.svelte +94 -0
- package/dist/components/ui/command-palette/command-palette.svelte.d.ts +40 -0
- package/dist/components/ui/command-palette/index.d.ts +3 -0
- package/dist/components/ui/command-palette/index.js +2 -0
- package/dist/components/ui/context-column/context-column.svelte +56 -0
- package/dist/components/ui/context-column/context-column.svelte.d.ts +14 -0
- package/dist/components/ui/context-column/index.d.ts +2 -0
- package/dist/components/ui/context-column/index.js +2 -0
- package/dist/components/ui/data-table/data-table.svelte.d.ts +40 -0
- package/dist/components/ui/data-table/data-table.svelte.js +110 -0
- package/dist/components/ui/data-table/flex-render.svelte +40 -0
- package/dist/components/ui/data-table/flex-render.svelte.d.ts +33 -0
- package/dist/components/ui/data-table/index.d.ts +3 -0
- package/dist/components/ui/data-table/index.js +3 -0
- package/dist/components/ui/data-table/render-helpers.d.ts +90 -0
- package/dist/components/ui/data-table/render-helpers.js +99 -0
- package/dist/components/ui/data-table-tanstack/data-table-tanstack.svelte +247 -0
- package/dist/components/ui/data-table-tanstack/data-table-tanstack.svelte.d.ts +79 -0
- package/dist/components/ui/data-table-tanstack/index.d.ts +3 -0
- package/dist/components/ui/data-table-tanstack/index.js +2 -0
- package/dist/components/ui/data-table-toolbar/data-table-toolbar.svelte +102 -0
- package/dist/components/ui/data-table-toolbar/data-table-toolbar.svelte.d.ts +32 -0
- package/dist/components/ui/data-table-toolbar/index.d.ts +3 -0
- package/dist/components/ui/data-table-toolbar/index.js +2 -0
- package/dist/components/ui/detail-panel/detail-panel.svelte +95 -0
- package/dist/components/ui/detail-panel/detail-panel.svelte.d.ts +26 -0
- package/dist/components/ui/detail-panel/index.d.ts +2 -0
- package/dist/components/ui/detail-panel/index.js +2 -0
- package/dist/components/ui/dialog/dialog-close.svelte +11 -0
- package/dist/components/ui/dialog/dialog-close.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-content.svelte +48 -0
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +13 -0
- package/dist/components/ui/dialog/dialog-description.svelte +20 -0
- package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-footer.svelte +35 -0
- package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +8 -0
- package/dist/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte +20 -0
- package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-portal.svelte +7 -0
- package/dist/components/ui/dialog/dialog-portal.svelte.d.ts +3 -0
- package/dist/components/ui/dialog/dialog-title.svelte +17 -0
- package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/dist/components/ui/dialog/dialog-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/dialog/dialog.svelte +7 -0
- package/dist/components/ui/dialog/dialog.svelte.d.ts +3 -0
- package/dist/components/ui/dialog/index.d.ts +11 -0
- package/dist/components/ui/dialog/index.js +13 -0
- package/dist/components/ui/dialog-section/dialog-section.svelte +20 -0
- package/dist/components/ui/dialog-section/dialog-section.svelte.d.ts +8 -0
- package/dist/components/ui/dialog-section/index.d.ts +2 -0
- package/dist/components/ui/dialog-section/index.js +2 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +9 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +10 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +27 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +23 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte.d.ts +5 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/index.d.ts +18 -0
- package/dist/components/ui/dropdown-menu/index.js +18 -0
- package/dist/components/ui/empty-state/empty-state.svelte +44 -0
- package/dist/components/ui/empty-state/empty-state.svelte.d.ts +14 -0
- package/dist/components/ui/empty-state/index.d.ts +2 -0
- package/dist/components/ui/empty-state/index.js +2 -0
- package/dist/components/ui/error-state/error-state.svelte +52 -0
- package/dist/components/ui/error-state/error-state.svelte.d.ts +10 -0
- package/dist/components/ui/error-state/index.d.ts +2 -0
- package/dist/components/ui/error-state/index.js +2 -0
- package/dist/components/ui/info-tip/index.d.ts +2 -0
- package/dist/components/ui/info-tip/index.js +2 -0
- package/dist/components/ui/info-tip/info-tip.svelte +37 -0
- package/dist/components/ui/info-tip/info-tip.svelte.d.ts +10 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +48 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/input-group/index.d.ts +7 -0
- package/dist/components/ui/input-group/index.js +9 -0
- package/dist/components/ui/input-group/input-group-addon.svelte +53 -0
- package/dist/components/ui/input-group/input-group-addon.svelte.d.ts +32 -0
- package/dist/components/ui/input-group/input-group-button.svelte +49 -0
- package/dist/components/ui/input-group/input-group-button.svelte.d.ts +32 -0
- package/dist/components/ui/input-group/input-group-input.svelte +23 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +11 -0
- package/dist/components/ui/input-group/input-group-text.svelte +22 -0
- package/dist/components/ui/input-group/input-group-text.svelte.d.ts +5 -0
- package/dist/components/ui/input-group/input-group-textarea.svelte +23 -0
- package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +3 -0
- package/dist/components/ui/input-group/input-group.svelte +24 -0
- package/dist/components/ui/input-group/input-group.svelte.d.ts +5 -0
- package/dist/components/ui/label/index.d.ts +2 -0
- package/dist/components/ui/label/index.js +4 -0
- package/dist/components/ui/label/label.svelte +20 -0
- package/dist/components/ui/label/label.svelte.d.ts +4 -0
- package/dist/components/ui/loading-state/index.d.ts +2 -0
- package/dist/components/ui/loading-state/index.js +2 -0
- package/dist/components/ui/loading-state/loading-state.svelte +28 -0
- package/dist/components/ui/loading-state/loading-state.svelte.d.ts +8 -0
- package/dist/components/ui/page-header/index.d.ts +2 -0
- package/dist/components/ui/page-header/index.js +2 -0
- package/dist/components/ui/page-header/page-header.svelte +100 -0
- package/dist/components/ui/page-header/page-header.svelte.d.ts +17 -0
- package/dist/components/ui/panel/index.d.ts +2 -0
- package/dist/components/ui/panel/index.js +2 -0
- package/dist/components/ui/panel/panel.svelte +54 -0
- package/dist/components/ui/panel/panel.svelte.d.ts +16 -0
- package/dist/components/ui/password-input/index.d.ts +2 -0
- package/dist/components/ui/password-input/index.js +4 -0
- package/dist/components/ui/password-input/password-input.svelte +48 -0
- package/dist/components/ui/password-input/password-input.svelte.d.ts +8 -0
- package/dist/components/ui/popover/index.d.ts +4 -0
- package/dist/components/ui/popover/index.js +6 -0
- package/dist/components/ui/popover/popover-content.svelte +45 -0
- package/dist/components/ui/popover/popover-content.svelte.d.ts +7 -0
- package/dist/components/ui/popover/popover-trigger.svelte +7 -0
- package/dist/components/ui/popover/popover-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/popover/popover.svelte +7 -0
- package/dist/components/ui/popover/popover.svelte.d.ts +3 -0
- package/dist/components/ui/progress/index.d.ts +2 -0
- package/dist/components/ui/progress/index.js +4 -0
- package/dist/components/ui/progress/progress.svelte +30 -0
- package/dist/components/ui/progress/progress.svelte.d.ts +4 -0
- package/dist/components/ui/scorecard/index.d.ts +2 -0
- package/dist/components/ui/scorecard/index.js +2 -0
- package/dist/components/ui/scorecard/scorecard.svelte +35 -0
- package/dist/components/ui/scorecard/scorecard.svelte.d.ts +8 -0
- package/dist/components/ui/select/index.d.ts +12 -0
- package/dist/components/ui/select/index.js +14 -0
- package/dist/components/ui/select/select-content.svelte +45 -0
- package/dist/components/ui/select/select-content.svelte.d.ts +11 -0
- package/dist/components/ui/select/select-group-heading.svelte +21 -0
- package/dist/components/ui/select/select-group-heading.svelte.d.ts +10 -0
- package/dist/components/ui/select/select-group.svelte +17 -0
- package/dist/components/ui/select/select-group.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-item.svelte +38 -0
- package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-label.svelte +20 -0
- package/dist/components/ui/select/select-label.svelte.d.ts +6 -0
- package/dist/components/ui/select/select-portal.svelte +7 -0
- package/dist/components/ui/select/select-portal.svelte.d.ts +3 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte +23 -0
- package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte +23 -0
- package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-separator.svelte +18 -0
- package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
- package/dist/components/ui/select/select-trigger.svelte +29 -0
- package/dist/components/ui/select/select-trigger.svelte.d.ts +8 -0
- package/dist/components/ui/select/select.svelte +11 -0
- package/dist/components/ui/select/select.svelte.d.ts +3 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/index.js +4 -0
- package/dist/components/ui/separator/separator.svelte +23 -0
- package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
- package/dist/components/ui/skeleton/index.d.ts +2 -0
- package/dist/components/ui/skeleton/index.js +4 -0
- package/dist/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
- package/dist/components/ui/sonner/index.d.ts +1 -0
- package/dist/components/ui/sonner/index.js +1 -0
- package/dist/components/ui/sonner/sonner.svelte +34 -0
- package/dist/components/ui/sonner/sonner.svelte.d.ts +4 -0
- package/dist/components/ui/sparkline/index.d.ts +3 -0
- package/dist/components/ui/sparkline/index.js +2 -0
- package/dist/components/ui/sparkline/sparkline.svelte +90 -0
- package/dist/components/ui/sparkline/sparkline.svelte.d.ts +16 -0
- package/dist/components/ui/stat-card/index.d.ts +2 -0
- package/dist/components/ui/stat-card/index.js +2 -0
- package/dist/components/ui/stat-card/stat-card.svelte +77 -0
- package/dist/components/ui/stat-card/stat-card.svelte.d.ts +34 -0
- package/dist/components/ui/stat-list/index.d.ts +3 -0
- package/dist/components/ui/stat-list/index.js +2 -0
- package/dist/components/ui/stat-list/stat-list.svelte +80 -0
- package/dist/components/ui/stat-list/stat-list.svelte.d.ts +23 -0
- package/dist/components/ui/status/index.d.ts +1 -0
- package/dist/components/ui/status/index.js +1 -0
- package/dist/components/ui/status-badge/index.d.ts +3 -0
- package/dist/components/ui/status-badge/index.js +2 -0
- package/dist/components/ui/status-badge/status-badge.svelte +53 -0
- package/dist/components/ui/status-badge/status-badge.svelte.d.ts +39 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +39 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
- package/dist/components/ui/table/index.d.ts +10 -0
- package/dist/components/ui/table/index.js +12 -0
- package/dist/components/ui/table/table-body.svelte +20 -0
- package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-caption.svelte +20 -0
- package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-cell.svelte +20 -0
- package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-classes.d.ts +15 -0
- package/dist/components/ui/table/table-classes.js +15 -0
- package/dist/components/ui/table/table-footer.svelte +20 -0
- package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-head.svelte +23 -0
- package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-header.svelte +20 -0
- package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
- package/dist/components/ui/table/table-row.svelte +23 -0
- package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
- package/dist/components/ui/table/table.svelte +37 -0
- package/dist/components/ui/table/table.svelte.d.ts +20 -0
- package/dist/components/ui/tabs/index.d.ts +5 -0
- package/dist/components/ui/tabs/index.js +7 -0
- package/dist/components/ui/tabs/tabs-content.svelte +13 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +16 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +16 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs.svelte +19 -0
- package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/ui/textarea/index.d.ts +2 -0
- package/dist/components/ui/textarea/index.js +4 -0
- package/dist/components/ui/textarea/textarea.svelte +22 -0
- package/dist/components/ui/textarea/textarea.svelte.d.ts +5 -0
- package/dist/components/ui/tile-grid/index.d.ts +2 -0
- package/dist/components/ui/tile-grid/index.js +2 -0
- package/dist/components/ui/tile-grid/tile-grid.svelte +66 -0
- package/dist/components/ui/tile-grid/tile-grid.svelte.d.ts +43 -0
- package/dist/components/ui/tooltip/index.d.ts +6 -0
- package/dist/components/ui/tooltip/index.js +8 -0
- package/dist/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +11 -0
- package/dist/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-portal.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-provider.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip.svelte.d.ts +4 -0
- package/dist/styles.css +961 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +5 -0
- 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>
|