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,194 @@
1
+ import { Component, defineComponent } from '../core/component.js';
2
+ export class NcSwitch extends Component {
3
+ static useShadowDOM = true;
4
+ static attributeOptions = {
5
+ variant: ['primary', 'success', 'danger'],
6
+ size: ['sm', 'md', 'lg'],
7
+ 'label-position': ['left', 'right']
8
+ };
9
+ static get observedAttributes() {
10
+ return ['label', 'label-position', 'name', 'value', 'checked', 'disabled', 'size', 'variant'];
11
+ }
12
+ template() {
13
+ const label = this.getAttribute('label') || '';
14
+ const labelPosition = this.getAttribute('label-position') || 'right';
15
+ const disabled = this.hasAttribute('disabled');
16
+ const labelElement = label ? `<span class="label">${label}</span>` : '<slot></slot>';
17
+ const track = `<span class="track"><span class="thumb"></span></span>`;
18
+ return `
19
+ <style>
20
+ :host {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: var(--nc-spacing-sm, 0.5rem);
24
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
25
+ user-select: none;
26
+ font-family: var(--nc-font-family);
27
+ opacity: ${disabled ? '0.5' : '1'};
28
+ }
29
+ .switch-wrapper {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: var(--nc-spacing-sm, 0.5rem);
33
+ }
34
+ input[type="checkbox"] {
35
+ position: absolute;
36
+ opacity: 0;
37
+ width: 0;
38
+ height: 0;
39
+ pointer-events: none;
40
+ }
41
+ .track {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ flex-shrink: 0;
45
+ border-radius: var(--nc-radius-full, 9999px);
46
+ background: var(--nc-gray-300, #d1d5db);
47
+ transition: background var(--nc-transition-fast, 160ms ease);
48
+ position: relative;
49
+ box-sizing: border-box;
50
+ padding: 2px;
51
+ }
52
+ :host([size="sm"]) .track {
53
+ width: 32px;
54
+ height: 18px;
55
+ }
56
+ :host([size="sm"]) .thumb {
57
+ width: 14px;
58
+ height: 14px;
59
+ }
60
+ .track {
61
+ width: 44px;
62
+ height: 24px;
63
+ }
64
+ .thumb {
65
+ width: 20px;
66
+ height: 20px;
67
+ }
68
+ :host([size="md"]) .track {
69
+ width: 44px;
70
+ height: 24px;
71
+ }
72
+ :host([size="md"]) .thumb {
73
+ width: 20px;
74
+ height: 20px;
75
+ }
76
+ :host([size="lg"]) .track {
77
+ width: 56px;
78
+ height: 30px;
79
+ }
80
+ :host([size="lg"]) .thumb {
81
+ width: 26px;
82
+ height: 26px;
83
+ }
84
+ .thumb {
85
+ border-radius: var(--nc-radius-full, 9999px);
86
+ background: var(--nc-white, #ffffff);
87
+ box-shadow: var(--nc-shadow-sm, 0 1px 2px rgba(0,0,0,0.08));
88
+ transition: transform var(--nc-transition-fast, 160ms ease);
89
+ transform: translateX(0);
90
+ flex-shrink: 0;
91
+ }
92
+ :host([checked]) .thumb {
93
+ transform: translateX(calc(100% - 0px));
94
+ }
95
+ :host([size="sm"][checked]) .thumb {
96
+ transform: translateX(14px);
97
+ }
98
+ :host([size="md"][checked]) .thumb,
99
+ :host([checked]:not([size])) .thumb {
100
+ transform: translateX(20px);
101
+ }
102
+ :host([size="lg"][checked]) .thumb {
103
+ transform: translateX(26px);
104
+ }
105
+ :host([checked]) .track {
106
+ background: var(--nc-primary, #10b981);
107
+ }
108
+ :host([variant="success"][checked]) .track {
109
+ background: var(--nc-success, #10b981);
110
+ }
111
+ :host([variant="danger"][checked]) .track {
112
+ background: var(--nc-danger, #ef4444);
113
+ }
114
+ :host(:not([disabled])) .track:hover {
115
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
116
+ }
117
+ :host([variant="success"]:not([disabled])) .track:hover {
118
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
119
+ }
120
+ :host([variant="danger"]:not([disabled])) .track:hover {
121
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
122
+ }
123
+ :host(:focus-visible) .track {
124
+ outline: 2px solid var(--nc-primary, #10b981);
125
+ outline-offset: 2px;
126
+ }
127
+ .label {
128
+ font-size: var(--nc-font-size-base, 1rem);
129
+ color: var(--nc-text, #111827);
130
+ line-height: var(--nc-line-height-normal, 1.5);
131
+ }
132
+ :host([size="sm"]) .label {
133
+ font-size: var(--nc-font-size-sm, 0.875rem);
134
+ }
135
+ :host([size="lg"]) .label {
136
+ font-size: var(--nc-font-size-lg, 1.125rem);
137
+ }
138
+ </style>
139
+
140
+ <input type="hidden" name="${this.getAttribute('name') || ''}" value="${this.hasAttribute('checked') ? (this.getAttribute('value') || 'on') : ''}" />
141
+ <span class="switch-wrapper">
142
+ ${labelPosition === 'left' ? labelElement : ''}
143
+ ${track}
144
+ ${labelPosition !== 'left' ? labelElement : ''}
145
+ </span>
146
+ `;
147
+ }
148
+ onMount() {
149
+ if (!this.hasAttribute('tabindex')) {
150
+ this.setAttribute('tabindex', '0');
151
+ }
152
+ this.setAttribute('role', 'switch');
153
+ this.setAttribute('aria-checked', String(this.hasAttribute('checked')));
154
+ this.addEventListener('click', () => {
155
+ if (this.hasAttribute('disabled'))
156
+ return;
157
+ this.toggle();
158
+ });
159
+ this.addEventListener('keydown', (event) => {
160
+ if (event.key === ' ' || event.key === 'Enter') {
161
+ event.preventDefault();
162
+ if (!this.hasAttribute('disabled'))
163
+ this.toggle();
164
+ }
165
+ });
166
+ }
167
+ toggle() {
168
+ if (this.hasAttribute('checked')) {
169
+ this.removeAttribute('checked');
170
+ }
171
+ else {
172
+ this.setAttribute('checked', '');
173
+ }
174
+ this.setAttribute('aria-checked', String(this.hasAttribute('checked')));
175
+ this.dispatchEvent(new CustomEvent('change', {
176
+ bubbles: true,
177
+ composed: true,
178
+ detail: {
179
+ checked: this.hasAttribute('checked'),
180
+ value: this.getAttribute('value') || 'on',
181
+ name: this.getAttribute('name') || ''
182
+ }
183
+ }));
184
+ }
185
+ attributeChangedCallback(name, oldValue, newValue) {
186
+ if (oldValue !== newValue) {
187
+ this.render();
188
+ if (name === 'checked') {
189
+ this.setAttribute('aria-checked', String(this.hasAttribute('checked')));
190
+ }
191
+ }
192
+ }
193
+ }
194
+ defineComponent('nc-switch', NcSwitch);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * NativeCore Tab Item Component (nc-tab-item)
3
+ *
4
+ * A single content panel inside an nc-tabs container.
5
+ * Visibility is driven entirely by the `active` attribute set by the parent
6
+ * nc-tabs - no JS show/hide logic required here.
7
+ *
8
+ * Attributes:
9
+ * label - Text shown in the tab bar button (read by nc-tabs)
10
+ * active - Boolean. Present = panel visible. Managed by nc-tabs.
11
+ * disabled - Boolean. Prevents the tab from being selected.
12
+ *
13
+ * Slots:
14
+ * default - Any content for this panel.
15
+ *
16
+ * Usage:
17
+ * <nc-tabs>
18
+ * <nc-tab-item label="Overview">...</nc-tab-item>
19
+ * <nc-tab-item label="Settings">...</nc-tab-item>
20
+ * <nc-tab-item label="Logs" disabled>...</nc-tab-item>
21
+ * </nc-tabs>
22
+ *
23
+ * Events emitted:
24
+ * (none - nc-tabs owns all interaction events)
25
+ */
26
+ import { Component } from '../core/component.js';
27
+ export declare class NcTabItem extends Component {
28
+ static useShadowDOM: boolean;
29
+ static get observedAttributes(): string[];
30
+ template(): string;
31
+ /**
32
+ * Override to suppress re-renders - :host([active]) CSS handles show/hide,
33
+ * and label/disabled changes are handled by nc-tabs rebuilding its bar.
34
+ * A full re-render would needlessly flash slot content on every tab click.
35
+ */
36
+ attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null): void;
37
+ onMount(): void;
38
+ onUnmount(): void;
39
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * NativeCore Tab Item Component (nc-tab-item)
3
+ *
4
+ * A single content panel inside an nc-tabs container.
5
+ * Visibility is driven entirely by the `active` attribute set by the parent
6
+ * nc-tabs - no JS show/hide logic required here.
7
+ *
8
+ * Attributes:
9
+ * label - Text shown in the tab bar button (read by nc-tabs)
10
+ * active - Boolean. Present = panel visible. Managed by nc-tabs.
11
+ * disabled - Boolean. Prevents the tab from being selected.
12
+ *
13
+ * Slots:
14
+ * default - Any content for this panel.
15
+ *
16
+ * Usage:
17
+ * <nc-tabs>
18
+ * <nc-tab-item label="Overview">...</nc-tab-item>
19
+ * <nc-tab-item label="Settings">...</nc-tab-item>
20
+ * <nc-tab-item label="Logs" disabled>...</nc-tab-item>
21
+ * </nc-tabs>
22
+ *
23
+ * Events emitted:
24
+ * (none - nc-tabs owns all interaction events)
25
+ */
26
+ import { Component, defineComponent } from '../core/component.js';
27
+ import { html } from '../utils/templates.js';
28
+ export class NcTabItem extends Component {
29
+ static useShadowDOM = true;
30
+ static get observedAttributes() {
31
+ return ['label', 'active', 'disabled'];
32
+ }
33
+ template() {
34
+ return html `
35
+ <style>
36
+ :host {
37
+ display: none;
38
+ }
39
+
40
+ :host([active]) {
41
+ display: block;
42
+ }
43
+
44
+ /* ── Animation variants driven by data-nc-transition set by nc-tabs ── */
45
+ :host([active]) .panel {
46
+ animation: nc-tab-fade 250ms ease both;
47
+ }
48
+
49
+ :host([data-nc-transition="fade"][active]) .panel {
50
+ animation: nc-tab-fade 250ms ease both;
51
+ }
52
+
53
+ :host([data-nc-transition="slide-up"][active]) .panel {
54
+ animation: nc-tab-slide-up 300ms ease both;
55
+ }
56
+
57
+ :host([data-nc-transition="slide-right"][active]) .panel {
58
+ animation: nc-tab-slide-right 300ms ease both;
59
+ }
60
+
61
+ :host([data-nc-transition="slide-left"][active]) .panel {
62
+ animation: nc-tab-slide-left 300ms ease both;
63
+ }
64
+
65
+ :host([data-nc-transition="slide-down"][active]) .panel {
66
+ animation: nc-tab-slide-down 300ms ease both;
67
+ }
68
+
69
+ :host([data-nc-transition="none"][active]) .panel {
70
+ animation: none;
71
+ }
72
+
73
+ /* ── Keyframes ───────────────────────────────────────────────── */
74
+ @keyframes nc-tab-fade {
75
+ from { opacity: 0; }
76
+ to { opacity: 1; }
77
+ }
78
+
79
+ @keyframes nc-tab-slide-up {
80
+ from { opacity: 0; transform: translateY(20px); }
81
+ to { opacity: 1; transform: translateY(0); }
82
+ }
83
+
84
+ @keyframes nc-tab-slide-right {
85
+ from { opacity: 0; transform: translateX(-24px); }
86
+ to { opacity: 1; transform: translateX(0); }
87
+ }
88
+
89
+ @keyframes nc-tab-slide-left {
90
+ from { opacity: 0; transform: translateX(24px); }
91
+ to { opacity: 1; transform: translateX(0); }
92
+ }
93
+
94
+ @keyframes nc-tab-slide-down {
95
+ from { opacity: 0; transform: translateY(-20px); }
96
+ to { opacity: 1; transform: translateY(0); }
97
+ }
98
+
99
+ .panel {
100
+ box-sizing: border-box;
101
+ padding: var(--nc-spacing-lg);
102
+ background: var(--nc-bg-secondary);
103
+ border-radius: var(--nc-radius-lg);
104
+ }
105
+
106
+ @media (max-width: 640px) {
107
+ .panel {
108
+ padding: var(--nc-spacing-sm);
109
+ border-radius: var(--nc-radius-md);
110
+ }
111
+ }
112
+ </style>
113
+ <div class="panel"><slot></slot></div>
114
+ `;
115
+ }
116
+ /**
117
+ * Override to suppress re-renders - :host([active]) CSS handles show/hide,
118
+ * and label/disabled changes are handled by nc-tabs rebuilding its bar.
119
+ * A full re-render would needlessly flash slot content on every tab click.
120
+ */
121
+ attributeChangedCallback(_name, _oldValue, _newValue) {
122
+ // intentionally empty - CSS selectors handle all visual state
123
+ }
124
+ onMount() { }
125
+ onUnmount() { }
126
+ }
127
+ defineComponent('nc-tab-item', NcTabItem);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * NcTable Component - lightweight sortable data table
3
+ *
4
+ * Renders a table from JSON data with optional sorting, striping, compact mode,
5
+ * sticky header, and simple empty state. Zero dependencies.
6
+ *
7
+ * Attributes:
8
+ * columns - JSON array of column defs:
9
+ * [{ key, label?, sortable?, align?, width?, format? }]
10
+ * format: 'text'(default)|'number'|'currency'|'date'|'badge'
11
+ * rows - JSON array of row objects
12
+ * sortable - boolean - enable sorting on all columns unless column.sortable=false
13
+ * striped - boolean - alternating row backgrounds
14
+ * compact - boolean - reduced cell padding
15
+ * sticky-header - boolean - sticky thead
16
+ * empty - empty state text (default: 'No data available')
17
+ * max-height - CSS value to constrain height and enable scrolling
18
+ *
19
+ * Events:
20
+ * sort - CustomEvent<{ key: string; direction: 'asc'|'desc' }>
21
+ * row-click - CustomEvent<{ row: Record<string, unknown>; index: number }>
22
+ *
23
+ * Usage:
24
+ * <nc-table
25
+ * sortable
26
+ * striped
27
+ * columns='[{"key":"name","label":"Name"},{"key":"role","label":"Role"}]'
28
+ * rows='[{"name":"Alice","role":"Admin"},{"name":"Bob","role":"Editor"}]'>
29
+ * </nc-table>
30
+ */
31
+ import { Component } from '../core/component.js';
32
+ export declare class NcTable extends Component {
33
+ static useShadowDOM: boolean;
34
+ static get observedAttributes(): string[];
35
+ private _sortKey;
36
+ private _sortDir;
37
+ private _parseColumns;
38
+ private _parseRows;
39
+ private _sortedRows;
40
+ private _fmt;
41
+ template(): string;
42
+ onMount(): void;
43
+ attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
44
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * NcTable Component - lightweight sortable data table
3
+ *
4
+ * Renders a table from JSON data with optional sorting, striping, compact mode,
5
+ * sticky header, and simple empty state. Zero dependencies.
6
+ *
7
+ * Attributes:
8
+ * columns - JSON array of column defs:
9
+ * [{ key, label?, sortable?, align?, width?, format? }]
10
+ * format: 'text'(default)|'number'|'currency'|'date'|'badge'
11
+ * rows - JSON array of row objects
12
+ * sortable - boolean - enable sorting on all columns unless column.sortable=false
13
+ * striped - boolean - alternating row backgrounds
14
+ * compact - boolean - reduced cell padding
15
+ * sticky-header - boolean - sticky thead
16
+ * empty - empty state text (default: 'No data available')
17
+ * max-height - CSS value to constrain height and enable scrolling
18
+ *
19
+ * Events:
20
+ * sort - CustomEvent<{ key: string; direction: 'asc'|'desc' }>
21
+ * row-click - CustomEvent<{ row: Record<string, unknown>; index: number }>
22
+ *
23
+ * Usage:
24
+ * <nc-table
25
+ * sortable
26
+ * striped
27
+ * columns='[{"key":"name","label":"Name"},{"key":"role","label":"Role"}]'
28
+ * rows='[{"name":"Alice","role":"Admin"},{"name":"Bob","role":"Editor"}]'>
29
+ * </nc-table>
30
+ */
31
+ import { Component, defineComponent } from '../core/component.js';
32
+ function esc(s) {
33
+ return String(s ?? '')
34
+ .replace(/&/g, '&amp;')
35
+ .replace(/</g, '&lt;')
36
+ .replace(/>/g, '&gt;')
37
+ .replace(/"/g, '&quot;');
38
+ }
39
+ export class NcTable extends Component {
40
+ static useShadowDOM = true;
41
+ static get observedAttributes() {
42
+ return ['columns', 'rows', 'sortable', 'striped', 'compact', 'sticky-header', 'empty', 'max-height'];
43
+ }
44
+ _sortKey = '';
45
+ _sortDir = 'asc';
46
+ _parseColumns() {
47
+ try {
48
+ const raw = this.getAttribute('columns') ?? '[]';
49
+ const cols = JSON.parse(raw);
50
+ return Array.isArray(cols) ? cols : [];
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ _parseRows() {
57
+ try {
58
+ const raw = this.getAttribute('rows') ?? '[]';
59
+ const rows = JSON.parse(raw);
60
+ return Array.isArray(rows) ? rows : [];
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ _sortedRows(rows, columns) {
67
+ if (!this._sortKey)
68
+ return rows;
69
+ const col = columns.find(c => c.key === this._sortKey);
70
+ if (!col)
71
+ return rows;
72
+ const dir = this._sortDir === 'asc' ? 1 : -1;
73
+ return [...rows].sort((a, b) => {
74
+ const va = a[this._sortKey];
75
+ const vb = b[this._sortKey];
76
+ if (va == null && vb == null)
77
+ return 0;
78
+ if (va == null)
79
+ return -1 * dir;
80
+ if (vb == null)
81
+ return 1 * dir;
82
+ if (typeof va === 'number' && typeof vb === 'number')
83
+ return (va - vb) * dir;
84
+ const sa = String(va).toLowerCase();
85
+ const sb = String(vb).toLowerCase();
86
+ return sa.localeCompare(sb) * dir;
87
+ });
88
+ }
89
+ _fmt(value, col) {
90
+ if (value == null)
91
+ return '';
92
+ switch (col.format) {
93
+ case 'number':
94
+ return typeof value === 'number' ? String(value) : esc(value);
95
+ case 'currency':
96
+ return typeof value === 'number'
97
+ ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value)
98
+ : esc(value);
99
+ case 'date': {
100
+ const d = new Date(String(value));
101
+ return isNaN(d.getTime()) ? esc(value) : d.toLocaleDateString();
102
+ }
103
+ case 'badge':
104
+ return `<span class="badge">${esc(value)}</span>`;
105
+ default:
106
+ return esc(value);
107
+ }
108
+ }
109
+ template() {
110
+ const columns = this._parseColumns();
111
+ const rows = this._sortedRows(this._parseRows(), columns);
112
+ const striped = this.hasAttribute('striped');
113
+ const compact = this.hasAttribute('compact');
114
+ const stickyHeader = this.hasAttribute('sticky-header');
115
+ const emptyText = this.getAttribute('empty') ?? 'No data available';
116
+ const maxHeight = this.getAttribute('max-height') ?? '';
117
+ const sortableAll = this.hasAttribute('sortable');
118
+ const tableRows = rows.length === 0
119
+ ? `<tr><td class="empty" colspan="${Math.max(columns.length, 1)}">${esc(emptyText)}</td></tr>`
120
+ : rows.map((row, rowIndex) => `
121
+ <tr data-row-index="${rowIndex}">
122
+ ${columns.map(col => {
123
+ const align = col.align ?? 'left';
124
+ return `<td style="text-align:${align}">${this._fmt(row[col.key], col)}</td>`;
125
+ }).join('')}
126
+ </tr>
127
+ `).join('');
128
+ const headers = columns.map(col => {
129
+ const align = col.align ?? 'left';
130
+ const sortable = sortableAll && col.sortable !== false;
131
+ const active = this._sortKey === col.key;
132
+ const arrow = active ? (this._sortDir === 'asc' ? '▲' : '▼') : '';
133
+ return `
134
+ <th style="text-align:${align};${col.width ? `width:${col.width};` : ''}">
135
+ <button class="head-btn ${sortable ? 'is-sortable' : ''} ${active ? 'is-active' : ''}" type="button" ${sortable ? `data-sort-key="${col.key}"` : 'disabled'}>
136
+ <span>${esc(col.label ?? col.key)}</span>
137
+ <span class="sort-indicator">${arrow}</span>
138
+ </button>
139
+ </th>
140
+ `;
141
+ }).join('');
142
+ return `
143
+ <style>
144
+ :host { display: block; font-family: var(--nc-font-family); }
145
+ .wrap {
146
+ border: 1px solid var(--nc-border);
147
+ border-radius: var(--nc-radius-lg);
148
+ overflow: auto;
149
+ background: var(--nc-bg);
150
+ ${maxHeight ? `max-height:${maxHeight};` : ''}
151
+ }
152
+ table {
153
+ width: 100%;
154
+ border-collapse: collapse;
155
+ min-width: 480px;
156
+ }
157
+ thead th {
158
+ position: ${stickyHeader ? 'sticky' : 'static'};
159
+ top: 0;
160
+ z-index: 1;
161
+ background: var(--nc-bg-secondary);
162
+ border-bottom: 1px solid var(--nc-border);
163
+ padding: 0;
164
+ font-size: var(--nc-font-size-xs);
165
+ text-transform: uppercase;
166
+ letter-spacing: .04em;
167
+ color: var(--nc-text-muted);
168
+ }
169
+ .head-btn {
170
+ width: 100%;
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: space-between;
174
+ gap: 8px;
175
+ padding: ${compact ? '10px 12px' : '14px 16px'};
176
+ background: none;
177
+ border: none;
178
+ cursor: default;
179
+ font: inherit;
180
+ color: inherit;
181
+ text-align: inherit;
182
+ }
183
+ .head-btn.is-sortable { cursor: pointer; }
184
+ .head-btn.is-sortable:hover { background: rgba(0,0,0,.03); }
185
+ .head-btn.is-active { color: var(--nc-text); }
186
+ tbody tr {
187
+ transition: background var(--nc-transition-fast);
188
+ cursor: pointer;
189
+ }
190
+ tbody tr:hover { background: rgba(0,0,0,.02); }
191
+ ${striped ? 'tbody tr:nth-child(even) { background: var(--nc-bg-secondary); }' : ''}
192
+ td {
193
+ padding: ${compact ? '10px 12px' : '14px 16px'};
194
+ border-bottom: 1px solid var(--nc-border);
195
+ font-size: var(--nc-font-size-sm);
196
+ color: var(--nc-text-secondary);
197
+ vertical-align: top;
198
+ }
199
+ tbody tr:last-child td { border-bottom: none; }
200
+ .empty {
201
+ text-align: center;
202
+ color: var(--nc-text-muted);
203
+ padding: 28px 16px;
204
+ cursor: default;
205
+ }
206
+ .badge {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ padding: 2px 8px;
210
+ border-radius: 99px;
211
+ background: rgba(var(--nc-primary-rgb, 99,102,241), .12);
212
+ color: var(--nc-primary);
213
+ font-size: var(--nc-font-size-xs);
214
+ font-weight: var(--nc-font-weight-medium);
215
+ }
216
+ .sort-indicator {
217
+ min-width: 1em;
218
+ font-size: 10px;
219
+ text-align: center;
220
+ color: var(--nc-text-muted);
221
+ }
222
+ </style>
223
+ <div class="wrap">
224
+ <table role="table">
225
+ <thead><tr>${headers}</tr></thead>
226
+ <tbody>${tableRows}</tbody>
227
+ </table>
228
+ </div>
229
+ `;
230
+ }
231
+ onMount() {
232
+ this.shadowRoot.addEventListener('click', (e) => {
233
+ const sortBtn = e.target.closest('[data-sort-key]');
234
+ if (sortBtn) {
235
+ const key = sortBtn.dataset.sortKey ?? '';
236
+ if (this._sortKey === key)
237
+ this._sortDir = this._sortDir === 'asc' ? 'desc' : 'asc';
238
+ else {
239
+ this._sortKey = key;
240
+ this._sortDir = 'asc';
241
+ }
242
+ this.render();
243
+ this.dispatchEvent(new CustomEvent('sort', {
244
+ detail: { key: this._sortKey, direction: this._sortDir }, bubbles: true, composed: true,
245
+ }));
246
+ return;
247
+ }
248
+ const row = e.target.closest('tbody tr[data-row-index]');
249
+ if (row) {
250
+ const index = parseInt(row.dataset.rowIndex ?? '-1', 10);
251
+ const rows = this._sortedRows(this._parseRows(), this._parseColumns());
252
+ if (index >= 0 && rows[index]) {
253
+ this.dispatchEvent(new CustomEvent('row-click', {
254
+ detail: { row: rows[index], index }, bubbles: true, composed: true,
255
+ }));
256
+ }
257
+ }
258
+ });
259
+ }
260
+ attributeChangedCallback(name, oldValue, newValue) {
261
+ if (oldValue !== newValue && this._mounted)
262
+ this.render();
263
+ }
264
+ }
265
+ defineComponent('nc-table', NcTable);