@backmatter/studio 0.0.16 → 0.0.18
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/dist/chunks/{UiKitApp-_EDqWID3.js → UiKitApp-DkuQBiQZ.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DVvb3N-V.js} +27489 -26622
- package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-B4FITg6c.js} +2 -2
- package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
- package/dist/chunks/{index-D_BHF_8q.js → index-CegtXnEI.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/define.js +1 -1
- package/dist/index.js +4 -4
- package/dist/site-widget.css +45 -1
- package/dist/site-widget.js +2799 -2791
- package/dist/studio.css +113 -15
- package/package.json +2 -2
- package/src/api.ts +22 -0
- package/src/chat/ChatProvider.tsx +6 -0
- package/src/chat/Composer.tsx +3 -1
- package/src/chrome/Toast.tsx +115 -0
- package/src/chrome/WorkspaceStatusBar.tsx +12 -3
- package/src/chrome/commit-error.ts +13 -0
- package/src/chrome/pull-failure.ts +61 -0
- package/src/{pages/code-session-routes.ts → content-routes.ts} +6 -6
- package/src/fields/FieldControl.tsx +771 -0
- package/src/fields/LinkFieldInput.tsx +315 -0
- package/src/fields/choose-field-input.ts +69 -0
- package/src/fields/link-targets.ts +60 -0
- package/src/fields/load-relation-documents.ts +35 -0
- package/src/fields/omit-field.ts +12 -0
- package/src/fields/relation-targets.ts +44 -0
- package/src/fields/resolve-object-fields.ts +21 -0
- package/src/host-path.ts +13 -0
- package/src/pages/BlockSettingsDialog.tsx +19 -115
- package/src/pages/BlocksList.tsx +43 -35
- package/src/pages/BodyProse.tsx +16 -1
- package/src/pages/CodeSessionPage.tsx +186 -248
- package/src/pages/DocEditorPage.tsx +168 -213
- package/src/pages/MediaFieldInput.tsx +609 -26
- package/src/pages/MediaGalleryPage.tsx +14 -3
- package/src/pages/PreviewAddress.tsx +263 -0
- package/src/pages/RenderedBody.tsx +17 -2
- package/src/pages/branch-notice.ts +16 -0
- package/src/pages/code-session-boot.ts +0 -12
- package/src/pages/code-session-context.tsx +2 -0
- package/src/pages/media-selection.ts +79 -0
- package/src/shell-resets.css +15 -0
- package/src/ui/Input.tsx +178 -20
- package/src/ui/popup.ts +11 -2
|
@@ -43,9 +43,13 @@ const CELL = 168;
|
|
|
43
43
|
export function MediaGalleryPage({
|
|
44
44
|
pick = false,
|
|
45
45
|
onPick,
|
|
46
|
+
pickedUrls,
|
|
47
|
+
onToggleUrl,
|
|
46
48
|
}: {
|
|
47
49
|
pick?: boolean;
|
|
48
50
|
onPick?: (url: string) => void;
|
|
51
|
+
pickedUrls?: string[];
|
|
52
|
+
onToggleUrl?: (url: string) => void;
|
|
49
53
|
}) {
|
|
50
54
|
const { workspace } = useOutletContext<WorkspaceOutletContext>();
|
|
51
55
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
@@ -168,7 +172,6 @@ export function MediaGalleryPage({
|
|
|
168
172
|
}, [uploadFiles]);
|
|
169
173
|
|
|
170
174
|
useEffect(() => {
|
|
171
|
-
if (pick) return;
|
|
172
175
|
const hasFiles = (event: DragEvent) =>
|
|
173
176
|
Array.from(event.dataTransfer?.types ?? []).includes("Files");
|
|
174
177
|
const onEnter = (event: DragEvent) => {
|
|
@@ -202,7 +205,7 @@ export function MediaGalleryPage({
|
|
|
202
205
|
window.removeEventListener("dragleave", onLeave);
|
|
203
206
|
window.removeEventListener("drop", onDrop);
|
|
204
207
|
};
|
|
205
|
-
}, [
|
|
208
|
+
}, [uploadFiles]);
|
|
206
209
|
|
|
207
210
|
const browsable = [
|
|
208
211
|
...state.pending.flatMap((item) => (item.media ? [item.media] : [])),
|
|
@@ -479,10 +482,18 @@ export function MediaGalleryPage({
|
|
|
479
482
|
<MediaHit
|
|
480
483
|
item={cell.item}
|
|
481
484
|
menu={!pick}
|
|
482
|
-
selected={
|
|
485
|
+
selected={
|
|
486
|
+
onToggleUrl
|
|
487
|
+
? (pickedUrls?.includes(cell.item.url) ?? false)
|
|
488
|
+
: !pick && !sharedId && cell.item.id === selectedId
|
|
489
|
+
}
|
|
483
490
|
canDelete={canDelete}
|
|
484
491
|
onOpen={() => {
|
|
485
492
|
setSelectedId(null);
|
|
493
|
+
if (onToggleUrl) {
|
|
494
|
+
onToggleUrl(cell.item.url);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
486
497
|
if (pick) {
|
|
487
498
|
if (onPick) onPick(cell.item.url);
|
|
488
499
|
return;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// packages/studio/src/pages/PreviewAddress.tsx
|
|
2
|
+
import { useEffect, useRef, useState, type FormEvent } from "react";
|
|
3
|
+
import { Combobox } from "@base-ui/react/combobox";
|
|
4
|
+
import {
|
|
5
|
+
IconArrowRotateClockwise,
|
|
6
|
+
IconArrowUpRight,
|
|
7
|
+
IconGlobe,
|
|
8
|
+
} from "@central-icons-react/round-outlined-radius-2-stroke-1.5";
|
|
9
|
+
import * as stylex from "@stylexjs/stylex";
|
|
10
|
+
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
11
|
+
import { cx, popup, popupClassName } from "../ui/popup";
|
|
12
|
+
import { hostPath } from "../host-path";
|
|
13
|
+
import { routesMatching } from "../content-routes";
|
|
14
|
+
|
|
15
|
+
const hover = "@media (hover: hover)";
|
|
16
|
+
|
|
17
|
+
/** Reload, the page path, and open. The path opens the route list. */
|
|
18
|
+
export function PreviewAddress({
|
|
19
|
+
path,
|
|
20
|
+
enabled,
|
|
21
|
+
routes,
|
|
22
|
+
routesPending,
|
|
23
|
+
onOpenRoutes,
|
|
24
|
+
onPath,
|
|
25
|
+
onReload,
|
|
26
|
+
onOpen,
|
|
27
|
+
}: {
|
|
28
|
+
path: string;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
routes: readonly string[];
|
|
31
|
+
routesPending: boolean;
|
|
32
|
+
onOpenRoutes?: () => void;
|
|
33
|
+
onPath: (path: string) => void;
|
|
34
|
+
onReload: () => void;
|
|
35
|
+
onOpen: () => void;
|
|
36
|
+
}) {
|
|
37
|
+
const [query, setQuery] = useState<string | null>(null);
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const picked = useRef(false);
|
|
40
|
+
const fieldRef = useRef<HTMLButtonElement>(null);
|
|
41
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
42
|
+
const shown = query ? routesMatching(routes, query) : [...routes];
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!open) return;
|
|
46
|
+
const input = inputRef.current;
|
|
47
|
+
input?.focus();
|
|
48
|
+
input?.select();
|
|
49
|
+
}, [open]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div {...stylex.props(styles.address)}>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
aria-label="Reload preview"
|
|
56
|
+
disabled={!enabled}
|
|
57
|
+
onClick={onReload}
|
|
58
|
+
{...stylex.props(styles.iconButton)}
|
|
59
|
+
>
|
|
60
|
+
<IconArrowRotateClockwise size={14} ariaHidden />
|
|
61
|
+
</button>
|
|
62
|
+
<Combobox.Root
|
|
63
|
+
items={shown}
|
|
64
|
+
filteredItems={shown}
|
|
65
|
+
inputValue={query ?? ""}
|
|
66
|
+
disabled={!enabled}
|
|
67
|
+
open={open}
|
|
68
|
+
value={routes.includes(path) ? path : null}
|
|
69
|
+
onOpenChange={(next) => {
|
|
70
|
+
if (!enabled && next) return;
|
|
71
|
+
setOpen(next);
|
|
72
|
+
if (next) onOpenRoutes?.();
|
|
73
|
+
else setQuery(null);
|
|
74
|
+
}}
|
|
75
|
+
onInputValueChange={(value, details) => {
|
|
76
|
+
if (details.reason === "input-change" || details.reason === "input-clear") {
|
|
77
|
+
setQuery(value);
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
onValueChange={(value) => {
|
|
81
|
+
if (typeof value !== "string") return;
|
|
82
|
+
picked.current = true;
|
|
83
|
+
setQuery(null);
|
|
84
|
+
onPath(hostPath(value));
|
|
85
|
+
queueMicrotask(() => {
|
|
86
|
+
picked.current = false;
|
|
87
|
+
});
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<Combobox.Trigger
|
|
91
|
+
ref={fieldRef}
|
|
92
|
+
aria-label="Preview path"
|
|
93
|
+
disabled={!enabled}
|
|
94
|
+
data-no-ring
|
|
95
|
+
className={cx(styles.addressTrigger)}
|
|
96
|
+
>
|
|
97
|
+
<IconGlobe size={13} ariaHidden />
|
|
98
|
+
<span {...stylex.props(styles.addressPath)}>{path}</span>
|
|
99
|
+
</Combobox.Trigger>
|
|
100
|
+
<Combobox.Portal>
|
|
101
|
+
<Combobox.Positioner
|
|
102
|
+
anchor={fieldRef}
|
|
103
|
+
side="bottom"
|
|
104
|
+
align="start"
|
|
105
|
+
sideOffset={6}
|
|
106
|
+
className={cx(popup.positioner)}
|
|
107
|
+
>
|
|
108
|
+
<Combobox.Popup
|
|
109
|
+
aria-label="Pages"
|
|
110
|
+
className={(state) => popupClassName(state, styles.routePopup)}
|
|
111
|
+
>
|
|
112
|
+
<form
|
|
113
|
+
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
const fromList = picked.current;
|
|
116
|
+
picked.current = false;
|
|
117
|
+
if (fromList) return;
|
|
118
|
+
const typed = event.currentTarget.querySelector("input")?.value;
|
|
119
|
+
if (typed?.trim()) onPath(hostPath(typed));
|
|
120
|
+
setOpen(false);
|
|
121
|
+
setQuery(null);
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<Combobox.Input
|
|
125
|
+
ref={inputRef}
|
|
126
|
+
placeholder="Find a page"
|
|
127
|
+
aria-label="Find a page"
|
|
128
|
+
spellCheck={false}
|
|
129
|
+
autoComplete="off"
|
|
130
|
+
data-no-ring
|
|
131
|
+
className={cx(popup.quietInput, styles.routeSearch)}
|
|
132
|
+
onKeyDown={(event) => {
|
|
133
|
+
if (event.key !== "Enter") return;
|
|
134
|
+
// A highlighted row is chosen by the combobox. A bare Enter goes to the typed path.
|
|
135
|
+
if (event.currentTarget.getAttribute("aria-activedescendant")) return;
|
|
136
|
+
const typed = event.currentTarget.value.trim();
|
|
137
|
+
if (!typed) return;
|
|
138
|
+
picked.current = true;
|
|
139
|
+
onPath(hostPath(typed));
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</form>
|
|
143
|
+
<div {...stylex.props(popup.separator)} />
|
|
144
|
+
<Combobox.Empty className={cx(popup.empty)}>
|
|
145
|
+
{routesPending ? "Loading pages" : "No pages"}
|
|
146
|
+
</Combobox.Empty>
|
|
147
|
+
<Combobox.List className={cx(styles.routeList)}>
|
|
148
|
+
{(route: string) => (
|
|
149
|
+
<Combobox.Item
|
|
150
|
+
key={route}
|
|
151
|
+
value={route}
|
|
152
|
+
className={(state) =>
|
|
153
|
+
cx(
|
|
154
|
+
popup.item,
|
|
155
|
+
styles.routeItem,
|
|
156
|
+
state.highlighted && popup.itemHighlighted,
|
|
157
|
+
state.selected && popup.itemSelected,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
{route}
|
|
162
|
+
</Combobox.Item>
|
|
163
|
+
)}
|
|
164
|
+
</Combobox.List>
|
|
165
|
+
</Combobox.Popup>
|
|
166
|
+
</Combobox.Positioner>
|
|
167
|
+
</Combobox.Portal>
|
|
168
|
+
</Combobox.Root>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
aria-label="Open preview in a new tab"
|
|
172
|
+
disabled={!enabled}
|
|
173
|
+
onClick={onOpen}
|
|
174
|
+
{...stylex.props(styles.iconButton)}
|
|
175
|
+
>
|
|
176
|
+
<IconArrowUpRight size={14} ariaHidden />
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const styles = stylex.create({
|
|
183
|
+
address: {
|
|
184
|
+
display: "flex",
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
gap: space.xs,
|
|
187
|
+
minWidth: 0,
|
|
188
|
+
},
|
|
189
|
+
addressTrigger: {
|
|
190
|
+
display: "flex",
|
|
191
|
+
flex: 1,
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
gap: space.xs,
|
|
194
|
+
minWidth: 0,
|
|
195
|
+
height: "28px",
|
|
196
|
+
paddingInline: space.sm,
|
|
197
|
+
borderWidth: 0,
|
|
198
|
+
borderRadius: "999px",
|
|
199
|
+
font: "inherit",
|
|
200
|
+
textAlign: "left",
|
|
201
|
+
cursor: { default: "pointer", ":disabled": "default" },
|
|
202
|
+
color: { default: colors.textMuted, ":disabled": colors.textMuted },
|
|
203
|
+
backgroundColor: colors.bgPanelInset,
|
|
204
|
+
":focus-visible": {
|
|
205
|
+
outlineWidth: 2,
|
|
206
|
+
outlineStyle: "solid",
|
|
207
|
+
outlineColor: colors.focus,
|
|
208
|
+
outlineOffset: 1,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
addressPath: {
|
|
212
|
+
flex: 1,
|
|
213
|
+
minWidth: 0,
|
|
214
|
+
overflow: "hidden",
|
|
215
|
+
textOverflow: "ellipsis",
|
|
216
|
+
whiteSpace: "nowrap",
|
|
217
|
+
fontFamily: type.mono,
|
|
218
|
+
fontSize: type.xs,
|
|
219
|
+
color: colors.text,
|
|
220
|
+
},
|
|
221
|
+
routeSearch: {
|
|
222
|
+
fontFamily: type.mono,
|
|
223
|
+
fontSize: type.xs,
|
|
224
|
+
},
|
|
225
|
+
routePopup: {
|
|
226
|
+
width: "var(--anchor-width)",
|
|
227
|
+
minWidth: "220px",
|
|
228
|
+
maxHeight: "min(320px, var(--available-height))",
|
|
229
|
+
display: "flex",
|
|
230
|
+
flexDirection: "column",
|
|
231
|
+
overflowY: "hidden",
|
|
232
|
+
},
|
|
233
|
+
routeList: {
|
|
234
|
+
minHeight: 0,
|
|
235
|
+
overflowY: "auto",
|
|
236
|
+
},
|
|
237
|
+
routeItem: {
|
|
238
|
+
fontFamily: type.mono,
|
|
239
|
+
},
|
|
240
|
+
iconButton: {
|
|
241
|
+
display: "inline-flex",
|
|
242
|
+
alignItems: "center",
|
|
243
|
+
justifyContent: "center",
|
|
244
|
+
flexShrink: 0,
|
|
245
|
+
width: "28px",
|
|
246
|
+
height: "28px",
|
|
247
|
+
padding: 0,
|
|
248
|
+
borderWidth: 0,
|
|
249
|
+
borderRadius: radius.sm,
|
|
250
|
+
cursor: { default: "pointer", ":disabled": "default" },
|
|
251
|
+
color: { default: colors.textMuted, ":disabled": colors.border },
|
|
252
|
+
backgroundColor: {
|
|
253
|
+
default: "transparent",
|
|
254
|
+
[hover]: { default: null, ":hover:not(:disabled)": colors.bgHover },
|
|
255
|
+
},
|
|
256
|
+
":focus-visible": {
|
|
257
|
+
outlineWidth: 2,
|
|
258
|
+
outlineStyle: "solid",
|
|
259
|
+
outlineColor: colors.focus,
|
|
260
|
+
outlineOffset: 1,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from "react";
|
|
11
11
|
import * as stylex from "@stylexjs/stylex";
|
|
12
|
-
import type { BlockSnapshot } from "../api";
|
|
12
|
+
import type { BlockSnapshot, SchemaField } from "../api";
|
|
13
13
|
import { contentBlockFields, contentBlockLabel, contentBlockProps } from "../content-blocks";
|
|
14
14
|
import { colors, radius, space } from "../styles/tokens.stylex";
|
|
15
15
|
import {
|
|
@@ -40,16 +40,28 @@ export type RenderedBodyHandle = {
|
|
|
40
40
|
|
|
41
41
|
const BODY_HOST = "body";
|
|
42
42
|
|
|
43
|
+
type ModelRef = { type: string; external?: boolean; fields?: Record<string, SchemaField> };
|
|
44
|
+
type RelationDocumentLists = Record<
|
|
45
|
+
string,
|
|
46
|
+
Array<{ slug: string; frontmatter: Record<string, unknown> }>
|
|
47
|
+
>;
|
|
48
|
+
|
|
43
49
|
export const RenderedBody = forwardRef<
|
|
44
50
|
RenderedBodyHandle,
|
|
45
51
|
{
|
|
46
52
|
value: string;
|
|
47
53
|
catalog?: Array<BlockSnapshot & { label?: string }>;
|
|
54
|
+
models: ModelRef[];
|
|
55
|
+
objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
|
|
56
|
+
documentLists?: RelationDocumentLists;
|
|
48
57
|
readOnly?: boolean;
|
|
49
58
|
labelledBy: string;
|
|
50
59
|
onChange: (value: string) => void;
|
|
51
60
|
}
|
|
52
|
-
>(function RenderedBody(
|
|
61
|
+
>(function RenderedBody(
|
|
62
|
+
{ value, catalog = [], models, objects = [], documentLists, readOnly, labelledBy, onChange },
|
|
63
|
+
ref,
|
|
64
|
+
) {
|
|
53
65
|
const local = useRef(value);
|
|
54
66
|
const hosts = useRef(new Map<string, HTMLElement>());
|
|
55
67
|
const linking = useRef(false);
|
|
@@ -218,6 +230,9 @@ export const RenderedBody = forwardRef<
|
|
|
218
230
|
title={contentBlockLabel(settings.def.name)}
|
|
219
231
|
fields={contentBlockFields(settings.def)}
|
|
220
232
|
value={contentBlockProps(settings.block.markdown, settings.def.name)}
|
|
233
|
+
models={models}
|
|
234
|
+
objects={objects}
|
|
235
|
+
documentLists={documentLists}
|
|
221
236
|
readOnly={readOnly}
|
|
222
237
|
onOpenChange={(open) => {
|
|
223
238
|
if (!open) setSettingsId(null);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// packages/studio/src/pages/branch-notice.ts
|
|
2
|
+
|
|
3
|
+
export type BranchNoticeKind = "update" | "edits" | "fold";
|
|
4
|
+
|
|
5
|
+
/** What to show when this session's commit is behind the branch tip. */
|
|
6
|
+
export function branchNoticeKind(input: {
|
|
7
|
+
baseSha: string | null;
|
|
8
|
+
headSha: string | null;
|
|
9
|
+
editCount: number;
|
|
10
|
+
foldPending: boolean;
|
|
11
|
+
}): BranchNoticeKind | null {
|
|
12
|
+
if (!input.baseSha || !input.headSha || input.baseSha === input.headSha) return null;
|
|
13
|
+
if (input.editCount > 0) return "edits";
|
|
14
|
+
if (input.foldPending) return "fold";
|
|
15
|
+
return "update";
|
|
16
|
+
}
|
|
@@ -29,15 +29,3 @@ export function bootSteps(input: {
|
|
|
29
29
|
if (input.phase === "stopped") return at(0, "failed");
|
|
30
30
|
return at(input.furthest === "starting" ? 2 : input.furthest === "installing" ? 1 : 0, "failed");
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
/** Normalizes what someone types in the address bar to a path inside the host. */
|
|
34
|
-
export function hostPath(input: string): string {
|
|
35
|
-
const trimmed = input.trim();
|
|
36
|
-
if (!trimmed) return "/";
|
|
37
|
-
try {
|
|
38
|
-
const url = new URL(trimmed);
|
|
39
|
-
return `${url.pathname}${url.search}${url.hash}` || "/";
|
|
40
|
-
} catch {
|
|
41
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -13,6 +13,8 @@ export type CodeSessionContextValue = {
|
|
|
13
13
|
sessionId: string;
|
|
14
14
|
editCount: number;
|
|
15
15
|
refresh: () => void;
|
|
16
|
+
/** The checkout is being replaced. Chat waits until the host boots. */
|
|
17
|
+
updating?: boolean;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const CodeSessionContext = createContext<CodeSessionContextValue | null>(null);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// packages/studio/src/pages/media-selection.ts
|
|
2
|
+
|
|
3
|
+
export function toggleMediaUrl(urls: readonly string[], url: string): string[] {
|
|
4
|
+
const index = urls.indexOf(url);
|
|
5
|
+
if (index >= 0) {
|
|
6
|
+
return urls.filter((_, i) => i !== index);
|
|
7
|
+
}
|
|
8
|
+
return [...urls, url];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function appendMediaUrls(
|
|
12
|
+
existing: readonly string[],
|
|
13
|
+
incoming: readonly string[],
|
|
14
|
+
): string[] {
|
|
15
|
+
const next = [...existing];
|
|
16
|
+
for (const url of incoming) {
|
|
17
|
+
if (!next.includes(url)) {
|
|
18
|
+
next.push(url);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function moveMediaUrl(urls: readonly string[], from: number, to: number): string[] {
|
|
25
|
+
if (from < 0 || from >= urls.length || to < 0 || to >= urls.length) {
|
|
26
|
+
return [...urls];
|
|
27
|
+
}
|
|
28
|
+
const next = [...urls];
|
|
29
|
+
const [item] = next.splice(from, 1);
|
|
30
|
+
next.splice(to, 0, item!);
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function acceptedDrop(
|
|
35
|
+
existing: readonly string[],
|
|
36
|
+
accepted: readonly string[],
|
|
37
|
+
multiple: boolean,
|
|
38
|
+
): { urls: string[]; notAdded: number } {
|
|
39
|
+
if (!multiple) {
|
|
40
|
+
if (accepted.length === 0) {
|
|
41
|
+
return { urls: [...existing], notAdded: 0 };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
urls: [accepted[0]!],
|
|
45
|
+
notAdded: Math.max(0, accepted.length - 1),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { urls: appendMediaUrls(existing, accepted), notAdded: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function notAddedMessage(count: number): string | null {
|
|
52
|
+
if (count === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (count === 1) {
|
|
56
|
+
return "1 image was not added";
|
|
57
|
+
}
|
|
58
|
+
return `${count} images were not added`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function editorUrls(
|
|
62
|
+
value: unknown,
|
|
63
|
+
multiple: boolean,
|
|
64
|
+
): { urls: string[]; raw: unknown } {
|
|
65
|
+
if (value === undefined || value === null || value === "") {
|
|
66
|
+
return { urls: [], raw: undefined };
|
|
67
|
+
}
|
|
68
|
+
if (typeof value === "string") {
|
|
69
|
+
return { urls: [value], raw: value };
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
if (multiple) {
|
|
73
|
+
const urls = value.filter((item): item is string => typeof item === "string");
|
|
74
|
+
return { urls, raw: value };
|
|
75
|
+
}
|
|
76
|
+
return { urls: [], raw: value };
|
|
77
|
+
}
|
|
78
|
+
return { urls: [], raw: undefined };
|
|
79
|
+
}
|
package/src/shell-resets.css
CHANGED
|
@@ -150,10 +150,20 @@ html[data-theme="dark"]:has(.backmatter-studio)
|
|
|
150
150
|
outline-color: #fafafa;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
[data-no-ring]:focus,
|
|
153
154
|
[data-no-ring]:focus-visible {
|
|
154
155
|
outline: none;
|
|
156
|
+
outline-offset: 0;
|
|
157
|
+
box-shadow: none;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
:is(
|
|
161
|
+
.backmatter-studio,
|
|
162
|
+
[data-backmatter-panel],
|
|
163
|
+
[data-base-ui-portal],
|
|
164
|
+
[data-selection-bar]
|
|
165
|
+
)
|
|
166
|
+
[data-no-ring]:focus,
|
|
157
167
|
:is(
|
|
158
168
|
.backmatter-studio,
|
|
159
169
|
[data-backmatter-panel],
|
|
@@ -161,8 +171,13 @@ html[data-theme="dark"]:has(.backmatter-studio)
|
|
|
161
171
|
[data-selection-bar]
|
|
162
172
|
)
|
|
163
173
|
[data-no-ring]:focus-visible,
|
|
174
|
+
html[data-theme="dark"]:has(.backmatter-studio)
|
|
175
|
+
:is(.backmatter-studio, [data-base-ui-portal], [data-selection-bar])
|
|
176
|
+
[data-no-ring]:focus,
|
|
164
177
|
html[data-theme="dark"]:has(.backmatter-studio)
|
|
165
178
|
:is(.backmatter-studio, [data-base-ui-portal], [data-selection-bar])
|
|
166
179
|
[data-no-ring]:focus-visible {
|
|
167
180
|
outline: none;
|
|
181
|
+
outline-offset: 0;
|
|
182
|
+
box-shadow: none;
|
|
168
183
|
}
|