@joewinke/jatui 0.26.0 → 0.27.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/CLAUDE.md +106 -1
- package/dist/actions/keyboardScope.d.ts +58 -0
- package/dist/actions/keyboardScope.js +110 -0
- package/dist/actions/listNav.d.ts +48 -3
- package/dist/actions/listNav.js +267 -77
- package/dist/actions/longpress.d.ts +37 -0
- package/dist/actions/longpress.js +105 -0
- package/dist/actions/stickToBottom.d.ts +66 -0
- package/dist/actions/stickToBottom.js +155 -0
- package/dist/actions/swipe.d.ts +64 -0
- package/dist/actions/swipe.js +179 -0
- package/dist/components/BottomSheet.svelte +280 -0
- package/dist/components/BottomSheet.svelte.d.ts +27 -0
- package/dist/components/ConfirmDialog.svelte +21 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
- package/dist/components/ConfirmModal.svelte +15 -0
- package/dist/components/ConfirmModal.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +18 -1
- package/dist/components/Drawer.svelte.d.ts +9 -1
- package/dist/components/InputDialog.svelte +21 -0
- package/dist/components/InputDialog.svelte.d.ts +11 -1
- package/dist/components/Modal.svelte +18 -1
- package/dist/components/Modal.svelte.d.ts +8 -0
- package/dist/components/basic/Empty.svelte +173 -9
- package/dist/components/basic/Empty.svelte.d.ts +50 -1
- package/dist/components/basic/ErrorState.svelte +128 -0
- package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
- package/dist/components/basic/JumpToLatest.svelte +60 -0
- package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
- package/dist/components/basic/PageHeader.svelte +216 -0
- package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
- package/dist/components/basic/ProgressButton.svelte +221 -0
- package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
- package/dist/components/graph/GraphApiBridge.svelte +36 -0
- package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
- package/dist/components/graph/GraphCanvas.svelte +212 -27
- package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
- package/dist/components/graph/api.d.ts +52 -0
- package/dist/components/graph/api.js +66 -0
- package/dist/components/graph/index.d.ts +2 -0
- package/dist/components/graph/index.js +1 -0
- package/dist/components/graph/theme.js +13 -0
- package/dist/components/messaging/MessageItem.svelte +5 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
- package/dist/components/table/DataTable.svelte +452 -78
- package/dist/components/table/DataTable.svelte.d.ts +2 -2
- package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
- package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
- package/dist/components/table/columnStorage.d.ts +29 -0
- package/dist/components/table/columnStorage.js +61 -0
- package/dist/components/table/groupRows.d.ts +72 -0
- package/dist/components/table/groupRows.js +118 -0
- package/dist/components/table/index.d.ts +7 -1
- package/dist/components/table/index.js +3 -0
- package/dist/components/table/types.d.ts +121 -20
- package/dist/index.d.ts +25 -2
- package/dist/index.js +32 -1
- package/dist/utils/ansiToHtml.d.ts +83 -0
- package/dist/utils/ansiToHtml.js +543 -0
- package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
- package/dist/utils/keyboardShortcutRegistry.js +1070 -76
- package/dist/utils/pollBackoff.d.ts +83 -0
- package/dist/utils/pollBackoff.js +151 -0
- package/dist/utils/snapFlick.d.ts +59 -0
- package/dist/utils/snapFlick.js +66 -0
- package/dist/video/advisors/qr.d.ts +1 -1
- package/dist/virtual/createRowWindow.d.ts +75 -0
- package/dist/virtual/createRowWindow.js +129 -0
- package/dist/virtual/index.d.ts +4 -0
- package/dist/virtual/index.js +5 -0
- package/dist/virtual/rowWindowCore.d.ts +202 -0
- package/dist/virtual/rowWindowCore.js +510 -0
- package/package.json +11 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PageHeader — the STANDARD route header (title + meta + actions).
|
|
4
|
+
* Upstreamed from the JAT IDE `ui/PageHeader.svelte` (adoption audit U7,
|
|
5
|
+
* jatui-rl7w6.5). The audit scored page headers 3/10 for consistency:
|
|
6
|
+
* title sizes ranged from `text-sm` to `text-2xl`, weights and spacing all
|
|
7
|
+
* varied, few had a place for actions or a subtitle. This standardizes on
|
|
8
|
+
* JAT's dominant operator pattern — the small, uppercase, letter-spaced
|
|
9
|
+
* "instrument label" — and gives it slots for a count chip, a subtitle, an
|
|
10
|
+
* actions cluster, and a metrics/meta strip.
|
|
11
|
+
*
|
|
12
|
+
* Layering rules: all color flows through `var(--color-*)` DaisyUI tokens
|
|
13
|
+
* feeding the component's own classes, so it themes cleanly; no
|
|
14
|
+
* DaisyUI class names in the public API.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <PageHeader title="Sessions" count={activeCount} subtitle="tmux + agents">
|
|
18
|
+
* {#snippet actions()}
|
|
19
|
+
* <Button size="sm" onclick={create}>New</Button>
|
|
20
|
+
* {/snippet}
|
|
21
|
+
* </PageHeader>
|
|
22
|
+
*
|
|
23
|
+
* <PageHeader title="Task History" sticky>
|
|
24
|
+
* {#snippet meta()}<Sparkline data={load} />{/snippet}
|
|
25
|
+
* {#snippet children()}<FilterBar … />{/snippet}
|
|
26
|
+
* </PageHeader>
|
|
27
|
+
*/
|
|
28
|
+
import type { Snippet } from 'svelte';
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
title,
|
|
32
|
+
subtitle = '',
|
|
33
|
+
count,
|
|
34
|
+
size = 'md',
|
|
35
|
+
sticky = false,
|
|
36
|
+
animate = false,
|
|
37
|
+
icon,
|
|
38
|
+
actions,
|
|
39
|
+
meta,
|
|
40
|
+
children
|
|
41
|
+
}: {
|
|
42
|
+
/** Route title. Rendered uppercase / tracked per the JAT page-title convention. */
|
|
43
|
+
title: string;
|
|
44
|
+
/** Optional secondary line under the title (lowercase, dimmer). */
|
|
45
|
+
subtitle?: string;
|
|
46
|
+
/** Optional count chip beside the title (e.g. number of rows). */
|
|
47
|
+
count?: number | string;
|
|
48
|
+
/** `md` = standard instrument label; `lg` = larger title for landing/first-run screens. */
|
|
49
|
+
size?: 'md' | 'lg';
|
|
50
|
+
/** Stick the header to the top of the scroll container. */
|
|
51
|
+
sticky?: boolean;
|
|
52
|
+
/** Play the letter-tracking entrance on the title (gated for reduced motion). */
|
|
53
|
+
animate?: boolean;
|
|
54
|
+
/** Optional leading icon snippet. */
|
|
55
|
+
icon?: Snippet;
|
|
56
|
+
/** Right-aligned action cluster (buttons, filters). */
|
|
57
|
+
actions?: Snippet;
|
|
58
|
+
/** Metrics / instrument strip rendered on the title row's trailing edge or below. */
|
|
59
|
+
meta?: Snippet;
|
|
60
|
+
/** Content rendered below the header row (e.g. a filter bar). */
|
|
61
|
+
children?: Snippet;
|
|
62
|
+
} = $props();
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
class="page-header"
|
|
67
|
+
class:page-header--sticky={sticky}
|
|
68
|
+
class:page-header--lg={size === 'lg'}
|
|
69
|
+
>
|
|
70
|
+
<div class="page-header__bar">
|
|
71
|
+
<div class="page-header__lead">
|
|
72
|
+
{#if icon}
|
|
73
|
+
<span class="page-header__icon text-base-content/60">{@render icon()}</span>
|
|
74
|
+
{/if}
|
|
75
|
+
<h1 class="page-header__title" class:page-header__title--animate={animate}>{title}</h1>
|
|
76
|
+
{#if count !== undefined && count !== null}
|
|
77
|
+
<span class="page-header__count">{count}</span>
|
|
78
|
+
{/if}
|
|
79
|
+
{#if subtitle}
|
|
80
|
+
<span class="page-header__subtitle">{subtitle}</span>
|
|
81
|
+
{/if}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{#if meta}
|
|
85
|
+
<div class="page-header__meta">{@render meta()}</div>
|
|
86
|
+
{/if}
|
|
87
|
+
|
|
88
|
+
{#if actions}
|
|
89
|
+
<div class="page-header__actions">{@render actions()}</div>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{#if children}
|
|
94
|
+
<div class="page-header__below">{@render children()}</div>
|
|
95
|
+
{/if}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
.page-header {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: var(--jatui-space-2xl, 24px);
|
|
103
|
+
padding: var(--jatui-space-xl, 16px) 1rem;
|
|
104
|
+
}
|
|
105
|
+
.page-header--sticky {
|
|
106
|
+
position: sticky;
|
|
107
|
+
top: 0;
|
|
108
|
+
z-index: 30; /* page-level sticky header */
|
|
109
|
+
background: var(--color-base-100);
|
|
110
|
+
border-bottom: 1px solid color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.page-header__bar {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: var(--jatui-space-xl, 16px);
|
|
117
|
+
min-width: 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.page-header__lead {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: baseline;
|
|
123
|
+
gap: 0.625rem;
|
|
124
|
+
min-width: 0;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.page-header__icon {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-self: center;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* The canonical JAT page-title: small, uppercase, tracked instrument label. */
|
|
134
|
+
.page-header__title {
|
|
135
|
+
font-size: 0.875rem;
|
|
136
|
+
font-weight: 700;
|
|
137
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
138
|
+
letter-spacing: 0.1em;
|
|
139
|
+
text-transform: uppercase;
|
|
140
|
+
margin: 0;
|
|
141
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
}
|
|
144
|
+
.page-header--lg .page-header__title {
|
|
145
|
+
font-size: 1.25rem;
|
|
146
|
+
letter-spacing: 0.04em;
|
|
147
|
+
color: color-mix(in oklch, var(--color-base-content) 85%, transparent);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.page-header__title--animate {
|
|
151
|
+
animation: page-header-tracking-in 0.7s cubic-bezier(0.215, 0.61, 0.355, 1) both;
|
|
152
|
+
}
|
|
153
|
+
@keyframes page-header-tracking-in {
|
|
154
|
+
0% {
|
|
155
|
+
letter-spacing: -0.5em;
|
|
156
|
+
opacity: 0;
|
|
157
|
+
}
|
|
158
|
+
40% {
|
|
159
|
+
opacity: 0.6;
|
|
160
|
+
}
|
|
161
|
+
100% {
|
|
162
|
+
opacity: 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.page-header__count {
|
|
167
|
+
font-family: ui-monospace, monospace;
|
|
168
|
+
font-size: 0.7rem;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
line-height: 1;
|
|
171
|
+
padding: 0.2rem 0.45rem;
|
|
172
|
+
border-radius: var(--jatui-radius-selector, 999px);
|
|
173
|
+
color: var(--color-primary);
|
|
174
|
+
background: color-mix(in oklch, var(--color-primary) 14%, transparent);
|
|
175
|
+
border: 1px solid color-mix(in oklch, var(--color-primary) 30%, transparent);
|
|
176
|
+
align-self: center;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.page-header__subtitle {
|
|
180
|
+
font-size: 0.75rem;
|
|
181
|
+
color: color-mix(in oklch, var(--color-base-content) 42%, transparent);
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
text-overflow: ellipsis;
|
|
185
|
+
min-width: 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.page-header__meta {
|
|
189
|
+
margin-left: auto;
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
min-width: 0;
|
|
193
|
+
}
|
|
194
|
+
/* When both meta and actions exist, meta keeps the auto margin and actions
|
|
195
|
+
sit at the trailing edge. When only actions exist, actions take the margin. */
|
|
196
|
+
.page-header__meta ~ .page-header__actions {
|
|
197
|
+
margin-left: var(--jatui-space-xl, 16px);
|
|
198
|
+
}
|
|
199
|
+
.page-header__actions {
|
|
200
|
+
margin-left: auto;
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 0.5rem;
|
|
204
|
+
flex-shrink: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.page-header__below {
|
|
208
|
+
min-width: 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@media (prefers-reduced-motion: reduce) {
|
|
212
|
+
.page-header__title--animate {
|
|
213
|
+
animation: none;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageHeader — the STANDARD route header (title + meta + actions).
|
|
3
|
+
* Upstreamed from the JAT IDE `ui/PageHeader.svelte` (adoption audit U7,
|
|
4
|
+
* jatui-rl7w6.5). The audit scored page headers 3/10 for consistency:
|
|
5
|
+
* title sizes ranged from `text-sm` to `text-2xl`, weights and spacing all
|
|
6
|
+
* varied, few had a place for actions or a subtitle. This standardizes on
|
|
7
|
+
* JAT's dominant operator pattern — the small, uppercase, letter-spaced
|
|
8
|
+
* "instrument label" — and gives it slots for a count chip, a subtitle, an
|
|
9
|
+
* actions cluster, and a metrics/meta strip.
|
|
10
|
+
*
|
|
11
|
+
* Layering rules: all color flows through `var(--color-*)` DaisyUI tokens
|
|
12
|
+
* feeding the component's own classes, so it themes cleanly; no
|
|
13
|
+
* DaisyUI class names in the public API.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <PageHeader title="Sessions" count={activeCount} subtitle="tmux + agents">
|
|
17
|
+
* {#snippet actions()}
|
|
18
|
+
* <Button size="sm" onclick={create}>New</Button>
|
|
19
|
+
* {/snippet}
|
|
20
|
+
* </PageHeader>
|
|
21
|
+
*
|
|
22
|
+
* <PageHeader title="Task History" sticky>
|
|
23
|
+
* {#snippet meta()}<Sparkline data={load} />{/snippet}
|
|
24
|
+
* {#snippet children()}<FilterBar … />{/snippet}
|
|
25
|
+
* </PageHeader>
|
|
26
|
+
*/
|
|
27
|
+
import type { Snippet } from 'svelte';
|
|
28
|
+
type $$ComponentProps = {
|
|
29
|
+
/** Route title. Rendered uppercase / tracked per the JAT page-title convention. */
|
|
30
|
+
title: string;
|
|
31
|
+
/** Optional secondary line under the title (lowercase, dimmer). */
|
|
32
|
+
subtitle?: string;
|
|
33
|
+
/** Optional count chip beside the title (e.g. number of rows). */
|
|
34
|
+
count?: number | string;
|
|
35
|
+
/** `md` = standard instrument label; `lg` = larger title for landing/first-run screens. */
|
|
36
|
+
size?: 'md' | 'lg';
|
|
37
|
+
/** Stick the header to the top of the scroll container. */
|
|
38
|
+
sticky?: boolean;
|
|
39
|
+
/** Play the letter-tracking entrance on the title (gated for reduced motion). */
|
|
40
|
+
animate?: boolean;
|
|
41
|
+
/** Optional leading icon snippet. */
|
|
42
|
+
icon?: Snippet;
|
|
43
|
+
/** Right-aligned action cluster (buttons, filters). */
|
|
44
|
+
actions?: Snippet;
|
|
45
|
+
/** Metrics / instrument strip rendered on the title row's trailing edge or below. */
|
|
46
|
+
meta?: Snippet;
|
|
47
|
+
/** Content rendered below the header row (e.g. a filter bar). */
|
|
48
|
+
children?: Snippet;
|
|
49
|
+
};
|
|
50
|
+
declare const PageHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
51
|
+
type PageHeader = ReturnType<typeof PageHeader>;
|
|
52
|
+
export default PageHeader;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ProgressButton — a button that narrates a multi-step async action inline.
|
|
4
|
+
* Upstreamed from the JAT IDE `ui/ProgressButton.svelte` (adoption audit
|
|
5
|
+
* U7, jatui-rl7w6.5).
|
|
6
|
+
*
|
|
7
|
+
* The "advanced dev UX" button: instead of a single generic spinner, the
|
|
8
|
+
* button walks through the *named phases* of the work it kicked off
|
|
9
|
+
* (e.g. analyzing → creating → spawning → closing), showing the current
|
|
10
|
+
* phase label, an animated fill that tracks progress across the phases, and
|
|
11
|
+
* a brief success flash when it lands. Use it anywhere a click triggers a
|
|
12
|
+
* sequence of steps the user would otherwise stare at a blank spinner for.
|
|
13
|
+
*
|
|
14
|
+
* Idle → renders `idleLabel` (or the `children` snippet).
|
|
15
|
+
* Busy → set `phase` to one of `phases[].key`; shows spinner + that phase's
|
|
16
|
+
* label and fills to (index+1)/phases.length. The button disables
|
|
17
|
+
* itself while a phase is active so it can't double-fire.
|
|
18
|
+
* Done → set `success` true for a green check-flash; clear `phase` + reset
|
|
19
|
+
* `success` (typically in your form reset) to return to idle.
|
|
20
|
+
*
|
|
21
|
+
* Drives only presentation — the caller owns the state machine:
|
|
22
|
+
*
|
|
23
|
+
* <ProgressButton
|
|
24
|
+
* idleLabel="Save & Start"
|
|
25
|
+
* {phase} // 'analyzing' | 'creating' | … | null
|
|
26
|
+
* phases={SUBMIT_PHASES} // [{ key, label }] in order
|
|
27
|
+
* {success}
|
|
28
|
+
* onclick={submit}
|
|
29
|
+
* />
|
|
30
|
+
*
|
|
31
|
+
* Renders through the shared `Button` (DaisyUI `btn`), so `class`
|
|
32
|
+
* (e.g. `join-item font-mono`) composes with split-button / join patterns.
|
|
33
|
+
*/
|
|
34
|
+
import type { Snippet } from 'svelte';
|
|
35
|
+
import Button from '../Button.svelte';
|
|
36
|
+
|
|
37
|
+
type Phase = { key: string; label: string };
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
phase = null,
|
|
41
|
+
phases = [],
|
|
42
|
+
idleLabel = '',
|
|
43
|
+
success = false,
|
|
44
|
+
successLabel = 'Done',
|
|
45
|
+
variant = 'primary',
|
|
46
|
+
size = 'md',
|
|
47
|
+
disabled = false,
|
|
48
|
+
type = 'button',
|
|
49
|
+
class: klass = '',
|
|
50
|
+
onclick,
|
|
51
|
+
children
|
|
52
|
+
}: {
|
|
53
|
+
/** Current active phase key (must match a `phases[].key`); `null` = idle. */
|
|
54
|
+
phase?: string | null;
|
|
55
|
+
/** Ordered phases — drives the fill width and the active label lookup. */
|
|
56
|
+
phases?: Phase[];
|
|
57
|
+
/** Text shown when idle (used if no `children` snippet is provided). */
|
|
58
|
+
idleLabel?: string;
|
|
59
|
+
/** Flash a success state (green fill + check) over the button. */
|
|
60
|
+
success?: boolean;
|
|
61
|
+
/** Label shown alongside the check during the success flash. */
|
|
62
|
+
successLabel?: string;
|
|
63
|
+
/** Button color (the shared Button variants, minus outline/ghost which hide the fill). */
|
|
64
|
+
variant?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral';
|
|
65
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
66
|
+
/** Disable from the outside; the button also self-disables while busy. */
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
type?: 'button' | 'submit';
|
|
69
|
+
/** Extra classes merged after the base `btn` classes (e.g. `join-item`). */
|
|
70
|
+
class?: string;
|
|
71
|
+
onclick?: (e: MouseEvent) => void;
|
|
72
|
+
/** Idle content; overrides `idleLabel` when provided. */
|
|
73
|
+
children?: Snippet;
|
|
74
|
+
} = $props();
|
|
75
|
+
|
|
76
|
+
const SPIN = { xs: 'loading-xs', sm: 'loading-xs', md: 'loading-sm', lg: 'loading-md' } as const;
|
|
77
|
+
|
|
78
|
+
// Presentation-only busy state: the button self-disables while a phase is
|
|
79
|
+
// active (the shared Button carries its own spinner for a `state="loading"`
|
|
80
|
+
// submit flow, so we never use that here — ProgressButton owns the visuals).
|
|
81
|
+
const isBusy = $derived(phase != null);
|
|
82
|
+
const activeIndex = $derived(phase != null ? phases.findIndex((p) => p.key === phase) : -1);
|
|
83
|
+
const currentLabel = $derived(activeIndex >= 0 ? phases[activeIndex].label : (phase ?? ''));
|
|
84
|
+
// Fraction of the bar to fill: complete on success, (index+1)/N while a known
|
|
85
|
+
// phase is active, a small head-start for an unlisted phase, else empty.
|
|
86
|
+
const progress = $derived(
|
|
87
|
+
success
|
|
88
|
+
? 1
|
|
89
|
+
: activeIndex >= 0 && phases.length > 0
|
|
90
|
+
? (activeIndex + 1) / phases.length
|
|
91
|
+
: isBusy
|
|
92
|
+
? 0.35
|
|
93
|
+
: 0
|
|
94
|
+
);
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<Button
|
|
98
|
+
{type}
|
|
99
|
+
{variant}
|
|
100
|
+
{size}
|
|
101
|
+
class="pb {klass}"
|
|
102
|
+
disabled={disabled || isBusy}
|
|
103
|
+
aria-busy={isBusy}
|
|
104
|
+
{onclick}
|
|
105
|
+
>
|
|
106
|
+
<span class="pb-fill" style="width: {progress * 100}%" aria-hidden="true"></span>
|
|
107
|
+
<span class="pb-content" aria-live="polite">
|
|
108
|
+
{#if success}
|
|
109
|
+
<svg class="pb-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
110
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
|
111
|
+
</svg>
|
|
112
|
+
<span>{successLabel}</span>
|
|
113
|
+
{:else if isBusy}
|
|
114
|
+
<span class="loading loading-spinner {SPIN[size]}" aria-hidden="true"></span>
|
|
115
|
+
<span class="pb-shimmer">{currentLabel}</span>
|
|
116
|
+
{:else if children}
|
|
117
|
+
{@render children()}
|
|
118
|
+
{:else}
|
|
119
|
+
{idleLabel}
|
|
120
|
+
{/if}
|
|
121
|
+
</span>
|
|
122
|
+
</Button>
|
|
123
|
+
|
|
124
|
+
<style>
|
|
125
|
+
.pb {
|
|
126
|
+
position: relative;
|
|
127
|
+
overflow: hidden;
|
|
128
|
+
isolation: isolate;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Keep the label/spinner above the animated fill. */
|
|
132
|
+
.pb-content {
|
|
133
|
+
position: relative;
|
|
134
|
+
z-index: 1;
|
|
135
|
+
display: inline-flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 0.4rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Translucent bar that grows left→right as phases advance, lightening the
|
|
141
|
+
button so it reads as "filling up". currentColor = the button's content
|
|
142
|
+
color, so it stays theme- and variant-correct. */
|
|
143
|
+
.pb-fill {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0 auto 0 0;
|
|
146
|
+
z-index: 0;
|
|
147
|
+
width: 0;
|
|
148
|
+
background: color-mix(in oklch, currentColor 20%, transparent);
|
|
149
|
+
transition: width 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
150
|
+
pointer-events: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Phase label shimmer (self-contained: no host app.css dependency). */
|
|
154
|
+
.pb-shimmer {
|
|
155
|
+
color: transparent;
|
|
156
|
+
background-image:
|
|
157
|
+
linear-gradient(90deg, transparent calc(50% - 30px), oklch(1 0.05 200), transparent calc(50% + 30px)),
|
|
158
|
+
linear-gradient(oklch(0.85 0.02 250), oklch(0.85 0.02 250));
|
|
159
|
+
background-size: 300px 100%, 100% 100%;
|
|
160
|
+
background-repeat: no-repeat;
|
|
161
|
+
-webkit-background-clip: text;
|
|
162
|
+
background-clip: text;
|
|
163
|
+
animation: pb-shimmer-slide 3s linear infinite;
|
|
164
|
+
}
|
|
165
|
+
@keyframes pb-shimmer-slide {
|
|
166
|
+
0% {
|
|
167
|
+
background-position: -150px center, 0 0;
|
|
168
|
+
}
|
|
169
|
+
100% {
|
|
170
|
+
background-position: calc(100% + 150px) center, 0 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Success flash: fill snaps to a full green wash regardless of variant,
|
|
175
|
+
recolors the content to match, and pops once. */
|
|
176
|
+
.pb-success .pb-fill,
|
|
177
|
+
.pb:has(.pb-check) .pb-fill {
|
|
178
|
+
background: var(--color-success);
|
|
179
|
+
}
|
|
180
|
+
.pb:has(.pb-check) .pb-content {
|
|
181
|
+
color: var(--color-success-content);
|
|
182
|
+
animation: pb-pop 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
183
|
+
}
|
|
184
|
+
.pb-check {
|
|
185
|
+
width: 1rem;
|
|
186
|
+
height: 1rem;
|
|
187
|
+
animation: pb-check-in 0.28s ease-out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@keyframes pb-pop {
|
|
191
|
+
0% {
|
|
192
|
+
transform: scale(1);
|
|
193
|
+
}
|
|
194
|
+
45% {
|
|
195
|
+
transform: scale(1.04);
|
|
196
|
+
}
|
|
197
|
+
100% {
|
|
198
|
+
transform: scale(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
@keyframes pb-check-in {
|
|
202
|
+
0% {
|
|
203
|
+
transform: scale(0.4);
|
|
204
|
+
opacity: 0;
|
|
205
|
+
}
|
|
206
|
+
100% {
|
|
207
|
+
transform: scale(1);
|
|
208
|
+
opacity: 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@media (prefers-reduced-motion: reduce) {
|
|
213
|
+
.pb-fill,
|
|
214
|
+
.pb-content,
|
|
215
|
+
.pb-check,
|
|
216
|
+
.pb-shimmer {
|
|
217
|
+
transition: none;
|
|
218
|
+
animation: none;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressButton — a button that narrates a multi-step async action inline.
|
|
3
|
+
* Upstreamed from the JAT IDE `ui/ProgressButton.svelte` (adoption audit
|
|
4
|
+
* U7, jatui-rl7w6.5).
|
|
5
|
+
*
|
|
6
|
+
* The "advanced dev UX" button: instead of a single generic spinner, the
|
|
7
|
+
* button walks through the *named phases* of the work it kicked off
|
|
8
|
+
* (e.g. analyzing → creating → spawning → closing), showing the current
|
|
9
|
+
* phase label, an animated fill that tracks progress across the phases, and
|
|
10
|
+
* a brief success flash when it lands. Use it anywhere a click triggers a
|
|
11
|
+
* sequence of steps the user would otherwise stare at a blank spinner for.
|
|
12
|
+
*
|
|
13
|
+
* Idle → renders `idleLabel` (or the `children` snippet).
|
|
14
|
+
* Busy → set `phase` to one of `phases[].key`; shows spinner + that phase's
|
|
15
|
+
* label and fills to (index+1)/phases.length. The button disables
|
|
16
|
+
* itself while a phase is active so it can't double-fire.
|
|
17
|
+
* Done → set `success` true for a green check-flash; clear `phase` + reset
|
|
18
|
+
* `success` (typically in your form reset) to return to idle.
|
|
19
|
+
*
|
|
20
|
+
* Drives only presentation — the caller owns the state machine:
|
|
21
|
+
*
|
|
22
|
+
* <ProgressButton
|
|
23
|
+
* idleLabel="Save & Start"
|
|
24
|
+
* {phase} // 'analyzing' | 'creating' | … | null
|
|
25
|
+
* phases={SUBMIT_PHASES} // [{ key, label }] in order
|
|
26
|
+
* {success}
|
|
27
|
+
* onclick={submit}
|
|
28
|
+
* />
|
|
29
|
+
*
|
|
30
|
+
* Renders through the shared `Button` (DaisyUI `btn`), so `class`
|
|
31
|
+
* (e.g. `join-item font-mono`) composes with split-button / join patterns.
|
|
32
|
+
*/
|
|
33
|
+
import type { Snippet } from 'svelte';
|
|
34
|
+
type Phase = {
|
|
35
|
+
key: string;
|
|
36
|
+
label: string;
|
|
37
|
+
};
|
|
38
|
+
type $$ComponentProps = {
|
|
39
|
+
/** Current active phase key (must match a `phases[].key`); `null` = idle. */
|
|
40
|
+
phase?: string | null;
|
|
41
|
+
/** Ordered phases — drives the fill width and the active label lookup. */
|
|
42
|
+
phases?: Phase[];
|
|
43
|
+
/** Text shown when idle (used if no `children` snippet is provided). */
|
|
44
|
+
idleLabel?: string;
|
|
45
|
+
/** Flash a success state (green fill + check) over the button. */
|
|
46
|
+
success?: boolean;
|
|
47
|
+
/** Label shown alongside the check during the success flash. */
|
|
48
|
+
successLabel?: string;
|
|
49
|
+
/** Button color (the shared Button variants, minus outline/ghost which hide the fill). */
|
|
50
|
+
variant?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral';
|
|
51
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
52
|
+
/** Disable from the outside; the button also self-disables while busy. */
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
type?: 'button' | 'submit';
|
|
55
|
+
/** Extra classes merged after the base `btn` classes (e.g. `join-item`). */
|
|
56
|
+
class?: string;
|
|
57
|
+
onclick?: (e: MouseEvent) => void;
|
|
58
|
+
/** Idle content; overrides `idleLabel` when provided. */
|
|
59
|
+
children?: Snippet;
|
|
60
|
+
};
|
|
61
|
+
declare const ProgressButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
62
|
+
type ProgressButton = ReturnType<typeof ProgressButton>;
|
|
63
|
+
export default ProgressButton;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Internal: rendered inside <SvelteFlow> so useSvelteFlow()/useViewport()
|
|
3
|
+
// resolve. Hands the helpers up to GraphCanvas and mirrors the live
|
|
4
|
+
// viewport out (xyflow only writes a bound viewport back when the caller
|
|
5
|
+
// seeded one, so an initially-undefined bind:viewport would never update).
|
|
6
|
+
import { useStore, useSvelteFlow, useViewport, type Viewport } from '@xyflow/svelte'
|
|
7
|
+
import { zoomViewportAround, type GraphFlowHelpers } from './api.js'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
onready: (helpers: GraphFlowHelpers | null) => void
|
|
11
|
+
onviewport: (viewport: Viewport) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { onready, onviewport }: Props = $props()
|
|
15
|
+
|
|
16
|
+
const flow = useSvelteFlow()
|
|
17
|
+
const live = useViewport()
|
|
18
|
+
const store = useStore()
|
|
19
|
+
|
|
20
|
+
$effect(() => {
|
|
21
|
+
onready({
|
|
22
|
+
fitView: (options) => flow.fitView(options),
|
|
23
|
+
zoomTo: (zoom, options) =>
|
|
24
|
+
flow.setViewport(zoomViewportAround(flow.getViewport(), zoom, store.width, store.height), options),
|
|
25
|
+
getZoom: () => flow.getZoom(),
|
|
26
|
+
setCenter: (x, y, options) => flow.setCenter(x, y, options),
|
|
27
|
+
screenToFlowPosition: (p) => flow.screenToFlowPosition(p),
|
|
28
|
+
getViewport: () => flow.getViewport(),
|
|
29
|
+
})
|
|
30
|
+
return () => onready(null)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
onviewport(live.current)
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Viewport } from '@xyflow/svelte';
|
|
2
|
+
import { type GraphFlowHelpers } from './api.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
onready: (helpers: GraphFlowHelpers | null) => void;
|
|
5
|
+
onviewport: (viewport: Viewport) => void;
|
|
6
|
+
}
|
|
7
|
+
declare const GraphApiBridge: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type GraphApiBridge = ReturnType<typeof GraphApiBridge>;
|
|
9
|
+
export default GraphApiBridge;
|