@arclux/arc-ui 1.0.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/package.json +186 -0
  4. package/src/content/accordion-item.js +27 -0
  5. package/src/content/accordion.js +151 -0
  6. package/src/content/animated-number.js +160 -0
  7. package/src/content/aspect-ratio.js +78 -0
  8. package/src/content/avatar-group.js +101 -0
  9. package/src/content/avatar.js +92 -0
  10. package/src/content/badge.js +98 -0
  11. package/src/content/callout.js +141 -0
  12. package/src/content/card.js +75 -0
  13. package/src/content/carousel.js +300 -0
  14. package/src/content/code-block.js +152 -0
  15. package/src/content/collapsible.js +142 -0
  16. package/src/content/color-swatch.js +86 -0
  17. package/src/content/column.js +28 -0
  18. package/src/content/data-table.js +332 -0
  19. package/src/content/divider.js +153 -0
  20. package/src/content/empty-state.js +78 -0
  21. package/src/content/feature-card.js +142 -0
  22. package/src/content/highlight.js +63 -0
  23. package/src/content/icon-library.js +30 -0
  24. package/src/content/icon-registry.js +39 -0
  25. package/src/content/icon.js +95 -0
  26. package/src/content/index.js +44 -0
  27. package/src/content/infinite-scroll.js +144 -0
  28. package/src/content/kbd.js +40 -0
  29. package/src/content/markdown.js +294 -0
  30. package/src/content/marquee.js +166 -0
  31. package/src/content/meter.js +167 -0
  32. package/src/content/scroll-area.js +107 -0
  33. package/src/content/skeleton.js +85 -0
  34. package/src/content/spinner.js +77 -0
  35. package/src/content/stack.js +68 -0
  36. package/src/content/stat.js +72 -0
  37. package/src/content/step.js +22 -0
  38. package/src/content/stepper.js +202 -0
  39. package/src/content/table.js +134 -0
  40. package/src/content/tag.js +156 -0
  41. package/src/content/text.js +111 -0
  42. package/src/content/timeline-item.js +29 -0
  43. package/src/content/timeline.js +170 -0
  44. package/src/content/truncate.js +161 -0
  45. package/src/content/value-card.js +94 -0
  46. package/src/feedback/alert.js +187 -0
  47. package/src/feedback/command-item.js +28 -0
  48. package/src/feedback/command-palette.js +346 -0
  49. package/src/feedback/context-menu.js +299 -0
  50. package/src/feedback/dialog.js +298 -0
  51. package/src/feedback/dropdown-menu.js +259 -0
  52. package/src/feedback/hover-card.js +186 -0
  53. package/src/feedback/index.js +17 -0
  54. package/src/feedback/modal.js +226 -0
  55. package/src/feedback/notification-panel.js +196 -0
  56. package/src/feedback/popover.js +184 -0
  57. package/src/feedback/progress.js +169 -0
  58. package/src/feedback/sheet.js +249 -0
  59. package/src/feedback/toast.js +207 -0
  60. package/src/feedback/tooltip.js +189 -0
  61. package/src/icons/lucide.d.ts +1915 -0
  62. package/src/icons/lucide.js +1915 -0
  63. package/src/icons/phosphor.d.ts +1517 -0
  64. package/src/icons/phosphor.js +1517 -0
  65. package/src/icons/types.d.ts +8 -0
  66. package/src/index.js +9 -0
  67. package/src/input/button.js +127 -0
  68. package/src/input/calendar.js +340 -0
  69. package/src/input/checkbox.js +159 -0
  70. package/src/input/chip.js +120 -0
  71. package/src/input/color-picker.js +461 -0
  72. package/src/input/combobox.js +295 -0
  73. package/src/input/copy-button.js +144 -0
  74. package/src/input/date-picker.js +534 -0
  75. package/src/input/file-upload.js +333 -0
  76. package/src/input/form.js +179 -0
  77. package/src/input/icon-button.js +179 -0
  78. package/src/input/index.js +31 -0
  79. package/src/input/input.js +158 -0
  80. package/src/input/multi-select.js +392 -0
  81. package/src/input/number-input.js +239 -0
  82. package/src/input/otp-input.js +221 -0
  83. package/src/input/pin-input.js +294 -0
  84. package/src/input/radio-group.js +177 -0
  85. package/src/input/radio.js +28 -0
  86. package/src/input/rating.js +195 -0
  87. package/src/input/search.js +371 -0
  88. package/src/input/segmented-control.js +174 -0
  89. package/src/input/select.js +267 -0
  90. package/src/input/slider.js +217 -0
  91. package/src/input/sortable-list.js +345 -0
  92. package/src/input/suggestion.js +26 -0
  93. package/src/input/textarea.js +203 -0
  94. package/src/input/theme-toggle.js +196 -0
  95. package/src/input/toggle.js +166 -0
  96. package/src/layout/app-shell.js +266 -0
  97. package/src/layout/auth-shell.js +153 -0
  98. package/src/layout/container.js +37 -0
  99. package/src/layout/dashboard-grid.js +62 -0
  100. package/src/layout/index.js +15 -0
  101. package/src/layout/page-header.js +100 -0
  102. package/src/layout/page-layout.js +112 -0
  103. package/src/layout/resizable.js +221 -0
  104. package/src/layout/section.js +54 -0
  105. package/src/layout/settings-layout.js +91 -0
  106. package/src/layout/split-pane.js +172 -0
  107. package/src/layout/status-bar.js +84 -0
  108. package/src/layout/toolbar.js +92 -0
  109. package/src/navigation/breadcrumb-item.js +26 -0
  110. package/src/navigation/breadcrumb.js +129 -0
  111. package/src/navigation/drawer.js +183 -0
  112. package/src/navigation/footer.js +99 -0
  113. package/src/navigation/index.js +22 -0
  114. package/src/navigation/link.js +120 -0
  115. package/src/navigation/nav-item.js +46 -0
  116. package/src/navigation/navigation-menu.js +687 -0
  117. package/src/navigation/pagination.js +186 -0
  118. package/src/navigation/scroll-spy.js +198 -0
  119. package/src/navigation/scroll-to-top.js +163 -0
  120. package/src/navigation/sidebar-link.js +28 -0
  121. package/src/navigation/sidebar-section.js +45 -0
  122. package/src/navigation/sidebar.js +336 -0
  123. package/src/navigation/spy-link.js +26 -0
  124. package/src/navigation/tab.js +26 -0
  125. package/src/navigation/tabs.js +202 -0
  126. package/src/navigation/top-bar.js +263 -0
  127. package/src/navigation/tree-item.js +38 -0
  128. package/src/navigation/tree-view.js +255 -0
  129. package/src/shared/index.js +6 -0
  130. package/src/shared/menu-divider.js +9 -0
  131. package/src/shared/menu-item.js +30 -0
  132. package/src/shared/option.js +31 -0
  133. package/src/shared-styles.js +81 -0
  134. package/src/tokens.js +445 -0
  135. package/types/index.d.ts +973 -0
