@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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/package.json +186 -0
- package/src/content/accordion-item.js +27 -0
- package/src/content/accordion.js +151 -0
- package/src/content/animated-number.js +160 -0
- package/src/content/aspect-ratio.js +78 -0
- package/src/content/avatar-group.js +101 -0
- package/src/content/avatar.js +92 -0
- package/src/content/badge.js +98 -0
- package/src/content/callout.js +141 -0
- package/src/content/card.js +75 -0
- package/src/content/carousel.js +300 -0
- package/src/content/code-block.js +152 -0
- package/src/content/collapsible.js +142 -0
- package/src/content/color-swatch.js +86 -0
- package/src/content/column.js +28 -0
- package/src/content/data-table.js +332 -0
- package/src/content/divider.js +153 -0
- package/src/content/empty-state.js +78 -0
- package/src/content/feature-card.js +142 -0
- package/src/content/highlight.js +63 -0
- package/src/content/icon-library.js +30 -0
- package/src/content/icon-registry.js +39 -0
- package/src/content/icon.js +95 -0
- package/src/content/index.js +44 -0
- package/src/content/infinite-scroll.js +144 -0
- package/src/content/kbd.js +40 -0
- package/src/content/markdown.js +294 -0
- package/src/content/marquee.js +166 -0
- package/src/content/meter.js +167 -0
- package/src/content/scroll-area.js +107 -0
- package/src/content/skeleton.js +85 -0
- package/src/content/spinner.js +77 -0
- package/src/content/stack.js +68 -0
- package/src/content/stat.js +72 -0
- package/src/content/step.js +22 -0
- package/src/content/stepper.js +202 -0
- package/src/content/table.js +134 -0
- package/src/content/tag.js +156 -0
- package/src/content/text.js +111 -0
- package/src/content/timeline-item.js +29 -0
- package/src/content/timeline.js +170 -0
- package/src/content/truncate.js +161 -0
- package/src/content/value-card.js +94 -0
- package/src/feedback/alert.js +187 -0
- package/src/feedback/command-item.js +28 -0
- package/src/feedback/command-palette.js +346 -0
- package/src/feedback/context-menu.js +299 -0
- package/src/feedback/dialog.js +298 -0
- package/src/feedback/dropdown-menu.js +259 -0
- package/src/feedback/hover-card.js +186 -0
- package/src/feedback/index.js +17 -0
- package/src/feedback/modal.js +226 -0
- package/src/feedback/notification-panel.js +196 -0
- package/src/feedback/popover.js +184 -0
- package/src/feedback/progress.js +169 -0
- package/src/feedback/sheet.js +249 -0
- package/src/feedback/toast.js +207 -0
- package/src/feedback/tooltip.js +189 -0
- package/src/icons/lucide.d.ts +1915 -0
- package/src/icons/lucide.js +1915 -0
- package/src/icons/phosphor.d.ts +1517 -0
- package/src/icons/phosphor.js +1517 -0
- package/src/icons/types.d.ts +8 -0
- package/src/index.js +9 -0
- package/src/input/button.js +127 -0
- package/src/input/calendar.js +340 -0
- package/src/input/checkbox.js +159 -0
- package/src/input/chip.js +120 -0
- package/src/input/color-picker.js +461 -0
- package/src/input/combobox.js +295 -0
- package/src/input/copy-button.js +144 -0
- package/src/input/date-picker.js +534 -0
- package/src/input/file-upload.js +333 -0
- package/src/input/form.js +179 -0
- package/src/input/icon-button.js +179 -0
- package/src/input/index.js +31 -0
- package/src/input/input.js +158 -0
- package/src/input/multi-select.js +392 -0
- package/src/input/number-input.js +239 -0
- package/src/input/otp-input.js +221 -0
- package/src/input/pin-input.js +294 -0
- package/src/input/radio-group.js +177 -0
- package/src/input/radio.js +28 -0
- package/src/input/rating.js +195 -0
- package/src/input/search.js +371 -0
- package/src/input/segmented-control.js +174 -0
- package/src/input/select.js +267 -0
- package/src/input/slider.js +217 -0
- package/src/input/sortable-list.js +345 -0
- package/src/input/suggestion.js +26 -0
- package/src/input/textarea.js +203 -0
- package/src/input/theme-toggle.js +196 -0
- package/src/input/toggle.js +166 -0
- package/src/layout/app-shell.js +266 -0
- package/src/layout/auth-shell.js +153 -0
- package/src/layout/container.js +37 -0
- package/src/layout/dashboard-grid.js +62 -0
- package/src/layout/index.js +15 -0
- package/src/layout/page-header.js +100 -0
- package/src/layout/page-layout.js +112 -0
- package/src/layout/resizable.js +221 -0
- package/src/layout/section.js +54 -0
- package/src/layout/settings-layout.js +91 -0
- package/src/layout/split-pane.js +172 -0
- package/src/layout/status-bar.js +84 -0
- package/src/layout/toolbar.js +92 -0
- package/src/navigation/breadcrumb-item.js +26 -0
- package/src/navigation/breadcrumb.js +129 -0
- package/src/navigation/drawer.js +183 -0
- package/src/navigation/footer.js +99 -0
- package/src/navigation/index.js +22 -0
- package/src/navigation/link.js +120 -0
- package/src/navigation/nav-item.js +46 -0
- package/src/navigation/navigation-menu.js +687 -0
- package/src/navigation/pagination.js +186 -0
- package/src/navigation/scroll-spy.js +198 -0
- package/src/navigation/scroll-to-top.js +163 -0
- package/src/navigation/sidebar-link.js +28 -0
- package/src/navigation/sidebar-section.js +45 -0
- package/src/navigation/sidebar.js +336 -0
- package/src/navigation/spy-link.js +26 -0
- package/src/navigation/tab.js +26 -0
- package/src/navigation/tabs.js +202 -0
- package/src/navigation/top-bar.js +263 -0
- package/src/navigation/tree-item.js +38 -0
- package/src/navigation/tree-view.js +255 -0
- package/src/shared/index.js +6 -0
- package/src/shared/menu-divider.js +9 -0
- package/src/shared/menu-item.js +30 -0
- package/src/shared/option.js +31 -0
- package/src/shared-styles.js +81 -0
- package/src/tokens.js +445 -0
- package/types/index.d.ts +973 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { tokenStyles } from '../shared-styles.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize an HTML string by stripping <script> tags and on* event attributes.
|
|
6
|
+
*/
|
|
7
|
+
function sanitizeHtml(raw) {
|
|
8
|
+
const doc = new DOMParser().parseFromString(raw, 'text/html');
|
|
9
|
+
for (const el of doc.querySelectorAll('script')) el.remove();
|
|
10
|
+
for (const el of doc.querySelectorAll('*')) {
|
|
11
|
+
for (const attr of [...el.attributes]) {
|
|
12
|
+
if (attr.name.startsWith('on')) el.removeAttribute(attr.name);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return doc.body.innerHTML;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lightweight regex-based Markdown-to-HTML parser.
|
|
20
|
+
* Handles headings, bold, italic, inline code, code blocks, lists,
|
|
21
|
+
* blockquotes, links, images, horizontal rules, and paragraphs.
|
|
22
|
+
*/
|
|
23
|
+
function parseMarkdown(src) {
|
|
24
|
+
if (!src) return '';
|
|
25
|
+
|
|
26
|
+
let out = '';
|
|
27
|
+
// Normalize line endings
|
|
28
|
+
const text = src.replace(/\r\n?/g, '\n');
|
|
29
|
+
|
|
30
|
+
// Extract fenced code blocks first to protect them from further parsing
|
|
31
|
+
const codeBlocks = [];
|
|
32
|
+
const withPlaceholders = text.replace(/^```(\w*)\n([\s\S]*?)^```/gm, (_match, lang, code) => {
|
|
33
|
+
const idx = codeBlocks.length;
|
|
34
|
+
const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
35
|
+
const langAttr = lang ? ` class="language-${lang}"` : '';
|
|
36
|
+
codeBlocks.push(`<pre><code${langAttr}>${escaped}</code></pre>`);
|
|
37
|
+
return `\x00CODEBLOCK_${idx}\x00`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Split into blocks by double newline
|
|
41
|
+
const blocks = withPlaceholders.split(/\n{2,}/);
|
|
42
|
+
|
|
43
|
+
for (const block of blocks) {
|
|
44
|
+
const trimmed = block.trim();
|
|
45
|
+
if (!trimmed) continue;
|
|
46
|
+
|
|
47
|
+
// Code block placeholder
|
|
48
|
+
const cbMatch = trimmed.match(/^\x00CODEBLOCK_(\d+)\x00$/);
|
|
49
|
+
if (cbMatch) {
|
|
50
|
+
out += codeBlocks[parseInt(cbMatch[1])];
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Heading
|
|
55
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
56
|
+
if (headingMatch) {
|
|
57
|
+
const level = headingMatch[1].length;
|
|
58
|
+
out += `<h${level}>${inlineMarkdown(headingMatch[2])}</h${level}>`;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Horizontal rule
|
|
63
|
+
if (/^[-*_]{3,}\s*$/.test(trimmed)) {
|
|
64
|
+
out += '<hr>';
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Blockquote
|
|
69
|
+
if (/^>\s?/.test(trimmed)) {
|
|
70
|
+
const content = trimmed
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map(l => l.replace(/^>\s?/, ''))
|
|
73
|
+
.join('\n');
|
|
74
|
+
out += `<blockquote>${parseMarkdown(content)}</blockquote>`;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Unordered list
|
|
79
|
+
if (/^[\-*]\s/.test(trimmed)) {
|
|
80
|
+
const items = trimmed.split('\n').filter(l => /^[\-*]\s/.test(l.trim()));
|
|
81
|
+
out += '<ul>' + items.map(l => `<li>${inlineMarkdown(l.trim().replace(/^[\-*]\s+/, ''))}</li>`).join('') + '</ul>';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ordered list
|
|
86
|
+
if (/^\d+\.\s/.test(trimmed)) {
|
|
87
|
+
const items = trimmed.split('\n').filter(l => /^\d+\.\s/.test(l.trim()));
|
|
88
|
+
out += '<ol>' + items.map(l => `<li>${inlineMarkdown(l.trim().replace(/^\d+\.\s+/, ''))}</li>`).join('') + '</ol>';
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Paragraph (default)
|
|
93
|
+
out += `<p>${inlineMarkdown(trimmed.replace(/\n/g, ' '))}</p>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse inline markdown: images, links, bold, italic, inline code.
|
|
101
|
+
*/
|
|
102
|
+
function inlineMarkdown(text) {
|
|
103
|
+
let s = text;
|
|
104
|
+
// Escape HTML entities in source (but not our generated tags)
|
|
105
|
+
s = s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
106
|
+
// Inline code (before bold/italic so backticks are handled first)
|
|
107
|
+
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
108
|
+
// Images
|
|
109
|
+
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
110
|
+
// Links
|
|
111
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
112
|
+
// Bold
|
|
113
|
+
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
114
|
+
// Italic
|
|
115
|
+
s = s.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @arc-prism static — renders Markdown content as styled HTML
|
|
121
|
+
*/
|
|
122
|
+
export class ArcMarkdown extends LitElement {
|
|
123
|
+
static properties = {
|
|
124
|
+
content: { type: String },
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
static styles = [
|
|
128
|
+
tokenStyles,
|
|
129
|
+
css`
|
|
130
|
+
:host { display: block; }
|
|
131
|
+
|
|
132
|
+
.markdown {
|
|
133
|
+
font-family: var(--font-body);
|
|
134
|
+
font-size: var(--body-size);
|
|
135
|
+
font-weight: var(--body-weight);
|
|
136
|
+
line-height: var(--body-lh);
|
|
137
|
+
color: var(--text-secondary);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.markdown h1,
|
|
141
|
+
.markdown h2,
|
|
142
|
+
.markdown h3,
|
|
143
|
+
.markdown h4,
|
|
144
|
+
.markdown h5,
|
|
145
|
+
.markdown h6 {
|
|
146
|
+
font-family: var(--font-body);
|
|
147
|
+
color: var(--text-primary);
|
|
148
|
+
line-height: 1.3;
|
|
149
|
+
margin-top: var(--space-xl);
|
|
150
|
+
margin-bottom: var(--space-md);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.markdown h1 { font-size: var(--text-3xl); font-weight: 600; }
|
|
154
|
+
.markdown h2 { font-size: var(--heading-size); font-weight: 600; }
|
|
155
|
+
.markdown h3 { font-size: var(--text-lg); font-weight: 600; }
|
|
156
|
+
.markdown h4 { font-size: var(--body-size); font-weight: 600; }
|
|
157
|
+
.markdown h5 { font-size: var(--body-size); font-weight: 500; }
|
|
158
|
+
.markdown h6 { font-size: var(--code-size); font-weight: 500; text-transform: uppercase; letter-spacing: 1px; }
|
|
159
|
+
|
|
160
|
+
.markdown h1:first-child,
|
|
161
|
+
.markdown h2:first-child,
|
|
162
|
+
.markdown h3:first-child,
|
|
163
|
+
.markdown h4:first-child,
|
|
164
|
+
.markdown h5:first-child,
|
|
165
|
+
.markdown h6:first-child { margin-top: 0; }
|
|
166
|
+
|
|
167
|
+
.markdown p {
|
|
168
|
+
margin: 0 0 var(--space-md) 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.markdown p:last-child { margin-bottom: 0; }
|
|
172
|
+
|
|
173
|
+
.markdown strong { color: var(--text-primary); font-weight: 600; }
|
|
174
|
+
|
|
175
|
+
.markdown em { font-style: italic; }
|
|
176
|
+
|
|
177
|
+
.markdown a {
|
|
178
|
+
color: var(--accent-primary);
|
|
179
|
+
text-decoration: none;
|
|
180
|
+
transition: color var(--transition-fast);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.markdown a:hover {
|
|
184
|
+
text-decoration: underline;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.markdown code {
|
|
188
|
+
font-family: var(--font-mono);
|
|
189
|
+
font-size: var(--code-size);
|
|
190
|
+
color: var(--accent-secondary);
|
|
191
|
+
background: var(--bg-surface);
|
|
192
|
+
padding: 2px calc(var(--space-xs) + 2px); /* cosmetic 2px vertical for inline code */
|
|
193
|
+
border-radius: var(--radius-sm);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.markdown pre {
|
|
197
|
+
background: var(--bg-surface);
|
|
198
|
+
border: 1px solid var(--border-subtle);
|
|
199
|
+
border-radius: var(--radius-md);
|
|
200
|
+
padding: var(--space-md);
|
|
201
|
+
overflow-x: auto;
|
|
202
|
+
margin: 0 0 var(--space-md) 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.markdown pre code {
|
|
206
|
+
background: none;
|
|
207
|
+
padding: 0;
|
|
208
|
+
border-radius: 0;
|
|
209
|
+
font-size: var(--code-size);
|
|
210
|
+
line-height: var(--code-lh);
|
|
211
|
+
color: var(--text-primary);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.markdown blockquote {
|
|
215
|
+
margin: 0 0 var(--space-md) 0;
|
|
216
|
+
padding: var(--space-sm) var(--space-md);
|
|
217
|
+
background: var(--bg-card);
|
|
218
|
+
border: 1px solid var(--border-subtle);
|
|
219
|
+
border-radius: var(--radius-md);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.markdown blockquote p:last-child { margin-bottom: 0; }
|
|
223
|
+
|
|
224
|
+
.markdown ul,
|
|
225
|
+
.markdown ol {
|
|
226
|
+
margin: 0 0 var(--space-md) 0;
|
|
227
|
+
padding-left: var(--space-lg);
|
|
228
|
+
color: var(--text-secondary);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.markdown li {
|
|
232
|
+
margin-bottom: var(--space-xs);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.markdown li:last-child { margin-bottom: 0; }
|
|
236
|
+
|
|
237
|
+
.markdown hr {
|
|
238
|
+
border: none;
|
|
239
|
+
height: 1px;
|
|
240
|
+
background: var(--border-subtle);
|
|
241
|
+
margin: var(--space-xl) 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.markdown img {
|
|
245
|
+
max-width: 100%;
|
|
246
|
+
height: auto;
|
|
247
|
+
border-radius: var(--radius-md);
|
|
248
|
+
margin: var(--space-sm) 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@media (prefers-reduced-motion: reduce) {
|
|
252
|
+
:host *,
|
|
253
|
+
:host *::before,
|
|
254
|
+
:host *::after {
|
|
255
|
+
animation-duration: 0.01ms !important;
|
|
256
|
+
animation-iteration-count: 1 !important;
|
|
257
|
+
transition-duration: 0.01ms !important;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
`,
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
constructor() {
|
|
264
|
+
super();
|
|
265
|
+
this.content = '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_getSource() {
|
|
269
|
+
if (this.content) return this.content;
|
|
270
|
+
// Fall back to slotted text content
|
|
271
|
+
const slot = this.shadowRoot?.querySelector('slot');
|
|
272
|
+
if (slot) {
|
|
273
|
+
const nodes = slot.assignedNodes({ flatten: true });
|
|
274
|
+
return nodes.map(n => n.textContent).join('');
|
|
275
|
+
}
|
|
276
|
+
return this.textContent || '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
render() {
|
|
280
|
+
const source = this.content || this.textContent || '';
|
|
281
|
+
const parsed = sanitizeHtml(parseMarkdown(source));
|
|
282
|
+
return html`
|
|
283
|
+
<div class="markdown" part="markdown" .innerHTML=${parsed}></div>
|
|
284
|
+
<slot style="display:none" @slotchange=${this._onSlotChange}></slot>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_onSlotChange() {
|
|
289
|
+
// Re-render when slot content changes and no content prop is set
|
|
290
|
+
if (!this.content) this.requestUpdate();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
customElements.define('arc-markdown', ArcMarkdown);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { tokenStyles } from '../shared-styles.js';
|
|
3
|
+
|
|
4
|
+
export class ArcMarquee extends LitElement {
|
|
5
|
+
static properties = {
|
|
6
|
+
speed: { type: Number },
|
|
7
|
+
direction: { type: String, reflect: true },
|
|
8
|
+
'pause-on-hover': { type: Boolean, reflect: true, attribute: 'pause-on-hover' },
|
|
9
|
+
gap: { type: String },
|
|
10
|
+
_animDuration: { state: true },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
static styles = [
|
|
14
|
+
tokenStyles,
|
|
15
|
+
css`
|
|
16
|
+
:host {
|
|
17
|
+
display: block;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.marquee {
|
|
22
|
+
display: flex;
|
|
23
|
+
width: max-content;
|
|
24
|
+
will-change: transform;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.marquee--left {
|
|
28
|
+
animation: marquee-scroll-left var(--marquee-duration, 10s) linear infinite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.marquee--right {
|
|
32
|
+
animation: marquee-scroll-right var(--marquee-duration, 10s) linear infinite;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:host([pause-on-hover]) .marquee:hover {
|
|
36
|
+
animation-play-state: paused;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@media (prefers-reduced-motion: reduce) {
|
|
40
|
+
.marquee {
|
|
41
|
+
animation-play-state: paused !important;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@keyframes marquee-scroll-left {
|
|
46
|
+
from { transform: translateX(0); }
|
|
47
|
+
to { transform: translateX(-50%); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@keyframes marquee-scroll-right {
|
|
51
|
+
from { transform: translateX(-50%); }
|
|
52
|
+
to { transform: translateX(0); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.marquee__group {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
::slotted(*) {
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
constructor() {
|
|
68
|
+
super();
|
|
69
|
+
this.speed = 40;
|
|
70
|
+
this.direction = 'left';
|
|
71
|
+
this['pause-on-hover'] = true;
|
|
72
|
+
this.gap = 'var(--space-xl)';
|
|
73
|
+
this._animDuration = '10s';
|
|
74
|
+
this._resizeObserver = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
firstUpdated() {
|
|
78
|
+
this._updateDuplicate();
|
|
79
|
+
this._setupResizeObserver();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updated(changed) {
|
|
83
|
+
if (changed.has('speed') || changed.has('gap')) {
|
|
84
|
+
this._recalcDuration();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disconnectedCallback() {
|
|
89
|
+
super.disconnectedCallback();
|
|
90
|
+
if (this._resizeObserver) {
|
|
91
|
+
this._resizeObserver.disconnect();
|
|
92
|
+
this._resizeObserver = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_setupResizeObserver() {
|
|
97
|
+
this._resizeObserver = new ResizeObserver(() => this._recalcDuration());
|
|
98
|
+
const group = this.shadowRoot.querySelector('.marquee__group--primary');
|
|
99
|
+
if (group) this._resizeObserver.observe(group);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_recalcDuration() {
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
const group = this.shadowRoot.querySelector('.marquee__group--primary');
|
|
105
|
+
if (!group) return;
|
|
106
|
+
const width = group.scrollWidth;
|
|
107
|
+
if (width > 0 && this.speed > 0) {
|
|
108
|
+
const seconds = width / this.speed;
|
|
109
|
+
this._animDuration = `${seconds.toFixed(2)}s`;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_onSlotChange() {
|
|
115
|
+
this._updateDuplicate();
|
|
116
|
+
this._recalcDuration();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clone slotted light-DOM children into the shadow-DOM duplicate group
|
|
121
|
+
* for seamless looping. The duplicate is aria-hidden since it's decorative.
|
|
122
|
+
*/
|
|
123
|
+
_updateDuplicate() {
|
|
124
|
+
const dupGroup = this.shadowRoot.querySelector('.marquee__group--duplicate');
|
|
125
|
+
if (!dupGroup) return;
|
|
126
|
+
|
|
127
|
+
// Clear previous clones
|
|
128
|
+
while (dupGroup.firstChild) dupGroup.removeChild(dupGroup.firstChild);
|
|
129
|
+
|
|
130
|
+
// Clone each assigned node from the primary slot
|
|
131
|
+
const slot = this.shadowRoot.querySelector('slot:not([name])');
|
|
132
|
+
if (!slot) return;
|
|
133
|
+
const nodes = slot.assignedNodes({ flatten: true });
|
|
134
|
+
for (const node of nodes) {
|
|
135
|
+
dupGroup.appendChild(node.cloneNode(true));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
render() {
|
|
140
|
+
const dirClass = this.direction === 'right' ? 'marquee--right' : 'marquee--left';
|
|
141
|
+
|
|
142
|
+
return html`
|
|
143
|
+
<div
|
|
144
|
+
class="marquee ${dirClass}"
|
|
145
|
+
style="--marquee-duration: ${this._animDuration}; gap: ${this.gap}"
|
|
146
|
+
part="track"
|
|
147
|
+
>
|
|
148
|
+
<div
|
|
149
|
+
class="marquee__group marquee__group--primary"
|
|
150
|
+
style="gap: ${this.gap}"
|
|
151
|
+
part="group"
|
|
152
|
+
>
|
|
153
|
+
<slot @slotchange=${this._onSlotChange}></slot>
|
|
154
|
+
</div>
|
|
155
|
+
<div
|
|
156
|
+
class="marquee__group marquee__group--duplicate"
|
|
157
|
+
style="gap: ${this.gap}"
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
part="group-duplicate"
|
|
160
|
+
></div>
|
|
161
|
+
</div>
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
customElements.define('arc-marquee', ArcMarquee);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { tokenStyles } from '../shared-styles.js';
|
|
3
|
+
|
|
4
|
+
export class ArcMeter extends LitElement {
|
|
5
|
+
static properties = {
|
|
6
|
+
value: { type: Number, reflect: true },
|
|
7
|
+
min: { type: Number },
|
|
8
|
+
max: { type: Number },
|
|
9
|
+
low: { type: Number },
|
|
10
|
+
high: { type: Number },
|
|
11
|
+
optimum: { type: Number },
|
|
12
|
+
label: { type: String },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
static styles = [
|
|
16
|
+
tokenStyles,
|
|
17
|
+
css`
|
|
18
|
+
:host { display: block; }
|
|
19
|
+
|
|
20
|
+
.meter {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--space-xs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.meter__header {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.meter__label {
|
|
33
|
+
font-family: var(--font-body);
|
|
34
|
+
font-size: var(--body-size);
|
|
35
|
+
color: var(--text-secondary);
|
|
36
|
+
user-select: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.meter__value {
|
|
40
|
+
font-family: var(--font-mono);
|
|
41
|
+
font-size: var(--code-size);
|
|
42
|
+
color: var(--text-muted);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.meter__track {
|
|
46
|
+
position: relative;
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 8px;
|
|
49
|
+
background: var(--bg-elevated);
|
|
50
|
+
border-radius: var(--radius-full);
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.meter__fill {
|
|
55
|
+
height: 100%;
|
|
56
|
+
border-radius: var(--radius-full);
|
|
57
|
+
transition: width var(--transition-base), background var(--transition-base);
|
|
58
|
+
min-width: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.meter__fill--success { background: var(--color-success); }
|
|
62
|
+
.meter__fill--warning { background: var(--color-warning); }
|
|
63
|
+
.meter__fill--error { background: var(--color-error); }
|
|
64
|
+
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
:host *,
|
|
67
|
+
:host *::before,
|
|
68
|
+
:host *::after {
|
|
69
|
+
animation-duration: 0.01ms !important;
|
|
70
|
+
animation-iteration-count: 1 !important;
|
|
71
|
+
transition-duration: 0.01ms !important;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
`,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
constructor() {
|
|
78
|
+
super();
|
|
79
|
+
this.value = 0;
|
|
80
|
+
this.min = 0;
|
|
81
|
+
this.max = 100;
|
|
82
|
+
this.low = undefined;
|
|
83
|
+
this.high = undefined;
|
|
84
|
+
this.optimum = undefined;
|
|
85
|
+
this.label = '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Clamp and compute fill percentage. */
|
|
89
|
+
get _percent() {
|
|
90
|
+
const range = this.max - this.min;
|
|
91
|
+
if (range <= 0) return 0;
|
|
92
|
+
const clamped = Math.max(this.min, Math.min(this.max, this.value));
|
|
93
|
+
return ((clamped - this.min) / range) * 100;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Determine color zone based on low / high / optimum thresholds.
|
|
98
|
+
*
|
|
99
|
+
* Logic mirrors the HTML <meter> algorithm:
|
|
100
|
+
* - If optimum is in the "good" segment and value is there too -> success
|
|
101
|
+
* - If value is in the middle segment (between low and high) -> warning
|
|
102
|
+
* - If value is in the far-from-optimum segment -> error
|
|
103
|
+
* - Fallback when thresholds are not set: use simple thirds.
|
|
104
|
+
*/
|
|
105
|
+
get _zone() {
|
|
106
|
+
const { value, min, max } = this;
|
|
107
|
+
const low = this.low ?? min + (max - min) * 0.33;
|
|
108
|
+
const high = this.high ?? min + (max - min) * 0.67;
|
|
109
|
+
const optimum = this.optimum ?? (low + high) / 2;
|
|
110
|
+
|
|
111
|
+
// Determine which segment the optimum lives in
|
|
112
|
+
const optimumInLow = optimum <= low;
|
|
113
|
+
const optimumInHigh = optimum >= high;
|
|
114
|
+
|
|
115
|
+
if (optimumInLow) {
|
|
116
|
+
// Lower is better (e.g. error count)
|
|
117
|
+
if (value <= low) return 'success';
|
|
118
|
+
if (value <= high) return 'warning';
|
|
119
|
+
return 'error';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (optimumInHigh) {
|
|
123
|
+
// Higher is better (e.g. battery level)
|
|
124
|
+
if (value >= high) return 'success';
|
|
125
|
+
if (value >= low) return 'warning';
|
|
126
|
+
return 'error';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Optimum is in the middle segment
|
|
130
|
+
if (value >= low && value <= high) return 'success';
|
|
131
|
+
if (value < low) return 'warning';
|
|
132
|
+
return 'warning';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render() {
|
|
136
|
+
const percent = this._percent;
|
|
137
|
+
const zone = this._zone;
|
|
138
|
+
|
|
139
|
+
return html`
|
|
140
|
+
<div
|
|
141
|
+
class="meter"
|
|
142
|
+
part="meter"
|
|
143
|
+
role="meter"
|
|
144
|
+
aria-valuemin=${this.min}
|
|
145
|
+
aria-valuemax=${this.max}
|
|
146
|
+
aria-valuenow=${this.value}
|
|
147
|
+
aria-label=${this.label || 'Meter'}
|
|
148
|
+
>
|
|
149
|
+
${this.label ? html`
|
|
150
|
+
<div class="meter__header" part="header">
|
|
151
|
+
<span class="meter__label" part="label">${this.label}</span>
|
|
152
|
+
<span class="meter__value" part="value">${Math.round(percent)}%</span>
|
|
153
|
+
</div>
|
|
154
|
+
` : ''}
|
|
155
|
+
<div class="meter__track" part="track">
|
|
156
|
+
<div
|
|
157
|
+
class="meter__fill meter__fill--${zone}"
|
|
158
|
+
style="width: ${percent}%"
|
|
159
|
+
part="fill"
|
|
160
|
+
></div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
customElements.define('arc-meter', ArcMeter);
|