@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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment — a file carried by a message.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes from one component: an image renders as a thumbnail, anything
|
|
5
|
+
* else as a chip with a name and a size. Splitting them into two components
|
|
6
|
+
* would push the choice onto the caller, who usually has a MIME type and no
|
|
7
|
+
* opinion.
|
|
8
|
+
*
|
|
9
|
+
* The size is formatted in binary units because that is what a file manager
|
|
10
|
+
* shows for the same file — a "1.2 MB" here and a "1.1 MiB" there for one
|
|
11
|
+
* upload reads as a bug.
|
|
12
|
+
*
|
|
13
|
+
* A removable attachment is a `<button>` beside the link, never a click
|
|
14
|
+
* handler on the chip itself. Merging the two makes "open" and "delete" the
|
|
15
|
+
* same gesture, distinguished only by where you land.
|
|
16
|
+
*/
|
|
17
|
+
import { component, html } from "@c9up/aurora";
|
|
18
|
+
import { cn } from "../lib/cn.js";
|
|
19
|
+
import { XIcon } from "../lib/icons.js";
|
|
20
|
+
import { read } from "../lib/props.js";
|
|
21
|
+
/**
|
|
22
|
+
* Human-readable byte count, binary units.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately not `Intl.NumberFormat`'s `unit: "byte"` style: it only speaks
|
|
25
|
+
* decimal units (kB, MB), which disagrees with every desktop file manager.
|
|
26
|
+
*/
|
|
27
|
+
export function formatBytes(bytes) {
|
|
28
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
29
|
+
return "";
|
|
30
|
+
if (bytes < 1024)
|
|
31
|
+
return `${bytes} B`;
|
|
32
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
33
|
+
let value = bytes / 1024;
|
|
34
|
+
let unit = 0;
|
|
35
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
36
|
+
value /= 1024;
|
|
37
|
+
unit += 1;
|
|
38
|
+
}
|
|
39
|
+
// One decimal below 10, none above — "9.4 MB" is useful, "941.3 MB" is noise.
|
|
40
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
41
|
+
}
|
|
42
|
+
export const Attachment = component((props) => {
|
|
43
|
+
const isImage = props.type?.startsWith("image/") === true;
|
|
44
|
+
const preview = props.previewSrc ?? props.href;
|
|
45
|
+
const removeButton = props.onRemove === undefined
|
|
46
|
+
? null
|
|
47
|
+
: html `<button
|
|
48
|
+
type="button"
|
|
49
|
+
data-slot="attachment-remove"
|
|
50
|
+
aria-label="${`Remove ${props.name}`}"
|
|
51
|
+
class="text-muted-foreground hover:text-foreground absolute end-1 top-1 rounded-sm bg-background/80 p-0.5 transition-colors"
|
|
52
|
+
@click="${() => props.onRemove?.()}"
|
|
53
|
+
>${XIcon({ class: "size-3.5" })}</button>`;
|
|
54
|
+
if (isImage && preview !== undefined) {
|
|
55
|
+
return html `<div
|
|
56
|
+
data-slot="attachment"
|
|
57
|
+
data-kind="image"
|
|
58
|
+
class="${() => cn("relative w-fit overflow-hidden rounded-md border", read(props.class))}"
|
|
59
|
+
>
|
|
60
|
+
<a href="${props.href}" class="block">
|
|
61
|
+
<img
|
|
62
|
+
src="${preview}"
|
|
63
|
+
alt="${props.name}"
|
|
64
|
+
loading="lazy"
|
|
65
|
+
class="max-h-48 max-w-64 object-cover"
|
|
66
|
+
/>
|
|
67
|
+
</a>
|
|
68
|
+
${removeButton}
|
|
69
|
+
</div>`;
|
|
70
|
+
}
|
|
71
|
+
const body = html `<span
|
|
72
|
+
data-slot="attachment-icon"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
class="bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded text-[0.625rem] font-medium uppercase"
|
|
75
|
+
>${extensionOf(props.name)}</span>
|
|
76
|
+
<span class="flex min-w-0 flex-col">
|
|
77
|
+
<span class="truncate text-sm font-medium">${props.name}</span>
|
|
78
|
+
${props.size === undefined
|
|
79
|
+
? null
|
|
80
|
+
: html `<span class="text-muted-foreground text-xs tabular-nums"
|
|
81
|
+
>${formatBytes(props.size)}</span
|
|
82
|
+
>`}
|
|
83
|
+
</span>`;
|
|
84
|
+
return html `<div
|
|
85
|
+
data-slot="attachment"
|
|
86
|
+
data-kind="file"
|
|
87
|
+
class="${() => cn("bg-background relative flex w-fit max-w-64 items-center gap-2 rounded-md border p-2 pe-7", read(props.class))}"
|
|
88
|
+
>
|
|
89
|
+
${props.href === undefined
|
|
90
|
+
? html `<span class="flex min-w-0 items-center gap-2">${body}</span>`
|
|
91
|
+
: html `<a href="${props.href}" class="flex min-w-0 items-center gap-2 hover:underline"
|
|
92
|
+
>${body}</a
|
|
93
|
+
>`}
|
|
94
|
+
${removeButton}
|
|
95
|
+
</div>`;
|
|
96
|
+
});
|
|
97
|
+
/** The extension, for the icon tile. `"report.final.pdf"` → `"pdf"`. */
|
|
98
|
+
function extensionOf(name) {
|
|
99
|
+
const dot = name.lastIndexOf(".");
|
|
100
|
+
if (dot <= 0 || dot === name.length - 1)
|
|
101
|
+
return "?";
|
|
102
|
+
return name.slice(dot + 1).slice(0, 4);
|
|
103
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb — the trail back up the hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* `aria-label="breadcrumb"` on the `<nav>` and `aria-current="page"` on the
|
|
5
|
+
* last entry. Both matter: the label is how a screen reader distinguishes this
|
|
6
|
+
* navigation from the other landmarks on the page, and `aria-current` is what
|
|
7
|
+
* tells the user which crumb is where they are — the visual weight change does
|
|
8
|
+
* not carry that.
|
|
9
|
+
*
|
|
10
|
+
* The separators are `aria-hidden` and marked `role="presentation"`. Read
|
|
11
|
+
* aloud, a chevron between every crumb is noise; the list structure already
|
|
12
|
+
* conveys the sequence.
|
|
13
|
+
*/
|
|
14
|
+
import type { Child } from "../lib/children.js";
|
|
15
|
+
import { type Reactive } from "../lib/props.js";
|
|
16
|
+
export interface Crumb {
|
|
17
|
+
label: Child;
|
|
18
|
+
/** Absent on the current page, and on a collapsed ellipsis. */
|
|
19
|
+
href?: string;
|
|
20
|
+
/** Renders an ellipsis standing in for hidden ancestors. */
|
|
21
|
+
ellipsis?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface BreadcrumbProps {
|
|
24
|
+
items: readonly Crumb[];
|
|
25
|
+
/** Replaces the chevron between entries. */
|
|
26
|
+
separator?: Child;
|
|
27
|
+
class?: Reactive<string>;
|
|
28
|
+
}
|
|
29
|
+
export declare const Breadcrumb: (props?: BreadcrumbProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumb — the trail back up the hierarchy.
|
|
3
|
+
*
|
|
4
|
+
* `aria-label="breadcrumb"` on the `<nav>` and `aria-current="page"` on the
|
|
5
|
+
* last entry. Both matter: the label is how a screen reader distinguishes this
|
|
6
|
+
* navigation from the other landmarks on the page, and `aria-current` is what
|
|
7
|
+
* tells the user which crumb is where they are — the visual weight change does
|
|
8
|
+
* not carry that.
|
|
9
|
+
*
|
|
10
|
+
* The separators are `aria-hidden` and marked `role="presentation"`. Read
|
|
11
|
+
* aloud, a chevron between every crumb is noise; the list structure already
|
|
12
|
+
* conveys the sequence.
|
|
13
|
+
*/
|
|
14
|
+
import { component, html } from "@c9up/aurora";
|
|
15
|
+
import { cn } from "../lib/cn.js";
|
|
16
|
+
import { ChevronRightIcon, MoreHorizontalIcon } from "../lib/icons.js";
|
|
17
|
+
import { read } from "../lib/props.js";
|
|
18
|
+
export const Breadcrumb = component((props) => {
|
|
19
|
+
return html `<nav
|
|
20
|
+
data-slot="breadcrumb"
|
|
21
|
+
aria-label="breadcrumb"
|
|
22
|
+
class="${() => cn(read(props.class))}"
|
|
23
|
+
>
|
|
24
|
+
<ol
|
|
25
|
+
data-slot="breadcrumb-list"
|
|
26
|
+
class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5"
|
|
27
|
+
>
|
|
28
|
+
${props.items.map((item, index) => renderCrumb(item, index === props.items.length - 1, index > 0, props.separator))}
|
|
29
|
+
</ol>
|
|
30
|
+
</nav>`;
|
|
31
|
+
});
|
|
32
|
+
function renderCrumb(item, isLast, needsSeparator, separator) {
|
|
33
|
+
return [
|
|
34
|
+
needsSeparator
|
|
35
|
+
? html `<li
|
|
36
|
+
data-slot="breadcrumb-separator"
|
|
37
|
+
role="presentation"
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
class="[&>svg]:size-3.5"
|
|
40
|
+
>${separator ?? ChevronRightIcon()}</li>`
|
|
41
|
+
: null,
|
|
42
|
+
html `<li data-slot="breadcrumb-item" class="inline-flex items-center gap-1.5">
|
|
43
|
+
${renderCrumbBody(item, isLast)}
|
|
44
|
+
</li>`,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
function renderCrumbBody(item, isLast) {
|
|
48
|
+
if (item.ellipsis === true) {
|
|
49
|
+
return html `<span
|
|
50
|
+
data-slot="breadcrumb-ellipsis"
|
|
51
|
+
role="presentation"
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
class="flex size-9 items-center justify-center"
|
|
54
|
+
>
|
|
55
|
+
${MoreHorizontalIcon({ class: "size-4" })}
|
|
56
|
+
<span class="sr-only">More</span>
|
|
57
|
+
</span>`;
|
|
58
|
+
}
|
|
59
|
+
if (isLast || item.href === undefined) {
|
|
60
|
+
return html `<span
|
|
61
|
+
data-slot="breadcrumb-page"
|
|
62
|
+
role="link"
|
|
63
|
+
aria-disabled="true"
|
|
64
|
+
aria-current="page"
|
|
65
|
+
class="text-foreground font-normal"
|
|
66
|
+
>${item.label}</span>`;
|
|
67
|
+
}
|
|
68
|
+
return html `<a
|
|
69
|
+
data-slot="breadcrumb-link"
|
|
70
|
+
href="${item.href}"
|
|
71
|
+
class="hover:text-foreground transition-colors"
|
|
72
|
+
>${item.label}</a>`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble — conversational content in a speech bubble.
|
|
3
|
+
*
|
|
4
|
+
* Alignment is **logical**, not physical: `align: "end"` uses `ms-auto` and an
|
|
5
|
+
* inline-end tail, so the same message sits on the right in English and on the
|
|
6
|
+
* left in Arabic without the caller knowing which. Writing `ml-auto` here would
|
|
7
|
+
* have been shorter and would have put every outgoing message on the wrong side
|
|
8
|
+
* of an RTL conversation.
|
|
9
|
+
*
|
|
10
|
+
* The collapse is `line-clamp` plus a toggle rather than a max-height with
|
|
11
|
+
* overflow hidden. `line-clamp` cuts at a line boundary and adds the ellipsis
|
|
12
|
+
* itself; a height cut slices through the middle of a line of text, which is
|
|
13
|
+
* the tell of a hand-rolled version.
|
|
14
|
+
*
|
|
15
|
+
* Reactions are a `<ul>`, and each one is a toggle button with
|
|
16
|
+
* `aria-pressed` — not a decorative span. A reaction the user can add is a
|
|
17
|
+
* control, and a count nobody can operate belongs in the message text.
|
|
18
|
+
*/
|
|
19
|
+
import { type Slot } from "../lib/children.js";
|
|
20
|
+
import { type VariantProps } from "../lib/cva.js";
|
|
21
|
+
import { type Reactive } from "../lib/props.js";
|
|
22
|
+
export declare const bubbleVariants: (props?: (import("../index.js").VariantSelection<{
|
|
23
|
+
variant: {
|
|
24
|
+
default: string;
|
|
25
|
+
accent: string;
|
|
26
|
+
outline: string;
|
|
27
|
+
ghost: string;
|
|
28
|
+
};
|
|
29
|
+
align: {
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
};
|
|
33
|
+
}> & import("../index.js").ClassOverrides) | undefined) => string;
|
|
34
|
+
export type BubbleVariants = VariantProps<typeof bubbleVariants>;
|
|
35
|
+
export interface BubbleReaction {
|
|
36
|
+
emoji: string;
|
|
37
|
+
count?: number;
|
|
38
|
+
/** The current user has reacted with this one. */
|
|
39
|
+
active?: boolean;
|
|
40
|
+
onToggle?: () => void;
|
|
41
|
+
}
|
|
42
|
+
export interface BubbleProps {
|
|
43
|
+
children?: Slot;
|
|
44
|
+
variant?: Reactive<BubbleVariants["variant"]>;
|
|
45
|
+
align?: Reactive<BubbleVariants["align"]>;
|
|
46
|
+
reactions?: readonly BubbleReaction[];
|
|
47
|
+
/** Clamp long content behind a toggle. */
|
|
48
|
+
collapsible?: boolean;
|
|
49
|
+
/** Lines shown while collapsed. Default `6`. */
|
|
50
|
+
collapsedLines?: number;
|
|
51
|
+
/** Label for the expand toggle. Default `"Show more"`. */
|
|
52
|
+
expandLabel?: string;
|
|
53
|
+
collapseLabel?: string;
|
|
54
|
+
class?: Reactive<string>;
|
|
55
|
+
}
|
|
56
|
+
export declare const Bubble: (props?: BubbleProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble — conversational content in a speech bubble.
|
|
3
|
+
*
|
|
4
|
+
* Alignment is **logical**, not physical: `align: "end"` uses `ms-auto` and an
|
|
5
|
+
* inline-end tail, so the same message sits on the right in English and on the
|
|
6
|
+
* left in Arabic without the caller knowing which. Writing `ml-auto` here would
|
|
7
|
+
* have been shorter and would have put every outgoing message on the wrong side
|
|
8
|
+
* of an RTL conversation.
|
|
9
|
+
*
|
|
10
|
+
* The collapse is `line-clamp` plus a toggle rather than a max-height with
|
|
11
|
+
* overflow hidden. `line-clamp` cuts at a line boundary and adds the ellipsis
|
|
12
|
+
* itself; a height cut slices through the middle of a line of text, which is
|
|
13
|
+
* the tell of a hand-rolled version.
|
|
14
|
+
*
|
|
15
|
+
* Reactions are a `<ul>`, and each one is a toggle button with
|
|
16
|
+
* `aria-pressed` — not a decorative span. A reaction the user can add is a
|
|
17
|
+
* control, and a count nobody can operate belongs in the message text.
|
|
18
|
+
*/
|
|
19
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
20
|
+
import { slot } from "../lib/children.js";
|
|
21
|
+
import { cva } from "../lib/cva.js";
|
|
22
|
+
import { read } from "../lib/props.js";
|
|
23
|
+
export const bubbleVariants = cva("relative w-fit max-w-[min(36rem,80%)] rounded-lg px-3 py-2 text-sm break-words", {
|
|
24
|
+
variants: {
|
|
25
|
+
variant: {
|
|
26
|
+
default: "bg-muted text-foreground",
|
|
27
|
+
accent: "bg-primary text-primary-foreground",
|
|
28
|
+
outline: "border bg-background text-foreground",
|
|
29
|
+
ghost: "text-foreground px-0",
|
|
30
|
+
},
|
|
31
|
+
align: {
|
|
32
|
+
// Logical margins, so the side follows the writing direction.
|
|
33
|
+
start: "me-auto",
|
|
34
|
+
end: "ms-auto",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: { variant: "default", align: "start" },
|
|
38
|
+
});
|
|
39
|
+
export const Bubble = component((props) => {
|
|
40
|
+
const expanded = signal(false);
|
|
41
|
+
const lines = props.collapsedLines ?? 6;
|
|
42
|
+
const bodyClass = () => {
|
|
43
|
+
if (props.collapsible !== true || expanded())
|
|
44
|
+
return "";
|
|
45
|
+
// An arbitrary value, because line-clamp-N only exists for 1–6 and the
|
|
46
|
+
// caller can ask for any depth.
|
|
47
|
+
return `line-clamp-[${lines}]`;
|
|
48
|
+
};
|
|
49
|
+
return html `<div
|
|
50
|
+
data-slot="bubble"
|
|
51
|
+
class="${() => bubbleVariants({
|
|
52
|
+
variant: read(props.variant),
|
|
53
|
+
align: read(props.align),
|
|
54
|
+
class: read(props.class),
|
|
55
|
+
})}"
|
|
56
|
+
>
|
|
57
|
+
<div data-slot="bubble-content" class="${bodyClass}">${slot(props.children)}</div>
|
|
58
|
+
${props.collapsible !== true
|
|
59
|
+
? null
|
|
60
|
+
: html `<button
|
|
61
|
+
type="button"
|
|
62
|
+
data-slot="bubble-toggle"
|
|
63
|
+
aria-expanded="${() => (expanded() ? "true" : "false")}"
|
|
64
|
+
class="mt-1 text-xs font-medium underline-offset-4 hover:underline"
|
|
65
|
+
@click="${() => expanded(!expanded())}"
|
|
66
|
+
>${() => expanded()
|
|
67
|
+
? (props.collapseLabel ?? "Show less")
|
|
68
|
+
: (props.expandLabel ?? "Show more")}</button>`}
|
|
69
|
+
${props.reactions === undefined || props.reactions.length === 0
|
|
70
|
+
? null
|
|
71
|
+
: html `<ul data-slot="bubble-reactions" class="mt-1.5 flex flex-wrap gap-1">
|
|
72
|
+
${props.reactions.map(renderReaction)}
|
|
73
|
+
</ul>`}
|
|
74
|
+
</div>`;
|
|
75
|
+
});
|
|
76
|
+
function renderReaction(reaction) {
|
|
77
|
+
return html `<li>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
data-slot="bubble-reaction"
|
|
81
|
+
aria-pressed="${reaction.active === true ? "true" : "false"}"
|
|
82
|
+
aria-label="${`${reaction.emoji}${reaction.count === undefined ? "" : `, ${reaction.count}`}`}"
|
|
83
|
+
class="bg-background/60 hover:bg-background aria-pressed:border-primary aria-pressed:bg-primary/10 flex h-6 items-center gap-1 rounded-full border px-1.5 text-xs transition-colors"
|
|
84
|
+
@click="${() => reaction.onToggle?.()}"
|
|
85
|
+
>
|
|
86
|
+
<span aria-hidden="true">${reaction.emoji}</span>
|
|
87
|
+
${reaction.count === undefined
|
|
88
|
+
? null
|
|
89
|
+
: html `<span class="tabular-nums">${reaction.count}</span>`}
|
|
90
|
+
</button>
|
|
91
|
+
</li>`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonGroup — buttons welded into one control.
|
|
3
|
+
*
|
|
4
|
+
* `role="group"` with a label, so a screen reader announces the set before its
|
|
5
|
+
* members rather than three unrelated buttons in a row.
|
|
6
|
+
*
|
|
7
|
+
* The rounding and border collapsing are done with `:first-child` /
|
|
8
|
+
* `:last-child` selectors on the group rather than props on each button. That
|
|
9
|
+
* is what lets the members stay ordinary `Button` calls — adding one does not
|
|
10
|
+
* mean re-deciding which button is now on the end.
|
|
11
|
+
*/
|
|
12
|
+
import { type Slot } from "../lib/children.js";
|
|
13
|
+
import { type Reactive } from "../lib/props.js";
|
|
14
|
+
export interface ButtonGroupProps {
|
|
15
|
+
children?: Slot;
|
|
16
|
+
orientation?: "horizontal" | "vertical";
|
|
17
|
+
/** Announced before the group's contents. */
|
|
18
|
+
label?: string;
|
|
19
|
+
class?: Reactive<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare const ButtonGroup: (props?: ButtonGroupProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
22
|
+
export declare const ButtonGroupSeparator: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
23
|
+
export declare const ButtonGroupText: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ButtonGroup — buttons welded into one control.
|
|
3
|
+
*
|
|
4
|
+
* `role="group"` with a label, so a screen reader announces the set before its
|
|
5
|
+
* members rather than three unrelated buttons in a row.
|
|
6
|
+
*
|
|
7
|
+
* The rounding and border collapsing are done with `:first-child` /
|
|
8
|
+
* `:last-child` selectors on the group rather than props on each button. That
|
|
9
|
+
* is what lets the members stay ordinary `Button` calls — adding one does not
|
|
10
|
+
* mean re-deciding which button is now on the end.
|
|
11
|
+
*/
|
|
12
|
+
import { component, html } from "@c9up/aurora";
|
|
13
|
+
import { slot } from "../lib/children.js";
|
|
14
|
+
import { cn } from "../lib/cn.js";
|
|
15
|
+
import { read } from "../lib/props.js";
|
|
16
|
+
import { styledDiv } from "../lib/styled.js";
|
|
17
|
+
export const ButtonGroup = component((props) => {
|
|
18
|
+
const vertical = props.orientation === "vertical";
|
|
19
|
+
return html `<div
|
|
20
|
+
data-slot="button-group"
|
|
21
|
+
role="group"
|
|
22
|
+
aria-label="${props.label}"
|
|
23
|
+
data-orientation="${vertical ? "vertical" : "horizontal"}"
|
|
24
|
+
class="${() => cn("flex w-fit items-stretch", vertical
|
|
25
|
+
? "flex-col [&>*]:rounded-none [&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md [&>*:not(:first-child)]:-mt-px"
|
|
26
|
+
: "[&>*]:rounded-none [&>*:first-child]:rounded-l-md [&>*:last-child]:rounded-r-md [&>*:not(:first-child)]:-ml-px", "[&>*]:focus-visible:z-10 [&>*]:focus:z-10", read(props.class))}"
|
|
27
|
+
>${slot(props.children)}</div>`;
|
|
28
|
+
});
|
|
29
|
+
export const ButtonGroupSeparator = styledDiv("button-group-separator", "bg-input relative !m-0 self-stretch w-px");
|
|
30
|
+
export const ButtonGroupText = styledDiv("button-group-text", "bg-muted text-muted-foreground flex items-center gap-2 border px-4 text-sm font-medium");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card — a bordered surface holding one unit of content.
|
|
3
|
+
*
|
|
4
|
+
* Seven parts, all presentational. The only one worth explaining is
|
|
5
|
+
* `CardAction`: it is grid-positioned into the header's second column so a
|
|
6
|
+
* button or menu trigger sits opposite the title without the header needing a
|
|
7
|
+
* flex wrapper and a spacer. The header switches to two columns only when an
|
|
8
|
+
* action is present, via `has-data-[slot=card-action]`.
|
|
9
|
+
*/
|
|
10
|
+
export type { StyledProps as CardProps } from "../lib/styled.js";
|
|
11
|
+
export declare const Card: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
12
|
+
export declare const CardHeader: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
13
|
+
export declare const CardTitle: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
14
|
+
export declare const CardDescription: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
15
|
+
export declare const CardAction: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
16
|
+
export declare const CardContent: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
17
|
+
export declare const CardFooter: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Card — a bordered surface holding one unit of content.
|
|
3
|
+
*
|
|
4
|
+
* Seven parts, all presentational. The only one worth explaining is
|
|
5
|
+
* `CardAction`: it is grid-positioned into the header's second column so a
|
|
6
|
+
* button or menu trigger sits opposite the title without the header needing a
|
|
7
|
+
* flex wrapper and a spacer. The header switches to two columns only when an
|
|
8
|
+
* action is present, via `has-data-[slot=card-action]`.
|
|
9
|
+
*/
|
|
10
|
+
import { styledDiv } from "../lib/styled.js";
|
|
11
|
+
export const Card = styledDiv("card", "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm");
|
|
12
|
+
export const CardHeader = styledDiv("card-header", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6");
|
|
13
|
+
export const CardTitle = styledDiv("card-title", "leading-none font-semibold");
|
|
14
|
+
export const CardDescription = styledDiv("card-description", "text-muted-foreground text-sm");
|
|
15
|
+
export const CardAction = styledDiv("card-action", "col-start-2 row-span-2 row-start-1 self-start justify-self-end");
|
|
16
|
+
export const CardContent = styledDiv("card-content", "px-6");
|
|
17
|
+
export const CardFooter = styledDiv("card-footer", "flex items-center px-6 [.border-t]:pt-6");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsible — a trigger that shows and hides a panel.
|
|
3
|
+
*
|
|
4
|
+
* Two things differ from Radix, both deliberate.
|
|
5
|
+
*
|
|
6
|
+
* **The API.** Radix composes through React context: `<Collapsible>` publishes
|
|
7
|
+
* state that `<CollapsibleTrigger>` and `<CollapsibleContent>` read from
|
|
8
|
+
* anywhere below it. Aurora has no context, and the usual workarounds — a
|
|
9
|
+
* factory returning bound parts, or threading a handle through props — trade a
|
|
10
|
+
* real problem for a clumsier one. So the parts are props: `trigger` and
|
|
11
|
+
* `children`. The rendered markup is identical to shadcn's; only the call
|
|
12
|
+
* shape changes.
|
|
13
|
+
*
|
|
14
|
+
* **The animation.** Radix measures the content and publishes its height as
|
|
15
|
+
* `--radix-collapsible-content-height` for the keyframes to interpolate,
|
|
16
|
+
* because `height: auto` is not animatable. A CSS grid whose single row goes
|
|
17
|
+
* from `0fr` to `1fr` animates the same transition with no measurement, no
|
|
18
|
+
* resize observer, and no stale height when the content changes while closed.
|
|
19
|
+
*
|
|
20
|
+
* The closed panel stays in the DOM — that is what makes the transition
|
|
21
|
+
* possible — so it is marked `inert`. Without it the panel keeps its tab stops
|
|
22
|
+
* and stays in the accessibility tree, and a keyboard user tabs into content
|
|
23
|
+
* that is not on screen.
|
|
24
|
+
*/
|
|
25
|
+
import { type Slot } from "../lib/children.js";
|
|
26
|
+
import { type Reactive } from "../lib/props.js";
|
|
27
|
+
export interface CollapsibleProps {
|
|
28
|
+
/** The clickable summary. Rendered inside a button. */
|
|
29
|
+
trigger?: Slot;
|
|
30
|
+
/** The panel revealed when open. */
|
|
31
|
+
children?: Slot;
|
|
32
|
+
open?: Reactive<boolean>;
|
|
33
|
+
defaultOpen?: boolean;
|
|
34
|
+
disabled?: Reactive<boolean>;
|
|
35
|
+
onOpenChange?: (open: boolean) => void;
|
|
36
|
+
class?: Reactive<string>;
|
|
37
|
+
triggerClass?: Reactive<string>;
|
|
38
|
+
contentClass?: Reactive<string>;
|
|
39
|
+
}
|
|
40
|
+
export declare const Collapsible: (props?: CollapsibleProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapsible — a trigger that shows and hides a panel.
|
|
3
|
+
*
|
|
4
|
+
* Two things differ from Radix, both deliberate.
|
|
5
|
+
*
|
|
6
|
+
* **The API.** Radix composes through React context: `<Collapsible>` publishes
|
|
7
|
+
* state that `<CollapsibleTrigger>` and `<CollapsibleContent>` read from
|
|
8
|
+
* anywhere below it. Aurora has no context, and the usual workarounds — a
|
|
9
|
+
* factory returning bound parts, or threading a handle through props — trade a
|
|
10
|
+
* real problem for a clumsier one. So the parts are props: `trigger` and
|
|
11
|
+
* `children`. The rendered markup is identical to shadcn's; only the call
|
|
12
|
+
* shape changes.
|
|
13
|
+
*
|
|
14
|
+
* **The animation.** Radix measures the content and publishes its height as
|
|
15
|
+
* `--radix-collapsible-content-height` for the keyframes to interpolate,
|
|
16
|
+
* because `height: auto` is not animatable. A CSS grid whose single row goes
|
|
17
|
+
* from `0fr` to `1fr` animates the same transition with no measurement, no
|
|
18
|
+
* resize observer, and no stale height when the content changes while closed.
|
|
19
|
+
*
|
|
20
|
+
* The closed panel stays in the DOM — that is what makes the transition
|
|
21
|
+
* possible — so it is marked `inert`. Without it the panel keeps its tab stops
|
|
22
|
+
* and stays in the accessibility tree, and a keyboard user tabs into content
|
|
23
|
+
* that is not on screen.
|
|
24
|
+
*/
|
|
25
|
+
import { component, html } from "@c9up/aurora";
|
|
26
|
+
import { slot } from "../lib/children.js";
|
|
27
|
+
import { cn } from "../lib/cn.js";
|
|
28
|
+
import { uid } from "../lib/id.js";
|
|
29
|
+
import { read, readOr } from "../lib/props.js";
|
|
30
|
+
import { controllable } from "../primitives/controllable.js";
|
|
31
|
+
export const Collapsible = component((props) => {
|
|
32
|
+
const state = controllable({
|
|
33
|
+
value: props.open,
|
|
34
|
+
initial: props.defaultOpen ?? false,
|
|
35
|
+
onChange: props.onOpenChange,
|
|
36
|
+
});
|
|
37
|
+
const contentId = uid("collapsible-content");
|
|
38
|
+
const triggerId = uid("collapsible-trigger");
|
|
39
|
+
const toggle = () => {
|
|
40
|
+
if (readOr(props.disabled, false))
|
|
41
|
+
return;
|
|
42
|
+
state.set(!state.current());
|
|
43
|
+
};
|
|
44
|
+
return html `<div
|
|
45
|
+
data-slot="collapsible"
|
|
46
|
+
data-state="${() => (state.current() ? "open" : "closed")}"
|
|
47
|
+
class="${() => cn("flex flex-col", read(props.class))}"
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
data-slot="collapsible-trigger"
|
|
52
|
+
id="${triggerId}"
|
|
53
|
+
aria-expanded="${() => (state.current() ? "true" : "false")}"
|
|
54
|
+
aria-controls="${contentId}"
|
|
55
|
+
?disabled="${() => readOr(props.disabled, false)}"
|
|
56
|
+
class="${() => cn("flex items-center justify-between gap-2 outline-none disabled:opacity-50", read(props.triggerClass))}"
|
|
57
|
+
@click="${toggle}"
|
|
58
|
+
>${slot(props.trigger)}</button>
|
|
59
|
+
<div
|
|
60
|
+
data-slot="collapsible-content"
|
|
61
|
+
id="${contentId}"
|
|
62
|
+
role="region"
|
|
63
|
+
aria-labelledby="${triggerId}"
|
|
64
|
+
?inert="${() => !state.current()}"
|
|
65
|
+
class="grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
|
|
66
|
+
style="${() => `grid-template-rows: ${state.current() ? "1fr" : "0fr"}`}"
|
|
67
|
+
>
|
|
68
|
+
<div class="${() => cn("overflow-hidden", read(props.contentClass))}">${slot(props.children)}</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>`;
|
|
71
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty — the placeholder for a list, table or panel with nothing in it.
|
|
3
|
+
*
|
|
4
|
+
* Worth having as a component rather than ad-hoc markup, because an empty
|
|
5
|
+
* state is where an app most often falls back to a bare "No results" and
|
|
6
|
+
* leaves the user without a next step. The parts push towards the useful
|
|
7
|
+
* shape: an icon, a title, a sentence of explanation, and something to do
|
|
8
|
+
* about it.
|
|
9
|
+
*/
|
|
10
|
+
export type { StyledProps as EmptyProps } from "../lib/styled.js";
|
|
11
|
+
export declare const Empty: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
12
|
+
export declare const EmptyHeader: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
13
|
+
export declare const EmptyMedia: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
14
|
+
export declare const EmptyTitle: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
15
|
+
export declare const EmptyDescription: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
16
|
+
export declare const EmptyContent: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty — the placeholder for a list, table or panel with nothing in it.
|
|
3
|
+
*
|
|
4
|
+
* Worth having as a component rather than ad-hoc markup, because an empty
|
|
5
|
+
* state is where an app most often falls back to a bare "No results" and
|
|
6
|
+
* leaves the user without a next step. The parts push towards the useful
|
|
7
|
+
* shape: an icon, a title, a sentence of explanation, and something to do
|
|
8
|
+
* about it.
|
|
9
|
+
*/
|
|
10
|
+
import { styledDiv } from "../lib/styled.js";
|
|
11
|
+
export const Empty = styledDiv("empty", "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12");
|
|
12
|
+
export const EmptyHeader = styledDiv("empty-header", "flex max-w-sm flex-col items-center gap-2 text-center");
|
|
13
|
+
export const EmptyMedia = styledDiv("empty-media", "bg-muted text-muted-foreground mb-2 flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-5");
|
|
14
|
+
export const EmptyTitle = styledDiv("empty-title", "text-lg font-medium tracking-tight");
|
|
15
|
+
export const EmptyDescription = styledDiv("empty-description", "text-muted-foreground text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4");
|
|
16
|
+
export const EmptyContent = styledDiv("empty-content", "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field — a form control with its label, hint and error.
|
|
3
|
+
*
|
|
4
|
+
* The reason this exists rather than each form writing the four elements by
|
|
5
|
+
* hand is the wiring. A control must point at its description and its error
|
|
6
|
+
* with `aria-describedby`, and at its own label with `for`/`id`. Done by hand
|
|
7
|
+
* that is three ids to invent and keep in step per field, and the failure is
|
|
8
|
+
* silent — a sighted user sees the error, a screen-reader user does not.
|
|
9
|
+
*
|
|
10
|
+
* `Field` mints the ids and returns them, so the control gets them by
|
|
11
|
+
* destructuring rather than by convention:
|
|
12
|
+
*
|
|
13
|
+
* const email = fieldIds()
|
|
14
|
+
* Field({
|
|
15
|
+
* ids: email,
|
|
16
|
+
* label: "Email",
|
|
17
|
+
* error: () => errors.email,
|
|
18
|
+
* children: Input({ id: email.control, describedBy: email.describedBy, invalid: … }),
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* The error slot is reactive and empty-tolerant: passing `() => errors.email`
|
|
22
|
+
* renders nothing until there is something to render, so a field does not
|
|
23
|
+
* reserve a blank line for an error that has not happened.
|
|
24
|
+
*/
|
|
25
|
+
import type { Child, Slot } from "../lib/children.js";
|
|
26
|
+
import { type Reactive } from "../lib/props.js";
|
|
27
|
+
export interface FieldIds {
|
|
28
|
+
/** Put on the control. The label's `for` points here. */
|
|
29
|
+
readonly control: string;
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly error: string;
|
|
33
|
+
/** Ready-made `aria-describedby` naming both the description and the error. */
|
|
34
|
+
readonly describedBy: string;
|
|
35
|
+
}
|
|
36
|
+
/** Mint the id set for one field. Call once, in the component that owns it. */
|
|
37
|
+
export declare function fieldIds(): FieldIds;
|
|
38
|
+
export interface FieldProps {
|
|
39
|
+
ids: FieldIds;
|
|
40
|
+
label?: Child;
|
|
41
|
+
/** The control itself. */
|
|
42
|
+
children?: Slot;
|
|
43
|
+
/** Static help text under the control. */
|
|
44
|
+
description?: Child;
|
|
45
|
+
/** Validation message. Reactive — renders nothing while absent. */
|
|
46
|
+
error?: Reactive<string | undefined>;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
disabled?: Reactive<boolean>;
|
|
49
|
+
orientation?: "vertical" | "horizontal";
|
|
50
|
+
class?: Reactive<string>;
|
|
51
|
+
}
|
|
52
|
+
export declare const Field: (props?: FieldProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
53
|
+
export declare const FieldGroup: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
54
|
+
export declare const FieldSeparator: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|