@haiyue/ui 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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/checkbox.d.ts +30 -0
  4. package/dist/checkbox.js +204 -0
  5. package/dist/context-menu.d.ts +41 -0
  6. package/dist/context-menu.js +225 -0
  7. package/dist/dialog.d.ts +30 -0
  8. package/dist/dialog.js +183 -0
  9. package/dist/dropdown.d.ts +45 -0
  10. package/dist/dropdown.js +228 -0
  11. package/dist/index.d.ts +32 -0
  12. package/dist/index.js +110 -0
  13. package/dist/input.d.ts +30 -0
  14. package/dist/input.js +131 -0
  15. package/dist/menu-shared.d.ts +14 -0
  16. package/dist/menu-shared.js +54 -0
  17. package/dist/radio.d.ts +30 -0
  18. package/dist/radio.js +205 -0
  19. package/dist/select.d.ts +40 -0
  20. package/dist/select.js +253 -0
  21. package/dist/split.d.ts +46 -0
  22. package/dist/split.js +277 -0
  23. package/dist/tabs.d.ts +28 -0
  24. package/dist/tabs.js +161 -0
  25. package/dist/tooltip.d.ts +37 -0
  26. package/dist/tooltip.js +286 -0
  27. package/dist/tree-model.d.ts +35 -0
  28. package/dist/tree-model.js +281 -0
  29. package/dist/tree-node.d.ts +11 -0
  30. package/dist/tree-node.js +63 -0
  31. package/dist/tree-types.d.ts +41 -0
  32. package/dist/tree-types.js +2 -0
  33. package/dist/tree.d.ts +102 -0
  34. package/dist/tree.js +942 -0
  35. package/package.json +46 -0
  36. package/src/checkbox.ts +215 -0
  37. package/src/context-menu.ts +248 -0
  38. package/src/dialog.ts +195 -0
  39. package/src/dropdown.ts +249 -0
  40. package/src/index.ts +192 -0
  41. package/src/input.ts +150 -0
  42. package/src/menu-shared.ts +65 -0
  43. package/src/radio.ts +216 -0
  44. package/src/select.ts +271 -0
  45. package/src/split.ts +306 -0
  46. package/src/tabs.ts +182 -0
  47. package/src/tooltip.ts +321 -0
  48. package/src/tree-model.ts +305 -0
  49. package/src/tree-node.ts +67 -0
  50. package/src/tree-types.ts +47 -0
  51. package/src/tree.ts +1002 -0