@@ -0,0 +1,333 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { tokenStyles } from '../shared-styles.js';
3
+
4
+ export class ArcFileUpload extends LitElement {
5
+ static properties = {
6
+ accept: { type: String },
7
+ multiple: { type: Boolean },
8
+ maxSize: { type: Number, attribute: 'max-size' },
9
+ disabled: { type: Boolean, reflect: true },
10
+ _files: { state: true },
11
+ _dragOver: { state: true },
12
+ _error: { state: true },
13
+ };
14
+
15
+ static styles = [
16
+ tokenStyles,
17
+ css`
18
+ :host { display: block; font-family: var(--font-body); }
19
+ :host([disabled]) { opacity: 0.4; pointer-events: none; }
20
+
21
+ .dropzone {
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ justify-content: center;
26
+ gap: var(--space-sm);
27
+ padding: var(--space-xl) var(--space-lg);
28
+ border: 2px dashed var(--border-default);
29
+ border-radius: var(--radius-lg);
30
+ background: var(--bg-surface);
31
+ cursor: pointer;
32
+ transition: border-color var(--transition-fast), background var(--transition-fast);
33
+ text-align: center;
34
+ }
35
+
36
+ .dropzone:hover {
37
+ border-color: var(--border-bright);
38
+ background: var(--bg-card);
39
+ }
40
+
41
+ .dropzone.drag-over {
42
+ border-color: rgba(var(--accent-primary-rgb), 0.4);
43
+ background: rgba(var(--accent-primary-rgb), 0.05);
44
+ box-shadow: var(--focus-glow);
45
+ }
46
+
47
+ .dropzone:focus-visible {
48
+ outline: none;
49
+ box-shadow: var(--focus-glow);
50
+ }
51
+
52
+ .upload-icon {
53
+ font-size: var(--text-md);
54
+ color: var(--text-muted);
55
+ line-height: 1;
56
+ transition: color var(--transition-fast);
57
+ }
58
+
59
+ .drag-over .upload-icon {
60
+ color: var(--accent-primary);
61
+ }
62
+
63
+ .upload-text {
64
+ font-size: var(--text-sm);
65
+ color: var(--text-secondary);
66
+ }
67
+
68
+ .upload-hint {
69
+ font-size: var(--text-sm);
70
+ color: var(--text-muted);
71
+ }
72
+
73
+ .browse-link {
74
+ color: var(--accent-primary);
75
+ text-decoration: underline;
76
+ cursor: pointer;
77
+ }
78
+
79
+ input[type="file"] {
80
+ display: none;
81
+ }
82
+
83
+ .file-list {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: var(--space-xs);
87
+ margin-top: var(--space-md);
88
+ }
89
+
90
+ .file-item {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: var(--space-sm);
94
+ padding: var(--space-xs) var(--space-sm);
95
+ background: var(--bg-card);
96
+ border: 1px solid var(--border-subtle);
97
+ border-radius: var(--radius-md);
98
+ font-size: var(--text-sm);
99
+ color: var(--text-secondary);
100
+ }
101
+
102
+ .file-icon {
103
+ flex-shrink: 0;
104
+ color: var(--text-muted);
105
+ font-size: var(--text-sm);
106
+ }
107
+
108
+ .file-name {
109
+ flex: 1;
110
+ overflow: hidden;
111
+ text-overflow: ellipsis;
112
+ white-space: nowrap;
113
+ color: var(--text-primary);
114
+ }
115
+
116
+ .file-size {
117
+ flex-shrink: 0;
118
+ color: var(--text-muted);
119
+ font-size: var(--text-sm);
120
+ }
121
+
122
+ .file-remove {
123
+ flex-shrink: 0;
124
+ background: none;
125
+ border: none;
126
+ color: var(--text-muted);
127
+ cursor: pointer;
128
+ font-size: var(--text-md);
129
+ padding: 2px calc(var(--space-xs) + 2px); /* cosmetic 2px vertical for tight remove button */
130
+ border-radius: var(--radius-sm);
131
+ line-height: 1;
132
+ transition: color var(--transition-fast), background var(--transition-fast);
133
+ }
134
+
135
+ .file-remove:hover {
136
+ color: var(--color-error);
137
+ background: var(--color-error-subtle);
138
+ }
139
+
140
+ .file-remove:focus-visible {
141
+ outline: none;
142
+ box-shadow: var(--focus-glow);
143
+ }
144
+
145
+ .error-message {
146
+ margin-top: var(--space-xs);
147
+ font-size: var(--text-sm);
148
+ color: var(--color-error);
149
+ }
150
+
151
+ @media (prefers-reduced-motion: reduce) {
152
+ :host *,
153
+ :host *::before,
154
+ :host *::after {
155
+ animation-duration: 0.01ms !important;
156
+ animation-iteration-count: 1 !important;
157
+ transition-duration: 0.01ms !important;
158
+ }
159
+ }
160
+ `,
161
+ ];
162
+
163
+ constructor() {
164
+ super();
165
+ this.accept = '';
166
+ this.multiple = false;
167
+ this.maxSize = 0;
168
+ this.disabled = false;
169
+ this._files = [];
170
+ this._dragOver = false;
171
+ this._error = '';
172
+ }
173
+
174
+ _formatSize(bytes) {
175
+ if (bytes === 0) return '0 B';
176
+ const units = ['B', 'KB', 'MB', 'GB'];
177
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
178
+ return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
179
+ }
180
+
181
+ _validateFiles(fileList) {
182
+ const files = [...fileList];
183
+ this._error = '';
184
+
185
+ if (this.maxSize > 0) {
186
+ const oversized = files.filter(f => f.size > this.maxSize);
187
+ if (oversized.length > 0) {
188
+ this._error = `${oversized.map(f => f.name).join(', ')} exceeded max size of ${this._formatSize(this.maxSize)}`;
189
+ return files.filter(f => f.size <= this.maxSize);
190
+ }
191
+ }
192
+
193
+ return files;
194
+ }
195
+
196
+ _addFiles(fileList) {
197
+ const valid = this._validateFiles(fileList);
198
+ if (valid.length === 0) return;
199
+
200
+ if (this.multiple) {
201
+ this._files = [...this._files, ...valid];
202
+ } else {
203
+ this._files = [valid[0]];
204
+ }
205
+
206
+ this.dispatchEvent(new CustomEvent('arc-change', {
207
+ detail: { value: [...this._files] },
208
+ bubbles: true,
209
+ composed: true,
210
+ }));
211
+ }
212
+
213
+ _removeFile(index) {
214
+ const removed = this._files[index];
215
+ this._files = this._files.filter((_, i) => i !== index);
216
+ this._error = '';
217
+
218
+ this.dispatchEvent(new CustomEvent('arc-remove', {
219
+ detail: { value: removed, index },
220
+ bubbles: true,
221
+ composed: true,
222
+ }));
223
+
224
+ this.dispatchEvent(new CustomEvent('arc-change', {
225
+ detail: { value: [...this._files] },
226
+ bubbles: true,
227
+ composed: true,
228
+ }));
229
+ }
230
+
231
+ _handleClick() {
232
+ this.shadowRoot.querySelector('input[type="file"]')?.click();
233
+ }
234
+
235
+ _handleInputChange(e) {
236
+ if (e.target.files.length > 0) {
237
+ this._addFiles(e.target.files);
238
+ }
239
+ // Reset so the same file can be re-selected
240
+ e.target.value = '';
241
+ }
242
+
243
+ _handleDragOver(e) {
244
+ e.preventDefault();
245
+ e.stopPropagation();
246
+ this._dragOver = true;
247
+ }
248
+
249
+ _handleDragLeave(e) {
250
+ e.preventDefault();
251
+ e.stopPropagation();
252
+ this._dragOver = false;
253
+ }
254
+
255
+ _handleDrop(e) {
256
+ e.preventDefault();
257
+ e.stopPropagation();
258
+ this._dragOver = false;
259
+
260
+ if (e.dataTransfer?.files.length > 0) {
261
+ this._addFiles(e.dataTransfer.files);
262
+ }
263
+ }
264
+
265
+ _handleKeydown(e) {
266
+ if (e.key === 'Enter' || e.key === ' ') {
267
+ e.preventDefault();
268
+ this._handleClick();
269
+ }
270
+ }
271
+
272
+ render() {
273
+ return html`
274
+ <div part="wrapper">
275
+ <div
276
+ class="dropzone ${this._dragOver ? 'drag-over' : ''}"
277
+ part="dropzone"
278
+ role="button"
279
+ tabindex="0"
280
+ aria-label="Upload files. Click or drag and drop."
281
+ @click=${this._handleClick}
282
+ @keydown=${this._handleKeydown}
283
+ @dragover=${this._handleDragOver}
284
+ @dragleave=${this._handleDragLeave}
285
+ @drop=${this._handleDrop}
286
+ >
287
+ <span class="upload-icon" aria-hidden="true">\u2191</span>
288
+ <span class="upload-text">
289
+ Drag & drop files here or <span class="browse-link">browse</span>
290
+ </span>
291
+ ${this.accept ? html`
292
+ <span class="upload-hint">Accepted: ${this.accept}</span>
293
+ ` : ''}
294
+ ${this.maxSize > 0 ? html`
295
+ <span class="upload-hint">Max size: ${this._formatSize(this.maxSize)}</span>
296
+ ` : ''}
297
+ </div>
298
+
299
+ <input
300
+ type="file"
301
+ accept=${this.accept || undefined}
302
+ ?multiple=${this.multiple}
303
+ @change=${this._handleInputChange}
304
+ aria-hidden="true"
305
+ tabindex="-1"
306
+ />
307
+
308
+ ${this._error ? html`
309
+ <div class="error-message" role="alert" part="error">${this._error}</div>
310
+ ` : ''}
311
+
312
+ ${this._files.length > 0 ? html`
313
+ <div class="file-list" part="file-list" role="list" aria-label="Selected files">
314
+ ${this._files.map((file, i) => html`
315
+ <div class="file-item" part="file-item" role="listitem">
316
+ <span class="file-icon" aria-hidden="true">\u{1F4CE}</span>
317
+ <span class="file-name">${file.name}</span>
318
+ <span class="file-size">${this._formatSize(file.size)}</span>
319
+ <button
320
+ class="file-remove"
321
+ @click=${(e) => { e.stopPropagation(); this._removeFile(i); }}
322
+ aria-label="Remove ${file.name}"
323
+ >&times;</button>
324
+ </div>
325
+ `)}
326
+ </div>
327
+ ` : ''}
328
+ </div>
329
+ `;
330
+ }
331
+ }
332
+
333
+ customElements.define('arc-file-upload', ArcFileUpload);
@@ -0,0 +1,179 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { tokenStyles } from '../shared-styles.js';
3
+
4
+ export class ArcForm extends LitElement {
5
+ static properties = {
6
+ action: { type: String },
7
+ method: { type: String },
8
+ novalidate: { type: Boolean },
9
+ };
10
+
11
+ static styles = [
12
+ tokenStyles,
13
+ css`
14
+ :host { display: block; font-family: var(--font-body); }
15
+
16
+ form {
17
+ display: contents;
18
+ }
19
+
20
+ .form-layout {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--space-md);
24
+ }
25
+
26
+ ::slotted(*) {
27
+ margin: 0;
28
+ }
29
+ `,
30
+ ];
31
+
32
+ constructor() {
33
+ super();
34
+ this.action = '';
35
+ this.method = '';
36
+ this.novalidate = false;
37
+ }
38
+
39
+ _collectValues() {
40
+ const slot = this.shadowRoot.querySelector('slot');
41
+ const children = slot ? slot.assignedElements({ flatten: true }) : [];
42
+ const values = {};
43
+ const errors = [];
44
+
45
+ const formControls = [];
46
+ const gather = (elements) => {
47
+ for (const el of elements) {
48
+ const tag = el.tagName?.toLowerCase();
49
+ if (
50
+ tag === 'arc-input' ||
51
+ tag === 'arc-textarea' ||
52
+ tag === 'arc-select' ||
53
+ tag === 'arc-checkbox' ||
54
+ tag === 'arc-toggle' ||
55
+ tag === 'arc-radio-group'
56
+ ) {
57
+ formControls.push(el);
58
+ }
59
+ if (!el.shadowRoot && el.children?.length) {
60
+ gather([...el.children]);
61
+ }
62
+ }
63
+ };
64
+
65
+ gather(children);
66
+
67
+ for (const control of formControls) {
68
+ const name = control.getAttribute('name') || control.label || control.tagName.toLowerCase();
69
+ const tag = control.tagName.toLowerCase();
70
+
71
+ if (tag === 'arc-checkbox' || tag === 'arc-toggle') {
72
+ values[name] = control.checked ?? false;
73
+ } else {
74
+ values[name] = control.value ?? '';
75
+ }
76
+
77
+ const required = control.hasAttribute('required');
78
+ if (required) {
79
+ const empty = (tag === 'arc-checkbox' || tag === 'arc-toggle')
80
+ ? !control.checked
81
+ : !control.value || control.value.trim() === '';
82
+
83
+ if (empty) {
84
+ errors.push({ name, message: `${control.label || name} is required` });
85
+ if (typeof control.error !== 'undefined') {
86
+ control.error = `${control.label || name} is required`;
87
+ }
88
+ } else {
89
+ if (typeof control.error !== 'undefined') {
90
+ control.error = '';
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ return { values, errors, valid: errors.length === 0 };
97
+ }
98
+
99
+ _handleSubmit(e) {
100
+ const { values, errors, valid } = this._collectValues();
101
+
102
+ if (!valid && !this.novalidate) {
103
+ e.preventDefault();
104
+ this.dispatchEvent(new CustomEvent('arc-invalid', {
105
+ detail: { errors },
106
+ bubbles: true,
107
+ composed: true,
108
+ }));
109
+ return;
110
+ }
111
+
112
+ // Native form submission — let the browser handle it
113
+ if (this.action) {
114
+ this.dispatchEvent(new CustomEvent('arc-submit', {
115
+ detail: { values, valid },
116
+ bubbles: true,
117
+ composed: true,
118
+ }));
119
+ // Don't preventDefault — the form submits natively
120
+ return;
121
+ }
122
+
123
+ // JS-only mode — prevent default and let the listener handle it
124
+ e.preventDefault();
125
+ this.dispatchEvent(new CustomEvent('arc-submit', {
126
+ detail: { values, valid },
127
+ bubbles: true,
128
+ composed: true,
129
+ }));
130
+ }
131
+
132
+ /** Programmatic submit — call from outside */
133
+ submit() {
134
+ this._handleSubmit(new Event('submit'));
135
+ }
136
+
137
+ /** Reset error states on child controls */
138
+ reset() {
139
+ const slot = this.shadowRoot.querySelector('slot');
140
+ const children = slot ? slot.assignedElements({ flatten: true }) : [];
141
+
142
+ const clearErrors = (elements) => {
143
+ for (const el of elements) {
144
+ if (typeof el.error !== 'undefined') {
145
+ el.error = '';
146
+ }
147
+ if (typeof el.value === 'string') {
148
+ el.value = '';
149
+ }
150
+ if (typeof el.checked === 'boolean') {
151
+ el.checked = false;
152
+ }
153
+ if (!el.shadowRoot && el.children?.length) {
154
+ clearErrors([...el.children]);
155
+ }
156
+ }
157
+ };
158
+
159
+ clearErrors(children);
160
+ }
161
+
162
+ render() {
163
+ return html`
164
+ <form
165
+ part="form"
166
+ action=${this.action || undefined}
167
+ method=${this.method || undefined}
168
+ ?novalidate=${this.novalidate}
169
+ @submit=${this._handleSubmit}
170
+ >
171
+ <div class="form-layout" part="layout">
172
+ <slot></slot>
173
+ </div>
174
+ </form>
175
+ `;
176
+ }
177
+ }
178
+
179
+ customElements.define('arc-form', ArcForm);
@@ -0,0 +1,179 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { tokenStyles } from '../shared-styles.js';
3
+ import '../content/icon.js';
4
+
5
+ export class ArcIconButton extends LitElement {
6
+ static properties = {
7
+ name: { type: String, reflect: true },
8
+ text: { type: String },
9
+ variant: { type: String, reflect: true },
10
+ size: { type: String, reflect: true },
11
+ label: { type: String },
12
+ href: { type: String },
13
+ disabled: { type: Boolean, reflect: true },
14
+ type: { type: String },
15
+ };
16
+
17
+ static styles = [
18
+ tokenStyles,
19
+ css`
20
+ :host { display: inline-flex; }
21
+ :host([disabled]) { pointer-events: none; }
22
+
23
+ .btn {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ gap: var(--space-xs);
28
+ border: 1px solid transparent;
29
+ border-radius: var(--radius-md);
30
+ cursor: pointer;
31
+ transition:
32
+ background var(--transition-base),
33
+ border-color var(--transition-base),
34
+ box-shadow var(--transition-base),
35
+ color var(--transition-base),
36
+ transform var(--transition-fast);
37
+ text-decoration: none;
38
+ box-sizing: border-box;
39
+ color: inherit;
40
+ }
41
+
42
+ /* Icon-only (square) */
43
+ .btn:not(.btn--has-text) { aspect-ratio: 1; }
44
+
45
+ /* With text */
46
+ .btn--has-text {
47
+ font-family: var(--font-accent);
48
+ font-weight: 600;
49
+ text-transform: uppercase;
50
+ letter-spacing: 1.5px;
51
+ white-space: nowrap;
52
+ }
53
+
54
+ .btn__text {
55
+ line-height: 1;
56
+ }
57
+
58
+ /* Sizes — icon-only */
59
+ :host([size="xs"]) .btn:not(.btn--has-text) { width: 28px; height: 28px; border-radius: var(--radius-sm); }
60
+ :host([size="sm"]) .btn:not(.btn--has-text) { width: 32px; height: 32px; }
61
+ :host(:not([size])) .btn:not(.btn--has-text),
62
+ :host([size="md"]) .btn:not(.btn--has-text) { width: 36px; height: 36px; }
63
+ :host([size="lg"]) .btn:not(.btn--has-text) { width: 44px; height: 44px; }
64
+
65
+ /* Sizes — with text */
66
+ :host([size="xs"]) .btn--has-text { padding: var(--space-xs) var(--space-sm); font-size: var(--text-xs); }
67
+ :host([size="sm"]) .btn--has-text { padding: calc(var(--space-xs) + 2px) calc(var(--space-sm) + 2px); font-size: var(--text-xs); }
68
+ :host(:not([size])) .btn--has-text,
69
+ :host([size="md"]) .btn--has-text { padding: var(--space-xs) var(--space-sm); font-size: var(--text-xs); }
70
+ :host([size="lg"]) .btn--has-text { padding: var(--space-sm) var(--space-md); font-size: var(--text-xs); }
71
+
72
+ /* Ghost (default) */
73
+ :host(:not([variant])) .btn,
74
+ :host([variant="ghost"]) .btn {
75
+ background: transparent;
76
+ color: var(--text-muted);
77
+ border-color: transparent;
78
+ }
79
+ :host(:not([variant])) .btn:hover,
80
+ :host([variant="ghost"]) .btn:hover {
81
+ color: var(--text-primary);
82
+ background: var(--bg-hover);
83
+ }
84
+ :host(:not([variant])) .btn:active,
85
+ :host([variant="ghost"]) .btn:active {
86
+ transform: scale(0.93);
87
+ background: var(--bg-elevated);
88
+ }
89
+
90
+ /* Secondary */
91
+ :host([variant="secondary"]) .btn {
92
+ background: transparent;
93
+ color: var(--text-secondary);
94
+ border-color: var(--border-default);
95
+ }
96
+ :host([variant="secondary"]) .btn:hover {
97
+ border-color: var(--accent-primary);
98
+ color: var(--accent-primary);
99
+ box-shadow: 0 0 16px var(--accent-primary-ring);
100
+ }
101
+ :host([variant="secondary"]) .btn:active {
102
+ transform: scale(0.93);
103
+ background: rgba(var(--accent-primary-rgb), 0.05);
104
+ }
105
+
106
+ /* Primary */
107
+ :host([variant="primary"]) .btn {
108
+ background: var(--accent-primary);
109
+ color: var(--bg-deep);
110
+ border-color: var(--accent-primary);
111
+ }
112
+ :host([variant="primary"]) .btn:hover {
113
+ box-shadow: var(--glow-primary);
114
+ }
115
+ :host([variant="primary"]) .btn:active {
116
+ transform: scale(0.93);
117
+ box-shadow: 0 0 8px rgba(var(--accent-primary-rgb), 0.5);
118
+ }
119
+
120
+ /* Focus */
121
+ .btn:focus-visible { outline: none; box-shadow: var(--focus-glow); }
122
+
123
+ /* Disabled */
124
+ :host([disabled]) .btn { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
125
+
126
+ @media (prefers-reduced-motion: reduce) {
127
+ :host *,
128
+ :host *::before,
129
+ :host *::after {
130
+ animation-duration: 0.01ms !important;
131
+ animation-iteration-count: 1 !important;
132
+ transition-duration: 0.01ms !important;
133
+ }
134
+ }
135
+ `,
136
+ ];
137
+
138
+ constructor() {
139
+ super();
140
+ this.name = '';
141
+ this.text = '';
142
+ this.variant = 'ghost';
143
+ this.size = 'md';
144
+ this.label = '';
145
+ this.href = '';
146
+ this.disabled = false;
147
+ this.type = 'button';
148
+ }
149
+
150
+ /** Map icon-button size to arc-icon size */
151
+ get _iconSize() {
152
+ const map = { xs: 'xs', sm: 'sm', md: 'sm', lg: 'md' };
153
+ return map[this.size] || 'sm';
154
+ }
155
+
156
+ render() {
157
+ const hasText = !!this.text;
158
+ const icon = this.name
159
+ ? html`<arc-icon name=${this.name} size=${this._iconSize}></arc-icon>`
160
+ : html`<slot></slot>`;
161
+ const textEl = hasText ? html`<span class="btn__text">${this.text}</span>` : null;
162
+ const classes = `btn${hasText ? ' btn--has-text' : ''}`;
163
+
164
+ if (this.href) {
165
+ return html`<a class=${classes} href=${this.href} aria-label=${this.label || this.text || ''} part="button">${icon}${textEl}</a>`;
166
+ }
167
+ return html`
168
+ <button
169
+ class=${classes}
170
+ type=${this.type}
171
+ ?disabled=${this.disabled}
172
+ aria-label=${this.label || this.text || ''}
173
+ part="button"
174
+ >${icon}${textEl}</button>
175
+ `;
176
+ }
177
+ }
178
+
179
+ customElements.define('arc-icon-button', ArcIconButton);