@ruckc/truss-ui 1.2.0 → 1.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/README.md +6 -1
- package/dist/commands.svelte.d.ts +41 -0
- package/dist/commands.svelte.js +59 -0
- package/dist/components/Accordion.svelte +1 -1
- package/dist/components/Button.svelte +65 -8
- package/dist/components/Button.svelte.d.ts +2 -0
- package/dist/components/Calendar.svelte +2 -2
- package/dist/components/Card.svelte +1 -0
- package/dist/components/Card.svelte.d.ts +1 -0
- package/dist/components/Checkbox.svelte +1 -1
- package/dist/components/CommandPalette.svelte +207 -0
- package/dist/components/CommandPalette.svelte.d.ts +39 -0
- package/dist/components/DataTable.svelte +201 -27
- package/dist/components/DataTable.svelte.d.ts +50 -4
- package/dist/components/Dialog.svelte +2 -1
- package/dist/components/Drawer.svelte +1 -1
- package/dist/components/EditableText.svelte +2 -2
- package/dist/components/Eyebrow.svelte +1 -1
- package/dist/components/FileUpload.svelte +1 -1
- package/dist/components/Footer.svelte +29 -2
- package/dist/components/Footer.svelte.d.ts +8 -0
- package/dist/components/GridLayout.svelte +242 -0
- package/dist/components/GridLayout.svelte.d.ts +30 -0
- package/dist/components/HoverCard.svelte +158 -0
- package/dist/components/HoverCard.svelte.d.ts +46 -0
- package/dist/components/Kbd.svelte +36 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Link.svelte +1 -1
- package/dist/components/List.svelte +113 -0
- package/dist/components/List.svelte.d.ts +17 -0
- package/dist/components/ListItem.svelte +13 -0
- package/dist/components/ListItem.svelte.d.ts +9 -0
- package/dist/components/Menu.svelte +4 -3
- package/dist/components/MultiSelect.svelte +2 -2
- package/dist/components/Navbar.svelte +13 -3
- package/dist/components/Navbar.svelte.d.ts +5 -1
- package/dist/components/Panel.svelte +2 -2
- package/dist/components/Popover.svelte +22 -1
- package/dist/components/Popover.svelte.d.ts +2 -0
- package/dist/components/RadioGroup.svelte +1 -1
- package/dist/components/Select.svelte +1 -1
- package/dist/components/Shell.svelte +12 -1
- package/dist/components/Shell.svelte.d.ts +2 -0
- package/dist/components/ShortcutSheet.svelte +62 -0
- package/dist/components/ShortcutSheet.svelte.d.ts +9 -0
- package/dist/components/Sidebar.svelte +2 -2
- package/dist/components/Skeleton.svelte +80 -6
- package/dist/components/Skeleton.svelte.d.ts +8 -1
- package/dist/components/Slider.svelte +1 -1
- package/dist/components/Switch.svelte +1 -1
- package/dist/components/Tabs.svelte +252 -32
- package/dist/components/Tabs.svelte.d.ts +21 -9
- package/dist/components/Tag.svelte +7 -7
- package/dist/components/TagInput.svelte +1 -1
- package/dist/components/Text.svelte +1 -1
- package/dist/components/Toaster.svelte +33 -2
- package/dist/components/ToggleChip.svelte +1 -1
- package/dist/components/Toolbar.svelte +27 -0
- package/dist/components/Toolbar.svelte.d.ts +10 -0
- package/dist/components/ToolbarSeparator.svelte +15 -0
- package/dist/components/ToolbarSeparator.svelte.d.ts +3 -0
- package/dist/components/Tooltip.svelte +19 -28
- package/dist/components/Tooltip.svelte.d.ts +6 -1
- package/dist/components/TooltipTip.svelte +59 -0
- package/dist/components/TooltipTip.svelte.d.ts +12 -0
- package/dist/components/Tree.svelte +343 -44
- package/dist/components/Tree.svelte.d.ts +29 -2
- package/dist/grid-layout.d.ts +42 -0
- package/dist/grid-layout.js +100 -0
- package/dist/hotkeys.svelte.d.ts +45 -0
- package/dist/hotkeys.svelte.js +108 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +13 -0
- package/dist/keys.d.ts +25 -0
- package/dist/keys.js +88 -0
- package/dist/roving.d.ts +25 -0
- package/dist/roving.js +129 -0
- package/dist/styles/base.css +2 -0
- package/dist/styles/tokens.css +49 -0
- package/dist/table-export.d.ts +24 -0
- package/dist/table-export.js +130 -0
- package/dist/toast.svelte.d.ts +25 -1
- package/dist/toast.svelte.js +42 -4
- package/dist/types.d.ts +6 -0
- package/package.json +7 -3
|
@@ -7,10 +7,18 @@ type $$ComponentProps = {
|
|
|
7
7
|
center?: Snippet;
|
|
8
8
|
/** Trailing region (e.g. social links). */
|
|
9
9
|
end?: Snippet;
|
|
10
|
+
/** Identity (e.g. a logo) at the start. With `brand`, `meta` or `links`, `start`/`center`/`end` are ignored. */
|
|
11
|
+
brand?: Snippet;
|
|
12
|
+
/** Small print (e.g. copyright) stacked directly under `brand`. */
|
|
13
|
+
meta?: Snippet;
|
|
14
|
+
/** One or more link groups, right-aligned on the same row when wide. */
|
|
15
|
+
links?: Snippet;
|
|
10
16
|
/** Free-form content; replaces the three regions. */
|
|
11
17
|
children?: Snippet;
|
|
12
18
|
/** `bordered` adds a top border. */
|
|
13
19
|
variant?: 'plain' | 'bordered';
|
|
20
|
+
/** Align the content to the `Container` max-width and gutters while the background stays full-bleed. `true` is `lg`; sizes match `Container`. */
|
|
21
|
+
contained?: boolean | 'sm' | 'md' | 'lg';
|
|
14
22
|
class?: string;
|
|
15
23
|
} & Omit<HTMLAttributes<HTMLElement>, 'children' | 'class'>;
|
|
16
24
|
declare const Footer: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { compact, moveItem, resizeItem, colsFor, sameLayout, bottom, type GridItem } from '../grid-layout';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
items = $bindable([]),
|
|
7
|
+
cols = 12,
|
|
8
|
+
rowHeight,
|
|
9
|
+
gap,
|
|
10
|
+
editable = true,
|
|
11
|
+
breakpoints,
|
|
12
|
+
handle,
|
|
13
|
+
label = 'Dashboard layout',
|
|
14
|
+
itemLabel = (item: GridItem) => `Tile ${item.id}`,
|
|
15
|
+
onchange,
|
|
16
|
+
tile
|
|
17
|
+
}: {
|
|
18
|
+
/** Tiles in grid cells. Bound: drops and resizes write the new layout back. */
|
|
19
|
+
items?: GridItem[];
|
|
20
|
+
/** Column count (the maximum when `breakpoints` is set). */
|
|
21
|
+
cols?: number;
|
|
22
|
+
/** Row height in px. Defaults to twice the density row height, so it follows density modes. */
|
|
23
|
+
rowHeight?: number;
|
|
24
|
+
/** Space between tiles in px. Defaults to the `--tr-gap` token. */
|
|
25
|
+
gap?: number;
|
|
26
|
+
/** Turn dragging, resizing and the keyboard model on or off. */
|
|
27
|
+
editable?: boolean;
|
|
28
|
+
/** Container width in px (minimum) → column count, e.g. `{ 0: 1, 600: 6, 1000: 12 }`. */
|
|
29
|
+
breakpoints?: Record<number, number>;
|
|
30
|
+
/** CSS selector of the element inside a tile that starts a drag. Omit to drag by the whole tile. */
|
|
31
|
+
handle?: string;
|
|
32
|
+
/** Accessible name of the grid. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Accessible name of a tile. */
|
|
35
|
+
itemLabel?: (item: GridItem) => string;
|
|
36
|
+
/** Called with the new layout after a drop, resize or keyboard commit. */
|
|
37
|
+
onchange?: (layout: GridItem[]) => void;
|
|
38
|
+
tile: Snippet<[{ item: GridItem }]>;
|
|
39
|
+
} = $props();
|
|
40
|
+
|
|
41
|
+
const uid = $props.id();
|
|
42
|
+
let root: HTMLDivElement;
|
|
43
|
+
let probe: HTMLDivElement;
|
|
44
|
+
let width = $state(0);
|
|
45
|
+
let animate = $state(false);
|
|
46
|
+
const eff = $derived(Math.max(1, breakpoints ? colsFor(width, breakpoints, cols) : cols));
|
|
47
|
+
|
|
48
|
+
/** Preview layout while a pointer drag or a keyboard pick-up is in flight. */
|
|
49
|
+
let live = $state<GridItem[] | null>(null);
|
|
50
|
+
const base = $derived(compact(items, eff));
|
|
51
|
+
const shown = $derived(live ?? base);
|
|
52
|
+
const rows = $derived(bottom(shown));
|
|
53
|
+
let announcement = $state('');
|
|
54
|
+
|
|
55
|
+
$effect(() => {
|
|
56
|
+
if (!root) return;
|
|
57
|
+
const ro = new ResizeObserver(() => (width = root.clientWidth));
|
|
58
|
+
width = root.clientWidth;
|
|
59
|
+
ro.observe(root);
|
|
60
|
+
// animate tile moves only once the first real layout is on screen, so the grid never slides in from a 0px-wide guess
|
|
61
|
+
const f = requestAnimationFrame(() => requestAnimationFrame(() => (animate = true)));
|
|
62
|
+
return () => { ro.disconnect(); cancelAnimationFrame(f); };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function commit(layout: GridItem[]) {
|
|
66
|
+
const next = compact(layout, eff);
|
|
67
|
+
if (sameLayout(next, base) && sameLayout(next, items)) return false;
|
|
68
|
+
items = next;
|
|
69
|
+
onchange?.(next);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pos = (it: GridItem) => `column ${it.x + 1}, row ${it.y + 1}`;
|
|
74
|
+
const find = (layout: GridItem[], id: string) => layout.find((i) => i.id === id)!;
|
|
75
|
+
|
|
76
|
+
// ───────────── pointer
|
|
77
|
+
type Drag = { id: string; mode: 'move' | 'resize'; x0: number; y0: number; dx: number; dy: number; stepX: number; stepY: number; orig: GridItem; moved: boolean };
|
|
78
|
+
let drag = $state<Drag | null>(null);
|
|
79
|
+
|
|
80
|
+
function measure() {
|
|
81
|
+
const g = probe.getBoundingClientRect().width;
|
|
82
|
+
const rh = probe.getBoundingClientRect().height;
|
|
83
|
+
const colW = (root.clientWidth - g * (eff - 1)) / eff;
|
|
84
|
+
return { stepX: colW + g, stepY: rh + g };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const INTERACTIVE = 'button, a[href], input, select, textarea, [contenteditable="true"], [role="button"], [data-no-drag]';
|
|
88
|
+
function down(e: PointerEvent, item: GridItem) {
|
|
89
|
+
if (!editable || item.static || (e.pointerType === 'mouse' && e.button !== 0) || kb) return;
|
|
90
|
+
const t = e.target as Element;
|
|
91
|
+
const resize = !!t.closest('.rh');
|
|
92
|
+
const el = e.currentTarget as HTMLElement;
|
|
93
|
+
if (!resize) {
|
|
94
|
+
if (handle ? !t.closest(handle) || !el.contains(t.closest(handle)) : t.closest(INTERACTIVE) && t.closest(INTERACTIVE) !== el) return;
|
|
95
|
+
}
|
|
96
|
+
el.setPointerCapture(e.pointerId);
|
|
97
|
+
const { stepX, stepY } = measure();
|
|
98
|
+
drag = { id: item.id, mode: resize ? 'resize' : 'move', x0: e.clientX, y0: e.clientY, dx: 0, dy: 0, stepX, stepY, orig: find(shown, item.id), moved: false };
|
|
99
|
+
live = shown;
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
el.focus({ preventScroll: true });
|
|
102
|
+
}
|
|
103
|
+
function move(e: PointerEvent) {
|
|
104
|
+
if (!drag) return;
|
|
105
|
+
const d = drag;
|
|
106
|
+
d.dx = e.clientX - d.x0;
|
|
107
|
+
d.dy = e.clientY - d.y0;
|
|
108
|
+
if (!d.moved && Math.hypot(d.dx, d.dy) < 3) return;
|
|
109
|
+
d.moved = true;
|
|
110
|
+
const gx = Math.round(d.dx / d.stepX), gy = Math.round(d.dy / d.stepY);
|
|
111
|
+
const o = d.orig;
|
|
112
|
+
const next = d.mode === 'move' ? moveItem(base, d.id, o.x + gx, o.y + gy, eff) : resizeItem(base, d.id, o.w + gx, o.h + gy, eff);
|
|
113
|
+
if (!sameLayout(next, live!)) live = next;
|
|
114
|
+
}
|
|
115
|
+
function up(e: PointerEvent) {
|
|
116
|
+
if (!drag) return;
|
|
117
|
+
const d = drag;
|
|
118
|
+
(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
|
|
119
|
+
drag = null;
|
|
120
|
+
const layout = live!;
|
|
121
|
+
live = null;
|
|
122
|
+
if (!d.moved || e.type === 'pointercancel') return;
|
|
123
|
+
const it = find(layout, d.id);
|
|
124
|
+
if (commit(layout)) announcement = `${d.mode === 'move' ? 'Moved' : 'Resized'} ${itemLabel(it)} to ${pos(it)}, ${it.w} wide, ${it.h} tall.`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ───────────── keyboard
|
|
128
|
+
let kb = $state<{ id: string } | null>(null);
|
|
129
|
+
const ARROWS: Record<string, [number, number]> = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
|
|
130
|
+
const status = (it: GridItem) => `${pos(it)}, ${it.w} wide, ${it.h} tall.`;
|
|
131
|
+
|
|
132
|
+
function key(e: KeyboardEvent, item: GridItem) {
|
|
133
|
+
if (drag && e.key === 'Escape') { drag = null; live = null; announcement = 'Cancelled.'; return; }
|
|
134
|
+
if (!editable || item.static || e.target !== e.currentTarget || drag) return;
|
|
135
|
+
if (!kb) {
|
|
136
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
kb = { id: item.id };
|
|
139
|
+
live = shown;
|
|
140
|
+
announcement = `Picked up ${itemLabel(item)}. ${status(item)} Arrow keys move, Shift plus arrow keys resize, Enter drops, Escape cancels.`;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const cur = find(live!, item.id);
|
|
144
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
const layout = live!;
|
|
147
|
+
kb = null;
|
|
148
|
+
live = null;
|
|
149
|
+
const it = find(layout, item.id);
|
|
150
|
+
commit(layout);
|
|
151
|
+
announcement = `Dropped ${itemLabel(it)} at ${status(it)}`;
|
|
152
|
+
} else if (e.key === 'Escape') {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
kb = null;
|
|
156
|
+
live = null;
|
|
157
|
+
announcement = `Cancelled. ${itemLabel(item)} stays at ${status(find(base, item.id))}`;
|
|
158
|
+
} else if (ARROWS[e.key]) {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
const [dx, dy] = ARROWS[e.key];
|
|
161
|
+
const next = e.shiftKey ? resizeItem(live!, item.id, cur.w + dx, cur.h + dy, eff) : moveItem(live!, item.id, cur.x + dx, cur.y + dy, eff);
|
|
162
|
+
const it = find(next, item.id);
|
|
163
|
+
if (it.x === cur.x && it.y === cur.y && it.w === cur.w && it.h === cur.h) announcement = `Cannot ${e.shiftKey ? 'resize' : 'move'} further. ${status(it)}`;
|
|
164
|
+
else { live = next; announcement = `${e.shiftKey ? 'Resized' : 'Moved'} to ${status(it)}`; }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function blur(e: FocusEvent) {
|
|
168
|
+
if (!kb || e.currentTarget !== e.target) return;
|
|
169
|
+
kb = null;
|
|
170
|
+
live = null;
|
|
171
|
+
announcement = 'Move cancelled.';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const cell = (it: GridItem) => `left: calc((100% + var(--_gap)) / ${eff} * ${it.x}); top: calc((var(--_rh) + var(--_gap)) * ${it.y}); width: calc((100% + var(--_gap)) / ${eff} * ${it.w} - var(--_gap)); height: calc((var(--_rh) + var(--_gap)) * ${it.h} - var(--_gap));`;
|
|
175
|
+
/** The dragged tile follows the pointer instead of snapping to its cell. */
|
|
176
|
+
function style(it: GridItem) {
|
|
177
|
+
if (drag?.id !== it.id) return cell(it);
|
|
178
|
+
const { orig: o, dx, dy, stepX, stepY } = drag;
|
|
179
|
+
return drag.mode === 'move'
|
|
180
|
+
? `left: ${Math.max(0, o.x * stepX + dx)}px; top: ${Math.max(0, o.y * stepY + dy)}px; ${cell(o).replace(/left:[^;]*;|top:[^;]*;/g, '')}`
|
|
181
|
+
: `${cell(o).replace(/width:[^;]*;|height:[^;]*;/g, '')} width: calc(${o.w * stepX + dx}px - var(--_gap)); height: calc(${o.h * stepY + dy}px - var(--_gap));`;
|
|
182
|
+
}
|
|
183
|
+
const placeholder = $derived(drag?.moved ? find(shown, drag.id) : kb ? find(shown, kb.id) : null);
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<div
|
|
187
|
+
bind:this={root} class="gl" role="group" aria-label={label} aria-describedby="{uid}-hint"
|
|
188
|
+
data-editable={editable} data-dragging={!!drag} data-cols={eff} data-animate={animate}
|
|
189
|
+
style="--_gap: {gap == null ? 'var(--tr-gap)' : `${gap}px`}; --_rh: {rowHeight == null ? 'calc(var(--tr-row-h) * 2)' : `${rowHeight}px`}; height: calc((var(--_rh) + var(--_gap)) * {rows} - var(--_gap));"
|
|
190
|
+
>
|
|
191
|
+
<div class="probe" bind:this={probe} aria-hidden="true"></div>
|
|
192
|
+
{#each shown as item (item.id)}
|
|
193
|
+
{@const picked = kb?.id === item.id}
|
|
194
|
+
{@const active = drag?.id === item.id && drag.moved}
|
|
195
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
196
|
+
<div
|
|
197
|
+
class="tile" role="group" aria-label={itemLabel(item)} aria-describedby={editable && !item.static ? `${uid}-hint` : undefined}
|
|
198
|
+
tabindex={editable && !item.static ? 0 : undefined}
|
|
199
|
+
data-id={item.id} data-static={item.static || undefined} data-picked={picked || undefined} data-active={active || undefined}
|
|
200
|
+
data-x={item.x} data-y={item.y} data-w={item.w} data-h={item.h}
|
|
201
|
+
style={style(item)}
|
|
202
|
+
onpointerdown={(e) => down(e, item)} onpointermove={move} onpointerup={up} onpointercancel={up}
|
|
203
|
+
onkeydown={(e) => key(e, item)} onfocusout={blur}
|
|
204
|
+
>
|
|
205
|
+
<div class="body">{@render tile({ item })}</div>
|
|
206
|
+
{#if editable && !item.static}<span class="rh" aria-hidden="true"></span>{/if}
|
|
207
|
+
</div>
|
|
208
|
+
{/each}
|
|
209
|
+
{#if placeholder}
|
|
210
|
+
<div class="ph" aria-hidden="true" style={cell(placeholder)}></div>
|
|
211
|
+
{/if}
|
|
212
|
+
<span id="{uid}-hint" class="tr-visually-hidden">Press Enter or Space to pick up a tile. Arrow keys move it, Shift plus arrow keys resize it, Enter drops it and Escape cancels.</span>
|
|
213
|
+
<div class="tr-visually-hidden" role="status" aria-live="polite" aria-atomic="true">{announcement}</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<style>
|
|
217
|
+
.gl { position: relative; width: 100%; }
|
|
218
|
+
.probe { position: absolute; left: 0; top: 0; width: var(--_gap); height: var(--_rh); visibility: hidden; pointer-events: none; }
|
|
219
|
+
.tile { position: absolute; box-sizing: border-box; min-width: 0; min-height: 0; border-radius: var(--tr-radius-md); outline: none; }
|
|
220
|
+
.gl[data-animate='true'] .tile {
|
|
221
|
+
transition: left var(--tr-dur) var(--tr-ease), top var(--tr-dur) var(--tr-ease), width var(--tr-dur) var(--tr-ease), height var(--tr-dur) var(--tr-ease), box-shadow var(--tr-dur) var(--tr-ease);
|
|
222
|
+
}
|
|
223
|
+
.body { height: 100%; display: grid; min-width: 0; min-height: 0; }
|
|
224
|
+
.body > :global(*) { min-width: 0; min-height: 0; }
|
|
225
|
+
.gl[data-editable='true'] .tile:not([data-static]) { cursor: grab; touch-action: none; }
|
|
226
|
+
.tile:focus-visible { box-shadow: var(--tr-focus-ring); }
|
|
227
|
+
.tile[data-picked] { box-shadow: 0 0 0 2px var(--tr-primary), var(--tr-elevation-3); z-index: 3; }
|
|
228
|
+
.tile[data-active], .gl[data-animate='true'] .tile[data-active] { z-index: 4; transition: none; box-shadow: var(--tr-elevation-4); cursor: grabbing; }
|
|
229
|
+
.gl[data-dragging='true'] { user-select: none; }
|
|
230
|
+
.ph {
|
|
231
|
+
position: absolute; box-sizing: border-box; border-radius: var(--tr-radius-md); z-index: 0; pointer-events: none;
|
|
232
|
+
background: color-mix(in srgb, var(--tr-primary) 12%, transparent); border: 2px dashed var(--tr-primary);
|
|
233
|
+
}
|
|
234
|
+
.gl[data-animate='true'] .ph {
|
|
235
|
+
transition: left var(--tr-dur) var(--tr-ease), top var(--tr-dur) var(--tr-ease), width var(--tr-dur) var(--tr-ease), height var(--tr-dur) var(--tr-ease);
|
|
236
|
+
}
|
|
237
|
+
.rh { position: absolute; right: 0; bottom: 0; width: 18px; height: 18px; cursor: nwse-resize; touch-action: none; opacity: 0; transition: opacity var(--tr-dur) var(--tr-ease); z-index: 2; }
|
|
238
|
+
.rh::after { content: ''; position: absolute; right: 4px; bottom: 4px; width: 8px; height: 8px; border-right: 2px solid var(--tr-on-surface-variant); border-bottom: 2px solid var(--tr-on-surface-variant); border-radius: 0 0 3px 0; }
|
|
239
|
+
.tile:hover > .rh, .tile:focus-visible > .rh, .tile[data-active] > .rh, .tile[data-picked] > .rh { opacity: 0.8; }
|
|
240
|
+
@media (pointer: coarse) { .rh { opacity: 0.6; } }
|
|
241
|
+
@media (prefers-reduced-motion: reduce) { .tile, .ph, .rh { transition: none; } }
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type GridItem } from '../grid-layout';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
/** Tiles in grid cells. Bound: drops and resizes write the new layout back. */
|
|
5
|
+
items?: GridItem[];
|
|
6
|
+
/** Column count (the maximum when `breakpoints` is set). */
|
|
7
|
+
cols?: number;
|
|
8
|
+
/** Row height in px. Defaults to twice the density row height, so it follows density modes. */
|
|
9
|
+
rowHeight?: number;
|
|
10
|
+
/** Space between tiles in px. Defaults to the `--tr-gap` token. */
|
|
11
|
+
gap?: number;
|
|
12
|
+
/** Turn dragging, resizing and the keyboard model on or off. */
|
|
13
|
+
editable?: boolean;
|
|
14
|
+
/** Container width in px (minimum) → column count, e.g. `{ 0: 1, 600: 6, 1000: 12 }`. */
|
|
15
|
+
breakpoints?: Record<number, number>;
|
|
16
|
+
/** CSS selector of the element inside a tile that starts a drag. Omit to drag by the whole tile. */
|
|
17
|
+
handle?: string;
|
|
18
|
+
/** Accessible name of the grid. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Accessible name of a tile. */
|
|
21
|
+
itemLabel?: (item: GridItem) => string;
|
|
22
|
+
/** Called with the new layout after a drop, resize or keyboard commit. */
|
|
23
|
+
onchange?: (layout: GridItem[]) => void;
|
|
24
|
+
tile: Snippet<[{
|
|
25
|
+
item: GridItem;
|
|
26
|
+
}]>;
|
|
27
|
+
};
|
|
28
|
+
declare const GridLayout: import("svelte").Component<$$ComponentProps, {}, "items">;
|
|
29
|
+
type GridLayout = ReturnType<typeof GridLayout>;
|
|
30
|
+
export default GridLayout;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script lang="ts" generics="T = unknown">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Popover from './Popover.svelte';
|
|
4
|
+
import Skeleton from './Skeleton.svelte';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A preview card for a trigger (a link, a record reference…). Opens on hover intent, keyboard (focused trigger +
|
|
8
|
+
* Enter or Space) or touch long-press; closes on Escape, outside press, or once the pointer has left both the
|
|
9
|
+
* trigger and the card for `closeDelay`. It is a non-modal dialog, so interactive content works, and it never takes
|
|
10
|
+
* focus on hover. A keyboard open moves focus into the card; Escape returns it to the trigger.
|
|
11
|
+
*/
|
|
12
|
+
let {
|
|
13
|
+
open = $bindable(false),
|
|
14
|
+
label,
|
|
15
|
+
trigger,
|
|
16
|
+
children,
|
|
17
|
+
load,
|
|
18
|
+
loading,
|
|
19
|
+
error,
|
|
20
|
+
placement = 'bottom',
|
|
21
|
+
openDelay = 500,
|
|
22
|
+
closeDelay = 200,
|
|
23
|
+
openKeys = ['Enter', ' ']
|
|
24
|
+
}: {
|
|
25
|
+
open?: boolean;
|
|
26
|
+
/** Accessible name of the card (it is announced as a dialog, not a tooltip). */
|
|
27
|
+
label: string;
|
|
28
|
+
/** The element the card previews. Its first focusable element (else the wrapper) gets `aria-haspopup`/`aria-expanded`. */
|
|
29
|
+
trigger: Snippet;
|
|
30
|
+
/** Card content; receives the value resolved by `load` (undefined without one). */
|
|
31
|
+
children: Snippet<[value: T]>;
|
|
32
|
+
/** Fetches the card's data when it opens; a Skeleton shows meanwhile. A successful result is kept. */
|
|
33
|
+
load?: () => Promise<T>;
|
|
34
|
+
/** Replaces the default Skeleton while `load` is pending. */
|
|
35
|
+
loading?: Snippet;
|
|
36
|
+
/** Shown when `load` rejects (default: a short message); reopening retries. */
|
|
37
|
+
error?: Snippet<[reason: unknown]>;
|
|
38
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
39
|
+
/** ms the pointer must rest on the trigger before the card opens. */
|
|
40
|
+
openDelay?: number;
|
|
41
|
+
/** ms grace after the pointer leaves, so it can travel from the trigger into the card. */
|
|
42
|
+
closeDelay?: number;
|
|
43
|
+
/** Keys that open the card while the trigger is focused. A key that opens the card does not also activate the trigger. */
|
|
44
|
+
openKeys?: string[];
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
const id = $props.id();
|
|
48
|
+
const LONG_PRESS = 500;
|
|
49
|
+
const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
50
|
+
let wrap = $state<HTMLSpanElement>();
|
|
51
|
+
let card = $state<HTMLDivElement>();
|
|
52
|
+
let hostEl = $state<HTMLSpanElement>();
|
|
53
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
54
|
+
let press: ReturnType<typeof setTimeout> | undefined;
|
|
55
|
+
let pressAt = { x: 0, y: 0 };
|
|
56
|
+
let swallowClick = false;
|
|
57
|
+
let pinned = false;
|
|
58
|
+
let value = $state<T>();
|
|
59
|
+
let loaded = $state(false);
|
|
60
|
+
let failure = $state<{ reason: unknown } | undefined>();
|
|
61
|
+
let token = 0;
|
|
62
|
+
|
|
63
|
+
const target = () => (hostEl?.querySelector<HTMLElement>(FOCUSABLE) ?? hostEl) as HTMLElement | undefined;
|
|
64
|
+
const clear = () => { clearTimeout(timer); clearTimeout(press); press = undefined; };
|
|
65
|
+
const show = (delay: number) => { clearTimeout(timer); if (open) return; timer = setTimeout(() => (open = true), delay); };
|
|
66
|
+
const hide = (delay: number) => { clearTimeout(timer); if (!open) return; timer = setTimeout(() => (open = false), delay); };
|
|
67
|
+
$effect(() => () => clear());
|
|
68
|
+
|
|
69
|
+
// Trigger semantics live on the real focusable element so assistive tech announces "dialog popup, expanded".
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const el = target();
|
|
72
|
+
if (!el) return;
|
|
73
|
+
el.setAttribute('aria-haspopup', 'dialog');
|
|
74
|
+
el.setAttribute('aria-expanded', String(open));
|
|
75
|
+
if (open) el.setAttribute('aria-controls', id); else el.removeAttribute('aria-controls');
|
|
76
|
+
if (el === hostEl && !el.hasAttribute('tabindex')) el.tabIndex = 0;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (!open) { pinned = false; return; }
|
|
81
|
+
if (!load || loaded) return;
|
|
82
|
+
const mine = ++token;
|
|
83
|
+
failure = undefined;
|
|
84
|
+
load().then(
|
|
85
|
+
(v) => { if (mine === token) { value = v; loaded = true; } },
|
|
86
|
+
(reason) => { if (mine === token) failure = { reason }; }
|
|
87
|
+
);
|
|
88
|
+
return () => { token++; };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function onclose(refocus: boolean) {
|
|
92
|
+
clear();
|
|
93
|
+
if (refocus) target()?.focus();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function onkeydown(e: KeyboardEvent) {
|
|
97
|
+
if (e.target !== target() || e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || !openKeys.includes(e.key)) return;
|
|
98
|
+
if (open) return; // already open: let the key activate the trigger
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
clear();
|
|
101
|
+
pinned = true;
|
|
102
|
+
open = true;
|
|
103
|
+
// the card renders on the next frame; move focus into it so keyboard users can reach its content
|
|
104
|
+
requestAnimationFrame(() => (card?.querySelector<HTMLElement>(FOCUSABLE) ?? card)?.focus());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onpointerenter(e: PointerEvent) { if (e.pointerType !== 'touch') show(openDelay); }
|
|
108
|
+
function onpointerleave(e: PointerEvent) { if (e.pointerType !== 'touch' && !pinned) hide(closeDelay); else clearTimeout(timer); }
|
|
109
|
+
function onpointerdown(e: PointerEvent) {
|
|
110
|
+
if (e.pointerType !== 'touch' || !hostEl?.contains(e.target as Node)) return;
|
|
111
|
+
pressAt = { x: e.clientX, y: e.clientY };
|
|
112
|
+
clearTimeout(press);
|
|
113
|
+
press = setTimeout(() => { press = undefined; swallowClick = true; pinned = true; open = true; }, LONG_PRESS);
|
|
114
|
+
}
|
|
115
|
+
function onpointermove(e: PointerEvent) {
|
|
116
|
+
if (press && Math.hypot(e.clientX - pressAt.x, e.clientY - pressAt.y) > 10) { clearTimeout(press); press = undefined; }
|
|
117
|
+
}
|
|
118
|
+
const endPress = () => { clearTimeout(press); press = undefined; };
|
|
119
|
+
// a long-press must not also follow the link (click) or raise the browser's context menu
|
|
120
|
+
function onclickcapture(e: MouseEvent) { if (swallowClick) { swallowClick = false; e.preventDefault(); e.stopPropagation(); } }
|
|
121
|
+
function oncontextmenu(e: MouseEvent) { if (press || swallowClick) e.preventDefault(); }
|
|
122
|
+
function onfocusout(e: FocusEvent) {
|
|
123
|
+
const to = e.relatedTarget as Node | null;
|
|
124
|
+
const left = () => { open = false; clear(); };
|
|
125
|
+
if (to) { if (!wrap?.contains(to)) left(); }
|
|
126
|
+
else setTimeout(() => { if (!wrap?.contains(document.activeElement)) left(); }); // focus went to the page chrome or nowhere
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
131
|
+
<span
|
|
132
|
+
class="tr-hovercard" bind:this={wrap}
|
|
133
|
+
{onpointerenter} {onpointerleave} {onpointerdown} {onpointermove} onpointerup={endPress} onpointercancel={endPress}
|
|
134
|
+
{onclickcapture} {oncontextmenu} {onkeydown} {onfocusout}
|
|
135
|
+
>
|
|
136
|
+
<span class="host" bind:this={hostEl}>{@render trigger()}</span>
|
|
137
|
+
<Popover bind:open fixed anchor={hostEl} {placement} {onclose}>
|
|
138
|
+
<div class="card" role="dialog" aria-label={label} {id} tabindex="-1" bind:this={card}>
|
|
139
|
+
{#if load && !loaded}
|
|
140
|
+
{#if failure}
|
|
141
|
+
{#if error}{@render error(failure.reason)}{:else}<p class="msg" role="alert">Could not load this preview.</p>{/if}
|
|
142
|
+
{:else if loading}{@render loading()}{:else}<Skeleton lines={3} label="Loading preview" />{/if}
|
|
143
|
+
{:else}
|
|
144
|
+
{@render children(value as T)}
|
|
145
|
+
{/if}
|
|
146
|
+
</div>
|
|
147
|
+
</Popover>
|
|
148
|
+
</span>
|
|
149
|
+
|
|
150
|
+
<style>
|
|
151
|
+
.tr-hovercard { display: inline-flex; }
|
|
152
|
+
.host { display: inline-flex; -webkit-touch-callout: none; }
|
|
153
|
+
.card {
|
|
154
|
+
box-sizing: border-box; min-width: 12rem; max-width: min(24rem, calc(100vw - 16px)); padding: var(--tr-space-3);
|
|
155
|
+
color: var(--tr-on-surface); font: 400 var(--tr-text-sm) / 1.45 var(--tr-font-sans); outline: none;
|
|
156
|
+
}
|
|
157
|
+
.msg { margin: 0; color: var(--tr-danger); }
|
|
158
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
declare function $$render<T = unknown>(): {
|
|
3
|
+
props: {
|
|
4
|
+
open?: boolean;
|
|
5
|
+
/** Accessible name of the card (it is announced as a dialog, not a tooltip). */
|
|
6
|
+
label: string;
|
|
7
|
+
/** The element the card previews. Its first focusable element (else the wrapper) gets `aria-haspopup`/`aria-expanded`. */
|
|
8
|
+
trigger: Snippet;
|
|
9
|
+
/** Card content; receives the value resolved by `load` (undefined without one). */
|
|
10
|
+
children: Snippet<[value: T]>;
|
|
11
|
+
/** Fetches the card's data when it opens; a Skeleton shows meanwhile. A successful result is kept. */
|
|
12
|
+
load?: () => Promise<T>;
|
|
13
|
+
/** Replaces the default Skeleton while `load` is pending. */
|
|
14
|
+
loading?: Snippet;
|
|
15
|
+
/** Shown when `load` rejects (default: a short message); reopening retries. */
|
|
16
|
+
error?: Snippet<[reason: unknown]>;
|
|
17
|
+
placement?: "top" | "bottom" | "left" | "right";
|
|
18
|
+
/** ms the pointer must rest on the trigger before the card opens. */
|
|
19
|
+
openDelay?: number;
|
|
20
|
+
/** ms grace after the pointer leaves, so it can travel from the trigger into the card. */
|
|
21
|
+
closeDelay?: number;
|
|
22
|
+
/** Keys that open the card while the trigger is focused. A key that opens the card does not also activate the trigger. */
|
|
23
|
+
openKeys?: string[];
|
|
24
|
+
};
|
|
25
|
+
exports: {};
|
|
26
|
+
bindings: "open";
|
|
27
|
+
slots: {};
|
|
28
|
+
events: {};
|
|
29
|
+
};
|
|
30
|
+
declare class __sveltets_Render<T = unknown> {
|
|
31
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
32
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
33
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
34
|
+
bindings(): "open";
|
|
35
|
+
exports(): {};
|
|
36
|
+
}
|
|
37
|
+
interface $$IsomorphicComponent {
|
|
38
|
+
new <T = unknown>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
39
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
40
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
41
|
+
<T = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
42
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
43
|
+
}
|
|
44
|
+
declare const HoverCard: $$IsomorphicComponent;
|
|
45
|
+
type HoverCard<T = unknown> = InstanceType<typeof HoverCard<T>>;
|
|
46
|
+
export default HoverCard;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatShortcut, isApple } from '../keys.js';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
keys,
|
|
6
|
+
class: klass = ''
|
|
7
|
+
}: {
|
|
8
|
+
/** A key combination such as `mod+shift+p`, or a sequence such as `g h`. `mod` shows as ⌘ on Apple platforms and Ctrl elsewhere. */
|
|
9
|
+
keys: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
} = $props();
|
|
12
|
+
|
|
13
|
+
// The platform is resolved after mount so server and client markup agree
|
|
14
|
+
let apple = $state(false);
|
|
15
|
+
$effect(() => { apple = isApple(); });
|
|
16
|
+
const steps = $derived(formatShortcut(keys, apple ? 'apple' : 'other'));
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<span class="tr-kbd {klass}">
|
|
20
|
+
{#each steps as step, s}
|
|
21
|
+
{#if s > 0}<span class="then" aria-hidden="true">then</span>{/if}
|
|
22
|
+
{#each step as k}<kbd>{k}</kbd>{/each}
|
|
23
|
+
{/each}
|
|
24
|
+
</span>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.tr-kbd { display: inline-flex; align-items: center; gap: 3px; vertical-align: baseline; color: inherit; }
|
|
28
|
+
kbd {
|
|
29
|
+
display: inline-flex; align-items: center; justify-content: center; min-width: 1.5em; height: 1.5em; padding: 0 0.4em; box-sizing: border-box;
|
|
30
|
+
font: 500 var(--tr-text-xs) / 1 var(--tr-font-mono); color: inherit;
|
|
31
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
32
|
+
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
|
33
|
+
border-bottom-width: 2px; border-radius: var(--tr-radius-sm);
|
|
34
|
+
}
|
|
35
|
+
.then { font: var(--tr-text-xs) var(--tr-font-sans); opacity: 0.7; margin: 0 2px; }
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** A key combination such as `mod+shift+p`, or a sequence such as `g h`. `mod` shows as ⌘ on Apple platforms and Ctrl elsewhere. */
|
|
3
|
+
keys: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const Kbd: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Kbd = ReturnType<typeof Kbd>;
|
|
8
|
+
export default Kbd;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
</a>
|
|
32
32
|
|
|
33
33
|
<style>
|
|
34
|
-
.ln { color: var(--tr-primary); text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px; border-radius: var(--tr-radius-sm); }
|
|
34
|
+
.ln { color: var(--tr-primary-text, var(--tr-primary)); text-decoration: underline; text-underline-offset: 2px; text-decoration-thickness: 1px; border-radius: var(--tr-radius-sm); }
|
|
35
35
|
.ln:hover { text-decoration-thickness: 2px; }
|
|
36
36
|
.ln:focus-visible { outline: 2px solid var(--tr-primary); outline-offset: 2px; }
|
|
37
37
|
.ic { display: inline-flex; vertical-align: -0.15em; margin-left: 0.2em; }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A prose list: a real `ul` (or `ol` when `ordered`) with spacing and marker styling from tokens.
|
|
7
|
+
*
|
|
8
|
+
* Markers: `disc` (default) and `ordered` numbering use native `list-style-type`, so they are
|
|
9
|
+
* drawn by the browser. `check` and `dash` hide the native marker and draw one with `::before`.
|
|
10
|
+
* `none` removes it. Ordered lists always number their items; `marker="none"` removes the numbers.
|
|
11
|
+
* When the marker is removed or replaced (`check`, `dash`, `none`), `role="list"` is set so
|
|
12
|
+
* Safari and VoiceOver still announce the list.
|
|
13
|
+
*
|
|
14
|
+
* Customise with CSS custom properties on the list or any ancestor:
|
|
15
|
+
* `--tr-list-marker-color` (default `--tr-primary`) and `--tr-list-indent` (default `1.25rem`).
|
|
16
|
+
* The default marker colour is the primary token, which reaches at least 3:1 contrast against
|
|
17
|
+
* `--tr-surface` in both the light and the dark theme. A custom colour must meet that bar too.
|
|
18
|
+
*/
|
|
19
|
+
let {
|
|
20
|
+
ordered = false,
|
|
21
|
+
marker = 'disc',
|
|
22
|
+
gap = 2,
|
|
23
|
+
size = 'md',
|
|
24
|
+
class: klass = '',
|
|
25
|
+
children,
|
|
26
|
+
...rest
|
|
27
|
+
}: {
|
|
28
|
+
/** Render an `ol` with numbered items instead of a `ul`. */
|
|
29
|
+
ordered?: boolean;
|
|
30
|
+
/** Marker style. Ordered lists number their items, so only `none` changes them. */
|
|
31
|
+
marker?: 'disc' | 'check' | 'dash' | 'none';
|
|
32
|
+
/** Space between items, from the `--tr-space-*` scale. */
|
|
33
|
+
gap?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
34
|
+
size?: 'sm' | 'md';
|
|
35
|
+
class?: string;
|
|
36
|
+
/** `ListItem`s, or plain `li` elements. */
|
|
37
|
+
children?: Snippet;
|
|
38
|
+
} & HTMLAttributes<HTMLUListElement | HTMLOListElement> = $props();
|
|
39
|
+
|
|
40
|
+
const effective = $derived(ordered ? (marker === 'none' ? 'none' : 'decimal') : marker);
|
|
41
|
+
const customMarker = $derived(effective === 'check' || effective === 'dash' || effective === 'none');
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<svelte:element
|
|
45
|
+
this={ordered ? 'ol' : 'ul'}
|
|
46
|
+
class="list {klass}"
|
|
47
|
+
role={customMarker ? 'list' : undefined}
|
|
48
|
+
data-marker={effective}
|
|
49
|
+
data-size={size}
|
|
50
|
+
data-gap={gap}
|
|
51
|
+
{...rest}
|
|
52
|
+
>
|
|
53
|
+
{@render children?.()}
|
|
54
|
+
</svelte:element>
|
|
55
|
+
|
|
56
|
+
<style>
|
|
57
|
+
.list {
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 0 0 0 var(--tr-list-indent, 1.25rem);
|
|
60
|
+
color: var(--tr-on-surface);
|
|
61
|
+
font-size: var(--tr-text-md);
|
|
62
|
+
line-height: 1.5;
|
|
63
|
+
list-style-position: outside;
|
|
64
|
+
}
|
|
65
|
+
.list[data-size='sm'] { font-size: var(--tr-text-sm); }
|
|
66
|
+
|
|
67
|
+
.list[data-marker='disc'] { list-style-type: disc; }
|
|
68
|
+
.list[data-marker='decimal'] { list-style-type: decimal; }
|
|
69
|
+
.list[data-marker='none'],
|
|
70
|
+
.list[data-marker='check'],
|
|
71
|
+
.list[data-marker='dash'] { list-style-type: none; }
|
|
72
|
+
|
|
73
|
+
/* the native marker takes the marker colour */
|
|
74
|
+
.list[data-marker='disc'] > :global(li)::marker,
|
|
75
|
+
.list[data-marker='decimal'] > :global(li)::marker {
|
|
76
|
+
color: var(--tr-list-marker-color, var(--tr-primary));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* spacing: space between items, from the token scale */
|
|
80
|
+
.list > :global(li + li) { margin-block-start: var(--tr-list-gap, 0); }
|
|
81
|
+
.list[data-gap='1'] { --tr-list-gap: var(--tr-space-1); }
|
|
82
|
+
.list[data-gap='2'] { --tr-list-gap: var(--tr-space-2); }
|
|
83
|
+
.list[data-gap='3'] { --tr-list-gap: var(--tr-space-3); }
|
|
84
|
+
.list[data-gap='4'] { --tr-list-gap: var(--tr-space-4); }
|
|
85
|
+
.list[data-gap='5'] { --tr-list-gap: var(--tr-space-5); }
|
|
86
|
+
.list[data-gap='6'] { --tr-list-gap: var(--tr-space-6); }
|
|
87
|
+
|
|
88
|
+
/* drawn markers for check and dash */
|
|
89
|
+
.list > :global(li) { position: relative; }
|
|
90
|
+
.list[data-marker='check'] > :global(li)::before,
|
|
91
|
+
.list[data-marker='dash'] > :global(li)::before {
|
|
92
|
+
content: '';
|
|
93
|
+
position: absolute;
|
|
94
|
+
left: calc(-1 * var(--tr-list-indent, 1.25rem));
|
|
95
|
+
top: 0.3em;
|
|
96
|
+
width: 1em;
|
|
97
|
+
height: 1em;
|
|
98
|
+
background-color: var(--tr-list-marker-color, var(--tr-primary));
|
|
99
|
+
}
|
|
100
|
+
.list[data-marker='check'] > :global(li)::before {
|
|
101
|
+
width: 0.9em;
|
|
102
|
+
height: 0.9em;
|
|
103
|
+
top: 0.3em;
|
|
104
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
105
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
106
|
+
}
|
|
107
|
+
.list[data-marker='dash'] > :global(li)::before {
|
|
108
|
+
left: calc(-1 * var(--tr-list-indent, 1.25rem) + 0.1em);
|
|
109
|
+
top: 0.7em;
|
|
110
|
+
width: 0.8em;
|
|
111
|
+
height: 2px;
|
|
112
|
+
}
|
|
113
|
+
</style>
|