@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,82 @@
|
|
|
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 { component, html } from "@c9up/aurora";
|
|
26
|
+
import { Label } from "../atoms/Label.js";
|
|
27
|
+
import { slot } from "../lib/children.js";
|
|
28
|
+
import { cn } from "../lib/cn.js";
|
|
29
|
+
import { uid } from "../lib/id.js";
|
|
30
|
+
import { read } from "../lib/props.js";
|
|
31
|
+
import { styledDiv } from "../lib/styled.js";
|
|
32
|
+
/** Mint the id set for one field. Call once, in the component that owns it. */
|
|
33
|
+
export function fieldIds() {
|
|
34
|
+
const control = uid("field");
|
|
35
|
+
const description = `${control}-description`;
|
|
36
|
+
const error = `${control}-error`;
|
|
37
|
+
return {
|
|
38
|
+
control,
|
|
39
|
+
label: `${control}-label`,
|
|
40
|
+
description,
|
|
41
|
+
error,
|
|
42
|
+
// Both are named unconditionally. A screen reader skips an id that
|
|
43
|
+
// resolves to nothing, so listing an absent error is harmless — whereas
|
|
44
|
+
// recomputing the attribute when an error appears means the control has to
|
|
45
|
+
// re-render, which Aurora does not do.
|
|
46
|
+
describedBy: `${description} ${error}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const Field = component((props) => {
|
|
50
|
+
const horizontal = props.orientation === "horizontal";
|
|
51
|
+
return html `<div
|
|
52
|
+
data-slot="field"
|
|
53
|
+
data-disabled="${() => (read(props.disabled) === true ? "true" : undefined)}"
|
|
54
|
+
class="${() => cn("group flex gap-2", horizontal ? "flex-row items-center justify-between" : "flex-col", read(props.class))}"
|
|
55
|
+
>
|
|
56
|
+
${props.label === undefined
|
|
57
|
+
? null
|
|
58
|
+
: Label({
|
|
59
|
+
for: props.ids.control,
|
|
60
|
+
id: props.ids.label,
|
|
61
|
+
children: html `${props.label}${props.required === true
|
|
62
|
+
? html `<span aria-hidden="true" class="text-destructive">*</span>`
|
|
63
|
+
: null}`,
|
|
64
|
+
})}
|
|
65
|
+
${slot(props.children)}
|
|
66
|
+
${props.description === undefined
|
|
67
|
+
? null
|
|
68
|
+
: html `<p
|
|
69
|
+
data-slot="field-description"
|
|
70
|
+
id="${props.ids.description}"
|
|
71
|
+
class="text-muted-foreground text-sm"
|
|
72
|
+
>${props.description}</p>`}
|
|
73
|
+
<p
|
|
74
|
+
data-slot="field-error"
|
|
75
|
+
id="${props.ids.error}"
|
|
76
|
+
role="alert"
|
|
77
|
+
class="text-destructive text-sm empty:hidden"
|
|
78
|
+
>${() => read(props.error) ?? ""}</p>
|
|
79
|
+
</div>`;
|
|
80
|
+
});
|
|
81
|
+
export const FieldGroup = styledDiv("field-group", "flex w-full flex-col gap-6");
|
|
82
|
+
export const FieldSeparator = styledDiv("field-separator", "bg-border h-px w-full");
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import { type Slot } from "../lib/children.js";
|
|
13
|
+
import { type Reactive } from "../lib/props.js";
|
|
14
|
+
export interface InputGroupProps {
|
|
15
|
+
children?: Slot;
|
|
16
|
+
/** Before the input — an icon or a static prefix. */
|
|
17
|
+
leading?: Slot;
|
|
18
|
+
/** After the input — a unit, a button, a spinner. */
|
|
19
|
+
trailing?: Slot;
|
|
20
|
+
invalid?: Reactive<boolean>;
|
|
21
|
+
disabled?: Reactive<boolean>;
|
|
22
|
+
class?: Reactive<string>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Strip the inner input of its own chrome.
|
|
26
|
+
*
|
|
27
|
+
* Exported so a caller passing a plain `<input>` rather than nebula's `Input`
|
|
28
|
+
* can apply it too — without this the input draws a second border inside the
|
|
29
|
+
* group's, which is the one way this component visibly goes wrong.
|
|
30
|
+
*/
|
|
31
|
+
export declare const inputGroupControlClasses = "flex-1 border-0 bg-transparent px-0 shadow-none outline-none focus-visible:border-0 focus-visible:ring-0 disabled:opacity-100";
|
|
32
|
+
export declare const InputGroup: (props?: InputGroupProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
33
|
+
export declare const InputGroupAddon: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
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
|
+
/**
|
|
18
|
+
* Strip the inner input of its own chrome.
|
|
19
|
+
*
|
|
20
|
+
* Exported so a caller passing a plain `<input>` rather than nebula's `Input`
|
|
21
|
+
* can apply it too — without this the input draws a second border inside the
|
|
22
|
+
* group's, which is the one way this component visibly goes wrong.
|
|
23
|
+
*/
|
|
24
|
+
export const inputGroupControlClasses = "flex-1 border-0 bg-transparent px-0 shadow-none outline-none focus-visible:border-0 focus-visible:ring-0 disabled:opacity-100";
|
|
25
|
+
export const InputGroup = component((props) => {
|
|
26
|
+
return html `<div
|
|
27
|
+
data-slot="input-group"
|
|
28
|
+
data-disabled="${() => (read(props.disabled) === true ? "" : undefined)}"
|
|
29
|
+
aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
|
|
30
|
+
class="${() => cn("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]", "focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]", "aria-invalid:border-destructive aria-invalid:ring-destructive/20", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", `[&_input]:${inputGroupControlClasses.split(" ").join(" [&_input]:")}`, read(props.class))}"
|
|
31
|
+
>
|
|
32
|
+
${slot(props.leading)}${slot(props.children)}${slot(props.trailing)}
|
|
33
|
+
</div>`;
|
|
34
|
+
});
|
|
35
|
+
export const InputGroupAddon = styledDiv("input-group-addon", "text-muted-foreground flex shrink-0 items-center gap-2 [&_svg:not([class*='size-'])]:size-4");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputOTP — a one-time code entered one character per box.
|
|
3
|
+
*
|
|
4
|
+
* The behaviour users expect from this control is entirely in the edge cases,
|
|
5
|
+
* and each one is a separate rule:
|
|
6
|
+
*
|
|
7
|
+
* - Typing advances to the next box; the last one stays put rather than
|
|
8
|
+
* wrapping round to the first.
|
|
9
|
+
* - Backspace in an empty box clears the *previous* one and moves back, which
|
|
10
|
+
* is what makes holding it down erase the whole code.
|
|
11
|
+
* - Pasting a code fills every box from wherever it was pasted, so the common
|
|
12
|
+
* case — copy the code out of the email, click the first box, paste — works.
|
|
13
|
+
* - `inputmode="numeric"` and `autocomplete="one-time-code"` are what get the
|
|
14
|
+
* numeric keypad on mobile and the OS's SMS autofill suggestion. Without the
|
|
15
|
+
* second, a user has to leave the app to read the code.
|
|
16
|
+
*
|
|
17
|
+
* The joined value is mirrored into a hidden input so the control submits as
|
|
18
|
+
* one field, rather than six.
|
|
19
|
+
*/
|
|
20
|
+
import { type Reactive } from "../lib/props.js";
|
|
21
|
+
export interface InputOTPProps {
|
|
22
|
+
/** Number of boxes. Default `6`. */
|
|
23
|
+
length?: number;
|
|
24
|
+
name?: string;
|
|
25
|
+
disabled?: Reactive<boolean>;
|
|
26
|
+
/** Insert a visual gap after this many boxes. `3` gives `123 456`. */
|
|
27
|
+
groupAfter?: number;
|
|
28
|
+
class?: Reactive<string>;
|
|
29
|
+
onValueChange?: (value: string) => void;
|
|
30
|
+
/** Fired once every box is filled. */
|
|
31
|
+
onComplete?: (value: string) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare const InputOTP: (props?: InputOTPProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputOTP — a one-time code entered one character per box.
|
|
3
|
+
*
|
|
4
|
+
* The behaviour users expect from this control is entirely in the edge cases,
|
|
5
|
+
* and each one is a separate rule:
|
|
6
|
+
*
|
|
7
|
+
* - Typing advances to the next box; the last one stays put rather than
|
|
8
|
+
* wrapping round to the first.
|
|
9
|
+
* - Backspace in an empty box clears the *previous* one and moves back, which
|
|
10
|
+
* is what makes holding it down erase the whole code.
|
|
11
|
+
* - Pasting a code fills every box from wherever it was pasted, so the common
|
|
12
|
+
* case — copy the code out of the email, click the first box, paste — works.
|
|
13
|
+
* - `inputmode="numeric"` and `autocomplete="one-time-code"` are what get the
|
|
14
|
+
* numeric keypad on mobile and the OS's SMS autofill suggestion. Without the
|
|
15
|
+
* second, a user has to leave the app to read the code.
|
|
16
|
+
*
|
|
17
|
+
* The joined value is mirrored into a hidden input so the control submits as
|
|
18
|
+
* one field, rather than six.
|
|
19
|
+
*/
|
|
20
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
21
|
+
import { cn } from "../lib/cn.js";
|
|
22
|
+
import { uid } from "../lib/id.js";
|
|
23
|
+
import { read, readOr } from "../lib/props.js";
|
|
24
|
+
const slotClasses = "border-input relative flex size-9 items-center justify-center border-y border-r text-center text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md focus:border-ring focus:ring-ring/50 focus:z-10 focus:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50";
|
|
25
|
+
export const InputOTP = component((props) => {
|
|
26
|
+
const length = props.length ?? 6;
|
|
27
|
+
const groupId = uid("input-otp");
|
|
28
|
+
const characters = signal(new Array(length).fill(""));
|
|
29
|
+
function boxes() {
|
|
30
|
+
const root = document.getElementById(groupId);
|
|
31
|
+
if (root === null)
|
|
32
|
+
return [];
|
|
33
|
+
const found = [];
|
|
34
|
+
for (const node of root.querySelectorAll("input[data-otp-index]")) {
|
|
35
|
+
if (node instanceof HTMLInputElement)
|
|
36
|
+
found.push(node);
|
|
37
|
+
}
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
function focusBox(index) {
|
|
41
|
+
const target = boxes()[Math.min(Math.max(index, 0), length - 1)];
|
|
42
|
+
target?.focus();
|
|
43
|
+
target?.select();
|
|
44
|
+
}
|
|
45
|
+
function commit(next) {
|
|
46
|
+
characters(next);
|
|
47
|
+
const joined = next.join("");
|
|
48
|
+
props.onValueChange?.(joined);
|
|
49
|
+
if (joined.length === length && !next.includes(""))
|
|
50
|
+
props.onComplete?.(joined);
|
|
51
|
+
}
|
|
52
|
+
function write(index, character) {
|
|
53
|
+
const next = [...characters()];
|
|
54
|
+
next[index] = character;
|
|
55
|
+
commit(next);
|
|
56
|
+
}
|
|
57
|
+
function onInput(index, event) {
|
|
58
|
+
const target = event.target;
|
|
59
|
+
if (!(target instanceof HTMLInputElement))
|
|
60
|
+
return;
|
|
61
|
+
// A box already holding a character receives the new one appended; keep
|
|
62
|
+
// the last, which is what the user just typed.
|
|
63
|
+
const typed = target.value.slice(-1);
|
|
64
|
+
target.value = typed;
|
|
65
|
+
write(index, typed);
|
|
66
|
+
if (typed !== "" && index < length - 1)
|
|
67
|
+
focusBox(index + 1);
|
|
68
|
+
}
|
|
69
|
+
function onKeyDown(index, event) {
|
|
70
|
+
if (event.key === "Backspace") {
|
|
71
|
+
if (characters()[index] === "" && index > 0) {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
write(index - 1, "");
|
|
74
|
+
focusBox(index - 1);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (event.key === "ArrowLeft") {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
focusBox(index - 1);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.key === "ArrowRight") {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
focusBox(index + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function onPaste(index, event) {
|
|
89
|
+
const pasted = event.clipboardData?.getData("text") ?? "";
|
|
90
|
+
if (pasted === "")
|
|
91
|
+
return;
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
const next = [...characters()];
|
|
94
|
+
let cursor = index;
|
|
95
|
+
for (const character of pasted) {
|
|
96
|
+
if (cursor >= length)
|
|
97
|
+
break;
|
|
98
|
+
next[cursor] = character;
|
|
99
|
+
cursor += 1;
|
|
100
|
+
}
|
|
101
|
+
commit(next);
|
|
102
|
+
// Reflect into the DOM: the boxes are uncontrolled between keystrokes, so
|
|
103
|
+
// writing the signal alone would leave the pasted characters invisible.
|
|
104
|
+
const inputs = boxes();
|
|
105
|
+
next.forEach((character, position) => {
|
|
106
|
+
const input = inputs[position];
|
|
107
|
+
if (input !== undefined)
|
|
108
|
+
input.value = character;
|
|
109
|
+
});
|
|
110
|
+
focusBox(Math.min(cursor, length - 1));
|
|
111
|
+
}
|
|
112
|
+
const indices = Array.from({ length }, (_unused, index) => index);
|
|
113
|
+
return html `<div
|
|
114
|
+
data-slot="input-otp"
|
|
115
|
+
id="${groupId}"
|
|
116
|
+
class="${() => cn("flex items-center gap-2", read(props.class))}"
|
|
117
|
+
>
|
|
118
|
+
<div class="flex items-center">
|
|
119
|
+
${indices.map((index) => html `${props.groupAfter !== undefined &&
|
|
120
|
+
index > 0 &&
|
|
121
|
+
index % props.groupAfter === 0
|
|
122
|
+
? html `<span aria-hidden="true" class="mx-1 text-muted-foreground">-</span>`
|
|
123
|
+
: null}<input
|
|
124
|
+
data-slot="input-otp-slot"
|
|
125
|
+
data-otp-index="${index}"
|
|
126
|
+
type="text"
|
|
127
|
+
inputmode="numeric"
|
|
128
|
+
autocomplete="${index === 0 ? "one-time-code" : "off"}"
|
|
129
|
+
maxlength="1"
|
|
130
|
+
aria-label="${`Character ${index + 1} of ${length}`}"
|
|
131
|
+
?disabled="${() => readOr(props.disabled, false)}"
|
|
132
|
+
class="${slotClasses}"
|
|
133
|
+
@input="${(event) => onInput(index, event)}"
|
|
134
|
+
@keydown="${(event) => onKeyDown(index, event)}"
|
|
135
|
+
@paste="${(event) => onPaste(index, event)}"
|
|
136
|
+
/>`)}
|
|
137
|
+
</div>
|
|
138
|
+
<input type="hidden" name="${props.name}" .value="${() => characters().join("")}" />
|
|
139
|
+
</div>`;
|
|
140
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item — one row in a list of things.
|
|
3
|
+
*
|
|
4
|
+
* The generic list row: a leading media slot, a title and description, and a
|
|
5
|
+
* trailing action. Sidebar entries, settings rows, search results and command
|
|
6
|
+
* results are all this shape, and each of them growing its own version is how
|
|
7
|
+
* a list of things ends up with four different vertical rhythms.
|
|
8
|
+
*
|
|
9
|
+
* `ItemContent` takes `min-w-0` so a long title truncates instead of pushing
|
|
10
|
+
* the trailing action off the row — the single most common bug in a flex row
|
|
11
|
+
* built by hand.
|
|
12
|
+
*/
|
|
13
|
+
import { type Slot } from "../lib/children.js";
|
|
14
|
+
import { type VariantProps } from "../lib/cva.js";
|
|
15
|
+
import { type Reactive } from "../lib/props.js";
|
|
16
|
+
export declare const itemVariants: (props?: (import("../index.js").VariantSelection<{
|
|
17
|
+
variant: {
|
|
18
|
+
default: string;
|
|
19
|
+
outline: string;
|
|
20
|
+
muted: string;
|
|
21
|
+
};
|
|
22
|
+
size: {
|
|
23
|
+
default: string;
|
|
24
|
+
sm: string;
|
|
25
|
+
};
|
|
26
|
+
}> & import("../index.js").ClassOverrides) | undefined) => string;
|
|
27
|
+
export type ItemVariants = VariantProps<typeof itemVariants>;
|
|
28
|
+
export interface ItemProps {
|
|
29
|
+
children?: Slot;
|
|
30
|
+
variant?: Reactive<ItemVariants["variant"]>;
|
|
31
|
+
size?: Reactive<ItemVariants["size"]>;
|
|
32
|
+
class?: Reactive<string>;
|
|
33
|
+
}
|
|
34
|
+
export declare const Item: (props?: ItemProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
35
|
+
export declare const ItemGroup: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
36
|
+
export declare const ItemMedia: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
37
|
+
export declare const ItemContent: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
38
|
+
export declare const ItemTitle: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
39
|
+
export declare const ItemDescription: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
40
|
+
export declare const ItemActions: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
41
|
+
export declare const ItemSeparator: (props?: import("./Card.js").CardProps) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item — one row in a list of things.
|
|
3
|
+
*
|
|
4
|
+
* The generic list row: a leading media slot, a title and description, and a
|
|
5
|
+
* trailing action. Sidebar entries, settings rows, search results and command
|
|
6
|
+
* results are all this shape, and each of them growing its own version is how
|
|
7
|
+
* a list of things ends up with four different vertical rhythms.
|
|
8
|
+
*
|
|
9
|
+
* `ItemContent` takes `min-w-0` so a long title truncates instead of pushing
|
|
10
|
+
* the trailing action off the row — the single most common bug in a flex row
|
|
11
|
+
* built by hand.
|
|
12
|
+
*/
|
|
13
|
+
import { component, html } from "@c9up/aurora";
|
|
14
|
+
import { slot } from "../lib/children.js";
|
|
15
|
+
import { cva } from "../lib/cva.js";
|
|
16
|
+
import { read } from "../lib/props.js";
|
|
17
|
+
import { styledDiv } from "../lib/styled.js";
|
|
18
|
+
export const itemVariants = cva("group/item flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] [a&]:hover:bg-accent/50", {
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "bg-transparent",
|
|
22
|
+
outline: "border-border",
|
|
23
|
+
muted: "bg-muted/50",
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
default: "gap-4 p-4",
|
|
27
|
+
sm: "gap-2.5 px-4 py-3",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
31
|
+
});
|
|
32
|
+
export const Item = component((props) => {
|
|
33
|
+
return html `<div
|
|
34
|
+
data-slot="item"
|
|
35
|
+
class="${() => itemVariants({
|
|
36
|
+
variant: read(props.variant),
|
|
37
|
+
size: read(props.size),
|
|
38
|
+
class: read(props.class),
|
|
39
|
+
})}"
|
|
40
|
+
>${slot(props.children)}</div>`;
|
|
41
|
+
});
|
|
42
|
+
export const ItemGroup = styledDiv("item-group", "group/item-group flex flex-col");
|
|
43
|
+
export const ItemMedia = styledDiv("item-media", "flex shrink-0 items-center justify-center gap-2 [&_svg:not([class*='size-'])]:size-4");
|
|
44
|
+
export const ItemContent = styledDiv("item-content", "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none min-w-0");
|
|
45
|
+
export const ItemTitle = styledDiv("item-title", "flex w-fit items-center gap-2 text-sm leading-snug font-medium");
|
|
46
|
+
export const ItemDescription = styledDiv("item-description", "text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance");
|
|
47
|
+
export const ItemActions = styledDiv("item-actions", "flex items-center gap-2");
|
|
48
|
+
export const ItemSeparator = styledDiv("item-separator", "bg-border my-0 h-px w-full");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message — one turn in a conversation.
|
|
3
|
+
*
|
|
4
|
+
* Avatar, author, timestamp and a `Bubble`, laid out in reading order and
|
|
5
|
+
* mirrored for an outgoing turn. `align: "end"` uses `flex-row-reverse`, so
|
|
6
|
+
* the avatar moves to the inline end and the whole row follows the writing
|
|
7
|
+
* direction — the same logical treatment as the bubble itself.
|
|
8
|
+
*
|
|
9
|
+
* The timestamp is a `<time datetime>`, always. The visible text is formatted
|
|
10
|
+
* for the reader and may be relative ("2 min ago"), which a machine cannot
|
|
11
|
+
* parse; `datetime` carries the ISO instant alongside it so a screen reader,
|
|
12
|
+
* a scraper or a copy-paste keeps the real value.
|
|
13
|
+
*
|
|
14
|
+
* `role="article"` groups the parts, so a screen reader announces the author
|
|
15
|
+
* once rather than reading a loose avatar, a name, a time and a paragraph as
|
|
16
|
+
* four unrelated things.
|
|
17
|
+
*/
|
|
18
|
+
import type { Child } from "../lib/children.js";
|
|
19
|
+
import { type Slot } from "../lib/children.js";
|
|
20
|
+
import { type Reactive } from "../lib/props.js";
|
|
21
|
+
import { type BubbleProps, type BubbleReaction } from "./Bubble.js";
|
|
22
|
+
export interface MessageProps {
|
|
23
|
+
/** The turn's content. Rendered inside a `Bubble`. */
|
|
24
|
+
children?: Slot;
|
|
25
|
+
author?: Child;
|
|
26
|
+
avatarSrc?: string;
|
|
27
|
+
/** Initials, when there is no picture. */
|
|
28
|
+
avatarFallback?: Child;
|
|
29
|
+
/** A `Date`, or an ISO string. Rendered as `<time datetime>`. */
|
|
30
|
+
timestamp?: Date | string;
|
|
31
|
+
/** How the timestamp reads. Defaults to a short local time. */
|
|
32
|
+
formatTime?: (date: Date) => string;
|
|
33
|
+
/** `"end"` for the current user's own turn. */
|
|
34
|
+
align?: BubbleProps["align"];
|
|
35
|
+
variant?: BubbleProps["variant"];
|
|
36
|
+
reactions?: readonly BubbleReaction[];
|
|
37
|
+
/** `Attachment`s, under the bubble. */
|
|
38
|
+
attachments?: Slot;
|
|
39
|
+
/** Shown under the bubble in place of a timestamp — "Sending…", "Failed". */
|
|
40
|
+
status?: Child;
|
|
41
|
+
class?: Reactive<string>;
|
|
42
|
+
}
|
|
43
|
+
export declare const Message: (props?: MessageProps | undefined) => import("@c9up/aurora").TemplateResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message — one turn in a conversation.
|
|
3
|
+
*
|
|
4
|
+
* Avatar, author, timestamp and a `Bubble`, laid out in reading order and
|
|
5
|
+
* mirrored for an outgoing turn. `align: "end"` uses `flex-row-reverse`, so
|
|
6
|
+
* the avatar moves to the inline end and the whole row follows the writing
|
|
7
|
+
* direction — the same logical treatment as the bubble itself.
|
|
8
|
+
*
|
|
9
|
+
* The timestamp is a `<time datetime>`, always. The visible text is formatted
|
|
10
|
+
* for the reader and may be relative ("2 min ago"), which a machine cannot
|
|
11
|
+
* parse; `datetime` carries the ISO instant alongside it so a screen reader,
|
|
12
|
+
* a scraper or a copy-paste keeps the real value.
|
|
13
|
+
*
|
|
14
|
+
* `role="article"` groups the parts, so a screen reader announces the author
|
|
15
|
+
* once rather than reading a loose avatar, a name, a time and a paragraph as
|
|
16
|
+
* four unrelated things.
|
|
17
|
+
*/
|
|
18
|
+
import { component, html } from "@c9up/aurora";
|
|
19
|
+
import { Avatar } from "../atoms/Avatar.js";
|
|
20
|
+
import { slot } from "../lib/children.js";
|
|
21
|
+
import { cn } from "../lib/cn.js";
|
|
22
|
+
import { read } from "../lib/props.js";
|
|
23
|
+
import { Bubble } from "./Bubble.js";
|
|
24
|
+
/** Accept both shapes without making every caller construct a `Date`. */
|
|
25
|
+
function asDate(value) {
|
|
26
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
27
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
28
|
+
}
|
|
29
|
+
export const Message = component((props) => {
|
|
30
|
+
const outgoing = props.align === "end";
|
|
31
|
+
const time = props.timestamp === undefined ? undefined : asDate(props.timestamp);
|
|
32
|
+
const timeFormat = new Intl.DateTimeFormat(undefined, { timeStyle: "short" });
|
|
33
|
+
const hasAvatar = props.avatarSrc !== undefined || props.avatarFallback !== undefined;
|
|
34
|
+
return html `<div
|
|
35
|
+
data-slot="message"
|
|
36
|
+
role="article"
|
|
37
|
+
data-align="${outgoing ? "end" : "start"}"
|
|
38
|
+
class="${() => cn("flex w-full items-end gap-2", outgoing ? "flex-row-reverse" : "flex-row", read(props.class))}"
|
|
39
|
+
>
|
|
40
|
+
${hasAvatar
|
|
41
|
+
? Avatar({
|
|
42
|
+
src: props.avatarSrc,
|
|
43
|
+
alt: typeof props.author === "string" ? props.author : "",
|
|
44
|
+
fallback: props.avatarFallback,
|
|
45
|
+
class: "size-7 shrink-0",
|
|
46
|
+
})
|
|
47
|
+
: null}
|
|
48
|
+
<div class="${cn("flex min-w-0 flex-1 flex-col gap-1", outgoing ? "items-end" : "items-start")}">
|
|
49
|
+
${props.author === undefined
|
|
50
|
+
? null
|
|
51
|
+
: html `<span data-slot="message-author" class="text-muted-foreground px-1 text-xs font-medium"
|
|
52
|
+
>${props.author}</span
|
|
53
|
+
>`}
|
|
54
|
+
${Bubble({
|
|
55
|
+
variant: props.variant,
|
|
56
|
+
align: props.align,
|
|
57
|
+
reactions: props.reactions,
|
|
58
|
+
children: props.children,
|
|
59
|
+
})}
|
|
60
|
+
${props.attachments === undefined
|
|
61
|
+
? null
|
|
62
|
+
: html `<div data-slot="message-attachments" class="flex flex-wrap gap-2">
|
|
63
|
+
${slot(props.attachments)}
|
|
64
|
+
</div>`}
|
|
65
|
+
${renderFooter(props, time, timeFormat)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>`;
|
|
68
|
+
});
|
|
69
|
+
function renderFooter(props, time, timeFormat) {
|
|
70
|
+
if (props.status !== undefined) {
|
|
71
|
+
return html `<span data-slot="message-status" class="text-muted-foreground px-1 text-xs"
|
|
72
|
+
>${props.status}</span
|
|
73
|
+
>`;
|
|
74
|
+
}
|
|
75
|
+
if (time === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
return html `<time
|
|
78
|
+
data-slot="message-time"
|
|
79
|
+
datetime="${time.toISOString()}"
|
|
80
|
+
class="text-muted-foreground px-1 text-xs tabular-nums"
|
|
81
|
+
>${(props.formatTime ?? ((date) => timeFormat.format(date)))(time)}</time>`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination — page links for a long list.
|
|
3
|
+
*
|
|
4
|
+
* The interesting part is `pageWindow`, which decides which page numbers to
|
|
5
|
+
* show. Rendering all of them breaks at a few hundred pages; the usual fix is
|
|
6
|
+
* a fixed window around the current page, which then jitters in width as you
|
|
7
|
+
* approach either end — the control visibly reflows while you click through
|
|
8
|
+
* it. `pageWindow` keeps the count constant instead, so the row stays the same
|
|
9
|
+
* size from page 1 to page 500.
|
|
10
|
+
*
|
|
11
|
+
* `aria-current="page"` marks the current page. Without it the only signal is
|
|
12
|
+
* the highlight, which a screen-reader user does not get.
|
|
13
|
+
*/
|
|
14
|
+
import { type Reactive } from "../lib/props.js";
|
|
15
|
+
export interface PaginationProps {
|
|
16
|
+
page: Reactive<number>;
|
|
17
|
+
pageCount: Reactive<number>;
|
|
18
|
+
/** How many numbered links to show, ellipses included. Default `7`. */
|
|
19
|
+
window?: number;
|
|
20
|
+
/** Build an href per page. Omit for a button-driven pagination. */
|
|
21
|
+
href?: (page: number) => string;
|
|
22
|
+
onPageChange?: (page: number) => void;
|
|
23
|
+
class?: Reactive<string>;
|
|
24
|
+
}
|
|
25
|
+
/** `null` marks a gap that renders as an ellipsis. */
|
|
26
|
+
export type PageSlot = number | null;
|
|
27
|
+
/**
|
|
28
|
+
* Which page numbers to show, at a fixed total width.
|
|
29
|
+
*
|
|
30
|
+
* Always the first page, the last page, a run around the current one, and one
|
|
31
|
+
* or two gaps — adding up to exactly `size` slots wherever the current page
|
|
32
|
+
* sits. The constant width is the whole reason this is computed rather than a
|
|
33
|
+
* range being sliced: a window that simply narrows near the ends makes the
|
|
34
|
+
* control reflow as you click through it, and the buttons move under the
|
|
35
|
+
* pointer.
|
|
36
|
+
*
|
|
37
|
+
* Near an end there is only one gap, so the run is one slot wider to make up
|
|
38
|
+
* for it. That is the case a naive implementation gets wrong.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pageWindow(page: number, pageCount: number, size?: number): readonly PageSlot[];
|
|
41
|
+
export declare const Pagination: (props?: PaginationProps | undefined) => import("@c9up/aurora").TemplateResult;
|