@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.
Files changed (108) hide show
  1. package/LICENSE +215 -0
  2. package/README.md +20 -0
  3. package/package.json +128 -0
  4. package/src/app.d.ts +8 -0
  5. package/src/app.html +12 -0
  6. package/src/astro/CTABand.astro +32 -0
  7. package/src/astro/Hero.astro +58 -0
  8. package/src/astro/SiteFooter.astro +47 -0
  9. package/src/astro/SiteHeader.astro +46 -0
  10. package/src/lib/actions/portal.ts +16 -0
  11. package/src/lib/assets/runwisp-logo.svg +16 -0
  12. package/src/lib/components/Accordion.svelte +20 -0
  13. package/src/lib/components/AccordionItem.svelte +74 -0
  14. package/src/lib/components/Alert.svelte +107 -0
  15. package/src/lib/components/AlertDialog.svelte +63 -0
  16. package/src/lib/components/Avatar.svelte +88 -0
  17. package/src/lib/components/Badge.svelte +131 -0
  18. package/src/lib/components/Breadcrumb.svelte +58 -0
  19. package/src/lib/components/Button.svelte +69 -0
  20. package/src/lib/components/Card.svelte +70 -0
  21. package/src/lib/components/Checkbox.svelte +90 -0
  22. package/src/lib/components/CodeBlock.svelte +66 -0
  23. package/src/lib/components/CronInput.svelte +130 -0
  24. package/src/lib/components/DataGrid.svelte +368 -0
  25. package/src/lib/components/Drawer.svelte +156 -0
  26. package/src/lib/components/Dropdown.svelte +195 -0
  27. package/src/lib/components/DurationInput.svelte +170 -0
  28. package/src/lib/components/EmptyState.svelte +46 -0
  29. package/src/lib/components/ErrorState.svelte +73 -0
  30. package/src/lib/components/FeatureCard.svelte +35 -0
  31. package/src/lib/components/FeatureGrid.svelte +37 -0
  32. package/src/lib/components/FormField.svelte +53 -0
  33. package/src/lib/components/Heading.svelte +46 -0
  34. package/src/lib/components/Input.svelte +114 -0
  35. package/src/lib/components/Kbd.svelte +31 -0
  36. package/src/lib/components/Link.svelte +56 -0
  37. package/src/lib/components/LinkButton.svelte +55 -0
  38. package/src/lib/components/LogConsole.svelte +424 -0
  39. package/src/lib/components/Logo.svelte +34 -0
  40. package/src/lib/components/Modal.svelte +138 -0
  41. package/src/lib/components/PageContainer.svelte +38 -0
  42. package/src/lib/components/Pagination.svelte +96 -0
  43. package/src/lib/components/Popover.svelte +130 -0
  44. package/src/lib/components/Progress.svelte +68 -0
  45. package/src/lib/components/Prose.svelte +43 -0
  46. package/src/lib/components/Radio.svelte +82 -0
  47. package/src/lib/components/RadioGroup.svelte +53 -0
  48. package/src/lib/components/Select.svelte +454 -0
  49. package/src/lib/components/Separator.svelte +30 -0
  50. package/src/lib/components/Skeleton.svelte +75 -0
  51. package/src/lib/components/Sparkline.svelte +58 -0
  52. package/src/lib/components/Spinner.svelte +44 -0
  53. package/src/lib/components/StatusIndicator.svelte +203 -0
  54. package/src/lib/components/Table.svelte +19 -0
  55. package/src/lib/components/TableBody.svelte +17 -0
  56. package/src/lib/components/TableCell.svelte +34 -0
  57. package/src/lib/components/TableHead.svelte +19 -0
  58. package/src/lib/components/TableRow.svelte +22 -0
  59. package/src/lib/components/Tabs.svelte +148 -0
  60. package/src/lib/components/Text.svelte +57 -0
  61. package/src/lib/components/Textarea.svelte +81 -0
  62. package/src/lib/components/ThemeToggle.svelte +47 -0
  63. package/src/lib/components/TimezoneSelect.svelte +127 -0
  64. package/src/lib/components/ToastContainer.svelte +84 -0
  65. package/src/lib/components/Toggle.svelte +84 -0
  66. package/src/lib/components/Tooltip.svelte +56 -0
  67. package/src/lib/components/button-styles.ts +51 -0
  68. package/src/lib/components/dashboard/RunDetailPanel.svelte +252 -0
  69. package/src/lib/components/dashboard/RunsList.svelte +331 -0
  70. package/src/lib/components/dashboard/run-helpers.ts +12 -0
  71. package/src/lib/components/dashboard/status-config.ts +141 -0
  72. package/src/lib/components/dashboard/types.ts +35 -0
  73. package/src/lib/index.ts +122 -0
  74. package/src/lib/log-console/LogCache.svelte.ts +93 -0
  75. package/src/lib/log-console/LogFetcher.svelte.ts +179 -0
  76. package/src/lib/log-console/types.ts +24 -0
  77. package/src/lib/theme-tokens.css +235 -0
  78. package/src/lib/theme.css +276 -0
  79. package/src/lib/utils/error.ts +18 -0
  80. package/src/lib/utils/format.ts +76 -0
  81. package/src/lib/utils/id.ts +6 -0
  82. package/src/lib/utils/toast.svelte.ts +87 -0
  83. package/src/lib/utils/toast.ts +60 -0
  84. package/src/routes/+layout.svelte +10 -0
  85. package/src/routes/+page.svelte +5 -0
  86. package/src/routes/layout.css +3 -0
  87. package/src/stories/Alert.stories.svelte +64 -0
  88. package/src/stories/Avatar.stories.svelte +76 -0
  89. package/src/stories/Badge.stories.svelte +76 -0
  90. package/src/stories/Breadcrumb.stories.svelte +64 -0
  91. package/src/stories/Button.stories.svelte +101 -0
  92. package/src/stories/Card.stories.svelte +106 -0
  93. package/src/stories/Checkbox.stories.svelte +77 -0
  94. package/src/stories/DataGrid.stories.svelte +130 -0
  95. package/src/stories/Dropdown.stories.svelte +81 -0
  96. package/src/stories/EmptyState.stories.svelte +74 -0
  97. package/src/stories/Input.stories.svelte +88 -0
  98. package/src/stories/Logo.stories.svelte +82 -0
  99. package/src/stories/Modal.stories.svelte +101 -0
  100. package/src/stories/Progress.stories.svelte +65 -0
  101. package/src/stories/Select.stories.svelte +158 -0
  102. package/src/stories/Tabs.stories.svelte +103 -0
  103. package/src/stories/Textarea.stories.svelte +45 -0
  104. package/src/stories/Toggle.stories.svelte +74 -0
  105. package/src/stories/Tooltip.stories.svelte +77 -0
  106. package/src/stories/Welcome.mdx +241 -0
  107. package/src/stories/assets/runwisp_c.svg +16 -0
  108. 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>