@classic-homes/theme-svelte 1.20.0 → 1.22.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.
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StudioCanvasItem: picking an item opens the inspector on phones unless it
|
|
3
|
+
* is edited in place; only picking it from elsewhere scrolls it into view;
|
|
4
|
+
* the "+" can sit in the gutter.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
7
|
+
import { mount, unmount, flushSync, createRawSnippet } from 'svelte';
|
|
8
|
+
const revealInspector = vi.fn();
|
|
9
|
+
vi.mock('../lib/components/studio/context.js', () => ({
|
|
10
|
+
getStudioContext: () => ({ revealInspector }),
|
|
11
|
+
setStudioContext: () => { },
|
|
12
|
+
}));
|
|
13
|
+
const { default: StudioCanvasItem } = await import('../lib/components/studio/StudioCanvasItem.svelte');
|
|
14
|
+
let instance = null;
|
|
15
|
+
let host;
|
|
16
|
+
function render(props) {
|
|
17
|
+
host = document.createElement('div');
|
|
18
|
+
document.body.appendChild(host);
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
instance = mount(StudioCanvasItem, {
|
|
21
|
+
target: host,
|
|
22
|
+
props: {
|
|
23
|
+
label: 'Block 1',
|
|
24
|
+
children: createRawSnippet(() => ({ render: () => '<p>Body</p>' })),
|
|
25
|
+
...props,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
flushSync();
|
|
29
|
+
return host;
|
|
30
|
+
}
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (instance)
|
|
33
|
+
unmount(instance);
|
|
34
|
+
instance = null;
|
|
35
|
+
host?.remove();
|
|
36
|
+
revealInspector.mockClear();
|
|
37
|
+
});
|
|
38
|
+
describe('StudioCanvasItem revealOnSelect', () => {
|
|
39
|
+
it('reveals the inspector when picked, by default', () => {
|
|
40
|
+
const el = render({ onSelect: vi.fn() });
|
|
41
|
+
el.querySelector('p').click();
|
|
42
|
+
expect(revealInspector).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
it('selects without revealing the inspector when edited in place', () => {
|
|
45
|
+
const onSelect = vi.fn();
|
|
46
|
+
const el = render({ onSelect, revealOnSelect: false });
|
|
47
|
+
el.querySelector('p').click();
|
|
48
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(revealInspector).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('StudioCanvasItem scrolling', () => {
|
|
53
|
+
const scrollIntoView = vi.fn();
|
|
54
|
+
Element.prototype.scrollIntoView = scrollIntoView;
|
|
55
|
+
afterEach(() => scrollIntoView.mockClear());
|
|
56
|
+
async function settle() {
|
|
57
|
+
flushSync();
|
|
58
|
+
await Promise.resolve();
|
|
59
|
+
await Promise.resolve();
|
|
60
|
+
}
|
|
61
|
+
it('scrolls into view when selected from elsewhere', async () => {
|
|
62
|
+
render({ selected: false });
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
unmount(instance);
|
|
65
|
+
render({ selected: true });
|
|
66
|
+
await settle();
|
|
67
|
+
expect(scrollIntoView).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it('does not move when picked on the canvas', async () => {
|
|
70
|
+
let selected = $state(false);
|
|
71
|
+
const el = render({
|
|
72
|
+
get selected() {
|
|
73
|
+
return selected;
|
|
74
|
+
},
|
|
75
|
+
onSelect: () => (selected = true),
|
|
76
|
+
});
|
|
77
|
+
const body = el.querySelector('p');
|
|
78
|
+
body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
|
|
79
|
+
body.click();
|
|
80
|
+
await settle();
|
|
81
|
+
expect(selected).toBe(true);
|
|
82
|
+
expect(scrollIntoView).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('StudioCanvasItem insertPlacement', () => {
|
|
86
|
+
it('centres the "+" by default and puts it in the gutter on request', () => {
|
|
87
|
+
let el = render({ onInsert: vi.fn() });
|
|
88
|
+
expect(el.querySelector('button[aria-label="Add below Block 1"]').className).toContain('left-1/2');
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
unmount(instance);
|
|
91
|
+
el = render({ onInsert: vi.fn(), insertPlacement: 'gutter', selected: true });
|
|
92
|
+
const plus = el.querySelector('button[aria-label="Add below Block 1"]');
|
|
93
|
+
expect(plus.className).toContain('right-full');
|
|
94
|
+
expect(plus.className).not.toContain('left-1/2');
|
|
95
|
+
// Not pinned visible over a selected item's text
|
|
96
|
+
expect(plus.className).not.toMatch(/(^| )opacity-100( |$)/);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* gets a ring and its `toolbar` (duplicate, delete...). With `onInsert`, a
|
|
13
13
|
* "+" between this item and the next adds a new one right there. On phones
|
|
14
14
|
* picking an item opens the inspector sheet.
|
|
15
|
+
*
|
|
16
|
+
* Picking an item from elsewhere (an outline, an issue list) scrolls it into
|
|
17
|
+
* view; picking it on the canvas does not, so it never moves under the
|
|
18
|
+
* pointer. `insertPlacement="gutter"` puts the "+" in the left margin, for
|
|
19
|
+
* items edited in place, where a centred "+" would sit on the text.
|
|
15
20
|
*/
|
|
16
21
|
interface Props {
|
|
17
22
|
children: Snippet;
|
|
@@ -27,6 +32,13 @@
|
|
|
27
32
|
insertLabel?: string;
|
|
28
33
|
/** Marks the item as having problems */
|
|
29
34
|
invalid?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Open the inspector sheet on phones when the item is picked (default).
|
|
37
|
+
* Turn off for items edited in place, where the sheet would cover them.
|
|
38
|
+
*/
|
|
39
|
+
revealOnSelect?: boolean;
|
|
40
|
+
/** Where the "+" sits: centred on the item's bottom edge, or in the left gutter beside it */
|
|
41
|
+
insertPlacement?: 'center' | 'gutter';
|
|
30
42
|
class?: string;
|
|
31
43
|
}
|
|
32
44
|
|
|
@@ -39,11 +51,15 @@
|
|
|
39
51
|
onInsert,
|
|
40
52
|
insertLabel,
|
|
41
53
|
invalid = false,
|
|
54
|
+
revealOnSelect = true,
|
|
55
|
+
insertPlacement = 'center',
|
|
42
56
|
class: className,
|
|
43
57
|
}: Props = $props();
|
|
44
58
|
|
|
45
59
|
const studio = getStudioContext();
|
|
46
60
|
let element: HTMLDivElement | undefined = $state();
|
|
61
|
+
/** The item was picked on the canvas (pointer or focus), so it is already in view. */
|
|
62
|
+
let pickedHere = false;
|
|
47
63
|
|
|
48
64
|
// Quick actions (toolbar, "+") act on the item without selecting it. A
|
|
49
65
|
// dialog they open (the insert palette) returns focus to them on close;
|
|
@@ -53,11 +69,18 @@
|
|
|
53
69
|
|
|
54
70
|
function select(reveal: boolean) {
|
|
55
71
|
if (!selected) onSelect?.();
|
|
56
|
-
if (reveal) studio?.revealInspector();
|
|
72
|
+
if (reveal && revealOnSelect) studio?.revealInspector();
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
$effect(() => {
|
|
60
|
-
if (!selected || !element)
|
|
76
|
+
if (!selected || !element) {
|
|
77
|
+
pickedHere = false;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (pickedHere) {
|
|
81
|
+
pickedHere = false;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
61
84
|
const el = element;
|
|
62
85
|
void tick().then(() => {
|
|
63
86
|
// Don't yank the canvas while the user is working inside the item
|
|
@@ -82,9 +105,14 @@
|
|
|
82
105
|
invalid && 'outline-dashed outline-2 -outline-offset-2 outline-warning/70',
|
|
83
106
|
className
|
|
84
107
|
)}
|
|
108
|
+
onpointerdowncapture={(e) => {
|
|
109
|
+
if (!selected && !inQuickAction(e.target)) pickedHere = true;
|
|
110
|
+
}}
|
|
85
111
|
onclickcapture={(e) => select(!inQuickAction(e.target))}
|
|
86
112
|
onfocusin={(e) => {
|
|
87
|
-
if (
|
|
113
|
+
if (inQuickAction(e.target)) return;
|
|
114
|
+
if (!selected) pickedHere = true;
|
|
115
|
+
select(false);
|
|
88
116
|
}}
|
|
89
117
|
>
|
|
90
118
|
{#if !selected}
|
|
@@ -115,8 +143,13 @@
|
|
|
115
143
|
type="button"
|
|
116
144
|
aria-label={insertLabel ?? `Add below ${label}`}
|
|
117
145
|
title={insertLabel ?? 'Add below'}
|
|
118
|
-
class=
|
|
119
|
-
|
|
146
|
+
class={cn(
|
|
147
|
+
'absolute top-0 z-10 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground opacity-0 shadow-sm hover:border-primary hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/item:opacity-100 [@media(hover:none)]:opacity-100',
|
|
148
|
+
insertPlacement === 'gutter'
|
|
149
|
+
? 'right-full mr-1.5 group-focus-within/item:opacity-100'
|
|
150
|
+
: 'left-1/2 -translate-x-1/2',
|
|
151
|
+
selected && insertPlacement === 'center' && 'opacity-100'
|
|
152
|
+
)}
|
|
120
153
|
onclick={(e) => {
|
|
121
154
|
e.stopPropagation();
|
|
122
155
|
onInsert?.();
|
|
@@ -7,6 +7,11 @@ import { type Snippet } from 'svelte';
|
|
|
7
7
|
* gets a ring and its `toolbar` (duplicate, delete...). With `onInsert`, a
|
|
8
8
|
* "+" between this item and the next adds a new one right there. On phones
|
|
9
9
|
* picking an item opens the inspector sheet.
|
|
10
|
+
*
|
|
11
|
+
* Picking an item from elsewhere (an outline, an issue list) scrolls it into
|
|
12
|
+
* view; picking it on the canvas does not, so it never moves under the
|
|
13
|
+
* pointer. `insertPlacement="gutter"` puts the "+" in the left margin, for
|
|
14
|
+
* items edited in place, where a centred "+" would sit on the text.
|
|
10
15
|
*/
|
|
11
16
|
interface Props {
|
|
12
17
|
children: Snippet;
|
|
@@ -22,6 +27,13 @@ interface Props {
|
|
|
22
27
|
insertLabel?: string;
|
|
23
28
|
/** Marks the item as having problems */
|
|
24
29
|
invalid?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Open the inspector sheet on phones when the item is picked (default).
|
|
32
|
+
* Turn off for items edited in place, where the sheet would cover them.
|
|
33
|
+
*/
|
|
34
|
+
revealOnSelect?: boolean;
|
|
35
|
+
/** Where the "+" sits: centred on the item's bottom edge, or in the left gutter beside it */
|
|
36
|
+
insertPlacement?: 'center' | 'gutter';
|
|
25
37
|
class?: string;
|
|
26
38
|
}
|
|
27
39
|
declare const StudioCanvasItem: import("svelte").Component<Props, {}, "">;
|