@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,424 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: PoppyCake, s.r.o. -->
|
|
2
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { untrack } from "svelte";
|
|
6
|
+
import Convert from "ansi-to-html";
|
|
7
|
+
import { LogCache } from "../log-console/LogCache.svelte.js";
|
|
8
|
+
import { LogFetcher } from "../log-console/LogFetcher.svelte.js";
|
|
9
|
+
import type { FetchLogsFn, LogEvent } from "../log-console/types.js";
|
|
10
|
+
import { formatBytes } from "../utils/format.js";
|
|
11
|
+
|
|
12
|
+
// Convert ANSI escape sequences in a single log line to safe HTML spans.
|
|
13
|
+
// A fresh converter per call ensures no colour state leaks between
|
|
14
|
+
// independently virtualised lines.
|
|
15
|
+
function ansiLineToHtml(text: string): string {
|
|
16
|
+
return new Convert({ escapeXML: true }).toHtml(text);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
fetchLogs?: FetchLogsFn;
|
|
21
|
+
chunkSize?: number;
|
|
22
|
+
lineHeight?: number;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { fetchLogs, chunkSize = 4096, lineHeight = 20, class: className = "" }: Props = $props();
|
|
27
|
+
|
|
28
|
+
const cache = new LogCache();
|
|
29
|
+
let fetcher: LogFetcher | null = $state(null);
|
|
30
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
31
|
+
let scrollTop = $state(0);
|
|
32
|
+
let containerHeight = $state(0);
|
|
33
|
+
let isAutoScroll = $state(true);
|
|
34
|
+
let userScrolledUp = $state(false);
|
|
35
|
+
let isStreaming = $derived(!cache.finished && cache.totalLines > 0);
|
|
36
|
+
|
|
37
|
+
const OVERSCAN = 50;
|
|
38
|
+
const BLANK_LINES_AT_END = 3;
|
|
39
|
+
|
|
40
|
+
let truncationBannerHeight = $derived(cache.firstAvailableLine > 0 ? lineHeight : 0);
|
|
41
|
+
|
|
42
|
+
// Convert a global line number to a pixel Y position.
|
|
43
|
+
function lineTop(lineNum: number): number {
|
|
44
|
+
return (lineNum - cache.firstAvailableLine) * lineHeight + truncationBannerHeight;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let visibleStart = $derived(
|
|
48
|
+
Math.max(
|
|
49
|
+
cache.firstAvailableLine,
|
|
50
|
+
cache.firstAvailableLine +
|
|
51
|
+
Math.floor(Math.max(0, scrollTop - truncationBannerHeight) / lineHeight) -
|
|
52
|
+
OVERSCAN,
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
let visibleEnd = $derived(
|
|
56
|
+
Math.max(
|
|
57
|
+
cache.firstAvailableLine,
|
|
58
|
+
Math.min(
|
|
59
|
+
Math.max(0, cache.totalLines - 1),
|
|
60
|
+
cache.firstAvailableLine +
|
|
61
|
+
Math.ceil(
|
|
62
|
+
Math.max(0, scrollTop - truncationBannerHeight + containerHeight) /
|
|
63
|
+
lineHeight,
|
|
64
|
+
) +
|
|
65
|
+
OVERSCAN,
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let totalHeight = $derived.by(() => {
|
|
71
|
+
const availableLines = cache.totalLines - cache.firstAvailableLine;
|
|
72
|
+
const truncationBannerHeight = cache.firstAvailableLine > 0 ? lineHeight : 0;
|
|
73
|
+
const linesHeight = availableLines * lineHeight + truncationBannerHeight;
|
|
74
|
+
const streamingHeight = !cache.finished && cache.totalLines > 0 ? lineHeight : 0;
|
|
75
|
+
const blankHeight = cache.totalLines > 0 ? BLANK_LINES_AT_END * lineHeight : 0;
|
|
76
|
+
return Math.max(linesHeight + streamingHeight + blankHeight, containerHeight);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let gutterWidth = $derived(Math.max(4, String(cache.totalLines || 1).length) * 10 + 16);
|
|
80
|
+
|
|
81
|
+
let renderedLines = $derived.by(() => {
|
|
82
|
+
const lines: Array<{ num: number; text: string | undefined }> = [];
|
|
83
|
+
const start = Math.max(cache.firstAvailableLine, visibleStart);
|
|
84
|
+
const end = Math.min(cache.totalLines - 1, visibleEnd);
|
|
85
|
+
|
|
86
|
+
for (let i = start; i <= end; i++) {
|
|
87
|
+
lines.push({
|
|
88
|
+
num: i,
|
|
89
|
+
text: cache.lines.get(i),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// The fetcher serves on-demand scroll-up loads only; the parent seeds
|
|
96
|
+
// the initial cache via onStream (typically a tail fetch) so the
|
|
97
|
+
// viewport lands at the end of the log without racing the auto-scroll.
|
|
98
|
+
$effect(() => {
|
|
99
|
+
const fn = fetchLogs;
|
|
100
|
+
const cs = chunkSize;
|
|
101
|
+
fetcher = new LogFetcher(cache, fn, cs, () => {
|
|
102
|
+
if (isAutoScroll && !userScrolledUp) {
|
|
103
|
+
requestAnimationFrame(() => scrollToBottom());
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
fetcher?.destroy();
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Request missing data for the visible range (read-only w.r.t. cache).
|
|
113
|
+
$effect(() => {
|
|
114
|
+
if (fetcher && cache.totalLines > 0) {
|
|
115
|
+
fetcher.pruneQueue(visibleStart, visibleEnd);
|
|
116
|
+
untrack(() => {
|
|
117
|
+
fetcher!.maybeRequestMissing(visibleStart, visibleEnd);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Prune stale cache entries — separate from reads to avoid a read-write cycle on SvelteMap.
|
|
123
|
+
$effect(() => {
|
|
124
|
+
const vs = visibleStart;
|
|
125
|
+
const ve = visibleEnd;
|
|
126
|
+
untrack(() => cache.prune(vs, ve));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let prevTotalLines = 0; // plain variable — intentionally non-reactive
|
|
130
|
+
|
|
131
|
+
$effect(() => {
|
|
132
|
+
const currentTotal = cache.totalLines;
|
|
133
|
+
if (currentTotal > prevTotalLines && isAutoScroll && !userScrolledUp) {
|
|
134
|
+
requestAnimationFrame(() => scrollToBottom());
|
|
135
|
+
}
|
|
136
|
+
prevTotalLines = currentTotal;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
function onScroll(e: Event) {
|
|
140
|
+
const target = e.target as HTMLDivElement;
|
|
141
|
+
scrollTop = target.scrollTop;
|
|
142
|
+
|
|
143
|
+
const scrollHeight = target.scrollHeight;
|
|
144
|
+
const clientHeight = target.clientHeight;
|
|
145
|
+
const currentScroll = target.scrollTop;
|
|
146
|
+
const distanceFromBottom = scrollHeight - currentScroll - clientHeight;
|
|
147
|
+
|
|
148
|
+
const atBottom = distanceFromBottom < lineHeight * 2;
|
|
149
|
+
|
|
150
|
+
if (atBottom) {
|
|
151
|
+
userScrolledUp = false;
|
|
152
|
+
isAutoScroll = true;
|
|
153
|
+
} else if (isAutoScroll) {
|
|
154
|
+
userScrolledUp = true;
|
|
155
|
+
isAutoScroll = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scrollToBottom(smooth = false) {
|
|
160
|
+
if (containerEl) {
|
|
161
|
+
if (smooth) {
|
|
162
|
+
containerEl.scrollTo({
|
|
163
|
+
top: containerEl.scrollHeight,
|
|
164
|
+
behavior: "smooth",
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
containerEl.scrollTop = containerEl.scrollHeight;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function enableAutoScroll() {
|
|
173
|
+
isAutoScroll = true;
|
|
174
|
+
userScrolledUp = false;
|
|
175
|
+
scrollToBottom();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onResize() {
|
|
179
|
+
if (containerEl) {
|
|
180
|
+
containerHeight = containerEl.clientHeight;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function onStream(event: LogEvent) {
|
|
185
|
+
const prevTotal = cache.totalLines;
|
|
186
|
+
cache.applyEvent(event);
|
|
187
|
+
|
|
188
|
+
if (cache.totalLines > prevTotal && isAutoScroll && !userScrolledUp) {
|
|
189
|
+
requestAnimationFrame(() => scrollToBottom());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (fetcher) {
|
|
193
|
+
fetcher.maybeRequestMissing(visibleStart, visibleEnd);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function reset() {
|
|
198
|
+
cache.reset();
|
|
199
|
+
isAutoScroll = true;
|
|
200
|
+
userScrolledUp = false;
|
|
201
|
+
scrollTop = 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
$effect(() => {
|
|
205
|
+
if (!containerEl) return;
|
|
206
|
+
containerHeight = containerEl.clientHeight;
|
|
207
|
+
|
|
208
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
209
|
+
onResize();
|
|
210
|
+
});
|
|
211
|
+
resizeObserver.observe(containerEl);
|
|
212
|
+
|
|
213
|
+
return () => {
|
|
214
|
+
resizeObserver.disconnect();
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<div
|
|
220
|
+
class="log-console relative flex h-full w-full flex-col overflow-hidden bg-slate-950 font-mono text-sm {className}"
|
|
221
|
+
>
|
|
222
|
+
<div bind:this={containerEl} class="flex-1 overflow-auto" onscroll={onScroll}>
|
|
223
|
+
<div class="relative" style="height: {totalHeight}px; min-height: 100%;">
|
|
224
|
+
{#if cache.firstAvailableLine > 0}
|
|
225
|
+
<div
|
|
226
|
+
class="absolute right-0 left-0 flex items-center bg-amber-950/60 text-amber-400"
|
|
227
|
+
style="top: 0px; height: {lineHeight}px;"
|
|
228
|
+
>
|
|
229
|
+
<div
|
|
230
|
+
class="flex-shrink-0 pr-3 text-right select-none"
|
|
231
|
+
style="width: {gutterWidth}px;"
|
|
232
|
+
></div>
|
|
233
|
+
<div class="flex-1 truncate pr-4 text-xs">
|
|
234
|
+
Log truncated: {cache.firstAvailableLine.toLocaleString()} earlier line{cache.firstAvailableLine ===
|
|
235
|
+
1
|
|
236
|
+
? ""
|
|
237
|
+
: "s"} removed due to size limits
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
{/if}
|
|
241
|
+
|
|
242
|
+
{#each renderedLines as line (line.num)}
|
|
243
|
+
<div
|
|
244
|
+
class="log-line absolute right-0 left-0 flex items-center hover:bg-slate-900/50"
|
|
245
|
+
style="top: {lineTop(line.num)}px; height: {lineHeight}px;"
|
|
246
|
+
>
|
|
247
|
+
<div
|
|
248
|
+
class="flex-shrink-0 pr-3 text-right text-slate-600 select-none"
|
|
249
|
+
style="width: {gutterWidth}px;"
|
|
250
|
+
>
|
|
251
|
+
{line.num + 1}
|
|
252
|
+
</div>
|
|
253
|
+
<div class="flex-1 truncate pr-4 text-slate-300">
|
|
254
|
+
{#if line.text !== undefined}
|
|
255
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
256
|
+
<span class="whitespace-pre">{@html ansiLineToHtml(line.text)}</span>
|
|
257
|
+
{:else}
|
|
258
|
+
<span class="text-slate-700 italic">Loading...</span>
|
|
259
|
+
{/if}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
{/each}
|
|
263
|
+
|
|
264
|
+
{#if isStreaming && cache.totalLines > 0}
|
|
265
|
+
<div
|
|
266
|
+
class="streaming-indicator absolute right-0 left-0 flex items-center"
|
|
267
|
+
style="top: {lineTop(cache.totalLines)}px; height: {lineHeight}px;"
|
|
268
|
+
>
|
|
269
|
+
<div
|
|
270
|
+
class="flex-shrink-0 pr-3 text-right select-none"
|
|
271
|
+
style="width: {gutterWidth}px;"
|
|
272
|
+
></div>
|
|
273
|
+
<div class="flex items-center gap-1 text-slate-500">
|
|
274
|
+
<span class="dot" style="animation-delay: 0ms;"></span>
|
|
275
|
+
<span class="dot" style="animation-delay: 150ms;"></span>
|
|
276
|
+
<span class="dot" style="animation-delay: 300ms;"></span>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{/if}
|
|
280
|
+
|
|
281
|
+
{#if cache.totalLines > 0}
|
|
282
|
+
{#each Array.from({ length: BLANK_LINES_AT_END }, (_, i) => i) as i (i)}
|
|
283
|
+
<div
|
|
284
|
+
class="absolute right-0 left-0 flex items-center"
|
|
285
|
+
style="top: {lineTop(
|
|
286
|
+
cache.totalLines + (isStreaming ? 1 : 0) + i,
|
|
287
|
+
)}px; height: {lineHeight}px;"
|
|
288
|
+
>
|
|
289
|
+
<div
|
|
290
|
+
class="flex-shrink-0 pr-3 text-right text-slate-800 opacity-0 select-none"
|
|
291
|
+
style="width: {gutterWidth}px;"
|
|
292
|
+
>
|
|
293
|
+
~
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
{/each}
|
|
297
|
+
{/if}
|
|
298
|
+
|
|
299
|
+
{#if cache.totalLines === 0 && !fetcher?.isFetching}
|
|
300
|
+
<div class="absolute inset-0 flex items-center justify-center text-slate-600">
|
|
301
|
+
<div class="text-center">
|
|
302
|
+
<div class="mb-1 text-lg">No output yet</div>
|
|
303
|
+
<div class="text-sm text-slate-700">Waiting for logs...</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
{/if}
|
|
307
|
+
|
|
308
|
+
{#if cache.totalLines === 0 && fetcher?.isFetching}
|
|
309
|
+
<div class="absolute inset-0 flex items-center justify-center text-slate-600">
|
|
310
|
+
<div class="flex items-center gap-2">
|
|
311
|
+
<span class="dot" style="animation-delay: 0ms;"></span>
|
|
312
|
+
<span class="dot" style="animation-delay: 150ms;"></span>
|
|
313
|
+
<span class="dot" style="animation-delay: 300ms;"></span>
|
|
314
|
+
<span class="ml-2">Loading logs...</span>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
{/if}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{#if userScrolledUp && cache.totalLines > 0}
|
|
322
|
+
<button
|
|
323
|
+
class="absolute right-4 bottom-12 z-10 flex items-center gap-2 rounded-full border
|
|
324
|
+
border-slate-700 bg-slate-800
|
|
325
|
+
px-3 py-1.5 text-xs
|
|
326
|
+
font-medium text-slate-300 shadow-lg
|
|
327
|
+
transition-all duration-200 hover:scale-105
|
|
328
|
+
hover:bg-slate-700 active:scale-95"
|
|
329
|
+
onclick={enableAutoScroll}
|
|
330
|
+
>
|
|
331
|
+
<svg
|
|
332
|
+
class="h-3.5 w-3.5"
|
|
333
|
+
viewBox="0 0 24 24"
|
|
334
|
+
fill="none"
|
|
335
|
+
stroke="currentColor"
|
|
336
|
+
stroke-width="2"
|
|
337
|
+
>
|
|
338
|
+
<path d="M12 5v14M19 12l-7 7-7-7" />
|
|
339
|
+
</svg>
|
|
340
|
+
Scroll to bottom
|
|
341
|
+
</button>
|
|
342
|
+
{/if}
|
|
343
|
+
|
|
344
|
+
<div
|
|
345
|
+
class="flex flex-shrink-0 items-center justify-between border-t border-slate-800
|
|
346
|
+
bg-slate-900/80 px-3 py-1.5 text-xs text-slate-500"
|
|
347
|
+
>
|
|
348
|
+
<div class="flex items-center gap-3">
|
|
349
|
+
{#if isStreaming}
|
|
350
|
+
<div class="flex items-center gap-1.5 text-aurora-400">
|
|
351
|
+
<div class="h-1.5 w-1.5 animate-pulse rounded-full bg-aurora-400"></div>
|
|
352
|
+
Streaming
|
|
353
|
+
</div>
|
|
354
|
+
{:else if cache.finished}
|
|
355
|
+
<span class="text-slate-600">Stream ended</span>
|
|
356
|
+
{/if}
|
|
357
|
+
{#if fetcher?.isFetching}
|
|
358
|
+
<span class="text-slate-600">Fetching...</span>
|
|
359
|
+
{/if}
|
|
360
|
+
</div>
|
|
361
|
+
<div class="flex items-center gap-4">
|
|
362
|
+
<span>{cache.totalLines.toLocaleString()} lines</span>
|
|
363
|
+
{#if cache.totalBytes > 0}
|
|
364
|
+
<span>{formatBytes(cache.totalBytes)}</span>
|
|
365
|
+
{/if}
|
|
366
|
+
{#if isAutoScroll}
|
|
367
|
+
<span class="text-aurora-500">Auto-scroll</span>
|
|
368
|
+
{/if}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<style>
|
|
374
|
+
.log-console {
|
|
375
|
+
--log-line-height: 20px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.dot {
|
|
379
|
+
display: inline-block;
|
|
380
|
+
width: 4px;
|
|
381
|
+
height: 4px;
|
|
382
|
+
border-radius: 50%;
|
|
383
|
+
background-color: currentColor;
|
|
384
|
+
animation: dot-pulse 1s ease-in-out infinite;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@keyframes dot-pulse {
|
|
388
|
+
0%,
|
|
389
|
+
60%,
|
|
390
|
+
100% {
|
|
391
|
+
opacity: 0.3;
|
|
392
|
+
transform: scale(0.8);
|
|
393
|
+
}
|
|
394
|
+
30% {
|
|
395
|
+
opacity: 1;
|
|
396
|
+
transform: scale(1);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.log-console ::-webkit-scrollbar {
|
|
401
|
+
width: 10px;
|
|
402
|
+
height: 10px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.log-console ::-webkit-scrollbar-track {
|
|
406
|
+
background: transparent;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.log-console ::-webkit-scrollbar-thumb {
|
|
410
|
+
background-color: var(--color-mist-700);
|
|
411
|
+
border-radius: 5px;
|
|
412
|
+
border: 2px solid transparent;
|
|
413
|
+
background-clip: content-box;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.log-console ::-webkit-scrollbar-thumb:hover {
|
|
417
|
+
background-color: var(--color-mist-600);
|
|
418
|
+
background-clip: content-box;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.log-console ::-webkit-scrollbar-corner {
|
|
422
|
+
background: transparent;
|
|
423
|
+
}
|
|
424
|
+
</style>
|
|
@@ -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 logoSvg from "../assets/runwisp-logo.svg?raw";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { size = "md", class: className = "" }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const sizeClasses = {
|
|
15
|
+
xs: "w-4 h-4",
|
|
16
|
+
sm: "w-5 h-5",
|
|
17
|
+
md: "w-6 h-6",
|
|
18
|
+
lg: "w-8 h-8",
|
|
19
|
+
xl: "w-12 h-12",
|
|
20
|
+
};
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<span class="inline-flex {sizeClasses[size]} {className}" aria-hidden="true">
|
|
24
|
+
<!-- SVG body is a static, repo-vendored asset imported via Vite's ?raw loader. -->
|
|
25
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
26
|
+
{@html logoSvg}
|
|
27
|
+
</span>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
span :global(svg) {
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,138 @@
|
|
|
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 { X } from "@lucide/svelte";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
open?: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string | undefined;
|
|
12
|
+
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
13
|
+
closable?: boolean;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
header?: Snippet;
|
|
16
|
+
footer?: Snippet;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import { portal } from "../actions/portal.js";
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
open = $bindable(false),
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
size = "md",
|
|
28
|
+
closable = true,
|
|
29
|
+
onClose,
|
|
30
|
+
header,
|
|
31
|
+
footer,
|
|
32
|
+
children,
|
|
33
|
+
class: className = "",
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const sizeClasses: Record<string, string> = {
|
|
37
|
+
sm: "max-w-sm",
|
|
38
|
+
md: "max-w-md",
|
|
39
|
+
lg: "max-w-lg",
|
|
40
|
+
xl: "max-w-xl",
|
|
41
|
+
full: "max-w-4xl",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function handleClose() {
|
|
45
|
+
open = false;
|
|
46
|
+
onClose?.();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
50
|
+
if (e.key === "Escape" && closable) {
|
|
51
|
+
handleClose();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
57
|
+
|
|
58
|
+
{#if open}
|
|
59
|
+
<div
|
|
60
|
+
use:portal
|
|
61
|
+
class="
|
|
62
|
+
fixed inset-0
|
|
63
|
+
z-50 flex items-center justify-center
|
|
64
|
+
p-4
|
|
65
|
+
"
|
|
66
|
+
>
|
|
67
|
+
{#if closable}
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="absolute inset-0 z-0 bg-backdrop backdrop-blur-sm"
|
|
71
|
+
aria-label="Close modal"
|
|
72
|
+
tabindex="-1"
|
|
73
|
+
onclick={handleClose}
|
|
74
|
+
></button>
|
|
75
|
+
{:else}
|
|
76
|
+
<div class="absolute inset-0 z-0 bg-backdrop backdrop-blur-sm"></div>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
79
|
+
<div
|
|
80
|
+
class="
|
|
81
|
+
relative z-10 w-full {sizeClasses[size]}
|
|
82
|
+
flex max-h-[90vh] flex-col
|
|
83
|
+
rounded-2xl bg-surface-overlay shadow-xl
|
|
84
|
+
{className}
|
|
85
|
+
"
|
|
86
|
+
role="dialog"
|
|
87
|
+
aria-modal="true"
|
|
88
|
+
aria-labelledby={title ? "modal-title" : undefined}
|
|
89
|
+
>
|
|
90
|
+
<!-- Header -->
|
|
91
|
+
{#if header}
|
|
92
|
+
<div class="border-b border-outline-faint px-6 py-4">
|
|
93
|
+
{@render header()}
|
|
94
|
+
</div>
|
|
95
|
+
{:else if title || closable}
|
|
96
|
+
<div
|
|
97
|
+
class="flex items-start justify-between gap-4 border-b border-outline-faint px-6 py-4"
|
|
98
|
+
>
|
|
99
|
+
<div>
|
|
100
|
+
{#if title}
|
|
101
|
+
<h2 id="modal-title" class="text-lg font-semibold text-on-surface">
|
|
102
|
+
{title}
|
|
103
|
+
</h2>
|
|
104
|
+
{/if}
|
|
105
|
+
{#if description}
|
|
106
|
+
<p class="mt-1 text-sm text-on-surface-muted">{description}</p>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
{#if closable}
|
|
110
|
+
<button
|
|
111
|
+
onclick={handleClose}
|
|
112
|
+
class="-m-2 shrink-0 rounded-lg p-2 text-on-surface-faint transition-colors hover:bg-surface-sunken hover:text-on-surface-muted"
|
|
113
|
+
aria-label="Close"
|
|
114
|
+
>
|
|
115
|
+
<X size={20} />
|
|
116
|
+
</button>
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<!-- Body -->
|
|
122
|
+
{#if children}
|
|
123
|
+
<div class="flex-1 overflow-y-auto px-6 py-4">
|
|
124
|
+
{@render children()}
|
|
125
|
+
</div>
|
|
126
|
+
{/if}
|
|
127
|
+
|
|
128
|
+
<!-- Footer -->
|
|
129
|
+
{#if footer}
|
|
130
|
+
<div
|
|
131
|
+
class="rounded-b-2xl border-t border-outline-faint bg-surface-sunken/50 px-6 py-4"
|
|
132
|
+
>
|
|
133
|
+
{@render footer()}
|
|
134
|
+
</div>
|
|
135
|
+
{/if}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
{/if}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* PageContainer — the single, standardized page-level wrapper.
|
|
9
|
+
*
|
|
10
|
+
* Variants:
|
|
11
|
+
* "standard" – Max-width + centering + padding + flex-col + min-h-full.
|
|
12
|
+
* "full" – Same as standard but h-full (fills viewport).
|
|
13
|
+
* "wide" – Max-width + centering only (consumer provides padding).
|
|
14
|
+
* "narrow" – Narrower max-width + centering.
|
|
15
|
+
* "flush" – Fills container height, no max-width.
|
|
16
|
+
*/
|
|
17
|
+
let {
|
|
18
|
+
children,
|
|
19
|
+
variant = "standard",
|
|
20
|
+
class: className = "",
|
|
21
|
+
}: {
|
|
22
|
+
children: Snippet;
|
|
23
|
+
variant?: "standard" | "full" | "wide" | "narrow" | "flush";
|
|
24
|
+
class?: string;
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
const variantClasses: Record<string, string> = {
|
|
28
|
+
standard: "mx-auto w-full max-w-7xl flex flex-col min-h-full p-4 md:p-8",
|
|
29
|
+
full: "mx-auto w-full max-w-7xl flex flex-col h-full min-h-0 p-4 md:p-8",
|
|
30
|
+
wide: "mx-auto w-full max-w-7xl",
|
|
31
|
+
narrow: "mx-auto w-full max-w-3xl",
|
|
32
|
+
flush: "flex flex-col h-full min-h-0",
|
|
33
|
+
};
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class="animate-in fade-in duration-300 {variantClasses[variant]} {className}">
|
|
37
|
+
{@render children()}
|
|
38
|
+
</div>
|