@runwisp/ui 1.0.1 → 1.1.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/package.json +4 -4
- package/src/astro/SiteFooter.astro +149 -31
- package/src/lib/components/AlertDialog.svelte +63 -25
- package/src/lib/components/Button.svelte +1 -1
- package/src/lib/components/DataGrid.svelte +151 -90
- package/src/lib/components/FilterBar.svelte +156 -0
- package/src/lib/components/FilterPill.svelte +180 -0
- package/src/lib/components/NotificationBell.svelte +70 -0
- package/src/lib/components/SearchInput.svelte +91 -0
- package/src/lib/components/StatusDot.svelte +38 -0
- package/src/lib/components/ThemeToggle.svelte +28 -33
- package/src/lib/components/filter-spec.ts +94 -0
- package/src/lib/index.ts +26 -1
- package/src/lib/utils/debounce.test.ts +31 -0
- package/src/lib/utils/debounce.ts +22 -0
- package/src/lib/utils/filter.test.ts +172 -0
- package/src/lib/utils/filter.ts +79 -0
- package/src/lib/utils/theme.svelte.test.ts +142 -0
- package/src/lib/utils/theme.svelte.ts +71 -0
- package/src/stories/DataGrid.stories.svelte +64 -1
- package/src/stories/FilterBar.stories.svelte +61 -0
- package/src/stories/Welcome.mdx +1 -1
- package/src/astro/CTABand.astro +0 -39
- package/src/astro/Hero.astro +0 -58
- package/src/astro/SiteHeader.astro +0 -46
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runwisp/ui",
|
|
3
3
|
"description": "Svelte 5 UI component library for RunWisp dashboards and admin interfaces.",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@chromatic-com/storybook": "^5.2.1",
|
|
51
51
|
"@eslint/js": "^10.0.1",
|
|
52
|
-
"@runwisp/eslint-config": "1.0
|
|
53
|
-
"@runwisp/typescript-config": "1.0
|
|
52
|
+
"@runwisp/eslint-config": "1.1.0",
|
|
53
|
+
"@runwisp/typescript-config": "1.1.0",
|
|
54
54
|
"@storybook/addon-a11y": "^10.5.5",
|
|
55
55
|
"@storybook/addon-docs": "^10.5.5",
|
|
56
56
|
"@storybook/addon-svelte-csf": "^5.1.2",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
94
94
|
"@fontsource/tasa-orbiter": "^5.3.0",
|
|
95
95
|
"@lucide/svelte": "^1.28.0",
|
|
96
|
-
"@runwisp/common": "1.0
|
|
96
|
+
"@runwisp/common": "1.1.0",
|
|
97
97
|
"@tanstack/svelte-virtual": "^3.13.35",
|
|
98
98
|
"ansi-to-html": "^0.7.2",
|
|
99
99
|
"cron-parser": "^5.7.0",
|
|
@@ -2,46 +2,164 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
// Brand footer shared by runwisp.com and docs.runwisp.com. Plain scoped CSS on
|
|
6
|
+
// --rw-* tokens (no Tailwind) so it renders the same under Starlight, which
|
|
7
|
+
// only loads theme-tokens.css.
|
|
8
|
+
import logoSvg from "../lib/assets/runwisp-logo.svg?raw";
|
|
9
|
+
|
|
10
|
+
export interface FooterLink {
|
|
11
|
+
label: string;
|
|
12
|
+
href: string;
|
|
13
|
+
external?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FooterColumn {
|
|
17
|
+
title: string;
|
|
18
|
+
links: FooterLink[];
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
interface Props {
|
|
6
|
-
|
|
22
|
+
columns: FooterColumn[];
|
|
23
|
+
homeHref?: string;
|
|
24
|
+
wordmark?: string;
|
|
7
25
|
class?: string;
|
|
8
26
|
}
|
|
9
27
|
|
|
10
|
-
const {
|
|
11
|
-
Astro.props;
|
|
12
|
-
|
|
13
|
-
const hasTeaser = Astro.slots.has("teaser");
|
|
14
|
-
const hasColumns = Astro.slots.has("columns");
|
|
15
|
-
const hasBadges = Astro.slots.has("badges");
|
|
28
|
+
const { columns, homeHref = "/", wordmark = "RunWisp", class: className = "" } = Astro.props;
|
|
16
29
|
---
|
|
17
30
|
|
|
18
|
-
<footer class:list={["
|
|
19
|
-
<div class="
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
hasColumns && (
|
|
29
|
-
<div class="grid gap-8 font-mono sm:grid-cols-2 lg:grid-cols-4 [&_a]:block [&_a]:text-on-surface-muted [&_a]:[&_a:hover]:text-primary [&_h4]:mb-3 [&_h4]:text-xs [&_h4]:font-medium [&_h4]:uppercase [&_h4]:tracking-[0.16em] [&_h4]:text-on-surface-faint [&_h4]:before:mr-1.5 [&_h4]:before:content-['──']">
|
|
30
|
-
<slot name="columns" />
|
|
31
|
-
</div>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
<div
|
|
35
|
-
class="text-on-surface-muted mt-10 flex flex-col items-start justify-between gap-4 border-t border-outline-faint pt-6 font-mono text-xs sm:flex-row sm:items-center"
|
|
36
|
-
>
|
|
37
|
-
<p>{tagline}</p>
|
|
31
|
+
<footer class:list={["rw-ft", className]}>
|
|
32
|
+
<div class="rw-ft-wrap">
|
|
33
|
+
<div class="rw-ft-grid" style={`--rw-ft-cols: ${columns.length}`}>
|
|
34
|
+
<div class="rw-ft-brand">
|
|
35
|
+
<a class="rw-ft-mark" href={homeHref}
|
|
36
|
+
><span class="rw-ft-logo" aria-hidden="true" set:html={logoSvg} />
|
|
37
|
+
{wordmark}</a
|
|
38
|
+
>
|
|
39
|
+
<slot />
|
|
40
|
+
</div>
|
|
38
41
|
{
|
|
39
|
-
|
|
40
|
-
<div class="
|
|
41
|
-
<
|
|
42
|
+
columns.map((column) => (
|
|
43
|
+
<div class="rw-ft-col">
|
|
44
|
+
<h4>{column.title}</h4>
|
|
45
|
+
{column.links.map((link) => (
|
|
46
|
+
<a
|
|
47
|
+
href={link.href}
|
|
48
|
+
target={link.external ? "_blank" : undefined}
|
|
49
|
+
rel={link.external ? "noopener noreferrer" : undefined}
|
|
50
|
+
>
|
|
51
|
+
{link.label}
|
|
52
|
+
</a>
|
|
53
|
+
))}
|
|
42
54
|
</div>
|
|
43
|
-
)
|
|
55
|
+
))
|
|
44
56
|
}
|
|
45
57
|
</div>
|
|
58
|
+
{
|
|
59
|
+
Astro.slots.has("bottom") && (
|
|
60
|
+
<div class="rw-ft-bottom">
|
|
61
|
+
<slot name="bottom" />
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
46
65
|
</div>
|
|
47
66
|
</footer>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.rw-ft {
|
|
70
|
+
border-top: 1px solid var(--rw-outline);
|
|
71
|
+
margin-top: 30px;
|
|
72
|
+
padding: 52px 0 0;
|
|
73
|
+
color: var(--rw-on-surface);
|
|
74
|
+
font-family: var(--rw-font-sans);
|
|
75
|
+
/* Container, not viewport: under Starlight the footer sits in the
|
|
76
|
+
* content column, not the full page width. */
|
|
77
|
+
container-type: inline-size;
|
|
78
|
+
}
|
|
79
|
+
/* Slotted content (blurb, bottom bar) doesn't carry this component's scope,
|
|
80
|
+
* so these reach it via :global, wrapped in :where() to stay at element
|
|
81
|
+
* specificity: any consumer class (e.g. a teaser paragraph) wins. */
|
|
82
|
+
:global(:where(.rw-ft) a) {
|
|
83
|
+
color: inherit;
|
|
84
|
+
text-decoration: none;
|
|
85
|
+
}
|
|
86
|
+
.rw-ft-wrap {
|
|
87
|
+
max-width: 1120px;
|
|
88
|
+
margin: 0 auto;
|
|
89
|
+
padding: 0 clamp(20px, 5vw, 40px);
|
|
90
|
+
}
|
|
91
|
+
.rw-ft-grid {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: 1.6fr repeat(var(--rw-ft-cols), 1fr);
|
|
94
|
+
gap: 30px;
|
|
95
|
+
}
|
|
96
|
+
@container (max-width: 780px) {
|
|
97
|
+
.rw-ft-grid {
|
|
98
|
+
grid-template-columns: 1fr 1fr;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
.rw-ft-mark {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 9px;
|
|
105
|
+
margin-bottom: 14px;
|
|
106
|
+
font-weight: 700;
|
|
107
|
+
font-size: 18px;
|
|
108
|
+
letter-spacing: -0.02em;
|
|
109
|
+
}
|
|
110
|
+
.rw-ft-logo {
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
width: 21px;
|
|
113
|
+
height: 21px;
|
|
114
|
+
color: var(--rw-primary);
|
|
115
|
+
}
|
|
116
|
+
.rw-ft-logo :global(svg) {
|
|
117
|
+
width: 100%;
|
|
118
|
+
height: 100%;
|
|
119
|
+
}
|
|
120
|
+
:global(:where(.rw-ft-brand) p) {
|
|
121
|
+
margin: 0;
|
|
122
|
+
color: var(--rw-on-surface-muted);
|
|
123
|
+
font-size: 13.5px;
|
|
124
|
+
max-width: 30ch;
|
|
125
|
+
}
|
|
126
|
+
.rw-ft-col h4 {
|
|
127
|
+
margin: 0 0 14px;
|
|
128
|
+
font-family: var(--rw-font-mono);
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
font-weight: 500;
|
|
131
|
+
letter-spacing: 0.1em;
|
|
132
|
+
text-transform: lowercase;
|
|
133
|
+
color: var(--rw-on-surface-faint);
|
|
134
|
+
}
|
|
135
|
+
.rw-ft-col h4::before {
|
|
136
|
+
content: "── ";
|
|
137
|
+
}
|
|
138
|
+
.rw-ft-col a {
|
|
139
|
+
display: block;
|
|
140
|
+
padding: 4px 0;
|
|
141
|
+
font-family: var(--rw-font-mono);
|
|
142
|
+
font-size: 13.5px;
|
|
143
|
+
color: var(--rw-on-surface-muted);
|
|
144
|
+
transition: color 0.15s;
|
|
145
|
+
}
|
|
146
|
+
.rw-ft-col a:hover,
|
|
147
|
+
:global(:where(.rw-ft) a:hover) {
|
|
148
|
+
color: var(--rw-primary);
|
|
149
|
+
}
|
|
150
|
+
.rw-ft-bottom {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-wrap: wrap;
|
|
153
|
+
gap: 8px 14px;
|
|
154
|
+
align-items: center;
|
|
155
|
+
margin-top: 44px;
|
|
156
|
+
padding: 16px 0;
|
|
157
|
+
border-top: 1px solid var(--rw-outline);
|
|
158
|
+
font-family: var(--rw-font-mono);
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
color: var(--rw-on-surface-faint);
|
|
161
|
+
}
|
|
162
|
+
.rw-ft-bottom :global(.sep) {
|
|
163
|
+
opacity: 0.4;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
+
import type { Component, Snippet } from "svelte";
|
|
5
6
|
import Modal from "./Modal.svelte";
|
|
6
7
|
import Button from "./Button.svelte";
|
|
7
|
-
|
|
8
|
-
type AlertVariant = "danger" | "warning" | "info";
|
|
8
|
+
import type { ButtonVariant } from "./button-styles.js";
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
open?: boolean;
|
|
12
12
|
title: string;
|
|
13
13
|
description?: string | undefined;
|
|
14
|
+
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
14
15
|
confirmLabel?: string;
|
|
15
16
|
cancelLabel?: string;
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
confirmVariant?: ButtonVariant;
|
|
18
|
+
confirmDisabled?: boolean;
|
|
19
|
+
confirmIcon?: Component<{ size?: number }>;
|
|
20
|
+
/** May be async: the dialog shows a spinner and closes once it resolves.
|
|
21
|
+
* If it throws, the dialog stays open so the caller can surface the error. */
|
|
22
|
+
onConfirm?: () => void | Promise<void>;
|
|
18
23
|
onCancel?: () => void;
|
|
19
|
-
|
|
24
|
+
children?: Snippet;
|
|
25
|
+
/** Replaces the default Cancel / Confirm buttons. */
|
|
26
|
+
footer?: Snippet;
|
|
20
27
|
class?: string;
|
|
21
28
|
}
|
|
22
29
|
|
|
@@ -24,40 +31,71 @@
|
|
|
24
31
|
open = $bindable(false),
|
|
25
32
|
title,
|
|
26
33
|
description,
|
|
34
|
+
size = "sm",
|
|
27
35
|
confirmLabel = "Confirm",
|
|
28
36
|
cancelLabel = "Cancel",
|
|
29
|
-
|
|
37
|
+
confirmVariant = "danger",
|
|
38
|
+
confirmDisabled = false,
|
|
39
|
+
confirmIcon: ConfirmIcon,
|
|
30
40
|
onConfirm,
|
|
31
41
|
onCancel,
|
|
32
|
-
|
|
42
|
+
children,
|
|
43
|
+
footer: footerProp,
|
|
33
44
|
class: className = "",
|
|
34
45
|
}: Props = $props();
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
let confirming = $state(false);
|
|
48
|
+
|
|
49
|
+
async function handleConfirm() {
|
|
50
|
+
if (confirming) return;
|
|
51
|
+
confirming = true;
|
|
52
|
+
try {
|
|
53
|
+
await onConfirm?.();
|
|
54
|
+
open = false;
|
|
55
|
+
} finally {
|
|
56
|
+
confirming = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
41
59
|
|
|
42
60
|
function handleCancel() {
|
|
61
|
+
if (confirming) return;
|
|
43
62
|
open = false;
|
|
44
63
|
onCancel?.();
|
|
45
64
|
}
|
|
46
|
-
|
|
47
|
-
function handleConfirm() {
|
|
48
|
-
onConfirm?.();
|
|
49
|
-
}
|
|
50
65
|
</script>
|
|
51
66
|
|
|
52
|
-
|
|
67
|
+
{#snippet confirmIconSnippet()}
|
|
68
|
+
{#if ConfirmIcon}<ConfirmIcon size={16} />{/if}
|
|
69
|
+
{/snippet}
|
|
70
|
+
|
|
71
|
+
<Modal
|
|
72
|
+
bind:open
|
|
73
|
+
{title}
|
|
74
|
+
{description}
|
|
75
|
+
{size}
|
|
76
|
+
closable={!confirming}
|
|
77
|
+
onClose={handleCancel}
|
|
78
|
+
{children}
|
|
79
|
+
class={className}
|
|
80
|
+
>
|
|
53
81
|
{#snippet footer()}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
{#if footerProp}
|
|
83
|
+
{@render footerProp()}
|
|
84
|
+
{:else}
|
|
85
|
+
<div class="flex items-center justify-end gap-3">
|
|
86
|
+
<Button variant="secondary" onclick={handleCancel} disabled={confirming}>
|
|
87
|
+
{cancelLabel}
|
|
88
|
+
</Button>
|
|
89
|
+
<Button
|
|
90
|
+
variant={confirmVariant}
|
|
91
|
+
loading={confirming}
|
|
92
|
+
disabled={confirming || confirmDisabled}
|
|
93
|
+
onclick={() => void handleConfirm()}
|
|
94
|
+
icon={ConfirmIcon ? confirmIconSnippet : undefined}
|
|
95
|
+
>
|
|
96
|
+
{confirmLabel}
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
{/if}
|
|
62
100
|
{/snippet}
|
|
63
101
|
</Modal>
|