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,360 @@
1
+ /**
2
+ * NativeCore Menu Component (nc-menu)
3
+ *
4
+ * A vertical command menu. Place nc-menu-item elements inside as direct children.
5
+ * Supports grouped items (via nc-menu-divider or label attribute), a searchable
6
+ * filter box, and several visual variants.
7
+ *
8
+ * Attributes:
9
+ * variant - 'default' | 'compact' | 'inset' | 'bordered' (default: 'default')
10
+ * searchable - Boolean. Adds a filter input at the top that hides non-matching items.
11
+ * label - Optional header label shown above the items.
12
+ * width - CSS width value, e.g. '220px'. Defaults to 'fit-content'.
13
+ * auto-active - Boolean. Automatically moves the `active` attribute to whichever
14
+ * item was last selected or navigated to. For nc-a items, also
15
+ * matches the current path on mount.
16
+ *
17
+ * Slots:
18
+ * default - nc-menu-item (and nc-menu-divider) elements.
19
+ *
20
+ * Events emitted:
21
+ * nc-menu-select - { item: HTMLElement, label: string } - fires when any
22
+ * nc-menu-item inside emits nc-select.
23
+ *
24
+ * Keyboard:
25
+ * ArrowDown / ArrowUp - move focus between items.
26
+ * Home / End - jump to first / last item.
27
+ * Escape - blur the menu.
28
+ *
29
+ * Usage:
30
+ * <nc-menu label="Actions">
31
+ * <nc-menu-item icon="/icons/edit.svg">Edit</nc-menu-item>
32
+ * <nc-menu-item icon="/icons/copy.svg">Duplicate</nc-menu-item>
33
+ * <nc-menu-item danger icon="/icons/trash.svg">Delete</nc-menu-item>
34
+ * </nc-menu>
35
+ *
36
+ * <nc-menu searchable variant="bordered" width="260px">
37
+ * <nc-menu-item>Dashboard</nc-menu-item>
38
+ * <nc-menu-item active>Components</nc-menu-item>
39
+ * <nc-menu-item>Settings</nc-menu-item>
40
+ * </nc-menu>
41
+ *
42
+ * <!-- Navigation sidebar: nc-a items, active managed automatically -->
43
+ * <nc-menu auto-active variant="inset" width="220px">
44
+ * <nc-a href="/dashboard" variant="ghost">Dashboard</nc-a>
45
+ * <nc-a href="/components" variant="ghost">Components</nc-a>
46
+ * <nc-a href="/settings" variant="ghost">Settings</nc-a>
47
+ * </nc-menu>
48
+ */
49
+ import { Component, defineComponent } from '../core/component.js';
50
+ import { html } from '../utils/templates.js';
51
+ export class NcMenu extends Component {
52
+ static useShadowDOM = true;
53
+ static attributeOptions = {
54
+ variant: ['default', 'compact', 'inset', 'bordered'],
55
+ };
56
+ static get observedAttributes() {
57
+ return ['variant', 'searchable', 'label', 'width', 'auto-active'];
58
+ }
59
+ _onSlotChange = null;
60
+ _onSelect = null;
61
+ _onNavigate = null;
62
+ _onKeydown = null;
63
+ _onSearchInput = null;
64
+ template() {
65
+ const label = this.getAttribute('label') || '';
66
+ const searchable = this.hasAttribute('searchable');
67
+ const width = this.getAttribute('width') || 'fit-content';
68
+ const labelHTML = label
69
+ ? `<div class="menu__label">${label}</div>`
70
+ : '';
71
+ const searchHTML = searchable
72
+ ? `<div class="menu__search-wrap">
73
+ <svg class="menu__search-icon" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
74
+ <circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" stroke-width="1.5"/>
75
+ <path d="M15 15l-3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
76
+ </svg>
77
+ <input class="menu__search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false" />
78
+ </div>`
79
+ : '';
80
+ return html `
81
+ <style>
82
+ :host {
83
+ display: inline-block;
84
+ width: ${width};
85
+ font-family: var(--nc-font-family);
86
+ box-sizing: border-box;
87
+ }
88
+
89
+ .menu {
90
+ background: var(--nc-bg);
91
+ border-radius: var(--nc-radius-lg);
92
+ padding: var(--nc-spacing-xs);
93
+ min-width: 180px;
94
+ box-sizing: border-box;
95
+ width: 100%;
96
+ }
97
+
98
+ /* ── Variants ──────────────────────────────────────────────── */
99
+ :host([variant="default"]) .menu,
100
+ :host(:not([variant])) .menu {
101
+ background: var(--nc-bg);
102
+ padding: var(--nc-spacing-xs);
103
+ }
104
+
105
+ :host([variant="compact"]) .menu {
106
+ padding: 2px;
107
+ }
108
+
109
+ :host([variant="compact"]) ::slotted(nc-menu-item) {
110
+ --nc-menu-item-py: var(--nc-spacing-xs);
111
+ }
112
+
113
+ :host([variant="inset"]) .menu {
114
+ background: var(--nc-bg-secondary);
115
+ padding: var(--nc-spacing-sm);
116
+ border-radius: var(--nc-radius-xl);
117
+ }
118
+
119
+ :host([variant="bordered"]) .menu {
120
+ background: var(--nc-bg);
121
+ border: 1px solid var(--nc-border);
122
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
123
+ padding: var(--nc-spacing-xs);
124
+ }
125
+
126
+ /* ── Label ─────────────────────────────────────────────────── */
127
+ .menu__label {
128
+ font-size: var(--nc-font-size-xs, 0.7rem);
129
+ font-weight: 700;
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.08em;
132
+ color: var(--nc-text-secondary);
133
+ padding: var(--nc-spacing-xs) var(--nc-spacing-md) var(--nc-spacing-xs);
134
+ margin-bottom: 2px;
135
+ }
136
+
137
+ /* ── Search ────────────────────────────────────────────────── */
138
+ .menu__search-wrap {
139
+ position: relative;
140
+ margin-bottom: var(--nc-spacing-xs);
141
+ }
142
+
143
+ .menu__search-icon {
144
+ position: absolute;
145
+ left: 10px;
146
+ top: 50%;
147
+ transform: translateY(-50%);
148
+ width: 14px;
149
+ height: 14px;
150
+ color: var(--nc-text-secondary);
151
+ pointer-events: none;
152
+ }
153
+
154
+ .menu__search {
155
+ width: 100%;
156
+ box-sizing: border-box;
157
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm) var(--nc-spacing-xs) 30px;
158
+ font-family: var(--nc-font-family);
159
+ font-size: var(--nc-font-size-sm);
160
+ background: var(--nc-bg-secondary);
161
+ border: 1px solid var(--nc-border);
162
+ border-radius: var(--nc-radius-md);
163
+ color: var(--nc-text);
164
+ outline: none;
165
+ transition: border-color var(--nc-transition-fast);
166
+ }
167
+
168
+ .menu__search:focus {
169
+ border-color: var(--nc-primary);
170
+ }
171
+
172
+ .menu__search::placeholder {
173
+ color: var(--nc-text-secondary);
174
+ opacity: 0.6;
175
+ }
176
+
177
+ /* ── Empty state ───────────────────────────────────────────── */
178
+ .menu__empty {
179
+ display: none;
180
+ font-size: var(--nc-font-size-sm);
181
+ color: var(--nc-text-secondary);
182
+ padding: var(--nc-spacing-md);
183
+ text-align: center;
184
+ opacity: 0.6;
185
+ }
186
+
187
+ .menu__empty.visible {
188
+ display: block;
189
+ }
190
+ </style>
191
+ <div class="menu" role="menu">
192
+ ${labelHTML}
193
+ ${searchHTML}
194
+ <slot></slot>
195
+ <div class="menu__empty">No results</div>
196
+ </div>
197
+ `;
198
+ }
199
+ onMount() {
200
+ // Delegate nc-select events from nc-menu-item children
201
+ this._onSelect = (e) => {
202
+ const item = e.target;
203
+ if (item.tagName.toLowerCase() !== 'nc-menu-item')
204
+ return;
205
+ const label = item.textContent?.trim() ?? '';
206
+ this.emitEvent('nc-menu-select', { item, label });
207
+ // Always track active for nc-menu-item - no opt-in required
208
+ this._setActive(item);
209
+ };
210
+ this.addEventListener('nc-select', this._onSelect);
211
+ // Delegate nc-navigate events from nc-a children
212
+ this._onNavigate = (e) => {
213
+ const item = e.target;
214
+ if (item.tagName.toLowerCase() !== 'nc-a')
215
+ return;
216
+ if (this.hasAttribute('auto-active'))
217
+ this._setActive(item);
218
+ };
219
+ this.addEventListener('nc-navigate', this._onNavigate);
220
+ // Arrow key navigation (supports both nc-menu-item and nc-a)
221
+ this._onKeydown = (e) => {
222
+ const ke = e;
223
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'].includes(ke.key))
224
+ return;
225
+ ke.preventDefault();
226
+ const items = this._getEnabledItems();
227
+ if (!items.length)
228
+ return;
229
+ const focused = document.activeElement;
230
+ const idx = items.findIndex(el => el === focused || el.shadowRoot?.contains(focused));
231
+ if (ke.key === 'Escape') {
232
+ focused?.blur?.();
233
+ return;
234
+ }
235
+ if (ke.key === 'Home') {
236
+ this._focusItem(items[0]);
237
+ return;
238
+ }
239
+ if (ke.key === 'End') {
240
+ this._focusItem(items[items.length - 1]);
241
+ return;
242
+ }
243
+ const next = ke.key === 'ArrowDown'
244
+ ? items[Math.min(idx + 1, items.length - 1)]
245
+ : items[Math.max(idx - 1, 0)];
246
+ this._focusItem(next);
247
+ };
248
+ this.addEventListener('keydown', this._onKeydown);
249
+ // Auto-active: match current path for nc-a items on mount
250
+ if (this.hasAttribute('auto-active')) {
251
+ Promise.resolve().then(() => this._syncActiveFromPath());
252
+ }
253
+ // Search filtering
254
+ if (this.hasAttribute('searchable')) {
255
+ this._attachSearch();
256
+ }
257
+ // Re-sync search + path when slot contents change
258
+ const slot = this.$('slot');
259
+ if (slot) {
260
+ this._onSlotChange = () => {
261
+ if (this.hasAttribute('searchable'))
262
+ this._filterItems('');
263
+ if (this.hasAttribute('auto-active'))
264
+ this._syncActiveFromPath();
265
+ };
266
+ slot.addEventListener('slotchange', this._onSlotChange);
267
+ }
268
+ }
269
+ onUnmount() {
270
+ if (this._onSelect)
271
+ this.removeEventListener('nc-select', this._onSelect);
272
+ if (this._onNavigate)
273
+ this.removeEventListener('nc-navigate', this._onNavigate);
274
+ if (this._onKeydown)
275
+ this.removeEventListener('keydown', this._onKeydown);
276
+ const slot = this.$('slot');
277
+ if (slot && this._onSlotChange)
278
+ slot.removeEventListener('slotchange', this._onSlotChange);
279
+ this._onSelect = null;
280
+ this._onNavigate = null;
281
+ this._onKeydown = null;
282
+ this._onSlotChange = null;
283
+ this._onSearchInput = null;
284
+ }
285
+ attributeChangedCallback(_name, oldValue, newValue) {
286
+ if (this._mounted && oldValue !== newValue)
287
+ this.render();
288
+ }
289
+ // ─── Private ────────────────────────────────────────────────────────────────
290
+ _getEnabledItems() {
291
+ return Array.from(this.querySelectorAll('nc-menu-item:not([disabled]), nc-a:not([disabled])'));
292
+ }
293
+ _focusItem(item) {
294
+ const tag = item.tagName.toLowerCase();
295
+ const selector = tag === 'nc-a' ? 'a' : '[role="menuitem"]';
296
+ item.shadowRoot?.querySelector(selector)?.focus();
297
+ }
298
+ /** Moves `active` to `target`, removes it from all siblings. */
299
+ _setActive(target) {
300
+ const all = Array.from(this.querySelectorAll('nc-menu-item, nc-a'));
301
+ all.forEach(el => {
302
+ if (el === target) {
303
+ el.setAttribute('active', '');
304
+ }
305
+ else {
306
+ el.removeAttribute('active');
307
+ }
308
+ });
309
+ }
310
+ /**
311
+ * For nc-a items with auto-active: set active on the item whose href
312
+ * matches the current pathname. Handles exact and prefix matches.
313
+ */
314
+ _syncActiveFromPath() {
315
+ const path = window.location.pathname;
316
+ const links = Array.from(this.querySelectorAll('nc-a[href]'));
317
+ if (!links.length)
318
+ return;
319
+ // Prefer exact match, fall back to longest prefix
320
+ let best = null;
321
+ let bestLen = 0;
322
+ links.forEach(link => {
323
+ const href = link.getAttribute('href') ?? '';
324
+ if (href === path) {
325
+ best = link;
326
+ bestLen = Infinity;
327
+ return;
328
+ }
329
+ if (bestLen < Infinity && path.startsWith(href) && href.length > bestLen) {
330
+ best = link;
331
+ bestLen = href.length;
332
+ }
333
+ });
334
+ if (best)
335
+ this._setActive(best);
336
+ }
337
+ _attachSearch() {
338
+ const input = this.$('.menu__search');
339
+ if (!input)
340
+ return;
341
+ this._onSearchInput = () => this._filterItems(input.value);
342
+ input.addEventListener('input', this._onSearchInput);
343
+ }
344
+ _filterItems(query) {
345
+ const q = query.toLowerCase().trim();
346
+ const items = Array.from(this.querySelectorAll('nc-menu-item, nc-a'));
347
+ let visible = 0;
348
+ items.forEach(item => {
349
+ const text = (item.textContent ?? '').toLowerCase();
350
+ const show = !q || text.includes(q);
351
+ item.style.display = show ? '' : 'none';
352
+ if (show)
353
+ visible++;
354
+ });
355
+ const empty = this.$('.menu__empty');
356
+ if (empty)
357
+ empty.classList.toggle('visible', visible === 0);
358
+ }
359
+ }
360
+ defineComponent('nc-menu', NcMenu);
@@ -0,0 +1,51 @@
1
+ /**
2
+ * NcModal Component
3
+ *
4
+ * Attributes:
5
+ * - open: boolean - visible state
6
+ * - size: 'sm'|'md'|'lg'|'xl'|'full' (default: 'md')
7
+ * - no-close-btn: boolean - hide header × button
8
+ * - close-on-overlay: boolean - click backdrop to close (default: true)
9
+ * - no-overlay: boolean - skip backdrop rendering
10
+ * - sticky-header: boolean - header doesn't scroll with body
11
+ * - sticky-footer: boolean - footer stays at bottom
12
+ *
13
+ * Slots:
14
+ * - header - modal title / header area
15
+ * - (default) - modal body
16
+ * - footer - action buttons area
17
+ *
18
+ * Events:
19
+ * - open: CustomEvent - after modal opens
20
+ * - close: CustomEvent - after modal closes
21
+ *
22
+ * Static API:
23
+ * NcModal.open(id) - open a modal by id
24
+ * NcModal.close(id) - close a modal by id
25
+ *
26
+ * Usage:
27
+ * <nc-modal id="confirm-modal">
28
+ * <span slot="header">Confirm Delete</span>
29
+ * <p>Are you sure?</p>
30
+ * <div slot="footer">
31
+ * <nc-button variant="ghost" id="cancel-btn">Cancel</nc-button>
32
+ * <nc-button variant="danger" id="confirm-btn">Delete</nc-button>
33
+ * </div>
34
+ * </nc-modal>
35
+ *
36
+ * NcModal.open('confirm-modal');
37
+ */
38
+ import { Component } from '../core/component.js';
39
+ export declare class NcModal extends Component {
40
+ static useShadowDOM: boolean;
41
+ static get observedAttributes(): string[];
42
+ static open(id: string): void;
43
+ static close(id: string): void;
44
+ private _onKeydown;
45
+ template(): string;
46
+ onMount(): void;
47
+ private _bindEvents;
48
+ private _close;
49
+ onUnmount(): void;
50
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
51
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * NcModal Component
3
+ *
4
+ * Attributes:
5
+ * - open: boolean - visible state
6
+ * - size: 'sm'|'md'|'lg'|'xl'|'full' (default: 'md')
7
+ * - no-close-btn: boolean - hide header × button
8
+ * - close-on-overlay: boolean - click backdrop to close (default: true)
9
+ * - no-overlay: boolean - skip backdrop rendering
10
+ * - sticky-header: boolean - header doesn't scroll with body
11
+ * - sticky-footer: boolean - footer stays at bottom
12
+ *
13
+ * Slots:
14
+ * - header - modal title / header area
15
+ * - (default) - modal body
16
+ * - footer - action buttons area
17
+ *
18
+ * Events:
19
+ * - open: CustomEvent - after modal opens
20
+ * - close: CustomEvent - after modal closes
21
+ *
22
+ * Static API:
23
+ * NcModal.open(id) - open a modal by id
24
+ * NcModal.close(id) - close a modal by id
25
+ *
26
+ * Usage:
27
+ * <nc-modal id="confirm-modal">
28
+ * <span slot="header">Confirm Delete</span>
29
+ * <p>Are you sure?</p>
30
+ * <div slot="footer">
31
+ * <nc-button variant="ghost" id="cancel-btn">Cancel</nc-button>
32
+ * <nc-button variant="danger" id="confirm-btn">Delete</nc-button>
33
+ * </div>
34
+ * </nc-modal>
35
+ *
36
+ * NcModal.open('confirm-modal');
37
+ */
38
+ import { Component, defineComponent } from '../core/component.js';
39
+ import { dom } from '../utils/dom.js';
40
+ export class NcModal extends Component {
41
+ static useShadowDOM = true;
42
+ static get observedAttributes() {
43
+ return ['open', 'size', 'no-close-btn', 'close-on-overlay', 'no-overlay', 'sticky-header', 'sticky-footer'];
44
+ }
45
+ static open(id) { dom.query(`#${id}`)?.setAttribute('open', ''); }
46
+ static close(id) { dom.query(`#${id}`)?.removeAttribute('open'); }
47
+ _onKeydown = null;
48
+ template() {
49
+ const open = this.hasAttribute('open');
50
+ const size = this.getAttribute('size') || 'md';
51
+ const noOverlay = this.hasAttribute('no-overlay');
52
+ const noCloseBtn = this.hasAttribute('no-close-btn');
53
+ const widths = {
54
+ sm: '420px', md: '560px', lg: '720px', xl: '960px', full: '100vw'
55
+ };
56
+ const maxWidth = widths[size] ?? widths.md;
57
+ return `
58
+ <style>
59
+ :host {
60
+ display: block;
61
+ position: fixed;
62
+ inset: 0;
63
+ z-index: 1000;
64
+ pointer-events: none;
65
+ }
66
+
67
+ .overlay {
68
+ position: absolute;
69
+ inset: 0;
70
+ background: rgba(0,0,0,.5);
71
+ z-index: 1000;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ padding: var(--nc-spacing-lg);
76
+ opacity: ${open ? '1' : '0'};
77
+ pointer-events: ${open ? 'auto' : 'none'};
78
+ transition: opacity var(--nc-transition-base);
79
+ ${noOverlay ? 'background: transparent;' : ''}
80
+ }
81
+
82
+ .dialog {
83
+ background: var(--nc-bg);
84
+ border-radius: var(--nc-radius-lg, 12px);
85
+ box-shadow: var(--nc-shadow-xl, 0 25px 60px rgba(0,0,0,.35));
86
+ width: 100%;
87
+ max-width: ${maxWidth};
88
+ max-height: calc(100vh - 2 * var(--nc-spacing-lg));
89
+ display: flex;
90
+ flex-direction: column;
91
+ transform: ${open ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.97)'};
92
+ transition: transform var(--nc-transition-base);
93
+ overflow: hidden;
94
+ ${size === 'full' ? 'height: calc(100vh - 2 * var(--nc-spacing-lg));' : ''}
95
+ }
96
+
97
+ .dialog__header {
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: space-between;
101
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
102
+ border-bottom: 1px solid var(--nc-border);
103
+ flex-shrink: 0;
104
+ font-family: var(--nc-font-family);
105
+ font-weight: var(--nc-font-weight-semibold);
106
+ font-size: var(--nc-font-size-lg);
107
+ color: var(--nc-text);
108
+ }
109
+
110
+ .dialog__body {
111
+ flex: 1;
112
+ overflow-y: auto;
113
+ padding: var(--nc-spacing-lg);
114
+ font-family: var(--nc-font-family);
115
+ font-size: var(--nc-font-size-base);
116
+ color: var(--nc-text);
117
+ line-height: var(--nc-line-height-relaxed, 1.7);
118
+ }
119
+
120
+ .dialog__footer {
121
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
122
+ border-top: 1px solid var(--nc-border);
123
+ flex-shrink: 0;
124
+ display: flex;
125
+ justify-content: flex-end;
126
+ gap: var(--nc-spacing-sm);
127
+ }
128
+ .dialog__footer:empty { display: none; }
129
+
130
+ .close-btn {
131
+ background: none;
132
+ border: none;
133
+ cursor: pointer;
134
+ padding: 4px;
135
+ color: var(--nc-text-muted);
136
+ border-radius: var(--nc-radius-sm, 4px);
137
+ display: flex;
138
+ transition: color var(--nc-transition-fast), background var(--nc-transition-fast);
139
+ flex-shrink: 0;
140
+ }
141
+ .close-btn:hover { color: var(--nc-text); background: var(--nc-bg-secondary); }
142
+ </style>
143
+
144
+ <div
145
+ class="overlay"
146
+ role="dialog"
147
+ aria-modal="true"
148
+ aria-hidden="${!open}"
149
+ tabindex="-1"
150
+ >
151
+ <div class="dialog">
152
+ <div class="dialog__header">
153
+ <slot name="header"></slot>
154
+ ${!noCloseBtn ? `
155
+ <button class="close-btn" type="button" aria-label="Close modal">
156
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="18" height="18">
157
+ <path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
158
+ </svg>
159
+ </button>` : ''}
160
+ </div>
161
+ <div class="dialog__body">
162
+ <slot></slot>
163
+ </div>
164
+ <div class="dialog__footer">
165
+ <slot name="footer"></slot>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ `;
170
+ }
171
+ onMount() {
172
+ this._bindEvents();
173
+ }
174
+ _bindEvents() {
175
+ const overlay = this.$('.overlay');
176
+ const dialog = this.$('.dialog');
177
+ const closeBtn = this.$('.close-btn');
178
+ if (closeBtn)
179
+ closeBtn.addEventListener('click', () => this._close());
180
+ overlay.addEventListener('click', (e) => {
181
+ if (this.getAttribute('close-on-overlay') === 'false')
182
+ return;
183
+ if (!dialog.contains(e.target))
184
+ this._close();
185
+ });
186
+ this._onKeydown = (e) => {
187
+ if (e.key === 'Escape' && this.hasAttribute('open'))
188
+ this._close();
189
+ };
190
+ document.addEventListener('keydown', this._onKeydown);
191
+ }
192
+ _close() {
193
+ this.removeAttribute('open');
194
+ }
195
+ onUnmount() {
196
+ if (this._onKeydown)
197
+ document.removeEventListener('keydown', this._onKeydown);
198
+ document.body.style.overflow = '';
199
+ }
200
+ attributeChangedCallback(name, oldValue, newValue) {
201
+ if (oldValue === newValue)
202
+ return;
203
+ if (name === 'open' && this._mounted) {
204
+ const open = this.hasAttribute('open');
205
+ const overlay = this.$('.overlay');
206
+ const dialog = this.$('.dialog');
207
+ if (overlay) {
208
+ overlay.style.opacity = open ? '1' : '0';
209
+ overlay.style.pointerEvents = open ? 'auto' : 'none';
210
+ overlay.setAttribute('aria-hidden', String(!open));
211
+ }
212
+ if (dialog) {
213
+ dialog.style.transform = open
214
+ ? 'translateY(0) scale(1)'
215
+ : 'translateY(12px) scale(0.97)';
216
+ if (open)
217
+ dialog.focus();
218
+ }
219
+ document.body.style.overflow = open ? 'hidden' : '';
220
+ this.dispatchEvent(new CustomEvent(open ? 'open' : 'close', {
221
+ bubbles: true, composed: true
222
+ }));
223
+ return;
224
+ }
225
+ if (this._mounted) {
226
+ this.render();
227
+ this._bindEvents();
228
+ }
229
+ }
230
+ }
231
+ defineComponent('nc-modal', NcModal);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * NcNavItem Component - sidebar / navigation link with icon, label, badge, active state
3
+ *
4
+ * Attributes:
5
+ * href - link URL; if omitted renders as a <button>
6
+ * label - visible text (can also use default slot)
7
+ * icon - preset icon name OR raw SVG string (see below)
8
+ * active - boolean - mark as active
9
+ * disabled - boolean
10
+ * badge - badge count (number string); shown as a pill on the right
11
+ * badge-variant - 'primary'(default)|'success'|'danger'|'warning'
12
+ * indent - indentation level for nested nav (default: 0)
13
+ * target - anchor target (default: '_self')
14
+ * exact - boolean - only activate on exact URL match (router integration)
15
+ *
16
+ * Slots:
17
+ * icon - custom icon (overrides icon attribute)
18
+ * (default) - label text (overrides label attribute)
19
+ * badge - custom badge content
20
+ *
21
+ * Events:
22
+ * nav-click - CustomEvent<{ href: string | null }> - bubbles from both <a> and <button>
23
+ *
24
+ * Usage:
25
+ * <nc-nav-item href="/dashboard" label="Dashboard" icon="home" active></nc-nav-item>
26
+ * <nc-nav-item href="/users" label="Users" icon="users" badge="14"></nc-nav-item>
27
+ */
28
+ import { Component } from '../core/component.js';
29
+ export declare class NcNavItem extends Component {
30
+ static useShadowDOM: boolean;
31
+ static get observedAttributes(): string[];
32
+ template(): string;
33
+ onMount(): void;
34
+ attributeChangedCallback(n: string, o: string, v: string): void;
35
+ }