nativecorejs 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 (145) hide show
  1. package/README.md +22 -0
  2. package/dist/components/builtinRegistry.d.ts +2 -0
  3. package/dist/components/builtinRegistry.js +72 -0
  4. package/dist/components/index.d.ts +59 -0
  5. package/dist/components/index.js +59 -0
  6. package/dist/components/loading-spinner.d.ts +5 -0
  7. package/dist/components/loading-spinner.js +48 -0
  8. package/dist/components/nc-a.d.ts +45 -0
  9. package/dist/components/nc-a.js +290 -0
  10. package/dist/components/nc-accordion.d.ts +36 -0
  11. package/dist/components/nc-accordion.js +186 -0
  12. package/dist/components/nc-alert.d.ts +11 -0
  13. package/dist/components/nc-alert.js +127 -0
  14. package/dist/components/nc-animation.d.ts +117 -0
  15. package/dist/components/nc-animation.js +1053 -0
  16. package/dist/components/nc-autocomplete.d.ts +41 -0
  17. package/dist/components/nc-autocomplete.js +275 -0
  18. package/dist/components/nc-avatar-group.d.ts +7 -0
  19. package/dist/components/nc-avatar-group.js +85 -0
  20. package/dist/components/nc-avatar.d.ts +9 -0
  21. package/dist/components/nc-avatar.js +127 -0
  22. package/dist/components/nc-badge.d.ts +7 -0
  23. package/dist/components/nc-badge.js +63 -0
  24. package/dist/components/nc-bottom-nav.d.ts +53 -0
  25. package/dist/components/nc-bottom-nav.js +198 -0
  26. package/dist/components/nc-breadcrumb.d.ts +10 -0
  27. package/dist/components/nc-breadcrumb.js +71 -0
  28. package/dist/components/nc-button.d.ts +38 -0
  29. package/dist/components/nc-button.js +293 -0
  30. package/dist/components/nc-card.d.ts +11 -0
  31. package/dist/components/nc-card.js +74 -0
  32. package/dist/components/nc-checkbox.d.ts +16 -0
  33. package/dist/components/nc-checkbox.js +194 -0
  34. package/dist/components/nc-chip.d.ts +8 -0
  35. package/dist/components/nc-chip.js +89 -0
  36. package/dist/components/nc-code.d.ts +37 -0
  37. package/dist/components/nc-code.js +315 -0
  38. package/dist/components/nc-collapsible.d.ts +33 -0
  39. package/dist/components/nc-collapsible.js +148 -0
  40. package/dist/components/nc-color-picker.d.ts +33 -0
  41. package/dist/components/nc-color-picker.js +265 -0
  42. package/dist/components/nc-copy-button.d.ts +10 -0
  43. package/dist/components/nc-copy-button.js +94 -0
  44. package/dist/components/nc-date-picker.d.ts +41 -0
  45. package/dist/components/nc-date-picker.js +443 -0
  46. package/dist/components/nc-div.d.ts +53 -0
  47. package/dist/components/nc-div.js +270 -0
  48. package/dist/components/nc-divider.d.ts +7 -0
  49. package/dist/components/nc-divider.js +57 -0
  50. package/dist/components/nc-drawer.d.ts +40 -0
  51. package/dist/components/nc-drawer.js +217 -0
  52. package/dist/components/nc-dropdown.d.ts +41 -0
  53. package/dist/components/nc-dropdown.js +170 -0
  54. package/dist/components/nc-empty-state.d.ts +5 -0
  55. package/dist/components/nc-empty-state.js +76 -0
  56. package/dist/components/nc-file-upload.d.ts +40 -0
  57. package/dist/components/nc-file-upload.js +336 -0
  58. package/dist/components/nc-form.d.ts +70 -0
  59. package/dist/components/nc-form.js +273 -0
  60. package/dist/components/nc-image.d.ts +10 -0
  61. package/dist/components/nc-image.js +139 -0
  62. package/dist/components/nc-input.d.ts +25 -0
  63. package/dist/components/nc-input.js +302 -0
  64. package/dist/components/nc-kbd.d.ts +5 -0
  65. package/dist/components/nc-kbd.js +34 -0
  66. package/dist/components/nc-menu-item.d.ts +43 -0
  67. package/dist/components/nc-menu-item.js +182 -0
  68. package/dist/components/nc-menu.d.ts +76 -0
  69. package/dist/components/nc-menu.js +360 -0
  70. package/dist/components/nc-modal.d.ts +51 -0
  71. package/dist/components/nc-modal.js +231 -0
  72. package/dist/components/nc-nav-item.d.ts +35 -0
  73. package/dist/components/nc-nav-item.js +142 -0
  74. package/dist/components/nc-number-input.d.ts +22 -0
  75. package/dist/components/nc-number-input.js +270 -0
  76. package/dist/components/nc-otp-input.d.ts +41 -0
  77. package/dist/components/nc-otp-input.js +227 -0
  78. package/dist/components/nc-pagination.d.ts +28 -0
  79. package/dist/components/nc-pagination.js +171 -0
  80. package/dist/components/nc-popover.d.ts +58 -0
  81. package/dist/components/nc-popover.js +301 -0
  82. package/dist/components/nc-progress-circular.d.ts +7 -0
  83. package/dist/components/nc-progress-circular.js +67 -0
  84. package/dist/components/nc-progress.d.ts +7 -0
  85. package/dist/components/nc-progress.js +109 -0
  86. package/dist/components/nc-radio.d.ts +13 -0
  87. package/dist/components/nc-radio.js +169 -0
  88. package/dist/components/nc-rating.d.ts +19 -0
  89. package/dist/components/nc-rating.js +187 -0
  90. package/dist/components/nc-rich-text.d.ts +43 -0
  91. package/dist/components/nc-rich-text.js +310 -0
  92. package/dist/components/nc-scroll-top.d.ts +28 -0
  93. package/dist/components/nc-scroll-top.js +103 -0
  94. package/dist/components/nc-select.d.ts +51 -0
  95. package/dist/components/nc-select.js +425 -0
  96. package/dist/components/nc-skeleton.d.ts +7 -0
  97. package/dist/components/nc-skeleton.js +90 -0
  98. package/dist/components/nc-slider.d.ts +41 -0
  99. package/dist/components/nc-slider.js +268 -0
  100. package/dist/components/nc-snackbar.d.ts +51 -0
  101. package/dist/components/nc-snackbar.js +200 -0
  102. package/dist/components/nc-splash.d.ts +25 -0
  103. package/dist/components/nc-splash.js +296 -0
  104. package/dist/components/nc-stepper.d.ts +50 -0
  105. package/dist/components/nc-stepper.js +236 -0
  106. package/dist/components/nc-switch.d.ts +14 -0
  107. package/dist/components/nc-switch.js +194 -0
  108. package/dist/components/nc-tab-item.d.ts +39 -0
  109. package/dist/components/nc-tab-item.js +127 -0
  110. package/dist/components/nc-table.d.ts +44 -0
  111. package/dist/components/nc-table.js +265 -0
  112. package/dist/components/nc-tabs.d.ts +79 -0
  113. package/dist/components/nc-tabs.js +519 -0
  114. package/dist/components/nc-tag-input.d.ts +49 -0
  115. package/dist/components/nc-tag-input.js +268 -0
  116. package/dist/components/nc-textarea.d.ts +15 -0
  117. package/dist/components/nc-textarea.js +164 -0
  118. package/dist/components/nc-time-picker.d.ts +51 -0
  119. package/dist/components/nc-time-picker.js +452 -0
  120. package/dist/components/nc-timeline.d.ts +53 -0
  121. package/dist/components/nc-timeline.js +171 -0
  122. package/dist/components/nc-tooltip.d.ts +27 -0
  123. package/dist/components/nc-tooltip.js +135 -0
  124. package/dist/core/component.d.ts +33 -0
  125. package/dist/core/component.js +208 -0
  126. package/dist/core/gpu-animation.d.ts +141 -0
  127. package/dist/core/gpu-animation.js +474 -0
  128. package/dist/core/lazyComponents.d.ts +13 -0
  129. package/dist/core/lazyComponents.js +73 -0
  130. package/dist/core/router.d.ts +55 -0
  131. package/dist/core/router.js +424 -0
  132. package/dist/core/state.d.ts +18 -0
  133. package/dist/core/state.js +153 -0
  134. package/dist/index.d.ts +14 -0
  135. package/dist/index.js +11 -0
  136. package/dist/utils/cacheBuster.d.ts +9 -0
  137. package/dist/utils/cacheBuster.js +12 -0
  138. package/dist/utils/dom.d.ts +16 -0
  139. package/dist/utils/dom.js +70 -0
  140. package/dist/utils/events.d.ts +20 -0
  141. package/dist/utils/events.js +80 -0
  142. package/dist/utils/templates.d.ts +2 -0
  143. package/dist/utils/templates.js +2 -0
  144. package/package.json +53 -0
  145. package/src/styles/base.css +40 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * NcScrollTop Component - floating "back to top" button
