@getdom/studio 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.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
export function cssColorToHex(value, fallback = '#000000') {
|
|
2
|
+
const color = String(value || '').trim();
|
|
3
|
+
if (!color) return fallback;
|
|
4
|
+
return parseKnownColorToHex(color) || resolveBrowserColorToHex(color) || fallback;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function cssColorWithAlpha(value, alpha = 100) {
|
|
8
|
+
const color = String(value || '#000000').trim();
|
|
9
|
+
const opacity = clamp(Number(alpha), 0, 100);
|
|
10
|
+
|
|
11
|
+
if (isHexColor(color)) return hexToRgba(color, opacity);
|
|
12
|
+
|
|
13
|
+
const rgb = parseRgbColor(color);
|
|
14
|
+
if (rgb) {
|
|
15
|
+
const [red, green, blue] = rgb.channels;
|
|
16
|
+
return `rgba(${red}, ${green}, ${blue}, ${Number(opacity / 100).toFixed(2)})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (/^oklch\(/i.test(color)) {
|
|
20
|
+
return setFunctionalColorAlpha(color, opacity);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (opacity >= 100) return color;
|
|
24
|
+
return `color-mix(in oklch, ${color} ${opacity}%, transparent)`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function colorToHexAlpha(value) {
|
|
28
|
+
const color = String(value || '').trim();
|
|
29
|
+
if (color === 'transparent') return { hex: '#000000', alpha: 0, color: '#000000' };
|
|
30
|
+
|
|
31
|
+
if (isHexColor(color)) {
|
|
32
|
+
const clean = color.replace('#', '');
|
|
33
|
+
const hasShortAlpha = clean.length === 4;
|
|
34
|
+
const hasAlpha = clean.length === 8 || hasShortAlpha;
|
|
35
|
+
const alphaValue = hasShortAlpha
|
|
36
|
+
? Number.parseInt(clean.at(-1) + clean.at(-1), 16)
|
|
37
|
+
: clean.length === 8
|
|
38
|
+
? Number.parseInt(clean.slice(6, 8), 16)
|
|
39
|
+
: 255;
|
|
40
|
+
const hex = expandHex(color);
|
|
41
|
+
const alpha = hasAlpha ? Math.round((alphaValue / 255) * 100) : 100;
|
|
42
|
+
return { hex, alpha, color: hex };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (/^oklch\(/i.test(color)) {
|
|
46
|
+
const hex = oklchToHex(color);
|
|
47
|
+
return { hex, alpha: parseFunctionalAlpha(color), color: stripFunctionalAlpha(color) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rgb = parseRgbColor(color);
|
|
51
|
+
if (rgb) {
|
|
52
|
+
const hex = rgbToHex(rgb.channels.map((channel) => channel / 255));
|
|
53
|
+
return { hex, alpha: rgb.alpha, color: hex };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { hex: cssColorToHex(color), alpha: 100, color };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isHexColor(value) {
|
|
60
|
+
return /^#[0-9a-f]{3,8}$/i.test(String(value || '').trim());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function expandHex(hex) {
|
|
64
|
+
const clean = String(hex || '').replace('#', '').trim();
|
|
65
|
+
if (clean.length === 3 || clean.length === 4) {
|
|
66
|
+
return `#${clean.slice(0, 3).split('').map((part) => part + part).join('').toLowerCase()}`;
|
|
67
|
+
}
|
|
68
|
+
return `#${clean.slice(0, 6).padEnd(6, '0').toLowerCase()}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function hexToRgba(hex, alpha = 100) {
|
|
72
|
+
const [red, green, blue] = hexToRgb(hex).map((channel) => Math.round(channel * 255));
|
|
73
|
+
return `rgba(${red}, ${green}, ${blue}, ${Number(clamp(Number(alpha), 0, 100) / 100).toFixed(2)})`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function hexToOklch(hex) {
|
|
77
|
+
const [red, green, blue] = hexToRgb(hex).map(srgbToLinear);
|
|
78
|
+
const l = 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue;
|
|
79
|
+
const m = 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue;
|
|
80
|
+
const s = 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue;
|
|
81
|
+
const lRoot = Math.cbrt(l);
|
|
82
|
+
const mRoot = Math.cbrt(m);
|
|
83
|
+
const sRoot = Math.cbrt(s);
|
|
84
|
+
const lightness = 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot;
|
|
85
|
+
const a = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot;
|
|
86
|
+
const b = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot;
|
|
87
|
+
const chroma = Math.sqrt((a * a) + (b * b));
|
|
88
|
+
const hue = chroma < 0.0005 ? 0 : (Math.atan2(b, a) * 180 / Math.PI + 360) % 360;
|
|
89
|
+
|
|
90
|
+
return `oklch(${formatNumber(lightness)} ${formatNumber(chroma)} ${formatNumber(hue)})`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function oklchToHex(value) {
|
|
94
|
+
const match = String(value || '').match(/oklch\(\s*([0-9.]+%?)\s+([0-9.]+)\s+([0-9.]+)/i);
|
|
95
|
+
if (!match) return '#000000';
|
|
96
|
+
|
|
97
|
+
const lightness = clamp(match[1].endsWith('%') ? Number.parseFloat(match[1]) / 100 : Number.parseFloat(match[1]), 0, 1);
|
|
98
|
+
const chroma = Number.parseFloat(match[2]);
|
|
99
|
+
const hue = Number.parseFloat(match[3]) * Math.PI / 180;
|
|
100
|
+
const a = Math.cos(hue) * chroma;
|
|
101
|
+
const b = Math.sin(hue) * chroma;
|
|
102
|
+
const lRoot = lightness + 0.3963377774 * a + 0.2158037573 * b;
|
|
103
|
+
const mRoot = lightness - 0.1055613458 * a - 0.0638541728 * b;
|
|
104
|
+
const sRoot = lightness - 0.0894841775 * a - 1.291485548 * b;
|
|
105
|
+
const l = lRoot ** 3;
|
|
106
|
+
const m = mRoot ** 3;
|
|
107
|
+
const s = sRoot ** 3;
|
|
108
|
+
const red = linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
|
|
109
|
+
const green = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
|
|
110
|
+
const blue = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s);
|
|
111
|
+
|
|
112
|
+
return rgbToHex([red, green, blue]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function hexToRgb(hex) {
|
|
116
|
+
const clean = expandHex(hex).replace('#', '');
|
|
117
|
+
const value = Number.parseInt(clean, 16);
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
((value >> 16) & 255) / 255,
|
|
121
|
+
((value >> 8) & 255) / 255,
|
|
122
|
+
(value & 255) / 255,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function rgbToHex(values) {
|
|
127
|
+
return `#${values
|
|
128
|
+
.map((value) => Math.round(clamp(value, 0, 1) * 255).toString(16).padStart(2, '0'))
|
|
129
|
+
.join('')}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function clamp(value, min, max) {
|
|
133
|
+
return Math.min(Math.max(value, min), max);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseRgbColor(value) {
|
|
137
|
+
const match = String(value || '').trim().match(/^rgba?\((.+)\)$/i);
|
|
138
|
+
if (!match) return null;
|
|
139
|
+
|
|
140
|
+
const body = match[1].trim();
|
|
141
|
+
const [channelPart, alphaPart] = body.includes('/')
|
|
142
|
+
? body.split('/').map((part) => part.trim())
|
|
143
|
+
: [body, null];
|
|
144
|
+
const channelValues = channelPart.includes(',')
|
|
145
|
+
? channelPart.split(',').slice(0, 3)
|
|
146
|
+
: channelPart.split(/\s+/).slice(0, 3);
|
|
147
|
+
const alphaValue = alphaPart ?? (body.includes(',') ? body.split(',')[3] : null);
|
|
148
|
+
const channels = channelValues.map((part) => clamp(Number.parseFloat(part), 0, 255));
|
|
149
|
+
|
|
150
|
+
if (channels.length !== 3 || channels.some((channel) => !Number.isFinite(channel))) return null;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
channels: channels.map(Math.round),
|
|
154
|
+
alpha: parseAlpha(alphaValue),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseKnownColorToHex(color) {
|
|
159
|
+
if (isHexColor(color)) return expandHex(color);
|
|
160
|
+
if (/^oklch\(/i.test(color)) return oklchToHex(color);
|
|
161
|
+
|
|
162
|
+
const rgb = parseRgbColor(color);
|
|
163
|
+
if (rgb) return rgbToHex(rgb.channels.map((channel) => channel / 255));
|
|
164
|
+
|
|
165
|
+
const srgb = parseSrgbColorFunction(color);
|
|
166
|
+
if (srgb) return rgbToHex(srgb);
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseSrgbColorFunction(value) {
|
|
172
|
+
const match = String(value || '').trim().match(/^color\(\s*srgb\s+(.+)\)$/i);
|
|
173
|
+
if (!match) return null;
|
|
174
|
+
|
|
175
|
+
const channelPart = match[1].split('/')[0].trim();
|
|
176
|
+
const channels = channelPart.split(/\s+/).slice(0, 3).map((part) => {
|
|
177
|
+
const raw = part.trim();
|
|
178
|
+
if (raw.endsWith('%')) return clamp(Number.parseFloat(raw) / 100, 0, 1);
|
|
179
|
+
return clamp(Number.parseFloat(raw), 0, 1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (channels.length !== 3 || channels.some((channel) => !Number.isFinite(channel))) return null;
|
|
183
|
+
return channels;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveBrowserColorToHex(value) {
|
|
187
|
+
if (typeof document === 'undefined') return null;
|
|
188
|
+
if (typeof CSS !== 'undefined' && CSS.supports && !CSS.supports('color', value)) return null;
|
|
189
|
+
|
|
190
|
+
const swatch = document.createElement('span');
|
|
191
|
+
swatch.style.color = value;
|
|
192
|
+
swatch.style.position = 'fixed';
|
|
193
|
+
swatch.style.pointerEvents = 'none';
|
|
194
|
+
swatch.style.visibility = 'hidden';
|
|
195
|
+
|
|
196
|
+
if (!swatch.style.color) return null;
|
|
197
|
+
|
|
198
|
+
document.documentElement.append(swatch);
|
|
199
|
+
const computed = getComputedStyle(swatch).color;
|
|
200
|
+
swatch.remove();
|
|
201
|
+
|
|
202
|
+
return parseKnownColorToHex(computed);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseFunctionalAlpha(value) {
|
|
206
|
+
const match = String(value || '').match(/\/\s*([0-9.]+%?)/);
|
|
207
|
+
return parseAlpha(match?.[1]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseAlpha(value) {
|
|
211
|
+
if (value === undefined || value === null || value === '') return 100;
|
|
212
|
+
|
|
213
|
+
const raw = String(value).trim();
|
|
214
|
+
if (raw.endsWith('%')) return clamp(Number.parseFloat(raw), 0, 100);
|
|
215
|
+
|
|
216
|
+
const number = Number.parseFloat(raw);
|
|
217
|
+
if (!Number.isFinite(number)) return 100;
|
|
218
|
+
return number <= 1 ? Math.round(number * 100) : clamp(number, 0, 100);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function stripFunctionalAlpha(value) {
|
|
222
|
+
const match = String(value || '').trim().match(/^([a-z]+)\((.*)\)$/i);
|
|
223
|
+
if (!match) return value;
|
|
224
|
+
|
|
225
|
+
return `${match[1]}(${match[2].split('/')[0].trim()})`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function setFunctionalColorAlpha(value, alpha) {
|
|
229
|
+
const match = String(value || '').trim().match(/^([a-z]+)\((.*)\)$/i);
|
|
230
|
+
if (!match) return value;
|
|
231
|
+
|
|
232
|
+
return `${match[1]}(${match[2].split('/')[0].trim()} / ${alpha}%)`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function srgbToLinear(value) {
|
|
236
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function linearToSrgb(value) {
|
|
240
|
+
return value <= 0.0031308 ? 12.92 * value : 1.055 * (value ** (1 / 2.4)) - 0.055;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatNumber(value) {
|
|
244
|
+
return Number(value.toFixed(3)).toString();
|
|
245
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Generated by scripts/generate-types.mjs. Do not edit by hand.
|
|
3
|
+
import { DomElementBase } from './base.js';
|
|
4
|
+
export declare class DomAccordionItemElement extends DomElementBase {
|
|
5
|
+
toggle(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare class DomAccordionElement extends DomElementBase {}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { DomElementBase, defineDomElement, uid } from './base.js';
|
|
2
|
+
|
|
3
|
+
// <dom-accordion multiple>
|
|
4
|
+
// <dom-accordion-item>
|
|
5
|
+
// <button slot="header">Title</button>
|
|
6
|
+
// <div slot="content">…</div>
|
|
7
|
+
// </dom-accordion-item>
|
|
8
|
+
// </dom-accordion>
|
|
9
|
+
export class DomAccordionItemElement extends DomElementBase {
|
|
10
|
+
static get observedAttributes() { return ['open']; }
|
|
11
|
+
|
|
12
|
+
static __doc = {
|
|
13
|
+
name: 'dom-accordion-item',
|
|
14
|
+
description: 'A single collapsible section. Lives inside an <dom-accordion> host.',
|
|
15
|
+
slots: [
|
|
16
|
+
{ name: 'header', description: 'The clickable header. Carries aria-expanded.' },
|
|
17
|
+
{ name: 'content', description: 'The collapsible region. Hidden when closed.' },
|
|
18
|
+
],
|
|
19
|
+
attributes: [
|
|
20
|
+
{ name: 'open', type: 'boolean', description: 'Reflects the open state of the item.' },
|
|
21
|
+
],
|
|
22
|
+
events: [
|
|
23
|
+
{ name: 'dom:change', payload: '{ open }', description: 'Fired when the item is opened or closed.' },
|
|
24
|
+
],
|
|
25
|
+
keyboard: [
|
|
26
|
+
{ keys: 'Enter / Space', action: 'Toggle the section (when header is focused).' },
|
|
27
|
+
],
|
|
28
|
+
example: `<dom-accordion-item open>
|
|
29
|
+
<button slot="header">Section A</button>
|
|
30
|
+
<div slot="content">Lorem ipsum…</div>
|
|
31
|
+
</dom-accordion-item>`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
connectedCallback() {
|
|
35
|
+
this._header = this.querySelector('[slot="header"], [data-header]');
|
|
36
|
+
this._contentSlot = this.querySelector('[slot="content"], [data-content]');
|
|
37
|
+
if (!this._header || !this._contentSlot) return;
|
|
38
|
+
this._ready = false;
|
|
39
|
+
this._content = this._ensurePanel(this._contentSlot);
|
|
40
|
+
|
|
41
|
+
const id = uid('acc');
|
|
42
|
+
this._header.id = this._header.id || `${id}-h`;
|
|
43
|
+
this._content.id = this._content.id || `${id}-c`;
|
|
44
|
+
this._header.setAttribute('aria-controls', this._content.id);
|
|
45
|
+
this._content.setAttribute('role', 'region');
|
|
46
|
+
this._content.setAttribute('aria-labelledby', this._header.id);
|
|
47
|
+
this._content.style.overflow = 'hidden';
|
|
48
|
+
this._content.style.willChange = 'height, opacity, transform';
|
|
49
|
+
this._content.style.transition = this._content.style.transition || this._enterTransition();
|
|
50
|
+
this._sync({ animate: false });
|
|
51
|
+
this._ready = true;
|
|
52
|
+
|
|
53
|
+
this.on(this._header, 'click', () => this.toggle());
|
|
54
|
+
this.on(this._header, 'keydown', (e) => {
|
|
55
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
this.toggle();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_ensurePanel(content) {
|
|
63
|
+
const existing = content.parentElement?.dataset?.accordionPanel === 'true'
|
|
64
|
+
? content.parentElement
|
|
65
|
+
: null;
|
|
66
|
+
if (existing) return existing;
|
|
67
|
+
|
|
68
|
+
const panel = document.createElement('div');
|
|
69
|
+
panel.dataset.accordionPanel = 'true';
|
|
70
|
+
content.before(panel);
|
|
71
|
+
panel.append(content);
|
|
72
|
+
return panel;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
attributeChangedCallback() {
|
|
76
|
+
this._sync({ animate: this._ready });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_sync({ animate = true } = {}) {
|
|
80
|
+
if (!this._header || !this._content) return;
|
|
81
|
+
const open = this.boolAttr('open');
|
|
82
|
+
this._header.setAttribute('aria-expanded', String(open));
|
|
83
|
+
this._content.setAttribute('aria-hidden', String(!open));
|
|
84
|
+
this._content.dataset.state = open ? 'open' : 'closed';
|
|
85
|
+
|
|
86
|
+
if (!animate || this._prefersReducedMotion()) {
|
|
87
|
+
this._finishTransition();
|
|
88
|
+
this._content.hidden = !open;
|
|
89
|
+
this._content.style.height = open ? '' : '0px';
|
|
90
|
+
this._content.style.opacity = open ? '1' : '0';
|
|
91
|
+
this._content.style.transform = open ? '' : 'translateY(-6px)';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (open) {
|
|
96
|
+
this._expand();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this._collapse();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
toggle() {
|
|
103
|
+
const parent = this.closest('dom-accordion');
|
|
104
|
+
const multi = parent?.hasAttribute('multiple');
|
|
105
|
+
const next = !this.boolAttr('open');
|
|
106
|
+
if (next && parent && !multi) {
|
|
107
|
+
parent.querySelectorAll('dom-accordion-item[open]').forEach((it) => {
|
|
108
|
+
if (it !== this) it.removeAttribute('open');
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
this.setBoolAttr('open', next);
|
|
112
|
+
this.emit('dom:change', { open: next });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_expand() {
|
|
116
|
+
this._finishTransition();
|
|
117
|
+
this._content.style.transition = this._enterTransition();
|
|
118
|
+
const startHeight = this._content.hidden ? 0 : this._content.getBoundingClientRect().height;
|
|
119
|
+
this._content.hidden = false;
|
|
120
|
+
this._content.style.height = `${startHeight}px`;
|
|
121
|
+
this._content.style.opacity = startHeight > 0 ? this._computedOpacity() : '0';
|
|
122
|
+
this._content.style.transform = 'translateY(-6px)';
|
|
123
|
+
this._afterFrame(() => {
|
|
124
|
+
this._content.style.height = `${this._content.scrollHeight}px`;
|
|
125
|
+
this._content.style.opacity = '1';
|
|
126
|
+
this._content.style.transform = 'translateY(0)';
|
|
127
|
+
this._onTransitionEnd(() => {
|
|
128
|
+
this._content.style.height = '';
|
|
129
|
+
this._content.style.transform = '';
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_collapse() {
|
|
135
|
+
this._finishTransition();
|
|
136
|
+
this._content.hidden = false;
|
|
137
|
+
this._content.style.transition = 'none';
|
|
138
|
+
const startHeight = this._content.getBoundingClientRect().height || this._content.scrollHeight;
|
|
139
|
+
const startOpacity = Number(this._computedOpacity()) || 1;
|
|
140
|
+
const duration = 340;
|
|
141
|
+
const startedAt = performance.now();
|
|
142
|
+
this._content.style.height = `${startHeight}px`;
|
|
143
|
+
this._content.style.opacity = String(startOpacity);
|
|
144
|
+
this._content.style.transform = 'translateY(0)';
|
|
145
|
+
this._content.offsetHeight;
|
|
146
|
+
const animate = () => {
|
|
147
|
+
const progress = Math.min((performance.now() - startedAt) / duration, 1);
|
|
148
|
+
const eased = this._easeInOut(progress);
|
|
149
|
+
const nextHeight = startHeight * (1 - eased);
|
|
150
|
+
this._content.style.height = `${nextHeight}px`;
|
|
151
|
+
this._content.style.opacity = String(Math.max(0.01, startOpacity * (1 - eased)));
|
|
152
|
+
this._content.style.transform = `translateY(${-3 * eased}px)`;
|
|
153
|
+
if (progress < 1) {
|
|
154
|
+
this._collapseFrame = requestAnimationFrame(animate);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this._collapseFrame = null;
|
|
158
|
+
this._content.style.height = '0px';
|
|
159
|
+
this._content.style.opacity = '0';
|
|
160
|
+
this._content.style.transform = 'translateY(-6px)';
|
|
161
|
+
this._content.hidden = true;
|
|
162
|
+
this._content.style.transition = this._enterTransition();
|
|
163
|
+
};
|
|
164
|
+
this._collapseFrame = requestAnimationFrame(animate);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_easeInOut(progress) {
|
|
168
|
+
return progress < 0.5
|
|
169
|
+
? 4 * progress * progress * progress
|
|
170
|
+
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_enterTransition() {
|
|
174
|
+
return 'height 300ms cubic-bezier(0.32, 0.72, 0, 1), opacity 220ms ease, transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_afterFrame(callback) {
|
|
178
|
+
requestAnimationFrame(() => requestAnimationFrame(callback));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_onTransitionEnd(callback) {
|
|
182
|
+
const token = Symbol('accordion-transition');
|
|
183
|
+
this._transitionToken = token;
|
|
184
|
+
const done = (event) => {
|
|
185
|
+
if (event && event.target !== this._content) return;
|
|
186
|
+
if (event && event.propertyName !== 'height') return;
|
|
187
|
+
if (this._transitionToken !== token) return;
|
|
188
|
+
this._content.removeEventListener('transitionend', done);
|
|
189
|
+
clearTimeout(this._transitionTimer);
|
|
190
|
+
this._transitionOff = null;
|
|
191
|
+
callback();
|
|
192
|
+
};
|
|
193
|
+
this._content.addEventListener('transitionend', done);
|
|
194
|
+
this._transitionOff = () => this._content.removeEventListener('transitionend', done);
|
|
195
|
+
this._transitionTimer = setTimeout(() => done(), 360);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_finishTransition() {
|
|
199
|
+
if (this._collapseFrame) cancelAnimationFrame(this._collapseFrame);
|
|
200
|
+
this._collapseFrame = null;
|
|
201
|
+
if (this._transitionTimer) clearTimeout(this._transitionTimer);
|
|
202
|
+
this._transitionOff?.();
|
|
203
|
+
this._transitionOff = null;
|
|
204
|
+
this._transitionToken = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
_computedOpacity() {
|
|
208
|
+
return getComputedStyle(this._content).opacity || '1';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_prefersReducedMotion() {
|
|
212
|
+
return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export class DomAccordionElement extends DomElementBase {
|
|
217
|
+
static __doc = {
|
|
218
|
+
name: 'dom-accordion',
|
|
219
|
+
description: 'Disclosure-panel group containing one or more <dom-accordion-item> children.',
|
|
220
|
+
slots: [
|
|
221
|
+
{ name: '(default)', description: 'A list of <dom-accordion-item> children.' },
|
|
222
|
+
],
|
|
223
|
+
attributes: [
|
|
224
|
+
{ name: 'multiple', type: 'boolean', description: 'Allow multiple items to be open at once. Otherwise opening one closes the others.' },
|
|
225
|
+
],
|
|
226
|
+
events: [],
|
|
227
|
+
keyboard: [],
|
|
228
|
+
example: `<dom-accordion multiple>
|
|
229
|
+
<dom-accordion-item open>
|
|
230
|
+
<button slot="header">Section A</button>
|
|
231
|
+
<div slot="content">Lorem ipsum…</div>
|
|
232
|
+
</dom-accordion-item>
|
|
233
|
+
<dom-accordion-item>
|
|
234
|
+
<button slot="header">Section B</button>
|
|
235
|
+
<div slot="content">Dolor sit amet…</div>
|
|
236
|
+
</dom-accordion-item>
|
|
237
|
+
</dom-accordion>`,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
connectedCallback() {
|
|
241
|
+
this.setAttribute('role', 'group');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
defineDomElement('dom-accordion-item', DomAccordionItemElement);
|
|
246
|
+
defineDomElement('dom-accordion', DomAccordionElement);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineDomElement } from './base.js';
|
|
2
|
+
import { ComboBase } from './combobox.js';
|
|
3
|
+
|
|
4
|
+
export class DomAutocompleteElement extends ComboBase {
|
|
5
|
+
static __doc = {
|
|
6
|
+
name: 'dom-autocomplete',
|
|
7
|
+
description: 'Free-text autocomplete. Suggestions are optional; typed values are valid and Enter commits the current text.',
|
|
8
|
+
slots: [
|
|
9
|
+
{ name: 'input', description: 'The text input. Gets role="combobox" and aria-controls wired automatically.' },
|
|
10
|
+
{ name: 'list', description: 'Optional suggestions. The list may be updated by a framework or server lookup.' },
|
|
11
|
+
],
|
|
12
|
+
attributes: [
|
|
13
|
+
{ name: 'value', type: 'string', description: 'Current text value.' },
|
|
14
|
+
{ name: 'open', type: 'boolean', description: 'Reflects whether suggestions are visible.' },
|
|
15
|
+
{ name: 'placement', type: 'string', description: 'Preferred floating placement before collision handling.' },
|
|
16
|
+
{ name: 'floating-mode', type: "'viewport' | 'anchor'", description: 'viewport keeps suggestions inside the browser; anchor keeps them attached to the input while scrolling.' },
|
|
17
|
+
{ name: 'data-menu-id', type: 'string', description: 'Id of an external or teleported suggestion list.' },
|
|
18
|
+
],
|
|
19
|
+
events: [
|
|
20
|
+
{ name: 'dom:input', payload: '{ value }', description: 'Fired whenever the text value changes.' },
|
|
21
|
+
{ name: 'dom:query', payload: '{ query }', description: 'Fired whenever the user types. Useful for server lookups.' },
|
|
22
|
+
{ name: 'dom:select', payload: '{ value, label, option }', description: 'Fired when a suggestion is selected.' },
|
|
23
|
+
{ name: 'dom:custom', payload: '{ value, label }', description: 'Cancelable event fired when Enter commits free text.' },
|
|
24
|
+
{ name: 'dom:change', payload: '{ value, option, custom }', description: 'Fired when text or a suggestion is committed.' },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
get _freeText() { return true; }
|
|
29
|
+
get _valueMode() { return 'text'; }
|
|
30
|
+
get _activeOnFilter() { return false; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
defineDomElement('dom-autocomplete', DomAutocompleteElement);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Generated by scripts/generate-types.mjs. Do not edit by hand.
|
|
3
|
+
export declare const isBrowser: boolean;
|
|
4
|
+
export declare function uid(prefix?: string): string;
|
|
5
|
+
export declare function defineDomElement(name: string, ctor: CustomElementConstructor): void;
|
|
6
|
+
export declare class DomElementBase extends HTMLElement {
|
|
7
|
+
emit<T = unknown>(type: string, detail?: T): void;
|
|
8
|
+
on(target: EventTarget, type: string, handler: EventListenerOrEventListenerObject, opts?: AddEventListenerOptions | boolean): () => void;
|
|
9
|
+
disconnectedCallback(): void;
|
|
10
|
+
boolAttr(name: string): boolean;
|
|
11
|
+
setBoolAttr(name: string, value: boolean): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function focusTrap(container: HTMLElement): () => void;
|
|
14
|
+
export interface RovingOptions {
|
|
15
|
+
items?: Array<HTMLElement>;
|
|
16
|
+
orientation?: 'horizontal' | 'vertical';
|
|
17
|
+
loop?: boolean;
|
|
18
|
+
onSelect?: (index: number, item: HTMLElement) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface RovingController {
|
|
21
|
+
readonly index: number;
|
|
22
|
+
setActive(index: number, options?: { focus?: boolean; select?: boolean }): void;
|
|
23
|
+
onKey(event: KeyboardEvent): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function createRoving(options?: RovingOptions): RovingController;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// DOM Studio — headless web component primitives
|
|
2
|
+
//
|
|
3
|
+
// The headless layer is pure DOM + JS. It carries the behavioural contract
|
|
4
|
+
// (state, ARIA wiring, keyboard handling, event emission) so the same logic
|
|
5
|
+
// can be consumed as a plain custom element or wrapped by any framework.
|
|
6
|
+
|
|
7
|
+
const isBrowser = typeof window !== 'undefined' && typeof customElements !== 'undefined';
|
|
8
|
+
|
|
9
|
+
let uidCounter = 0;
|
|
10
|
+
export function uid(prefix = 'el') {
|
|
11
|
+
uidCounter += 1;
|
|
12
|
+
return `${prefix}-${uidCounter.toString(36)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function defineDomElement(name, ctor) {
|
|
16
|
+
if (!isBrowser) return;
|
|
17
|
+
if (!customElements.get(name)) {
|
|
18
|
+
customElements.define(name, ctor);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DomElementBase extends (isBrowser ? HTMLElement : class {}) {
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
this._listeners = new Map();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
emit(type, detail) {
|
|
29
|
+
this.dispatchEvent(
|
|
30
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true }),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
on(target, type, handler, opts) {
|
|
35
|
+
target.addEventListener(type, handler, opts);
|
|
36
|
+
const key = Symbol();
|
|
37
|
+
this._listeners.set(key, () => target.removeEventListener(type, handler, opts));
|
|
38
|
+
return () => {
|
|
39
|
+
const off = this._listeners.get(key);
|
|
40
|
+
if (off) {
|
|
41
|
+
off();
|
|
42
|
+
this._listeners.delete(key);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
for (const off of this._listeners.values()) off();
|
|
49
|
+
this._listeners.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
boolAttr(name) {
|
|
53
|
+
return this.hasAttribute(name) && this.getAttribute(name) !== 'false';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setBoolAttr(name, value) {
|
|
57
|
+
const hasAttr = this.hasAttribute(name);
|
|
58
|
+
if (value && !hasAttr) this.setAttribute(name, '');
|
|
59
|
+
else if (!value && hasAttr) this.removeAttribute(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Tiny focus-trap used by dialog & combobox panel.
|
|
64
|
+
export function focusTrap(container) {
|
|
65
|
+
const sel = [
|
|
66
|
+
'a[href]', 'button:not([disabled])', 'input:not([disabled])',
|
|
67
|
+
'select:not([disabled])', 'textarea:not([disabled])',
|
|
68
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
69
|
+
].join(',');
|
|
70
|
+
const onKey = (e) => {
|
|
71
|
+
if (e.key !== 'Tab') return;
|
|
72
|
+
const nodes = container.querySelectorAll(sel);
|
|
73
|
+
if (!nodes.length) return;
|
|
74
|
+
const first = nodes[0];
|
|
75
|
+
const last = nodes[nodes.length - 1];
|
|
76
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
last.focus();
|
|
79
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
first.focus();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
container.addEventListener('keydown', onKey);
|
|
85
|
+
return () => container.removeEventListener('keydown', onKey);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Simple roving-tabindex helper for lists (tabs, menus, accordion).
|
|
89
|
+
export function createRoving({ items, orientation = 'horizontal', loop = true, onSelect } = {}) {
|
|
90
|
+
let index = 0;
|
|
91
|
+
const horiz = orientation === 'horizontal';
|
|
92
|
+
const nextKey = horiz ? 'ArrowRight' : 'ArrowDown';
|
|
93
|
+
const prevKey = horiz ? 'ArrowLeft' : 'ArrowUp';
|
|
94
|
+
|
|
95
|
+
const setActive = (i, options = {}) => {
|
|
96
|
+
const { focus = true, select = true } = options;
|
|
97
|
+
if (!items.length) return;
|
|
98
|
+
if (loop) i = (i + items.length) % items.length;
|
|
99
|
+
else i = Math.max(0, Math.min(items.length - 1, i));
|
|
100
|
+
index = i;
|
|
101
|
+
items.forEach((el, idx) => {
|
|
102
|
+
el.tabIndex = idx === i ? 0 : -1;
|
|
103
|
+
});
|
|
104
|
+
if (focus) items[i]?.focus();
|
|
105
|
+
if (select) onSelect?.(i, items[i]);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onKey = (e) => {
|
|
109
|
+
if (e.key === nextKey) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
setActive(index + 1);
|
|
112
|
+
} else if (e.key === prevKey) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
setActive(index - 1);
|
|
115
|
+
} else if (e.key === 'Home') {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
setActive(0);
|
|
118
|
+
} else if (e.key === 'End') {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
setActive(items.length - 1);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return { setActive, onKey, get index() { return index; } };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { isBrowser };
|