@haiyue/ui 0.1.2 → 0.2.2

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.
Files changed (110) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/LICENSE +27 -0
  3. package/README.md +643 -2
  4. package/dist/adapters/web/controls.d.ts +19 -0
  5. package/dist/adapters/web/controls.js +307 -0
  6. package/dist/adapters/web/dom.d.ts +10 -0
  7. package/dist/adapters/web/dom.js +98 -0
  8. package/dist/adapters/web/index.d.ts +3 -0
  9. package/dist/adapters/web/index.js +3 -0
  10. package/dist/adapters/web/renderer.d.ts +18 -0
  11. package/dist/adapters/web/renderer.js +553 -0
  12. package/dist/adapters/web/styles.d.ts +3 -0
  13. package/dist/adapters/web/styles.js +83 -0
  14. package/dist/adapters/web/types.d.ts +46 -0
  15. package/dist/adapters/web/types.js +2 -0
  16. package/dist/affix.d.ts +35 -0
  17. package/dist/affix.js +137 -0
  18. package/dist/badge.d.ts +45 -0
  19. package/dist/badge.js +168 -0
  20. package/dist/breadcrumb.d.ts +37 -0
  21. package/dist/breadcrumb.js +210 -0
  22. package/dist/button-group.d.ts +12 -0
  23. package/dist/button-group.js +72 -0
  24. package/dist/button.d.ts +15 -1
  25. package/dist/button.js +101 -12
  26. package/dist/carousel.d.ts +92 -0
  27. package/dist/carousel.js +509 -0
  28. package/dist/cascader.d.ts +65 -0
  29. package/dist/cascader.js +479 -0
  30. package/dist/collapse.d.ts +68 -0
  31. package/dist/collapse.js +348 -0
  32. package/dist/core/actions.d.ts +32 -0
  33. package/dist/core/actions.js +145 -0
  34. package/dist/core/data.d.ts +4 -0
  35. package/dist/core/data.js +27 -0
  36. package/dist/core/index.d.ts +7 -0
  37. package/dist/core/index.js +6 -0
  38. package/dist/core/platform-view.d.ts +17 -0
  39. package/dist/core/platform-view.js +84 -0
  40. package/dist/core/selection.d.ts +14 -0
  41. package/dist/core/selection.js +93 -0
  42. package/dist/core/text.d.ts +31 -0
  43. package/dist/core/text.js +154 -0
  44. package/dist/core/types.d.ts +269 -0
  45. package/dist/core/types.js +2 -0
  46. package/dist/core/validation.d.ts +24 -0
  47. package/dist/core/validation.js +243 -0
  48. package/dist/date-picker.d.ts +60 -0
  49. package/dist/date-picker.js +355 -0
  50. package/dist/dialog.d.ts +4 -0
  51. package/dist/dialog.js +23 -5
  52. package/dist/divider.d.ts +33 -0
  53. package/dist/divider.js +113 -0
  54. package/dist/draggable.d.ts +78 -0
  55. package/dist/draggable.js +255 -0
  56. package/dist/index.d.ts +53 -1
  57. package/dist/index.js +78 -0
  58. package/dist/input.d.ts +30 -1
  59. package/dist/input.js +166 -54
  60. package/dist/internal/qrcodegen.d.ts +94 -0
  61. package/dist/internal/qrcodegen.js +856 -0
  62. package/dist/masonry.d.ts +36 -0
  63. package/dist/masonry.js +171 -0
  64. package/dist/modal.d.ts +73 -0
  65. package/dist/modal.js +361 -0
  66. package/dist/portable/icons.d.ts +5 -0
  67. package/dist/portable/icons.js +24 -0
  68. package/dist/portable/index.d.ts +7 -0
  69. package/dist/portable/index.js +5 -0
  70. package/dist/portable/page.d.ts +38 -0
  71. package/dist/portable/page.js +361 -0
  72. package/dist/portable/responsive.d.ts +20 -0
  73. package/dist/portable/responsive.js +15 -0
  74. package/dist/portable/theme.d.ts +24 -0
  75. package/dist/portable/theme.js +60 -0
  76. package/dist/portable/tree.d.ts +10 -0
  77. package/dist/portable/tree.js +96 -0
  78. package/dist/qr-code.d.ts +62 -0
  79. package/dist/qr-code.js +224 -0
  80. package/dist/range.d.ts +3 -0
  81. package/dist/range.js +16 -5
  82. package/dist/rate.d.ts +46 -0
  83. package/dist/rate.js +193 -0
  84. package/dist/resizable.d.ts +97 -0
  85. package/dist/resizable.js +426 -0
  86. package/dist/skeleton.d.ts +41 -0
  87. package/dist/skeleton.js +141 -0
  88. package/dist/steps.d.ts +45 -0
  89. package/dist/steps.js +203 -0
  90. package/dist/table.d.ts +106 -0
  91. package/dist/table.js +689 -0
  92. package/dist/tabs.d.ts +3 -0
  93. package/dist/tabs.js +11 -1
  94. package/dist/tag.d.ts +55 -0
  95. package/dist/tag.js +225 -0
  96. package/dist/time-picker.d.ts +62 -0
  97. package/dist/time-picker.js +298 -0
  98. package/dist/timeline.d.ts +48 -0
  99. package/dist/timeline.js +201 -0
  100. package/dist/toast.d.ts +55 -0
  101. package/dist/toast.js +224 -0
  102. package/dist/tour.d.ts +89 -0
  103. package/dist/tour.js +306 -0
  104. package/dist/virtual-keyboard.d.ts +95 -0
  105. package/dist/virtual-keyboard.js +432 -0
  106. package/dist/virtual-mouse.d.ts +82 -0
  107. package/dist/virtual-mouse.js +358 -0
  108. package/dist/watermark.d.ts +44 -0
  109. package/dist/watermark.js +148 -0
  110. package/package.json +132 -4