3
+ *
4
+ * Appends itself to document.body so it always sits over all content.
5
+ * Becomes visible after the user scrolls past `threshold` px.
6
+ *
7
+ * Attributes:
8
+ * threshold - scroll distance in px before appearing (default: 300)
9
+ * position - 'bottom-right'(default)|'bottom-left'|'bottom-center'
10
+ * smooth - boolean - use smooth scrolling (default: true)
11
+ * label - accessible aria-label (default: 'Back to top')
12
+ * offset - distance from screen edge in px (default: 24)
13
+ * target - optional CSS selector for the scroll container (default: window)
14
+ *
15
+ * Usage:
16
+ * <nc-scroll-top></nc-scroll-top>
17
+ */
18
+ import { Component, defineComponent } from '../core/component.js';
19
+ import { addPassiveListener } from '../core/gpu-animation.js';
20
+ import { dom } from '../utils/dom.js';
21
+ export class NcScrollTop extends Component {
22
+ static useShadowDOM = true;
23
+ _visible = false;
24
+ _removeScroll = null;
25
+ _scrollTarget = window;
26
+ template() {
27
+ const pos = this.getAttribute('position') ?? 'bottom-right';
28
+ const offset = parseInt(this.getAttribute('offset') ?? '24', 10);
29
+ const label = this.getAttribute('label') ?? 'Back to top';
30
+ const v = this._visible;
31
+ const posStyle = pos === 'bottom-left' ? `left:${offset}px;right:auto;` :
32
+ pos === 'bottom-center' ? `left:50%;transform:translateX(-50%);` :
33
+ `right:${offset}px;left:auto;`;
34
+ return `
35
+ <style>
36
+ :host { display: contents; }
37
+ button {
38
+ position: fixed;
39
+ bottom: ${offset}px;
40
+ ${posStyle}
41
+ z-index: 900;
42
+ width: 44px;
43
+ height: 44px;
44
+ border-radius: 50%;
45
+ background: var(--nc-primary);
46
+ color: var(--nc-white);
47
+ border: none;
48
+ cursor: pointer;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ box-shadow: var(--nc-shadow-md);
53
+ opacity: ${v ? '1' : '0'};
54
+ visibility: ${v ? 'visible' : 'hidden'};
55
+ transform: ${v ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.9)'};
56
+ pointer-events: ${v ? 'auto' : 'none'};
57
+ transition:
58
+ opacity var(--nc-transition-base),
59
+ transform var(--nc-transition-base);
60
+ outline: none;
61
+ }
62
+ button:hover { opacity: 0.85; }
63
+ button:active { transform: scale(0.94); }
64
+ button:focus-visible { outline: 2px solid var(--nc-primary); outline-offset: 3px; }
65
+ </style>
66
+ <button type="button" aria-label="${label}" tabindex="${v ? '0' : '-1'}">
67
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
68
+ fill="none" stroke="currentColor" stroke-width="2.5"
69
+ stroke-linecap="round" stroke-linejoin="round">
70
+ <polyline points="18 15 12 9 6 15"/>
71
+ </svg>
72
+ </button>
73
+ `;
74
+ }
75
+ onMount() {
76
+ const threshold = parseInt(this.getAttribute('threshold') ?? '300', 10);
77
+ const targetSelector = this.getAttribute('target');
78
+ this._scrollTarget = targetSelector
79
+ ? dom.query(targetSelector) ?? window
80
+ : window;
81
+ const updateVisibility = () => {
82
+ const currentScroll = this._scrollTarget instanceof Window
83
+ ? this._scrollTarget.scrollY
84
+ : this._scrollTarget.scrollTop;
85
+ const shouldShow = currentScroll > threshold;
86
+ if (shouldShow !== this._visible) {
87
+ this._visible = shouldShow;
88
+ this.render();
89
+ }
90
+ };
91
+ this._removeScroll = addPassiveListener(this._scrollTarget, 'scroll', updateVisibility);
92
+ updateVisibility();
93
+ this.shadowRoot.addEventListener('click', () => this._scrollTop());
94
+ }
95
+ _scrollTop() {
96
+ const smooth = this.getAttribute('smooth') !== 'false';
97
+ this._scrollTarget.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
98
+ }
99
+ onUnmount() {
100
+ this._removeScroll?.();
101
+ }
102
+ }
103
+ defineComponent('nc-scroll-top', NcScrollTop);
@@ -0,0 +1,51 @@
1
+ /**
2
+ * NcSelect Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Options are provided as a JSON array via the `options` attribute or by
7
+ * populating child `<option>` elements before the component mounts.
8
+ *
9
+ * Attributes:
10
+ * - options: JSON string - array of { value, label, disabled? }
11
+ * - value: string - currently selected value
12
+ * - placeholder: string - shown when no value selected (default: 'Select...')
13
+ * - name: string - form field name
14
+ * - disabled: boolean - disabled state
15
+ * - size: 'sm' | 'md' | 'lg' (default: 'md')
16
+ * - variant: 'default' | 'filled' (default: 'default')
17
+ * - searchable: boolean - adds a live filter input inside the dropdown
18
+ *
19
+ * Events:
20
+ * - change: CustomEvent<{ value: string; label: string; name: string }>
21
+ *
22
+ * Usage:
23
+ * <nc-select
24
+ * name="country"
25
+ * placeholder="Pick a country"
26
+ * options='[{"value":"us","label":"United States"},{"value":"ca","label":"Canada"}]'>
27
+ * </nc-select>
28
+ */
29
+ import { Component } from '../core/component.js';
30
+ export declare class NcSelect extends Component {
31
+ static useShadowDOM: boolean;
32
+ static attributeOptions: {
33
+ variant: string[];
34
+ size: string[];
35
+ };
36
+ static get observedAttributes(): string[];
37
+ private _open;
38
+ private _filterText;
39
+ constructor();
40
+ private _getOptions;
41
+ private _getSelectedLabel;
42
+ template(): string;
43
+ onMount(): void;
44
+ private _onOutsideClick;
45
+ private _setOpen;
46
+ private _rerenderDropdown;
47
+ private _select;
48
+ private _navigateOptions;
49
+ onUnmount(): void;
50
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
51
+ }
@@ -0,0 +1,425 @@
1
+ /**
2
+ * NcSelect Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Options are provided as a JSON array via the `options` attribute or by
7
+ * populating child `<option>` elements before the component mounts.
8
+ *
9
+ * Attributes:
10
+ * - options: JSON string - array of { value, label, disabled? }
11
+ * - value: string - currently selected value
12
+ * - placeholder: string - shown when no value selected (default: 'Select...')
13
+ * - name: string - form field name
14
+ * - disabled: boolean - disabled state
15
+ * - size: 'sm' | 'md' | 'lg' (default: 'md')
16
+ * - variant: 'default' | 'filled' (default: 'default')
17
+ * - searchable: boolean - adds a live filter input inside the dropdown
18
+ *
19
+ * Events:
20
+ * - change: CustomEvent<{ value: string; label: string; name: string }>
21
+ *
22
+ * Usage:
23
+ * <nc-select
24
+ * name="country"
25
+ * placeholder="Pick a country"
26
+ * options='[{"value":"us","label":"United States"},{"value":"ca","label":"Canada"}]'>
27
+ * </nc-select>
28
+ */
29
+ import { Component, defineComponent } from '../core/component.js';
30
+ export class NcSelect extends Component {
31
+ static useShadowDOM = true;
32
+ static attributeOptions = {
33
+ variant: ['default', 'filled'],
34
+ size: ['sm', 'md', 'lg']
35
+ };
36
+ static get observedAttributes() {
37
+ return ['options', 'value', 'placeholder', 'name', 'disabled', 'size', 'variant', 'searchable'];
38
+ }
39
+ _open = false;
40
+ _filterText = '';
41
+ constructor() {
42
+ super();
43
+ }
44
+ _getOptions() {
45
+ try {
46
+ const raw = this.getAttribute('options');
47
+ if (raw)
48
+ return JSON.parse(raw);
49
+ }
50
+ catch {
51
+ // fall through
52
+ }
53
+ return [];
54
+ }
55
+ _getSelectedLabel() {
56
+ const value = this.getAttribute('value') || '';
57
+ if (!value)
58
+ return '';
59
+ const opt = this._getOptions().find(o => o.value === value);
60
+ return opt?.label ?? value;
61
+ }
62
+ template() {
63
+ const value = this.getAttribute('value') || '';
64
+ const placeholder = this.getAttribute('placeholder') || 'Select...';
65
+ const disabled = this.hasAttribute('disabled');
66
+ const searchable = this.hasAttribute('searchable');
67
+ const selectedLabel = this._getSelectedLabel() || placeholder;
68
+ const hasValue = !!value;
69
+ const options = this._getOptions();
70
+ const filtered = this._filterText
71
+ ? options.filter(o => o.label.toLowerCase().includes(this._filterText.toLowerCase()))
72
+ : options;
73
+ const optionItems = filtered.map(o => `
74
+ <div class="option${o.value === value ? ' option--selected' : ''}${o.disabled ? ' option--disabled' : ''}"
75
+ data-value="${o.value}"
76
+ role="option"
77
+ aria-selected="${o.value === value}"
78
+ aria-disabled="${o.disabled ? 'true' : 'false'}">
79
+ ${o.label}
80
+ ${o.value === value ? `
81
+ <svg class="option__check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
82
+ <path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
83
+ </svg>` : ''}
84
+ </div>
85
+ `).join('');
86
+ return `
87
+ <style>
88
+ :host {
89
+ display: inline-block;
90
+ position: relative;
91
+ font-family: var(--nc-font-family);
92
+ width: 100%;
93
+ }
94
+
95
+ .select-trigger {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ width: 100%;
100
+ box-sizing: border-box;
101
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
102
+ background: var(--nc-bg);
103
+ border: var(--nc-input-border);
104
+ border-radius: var(--nc-input-radius);
105
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
106
+ color: ${hasValue ? 'var(--nc-text)' : 'var(--nc-text-muted)'};
107
+ font-size: var(--nc-font-size-base);
108
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
109
+ opacity: ${disabled ? '0.5' : '1'};
110
+ user-select: none;
111
+ gap: var(--nc-spacing-sm);
112
+ min-height: 40px;
113
+ }
114
+
115
+ /* Size variants */
116
+ :host([size="sm"]) .select-trigger {
117
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
118
+ font-size: var(--nc-font-size-sm);
119
+ min-height: 32px;
120
+ }
121
+
122
+ :host([size="lg"]) .select-trigger {
123
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
124
+ font-size: var(--nc-font-size-lg);
125
+ min-height: 48px;
126
+ }
127
+
128
+ /* Filled variant */
129
+ :host([variant="filled"]) .select-trigger {
130
+ background: var(--nc-bg-tertiary);
131
+ border-color: transparent;
132
+ }
133
+
134
+ :host([variant="filled"]) .select-trigger:hover:not([disabled]) {
135
+ background: var(--nc-bg-secondary);
136
+ }
137
+
138
+ .select-trigger:hover {
139
+ border-color: var(--nc-input-focus-border);
140
+ }
141
+
142
+ .select-trigger.open {
143
+ border-color: var(--nc-input-focus-border);
144
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
145
+ }
146
+
147
+ .trigger-label {
148
+ flex: 1;
149
+ overflow: hidden;
150
+ white-space: nowrap;
151
+ text-overflow: ellipsis;
152
+ }
153
+
154
+ .chevron {
155
+ flex-shrink: 0;
156
+ transition: transform var(--nc-transition-fast);
157
+ color: var(--nc-text-muted);
158
+ }
159
+
160
+ .chevron.open {
161
+ transform: rotate(180deg);
162
+ }
163
+
164
+ /* Dropdown */
165
+ .dropdown {
166
+ display: none;
167
+ position: absolute;
168
+ top: calc(100% + 4px);
169
+ left: 0;
170
+ right: 0;
171
+ background: var(--nc-bg);
172
+ border: var(--nc-input-border);
173
+ border-radius: var(--nc-radius-md);
174
+ box-shadow: var(--nc-shadow-lg);
175
+ z-index: var(--nc-z-dropdown);
176
+ overflow: hidden;
177
+ max-height: 240px;
178
+ flex-direction: column;
179
+ }
180
+
181
+ .dropdown.open {
182
+ display: flex;
183
+ }
184
+
185
+ .search-wrap {
186
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
187
+ border-bottom: 1px solid var(--nc-border);
188
+ }
189
+
190
+ .search-input {
191
+ width: 100%;
192
+ box-sizing: border-box;
193
+ border: var(--nc-input-border);
194
+ border-radius: var(--nc-radius-sm);
195
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
196
+ font-size: var(--nc-font-size-sm);
197
+ font-family: var(--nc-font-family);
198
+ color: var(--nc-text);
199
+ background: var(--nc-bg-secondary);
200
+ outline: none;
201
+ }
202
+
203
+ .search-input:focus {
204
+ border-color: var(--nc-input-focus-border);
205
+ }
206
+
207
+ .options-list {
208
+ overflow-y: auto;
209
+ flex: 1;
210
+ }
211
+
212
+ .option {
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: space-between;
216
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
217
+ cursor: pointer;
218
+ font-size: var(--nc-font-size-base);
219
+ color: var(--nc-text);
220
+ transition: background var(--nc-transition-fast);
221
+ gap: var(--nc-spacing-sm);
222
+ }
223
+
224
+ .option:hover:not(.option--disabled) {
225
+ background: var(--nc-bg-secondary);
226
+ }
227
+
228
+ .option--selected {
229
+ color: var(--nc-primary);
230
+ font-weight: var(--nc-font-weight-medium);
231
+ }
232
+
233
+ .option--disabled {
234
+ opacity: 0.4;
235
+ cursor: not-allowed;
236
+ }
237
+
238
+ .option__check {
239
+ flex-shrink: 0;
240
+ }
241
+
242
+ .empty {
243
+ padding: var(--nc-spacing-md);
244
+ text-align: center;
245
+ color: var(--nc-text-muted);
246
+ font-size: var(--nc-font-size-sm);
247
+ }
248
+ </style>
249
+
250
+ <input type="hidden"
251
+ name="${this.getAttribute('name') || ''}"
252
+ value="${value}"
253
+ />
254
+
255
+ <div class="select-trigger${this._open ? ' open' : ''}" role="combobox" aria-expanded="${this._open}" aria-haspopup="listbox">
256
+ <span class="trigger-label">${selectedLabel}</span>
257
+ <svg class="chevron${this._open ? ' open' : ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="16" height="16">
258
+ <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
259
+ </svg>
260
+ </div>
261
+
262
+ <div class="dropdown${this._open ? ' open' : ''}" role="listbox">
263
+ ${searchable ? `
264
+ <div class="search-wrap">
265
+ <input class="search-input" type="text" placeholder="Search..." value="${this._filterText}" autocomplete="off" />
266
+ </div>` : ''}
267
+ <div class="options-list">
268
+ ${optionItems || `<div class="empty">No options</div>`}
269
+ </div>
270
+ </div>
271
+ `;
272
+ }
273
+ onMount() {
274
+ if (!this.hasAttribute('tabindex')) {
275
+ this.setAttribute('tabindex', '0');
276
+ }
277
+ const sr = this.shadowRoot;
278
+ // Single click listener - never re-added
279
+ sr.addEventListener('click', (e) => {
280
+ if (this.hasAttribute('disabled'))
281
+ return;
282
+ const target = e.target;
283
+ const option = target.closest('.option');
284
+ if (option) {
285
+ if (option.classList.contains('option--disabled'))
286
+ return;
287
+ this._select(option.dataset.value ?? '');
288
+ return;
289
+ }
290
+ if (target.closest('.select-trigger')) {
291
+ this._setOpen(!this._open);
292
+ }
293
+ });
294
+ // Search filter
295
+ sr.addEventListener('input', (e) => {
296
+ const input = e.target;
297
+ if (input.classList.contains('search-input')) {
298
+ this._filterText = input.value;
299
+ this._rerenderDropdown();
300
+ }
301
+ });
302
+ // Outside click - registered once, cleaned up in onUnmount
303
+ document.addEventListener('click', this._onOutsideClick);
304
+ // Keyboard
305
+ this.addEventListener('keydown', (e) => {
306
+ if (e.key === 'Escape') {
307
+ this._setOpen(false);
308
+ return;
309
+ }
310
+ if (e.key === 'Enter' || e.key === ' ') {
311
+ e.preventDefault();
312
+ this._setOpen(!this._open);
313
+ }
314
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
315
+ e.preventDefault();
316
+ this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);
317
+ }
318
+ });
319
+ }
320
+ _onOutsideClick = (e) => {
321
+ if (!this.contains(e.target) && !this.shadowRoot.contains(e.target)) {
322
+ this._setOpen(false);
323
+ }
324
+ };
325
+ _setOpen(open) {
326
+ this._open = open;
327
+ if (!open)
328
+ this._filterText = '';
329
+ const sr = this.shadowRoot;
330
+ const trigger = sr.querySelector('.select-trigger');
331
+ const chevron = sr.querySelector('.chevron');
332
+ const dropdown = sr.querySelector('.dropdown');
333
+ if (trigger) {
334
+ trigger.classList.toggle('open', open);
335
+ trigger.setAttribute('aria-expanded', String(open));
336
+ }
337
+ if (chevron)
338
+ chevron.classList.toggle('open', open);
339
+ if (dropdown) {
340
+ dropdown.classList.toggle('open', open);
341
+ if (!open) {
342
+ // Clear search when closing
343
+ const searchInput = dropdown.querySelector('.search-input');
344
+ if (searchInput)
345
+ searchInput.value = '';
346
+ this._rerenderDropdown();
347
+ }
348
+ }
349
+ if (open) {
350
+ const search = sr.querySelector('.search-input');
351
+ if (search)
352
+ search.focus();
353
+ }
354
+ }
355
+ _rerenderDropdown() {
356
+ const sr = this.shadowRoot;
357
+ const list = sr.querySelector('.options-list');
358
+ if (!list)
359
+ return;
360
+ const value = this.getAttribute('value') || '';
361
+ const options = this._getOptions();
362
+ const filtered = this._filterText
363
+ ? options.filter(o => o.label.toLowerCase().includes(this._filterText.toLowerCase()))
364
+ : options;
365
+ list.innerHTML = filtered.length ? filtered.map(o => `
366
+ <div class="option${o.value === value ? ' option--selected' : ''}${o.disabled ? ' option--disabled' : ''}"
367
+ data-value="${o.value}"
368
+ role="option"
369
+ aria-selected="${o.value === value}"
370
+ aria-disabled="${o.disabled ? 'true' : 'false'}">
371
+ ${o.label}
372
+ ${o.value === value ? `
373
+ <svg class="option__check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
374
+ <path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
375
+ </svg>` : ''}
376
+ </div>
377
+ `).join('') : '<div class="empty">No results</div>';
378
+ }
379
+ _select(value) {
380
+ const opts = this._getOptions();
381
+ const opt = opts.find(o => o.value === value);
382
+ if (!opt)
383
+ return;
384
+ this._open = false;
385
+ this._filterText = '';
386
+ // Update trigger label and hidden input directly
387
+ const sr = this.shadowRoot;
388
+ const label = sr.querySelector('.trigger-label');
389
+ if (label)
390
+ label.textContent = opt.label;
391
+ const hidden = sr.querySelector('input[type="hidden"]');
392
+ if (hidden)
393
+ hidden.value = value;
394
+ // Close the dropdown
395
+ this._setOpen(false);
396
+ // Re-render option list to show the new checkmark
397
+ this.setAttribute('value', value);
398
+ this.dispatchEvent(new CustomEvent('change', {
399
+ bubbles: true,
400
+ composed: true,
401
+ detail: {
402
+ value,
403
+ label: opt.label,
404
+ name: this.getAttribute('name') || ''
405
+ }
406
+ }));
407
+ }
408
+ _navigateOptions(direction) {
409
+ const opts = this._getOptions().filter(o => !o.disabled);
410
+ const current = this.getAttribute('value') || '';
411
+ const idx = opts.findIndex(o => o.value === current);
412
+ const next = opts[Math.max(0, Math.min(opts.length - 1, idx + direction))];
413
+ if (next)
414
+ this._select(next.value);
415
+ }
416
+ onUnmount() {
417
+ document.removeEventListener('click', this._onOutsideClick);
418
+ }
419
+ attributeChangedCallback(name, oldValue, newValue) {
420
+ if (oldValue !== newValue && this._mounted) {
421
+ this.render();
422
+ }
423
+ }
424
+ }
425
+ defineComponent('nc-select', NcSelect);
@@ -0,0 +1,7 @@
1
+ import { Component } from '../core/component.js';
2
+ export declare class NcSkeleton extends Component {
3
+ static useShadowDOM: boolean;
4
+ static get observedAttributes(): string[];
5
+ template(): string;
6
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
7
+ }
@@ -0,0 +1,90 @@
1
+ import { Component, defineComponent } from '../core/component.js';
2
+ export class NcSkeleton extends Component {
3
+ static useShadowDOM = true;
4
+ static get observedAttributes() {
5
+ return ['variant', 'width', 'height', 'lines', 'animate'];
6
+ }
7
+ template() {
8
+ const variant = this.getAttribute('variant') || 'text';
9
+ const width = this.getAttribute('width') || '100%';
10
+ const height = this.getAttribute('height') || '';
11
+ const lines = Math.max(1, Number(this.getAttribute('lines') || 1));
12
+ const animate = this.getAttribute('animate') ?? 'wave';
13
+ const baseHeight = height || (variant === 'text'
14
+ ? '0.875em'
15
+ : variant === 'rect'
16
+ ? '120px'
17
+ : variant === 'circle'
18
+ ? width
19
+ : '160px');
20
+ let content = '';
21
+ if (variant === 'text') {
22
+ content = Array.from({ length: lines }, (_, index) => {
23
+ const lineWidth = index === lines - 1 && lines > 1 ? '75%' : '100%';
24
+ return `<span class="bone bone--text" style="width:${lineWidth}"></span>`;
25
+ }).join('');
26
+ }
27
+ else if (variant === 'circle') {
28
+ content = `<span class="bone bone--circle" style="width:${width};height:${width}"></span>`;
29
+ }
30
+ else if (variant === 'card') {
31
+ content = `
32
+ <span class="bone bone--rect" style="height:120px;margin-bottom:12px"></span>
33
+ <span class="bone bone--text" style="width:60%;margin-bottom:8px"></span>
34
+ <span class="bone bone--text" style="width:90%;margin-bottom:8px"></span>
35
+ <span class="bone bone--text" style="width:75%"></span>`;
36
+ }
37
+ else {
38
+ content = `<span class="bone bone--rect" style="height:${baseHeight}"></span>`;
39
+ }
40
+ return `
41
+ <style>
42
+ :host { display: block; width: ${variant === 'circle' ? 'auto' : width}; }
43
+
44
+ .skeleton {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: 6px;
48
+ }
49
+
50
+ .bone {
51
+ display: block;
52
+ background: var(--nc-bg-tertiary, #e5e7eb);
53
+ border-radius: 4px;
54
+ position: relative;
55
+ overflow: hidden;
56
+ }
57
+
58
+ .bone--text { height: ${baseHeight}; border-radius: 3px; }
59
+ .bone--circle { border-radius: 50%; flex-shrink: 0; }
60
+ .bone--rect { border-radius: var(--nc-radius-md, 8px); width: 100%; }
61
+
62
+ ${animate === 'wave' ? `
63
+ @keyframes nc-skeleton-wave {
64
+ 0% { transform: translateX(-100%); }
65
+ 100% { transform: translateX(100%); }
66
+ }
67
+ .bone::after {
68
+ content: '';
69
+ position: absolute;
70
+ inset: 0;
71
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.08) 50%, transparent 100%);
72
+ animation: nc-skeleton-wave 1.6s ease-in-out infinite;
73
+ }` : ''}
74
+
75
+ ${animate === 'pulse' ? `
76
+ @keyframes nc-skeleton-pulse {
77
+ 0%, 100% { opacity: 1; }
78
+ 50% { opacity: 0.4; }
79
+ }
80
+ .bone { animation: nc-skeleton-pulse 1.8s ease-in-out infinite; }` : ''}
81
+ </style>
82
+ <div class="skeleton" aria-hidden="true">${content}</div>
83
+ `;
84
+ }
85
+ attributeChangedCallback(name, oldValue, newValue) {
86
+ if (oldValue !== newValue && this._mounted)
87
+ this.render();
88
+ }
89
+ }
90
+ defineComponent('nc-skeleton', NcSkeleton);