create-nativecore 0.1.1 → 0.2.1

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 (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +403 -431
  3. package/package.json +3 -2
  4. package/template/.env.example +28 -0
  5. package/template/.htmlhintrc +14 -0
  6. package/template/api/data/dashboard.json +11 -0
  7. package/template/api/data/users.json +18 -0
  8. package/template/api/mockApi.js +161 -0
  9. package/template/assets/icon.svg +13 -0
  10. package/template/assets/logo.svg +25 -0
  11. package/template/eslint.config.js +94 -0
  12. package/template/index.html +137 -0
  13. package/template/manifest.json +19 -0
  14. package/template/public/.well-known/security.txt +9 -0
  15. package/template/public/_headers +24 -0
  16. package/template/public/_redirects +14 -0
  17. package/template/public/assets/icon.svg +13 -0
  18. package/template/public/assets/logo.svg +25 -0
  19. package/template/public/manifest.json +19 -0
  20. package/template/public/robots.txt +13 -0
  21. package/template/public/sitemap.xml +27 -0
  22. package/template/scripts/build-for-bots.mjs +121 -0
  23. package/template/scripts/convert-to-ts.mjs +106 -0
  24. package/template/scripts/fix-encoding.mjs +38 -0
  25. package/template/scripts/fix-svg-paths.mjs +32 -0
  26. package/template/scripts/generate-cf-router.mjs +52 -0
  27. package/template/scripts/inject-dev-tools.mjs +41 -0
  28. package/template/scripts/inject-version.mjs +65 -0
  29. package/template/scripts/make-component.mjs +445 -0
  30. package/template/scripts/make-component.mjs.backup +432 -0
  31. package/template/scripts/make-controller.mjs +119 -0
  32. package/template/scripts/make-core-component.mjs +303 -0
  33. package/template/scripts/make-view.mjs +346 -0
  34. package/template/scripts/minify.mjs +71 -0
  35. package/template/scripts/prepare-static-assets.mjs +141 -0
  36. package/template/scripts/prompt-bot-build.mjs +223 -0
  37. package/template/scripts/remove-component.mjs +170 -0
  38. package/template/scripts/remove-core-component.mjs +156 -0
  39. package/template/scripts/remove-dev.mjs +13 -0
  40. package/template/scripts/remove-view.mjs +200 -0
  41. package/template/scripts/strip-dev-blocks.mjs +30 -0
  42. package/template/scripts/watch-compile.mjs +69 -0
  43. package/template/server.js +1066 -0
  44. package/template/src/app.ts +115 -0
  45. package/template/src/components/appRegistry.ts +8 -0
  46. package/template/src/components/core/app-footer.ts +27 -0
  47. package/template/src/components/core/app-header.ts +175 -0
  48. package/template/src/components/core/app-sidebar.ts +238 -0
  49. package/template/src/components/core/loading-spinner.ts +25 -0
  50. package/template/src/components/core/nc-a.ts +313 -0
  51. package/template/src/components/core/nc-accordion.ts +186 -0
  52. package/template/src/components/core/nc-alert.ts +153 -0
  53. package/template/src/components/core/nc-animation.ts +1150 -0
  54. package/template/src/components/core/nc-autocomplete.ts +271 -0
  55. package/template/src/components/core/nc-avatar-group.ts +113 -0
  56. package/template/src/components/core/nc-avatar.ts +148 -0
  57. package/template/src/components/core/nc-badge.ts +86 -0
  58. package/template/src/components/core/nc-bottom-nav.ts +214 -0
  59. package/template/src/components/core/nc-breadcrumb.ts +96 -0
  60. package/template/src/components/core/nc-button.ts +307 -0
  61. package/template/src/components/core/nc-card.ts +160 -0
  62. package/template/src/components/core/nc-checkbox.ts +282 -0
  63. package/template/src/components/core/nc-chip.ts +115 -0
  64. package/template/src/components/core/nc-code.ts +314 -0
  65. package/template/src/components/core/nc-collapsible.ts +154 -0
  66. package/template/src/components/core/nc-color-picker.ts +268 -0
  67. package/template/src/components/core/nc-copy-button.ts +119 -0
  68. package/template/src/components/core/nc-date-picker.ts +443 -0
  69. package/template/src/components/core/nc-div.ts +280 -0
  70. package/template/src/components/core/nc-divider.ts +81 -0
  71. package/template/src/components/core/nc-drawer.ts +230 -0
  72. package/template/src/components/core/nc-dropdown.ts +178 -0
  73. package/template/src/components/core/nc-empty-state.ts +134 -0
  74. package/template/src/components/core/nc-file-upload.ts +354 -0
  75. package/template/src/components/core/nc-form.ts +312 -0
  76. package/template/src/components/core/nc-image.ts +184 -0
  77. package/template/src/components/core/nc-input.ts +383 -0
  78. package/template/src/components/core/nc-kbd.ts +48 -0
  79. package/template/src/components/core/nc-menu-item.ts +193 -0
  80. package/template/src/components/core/nc-menu.ts +376 -0
  81. package/template/src/components/core/nc-modal.ts +238 -0
  82. package/template/src/components/core/nc-nav-item.ts +151 -0
  83. package/template/src/components/core/nc-number-input.ts +350 -0
  84. package/template/src/components/core/nc-otp-input.ts +235 -0
  85. package/template/src/components/core/nc-pagination.ts +178 -0
  86. package/template/src/components/core/nc-popover.ts +260 -0
  87. package/template/src/components/core/nc-progress-circular.ts +119 -0
  88. package/template/src/components/core/nc-progress.ts +134 -0
  89. package/template/src/components/core/nc-radio.ts +235 -0
  90. package/template/src/components/core/nc-rating.ts +266 -0
  91. package/template/src/components/core/nc-rich-text.ts +283 -0
  92. package/template/src/components/core/nc-scroll-top.ts +116 -0
  93. package/template/src/components/core/nc-select.ts +452 -0
  94. package/template/src/components/core/nc-skeleton.ts +107 -0
  95. package/template/src/components/core/nc-slider.ts +285 -0
  96. package/template/src/components/core/nc-snackbar.ts +230 -0
  97. package/template/src/components/core/nc-splash.ts +343 -0
  98. package/template/src/components/core/nc-stepper.ts +247 -0
  99. package/template/src/components/core/nc-switch.ts +281 -0
  100. package/template/src/components/core/nc-tab-item.ts +138 -0
  101. package/template/src/components/core/nc-table.ts +279 -0
  102. package/template/src/components/core/nc-tabs.ts +554 -0
  103. package/template/src/components/core/nc-tag-input.ts +279 -0
  104. package/template/src/components/core/nc-textarea.ts +216 -0
  105. package/template/src/components/core/nc-time-picker.ts +438 -0
  106. package/template/src/components/core/nc-timeline.ts +186 -0
  107. package/template/src/components/core/nc-tooltip.ts +143 -0
  108. package/template/src/components/frameworkRegistry.ts +68 -0
  109. package/template/src/components/preloadRegistry.ts +28 -0
  110. package/template/src/components/registry.ts +8 -0
  111. package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
  112. package/template/src/constants/apiEndpoints.ts +27 -0
  113. package/template/src/constants/errorMessages.ts +23 -0
  114. package/template/src/constants/index.ts +8 -0
  115. package/template/src/constants/routePaths.ts +15 -0
  116. package/template/src/constants/storageKeys.ts +18 -0
  117. package/template/src/controllers/dashboard.controller.ts +200 -0
  118. package/template/src/controllers/home.controller.ts +21 -0
  119. package/template/src/controllers/index.ts +11 -0
  120. package/template/src/controllers/login.controller.ts +131 -0
  121. package/template/src/core/component.ts +354 -0
  122. package/template/src/core/errorHandler.ts +85 -0
  123. package/template/src/core/gpu-animation.ts +604 -0
  124. package/template/src/core/http.ts +173 -0
  125. package/template/src/core/lazyComponents.ts +90 -0
  126. package/template/src/core/router.ts +653 -0
  127. package/template/src/core/signals.ts +146 -0
  128. package/template/src/core/state.ts +248 -0
  129. package/template/src/dev/component-editor.ts +1363 -0
  130. package/template/src/dev/component-overlay.ts +278 -0
  131. package/template/src/dev/context-menu.ts +223 -0
  132. package/template/src/dev/denc-tools.ts +250 -0
  133. package/template/src/dev/hmr.ts +189 -0
  134. package/template/src/dev/nfbs.code-workspace +27 -0
  135. package/template/src/dev/outline-panel.ts +1247 -0
  136. package/template/src/middleware/auth.middleware.ts +23 -0
  137. package/template/src/routes/routes.ts +38 -0
  138. package/template/src/services/api.service.ts +394 -0
  139. package/template/src/services/auth.service.ts +176 -0
  140. package/template/src/services/index.ts +8 -0
  141. package/template/src/services/logger.service.ts +74 -0
  142. package/template/src/services/storage.service.ts +88 -0
  143. package/template/src/stores/appStore.ts +57 -0
  144. package/template/src/stores/uiStore.ts +36 -0
  145. package/template/src/styles/core-variables.css +219 -0
  146. package/template/src/styles/core.css +710 -0
  147. package/template/src/styles/main.css +3164 -0
  148. package/template/src/styles/variables.css +152 -0
  149. package/template/src/types/global.d.ts +47 -0
  150. package/template/src/utils/cacheBuster.ts +20 -0
  151. package/template/src/utils/dom.ts +149 -0
  152. package/template/src/utils/events.ts +203 -0
  153. package/template/src/utils/form.ts +176 -0
  154. package/template/src/utils/formatters.ts +169 -0
  155. package/template/src/utils/helpers.ts +195 -0
  156. package/template/src/utils/markdown.ts +307 -0
  157. package/template/src/utils/sidebar.ts +96 -0
  158. package/template/src/utils/smoothScroll.ts +85 -0
  159. package/template/src/utils/templates.ts +23 -0
  160. package/template/src/utils/validation.ts +73 -0
  161. package/template/src/views/protected/dashboard.html +293 -0
  162. package/template/src/views/public/home.html +150 -0
  163. package/template/src/views/public/login.html +102 -0
  164. package/template/tests/unit/component.test.ts +87 -0
  165. package/template/tests/unit/computed.test.ts +79 -0
  166. package/template/tests/unit/form.test.ts +68 -0
  167. package/template/tests/unit/formatters.test.ts +49 -0
  168. package/template/tests/unit/lazy-components.test.ts +59 -0
  169. package/template/tests/unit/markdown.test.ts +62 -0
  170. package/template/tests/unit/router.test.ts +112 -0
  171. package/template/tests/unit/signals.test.ts +54 -0
  172. package/template/tests/unit/validation.test.ts +50 -0
  173. package/template/tsconfig.build.json +21 -0
  174. package/template/tsconfig.json +51 -0
  175. package/template/vitest.config.ts +36 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * NcAutocomplete Component
3
+ *
4
+ * Attributes:
5
+ * - name: string
6
+ * - value: string — current input value
7
+ * - placeholder: string
8
+ * - options: JSON string array OR comma-separated — static suggestions
9
+ * - min-chars: number — chars before showing suggestions (default: 1)
10
+ * - max-results: number — max visible items (default: 8)
11
+ * - disabled: boolean
12
+ * - size: 'sm'|'md'|'lg' (default: 'md')
13
+ * - variant: 'default'|'filled' (default: 'default')
14
+ *
15
+ * Dynamic options — dispatch 'nc-autocomplete-options' on the element:
16
+ * el.dispatchEvent(new CustomEvent('nc-autocomplete-options', { detail: ['a','b'] }))
17
+ *
18
+ * Events:
19
+ * - input: CustomEvent<{ value: string; name: string }>
20
+ * - select: CustomEvent<{ value: string; name: string }>
21
+ * - change: CustomEvent<{ value: string; name: string }>
22
+ */
23
+
24
+ import { Component, defineComponent } from '@core/component.js';
25
+
26
+ export class NcAutocomplete extends Component {
27
+ static useShadowDOM = true;
28
+
29
+ static get observedAttributes() {
30
+ return ['name', 'value', 'placeholder', 'options', 'min-chars', 'max-results', 'disabled', 'size', 'variant'];
31
+ }
32
+
33
+ private _inputValue = '';
34
+ private _dynamicOptions: string[] = [];
35
+ private _activeIndex = -1;
36
+ private _open = false;
37
+
38
+ constructor() { super(); }
39
+
40
+ private _getOptions(): string[] {
41
+ if (this._dynamicOptions.length) return this._dynamicOptions;
42
+ const raw = this.getAttribute('options') || '';
43
+ if (!raw) return [];
44
+ try { return JSON.parse(raw); } catch { return raw.split(',').map(s => s.trim()).filter(Boolean); }
45
+ }
46
+
47
+ private _filtered(): string[] {
48
+ const query = this._inputValue.trim().toLowerCase();
49
+ const minChars = Number(this.getAttribute('min-chars') ?? 1);
50
+ const max = Number(this.getAttribute('max-results') ?? 8);
51
+ if (query.length < minChars) return [];
52
+ return this._getOptions()
53
+ .filter(o => o.toLowerCase().includes(query))
54
+ .slice(0, max);
55
+ }
56
+
57
+ template() {
58
+ if (!this._mounted) {
59
+ this._inputValue = this.getAttribute('value') || '';
60
+ }
61
+ const disabled = this.hasAttribute('disabled');
62
+ const placeholder = this.getAttribute('placeholder') || '';
63
+ const name = this.getAttribute('name') || '';
64
+ const results = this._open ? this._filtered() : [];
65
+
66
+ return `
67
+ <style>
68
+ :host { display: block; position: relative; width: 100%; font-family: var(--nc-font-family); }
69
+
70
+ .input-wrap {
71
+ position: relative;
72
+ display: flex;
73
+ align-items: center;
74
+ }
75
+
76
+ input {
77
+ width: 100%;
78
+ box-sizing: border-box;
79
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
80
+ background: var(--nc-bg);
81
+ border: var(--nc-input-border);
82
+ border-radius: var(--nc-input-radius);
83
+ color: var(--nc-text);
84
+ font-size: var(--nc-font-size-base);
85
+ font-family: var(--nc-font-family);
86
+ outline: none;
87
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
88
+ opacity: ${disabled ? '0.5' : '1'};
89
+ cursor: ${disabled ? 'not-allowed' : 'auto'};
90
+ }
91
+
92
+ :host([size="sm"]) input { font-size: var(--nc-font-size-sm); padding: var(--nc-spacing-xs) var(--nc-spacing-sm); }
93
+ :host([size="lg"]) input { font-size: var(--nc-font-size-lg); padding: var(--nc-spacing-md) var(--nc-spacing-lg); }
94
+ :host([variant="filled"]) input { background: var(--nc-bg-tertiary); border-color: transparent; }
95
+
96
+ input:focus { border-color: var(--nc-input-focus-border); box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
97
+ input::placeholder { color: var(--nc-text-muted); }
98
+
99
+ .dropdown {
100
+ position: absolute;
101
+ top: calc(100% + 4px);
102
+ left: 0; right: 0;
103
+ background: var(--nc-bg);
104
+ border: 1px solid var(--nc-border);
105
+ border-radius: var(--nc-radius-md, 8px);
106
+ box-shadow: var(--nc-shadow-lg);
107
+ overflow: hidden;
108
+ z-index: 500;
109
+ display: ${results.length ? 'block' : 'none'};
110
+ }
111
+
112
+ .option {
113
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
114
+ cursor: pointer;
115
+ font-size: var(--nc-font-size-sm);
116
+ color: var(--nc-text);
117
+ transition: background var(--nc-transition-fast);
118
+ }
119
+
120
+ .option:hover,
121
+ .option.active { background: var(--nc-bg-secondary); }
122
+
123
+ .option mark {
124
+ background: none;
125
+ color: var(--nc-primary);
126
+ font-weight: var(--nc-font-weight-semibold);
127
+ }
128
+ </style>
129
+ <div class="input-wrap">
130
+ <input
131
+ type="text"
132
+ name="${name}"
133
+ value="${this._inputValue}"
134
+ placeholder="${placeholder}"
135
+ ${disabled ? 'disabled' : ''}
136
+ autocomplete="off"
137
+ role="combobox"
138
+ aria-expanded="${results.length > 0}"
139
+ aria-autocomplete="list"
140
+ aria-haspopup="listbox"
141
+ />
142
+ </div>
143
+ <div class="dropdown" role="listbox">
144
+ ${results.map((opt, i) => {
145
+ const hl = opt.replace(new RegExp(`(${this._inputValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>');
146
+ return `<div class="option${i === this._activeIndex ? ' active' : ''}" role="option" data-value="${opt}" aria-selected="${i === this._activeIndex}">${hl}</div>`;
147
+ }).join('')}
148
+ </div>
149
+ `;
150
+ }
151
+
152
+ onMount() {
153
+ this._bindEvents();
154
+
155
+ // Dynamic options API
156
+ this.addEventListener('nc-autocomplete-options', (e: Event) => {
157
+ this._dynamicOptions = (e as CustomEvent<string[]>).detail || [];
158
+ if (this._open) { this.render(); this._bindEvents(); }
159
+ });
160
+ }
161
+
162
+ private _bindEvents() {
163
+ const input = this.$<HTMLInputElement>('input')!;
164
+ const dropdown = this.$<HTMLElement>('.dropdown')!;
165
+
166
+ input.addEventListener('input', () => {
167
+ this._inputValue = input.value;
168
+ this._activeIndex = -1;
169
+ this._open = true;
170
+ this._refreshDropdown();
171
+ this.dispatchEvent(new CustomEvent('input', {
172
+ bubbles: true, composed: true,
173
+ detail: { value: input.value, name: this.getAttribute('name') || '' }
174
+ }));
175
+ });
176
+
177
+ input.addEventListener('focus', () => {
178
+ this._open = true;
179
+ this._refreshDropdown();
180
+ });
181
+
182
+ input.addEventListener('blur', () => {
183
+ // Delay so click on option fires first
184
+ setTimeout(() => {
185
+ this._open = false;
186
+ this._refreshDropdown();
187
+ }, 150);
188
+ });
189
+
190
+ input.addEventListener('keydown', (e: KeyboardEvent) => {
191
+ const results = this._filtered();
192
+ if (!results.length) return;
193
+ if (e.key === 'ArrowDown') {
194
+ e.preventDefault();
195
+ this._activeIndex = Math.min(this._activeIndex + 1, results.length - 1);
196
+ this._refreshActive();
197
+ } else if (e.key === 'ArrowUp') {
198
+ e.preventDefault();
199
+ this._activeIndex = Math.max(this._activeIndex - 1, -1);
200
+ this._refreshActive();
201
+ } else if (e.key === 'Enter' && this._activeIndex >= 0) {
202
+ e.preventDefault();
203
+ this._selectOption(results[this._activeIndex]);
204
+ } else if (e.key === 'Escape') {
205
+ this._open = false;
206
+ this._refreshDropdown();
207
+ }
208
+ });
209
+
210
+ dropdown.addEventListener('mousedown', (e) => {
211
+ const opt = (e.target as HTMLElement).closest<HTMLElement>('[data-value]');
212
+ if (opt) {
213
+ e.preventDefault();
214
+ this._selectOption(opt.dataset.value!);
215
+ }
216
+ });
217
+ }
218
+
219
+ private _selectOption(value: string) {
220
+ this._inputValue = value;
221
+ this._open = false;
222
+ this._activeIndex = -1;
223
+ this.setAttribute('value', value);
224
+ const input = this.$<HTMLInputElement>('input');
225
+ if (input) input.value = value;
226
+ this._refreshDropdown();
227
+ this.dispatchEvent(new CustomEvent('select', {
228
+ bubbles: true, composed: true,
229
+ detail: { value, name: this.getAttribute('name') || '' }
230
+ }));
231
+ this.dispatchEvent(new CustomEvent('change', {
232
+ bubbles: true, composed: true,
233
+ detail: { value, name: this.getAttribute('name') || '' }
234
+ }));
235
+ }
236
+
237
+ private _refreshDropdown() {
238
+ const dropdown = this.$<HTMLElement>('.dropdown');
239
+ if (!dropdown) return;
240
+ const results = this._open ? this._filtered() : [];
241
+ if (!results.length) { dropdown.style.display = 'none'; return; }
242
+ dropdown.style.display = 'block';
243
+ dropdown.innerHTML = results.map((opt, i) => {
244
+ const escaped = this._inputValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
245
+ const hl = escaped ? opt.replace(new RegExp(`(${escaped})`, 'gi'), '<mark>$1</mark>') : opt;
246
+ return `<div class="option${i === this._activeIndex ? ' active' : ''}" role="option" data-value="${opt}" aria-selected="${i === this._activeIndex}">${hl}</div>`;
247
+ }).join('');
248
+ const input = this.$<HTMLInputElement>('input');
249
+ if (input) input.setAttribute('aria-expanded', String(results.length > 0));
250
+ }
251
+
252
+ private _refreshActive() {
253
+ this.$$<HTMLElement>('.option').forEach((opt, i) => {
254
+ opt.classList.toggle('active', i === this._activeIndex);
255
+ opt.setAttribute('aria-selected', String(i === this._activeIndex));
256
+ });
257
+ }
258
+
259
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
260
+ if (oldValue === newValue) return;
261
+ if (name === 'value' && this._mounted) {
262
+ this._inputValue = newValue || '';
263
+ const input = this.$<HTMLInputElement>('input');
264
+ if (input) input.value = this._inputValue;
265
+ return;
266
+ }
267
+ if (this._mounted) { this.render(); this._bindEvents(); }
268
+ }
269
+ }
270
+
271
+ defineComponent('nc-autocomplete', NcAutocomplete);
@@ -0,0 +1,113 @@
1
+ /**
2
+ * NcAvatarGroup Component — stacked overlapping avatars with overflow count
3
+ *
4
+ * Slots the first `max` nc-avatar elements, then shows "+N" for the rest.
5
+ *
6
+ * Attributes:
7
+ * max — max visible avatars (default: 4)
8
+ * size — 'xs'|'sm'|'md'(default)|'lg'|'xl' — passed to overflow bubble
9
+ * overlap — overlap amount in px (default: 10)
10
+ * total — total count override (used when only some avatars are slotted)
11
+ * If not set, derived from slotted nc-avatars count.
12
+ *
13
+ * Usage:
14
+ * <nc-avatar-group max="3">
15
+ * <nc-avatar alt="Alice" variant="primary"></nc-avatar>
16
+ * <nc-avatar alt="Bob" variant="success"></nc-avatar>
17
+ * <nc-avatar alt="Carol" variant="warning"></nc-avatar>
18
+ * <nc-avatar alt="Dave" variant="danger"></nc-avatar>
19
+ * <nc-avatar alt="Eve" variant="secondary"></nc-avatar>
20
+ * </nc-avatar-group>
21
+ */
22
+ import { Component, defineComponent } from '@core/component.js';
23
+
24
+ export class NcAvatarGroup extends Component {
25
+ static useShadowDOM = true;
26
+
27
+ template() {
28
+ const overlap = parseInt(this.getAttribute('overlap') ?? '10', 10);
29
+ const size = this.getAttribute('size') ?? 'md';
30
+
31
+ const sizePx: Record<string, number> = {
32
+ xs: 24, sm: 32, md: 40, lg: 48, xl: 56,
33
+ };
34
+ const sz = sizePx[size] ?? 40;
35
+ const fs = Math.round(sz * 0.32);
36
+ const bw = Math.max(2, Math.round(sz * 0.06));
37
+
38
+ return `
39
+ <style>
40
+ :host { display: inline-block; }
41
+ .group {
42
+ display: flex;
43
+ flex-direction: row;
44
+ align-items: center;
45
+ }
46
+ /* Offset each child leftwards to create the overlap stack */
47
+ ::slotted(*) {
48
+ margin-left: -${overlap}px;
49
+ box-shadow: 0 0 0 ${bw}px var(--nc-bg, #fff);
50
+ border-radius: 50%;
51
+ flex-shrink: 0;
52
+ position: relative;
53
+ transition: transform var(--nc-transition-fast), z-index 0s;
54
+ z-index: 0;
55
+ }
56
+ ::slotted(*:first-child) { margin-left: 0; }
57
+ ::slotted(*:hover) { transform: translateY(-3px); z-index: 10; }
58
+ .overflow {
59
+ margin-left: -${overlap}px;
60
+ width: ${sz}px;
61
+ height: ${sz}px;
62
+ border-radius: 50%;
63
+ background: var(--nc-bg-tertiary, #e5e7eb);
64
+ color: var(--nc-text-secondary);
65
+ font-family: var(--nc-font-family);
66
+ font-size: ${fs}px;
67
+ font-weight: var(--nc-font-weight-semibold);
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ box-shadow: 0 0 0 ${bw}px var(--nc-bg, #fff);
72
+ flex-shrink: 0;
73
+ user-select: none;
74
+ }
75
+ </style>
76
+ <div class="group" role="group" aria-label="Avatar group">
77
+ <slot></slot>
78
+ <div class="overflow" id="overflow" style="display:none" aria-hidden="true"></div>
79
+ </div>
80
+ `;
81
+ }
82
+
83
+ onMount() {
84
+ this._update();
85
+ }
86
+
87
+ private _update() {
88
+ const slot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot');
89
+ const overflow = this.shadowRoot!.querySelector<HTMLElement>('#overflow');
90
+ if (!slot || !overflow) return;
91
+
92
+ const max = parseInt(this.getAttribute('max') ?? '4', 10);
93
+ const total = parseInt(this.getAttribute('total') ?? '0', 10);
94
+
95
+ const items = slot.assignedElements({ flatten: true }) as HTMLElement[];
96
+ const count = total > 0 ? total : items.length;
97
+
98
+ // Hide items beyond max
99
+ items.forEach((el, idx) => {
100
+ (el as HTMLElement).style.display = idx < max ? '' : 'none';
101
+ });
102
+
103
+ const extra = count - Math.min(max, items.length);
104
+ if (extra > 0) {
105
+ overflow.textContent = `+${extra}`;
106
+ overflow.style.display = 'flex';
107
+ } else {
108
+ overflow.style.display = 'none';
109
+ }
110
+ }
111
+ }
112
+
113
+ defineComponent('nc-avatar-group', NcAvatarGroup);
@@ -0,0 +1,148 @@
1
+ /**
2
+ * NcAvatar Component
3
+ *
4
+ * Attributes:
5
+ * - src: string — image URL
6
+ * - alt: string — alt text / fallback initials (e.g. "John Doe" → "JD")
7
+ * - size: 'xs'|'sm'|'md'|'lg'|'xl'|'2xl' — or any CSS size string (default: 'md')
8
+ * - shape: 'circle'|'square'|'rounded' (default: 'circle')
9
+ * - variant: 'primary'|'secondary'|'success'|'warning'|'danger'|'neutral' — fallback bg color (default: 'neutral')
10
+ * - status: 'online'|'offline'|'away'|'busy' — status dot
11
+ * - status-position: 'top-right'|'bottom-right'|'bottom-left'|'top-left' (default: 'bottom-right')
12
+ *
13
+ * Usage:
14
+ * <nc-avatar src="/user.jpg" alt="Jane Doe" size="md"></nc-avatar>
15
+ * <nc-avatar alt="David Toledo" variant="primary" status="online"></nc-avatar>
16
+ */
17
+
18
+ import { Component, defineComponent } from '@core/component.js';
19
+
20
+ const SIZE_MAP: Record<string, string> = {
21
+ xs: '24px', sm: '32px', md: '40px', lg: '48px', xl: '64px', '2xl': '80px',
22
+ };
23
+
24
+ const STATUS_COLORS: Record<string, string> = {
25
+ online: '#22c55e', offline: '#94a3b8', away: '#f59e0b', busy: '#ef4444',
26
+ };
27
+
28
+ function initials(name: string): string {
29
+ return name.trim().split(/\s+/).map(w => w[0] ?? '').join('').toUpperCase().slice(0, 2);
30
+ }
31
+
32
+ export class NcAvatar extends Component {
33
+ static useShadowDOM = true;
34
+
35
+ static get observedAttributes() {
36
+ return ['src', 'alt', 'size', 'shape', 'variant', 'status', 'status-position'];
37
+ }
38
+
39
+ private _imgError = false;
40
+
41
+ template() {
42
+ const src = this.getAttribute('src') || '';
43
+ const alt = this.getAttribute('alt') || '';
44
+ const sizeAttr = this.getAttribute('size') || 'md';
45
+ const shape = this.getAttribute('shape') || 'circle';
46
+ const variant = this.getAttribute('variant') || 'neutral';
47
+ const status = this.getAttribute('status') || '';
48
+ const statusPos = this.getAttribute('status-position') || 'bottom-right';
49
+
50
+ const sizeVal = SIZE_MAP[sizeAttr] ?? sizeAttr;
51
+ const fontSize = `calc(${sizeVal} * 0.38)`;
52
+ const statusSize = `calc(${sizeVal} * 0.26)`;
53
+
54
+ const borderRadius = shape === 'circle' ? '50%' : shape === 'rounded' ? '25%' : '8px';
55
+
56
+ const letters = initials(alt);
57
+ const showImg = src && !this._imgError;
58
+
59
+ const [svPos, shPos] = statusPos.split('-');
60
+ const statusDotStyle = [
61
+ svPos === 'top' ? 'top: 0;' : 'bottom: 0;',
62
+ shPos === 'right' ? 'right: 0;' : 'left: 0;',
63
+ ].join(' ');
64
+
65
+ const variantBg: Record<string, string> = {
66
+ primary: 'var(--nc-primary)',
67
+ secondary: 'var(--nc-secondary, #6366f1)',
68
+ success: 'var(--nc-success, #10b981)',
69
+ warning: 'var(--nc-warning, #f59e0b)',
70
+ danger: 'var(--nc-danger, #ef4444)',
71
+ neutral: 'var(--nc-bg-tertiary)',
72
+ };
73
+
74
+ return `
75
+ <style>
76
+ :host { display: inline-flex; position: relative; flex-shrink: 0; }
77
+
78
+ .avatar {
79
+ width: ${sizeVal};
80
+ height: ${sizeVal};
81
+ border-radius: ${borderRadius};
82
+ overflow: hidden;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ background: ${variantBg[variant] ?? variantBg.neutral};
87
+ color: ${variant === 'neutral' ? 'var(--nc-text)' : '#fff'};
88
+ font-family: var(--nc-font-family);
89
+ font-size: ${fontSize};
90
+ font-weight: var(--nc-font-weight-semibold);
91
+ user-select: none;
92
+ flex-shrink: 0;
93
+ }
94
+
95
+ img {
96
+ width: 100%;
97
+ height: 100%;
98
+ object-fit: cover;
99
+ display: ${showImg ? 'block' : 'none'};
100
+ }
101
+
102
+ .initials {
103
+ display: ${showImg ? 'none' : 'flex'};
104
+ align-items: center;
105
+ justify-content: center;
106
+ width: 100%;
107
+ height: 100%;
108
+ line-height: 1;
109
+ }
110
+
111
+ .status-dot {
112
+ position: absolute;
113
+ ${statusDotStyle}
114
+ width: ${statusSize};
115
+ height: ${statusSize};
116
+ border-radius: 50%;
117
+ background: ${STATUS_COLORS[status] ?? STATUS_COLORS.offline};
118
+ border: 2px solid var(--nc-bg);
119
+ display: ${status ? 'block' : 'none'};
120
+ }
121
+ </style>
122
+ <div class="avatar" title="${alt}" aria-label="${alt}" role="img">
123
+ ${showImg ? `<img src="${src}" alt="${alt}" />` : ''}
124
+ <span class="initials">${letters || '?'}</span>
125
+ </div>
126
+ ${status ? `<span class="status-dot" aria-label="${status}"></span>` : ''}
127
+ `;
128
+ }
129
+
130
+ onMount() {
131
+ const img = this.$<HTMLImageElement>('img');
132
+ if (img) {
133
+ img.addEventListener('error', () => {
134
+ this._imgError = true;
135
+ this.render();
136
+ });
137
+ }
138
+ }
139
+
140
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
141
+ if (oldValue !== newValue) {
142
+ if (name === 'src') this._imgError = false;
143
+ if (this._mounted) { this.render(); this.onMount(); }
144
+ }
145
+ }
146
+ }
147
+
148
+ defineComponent('nc-avatar', NcAvatar);
@@ -0,0 +1,86 @@
1
+ /**
2
+ * NcBadge Component
3
+ *
4
+ * Attributes:
5
+ * - count: number — value to display (hidden when 0 unless show-zero)
6
+ * - max: number — cap value (default: 99); shows "max+" when exceeded
7
+ * - show-zero: boolean — show badge even when count is 0
8
+ * - dot: boolean — render as a small dot with no count
9
+ * - variant: 'primary'|'secondary'|'danger'|'warning'|'success'|'info'|'neutral' (default: 'danger')
10
+ * - position: 'top-right'|'top-left'|'bottom-right'|'bottom-left' (default: 'top-right')
11
+ *
12
+ * Usage:
13
+ * <nc-badge count="5"><nc-button>Inbox</nc-button></nc-badge>
14
+ * <nc-badge dot variant="success"><nc-button>Status</nc-button></nc-badge>
15
+ */
16
+
17
+ import { Component, defineComponent } from '@core/component.js';
18
+
19
+ export class NcBadge extends Component {
20
+ static useShadowDOM = true;
21
+
22
+ static get observedAttributes() {
23
+ return ['count', 'max', 'show-zero', 'dot', 'variant', 'position'];
24
+ }
25
+
26
+ template() {
27
+ const count = Number(this.getAttribute('count') || 0);
28
+ const max = Number(this.getAttribute('max') || 99);
29
+ const showZero = this.hasAttribute('show-zero');
30
+ const dot = this.hasAttribute('dot');
31
+ const variant = this.getAttribute('variant') || 'danger';
32
+ const position = this.getAttribute('position') || 'top-right';
33
+
34
+ const visible = dot || showZero || count > 0;
35
+ const label = dot ? '' : count > max ? `${max}+` : String(count);
36
+
37
+ const [vPos, hPos] = position.split('-');
38
+
39
+ return `
40
+ <style>
41
+ :host { display: inline-flex; position: relative; vertical-align: middle; }
42
+
43
+ .badge {
44
+ position: absolute;
45
+ ${vPos === 'top' ? 'top: -6px;' : 'bottom: -6px;'}
46
+ ${hPos === 'right' ? 'right: -6px;' : 'left: -6px;'}
47
+ z-index: 1;
48
+ display: ${visible ? 'inline-flex' : 'none'};
49
+ align-items: center;
50
+ justify-content: center;
51
+ font-family: var(--nc-font-family);
52
+ font-size: 0.65rem;
53
+ font-weight: var(--nc-font-weight-bold);
54
+ line-height: 1;
55
+ min-width: ${dot ? '8px' : '18px'};
56
+ height: ${dot ? '8px' : '18px'};
57
+ padding: ${dot ? '0' : '0 5px'};
58
+ border-radius: 999px;
59
+ border: 2px solid var(--nc-bg);
60
+ white-space: nowrap;
61
+ pointer-events: none;
62
+ transition: transform var(--nc-transition-fast);
63
+ transform: scale(${visible ? '1' : '0'});
64
+ }
65
+
66
+ .badge--primary { background: var(--nc-primary); color: #fff; }
67
+ .badge--secondary { background: var(--nc-secondary, #6366f1); color: #fff; }
68
+ .badge--danger { background: var(--nc-danger, #ef4444); color: #fff; }
69
+ .badge--warning { background: var(--nc-warning, #f59e0b); color: #fff; }
70
+ .badge--success { background: var(--nc-success, #10b981); color: #fff; }
71
+ .badge--info { background: var(--nc-info, #3b82f6); color: #fff; }
72
+ .badge--neutral { background: var(--nc-text-muted); color: #fff; }
73
+
74
+ ::slotted(*) { display: inline-flex; }
75
+ </style>
76
+ <slot></slot>
77
+ <span class="badge badge--${variant}" aria-label="${dot ? 'indicator' : `${label} notifications`}">${label}</span>
78
+ `;
79
+ }
80
+
81
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
82
+ if (oldValue !== newValue && this._mounted) this.render();
83
+ }
84
+ }
85
+
86
+ defineComponent('nc-badge', NcBadge);