@arclux/arc-ui 1.4.0 → 1.5.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.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "ARC UI — Lit Web Components implementing the Arclight design system.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -4,6 +4,7 @@ import './accordion-item.js';
4
4
 
5
5
  export class ArcAccordion extends LitElement {
6
6
  static properties = {
7
+ multiple: { type: Boolean, reflect: true },
7
8
  _items: { state: true },
8
9
  _openItems: { state: true },
9
10
  };
@@ -98,6 +99,7 @@ export class ArcAccordion extends LitElement {
98
99
 
99
100
  constructor() {
100
101
  super();
102
+ this.multiple = false;
101
103
  this._items = [];
102
104
  this._openItems = new Set();
103
105
  }
@@ -112,6 +114,7 @@ export class ArcAccordion extends LitElement {
112
114
  if (next.has(index)) {
113
115
  next.delete(index);
114
116
  } else {
117
+ if (!this.multiple) next.clear();
115
118
  next.add(index);
116
119
  }
117
120
  this._openItems = next;
@@ -3,15 +3,17 @@ import { tokenStyles } from '../shared-styles.js';
3
3
 
4
4
  export class ArcAvatar extends LitElement {
5
5
  static properties = {
6
- src: { type: String },
7
- name: { type: String, reflect: true },
8
- size: { type: String, reflect: true },
6
+ src: { type: String },
7
+ name: { type: String, reflect: true },
8
+ size: { type: String, reflect: true },
9
+ shape: { type: String, reflect: true },
10
+ status: { type: String, reflect: true },
9
11
  };
10
12
 
11
13
  static styles = [
12
14
  tokenStyles,
13
15
  css`
14
- :host { display: inline-flex; }
16
+ :host { display: inline-flex; position: relative; }
15
17
 
16
18
  .avatar {
17
19
  display: inline-flex;
@@ -48,10 +50,36 @@ export class ArcAvatar extends LitElement {
48
50
  user-select: none;
49
51
  }
50
52
 
53
+ /* Shape variants */
54
+ :host([shape="square"]) .avatar,
55
+ :host([shape="square"]) .avatar__img { border-radius: var(--radius-md); }
56
+ :host([shape="rounded"]) .avatar,
57
+ :host([shape="rounded"]) .avatar__img { border-radius: var(--radius-lg); }
58
+
51
59
  :host([size="sm"]) .avatar__initials { font-size: var(--text-xs); }
52
60
  :host([size="md"]) .avatar__initials { font-size: var(--text-sm); }
53
61
  :host([size="lg"]) .avatar__initials { font-size: var(--text-lg); }
54
62
 
63
+ /* Status indicator */
64
+ .avatar__status {
65
+ position: absolute;
66
+ bottom: 0;
67
+ right: 0;
68
+ width: 10px;
69
+ height: 10px;
70
+ border-radius: var(--radius-full);
71
+ border: 2px solid var(--bg-deep);
72
+ box-sizing: border-box;
73
+ }
74
+
75
+ :host([size="sm"]) .avatar__status { width: 8px; height: 8px; }
76
+ :host([size="lg"]) .avatar__status { width: 14px; height: 14px; border-width: 3px; }
77
+
78
+ :host([status="online"]) .avatar__status { background: var(--color-success); }
79
+ :host([status="offline"]) .avatar__status { background: var(--text-ghost); }
80
+ :host([status="busy"]) .avatar__status { background: var(--color-error); }
81
+ :host([status="away"]) .avatar__status { background: var(--color-warning); }
82
+
55
83
  @media (prefers-reduced-motion: reduce) {
56
84
  :host *,
57
85
  :host *::before,
@@ -69,6 +97,8 @@ export class ArcAvatar extends LitElement {
69
97
  this.src = '';
70
98
  this.name = '';
71
99
  this.size = 'md';
100
+ this.shape = 'circle';
101
+ this.status = '';
72
102
  }
73
103
 
74
104
  /** @private */
@@ -85,6 +115,7 @@ export class ArcAvatar extends LitElement {
85
115
  <div class="avatar" part="avatar" role="img" aria-label=${this.name || 'Avatar'}>
86
116
  ${content}
87
117
  </div>
118
+ ${this.status ? html`<span class="avatar__status" part="status" aria-label=${this.status}></span>` : ''}
88
119
  `;
89
120
  }
90
121
  }
@@ -4,6 +4,7 @@ import { tokenStyles } from '../shared-styles.js';
4
4
  export class ArcBadge extends LitElement {
5
5
  static properties = {
6
6
  variant: { type: String, reflect: true },
7
+ size: { type: String, reflect: true },
7
8
  color: { type: String },
8
9
  };
9
10
 
@@ -74,6 +75,10 @@ export class ArcBadge extends LitElement {
74
75
  :host([variant="error"]:hover) .badge { box-shadow: 0 0 12px rgba(var(--color-error-rgb), 0.15); }
75
76
  :host([variant="info"]:hover) .badge { box-shadow: 0 0 12px rgba(var(--color-info-rgb), 0.15); }
76
77
 
78
+ /* Sizes */
79
+ :host([size="sm"]) .badge { font-size: calc(var(--text-xs) - 1px); padding: 2px var(--space-xs); }
80
+ :host([size="lg"]) .badge { font-size: var(--text-sm); padding: var(--space-sm) var(--space-md); }
81
+
77
82
  @media (prefers-reduced-motion: reduce) {
78
83
  :host *,
79
84
  :host *::before,
@@ -89,6 +94,7 @@ export class ArcBadge extends LitElement {
89
94
  constructor() {
90
95
  super();
91
96
  this.variant = 'default';
97
+ this.size = 'md';
92
98
  this.color = '';
93
99
  }
94
100
 
@@ -5,7 +5,9 @@ import './icon.js';
5
5
 
6
6
  export class ArcCallout extends LitElement {
7
7
  static properties = {
8
- variant: { type: String, reflect: true },
8
+ variant: { type: String, reflect: true },
9
+ dismissible: { type: Boolean, reflect: true },
10
+ _dismissed: { state: true },
9
11
  };
10
12
 
11
13
  static styles = [
@@ -54,6 +56,36 @@ export class ArcCallout extends LitElement {
54
56
  min-width: 0;
55
57
  }
56
58
 
59
+ .callout__dismiss {
60
+ margin-left: auto;
61
+ background: none;
62
+ border: none;
63
+ color: var(--text-muted);
64
+ cursor: pointer;
65
+ padding: var(--space-xs);
66
+ border-radius: var(--radius-sm);
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ opacity: 0.6;
71
+ transition: opacity var(--transition-fast), background var(--transition-fast);
72
+ flex-shrink: 0;
73
+ }
74
+
75
+ .callout__dismiss:hover {
76
+ opacity: 1;
77
+ background: rgba(var(--_status-rgb), 0.08);
78
+ }
79
+
80
+ .callout__dismiss:focus-visible {
81
+ outline: none;
82
+ box-shadow: var(--focus-glow);
83
+ }
84
+
85
+ :host(.dismissed) {
86
+ display: none;
87
+ }
88
+
57
89
  @media (prefers-reduced-motion: reduce) {
58
90
  :host *,
59
91
  :host *::before,
@@ -69,6 +101,8 @@ export class ArcCallout extends LitElement {
69
101
  constructor() {
70
102
  super();
71
103
  this.variant = 'info';
104
+ this.dismissible = false;
105
+ this._dismissed = false;
72
106
  }
73
107
 
74
108
  /** @private */
@@ -93,7 +127,15 @@ export class ArcCallout extends LitElement {
93
127
  return labels[this.variant] || labels.info;
94
128
  }
95
129
 
130
+ _dismiss() {
131
+ this._dismissed = true;
132
+ this.classList.add('dismissed');
133
+ this.dispatchEvent(new CustomEvent('arc-dismiss', { bubbles: true, composed: true }));
134
+ }
135
+
96
136
  render() {
137
+ if (this._dismissed) return html``;
138
+
97
139
  return html`
98
140
  <div class="callout" part="callout" role="note">
99
141
  <div class="callout__header" part="header">
@@ -101,6 +143,13 @@ export class ArcCallout extends LitElement {
101
143
  <slot name="icon"><arc-icon name=${this._getDefaultIcon()} size="sm"></arc-icon></slot>
102
144
  </span>
103
145
  <span class="callout__label" part="label">${this._getLabel()}</span>
146
+ ${this.dismissible ? html`
147
+ <button class="callout__dismiss" @click=${this._dismiss} aria-label="Dismiss" part="dismiss">
148
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
149
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
150
+ </svg>
151
+ </button>
152
+ ` : ''}
104
153
  </div>
105
154
  <div class="callout__content" part="content">
106
155
  <slot></slot>
@@ -4,8 +4,10 @@ import { cardHoverStyles } from '../card-styles.js';
4
4
 
5
5
  export class ArcCard extends LitElement {
6
6
  static properties = {
7
- href: { type: String },
8
- _hasFooter: { state: true },
7
+ href: { type: String },
8
+ padding: { type: String, reflect: true },
9
+ interactive: { type: Boolean, reflect: true },
10
+ _hasFooter: { state: true },
9
11
  };
10
12
 
11
13
  static styles = [
@@ -16,18 +18,25 @@ export class ArcCard extends LitElement {
16
18
 
17
19
  .card { height: 100%; }
18
20
 
19
- /* Suppress hover effect when no href */
20
- :host(:not([href])) .card:hover {
21
+ /* Suppress hover effect when no href and not interactive */
22
+ :host(:not([href]):not([interactive])) .card:hover {
21
23
  background: var(--border-subtle);
22
24
  }
23
- :host(:not([href])) .card:hover .card__inner {
25
+ :host(:not([href]):not([interactive])) .card:hover .card__inner {
24
26
  box-shadow: none;
25
27
  }
26
28
 
29
+ :host([interactive]) .card { cursor: pointer; }
30
+
27
31
  .card__inner {
28
32
  padding: var(--space-xl) var(--space-lg);
29
33
  }
30
34
 
35
+ /* Padding variants */
36
+ :host([padding="none"]) .card__inner { padding: 0; }
37
+ :host([padding="sm"]) .card__inner { padding: var(--space-md) var(--space-sm); }
38
+ :host([padding="lg"]) .card__inner { padding: var(--space-2xl) var(--space-xl); }
39
+
31
40
  .card__body {
32
41
  flex: 1;
33
42
  }
@@ -59,6 +68,8 @@ export class ArcCard extends LitElement {
59
68
  constructor() {
60
69
  super();
61
70
  this.href = '';
71
+ this.padding = 'md';
72
+ this.interactive = false;
62
73
  this._hasFooter = false;
63
74
  }
64
75
 
@@ -3,9 +3,10 @@ import { tokenStyles } from '../shared-styles.js';
3
3
 
4
4
  export class ArcDivider extends LitElement {
5
5
  static properties = {
6
- variant: { type: String, reflect: true },
7
- align: { type: String, reflect: true },
6
+ variant: { type: String, reflect: true },
7
+ align: { type: String, reflect: true },
8
8
  vertical: { type: Boolean, reflect: true },
9
+ label: { type: String },
9
10
  };
10
11
 
11
12
  static styles = [
@@ -128,6 +129,33 @@ export class ArcDivider extends LitElement {
128
129
  background: linear-gradient(180deg, transparent, rgba(var(--text-primary-rgb),0.35), transparent);
129
130
  }
130
131
 
132
+ /* Labeled divider */
133
+ .divider--labeled {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: var(--space-md);
137
+ height: auto;
138
+ background: none !important;
139
+ box-shadow: none !important;
140
+ }
141
+
142
+ .divider__line {
143
+ flex: 1;
144
+ height: 1px;
145
+ background: var(--gradient-divider);
146
+ }
147
+
148
+ .divider__label {
149
+ font-family: var(--font-accent);
150
+ font-size: var(--text-xs);
151
+ font-weight: 600;
152
+ letter-spacing: 1.5px;
153
+ text-transform: uppercase;
154
+ color: var(--text-muted);
155
+ white-space: nowrap;
156
+ flex-shrink: 0;
157
+ }
158
+
131
159
  @keyframes divider-shimmer {
132
160
  0%, 100% { background-position: 200% 0; }
133
161
  50% { background-position: -100% 0; }
@@ -143,9 +171,19 @@ export class ArcDivider extends LitElement {
143
171
  super();
144
172
  this.variant = 'subtle';
145
173
  this.vertical = false;
174
+ this.label = '';
146
175
  }
147
176
 
148
177
  render() {
178
+ if (this.label && !this.vertical) {
179
+ return html`
180
+ <div class="divider divider--labeled" role="separator" part="divider">
181
+ <span class="divider__line" part="line"></span>
182
+ <span class="divider__label" part="label">${this.label}</span>
183
+ <span class="divider__line" part="line"></span>
184
+ </div>
185
+ `;
186
+ }
149
187
  return html`<div class="divider" role="separator" aria-orientation=${this.vertical ? 'vertical' : 'horizontal'} part="divider"></div>`;
150
188
  }
151
189
  }
@@ -6,6 +6,7 @@ export class ArcSkeleton extends LitElement {
6
6
  variant: { type: String, reflect: true },
7
7
  width: { type: String },
8
8
  height: { type: String },
9
+ count: { type: Number },
9
10
  };
10
11
 
11
12
  static styles = [
@@ -43,6 +44,12 @@ export class ArcSkeleton extends LitElement {
43
44
  border-radius: var(--radius-md);
44
45
  }
45
46
 
47
+ .skeleton-group {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--space-sm);
51
+ }
52
+
46
53
  @media (prefers-reduced-motion: reduce) {
47
54
  :host *,
48
55
  :host *::before,
@@ -60,24 +67,28 @@ export class ArcSkeleton extends LitElement {
60
67
  this.variant = 'text';
61
68
  this.width = '';
62
69
  this.height = '';
70
+ this.count = 1;
63
71
  }
64
72
 
65
- render() {
73
+ _renderOne() {
66
74
  const styles = [
67
75
  this.width ? `width:${this.width}` : '',
68
76
  this.height ? `height:${this.height}` : '',
69
77
  this.variant === 'circle' && !this.height && this.width ? `height:${this.width}` : '',
70
78
  ].filter(Boolean).join(';');
71
79
 
80
+ return html`<div class="skeleton" part="skeleton" style=${styles}></div>`;
81
+ }
82
+
83
+ render() {
84
+ const n = Math.max(1, this.count);
85
+ if (n === 1) {
86
+ return html`<div role="status" aria-label="Loading" aria-busy="true">${this._renderOne()}</div>`;
87
+ }
72
88
  return html`
73
- <div
74
- class="skeleton"
75
- part="skeleton"
76
- role="status"
77
- aria-label="Loading"
78
- aria-busy="true"
79
- style=${styles}
80
- ></div>
89
+ <div class="skeleton-group" role="status" aria-label="Loading" aria-busy="true">
90
+ ${Array.from({ length: n }, () => this._renderOne())}
91
+ </div>
81
92
  `;
82
93
  }
83
94
  }
@@ -3,8 +3,10 @@ import { tokenStyles } from '../shared-styles.js';
3
3
 
4
4
  export class ArcStat extends LitElement {
5
5
  static properties = {
6
- value: { type: String },
7
- label: { type: String },
6
+ value: { type: String },
7
+ label: { type: String },
8
+ trend: { type: String, reflect: true },
9
+ change: { type: String },
8
10
  };
9
11
 
10
12
  static styles = [
@@ -49,6 +51,25 @@ export class ArcStat extends LitElement {
49
51
  text-transform: uppercase;
50
52
  color: var(--text-muted);
51
53
  }
54
+
55
+ .stat__trend {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: 4px;
59
+ font-family: var(--font-mono);
60
+ font-size: var(--text-sm);
61
+ font-weight: 500;
62
+ margin-top: var(--space-xs);
63
+ }
64
+
65
+ :host([trend="up"]) .stat__trend { color: var(--color-success); }
66
+ :host([trend="down"]) .stat__trend { color: var(--color-error); }
67
+ :host([trend="neutral"]) .stat__trend { color: var(--text-muted); }
68
+
69
+ .stat__trend-arrow {
70
+ width: 12px;
71
+ height: 12px;
72
+ }
52
73
  `,
53
74
  ];
54
75
 
@@ -56,14 +77,26 @@ export class ArcStat extends LitElement {
56
77
  super();
57
78
  this.value = '';
58
79
  this.label = '';
80
+ this.trend = '';
81
+ this.change = '';
59
82
  }
60
83
 
61
84
  render() {
85
+ const arrowUp = html`<svg class="stat__trend-arrow" viewBox="0 0 16 16" fill="currentColor"><path d="M8 3.5l4.5 5H3.5z"/></svg>`;
86
+ const arrowDown = html`<svg class="stat__trend-arrow" viewBox="0 0 16 16" fill="currentColor"><path d="M8 12.5l4.5-5H3.5z"/></svg>`;
87
+ const dash = html`<svg class="stat__trend-arrow" viewBox="0 0 16 16" fill="currentColor"><rect x="3" y="7" width="10" height="2" rx="1"/></svg>`;
88
+
62
89
  return html`
63
90
  <div class="stat" part="stat">
64
91
  <span class="stat__value" part="value">${this.value}</span>
65
92
  <span class="stat__rule"></span>
66
93
  <span class="stat__label" part="label">${this.label}</span>
94
+ ${this.trend ? html`
95
+ <span class="stat__trend" part="trend">
96
+ ${this.trend === 'up' ? arrowUp : this.trend === 'down' ? arrowDown : dash}
97
+ ${this.change || ''}
98
+ </span>
99
+ ` : ''}
67
100
  </div>
68
101
  `;
69
102
  }
@@ -4,6 +4,7 @@ import { tokenStyles } from '../shared-styles.js';
4
4
  export class ArcTag extends LitElement {
5
5
  static properties = {
6
6
  variant: { type: String, reflect: true },
7
+ size: { type: String, reflect: true },
7
8
  removable: { type: Boolean, reflect: true },
8
9
  disabled: { type: Boolean, reflect: true },
9
10
  color: { type: String },
@@ -70,6 +71,10 @@ export class ArcTag extends LitElement {
70
71
  :host([variant="warning"]) .tag:hover { box-shadow: 0 0 12px rgba(var(--color-warning-rgb), 0.15); }
71
72
  :host([variant="danger"]) .tag:hover { box-shadow: 0 0 12px rgba(var(--color-error-rgb), 0.15); }
72
73
 
74
+ /* Sizes */
75
+ :host([size="sm"]) .tag { font-size: calc(var(--text-xs) - 1px); padding: 2px var(--space-sm); letter-spacing: 1.5px; }
76
+ :host([size="lg"]) .tag { font-size: var(--text-sm); padding: var(--space-sm) var(--space-lg); }
77
+
73
78
  .tag__label {
74
79
  display: inline-flex;
75
80
  align-items: center;
@@ -118,6 +123,7 @@ export class ArcTag extends LitElement {
118
123
  constructor() {
119
124
  super();
120
125
  this.variant = 'default';
126
+ this.size = 'md';
121
127
  this.removable = false;
122
128
  this.disabled = false;
123
129
  this.color = '';
@@ -6,6 +6,7 @@ import { getStatusIcon } from '../status-utils.js';
6
6
  export class ArcAlert extends LitElement {
7
7
  static properties = {
8
8
  variant: { type: String, reflect: true },
9
+ compact: { type: Boolean, reflect: true },
9
10
  dismissible: { type: Boolean },
10
11
  heading: { type: String },
11
12
  };
@@ -72,6 +73,12 @@ export class ArcAlert extends LitElement {
72
73
  color: var(--text-secondary);
73
74
  }
74
75
 
76
+ /* Compact variant */
77
+ :host([compact]) .alert { padding: var(--space-sm) var(--space-md); gap: var(--space-sm); }
78
+ :host([compact]) .alert__icon-wrap { width: 24px; height: 24px; font-size: var(--text-sm); }
79
+ :host([compact]) .alert__heading { font-size: var(--text-sm); margin-bottom: 2px; }
80
+ :host([compact]) .alert__content { font-size: var(--text-xs); }
81
+
75
82
  .alert__dismiss {
76
83
  position: absolute;
77
84
  top: var(--space-sm);
@@ -116,6 +123,7 @@ export class ArcAlert extends LitElement {
116
123
  constructor() {
117
124
  super();
118
125
  this.variant = 'info';
126
+ this.compact = false;
119
127
  this.dismissible = false;
120
128
  this.heading = '';
121
129
  }
@@ -3,10 +3,11 @@ import { tokenStyles } from '../shared-styles.js';
3
3
 
4
4
  export class ArcModal extends LitElement {
5
5
  static properties = {
6
- open: { type: Boolean, reflect: true },
7
- heading: { type: String },
8
- size: { type: String, reflect: true },
9
- closable: { type: Boolean },
6
+ open: { type: Boolean, reflect: true },
7
+ heading: { type: String },
8
+ size: { type: String, reflect: true },
9
+ fullscreen: { type: Boolean, reflect: true },
10
+ closable: { type: Boolean },
10
11
  };
11
12
 
12
13
  static styles = [
@@ -58,6 +59,19 @@ export class ArcModal extends LitElement {
58
59
  :host([size="md"]) .modal__dialog { max-width: 560px; }
59
60
  :host([size="lg"]) .modal__dialog { max-width: 720px; }
60
61
 
62
+ :host([fullscreen]) .modal__dialog {
63
+ max-width: none;
64
+ max-height: none;
65
+ width: 100%;
66
+ height: 100%;
67
+ border-radius: 0;
68
+ border: none;
69
+ }
70
+
71
+ :host([fullscreen]) .modal__backdrop {
72
+ padding: 0;
73
+ }
74
+
61
75
  .modal__header {
62
76
  display: flex;
63
77
  align-items: center;
@@ -127,6 +141,7 @@ export class ArcModal extends LitElement {
127
141
  this.open = false;
128
142
  this.heading = '';
129
143
  this.size = 'md';
144
+ this.fullscreen = false;
130
145
  this.closable = true;
131
146
  this._handleKeydown = this._handleKeydown.bind(this);
132
147
  }
@@ -7,6 +7,7 @@ export class ArcProgress extends LitElement {
7
7
  variant: { type: String, reflect: true },
8
8
  size: { type: String, reflect: true },
9
9
  indeterminate: { type: Boolean, reflect: true },
10
+ showValue: { type: Boolean, attribute: 'show-value', reflect: true },
10
11
  label: { type: String },
11
12
  };
12
13
 
@@ -84,6 +85,19 @@ export class ArcProgress extends LitElement {
84
85
  animation: spinner-dash 1.4s ease-in-out infinite;
85
86
  }
86
87
 
88
+ .progress__header {
89
+ display: flex;
90
+ justify-content: space-between;
91
+ align-items: baseline;
92
+ margin-bottom: var(--space-xs);
93
+ }
94
+
95
+ .progress__value {
96
+ font-family: var(--font-mono);
97
+ font-size: var(--text-xs);
98
+ color: var(--text-muted);
99
+ }
100
+
87
101
  @keyframes progress-indeterminate {
88
102
  0% { transform: translateX(-100%); }
89
103
  100% { transform: translateX(400%); }
@@ -115,6 +129,7 @@ export class ArcProgress extends LitElement {
115
129
  this.variant = 'bar';
116
130
  this.size = 'md';
117
131
  this.indeterminate = false;
132
+ this.showValue = false;
118
133
  this.label = '';
119
134
  }
120
135
 
@@ -143,9 +158,16 @@ export class ArcProgress extends LitElement {
143
158
  `;
144
159
  }
145
160
 
161
+ const hasHeader = this.label || (this.showValue && !this.indeterminate);
162
+
146
163
  return html`
147
164
  <div part="progress">
148
- ${this.label ? html`<span class="progress__label" part="label">${this.label}</span>` : ''}
165
+ ${hasHeader ? html`
166
+ <div class="progress__header">
167
+ ${this.label ? html`<span class="progress__label" style="margin-bottom:0" part="label">${this.label}</span>` : html`<span></span>`}
168
+ ${this.showValue && !this.indeterminate ? html`<span class="progress__value" part="value">${clampedValue}%</span>` : ''}
169
+ </div>
170
+ ` : ''}
149
171
  <div
150
172
  class="progress__track"
151
173
  role="progressbar"
@@ -110,6 +110,26 @@ export class ArcToast extends LitElement {
110
110
 
111
111
  .toast__dismiss:hover { color: var(--text-primary); }
112
112
 
113
+ .toast__action {
114
+ background: none;
115
+ border: none;
116
+ color: var(--_status-color);
117
+ cursor: pointer;
118
+ font-family: var(--font-accent);
119
+ font-size: var(--text-xs);
120
+ font-weight: 600;
121
+ letter-spacing: 1px;
122
+ text-transform: uppercase;
123
+ padding: var(--space-xs) var(--space-sm);
124
+ border-radius: var(--radius-sm);
125
+ transition: background var(--transition-fast);
126
+ white-space: nowrap;
127
+ }
128
+
129
+ .toast__action:hover {
130
+ background: rgba(var(--_status-rgb), 0.1);
131
+ }
132
+
113
133
  .toast.is-exiting {
114
134
  animation: toast-out 200ms ease-in forwards;
115
135
  }
@@ -145,16 +165,21 @@ export class ArcToast extends LitElement {
145
165
  this._counter = 0;
146
166
  }
147
167
 
148
- show({ message, variant = 'info', duration }) {
168
+ show({ message, variant = 'info', duration, action, actionLabel, persistent = false }) {
149
169
  const id = ++this._counter;
150
170
  const dur = duration ?? this.duration;
151
- this._toasts = [...this._toasts, { id, message, variant }];
171
+ this._toasts = [...this._toasts, { id, message, variant, action, actionLabel }];
152
172
 
153
- if (dur > 0) {
173
+ if (!persistent && dur > 0) {
154
174
  setTimeout(() => this._dismiss(id), dur);
155
175
  }
156
176
  }
157
177
 
178
+ _handleAction(toast) {
179
+ if (toast.action) toast.action();
180
+ this._dismiss(toast.id);
181
+ }
182
+
158
183
  _dismiss(id) {
159
184
  if (!this._toasts.some(t => t.id === id)) return;
160
185
  const el = this.shadowRoot.querySelector(`[data-toast-id="${id}"]`);
@@ -183,6 +208,9 @@ export class ArcToast extends LitElement {
183
208
  <div class="toast" style=${statusStyle(t.variant)} data-toast-id=${t.id} part="toast">
184
209
  <span class="toast__icon" aria-hidden="true">${getStatusIcon(t.variant)}</span>
185
210
  <span class="toast__message">${t.message}</span>
211
+ ${t.actionLabel ? html`
212
+ <button class="toast__action" @click=${() => this._handleAction(t)} part="action">${t.actionLabel}</button>
213
+ ` : ''}
186
214
  <button class="toast__dismiss" @click=${() => this._dismiss(t.id)} aria-label="Dismiss">&times;</button>
187
215
  </div>
188
216
  `)}