package/src/radio.ts ADDED
@@ -0,0 +1,216 @@
1
+ export interface GERadioChangeDetail {
2
+ checked: boolean;
3
+ value: string;
4
+ name: string;
5
+ }
6
+
7
+ export class GERadio extends HTMLElement {
8
+ private readonly _input = document.createElement('input');
9
+ private readonly _marker = document.createElement('span');
10
+ private readonly _label = document.createElement('span');
11
+ private readonly _slot = document.createElement('slot');
12
+
13
+ static get observedAttributes(): string[] {
14
+ return ['checked', 'disabled', 'label', 'value', 'name', 'aria-label'];
15
+ }
16
+
17
+ constructor() {
18
+ super();
19
+ const root = this.attachShadow({ mode: 'open' });
20
+ const style = document.createElement('style');
21
+ style.textContent = `
22
+ :host {
23
+ display: inline-block;
24
+ width: max-content;
25
+ min-width: 0;
26
+ color: var(--ge-radio-text-color, var(--ge-text-color, #d8e2f2));
27
+ font: 12px system-ui, sans-serif;
28
+ }
29
+ :host([disabled]) {
30
+ opacity: 0.55;
31
+ cursor: default;
32
+ }
33
+ label {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: 8px;
37
+ min-width: 0;
38
+ cursor: pointer;
39
+ user-select: none;
40
+ }
41
+ :host([disabled]) label {
42
+ cursor: default;
43
+ }
44
+ input {
45
+ position: absolute;
46
+ width: 1px;
47
+ height: 1px;
48
+ margin: -1px;
49
+ padding: 0;
50
+ overflow: hidden;
51
+ clip: rect(0 0 0 0);
52
+ white-space: nowrap;
53
+ border: 0;
54
+ }
55
+ .marker {
56
+ box-sizing: border-box;
57
+ width: 14px;
58
+ height: 14px;
59
+ flex: 0 0 14px;
60
+ display: inline-grid;
61
+ place-items: center;
62
+ border: 1px solid var(--ge-radio-border-color, var(--ge-border-color, #303746));
63
+ border-radius: 50%;
64
+ background: var(--ge-radio-bg-color, var(--ge-input-bg-color, #121822));
65
+ color: var(--ge-radio-dot-color, var(--ge-selected-text-color, #ffffff));
66
+ transition: background 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
67
+ }
68
+ input:focus-visible + .marker {
69
+ border-color: var(--ge-focus-border-color, #3d6fa8);
70
+ box-shadow: 0 0 0 2px var(--ge-focus-ring-color, rgba(61, 111, 168, 0.28));
71
+ }
72
+ input:checked + .marker {
73
+ border-color: var(--ge-radio-checked-border-color, var(--ge-accent-color, #3d6fa8));
74
+ background: var(--ge-radio-checked-bg-color, var(--ge-accent-strong-color, #255a91));
75
+ }
76
+ .marker::after {
77
+ content: "";
78
+ width: 6px;
79
+ height: 6px;
80
+ border-radius: 50%;
81
+ background: currentColor;
82
+ transform: scale(0);
83
+ transition: transform 0.12s ease;
84
+ }
85
+ input:checked + .marker::after {
86
+ transform: scale(1);
87
+ }
88
+ .label {
89
+ min-width: 0;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ }
94
+ `;
95
+
96
+ const label = document.createElement('label');
97
+ this._input.type = 'radio';
98
+ this._marker.className = 'marker';
99
+ this._marker.setAttribute('aria-hidden', 'true');
100
+ this._label.className = 'label';
101
+ this._label.append(this._slot);
102
+ label.append(this._input, this._marker, this._label);
103
+ root.append(style, label);
104
+ }
105
+
106
+ connectedCallback(): void {
107
+ this._input.addEventListener('change', this._onChange);
108
+ this._sync();
109
+ if (this.checked) this._uncheckNamedPeers();
110
+ }
111
+
112
+ disconnectedCallback(): void {
113
+ this._input.removeEventListener('change', this._onChange);
114
+ }
115
+
116
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
117
+ this._sync();
118
+ if (name === 'checked' && oldValue !== newValue && newValue !== null) {
119
+ this._uncheckNamedPeers();
120
+ }
121
+ }
122
+
123
+ get checked(): boolean {
124
+ return this.hasAttribute('checked');
125
+ }
126
+
127
+ set checked(value: boolean) {
128
+ if (value) this.setAttribute('checked', '');
129
+ else this.removeAttribute('checked');
130
+ }
131
+
132
+ get disabled(): boolean {
133
+ return this.hasAttribute('disabled');
134
+ }
135
+
136
+ set disabled(value: boolean) {
137
+ if (value) this.setAttribute('disabled', '');
138
+ else this.removeAttribute('disabled');
139
+ }
140
+
141
+ get label(): string {
142
+ return this.getAttribute('label') ?? '';
143
+ }
144
+
145
+ set label(value: string) {
146
+ if (value) this.setAttribute('label', value);
147
+ else this.removeAttribute('label');
148
+ }
149
+
150
+ get value(): string {
151
+ return this.getAttribute('value') ?? 'on';
152
+ }
153
+
154
+ set value(value: string) {
155
+ this.setAttribute('value', value);
156
+ }
157
+
158
+ get name(): string {
159
+ return this.getAttribute('name') ?? '';
160
+ }
161
+
162
+ set name(value: string) {
163
+ if (value) this.setAttribute('name', value);
164
+ else this.removeAttribute('name');
165
+ }
166
+
167
+ private _sync(): void {
168
+ this._input.checked = this.checked;
169
+ this._input.disabled = this.disabled;
170
+ this._input.value = this.value;
171
+ this._input.name = this.name;
172
+
173
+ const label = this.getAttribute('label');
174
+ if (label !== null) this._slot.textContent = label;
175
+ else if (this._slot.textContent !== '') this._slot.textContent = '';
176
+
177
+ const ariaLabel = this.getAttribute('aria-label') ?? label ?? '';
178
+ if (ariaLabel) this._input.setAttribute('aria-label', ariaLabel);
179
+ else this._input.removeAttribute('aria-label');
180
+ }
181
+
182
+ private _onChange = (): void => {
183
+ if (!this._input.checked) return;
184
+ this.setAttribute('checked', '');
185
+ this._uncheckNamedPeers();
186
+ this.dispatchEvent(new CustomEvent<GERadioChangeDetail>('checked-change', {
187
+ detail: { checked: true, value: this.value, name: this.name },
188
+ bubbles: true,
189
+ composed: true,
190
+ }));
191
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
192
+ };
193
+
194
+ private _uncheckNamedPeers(): void {
195
+ if (!this.name) return;
196
+ const root = this.getRootNode();
197
+ const peers = root instanceof Document || root instanceof ShadowRoot
198
+ ? root.querySelectorAll<GERadio>(`ge-radio[name="${cssEscape(this.name)}"]`)
199
+ : [];
200
+ for (const peer of peers) {
201
+ if (peer === this || peer.disabled) continue;
202
+ peer.removeAttribute('checked');
203
+ }
204
+ }
205
+ }
206
+
207
+ function cssEscape(value: string): string {
208
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(value);
209
+ return value.replace(/["\\]/g, '\\$&');
210
+ }
211
+
212
+ export function defineRadioComponents(): void {
213
+ if (!customElements.get('ge-radio')) {
214
+ customElements.define('ge-radio', GERadio);
215
+ }
216
+ }
package/src/select.ts ADDED
@@ -0,0 +1,271 @@
1
+ export interface GESelectOption {
2
+ label: string;
3
+ value: string;
4
+ disabled?: boolean;
5
+ }
6
+
7
+ export interface GESelectChangeDetail {
8
+ value: string;
9
+ option: GESelectOption | null;
10
+ }
11
+
12
+ export class GESelect extends HTMLElement {
13
+ private readonly _wrap = document.createElement('div');
14
+ private readonly _search = document.createElement('input');
15
+ private readonly _select = document.createElement('select');
16
+ private _options: GESelectOption[] = [];
17
+ private _filterText = '';
18
+
19
+ static get observedAttributes(): string[] {
20
+ return ['options', 'value', 'disabled', 'searchable', 'aria-label', 'label'];
21
+ }
22
+
23
+ constructor() {
24
+ super();
25
+ const root = this.attachShadow({ mode: 'open' });
26
+ const style = document.createElement('style');
27
+ style.textContent = `
28
+ :host {
29
+ display: block;
30
+ width: 100%;
31
+ min-width: 0;
32
+ }
33
+ .wrap {
34
+ display: grid;
35
+ gap: 4px;
36
+ width: 100%;
37
+ min-width: 0;
38
+ }
39
+ input,
40
+ select {
41
+ box-sizing: border-box;
42
+ width: 100%;
43
+ height: 28px;
44
+ min-width: 0;
45
+ padding: 0 28px 0 8px;
46
+ border: 1px solid var(--ge-border-color, #303746);
47
+ border-radius: 4px;
48
+ color: var(--ge-text-color, #d8e2f2);
49
+ background-color: var(--ge-input-bg-color, var(--ge-surface-color, #121822));
50
+ font: 12px system-ui, sans-serif;
51
+ outline: none;
52
+ color-scheme: dark;
53
+ }
54
+ select {
55
+ appearance: none;
56
+ cursor: pointer;
57
+ background-image:
58
+ linear-gradient(45deg, transparent 50%, var(--ge-select-arrow-color, var(--ge-secondary-text-color, #8fa7c8)) 50%),
59
+ linear-gradient(135deg, var(--ge-select-arrow-color, var(--ge-secondary-text-color, #8fa7c8)) 50%, transparent 50%);
60
+ background-position:
61
+ calc(100% - 15px) 11px,
62
+ calc(100% - 10px) 11px;
63
+ background-size: 5px 5px, 5px 5px;
64
+ background-repeat: no-repeat;
65
+ }
66
+ select:hover:not(:disabled) {
67
+ border-color: var(--ge-hover-border-color, #435268);
68
+ background-color: var(--ge-select-hover-bg-color, var(--ge-input-bg-color, #151d2a));
69
+ }
70
+ option {
71
+ min-height: 28px;
72
+ padding: 6px 8px;
73
+ color: var(--ge-select-option-text-color, var(--ge-text-color, #d8e2f2));
74
+ background: var(--ge-select-option-bg-color, var(--ge-menu-bg-color, #1b2230));
75
+ font: 12px system-ui, sans-serif;
76
+ }
77
+ option:checked {
78
+ color: var(--ge-select-option-selected-text-color, var(--ge-selected-text-color, #ffffff));
79
+ background: var(--ge-select-option-selected-bg-color, var(--ge-selected-bg-color, #255a91));
80
+ }
81
+ option:disabled {
82
+ color: var(--ge-disabled-text-color, #65738a);
83
+ background: var(--ge-select-option-bg-color, var(--ge-menu-bg-color, #1b2230));
84
+ }
85
+ input {
86
+ display: none;
87
+ }
88
+ :host([searchable]) input {
89
+ display: block;
90
+ }
91
+ input:focus,
92
+ select:focus {
93
+ border-color: var(--ge-focus-border-color, #3d6fa8);
94
+ box-shadow: 0 0 0 2px var(--ge-focus-ring-color, rgba(61, 111, 168, 0.24));
95
+ }
96
+ input:disabled,
97
+ select:disabled {
98
+ opacity: 0.55;
99
+ cursor: default;
100
+ background-image: none;
101
+ }
102
+ `;
103
+ this._wrap.className = 'wrap';
104
+ this._search.type = 'search';
105
+ this._search.placeholder = 'Search...';
106
+ this._wrap.append(this._search, this._select);
107
+ root.append(style, this._wrap);
108
+ }
109
+
110
+ connectedCallback(): void {
111
+ this._select.addEventListener('change', this._onChange);
112
+ this._search.addEventListener('input', this._onSearchInput);
113
+ this._syncFromAttributes();
114
+ this._render();
115
+ }
116
+
117
+ disconnectedCallback(): void {
118
+ this._select.removeEventListener('change', this._onChange);
119
+ this._search.removeEventListener('input', this._onSearchInput);
120
+ this._filterText = '';
121
+ this._search.value = '';
122
+ }
123
+
124
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {
125
+ if (name === 'options') this._readOptionsAttribute();
126
+ if (name === 'value') this._select.value = newValue ?? '';
127
+ if (name === 'disabled' || name === 'searchable') this._syncDisabled();
128
+ if (name === 'aria-label' || name === 'label') this._syncAriaLabel();
129
+ if (name === 'options' || name === 'searchable') this._render();
130
+ }
131
+
132
+ get options(): GESelectOption[] {
133
+ return this._options;
134
+ }
135
+
136
+ set options(value: GESelectOption[]) {
137
+ this._options = Array.isArray(value) ? value : [];
138
+ this._render();
139
+ }
140
+
141
+ get value(): string {
142
+ return this.getAttribute('value') ?? '';
143
+ }
144
+
145
+ set value(value: string) {
146
+ this.setAttribute('value', value);
147
+ this._select.value = value;
148
+ }
149
+
150
+ get disabled(): boolean {
151
+ return this.hasAttribute('disabled');
152
+ }
153
+
154
+ set disabled(value: boolean) {
155
+ if (value) this.setAttribute('disabled', '');
156
+ else this.removeAttribute('disabled');
157
+ }
158
+
159
+ get searchable(): boolean {
160
+ return this.hasAttribute('searchable');
161
+ }
162
+
163
+ set searchable(value: boolean) {
164
+ if (value) this.setAttribute('searchable', '');
165
+ else this.removeAttribute('searchable');
166
+ }
167
+
168
+ get label(): string {
169
+ return this.getAttribute('label') ?? '';
170
+ }
171
+
172
+ set label(value: string) {
173
+ if (value) this.setAttribute('label', value);
174
+ else this.removeAttribute('label');
175
+ }
176
+
177
+ private _syncFromAttributes(): void {
178
+ // Dynamic consumers commonly assign `options` before appending the element.
179
+ // Do not erase that programmatic state merely because no attribute exists.
180
+ if (this.hasAttribute('options')) this._readOptionsAttribute();
181
+ this._select.value = this.value;
182
+ this._syncDisabled();
183
+ this._syncAriaLabel();
184
+ }
185
+
186
+ private _readOptionsAttribute(): void {
187
+ const raw = this.getAttribute('options');
188
+ if (!raw) {
189
+ this._options = [];
190
+ return;
191
+ }
192
+ try {
193
+ const parsed = JSON.parse(raw);
194
+ this._options = Array.isArray(parsed)
195
+ ? parsed.map((item) => ({
196
+ label: String(item.label ?? item.value ?? ''),
197
+ value: String(item.value ?? item.label ?? ''),
198
+ disabled: Boolean(item.disabled),
199
+ }))
200
+ : [];
201
+ } catch (error) {
202
+ console.warn('Invalid ge-select options attribute.', error);
203
+ }
204
+ }
205
+
206
+ private _render(): void {
207
+ this._select.replaceChildren();
208
+ const visibleOptions = this.searchable && this._filterText
209
+ ? this._options.filter(item => this._matchesFilter(item, this._filterText))
210
+ : this._options;
211
+ for (const item of visibleOptions) {
212
+ const option = document.createElement('option');
213
+ option.value = item.value;
214
+ option.textContent = item.label;
215
+ option.disabled = Boolean(item.disabled);
216
+ this._select.append(option);
217
+ }
218
+ this._select.value = this.value;
219
+ this._syncDisabled();
220
+ }
221
+
222
+ private _syncDisabled(): void {
223
+ this._select.disabled = this.disabled;
224
+ this._search.disabled = this.disabled;
225
+ }
226
+
227
+ private _syncAriaLabel(): void {
228
+ const label = this.getAttribute('aria-label') ?? this.getAttribute('label') ?? '';
229
+ if (label) {
230
+ this._select.setAttribute('aria-label', label);
231
+ this._search.setAttribute('aria-label', `${label} search`);
232
+ } else {
233
+ this._select.removeAttribute('aria-label');
234
+ this._search.removeAttribute('aria-label');
235
+ }
236
+ }
237
+
238
+ private _matchesFilter(option: GESelectOption, filter: string): boolean {
239
+ const haystack = `${option.label} ${option.value}`.toLowerCase();
240
+ const needle = filter.trim().toLowerCase();
241
+ if (!needle) return true;
242
+ let index = 0;
243
+ for (const char of needle) {
244
+ index = haystack.indexOf(char, index);
245
+ if (index < 0) return false;
246
+ index++;
247
+ }
248
+ return true;
249
+ }
250
+
251
+ private _onChange = (): void => {
252
+ this.value = this._select.value;
253
+ const option = this._options.find(item => item.value === this.value) ?? null;
254
+ this.dispatchEvent(new CustomEvent<GESelectChangeDetail>('value-change', {
255
+ detail: { value: this.value, option },
256
+ bubbles: true,
257
+ composed: true,
258
+ }));
259
+ };
260
+
261
+ private _onSearchInput = (): void => {
262
+ this._filterText = this._search.value;
263
+ this._render();
264
+ };
265
+ }
266
+
267
+ export function defineSelectComponents(): void {
268
+ if (!customElements.get('ge-select')) {
269
+ customElements.define('ge-select', GESelect);
270
+ }
271
+ }