@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,134 @@
|
|
|
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
|
+
|
|
20
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
21
|
+
import type { Child } from "../lib/children.js";
|
|
22
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
23
|
+
import { cva, type VariantProps } from "../lib/cva.js";
|
|
24
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
25
|
+
|
|
26
|
+
export const bubbleVariants = cva(
|
|
27
|
+
"relative w-fit max-w-[min(36rem,80%)] rounded-lg px-3 py-2 text-sm break-words",
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
default: "bg-muted text-foreground",
|
|
32
|
+
accent: "bg-primary text-primary-foreground",
|
|
33
|
+
outline: "border bg-background text-foreground",
|
|
34
|
+
ghost: "text-foreground px-0",
|
|
35
|
+
},
|
|
36
|
+
align: {
|
|
37
|
+
// Logical margins, so the side follows the writing direction.
|
|
38
|
+
start: "me-auto",
|
|
39
|
+
end: "ms-auto",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: { variant: "default", align: "start" },
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export type BubbleVariants = VariantProps<typeof bubbleVariants>;
|
|
47
|
+
|
|
48
|
+
export interface BubbleReaction {
|
|
49
|
+
emoji: string;
|
|
50
|
+
count?: number;
|
|
51
|
+
/** The current user has reacted with this one. */
|
|
52
|
+
active?: boolean;
|
|
53
|
+
onToggle?: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BubbleProps {
|
|
57
|
+
children?: Slot;
|
|
58
|
+
variant?: Reactive<BubbleVariants["variant"]>;
|
|
59
|
+
align?: Reactive<BubbleVariants["align"]>;
|
|
60
|
+
reactions?: readonly BubbleReaction[];
|
|
61
|
+
/** Clamp long content behind a toggle. */
|
|
62
|
+
collapsible?: boolean;
|
|
63
|
+
/** Lines shown while collapsed. Default `6`. */
|
|
64
|
+
collapsedLines?: number;
|
|
65
|
+
/** Label for the expand toggle. Default `"Show more"`. */
|
|
66
|
+
expandLabel?: string;
|
|
67
|
+
collapseLabel?: string;
|
|
68
|
+
class?: Reactive<string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const Bubble = component<BubbleProps>((props) => {
|
|
72
|
+
const expanded = signal(false);
|
|
73
|
+
const lines = props.collapsedLines ?? 6;
|
|
74
|
+
|
|
75
|
+
const bodyClass = (): string => {
|
|
76
|
+
if (props.collapsible !== true || expanded()) return "";
|
|
77
|
+
// An arbitrary value, because line-clamp-N only exists for 1–6 and the
|
|
78
|
+
// caller can ask for any depth.
|
|
79
|
+
return `line-clamp-[${lines}]`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return html`<div
|
|
83
|
+
data-slot="bubble"
|
|
84
|
+
class="${() =>
|
|
85
|
+
bubbleVariants({
|
|
86
|
+
variant: read(props.variant),
|
|
87
|
+
align: read(props.align),
|
|
88
|
+
class: read(props.class),
|
|
89
|
+
})}"
|
|
90
|
+
>
|
|
91
|
+
<div data-slot="bubble-content" class="${bodyClass}">${slot(props.children)}</div>
|
|
92
|
+
${
|
|
93
|
+
props.collapsible !== true
|
|
94
|
+
? null
|
|
95
|
+
: html`<button
|
|
96
|
+
type="button"
|
|
97
|
+
data-slot="bubble-toggle"
|
|
98
|
+
aria-expanded="${() => (expanded() ? "true" : "false")}"
|
|
99
|
+
class="mt-1 text-xs font-medium underline-offset-4 hover:underline"
|
|
100
|
+
@click="${() => expanded(!expanded())}"
|
|
101
|
+
>${() =>
|
|
102
|
+
expanded()
|
|
103
|
+
? (props.collapseLabel ?? "Show less")
|
|
104
|
+
: (props.expandLabel ?? "Show more")}</button>`
|
|
105
|
+
}
|
|
106
|
+
${
|
|
107
|
+
props.reactions === undefined || props.reactions.length === 0
|
|
108
|
+
? null
|
|
109
|
+
: html`<ul data-slot="bubble-reactions" class="mt-1.5 flex flex-wrap gap-1">
|
|
110
|
+
${props.reactions.map(renderReaction)}
|
|
111
|
+
</ul>`
|
|
112
|
+
}
|
|
113
|
+
</div>`;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function renderReaction(reaction: BubbleReaction): Child {
|
|
117
|
+
return html`<li>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
data-slot="bubble-reaction"
|
|
121
|
+
aria-pressed="${reaction.active === true ? "true" : "false"}"
|
|
122
|
+
aria-label="${`${reaction.emoji}${reaction.count === undefined ? "" : `, ${reaction.count}`}`}"
|
|
123
|
+
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"
|
|
124
|
+
@click="${() => reaction.onToggle?.()}"
|
|
125
|
+
>
|
|
126
|
+
<span aria-hidden="true">${reaction.emoji}</span>
|
|
127
|
+
${
|
|
128
|
+
reaction.count === undefined
|
|
129
|
+
? null
|
|
130
|
+
: html`<span class="tabular-nums">${reaction.count}</span>`
|
|
131
|
+
}
|
|
132
|
+
</button>
|
|
133
|
+
</li>`;
|
|
134
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
13
|
+
import { component, html } from "@c9up/aurora";
|
|
14
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
15
|
+
import { cn } from "../lib/cn.js";
|
|
16
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
17
|
+
import { styledDiv } from "../lib/styled.js";
|
|
18
|
+
|
|
19
|
+
export interface ButtonGroupProps {
|
|
20
|
+
children?: Slot;
|
|
21
|
+
orientation?: "horizontal" | "vertical";
|
|
22
|
+
/** Announced before the group's contents. */
|
|
23
|
+
label?: string;
|
|
24
|
+
class?: Reactive<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ButtonGroup = component<ButtonGroupProps>((props) => {
|
|
28
|
+
const vertical = props.orientation === "vertical";
|
|
29
|
+
|
|
30
|
+
return html`<div
|
|
31
|
+
data-slot="button-group"
|
|
32
|
+
role="group"
|
|
33
|
+
aria-label="${props.label}"
|
|
34
|
+
data-orientation="${vertical ? "vertical" : "horizontal"}"
|
|
35
|
+
class="${() =>
|
|
36
|
+
cn(
|
|
37
|
+
"flex w-fit items-stretch",
|
|
38
|
+
vertical
|
|
39
|
+
? "flex-col [&>*]:rounded-none [&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md [&>*:not(:first-child)]:-mt-px"
|
|
40
|
+
: "[&>*]:rounded-none [&>*:first-child]:rounded-l-md [&>*:last-child]:rounded-r-md [&>*:not(:first-child)]:-ml-px",
|
|
41
|
+
"[&>*]:focus-visible:z-10 [&>*]:focus:z-10",
|
|
42
|
+
read(props.class),
|
|
43
|
+
)}"
|
|
44
|
+
>${slot(props.children)}</div>`;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const ButtonGroupSeparator = styledDiv(
|
|
48
|
+
"button-group-separator",
|
|
49
|
+
"bg-input relative !m-0 self-stretch w-px",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const ButtonGroupText = styledDiv(
|
|
53
|
+
"button-group-text",
|
|
54
|
+
"bg-muted text-muted-foreground flex items-center gap-2 border px-4 text-sm font-medium",
|
|
55
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
11
|
+
import { styledDiv } from "../lib/styled.js";
|
|
12
|
+
|
|
13
|
+
export type { StyledProps as CardProps } from "../lib/styled.js";
|
|
14
|
+
|
|
15
|
+
export const Card = styledDiv(
|
|
16
|
+
"card",
|
|
17
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const CardHeader = styledDiv(
|
|
21
|
+
"card-header",
|
|
22
|
+
"@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",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const CardTitle = styledDiv("card-title", "leading-none font-semibold");
|
|
26
|
+
|
|
27
|
+
export const CardDescription = styledDiv(
|
|
28
|
+
"card-description",
|
|
29
|
+
"text-muted-foreground text-sm",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const CardAction = styledDiv(
|
|
33
|
+
"card-action",
|
|
34
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const CardContent = styledDiv("card-content", "px-6");
|
|
38
|
+
|
|
39
|
+
export const CardFooter = styledDiv(
|
|
40
|
+
"card-footer",
|
|
41
|
+
"flex items-center px-6 [.border-t]:pt-6",
|
|
42
|
+
);
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
|
|
26
|
+
import { component, html } from "@c9up/aurora";
|
|
27
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
28
|
+
import { cn } from "../lib/cn.js";
|
|
29
|
+
import { uid } from "../lib/id.js";
|
|
30
|
+
import { type Reactive, read, readOr } from "../lib/props.js";
|
|
31
|
+
import { controllable } from "../primitives/controllable.js";
|
|
32
|
+
|
|
33
|
+
export interface CollapsibleProps {
|
|
34
|
+
/** The clickable summary. Rendered inside a button. */
|
|
35
|
+
trigger?: Slot;
|
|
36
|
+
/** The panel revealed when open. */
|
|
37
|
+
children?: Slot;
|
|
38
|
+
open?: Reactive<boolean>;
|
|
39
|
+
defaultOpen?: boolean;
|
|
40
|
+
disabled?: Reactive<boolean>;
|
|
41
|
+
onOpenChange?: (open: boolean) => void;
|
|
42
|
+
class?: Reactive<string>;
|
|
43
|
+
triggerClass?: Reactive<string>;
|
|
44
|
+
contentClass?: Reactive<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Collapsible = component<CollapsibleProps>((props) => {
|
|
48
|
+
const state = controllable<boolean>({
|
|
49
|
+
value: props.open,
|
|
50
|
+
initial: props.defaultOpen ?? false,
|
|
51
|
+
onChange: props.onOpenChange,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const contentId = uid("collapsible-content");
|
|
55
|
+
const triggerId = uid("collapsible-trigger");
|
|
56
|
+
|
|
57
|
+
const toggle = (): void => {
|
|
58
|
+
if (readOr(props.disabled, false)) return;
|
|
59
|
+
state.set(!state.current());
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return html`<div
|
|
63
|
+
data-slot="collapsible"
|
|
64
|
+
data-state="${() => (state.current() ? "open" : "closed")}"
|
|
65
|
+
class="${() => cn("flex flex-col", read(props.class))}"
|
|
66
|
+
>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
data-slot="collapsible-trigger"
|
|
70
|
+
id="${triggerId}"
|
|
71
|
+
aria-expanded="${() => (state.current() ? "true" : "false")}"
|
|
72
|
+
aria-controls="${contentId}"
|
|
73
|
+
?disabled="${() => readOr(props.disabled, false)}"
|
|
74
|
+
class="${() => cn("flex items-center justify-between gap-2 outline-none disabled:opacity-50", read(props.triggerClass))}"
|
|
75
|
+
@click="${toggle}"
|
|
76
|
+
>${slot(props.trigger)}</button>
|
|
77
|
+
<div
|
|
78
|
+
data-slot="collapsible-content"
|
|
79
|
+
id="${contentId}"
|
|
80
|
+
role="region"
|
|
81
|
+
aria-labelledby="${triggerId}"
|
|
82
|
+
?inert="${() => !state.current()}"
|
|
83
|
+
class="grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
|
|
84
|
+
style="${() => `grid-template-rows: ${state.current() ? "1fr" : "0fr"}`}"
|
|
85
|
+
>
|
|
86
|
+
<div class="${() => cn("overflow-hidden", read(props.contentClass))}">${slot(props.children)}</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>`;
|
|
89
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
|
|
11
|
+
import { styledDiv } from "../lib/styled.js";
|
|
12
|
+
|
|
13
|
+
export type { StyledProps as EmptyProps } from "../lib/styled.js";
|
|
14
|
+
|
|
15
|
+
export const Empty = styledDiv(
|
|
16
|
+
"empty",
|
|
17
|
+
"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",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const EmptyHeader = styledDiv(
|
|
21
|
+
"empty-header",
|
|
22
|
+
"flex max-w-sm flex-col items-center gap-2 text-center",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const EmptyMedia = styledDiv(
|
|
26
|
+
"empty-media",
|
|
27
|
+
"bg-muted text-muted-foreground mb-2 flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-5",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const EmptyTitle = styledDiv(
|
|
31
|
+
"empty-title",
|
|
32
|
+
"text-lg font-medium tracking-tight",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const EmptyDescription = styledDiv(
|
|
36
|
+
"empty-description",
|
|
37
|
+
"text-muted-foreground text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const EmptyContent = styledDiv(
|
|
41
|
+
"empty-content",
|
|
42
|
+
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
|
43
|
+
);
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
|
|
26
|
+
import { component, html } from "@c9up/aurora";
|
|
27
|
+
import { Label } from "../atoms/Label.js";
|
|
28
|
+
import type { Child, Slot } from "../lib/children.js";
|
|
29
|
+
import { slot } from "../lib/children.js";
|
|
30
|
+
import { cn } from "../lib/cn.js";
|
|
31
|
+
import { uid } from "../lib/id.js";
|
|
32
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
33
|
+
import { styledDiv } from "../lib/styled.js";
|
|
34
|
+
|
|
35
|
+
export interface FieldIds {
|
|
36
|
+
/** Put on the control. The label's `for` points here. */
|
|
37
|
+
readonly control: string;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
readonly description: string;
|
|
40
|
+
readonly error: string;
|
|
41
|
+
/** Ready-made `aria-describedby` naming both the description and the error. */
|
|
42
|
+
readonly describedBy: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Mint the id set for one field. Call once, in the component that owns it. */
|
|
46
|
+
export function fieldIds(): FieldIds {
|
|
47
|
+
const control = uid("field");
|
|
48
|
+
const description = `${control}-description`;
|
|
49
|
+
const error = `${control}-error`;
|
|
50
|
+
return {
|
|
51
|
+
control,
|
|
52
|
+
label: `${control}-label`,
|
|
53
|
+
description,
|
|
54
|
+
error,
|
|
55
|
+
// Both are named unconditionally. A screen reader skips an id that
|
|
56
|
+
// resolves to nothing, so listing an absent error is harmless — whereas
|
|
57
|
+
// recomputing the attribute when an error appears means the control has to
|
|
58
|
+
// re-render, which Aurora does not do.
|
|
59
|
+
describedBy: `${description} ${error}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface FieldProps {
|
|
64
|
+
ids: FieldIds;
|
|
65
|
+
label?: Child;
|
|
66
|
+
/** The control itself. */
|
|
67
|
+
children?: Slot;
|
|
68
|
+
/** Static help text under the control. */
|
|
69
|
+
description?: Child;
|
|
70
|
+
/** Validation message. Reactive — renders nothing while absent. */
|
|
71
|
+
error?: Reactive<string | undefined>;
|
|
72
|
+
required?: boolean;
|
|
73
|
+
disabled?: Reactive<boolean>;
|
|
74
|
+
orientation?: "vertical" | "horizontal";
|
|
75
|
+
class?: Reactive<string>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const Field = component<FieldProps>((props) => {
|
|
79
|
+
const horizontal = props.orientation === "horizontal";
|
|
80
|
+
|
|
81
|
+
return html`<div
|
|
82
|
+
data-slot="field"
|
|
83
|
+
data-disabled="${() => (read(props.disabled) === true ? "true" : undefined)}"
|
|
84
|
+
class="${() =>
|
|
85
|
+
cn(
|
|
86
|
+
"group flex gap-2",
|
|
87
|
+
horizontal ? "flex-row items-center justify-between" : "flex-col",
|
|
88
|
+
read(props.class),
|
|
89
|
+
)}"
|
|
90
|
+
>
|
|
91
|
+
${
|
|
92
|
+
props.label === undefined
|
|
93
|
+
? null
|
|
94
|
+
: Label({
|
|
95
|
+
for: props.ids.control,
|
|
96
|
+
id: props.ids.label,
|
|
97
|
+
children: html`${props.label}${
|
|
98
|
+
props.required === true
|
|
99
|
+
? html`<span aria-hidden="true" class="text-destructive">*</span>`
|
|
100
|
+
: null
|
|
101
|
+
}`,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
${slot(props.children)}
|
|
105
|
+
${
|
|
106
|
+
props.description === undefined
|
|
107
|
+
? null
|
|
108
|
+
: html`<p
|
|
109
|
+
data-slot="field-description"
|
|
110
|
+
id="${props.ids.description}"
|
|
111
|
+
class="text-muted-foreground text-sm"
|
|
112
|
+
>${props.description}</p>`
|
|
113
|
+
}
|
|
114
|
+
<p
|
|
115
|
+
data-slot="field-error"
|
|
116
|
+
id="${props.ids.error}"
|
|
117
|
+
role="alert"
|
|
118
|
+
class="text-destructive text-sm empty:hidden"
|
|
119
|
+
>${() => read(props.error) ?? ""}</p>
|
|
120
|
+
</div>`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const FieldGroup = styledDiv(
|
|
124
|
+
"field-group",
|
|
125
|
+
"flex w-full flex-col gap-6",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
export const FieldSeparator = styledDiv(
|
|
129
|
+
"field-separator",
|
|
130
|
+
"bg-border h-px w-full",
|
|
131
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputGroup — an input with something attached to it.
|
|
3
|
+
*
|
|
4
|
+
* A currency prefix, a `.com` suffix, a search icon, a clear button. The group
|
|
5
|
+
* carries the border and the focus ring; the input inside is stripped bare.
|
|
6
|
+
*
|
|
7
|
+
* `focus-within:` is what moves the ring onto the wrapper, so focusing the
|
|
8
|
+
* input lights up the whole control including the addons. Styling the input's
|
|
9
|
+
* own `:focus-visible` would draw a ring around the middle third of a control
|
|
10
|
+
* that visually reads as one box.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { component, html } from "@c9up/aurora";
|
|
14
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
15
|
+
import { cn } from "../lib/cn.js";
|
|
16
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
17
|
+
import { styledDiv } from "../lib/styled.js";
|
|
18
|
+
|
|
19
|
+
export interface InputGroupProps {
|
|
20
|
+
children?: Slot;
|
|
21
|
+
/** Before the input — an icon or a static prefix. */
|
|
22
|
+
leading?: Slot;
|
|
23
|
+
/** After the input — a unit, a button, a spinner. */
|
|
24
|
+
trailing?: Slot;
|
|
25
|
+
invalid?: Reactive<boolean>;
|
|
26
|
+
disabled?: Reactive<boolean>;
|
|
27
|
+
class?: Reactive<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Strip the inner input of its own chrome.
|
|
32
|
+
*
|
|
33
|
+
* Exported so a caller passing a plain `<input>` rather than nebula's `Input`
|
|
34
|
+
* can apply it too — without this the input draws a second border inside the
|
|
35
|
+
* group's, which is the one way this component visibly goes wrong.
|
|
36
|
+
*/
|
|
37
|
+
export const inputGroupControlClasses =
|
|
38
|
+
"flex-1 border-0 bg-transparent px-0 shadow-none outline-none focus-visible:border-0 focus-visible:ring-0 disabled:opacity-100";
|
|
39
|
+
|
|
40
|
+
export const InputGroup = component<InputGroupProps>((props) => {
|
|
41
|
+
return html`<div
|
|
42
|
+
data-slot="input-group"
|
|
43
|
+
data-disabled="${() => (read(props.disabled) === true ? "" : undefined)}"
|
|
44
|
+
aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
|
|
45
|
+
class="${() =>
|
|
46
|
+
cn(
|
|
47
|
+
"border-input dark:bg-input/30 flex h-9 w-full min-w-0 items-center gap-2 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow]",
|
|
48
|
+
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
|
|
49
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
|
50
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
51
|
+
`[&_input]:${inputGroupControlClasses.split(" ").join(" [&_input]:")}`,
|
|
52
|
+
read(props.class),
|
|
53
|
+
)}"
|
|
54
|
+
>
|
|
55
|
+
${slot(props.leading)}${slot(props.children)}${slot(props.trailing)}
|
|
56
|
+
</div>`;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const InputGroupAddon = styledDiv(
|
|
60
|
+
"input-group-addon",
|
|
61
|
+
"text-muted-foreground flex shrink-0 items-center gap-2 [&_svg:not([class*='size-'])]:size-4",
|
|
62
|
+
);
|