@lyeve-labs/ui-kit 0.42.2 → 0.44.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/README.md +5 -5
- package/dist/components/Alert.svelte +44 -8
- package/dist/components/Alert.svelte.d.ts +8 -0
- package/dist/components/Banner.svelte +41 -6
- package/dist/components/Banner.svelte.d.ts +8 -0
- package/dist/components/ColorField.svelte +221 -0
- package/dist/components/ColorField.svelte.d.ts +38 -0
- package/dist/components/CopyButton.svelte +8 -2
- package/dist/components/CopyField.svelte +4 -4
- package/dist/components/DiffView.svelte +152 -0
- package/dist/components/DiffView.svelte.d.ts +27 -0
- package/dist/components/FormMessage.svelte +11 -5
- package/dist/components/Logo.svelte +1 -1
- package/dist/components/SortableList.svelte +520 -0
- package/dist/components/SortableList.svelte.d.ts +57 -0
- package/dist/components/Tabs.svelte +9 -5
- package/dist/components/Toaster.svelte +2 -2
- package/dist/index.d.ts +7 -1
- package/dist/index.js +4 -1
- package/dist/internal/reorder.d.ts +50 -0
- package/dist/internal/reorder.js +58 -0
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/** One difference between two versions, named by the field it is in. */
|
|
3
|
+
export type DiffChange =
|
|
4
|
+
| { kind: 'added'; label: string; value: string }
|
|
5
|
+
| { kind: 'removed'; label: string; value: string }
|
|
6
|
+
| { kind: 'changed'; label: string; before: string; after: string }
|
|
7
|
+
| {
|
|
8
|
+
kind: 'moved';
|
|
9
|
+
label: string;
|
|
10
|
+
/** Where it moved, in the caller's words, such as "From position 2 to 4". */
|
|
11
|
+
value: string;
|
|
12
|
+
};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
/**
|
|
17
|
+
* What changed between two versions, one row per field.
|
|
18
|
+
*
|
|
19
|
+
* Each row names its field and says what happened to it in a word: Added,
|
|
20
|
+
* Removed, Changed or Moved, so the kind of change never rests on color. A
|
|
21
|
+
* changed field shows the old value marked Removed above the new one marked
|
|
22
|
+
* Added. Every value takes its direction from its own text, and a value
|
|
23
|
+
* past 400 characters shows its start with a Show all button, cut between
|
|
24
|
+
* two characters a reader sees as whole, so an emoji or an accent is never
|
|
25
|
+
* split.
|
|
26
|
+
*
|
|
27
|
+
* The kit draws the list. Working out what changed is the caller's.
|
|
28
|
+
*/
|
|
29
|
+
import { cn } from '../utils/cn.js';
|
|
30
|
+
import type { AccentTone } from '../internal/tone.js';
|
|
31
|
+
import Badge from './Badge.svelte';
|
|
32
|
+
import Button from './Button.svelte';
|
|
33
|
+
|
|
34
|
+
interface Props {
|
|
35
|
+
changes: DiffChange[];
|
|
36
|
+
class?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let { changes, class: klass = '' }: Props = $props();
|
|
40
|
+
|
|
41
|
+
/** How many characters of a value show before Show all. */
|
|
42
|
+
const CLAMP = 400;
|
|
43
|
+
|
|
44
|
+
const KIND: Record<DiffChange['kind'], { word: string; tone: AccentTone }> = {
|
|
45
|
+
added: { word: 'Added', tone: 'success' },
|
|
46
|
+
removed: { word: 'Removed', tone: 'danger' },
|
|
47
|
+
changed: { word: 'Changed', tone: 'warn' },
|
|
48
|
+
moved: { word: 'Moved', tone: 'brand' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The values a reader opened past the clamp, by row and side, and the
|
|
53
|
+
* changes they were opened in. Other changes start folded, because a row's
|
|
54
|
+
* position there names another value.
|
|
55
|
+
*/
|
|
56
|
+
let opened = $state.raw<{ changes: DiffChange[]; keys: Set<string> } | null>(null);
|
|
57
|
+
|
|
58
|
+
function isOpen(key: string): boolean {
|
|
59
|
+
return opened?.changes === changes && opened.keys.has(key);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toggle(key: string) {
|
|
63
|
+
const keys = new Set(opened?.changes === changes ? opened.keys : []);
|
|
64
|
+
if (!keys.delete(key)) keys.add(key);
|
|
65
|
+
opened = { changes, keys };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The first CLAMP characters a reader sees as whole, or null when the text
|
|
70
|
+
* is no longer than that. A string no longer in code units than the clamp
|
|
71
|
+
* cannot hold more characters, so only a long one is segmented.
|
|
72
|
+
*/
|
|
73
|
+
function clip(text: string): string | null {
|
|
74
|
+
if (text.length <= CLAMP) return null;
|
|
75
|
+
let seen = 0;
|
|
76
|
+
for (const { index } of new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(
|
|
77
|
+
text,
|
|
78
|
+
)) {
|
|
79
|
+
if (seen === CLAMP) return text.slice(0, index);
|
|
80
|
+
seen++;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<!-- `what` names the value in its button, so a list of buttons tells each
|
|
87
|
+
Show all apart. -->
|
|
88
|
+
{#snippet shown(key: string, text: string, tint: string, what: string)}
|
|
89
|
+
{@const head = clip(text)}
|
|
90
|
+
{@const full = isOpen(key)}
|
|
91
|
+
<div class="min-w-0">
|
|
92
|
+
{#if text === ''}
|
|
93
|
+
<p class="text-faint italic">Empty</p>
|
|
94
|
+
{:else}
|
|
95
|
+
<p
|
|
96
|
+
dir="auto"
|
|
97
|
+
class={cn(
|
|
98
|
+
'rounded-md whitespace-pre-wrap text-fg [overflow-wrap:anywhere]',
|
|
99
|
+
tint && `px-2 py-1 ${tint}`,
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{head === null || full ? text : head}
|
|
103
|
+
</p>
|
|
104
|
+
{#if head !== null}
|
|
105
|
+
<Button
|
|
106
|
+
variant="ghost"
|
|
107
|
+
size="sm"
|
|
108
|
+
class="mt-1"
|
|
109
|
+
aria-label="{full ? 'Show less' : 'Show all'} of {what}"
|
|
110
|
+
aria-expanded={full}
|
|
111
|
+
onclick={() => toggle(key)}
|
|
112
|
+
>
|
|
113
|
+
{full ? 'Show less' : 'Show all'}
|
|
114
|
+
</Button>
|
|
115
|
+
{/if}
|
|
116
|
+
{/if}
|
|
117
|
+
</div>
|
|
118
|
+
{/snippet}
|
|
119
|
+
|
|
120
|
+
<div class={cn('text-sm', klass)}>
|
|
121
|
+
{#if changes.length === 0}
|
|
122
|
+
<p class="text-muted">These versions are the same.</p>
|
|
123
|
+
{:else}
|
|
124
|
+
<ul class="divide-y divide-line">
|
|
125
|
+
{#each changes as change, i (i)}
|
|
126
|
+
<li class="flex flex-col gap-2 py-3 first:pt-0 last:pb-0">
|
|
127
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
128
|
+
<span dir="auto" class="min-w-0 font-medium text-fg [overflow-wrap:anywhere]">
|
|
129
|
+
{change.label}
|
|
130
|
+
</span>
|
|
131
|
+
<Badge tone={KIND[change.kind].tone}>{KIND[change.kind].word}</Badge>
|
|
132
|
+
</div>
|
|
133
|
+
{#if change.kind === 'changed'}
|
|
134
|
+
<div class="grid grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-3 gap-y-1.5">
|
|
135
|
+
<span class="text-xs font-medium text-muted">Removed</span>
|
|
136
|
+
{@render shown(
|
|
137
|
+
`${i}-before`,
|
|
138
|
+
change.before,
|
|
139
|
+
'bg-danger/10',
|
|
140
|
+
`${change.label}, removed`,
|
|
141
|
+
)}
|
|
142
|
+
<span class="text-xs font-medium text-muted">Added</span>
|
|
143
|
+
{@render shown(`${i}-after`, change.after, 'bg-success/10', `${change.label}, added`)}
|
|
144
|
+
</div>
|
|
145
|
+
{:else}
|
|
146
|
+
{@render shown(`${i}`, change.value, '', change.label)}
|
|
147
|
+
{/if}
|
|
148
|
+
</li>
|
|
149
|
+
{/each}
|
|
150
|
+
</ul>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** One difference between two versions, named by the field it is in. */
|
|
2
|
+
export type DiffChange = {
|
|
3
|
+
kind: 'added';
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'removed';
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'changed';
|
|
12
|
+
label: string;
|
|
13
|
+
before: string;
|
|
14
|
+
after: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'moved';
|
|
17
|
+
label: string;
|
|
18
|
+
/** Where it moved, in the caller's words, such as "From position 2 to 4". */
|
|
19
|
+
value: string;
|
|
20
|
+
};
|
|
21
|
+
interface Props {
|
|
22
|
+
changes: DiffChange[];
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
declare const DiffView: import("svelte").Component<Props, {}, "">;
|
|
26
|
+
type DiffView = ReturnType<typeof DiffView>;
|
|
27
|
+
export default DiffView;
|
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
*
|
|
35
35
|
* A submit outcome has no such permanent host, because the page mounts this
|
|
36
36
|
* component at the moment the submit resolves. So the region goes in empty
|
|
37
|
-
* and the message is placed inside it
|
|
38
|
-
* shape of change a toast makes.
|
|
37
|
+
* and the message is placed inside it two frames later, which is the same
|
|
38
|
+
* shape of change a toast makes. One frame is not enough: Firefox builds its
|
|
39
|
+
* accessibility tree after the frame's callbacks run, so a region filled on
|
|
40
|
+
* the first frame reaches it already full and Orca hears nothing.
|
|
39
41
|
*
|
|
40
42
|
* The server runs no effect and has no frames, so it renders the message
|
|
41
43
|
* straight away: a page delivered without its client bundle still carries
|
|
@@ -44,7 +46,9 @@
|
|
|
44
46
|
let filled = $state(typeof window === 'undefined');
|
|
45
47
|
|
|
46
48
|
$effect(() => {
|
|
47
|
-
|
|
49
|
+
let frame = requestAnimationFrame(() => {
|
|
50
|
+
frame = requestAnimationFrame(() => (filled = true));
|
|
51
|
+
});
|
|
48
52
|
return () => cancelAnimationFrame(frame);
|
|
49
53
|
});
|
|
50
54
|
</script>
|
|
@@ -54,12 +58,14 @@
|
|
|
54
58
|
interrupts whatever the reader is doing, because the submit did not go
|
|
55
59
|
through and the user has to act on it. A confirmation takes aria-live
|
|
56
60
|
"polite" and waits for a pause, because the work is done and nothing is being
|
|
57
|
-
asked of anyone.
|
|
61
|
+
asked of anyone. It takes no role: browsers expose role="status" as a status
|
|
62
|
+
bar, and Orca speaks nothing inserted into one.
|
|
58
63
|
-->
|
|
59
64
|
<div
|
|
60
65
|
class="flex items-start gap-2 text-sm {tones[tone]} {klass}"
|
|
61
|
-
role={tone === 'danger' ? 'alert' :
|
|
66
|
+
role={tone === 'danger' ? 'alert' : undefined}
|
|
62
67
|
aria-live={tone === 'danger' ? 'assertive' : 'polite'}
|
|
68
|
+
aria-atomic={tone === 'danger' ? undefined : 'true'}
|
|
63
69
|
>
|
|
64
70
|
{#if filled}
|
|
65
71
|
<!-- Stroked SVG, not a literal cross or tick: a font glyph lands at
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
xmlns="http://www.w3.org/2000/svg"
|
|
59
59
|
aria-hidden="true"
|
|
60
60
|
>
|
|
61
|
-
<g transform="translate(6
|
|
61
|
+
<g transform="translate(6 3)">
|
|
62
62
|
<polygon class="fill-mark-ink" points="0,14 14,7 14,40 0,47" />
|
|
63
63
|
<polygon class="fill-mark-foot" points="0,47 14,40 14,54 0,61" />
|
|
64
64
|
<polygon class="fill-mark-step" points="14,40 28,33 28,47 14,54" />
|