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