@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,203 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { fade } from "svelte/transition";
|
|
6
|
+
|
|
7
|
+
type Status =
|
|
8
|
+
| "running"
|
|
9
|
+
| "success"
|
|
10
|
+
| "failed"
|
|
11
|
+
| "pending"
|
|
12
|
+
| "paused"
|
|
13
|
+
| "scheduled"
|
|
14
|
+
| "cancelled";
|
|
15
|
+
type Size = "sm" | "md" | "lg";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
status: Status;
|
|
19
|
+
size?: Size;
|
|
20
|
+
showLabel?: boolean;
|
|
21
|
+
pulse?: boolean;
|
|
22
|
+
label?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
status,
|
|
28
|
+
size = "md",
|
|
29
|
+
showLabel = true,
|
|
30
|
+
pulse = false,
|
|
31
|
+
label = undefined,
|
|
32
|
+
class: className = "",
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
// Status configuration lookup
|
|
36
|
+
const STATUS_CONFIG: Record<Status, { label: string; classes: string; iconColor: string }> = {
|
|
37
|
+
running: {
|
|
38
|
+
label: "Running",
|
|
39
|
+
classes: "bg-info-soft text-info-soft-text border-info-soft ring-1 ring-info-soft",
|
|
40
|
+
iconColor: "text-info-surface",
|
|
41
|
+
},
|
|
42
|
+
success: {
|
|
43
|
+
label: "Success",
|
|
44
|
+
classes:
|
|
45
|
+
"bg-success-soft text-success-soft-text border-success-soft ring-1 ring-success-soft/50",
|
|
46
|
+
iconColor: "text-success-surface",
|
|
47
|
+
},
|
|
48
|
+
failed: {
|
|
49
|
+
label: "Failed",
|
|
50
|
+
classes:
|
|
51
|
+
"bg-danger-soft text-danger-soft-text border-danger-soft ring-1 ring-danger-soft/50",
|
|
52
|
+
iconColor: "text-danger-surface",
|
|
53
|
+
},
|
|
54
|
+
pending: {
|
|
55
|
+
label: "Pending",
|
|
56
|
+
classes: "bg-warning-soft text-warning-soft-text border-warning-soft",
|
|
57
|
+
iconColor: "text-warning-surface",
|
|
58
|
+
},
|
|
59
|
+
paused: {
|
|
60
|
+
label: "Paused",
|
|
61
|
+
classes: "bg-surface-sunken text-on-surface-muted border-outline",
|
|
62
|
+
iconColor: "text-on-surface-faint",
|
|
63
|
+
},
|
|
64
|
+
scheduled: {
|
|
65
|
+
label: "Scheduled",
|
|
66
|
+
classes: "bg-primary-soft text-primary-soft-text border-primary-soft",
|
|
67
|
+
iconColor: "text-primary",
|
|
68
|
+
},
|
|
69
|
+
cancelled: {
|
|
70
|
+
label: "Cancelled",
|
|
71
|
+
classes: "bg-surface-sunken text-on-surface-faint border-outline opacity-75",
|
|
72
|
+
iconColor: "text-on-surface-faint",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Size configuration lookup
|
|
77
|
+
const SIZE_CONFIG: Record<Size, { classes: string; iconSize: number }> = {
|
|
78
|
+
sm: { classes: "px-2 py-0.5 text-2xs gap-1.5", iconSize: 10 },
|
|
79
|
+
md: { classes: "px-2.5 py-0.5 text-xs gap-1.5", iconSize: 12 },
|
|
80
|
+
lg: { classes: "px-3 py-1 text-sm gap-2", iconSize: 14 },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const config = $derived(STATUS_CONFIG[status]);
|
|
84
|
+
const sizeConfig = $derived(SIZE_CONFIG[size]);
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<div
|
|
88
|
+
class="duration-normal inline-flex items-center justify-center rounded-full border font-medium transition-all {config.classes} {sizeConfig.classes} {className}"
|
|
89
|
+
role="status"
|
|
90
|
+
in:fade={{ duration: 150 }}
|
|
91
|
+
>
|
|
92
|
+
<!-- Icon Container -->
|
|
93
|
+
<span class="flex shrink-0 items-center justify-center {config.iconColor}">
|
|
94
|
+
{#if status === "running" || pulse}
|
|
95
|
+
<svg
|
|
96
|
+
width={sizeConfig.iconSize}
|
|
97
|
+
height={sizeConfig.iconSize}
|
|
98
|
+
viewBox="0 0 24 24"
|
|
99
|
+
fill="none"
|
|
100
|
+
class="animate-spin"
|
|
101
|
+
>
|
|
102
|
+
<circle
|
|
103
|
+
class="opacity-25"
|
|
104
|
+
cx="12"
|
|
105
|
+
cy="12"
|
|
106
|
+
r="10"
|
|
107
|
+
stroke="currentColor"
|
|
108
|
+
stroke-width="4"
|
|
109
|
+
></circle>
|
|
110
|
+
<path
|
|
111
|
+
class="opacity-75"
|
|
112
|
+
fill="currentColor"
|
|
113
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
114
|
+
></path>
|
|
115
|
+
</svg>
|
|
116
|
+
{:else if status === "success"}
|
|
117
|
+
<svg
|
|
118
|
+
width={sizeConfig.iconSize}
|
|
119
|
+
height={sizeConfig.iconSize}
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
fill="none"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
stroke-width="3"
|
|
124
|
+
stroke-linecap="round"
|
|
125
|
+
stroke-linejoin="round"
|
|
126
|
+
>
|
|
127
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
128
|
+
</svg>
|
|
129
|
+
{:else if status === "failed"}
|
|
130
|
+
<svg
|
|
131
|
+
width={sizeConfig.iconSize}
|
|
132
|
+
height={sizeConfig.iconSize}
|
|
133
|
+
viewBox="0 0 24 24"
|
|
134
|
+
fill="none"
|
|
135
|
+
stroke="currentColor"
|
|
136
|
+
stroke-width="3"
|
|
137
|
+
stroke-linecap="round"
|
|
138
|
+
stroke-linejoin="round"
|
|
139
|
+
>
|
|
140
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
141
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
142
|
+
</svg>
|
|
143
|
+
{:else if status === "pending"}
|
|
144
|
+
<svg
|
|
145
|
+
width={sizeConfig.iconSize}
|
|
146
|
+
height={sizeConfig.iconSize}
|
|
147
|
+
viewBox="0 0 24 24"
|
|
148
|
+
fill="none"
|
|
149
|
+
stroke="currentColor"
|
|
150
|
+
stroke-width="3"
|
|
151
|
+
stroke-linecap="round"
|
|
152
|
+
stroke-linejoin="round"
|
|
153
|
+
>
|
|
154
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
155
|
+
<polyline points="12 6 12 12 16 14"></polyline>
|
|
156
|
+
</svg>
|
|
157
|
+
{:else if status === "scheduled"}
|
|
158
|
+
<svg
|
|
159
|
+
width={sizeConfig.iconSize}
|
|
160
|
+
height={sizeConfig.iconSize}
|
|
161
|
+
viewBox="0 0 24 24"
|
|
162
|
+
fill="none"
|
|
163
|
+
stroke="currentColor"
|
|
164
|
+
stroke-width="2.5"
|
|
165
|
+
stroke-linecap="round"
|
|
166
|
+
stroke-linejoin="round"
|
|
167
|
+
>
|
|
168
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
|
169
|
+
<line x1="16" y1="2" x2="16" y2="6"></line>
|
|
170
|
+
<line x1="8" y1="2" x2="8" y2="6"></line>
|
|
171
|
+
<line x1="3" y1="10" x2="21" y2="10"></line>
|
|
172
|
+
</svg>
|
|
173
|
+
{:else if status === "paused"}
|
|
174
|
+
<svg
|
|
175
|
+
width={sizeConfig.iconSize}
|
|
176
|
+
height={sizeConfig.iconSize}
|
|
177
|
+
viewBox="0 0 24 24"
|
|
178
|
+
fill="currentColor"
|
|
179
|
+
>
|
|
180
|
+
<rect x="6" y="4" width="4" height="16" rx="1" />
|
|
181
|
+
<rect x="14" y="4" width="4" height="16" rx="1" />
|
|
182
|
+
</svg>
|
|
183
|
+
{:else if status === "cancelled"}
|
|
184
|
+
<svg
|
|
185
|
+
width={sizeConfig.iconSize}
|
|
186
|
+
height={sizeConfig.iconSize}
|
|
187
|
+
viewBox="0 0 24 24"
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
stroke-width="2.5"
|
|
191
|
+
stroke-linecap="round"
|
|
192
|
+
stroke-linejoin="round"
|
|
193
|
+
>
|
|
194
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
195
|
+
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
|
|
196
|
+
</svg>
|
|
197
|
+
{/if}
|
|
198
|
+
</span>
|
|
199
|
+
|
|
200
|
+
{#if showLabel}
|
|
201
|
+
<span class="truncate">{label ?? config.label}</span>
|
|
202
|
+
{/if}
|
|
203
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
interface Props {
|
|
8
|
+
children: Snippet;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { children, class: className = "" }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="overflow-x-auto rounded-xl border border-outline {className}">
|
|
16
|
+
<table class="w-full text-sm">
|
|
17
|
+
{@render children()}
|
|
18
|
+
</table>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
interface Props {
|
|
8
|
+
children: Snippet;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { children, class: className = "" }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<tbody class={className}>
|
|
16
|
+
{@render children()}
|
|
17
|
+
</tbody>
|
|
@@ -0,0 +1,34 @@
|
|
|
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 CellAlign = "left" | "center" | "right";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
header?: boolean;
|
|
11
|
+
align?: CellAlign;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { header = false, align = "left", children, class: className = "" }: Props = $props();
|
|
17
|
+
|
|
18
|
+
const alignClasses: Record<CellAlign, string> = {
|
|
19
|
+
left: "text-left",
|
|
20
|
+
center: "text-center",
|
|
21
|
+
right: "text-right",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const tag = $derived(header ? "th" : "td");
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<svelte:element
|
|
28
|
+
this={tag}
|
|
29
|
+
class="px-4 py-3 {alignClasses[align]} {header
|
|
30
|
+
? 'text-xs font-medium tracking-wider text-on-surface-muted uppercase'
|
|
31
|
+
: 'text-on-surface'} {className}"
|
|
32
|
+
>
|
|
33
|
+
{@render children()}
|
|
34
|
+
</svelte:element>
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
interface Props {
|
|
8
|
+
children: Snippet;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { children, class: className = "" }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<thead class="bg-surface-sunken/50 {className}">
|
|
16
|
+
<tr>
|
|
17
|
+
{@render children()}
|
|
18
|
+
</tr>
|
|
19
|
+
</thead>
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
interface Props {
|
|
8
|
+
children: Snippet;
|
|
9
|
+
hoverable?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { children, hoverable = false, class: className = "" }: Props = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<tr
|
|
17
|
+
class="border-b border-outline-faint {hoverable
|
|
18
|
+
? 'duration-fast transition-colors hover:bg-surface-sunken'
|
|
19
|
+
: ''} {className}"
|
|
20
|
+
>
|
|
21
|
+
{@render children()}
|
|
22
|
+
</tr>
|
|
@@ -0,0 +1,148 @@
|
|
|
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 { Component } from "svelte";
|
|
7
|
+
|
|
8
|
+
interface Tab {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: Component;
|
|
12
|
+
badge?: string | number;
|
|
13
|
+
dirty?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
tabs: Tab[];
|
|
19
|
+
activeTab?: string;
|
|
20
|
+
onChange?: (tabId: string) => void;
|
|
21
|
+
variant?: "underline" | "pills" | "enclosed";
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
children?: Snippet<[string]>;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
tabs,
|
|
29
|
+
activeTab = $bindable(),
|
|
30
|
+
onChange,
|
|
31
|
+
variant = "underline",
|
|
32
|
+
fullWidth = false,
|
|
33
|
+
children,
|
|
34
|
+
class: className = "",
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (!activeTab && tabs.length > 0) {
|
|
39
|
+
const firstId = tabs[0]?.id;
|
|
40
|
+
if (firstId) activeTab = firstId;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const renderedActiveTab = $derived(activeTab ?? tabs[0]?.id ?? "");
|
|
45
|
+
|
|
46
|
+
function handleTabClick(tabId: string) {
|
|
47
|
+
if (tabs.find((t) => t.id === tabId)?.disabled) return;
|
|
48
|
+
activeTab = tabId;
|
|
49
|
+
onChange?.(tabId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTabButtonClasses(tab: Tab): string {
|
|
53
|
+
const baseClasses = `
|
|
54
|
+
${fullWidth ? "flex-1" : ""}
|
|
55
|
+
inline-flex items-center justify-center gap-2 px-4 py-2.5
|
|
56
|
+
text-sm font-medium
|
|
57
|
+
transition-colors duration-150
|
|
58
|
+
disabled:cursor-not-allowed disabled:opacity-50
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const isActive = activeTab === tab.id;
|
|
62
|
+
|
|
63
|
+
if (variant === "underline") {
|
|
64
|
+
return `${baseClasses} -mb-px border-b-2 ${
|
|
65
|
+
isActive
|
|
66
|
+
? "border-primary text-primary-soft-text"
|
|
67
|
+
: "border-transparent text-on-surface-muted hover:border-outline-hover hover:text-on-surface"
|
|
68
|
+
}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (variant === "pills" || variant === "enclosed") {
|
|
72
|
+
return `${baseClasses} rounded-lg ${
|
|
73
|
+
isActive
|
|
74
|
+
? "bg-surface-raised text-on-surface shadow-sm"
|
|
75
|
+
: "text-on-surface-muted hover:text-on-surface"
|
|
76
|
+
}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return baseClasses;
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class={className}>
|
|
84
|
+
<!-- Tab list -->
|
|
85
|
+
<div
|
|
86
|
+
class="
|
|
87
|
+
{variant === 'underline' ? 'border-b border-outline' : ''}
|
|
88
|
+
{variant === 'pills' ? 'rounded-xl bg-surface-sunken p-1' : ''}
|
|
89
|
+
{variant === 'enclosed' ? 'rounded-xl border border-outline bg-surface-sunken p-1' : ''}
|
|
90
|
+
"
|
|
91
|
+
role="tablist"
|
|
92
|
+
>
|
|
93
|
+
<nav class="flex {fullWidth ? '' : 'gap-1'}" aria-label="Tabs">
|
|
94
|
+
{#each tabs as tab (tab.id)}
|
|
95
|
+
<button
|
|
96
|
+
onclick={() => handleTabClick(tab.id)}
|
|
97
|
+
class={getTabButtonClasses(tab)}
|
|
98
|
+
role="tab"
|
|
99
|
+
aria-selected={activeTab === tab.id}
|
|
100
|
+
aria-controls="tabpanel-{tab.id}"
|
|
101
|
+
disabled={tab.disabled}
|
|
102
|
+
>
|
|
103
|
+
{#if tab.icon}
|
|
104
|
+
{@const Icon = tab.icon}
|
|
105
|
+
<Icon
|
|
106
|
+
size={16}
|
|
107
|
+
class={activeTab === tab.id ? "text-primary" : "text-on-surface-faint"}
|
|
108
|
+
/>
|
|
109
|
+
{/if}
|
|
110
|
+
<span>{tab.label}</span>
|
|
111
|
+
{#if tab.dirty}
|
|
112
|
+
<span
|
|
113
|
+
class="h-1.5 w-1.5 rounded-full {activeTab === tab.id
|
|
114
|
+
? 'bg-warning-surface'
|
|
115
|
+
: 'bg-on-surface-faint'}"
|
|
116
|
+
aria-label="Unsaved changes"
|
|
117
|
+
title="Unsaved changes"
|
|
118
|
+
></span>
|
|
119
|
+
{/if}
|
|
120
|
+
{#if tab.badge || tab.badge === 0}
|
|
121
|
+
<span
|
|
122
|
+
class="
|
|
123
|
+
rounded-full px-1.5 py-0.5 text-xs font-medium
|
|
124
|
+
{activeTab === tab.id
|
|
125
|
+
? 'bg-primary-soft text-primary-soft-text'
|
|
126
|
+
: 'bg-surface-sunken text-on-surface-muted'}
|
|
127
|
+
"
|
|
128
|
+
>
|
|
129
|
+
{tab.badge}
|
|
130
|
+
</span>
|
|
131
|
+
{/if}
|
|
132
|
+
</button>
|
|
133
|
+
{/each}
|
|
134
|
+
</nav>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Tab panels -->
|
|
138
|
+
{#if children && renderedActiveTab}
|
|
139
|
+
<div
|
|
140
|
+
id="tabpanel-{renderedActiveTab}"
|
|
141
|
+
role="tabpanel"
|
|
142
|
+
aria-labelledby="tab-{renderedActiveTab}"
|
|
143
|
+
class="mt-4"
|
|
144
|
+
>
|
|
145
|
+
{@render children(renderedActiveTab)}
|
|
146
|
+
</div>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
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 TextSize = "xs" | "sm" | "base" | "lg" | "xl";
|
|
8
|
+
type TextWeight = "normal" | "medium" | "semibold" | "bold";
|
|
9
|
+
type TextColor = "default" | "muted" | "faint";
|
|
10
|
+
type TextElement = "p" | "span" | "div";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
size?: TextSize;
|
|
14
|
+
weight?: TextWeight;
|
|
15
|
+
color?: TextColor;
|
|
16
|
+
as?: TextElement;
|
|
17
|
+
children: Snippet;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
size = "base",
|
|
23
|
+
weight = "normal",
|
|
24
|
+
color = "default",
|
|
25
|
+
as = "p",
|
|
26
|
+
children,
|
|
27
|
+
class: className = "",
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const sizeClasses: Record<TextSize, string> = {
|
|
31
|
+
xs: "text-xs",
|
|
32
|
+
sm: "text-sm",
|
|
33
|
+
base: "text-base",
|
|
34
|
+
lg: "text-lg",
|
|
35
|
+
xl: "text-xl",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const weightClasses: Record<TextWeight, string> = {
|
|
39
|
+
normal: "font-normal",
|
|
40
|
+
medium: "font-medium",
|
|
41
|
+
semibold: "font-semibold",
|
|
42
|
+
bold: "font-bold",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const colorClasses: Record<TextColor, string> = {
|
|
46
|
+
default: "text-on-surface",
|
|
47
|
+
muted: "text-on-surface-muted",
|
|
48
|
+
faint: "text-on-surface-faint",
|
|
49
|
+
};
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<svelte:element
|
|
53
|
+
this={as}
|
|
54
|
+
class="{sizeClasses[size]} {weightClasses[weight]} {colorClasses[color]} {className}"
|
|
55
|
+
>
|
|
56
|
+
{@render children()}
|
|
57
|
+
</svelte:element>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
6
|
+
import { CircleAlert } from "@lucide/svelte";
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLTextareaAttributes {
|
|
9
|
+
label?: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
resize?: "none" | "vertical" | "horizontal" | "both";
|
|
13
|
+
class?: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
label,
|
|
19
|
+
hint,
|
|
20
|
+
error,
|
|
21
|
+
disabled = false,
|
|
22
|
+
resize = "vertical",
|
|
23
|
+
class: className = "",
|
|
24
|
+
value = $bindable(""),
|
|
25
|
+
id,
|
|
26
|
+
...restProps
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const resizeClasses: Record<string, string> = {
|
|
30
|
+
none: "resize-none",
|
|
31
|
+
vertical: "resize-y",
|
|
32
|
+
horizontal: "resize-x",
|
|
33
|
+
both: "resize",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const textareaClasses = `
|
|
37
|
+
w-full px-3.5 py-2.5 rounded-lg border
|
|
38
|
+
text-sm bg-surface-raised text-on-surface placeholder:text-on-surface-faint
|
|
39
|
+
focus:outline-none focus:ring-2 focus:ring-offset-0
|
|
40
|
+
disabled:bg-surface-sunken disabled:text-on-surface-muted disabled:cursor-not-allowed
|
|
41
|
+
transition-colors duration-fast
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
const normalBorder =
|
|
45
|
+
"border-outline hover:border-outline-hover focus:border-ring focus:ring-ring/20";
|
|
46
|
+
const errorBorder = "border-danger-400 focus:border-danger-500 focus:ring-danger-500/20";
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="space-y-1.5 {className}">
|
|
50
|
+
{#if label}
|
|
51
|
+
<label class="block text-sm font-medium text-on-surface-muted" for={id}>
|
|
52
|
+
{label}
|
|
53
|
+
</label>
|
|
54
|
+
{/if}
|
|
55
|
+
|
|
56
|
+
<div class="relative">
|
|
57
|
+
<textarea
|
|
58
|
+
class="
|
|
59
|
+
{textareaClasses}
|
|
60
|
+
{error ? errorBorder : normalBorder}
|
|
61
|
+
{resizeClasses[resize]}
|
|
62
|
+
"
|
|
63
|
+
{disabled}
|
|
64
|
+
{id}
|
|
65
|
+
bind:value
|
|
66
|
+
{...restProps}
|
|
67
|
+
></textarea>
|
|
68
|
+
|
|
69
|
+
{#if error}
|
|
70
|
+
<div class="pointer-events-none absolute top-3 right-3 text-danger-500">
|
|
71
|
+
<CircleAlert size={18} />
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{#if error}
|
|
77
|
+
<p class="text-sm text-danger-600">{error}</p>
|
|
78
|
+
{:else if hint}
|
|
79
|
+
<p class="text-sm text-on-surface-muted">{hint}</p>
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { Moon, Sun } from "@lucide/svelte";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
storageKey?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { storageKey = "runwisp:theme", class: className = "" }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let isDark = $state(false);
|
|
15
|
+
let mounted = $state(false);
|
|
16
|
+
|
|
17
|
+
$effect(() => {
|
|
18
|
+
isDark = document.documentElement.classList.contains("dark");
|
|
19
|
+
mounted = true;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function toggle(): void {
|
|
23
|
+
const next = !isDark;
|
|
24
|
+
isDark = next;
|
|
25
|
+
document.documentElement.classList.toggle("dark", next);
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(storageKey, next ? "dark" : "light");
|
|
28
|
+
} catch {
|
|
29
|
+
// localStorage may be unavailable (private mode, embedded contexts) — fall through.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onclick={toggle}
|
|
37
|
+
aria-pressed={isDark}
|
|
38
|
+
aria-label={isDark ? "Switch to light theme" : "Switch to dark theme"}
|
|
39
|
+
title={isDark ? "Switch to light theme" : "Switch to dark theme"}
|
|
40
|
+
class="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-outline bg-surface-raised text-on-surface-muted transition-colors hover:border-outline-hover hover:text-on-surface {className}"
|
|
41
|
+
>
|
|
42
|
+
{#if mounted && isDark}
|
|
43
|
+
<Sun size={16} />
|
|
44
|
+
{:else}
|
|
45
|
+
<Moon size={16} />
|
|
46
|
+
{/if}
|
|
47
|
+
</button>
|