@c9up/nebula 0.1.0
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/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/adapters/css.d.ts +33 -0
- package/dist/adapters/css.js +67 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.js +27 -0
- package/dist/adapters/tailwind.d.ts +25 -0
- package/dist/adapters/tailwind.js +122 -0
- package/dist/adapters/types.d.ts +49 -0
- package/dist/adapters/types.js +21 -0
- package/dist/adapters/unocss.d.ts +17 -0
- package/dist/adapters/unocss.js +107 -0
- package/dist/atoms/AspectRatio.d.ts +17 -0
- package/dist/atoms/AspectRatio.js +19 -0
- package/dist/atoms/Avatar.d.ts +23 -0
- package/dist/atoms/Avatar.js +46 -0
- package/dist/atoms/Badge.d.ts +25 -0
- package/dist/atoms/Badge.js +28 -0
- package/dist/atoms/Button.d.ts +58 -0
- package/dist/atoms/Button.js +67 -0
- package/dist/atoms/Checkbox.d.ts +41 -0
- package/dist/atoms/Checkbox.js +47 -0
- package/dist/atoms/Input.d.ts +36 -0
- package/dist/atoms/Input.js +50 -0
- package/dist/atoms/Kbd.d.ts +14 -0
- package/dist/atoms/Kbd.js +17 -0
- package/dist/atoms/Label.d.ts +24 -0
- package/dist/atoms/Label.js +26 -0
- package/dist/atoms/Marker.d.ts +42 -0
- package/dist/atoms/Marker.js +55 -0
- package/dist/atoms/NativeSelect.d.ts +43 -0
- package/dist/atoms/NativeSelect.js +81 -0
- package/dist/atoms/Progress.d.ts +21 -0
- package/dist/atoms/Progress.js +48 -0
- package/dist/atoms/ScrollArea.d.ts +28 -0
- package/dist/atoms/ScrollArea.js +37 -0
- package/dist/atoms/Separator.d.ts +16 -0
- package/dist/atoms/Separator.js +21 -0
- package/dist/atoms/Skeleton.d.ts +13 -0
- package/dist/atoms/Skeleton.js +18 -0
- package/dist/atoms/Slider.d.ts +30 -0
- package/dist/atoms/Slider.js +40 -0
- package/dist/atoms/Spinner.d.ts +17 -0
- package/dist/atoms/Spinner.js +20 -0
- package/dist/atoms/Switch.d.ts +24 -0
- package/dist/atoms/Switch.js +41 -0
- package/dist/atoms/Textarea.d.ts +28 -0
- package/dist/atoms/Textarea.js +36 -0
- package/dist/atoms/Toggle.d.ts +38 -0
- package/dist/atoms/Toggle.js +45 -0
- package/dist/atoms/index.d.ts +32 -0
- package/dist/atoms/index.js +32 -0
- package/dist/cli/add.d.ts +44 -0
- package/dist/cli/add.js +99 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/registry.d.ts +31 -0
- package/dist/cli/registry.js +83 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.js +43 -0
- package/dist/configure.d.ts +34 -0
- package/dist/configure.js +85 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +41 -0
- package/dist/lib/children.d.ts +28 -0
- package/dist/lib/children.js +27 -0
- package/dist/lib/cn.d.ts +13 -0
- package/dist/lib/cn.js +13 -0
- package/dist/lib/cva.d.ts +83 -0
- package/dist/lib/cva.js +178 -0
- package/dist/lib/icons.d.ts +44 -0
- package/dist/lib/icons.js +79 -0
- package/dist/lib/id.d.ts +31 -0
- package/dist/lib/id.js +41 -0
- package/dist/lib/index.d.ts +12 -0
- package/dist/lib/index.js +12 -0
- package/dist/lib/motion.d.ts +31 -0
- package/dist/lib/motion.js +39 -0
- package/dist/lib/props.d.ts +48 -0
- package/dist/lib/props.js +59 -0
- package/dist/lib/styled.d.ts +31 -0
- package/dist/lib/styled.js +39 -0
- package/dist/molecules/Accordion.d.ts +35 -0
- package/dist/molecules/Accordion.js +117 -0
- package/dist/molecules/Alert.d.ts +30 -0
- package/dist/molecules/Alert.js +35 -0
- package/dist/molecules/Attachment.d.ts +39 -0
- package/dist/molecules/Attachment.js +103 -0
- package/dist/molecules/Breadcrumb.d.ts +29 -0
- package/dist/molecules/Breadcrumb.js +73 -0
- package/dist/molecules/Bubble.d.ts +56 -0
- package/dist/molecules/Bubble.js +92 -0
- package/dist/molecules/ButtonGroup.d.ts +23 -0
- package/dist/molecules/ButtonGroup.js +30 -0
- package/dist/molecules/Card.d.ts +17 -0
- package/dist/molecules/Card.js +17 -0
- package/dist/molecules/Collapsible.d.ts +40 -0
- package/dist/molecules/Collapsible.js +71 -0
- package/dist/molecules/Empty.d.ts +16 -0
- package/dist/molecules/Empty.js +16 -0
- package/dist/molecules/Field.d.ts +54 -0
- package/dist/molecules/Field.js +82 -0
- package/dist/molecules/InputGroup.d.ts +33 -0
- package/dist/molecules/InputGroup.js +35 -0
- package/dist/molecules/InputOTP.d.ts +33 -0
- package/dist/molecules/InputOTP.js +140 -0
- package/dist/molecules/Item.d.ts +41 -0
- package/dist/molecules/Item.js +48 -0
- package/dist/molecules/Message.d.ts +43 -0
- package/dist/molecules/Message.js +82 -0
- package/dist/molecules/Pagination.d.ts +41 -0
- package/dist/molecules/Pagination.js +144 -0
- package/dist/molecules/RadioGroup.d.ts +32 -0
- package/dist/molecules/RadioGroup.js +65 -0
- package/dist/molecules/Resizable.d.ts +37 -0
- package/dist/molecules/Resizable.js +136 -0
- package/dist/molecules/Table.d.ts +37 -0
- package/dist/molecules/Table.js +78 -0
- package/dist/molecules/Tabs.d.ts +37 -0
- package/dist/molecules/Tabs.js +109 -0
- package/dist/molecules/ToggleGroup.d.ts +37 -0
- package/dist/molecules/ToggleGroup.js +68 -0
- package/dist/molecules/Typography.d.ts +26 -0
- package/dist/molecules/Typography.js +67 -0
- package/dist/molecules/index.d.ts +37 -0
- package/dist/molecules/index.js +37 -0
- package/dist/organisms/AlertDialog.d.ts +40 -0
- package/dist/organisms/AlertDialog.js +104 -0
- package/dist/organisms/Calendar.d.ts +96 -0
- package/dist/organisms/Calendar.js +353 -0
- package/dist/organisms/Carousel.d.ts +29 -0
- package/dist/organisms/Carousel.js +128 -0
- package/dist/organisms/Chart.d.ts +55 -0
- package/dist/organisms/Chart.js +189 -0
- package/dist/organisms/Combobox.d.ts +41 -0
- package/dist/organisms/Combobox.js +102 -0
- package/dist/organisms/Command.d.ts +46 -0
- package/dist/organisms/Command.js +171 -0
- package/dist/organisms/CommandDialog.d.ts +42 -0
- package/dist/organisms/CommandDialog.js +116 -0
- package/dist/organisms/ContextMenu.d.ts +29 -0
- package/dist/organisms/ContextMenu.js +102 -0
- package/dist/organisms/DataTable.d.ts +50 -0
- package/dist/organisms/DataTable.js +257 -0
- package/dist/organisms/DatePicker.d.ts +34 -0
- package/dist/organisms/DatePicker.js +115 -0
- package/dist/organisms/DateRangePicker.d.ts +34 -0
- package/dist/organisms/DateRangePicker.js +122 -0
- package/dist/organisms/Dialog.d.ts +50 -0
- package/dist/organisms/Dialog.js +116 -0
- package/dist/organisms/Drawer.d.ts +40 -0
- package/dist/organisms/Drawer.js +131 -0
- package/dist/organisms/DropdownMenu.d.ts +30 -0
- package/dist/organisms/DropdownMenu.js +91 -0
- package/dist/organisms/Form.d.ts +114 -0
- package/dist/organisms/Form.js +151 -0
- package/dist/organisms/HoverCard.d.ts +30 -0
- package/dist/organisms/HoverCard.js +68 -0
- package/dist/organisms/Menubar.d.ts +28 -0
- package/dist/organisms/Menubar.js +147 -0
- package/dist/organisms/MessageScroller.d.ts +36 -0
- package/dist/organisms/MessageScroller.js +124 -0
- package/dist/organisms/NavigationMenu.d.ts +41 -0
- package/dist/organisms/NavigationMenu.js +139 -0
- package/dist/organisms/Popover.d.ts +33 -0
- package/dist/organisms/Popover.js +58 -0
- package/dist/organisms/Questionnaire.d.ts +74 -0
- package/dist/organisms/Questionnaire.js +235 -0
- package/dist/organisms/Select.d.ts +47 -0
- package/dist/organisms/Select.js +250 -0
- package/dist/organisms/Sheet.d.ts +34 -0
- package/dist/organisms/Sheet.js +107 -0
- package/dist/organisms/Sidebar.d.ts +100 -0
- package/dist/organisms/Sidebar.js +228 -0
- package/dist/organisms/Toaster.d.ts +67 -0
- package/dist/organisms/Toaster.js +189 -0
- package/dist/organisms/Tooltip.d.ts +39 -0
- package/dist/organisms/Tooltip.js +94 -0
- package/dist/organisms/index.d.ts +40 -0
- package/dist/organisms/index.js +40 -0
- package/dist/primitives/controllable.d.ts +38 -0
- package/dist/primitives/controllable.js +43 -0
- package/dist/primitives/dismissable.d.ts +53 -0
- package/dist/primitives/dismissable.js +158 -0
- package/dist/primitives/floating.d.ts +116 -0
- package/dist/primitives/floating.js +289 -0
- package/dist/primitives/floatingSurface.d.ts +56 -0
- package/dist/primitives/floatingSurface.js +142 -0
- package/dist/primitives/focusTrap.d.ts +47 -0
- package/dist/primitives/focusTrap.js +101 -0
- package/dist/primitives/focusable.d.ts +53 -0
- package/dist/primitives/focusable.js +109 -0
- package/dist/primitives/index.d.ts +27 -0
- package/dist/primitives/index.js +27 -0
- package/dist/primitives/menu.d.ts +102 -0
- package/dist/primitives/menu.js +356 -0
- package/dist/primitives/modalSurface.d.ts +54 -0
- package/dist/primitives/modalSurface.js +146 -0
- package/dist/primitives/portal.d.ts +34 -0
- package/dist/primitives/portal.js +42 -0
- package/dist/primitives/presence.d.ts +45 -0
- package/dist/primitives/presence.js +152 -0
- package/dist/primitives/rovingFocus.d.ts +47 -0
- package/dist/primitives/rovingFocus.js +176 -0
- package/dist/primitives/scrollLock.d.ts +21 -0
- package/dist/primitives/scrollLock.js +75 -0
- package/dist/primitives/typeahead.d.ts +39 -0
- package/dist/primitives/typeahead.js +123 -0
- package/dist/templates/AppShell.d.ts +30 -0
- package/dist/templates/AppShell.js +42 -0
- package/dist/templates/AuthLayout.d.ts +23 -0
- package/dist/templates/AuthLayout.js +42 -0
- package/dist/templates/SettingsLayout.d.ts +27 -0
- package/dist/templates/SettingsLayout.js +42 -0
- package/dist/templates/index.d.ts +16 -0
- package/dist/templates/index.js +16 -0
- package/nebula.css +2 -0
- package/package.json +92 -0
- package/registry.json +1107 -0
- package/src/adapters/css.ts +75 -0
- package/src/adapters/index.ts +40 -0
- package/src/adapters/tailwind.ts +134 -0
- package/src/adapters/types.ts +53 -0
- package/src/adapters/unocss.ts +116 -0
- package/src/atoms/AspectRatio.ts +28 -0
- package/src/atoms/Avatar.ts +67 -0
- package/src/atoms/Badge.ts +46 -0
- package/src/atoms/Button.ts +96 -0
- package/src/atoms/Checkbox.ts +76 -0
- package/src/atoms/Input.ts +76 -0
- package/src/atoms/Kbd.ts +28 -0
- package/src/atoms/Label.ts +38 -0
- package/src/atoms/Marker.ts +79 -0
- package/src/atoms/NativeSelect.ts +115 -0
- package/src/atoms/Progress.ts +64 -0
- package/src/atoms/ScrollArea.ts +53 -0
- package/src/atoms/Separator.ts +36 -0
- package/src/atoms/Skeleton.ts +24 -0
- package/src/atoms/Slider.ts +65 -0
- package/src/atoms/Spinner.ts +28 -0
- package/src/atoms/Switch.ts +56 -0
- package/src/atoms/Textarea.ts +58 -0
- package/src/atoms/Toggle.ts +67 -0
- package/src/atoms/index.ts +62 -0
- package/src/cli/add.ts +126 -0
- package/src/cli/index.ts +22 -0
- package/src/cli/registry.ts +116 -0
- package/src/config.ts +102 -0
- package/src/configure.ts +127 -0
- package/src/index.ts +53 -0
- package/src/lib/children.ts +43 -0
- package/src/lib/cn.ts +14 -0
- package/src/lib/cva.ts +249 -0
- package/src/lib/icons.ts +138 -0
- package/src/lib/id.ts +44 -0
- package/src/lib/index.ts +21 -0
- package/src/lib/motion.ts +48 -0
- package/src/lib/props.ts +83 -0
- package/src/lib/styled.ts +60 -0
- package/src/molecules/Accordion.ts +161 -0
- package/src/molecules/Alert.ts +58 -0
- package/src/molecules/Attachment.ts +132 -0
- package/src/molecules/Breadcrumb.ts +107 -0
- package/src/molecules/Bubble.ts +134 -0
- package/src/molecules/ButtonGroup.ts +55 -0
- package/src/molecules/Card.ts +42 -0
- package/src/molecules/Collapsible.ts +89 -0
- package/src/molecules/Empty.ts +43 -0
- package/src/molecules/Field.ts +131 -0
- package/src/molecules/InputGroup.ts +62 -0
- package/src/molecules/InputOTP.ts +166 -0
- package/src/molecules/Item.ts +89 -0
- package/src/molecules/Message.ts +127 -0
- package/src/molecules/Pagination.ts +197 -0
- package/src/molecules/RadioGroup.ts +104 -0
- package/src/molecules/Resizable.ts +166 -0
- package/src/molecules/Table.ts +118 -0
- package/src/molecules/Tabs.ts +148 -0
- package/src/molecules/ToggleGroup.ts +109 -0
- package/src/molecules/Typography.ts +94 -0
- package/src/molecules/index.ts +144 -0
- package/src/organisms/AlertDialog.ts +133 -0
- package/src/organisms/Calendar.ts +470 -0
- package/src/organisms/Carousel.ts +161 -0
- package/src/organisms/Chart.ts +257 -0
- package/src/organisms/Combobox.ts +140 -0
- package/src/organisms/Command.ts +211 -0
- package/src/organisms/CommandDialog.ts +149 -0
- package/src/organisms/ContextMenu.ts +125 -0
- package/src/organisms/DataTable.ts +328 -0
- package/src/organisms/DatePicker.ts +141 -0
- package/src/organisms/DateRangePicker.ts +149 -0
- package/src/organisms/Dialog.ts +162 -0
- package/src/organisms/Drawer.ts +167 -0
- package/src/organisms/DropdownMenu.ts +112 -0
- package/src/organisms/Form.ts +217 -0
- package/src/organisms/HoverCard.ts +92 -0
- package/src/organisms/Menubar.ts +167 -0
- package/src/organisms/MessageScroller.ts +145 -0
- package/src/organisms/NavigationMenu.ts +184 -0
- package/src/organisms/Popover.ts +84 -0
- package/src/organisms/Questionnaire.ts +322 -0
- package/src/organisms/Select.ts +286 -0
- package/src/organisms/Sheet.ts +144 -0
- package/src/organisms/Sidebar.ts +336 -0
- package/src/organisms/Toaster.ts +246 -0
- package/src/organisms/Tooltip.ts +124 -0
- package/src/organisms/index.ts +134 -0
- package/src/primitives/controllable.ts +64 -0
- package/src/primitives/dismissable.ts +195 -0
- package/src/primitives/floating.ts +453 -0
- package/src/primitives/floatingSurface.ts +209 -0
- package/src/primitives/focusTrap.ts +132 -0
- package/src/primitives/focusable.ts +108 -0
- package/src/primitives/index.ts +88 -0
- package/src/primitives/menu.ts +491 -0
- package/src/primitives/modalSurface.ts +199 -0
- package/src/primitives/portal.ts +61 -0
- package/src/primitives/presence.ts +185 -0
- package/src/primitives/rovingFocus.ts +212 -0
- package/src/primitives/scrollLock.ts +79 -0
- package/src/primitives/typeahead.ts +156 -0
- package/src/templates/AppShell.ts +56 -0
- package/src/templates/AuthLayout.ts +66 -0
- package/src/templates/SettingsLayout.ts +63 -0
- package/src/templates/index.ts +21 -0
- package/theme.css +151 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sheet — a panel that slides in from an edge.
|
|
3
|
+
*
|
|
4
|
+
* Structurally a Dialog whose panel is pinned to a side rather than centred,
|
|
5
|
+
* so it shares `modalSurface` and everything that comes with it. What differs
|
|
6
|
+
* is the motion: it translates rather than scaling, and the distance depends
|
|
7
|
+
* on the panel's own size, which only `translate-x-full` knows. That is why
|
|
8
|
+
* `slideFrom` produces a transition and not a keyframe animation.
|
|
9
|
+
*
|
|
10
|
+
* `inset-y-0` for the side panels and `inset-x-0` for the top and bottom ones
|
|
11
|
+
* is what makes the sheet span its edge; the opposite axis takes the width or
|
|
12
|
+
* height. Getting that pair the wrong way round produces a panel floating in a
|
|
13
|
+
* corner, which is the usual first attempt.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { component, html } from "@c9up/aurora";
|
|
17
|
+
import type { Child } from "../lib/children.js";
|
|
18
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
19
|
+
import { cn } from "../lib/cn.js";
|
|
20
|
+
import { XIcon } from "../lib/icons.js";
|
|
21
|
+
import { uid } from "../lib/id.js";
|
|
22
|
+
import { fadeInOut, slideFrom } from "../lib/motion.js";
|
|
23
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
24
|
+
import { controllable } from "../primitives/controllable.js";
|
|
25
|
+
import type { Side } from "../primitives/floating.js";
|
|
26
|
+
import { modalSurface } from "../primitives/modalSurface.js";
|
|
27
|
+
import { dialogBackdropClasses } from "./Dialog.js";
|
|
28
|
+
|
|
29
|
+
export interface SheetProps {
|
|
30
|
+
trigger?: Slot;
|
|
31
|
+
title: Child;
|
|
32
|
+
description?: Child;
|
|
33
|
+
children?: Slot;
|
|
34
|
+
footer?: Slot;
|
|
35
|
+
/** Which edge it comes from. Default `"right"`. */
|
|
36
|
+
side?: Side;
|
|
37
|
+
open?: Reactive<boolean>;
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
onOpenChange?: (open: boolean) => void;
|
|
40
|
+
srOnlyTitle?: boolean;
|
|
41
|
+
triggerClass?: Reactive<string>;
|
|
42
|
+
contentClass?: Reactive<string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Edge placement and the axis the panel is sized on. */
|
|
46
|
+
function panelClassesFor(side: Side): string {
|
|
47
|
+
if (side === "left") {
|
|
48
|
+
return "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm";
|
|
49
|
+
}
|
|
50
|
+
if (side === "right") {
|
|
51
|
+
return "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm";
|
|
52
|
+
}
|
|
53
|
+
if (side === "top") return "inset-x-0 top-0 h-auto border-b";
|
|
54
|
+
return "inset-x-0 bottom-0 h-auto border-t";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Sheet = component<SheetProps>((props) => {
|
|
58
|
+
const side = props.side ?? "right";
|
|
59
|
+
const triggerId = uid("sheet-trigger");
|
|
60
|
+
const panelId = uid("sheet-panel");
|
|
61
|
+
const titleId = uid("sheet-title");
|
|
62
|
+
const descriptionId = uid("sheet-description");
|
|
63
|
+
|
|
64
|
+
const state = controllable<boolean>({
|
|
65
|
+
value: props.open,
|
|
66
|
+
initial: props.defaultOpen ?? false,
|
|
67
|
+
onChange: props.onOpenChange,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
modalSurface({
|
|
71
|
+
open: () => state.current(),
|
|
72
|
+
onClose: () => state.set(false),
|
|
73
|
+
panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
|
|
74
|
+
returnFocus: () => document.getElementById(triggerId),
|
|
75
|
+
content: () =>
|
|
76
|
+
html`<div data-slot="sheet-overlay" class="${cn("fixed inset-0 z-50", fadeInOut)}">
|
|
77
|
+
<div class="${dialogBackdropClasses}"></div>
|
|
78
|
+
<div
|
|
79
|
+
data-slot="sheet-content"
|
|
80
|
+
data-side="${side}"
|
|
81
|
+
id="${panelId}"
|
|
82
|
+
role="dialog"
|
|
83
|
+
aria-modal="true"
|
|
84
|
+
aria-labelledby="${titleId}"
|
|
85
|
+
aria-describedby="${props.description === undefined ? undefined : descriptionId}"
|
|
86
|
+
tabindex="-1"
|
|
87
|
+
class="${cn(
|
|
88
|
+
"bg-background fixed z-50 flex flex-col gap-4 p-6 shadow-lg outline-none",
|
|
89
|
+
panelClassesFor(side),
|
|
90
|
+
slideFrom(side),
|
|
91
|
+
read(props.contentClass),
|
|
92
|
+
)}"
|
|
93
|
+
>
|
|
94
|
+
<div data-slot="sheet-header" class="flex flex-col gap-1.5">
|
|
95
|
+
<h2
|
|
96
|
+
id="${titleId}"
|
|
97
|
+
class="${cn(
|
|
98
|
+
"text-foreground font-semibold",
|
|
99
|
+
props.srOnlyTitle === true ? "sr-only" : "",
|
|
100
|
+
)}"
|
|
101
|
+
>${props.title}</h2>
|
|
102
|
+
${
|
|
103
|
+
props.description === undefined
|
|
104
|
+
? null
|
|
105
|
+
: html`<p id="${descriptionId}" class="text-muted-foreground text-sm">
|
|
106
|
+
${props.description}
|
|
107
|
+
</p>`
|
|
108
|
+
}
|
|
109
|
+
</div>
|
|
110
|
+
<div data-slot="sheet-body" class="flex-1 overflow-y-auto">${slot(props.children)}</div>
|
|
111
|
+
${
|
|
112
|
+
props.footer === undefined
|
|
113
|
+
? null
|
|
114
|
+
: html`<div data-slot="sheet-footer" class="mt-auto flex flex-col gap-2">
|
|
115
|
+
${slot(props.footer)}
|
|
116
|
+
</div>`
|
|
117
|
+
}
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
data-slot="sheet-close"
|
|
121
|
+
class="ring-offset-background focus-visible:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:outline-none"
|
|
122
|
+
@click="${() => state.set(false)}"
|
|
123
|
+
>
|
|
124
|
+
${XIcon({ class: "size-4" })}
|
|
125
|
+
<span class="sr-only">Close</span>
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>`,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (props.trigger === undefined) {
|
|
132
|
+
return html`<span data-slot="sheet" hidden></span>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return html`<button
|
|
136
|
+
type="button"
|
|
137
|
+
data-slot="sheet-trigger"
|
|
138
|
+
id="${triggerId}"
|
|
139
|
+
aria-haspopup="dialog"
|
|
140
|
+
aria-expanded="${() => (state.current() ? "true" : "false")}"
|
|
141
|
+
class="${() => cn(read(props.triggerClass))}"
|
|
142
|
+
@click="${() => state.set(true)}"
|
|
143
|
+
>${slot(props.trigger)}</button>`;
|
|
144
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar — the application shell's navigation column.
|
|
3
|
+
*
|
|
4
|
+
* Two presentations from one definition, because a sidebar is not a component
|
|
5
|
+
* so much as a layout decision that changes with the viewport:
|
|
6
|
+
*
|
|
7
|
+
* - On a wide screen it is a column beside the content, collapsible to a strip
|
|
8
|
+
* of icons or away entirely.
|
|
9
|
+
* - On a narrow one it is a Sheet. A 16rem column on a phone leaves nothing
|
|
10
|
+
* for the content, and every attempt to keep it visible ends up as a drawer
|
|
11
|
+
* anyway.
|
|
12
|
+
*
|
|
13
|
+
* The collapsed state is persisted in a cookie rather than `localStorage`. The
|
|
14
|
+
* server renders the shell, and only a cookie is readable there — with
|
|
15
|
+
* `localStorage` the sidebar renders expanded, then snaps shut once the client
|
|
16
|
+
* hydrates, which is visible on every page load.
|
|
17
|
+
*
|
|
18
|
+
* `cmd/ctrl + B` toggles it, the convention from every editor that has one.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
booleanCookie,
|
|
23
|
+
component,
|
|
24
|
+
cookieState,
|
|
25
|
+
html,
|
|
26
|
+
onMount,
|
|
27
|
+
onUnmount,
|
|
28
|
+
type Signal,
|
|
29
|
+
signal,
|
|
30
|
+
} from "@c9up/aurora";
|
|
31
|
+
import { Button } from "../atoms/Button.js";
|
|
32
|
+
import type { Child, Slot } from "../lib/children.js";
|
|
33
|
+
import { slot } from "../lib/children.js";
|
|
34
|
+
import { cn } from "../lib/cn.js";
|
|
35
|
+
import { PanelLeftIcon } from "../lib/icons.js";
|
|
36
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
37
|
+
import { type StyledProps, styledDiv } from "../lib/styled.js";
|
|
38
|
+
import { Sheet } from "./Sheet.js";
|
|
39
|
+
import { Tooltip } from "./Tooltip.js";
|
|
40
|
+
|
|
41
|
+
const COOKIE_NAME = "nebula:sidebar";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The open/collapsed state, shared by every part of the sidebar.
|
|
45
|
+
*
|
|
46
|
+
* shadcn publishes this through a `SidebarProvider` and React context, so that
|
|
47
|
+
* a menu item three levels down can know the sidebar is collapsed and show a
|
|
48
|
+
* tooltip instead of a label. Aurora has no context, and the honest
|
|
49
|
+
* replacement is a module-level signal: an app has one sidebar, so one signal
|
|
50
|
+
* is the whole of what the provider was carrying.
|
|
51
|
+
*
|
|
52
|
+
* Created on first use rather than at import, because `cookieState` reads the
|
|
53
|
+
* cookie immediately and this module is imported during SSR too.
|
|
54
|
+
*/
|
|
55
|
+
let sharedOpen: Signal<boolean> | undefined;
|
|
56
|
+
|
|
57
|
+
export function sidebarState(defaultOpen = true): Signal<boolean> {
|
|
58
|
+
sharedOpen ??= cookieState(COOKIE_NAME, defaultOpen, booleanCookie, {
|
|
59
|
+
path: "/",
|
|
60
|
+
maxAge: COOKIE_MAX_AGE_DAYS * 24 * 60 * 60,
|
|
61
|
+
sameSite: "lax",
|
|
62
|
+
});
|
|
63
|
+
return sharedOpen;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Is the sidebar collapsed to its icon rail right now? */
|
|
67
|
+
export function sidebarCollapsed(): boolean {
|
|
68
|
+
return !sidebarState()();
|
|
69
|
+
}
|
|
70
|
+
const COOKIE_MAX_AGE_DAYS = 365;
|
|
71
|
+
/** Below this width the sidebar becomes a Sheet. Matches Tailwind's `md`. */
|
|
72
|
+
const MOBILE_BREAKPOINT = 768;
|
|
73
|
+
|
|
74
|
+
export interface SidebarProps {
|
|
75
|
+
children?: Slot;
|
|
76
|
+
header?: Slot;
|
|
77
|
+
footer?: Slot;
|
|
78
|
+
/** Which edge. Default `"left"`. */
|
|
79
|
+
side?: "left" | "right";
|
|
80
|
+
/** What collapsing does: shrink to icons, or disappear. Default `"icon"`. */
|
|
81
|
+
collapsible?: "icon" | "offcanvas";
|
|
82
|
+
/** Starting state. Read from the cookie when there is one. */
|
|
83
|
+
defaultOpen?: boolean;
|
|
84
|
+
/** Announced as the navigation landmark's name. */
|
|
85
|
+
label?: string;
|
|
86
|
+
class?: Reactive<string>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const Sidebar = component<SidebarProps>((props) => {
|
|
90
|
+
const side = props.side ?? "left";
|
|
91
|
+
const collapsible = props.collapsible ?? "icon";
|
|
92
|
+
// The shared signal, so the menu items collapse in step with the panel.
|
|
93
|
+
const open = sidebarState(props.defaultOpen ?? true);
|
|
94
|
+
const mobile = signal(false);
|
|
95
|
+
|
|
96
|
+
function toggle(): void {
|
|
97
|
+
open(!open());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
101
|
+
if (event.key !== "b" && event.key !== "B") return;
|
|
102
|
+
if (!event.metaKey && !event.ctrlKey) return;
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
toggle();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// `matchMedia` rather than a resize listener: it fires only when the
|
|
108
|
+
// breakpoint is actually crossed, not on every pixel of a window drag.
|
|
109
|
+
let media: MediaQueryList | undefined;
|
|
110
|
+
function syncMobile(): void {
|
|
111
|
+
mobile(media?.matches === true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onMount(() => {
|
|
115
|
+
media = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
116
|
+
syncMobile();
|
|
117
|
+
media.addEventListener("change", syncMobile);
|
|
118
|
+
document.addEventListener("keydown", onKeyDown);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
onUnmount(() => {
|
|
122
|
+
media?.removeEventListener("change", syncMobile);
|
|
123
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const body = (): Child =>
|
|
127
|
+
html`<div data-slot="sidebar-inner" class="flex h-full w-full flex-col">
|
|
128
|
+
${
|
|
129
|
+
props.header === undefined
|
|
130
|
+
? null
|
|
131
|
+
: html`<div data-slot="sidebar-header" class="flex flex-col gap-2 p-2">
|
|
132
|
+
${slot(props.header)}
|
|
133
|
+
</div>`
|
|
134
|
+
}
|
|
135
|
+
<div data-slot="sidebar-body" class="min-h-0 flex-1 overflow-auto p-2">
|
|
136
|
+
${slot(props.children)}
|
|
137
|
+
</div>
|
|
138
|
+
${
|
|
139
|
+
props.footer === undefined
|
|
140
|
+
? null
|
|
141
|
+
: html`<div data-slot="sidebar-footer" class="flex flex-col gap-2 p-2">
|
|
142
|
+
${slot(props.footer)}
|
|
143
|
+
</div>`
|
|
144
|
+
}
|
|
145
|
+
</div>`;
|
|
146
|
+
|
|
147
|
+
return html`<div data-slot="sidebar-root">
|
|
148
|
+
${() =>
|
|
149
|
+
mobile()
|
|
150
|
+
? Sheet({
|
|
151
|
+
title: props.label ?? "Navigation",
|
|
152
|
+
srOnlyTitle: true,
|
|
153
|
+
side,
|
|
154
|
+
open: () => open(),
|
|
155
|
+
onOpenChange: (next) => open(next),
|
|
156
|
+
children: body(),
|
|
157
|
+
contentClass: "bg-sidebar text-sidebar-foreground w-[18rem] p-0",
|
|
158
|
+
})
|
|
159
|
+
: html`<nav
|
|
160
|
+
data-slot="sidebar"
|
|
161
|
+
data-state="${() => (open() ? "expanded" : "collapsed")}"
|
|
162
|
+
data-side="${side}"
|
|
163
|
+
data-collapsible="${collapsible}"
|
|
164
|
+
aria-label="${props.label ?? "Sidebar"}"
|
|
165
|
+
class="${() =>
|
|
166
|
+
cn(
|
|
167
|
+
"bg-sidebar text-sidebar-foreground h-svh shrink-0 overflow-hidden transition-[width] duration-200 ease-linear motion-reduce:transition-none",
|
|
168
|
+
side === "left" ? "border-r" : "border-l",
|
|
169
|
+
open()
|
|
170
|
+
? "w-(--sidebar-width,16rem)"
|
|
171
|
+
: collapsible === "icon"
|
|
172
|
+
? "w-(--sidebar-width-icon,3rem)"
|
|
173
|
+
: "w-0 border-0",
|
|
174
|
+
read(props.class),
|
|
175
|
+
)}"
|
|
176
|
+
>${body()}</nav>`}
|
|
177
|
+
</div>`;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export interface SidebarTriggerProps {
|
|
181
|
+
onToggle: () => void;
|
|
182
|
+
class?: Reactive<string>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The button that opens and closes the sidebar.
|
|
187
|
+
*
|
|
188
|
+
* Kept apart from `Sidebar` because it belongs in the page header, not in the
|
|
189
|
+
* panel — a trigger inside an off-canvas sidebar disappears with it, and there
|
|
190
|
+
* is then no way back.
|
|
191
|
+
*/
|
|
192
|
+
export const SidebarTrigger = component<SidebarTriggerProps>((props) => {
|
|
193
|
+
return Button({
|
|
194
|
+
variant: "ghost",
|
|
195
|
+
size: "icon",
|
|
196
|
+
label: "Toggle sidebar",
|
|
197
|
+
class: props.class,
|
|
198
|
+
onClick: props.onToggle,
|
|
199
|
+
children: PanelLeftIcon({ class: "size-4" }),
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export const SidebarGroup = styledDiv(
|
|
204
|
+
"sidebar-group",
|
|
205
|
+
"flex w-full flex-col gap-1 p-2",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const SidebarGroupLabel = styledDiv(
|
|
209
|
+
"sidebar-group-label",
|
|
210
|
+
"text-sidebar-foreground/70 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
export const SidebarMenu = styledDiv(
|
|
214
|
+
"sidebar-menu",
|
|
215
|
+
"flex w-full min-w-0 flex-col gap-1",
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export interface SidebarMenuItemProps {
|
|
219
|
+
label: Child;
|
|
220
|
+
href?: string;
|
|
221
|
+
icon?: Child;
|
|
222
|
+
active?: Reactive<boolean>;
|
|
223
|
+
/**
|
|
224
|
+
* Shown as a tooltip while the sidebar is collapsed to icons.
|
|
225
|
+
*
|
|
226
|
+
* Only then: an entry whose label is already on screen does not need the
|
|
227
|
+
* same words repeated on hover. Without it a collapsed rail is a column of
|
|
228
|
+
* unlabelled glyphs, so give one to every entry that has an icon.
|
|
229
|
+
*/
|
|
230
|
+
tooltip?: string;
|
|
231
|
+
/** A trailing control — the count of unread items, a status dot. */
|
|
232
|
+
badge?: Child;
|
|
233
|
+
onClick?: () => void;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* One navigation entry.
|
|
238
|
+
*
|
|
239
|
+
* `aria-current="page"` on the active entry, not just a highlight class. The
|
|
240
|
+
* highlight tells a sighted user where they are; without the attribute nobody
|
|
241
|
+
* else is told at all.
|
|
242
|
+
*/
|
|
243
|
+
export const SidebarMenuItem = component<SidebarMenuItemProps>((props) => {
|
|
244
|
+
const classes =
|
|
245
|
+
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground aria-[current=page]:bg-sidebar-accent aria-[current=page]:font-medium flex h-8 w-full items-center gap-2 overflow-hidden rounded-md px-2 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The label and badge fold away with the rail, the icon does not.
|
|
249
|
+
*
|
|
250
|
+
* `sr-only` rather than `hidden`: the entry keeps its accessible name when
|
|
251
|
+
* collapsed, so a screen reader still reads "Settings" off a column that
|
|
252
|
+
* shows only a cog.
|
|
253
|
+
*/
|
|
254
|
+
const foldable = (): string => (sidebarCollapsed() ? "sr-only" : "truncate");
|
|
255
|
+
|
|
256
|
+
const body = html`${props.icon}<span class="${foldable}">${props.label}</span>${() =>
|
|
257
|
+
props.badge === undefined || sidebarCollapsed()
|
|
258
|
+
? null
|
|
259
|
+
: html`<span
|
|
260
|
+
data-slot="sidebar-menu-badge"
|
|
261
|
+
class="text-sidebar-foreground/70 ml-auto flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums"
|
|
262
|
+
>${props.badge}</span>`}`;
|
|
263
|
+
|
|
264
|
+
const entry =
|
|
265
|
+
props.href !== undefined
|
|
266
|
+
? html`<a
|
|
267
|
+
data-slot="sidebar-menu-item"
|
|
268
|
+
href="${props.href}"
|
|
269
|
+
aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
|
|
270
|
+
class="${classes}"
|
|
271
|
+
>${body}</a>`
|
|
272
|
+
: html`<button
|
|
273
|
+
type="button"
|
|
274
|
+
data-slot="sidebar-menu-item"
|
|
275
|
+
aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
|
|
276
|
+
class="${classes}"
|
|
277
|
+
@click="${props.onClick}"
|
|
278
|
+
>${body}</button>`;
|
|
279
|
+
|
|
280
|
+
if (props.tooltip === undefined) return entry;
|
|
281
|
+
|
|
282
|
+
// Wrapped once, not toggled: swapping the wrapper on collapse would remount
|
|
283
|
+
// the entry and lose focus mid-keyboard-navigation. The tooltip decides for
|
|
284
|
+
// itself whether to open, which is the cheaper half to make conditional.
|
|
285
|
+
return html`${() =>
|
|
286
|
+
sidebarCollapsed()
|
|
287
|
+
? Tooltip({ trigger: entry, content: props.tooltip, placement: "right" })
|
|
288
|
+
: entry}`;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* A second control on a menu row — a "more" menu, a remove button.
|
|
293
|
+
*
|
|
294
|
+
* Absolutely positioned rather than a flex sibling, so it can overlap a long
|
|
295
|
+
* label instead of squeezing it. It disappears with the rail: there is no room
|
|
296
|
+
* for two controls in an icon-width column, and the row's own action is the
|
|
297
|
+
* one that matters.
|
|
298
|
+
*/
|
|
299
|
+
export const SidebarMenuAction = component<SidebarMenuItemProps>((props) => {
|
|
300
|
+
return html`<button
|
|
301
|
+
type="button"
|
|
302
|
+
data-slot="sidebar-menu-action"
|
|
303
|
+
aria-label="${props.tooltip}"
|
|
304
|
+
class="${() =>
|
|
305
|
+
cn(
|
|
306
|
+
"text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-1 right-1 flex aspect-square w-5 items-center justify-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0",
|
|
307
|
+
sidebarCollapsed() ? "hidden" : "",
|
|
308
|
+
)}"
|
|
309
|
+
@click="${props.onClick}"
|
|
310
|
+
>${props.icon ?? props.label}</button>`;
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* A nested list under a menu entry.
|
|
315
|
+
*
|
|
316
|
+
* A real `<ul>`, so the nesting is structure a screen reader can announce
|
|
317
|
+
* rather than an indent. Hidden entirely when collapsed — an indented tree in
|
|
318
|
+
* an icon-width rail is unreadable, and the parent entry is still reachable.
|
|
319
|
+
*/
|
|
320
|
+
export const SidebarMenuSub = component<StyledProps>((props) => {
|
|
321
|
+
return html`<ul
|
|
322
|
+
data-slot="sidebar-menu-sub"
|
|
323
|
+
class="${() =>
|
|
324
|
+
cn(
|
|
325
|
+
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
|
326
|
+
sidebarCollapsed() ? "hidden" : "",
|
|
327
|
+
read(props.class),
|
|
328
|
+
)}"
|
|
329
|
+
>${slot(props.children)}</ul>`;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/** One row of a nested list. Wrap a `SidebarMenuItem` in it. */
|
|
333
|
+
export const SidebarMenuSubItem = styledDiv(
|
|
334
|
+
"sidebar-menu-sub-item",
|
|
335
|
+
"relative flex min-w-0 items-center",
|
|
336
|
+
);
|