@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,100 @@
|
|
|
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
|
+
import { type Signal } from "@c9up/aurora";
|
|
21
|
+
import type { Child, Slot } from "../lib/children.js";
|
|
22
|
+
import { type Reactive } from "../lib/props.js";
|
|
23
|
+
import { type StyledProps } from "../lib/styled.js";
|
|
24
|
+
export declare function sidebarState(defaultOpen?: boolean): Signal<boolean>;
|
|
25
|
+
/** Is the sidebar collapsed to its icon rail right now? */
|
|
26
|
+
export declare function sidebarCollapsed(): boolean;
|
|
27
|
+
export interface SidebarProps {
|
|
28
|
+
children?: Slot;
|
|
29
|
+
header?: Slot;
|
|
30
|
+
footer?: Slot;
|
|
31
|
+
/** Which edge. Default `"left"`. */
|
|
32
|
+
side?: "left" | "right";
|
|
33
|
+
/** What collapsing does: shrink to icons, or disappear. Default `"icon"`. */
|
|
34
|
+
collapsible?: "icon" | "offcanvas";
|
|
35
|
+
/** Starting state. Read from the cookie when there is one. */
|
|
36
|
+
defaultOpen?: boolean;
|
|
37
|
+
/** Announced as the navigation landmark's name. */
|
|
38
|
+
label?: string;
|
|
39
|
+
class?: Reactive<string>;
|
|
40
|
+
}
|
|
41
|
+
export declare const Sidebar: (props?: SidebarProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
42
|
+
export interface SidebarTriggerProps {
|
|
43
|
+
onToggle: () => void;
|
|
44
|
+
class?: Reactive<string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The button that opens and closes the sidebar.
|
|
48
|
+
*
|
|
49
|
+
* Kept apart from `Sidebar` because it belongs in the page header, not in the
|
|
50
|
+
* panel — a trigger inside an off-canvas sidebar disappears with it, and there
|
|
51
|
+
* is then no way back.
|
|
52
|
+
*/
|
|
53
|
+
export declare const SidebarTrigger: (props?: SidebarTriggerProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
54
|
+
export declare const SidebarGroup: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
|
|
55
|
+
export declare const SidebarGroupLabel: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
|
|
56
|
+
export declare const SidebarMenu: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
|
|
57
|
+
export interface SidebarMenuItemProps {
|
|
58
|
+
label: Child;
|
|
59
|
+
href?: string;
|
|
60
|
+
icon?: Child;
|
|
61
|
+
active?: Reactive<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Shown as a tooltip while the sidebar is collapsed to icons.
|
|
64
|
+
*
|
|
65
|
+
* Only then: an entry whose label is already on screen does not need the
|
|
66
|
+
* same words repeated on hover. Without it a collapsed rail is a column of
|
|
67
|
+
* unlabelled glyphs, so give one to every entry that has an icon.
|
|
68
|
+
*/
|
|
69
|
+
tooltip?: string;
|
|
70
|
+
/** A trailing control — the count of unread items, a status dot. */
|
|
71
|
+
badge?: Child;
|
|
72
|
+
onClick?: () => void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* One navigation entry.
|
|
76
|
+
*
|
|
77
|
+
* `aria-current="page"` on the active entry, not just a highlight class. The
|
|
78
|
+
* highlight tells a sighted user where they are; without the attribute nobody
|
|
79
|
+
* else is told at all.
|
|
80
|
+
*/
|
|
81
|
+
export declare const SidebarMenuItem: (props?: SidebarMenuItemProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
82
|
+
/**
|
|
83
|
+
* A second control on a menu row — a "more" menu, a remove button.
|
|
84
|
+
*
|
|
85
|
+
* Absolutely positioned rather than a flex sibling, so it can overlap a long
|
|
86
|
+
* label instead of squeezing it. It disappears with the rail: there is no room
|
|
87
|
+
* for two controls in an icon-width column, and the row's own action is the
|
|
88
|
+
* one that matters.
|
|
89
|
+
*/
|
|
90
|
+
export declare const SidebarMenuAction: (props?: SidebarMenuItemProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
91
|
+
/**
|
|
92
|
+
* A nested list under a menu entry.
|
|
93
|
+
*
|
|
94
|
+
* A real `<ul>`, so the nesting is structure a screen reader can announce
|
|
95
|
+
* rather than an indent. Hidden entirely when collapsed — an indented tree in
|
|
96
|
+
* an icon-width rail is unreadable, and the parent entry is still reachable.
|
|
97
|
+
*/
|
|
98
|
+
export declare const SidebarMenuSub: (props?: StyledProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
99
|
+
/** One row of a nested list. Wrap a `SidebarMenuItem` in it. */
|
|
100
|
+
export declare const SidebarMenuSubItem: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
import { booleanCookie, component, cookieState, html, onMount, onUnmount, signal, } from "@c9up/aurora";
|
|
21
|
+
import { Button } from "../atoms/Button.js";
|
|
22
|
+
import { slot } from "../lib/children.js";
|
|
23
|
+
import { cn } from "../lib/cn.js";
|
|
24
|
+
import { PanelLeftIcon } from "../lib/icons.js";
|
|
25
|
+
import { read } from "../lib/props.js";
|
|
26
|
+
import { styledDiv } from "../lib/styled.js";
|
|
27
|
+
import { Sheet } from "./Sheet.js";
|
|
28
|
+
import { Tooltip } from "./Tooltip.js";
|
|
29
|
+
const COOKIE_NAME = "nebula:sidebar";
|
|
30
|
+
/**
|
|
31
|
+
* The open/collapsed state, shared by every part of the sidebar.
|
|
32
|
+
*
|
|
33
|
+
* shadcn publishes this through a `SidebarProvider` and React context, so that
|
|
34
|
+
* a menu item three levels down can know the sidebar is collapsed and show a
|
|
35
|
+
* tooltip instead of a label. Aurora has no context, and the honest
|
|
36
|
+
* replacement is a module-level signal: an app has one sidebar, so one signal
|
|
37
|
+
* is the whole of what the provider was carrying.
|
|
38
|
+
*
|
|
39
|
+
* Created on first use rather than at import, because `cookieState` reads the
|
|
40
|
+
* cookie immediately and this module is imported during SSR too.
|
|
41
|
+
*/
|
|
42
|
+
let sharedOpen;
|
|
43
|
+
export function sidebarState(defaultOpen = true) {
|
|
44
|
+
sharedOpen ??= cookieState(COOKIE_NAME, defaultOpen, booleanCookie, {
|
|
45
|
+
path: "/",
|
|
46
|
+
maxAge: COOKIE_MAX_AGE_DAYS * 24 * 60 * 60,
|
|
47
|
+
sameSite: "lax",
|
|
48
|
+
});
|
|
49
|
+
return sharedOpen;
|
|
50
|
+
}
|
|
51
|
+
/** Is the sidebar collapsed to its icon rail right now? */
|
|
52
|
+
export function sidebarCollapsed() {
|
|
53
|
+
return !sidebarState()();
|
|
54
|
+
}
|
|
55
|
+
const COOKIE_MAX_AGE_DAYS = 365;
|
|
56
|
+
/** Below this width the sidebar becomes a Sheet. Matches Tailwind's `md`. */
|
|
57
|
+
const MOBILE_BREAKPOINT = 768;
|
|
58
|
+
export const Sidebar = component((props) => {
|
|
59
|
+
const side = props.side ?? "left";
|
|
60
|
+
const collapsible = props.collapsible ?? "icon";
|
|
61
|
+
// The shared signal, so the menu items collapse in step with the panel.
|
|
62
|
+
const open = sidebarState(props.defaultOpen ?? true);
|
|
63
|
+
const mobile = signal(false);
|
|
64
|
+
function toggle() {
|
|
65
|
+
open(!open());
|
|
66
|
+
}
|
|
67
|
+
function onKeyDown(event) {
|
|
68
|
+
if (event.key !== "b" && event.key !== "B")
|
|
69
|
+
return;
|
|
70
|
+
if (!event.metaKey && !event.ctrlKey)
|
|
71
|
+
return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
toggle();
|
|
74
|
+
}
|
|
75
|
+
// `matchMedia` rather than a resize listener: it fires only when the
|
|
76
|
+
// breakpoint is actually crossed, not on every pixel of a window drag.
|
|
77
|
+
let media;
|
|
78
|
+
function syncMobile() {
|
|
79
|
+
mobile(media?.matches === true);
|
|
80
|
+
}
|
|
81
|
+
onMount(() => {
|
|
82
|
+
media = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
83
|
+
syncMobile();
|
|
84
|
+
media.addEventListener("change", syncMobile);
|
|
85
|
+
document.addEventListener("keydown", onKeyDown);
|
|
86
|
+
});
|
|
87
|
+
onUnmount(() => {
|
|
88
|
+
media?.removeEventListener("change", syncMobile);
|
|
89
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
90
|
+
});
|
|
91
|
+
const body = () => html `<div data-slot="sidebar-inner" class="flex h-full w-full flex-col">
|
|
92
|
+
${props.header === undefined
|
|
93
|
+
? null
|
|
94
|
+
: html `<div data-slot="sidebar-header" class="flex flex-col gap-2 p-2">
|
|
95
|
+
${slot(props.header)}
|
|
96
|
+
</div>`}
|
|
97
|
+
<div data-slot="sidebar-body" class="min-h-0 flex-1 overflow-auto p-2">
|
|
98
|
+
${slot(props.children)}
|
|
99
|
+
</div>
|
|
100
|
+
${props.footer === undefined
|
|
101
|
+
? null
|
|
102
|
+
: html `<div data-slot="sidebar-footer" class="flex flex-col gap-2 p-2">
|
|
103
|
+
${slot(props.footer)}
|
|
104
|
+
</div>`}
|
|
105
|
+
</div>`;
|
|
106
|
+
return html `<div data-slot="sidebar-root">
|
|
107
|
+
${() => mobile()
|
|
108
|
+
? Sheet({
|
|
109
|
+
title: props.label ?? "Navigation",
|
|
110
|
+
srOnlyTitle: true,
|
|
111
|
+
side,
|
|
112
|
+
open: () => open(),
|
|
113
|
+
onOpenChange: (next) => open(next),
|
|
114
|
+
children: body(),
|
|
115
|
+
contentClass: "bg-sidebar text-sidebar-foreground w-[18rem] p-0",
|
|
116
|
+
})
|
|
117
|
+
: html `<nav
|
|
118
|
+
data-slot="sidebar"
|
|
119
|
+
data-state="${() => (open() ? "expanded" : "collapsed")}"
|
|
120
|
+
data-side="${side}"
|
|
121
|
+
data-collapsible="${collapsible}"
|
|
122
|
+
aria-label="${props.label ?? "Sidebar"}"
|
|
123
|
+
class="${() => cn("bg-sidebar text-sidebar-foreground h-svh shrink-0 overflow-hidden transition-[width] duration-200 ease-linear motion-reduce:transition-none", side === "left" ? "border-r" : "border-l", open()
|
|
124
|
+
? "w-(--sidebar-width,16rem)"
|
|
125
|
+
: collapsible === "icon"
|
|
126
|
+
? "w-(--sidebar-width-icon,3rem)"
|
|
127
|
+
: "w-0 border-0", read(props.class))}"
|
|
128
|
+
>${body()}</nav>`}
|
|
129
|
+
</div>`;
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* The button that opens and closes the sidebar.
|
|
133
|
+
*
|
|
134
|
+
* Kept apart from `Sidebar` because it belongs in the page header, not in the
|
|
135
|
+
* panel — a trigger inside an off-canvas sidebar disappears with it, and there
|
|
136
|
+
* is then no way back.
|
|
137
|
+
*/
|
|
138
|
+
export const SidebarTrigger = component((props) => {
|
|
139
|
+
return Button({
|
|
140
|
+
variant: "ghost",
|
|
141
|
+
size: "icon",
|
|
142
|
+
label: "Toggle sidebar",
|
|
143
|
+
class: props.class,
|
|
144
|
+
onClick: props.onToggle,
|
|
145
|
+
children: PanelLeftIcon({ class: "size-4" }),
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
export const SidebarGroup = styledDiv("sidebar-group", "flex w-full flex-col gap-1 p-2");
|
|
149
|
+
export const SidebarGroupLabel = styledDiv("sidebar-group-label", "text-sidebar-foreground/70 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium");
|
|
150
|
+
export const SidebarMenu = styledDiv("sidebar-menu", "flex w-full min-w-0 flex-col gap-1");
|
|
151
|
+
/**
|
|
152
|
+
* One navigation entry.
|
|
153
|
+
*
|
|
154
|
+
* `aria-current="page"` on the active entry, not just a highlight class. The
|
|
155
|
+
* highlight tells a sighted user where they are; without the attribute nobody
|
|
156
|
+
* else is told at all.
|
|
157
|
+
*/
|
|
158
|
+
export const SidebarMenuItem = component((props) => {
|
|
159
|
+
const classes = "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";
|
|
160
|
+
/**
|
|
161
|
+
* The label and badge fold away with the rail, the icon does not.
|
|
162
|
+
*
|
|
163
|
+
* `sr-only` rather than `hidden`: the entry keeps its accessible name when
|
|
164
|
+
* collapsed, so a screen reader still reads "Settings" off a column that
|
|
165
|
+
* shows only a cog.
|
|
166
|
+
*/
|
|
167
|
+
const foldable = () => (sidebarCollapsed() ? "sr-only" : "truncate");
|
|
168
|
+
const body = html `${props.icon}<span class="${foldable}">${props.label}</span>${() => props.badge === undefined || sidebarCollapsed()
|
|
169
|
+
? null
|
|
170
|
+
: html `<span
|
|
171
|
+
data-slot="sidebar-menu-badge"
|
|
172
|
+
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"
|
|
173
|
+
>${props.badge}</span>`}`;
|
|
174
|
+
const entry = props.href !== undefined
|
|
175
|
+
? html `<a
|
|
176
|
+
data-slot="sidebar-menu-item"
|
|
177
|
+
href="${props.href}"
|
|
178
|
+
aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
|
|
179
|
+
class="${classes}"
|
|
180
|
+
>${body}</a>`
|
|
181
|
+
: html `<button
|
|
182
|
+
type="button"
|
|
183
|
+
data-slot="sidebar-menu-item"
|
|
184
|
+
aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
|
|
185
|
+
class="${classes}"
|
|
186
|
+
@click="${props.onClick}"
|
|
187
|
+
>${body}</button>`;
|
|
188
|
+
if (props.tooltip === undefined)
|
|
189
|
+
return entry;
|
|
190
|
+
// Wrapped once, not toggled: swapping the wrapper on collapse would remount
|
|
191
|
+
// the entry and lose focus mid-keyboard-navigation. The tooltip decides for
|
|
192
|
+
// itself whether to open, which is the cheaper half to make conditional.
|
|
193
|
+
return html `${() => sidebarCollapsed()
|
|
194
|
+
? Tooltip({ trigger: entry, content: props.tooltip, placement: "right" })
|
|
195
|
+
: entry}`;
|
|
196
|
+
});
|
|
197
|
+
/**
|
|
198
|
+
* A second control on a menu row — a "more" menu, a remove button.
|
|
199
|
+
*
|
|
200
|
+
* Absolutely positioned rather than a flex sibling, so it can overlap a long
|
|
201
|
+
* label instead of squeezing it. It disappears with the rail: there is no room
|
|
202
|
+
* for two controls in an icon-width column, and the row's own action is the
|
|
203
|
+
* one that matters.
|
|
204
|
+
*/
|
|
205
|
+
export const SidebarMenuAction = component((props) => {
|
|
206
|
+
return html `<button
|
|
207
|
+
type="button"
|
|
208
|
+
data-slot="sidebar-menu-action"
|
|
209
|
+
aria-label="${props.tooltip}"
|
|
210
|
+
class="${() => cn("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", sidebarCollapsed() ? "hidden" : "")}"
|
|
211
|
+
@click="${props.onClick}"
|
|
212
|
+
>${props.icon ?? props.label}</button>`;
|
|
213
|
+
});
|
|
214
|
+
/**
|
|
215
|
+
* A nested list under a menu entry.
|
|
216
|
+
*
|
|
217
|
+
* A real `<ul>`, so the nesting is structure a screen reader can announce
|
|
218
|
+
* rather than an indent. Hidden entirely when collapsed — an indented tree in
|
|
219
|
+
* an icon-width rail is unreadable, and the parent entry is still reachable.
|
|
220
|
+
*/
|
|
221
|
+
export const SidebarMenuSub = component((props) => {
|
|
222
|
+
return html `<ul
|
|
223
|
+
data-slot="sidebar-menu-sub"
|
|
224
|
+
class="${() => cn("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", sidebarCollapsed() ? "hidden" : "", read(props.class))}"
|
|
225
|
+
>${slot(props.children)}</ul>`;
|
|
226
|
+
});
|
|
227
|
+
/** One row of a nested list. Wrap a `SidebarMenuItem` in it. */
|
|
228
|
+
export const SidebarMenuSubItem = styledDiv("sidebar-menu-sub-item", "relative flex min-w-0 items-center");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toaster — transient notifications.
|
|
3
|
+
*
|
|
4
|
+
* shadcn delegates this to `sonner`. nebula keeps it in-house, and the shape
|
|
5
|
+
* is the same: one region mounted once near the app root, and a `toast()`
|
|
6
|
+
* function callable from anywhere with no reference to it.
|
|
7
|
+
*
|
|
8
|
+
* That decoupling is the whole design. A queue in module state, a signal the
|
|
9
|
+
* region subscribes to, and `toast.error(…)` works from a route handler's
|
|
10
|
+
* error branch without threading a handle down to it.
|
|
11
|
+
*
|
|
12
|
+
* The accessibility rules are stricter than they look, and the region is
|
|
13
|
+
* static so they can be satisfied:
|
|
14
|
+
*
|
|
15
|
+
* - The live region must exist **before** the toast does. A live region
|
|
16
|
+
* inserted with content already in it is not announced by most screen
|
|
17
|
+
* readers, which is why the region mounts empty and stays.
|
|
18
|
+
* - `polite` for ordinary toasts, `assertive` for errors. Everything
|
|
19
|
+
* interrupting the user is the fastest way to make a screen reader
|
|
20
|
+
* unusable — so there are two regions, and each toast picks one.
|
|
21
|
+
* - Hovering pauses the timers. A toast that vanishes while being read is the
|
|
22
|
+
* single most common complaint about the pattern.
|
|
23
|
+
*/
|
|
24
|
+
import type { Child } from "../lib/children.js";
|
|
25
|
+
import { type Reactive } from "../lib/props.js";
|
|
26
|
+
export type ToastVariant = "default" | "success" | "error" | "warning";
|
|
27
|
+
export interface ToastOptions {
|
|
28
|
+
title: Child;
|
|
29
|
+
description?: Child;
|
|
30
|
+
variant?: ToastVariant;
|
|
31
|
+
/** Milliseconds on screen. `0` keeps it until dismissed. */
|
|
32
|
+
duration?: number;
|
|
33
|
+
action?: {
|
|
34
|
+
label: string;
|
|
35
|
+
onClick: () => void;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
declare function dismiss(id: number): void;
|
|
39
|
+
declare function show(options: ToastOptions): number;
|
|
40
|
+
/**
|
|
41
|
+
* Raise a toast. Returns its id, which `dismiss` accepts.
|
|
42
|
+
*
|
|
43
|
+
* Callable from anywhere — no component, no context, no handle.
|
|
44
|
+
*/
|
|
45
|
+
export declare const toast: {
|
|
46
|
+
show: typeof show;
|
|
47
|
+
dismiss: typeof dismiss;
|
|
48
|
+
success: (title: Child, description?: Child) => number;
|
|
49
|
+
error: (title: Child, description?: Child) => number;
|
|
50
|
+
warning: (title: Child, description?: Child) => number;
|
|
51
|
+
/** Clear everything — on navigation, say. */
|
|
52
|
+
clear(): void;
|
|
53
|
+
};
|
|
54
|
+
export interface ToasterProps {
|
|
55
|
+
/** Which corner. Default `"bottom-right"`. */
|
|
56
|
+
position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
|
|
57
|
+
class?: Reactive<string>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Mount once, near the root of the app.
|
|
61
|
+
*
|
|
62
|
+
* `data-nebula-live` marks it as exempt from the `aria-hidden` sweep a modal
|
|
63
|
+
* performs over its siblings — a toast raised while a dialog is open still has
|
|
64
|
+
* to be announced.
|
|
65
|
+
*/
|
|
66
|
+
export declare const Toaster: (props?: ToasterProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toaster — transient notifications.
|
|
3
|
+
*
|
|
4
|
+
* shadcn delegates this to `sonner`. nebula keeps it in-house, and the shape
|
|
5
|
+
* is the same: one region mounted once near the app root, and a `toast()`
|
|
6
|
+
* function callable from anywhere with no reference to it.
|
|
7
|
+
*
|
|
8
|
+
* That decoupling is the whole design. A queue in module state, a signal the
|
|
9
|
+
* region subscribes to, and `toast.error(…)` works from a route handler's
|
|
10
|
+
* error branch without threading a handle down to it.
|
|
11
|
+
*
|
|
12
|
+
* The accessibility rules are stricter than they look, and the region is
|
|
13
|
+
* static so they can be satisfied:
|
|
14
|
+
*
|
|
15
|
+
* - The live region must exist **before** the toast does. A live region
|
|
16
|
+
* inserted with content already in it is not announced by most screen
|
|
17
|
+
* readers, which is why the region mounts empty and stays.
|
|
18
|
+
* - `polite` for ordinary toasts, `assertive` for errors. Everything
|
|
19
|
+
* interrupting the user is the fastest way to make a screen reader
|
|
20
|
+
* unusable — so there are two regions, and each toast picks one.
|
|
21
|
+
* - Hovering pauses the timers. A toast that vanishes while being read is the
|
|
22
|
+
* single most common complaint about the pattern.
|
|
23
|
+
*/
|
|
24
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
25
|
+
import { cn } from "../lib/cn.js";
|
|
26
|
+
import { XIcon } from "../lib/icons.js";
|
|
27
|
+
import { fadeInOut } from "../lib/motion.js";
|
|
28
|
+
import { read } from "../lib/props.js";
|
|
29
|
+
const DEFAULT_DURATION_MS = 5000;
|
|
30
|
+
const queue = signal([]);
|
|
31
|
+
const timers = new Map();
|
|
32
|
+
/** When each toast is due to disappear, as an epoch timestamp. */
|
|
33
|
+
const deadlines = new Map();
|
|
34
|
+
/** Time each toast had left when the pointer entered the region. */
|
|
35
|
+
const remainders = new Map();
|
|
36
|
+
let nextId = 0;
|
|
37
|
+
function dismiss(id) {
|
|
38
|
+
clearTimer(id);
|
|
39
|
+
deadlines.delete(id);
|
|
40
|
+
remainders.delete(id);
|
|
41
|
+
queue(queue().filter((entry) => entry.id !== id));
|
|
42
|
+
}
|
|
43
|
+
function clearTimer(id) {
|
|
44
|
+
const timer = timers.get(id);
|
|
45
|
+
if (timer !== undefined)
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
timers.delete(id);
|
|
48
|
+
}
|
|
49
|
+
function schedule(id, duration) {
|
|
50
|
+
// A duration of zero means "stays until dismissed", so no timer and no
|
|
51
|
+
// deadline — which is also what keeps pause/resume from resurrecting one.
|
|
52
|
+
if (duration <= 0)
|
|
53
|
+
return;
|
|
54
|
+
clearTimer(id);
|
|
55
|
+
timers.set(id, setTimeout(() => dismiss(id), duration));
|
|
56
|
+
deadlines.set(id, Date.now() + duration);
|
|
57
|
+
}
|
|
58
|
+
/** Hold every countdown while the pointer is over the region. */
|
|
59
|
+
function pauseAll() {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
for (const [id, deadline] of deadlines) {
|
|
62
|
+
clearTimer(id);
|
|
63
|
+
remainders.set(id, Math.max(deadline - now, 0));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resume every held countdown.
|
|
68
|
+
*
|
|
69
|
+
* A minimum is applied: a toast the pointer rested on until its time ran out
|
|
70
|
+
* would otherwise vanish the instant the pointer left, which reads as the
|
|
71
|
+
* hover having dismissed it.
|
|
72
|
+
*/
|
|
73
|
+
function resumeAll(minimumMs) {
|
|
74
|
+
for (const [id, remaining] of remainders) {
|
|
75
|
+
schedule(id, Math.max(remaining, minimumMs));
|
|
76
|
+
}
|
|
77
|
+
remainders.clear();
|
|
78
|
+
}
|
|
79
|
+
function show(options) {
|
|
80
|
+
nextId += 1;
|
|
81
|
+
const id = nextId;
|
|
82
|
+
const entry = {
|
|
83
|
+
...options,
|
|
84
|
+
id,
|
|
85
|
+
variant: options.variant ?? "default",
|
|
86
|
+
};
|
|
87
|
+
queue([...queue(), entry]);
|
|
88
|
+
schedule(id, options.duration ?? DEFAULT_DURATION_MS);
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Raise a toast. Returns its id, which `dismiss` accepts.
|
|
93
|
+
*
|
|
94
|
+
* Callable from anywhere — no component, no context, no handle.
|
|
95
|
+
*/
|
|
96
|
+
export const toast = {
|
|
97
|
+
show,
|
|
98
|
+
dismiss,
|
|
99
|
+
success: (title, description) => show({ title, description, variant: "success" }),
|
|
100
|
+
error: (title, description) =>
|
|
101
|
+
// Errors stay twice as long: the user has to read them, and often act.
|
|
102
|
+
show({
|
|
103
|
+
title,
|
|
104
|
+
description,
|
|
105
|
+
variant: "error",
|
|
106
|
+
duration: DEFAULT_DURATION_MS * 2,
|
|
107
|
+
}),
|
|
108
|
+
warning: (title, description) => show({ title, description, variant: "warning" }),
|
|
109
|
+
/** Clear everything — on navigation, say. */
|
|
110
|
+
clear() {
|
|
111
|
+
for (const timer of timers.values())
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
timers.clear();
|
|
114
|
+
deadlines.clear();
|
|
115
|
+
remainders.clear();
|
|
116
|
+
queue([]);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const variantClasses = {
|
|
120
|
+
default: "bg-popover text-popover-foreground border",
|
|
121
|
+
success: "bg-popover text-popover-foreground border-l-4 border-l-primary border",
|
|
122
|
+
error: "bg-popover text-destructive border-l-4 border-l-destructive border",
|
|
123
|
+
warning: "bg-popover text-popover-foreground border-l-4 border-l-chart-4 border",
|
|
124
|
+
};
|
|
125
|
+
const positionClasses = {
|
|
126
|
+
"top-right": "top-0 right-0 flex-col",
|
|
127
|
+
"top-left": "top-0 left-0 flex-col",
|
|
128
|
+
"bottom-right": "bottom-0 right-0 flex-col-reverse",
|
|
129
|
+
"bottom-left": "bottom-0 left-0 flex-col-reverse",
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Mount once, near the root of the app.
|
|
133
|
+
*
|
|
134
|
+
* `data-nebula-live` marks it as exempt from the `aria-hidden` sweep a modal
|
|
135
|
+
* performs over its siblings — a toast raised while a dialog is open still has
|
|
136
|
+
* to be announced.
|
|
137
|
+
*/
|
|
138
|
+
export const Toaster = component((props) => {
|
|
139
|
+
const position = props.position ?? "bottom-right";
|
|
140
|
+
return html `<div
|
|
141
|
+
data-slot="toaster"
|
|
142
|
+
data-nebula-live
|
|
143
|
+
class="${() => cn("pointer-events-none fixed z-100 flex max-h-screen w-full p-4 sm:max-w-sm", positionClasses[position], read(props.class))}"
|
|
144
|
+
@pointerenter="${pauseAll}"
|
|
145
|
+
@pointerleave="${() => resumeAll(1000)}"
|
|
146
|
+
>
|
|
147
|
+
<div role="status" aria-live="polite" aria-atomic="false" class="contents">
|
|
148
|
+
${() => queue()
|
|
149
|
+
.filter((entry) => entry.variant !== "error")
|
|
150
|
+
.map(renderToast)}
|
|
151
|
+
</div>
|
|
152
|
+
<div role="alert" aria-live="assertive" aria-atomic="false" class="contents">
|
|
153
|
+
${() => queue()
|
|
154
|
+
.filter((entry) => entry.variant === "error")
|
|
155
|
+
.map(renderToast)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>`;
|
|
158
|
+
});
|
|
159
|
+
function renderToast(entry) {
|
|
160
|
+
return html `<div
|
|
161
|
+
data-slot="toast"
|
|
162
|
+
data-variant="${entry.variant}"
|
|
163
|
+
data-state="open"
|
|
164
|
+
class="${cn("pointer-events-auto mt-2 flex w-full items-start gap-3 rounded-md p-4 shadow-lg", variantClasses[entry.variant], fadeInOut)}"
|
|
165
|
+
>
|
|
166
|
+
<div class="flex-1 text-sm">
|
|
167
|
+
<div class="font-medium">${entry.title}</div>
|
|
168
|
+
${entry.description === undefined
|
|
169
|
+
? null
|
|
170
|
+
: html `<div class="text-muted-foreground mt-1">${entry.description}</div>`}
|
|
171
|
+
</div>
|
|
172
|
+
${entry.action === undefined
|
|
173
|
+
? null
|
|
174
|
+
: html `<button
|
|
175
|
+
type="button"
|
|
176
|
+
class="text-sm font-medium underline-offset-4 hover:underline"
|
|
177
|
+
@click="${() => {
|
|
178
|
+
entry.action?.onClick();
|
|
179
|
+
dismiss(entry.id);
|
|
180
|
+
}}"
|
|
181
|
+
>${entry.action.label}</button>`}
|
|
182
|
+
<button
|
|
183
|
+
type="button"
|
|
184
|
+
aria-label="Dismiss notification"
|
|
185
|
+
class="opacity-60 transition-opacity hover:opacity-100"
|
|
186
|
+
@click="${() => dismiss(entry.id)}"
|
|
187
|
+
>${XIcon({ class: "size-4" })}</button>
|
|
188
|
+
</div>`;
|
|
189
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tooltip — a label that appears on hover or focus.
|
|
3
|
+
*
|
|
4
|
+
* Three rules separate a usable tooltip from an irritating one, and all three
|
|
5
|
+
* are about time rather than appearance:
|
|
6
|
+
*
|
|
7
|
+
* - An **open delay**, so moving the pointer across a toolbar does not fire
|
|
8
|
+
* six tooltips on the way past.
|
|
9
|
+
* - A **close delay**, so travelling from the trigger to the tooltip — which a
|
|
10
|
+
* user does to read a long one, or click a link inside — does not dismiss it
|
|
11
|
+
* halfway.
|
|
12
|
+
* - **No delay between neighbours.** Once one tooltip is showing, the next one
|
|
13
|
+
* appears instantly; the user has clearly decided to browse the toolbar, and
|
|
14
|
+
* re-serving the delay each time makes the interface feel stuck. That is why
|
|
15
|
+
* `lastClosedAt` is module state and not per-component.
|
|
16
|
+
*
|
|
17
|
+
* Focus opens it with no delay at all. A keyboard user landing on a control
|
|
18
|
+
* has already committed to it, and a delay just looks like lag.
|
|
19
|
+
*
|
|
20
|
+
* `role="tooltip"` plus `aria-describedby` — never `aria-labelledby`. A
|
|
21
|
+
* tooltip supplements a control's name; it is not the name. A button labelled
|
|
22
|
+
* only by its tooltip is unlabelled to anything that does not hover.
|
|
23
|
+
*/
|
|
24
|
+
import type { Child } from "../lib/children.js";
|
|
25
|
+
import { type Slot } from "../lib/children.js";
|
|
26
|
+
import { type Reactive } from "../lib/props.js";
|
|
27
|
+
import type { Placement } from "../primitives/floating.js";
|
|
28
|
+
export interface TooltipProps {
|
|
29
|
+
/** The control being described. */
|
|
30
|
+
trigger?: Slot;
|
|
31
|
+
/** The tooltip text. Keep it to a phrase. */
|
|
32
|
+
content?: Child;
|
|
33
|
+
placement?: Placement;
|
|
34
|
+
openDelay?: number;
|
|
35
|
+
closeDelay?: number;
|
|
36
|
+
class?: Reactive<string>;
|
|
37
|
+
contentClass?: Reactive<string>;
|
|
38
|
+
}
|
|
39
|
+
export declare const Tooltip: (props?: TooltipProps | undefined) => import("@c9up/aurora").TemplateResult;
|