@@ -0,0 +1,243 @@
1
+ import { isSnapshot, snapshot } from './data.js';
2
+ export class HYUIError extends Error {
3
+ code;
4
+ context;
5
+ constructor(code, message, context = {}, cause) {
6
+ const place = [context.source && `${context.source.file}:${context.source.line ?? 1}`, context.key, context.property,
7
+ context.operation, context.revision === undefined ? undefined : `revision=${context.revision}`].filter(Boolean).join(' ');
8
+ super(`${message}${place ? ` [${place}]` : ''}`, cause === undefined ? undefined : { cause });
9
+ this.name = 'HYUIError';
10
+ this.code = code;
11
+ this.context = Object.freeze({ ...context });
12
+ }
13
+ }
14
+ export const HY_FEATURES = Object.freeze([
15
+ 'text.composition', 'text.selection', 'text.secure', 'accessibility', 'icons.system', 'layout.rtl', 'theme.highContrast', 'motion.reduced',
16
+ ]);
17
+ const fail = (message, context) => { throw new HYUIError('invalid-props', message, context); };
18
+ const field = (context, key) => ({ ...context, property: context.property ? `${context.property}.${key}` : key });
19
+ function check(test, message) {
20
+ return (value, context) => { if (!test(value))
21
+ fail(message, context); };
22
+ }
23
+ const string = check(v => typeof v === 'string', 'Expected string');
24
+ const name = check(v => typeof v === 'string' && v.trim().length > 0, 'Expected non-empty identifier');
25
+ const boolean = check(v => typeof v === 'boolean', 'Expected boolean');
26
+ const number = check(v => typeof v === 'number' && Number.isFinite(v), 'Expected finite number');
27
+ const positive = check(v => typeof v === 'number' && Number.isFinite(v) && v > 0, 'Expected positive finite number');
28
+ const size = check(v => typeof v === 'number' && Number.isFinite(v) && v >= 0, 'Expected non-negative finite number');
29
+ const integer = check(v => Number.isSafeInteger(v) && v >= 0, 'Expected non-negative integer');
30
+ const choice = (...values) => check(v => values.includes(v), `Expected one of ${values.join(', ')}`);
31
+ const length = (v, c) => { if (v !== 'auto')
32
+ size(v, c); };
33
+ function record(value, context) {
34
+ if (!value || typeof value !== 'object' || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
35
+ fail('Expected plain data object', context);
36
+ return value;
37
+ }
38
+ function shape(fields, required = []) {
39
+ return (v, c) => {
40
+ const obj = record(v, c);
41
+ for (const key of required)
42
+ if (!Object.hasOwn(obj, key))
43
+ fail('Missing required property', field(c, key));
44
+ for (const key of Object.keys(obj)) {
45
+ if (!Object.hasOwn(fields, key))
46
+ fail('Unknown property', field(c, key));
47
+ const descriptor = Object.getOwnPropertyDescriptor(obj, key);
48
+ if (!('value' in descriptor))
49
+ fail('Accessors are not protocol data', field(c, key));
50
+ fields[key](descriptor.value, field(c, key));
51
+ }
52
+ };
53
+ }
54
+ const list = (item) => (v, c) => {
55
+ if (!Array.isArray(v))
56
+ fail('Expected array', c);
57
+ for (let i = 0; i < v.length; i++)
58
+ item(v[i], field(c, String(i)));
59
+ };
60
+ const track = (v, c) => typeof v === 'object' && v !== null ? shape({ grow: positive }, ['grow'])(v, c) : length(v, c);
61
+ const layout = (v, c) => {
62
+ shape({ width: length, height: length, minWidth: size, maxWidth: size, minHeight: size, maxHeight: size,
63
+ padding: (p, ctx) => { list(size)(p, ctx); if (p.length !== 4)
64
+ fail('Expected four padding values', ctx); },
65
+ gap: size, grow: size, align: choice('start', 'center', 'end', 'stretch'), columns: list(track), rows: list(track),
66
+ row: integer, column: integer, clip: boolean })(v, c);
67
+ const p = v;
68
+ for (const dimension of ['Width', 'Height'])
69
+ if (p[`min${dimension}`] > p[`max${dimension}`])
70
+ fail('Minimum exceeds maximum', c);
71
+ };
72
+ const common = {
73
+ visible: boolean, layout,
74
+ a11y: shape({ label: string, description: string, error: string,
75
+ role: choice('button', 'text', 'textbox', 'checkbox', 'tab', 'dialog', 'image', 'slider', 'list', 'tree') }, ['label']),
76
+ };
77
+ const resource = shape({ id: name, alt: string }, ['id', 'alt']);
78
+ const selection = { value: string, options: list(shape({ value: string, label: string, disabled: boolean }, ['value', 'label'])), disabled: boolean };
79
+ const text = { value: string, disabled: boolean, readOnly: boolean, placeholder: string };
80
+ const schemas = {
81
+ Row: shape(common), Column: shape(common), Grid: shape(common), Overlay: shape(common),
82
+ Scroll: shape({ ...common, axis: choice('horizontal', 'vertical') }, ['axis']),
83
+ Text: shape({ ...common, text: string, wrap: boolean, textStyle: choice('title', 'body', 'caption'), tone: choice('default', 'muted', 'danger') }, ['text']),
84
+ Image: shape({ ...common, resource, fit: choice('contain', 'cover') }, ['resource']),
85
+ Icon: shape({ ...common, resource, systemName: name }, ['resource']),
86
+ Button: shape({ ...common, text: string, disabled: boolean, busy: boolean, icon: choice('save', 'export', 'play', 'pause', 'reset', 'step', 'settings', 'back', 'chevronDown', 'check', 'close', 'add'), variant: choice('primary', 'secondary', 'quiet', 'danger'), size: choice('regular', 'compact') }, ['text']),
87
+ Checkbox: shape({ ...common, checked: boolean, disabled: boolean, text: string }, ['checked', 'text']),
88
+ RadioGroup: shape({ ...common, ...selection }, ['value', 'options']),
89
+ Rate: shape({ ...common, value: integer, count: check(v => Number.isInteger(v) && v >= 1 && v <= 10, 'Expected count 1–10'), disabled: boolean, readOnly: boolean }, ['value', 'count']),
90
+ Badge: shape({ ...common, text: string }, ['text']), Divider: shape({ ...common, text: string }),
91
+ Toggle: shape({ ...common, checked: boolean, disabled: boolean }, ['checked']),
92
+ Select: shape({ ...common, ...selection, presentation: choice('auto', 'popover', 'sheet') }, ['value', 'options']), Tabs: shape({ ...common, ...selection, variant: choice('underline', 'segmented') }, ['value', 'options']),
93
+ TextInput: shape({ ...common, ...text, secure: boolean }, ['value']), TextArea: shape({ ...common, ...text }, ['value']),
94
+ Slider: shape({ ...common, value: number, min: number, max: number, step: positive, disabled: boolean }, ['value', 'min', 'max', 'step']),
95
+ Dialog: shape({ ...common, open: boolean, title: string, dismissible: boolean }, ['open', 'title']),
96
+ FormGroup: shape({ ...common, label: string, description: string, error: string }, ['label']),
97
+ PlatformView: shape({ ...common, slot: name, resourceId: name }, ['slot', 'resourceId']),
98
+ };
99
+ export const HY_COMPONENTS = Object.freeze(Object.keys(schemas));
100
+ export function hyValidateCapabilities(capabilities) {
101
+ const context = { property: 'capabilities' };
102
+ shape({ contract: choice(1), backend: choice('web', 'nativescript', 'desktop'), components: list(choice(...HY_COMPONENTS)),
103
+ features: list((v, c) => { if (!HY_FEATURES.includes(v))
104
+ throw new HYUIError('capability', 'Unknown capability', c); }) }, ['contract', 'backend', 'components', 'features'])(capabilities, context);
105
+ if (new Set(capabilities.components).size !== capabilities.components.length || new Set(capabilities.features).size !== capabilities.features.length) {
106
+ throw new HYUIError('capability', 'Duplicate capability declaration', context);
107
+ }
108
+ }
109
+ export function hySupports(capabilities, feature) {
110
+ hyValidateCapabilities(capabilities);
111
+ if (!HY_FEATURES.includes(feature))
112
+ throw new HYUIError('capability', 'Unknown capability', { property: feature });
113
+ return capabilities.features.includes(feature);
114
+ }
115
+ const validatedSnapshots = new WeakMap();
116
+ function remember(props, kind) {
117
+ let kinds = validatedSnapshots.get(props);
118
+ if (!kinds) {
119
+ kinds = new Set();
120
+ validatedSnapshots.set(props, kinds);
121
+ }
122
+ kinds.add(kind);
123
+ }
124
+ /** Internal immutable hand-off. Mutable and merely shallow-frozen inputs are always validated. */
125
+ export function snapshotValidatedProps(kind, props, context, capabilities) {
126
+ hyValidateProps(kind, props, context, capabilities);
127
+ const result = snapshot(props);
128
+ remember(result, kind);
129
+ return result;
130
+ }
131
+ export function hyValidateProps(kind, props, context = {}, capabilities) {
132
+ if (!Object.hasOwn(schemas, kind))
133
+ fail('Unknown component kind', context);
134
+ if (!validatedSnapshots.get(props)?.has(kind)) {
135
+ schemas[kind](props, context);
136
+ if (kind === 'Select' || kind === 'Tabs' || kind === 'RadioGroup') {
137
+ const p = props;
138
+ if (new Set(p.options.map(o => o.value)).size !== p.options.length)
139
+ fail('Duplicate option value', field(context, 'options'));
140
+ if (!p.options.some(o => o.value === p.value))
141
+ fail('Unknown controlled value', field(context, 'value'));
142
+ }
143
+ if (kind === 'Button') {
144
+ const p = props;
145
+ if (p.icon && !p.text.trim() && !p.a11y?.label.trim())
146
+ fail('Icon-only button needs an accessible label', field(context, 'a11y.label'));
147
+ }
148
+ if (kind === 'Rate' && props.value > props.count)
149
+ fail('Rating exceeds count', context);
150
+ if (kind === 'Slider') {
151
+ const p = props;
152
+ if (p.min > p.max || p.value < p.min || p.value > p.max)
153
+ fail('Slider value/range is invalid', context);
154
+ }
155
+ if (kind !== 'Grid' && (props.layout?.rows || props.layout?.columns))
156
+ fail('Track definitions require Grid', field(context, 'layout'));
157
+ if (isSnapshot(props))
158
+ remember(props, kind);
159
+ }
160
+ if (capabilities) {
161
+ if (!capabilities.components.includes(kind))
162
+ throw new HYUIError('capability', `Unsupported component ${kind}`, context);
163
+ const needs = [];
164
+ if (props.a11y)
165
+ needs.push('accessibility');
166
+ if (kind === 'TextInput' && props.secure)
167
+ needs.push('text.secure');
168
+ if (kind === 'Icon' && props.systemName)
169
+ needs.push('icons.system');
170
+ for (const feature of needs)
171
+ if (!capabilities.features.includes(feature))
172
+ throw new HYUIError('capability', `Missing capability ${feature}`, context);
173
+ }
174
+ }
175
+ /** Escaped, stable backend key. Author keys are unique across all slots of one parent. */
176
+ export function hyKey(...parts) {
177
+ for (const key of parts)
178
+ name(key, { key });
179
+ if (!parts.length)
180
+ throw new HYUIError('invalid-props', 'A node path needs at least one key');
181
+ return '/' + parts.map(key => key.replace(/~/g, '~0').replace(/\//g, '~1')).join('/');
182
+ }
183
+ export function hyValidateNode(node, capabilities) {
184
+ if (capabilities)
185
+ hyValidateCapabilities(capabilities);
186
+ const ancestors = new Set();
187
+ const walk = (value, parent) => {
188
+ const raw = record(value, { property: 'node' });
189
+ for (const key of Object.keys(raw))
190
+ if (!('value' in Object.getOwnPropertyDescriptor(raw, key)))
191
+ fail('Accessors are not protocol data', { property: `node.${key}` });
192
+ name(raw.key, { property: 'key' });
193
+ const path = [...parent, raw.key];
194
+ const context = { key: hyKey(...path), ...(value.source ? { source: value.source } : {}) };
195
+ if (ancestors.has(value))
196
+ fail('Cyclic node tree', context);
197
+ ancestors.add(value);
198
+ for (const key of Object.keys(raw))
199
+ if (!['key', 'kind', 'props', 'children', 'slots', 'source', 'requires'].includes(key))
200
+ fail('Unknown node property', field(context, key));
201
+ if (value.source !== undefined)
202
+ shape({ file: name, line: check(v => Number.isSafeInteger(v) && v > 0, 'Expected positive line number') }, ['file'])(value.source, field(context, 'source'));
203
+ hyValidateProps(value.kind, value.props, context, capabilities);
204
+ if (value.requires !== undefined) {
205
+ list((v, c) => { if (!HY_FEATURES.includes(v))
206
+ throw new HYUIError('capability', 'Unknown capability', c); })(value.requires, field(context, 'requires'));
207
+ for (const feature of value.requires)
208
+ if (capabilities && !capabilities.features.includes(feature))
209
+ throw new HYUIError('capability', `Missing capability ${feature}`, context);
210
+ }
211
+ const siblings = new Set();
212
+ const visit = (children, property) => {
213
+ if (!Array.isArray(children))
214
+ fail('Expected children array', field(context, property));
215
+ for (const child of children) {
216
+ record(child, field(context, property));
217
+ if (siblings.has(child.key))
218
+ throw new HYUIError('duplicate-key', `Duplicate key ${child.key}`, { ...context, property });
219
+ siblings.add(child.key);
220
+ walk(child, path);
221
+ }
222
+ };
223
+ if (value.children !== undefined)
224
+ visit(value.children, 'children');
225
+ if (value.slots !== undefined) {
226
+ record(value.slots, field(context, 'slots'));
227
+ for (const [slot, children] of Object.entries(value.slots)) {
228
+ name(slot, context);
229
+ visit(children, `slots.${slot}`);
230
+ }
231
+ }
232
+ if ((value.children?.length || Object.values(value.slots ?? {}).some(a => a.length))
233
+ && !['Row', 'Column', 'Grid', 'Overlay', 'Scroll', 'Dialog', 'FormGroup', 'Tabs', 'Button'].includes(value.kind))
234
+ fail('Leaf component cannot contain children; use a container or PlatformView', context);
235
+ ancestors.delete(value);
236
+ };
237
+ walk(node, []);
238
+ return snapshot(node);
239
+ }
240
+ export function hyNode(kind, key, props, content = {}) {
241
+ return hyValidateNode({ kind, key, props, ...content });
242
+ }
243
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1,60 @@
1
+ export interface HYDatePickerChangeDetail {
2
+ readonly value: string;
3
+ readonly date: Date | null;
4
+ }
5
+ export interface HYDatePickerOpenChangeDetail {
6
+ readonly open: boolean;
7
+ }
8
+ /** Accessible calendar date picker with stable ISO values. */
9
+ export declare class HYDatePicker extends HTMLElement {
10
+ private readonly _control;
11
+ private readonly _trigger;
12
+ private readonly _label;
13
+ private readonly _arrow;
14
+ private readonly _clear;
15
+ private readonly _panel;
16
+ private readonly _panelId;
17
+ private _value;
18
+ private _viewDate;
19
+ private _reflecting;
20
+ private _outsideAbort;
21
+ static get observedAttributes(): string[];
22
+ constructor();
23
+ connectedCallback(): void;
24
+ disconnectedCallback(): void;
25
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
26
+ get value(): string;
27
+ set value(value: string);
28
+ get open(): boolean;
29
+ set open(value: boolean);
30
+ get disabled(): boolean;
31
+ set disabled(value: boolean);
32
+ get clearable(): boolean;
33
+ set clearable(value: boolean);
34
+ get placeholder(): string;
35
+ set placeholder(value: string);
36
+ get min(): string;
37
+ set min(value: string);
38
+ get max(): string;
39
+ set max(value: string);
40
+ get locale(): string;
41
+ set locale(value: string);
42
+ get weekStart(): 0 | 1;
43
+ set weekStart(value: 0 | 1);
44
+ private _syncAttributes;
45
+ private _normalizeValue;
46
+ private _reflectValue;
47
+ private _isAllowed;
48
+ private _setViewFromValue;
49
+ private _formatDate;
50
+ private _render;
51
+ private _renderPanel;
52
+ private _monthButton;
53
+ private _handleDayKeydown;
54
+ private _focusDate;
55
+ private _focusPreferredDate;
56
+ private _commit;
57
+ private _syncOutsideListener;
58
+ private _dispatchOpenChange;
59
+ }
60
+ export declare function defineDatePickerComponents(): void;
@@ -0,0 +1,355 @@
1
+ function parseISODate(value) {
2
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(String(value ?? ''));
3
+ if (!match)
4
+ return null;
5
+ const year = Number(match[1]);
6
+ const month = Number(match[2]);
7
+ const day = Number(match[3]);
8
+ const date = new Date(year, month - 1, day);
9
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ? date : null;
10
+ }
11
+ function toISODate(date) {
12
+ return `${String(date.getFullYear()).padStart(4, '0')}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
13
+ }
14
+ function addDays(date, amount) {
15
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
16
+ }
17
+ function addMonths(date, amount) {
18
+ return new Date(date.getFullYear(), date.getMonth() + amount, 1);
19
+ }
20
+ function addCalendarMonths(date, amount) {
21
+ const first = addMonths(date, amount);
22
+ const lastDay = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
23
+ return new Date(first.getFullYear(), first.getMonth(), Math.min(date.getDate(), lastDay));
24
+ }
25
+ /** Accessible calendar date picker with stable ISO values. */
26
+ export class HYDatePicker extends HTMLElement {
27
+ _control = document.createElement('div');
28
+ _trigger = document.createElement('button');
29
+ _label = document.createElement('span');
30
+ _arrow = document.createElement('span');
31
+ _clear = document.createElement('button');
32
+ _panel = document.createElement('div');
33
+ _panelId = `hy-date-picker-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
34
+ _value = '';
35
+ _viewDate = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
36
+ _reflecting = false;
37
+ _outsideAbort = null;
38
+ static get observedAttributes() {
39
+ return ['value', 'min', 'max', 'placeholder', 'disabled', 'clearable', 'open', 'locale', 'week-start', 'aria-label'];
40
+ }
41
+ constructor() {
42
+ super();
43
+ const root = this.attachShadow({ mode: 'open' });
44
+ const style = document.createElement('style');
45
+ style.textContent = `
46
+ :host { position: relative; display: inline-block; min-width: 180px; color: var(--hy-text-color, #eef3ff); font: 12px/1.4 system-ui, sans-serif; }
47
+ .control { display: grid; grid-template-columns: minmax(0, 1fr) auto; height: var(--hy-date-picker-height, 34px); box-sizing: border-box; border: 1px solid var(--hy-border-color, #3d4654); border-radius: var(--hy-date-picker-radius, 6px); background: var(--hy-input-bg-color, #111722); }
48
+ :host(:not([disabled])) .control:hover { border-color: var(--hy-hover-border-color, #596779); }
49
+ :host(:focus-within) .control, :host([open]) .control { border-color: var(--hy-focus-border-color, #79a8ff); box-shadow: 0 0 0 2px var(--hy-focus-ring-color, rgba(121, 168, 255, 0.22)); }
50
+ .trigger { display: flex; align-items: center; min-width: 0; gap: 8px; padding: 0 8px 0 10px; overflow: hidden; border: 0; color: inherit; background: transparent; cursor: pointer; font: inherit; text-align: left; }
51
+ .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52
+ .label.placeholder { color: var(--hy-muted-color, #788697); }
53
+ .arrow { flex: 0 0 auto; color: var(--hy-muted-color, #788697); font-size: 14px; }
54
+ .clear { width: 28px; padding: 0; border: 0; color: var(--hy-muted-color, #788697); background: transparent; cursor: pointer; font: 15px/1 system-ui, sans-serif; }
55
+ .clear:hover { color: var(--hy-text-color, #eef3ff); }
56
+ .clear[hidden] { display: none; }
57
+ .trigger:focus-visible, .clear:focus-visible { outline: none; }
58
+ :host([disabled]) { opacity: 0.52; }
59
+ :host([disabled]) .trigger, :host([disabled]) .clear { cursor: not-allowed; }
60
+ .panel { position: absolute; z-index: var(--hy-date-picker-z-index, 50); top: calc(100% + 6px); left: 0; width: var(--hy-date-picker-panel-width, 286px); box-sizing: border-box; padding: 10px; border: 1px solid var(--hy-border-color, #3d4654); border-radius: var(--hy-date-picker-panel-radius, 8px); background: var(--hy-menu-bg-color, var(--hy-surface-elevated-color, #171d27)); box-shadow: 0 12px 32px var(--hy-menu-shadow-color, rgba(0, 0, 0, 0.36)); }
61
+ .panel[hidden] { display: none; }
62
+ .calendar-header { display: grid; grid-template-columns: 30px 1fr 30px; align-items: center; margin-bottom: 7px; }
63
+ .month-label { color: var(--hy-text-color, #eef3ff); font-weight: 650; text-align: center; }
64
+ .month-button, .footer button { height: 28px; border: 1px solid transparent; border-radius: 4px; color: var(--hy-secondary-text-color, #aebbd0); background: transparent; cursor: pointer; font: inherit; }
65
+ .month-button:hover:not(:disabled), .footer button:hover:not(:disabled) { color: var(--hy-accent-color, #79a8ff); background: var(--hy-hover-bg-color, #273241); }
66
+ .month-button:focus-visible, .footer button:focus-visible { outline: 2px solid var(--hy-focus-border-color, #79a8ff); outline-offset: 1px; }
67
+ .month-button:disabled, .footer button:disabled { cursor: not-allowed; opacity: 0.35; }
68
+ .weekdays, .days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
69
+ .weekday { padding: 4px 0; color: var(--hy-muted-color, #788697); font-size: 10px; text-align: center; }
70
+ .day { position: relative; width: 100%; aspect-ratio: 1; min-height: 31px; padding: 0; border: 1px solid transparent; border-radius: var(--hy-date-picker-day-radius, 50%); color: var(--hy-secondary-text-color, #aebbd0); background: transparent; cursor: pointer; font: inherit; }
71
+ .day:hover:not(:disabled), .day:focus-visible { outline: none; color: var(--hy-text-color, #eef3ff); border-color: var(--hy-hover-border-color, #596779); background: var(--hy-hover-bg-color, #273241); }
72
+ .day.outside { color: var(--hy-muted-color, #788697); opacity: 0.6; }
73
+ .day.today::after { position: absolute; right: 50%; bottom: 3px; width: 3px; height: 3px; border-radius: 50%; background: var(--hy-accent-color, #79a8ff); content: ''; transform: translateX(50%); }
74
+ .day[aria-selected="true"] { color: var(--hy-selected-text-color, #fff); border-color: var(--hy-selected-bg-color, #554cc6); background: var(--hy-selected-bg-color, #554cc6); }
75
+ .day:disabled { cursor: not-allowed; opacity: 0.24; }
76
+ .footer { display: flex; justify-content: flex-end; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--hy-border-color, #3d4654); }
77
+ .footer button { padding: 0 9px; color: var(--hy-accent-color, #79a8ff); }
78
+ `;
79
+ this._control.className = 'control';
80
+ this._control.part.add('control');
81
+ this._trigger.type = 'button';
82
+ this._trigger.className = 'trigger';
83
+ this._trigger.part.add('trigger');
84
+ this._trigger.setAttribute('aria-haspopup', 'dialog');
85
+ this._trigger.setAttribute('aria-controls', this._panelId);
86
+ this._label.className = 'label';
87
+ this._label.part.add('label');
88
+ this._arrow.className = 'arrow';
89
+ this._arrow.part.add('arrow');
90
+ this._arrow.setAttribute('aria-hidden', 'true');
91
+ this._arrow.textContent = '▣';
92
+ this._trigger.append(this._label, this._arrow);
93
+ this._clear.type = 'button';
94
+ this._clear.className = 'clear';
95
+ this._clear.part.add('clear');
96
+ this._clear.setAttribute('aria-label', 'Clear date');
97
+ this._clear.textContent = '×';
98
+ this._panel.id = this._panelId;
99
+ this._panel.className = 'panel';
100
+ this._panel.part.add('panel');
101
+ this._panel.setAttribute('role', 'dialog');
102
+ this._control.append(this._trigger, this._clear);
103
+ root.append(style, this._control, this._panel);
104
+ this._trigger.addEventListener('click', () => { if (!this.disabled)
105
+ this.open = !this.open; });
106
+ this._trigger.addEventListener('keydown', event => {
107
+ if (event.key === 'ArrowDown' && !this.disabled) {
108
+ event.preventDefault();
109
+ this.open = true;
110
+ queueMicrotask(() => this._focusPreferredDate());
111
+ }
112
+ else if (event.key === 'Escape' && this.open) {
113
+ event.preventDefault();
114
+ this.open = false;
115
+ }
116
+ });
117
+ this._clear.addEventListener('click', event => { event.stopPropagation(); if (!this.disabled)
118
+ this._commit(''); });
119
+ }
120
+ connectedCallback() { this._syncAttributes(); this._render(); this._syncOutsideListener(); }
121
+ disconnectedCallback() { this._outsideAbort?.abort(); this._outsideAbort = null; }
122
+ attributeChangedCallback(name, oldValue, newValue) {
123
+ if (oldValue === newValue || this._reflecting)
124
+ return;
125
+ if (name === 'value' || name === 'min' || name === 'max') {
126
+ this._value = this._normalizeValue(this.getAttribute('value') ?? '');
127
+ this._reflectValue();
128
+ }
129
+ if (name === 'open' && this.open)
130
+ this._setViewFromValue();
131
+ this._render();
132
+ if (name === 'open') {
133
+ this._syncOutsideListener();
134
+ this._dispatchOpenChange();
135
+ }
136
+ }
137
+ get value() { return this._value; }
138
+ set value(value) { this._value = this._normalizeValue(value); this._reflectValue(); this._render(); }
139
+ get open() { return this.hasAttribute('open'); }
140
+ set open(value) { this.toggleAttribute('open', Boolean(value)); }
141
+ get disabled() { return this.hasAttribute('disabled'); }
142
+ set disabled(value) { this.toggleAttribute('disabled', Boolean(value)); }
143
+ get clearable() { return this.getAttribute('clearable') !== 'false'; }
144
+ set clearable(value) { this.setAttribute('clearable', String(Boolean(value))); }
145
+ get placeholder() { return this.getAttribute('placeholder') ?? 'Select date'; }
146
+ set placeholder(value) { this.setAttribute('placeholder', String(value)); }
147
+ get min() { return parseISODate(this.getAttribute('min')) ? this.getAttribute('min') : ''; }
148
+ set min(value) { if (parseISODate(value))
149
+ this.setAttribute('min', value);
150
+ else
151
+ this.removeAttribute('min'); }
152
+ get max() { return parseISODate(this.getAttribute('max')) ? this.getAttribute('max') : ''; }
153
+ set max(value) { if (parseISODate(value))
154
+ this.setAttribute('max', value);
155
+ else
156
+ this.removeAttribute('max'); }
157
+ get locale() { return this.getAttribute('locale') || document.documentElement.lang || navigator.language; }
158
+ set locale(value) { this.setAttribute('locale', String(value)); }
159
+ get weekStart() { return this.getAttribute('week-start') === '0' ? 0 : 1; }
160
+ set weekStart(value) { this.setAttribute('week-start', value === 0 ? '0' : '1'); }
161
+ _syncAttributes() { this._value = this._normalizeValue(this.getAttribute('value') ?? ''); this._reflectValue(); if (this.open)
162
+ this._setViewFromValue(); }
163
+ _normalizeValue(value) {
164
+ const date = parseISODate(value);
165
+ if (!date)
166
+ return '';
167
+ const iso = toISODate(date);
168
+ return this._isAllowed(iso) ? iso : '';
169
+ }
170
+ _reflectValue() {
171
+ if (this.getAttribute('value') === this._value)
172
+ return;
173
+ this._reflecting = true;
174
+ if (this._value)
175
+ this.setAttribute('value', this._value);
176
+ else
177
+ this.removeAttribute('value');
178
+ this._reflecting = false;
179
+ }
180
+ _isAllowed(value) { return (!this.min || value >= this.min) && (!this.max || value <= this.max); }
181
+ _setViewFromValue() {
182
+ const selected = parseISODate(this._value);
183
+ const today = new Date();
184
+ const source = selected ?? today;
185
+ this._viewDate = new Date(source.getFullYear(), source.getMonth(), 1);
186
+ }
187
+ _formatDate(value) {
188
+ const date = parseISODate(value);
189
+ if (!date)
190
+ return '';
191
+ return new Intl.DateTimeFormat(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(date);
192
+ }
193
+ _render() {
194
+ const text = this._formatDate(this._value);
195
+ this._label.textContent = text || this.placeholder;
196
+ this._label.classList.toggle('placeholder', !text);
197
+ this._trigger.disabled = this.disabled;
198
+ this._trigger.setAttribute('aria-expanded', String(this.open));
199
+ const name = this.getAttribute('aria-label');
200
+ if (name)
201
+ this._trigger.setAttribute('aria-label', name);
202
+ else
203
+ this._trigger.removeAttribute('aria-label');
204
+ this._clear.hidden = !this.clearable || !this._value;
205
+ this._clear.disabled = this.disabled;
206
+ this._panel.hidden = !this.open;
207
+ if (this.open)
208
+ this._renderPanel();
209
+ else
210
+ this._panel.replaceChildren();
211
+ }
212
+ _renderPanel() {
213
+ const header = document.createElement('div');
214
+ header.className = 'calendar-header';
215
+ header.part.add('header');
216
+ const previous = this._monthButton('‹', 'Previous month', -1);
217
+ const next = this._monthButton('›', 'Next month', 1);
218
+ const month = document.createElement('div');
219
+ month.className = 'month-label';
220
+ month.part.add('month-label');
221
+ month.setAttribute('aria-live', 'polite');
222
+ month.textContent = new Intl.DateTimeFormat(this.locale, { year: 'numeric', month: 'long' }).format(this._viewDate);
223
+ header.append(previous, month, next);
224
+ const weekdays = document.createElement('div');
225
+ weekdays.className = 'weekdays';
226
+ weekdays.part.add('weekdays');
227
+ const sunday = new Date(2024, 0, 7);
228
+ for (let index = 0; index < 7; index += 1) {
229
+ const element = document.createElement('span');
230
+ element.className = 'weekday';
231
+ element.part.add('weekday');
232
+ element.textContent = new Intl.DateTimeFormat(this.locale, { weekday: 'short' }).format(addDays(sunday, (index + this.weekStart) % 7));
233
+ weekdays.append(element);
234
+ }
235
+ const days = document.createElement('div');
236
+ days.className = 'days';
237
+ days.part.add('days');
238
+ days.setAttribute('role', 'grid');
239
+ const first = new Date(this._viewDate.getFullYear(), this._viewDate.getMonth(), 1);
240
+ const offset = (first.getDay() - this.weekStart + 7) % 7;
241
+ const start = addDays(first, -offset);
242
+ const today = toISODate(new Date());
243
+ for (let index = 0; index < 42; index += 1) {
244
+ const date = addDays(start, index);
245
+ const iso = toISODate(date);
246
+ const button = document.createElement('button');
247
+ button.type = 'button';
248
+ button.className = 'day';
249
+ button.part.add('day');
250
+ button.dataset.date = iso;
251
+ button.setAttribute('role', 'gridcell');
252
+ button.setAttribute('aria-label', new Intl.DateTimeFormat(this.locale, { dateStyle: 'full' }).format(date));
253
+ button.setAttribute('aria-selected', String(iso === this._value));
254
+ button.textContent = String(date.getDate());
255
+ button.disabled = !this._isAllowed(iso);
256
+ button.classList.toggle('outside', date.getMonth() !== this._viewDate.getMonth());
257
+ button.classList.toggle('today', iso === today);
258
+ button.addEventListener('click', () => this._commit(iso));
259
+ button.addEventListener('keydown', event => this._handleDayKeydown(event, date));
260
+ days.append(button);
261
+ }
262
+ const footer = document.createElement('div');
263
+ footer.className = 'footer';
264
+ footer.part.add('footer');
265
+ const todayButton = document.createElement('button');
266
+ todayButton.type = 'button';
267
+ todayButton.textContent = 'Today';
268
+ todayButton.disabled = !this._isAllowed(today);
269
+ todayButton.addEventListener('click', () => this._commit(today));
270
+ footer.append(todayButton);
271
+ this._panel.replaceChildren(header, weekdays, days, footer);
272
+ }
273
+ _monthButton(text, label, direction) {
274
+ const button = document.createElement('button');
275
+ button.type = 'button';
276
+ button.className = 'month-button';
277
+ button.part.add(direction < 0 ? 'previous' : 'next');
278
+ button.textContent = text;
279
+ button.setAttribute('aria-label', label);
280
+ const candidate = addMonths(this._viewDate, direction);
281
+ const first = toISODate(candidate);
282
+ const last = toISODate(new Date(candidate.getFullYear(), candidate.getMonth() + 1, 0));
283
+ button.disabled = Boolean((this.min && last < this.min) || (this.max && first > this.max));
284
+ button.addEventListener('click', () => { this._viewDate = candidate; this._renderPanel(); });
285
+ return button;
286
+ }
287
+ _handleDayKeydown(event, date) {
288
+ let target = null;
289
+ if (event.key === 'ArrowLeft')
290
+ target = addDays(date, -1);
291
+ else if (event.key === 'ArrowRight')
292
+ target = addDays(date, 1);
293
+ else if (event.key === 'ArrowUp')
294
+ target = addDays(date, -7);
295
+ else if (event.key === 'ArrowDown')
296
+ target = addDays(date, 7);
297
+ else if (event.key === 'Home')
298
+ target = addDays(date, -(date.getDay() - this.weekStart + 7) % 7);
299
+ else if (event.key === 'End')
300
+ target = addDays(date, 6 - (date.getDay() - this.weekStart + 7) % 7);
301
+ else if (event.key === 'PageUp')
302
+ target = addCalendarMonths(date, -1);
303
+ else if (event.key === 'PageDown')
304
+ target = addCalendarMonths(date, 1);
305
+ else if (event.key === 'Escape') {
306
+ event.preventDefault();
307
+ this.open = false;
308
+ this._trigger.focus();
309
+ return;
310
+ }
311
+ if (!target)
312
+ return;
313
+ event.preventDefault();
314
+ if (!this._isAllowed(toISODate(target)))
315
+ return;
316
+ this._focusDate(target);
317
+ }
318
+ _focusDate(date) {
319
+ if (date.getMonth() !== this._viewDate.getMonth() || date.getFullYear() !== this._viewDate.getFullYear()) {
320
+ this._viewDate = new Date(date.getFullYear(), date.getMonth(), 1);
321
+ this._renderPanel();
322
+ }
323
+ queueMicrotask(() => this._panel.querySelector(`.day[data-date="${toISODate(date)}"]`)?.focus());
324
+ }
325
+ _focusPreferredDate() { const preferred = parseISODate(this._value) ?? new Date(); if (this._isAllowed(toISODate(preferred)))
326
+ this._focusDate(preferred);
327
+ else
328
+ this._panel.querySelector('.day:not(:disabled)')?.focus(); }
329
+ _commit(value) {
330
+ this._value = this._normalizeValue(value);
331
+ this._reflectValue();
332
+ if (this.open)
333
+ this.open = false;
334
+ else
335
+ this._render();
336
+ this.dispatchEvent(new CustomEvent('value-change', { bubbles: true, composed: true, detail: { value: this._value, date: parseISODate(this._value) } }));
337
+ }
338
+ _syncOutsideListener() {
339
+ this._outsideAbort?.abort();
340
+ this._outsideAbort = null;
341
+ if (!this.open || !this.isConnected)
342
+ return;
343
+ const abort = new AbortController();
344
+ this._outsideAbort = abort;
345
+ queueMicrotask(() => { if (!abort.signal.aborted)
346
+ document.addEventListener('pointerdown', event => { if (!event.composedPath().includes(this))
347
+ this.open = false; }, { capture: true, signal: abort.signal }); });
348
+ }
349
+ _dispatchOpenChange() { this.dispatchEvent(new CustomEvent('open-change', { bubbles: true, composed: true, detail: { open: this.open } })); }
350
+ }
351
+ export function defineDatePickerComponents() {
352
+ if (!customElements.get('hy-date-picker'))
353
+ customElements.define('hy-date-picker', HYDatePicker);
354
+ }
355
+ //# sourceMappingURL=date-picker.js.map