@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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandDialog — the command palette, full screen.
|
|
3
|
+
*
|
|
4
|
+
* `Command` on its own is a list you place somewhere. This is the form people
|
|
5
|
+
* mean by "command palette": a modal over the page, opened by a keyboard
|
|
6
|
+
* shortcut, dismissed by Escape or a click outside.
|
|
7
|
+
*
|
|
8
|
+
* An organism composing another organism — `Command` inside `modalSurface`.
|
|
9
|
+
* Nothing about the palette's behaviour is re-implemented here; the filter,
|
|
10
|
+
* the highlight and the keyboard model stay in `Command`, and this file adds
|
|
11
|
+
* only the modal shell and the shortcut.
|
|
12
|
+
*
|
|
13
|
+
* The shortcut is opt-in but built in, because binding it correctly is
|
|
14
|
+
* fiddlier than it looks: it has to be a capturing document listener (a menu
|
|
15
|
+
* or a dialog already open would otherwise swallow it), it has to preventDefault
|
|
16
|
+
* (⌘K is "focus the search bar" in several browsers), and it has to toggle
|
|
17
|
+
* rather than open, so the same keystroke closes it.
|
|
18
|
+
*
|
|
19
|
+
* The title is present but visually hidden. A dialog with no accessible name
|
|
20
|
+
* is announced as just "dialog"; giving it one costs nothing and the palette's
|
|
21
|
+
* own search field is the visible affordance.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { component, html, onMount, onUnmount } from "@c9up/aurora";
|
|
25
|
+
import type { Child } from "../lib/children.js";
|
|
26
|
+
import { cn } from "../lib/cn.js";
|
|
27
|
+
import { uid } from "../lib/id.js";
|
|
28
|
+
import { fadeInOut, zoomInOut } from "../lib/motion.js";
|
|
29
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
30
|
+
import { controllable } from "../primitives/controllable.js";
|
|
31
|
+
import { modalSurface } from "../primitives/modalSurface.js";
|
|
32
|
+
import { Command, type CommandItem, type CommandProps } from "./Command.js";
|
|
33
|
+
import { dialogBackdropClasses } from "./Dialog.js";
|
|
34
|
+
|
|
35
|
+
export interface CommandDialogProps extends Omit<CommandProps, "class"> {
|
|
36
|
+
/** Announced on open. Hidden visually unless `showTitle`. */
|
|
37
|
+
title?: Child;
|
|
38
|
+
description?: Child;
|
|
39
|
+
showTitle?: boolean;
|
|
40
|
+
open?: Reactive<boolean>;
|
|
41
|
+
defaultOpen?: boolean;
|
|
42
|
+
onOpenChange?: (open: boolean) => void;
|
|
43
|
+
/**
|
|
44
|
+
* A single character that opens the palette with the platform's command
|
|
45
|
+
* modifier — `"k"` gives ⌘K on macOS and Ctrl+K elsewhere. Omit for a
|
|
46
|
+
* palette opened only from your own code.
|
|
47
|
+
*/
|
|
48
|
+
shortcut?: string;
|
|
49
|
+
contentClass?: Reactive<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const CommandDialog = component<CommandDialogProps>((props) => {
|
|
53
|
+
const panelId = uid("command-dialog-panel");
|
|
54
|
+
const titleId = uid("command-dialog-title");
|
|
55
|
+
const descriptionId = uid("command-dialog-description");
|
|
56
|
+
|
|
57
|
+
const state = controllable<boolean>({
|
|
58
|
+
value: props.open,
|
|
59
|
+
initial: props.defaultOpen ?? false,
|
|
60
|
+
onChange: props.onOpenChange,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Toggle, not open — the same keystroke has to close it.
|
|
65
|
+
*
|
|
66
|
+
* Captured at the document, so a menu or popover that is already open does
|
|
67
|
+
* not consume the key on its way up. `metaKey || ctrlKey` covers both
|
|
68
|
+
* platforms without sniffing the user agent.
|
|
69
|
+
*/
|
|
70
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
71
|
+
if (props.shortcut === undefined) return;
|
|
72
|
+
if (event.key.toLowerCase() !== props.shortcut.toLowerCase()) return;
|
|
73
|
+
if (!event.metaKey && !event.ctrlKey) return;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
state.set(!state.current());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onMount(() => {
|
|
79
|
+
if (props.shortcut === undefined) return;
|
|
80
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
81
|
+
});
|
|
82
|
+
onUnmount(() => document.removeEventListener("keydown", onKeyDown, true));
|
|
83
|
+
|
|
84
|
+
function close(): void {
|
|
85
|
+
state.set(false);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runAndClose(item: CommandItem): void {
|
|
89
|
+
props.onSelect?.(item);
|
|
90
|
+
// Closing after the action, not before: a handler that opens another
|
|
91
|
+
// surface should not have this one tearing down focus underneath it.
|
|
92
|
+
close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
modalSurface({
|
|
96
|
+
open: () => state.current(),
|
|
97
|
+
onClose: close,
|
|
98
|
+
panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
|
|
99
|
+
// The search field, not the first focusable — the whole point of the
|
|
100
|
+
// palette is that you can start typing immediately.
|
|
101
|
+
initialFocus: (panel) => panel.querySelector("input"),
|
|
102
|
+
content: () =>
|
|
103
|
+
html`<div
|
|
104
|
+
data-slot="command-dialog-overlay"
|
|
105
|
+
class="${cn("fixed inset-0 z-50", fadeInOut)}"
|
|
106
|
+
>
|
|
107
|
+
<div class="${dialogBackdropClasses}"></div>
|
|
108
|
+
<div
|
|
109
|
+
data-slot="command-dialog"
|
|
110
|
+
id="${panelId}"
|
|
111
|
+
role="dialog"
|
|
112
|
+
aria-modal="true"
|
|
113
|
+
aria-labelledby="${titleId}"
|
|
114
|
+
aria-describedby="${props.description === undefined ? undefined : descriptionId}"
|
|
115
|
+
tabindex="-1"
|
|
116
|
+
class="${cn(
|
|
117
|
+
"bg-popover fixed top-[20%] left-1/2 z-50 w-full max-w-lg -translate-x-1/2 overflow-hidden rounded-lg border shadow-lg outline-none",
|
|
118
|
+
zoomInOut,
|
|
119
|
+
read(props.contentClass),
|
|
120
|
+
)}"
|
|
121
|
+
>
|
|
122
|
+
<h2
|
|
123
|
+
id="${titleId}"
|
|
124
|
+
class="${cn(
|
|
125
|
+
"px-4 pt-4 text-sm font-medium",
|
|
126
|
+
props.showTitle === true ? "" : "sr-only",
|
|
127
|
+
)}"
|
|
128
|
+
>${props.title ?? "Command palette"}</h2>
|
|
129
|
+
${
|
|
130
|
+
props.description === undefined
|
|
131
|
+
? null
|
|
132
|
+
: html`<p id="${descriptionId}" class="sr-only">${props.description}</p>`
|
|
133
|
+
}
|
|
134
|
+
${Command({
|
|
135
|
+
items: props.items,
|
|
136
|
+
placeholder: props.placeholder,
|
|
137
|
+
emptyMessage: props.emptyMessage,
|
|
138
|
+
filter: props.filter,
|
|
139
|
+
onSelect: runAndClose,
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
</div>`,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// The palette has no trigger of its own — it is opened by the shortcut or
|
|
146
|
+
// by the `open` prop. Something still has to sit in the tree for the
|
|
147
|
+
// component to mount into; the surface itself lives in a portal.
|
|
148
|
+
return html`<span data-slot="command-dialog-root" hidden></span>`;
|
|
149
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextMenu — a menu opened by right-clicking a region.
|
|
3
|
+
*
|
|
4
|
+
* Same panel as DropdownMenu; the difference is the anchor. There is no
|
|
5
|
+
* trigger element to position against — the menu belongs at the pointer, which
|
|
6
|
+
* could be anywhere inside the region.
|
|
7
|
+
*
|
|
8
|
+
* So a zero-size element is parked at the click coordinates and used as the
|
|
9
|
+
* anchor. It is a real element on purpose: `autoPosition` measures its rect,
|
|
10
|
+
* and that rect flips and shifts exactly like a button's would, which is what
|
|
11
|
+
* gets a menu opened near the bottom of the window to open upwards instead of
|
|
12
|
+
* off screen.
|
|
13
|
+
*
|
|
14
|
+
* Long-press opens it on touch, where there is no right button. The 500ms
|
|
15
|
+
* threshold matches the platform convention, and any movement cancels it so a
|
|
16
|
+
* scroll gesture does not surface a menu.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { component, html, onUnmount, signal } from "@c9up/aurora";
|
|
20
|
+
import { type Slot, slot } from "../lib/children.js";
|
|
21
|
+
import { cn } from "../lib/cn.js";
|
|
22
|
+
import { uid } from "../lib/id.js";
|
|
23
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
24
|
+
import { floatingSurface } from "../primitives/floatingSurface.js";
|
|
25
|
+
import { type MenuEntry, menuPanel, wireMenu } from "../primitives/menu.js";
|
|
26
|
+
|
|
27
|
+
const LONG_PRESS_MS = 500;
|
|
28
|
+
|
|
29
|
+
export interface ContextMenuProps {
|
|
30
|
+
/** The region that answers to right-click. */
|
|
31
|
+
children?: Slot;
|
|
32
|
+
entries: readonly MenuEntry[];
|
|
33
|
+
onOpenChange?: (open: boolean) => void;
|
|
34
|
+
class?: Reactive<string>;
|
|
35
|
+
contentClass?: Reactive<string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ContextMenu = component<ContextMenuProps>((props) => {
|
|
39
|
+
const regionId = uid("context-menu-region");
|
|
40
|
+
const anchorId = uid("context-menu-anchor");
|
|
41
|
+
const contentId = uid("context-menu-content");
|
|
42
|
+
const open = signal(false);
|
|
43
|
+
|
|
44
|
+
let unwire: (() => void) | null = null;
|
|
45
|
+
let pressTimer: ReturnType<typeof setTimeout> | undefined;
|
|
46
|
+
|
|
47
|
+
function setOpen(next: boolean): void {
|
|
48
|
+
open(next);
|
|
49
|
+
props.onOpenChange?.(next);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function moveAnchorTo(x: number, y: number): void {
|
|
53
|
+
const anchor = document.getElementById(anchorId);
|
|
54
|
+
if (anchor === null) return;
|
|
55
|
+
anchor.style.left = `${x}px`;
|
|
56
|
+
anchor.style.top = `${y}px`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function openAt(x: number, y: number): void {
|
|
60
|
+
moveAnchorTo(x, y);
|
|
61
|
+
setOpen(true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onContextMenu(event: MouseEvent): void {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
openAt(event.clientX, event.clientY);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onPointerDown(event: PointerEvent): void {
|
|
70
|
+
if (event.pointerType !== "touch") return;
|
|
71
|
+
cancelPress();
|
|
72
|
+
pressTimer = setTimeout(
|
|
73
|
+
() => openAt(event.clientX, event.clientY),
|
|
74
|
+
LONG_PRESS_MS,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cancelPress(): void {
|
|
79
|
+
if (pressTimer !== undefined) clearTimeout(pressTimer);
|
|
80
|
+
pressTimer = undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onUnmount(cancelPress);
|
|
84
|
+
|
|
85
|
+
floatingSurface({
|
|
86
|
+
anchor: () => document.getElementById(anchorId),
|
|
87
|
+
open: () => open(),
|
|
88
|
+
onClose: () => setOpen(false),
|
|
89
|
+
placement: "bottom-start",
|
|
90
|
+
offset: 2,
|
|
91
|
+
content: () =>
|
|
92
|
+
menuPanel({
|
|
93
|
+
id: contentId,
|
|
94
|
+
entries: props.entries,
|
|
95
|
+
onCloseAll: () => setOpen(false),
|
|
96
|
+
class: read(props.contentClass),
|
|
97
|
+
}),
|
|
98
|
+
onOpened: (panel) => {
|
|
99
|
+
unwire = wireMenu(panel, { onCloseAll: () => setOpen(false) });
|
|
100
|
+
panel.focus({ preventScroll: true });
|
|
101
|
+
},
|
|
102
|
+
onClosed: () => {
|
|
103
|
+
unwire?.();
|
|
104
|
+
unwire = null;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return html`<div
|
|
109
|
+
data-slot="context-menu"
|
|
110
|
+
id="${regionId}"
|
|
111
|
+
class="${() => cn(read(props.class))}"
|
|
112
|
+
@contextmenu="${onContextMenu}"
|
|
113
|
+
@pointerdown="${onPointerDown}"
|
|
114
|
+
@pointerup="${cancelPress}"
|
|
115
|
+
@pointermove="${cancelPress}"
|
|
116
|
+
@pointercancel="${cancelPress}"
|
|
117
|
+
>
|
|
118
|
+
${slot(props.children)}
|
|
119
|
+
<span
|
|
120
|
+
id="${anchorId}"
|
|
121
|
+
aria-hidden="true"
|
|
122
|
+
class="pointer-events-none fixed size-0"
|
|
123
|
+
></span>
|
|
124
|
+
</div>`;
|
|
125
|
+
});
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTable — a table with sorting, filtering, paging and row selection.
|
|
3
|
+
*
|
|
4
|
+
* shadcn wraps `@tanstack/react-table`, a headless engine with column
|
|
5
|
+
* grouping, virtualisation, pinning and faceted filters. nebula implements the
|
|
6
|
+
* four features its own examples actually use, over the `Table` molecule.
|
|
7
|
+
*
|
|
8
|
+
* The scope line is drawn where the data does. Everything here operates on an
|
|
9
|
+
* in-memory array: sorting compares values, filtering scans rows, paging
|
|
10
|
+
* slices. That is the right answer up to a few thousand rows and the wrong one
|
|
11
|
+
* beyond, where the work belongs on the server — so rather than growing a
|
|
12
|
+
* half-server-side mode, the component stays client-side and honest about it.
|
|
13
|
+
*
|
|
14
|
+
* A generic function rather than `component<P>`: `component` needs a concrete
|
|
15
|
+
* prop type, and `Row` has to stay open so `cell` receives the caller's own
|
|
16
|
+
* row type instead of a record of unknowns.
|
|
17
|
+
*
|
|
18
|
+
* `aria-sort` on the sorted column is the part hand-rolled tables always miss.
|
|
19
|
+
* The little chevron says which column is sorted to anyone who can see it; the
|
|
20
|
+
* attribute is what says it to everyone else.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { html, signal, type TemplateResult } from "@c9up/aurora";
|
|
24
|
+
import { Checkbox } from "../atoms/Checkbox.js";
|
|
25
|
+
import { Input } from "../atoms/Input.js";
|
|
26
|
+
import type { Child } from "../lib/children.js";
|
|
27
|
+
import { cn } from "../lib/cn.js";
|
|
28
|
+
import {
|
|
29
|
+
ChevronDownIcon,
|
|
30
|
+
ChevronsUpDownIcon,
|
|
31
|
+
ChevronUpIcon,
|
|
32
|
+
} from "../lib/icons.js";
|
|
33
|
+
import { Pagination } from "../molecules/Pagination.js";
|
|
34
|
+
import {
|
|
35
|
+
Table,
|
|
36
|
+
TableBody,
|
|
37
|
+
TableCell,
|
|
38
|
+
TableHead,
|
|
39
|
+
TableHeader,
|
|
40
|
+
TableRow,
|
|
41
|
+
} from "../molecules/Table.js";
|
|
42
|
+
|
|
43
|
+
export interface Column<Row> {
|
|
44
|
+
key: string;
|
|
45
|
+
header: Child;
|
|
46
|
+
/** Renders the cell. Defaults to the row's property of the same key. */
|
|
47
|
+
cell?: (row: Row) => Child;
|
|
48
|
+
sortable?: boolean;
|
|
49
|
+
/** What to compare when sorting. Defaults to the rendered property. */
|
|
50
|
+
sortValue?: (row: Row) => string | number;
|
|
51
|
+
class?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DataTableProps<Row> {
|
|
55
|
+
columns: readonly Column<Row>[];
|
|
56
|
+
rows: readonly Row[];
|
|
57
|
+
/** Stable identity per row — used for selection and as the render key. */
|
|
58
|
+
rowKey: (row: Row) => string;
|
|
59
|
+
/** Rows per page. Omit to show everything. */
|
|
60
|
+
pageSize?: number;
|
|
61
|
+
/** Show a filter box. Provide `filterMatch` to say what it searches. */
|
|
62
|
+
filterPlaceholder?: string;
|
|
63
|
+
filterMatch?: (row: Row, query: string) => boolean;
|
|
64
|
+
/** Add a checkbox column. */
|
|
65
|
+
selectable?: boolean;
|
|
66
|
+
onSelectionChange?: (keys: readonly string[]) => void;
|
|
67
|
+
emptyMessage?: Child;
|
|
68
|
+
class?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type SortDirection = "asc" | "desc";
|
|
72
|
+
|
|
73
|
+
export function DataTable<Row>(props: DataTableProps<Row>): TemplateResult {
|
|
74
|
+
const sortKey = signal<string | undefined>(undefined);
|
|
75
|
+
const sortDirection = signal<SortDirection>("asc");
|
|
76
|
+
const query = signal("");
|
|
77
|
+
const page = signal(1);
|
|
78
|
+
const selected = signal<readonly string[]>([]);
|
|
79
|
+
|
|
80
|
+
function readCell(row: Row, column: Column<Row>): Child {
|
|
81
|
+
if (column.cell !== undefined) return column.cell(row);
|
|
82
|
+
return propertyOf(row, column.key);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sortableValue(row: Row, column: Column<Row>): string | number {
|
|
86
|
+
if (column.sortValue !== undefined) return column.sortValue(row);
|
|
87
|
+
const value = propertyOf(row, column.key);
|
|
88
|
+
return typeof value === "number" ? value : String(value ?? "");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function filtered(): readonly Row[] {
|
|
92
|
+
const needle = query().trim().toLowerCase();
|
|
93
|
+
if (needle === "" || props.filterMatch === undefined) return props.rows;
|
|
94
|
+
return props.rows.filter(
|
|
95
|
+
(row) => props.filterMatch?.(row, needle) === true,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sorted(): readonly Row[] {
|
|
100
|
+
const key = sortKey();
|
|
101
|
+
const rows = filtered();
|
|
102
|
+
if (key === undefined) return rows;
|
|
103
|
+
|
|
104
|
+
const column = props.columns.find((entry) => entry.key === key);
|
|
105
|
+
if (column === undefined) return rows;
|
|
106
|
+
|
|
107
|
+
const direction = sortDirection() === "asc" ? 1 : -1;
|
|
108
|
+
// A copy: sorting the caller's array in place would reorder their data as
|
|
109
|
+
// a side effect of rendering it.
|
|
110
|
+
return [...rows].sort((left, right) => {
|
|
111
|
+
const a = sortableValue(left, column);
|
|
112
|
+
const b = sortableValue(right, column);
|
|
113
|
+
if (typeof a === "number" && typeof b === "number")
|
|
114
|
+
return (a - b) * direction;
|
|
115
|
+
return String(a).localeCompare(String(b)) * direction;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pageCount(): number {
|
|
120
|
+
if (props.pageSize === undefined) return 1;
|
|
121
|
+
return Math.max(1, Math.ceil(sorted().length / props.pageSize));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function visible(): readonly Row[] {
|
|
125
|
+
const rows = sorted();
|
|
126
|
+
if (props.pageSize === undefined) return rows;
|
|
127
|
+
// Clamp: filtering down to fewer pages while on the last one would
|
|
128
|
+
// otherwise show an empty table with no way back.
|
|
129
|
+
const current = Math.min(page(), pageCount());
|
|
130
|
+
const from = (current - 1) * props.pageSize;
|
|
131
|
+
return rows.slice(from, from + props.pageSize);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toggleSort(column: Column<Row>): void {
|
|
135
|
+
if (column.sortable === false) return;
|
|
136
|
+
if (sortKey() === column.key) {
|
|
137
|
+
sortDirection(sortDirection() === "asc" ? "desc" : "asc");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
sortKey(column.key);
|
|
141
|
+
sortDirection("asc");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setSelection(keys: readonly string[]): void {
|
|
145
|
+
selected(keys);
|
|
146
|
+
props.onSelectionChange?.(keys);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toggleRow(key: string): void {
|
|
150
|
+
const current = selected();
|
|
151
|
+
setSelection(
|
|
152
|
+
current.includes(key)
|
|
153
|
+
? current.filter((entry) => entry !== key)
|
|
154
|
+
: [...current, key],
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The header checkbox acts on the *visible* page, not the whole dataset.
|
|
160
|
+
*
|
|
161
|
+
* Selecting rows the user cannot see — and may have filtered away on
|
|
162
|
+
* purpose — is the behaviour behind every "I deleted more than I meant to"
|
|
163
|
+
* story about a bulk action.
|
|
164
|
+
*/
|
|
165
|
+
function toggleAllVisible(): void {
|
|
166
|
+
const keys = visible().map(props.rowKey);
|
|
167
|
+
const allSelected = keys.every((key) => selected().includes(key));
|
|
168
|
+
if (allSelected) {
|
|
169
|
+
setSelection(selected().filter((key) => !keys.includes(key)));
|
|
170
|
+
} else {
|
|
171
|
+
const merged = new Set([...selected(), ...keys]);
|
|
172
|
+
setSelection([...merged]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ariaSortFor(
|
|
177
|
+
column: Column<Row>,
|
|
178
|
+
): "ascending" | "descending" | "none" | undefined {
|
|
179
|
+
if (column.sortable === false) return undefined;
|
|
180
|
+
if (sortKey() !== column.key) return "none";
|
|
181
|
+
return sortDirection() === "asc" ? "ascending" : "descending";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sortIcon(column: Column<Row>): Child {
|
|
185
|
+
if (column.sortable === false) return null;
|
|
186
|
+
if (sortKey() !== column.key) {
|
|
187
|
+
return ChevronsUpDownIcon({ class: "ml-2 size-3.5 opacity-50" });
|
|
188
|
+
}
|
|
189
|
+
return sortDirection() === "asc"
|
|
190
|
+
? ChevronUpIcon({ class: "ml-2 size-3.5" })
|
|
191
|
+
: ChevronDownIcon({ class: "ml-2 size-3.5" });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const columnCount =
|
|
195
|
+
props.columns.length + (props.selectable === true ? 1 : 0);
|
|
196
|
+
|
|
197
|
+
return html`<div data-slot="data-table" class="${cn("flex flex-col gap-4", props.class)}">
|
|
198
|
+
${
|
|
199
|
+
props.filterMatch === undefined
|
|
200
|
+
? null
|
|
201
|
+
: Input({
|
|
202
|
+
type: "search",
|
|
203
|
+
placeholder: props.filterPlaceholder ?? "Filter…",
|
|
204
|
+
class: "max-w-sm",
|
|
205
|
+
onInput: (value) => {
|
|
206
|
+
query(value);
|
|
207
|
+
page(1);
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
${Table({
|
|
212
|
+
children: [
|
|
213
|
+
TableHeader({
|
|
214
|
+
children: TableRow({
|
|
215
|
+
children: [
|
|
216
|
+
props.selectable === true
|
|
217
|
+
? TableHead({
|
|
218
|
+
class: "w-10",
|
|
219
|
+
children: Checkbox({
|
|
220
|
+
label: "Select all rows on this page",
|
|
221
|
+
checked: () => {
|
|
222
|
+
const keys = visible().map(props.rowKey);
|
|
223
|
+
return (
|
|
224
|
+
keys.length > 0 &&
|
|
225
|
+
keys.every((key) => selected().includes(key))
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
indeterminate: () => {
|
|
229
|
+
const keys = visible().map(props.rowKey);
|
|
230
|
+
const chosen = keys.filter((key) =>
|
|
231
|
+
selected().includes(key),
|
|
232
|
+
).length;
|
|
233
|
+
return chosen > 0 && chosen < keys.length;
|
|
234
|
+
},
|
|
235
|
+
onCheckedChange: toggleAllVisible,
|
|
236
|
+
}),
|
|
237
|
+
})
|
|
238
|
+
: null,
|
|
239
|
+
...props.columns.map((column) =>
|
|
240
|
+
TableHead({
|
|
241
|
+
class: cn(
|
|
242
|
+
column.sortable === false
|
|
243
|
+
? ""
|
|
244
|
+
: "cursor-pointer select-none",
|
|
245
|
+
column.class,
|
|
246
|
+
),
|
|
247
|
+
sort: () => ariaSortFor(column),
|
|
248
|
+
onClick: () => toggleSort(column),
|
|
249
|
+
children: html`<span class="inline-flex items-center"
|
|
250
|
+
>${column.header}${() => sortIcon(column)}</span
|
|
251
|
+
>`,
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
254
|
+
],
|
|
255
|
+
}),
|
|
256
|
+
}),
|
|
257
|
+
TableBody({
|
|
258
|
+
children: () => {
|
|
259
|
+
const rows = visible();
|
|
260
|
+
if (rows.length === 0) {
|
|
261
|
+
return TableRow({
|
|
262
|
+
children: TableCell({
|
|
263
|
+
colspan: columnCount,
|
|
264
|
+
class: "h-24 text-center",
|
|
265
|
+
children: props.emptyMessage ?? "No results.",
|
|
266
|
+
}),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return rows.map((row) => renderRow(row));
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
],
|
|
273
|
+
})}
|
|
274
|
+
${
|
|
275
|
+
props.pageSize === undefined
|
|
276
|
+
? null
|
|
277
|
+
: Pagination({
|
|
278
|
+
page: () => Math.min(page(), pageCount()),
|
|
279
|
+
pageCount,
|
|
280
|
+
onPageChange: (next) => page(next),
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
</div>`;
|
|
284
|
+
|
|
285
|
+
function renderRow(row: Row): Child {
|
|
286
|
+
const key = props.rowKey(row);
|
|
287
|
+
return TableRow({
|
|
288
|
+
selected: () => selected().includes(key),
|
|
289
|
+
children: [
|
|
290
|
+
props.selectable === true
|
|
291
|
+
? TableCell({
|
|
292
|
+
children: Checkbox({
|
|
293
|
+
label: `Select row ${key}`,
|
|
294
|
+
checked: () => selected().includes(key),
|
|
295
|
+
onCheckedChange: () => toggleRow(key),
|
|
296
|
+
}),
|
|
297
|
+
})
|
|
298
|
+
: null,
|
|
299
|
+
...props.columns.map((column) =>
|
|
300
|
+
TableCell({ class: column.class, children: readCell(row, column) }),
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Read a property by name from a row of unknown shape.
|
|
309
|
+
*
|
|
310
|
+
* `Row` is the caller's own type and may be an interface, which cannot be
|
|
311
|
+
* indexed by an arbitrary string. The guard turns the lookup into a checked
|
|
312
|
+
* read that yields `undefined` for a missing key — a blank cell — instead of
|
|
313
|
+
* an assertion that it exists.
|
|
314
|
+
*/
|
|
315
|
+
function propertyOf(row: unknown, key: string): Child {
|
|
316
|
+
if (typeof row !== "object" || row === null) return null;
|
|
317
|
+
if (!(key in row)) return null;
|
|
318
|
+
const value = Reflect.get(row, key);
|
|
319
|
+
if (value === null || value === undefined) return null;
|
|
320
|
+
if (
|
|
321
|
+
typeof value === "string" ||
|
|
322
|
+
typeof value === "number" ||
|
|
323
|
+
typeof value === "boolean"
|
|
324
|
+
) {
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
return String(value);
|
|
328
|
+
}
|