@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,298 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { tokenStyles } from '../shared-styles.js';
3
+
4
+ export class ArcDialog extends LitElement {
5
+ static properties = {
6
+ open: { type: Boolean, reflect: true },
7
+ heading: { type: String },
8
+ message: { type: String },
9
+ 'confirm-label': { type: String, attribute: 'confirm-label' },
10
+ 'cancel-label': { type: String, attribute: 'cancel-label' },
11
+ variant: { type: String, reflect: true },
12
+ };
13
+
14
+ static styles = [
15
+ tokenStyles,
16
+ css`
17
+ :host { display: contents; }
18
+
19
+ .dialog__backdrop {
20
+ position: fixed;
21
+ inset: 0;
22
+ background: rgba(0, 0, 0, 0.3);
23
+ z-index: 1000;
24
+ display: flex;
25
+ align-items: flex-start;
26
+ justify-content: center;
27
+ padding-top: 15vh;
28
+ padding-left: var(--space-lg);
29
+ padding-right: var(--space-lg);
30
+ opacity: 0;
31
+ visibility: hidden;
32
+ transition: opacity var(--transition-fast), visibility var(--transition-fast);
33
+ }
34
+
35
+ :host([open]) .dialog__backdrop {
36
+ opacity: 1;
37
+ visibility: visible;
38
+ }
39
+
40
+ .dialog__card {
41
+ position: relative;
42
+ background: var(--bg-card);
43
+ border: 1px solid var(--border-subtle);
44
+ border-radius: var(--radius-lg) var(--radius-lg) var(--radius-md) var(--radius-md);
45
+ box-shadow: var(--shadow-overlay);
46
+ width: 100%;
47
+ max-width: 380px;
48
+ display: flex;
49
+ flex-direction: column;
50
+ overflow: hidden;
51
+ transform: translateY(-8px);
52
+ transition: transform var(--transition-fast);
53
+ }
54
+
55
+ :host([open]) .dialog__card {
56
+ transform: translateY(0);
57
+ }
58
+
59
+ /* Top accent line */
60
+ .dialog__accent {
61
+ height: 3px;
62
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary, var(--accent-primary)));
63
+ }
64
+
65
+ :host([variant="danger"]) .dialog__accent {
66
+ background: linear-gradient(90deg, var(--color-error), var(--color-warning, var(--color-error)));
67
+ }
68
+
69
+ :host([variant="danger"]) .dialog__card {
70
+ border-color: rgba(var(--color-error-rgb, 239, 68, 68), 0.25);
71
+ box-shadow: var(--shadow-overlay), 0 0 20px rgba(var(--color-error-rgb, 239, 68, 68), 0.12);
72
+ }
73
+
74
+ .dialog__header {
75
+ padding: var(--space-md) var(--space-lg) 0;
76
+ }
77
+
78
+ .dialog__heading {
79
+ font-size: var(--text-md);
80
+ font-weight: 600;
81
+ color: var(--text-primary);
82
+ margin: 0;
83
+ }
84
+
85
+ .dialog__body {
86
+ padding: var(--space-sm) var(--space-lg);
87
+ color: var(--text-secondary);
88
+ font-family: var(--font-body);
89
+ font-size: var(--body-size);
90
+ line-height: var(--body-lh);
91
+ }
92
+
93
+ .dialog__footer {
94
+ padding: var(--space-sm) var(--space-lg) var(--space-md);
95
+ display: flex;
96
+ justify-content: flex-end;
97
+ gap: var(--space-sm);
98
+ }
99
+
100
+ .dialog__btn {
101
+ font-family: var(--font-accent);
102
+ font-weight: 600;
103
+ font-size: var(--text-sm);
104
+ letter-spacing: 1px;
105
+ text-transform: uppercase;
106
+ border-radius: var(--radius-md);
107
+ padding: var(--space-sm) var(--space-md);
108
+ cursor: pointer;
109
+ transition: background var(--transition-fast), border-color var(--transition-fast),
110
+ color var(--transition-fast), box-shadow var(--transition-fast);
111
+ border: 1px solid transparent;
112
+ line-height: 1;
113
+ }
114
+
115
+ .dialog__btn:focus-visible {
116
+ outline: none;
117
+ box-shadow: var(--focus-ring);
118
+ }
119
+
120
+ .dialog__btn--cancel {
121
+ background: transparent;
122
+ border-color: var(--border-default);
123
+ color: var(--text-secondary);
124
+ }
125
+
126
+ .dialog__btn--cancel:hover {
127
+ background: var(--bg-hover);
128
+ border-color: var(--border-bright);
129
+ color: var(--text-primary);
130
+ }
131
+
132
+ .dialog__btn--confirm {
133
+ background: var(--accent-primary);
134
+ border-color: var(--accent-primary);
135
+ color: var(--text-on-accent, #fff);
136
+ }
137
+
138
+ .dialog__btn--confirm:hover {
139
+ box-shadow: 0 0 16px rgba(var(--accent-primary-rgb), 0.4);
140
+ }
141
+
142
+ :host([variant="danger"]) .dialog__btn--confirm {
143
+ background: var(--color-error);
144
+ border-color: var(--color-error);
145
+ }
146
+
147
+ :host([variant="danger"]) .dialog__btn--confirm:hover {
148
+ box-shadow: 0 0 16px rgba(var(--color-error-rgb, 239, 68, 68), 0.4);
149
+ }
150
+
151
+ @media (prefers-reduced-motion: reduce) {
152
+ .dialog__backdrop,
153
+ .dialog__card { transition: none; }
154
+ }
155
+ `,
156
+ ];
157
+
158
+ constructor() {
159
+ super();
160
+ this.open = false;
161
+ this.heading = '';
162
+ this.message = '';
163
+ this['confirm-label'] = 'Confirm';
164
+ this['cancel-label'] = 'Cancel';
165
+ this.variant = 'default';
166
+ this._handleKeydown = this._handleKeydown.bind(this);
167
+ this._resolvePromise = null;
168
+ }
169
+
170
+ disconnectedCallback() {
171
+ super.disconnectedCallback();
172
+ document.removeEventListener('keydown', this._handleKeydown);
173
+ document.body.style.overflow = '';
174
+ }
175
+
176
+ /**
177
+ * Opens the dialog and returns a Promise that resolves to `true` on confirm
178
+ * or `false` on cancel (including Escape and backdrop click).
179
+ */
180
+ confirm() {
181
+ this.open = true;
182
+ return new Promise((resolve) => {
183
+ this._resolvePromise = resolve;
184
+ });
185
+ }
186
+
187
+ _handleKeydown(e) {
188
+ if (e.key === 'Escape') {
189
+ this._cancel();
190
+ return;
191
+ }
192
+ // Focus trap
193
+ if (e.key === 'Tab') {
194
+ if (!this._focusableEls) {
195
+ this._focusableEls = this.shadowRoot.querySelectorAll(
196
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
197
+ );
198
+ }
199
+ const focusable = this._focusableEls;
200
+ const first = focusable[0];
201
+ const last = focusable[focusable.length - 1];
202
+ const active = this.shadowRoot.activeElement;
203
+
204
+ if (e.shiftKey && (!active || active === first)) {
205
+ e.preventDefault();
206
+ last?.focus();
207
+ } else if (!e.shiftKey && active === last) {
208
+ e.preventDefault();
209
+ first?.focus();
210
+ }
211
+ }
212
+ }
213
+
214
+ _doConfirm() {
215
+ this.open = false;
216
+ this.dispatchEvent(new CustomEvent('arc-confirm', { bubbles: true, composed: true }));
217
+ if (this._resolvePromise) {
218
+ this._resolvePromise(true);
219
+ this._resolvePromise = null;
220
+ }
221
+ }
222
+
223
+ _cancel() {
224
+ this.open = false;
225
+ this.dispatchEvent(new CustomEvent('arc-cancel', { bubbles: true, composed: true }));
226
+ if (this._resolvePromise) {
227
+ this._resolvePromise(false);
228
+ this._resolvePromise = null;
229
+ }
230
+ }
231
+
232
+ _backdropClick(e) {
233
+ if (e.target === e.currentTarget) {
234
+ this._cancel();
235
+ }
236
+ }
237
+
238
+ updated(changed) {
239
+ if (changed.has('open')) {
240
+ if (this.open) {
241
+ this._focusableEls = null;
242
+ document.addEventListener('keydown', this._handleKeydown);
243
+ document.body.style.overflow = 'hidden';
244
+ this.updateComplete.then(() => {
245
+ const confirmBtn = this.shadowRoot.querySelector('.dialog__btn--confirm');
246
+ confirmBtn?.focus();
247
+ });
248
+ } else {
249
+ document.removeEventListener('keydown', this._handleKeydown);
250
+ document.body.style.overflow = '';
251
+ }
252
+ }
253
+ }
254
+
255
+ render() {
256
+ return html`
257
+ <div
258
+ class="dialog__backdrop"
259
+ @click=${this._backdropClick}
260
+ part="backdrop"
261
+ >
262
+ <div
263
+ class="dialog__card"
264
+ role="alertdialog"
265
+ aria-modal="true"
266
+ aria-label=${this.heading || 'Dialog'}
267
+ part="card"
268
+ >
269
+ <div class="dialog__accent" part="accent"></div>
270
+ ${this.heading ? html`
271
+ <div class="dialog__header" part="header">
272
+ <h2 class="dialog__heading">${this.heading}</h2>
273
+ </div>
274
+ ` : ''}
275
+ ${this.message ? html`
276
+ <div class="dialog__body" part="body">
277
+ ${this.message}
278
+ </div>
279
+ ` : ''}
280
+ <div class="dialog__footer" part="footer">
281
+ <button
282
+ class="dialog__btn dialog__btn--cancel"
283
+ @click=${this._cancel}
284
+ part="cancel"
285
+ >${this['cancel-label']}</button>
286
+ <button
287
+ class="dialog__btn dialog__btn--confirm"
288
+ @click=${this._doConfirm}
289
+ part="confirm"
290
+ >${this['confirm-label']}</button>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ `;
295
+ }
296
+ }
297
+
298
+ customElements.define('arc-dialog', ArcDialog);
@@ -0,0 +1,259 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { tokenStyles } from '../shared-styles.js';
3
+ import '../shared/menu-item.js';
4
+ import '../shared/menu-divider.js';
5
+
6
+ export class ArcDropdownMenu extends LitElement {
7
+ static properties = {
8
+ open: { type: Boolean, reflect: true },
9
+ _focusedIndex: { state: true },
10
+ _children: { state: true },
11
+ };
12
+
13
+ static styles = [
14
+ tokenStyles,
15
+ css`
16
+ :host {
17
+ display: inline-block;
18
+ position: relative;
19
+ }
20
+
21
+ .dropdown__trigger {
22
+ display: inline-block;
23
+ cursor: pointer;
24
+ }
25
+
26
+ .dropdown__panel {
27
+ position: absolute;
28
+ z-index: 100;
29
+ top: calc(100% + var(--space-xs));
30
+ left: 0;
31
+ min-width: 200px;
32
+ background: var(--bg-card);
33
+ border: 1px solid var(--border-default);
34
+ border-radius: var(--radius-md);
35
+ padding: var(--space-xs) 0;
36
+ box-shadow: var(--shadow-overlay);
37
+ opacity: 0;
38
+ visibility: hidden;
39
+ transform: translateY(-4px);
40
+ transition:
41
+ opacity var(--transition-base),
42
+ visibility var(--transition-base),
43
+ transform var(--transition-base);
44
+ }
45
+
46
+ :host([open]) .dropdown__panel {
47
+ opacity: 1;
48
+ visibility: visible;
49
+ transform: translateY(0);
50
+ }
51
+
52
+ .dropdown__item {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: var(--space-sm);
56
+ width: 100%;
57
+ padding: var(--touch-pad) var(--space-md);
58
+ min-height: var(--touch-min);
59
+ border: none;
60
+ background: transparent;
61
+ color: var(--text-secondary);
62
+ font-family: var(--font-body);
63
+ font-size: var(--text-sm);
64
+ cursor: pointer;
65
+ text-align: left;
66
+ transition: background var(--transition-fast), color var(--transition-fast);
67
+ outline: none;
68
+ }
69
+
70
+ .dropdown__item:hover,
71
+ .dropdown__item.is-focused {
72
+ background: var(--bg-hover);
73
+ color: var(--text-primary);
74
+ }
75
+
76
+ .dropdown__item:focus-visible {
77
+ box-shadow: inset var(--focus-glow);
78
+ }
79
+
80
+ .dropdown__item-label {
81
+ flex: 1;
82
+ }
83
+
84
+ .dropdown__item-shortcut {
85
+ font-family: var(--font-mono);
86
+ font-size: var(--text-sm);
87
+ color: var(--text-muted);
88
+ opacity: 0.6;
89
+ }
90
+
91
+ .dropdown__divider {
92
+ height: 1px;
93
+ background: var(--border-default);
94
+ margin: var(--space-xs) 0;
95
+ }
96
+
97
+ .dropdown__slot-host { display: none; }
98
+
99
+ @media (prefers-reduced-motion: reduce) {
100
+ :host *,
101
+ :host *::before,
102
+ :host *::after {
103
+ animation-duration: 0.01ms !important;
104
+ animation-iteration-count: 1 !important;
105
+ transition-duration: 0.01ms !important;
106
+ }
107
+ }
108
+ `,
109
+ ];
110
+
111
+ constructor() {
112
+ super();
113
+ this.open = false;
114
+ this._focusedIndex = -1;
115
+ this._children = [];
116
+ this._onDocumentClick = this._onDocumentClick.bind(this);
117
+ this._onKeyDown = this._onKeyDown.bind(this);
118
+ }
119
+
120
+ _onSlotChange(e) {
121
+ this._children = e.target.assignedElements({ flatten: true })
122
+ .filter(el => el.tagName === 'ARC-MENU-ITEM' || el.tagName === 'ARC-MENU-DIVIDER');
123
+ }
124
+
125
+ get _menuItems() {
126
+ return this._children.filter(el => el.tagName === 'ARC-MENU-ITEM');
127
+ }
128
+
129
+ updated(changed) {
130
+ if (changed.has('open')) {
131
+ if (this.open) {
132
+ this._focusedIndex = -1;
133
+ requestAnimationFrame(() => {
134
+ document.addEventListener('click', this._onDocumentClick);
135
+ document.addEventListener('keydown', this._onKeyDown);
136
+ });
137
+ } else {
138
+ document.removeEventListener('click', this._onDocumentClick);
139
+ document.removeEventListener('keydown', this._onKeyDown);
140
+ this._focusedIndex = -1;
141
+ }
142
+ }
143
+ }
144
+
145
+ disconnectedCallback() {
146
+ super.disconnectedCallback();
147
+ document.removeEventListener('click', this._onDocumentClick);
148
+ document.removeEventListener('keydown', this._onKeyDown);
149
+ }
150
+
151
+ _onDocumentClick(e) {
152
+ const path = e.composedPath();
153
+ if (!path.includes(this)) {
154
+ this._close();
155
+ }
156
+ }
157
+
158
+ _onKeyDown(e) {
159
+ if (!this.open) return;
160
+
161
+ const selectable = this._menuItems;
162
+
163
+ switch (e.key) {
164
+ case 'ArrowDown':
165
+ e.preventDefault();
166
+ this._focusedIndex = this._focusedIndex < selectable.length - 1
167
+ ? this._focusedIndex + 1
168
+ : 0;
169
+ break;
170
+ case 'ArrowUp':
171
+ e.preventDefault();
172
+ this._focusedIndex = this._focusedIndex > 0
173
+ ? this._focusedIndex - 1
174
+ : selectable.length - 1;
175
+ break;
176
+ case 'Enter':
177
+ e.preventDefault();
178
+ if (this._focusedIndex >= 0 && this._focusedIndex < selectable.length) {
179
+ this._selectItem(selectable[this._focusedIndex], this._focusedIndex);
180
+ }
181
+ break;
182
+ case 'Escape':
183
+ e.preventDefault();
184
+ this._close();
185
+ break;
186
+ }
187
+ }
188
+
189
+ _toggle() {
190
+ this.open = !this.open;
191
+ }
192
+
193
+ _close() {
194
+ if (!this.open) return;
195
+ this.open = false;
196
+ this.dispatchEvent(new CustomEvent('arc-close', {
197
+ bubbles: true,
198
+ composed: true,
199
+ }));
200
+ }
201
+
202
+ _selectItem(item, index) {
203
+ this.dispatchEvent(new CustomEvent('arc-select', {
204
+ detail: { item: { label: item.label, shortcut: item.shortcut }, index },
205
+ bubbles: true,
206
+ composed: true,
207
+ }));
208
+ this._close();
209
+ }
210
+
211
+ _renderChild(child, globalIndex) {
212
+ if (child.tagName === 'ARC-MENU-DIVIDER') {
213
+ return html`<div class="dropdown__divider" role="separator" part="divider"></div>`;
214
+ }
215
+
216
+ const selectableIndex = this._menuItems.indexOf(child);
217
+
218
+ return html`
219
+ <button
220
+ class="dropdown__item ${selectableIndex === this._focusedIndex ? 'is-focused' : ''}"
221
+ role="menuitem"
222
+ tabindex="-1"
223
+ @click=${() => this._selectItem(child, globalIndex)}
224
+ @mouseenter=${() => { this._focusedIndex = selectableIndex; }}
225
+ part="item"
226
+ >
227
+ <span class="dropdown__item-label">${child.label || ''}</span>
228
+ ${child.shortcut ? html`<span class="dropdown__item-shortcut" part="shortcut">${child.shortcut}</span>` : ''}
229
+ </button>
230
+ `;
231
+ }
232
+
233
+ render() {
234
+ return html`
235
+ <div class="dropdown__slot-host">
236
+ <slot @slotchange=${this._onSlotChange}></slot>
237
+ </div>
238
+ <div
239
+ class="dropdown__trigger"
240
+ @click=${this._toggle}
241
+ aria-haspopup="menu"
242
+ aria-expanded=${this.open ? 'true' : 'false'}
243
+ part="trigger"
244
+ >
245
+ <slot name="trigger"></slot>
246
+ </div>
247
+ <div
248
+ class="dropdown__panel"
249
+ role="menu"
250
+ aria-hidden=${this.open ? 'false' : 'true'}
251
+ part="panel"
252
+ >
253
+ ${this._children.map((child, i) => this._renderChild(child, i))}
254
+ </div>
255
+ `;
256
+ }
257
+ }
258
+
259
+ customElements.define('arc-dropdown-menu', ArcDropdownMenu);