create-nativecore 0.1.0 → 0.2.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 (175) hide show
  1. package/README.md +10 -18
  2. package/bin/index.mjs +407 -489
  3. package/package.json +4 -3
  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 +642 -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,178 @@
1
+ /**
2
+ * NcDropdown Component
3
+ *
4
+ * A generic trigger + floating-panel component. The trigger is whatever
5
+ * is in slot[name="trigger"]; the content panel is the default slot.
6
+ *
7
+ * Attributes:
8
+ * - open: boolean — visible state
9
+ * - placement: 'bottom-start'|'bottom-end'|'bottom'|'top-start'|'top-end'|'top' (default: 'bottom-start')
10
+ * - close-on-select: boolean — close when a [data-value] child is clicked (default: true)
11
+ * - disabled: boolean
12
+ * - offset: number — gap in px between trigger and panel (default: 6)
13
+ * - width: string — CSS width of panel (default: 'auto'; use 'trigger' to match trigger width)
14
+ *
15
+ * Events:
16
+ * - open: CustomEvent
17
+ * - close: CustomEvent
18
+ * - select: CustomEvent<{ value: string; label: string }> — when a [data-value] child is clicked
19
+ *
20
+ * Usage:
21
+ * <nc-dropdown>
22
+ * <nc-button slot="trigger">Options</nc-button>
23
+ * <nc-menu>
24
+ * <nc-menu-item data-value="edit">Edit</nc-menu-item>
25
+ * <nc-menu-item data-value="delete">Delete</nc-menu-item>
26
+ * </nc-menu>
27
+ * </nc-dropdown>
28
+ */
29
+
30
+ import { Component, defineComponent } from '@core/component.js';
31
+
32
+ export class NcDropdown extends Component {
33
+ static useShadowDOM = true;
34
+
35
+ static get observedAttributes() {
36
+ return ['open', 'placement', 'close-on-select', 'disabled', 'offset', 'width'];
37
+ }
38
+
39
+ private _outsideClick: ((e: MouseEvent) => void) | null = null;
40
+
41
+ template() {
42
+ const open = this.hasAttribute('open');
43
+ const placement = this.getAttribute('placement') || 'bottom-start';
44
+
45
+ // Derive alignment classes from placement string
46
+ const [vSide, hAlign] = placement.split('-') as [string, string | undefined];
47
+ const above = vSide === 'top';
48
+
49
+ return `
50
+ <style>
51
+ :host { display: inline-flex; position: relative; vertical-align: middle; }
52
+
53
+ .trigger-slot {
54
+ display: contents;
55
+ }
56
+
57
+ .panel {
58
+ position: absolute;
59
+ ${above ? 'bottom: calc(100% + var(--dropdown-offset, 6px));' : 'top: calc(100% + var(--dropdown-offset, 6px));'}
60
+ ${!hAlign || hAlign === 'start' ? 'left: 0;' : hAlign === 'end' ? 'right: 0;' : 'left: 50%; transform: translateX(-50%);'}
61
+ z-index: 600;
62
+ background: var(--nc-bg);
63
+ border: 1px solid var(--nc-border);
64
+ border-radius: var(--nc-radius-md, 8px);
65
+ box-shadow: var(--nc-shadow-lg);
66
+ min-width: 160px;
67
+ overflow: hidden;
68
+ opacity: ${open ? '1' : '0'};
69
+ pointer-events: ${open ? 'auto' : 'none'};
70
+ transform-origin: ${above ? 'bottom' : 'top'} ${!hAlign || hAlign === 'start' ? 'left' : hAlign === 'end' ? 'right' : 'center'};
71
+ transform: ${open
72
+ ? (!hAlign || hAlign !== 'center' ? 'none' : 'translateX(-50%)')
73
+ : (!hAlign || hAlign !== 'center'
74
+ ? `scale(0.97) translateY(${above ? '4px' : '-4px'})`
75
+ : `translateX(-50%) scale(0.97) translateY(${above ? '4px' : '-4px'})`)};
76
+ transition: opacity var(--nc-transition-fast), transform var(--nc-transition-fast);
77
+ }
78
+ </style>
79
+ <span class="trigger-slot">
80
+ <slot name="trigger"></slot>
81
+ </span>
82
+ <div class="panel" role="menu" aria-hidden="${!open}">
83
+ <slot></slot>
84
+ </div>
85
+ `;
86
+ }
87
+
88
+ onMount() {
89
+ this._bindEvents();
90
+ }
91
+
92
+ private _bindEvents() {
93
+ // Toggle on trigger click
94
+ const triggerSlot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="trigger"]')!;
95
+ triggerSlot.addEventListener('slotchange', () => this._hookTrigger());
96
+ this._hookTrigger();
97
+
98
+ // Close on outside click
99
+ this._outsideClick = (e: MouseEvent) => {
100
+ if (!this.contains(e.target as Node) && !this.shadowRoot!.contains(e.target as Node)) {
101
+ this._setOpen(false);
102
+ }
103
+ };
104
+ document.addEventListener('mousedown', this._outsideClick);
105
+
106
+ // Close on Escape
107
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
108
+ if (e.key === 'Escape' && this.hasAttribute('open')) this._setOpen(false);
109
+ });
110
+
111
+ // Select via [data-value] children in light DOM
112
+ this.addEventListener('click', (e: Event) => {
113
+ const target = (e.target as HTMLElement).closest<HTMLElement>('[data-value]');
114
+ if (!target) return;
115
+ const value = target.dataset.value ?? '';
116
+ const label = target.textContent?.trim() ?? '';
117
+ this.dispatchEvent(new CustomEvent('select', {
118
+ bubbles: true, composed: true,
119
+ detail: { value, label }
120
+ }));
121
+ if (this.getAttribute('close-on-select') !== 'false') {
122
+ this._setOpen(false);
123
+ }
124
+ });
125
+ }
126
+
127
+ private _hookTrigger() {
128
+ const slot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="trigger"]')!;
129
+ const nodes = slot.assignedElements();
130
+ nodes.forEach(node => {
131
+ (node as HTMLElement).addEventListener('click', (e: Event) => {
132
+ e.stopPropagation();
133
+ if (!this.hasAttribute('disabled')) this._setOpen(!this.hasAttribute('open'));
134
+ });
135
+ });
136
+ }
137
+
138
+ private _setOpen(open: boolean) {
139
+ if (open) {
140
+ this.setAttribute('open', '');
141
+ } else {
142
+ this.removeAttribute('open');
143
+ }
144
+ }
145
+
146
+ onUnmount() {
147
+ if (this._outsideClick) document.removeEventListener('mousedown', this._outsideClick);
148
+ }
149
+
150
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
151
+ if (oldValue === newValue) return;
152
+ if (name === 'open' && this._mounted) {
153
+ const open = this.hasAttribute('open');
154
+ const panel = this.$<HTMLElement>('.panel');
155
+ if (panel) {
156
+ const placement = this.getAttribute('placement') || 'bottom-start';
157
+ const [vSide, hAlign] = placement.split('-') as [string, string | undefined];
158
+ const above = vSide === 'top';
159
+ const center = hAlign === 'center';
160
+ panel.style.opacity = open ? '1' : '0';
161
+ panel.style.pointerEvents = open ? 'auto' : 'none';
162
+ panel.style.transform = open
163
+ ? (center ? 'translateX(-50%)' : 'none')
164
+ : (center
165
+ ? `translateX(-50%) scale(0.97) translateY(${above ? '4px' : '-4px'})`
166
+ : `scale(0.97) translateY(${above ? '4px' : '-4px'})`);
167
+ panel.setAttribute('aria-hidden', String(!open));
168
+ }
169
+ this.dispatchEvent(new CustomEvent(open ? 'open' : 'close', {
170
+ bubbles: true, composed: true
171
+ }));
172
+ return;
173
+ }
174
+ if (this._mounted) { this.render(); this._bindEvents(); }
175
+ }
176
+ }
177
+
178
+ defineComponent('nc-dropdown', NcDropdown);
@@ -0,0 +1,134 @@
1
+ /**
2
+ * NcEmptyState Component — empty state illustration + heading + action
3
+ *
4
+ * Attributes:
5
+ * title — main heading text
6
+ * description — secondary description text
7
+ * icon — preset icon name: 'inbox'|'search'|'folder'|'data'|'error'|'lock'|'custom'
8
+ * Use 'custom' and the icon slot for your own SVG.
9
+ * size — 'sm'|'md'(default)|'lg'
10
+ * variant — 'default'|'bordered'|'filled'
11
+ *
12
+ * Slots:
13
+ * icon — custom illustration/icon (use with icon="custom")
14
+ * title — overrides title attribute
15
+ * description — overrides description attribute
16
+ * actions — buttons / links below the description
17
+ *
18
+ * Usage:
19
+ * <nc-empty-state title="No results found" description="Try adjusting your search." icon="search">
20
+ * <div slot="actions">
21
+ * <nc-button>Clear filters</nc-button>
22
+ * </div>
23
+ * </nc-empty-state>
24
+ */
25
+ import { Component, defineComponent } from '@core/component.js';
26
+
27
+ const ICONS: Record<string, string> = {
28
+ inbox: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
29
+ <rect x="8" y="16" width="48" height="36" rx="4" stroke="currentColor" stroke-width="2.5"/>
30
+ <polyline points="8,30 26,42 38,42 56,30" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"/>
31
+ <line x1="20" y1="24" x2="44" y2="24" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity=".5"/>
32
+ <line x1="20" y1="30" x2="32" y2="30" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity=".5"/>
33
+ </svg>`,
34
+ search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
35
+ <circle cx="27" cy="27" r="17" stroke="currentColor" stroke-width="2.5"/>
36
+ <line x1="39" y1="39" x2="56" y2="56" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
37
+ <line x1="21" y1="27" x2="33" y2="27" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity=".5"/>
38
+ <line x1="27" y1="21" x2="27" y2="33" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity=".5"/>
39
+ </svg>`,
40
+ folder: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
41
+ <path d="M8 20a4 4 0 0 1 4-4h12l6 6h22a4 4 0 0 1 4 4v18a4 4 0 0 1-4 4H12a4 4 0 0 1-4-4V20z"
42
+ stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"/>
43
+ <line x1="24" y1="38" x2="40" y2="38" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity=".5"/>
44
+ </svg>`,
45
+ data: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
46
+ <ellipse cx="32" cy="18" rx="20" ry="8" stroke="currentColor" stroke-width="2.5"/>
47
+ <path d="M12 18v10c0 4.4 9 8 20 8s20-3.6 20-8V18" stroke="currentColor" stroke-width="2.5"/>
48
+ <path d="M12 28v10c0 4.4 9 8 20 8s20-3.6 20-8V28" stroke="currentColor" stroke-width="2.5"/>
49
+ </svg>`,
50
+ error: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
51
+ <circle cx="32" cy="32" r="24" stroke="currentColor" stroke-width="2.5"/>
52
+ <line x1="32" y1="20" x2="32" y2="36" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
53
+ <circle cx="32" cy="44" r="2.5" fill="currentColor"/>
54
+ </svg>`,
55
+ lock: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
56
+ <rect x="14" y="28" width="36" height="26" rx="4" stroke="currentColor" stroke-width="2.5"/>
57
+ <path d="M20 28V20a12 12 0 0 1 24 0v8" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
58
+ <circle cx="32" cy="41" r="4" stroke="currentColor" stroke-width="2.5"/>
59
+ <line x1="32" y1="45" x2="32" y2="50" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
60
+ </svg>`,
61
+ };
62
+
63
+ export class NcEmptyState extends Component {
64
+ static useShadowDOM = true;
65
+
66
+ template() {
67
+ const title = this.getAttribute('title') ?? '';
68
+ const description = this.getAttribute('description') ?? '';
69
+ const icon = this.getAttribute('icon') ?? 'inbox';
70
+ const size = this.getAttribute('size') ?? 'md';
71
+ const variant = this.getAttribute('variant') ?? 'default';
72
+
73
+ const iconSz = size === 'sm' ? '56px' : size === 'lg' ? '96px' : '72px';
74
+ const titleFs = size === 'sm' ? 'var(--nc-font-size-base)' : size === 'lg' ? 'var(--nc-font-size-xl)' : 'var(--nc-font-size-lg)';
75
+ const padding = size === 'sm' ? 'var(--nc-spacing-lg)' : size === 'lg' ? 'var(--nc-spacing-2xl, 48px)' : 'var(--nc-spacing-xl, 40px)';
76
+
77
+ const variantStyle =
78
+ variant === 'bordered' ? 'border: 1px dashed var(--nc-border); border-radius: var(--nc-radius-lg);' :
79
+ variant === 'filled' ? 'background: var(--nc-bg-secondary); border-radius: var(--nc-radius-lg);' :
80
+ '';
81
+
82
+ const hasCustomIcon = icon === 'custom';
83
+ const iconHtml = hasCustomIcon ? '' : (ICONS[icon] ?? ICONS.inbox);
84
+
85
+ return `
86
+ <style>
87
+ :host { display: block; }
88
+ .wrap {
89
+ display: flex;
90
+ flex-direction: column;
91
+ align-items: center;
92
+ text-align: center;
93
+ padding: ${padding};
94
+ ${variantStyle}
95
+ font-family: var(--nc-font-family);
96
+ }
97
+ .icon-wrap {
98
+ width: ${iconSz};
99
+ height: ${iconSz};
100
+ color: var(--nc-text-muted);
101
+ margin-bottom: var(--nc-spacing-md);
102
+ opacity: 0.6;
103
+ }
104
+ .icon-wrap svg { width: 100%; height: 100%; }
105
+ .title {
106
+ font-size: ${titleFs};
107
+ font-weight: var(--nc-font-weight-semibold);
108
+ color: var(--nc-text);
109
+ margin: 0 0 var(--nc-spacing-xs);
110
+ }
111
+ .desc {
112
+ font-size: var(--nc-font-size-sm);
113
+ color: var(--nc-text-secondary);
114
+ margin: 0 0 var(--nc-spacing-md);
115
+ max-width: 360px;
116
+ line-height: var(--nc-line-height-relaxed, 1.7);
117
+ }
118
+ .actions { display: flex; gap: var(--nc-spacing-sm); flex-wrap: wrap; justify-content: center; }
119
+ slot[name="title"]::slotted(*),
120
+ slot[name="description"]::slotted(*) { margin: 0; }
121
+ </style>
122
+ <div class="wrap">
123
+ <div class="icon-wrap">
124
+ ${hasCustomIcon ? '<slot name="icon"></slot>' : iconHtml}
125
+ </div>
126
+ ${title ? `<p class="title">${title}</p>` : '<slot name="title"></slot>'}
127
+ ${description ? `<p class="desc">${description}</p>` : '<slot name="description"></slot>'}
128
+ <div class="actions"><slot name="actions"></slot></div>
129
+ </div>
130
+ `;
131
+ }
132
+ }
133
+
134
+ defineComponent('nc-empty-state', NcEmptyState);
@@ -0,0 +1,354 @@
1
+ /**
2
+ * NcFileUpload Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Attributes:
7
+ * - name: string — form field name
8
+ * - accept: string — file types (e.g. "image/*,.pdf")
9
+ * - multiple: boolean — allow multiple file selection
10
+ * - disabled: boolean — disabled state
11
+ * - max-size: number — max file size in MB (default: no limit)
12
+ * - variant: 'default' | 'compact' (default: 'default')
13
+ *
14
+ * Events:
15
+ * - change: CustomEvent<{ files: File[]; name: string }>
16
+ * - error: CustomEvent<{ message: string; files: File[] }>
17
+ *
18
+ * Usage:
19
+ * <nc-file-upload name="avatar" accept="image/*"></nc-file-upload>
20
+ * <nc-file-upload name="docs" accept=".pdf,.docx" multiple max-size="10"></nc-file-upload>
21
+ */
22
+
23
+ import { Component, defineComponent } from '@core/component.js';
24
+
25
+ export class NcFileUpload extends Component {
26
+ static useShadowDOM = true;
27
+
28
+ static attributeOptions = {
29
+ variant: ['default', 'compact']
30
+ };
31
+
32
+ static get observedAttributes() {
33
+ return ['name', 'accept', 'multiple', 'disabled', 'max-size', 'variant'];
34
+ }
35
+
36
+ private _files: File[] = [];
37
+ private _dragging = false;
38
+
39
+ constructor() {
40
+ super();
41
+ }
42
+
43
+ template() {
44
+ const disabled = this.hasAttribute('disabled');
45
+ const multiple = this.hasAttribute('multiple');
46
+ const accept = this.getAttribute('accept') || '';
47
+ const maxSize = this.getAttribute('max-size');
48
+ const variant = this.getAttribute('variant') || 'default';
49
+ const isCompact = variant === 'compact';
50
+
51
+ const fileList = this._files.length
52
+ ? this._files.map((f, i) => `
53
+ <div class="file-item" data-index="${i}">
54
+ <span class="file-icon">
55
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14">
56
+ <path d="M9 1H4a1 1 0 00-1 1v12a1 1 0 001 1h8a1 1 0 001-1V6L9 1z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
57
+ <path d="M9 1v5h5" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
58
+ </svg>
59
+ </span>
60
+ <span class="file-name">${f.name}</span>
61
+ <span class="file-size">${this._formatSize(f.size)}</span>
62
+ <button class="file-remove" data-index="${i}" aria-label="Remove ${f.name}" type="button">
63
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
64
+ <path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
65
+ </svg>
66
+ </button>
67
+ </div>
68
+ `).join('')
69
+ : '';
70
+
71
+ return `
72
+ <style>
73
+ :host {
74
+ display: block;
75
+ font-family: var(--nc-font-family);
76
+ width: 100%;
77
+ }
78
+
79
+ .drop-zone {
80
+ display: flex;
81
+ flex-direction: column;
82
+ align-items: center;
83
+ justify-content: center;
84
+ gap: var(--nc-spacing-sm);
85
+ border: 2px dashed var(--nc-border-dark);
86
+ border-radius: var(--nc-radius-lg);
87
+ padding: ${isCompact ? 'var(--nc-spacing-md) var(--nc-spacing-lg)' : 'var(--nc-spacing-2xl) var(--nc-spacing-xl)'};
88
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
89
+ transition: border-color var(--nc-transition-fast), background var(--nc-transition-fast);
90
+ background: var(--nc-bg-secondary);
91
+ opacity: ${disabled ? '0.5' : '1'};
92
+ text-align: center;
93
+ position: relative;
94
+ }
95
+
96
+ .drop-zone.dragging {
97
+ border-color: var(--nc-primary);
98
+ background: rgba(16, 185, 129, 0.06);
99
+ }
100
+
101
+ .drop-zone:hover:not(.disabled) {
102
+ border-color: var(--nc-primary);
103
+ background: rgba(16, 185, 129, 0.04);
104
+ }
105
+
106
+ .upload-icon {
107
+ color: var(--nc-text-muted);
108
+ flex-shrink: 0;
109
+ }
110
+
111
+ .drop-zone.dragging .upload-icon {
112
+ color: var(--nc-primary);
113
+ }
114
+
115
+ .drop-label {
116
+ font-size: var(--nc-font-size-base);
117
+ color: var(--nc-text);
118
+ font-weight: var(--nc-font-weight-medium);
119
+ }
120
+
121
+ .drop-sub {
122
+ font-size: var(--nc-font-size-sm);
123
+ color: var(--nc-text-muted);
124
+ }
125
+
126
+ .browse-link {
127
+ color: var(--nc-primary);
128
+ font-weight: var(--nc-font-weight-semibold);
129
+ cursor: pointer;
130
+ text-decoration: underline;
131
+ text-underline-offset: 2px;
132
+ }
133
+
134
+ /* Hidden native input */
135
+ input[type="file"] {
136
+ position: absolute;
137
+ inset: 0;
138
+ opacity: 0;
139
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
140
+ width: 100%;
141
+ height: 100%;
142
+ }
143
+
144
+ /* File list */
145
+ .file-list {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: var(--nc-spacing-xs);
149
+ margin-top: ${this._files.length ? 'var(--nc-spacing-sm)' : '0'};
150
+ }
151
+
152
+ .file-item {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: var(--nc-spacing-sm);
156
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
157
+ background: var(--nc-bg);
158
+ border: 1px solid var(--nc-border);
159
+ border-radius: var(--nc-radius-md);
160
+ font-size: var(--nc-font-size-sm);
161
+ }
162
+
163
+ .file-icon {
164
+ color: var(--nc-primary);
165
+ flex-shrink: 0;
166
+ display: flex;
167
+ }
168
+
169
+ .file-name {
170
+ flex: 1;
171
+ overflow: hidden;
172
+ text-overflow: ellipsis;
173
+ white-space: nowrap;
174
+ color: var(--nc-text);
175
+ }
176
+
177
+ .file-size {
178
+ color: var(--nc-text-muted);
179
+ flex-shrink: 0;
180
+ font-size: var(--nc-font-size-xs);
181
+ }
182
+
183
+ .file-remove {
184
+ background: none;
185
+ border: none;
186
+ cursor: pointer;
187
+ color: var(--nc-text-muted);
188
+ display: flex;
189
+ align-items: center;
190
+ padding: 2px;
191
+ border-radius: var(--nc-radius-sm);
192
+ transition: color var(--nc-transition-fast), background var(--nc-transition-fast);
193
+ flex-shrink: 0;
194
+ }
195
+
196
+ .file-remove:hover {
197
+ color: var(--nc-danger);
198
+ background: rgba(239, 68, 68, 0.08);
199
+ }
200
+
201
+ .accept-hint {
202
+ font-size: var(--nc-font-size-xs);
203
+ color: var(--nc-text-muted);
204
+ margin-top: var(--nc-spacing-xs);
205
+ }
206
+ </style>
207
+
208
+ <div class="drop-zone${this._dragging ? ' dragging' : ''}${disabled ? ' disabled' : ''}">
209
+ <input
210
+ type="file"
211
+ ${accept ? `accept="${accept}"` : ''}
212
+ ${multiple ? 'multiple' : ''}
213
+ ${disabled ? 'disabled' : ''}
214
+ name="${this.getAttribute('name') || ''}"
215
+ tabindex="-1"
216
+ />
217
+
218
+ <span class="upload-icon">
219
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
220
+ width="${isCompact ? '20' : '32'}" height="${isCompact ? '20' : '32'}">
221
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
222
+ <polyline points="17 8 12 3 7 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
223
+ <line x1="12" y1="3" x2="12" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
224
+ </svg>
225
+ </span>
226
+
227
+ ${isCompact
228
+ ? `<span class="drop-label"><span class="browse-link">Browse</span> or drop files here</span>`
229
+ : `<span class="drop-label">Drop files here or <span class="browse-link">browse</span></span>
230
+ <span class="drop-sub">${[accept ? `Accepted: ${accept}` : '', maxSize ? `Max ${maxSize} MB` : ''].filter(Boolean).join(' &bull; ') || 'Any file type accepted'}</span>`
231
+ }
232
+ </div>
233
+
234
+ ${this._files.length ? `<div class="file-list">${fileList}</div>` : ''}
235
+ `;
236
+ }
237
+
238
+ onMount() {
239
+ this._bindEvents();
240
+ }
241
+
242
+ private _bindEvents() {
243
+ const sr = this.shadowRoot!;
244
+ const dropZone = sr.querySelector('.drop-zone')!;
245
+ const input = sr.querySelector<HTMLInputElement>('input[type="file"]')!;
246
+
247
+ // Native input change
248
+ input.addEventListener('change', () => {
249
+ if (input.files) this._handleFiles(Array.from(input.files));
250
+ });
251
+
252
+ // Drag events — update _dragging state directly on the element, re-render only needed for file list
253
+ dropZone.addEventListener('dragover', (e) => {
254
+ e.preventDefault();
255
+ if (!this._dragging) {
256
+ this._dragging = true;
257
+ dropZone.classList.add('dragging');
258
+ sr.querySelector('.upload-icon')?.classList.add('dragging');
259
+ }
260
+ });
261
+
262
+ dropZone.addEventListener('dragleave', (e) => {
263
+ // Only clear if leaving the drop zone entirely
264
+ if (!(e as DragEvent).relatedTarget || !dropZone.contains((e as DragEvent).relatedTarget as Node)) {
265
+ this._dragging = false;
266
+ dropZone.classList.remove('dragging');
267
+ }
268
+ });
269
+
270
+ dropZone.addEventListener('drop', (e) => {
271
+ e.preventDefault();
272
+ this._dragging = false;
273
+ dropZone.classList.remove('dragging');
274
+ const dt = (e as DragEvent).dataTransfer;
275
+ if (dt?.files) this._handleFiles(Array.from(dt.files));
276
+ });
277
+
278
+ // Remove file button — event delegation
279
+ sr.addEventListener('click', (ev) => {
280
+ const btn = (ev.target as HTMLElement).closest('.file-remove') as HTMLElement | null;
281
+ if (!btn) return;
282
+ const idx = Number(btn.dataset.index);
283
+ this._removeFile(idx);
284
+ });
285
+ }
286
+
287
+ private _handleFiles(incoming: File[]) {
288
+ const maxSizeAttr = this.getAttribute('max-size');
289
+ const maxBytes = maxSizeAttr ? Number(maxSizeAttr) * 1024 * 1024 : Infinity;
290
+ const accept = this.getAttribute('accept') || '';
291
+ const multiple = this.hasAttribute('multiple');
292
+
293
+ const oversized: File[] = [];
294
+ let valid = incoming.filter(f => {
295
+ if (f.size > maxBytes) { oversized.push(f); return false; }
296
+ return true;
297
+ });
298
+
299
+ if (accept) {
300
+ const patterns = accept.split(',').map(p => p.trim());
301
+ valid = valid.filter(f => this._matchesAccept(f, patterns));
302
+ }
303
+
304
+ if (oversized.length) {
305
+ this.dispatchEvent(new CustomEvent('error', {
306
+ bubbles: true, composed: true,
307
+ detail: {
308
+ message: `${oversized.map(f => f.name).join(', ')} exceed${oversized.length === 1 ? 's' : ''} the ${maxSizeAttr} MB limit.`,
309
+ files: oversized
310
+ }
311
+ }));
312
+ }
313
+
314
+ if (!valid.length) return;
315
+
316
+ this._files = multiple ? [...this._files, ...valid] : [valid[0]];
317
+ this.render();
318
+ this._bindEvents();
319
+
320
+ this.dispatchEvent(new CustomEvent('change', {
321
+ bubbles: true, composed: true,
322
+ detail: { files: this._files, name: this.getAttribute('name') || '' }
323
+ }));
324
+ }
325
+
326
+ private _removeFile(index: number) {
327
+ this._files.splice(index, 1);
328
+ this.render();
329
+ this._bindEvents();
330
+ }
331
+
332
+ private _matchesAccept(file: File, patterns: string[]): boolean {
333
+ return patterns.some(p => {
334
+ if (p.startsWith('.')) return file.name.toLowerCase().endsWith(p.toLowerCase());
335
+ if (p.endsWith('/*')) return file.type.startsWith(p.slice(0, -2));
336
+ return file.type === p;
337
+ });
338
+ }
339
+
340
+ private _formatSize(bytes: number): string {
341
+ if (bytes < 1024) return `${bytes} B`;
342
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
343
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
344
+ }
345
+
346
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
347
+ if (oldValue !== newValue && this._mounted) {
348
+ this.render();
349
+ this._bindEvents();
350
+ }
351
+ }
352
+ }
353
+
354
+ defineComponent('nc-file-upload', NcFileUpload);