@threadlabs/looma 0.23.21 → 0.23.23
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/components/shared/overlay.js +1 -1
- package/components/ui-button/ui-button.html +68 -2
- package/components/ui-image/ui-image.html +177 -0
- package/components/ui-image/ui-image.js +107 -0
- package/components/ui-menu/ui-menu.js +1 -1
- package/components/ui-popover/ui-popover.js +4 -1
- package/components/ui-separator/ui-separator.html +26 -2
- package/dist/index.js +24 -23
- package/editor/chunks/extensions.js +633 -20
- package/editor/extensions/block-selection.d.ts +6 -0
- package/editor/extensions/expand.d.ts +31 -0
- package/editor/extensions/image.d.ts +12 -0
- package/editor/extensions/index.d.ts +4 -1
- package/editor/extensions/index.js +2 -2
- package/editor/extensions/preset.d.ts +8 -0
- package/editor/extensions/slash-command.d.ts +31 -3
- package/editor/extensions/table-of-contents.d.ts +58 -0
- package/editor/index.js +2 -2
- package/package.json +1 -1
- package/styles/ui-button.css +39 -0
- package/styles/ui-image.css +153 -0
- package/styles/ui-separator.css +22 -2
- package/vanilla/UiButton.d.ts +1 -1
- package/vanilla/UiButton.js +126 -10
- package/vanilla/UiImage.d.ts +21 -0
- package/vanilla/UiImage.js +84 -0
- package/vanilla/UiSeparator.d.ts +3 -0
- package/vanilla/UiSeparator.js +14 -4
- package/vanilla/index.js +1 -0
- package/vue/UiButton.d.ts +18 -6
- package/vue/UiButton.vue +269 -8
- package/vue/UiImage.d.ts +100 -0
- package/vue/UiImage.js +2 -0
- package/vue/UiImage.vue +463 -0
- package/vue/UiListItem.d.ts +1 -1
- package/vue/UiSection.d.ts +1 -1
- package/vue/UiSeparator.d.ts +27 -0
- package/vue/UiSeparator.vue +94 -3
- package/vue/UiSidebar.d.ts +1 -1
- package/vue/chunks/UiButton.js +196 -12
- package/vue/chunks/UiImage.js +313 -0
- package/vue/chunks/UiSeparator.js +106 -22
- package/vue/components.css +252 -53
- package/vue/editor/LoomaEditor.d.ts +19 -2
- package/vue/editor/divider.d.ts +6 -0
- package/vue/editor/expand.d.ts +7 -0
- package/vue/editor/image-controls.d.ts +19 -0
- package/vue/editor/image-delivery.d.ts +2 -2
- package/vue/editor/image.d.ts +7 -0
- package/vue/editor/index.d.ts +1 -1
- package/vue/editor/index.js +1188 -52
- package/vue/editor/table-of-contents.d.ts +7 -0
- package/vue/index.d.ts +1 -0
- package/vue/index.js +2 -1
|
@@ -31,7 +31,7 @@ function requestClose(document, reason, trigger) {
|
|
|
31
31
|
function ensureListeners(document, state) {
|
|
32
32
|
if (state.listening) return;
|
|
33
33
|
state.onKeydown = (event) => {
|
|
34
|
-
if (event.key === "Escape"
|
|
34
|
+
if (event.key === "Escape" && !event.defaultPrevented && requestClose(document, "escape", "keyboard")) event.preventDefault();
|
|
35
35
|
};
|
|
36
36
|
state.onPointerdown = (event) => {
|
|
37
37
|
const top = state.records.at(-1);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<link rel="component" href="../ui-spinner/ui-spinner.html">
|
|
2
|
+
<link rel="component" href="../ui-icon/ui-icon.html">
|
|
2
3
|
<template component="ui-button" controller="./ui-button.js">
|
|
3
4
|
<defs>
|
|
4
|
-
<prop name="variant" type="keyword" values="outline, solid, danger, ghost, link" default="outline"
|
|
5
|
+
<prop name="variant" type="keyword" values="outline, solid, danger, ghost, link, card" default="outline"
|
|
5
6
|
>Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
|
|
6
|
-
hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it
|
|
7
|
+
hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it. card uses the
|
|
8
|
+
same tone on a full-width action with an icon slot, wrapping content, and a centered action slot.</prop>
|
|
7
9
|
<prop name="size" type="keyword" values="xs, sm, md, lg" default="md"
|
|
8
10
|
>Height, padding, and type size. xs is 1.5rem with the smallest text, for a control inside a line of small text; sm is the small control height (2rem by
|
|
9
11
|
default) with tighter padding and smaller text, md is the standard control height (2.5rem), and lg is taller (3rem) with roomier padding. Under touch, sm
|
|
@@ -37,6 +39,34 @@
|
|
|
37
39
|
table keyed by a boolean's text picks each value, and null leaves the attribute off. -->
|
|
38
40
|
</defs>
|
|
39
41
|
<template $match>
|
|
42
|
+
<a
|
|
43
|
+
$when="variant = 'card' and as = 'a'"
|
|
44
|
+
from:href="{ true: null, false: href }[concat(disabled or loading or pending)]"
|
|
45
|
+
from:target="target"
|
|
46
|
+
from:rel="rel"
|
|
47
|
+
from:aria-disabled="{ true: 'true', false: null }[concat(disabled or loading or pending)]"
|
|
48
|
+
from:aria-busy="loading or pending"
|
|
49
|
+
from:tabindex="{ true: '0', false: null }[concat((loading or pending) and not disabled)]"
|
|
50
|
+
>
|
|
51
|
+
<span class="card-icon"><ui-spinner $if="loading or pending" size="sm"></ui-spinner><template $if="not (loading or pending)"><slot
|
|
52
|
+
name="icon"
|
|
53
|
+
></slot></template></span>
|
|
54
|
+
<span class="card-content"><slot></slot></span>
|
|
55
|
+
<span class="card-action"><slot name="action"><ui-icon name="chevron-right" aria-hidden="true"></ui-icon></slot></span>
|
|
56
|
+
</a>
|
|
57
|
+
<button
|
|
58
|
+
$when="variant = 'card'"
|
|
59
|
+
from:type="type"
|
|
60
|
+
from:disabled="disabled"
|
|
61
|
+
from:aria-disabled="{ true: 'true', false: null }[concat(loading or pending)]"
|
|
62
|
+
from:aria-busy="loading or pending"
|
|
63
|
+
>
|
|
64
|
+
<span class="card-icon"><ui-spinner $if="loading or pending" size="sm"></ui-spinner><template $if="not (loading or pending)"><slot
|
|
65
|
+
name="icon"
|
|
66
|
+
></slot></template></span>
|
|
67
|
+
<span class="card-content"><slot></slot></span>
|
|
68
|
+
<span class="card-action"><slot name="action"><ui-icon name="chevron-right" aria-hidden="true"></ui-icon></slot></span>
|
|
69
|
+
</button>
|
|
40
70
|
<a
|
|
41
71
|
$when="as = 'a'"
|
|
42
72
|
from:href="{ true: null, false: href }[concat(disabled or loading or pending)]"
|
|
@@ -233,6 +263,42 @@
|
|
|
233
263
|
font-size: var(--ui-font-size-md);
|
|
234
264
|
}
|
|
235
265
|
|
|
266
|
+
/* Card actions inherit Button's palette and behavior; only their shared layout changes.
|
|
267
|
+
The icon may contain any decorative composition. Empty icons remove their column. */
|
|
268
|
+
:host-state([variant="card"]) {
|
|
269
|
+
display: grid;
|
|
270
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
271
|
+
align-items: start;
|
|
272
|
+
gap: var(--ui-space-3);
|
|
273
|
+
inline-size: 100%;
|
|
274
|
+
min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-md));
|
|
275
|
+
text-align: start;
|
|
276
|
+
white-space: normal;
|
|
277
|
+
box-shadow: none;
|
|
278
|
+
background-image: none;
|
|
279
|
+
}
|
|
280
|
+
:host-state([variant="card"][size="lg"]) {
|
|
281
|
+
min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-lg));
|
|
282
|
+
}
|
|
283
|
+
:host-state([variant="card"]):has(.card-icon:empty) {
|
|
284
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
285
|
+
}
|
|
286
|
+
.card-icon {
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: flex-start;
|
|
289
|
+
}
|
|
290
|
+
.card-icon:empty {
|
|
291
|
+
display: none;
|
|
292
|
+
}
|
|
293
|
+
.card-content {
|
|
294
|
+
min-inline-size: 0;
|
|
295
|
+
}
|
|
296
|
+
.card-action {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
align-self: center;
|
|
300
|
+
}
|
|
301
|
+
|
|
236
302
|
/* A link is never :enabled, so interactive states test "not unavailable" instead: for a button that
|
|
237
303
|
is exactly :enabled, at the same specificity, and it also matches a link that can be followed. */
|
|
238
304
|
:host:hover:not(:disabled, [aria-disabled="true"]) {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<template component="ui-image" controller="./ui-image.js">
|
|
2
|
+
<defs>
|
|
3
|
+
<prop name="src" type="string" default="">The image source, used by the default media.</prop>
|
|
4
|
+
<prop name="alt" type="string" default="">A text alternative for the image.</prop>
|
|
5
|
+
<prop name="width" type="integer" nullable
|
|
6
|
+
>Image width in CSS pixels; omitted uses intrinsic size in block/center placement or responsive sizing when wrapping.</prop>
|
|
7
|
+
<prop name="height" type="integer" nullable>Image height in CSS pixels; resizing preserves the intrinsic aspect ratio.</prop>
|
|
8
|
+
<prop name="placement" type="keyword" values="block, center, wrap-left, wrap-right" default="block"
|
|
9
|
+
>Block keeps text below; center centers the image; wrap-left and wrap-right let following text flow beside it.</prop>
|
|
10
|
+
<prop name="selected" type="boolean" default="false">Outlines the media as the current selection.</prop>
|
|
11
|
+
<prop name="resizable" type="boolean" default="false"
|
|
12
|
+
>Shows four corner handles while selected. Drag to resize, or focus a handle and use arrow keys; Shift uses a larger increment.</prop>
|
|
13
|
+
<event name="resize" type="object">
|
|
14
|
+
<prop name="width" type="integer" required></prop>
|
|
15
|
+
<prop name="height" type="integer" required></prop>
|
|
16
|
+
<prop name="phase" type="keyword" values="preview, commit, cancel" required></prop>
|
|
17
|
+
<prop name="trigger" type="keyword" values="pointer, keyboard" required></prop>
|
|
18
|
+
Reports resize intent. Preview is transient; the consumer persists a commit. Escape and pointer cancellation restore the original size.
|
|
19
|
+
</event>
|
|
20
|
+
</defs>
|
|
21
|
+
<figure>
|
|
22
|
+
<div class="frame" $ref="frame">
|
|
23
|
+
<slot name="media"><img from:src="src" from:alt="alt" from:width="width" from:height="height"></slot>
|
|
24
|
+
<div class="handles" from:hidden="not (selected and resizable)">
|
|
25
|
+
<button type="button" class="handle" data-corner="top-left" aria-label="Resize image from top left"></button>
|
|
26
|
+
<button type="button" class="handle" data-corner="top-right" aria-label="Resize image from top right"></button>
|
|
27
|
+
<button type="button" class="handle" data-corner="bottom-left" aria-label="Resize image from bottom left"></button>
|
|
28
|
+
<button type="button" class="handle" data-corner="bottom-right" aria-label="Resize image from bottom right"></button>
|
|
29
|
+
</div>
|
|
30
|
+
<slot name="controls"></slot>
|
|
31
|
+
</div>
|
|
32
|
+
</figure>
|
|
33
|
+
<style>
|
|
34
|
+
:host {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: flex-start;
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
max-inline-size: 100%;
|
|
39
|
+
min-inline-size: 0;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding: var(--ui-space-3) 0;
|
|
42
|
+
}
|
|
43
|
+
:host-state([placement="center"]) {
|
|
44
|
+
justify-content: center;
|
|
45
|
+
}
|
|
46
|
+
:host-state([placement="wrap-left"]) {
|
|
47
|
+
float: left;
|
|
48
|
+
padding-inline-end: var(--ui-space-4);
|
|
49
|
+
}
|
|
50
|
+
:host-state([placement="wrap-right"]) {
|
|
51
|
+
float: right;
|
|
52
|
+
padding-inline-start: var(--ui-space-4);
|
|
53
|
+
}
|
|
54
|
+
:host:is(:host-state([placement="wrap-left"]), :host-state([placement="wrap-right"])) {
|
|
55
|
+
inline-size: calc(var(--_ui-image-width, calc(100% / 3)) + var(--ui-space-4));
|
|
56
|
+
}
|
|
57
|
+
:host:is(:host-state([placement="wrap-left"]), :host-state([placement="wrap-right"])) .frame {
|
|
58
|
+
inline-size: 100%;
|
|
59
|
+
}
|
|
60
|
+
:host:is(:host-state([placement="wrap-left"]), :host-state([placement="wrap-right"])) .frame img,
|
|
61
|
+
:host:is(:host-state([placement="wrap-left"]), :host-state([placement="wrap-right"])) .frame :slotted(img) {
|
|
62
|
+
inline-size: 100%;
|
|
63
|
+
}
|
|
64
|
+
.frame {
|
|
65
|
+
position: relative;
|
|
66
|
+
flex: 0 1 auto;
|
|
67
|
+
inline-size: var(--_ui-image-width, fit-content);
|
|
68
|
+
max-inline-size: 100%;
|
|
69
|
+
min-inline-size: 0;
|
|
70
|
+
align-self: flex-start;
|
|
71
|
+
border-radius: var(--ui-radius-md);
|
|
72
|
+
}
|
|
73
|
+
.frame img, .frame :slotted(img) {
|
|
74
|
+
display: block;
|
|
75
|
+
max-inline-size: 100%;
|
|
76
|
+
block-size: auto;
|
|
77
|
+
margin: 0;
|
|
78
|
+
border-radius: inherit;
|
|
79
|
+
}
|
|
80
|
+
:host[data-resizing] .frame img, :host[data-resizing] .frame :slotted(img) {
|
|
81
|
+
inline-size: 100%;
|
|
82
|
+
}
|
|
83
|
+
:host-state([selected]) .frame {
|
|
84
|
+
outline: var(--ui-focus-width) solid var(--ui-focus-ring);
|
|
85
|
+
outline-offset: var(--ui-border-width);
|
|
86
|
+
}
|
|
87
|
+
.handle {
|
|
88
|
+
position: absolute;
|
|
89
|
+
z-index: 1;
|
|
90
|
+
inline-size: 24px;
|
|
91
|
+
block-size: 24px;
|
|
92
|
+
padding: 0;
|
|
93
|
+
border: 0;
|
|
94
|
+
background: transparent;
|
|
95
|
+
touch-action: none;
|
|
96
|
+
}
|
|
97
|
+
.handle::after {
|
|
98
|
+
content: "";
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset: 8px;
|
|
101
|
+
background: var(--ui-surface);
|
|
102
|
+
border: var(--ui-border-width) solid var(--ui-focus-ring);
|
|
103
|
+
border-radius: var(--ui-radius-sm);
|
|
104
|
+
}
|
|
105
|
+
.handle:focus-visible {
|
|
106
|
+
outline: var(--ui-focus-width) solid var(--ui-focus-ring);
|
|
107
|
+
border-radius: var(--ui-radius-sm);
|
|
108
|
+
}
|
|
109
|
+
.handle[data-corner="top-left"] {
|
|
110
|
+
top: -12px;
|
|
111
|
+
left: -12px;
|
|
112
|
+
cursor: nwse-resize;
|
|
113
|
+
}
|
|
114
|
+
.handle[data-corner="top-right"] {
|
|
115
|
+
top: -12px;
|
|
116
|
+
right: -12px;
|
|
117
|
+
cursor: nesw-resize;
|
|
118
|
+
}
|
|
119
|
+
.handle[data-corner="bottom-left"] {
|
|
120
|
+
bottom: -12px;
|
|
121
|
+
left: -12px;
|
|
122
|
+
cursor: nesw-resize;
|
|
123
|
+
}
|
|
124
|
+
.handle[data-corner="bottom-right"] {
|
|
125
|
+
bottom: -12px;
|
|
126
|
+
right: -12px;
|
|
127
|
+
cursor: nwse-resize;
|
|
128
|
+
}
|
|
129
|
+
.handles[hidden], :host[hidden] {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
@media (max-width: 767px) {
|
|
133
|
+
:host:is(:host-state([placement="wrap-left"]), :host-state([placement="wrap-right"])) {
|
|
134
|
+
float: none;
|
|
135
|
+
inline-size: 100%;
|
|
136
|
+
padding-inline: 0;
|
|
137
|
+
}
|
|
138
|
+
:host:is(:host-state([placement="wrap-left"]), :host-state([placement="wrap-right"])) .handles {
|
|
139
|
+
display: none;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
@media (pointer: coarse) {
|
|
143
|
+
.handle {
|
|
144
|
+
inline-size: 44px;
|
|
145
|
+
block-size: 44px;
|
|
146
|
+
}
|
|
147
|
+
.handle::after {
|
|
148
|
+
inset: 18px;
|
|
149
|
+
}
|
|
150
|
+
.handle[data-corner="top-left"] {
|
|
151
|
+
top: -22px;
|
|
152
|
+
left: -22px;
|
|
153
|
+
}
|
|
154
|
+
.handle[data-corner="top-right"] {
|
|
155
|
+
top: -22px;
|
|
156
|
+
right: -22px;
|
|
157
|
+
}
|
|
158
|
+
.handle[data-corner="bottom-left"] {
|
|
159
|
+
bottom: -22px;
|
|
160
|
+
left: -22px;
|
|
161
|
+
}
|
|
162
|
+
.handle[data-corner="bottom-right"] {
|
|
163
|
+
bottom: -22px;
|
|
164
|
+
right: -22px;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
@media (forced-colors: active) {
|
|
168
|
+
:host-state([selected]) .frame, .handle:focus-visible {
|
|
169
|
+
outline-color: Highlight;
|
|
170
|
+
}
|
|
171
|
+
.handle::after {
|
|
172
|
+
border-color: Highlight;
|
|
173
|
+
background: Canvas;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
177
|
+
</template>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Owns transient resize geometry; consumers own persistence and selection.
|
|
2
|
+
* @lifecycle Pointer capture ends on release, Escape, cancellation, or teardown.
|
|
3
|
+
* Only release commits a drag, so consumers can make it one undo operation.
|
|
4
|
+
*/
|
|
5
|
+
export default function controller(host) {
|
|
6
|
+
const root = host.element;
|
|
7
|
+
const frame = host.refs.frame;
|
|
8
|
+
const document = root.ownerDocument;
|
|
9
|
+
let drag = null;
|
|
10
|
+
const image = () => frame.querySelector("img");
|
|
11
|
+
const enabled = () => host.props.selected.value && host.props.resizable.value;
|
|
12
|
+
const measure = () => {
|
|
13
|
+
const media = image();
|
|
14
|
+
const rect = media.getBoundingClientRect();
|
|
15
|
+
return { width: rect.width, height: rect.height, ratio: (media.naturalHeight || rect.height || 1) / (media.naturalWidth || rect.width || 1) };
|
|
16
|
+
};
|
|
17
|
+
const dimensions = (width, ratio) => {
|
|
18
|
+
// The containing document bounds the media even when this figure floats.
|
|
19
|
+
const container = root.parentElement;
|
|
20
|
+
const style = document.defaultView.getComputedStyle(container);
|
|
21
|
+
const available = container.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
22
|
+
const gap = parseFloat(document.defaultView.getComputedStyle(root).paddingLeft) + parseFloat(document.defaultView.getComputedStyle(root).paddingRight);
|
|
23
|
+
const maximum = Math.max(1, available - gap);
|
|
24
|
+
const next = Math.round(Math.min(maximum, Math.max(Math.min(32, maximum), width)));
|
|
25
|
+
return { width: next, height: Math.max(1, Math.round(next * ratio)) };
|
|
26
|
+
};
|
|
27
|
+
const restore = () => {
|
|
28
|
+
root.removeAttribute("data-resizing");
|
|
29
|
+
const width = host.props.width.value;
|
|
30
|
+
if (width > 0) root.style.setProperty("--_ui-image-width", `${width}px`);
|
|
31
|
+
else root.style.removeProperty("--_ui-image-width");
|
|
32
|
+
};
|
|
33
|
+
const dispatch = (size, phase, trigger) => host.dispatch("resize", { ...size, phase, trigger });
|
|
34
|
+
const finish = (phase) => {
|
|
35
|
+
if (!drag) return;
|
|
36
|
+
const current = drag;
|
|
37
|
+
drag = null;
|
|
38
|
+
document.removeEventListener("keydown", onEscape, true);
|
|
39
|
+
if (current.handle.hasPointerCapture(current.pointer)) current.handle.releasePointerCapture(current.pointer);
|
|
40
|
+
if (phase === "commit" && !current.moved) phase = "cancel";
|
|
41
|
+
restore();
|
|
42
|
+
dispatch(phase === "commit" ? current.size : { width: Math.round(current.width), height: Math.round(current.height) }, phase, "pointer");
|
|
43
|
+
};
|
|
44
|
+
const onDown = (event) => {
|
|
45
|
+
const handle = event.target.closest("[data-corner]");
|
|
46
|
+
if (!enabled() || !handle || !root.contains(handle) || event.button !== 0 || drag) return;
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
const measured = measure();
|
|
49
|
+
drag = { ...measured, x: event.clientX, y: event.clientY, handle, pointer: event.pointerId, size: dimensions(measured.width, measured.ratio) };
|
|
50
|
+
handle.setPointerCapture(event.pointerId);
|
|
51
|
+
document.addEventListener("keydown", onEscape, true);
|
|
52
|
+
root.setAttribute("data-resizing", "");
|
|
53
|
+
};
|
|
54
|
+
const onMove = (event) => {
|
|
55
|
+
if (!drag || drag.pointer !== event.pointerId) return;
|
|
56
|
+
const corner = drag.handle.dataset.corner;
|
|
57
|
+
const dx = (event.clientX - drag.x) * (corner.endsWith("left") ? -1 : 1);
|
|
58
|
+
const dy = (event.clientY - drag.y) * (corner.startsWith("top") ? -1 : 1);
|
|
59
|
+
// Project onto the aspect-ratio diagonal: either axis can drive a corner.
|
|
60
|
+
const delta = (dx + drag.ratio * dy) / (1 + drag.ratio * drag.ratio);
|
|
61
|
+
drag.size = dimensions(drag.width + delta, drag.ratio);
|
|
62
|
+
drag.moved = drag.size.width !== Math.round(drag.width);
|
|
63
|
+
root.style.setProperty("--_ui-image-width", `${drag.size.width}px`);
|
|
64
|
+
dispatch(drag.size, "preview", "pointer");
|
|
65
|
+
};
|
|
66
|
+
const onUp = event => { if (drag?.pointer === event.pointerId) finish("commit"); };
|
|
67
|
+
const onCancel = event => { if (drag?.pointer === event.pointerId) finish("cancel"); };
|
|
68
|
+
const onEscape = (event) => {
|
|
69
|
+
if (event.key !== "Escape" || !drag) return;
|
|
70
|
+
event.preventDefault(); event.stopPropagation(); finish("cancel");
|
|
71
|
+
};
|
|
72
|
+
const onKey = (event) => {
|
|
73
|
+
const handle = event.target.closest?.("[data-corner]");
|
|
74
|
+
if (!enabled() || !handle || !root.contains(handle) || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
75
|
+
event.preventDefault(); event.stopPropagation();
|
|
76
|
+
const measured = measure();
|
|
77
|
+
const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight";
|
|
78
|
+
const backwards = event.key === "ArrowLeft" || event.key === "ArrowUp";
|
|
79
|
+
const reverse = horizontal ? handle.dataset.corner.endsWith("left") : handle.dataset.corner.startsWith("top");
|
|
80
|
+
const amount = (backwards !== reverse ? -1 : 1) * (event.shiftKey ? 32 : 8);
|
|
81
|
+
dispatch(dimensions(measured.width + amount / (horizontal ? 1 : measured.ratio), measured.ratio), "commit", "keyboard");
|
|
82
|
+
};
|
|
83
|
+
const stop = host.effect(() => {
|
|
84
|
+
// Read all external geometry before deciding whether to abandon a preview.
|
|
85
|
+
host.props.width.value; host.props.height.value; host.props.placement.value;
|
|
86
|
+
if (drag) finish("cancel");
|
|
87
|
+
restore();
|
|
88
|
+
enabled();
|
|
89
|
+
});
|
|
90
|
+
root.addEventListener("pointerdown", onDown);
|
|
91
|
+
root.addEventListener("pointermove", onMove);
|
|
92
|
+
root.addEventListener("pointerup", onUp);
|
|
93
|
+
root.addEventListener("pointercancel", onCancel);
|
|
94
|
+
root.addEventListener("lostpointercapture", onCancel);
|
|
95
|
+
root.addEventListener("keydown", onKey);
|
|
96
|
+
return () => {
|
|
97
|
+
stop();
|
|
98
|
+
if (drag) { const active = drag; drag = null; if (active.handle.hasPointerCapture(active.pointer)) active.handle.releasePointerCapture(active.pointer); }
|
|
99
|
+
root.removeEventListener("pointerdown", onDown);
|
|
100
|
+
root.removeEventListener("pointermove", onMove);
|
|
101
|
+
root.removeEventListener("pointerup", onUp);
|
|
102
|
+
root.removeEventListener("pointercancel", onCancel);
|
|
103
|
+
root.removeEventListener("lostpointercapture", onCancel);
|
|
104
|
+
root.removeEventListener("keydown", onKey);
|
|
105
|
+
document.removeEventListener("keydown", onEscape, true);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -104,7 +104,7 @@ export default function controller(host) {
|
|
|
104
104
|
const onClick = (event) => select(menuItemFrom(event.target), triggerFor(event));
|
|
105
105
|
const onKeydown = (event) => {
|
|
106
106
|
if (event.key === "Escape") {
|
|
107
|
-
if (host.props.inline.value) return;
|
|
107
|
+
if (host.props.inline.value || event.defaultPrevented) return;
|
|
108
108
|
event.preventDefault();
|
|
109
109
|
requestTopOverlayClose(document, "escape", "keyboard");
|
|
110
110
|
return;
|
|
@@ -81,7 +81,10 @@ export default function controller(host) {
|
|
|
81
81
|
lastOpen = open;
|
|
82
82
|
};
|
|
83
83
|
const onKeydown = (event) => {
|
|
84
|
-
if (event.key === "Escape"
|
|
84
|
+
if (event.key === "Escape" && !event.defaultPrevented) {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
requestTopOverlayClose(document, "escape", "keyboard");
|
|
87
|
+
}
|
|
85
88
|
};
|
|
86
89
|
element.addEventListener("keydown", onKeydown);
|
|
87
90
|
const stop = host.effect(apply);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<template component="ui-separator">
|
|
2
2
|
<defs>
|
|
3
|
+
<prop name="emphasis" type="keyword" values="normal, strong" default="normal"
|
|
4
|
+
>Normal uses the decorative divider color; strong uses the stronger enclosing-edge color for clearer separation.</prop>
|
|
3
5
|
<prop name="orientation" type="keyword" values="horizontal, vertical" default="horizontal"
|
|
4
6
|
>Whether the separator divides stacked content (horizontal) or inline content (vertical).</prop>
|
|
7
|
+
<prop name="selectable" type="boolean" default="false">Gives a document divider a pointer-sized hit area. The editor owns selection and deletion.</prop>
|
|
8
|
+
<prop name="selected" type="boolean" default="false">Shows the selected document divider. Selection does not change separator semantics.</prop>
|
|
5
9
|
</defs>
|
|
6
10
|
<hr from:aria-orientation="orientation">
|
|
7
11
|
<style>
|
|
@@ -17,13 +21,33 @@
|
|
|
17
21
|
align-self: stretch;
|
|
18
22
|
margin: 0;
|
|
19
23
|
border: 0;
|
|
20
|
-
border-block-start: var(--ui-border-width) solid var(--ui-separator-color, var(--ui-border-divider));
|
|
24
|
+
border-block-start: var(--ui-border-width) solid var(--ui-separator-color, var(--_separator-color, var(--ui-border-divider)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:host-state([emphasis="strong"]) {
|
|
28
|
+
--_separator-color: var(--ui-border-strong);
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
:host[aria-orientation="vertical"] {
|
|
24
32
|
block-size: auto;
|
|
25
33
|
border-block-start: 0;
|
|
26
|
-
border-inline-start: var(--ui-border-width) solid var(--ui-separator-color, var(--ui-border-divider));
|
|
34
|
+
border-inline-start: var(--ui-border-width) solid var(--ui-separator-color, var(--_separator-color, var(--ui-border-divider)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:host-state([selectable]) {
|
|
38
|
+
min-block-size: var(--ui-control-size-sm);
|
|
39
|
+
border: 0;
|
|
40
|
+
background: linear-gradient(var(--ui-separator-color, var(--ui-border-divider)), var(--ui-separator-color, var(--ui-border-divider))) center / 100%
|
|
41
|
+
var(--ui-border-width) no-repeat;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
}
|
|
44
|
+
:host-state([selectable]):hover {
|
|
45
|
+
background-color: var(--ui-surface-hover);
|
|
46
|
+
}
|
|
47
|
+
:host-state([selected]) {
|
|
48
|
+
outline: var(--ui-focus-width) solid var(--ui-focus-ring);
|
|
49
|
+
outline-offset: var(--ui-focus-width);
|
|
50
|
+
border-radius: var(--ui-radius-sm);
|
|
27
51
|
}
|
|
28
52
|
</style>
|
|
29
53
|
</template>
|