@runwisp/ui 0.4.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 +215 -0
- package/README.md +20 -0
- package/package.json +128 -0
- package/src/app.d.ts +8 -0
- package/src/app.html +12 -0
- package/src/astro/CTABand.astro +32 -0
- package/src/astro/Hero.astro +58 -0
- package/src/astro/SiteFooter.astro +47 -0
- package/src/astro/SiteHeader.astro +46 -0
- package/src/lib/actions/portal.ts +16 -0
- package/src/lib/assets/runwisp-logo.svg +16 -0
- package/src/lib/components/Accordion.svelte +20 -0
- package/src/lib/components/AccordionItem.svelte +74 -0
- package/src/lib/components/Alert.svelte +107 -0
- package/src/lib/components/AlertDialog.svelte +63 -0
- package/src/lib/components/Avatar.svelte +88 -0
- package/src/lib/components/Badge.svelte +131 -0
- package/src/lib/components/Breadcrumb.svelte +58 -0
- package/src/lib/components/Button.svelte +69 -0
- package/src/lib/components/Card.svelte +70 -0
- package/src/lib/components/Checkbox.svelte +90 -0
- package/src/lib/components/CodeBlock.svelte +66 -0
- package/src/lib/components/CronInput.svelte +130 -0
- package/src/lib/components/DataGrid.svelte +368 -0
- package/src/lib/components/Drawer.svelte +156 -0
- package/src/lib/components/Dropdown.svelte +195 -0
- package/src/lib/components/DurationInput.svelte +170 -0
- package/src/lib/components/EmptyState.svelte +46 -0
- package/src/lib/components/ErrorState.svelte +73 -0
- package/src/lib/components/FeatureCard.svelte +35 -0
- package/src/lib/components/FeatureGrid.svelte +37 -0
- package/src/lib/components/FormField.svelte +53 -0
- package/src/lib/components/Heading.svelte +46 -0
- package/src/lib/components/Input.svelte +114 -0
- package/src/lib/components/Kbd.svelte +31 -0
- package/src/lib/components/Link.svelte +56 -0
- package/src/lib/components/LinkButton.svelte +55 -0
- package/src/lib/components/LogConsole.svelte +424 -0
- package/src/lib/components/Logo.svelte +34 -0
- package/src/lib/components/Modal.svelte +138 -0
- package/src/lib/components/PageContainer.svelte +38 -0
- package/src/lib/components/Pagination.svelte +96 -0
- package/src/lib/components/Popover.svelte +130 -0
- package/src/lib/components/Progress.svelte +68 -0
- package/src/lib/components/Prose.svelte +43 -0
- package/src/lib/components/Radio.svelte +82 -0
- package/src/lib/components/RadioGroup.svelte +53 -0
- package/src/lib/components/Select.svelte +454 -0
- package/src/lib/components/Separator.svelte +30 -0
- package/src/lib/components/Skeleton.svelte +75 -0
- package/src/lib/components/Sparkline.svelte +58 -0
- package/src/lib/components/Spinner.svelte +44 -0
- package/src/lib/components/StatusIndicator.svelte +203 -0
- package/src/lib/components/Table.svelte +19 -0
- package/src/lib/components/TableBody.svelte +17 -0
- package/src/lib/components/TableCell.svelte +34 -0
- package/src/lib/components/TableHead.svelte +19 -0
- package/src/lib/components/TableRow.svelte +22 -0
- package/src/lib/components/Tabs.svelte +148 -0
- package/src/lib/components/Text.svelte +57 -0
- package/src/lib/components/Textarea.svelte +81 -0
- package/src/lib/components/ThemeToggle.svelte +47 -0
- package/src/lib/components/TimezoneSelect.svelte +127 -0
- package/src/lib/components/ToastContainer.svelte +84 -0
- package/src/lib/components/Toggle.svelte +84 -0
- package/src/lib/components/Tooltip.svelte +56 -0
- package/src/lib/components/button-styles.ts +51 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +252 -0
- package/src/lib/components/dashboard/RunsList.svelte +331 -0
- package/src/lib/components/dashboard/run-helpers.ts +12 -0
- package/src/lib/components/dashboard/status-config.ts +141 -0
- package/src/lib/components/dashboard/types.ts +35 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/log-console/LogCache.svelte.ts +93 -0
- package/src/lib/log-console/LogFetcher.svelte.ts +179 -0
- package/src/lib/log-console/types.ts +24 -0
- package/src/lib/theme-tokens.css +235 -0
- package/src/lib/theme.css +276 -0
- package/src/lib/utils/error.ts +18 -0
- package/src/lib/utils/format.ts +76 -0
- package/src/lib/utils/id.ts +6 -0
- package/src/lib/utils/toast.svelte.ts +87 -0
- package/src/lib/utils/toast.ts +60 -0
- package/src/routes/+layout.svelte +10 -0
- package/src/routes/+page.svelte +5 -0
- package/src/routes/layout.css +3 -0
- package/src/stories/Alert.stories.svelte +64 -0
- package/src/stories/Avatar.stories.svelte +76 -0
- package/src/stories/Badge.stories.svelte +76 -0
- package/src/stories/Breadcrumb.stories.svelte +64 -0
- package/src/stories/Button.stories.svelte +101 -0
- package/src/stories/Card.stories.svelte +106 -0
- package/src/stories/Checkbox.stories.svelte +77 -0
- package/src/stories/DataGrid.stories.svelte +130 -0
- package/src/stories/Dropdown.stories.svelte +81 -0
- package/src/stories/EmptyState.stories.svelte +74 -0
- package/src/stories/Input.stories.svelte +88 -0
- package/src/stories/Logo.stories.svelte +82 -0
- package/src/stories/Modal.stories.svelte +101 -0
- package/src/stories/Progress.stories.svelte +65 -0
- package/src/stories/Select.stories.svelte +158 -0
- package/src/stories/Tabs.stories.svelte +103 -0
- package/src/stories/Textarea.stories.svelte +45 -0
- package/src/stories/Toggle.stories.svelte +74 -0
- package/src/stories/Tooltip.stories.svelte +77 -0
- package/src/stories/Welcome.mdx +241 -0
- package/src/stories/assets/runwisp_c.svg +16 -0
- package/src/svelte-shims.d.ts +18 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import Button from "./Button.svelte";
|
|
6
|
+
import { ChevronDown } from "@lucide/svelte";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
page: number;
|
|
10
|
+
pageSize: number;
|
|
11
|
+
totalItems: number;
|
|
12
|
+
pageSizeOptions?: number[] | undefined;
|
|
13
|
+
onPageChange?: ((page: number) => void) | undefined;
|
|
14
|
+
onPageSizeChange?: ((pageSize: number) => void) | undefined;
|
|
15
|
+
class?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
page,
|
|
20
|
+
pageSize,
|
|
21
|
+
totalItems,
|
|
22
|
+
pageSizeOptions = [10, 20, 50, 100],
|
|
23
|
+
onPageChange,
|
|
24
|
+
onPageSizeChange,
|
|
25
|
+
class: className = "",
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const totalPages = $derived(Math.max(1, Math.ceil(totalItems / Math.max(1, pageSize))));
|
|
29
|
+
const safePage = $derived(Math.min(Math.max(1, page), totalPages));
|
|
30
|
+
|
|
31
|
+
const start = $derived(totalItems === 0 ? 0 : (safePage - 1) * pageSize + 1);
|
|
32
|
+
const end = $derived(totalItems === 0 ? 0 : Math.min(totalItems, safePage * pageSize));
|
|
33
|
+
|
|
34
|
+
function goTo(nextPage: number) {
|
|
35
|
+
const clamped = Math.min(Math.max(1, nextPage), totalPages);
|
|
36
|
+
onPageChange?.(clamped);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handlePageSizeChange(e: Event) {
|
|
40
|
+
const newSize = Number((e.target as HTMLSelectElement).value);
|
|
41
|
+
onPageSizeChange?.(newSize);
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between {className}">
|
|
46
|
+
<div class="flex items-center gap-4 text-sm text-on-surface-muted">
|
|
47
|
+
{#if totalItems === 0}
|
|
48
|
+
<span>No results</span>
|
|
49
|
+
{:else}
|
|
50
|
+
<span>Showing {start}–{end} of {totalItems}</span>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
<div class="flex items-center gap-2">
|
|
54
|
+
<span class="text-on-surface-muted">Rows per page</span>
|
|
55
|
+
<div class="relative">
|
|
56
|
+
<select
|
|
57
|
+
value={pageSize}
|
|
58
|
+
onchange={handlePageSizeChange}
|
|
59
|
+
class="
|
|
60
|
+
appearance-none rounded-lg border border-outline bg-surface-raised py-1 pr-7 pl-2 text-sm
|
|
61
|
+
font-medium text-on-surface-muted shadow-sm focus:border-ring focus:ring-1 focus:ring-ring focus:outline-none
|
|
62
|
+
"
|
|
63
|
+
>
|
|
64
|
+
{#each pageSizeOptions as size (size)}
|
|
65
|
+
<option value={size}>{size}</option>
|
|
66
|
+
{/each}
|
|
67
|
+
</select>
|
|
68
|
+
<div
|
|
69
|
+
class="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 text-on-surface-faint"
|
|
70
|
+
>
|
|
71
|
+
<ChevronDown size={14} />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="flex items-center gap-2">
|
|
78
|
+
<Button
|
|
79
|
+
variant="secondary"
|
|
80
|
+
size="xs"
|
|
81
|
+
disabled={safePage <= 1}
|
|
82
|
+
onclick={() => goTo(safePage - 1)}>Prev</Button
|
|
83
|
+
>
|
|
84
|
+
<div class="min-w-[4rem] text-center text-xs text-on-surface-muted">
|
|
85
|
+
Page {safePage} of {totalPages}
|
|
86
|
+
</div>
|
|
87
|
+
<Button
|
|
88
|
+
variant="secondary"
|
|
89
|
+
size="xs"
|
|
90
|
+
disabled={safePage >= totalPages}
|
|
91
|
+
onclick={() => goTo(safePage + 1)}
|
|
92
|
+
>
|
|
93
|
+
Next
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import type { Placement } from "@floating-ui/dom";
|
|
7
|
+
import { tick, onDestroy } from "svelte";
|
|
8
|
+
import { computePosition, autoUpdate, flip, shift, offset } from "@floating-ui/dom";
|
|
9
|
+
import { scale } from "svelte/transition";
|
|
10
|
+
import { quintOut } from "svelte/easing";
|
|
11
|
+
import { portal } from "../actions/portal.js";
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
placement?: Placement;
|
|
16
|
+
trigger?: Snippet;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
open = $bindable(false),
|
|
23
|
+
placement = "bottom",
|
|
24
|
+
trigger,
|
|
25
|
+
children,
|
|
26
|
+
class: className = "",
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let triggerEl = $state<HTMLElement | null>(null);
|
|
30
|
+
let contentEl = $state<HTMLElement | null>(null);
|
|
31
|
+
let cleanupFloating = $state<(() => void) | null>(null);
|
|
32
|
+
|
|
33
|
+
async function setupFloating() {
|
|
34
|
+
if (!triggerEl || !contentEl) return;
|
|
35
|
+
|
|
36
|
+
cleanupFloating = autoUpdate(triggerEl, contentEl, () => {
|
|
37
|
+
if (!triggerEl || !contentEl) return;
|
|
38
|
+
|
|
39
|
+
void computePosition(triggerEl, contentEl, {
|
|
40
|
+
placement,
|
|
41
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
42
|
+
}).then(({ x, y }) => {
|
|
43
|
+
if (contentEl) {
|
|
44
|
+
Object.assign(contentEl.style, {
|
|
45
|
+
left: `${x}px`,
|
|
46
|
+
top: `${y}px`,
|
|
47
|
+
position: "absolute",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toggle() {
|
|
55
|
+
open = !open;
|
|
56
|
+
if (open) {
|
|
57
|
+
void tick().then(() => {
|
|
58
|
+
void setupFloating();
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
cleanupFloating?.();
|
|
62
|
+
cleanupFloating = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleOutsideClick(e: MouseEvent) {
|
|
67
|
+
const target = e.target as HTMLElement;
|
|
68
|
+
if (triggerEl?.contains(target)) return;
|
|
69
|
+
if (contentEl?.contains(target)) return;
|
|
70
|
+
if (open) {
|
|
71
|
+
open = false;
|
|
72
|
+
cleanupFloating?.();
|
|
73
|
+
cleanupFloating = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
78
|
+
if (e.key === "Escape" && open) {
|
|
79
|
+
open = false;
|
|
80
|
+
cleanupFloating?.();
|
|
81
|
+
cleanupFloating = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onDestroy(() => {
|
|
86
|
+
cleanupFloating?.();
|
|
87
|
+
});
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<svelte:window onclick={handleOutsideClick} onkeydown={handleKeyDown} />
|
|
91
|
+
|
|
92
|
+
<div class="inline-block {className}">
|
|
93
|
+
<div
|
|
94
|
+
bind:this={triggerEl}
|
|
95
|
+
role="button"
|
|
96
|
+
tabindex="0"
|
|
97
|
+
onclick={(e) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
toggle();
|
|
100
|
+
}}
|
|
101
|
+
onkeydown={(e) => {
|
|
102
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
toggle();
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{#if trigger}
|
|
110
|
+
{@render trigger()}
|
|
111
|
+
{/if}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{#if open}
|
|
115
|
+
<div
|
|
116
|
+
use:portal
|
|
117
|
+
bind:this={contentEl}
|
|
118
|
+
transition:scale={{ duration: 150, start: 0.95, easing: quintOut }}
|
|
119
|
+
class="
|
|
120
|
+
z-[9999]
|
|
121
|
+
rounded-xl
|
|
122
|
+
border border-outline bg-surface-overlay p-4 shadow-xl shadow-on-surface/5
|
|
123
|
+
"
|
|
124
|
+
>
|
|
125
|
+
{#if children}
|
|
126
|
+
{@render children()}
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
type ProgressVariant = "default" | "success" | "warning" | "danger";
|
|
6
|
+
type ProgressSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
value: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
variant?: ProgressVariant;
|
|
12
|
+
size?: ProgressSize;
|
|
13
|
+
showLabel?: boolean;
|
|
14
|
+
label?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
value,
|
|
20
|
+
max = 100,
|
|
21
|
+
variant = "default",
|
|
22
|
+
size = "md",
|
|
23
|
+
showLabel = false,
|
|
24
|
+
label,
|
|
25
|
+
class: className = "",
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const percentage = $derived(Math.min(Math.max((value / max) * 100, 0), 100));
|
|
29
|
+
|
|
30
|
+
const variantClasses: Record<ProgressVariant, string> = {
|
|
31
|
+
default: "bg-wisp-500",
|
|
32
|
+
success: "bg-success-500",
|
|
33
|
+
warning: "bg-warning-500",
|
|
34
|
+
danger: "bg-danger-500",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const sizeClasses: Record<ProgressSize, string> = {
|
|
38
|
+
sm: "h-1",
|
|
39
|
+
md: "h-2",
|
|
40
|
+
lg: "h-3",
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<div class={className}>
|
|
45
|
+
{#if showLabel || label}
|
|
46
|
+
<div class="mb-1.5 flex items-center justify-between">
|
|
47
|
+
{#if label}
|
|
48
|
+
<span class="text-sm font-medium text-on-surface-muted">{label}</span>
|
|
49
|
+
{/if}
|
|
50
|
+
{#if showLabel}
|
|
51
|
+
<span class="text-sm text-on-surface-muted">{Math.round(percentage)}%</span>
|
|
52
|
+
{/if}
|
|
53
|
+
</div>
|
|
54
|
+
{/if}
|
|
55
|
+
|
|
56
|
+
<div class="w-full {sizeClasses[size]} overflow-hidden rounded-full bg-outline">
|
|
57
|
+
<div
|
|
58
|
+
class="{sizeClasses[size]} {variantClasses[
|
|
59
|
+
variant
|
|
60
|
+
]} rounded-full transition-all duration-300"
|
|
61
|
+
style="width: {percentage}%"
|
|
62
|
+
role="progressbar"
|
|
63
|
+
aria-valuenow={value}
|
|
64
|
+
aria-valuemin={0}
|
|
65
|
+
aria-valuemax={max}
|
|
66
|
+
></div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
|
|
7
|
+
type ProseSize = "sm" | "base" | "lg";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
size?: ProseSize;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { size = "base", children, class: className = "" }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const sizeClasses: Record<ProseSize, string> = {
|
|
18
|
+
sm: "prose-sm",
|
|
19
|
+
base: "prose-base",
|
|
20
|
+
lg: "prose-lg",
|
|
21
|
+
};
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
class="
|
|
26
|
+
prose {sizeClasses[size]}
|
|
27
|
+
max-w-none
|
|
28
|
+
prose-headings:text-on-surface
|
|
29
|
+
prose-p:text-on-surface-muted prose-a:text-primary prose-a:no-underline
|
|
30
|
+
hover:prose-a:underline
|
|
31
|
+
prose-blockquote:border-outline prose-blockquote:text-on-surface-muted prose-strong:text-on-surface prose-code:rounded prose-code:bg-surface-sunken
|
|
32
|
+
prose-code:px-1 prose-code:py-0.5 prose-code:text-on-surface
|
|
33
|
+
prose-pre:border prose-pre:border-outline
|
|
34
|
+
prose-pre:bg-surface-sunken
|
|
35
|
+
prose-li:text-on-surface-muted
|
|
36
|
+
prose-th:text-on-surface
|
|
37
|
+
prose-td:text-on-surface-muted
|
|
38
|
+
prose-hr:border-outline-faint
|
|
39
|
+
{className}
|
|
40
|
+
"
|
|
41
|
+
>
|
|
42
|
+
{@render children()}
|
|
43
|
+
</div>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
type RadioSize = "sm" | "md" | "lg";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
value: string;
|
|
9
|
+
groupValue?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
size?: RadioSize;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
value,
|
|
20
|
+
groupValue = $bindable(""),
|
|
21
|
+
name,
|
|
22
|
+
label,
|
|
23
|
+
description,
|
|
24
|
+
disabled = false,
|
|
25
|
+
size = "md",
|
|
26
|
+
class: className = "",
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const sizeClasses: Record<RadioSize, { radio: string; dot: string; text: string }> = {
|
|
30
|
+
sm: { radio: "h-4 w-4", dot: "h-1.5 w-1.5", text: "text-sm" },
|
|
31
|
+
md: { radio: "h-5 w-5", dot: "h-2 w-2", text: "text-sm" },
|
|
32
|
+
lg: { radio: "h-6 w-6", dot: "h-2.5 w-2.5", text: "text-base" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isChecked = $derived(groupValue === value);
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<label
|
|
39
|
+
class="inline-flex items-start gap-3 {disabled
|
|
40
|
+
? 'cursor-not-allowed opacity-50'
|
|
41
|
+
: 'cursor-pointer'} {className}"
|
|
42
|
+
>
|
|
43
|
+
<div class="relative flex shrink-0 items-center justify-center">
|
|
44
|
+
<input
|
|
45
|
+
type="radio"
|
|
46
|
+
{name}
|
|
47
|
+
{value}
|
|
48
|
+
{disabled}
|
|
49
|
+
checked={isChecked}
|
|
50
|
+
onchange={() => (groupValue = value)}
|
|
51
|
+
class="
|
|
52
|
+
peer {sizeClasses[size].radio} duration-fast
|
|
53
|
+
cursor-pointer appearance-none rounded-full
|
|
54
|
+
border-2 border-outline-hover
|
|
55
|
+
bg-surface-raised transition-colors
|
|
56
|
+
checked:border-primary checked:bg-primary
|
|
57
|
+
hover:border-on-surface-faint
|
|
58
|
+
checked:hover:border-primary-hover checked:hover:bg-primary-hover
|
|
59
|
+
focus:ring-2 focus:ring-ring/20
|
|
60
|
+
focus:ring-offset-0 focus:outline-none
|
|
61
|
+
disabled:cursor-not-allowed disabled:hover:border-outline-hover
|
|
62
|
+
disabled:checked:hover:bg-primary
|
|
63
|
+
"
|
|
64
|
+
/>
|
|
65
|
+
<div
|
|
66
|
+
class="duration-fast pointer-events-none absolute rounded-full bg-on-primary opacity-0 transition-opacity peer-checked:opacity-100 {sizeClasses[
|
|
67
|
+
size
|
|
68
|
+
].dot}"
|
|
69
|
+
></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{#if label || description}
|
|
73
|
+
<div class="flex flex-col gap-0.5">
|
|
74
|
+
{#if label}
|
|
75
|
+
<span class="{sizeClasses[size].text} font-medium text-on-surface">{label}</span>
|
|
76
|
+
{/if}
|
|
77
|
+
{#if description}
|
|
78
|
+
<span class="text-sm text-on-surface-muted">{description}</span>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</label>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
|
|
7
|
+
type Orientation = "horizontal" | "vertical";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
value?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
orientation?: Orientation;
|
|
16
|
+
children: Snippet;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
value = $bindable(""),
|
|
22
|
+
name: _name,
|
|
23
|
+
label,
|
|
24
|
+
error,
|
|
25
|
+
hint,
|
|
26
|
+
orientation = "vertical",
|
|
27
|
+
children,
|
|
28
|
+
class: className = "",
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const orientationClasses: Record<Orientation, string> = {
|
|
32
|
+
horizontal: "flex flex-row flex-wrap gap-x-6 gap-y-3",
|
|
33
|
+
vertical: "flex flex-col gap-3",
|
|
34
|
+
};
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<fieldset class="space-y-3 {className}" role="radiogroup" aria-label={label}>
|
|
38
|
+
{#if label}
|
|
39
|
+
<legend class="text-sm font-medium text-on-surface-muted">{label}</legend>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<div class={orientationClasses[orientation]}>
|
|
43
|
+
{@render children()}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{#if error}
|
|
47
|
+
<p class="animate-in slide-in-from-top-1 fade-in text-sm text-danger-600 duration-200">
|
|
48
|
+
{error}
|
|
49
|
+
</p>
|
|
50
|
+
{:else if hint}
|
|
51
|
+
<p class="text-sm text-on-surface-muted">{hint}</p>
|
|
52
|
+
{/if}
|
|
53
|
+
</fieldset>
|