@arclux/arc-ui 1.0.0 → 1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arclux/arc-ui",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "ARC UI — Lit Web Components implementing the Arclight design system.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -157,7 +157,7 @@
157
157
  "src/",
158
158
  "types/"
159
159
  ],
160
- "sideEffects": false,
160
+ "sideEffects": true,
161
161
  "dependencies": {
162
162
  "lit": "^3.3.0"
163
163
  },
@@ -4,6 +4,7 @@ import { tokenStyles } from '../shared-styles.js';
4
4
  export class ArcCard extends LitElement {
5
5
  static properties = {
6
6
  href: { type: String },
7
+ _hasFooter: { state: true },
7
8
  };
8
9
 
9
10
  static styles = [
@@ -34,6 +35,8 @@ export class ArcCard extends LitElement {
34
35
  padding: var(--space-xl) var(--space-lg);
35
36
  flex: 1;
36
37
  min-height: 0;
38
+ display: flex;
39
+ flex-direction: column;
37
40
  transition: box-shadow var(--transition-slow);
38
41
  }
39
42
 
@@ -43,6 +46,18 @@ export class ArcCard extends LitElement {
43
46
 
44
47
  .card:focus-visible { outline: none; box-shadow: var(--focus-glow); border-radius: var(--radius-lg); }
45
48
 
49
+ .card__body {
50
+ flex: 1;
51
+ }
52
+
53
+ .card__footer {
54
+ margin-top: var(--space-md);
55
+ }
56
+
57
+ .card__footer--empty {
58
+ display: none;
59
+ }
60
+
46
61
  @media (max-width: 768px) {
47
62
  .card__inner { padding: var(--space-lg) var(--space-md); }
48
63
  }
@@ -62,13 +77,25 @@ export class ArcCard extends LitElement {
62
77
  constructor() {
63
78
  super();
64
79
  this.href = '';
80
+ this._hasFooter = false;
81
+ }
82
+
83
+ _onFooterSlotChange(e) {
84
+ this._hasFooter = e.target.assignedNodes({ flatten: true }).length > 0;
65
85
  }
66
86
 
67
87
  render() {
88
+ const content = html`
89
+ <div class="card__body" part="body"><slot></slot></div>
90
+ <div class="card__footer ${this._hasFooter ? '' : 'card__footer--empty'}" part="footer">
91
+ <slot name="footer" @slotchange=${this._onFooterSlotChange}></slot>
92
+ </div>
93
+ `;
94
+
68
95
  if (this.href) {
69
- return html`<a class="card" href=${this.href} part="card"><div class="card__inner" part="inner"><slot></slot></div></a>`;
96
+ return html`<a class="card" href=${this.href} part="card"><div class="card__inner" part="inner">${content}</div></a>`;
70
97
  }
71
- return html`<div class="card" part="card"><div class="card__inner" part="inner"><slot></slot></div></div>`;
98
+ return html`<div class="card" part="card"><div class="card__inner" part="inner">${content}</div></div>`;
72
99
  }
73
100
  }
74
101
 
@@ -0,0 +1,126 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { tokenStyles } from '../shared-styles.js';
3
+
4
+ /**
5
+ * @arc-prism content — call-to-action banner with gradient background
6
+ */
7
+ export class ArcCtaBanner extends LitElement {
8
+ static properties = {
9
+ eyebrow: { type: String },
10
+ headline: { type: String },
11
+ nogradient: { type: Boolean, reflect: true },
12
+ };
13
+
14
+ static styles = [
15
+ tokenStyles,
16
+ css`
17
+ :host { display: block; position: relative; overflow: hidden; }
18
+
19
+ .cta {
20
+ position: relative;
21
+ padding: var(--space-3xl) var(--space-lg);
22
+ }
23
+
24
+ .cta__bg {
25
+ position: absolute;
26
+ inset: 0;
27
+ background:
28
+ radial-gradient(ellipse at 30% 50%, rgba(var(--accent-primary-rgb), 0.1), transparent 60%),
29
+ radial-gradient(ellipse at 70% 50%, rgba(var(--accent-secondary-rgb), 0.08), transparent 60%);
30
+ pointer-events: none;
31
+ }
32
+
33
+ :host([nogradient]) .cta__bg { display: none; }
34
+
35
+ .cta__inner {
36
+ position: relative;
37
+ max-width: var(--max-width, 1200px);
38
+ margin-inline: auto;
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ text-align: center;
43
+ gap: var(--space-md);
44
+ }
45
+
46
+ .cta__eyebrow {
47
+ font-family: var(--font-accent);
48
+ font-weight: 600;
49
+ font-size: var(--text-xs);
50
+ letter-spacing: 4px;
51
+ text-transform: uppercase;
52
+ background: var(--gradient-accent-text);
53
+ -webkit-background-clip: text;
54
+ -webkit-text-fill-color: transparent;
55
+ background-clip: text;
56
+ }
57
+
58
+ .cta__headline {
59
+ font-size: clamp(28px, 4vw, 40px);
60
+ font-weight: 500;
61
+ letter-spacing: -1px;
62
+ background: var(--gradient-display-text);
63
+ -webkit-background-clip: text;
64
+ -webkit-text-fill-color: transparent;
65
+ background-clip: text;
66
+ margin: 0;
67
+ }
68
+
69
+ .cta__body {
70
+ color: var(--text-secondary);
71
+ font-size: var(--text-md);
72
+ max-width: 480px;
73
+ text-wrap: balance;
74
+ line-height: 1.7;
75
+ }
76
+
77
+ .cta__body ::slotted(*) { margin: 0; }
78
+
79
+ .cta__actions {
80
+ display: flex;
81
+ gap: var(--space-md);
82
+ margin-top: var(--space-sm);
83
+ }
84
+
85
+ @media (max-width: 768px) {
86
+ .cta { padding: var(--space-xl) var(--space-md); }
87
+ .cta__actions { flex-direction: column; align-items: center; }
88
+ }
89
+ `,
90
+ ];
91
+
92
+ constructor() {
93
+ super();
94
+ this.eyebrow = '';
95
+ this.headline = '';
96
+ this.nogradient = false;
97
+ }
98
+
99
+ render() {
100
+ return html`
101
+ <div class="cta" part="container">
102
+ <div class="cta__bg" part="background"></div>
103
+ <div class="cta__inner" part="inner">
104
+ ${this.eyebrow ? html`
105
+ <span class="cta__eyebrow" part="eyebrow">
106
+ <slot name="eyebrow">${this.eyebrow}</slot>
107
+ </span>
108
+ ` : html`<slot name="eyebrow"></slot>`}
109
+ ${this.headline ? html`
110
+ <h2 class="cta__headline" part="headline">
111
+ <slot name="headline">${this.headline}</slot>
112
+ </h2>
113
+ ` : html`<slot name="headline"></slot>`}
114
+ <div class="cta__body" part="body">
115
+ <slot></slot>
116
+ </div>
117
+ <div class="cta__actions" part="actions">
118
+ <slot name="actions"></slot>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ `;
123
+ }
124
+ }
125
+
126
+ customElements.define('arc-cta-banner', ArcCtaBanner);
@@ -15,6 +15,7 @@ export { ArcCodeBlock } from './code-block.js';
15
15
  export { ArcCollapsible } from './collapsible.js';
16
16
  export { ArcColorSwatch } from './color-swatch.js';
17
17
  export { ArcColumn } from './column.js';
18
+ export { ArcCtaBanner } from './cta-banner.js';
18
19
  export { ArcDataTable } from './data-table.js';
19
20
  export { ArcDivider } from './divider.js';
20
21
  export { ArcEmptyState } from './empty-state.js';
@@ -6,6 +6,7 @@ export class ArcTag extends LitElement {
6
6
  variant: { type: String, reflect: true },
7
7
  removable: { type: Boolean, reflect: true },
8
8
  disabled: { type: Boolean, reflect: true },
9
+ color: { type: String },
9
10
  };
10
11
 
11
12
  static styles = [
@@ -120,6 +121,7 @@ export class ArcTag extends LitElement {
120
121
  this.variant = 'default';
121
122
  this.removable = false;
122
123
  this.disabled = false;
124
+ this.color = '';
123
125
  }
124
126
 
125
127
  _remove(e) {
@@ -132,8 +134,15 @@ export class ArcTag extends LitElement {
132
134
  }
133
135
 
134
136
  render() {
137
+ const colorStyle = this.color
138
+ ? `border-color: rgba(${this.color}, 0.2); color: rgb(${this.color}); background: rgba(${this.color}, 0.06);`
139
+ : '';
140
+
135
141
  return html`
136
- <span class="tag" part="tag">
142
+ <span class="tag" part="tag" style=${colorStyle}
143
+ @mouseenter=${this.color ? (e) => { e.currentTarget.style.boxShadow = `0 0 12px rgba(${this.color}, 0.15)`; } : null}
144
+ @mouseleave=${this.color ? (e) => { e.currentTarget.style.boxShadow = ''; } : null}
145
+ >
137
146
  <span class="tag__label" part="label"><slot></slot></span>
138
147
  ${this.removable ? html`
139
148
  <button
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // ARC UI — Web Components
2
2
  // Re-exports from all tiers
3
3
 
4
- export { ArcAccordion, ArcAccordionItem, ArcAnimatedNumber, ArcAspectRatio, ArcAvatar, ArcAvatarGroup, ArcBadge, ArcCallout, ArcCard, ArcCarousel, ArcCodeBlock, ArcCollapsible, ArcColorSwatch, ArcColumn, ArcDataTable, ArcDivider, ArcEmptyState, ArcFeatureCard, ArcHighlight, ArcIcon, ArcIconLibrary, iconRegistry, ArcInfiniteScroll, ArcKbd, ArcMarkdown, ArcMarquee, ArcMeter, ArcScrollArea, ArcSkeleton, ArcSpinner, ArcStack, ArcStat, ArcStep, ArcStepper, ArcTable, ArcTag, ArcText, ArcTimeline, ArcTimelineItem, ArcTruncate, ArcValueCard } from './content/index.js';
4
+ export { ArcAccordion, ArcAccordionItem, ArcAnimatedNumber, ArcAspectRatio, ArcAvatar, ArcAvatarGroup, ArcBadge, ArcCallout, ArcCard, ArcCarousel, ArcCodeBlock, ArcCollapsible, ArcColorSwatch, ArcColumn, ArcDataTable, ArcDivider, ArcEmptyState, ArcFeatureCard, ArcHighlight, ArcIcon, ArcIconLibrary, iconRegistry, ArcInfiniteScroll, ArcKbd, ArcMarkdown, ArcMarquee, ArcMeter, ArcScrollArea, ArcSkeleton, ArcSpinner, ArcStack, ArcStat, ArcStep, ArcStepper, ArcTable, ArcTag, ArcText, ArcTimeline, ArcTimelineItem, ArcTruncate, ArcValueCard , ArcCtaBanner } from './content/index.js';
5
5
  export { ArcButton, ArcCalendar, ArcCheckbox, ArcChip, ArcColorPicker, ArcCombobox, ArcCopyButton, ArcDatePicker, ArcFileUpload, ArcForm, ArcIconButton, ArcInput, ArcMultiSelect, ArcNumberInput, ArcOtpInput, ArcPinInput, ArcRadio, ArcRadioGroup, ArcRating, ArcSearch, ArcSegmentedControl, ArcSelect, ArcSlider, ArcSortableList, ArcSuggestion, ArcTextarea, ArcThemeToggle, ArcToggle } from './input/index.js';
6
6
  export { ArcAlert, ArcCommandItem, ArcCommandPalette, ArcContextMenu, ArcDialog, ArcDropdownMenu, ArcHoverCard, ArcModal, ArcNotificationPanel, ArcPopover, ArcProgress, ArcSheet, ArcToast, ArcTooltip } from './feedback/index.js';
7
7
  export { ArcBreadcrumb, ArcBreadcrumbItem, ArcDrawer, ArcFooter, ArcLink, ArcNavItem, ArcNavigationMenu, ArcPagination, ArcScrollSpy, ArcScrollToTop, ArcSidebar, ArcSidebarLink, ArcSidebarSection, ArcSpyLink, ArcTab, ArcTabs, ArcTopBar, ArcTreeItem, ArcTreeView } from './navigation/index.js';
@@ -3,11 +3,13 @@ import { tokenStyles } from '../shared-styles.js';
3
3
 
4
4
  export class ArcButton extends LitElement {
5
5
  static properties = {
6
- variant: { type: String, reflect: true },
7
- size: { type: String, reflect: true },
8
- href: { type: String },
9
- disabled: { type: Boolean, reflect: true },
10
- type: { type: String },
6
+ variant: { type: String, reflect: true },
7
+ size: { type: String, reflect: true },
8
+ href: { type: String },
9
+ disabled: { type: Boolean, reflect: true },
10
+ type: { type: String },
11
+ _hasPrefix: { state: true },
12
+ _hasSuffix: { state: true },
11
13
  };
12
14
 
13
15
  static styles = [
@@ -95,6 +97,21 @@ export class ArcButton extends LitElement {
95
97
  /* Disabled */
96
98
  :host([disabled]) .btn { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
97
99
 
100
+ /* Prefix / Suffix */
101
+ .btn__prefix,
102
+ .btn__suffix {
103
+ display: inline-flex;
104
+ align-items: center;
105
+ }
106
+
107
+ .btn__prefix--empty,
108
+ .btn__suffix--empty { display: none; }
109
+
110
+ ::slotted([slot="prefix"]),
111
+ ::slotted([slot="suffix"]) {
112
+ display: flex;
113
+ }
114
+
98
115
  @media (prefers-reduced-motion: reduce) {
99
116
  :host *,
100
117
  :host *::before,
@@ -114,13 +131,35 @@ export class ArcButton extends LitElement {
114
131
  this.href = '';
115
132
  this.disabled = false;
116
133
  this.type = 'button';
134
+ this._hasPrefix = false;
135
+ this._hasSuffix = false;
136
+ }
137
+
138
+ _onPrefixSlotChange(e) {
139
+ this._hasPrefix = e.target.assignedNodes({ flatten: true }).length > 0;
140
+ }
141
+
142
+ _onSuffixSlotChange(e) {
143
+ this._hasSuffix = e.target.assignedNodes({ flatten: true }).length > 0;
144
+ }
145
+
146
+ _renderContent() {
147
+ return html`
148
+ <span class="btn__prefix ${this._hasPrefix ? '' : 'btn__prefix--empty'}">
149
+ <slot name="prefix" @slotchange=${this._onPrefixSlotChange}></slot>
150
+ </span>
151
+ <slot></slot>
152
+ <span class="btn__suffix ${this._hasSuffix ? '' : 'btn__suffix--empty'}">
153
+ <slot name="suffix" @slotchange=${this._onSuffixSlotChange}></slot>
154
+ </span>
155
+ `;
117
156
  }
118
157
 
119
158
  render() {
120
159
  if (this.href) {
121
- return html`<a class="btn" href=${this.href} part="button"><slot></slot></a>`;
160
+ return html`<a class="btn" href=${this.href} part="button">${this._renderContent()}</a>`;
122
161
  }
123
- return html`<button class="btn" type=${this.type} ?disabled=${this.disabled} part="button"><slot></slot></button>`;
162
+ return html`<button class="btn" type=${this.type} ?disabled=${this.disabled} part="button">${this._renderContent()}</button>`;
124
163
  }
125
164
  }
126
165
 
package/src/input/form.js CHANGED
@@ -1,11 +1,15 @@
1
- import { LitElement, html, css } from 'lit';
1
+ import { LitElement, html, css, nothing } from 'lit';
2
2
  import { tokenStyles } from '../shared-styles.js';
3
3
 
4
4
  export class ArcForm extends LitElement {
5
5
  static properties = {
6
- action: { type: String },
7
- method: { type: String },
8
- novalidate: { type: Boolean },
6
+ action: { type: String },
7
+ method: { type: String },
8
+ novalidate: { type: Boolean },
9
+ loading: { type: Boolean, reflect: true },
10
+ disabled: { type: Boolean, reflect: true },
11
+ errorSummary: { type: Boolean, attribute: 'error-summary' },
12
+ _errors: { state: true },
9
13
  };
10
14
 
11
15
  static styles = [
@@ -26,6 +30,45 @@ export class ArcForm extends LitElement {
26
30
  ::slotted(*) {
27
31
  margin: 0;
28
32
  }
33
+
34
+ /* Loading state */
35
+ :host([loading]) .form-layout {
36
+ pointer-events: none;
37
+ opacity: 0.7;
38
+ }
39
+
40
+ /* Disabled state */
41
+ :host([disabled]) .form-layout {
42
+ pointer-events: none;
43
+ opacity: 0.5;
44
+ }
45
+
46
+ /* Error summary */
47
+ .form-errors {
48
+ border: 1px solid var(--color-error, #ef4444);
49
+ border-radius: var(--radius-md);
50
+ background: rgba(239, 68, 68, 0.06);
51
+ padding: var(--space-sm) var(--space-md);
52
+ margin-bottom: var(--space-sm);
53
+ }
54
+
55
+ .form-errors__title {
56
+ font-family: var(--font-accent);
57
+ font-size: var(--text-xs);
58
+ font-weight: 600;
59
+ letter-spacing: 1px;
60
+ text-transform: uppercase;
61
+ color: var(--color-error, #ef4444);
62
+ margin: 0 0 var(--space-xs) 0;
63
+ }
64
+
65
+ .form-errors__list {
66
+ margin: 0;
67
+ padding: 0 0 0 var(--space-md);
68
+ font-size: var(--text-sm);
69
+ color: var(--color-error, #ef4444);
70
+ line-height: 1.6;
71
+ }
29
72
  `,
30
73
  ];
31
74
 
@@ -34,15 +77,18 @@ export class ArcForm extends LitElement {
34
77
  this.action = '';
35
78
  this.method = '';
36
79
  this.novalidate = false;
80
+ this.loading = false;
81
+ this.disabled = false;
82
+ this.errorSummary = true;
83
+ this._errors = [];
37
84
  }
38
85
 
39
- _collectValues() {
86
+ /** Gather all form controls and propagate disabled */
87
+ _getFormControls() {
40
88
  const slot = this.shadowRoot.querySelector('slot');
41
89
  const children = slot ? slot.assignedElements({ flatten: true }) : [];
42
- const values = {};
43
- const errors = [];
90
+ const controls = [];
44
91
 
45
- const formControls = [];
46
92
  const gather = (elements) => {
47
93
  for (const el of elements) {
48
94
  const tag = el.tagName?.toLowerCase();
@@ -54,7 +100,7 @@ export class ArcForm extends LitElement {
54
100
  tag === 'arc-toggle' ||
55
101
  tag === 'arc-radio-group'
56
102
  ) {
57
- formControls.push(el);
103
+ controls.push(el);
58
104
  }
59
105
  if (!el.shadowRoot && el.children?.length) {
60
106
  gather([...el.children]);
@@ -63,15 +109,42 @@ export class ArcForm extends LitElement {
63
109
  };
64
110
 
65
111
  gather(children);
112
+ return controls;
113
+ }
114
+
115
+ updated(changed) {
116
+ super.updated(changed);
117
+
118
+ if (changed.has('disabled')) {
119
+ const controls = this._getFormControls();
120
+ for (const control of controls) {
121
+ if (this.disabled) {
122
+ control.setAttribute('disabled', '');
123
+ } else {
124
+ control.removeAttribute('disabled');
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ _collectValues() {
131
+ const controls = this._getFormControls();
132
+ const values = {};
133
+ const formData = new FormData();
134
+ const errors = [];
66
135
 
67
- for (const control of formControls) {
136
+ for (const control of controls) {
68
137
  const name = control.getAttribute('name') || control.label || control.tagName.toLowerCase();
69
138
  const tag = control.tagName.toLowerCase();
70
139
 
71
140
  if (tag === 'arc-checkbox' || tag === 'arc-toggle') {
72
141
  values[name] = control.checked ?? false;
142
+ if (control.checked) {
143
+ formData.append(name, 'on');
144
+ }
73
145
  } else {
74
146
  values[name] = control.value ?? '';
147
+ formData.append(name, control.value ?? '');
75
148
  }
76
149
 
77
150
  const required = control.hasAttribute('required');
@@ -93,14 +166,20 @@ export class ArcForm extends LitElement {
93
166
  }
94
167
  }
95
168
 
96
- return { values, errors, valid: errors.length === 0 };
169
+ return { values, formData, errors, valid: errors.length === 0 };
97
170
  }
98
171
 
99
172
  _handleSubmit(e) {
100
- const { values, errors, valid } = this._collectValues();
173
+ if (this.loading) {
174
+ e.preventDefault();
175
+ return;
176
+ }
177
+
178
+ const { values, formData, errors, valid } = this._collectValues();
101
179
 
102
180
  if (!valid && !this.novalidate) {
103
181
  e.preventDefault();
182
+ this._errors = errors;
104
183
  this.dispatchEvent(new CustomEvent('arc-invalid', {
105
184
  detail: { errors },
106
185
  bubbles: true,
@@ -109,10 +188,12 @@ export class ArcForm extends LitElement {
109
188
  return;
110
189
  }
111
190
 
191
+ this._errors = [];
192
+
112
193
  // Native form submission — let the browser handle it
113
194
  if (this.action) {
114
195
  this.dispatchEvent(new CustomEvent('arc-submit', {
115
- detail: { values, valid },
196
+ detail: { values, formData, valid },
116
197
  bubbles: true,
117
198
  composed: true,
118
199
  }));
@@ -123,7 +204,7 @@ export class ArcForm extends LitElement {
123
204
  // JS-only mode — prevent default and let the listener handle it
124
205
  e.preventDefault();
125
206
  this.dispatchEvent(new CustomEvent('arc-submit', {
126
- detail: { values, valid },
207
+ detail: { values, formData, valid },
127
208
  bubbles: true,
128
209
  composed: true,
129
210
  }));
@@ -136,27 +217,26 @@ export class ArcForm extends LitElement {
136
217
 
137
218
  /** Reset error states on child controls */
138
219
  reset() {
139
- const slot = this.shadowRoot.querySelector('slot');
140
- const children = slot ? slot.assignedElements({ flatten: true }) : [];
220
+ const controls = this._getFormControls();
141
221
 
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
- }
222
+ for (const control of controls) {
223
+ if (typeof control.error !== 'undefined') {
224
+ control.error = '';
156
225
  }
157
- };
226
+ if (typeof control.value === 'string') {
227
+ control.value = '';
228
+ }
229
+ if (typeof control.checked === 'boolean') {
230
+ control.checked = false;
231
+ }
232
+ }
233
+
234
+ this._errors = [];
158
235
 
159
- clearErrors(children);
236
+ this.dispatchEvent(new CustomEvent('arc-reset', {
237
+ bubbles: true,
238
+ composed: true,
239
+ }));
160
240
  }
161
241
 
162
242
  render() {
@@ -169,6 +249,14 @@ export class ArcForm extends LitElement {
169
249
  @submit=${this._handleSubmit}
170
250
  >
171
251
  <div class="form-layout" part="layout">
252
+ ${this.errorSummary && this._errors.length > 0 ? html`
253
+ <div class="form-errors" role="alert" part="errors">
254
+ <p class="form-errors__title">Please fix the following errors</p>
255
+ <ul class="form-errors__list">
256
+ ${this._errors.map(err => html`<li>${err.message}</li>`)}
257
+ </ul>
258
+ </div>
259
+ ` : nothing}
172
260
  <slot></slot>
173
261
  </div>
174
262
  </form>
@@ -16,6 +16,8 @@ export class ArcInput extends LitElement {
16
16
  required: { type: Boolean },
17
17
  multiline: { type: Boolean },
18
18
  rows: { type: Number },
19
+ _hasPrefix: { state: true },
20
+ _hasSuffix: { state: true },
19
21
  };
20
22
 
21
23
  static styles = [
@@ -38,15 +40,12 @@ export class ArcInput extends LitElement {
38
40
  color: var(--text-muted);
39
41
  }
40
42
 
41
- .input-group__field {
42
- font-family: var(--font-body);
43
- font-size: var(--body-size);
44
- font-weight: 300;
45
- color: var(--text-primary);
43
+ .input-group__wrapper {
44
+ display: flex;
45
+ align-items: center;
46
46
  background: var(--bg-surface);
47
47
  border: 1px solid var(--border-default);
48
48
  border-radius: var(--radius-md);
49
- padding: var(--space-sm) var(--space-md);
50
49
  transition:
51
50
  border-color var(--transition-fast),
52
51
  box-shadow var(--transition-fast),
@@ -55,17 +54,53 @@ export class ArcInput extends LitElement {
55
54
  width: 100%;
56
55
  }
57
56
 
58
- textarea.input-group__field { resize: vertical; }
59
-
60
- .input-group__field::placeholder { color: var(--text-ghost); }
61
- .input-group__field:hover:not(:focus) { border-color: var(--border-bright); }
62
- .input-group__field:focus {
63
- outline: none;
57
+ .input-group__wrapper:hover:not(:focus-within) { border-color: var(--border-bright); }
58
+ .input-group__wrapper:focus-within {
64
59
  border-color: rgba(var(--accent-primary-rgb), 0.4);
65
60
  box-shadow: var(--focus-glow);
66
61
  background: var(--bg-card);
67
62
  }
68
- .input-group__field:disabled { opacity: 0.4; cursor: not-allowed; }
63
+
64
+ :host([disabled]) .input-group__wrapper { opacity: 0.4; cursor: not-allowed; }
65
+
66
+ .input-group__field {
67
+ font-family: var(--font-body);
68
+ font-size: var(--body-size);
69
+ font-weight: 300;
70
+ color: var(--text-primary);
71
+ background: transparent;
72
+ border: none;
73
+ padding: var(--space-sm) var(--space-md);
74
+ box-sizing: border-box;
75
+ width: 100%;
76
+ min-width: 0;
77
+ }
78
+
79
+ .input-group__field:focus { outline: none; }
80
+ .input-group__field::placeholder { color: var(--text-ghost); }
81
+ .input-group__field:disabled { cursor: not-allowed; }
82
+
83
+ textarea.input-group__field { resize: vertical; }
84
+
85
+ .input-group__prefix,
86
+ .input-group__suffix {
87
+ display: flex;
88
+ align-items: center;
89
+ color: var(--text-muted);
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ .input-group__prefix { padding-left: var(--space-md); }
94
+ .input-group__suffix { padding-right: var(--space-md); }
95
+
96
+ .input-group__prefix--empty,
97
+ .input-group__suffix--empty { display: none; }
98
+
99
+ ::slotted([slot="prefix"]),
100
+ ::slotted([slot="suffix"]) {
101
+ width: 20px;
102
+ height: 20px;
103
+ }
69
104
 
70
105
  @media (prefers-reduced-motion: reduce) {
71
106
  :host *,
@@ -92,6 +127,8 @@ export class ArcInput extends LitElement {
92
127
  this.multiline = false;
93
128
  this.rows = 5;
94
129
  this._fieldId = `arc-input-${++inputIdCounter}`;
130
+ this._hasPrefix = false;
131
+ this._hasSuffix = false;
95
132
  }
96
133
 
97
134
  updated(changed) {
@@ -112,6 +149,14 @@ export class ArcInput extends LitElement {
112
149
  this.dispatchEvent(new CustomEvent('arc-change', { detail: { value: this.value }, bubbles: true, composed: true }));
113
150
  }
114
151
 
152
+ _onPrefixSlotChange(e) {
153
+ this._hasPrefix = e.target.assignedNodes({ flatten: true }).length > 0;
154
+ }
155
+
156
+ _onSuffixSlotChange(e) {
157
+ this._hasSuffix = e.target.assignedNodes({ flatten: true }).length > 0;
158
+ }
159
+
115
160
  render() {
116
161
  const id = this.name || this._fieldId;
117
162
  const field = this.multiline
@@ -149,7 +194,15 @@ export class ArcInput extends LitElement {
149
194
  return html`
150
195
  <div class="input-group">
151
196
  ${this.label ? html`<label class="input-group__label" for=${id} part="label">${this.label}</label>` : ''}
152
- ${field}
197
+ <div class="input-group__wrapper" part="wrapper">
198
+ <div class="input-group__prefix ${this._hasPrefix ? '' : 'input-group__prefix--empty'}" part="prefix">
199
+ <slot name="prefix" @slotchange=${this._onPrefixSlotChange}></slot>
200
+ </div>
201
+ ${field}
202
+ <div class="input-group__suffix ${this._hasSuffix ? '' : 'input-group__suffix--empty'}" part="suffix">
203
+ <slot name="suffix" @slotchange=${this._onSuffixSlotChange}></slot>
204
+ </div>
205
+ </div>
153
206
  </div>
154
207
  `;
155
208
  }
@@ -5,6 +5,7 @@ export class ArcPageHeader extends LitElement {
5
5
  static properties = {
6
6
  heading: { type: String },
7
7
  description: { type: String },
8
+ border: { type: Boolean, reflect: true },
8
9
  };
9
10
 
10
11
  static styles = [
@@ -17,10 +18,13 @@ export class ArcPageHeader extends LitElement {
17
18
 
18
19
  .page-header {
19
20
  padding: var(--space-lg) 0 var(--space-md);
21
+ }
22
+
23
+ :host([border]) .page-header {
20
24
  border-bottom: 1px solid var(--border-subtle);
21
25
  }
22
26
 
23
- .page-header__breadcrumb {
27
+ .page-header__above {
24
28
  margin-bottom: var(--space-sm);
25
29
  }
26
30
 
@@ -35,13 +39,13 @@ export class ArcPageHeader extends LitElement {
35
39
  .page-header__heading {
36
40
  margin: 0;
37
41
  font-family: var(--font-body);
38
- font-size: 28px; /* size-variant, keep hardcoded */
42
+ font-size: 28px;
39
43
  font-weight: 700;
40
44
  color: var(--text-primary);
41
45
  line-height: 1.2;
42
46
  }
43
47
 
44
- .page-header__actions {
48
+ .page-header__aside {
45
49
  display: flex;
46
50
  align-items: center;
47
51
  gap: var(--space-sm);
@@ -55,7 +59,7 @@ export class ArcPageHeader extends LitElement {
55
59
  line-height: 1.5;
56
60
  }
57
61
 
58
- .page-header__tabs {
62
+ .page-header__below {
59
63
  margin-top: var(--space-md);
60
64
  }
61
65
 
@@ -69,25 +73,26 @@ export class ArcPageHeader extends LitElement {
69
73
  super();
70
74
  this.heading = '';
71
75
  this.description = '';
76
+ this.border = false;
72
77
  }
73
78
 
74
79
  render() {
75
80
  return html`
76
81
  <div class="page-header" part="base">
77
- <div class="page-header__breadcrumb" part="breadcrumb">
78
- <slot name="breadcrumb"></slot>
82
+ <div class="page-header__above" part="above">
83
+ <slot name="above"></slot>
79
84
  </div>
80
85
  <div class="page-header__title-row" part="title-row">
81
86
  <h1 class="page-header__heading" part="heading">${this.heading}</h1>
82
- <div class="page-header__actions" part="actions">
83
- <slot name="actions"></slot>
87
+ <div class="page-header__aside" part="aside">
88
+ <slot name="aside"></slot>
84
89
  </div>
85
90
  </div>
86
91
  ${this.description
87
92
  ? html`<p class="page-header__description" part="description">${this.description}</p>`
88
93
  : ''}
89
- <div class="page-header__tabs" part="tabs">
90
- <slot name="tabs"></slot>
94
+ <div class="page-header__below" part="below">
95
+ <slot name="below"></slot>
91
96
  </div>
92
97
  <div class="page-header__content" part="content">
93
98
  <slot></slot>
@@ -7,6 +7,7 @@ export class ArcNavItem extends LitElement {
7
7
  static properties = {
8
8
  href: { type: String, reflect: true },
9
9
  active: { type: Boolean, reflect: true },
10
+ muted: { type: Boolean, reflect: true },
10
11
  description: { type: String },
11
12
  };
12
13
 
@@ -18,6 +19,7 @@ export class ArcNavItem extends LitElement {
18
19
  super();
19
20
  this.href = '';
20
21
  this.active = false;
22
+ this.muted = false;
21
23
  this.description = '';
22
24
  }
23
25
 
@@ -71,6 +71,18 @@ export class ArcNavigationMenu extends LitElement {
71
71
  box-shadow: inset 0 0 8px rgba(var(--accent-primary-rgb), 0.08), 0 0 12px rgba(var(--accent-primary-rgb), 0.12);
72
72
  }
73
73
 
74
+ .nav__trigger--muted {
75
+ color: var(--text-muted);
76
+ font-weight: 500;
77
+ border-color: transparent;
78
+ }
79
+
80
+ .nav__trigger--muted:hover {
81
+ color: var(--text-secondary);
82
+ background: transparent;
83
+ border-color: transparent;
84
+ }
85
+
74
86
  .nav__trigger:focus-visible {
75
87
  outline: none;
76
88
  box-shadow: var(--focus-glow);
@@ -112,7 +124,6 @@ export class ArcNavigationMenu extends LitElement {
112
124
 
113
125
  .nav__dropdown-item {
114
126
  display: block;
115
- width: 100%;
116
127
  text-align: left;
117
128
  background: none;
118
129
  border: none;
@@ -294,6 +305,15 @@ export class ArcNavigationMenu extends LitElement {
294
305
  background: rgba(var(--accent-primary-rgb), 0.1);
295
306
  }
296
307
 
308
+ .mobile-trigger--muted {
309
+ color: var(--text-muted);
310
+ font-weight: 400;
311
+ }
312
+
313
+ .mobile-trigger--muted:hover {
314
+ color: var(--text-secondary);
315
+ }
316
+
297
317
  .mobile-chevron {
298
318
  width: 12px;
299
319
  height: 12px;
@@ -569,7 +589,7 @@ export class ArcNavigationMenu extends LitElement {
569
589
  >
570
590
  ${hasChildren ? html`
571
591
  <button
572
- class="nav__trigger nav__trigger--has-children ${isOpen ? 'nav__trigger--open' : ''} ${item.active ? 'nav__trigger--active' : ''}"
592
+ class="nav__trigger nav__trigger--has-children ${isOpen ? 'nav__trigger--open' : ''} ${item.active ? 'nav__trigger--active' : ''} ${item.muted ? 'nav__trigger--muted' : ''}"
573
593
  @click=${(e) => this._handleTriggerClick(e, item, i)}
574
594
  aria-expanded=${String(isOpen)}
575
595
  aria-haspopup="true"
@@ -582,7 +602,7 @@ export class ArcNavigationMenu extends LitElement {
582
602
  </button>
583
603
  ` : html`
584
604
  <a
585
- class="nav__trigger ${item.active ? 'nav__trigger--active' : ''}"
605
+ class="nav__trigger ${item.active ? 'nav__trigger--active' : ''} ${item.muted ? 'nav__trigger--muted' : ''}"
586
606
  href=${item.href}
587
607
  @click=${(e) => this._handleTriggerClick(e, item, i)}
588
608
  part="trigger"
@@ -643,7 +663,7 @@ export class ArcNavigationMenu extends LitElement {
643
663
  <li class="mobile-item" style="animation-delay: ${i * 60}ms">
644
664
  ${hasChildren ? html`
645
665
  <button
646
- class="mobile-trigger ${item.active ? 'mobile-trigger--active' : ''}"
666
+ class="mobile-trigger ${item.active ? 'mobile-trigger--active' : ''} ${item.muted ? 'mobile-trigger--muted' : ''}"
647
667
  @click=${() => this._toggleMobileDropdown(i)}
648
668
  aria-expanded=${String(isExpanded)}
649
669
  >
@@ -668,7 +688,7 @@ export class ArcNavigationMenu extends LitElement {
668
688
  </div>
669
689
  ` : html`
670
690
  <a
671
- class="mobile-trigger ${item.active ? 'mobile-trigger--active' : ''}"
691
+ class="mobile-trigger ${item.active ? 'mobile-trigger--active' : ''} ${item.muted ? 'mobile-trigger--muted' : ''}"
672
692
  href=${item.href}
673
693
  @click=${(e) => this._handleMobileTriggerClick(e, item, i)}
674
694
  >