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,279 @@
1
+ /**
2
+ * NcTagInput Component — text input that creates dismissible tag chips
3
+ *
4
+ * Attributes:
5
+ * placeholder — input placeholder text
6
+ * value — comma-separated initial tags (e.g. "react,vue,svelte")
7
+ * max — maximum number of tags allowed
8
+ * min-length — minimum character length for a tag (default: 1)
9
+ * max-length — maximum character length per tag
10
+ * delimiter — character(s) that trigger tag creation in addition to Enter (default: ',')
11
+ * disabled — boolean
12
+ * readonly — boolean — show tags but cannot add/remove
13
+ * duplicate — boolean — allow duplicate tags (default: false)
14
+ * variant — 'default'|'filled' (default: 'default')
15
+ * label — visible label text
16
+ * hint — helper text below input
17
+ * error — error message (shown in red)
18
+ *
19
+ * Events:
20
+ * change — CustomEvent<{ tags: string[] }> — tag list changed
21
+ * add — CustomEvent<{ tag: string }>
22
+ * remove — CustomEvent<{ tag: string; index: number }>
23
+ * max-reached — CustomEvent — fired when max is exceeded
24
+ *
25
+ * Methods:
26
+ * el.getTags() — string[]
27
+ * el.setTags(tags) — replace all tags
28
+ * el.addTag(tag) — programmatic add
29
+ * el.removeTag(index) — programmatic remove
30
+ * el.clear() — remove all tags
31
+ */
32
+ import { Component, defineComponent } from '@core/component.js';
33
+
34
+ export class NcTagInput extends Component {
35
+ static useShadowDOM = true;
36
+
37
+ private _tags: string[] = [];
38
+
39
+ static get observedAttributes() {
40
+ return ['value', 'disabled', 'readonly', 'error'];
41
+ }
42
+
43
+ connectedCallback() {
44
+ super.connectedCallback?.();
45
+ const raw = this.getAttribute('value') ?? '';
46
+ if (raw) this._tags = raw.split(',').map(t => t.trim()).filter(Boolean);
47
+ }
48
+
49
+ template() {
50
+ const placeholder = this.getAttribute('placeholder') ?? 'Add tag...';
51
+ const label = this.getAttribute('label') ?? '';
52
+ const hint = this.getAttribute('hint') ?? '';
53
+ const error = this.getAttribute('error') ?? '';
54
+ const disabled = this.hasAttribute('disabled');
55
+ const readonly = this.hasAttribute('readonly');
56
+ const variant = this.getAttribute('variant') ?? 'default';
57
+
58
+ const bg = variant === 'filled' ? 'var(--nc-bg-secondary)' : 'var(--nc-bg)';
59
+ const border = error ? 'var(--nc-danger)' : 'var(--nc-border)';
60
+
61
+ const tagsHtml = this._tags.map((tag, i) => `
62
+ <span class="tag" data-index="${i}">
63
+ <span class="tag-text">${this._escape(tag)}</span>
64
+ ${!disabled && !readonly
65
+ ? `<button class="tag-remove" type="button" data-index="${i}" aria-label="Remove ${this._escape(tag)}">
66
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 12 12" fill="none">
67
+ <path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
68
+ </svg>
69
+ </button>`
70
+ : ''}
71
+ </span>
72
+ `).join('');
73
+
74
+ return `
75
+ <style>
76
+ :host { display: block; font-family: var(--nc-font-family); }
77
+ .label {
78
+ display: block;
79
+ font-size: var(--nc-font-size-sm);
80
+ font-weight: var(--nc-font-weight-medium);
81
+ color: var(--nc-text);
82
+ margin-bottom: 6px;
83
+ }
84
+ .field {
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ align-items: center;
88
+ gap: 6px;
89
+ padding: 8px 10px;
90
+ background: ${bg};
91
+ border: 1px solid ${border};
92
+ border-radius: var(--nc-radius-md);
93
+ min-height: 42px;
94
+ cursor: ${disabled ? 'not-allowed' : 'text'};
95
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
96
+ opacity: ${disabled ? 0.5 : 1};
97
+ }
98
+ .field:focus-within {
99
+ border-color: var(--nc-primary);
100
+ box-shadow: 0 0 0 3px rgba(var(--nc-primary-rgb, 99,102,241),.15);
101
+ }
102
+ .tag {
103
+ display: inline-flex;
104
+ align-items: center;
105
+ gap: 4px;
106
+ background: var(--nc-primary);
107
+ color: var(--nc-white);
108
+ border-radius: var(--nc-radius-sm);
109
+ padding: 2px 8px;
110
+ font-size: var(--nc-font-size-xs);
111
+ font-weight: var(--nc-font-weight-medium);
112
+ line-height: 1.6;
113
+ white-space: nowrap;
114
+ max-width: 200px;
115
+ }
116
+ .tag-text {
117
+ overflow: hidden;
118
+ text-overflow: ellipsis;
119
+ white-space: nowrap;
120
+ }
121
+ .tag-remove {
122
+ background: none;
123
+ border: none;
124
+ padding: 0;
125
+ cursor: pointer;
126
+ color: inherit;
127
+ opacity: 0.7;
128
+ display: flex;
129
+ align-items: center;
130
+ line-height: 1;
131
+ flex-shrink: 0;
132
+ }
133
+ .tag-remove:hover { opacity: 1; }
134
+ input {
135
+ flex: 1 1 80px;
136
+ min-width: 80px;
137
+ border: none;
138
+ outline: none;
139
+ background: transparent;
140
+ font-family: inherit;
141
+ font-size: var(--nc-font-size-sm);
142
+ color: var(--nc-text);
143
+ padding: 0;
144
+ caret-color: var(--nc-primary);
145
+ }
146
+ input::placeholder { color: var(--nc-text-muted); }
147
+ input:disabled { cursor: not-allowed; }
148
+ .hint { font-size: var(--nc-font-size-xs); color: var(--nc-text-muted); margin-top: 5px; }
149
+ .error { font-size: var(--nc-font-size-xs); color: var(--nc-danger); margin-top: 5px; }
150
+ </style>
151
+ ${label ? `<label class="label">${label}</label>` : ''}
152
+ <div class="field" id="field">
153
+ ${tagsHtml}
154
+ ${!readonly
155
+ ? `<input
156
+ type="text"
157
+ id="input"
158
+ placeholder="${this._tags.length === 0 ? placeholder : ''}"
159
+ ${disabled ? 'disabled' : ''}
160
+ autocomplete="off"
161
+ spellcheck="false"
162
+ />`
163
+ : ''}
164
+ </div>
165
+ ${error ? `<p class="error">${error}</p>` : hint ? `<p class="hint">${hint}</p>` : ''}
166
+ `;
167
+ }
168
+
169
+ onMount() {
170
+ this._bindEvents();
171
+ }
172
+
173
+ private _bindEvents() {
174
+ const input = this.$<HTMLInputElement>('#input');
175
+ if (!input) return;
176
+
177
+ const delimiter = this.getAttribute('delimiter') ?? ',';
178
+
179
+ input.addEventListener('keydown', (e: KeyboardEvent) => {
180
+ if (e.key === 'Enter' || (delimiter && e.key === delimiter)) {
181
+ e.preventDefault();
182
+ const val = input.value.trim();
183
+ if (val) { this.addTag(val); input.value = ''; }
184
+ } else if (e.key === 'Backspace' && input.value === '' && this._tags.length > 0) {
185
+ this.removeTag(this._tags.length - 1);
186
+ }
187
+ });
188
+
189
+ input.addEventListener('input', () => {
190
+ const val = input.value;
191
+ const delim = this.getAttribute('delimiter') ?? ',';
192
+ if (delim && val.endsWith(delim)) {
193
+ const tag = val.slice(0, -delim.length).trim();
194
+ if (tag) { this.addTag(tag); input.value = ''; }
195
+ }
196
+ });
197
+
198
+ // Clicking the field focuses the input
199
+ this.$<HTMLElement>('#field')?.addEventListener('click', (e) => {
200
+ if ((e.target as HTMLElement).closest('.tag-remove')) return;
201
+ input.focus();
202
+ });
203
+
204
+ this.shadowRoot!.addEventListener('click', (e) => {
205
+ const btn = (e.target as HTMLElement).closest<HTMLElement>('.tag-remove');
206
+ if (btn) {
207
+ const idx = parseInt(btn.dataset.index ?? '-1', 10);
208
+ if (idx >= 0) this.removeTag(idx);
209
+ }
210
+ });
211
+ }
212
+
213
+ // ── Public API ─────────────────────────────────────────────────────────────
214
+
215
+ getTags(): string[] { return [...this._tags]; }
216
+
217
+ setTags(tags: string[]) {
218
+ this._tags = [...tags];
219
+ this.render();
220
+ this._bindEvents();
221
+ this._emit('change');
222
+ }
223
+
224
+ addTag(tag: string) {
225
+ const maxAttr = this.getAttribute('max');
226
+ const minLen = parseInt(this.getAttribute('min-length') ?? '1', 10);
227
+ const maxLen = this.getAttribute('max-length');
228
+ const allowDup = this.hasAttribute('duplicate');
229
+
230
+ tag = tag.trim();
231
+ if (!tag || tag.length < minLen) return;
232
+ if (maxLen && tag.length > parseInt(maxLen, 10)) return;
233
+ if (!allowDup && this._tags.includes(tag)) return;
234
+ if (maxAttr && this._tags.length >= parseInt(maxAttr, 10)) {
235
+ this.dispatchEvent(new CustomEvent('max-reached', { bubbles: true, composed: true }));
236
+ return;
237
+ }
238
+
239
+ this._tags.push(tag);
240
+ this.render();
241
+ this._bindEvents();
242
+ this.dispatchEvent(new CustomEvent('add', { detail: { tag }, bubbles: true, composed: true }));
243
+ this._emit('change');
244
+ }
245
+
246
+ removeTag(index: number) {
247
+ const tag = this._tags[index];
248
+ if (tag === undefined) return;
249
+ this._tags.splice(index, 1);
250
+ this.render();
251
+ this._bindEvents();
252
+ this.dispatchEvent(new CustomEvent('remove', { detail: { tag, index }, bubbles: true, composed: true }));
253
+ this._emit('change');
254
+ this.$<HTMLInputElement>('#input')?.focus();
255
+ }
256
+
257
+ clear() { this.setTags([]); }
258
+
259
+ private _emit(event: string) {
260
+ this.dispatchEvent(new CustomEvent(event, {
261
+ detail: { tags: [...this._tags] }, bubbles: true, composed: true,
262
+ }));
263
+ }
264
+
265
+ private _escape(s: string) {
266
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
267
+ }
268
+
269
+ attributeChangedCallback(name: string, oldVal: string, newVal: string) {
270
+ if (oldVal === newVal || !this._mounted) return;
271
+ if (name === 'value') {
272
+ this._tags = (newVal ?? '').split(',').map(t => t.trim()).filter(Boolean);
273
+ }
274
+ this.render();
275
+ this._bindEvents();
276
+ }
277
+ }
278
+
279
+ defineComponent('nc-tag-input', NcTagInput);
@@ -0,0 +1,216 @@
1
+ /**
2
+ * NcTextarea Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Attributes:
7
+ * - name: string — form field name
8
+ * - value: string — current value
9
+ * - placeholder: string — placeholder text
10
+ * - rows: number — visible row count (default: 4)
11
+ * - disabled: boolean — disabled state
12
+ * - readonly: boolean — read-only state
13
+ * - maxlength: number — character limit (shows counter when set)
14
+ * - autoresize: boolean — grow to fit content automatically
15
+ * - size: 'sm' | 'md' | 'lg' (default: 'md')
16
+ * - variant: 'default' | 'filled' (default: 'default')
17
+ *
18
+ * Events:
19
+ * - input: CustomEvent<{ value: string; name: string }>
20
+ * - change: CustomEvent<{ value: string; name: string }>
21
+ *
22
+ * Usage:
23
+ * <nc-textarea name="bio" placeholder="Tell us about yourself" rows="4"></nc-textarea>
24
+ * <nc-textarea name="notes" maxlength="200" autoresize></nc-textarea>
25
+ */
26
+
27
+ import { Component, defineComponent } from '@core/component.js';
28
+
29
+ export class NcTextarea extends Component {
30
+ static useShadowDOM = true;
31
+
32
+ static attributeOptions = {
33
+ variant: ['default', 'filled'],
34
+ size: ['sm', 'md', 'lg']
35
+ };
36
+
37
+ static get observedAttributes() {
38
+ return ['name', 'value', 'placeholder', 'rows', 'disabled', 'readonly', 'maxlength', 'autoresize', 'size', 'variant'];
39
+ }
40
+
41
+ constructor() {
42
+ super();
43
+ }
44
+
45
+ template() {
46
+ const value = this.getAttribute('value') || '';
47
+ const placeholder = this.getAttribute('placeholder') || '';
48
+ const rows = this.getAttribute('rows') || '4';
49
+ const disabled = this.hasAttribute('disabled');
50
+ const readonly = this.hasAttribute('readonly');
51
+ const maxlength = this.getAttribute('maxlength');
52
+ const autoresize = this.hasAttribute('autoresize');
53
+ const charCount = value.length;
54
+
55
+ return `
56
+ <style>
57
+ :host {
58
+ display: block;
59
+ font-family: var(--nc-font-family);
60
+ width: 100%;
61
+ }
62
+
63
+ .wrap {
64
+ position: relative;
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: var(--nc-spacing-xs);
68
+ }
69
+
70
+ textarea {
71
+ width: 100%;
72
+ box-sizing: border-box;
73
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
74
+ background: var(--nc-bg);
75
+ border: var(--nc-input-border);
76
+ border-radius: var(--nc-input-radius);
77
+ color: var(--nc-text);
78
+ font-size: var(--nc-font-size-base);
79
+ font-family: var(--nc-font-family);
80
+ line-height: var(--nc-line-height-normal);
81
+ resize: ${autoresize ? 'none' : 'vertical'};
82
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
83
+ outline: none;
84
+ min-height: 80px;
85
+ opacity: ${disabled ? '0.5' : '1'};
86
+ cursor: ${disabled ? 'not-allowed' : 'auto'};
87
+ }
88
+
89
+ :host([size="sm"]) textarea {
90
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
91
+ font-size: var(--nc-font-size-sm);
92
+ }
93
+
94
+ :host([size="lg"]) textarea {
95
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
96
+ font-size: var(--nc-font-size-lg);
97
+ }
98
+
99
+ :host([variant="filled"]) textarea {
100
+ background: var(--nc-bg-tertiary);
101
+ border-color: transparent;
102
+ }
103
+
104
+ :host([variant="filled"]) textarea:hover:not(:disabled) {
105
+ background: var(--nc-bg-secondary);
106
+ }
107
+
108
+ textarea:hover:not(:disabled) {
109
+ border-color: var(--nc-input-focus-border);
110
+ }
111
+
112
+ textarea:focus {
113
+ border-color: var(--nc-input-focus-border);
114
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
115
+ }
116
+
117
+ textarea::placeholder {
118
+ color: var(--nc-text-muted);
119
+ }
120
+
121
+ .counter {
122
+ align-self: flex-end;
123
+ font-size: var(--nc-font-size-xs);
124
+ color: var(--nc-text-muted);
125
+ line-height: 1;
126
+ }
127
+
128
+ .counter.over {
129
+ color: var(--nc-danger);
130
+ font-weight: var(--nc-font-weight-semibold);
131
+ }
132
+ </style>
133
+
134
+ <div class="wrap">
135
+ <textarea
136
+ rows="${rows}"
137
+ ${maxlength ? `maxlength="${maxlength}"` : ''}
138
+ ${disabled ? 'disabled' : ''}
139
+ ${readonly ? 'readonly' : ''}
140
+ placeholder="${placeholder}"
141
+ name="${this.getAttribute('name') || ''}"
142
+ aria-multiline="true"
143
+ >${value}</textarea>
144
+ ${maxlength ? `
145
+ <span class="counter${charCount > Number(maxlength) ? ' over' : ''}">${charCount} / ${maxlength}</span>` : ''}
146
+ </div>
147
+ `;
148
+ }
149
+
150
+ onMount() {
151
+ this._bindEvents();
152
+ }
153
+
154
+ private _bindEvents() {
155
+ const ta = this.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea');
156
+ if (!ta) return;
157
+
158
+ if (this.hasAttribute('autoresize')) {
159
+ this._autoResize(ta);
160
+ }
161
+
162
+ ta.addEventListener('input', () => {
163
+ if (this.hasAttribute('autoresize')) this._autoResize(ta);
164
+ this._updateCounter(ta.value);
165
+
166
+ this.dispatchEvent(new CustomEvent('input', {
167
+ bubbles: true, composed: true,
168
+ detail: { value: ta.value, name: this.getAttribute('name') || '' }
169
+ }));
170
+ });
171
+
172
+ ta.addEventListener('change', () => {
173
+ this.setAttribute('value', ta.value);
174
+ this.dispatchEvent(new CustomEvent('change', {
175
+ bubbles: true, composed: true,
176
+ detail: { value: ta.value, name: this.getAttribute('name') || '' }
177
+ }));
178
+ });
179
+ }
180
+
181
+ private _autoResize(ta: HTMLTextAreaElement) {
182
+ ta.style.height = 'auto';
183
+ ta.style.height = `${ta.scrollHeight}px`;
184
+ }
185
+
186
+ private _updateCounter(value: string) {
187
+ const maxlength = this.getAttribute('maxlength');
188
+ if (!maxlength) return;
189
+ const counter = this.shadowRoot!.querySelector('.counter');
190
+ if (!counter) return;
191
+ const count = value.length;
192
+ const max = Number(maxlength);
193
+ counter.textContent = `${count} / ${maxlength}`;
194
+ counter.classList.toggle('over', count > max);
195
+ }
196
+
197
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
198
+ if (oldValue === newValue) return;
199
+ // Don't rebuild DOM for live value changes — the textarea manages its own value
200
+ if (name === 'value' && this._mounted) {
201
+ const ta = this.shadowRoot!.querySelector<HTMLTextAreaElement>('textarea');
202
+ if (ta) {
203
+ ta.value = newValue || '';
204
+ this._updateCounter(ta.value);
205
+ if (this.hasAttribute('autoresize')) this._autoResize(ta);
206
+ }
207
+ return;
208
+ }
209
+ if (this._mounted) {
210
+ this.render();
211
+ this._bindEvents();
212
+ }
213
+ }
214
+ }
215
+
216
+ defineComponent('nc-textarea', NcTextarea);