@backmatter/studio 0.0.17 → 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-D7vllRQE.js → UiKitApp-DkuQBiQZ.js} +2 -2
- package/dist/chunks/{WorkspaceRoutes-DH1LD-2Q.js → WorkspaceRoutes-DVvb3N-V.js} +21440 -21060
- package/dist/chunks/{dashboard-Dsaeud2x.js → dashboard-B4FITg6c.js} +2 -2
- package/dist/chunks/{index-CGMqw2NH.js → index-CegtXnEI.js} +1 -1
- package/dist/dashboard.js +2 -2
- package/dist/index.js +2 -2
- package/dist/studio.css +20 -14
- package/package.json +2 -2
- package/src/api.ts +3 -0
- package/src/{pages/code-session-routes.ts → content-routes.ts} +6 -6
- package/src/fields/FieldControl.tsx +25 -2
- package/src/fields/LinkFieldInput.tsx +315 -0
- package/src/fields/choose-field-input.ts +5 -0
- package/src/fields/link-targets.ts +60 -0
- package/src/host-path.ts +13 -0
- package/src/pages/CodeSessionPage.tsx +6 -241
- package/src/pages/DocEditorPage.tsx +81 -121
- package/src/pages/MediaFieldInput.tsx +609 -26
- package/src/pages/MediaGalleryPage.tsx +14 -3
- package/src/pages/PreviewAddress.tsx +263 -0
- package/src/pages/code-session-boot.ts +0 -12
- package/src/pages/media-selection.ts +79 -0
|
@@ -3,7 +3,7 @@ var Xe = (e, t, n) => t in e ? Ke(e, t, { enumerable: !0, configurable: !0, writ
|
|
|
3
3
|
var D = (e, t, n) => Xe(e, typeof t != "symbol" ? t + "" : t, n);
|
|
4
4
|
import { jsxs as k, jsx as o, Fragment as ge } from "react/jsx-runtime";
|
|
5
5
|
import { useRef as Ye, useCallback as Qe, useSyncExternalStore as Ze, useState as w, useEffect as V, lazy as et, Suspense as tt } from "react";
|
|
6
|
-
import { U as nt, aj as ve, ak as rt, P as _, al as st, p as it, am as at, an as Ne, ao as Ce, ap as Ie, aq as ot, ar as lt, N as qe, as as ct, at as ie, ad as J, W as ze, au as ut, av as Fe, ae as dt, ag as re, a7 as Q, a8 as Z, V as ae, aw as ft, Q as z, ax as ht, G as be, ay as ee, af as pt, az as gt, aA as mt, aB as xt, f as yt, aC as vt, a3 as bt, aD as wt, aE as kt, w as St, B as Et, F as Rt, aF as _t, aG as Ot, aH as Tt, H as Pt, aI as jt, R as Lt, I as At, J as Ut, K as Nt, L as F, M as Ct, O as It } from "./WorkspaceRoutes-
|
|
6
|
+
import { U as nt, aj as ve, ak as rt, P as _, al as st, p as it, am as at, an as Ne, ao as Ce, ap as Ie, aq as ot, ar as lt, N as qe, as as ct, at as ie, ad as J, W as ze, au as ut, av as Fe, ae as dt, ag as re, a7 as Q, a8 as Z, V as ae, aw as ft, Q as z, ax as ht, G as be, ay as ee, af as pt, az as gt, aA as mt, aB as xt, f as yt, aC as vt, a3 as bt, aD as wt, aE as kt, w as St, B as Et, F as Rt, aF as _t, aG as Ot, aH as Tt, H as Pt, aI as jt, R as Lt, I as At, J as Ut, K as Nt, L as F, M as Ct, O as It } from "./WorkspaceRoutes-DVvb3N-V.js";
|
|
7
7
|
const te = /* @__PURE__ */ Object.create(null), W = (e) => {
|
|
8
8
|
var t, n;
|
|
9
9
|
return ((t = globalThis.process) == null ? void 0 : t.env) || ((n = globalThis.Deno) == null ? void 0 : n.env.toObject()) || globalThis.__env__ || (e ? te : globalThis);
|
|
@@ -1702,7 +1702,7 @@ function kr(e) {
|
|
|
1702
1702
|
const t = e.replace(/\/$/, "") || "/";
|
|
1703
1703
|
return t === "/__ui" || /^\/__ui\/[^/]+$/.test(t);
|
|
1704
1704
|
}
|
|
1705
|
-
const Sr = et(() => import("./UiKitApp-
|
|
1705
|
+
const Sr = et(() => import("./UiKitApp-DkuQBiQZ.js").then((e) => ({
|
|
1706
1706
|
default: e.UiKitApp
|
|
1707
1707
|
})));
|
|
1708
1708
|
function Pr() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
var Rs = Object.defineProperty;
|
|
2
2
|
var Es = (r, e, t) => e in r ? Rs(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
|
|
3
3
|
var M = (r, e, t) => Es(r, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
-
import { aJ as Bt, aK as Bs, aL as a0, aM as ct, aN as xt, aO as ea, aP as Gn, aQ as Is, aR as Ls, aS as Ds, aT as La, aU as Ps, aV as Os, aW as Le, aX as qs } from "./WorkspaceRoutes-
|
|
4
|
+
import { aJ as Bt, aK as Bs, aL as a0, aM as ct, aN as xt, aO as ea, aP as Gn, aQ as Is, aR as Ls, aS as Ds, aT as La, aU as Ps, aV as Os, aW as Le, aX as qs } from "./WorkspaceRoutes-DVvb3N-V.js";
|
|
5
5
|
function Fs() {
|
|
6
6
|
return {
|
|
7
7
|
enter: {
|
package/dist/dashboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as a, i as s } from "./chunks/dashboard-
|
|
2
|
-
import { z as t, A as f } from "./chunks/WorkspaceRoutes-
|
|
1
|
+
import { A as a, i as s } from "./chunks/dashboard-B4FITg6c.js";
|
|
2
|
+
import { z as t, A as f } from "./chunks/WorkspaceRoutes-DVvb3N-V.js";
|
|
3
3
|
export {
|
|
4
4
|
a as DashboardApp,
|
|
5
5
|
t as configureStudio,
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as t, jsxs as u } from "react/jsx-runtime";
|
|
2
2
|
import { useLayoutEffect as ae, useEffect as f, useRef as Q, useState as h, useMemo as se } from "react";
|
|
3
|
-
import { i as X, T as ce, E as le, C as ue, a as de, b as xe, u as me, D as _, c as Y, d as I, e as fe, f as Z, g as G, h as pe, p as E, j as C, k as he, l as ge, m as S, r as we, n as be, s as ye, o as ve, q as ke, t as qe, v as ze, w as Ee, B as Ce, x as Ne, S as Se, y as Re, z as ee, A as U, F as Pe, G as Le, H as Ue, R as je, I as Te, J as Me, K as $e, L as R, N as K, M as De, O as Fe, P as H, Q as Oe } from "./chunks/WorkspaceRoutes-
|
|
4
|
-
import { U as lt } from "./chunks/WorkspaceRoutes-
|
|
3
|
+
import { i as X, T as ce, E as le, C as ue, a as de, b as xe, u as me, D as _, c as Y, d as I, e as fe, f as Z, g as G, h as pe, p as E, j as C, k as he, l as ge, m as S, r as we, n as be, s as ye, o as ve, q as ke, t as qe, v as ze, w as Ee, B as Ce, x as Ne, S as Se, y as Re, z as ee, A as U, F as Pe, G as Le, H as Ue, R as je, I as Te, J as Me, K as $e, L as R, N as K, M as De, O as Fe, P as H, Q as Oe } from "./chunks/WorkspaceRoutes-DVvb3N-V.js";
|
|
4
|
+
import { U as lt } from "./chunks/WorkspaceRoutes-DVvb3N-V.js";
|
|
5
5
|
import { u as Ae, S as Be } from "./chunks/define-studio-DlYjgrwr.js";
|
|
6
6
|
import { d as dt } from "./chunks/define-studio-DlYjgrwr.js";
|
|
7
7
|
const P = {
|
package/dist/studio.css
CHANGED
|
@@ -537,10 +537,6 @@
|
|
|
537
537
|
animation-duration: .2s;
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
-
.xof6966 {
|
|
541
|
-
animation-duration: .7s;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
540
|
.xemfg65 {
|
|
545
541
|
animation-duration: .8s;
|
|
546
542
|
}
|
|
@@ -561,10 +557,6 @@
|
|
|
561
557
|
animation-name: x18re5ia-B;
|
|
562
558
|
}
|
|
563
559
|
|
|
564
|
-
.xpedhrg {
|
|
565
|
-
animation-name: x1fy8ia8-B;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
560
|
.x1bzz7ut {
|
|
569
561
|
animation-name: x1tbvpe3-B;
|
|
570
562
|
}
|
|
@@ -1125,6 +1117,10 @@
|
|
|
1125
1117
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1126
1118
|
}
|
|
1127
1119
|
|
|
1120
|
+
.xbqmlv7 {
|
|
1121
|
+
grid-template-columns: repeat(4, 48px);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1128
1124
|
.x1sipb5g {
|
|
1129
1125
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1130
1126
|
}
|
|
@@ -1974,6 +1970,10 @@
|
|
|
1974
1970
|
bottom: var(--xvc7yhn);
|
|
1975
1971
|
}
|
|
1976
1972
|
|
|
1973
|
+
.xmll18r {
|
|
1974
|
+
height: 1.25rem;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
1977
|
.x5yr21d {
|
|
1978
1978
|
height: 100%;
|
|
1979
1979
|
}
|
|
@@ -2638,6 +2638,10 @@
|
|
|
2638
2638
|
top: var(--x1y1rrep);
|
|
2639
2639
|
}
|
|
2640
2640
|
|
|
2641
|
+
.xwqq7k2 {
|
|
2642
|
+
width: 1.25rem;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2641
2645
|
.xh8yej3 {
|
|
2642
2646
|
width: 100%;
|
|
2643
2647
|
}
|
|
@@ -2738,6 +2742,10 @@
|
|
|
2738
2742
|
width: 4.5rem;
|
|
2739
2743
|
}
|
|
2740
2744
|
|
|
2745
|
+
.x100vrsf {
|
|
2746
|
+
width: 40px;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2741
2749
|
.x187nhsf {
|
|
2742
2750
|
width: 44px;
|
|
2743
2751
|
}
|
|
@@ -2778,6 +2786,10 @@
|
|
|
2778
2786
|
width: calc(min(360px, 38vw) - 48px);
|
|
2779
2787
|
}
|
|
2780
2788
|
|
|
2789
|
+
.xeq5yr9 {
|
|
2790
|
+
width: fit-content;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2781
2793
|
.x3bfboh {
|
|
2782
2794
|
width: min(360px, 38vw);
|
|
2783
2795
|
}
|
|
@@ -3022,12 +3034,6 @@
|
|
|
3022
3034
|
}
|
|
3023
3035
|
}
|
|
3024
3036
|
|
|
3025
|
-
@keyframes x1fy8ia8-B {
|
|
3026
|
-
to {
|
|
3027
|
-
transform: rotate(360deg);
|
|
3028
|
-
}
|
|
3029
|
-
}
|
|
3030
|
-
|
|
3031
3037
|
@keyframes x1wc8ddo-B {
|
|
3032
3038
|
from {
|
|
3033
3039
|
transform: rotate(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "The Backmatter editing surface: document editor, AI chat, publish. Mounted on your site or hosted on the dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@ai-sdk/react": "^4.0.100",
|
|
59
|
-
"@backmatter/core": "^0.0.
|
|
59
|
+
"@backmatter/core": "^0.0.35",
|
|
60
60
|
"@base-ui/react": "^1.6.0",
|
|
61
61
|
"@central-icons-react/round-outlined-radius-2-stroke-1.5": "1.2.15",
|
|
62
62
|
"@dnd-kit/abstract": "^0.5.0",
|
package/src/api.ts
CHANGED
|
@@ -350,8 +350,11 @@ export function unbindGithub(workspaceId: string) {
|
|
|
350
350
|
|
|
351
351
|
export type SchemaField = {
|
|
352
352
|
kind: string;
|
|
353
|
+
multiple?: boolean;
|
|
353
354
|
required?: boolean;
|
|
354
355
|
relationTo?: string;
|
|
356
|
+
to?: "internal" | "external";
|
|
357
|
+
models?: string[];
|
|
355
358
|
object?: string;
|
|
356
359
|
fields?: Record<string, SchemaField>;
|
|
357
360
|
/** Studio input name from `field.text({ input })`; see `defineStudio`. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// packages/studio/src/
|
|
1
|
+
// packages/studio/src/content-routes.ts
|
|
2
2
|
import {
|
|
3
3
|
isRoutable,
|
|
4
4
|
isTemplateSlug,
|
|
@@ -7,15 +7,15 @@ import {
|
|
|
7
7
|
type RouteModel,
|
|
8
8
|
} from "@backmatter/core";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
type ContentFileModel = RouteModel & {
|
|
11
11
|
path: string;
|
|
12
12
|
external?: boolean;
|
|
13
13
|
template?: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
/** Public routes for
|
|
17
|
-
export function
|
|
18
|
-
models: readonly
|
|
16
|
+
/** Public routes for content files. A file that is not a routable document is skipped. */
|
|
17
|
+
export function routesForContentFiles(
|
|
18
|
+
models: readonly ContentFileModel[],
|
|
19
19
|
files: readonly string[],
|
|
20
20
|
): string[] {
|
|
21
21
|
const routes = new Set<string>();
|
|
@@ -35,7 +35,7 @@ export function contentRoutes(
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** Routes whose path contains the address query. A blank query keeps the full list. */
|
|
38
|
-
export function
|
|
38
|
+
export function routesMatching(routes: readonly string[], query: string): string[] {
|
|
39
39
|
const needle = query.trim().toLowerCase();
|
|
40
40
|
if (!needle) return [...routes];
|
|
41
41
|
return routes.filter((route) => route.toLowerCase().includes(needle));
|
|
@@ -8,6 +8,7 @@ import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
|
8
8
|
import { Input, Select, Textarea } from "../ui/Input";
|
|
9
9
|
import { MediaFieldInput } from "../pages/MediaFieldInput";
|
|
10
10
|
import { chooseFieldInput } from "./choose-field-input";
|
|
11
|
+
import { LinkFieldInput } from "./LinkFieldInput";
|
|
11
12
|
import { nestedStartsCollapsed, resolveObjectFields } from "./resolve-object-fields";
|
|
12
13
|
|
|
13
14
|
function clamp(n: number, min: number, max: number): number {
|
|
@@ -88,7 +89,13 @@ export function JsonField({
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
type ModelRef = {
|
|
92
|
+
type ModelRef = {
|
|
93
|
+
type: string;
|
|
94
|
+
external?: boolean;
|
|
95
|
+
route?: string | false;
|
|
96
|
+
indexSlug?: string;
|
|
97
|
+
fields?: Record<string, SchemaField>;
|
|
98
|
+
};
|
|
92
99
|
type RelationDocument = { slug: string; frontmatter: Record<string, unknown> };
|
|
93
100
|
type RelationDocumentLists = Record<string, RelationDocument[]>;
|
|
94
101
|
|
|
@@ -542,7 +549,9 @@ export function FieldControl({
|
|
|
542
549
|
}
|
|
543
550
|
|
|
544
551
|
if (kind === "media") {
|
|
545
|
-
return
|
|
552
|
+
return (
|
|
553
|
+
<MediaFieldInput name={name} field={field} value={value} readOnly={readOnly} onChange={onChange} />
|
|
554
|
+
);
|
|
546
555
|
}
|
|
547
556
|
|
|
548
557
|
if (kind === "relation") {
|
|
@@ -559,6 +568,20 @@ export function FieldControl({
|
|
|
559
568
|
);
|
|
560
569
|
}
|
|
561
570
|
|
|
571
|
+
if (kind === "link") {
|
|
572
|
+
return (
|
|
573
|
+
<LinkFieldInput
|
|
574
|
+
name={name}
|
|
575
|
+
field={field}
|
|
576
|
+
value={value}
|
|
577
|
+
models={models}
|
|
578
|
+
documentLists={documentLists}
|
|
579
|
+
readOnly={readOnly}
|
|
580
|
+
onChange={onChange}
|
|
581
|
+
/>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
562
585
|
if (kind === "boolean") {
|
|
563
586
|
return (
|
|
564
587
|
<input
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// packages/studio/src/fields/LinkFieldInput.tsx
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import * as stylex from "@stylexjs/stylex";
|
|
4
|
+
import {
|
|
5
|
+
documentLabel,
|
|
6
|
+
internalSuffixError,
|
|
7
|
+
joinInternalLink,
|
|
8
|
+
linkSide,
|
|
9
|
+
matchesWhere,
|
|
10
|
+
routeFor,
|
|
11
|
+
splitInternalLink,
|
|
12
|
+
} from "@backmatter/core";
|
|
13
|
+
import type { SchemaField } from "../api";
|
|
14
|
+
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
15
|
+
import { Input, Select } from "../ui/Input";
|
|
16
|
+
|
|
17
|
+
type ModelRef = {
|
|
18
|
+
type: string;
|
|
19
|
+
external?: boolean;
|
|
20
|
+
route?: string | false;
|
|
21
|
+
indexSlug?: string;
|
|
22
|
+
fields?: Record<string, SchemaField>;
|
|
23
|
+
};
|
|
24
|
+
type RelationDocument = { slug: string; frontmatter: Record<string, unknown> };
|
|
25
|
+
type RelationDocumentLists = Record<string, RelationDocument[]>;
|
|
26
|
+
|
|
27
|
+
function routableTypes(field: SchemaField, models: ModelRef[]): ModelRef[] {
|
|
28
|
+
if (field.models && field.models.length > 0) {
|
|
29
|
+
return field.models
|
|
30
|
+
.map((type) => models.find((model) => model.type === type))
|
|
31
|
+
.filter((model): model is ModelRef => Boolean(model && !model.external && model.route !== false));
|
|
32
|
+
}
|
|
33
|
+
return models.filter((model) => !model.external && model.route !== false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function suffixPresent(value: string): { query: boolean; anchor: boolean } {
|
|
37
|
+
const hashIndex = value.indexOf("#");
|
|
38
|
+
const preHash = hashIndex === -1 ? value : value.slice(0, hashIndex);
|
|
39
|
+
return { query: preHash.includes("?"), anchor: hashIndex !== -1 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function internalSuffixState(value: string): {
|
|
43
|
+
parts: ReturnType<typeof splitInternalLink>;
|
|
44
|
+
showSuffix: boolean;
|
|
45
|
+
selectValue: string;
|
|
46
|
+
} {
|
|
47
|
+
const parts = splitInternalLink(value);
|
|
48
|
+
const side = linkSide(value);
|
|
49
|
+
if (side !== "internal") {
|
|
50
|
+
return { parts, showSuffix: false, selectValue: value };
|
|
51
|
+
}
|
|
52
|
+
const present = suffixPresent(value);
|
|
53
|
+
const suffixErr = internalSuffixError(
|
|
54
|
+
{ query: parts.query, anchor: parts.anchor },
|
|
55
|
+
present,
|
|
56
|
+
);
|
|
57
|
+
if (suffixErr !== null) {
|
|
58
|
+
return { parts, showSuffix: false, selectValue: value };
|
|
59
|
+
}
|
|
60
|
+
return { parts, showSuffix: true, selectValue: parts.path };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildRouteOptions(
|
|
64
|
+
field: SchemaField,
|
|
65
|
+
models: ModelRef[],
|
|
66
|
+
documentLists: RelationDocumentLists | undefined,
|
|
67
|
+
): Array<{ value: string; label: string }> {
|
|
68
|
+
if (!documentLists) return [];
|
|
69
|
+
const options: Array<{ value: string; label: string }> = [];
|
|
70
|
+
for (const model of routableTypes(field, models)) {
|
|
71
|
+
const docs = documentLists[model.type] ?? [];
|
|
72
|
+
for (const doc of docs) {
|
|
73
|
+
if (!matchesWhere({ slug: doc.slug, frontmatter: doc.frontmatter }, field.where)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const route = routeFor(model, doc.slug);
|
|
77
|
+
if (route === "") continue;
|
|
78
|
+
const label = documentLabel(doc.frontmatter, model);
|
|
79
|
+
options.push({
|
|
80
|
+
value: route,
|
|
81
|
+
label: label ? `${label} ${route}` : route,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
options.sort((a, b) => a.label.localeCompare(b.label));
|
|
86
|
+
return options;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function SideToggle({
|
|
90
|
+
internalPressed,
|
|
91
|
+
readOnly,
|
|
92
|
+
onPick,
|
|
93
|
+
}: {
|
|
94
|
+
internalPressed: boolean;
|
|
95
|
+
readOnly?: boolean;
|
|
96
|
+
onPick: (side: "internal" | "external") => void;
|
|
97
|
+
}) {
|
|
98
|
+
return (
|
|
99
|
+
<div {...stylex.props(styles.sideRow)}>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
aria-pressed={internalPressed}
|
|
103
|
+
disabled={readOnly}
|
|
104
|
+
{...stylex.props(styles.sideButton, internalPressed && styles.sideButtonPressed)}
|
|
105
|
+
onClick={() => {
|
|
106
|
+
if (readOnly || internalPressed) return;
|
|
107
|
+
onPick("internal");
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
Internal
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
aria-pressed={!internalPressed}
|
|
115
|
+
disabled={readOnly}
|
|
116
|
+
{...stylex.props(styles.sideButton, !internalPressed && styles.sideButtonPressed)}
|
|
117
|
+
onClick={() => {
|
|
118
|
+
if (readOnly || !internalPressed) return;
|
|
119
|
+
onPick("external");
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
External
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function LinkFieldInput({
|
|
129
|
+
name,
|
|
130
|
+
field,
|
|
131
|
+
value,
|
|
132
|
+
models,
|
|
133
|
+
documentLists,
|
|
134
|
+
readOnly,
|
|
135
|
+
onChange,
|
|
136
|
+
}: {
|
|
137
|
+
name: string;
|
|
138
|
+
field: SchemaField;
|
|
139
|
+
value: unknown;
|
|
140
|
+
models: ModelRef[];
|
|
141
|
+
documentLists?: RelationDocumentLists;
|
|
142
|
+
readOnly?: boolean;
|
|
143
|
+
onChange: (value: unknown) => void;
|
|
144
|
+
}) {
|
|
145
|
+
const raw = value == null || value === "" ? "" : String(value);
|
|
146
|
+
const sideToggle = field.to === undefined;
|
|
147
|
+
const lastEmittedRawRef = useRef<string | null>(null);
|
|
148
|
+
const emitChange = (next: unknown) => {
|
|
149
|
+
lastEmittedRawRef.current = next == null || next === "" ? "" : String(next);
|
|
150
|
+
onChange(next);
|
|
151
|
+
};
|
|
152
|
+
const [editorSide, setEditorSide] = useState<"internal" | "external">(() =>
|
|
153
|
+
linkSide(raw) === "external" ? "external" : "internal",
|
|
154
|
+
);
|
|
155
|
+
const [pendingClearOf, setPendingClearOf] = useState<string | null>(null);
|
|
156
|
+
const [lastSyncedRaw, setLastSyncedRaw] = useState(raw);
|
|
157
|
+
if (raw !== lastSyncedRaw) {
|
|
158
|
+
setLastSyncedRaw(raw);
|
|
159
|
+
if (raw !== lastEmittedRawRef.current) {
|
|
160
|
+
lastEmittedRawRef.current = null;
|
|
161
|
+
if (raw !== "") {
|
|
162
|
+
setEditorSide(linkSide(raw) === "external" ? "external" : "internal");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (pendingClearOf !== null && raw !== pendingClearOf) {
|
|
166
|
+
setPendingClearOf(null);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (field.to === "external") {
|
|
171
|
+
return (
|
|
172
|
+
<Input
|
|
173
|
+
value={raw}
|
|
174
|
+
readOnly={readOnly}
|
|
175
|
+
onChange={(event) => {
|
|
176
|
+
if (readOnly) return;
|
|
177
|
+
const next = event.target.value;
|
|
178
|
+
emitChange(next === "" ? undefined : next);
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const forceInternal = field.to === "internal";
|
|
185
|
+
const internalUi = forceInternal || editorSide !== "external";
|
|
186
|
+
const staleInternalOnExternalSide =
|
|
187
|
+
editorSide === "external" && raw !== "" && raw === pendingClearOf;
|
|
188
|
+
const externalInputValue = staleInternalOnExternalSide ? "" : raw;
|
|
189
|
+
|
|
190
|
+
if (!internalUi) {
|
|
191
|
+
return (
|
|
192
|
+
<div {...stylex.props(styles.stack)}>
|
|
193
|
+
{sideToggle ? (
|
|
194
|
+
<SideToggle
|
|
195
|
+
internalPressed={false}
|
|
196
|
+
readOnly={readOnly}
|
|
197
|
+
onPick={(picked) => {
|
|
198
|
+
if (readOnly) return;
|
|
199
|
+
setEditorSide(picked);
|
|
200
|
+
if (raw !== "") {
|
|
201
|
+
setPendingClearOf(raw);
|
|
202
|
+
emitChange(undefined);
|
|
203
|
+
}
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
) : null}
|
|
207
|
+
<Input
|
|
208
|
+
value={externalInputValue}
|
|
209
|
+
readOnly={readOnly}
|
|
210
|
+
onChange={(event) => {
|
|
211
|
+
if (readOnly) return;
|
|
212
|
+
const next = event.target.value;
|
|
213
|
+
emitChange(next === "" ? undefined : next);
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { parts, showSuffix, selectValue } = raw === "" ? {
|
|
221
|
+
parts: { path: "", query: "", anchor: "" },
|
|
222
|
+
showSuffix: false,
|
|
223
|
+
selectValue: "",
|
|
224
|
+
} : internalSuffixState(raw);
|
|
225
|
+
|
|
226
|
+
const options = buildRouteOptions(field, models, documentLists);
|
|
227
|
+
if (selectValue && !options.some((option) => option.value === selectValue)) {
|
|
228
|
+
options.unshift({ value: selectValue, label: selectValue });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const internalPressed = editorSide !== "external";
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div {...stylex.props(styles.stack)}>
|
|
235
|
+
{sideToggle ? (
|
|
236
|
+
<SideToggle
|
|
237
|
+
internalPressed={internalPressed}
|
|
238
|
+
readOnly={readOnly}
|
|
239
|
+
onPick={(picked) => {
|
|
240
|
+
if (readOnly) return;
|
|
241
|
+
setEditorSide(picked);
|
|
242
|
+
if (raw !== "") {
|
|
243
|
+
setPendingClearOf(raw);
|
|
244
|
+
emitChange(undefined);
|
|
245
|
+
}
|
|
246
|
+
}}
|
|
247
|
+
/>
|
|
248
|
+
) : null}
|
|
249
|
+
<Select
|
|
250
|
+
name={name}
|
|
251
|
+
chip
|
|
252
|
+
searchable
|
|
253
|
+
clearable={!field.required}
|
|
254
|
+
placeholder="Select a value"
|
|
255
|
+
options={options}
|
|
256
|
+
value={selectValue || null}
|
|
257
|
+
disabled={readOnly}
|
|
258
|
+
onValueChange={(next) => {
|
|
259
|
+
if (readOnly) return;
|
|
260
|
+
if (next == null) {
|
|
261
|
+
emitChange(undefined);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
emitChange(joinInternalLink(next, showSuffix ? parts.query : "", showSuffix ? parts.anchor : ""));
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
<Input
|
|
268
|
+
aria-label="Query"
|
|
269
|
+
value={showSuffix ? parts.query : ""}
|
|
270
|
+
readOnly={readOnly || !showSuffix}
|
|
271
|
+
onChange={(event) => {
|
|
272
|
+
if (readOnly || !showSuffix) return;
|
|
273
|
+
emitChange(joinInternalLink(parts.path, event.target.value, parts.anchor));
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
<Input
|
|
277
|
+
aria-label="Anchor"
|
|
278
|
+
value={showSuffix ? parts.anchor : ""}
|
|
279
|
+
readOnly={readOnly || !showSuffix}
|
|
280
|
+
onChange={(event) => {
|
|
281
|
+
if (readOnly || !showSuffix) return;
|
|
282
|
+
emitChange(joinInternalLink(parts.path, parts.query, event.target.value));
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const styles = stylex.create({
|
|
290
|
+
stack: {
|
|
291
|
+
display: "flex",
|
|
292
|
+
flexDirection: "column",
|
|
293
|
+
gap: space.sm,
|
|
294
|
+
},
|
|
295
|
+
sideRow: {
|
|
296
|
+
display: "flex",
|
|
297
|
+
gap: space.xs,
|
|
298
|
+
},
|
|
299
|
+
sideButton: {
|
|
300
|
+
paddingInline: space.md,
|
|
301
|
+
paddingBlock: space.xs,
|
|
302
|
+
borderRadius: radius.md,
|
|
303
|
+
borderWidth: 1,
|
|
304
|
+
borderStyle: "solid",
|
|
305
|
+
borderColor: colors.border,
|
|
306
|
+
backgroundColor: colors.bgInput,
|
|
307
|
+
color: colors.text,
|
|
308
|
+
fontSize: type.sm,
|
|
309
|
+
cursor: "pointer",
|
|
310
|
+
},
|
|
311
|
+
sideButtonPressed: {
|
|
312
|
+
borderColor: colors.text,
|
|
313
|
+
fontWeight: 600,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
@@ -14,6 +14,7 @@ export type FieldInputKind =
|
|
|
14
14
|
| "boolean"
|
|
15
15
|
| "number"
|
|
16
16
|
| "blocks"
|
|
17
|
+
| "link"
|
|
17
18
|
| "text";
|
|
18
19
|
|
|
19
20
|
export function chooseFieldInput(
|
|
@@ -48,6 +49,10 @@ export function chooseFieldInput(
|
|
|
48
49
|
return "text";
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
if (field.kind === "link") {
|
|
53
|
+
return "link";
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
if (field.kind === "object") {
|
|
52
57
|
return "object";
|
|
53
58
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// packages/studio/src/fields/link-targets.ts
|
|
2
|
+
import type { SchemaField } from "../api";
|
|
3
|
+
import { resolveObjectFields } from "./resolve-object-fields";
|
|
4
|
+
|
|
5
|
+
type ModelRef = { type: string; external?: boolean; route?: string | false };
|
|
6
|
+
type ObjectDef = { name: string; fields: Record<string, SchemaField> };
|
|
7
|
+
|
|
8
|
+
function modelTypeLoadable(type: string, models: ModelRef[]): boolean {
|
|
9
|
+
const model = models.find((item) => item.type === type);
|
|
10
|
+
return Boolean(model && !model.external && model.route !== false);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function collectFromFields(
|
|
14
|
+
fields: Record<string, SchemaField>,
|
|
15
|
+
models: ModelRef[],
|
|
16
|
+
objects: ObjectDef[],
|
|
17
|
+
visiting: Set<string>,
|
|
18
|
+
targets: Set<string>,
|
|
19
|
+
): void {
|
|
20
|
+
for (const field of Object.values(fields)) {
|
|
21
|
+
if (field.kind === "link") {
|
|
22
|
+
if (field.to === "external") {
|
|
23
|
+
// external-only links do not load documents
|
|
24
|
+
} else if (field.models && field.models.length > 0) {
|
|
25
|
+
for (const type of field.models) {
|
|
26
|
+
if (modelTypeLoadable(type, models)) {
|
|
27
|
+
targets.add(type);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
for (const model of models) {
|
|
32
|
+
if (!model.external && model.route !== false) {
|
|
33
|
+
targets.add(model.type);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nested = resolveObjectFields(field, objects);
|
|
40
|
+
if (!nested) continue;
|
|
41
|
+
|
|
42
|
+
let nextVisiting = visiting;
|
|
43
|
+
if (field.object && (field.kind === "object" || field.kind === "array")) {
|
|
44
|
+
if (visiting.has(field.object)) continue;
|
|
45
|
+
nextVisiting = new Set(visiting).add(field.object);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
collectFromFields(nested, models, objects, nextVisiting, targets);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function linkDocumentTypes(
|
|
53
|
+
fields: Record<string, SchemaField>,
|
|
54
|
+
models: ModelRef[],
|
|
55
|
+
objects: ObjectDef[],
|
|
56
|
+
): string[] {
|
|
57
|
+
const targets = new Set<string>();
|
|
58
|
+
collectFromFields(fields, models, objects, new Set<string>(), targets);
|
|
59
|
+
return [...targets].sort();
|
|
60
|
+
}
|
package/src/host-path.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// packages/studio/src/host-path.ts
|
|
2
|
+
|
|
3
|
+
/** A typed or pasted address, as a path on the host. */
|
|
4
|
+
export function hostPath(input: string): string {
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed) return "/";
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(trimmed);
|
|
9
|
+
return `${url.pathname}${url.search}${url.hash}` || "/";
|
|
10
|
+
} catch {
|
|
11
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
12
|
+
}
|
|
13
|
+
}
|