svelte-comp 1.0.7 → 1.1.2
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/README.md +8 -6
- package/dist/App.svelte +555 -0
- package/dist/App.svelte.d.ts +3 -0
- package/dist/app.css +3 -0
- package/dist/lib/Carousel.svelte +6 -3
- package/dist/lib/CheckBox.svelte +1 -1
- package/dist/lib/CheckBox.svelte.d.ts +1 -1
- package/dist/lib/CodeView.svelte +26 -8
- package/dist/lib/FilePicker.svelte +1 -1
- package/dist/lib/Form.svelte +2 -2
- package/dist/lib/Hamburger.svelte +2 -2
- package/dist/lib/Hamburger.svelte.d.ts +1 -1
- package/dist/lib/Menu.svelte +59 -17
- package/dist/lib/PaginatedCard.svelte +1 -1
- package/dist/lib/Pagination.svelte +1 -1
- package/dist/lib/PrimaryColorSelect.svelte +99 -0
- package/dist/lib/PrimaryColorSelect.svelte.d.ts +9 -0
- package/dist/lib/ProgressCircle.svelte +191 -0
- package/dist/lib/ProgressCircle.svelte.d.ts +39 -0
- package/dist/lib/Select.svelte +28 -6
- package/dist/lib/Splitter.svelte +1 -1
- package/dist/lib/Table.svelte +1 -1
- package/dist/lib/Tabs.svelte +12 -8
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/lang.d.ts +9 -0
- package/dist/lib/lang.js +3 -0
- package/dist/lib/types/index.d.ts +7 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.js +8 -0
- package/dist/styles.css +64 -1
- package/package.json +9 -5
package/dist/lib/CodeView.svelte
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
import "prismjs/components/prism-json";
|
|
49
49
|
import "prismjs/components/prism-python";
|
|
50
50
|
import "prismjs/themes/prism.css";
|
|
51
|
+
import { cx } from "../utils";
|
|
51
52
|
|
|
52
53
|
type Props = {
|
|
53
54
|
code?: string;
|
|
@@ -140,20 +141,27 @@
|
|
|
140
141
|
</script>
|
|
141
142
|
|
|
142
143
|
<div
|
|
143
|
-
class=
|
|
144
|
+
class={cx(
|
|
145
|
+
"w-full border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]",
|
|
146
|
+
"text-[var(--color-text-default)]"
|
|
147
|
+
)}
|
|
144
148
|
>
|
|
145
149
|
{#if title}
|
|
146
150
|
<div
|
|
147
|
-
class=
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
class={cx(
|
|
152
|
+
"px-3 py-1 bg-[var(--color-bg-muted)] font-semibold uppercase flex items-center justify-between",
|
|
153
|
+
TEXT[sz]
|
|
154
|
+
)}
|
|
150
155
|
>
|
|
151
156
|
<div>{title}</div>
|
|
152
157
|
|
|
153
158
|
{#if showCopyButton}
|
|
154
159
|
<button
|
|
155
160
|
onclick={copyToClipboard}
|
|
156
|
-
class=
|
|
161
|
+
class={cx(
|
|
162
|
+
"px-3 py-0.5 text-xs rounded bg-[var(--color-primary)] text-white hover:opacity-[var(--opacity-hover)]",
|
|
163
|
+
"transition focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] focus:outline-none"
|
|
164
|
+
)}
|
|
157
165
|
class:!bg-green-600={copied}
|
|
158
166
|
>
|
|
159
167
|
{copied ? "Copied" : "Copy"}
|
|
@@ -166,9 +174,11 @@
|
|
|
166
174
|
{#if showLineNumbers}
|
|
167
175
|
<div
|
|
168
176
|
bind:this={gutterEl}
|
|
169
|
-
class=
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
class={cx(
|
|
178
|
+
"select-none px-3 py-[12px] border-r border-[var(--border-color-default)]",
|
|
179
|
+
"text-[var(--color-text-muted)] text-right overflow-hidden",
|
|
180
|
+
"bg-[var(--color-bg-surface)] tabular-nums min-h-[180px] max-h-[480px]"
|
|
181
|
+
)}
|
|
172
182
|
>
|
|
173
183
|
{#each lines as _, i (i)}
|
|
174
184
|
<div class={LINE_HEIGHT[sz]}>{i + 1}</div>
|
|
@@ -252,6 +262,14 @@
|
|
|
252
262
|
box-sizing: border-box;
|
|
253
263
|
}
|
|
254
264
|
|
|
265
|
+
.cv-input:focus {
|
|
266
|
+
outline: none;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.cv-input:focus-visible {
|
|
270
|
+
outline: none !important;
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
/* Prism */
|
|
256
274
|
.token.comment {
|
|
257
275
|
color: oklch(0.937 0.019 256 / 0.45);
|
package/dist/lib/Form.svelte
CHANGED
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
* @prop compact {boolean} - Enables denser sizing across controls
|
|
39
39
|
* @default false
|
|
40
40
|
*
|
|
41
|
-
* @note Initial value for each field: `value[name]`
|
|
41
|
+
* @note Initial value for each field: `value[name]` → `schema.default` → `''` (or `false` for checkboxes).
|
|
42
42
|
* @note `validateOn='input'|'blur'|'submit'` controls when validators run; built-in checks: `required`, `number`, and `email` regex.
|
|
43
43
|
* @note `when(form)` hides a field dynamically; hidden fields are skipped during validation.
|
|
44
44
|
* @note `Select` options are coerced to strings for the underlying control; provide string values if you rely on strict equality.
|
|
45
45
|
* @note Errors are rendered with stable `id`s and wired via `aria-describedby`; `invalid` flags are passed to inputs.
|
|
46
46
|
* @note `expose` provides `{ reset, submit, validate, getData }`; `validate` returns `Promise<boolean>`.
|
|
47
|
-
* @note `compact` reduces control sizes (`xs
|
|
47
|
+
* @note `compact` reduces control sizes (`xs→xs`, `sm→xs`, `md→sm`, `lg→md`, `xl→lg`) and centers labels where applicable.
|
|
48
48
|
*/
|
|
49
49
|
import Field from "./Field.svelte";
|
|
50
50
|
import Select from "./Select.svelte";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- src/lib/Hamburger.svelte -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
/**
|
|
4
4
|
* @component Hamburger
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* @note Clicking outside the panel or pressing `Escape` closes the drawer.
|
|
33
33
|
* @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
|
|
34
34
|
* @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
|
|
35
|
-
* @note When `menuItems` is empty, a
|
|
35
|
+
* @note When `menuItems` is empty, a "No items" placeholder is shown.
|
|
36
36
|
* @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
|
|
37
37
|
*/
|
|
38
38
|
import type { Snippet } from "svelte";
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* @note Clicking outside the panel or pressing `Escape` closes the drawer.
|
|
31
31
|
* @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
|
|
32
32
|
* @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
|
|
33
|
-
* @note When `menuItems` is empty, a
|
|
33
|
+
* @note When `menuItems` is empty, a "No items" placeholder is shown.
|
|
34
34
|
* @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
|
|
35
35
|
*/
|
|
36
36
|
import type { Snippet } from "svelte";
|
package/dist/lib/Menu.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- src/lib/Menu.svelte -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
/**
|
|
4
4
|
* @component Menu
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
// Refs for focus control
|
|
50
50
|
let triggerRefs = $state<Record<string, HTMLButtonElement>>({});
|
|
51
|
+
let menuRefs = $state<Record<string, HTMLDivElement>>({});
|
|
51
52
|
let itemRefs = $state<Record<string, HTMLButtonElement>>({});
|
|
52
53
|
let subItemRefs = $state<Record<string, HTMLButtonElement>>({});
|
|
53
54
|
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
let menuTop = $state(0);
|
|
56
57
|
let menuLeft = $state(0);
|
|
57
58
|
|
|
59
|
+
let subMenuRefs = $state<Record<string, HTMLDivElement>>({});
|
|
58
60
|
let subMenuTop = $state(0);
|
|
59
61
|
let subMenuLeft = $state(0);
|
|
60
62
|
|
|
@@ -69,8 +71,10 @@
|
|
|
69
71
|
const navBase =
|
|
70
72
|
"flex items-stretch pl-2 gap-1 border-b relative z-10 bg-[var(--color-bg-surface)] text-[var(--color-text-default)] border-[var(--border-color-default)]";
|
|
71
73
|
|
|
74
|
+
const subMenuGutter = 8;
|
|
75
|
+
|
|
72
76
|
const topButtonBase =
|
|
73
|
-
"px-4 rounded-xs leading-none transition-colors
|
|
77
|
+
"px-4 rounded-xs leading-none transition-colors outline-none focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]";
|
|
74
78
|
|
|
75
79
|
const topButtonActive =
|
|
76
80
|
"bg-[var(--color-bg-muted)] text-[var(--color-text-default)]";
|
|
@@ -78,10 +82,10 @@
|
|
|
78
82
|
"hover:bg-[var(--color-bg-muted)] text-[var(--color-text-default)]";
|
|
79
83
|
|
|
80
84
|
const menuStyle = $derived(
|
|
81
|
-
`position:fixed; top:${menuTop}px; left:${menuLeft}px; width:max-content;`
|
|
85
|
+
`position:fixed; top:${menuTop}px; left:${menuLeft}px; width:max-content; max-width:calc(100vw - 16px);`
|
|
82
86
|
);
|
|
83
87
|
const subMenuStyle = $derived(
|
|
84
|
-
`position:fixed; top:${subMenuTop}px; left:${subMenuLeft}px; width:max-content;`
|
|
88
|
+
`position:fixed; top:${subMenuTop}px; left:${subMenuLeft}px; width:max-content; max-width:calc(100vw - 16px);`
|
|
85
89
|
);
|
|
86
90
|
|
|
87
91
|
const textCls = $derived(TEXT[sz]);
|
|
@@ -136,16 +140,51 @@
|
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
// Positioning dropdown
|
|
139
|
-
function updateMenuPosition(
|
|
143
|
+
function updateMenuPosition(
|
|
144
|
+
triggerEl: HTMLElement,
|
|
145
|
+
menuEl?: HTMLElement | null
|
|
146
|
+
) {
|
|
140
147
|
const rect = triggerEl.getBoundingClientRect();
|
|
148
|
+
const menuWidth = Math.min(
|
|
149
|
+
menuEl?.getBoundingClientRect().width ?? rect.width,
|
|
150
|
+
window.innerWidth - 16
|
|
151
|
+
);
|
|
152
|
+
const spaceRight = window.innerWidth - rect.left;
|
|
153
|
+
const spaceLeft = rect.right;
|
|
154
|
+
const alignRight = spaceRight < menuWidth && spaceLeft > spaceRight;
|
|
155
|
+
const viewportLeft = window.scrollX;
|
|
156
|
+
const viewportRight = window.scrollX + window.innerWidth;
|
|
157
|
+
|
|
141
158
|
menuTop = rect.bottom + window.scrollY;
|
|
142
|
-
|
|
159
|
+
const targetLeft = alignRight
|
|
160
|
+
? rect.right + window.scrollX - menuWidth
|
|
161
|
+
: rect.left + window.scrollX;
|
|
162
|
+
const maxLeft = viewportRight - menuWidth;
|
|
163
|
+
menuLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
function updateSubMenuPosition(
|
|
166
|
+
function updateSubMenuPosition(
|
|
167
|
+
parentItemEl: HTMLElement,
|
|
168
|
+
subMenuEl?: HTMLElement | null
|
|
169
|
+
) {
|
|
146
170
|
const rect = parentItemEl.getBoundingClientRect();
|
|
171
|
+
const subRect = subMenuEl?.getBoundingClientRect();
|
|
172
|
+
const subWidth = Math.min(
|
|
173
|
+
subRect?.width ?? rect.width,
|
|
174
|
+
window.innerWidth - 16
|
|
175
|
+
);
|
|
176
|
+
const spaceRight = window.innerWidth - rect.right;
|
|
177
|
+
const spaceLeft = rect.left;
|
|
178
|
+
const shouldFlipLeft = spaceRight < subWidth && spaceLeft > spaceRight;
|
|
179
|
+
|
|
147
180
|
subMenuTop = rect.top + window.scrollY;
|
|
148
|
-
|
|
181
|
+
const viewportLeft = window.scrollX;
|
|
182
|
+
const viewportRight = window.scrollX + window.innerWidth;
|
|
183
|
+
const targetLeft = shouldFlipLeft
|
|
184
|
+
? rect.left + window.scrollX - subWidth - subMenuGutter
|
|
185
|
+
: rect.right + window.scrollX + subMenuGutter;
|
|
186
|
+
const maxLeft = viewportRight - subWidth - subMenuGutter;
|
|
187
|
+
subMenuLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
|
|
149
188
|
}
|
|
150
189
|
|
|
151
190
|
function firstActionIndex(actions: MenuAction[]) {
|
|
@@ -209,7 +248,7 @@
|
|
|
209
248
|
activeIndex = firstIndex;
|
|
210
249
|
const triggerEl = triggerRefs[menuItem.name];
|
|
211
250
|
if (triggerEl) {
|
|
212
|
-
updateMenuPosition(triggerEl);
|
|
251
|
+
updateMenuPosition(triggerEl, menuRefs[menuItem.name]);
|
|
213
252
|
}
|
|
214
253
|
if (focusFirst && firstIndex !== -1) {
|
|
215
254
|
focusMenuAction(menuItem, firstIndex);
|
|
@@ -221,7 +260,7 @@
|
|
|
221
260
|
openSub = actionId(parentAction);
|
|
222
261
|
const parentEl = itemRefs[actionId(parentAction)];
|
|
223
262
|
if (parentEl) {
|
|
224
|
-
updateSubMenuPosition(parentEl);
|
|
263
|
+
updateSubMenuPosition(parentEl, subMenuRefs[actionId(parentAction)]);
|
|
225
264
|
}
|
|
226
265
|
const firstIndex = focusFirst ? firstActionIndex(parentAction.submenu) : -1;
|
|
227
266
|
activeSubIndex = firstIndex;
|
|
@@ -352,10 +391,10 @@
|
|
|
352
391
|
if (open) {
|
|
353
392
|
const triggerEl = triggerRefs[open];
|
|
354
393
|
if (triggerEl) {
|
|
355
|
-
updateMenuPosition(triggerEl);
|
|
394
|
+
updateMenuPosition(triggerEl, menuRefs[open]);
|
|
356
395
|
|
|
357
396
|
const handleScrollResize = () => {
|
|
358
|
-
updateMenuPosition(triggerEl);
|
|
397
|
+
updateMenuPosition(triggerEl, menuRefs[open]);
|
|
359
398
|
};
|
|
360
399
|
|
|
361
400
|
window.addEventListener("scroll", handleScrollResize, true);
|
|
@@ -372,11 +411,12 @@
|
|
|
372
411
|
$effect(() => {
|
|
373
412
|
if (openSub) {
|
|
374
413
|
const itemEl = itemRefs[openSub];
|
|
414
|
+
const subEl = subMenuRefs[openSub];
|
|
375
415
|
if (itemEl) {
|
|
376
|
-
updateSubMenuPosition(itemEl);
|
|
416
|
+
updateSubMenuPosition(itemEl, subEl);
|
|
377
417
|
|
|
378
418
|
const handleScrollResize = () => {
|
|
379
|
-
updateSubMenuPosition(itemEl);
|
|
419
|
+
updateSubMenuPosition(itemEl, subMenuRefs[openSub]);
|
|
380
420
|
};
|
|
381
421
|
|
|
382
422
|
window.addEventListener("scroll", handleScrollResize, true);
|
|
@@ -438,6 +478,7 @@
|
|
|
438
478
|
|
|
439
479
|
<!-- Main Menu -->
|
|
440
480
|
<div
|
|
481
|
+
bind:this={menuRefs[menuItem.name]}
|
|
441
482
|
class={cx(
|
|
442
483
|
"fixed z-50 min-w-44 p-2 rounded-xs shadow-[0_2px_4px_var(--shadow-color)] ",
|
|
443
484
|
"border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]"
|
|
@@ -462,6 +503,7 @@
|
|
|
462
503
|
class={cx(
|
|
463
504
|
"relative text-left rounded-xs transition-colors outline-none px-1.5 py-0.5 my-1 mr-1 min-w-full flex items-center",
|
|
464
505
|
"gap-3 hover:bg-[var(--color-bg-muted)] focus-visible:bg-[var(--color-bg-muted)]",
|
|
506
|
+
"focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]",
|
|
465
507
|
textCls
|
|
466
508
|
)}
|
|
467
509
|
onmousedown={(e) => e.preventDefault()}
|
|
@@ -518,9 +560,10 @@
|
|
|
518
560
|
<!-- Sub Menu -->
|
|
519
561
|
{#if hasSubmenu(action) && openSub === actionId(action)}
|
|
520
562
|
<div
|
|
563
|
+
bind:this={subMenuRefs[actionId(action)]}
|
|
521
564
|
class={cx(
|
|
522
565
|
"fixed z-50 min-w-44 p-2 rounded-xs shadow-[0_2px_4px_var(--shadow-color)]",
|
|
523
|
-
"border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]
|
|
566
|
+
"border border-[var(--border-color-default)] bg-[var(--color-bg-surface)]"
|
|
524
567
|
)}
|
|
525
568
|
style={subMenuStyle}
|
|
526
569
|
role="menu"
|
|
@@ -543,8 +586,7 @@
|
|
|
543
586
|
"relative text-left rounded-xs transition-colors outline-none px-1.5 py-0.5",
|
|
544
587
|
"my-1 mr-1 w-full flex items-center justify-between gap-3",
|
|
545
588
|
"hover:bg-[var(--color-bg-muted)] focus-visible:bg-[var(--color-bg-muted)]",
|
|
546
|
-
"focus-visible:
|
|
547
|
-
"focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-surface)]",
|
|
589
|
+
"focus-visible:shadow-[inset_0_0_0_2px_var(--border-color-focus)]",
|
|
548
590
|
"decoration-[var(--color-text-default)]",
|
|
549
591
|
textCls
|
|
550
592
|
)}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @prop class {string} - Custom classes applied to the pagination wrapper
|
|
14
14
|
* @default ""
|
|
15
15
|
*
|
|
16
|
-
* @note Displays
|
|
16
|
+
* @note Displays “Page X of Y” and numbered page buttons.
|
|
17
17
|
* @note Prev/next buttons are disabled at the edges.
|
|
18
18
|
* @note Shows up to 3 numbered buttons centered around the current page.
|
|
19
19
|
* @note Uses `aria-current=\"page\"` on the active page for accessibility.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!-- src/lib/PrimaryColorSelect.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* @component PrimaryColorSelect
|
|
5
|
+
* @description Theme primary-color selector built on top of Select. Provides a fixed palette,
|
|
6
|
+
* handles persistence, and updates the global <html> attribute.
|
|
7
|
+
*
|
|
8
|
+
* @prop sz {SizeKey} - Sizing preset passed directly to Select
|
|
9
|
+
* @options xs|sm|md|lg|xl
|
|
10
|
+
* @default sm
|
|
11
|
+
*
|
|
12
|
+
* @prop label {string} - Custom label text. Falls back to localized copy when omitted.
|
|
13
|
+
*
|
|
14
|
+
* @prop class {string} - Extra classes forwarded to the underlying Select component
|
|
15
|
+
* @default ""
|
|
16
|
+
*
|
|
17
|
+
* @note The palette is predefined internally (`{ value, label, swatch }`).
|
|
18
|
+
* @note Selected value is stored in localStorage under "primary".
|
|
19
|
+
* @note The `html` element receives `data-primary="{value}"` for theme styling.
|
|
20
|
+
* @note Uses the same onChange contract as Select and forwards palette options as-is.
|
|
21
|
+
*/
|
|
22
|
+
import { getContext } from "svelte";
|
|
23
|
+
import Select from "./Select.svelte";
|
|
24
|
+
import type { PrimaryKey, PaletteOption, SizeKey } from "./types";
|
|
25
|
+
import { TEXTS } from "./lang";
|
|
26
|
+
|
|
27
|
+
type Props = {
|
|
28
|
+
sz?: SizeKey;
|
|
29
|
+
label?: string;
|
|
30
|
+
class?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let { sz = "sm", label, class: externalClass = "" }: Props = $props();
|
|
34
|
+
|
|
35
|
+
const langCtx =
|
|
36
|
+
getContext<{ value: keyof typeof TEXTS } | undefined>("lang") ?? null;
|
|
37
|
+
const langKey = $derived(langCtx?.value ?? "en");
|
|
38
|
+
const L = $derived(TEXTS[langKey].components.primaryColorSelect);
|
|
39
|
+
|
|
40
|
+
const labelFinal = $derived(label ?? L.text);
|
|
41
|
+
|
|
42
|
+
const palette: PaletteOption[] = [
|
|
43
|
+
{
|
|
44
|
+
value: "default",
|
|
45
|
+
label: "Indigo",
|
|
46
|
+
swatch: "oklch(62.3% 0.214 259.8deg)",
|
|
47
|
+
},
|
|
48
|
+
{ value: "cyan", label: "Cyan", swatch: "oklch(71.5% 0.143 215.221)" },
|
|
49
|
+
{ value: "red", label: "Red", swatch: "oklch(58% 0.24 30deg)" },
|
|
50
|
+
{ value: "green", label: "Green", swatch: "oklch(65% 0.22 140deg)" },
|
|
51
|
+
{ value: "yellow", label: "Yellow", swatch: "oklch(75% 0.18 90deg)" },
|
|
52
|
+
{ value: "pink", label: "Pink", swatch: "oklch(70% 0.25 350deg)" },
|
|
53
|
+
{ value: "orange", label: "Orange", swatch: "oklch(72% 0.22 60deg)" },
|
|
54
|
+
{ value: "purple", label: "Purple", swatch: "oklch(55% 0.22 290deg)" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const paletteMap = palette.reduce<Record<PrimaryKey, PaletteOption>>(
|
|
58
|
+
(acc, option) => {
|
|
59
|
+
acc[option.value] = option;
|
|
60
|
+
return acc;
|
|
61
|
+
},
|
|
62
|
+
{} as Record<PrimaryKey, PaletteOption>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
let selected = $state<PrimaryKey>("default");
|
|
66
|
+
let mounted = $state(false);
|
|
67
|
+
|
|
68
|
+
function isPrimaryKey(value: unknown): value is PrimaryKey {
|
|
69
|
+
return typeof value === "string" && value in paletteMap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (!mounted) {
|
|
74
|
+
const saved = localStorage.getItem("primary");
|
|
75
|
+
if (isPrimaryKey(saved)) {
|
|
76
|
+
selected = saved;
|
|
77
|
+
}
|
|
78
|
+
mounted = true;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (mounted) {
|
|
84
|
+
document.documentElement.setAttribute("data-primary", selected);
|
|
85
|
+
localStorage.setItem("primary", selected);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<Select
|
|
91
|
+
{sz}
|
|
92
|
+
label={labelFinal}
|
|
93
|
+
options={palette}
|
|
94
|
+
value={selected}
|
|
95
|
+
onChange={(v) => {
|
|
96
|
+
if (isPrimaryKey(v)) selected = v;
|
|
97
|
+
}}
|
|
98
|
+
class={externalClass}
|
|
99
|
+
/>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SizeKey } from "./types";
|
|
2
|
+
type Props = {
|
|
3
|
+
sz?: SizeKey;
|
|
4
|
+
label?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const PrimaryColorSelect: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type PrimaryColorSelect = ReturnType<typeof PrimaryColorSelect>;
|
|
9
|
+
export default PrimaryColorSelect;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<!-- src/lib/ProgressCircle.svelte -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
/**
|
|
4
|
+
* @component ProgressCircle
|
|
5
|
+
* @description Circular progress indicator for visualizing completion or load state (0-100). Supports indeterminate mode.
|
|
6
|
+
* @prop value {number} - Current progress value
|
|
7
|
+
* @default 0
|
|
8
|
+
* @prop indeterminate {boolean} - Enables spinning infinite mode
|
|
9
|
+
* @default false
|
|
10
|
+
* @prop size {number} - Diameter in px
|
|
11
|
+
* @default 48
|
|
12
|
+
* @prop stroke {number} - Stroke width in px
|
|
13
|
+
* @default 4
|
|
14
|
+
* @prop variant {ComponentVariant} - Color/style variant
|
|
15
|
+
* @options default|neutral|success|warning|error
|
|
16
|
+
* @default default
|
|
17
|
+
* @prop label {string} - Optional text shown in center
|
|
18
|
+
* @default ""
|
|
19
|
+
* @prop max {number} - Max progress value for normalization
|
|
20
|
+
* @default 100
|
|
21
|
+
* @prop class {string} - Extra wrapper classes
|
|
22
|
+
* @default ""
|
|
23
|
+
* @note Clamps value between 0-max
|
|
24
|
+
* @note Uses SVG stroke-dashoffset animation
|
|
25
|
+
* @note Accessible role=progressbar with aria-valuenow
|
|
26
|
+
* @note Works in both determinate/indeterminate modes
|
|
27
|
+
*/
|
|
28
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
29
|
+
import { type SizeKey, type ComponentVariant, TEXT } from "./types";
|
|
30
|
+
import { cx, clamp } from "../utils";
|
|
31
|
+
|
|
32
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
33
|
+
value?: number;
|
|
34
|
+
indeterminate?: boolean;
|
|
35
|
+
sz?: SizeKey;
|
|
36
|
+
variant?: ComponentVariant;
|
|
37
|
+
class?: string;
|
|
38
|
+
label?: string;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
value = 0,
|
|
44
|
+
indeterminate = false,
|
|
45
|
+
sz = "md",
|
|
46
|
+
variant = "default",
|
|
47
|
+
class: externalClass = "",
|
|
48
|
+
label = "",
|
|
49
|
+
disabled = false,
|
|
50
|
+
...rest
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
const sizes: Record<SizeKey, { diameter: number; stroke: number }> = {
|
|
54
|
+
xs: { diameter: 40, stroke: 4 },
|
|
55
|
+
sm: { diameter: 48, stroke: 5 },
|
|
56
|
+
md: { diameter: 56, stroke: 6 },
|
|
57
|
+
lg: { diameter: 64, stroke: 7 },
|
|
58
|
+
xl: { diameter: 72, stroke: 8 },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const pctValue = $derived(clamp(value, 0, 100));
|
|
62
|
+
const pctText = $derived(Math.round(pctValue));
|
|
63
|
+
|
|
64
|
+
const geometry = $derived(sizes[sz]);
|
|
65
|
+
const center = $derived(geometry.diameter / 2);
|
|
66
|
+
const radius = $derived(center - geometry.stroke / 2);
|
|
67
|
+
const circumference = $derived(2 * Math.PI * radius);
|
|
68
|
+
|
|
69
|
+
const dashOffset = $derived(((100 - pctValue) / 100) * circumference);
|
|
70
|
+
const dashArray = $derived(`${circumference} ${circumference}`);
|
|
71
|
+
const indeterminateDash = $derived(`${circumference * 0.3} ${circumference}`);
|
|
72
|
+
|
|
73
|
+
const strokeColor = $derived(
|
|
74
|
+
variant === "neutral"
|
|
75
|
+
? "stroke-[var(--color-bg-secondary)]"
|
|
76
|
+
: "stroke-[var(--color-bg-primary)]"
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const rootClass = $derived(
|
|
80
|
+
cx(
|
|
81
|
+
"inline-flex flex-col items-center gap-1 data-[disabled=true]:opacity-[var(--opacity-disabled)] data-[disabled=true]:cursor-not-allowed",
|
|
82
|
+
externalClass
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<div
|
|
88
|
+
class={rootClass}
|
|
89
|
+
role="progressbar"
|
|
90
|
+
aria-valuemin="0"
|
|
91
|
+
aria-valuemax="100"
|
|
92
|
+
aria-valuenow={indeterminate ? undefined : pctText}
|
|
93
|
+
data-disabled={disabled ? "true" : undefined}
|
|
94
|
+
{...rest}
|
|
95
|
+
>
|
|
96
|
+
{#if label}
|
|
97
|
+
<span class="text-[var(--color-text-muted)] select-none {TEXT[sz]}">
|
|
98
|
+
{label}
|
|
99
|
+
</span>
|
|
100
|
+
{/if}
|
|
101
|
+
|
|
102
|
+
<div
|
|
103
|
+
class="relative inline-flex items-center justify-center"
|
|
104
|
+
style={`width:${geometry.diameter}px;height:${geometry.diameter}px;`}
|
|
105
|
+
>
|
|
106
|
+
<svg
|
|
107
|
+
class="pc-svg"
|
|
108
|
+
viewBox={`0 0 ${geometry.diameter} ${geometry.diameter}`}
|
|
109
|
+
role="presentation"
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
>
|
|
112
|
+
<g class="pc-rot">
|
|
113
|
+
<circle
|
|
114
|
+
class="pc-track"
|
|
115
|
+
cx={center}
|
|
116
|
+
cy={center}
|
|
117
|
+
r={radius}
|
|
118
|
+
stroke-width={geometry.stroke}
|
|
119
|
+
></circle>
|
|
120
|
+
|
|
121
|
+
{#if indeterminate}
|
|
122
|
+
<circle
|
|
123
|
+
class={cx("pc-bar pc-indet", strokeColor)}
|
|
124
|
+
cx={center}
|
|
125
|
+
cy={center}
|
|
126
|
+
r={radius}
|
|
127
|
+
stroke-width={geometry.stroke}
|
|
128
|
+
stroke-dasharray={indeterminateDash}
|
|
129
|
+
></circle>
|
|
130
|
+
{:else}
|
|
131
|
+
<circle
|
|
132
|
+
class={cx("pc-bar", strokeColor)}
|
|
133
|
+
cx={center}
|
|
134
|
+
cy={center}
|
|
135
|
+
r={radius}
|
|
136
|
+
stroke-width={geometry.stroke}
|
|
137
|
+
stroke-dasharray={dashArray}
|
|
138
|
+
stroke-dashoffset={dashOffset}
|
|
139
|
+
></circle>
|
|
140
|
+
{/if}
|
|
141
|
+
</g>
|
|
142
|
+
</svg>
|
|
143
|
+
|
|
144
|
+
{#if !indeterminate}
|
|
145
|
+
<div
|
|
146
|
+
class="absolute inset-0 flex items-center justify-center text-[var(--color-text-muted)] font-medium select-none {TEXT[
|
|
147
|
+
sz
|
|
148
|
+
]}"
|
|
149
|
+
>
|
|
150
|
+
{pctText}%
|
|
151
|
+
</div>
|
|
152
|
+
{/if}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<style>
|
|
157
|
+
.pc-svg {
|
|
158
|
+
width: 100%;
|
|
159
|
+
height: 100%;
|
|
160
|
+
}
|
|
161
|
+
.pc-rot {
|
|
162
|
+
transform: rotate(-90deg);
|
|
163
|
+
transform-origin: center;
|
|
164
|
+
}
|
|
165
|
+
.pc-track {
|
|
166
|
+
fill: none;
|
|
167
|
+
stroke: var(--border-color-default);
|
|
168
|
+
}
|
|
169
|
+
.pc-bar {
|
|
170
|
+
fill: none;
|
|
171
|
+
stroke-linecap: round;
|
|
172
|
+
transition:
|
|
173
|
+
stroke-dashoffset 0.25s ease,
|
|
174
|
+
stroke 0.2s ease;
|
|
175
|
+
transform-origin: center;
|
|
176
|
+
}
|
|
177
|
+
.pc-indet {
|
|
178
|
+
animation: pc-spin 1.2s linear infinite;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
:global {
|
|
182
|
+
@keyframes pc-spin {
|
|
183
|
+
0% {
|
|
184
|
+
transform: rotate(0deg);
|
|
185
|
+
}
|
|
186
|
+
100% {
|
|
187
|
+
transform: rotate(360deg);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @component ProgressCircle
|
|
3
|
+
* @description Circular progress indicator for visualizing completion or load state (0-100). Supports indeterminate mode.
|
|
4
|
+
* @prop value {number} - Current progress value
|
|
5
|
+
* @default 0
|
|
6
|
+
* @prop indeterminate {boolean} - Enables spinning infinite mode
|
|
7
|
+
* @default false
|
|
8
|
+
* @prop size {number} - Diameter in px
|
|
9
|
+
* @default 48
|
|
10
|
+
* @prop stroke {number} - Stroke width in px
|
|
11
|
+
* @default 4
|
|
12
|
+
* @prop variant {ComponentVariant} - Color/style variant
|
|
13
|
+
* @options default|neutral|success|warning|error
|
|
14
|
+
* @default default
|
|
15
|
+
* @prop label {string} - Optional text shown in center
|
|
16
|
+
* @default ""
|
|
17
|
+
* @prop max {number} - Max progress value for normalization
|
|
18
|
+
* @default 100
|
|
19
|
+
* @prop class {string} - Extra wrapper classes
|
|
20
|
+
* @default ""
|
|
21
|
+
* @note Clamps value between 0-max
|
|
22
|
+
* @note Uses SVG stroke-dashoffset animation
|
|
23
|
+
* @note Accessible role=progressbar with aria-valuenow
|
|
24
|
+
* @note Works in both determinate/indeterminate modes
|
|
25
|
+
*/
|
|
26
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
27
|
+
import { type SizeKey, type ComponentVariant } from "./types";
|
|
28
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
29
|
+
value?: number;
|
|
30
|
+
indeterminate?: boolean;
|
|
31
|
+
sz?: SizeKey;
|
|
32
|
+
variant?: ComponentVariant;
|
|
33
|
+
class?: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
};
|
|
37
|
+
declare const ProgressCircle: import("svelte").Component<Props, {}, "">;
|
|
38
|
+
type ProgressCircle = ReturnType<typeof ProgressCircle>;
|
|
39
|
+
export default ProgressCircle;
|