@jxsuite/studio 1.0.0 → 1.1.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/dist/iframe-entry.js +622 -49
- package/dist/iframe-entry.js.map +15 -12
- package/dist/studio.css +1333 -0
- package/dist/studio.js +53069 -31746
- package/dist/studio.js.map +109 -74
- package/package.json +11 -9
- package/src/canvas/canvas-live-render.ts +21 -2
- package/src/canvas/canvas-render.ts +35 -16
- package/src/canvas/canvas-utils.ts +118 -72
- package/src/canvas/iframe-entry.ts +20 -0
- package/src/canvas/iframe-eval.ts +155 -0
- package/src/canvas/iframe-host.ts +131 -5
- package/src/canvas/iframe-inline-edit.ts +107 -1
- package/src/canvas/iframe-protocol.ts +43 -3
- package/src/canvas/iframe-render.ts +18 -0
- package/src/collab/collab-session.ts +23 -8
- package/src/collab/collab-state.ts +6 -3
- package/src/component-props.ts +104 -0
- package/src/editor/inline-edit-apply.ts +36 -1
- package/src/editor/inline-edit.ts +58 -1
- package/src/editor/shortcuts.ts +41 -12
- package/src/files/file-ops.ts +14 -0
- package/src/files/files.ts +53 -1
- package/src/grid/cell-editors.ts +288 -0
- package/src/grid/cell-popovers.ts +108 -0
- package/src/grid/csv-codec.ts +122 -0
- package/src/grid/edit-buffer.ts +490 -0
- package/src/grid/grid-controller.ts +417 -0
- package/src/grid/grid-layout.ts +83 -0
- package/src/grid/grid-open.ts +167 -0
- package/src/grid/grid-panel.ts +302 -0
- package/src/grid/grid-source.ts +210 -0
- package/src/grid/grid-view.ts +367 -0
- package/src/grid/schema-columns.ts +261 -0
- package/src/grid/sources/connector-source.ts +217 -0
- package/src/grid/sources/content-source.ts +542 -0
- package/src/grid/sources/csv-file-source.ts +247 -0
- package/src/grid/tabulator-tables.d.ts +120 -0
- package/src/panels/block-action-bar.ts +8 -3
- package/src/panels/chat-panel.ts +98 -0
- package/src/panels/data-grid.ts +9 -387
- package/src/panels/editors.ts +43 -17
- package/src/panels/events-panel.ts +137 -44
- package/src/panels/formula-workspace.ts +379 -0
- package/src/panels/frontmatter-fields.ts +231 -0
- package/src/panels/frontmatter-panel.ts +132 -0
- package/src/panels/git-panel.ts +1 -1
- package/src/panels/head-panel.ts +11 -175
- package/src/panels/properties-panel.ts +154 -144
- package/src/panels/right-panel.ts +9 -47
- package/src/panels/signals-panel.ts +115 -23
- package/src/panels/statement-editor.ts +710 -0
- package/src/panels/style-panel.ts +25 -1
- package/src/panels/stylebook-panel.ts +0 -2
- package/src/panels/tab-bar.ts +175 -6
- package/src/panels/tab-strip.ts +62 -6
- package/src/panels/toolbar.ts +37 -3
- package/src/services/ai-project-tools.ts +541 -0
- package/src/services/ai-session-store.ts +28 -0
- package/src/services/ai-system-prompt.ts +194 -24
- package/src/services/ai-tools.ts +88 -21
- package/src/services/automation.ts +16 -0
- package/src/services/document-assistant.ts +81 -11
- package/src/services/gated-registry.ts +72 -0
- package/src/services/live-preview.ts +0 -0
- package/src/services/preview-eval.ts +68 -0
- package/src/services/project-adoption.ts +31 -0
- package/src/site-context.ts +2 -0
- package/src/store.ts +4 -0
- package/src/studio.ts +83 -52
- package/src/tabs/patch-ops.ts +25 -0
- package/src/tabs/tab.ts +39 -1
- package/src/tabs/transact.ts +60 -13
- package/src/types.ts +12 -0
- package/src/ui/dynamic-slot.ts +272 -0
- package/src/ui/expression-editor.ts +423 -125
- package/src/ui/field-row.ts +5 -0
- package/src/ui/formula-catalog.ts +557 -0
- package/src/ui/formula-chips.ts +216 -0
- package/src/ui/formula-palette.ts +211 -0
- package/src/ui/layers.ts +40 -0
- package/src/ui/media-picker.ts +1 -1
- package/src/ui/panel-resize.ts +15 -1
- package/src/utils/preview-format.ts +26 -0
- package/src/view.ts +4 -4
- package/src/workspace/workspace.ts +14 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Frontmatter-fields.ts — Shared schema-driven frontmatter field collection and renderers.
|
|
4
|
+
*
|
|
5
|
+
* Used by both frontmatter editing surfaces: the Document tab (head-panel) and the above-canvas
|
|
6
|
+
* Properties panel (frontmatter-panel). Fields come from the content-collection schema
|
|
7
|
+
* (`findContentTypeSchema`) plus any extra keys already present in the frontmatter; each renders as
|
|
8
|
+
* a typed widget committing through `mutateUpdateFrontmatter`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { html } from "lit-html";
|
|
12
|
+
import { live } from "lit-html/directives/live.js";
|
|
13
|
+
import { renderFieldRow } from "../ui/field-row";
|
|
14
|
+
import { spNumberField, spTextField } from "../ui/field-input";
|
|
15
|
+
import { renderMediaPicker } from "../ui/media-picker";
|
|
16
|
+
import { activeTab } from "../workspace/workspace";
|
|
17
|
+
import { mutateUpdateFrontmatter, transactDoc } from "../tabs/transact";
|
|
18
|
+
import { findContentTypeSchema } from "../utils/studio-utils";
|
|
19
|
+
|
|
20
|
+
import type { JsonValue } from "../types";
|
|
21
|
+
import type { Tab } from "../tabs/tab";
|
|
22
|
+
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
23
|
+
|
|
24
|
+
export interface FmSchemaEntry {
|
|
25
|
+
type?: string;
|
|
26
|
+
enum?: string[];
|
|
27
|
+
format?: string;
|
|
28
|
+
properties?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FmField {
|
|
32
|
+
field: string;
|
|
33
|
+
entry: FmSchemaEntry;
|
|
34
|
+
value: JsonValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FmFieldSet {
|
|
38
|
+
/** Matched content collection, or null when the doc isn't part of one. */
|
|
39
|
+
collection: { name: string; schema: unknown } | null;
|
|
40
|
+
/** Schema-declared fields (schema order) followed by extra frontmatter keys. */
|
|
41
|
+
fields: FmField[];
|
|
42
|
+
requiredFields: Set<string>;
|
|
43
|
+
/** True when the matched collection declares `schema.properties`. */
|
|
44
|
+
hasSchema: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Collect the frontmatter fields to display for a tab: schema-declared fields first (in schema
|
|
49
|
+
* order), then any extra frontmatter keys not in the schema with types inferred from their values.
|
|
50
|
+
* `$`-prefixed keys and `reserved` keys are skipped.
|
|
51
|
+
*
|
|
52
|
+
* @param {Tab} tab
|
|
53
|
+
* @param {ProjectConfig | null | undefined} projectConfig
|
|
54
|
+
* @param {Set<string>} reserved — keys managed by dedicated controls elsewhere
|
|
55
|
+
* @returns {FmFieldSet}
|
|
56
|
+
*/
|
|
57
|
+
export function collectFmFields(
|
|
58
|
+
tab: Tab,
|
|
59
|
+
projectConfig: ProjectConfig | null | undefined,
|
|
60
|
+
reserved: Set<string>,
|
|
61
|
+
): FmFieldSet {
|
|
62
|
+
const fm = tab.doc.content?.frontmatter || {};
|
|
63
|
+
const collection = findContentTypeSchema(tab.documentPath, projectConfig);
|
|
64
|
+
const schema = collection?.schema as
|
|
65
|
+
| { properties?: Record<string, FmSchemaEntry>; required?: string[] }
|
|
66
|
+
| undefined;
|
|
67
|
+
const schemaProps = schema?.properties;
|
|
68
|
+
const requiredFields = new Set(schema?.required || []);
|
|
69
|
+
|
|
70
|
+
const fields: FmField[] = [];
|
|
71
|
+
if (schemaProps) {
|
|
72
|
+
for (const [field, fieldSchema] of Object.entries(schemaProps)) {
|
|
73
|
+
if (reserved.has(field)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
fields.push({ entry: fieldSchema, field, value: fm[field] as JsonValue });
|
|
77
|
+
}
|
|
78
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
79
|
+
if (schemaProps[field] || field.startsWith("$") || reserved.has(field)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
fields.push({
|
|
83
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
84
|
+
field,
|
|
85
|
+
value: value as JsonValue,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
for (const [field, value] of Object.entries(fm)) {
|
|
90
|
+
if (field.startsWith("$") || reserved.has(field)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
fields.push({
|
|
94
|
+
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
95
|
+
field,
|
|
96
|
+
value: value as JsonValue,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { collection, fields, hasSchema: Boolean(schemaProps), requiredFields };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render one frontmatter field as a typed widget row. Commits through `transactDoc` +
|
|
106
|
+
* `mutateUpdateFrontmatter` on the active tab.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} field
|
|
109
|
+
* @param {FmSchemaEntry} entry
|
|
110
|
+
* @param {JsonValue} value
|
|
111
|
+
* @param {Set<string>} requiredFields
|
|
112
|
+
* @returns {import("lit-html").TemplateResult}
|
|
113
|
+
*/
|
|
114
|
+
export function renderFmField(
|
|
115
|
+
field: string,
|
|
116
|
+
entry: FmSchemaEntry,
|
|
117
|
+
value: JsonValue,
|
|
118
|
+
requiredFields: Set<string>,
|
|
119
|
+
) {
|
|
120
|
+
const isRequired = requiredFields.has(field);
|
|
121
|
+
const label = field.replaceAll(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
|
|
122
|
+
const displayLabel = label + (isRequired ? " *" : "");
|
|
123
|
+
const hasVal = value !== undefined && value !== "" && value !== false;
|
|
124
|
+
const onClear = () => transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field));
|
|
125
|
+
|
|
126
|
+
if (entry.type === "boolean") {
|
|
127
|
+
return renderFieldRow({
|
|
128
|
+
hasValue: hasVal,
|
|
129
|
+
label: displayLabel,
|
|
130
|
+
onClear,
|
|
131
|
+
prop: field,
|
|
132
|
+
widget: html`
|
|
133
|
+
<sp-checkbox
|
|
134
|
+
size="s"
|
|
135
|
+
.checked=${live(Boolean(value))}
|
|
136
|
+
@change=${(e: Event) =>
|
|
137
|
+
transactDoc(activeTab.value, (t) =>
|
|
138
|
+
mutateUpdateFrontmatter(
|
|
139
|
+
t,
|
|
140
|
+
field,
|
|
141
|
+
(e.target as HTMLInputElement).checked || undefined,
|
|
142
|
+
),
|
|
143
|
+
)}
|
|
144
|
+
></sp-checkbox>
|
|
145
|
+
`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (entry.type === "array") {
|
|
150
|
+
const display = Array.isArray(value) ? value.join(", ") : (value as string) || "";
|
|
151
|
+
return renderFieldRow({
|
|
152
|
+
hasValue: hasVal,
|
|
153
|
+
label: displayLabel,
|
|
154
|
+
onClear,
|
|
155
|
+
prop: field,
|
|
156
|
+
widget: spTextField(
|
|
157
|
+
`fm:${field}`,
|
|
158
|
+
display,
|
|
159
|
+
(v: string) => {
|
|
160
|
+
const arr = v
|
|
161
|
+
? v
|
|
162
|
+
.split(",")
|
|
163
|
+
.map((s: string) => s.trim())
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
: undefined;
|
|
166
|
+
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
|
|
167
|
+
},
|
|
168
|
+
{ placeholder: "comma, separated" },
|
|
169
|
+
),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Array.isArray(entry.enum)) {
|
|
174
|
+
return renderFieldRow({
|
|
175
|
+
hasValue: hasVal,
|
|
176
|
+
label: displayLabel,
|
|
177
|
+
onClear,
|
|
178
|
+
prop: field,
|
|
179
|
+
widget: html`
|
|
180
|
+
<sp-picker
|
|
181
|
+
size="s"
|
|
182
|
+
.value=${live(value || "")}
|
|
183
|
+
@change=${(e: Event) =>
|
|
184
|
+
transactDoc(activeTab.value, (t) =>
|
|
185
|
+
mutateUpdateFrontmatter(t, field, (e.target as HTMLInputElement).value || undefined),
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
${entry.enum.map((opt: string) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`)}
|
|
189
|
+
</sp-picker>
|
|
190
|
+
`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (entry.format === "image") {
|
|
195
|
+
return renderFieldRow({
|
|
196
|
+
hasValue: hasVal,
|
|
197
|
+
label: displayLabel,
|
|
198
|
+
onClear,
|
|
199
|
+
prop: field,
|
|
200
|
+
widget: renderMediaPicker(field, value as string, (v: string) =>
|
|
201
|
+
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
|
|
202
|
+
),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (entry.type === "number") {
|
|
207
|
+
return renderFieldRow({
|
|
208
|
+
hasValue: hasVal,
|
|
209
|
+
label: displayLabel,
|
|
210
|
+
onClear,
|
|
211
|
+
prop: field,
|
|
212
|
+
widget: spNumberField(value !== undefined ? Number(value) : undefined, (n) =>
|
|
213
|
+
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, n)),
|
|
214
|
+
),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return renderFieldRow({
|
|
219
|
+
hasValue: hasVal,
|
|
220
|
+
label: displayLabel,
|
|
221
|
+
onClear,
|
|
222
|
+
prop: field,
|
|
223
|
+
widget: spTextField(
|
|
224
|
+
`fm:${field}`,
|
|
225
|
+
(value as string) || "",
|
|
226
|
+
(v: string) =>
|
|
227
|
+
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
|
|
228
|
+
{ placeholder: entry.format === "date" ? "YYYY-MM-DD" : "" },
|
|
229
|
+
),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Frontmatter-panel.ts — Obsidian-style "Properties" panel above the canvas.
|
|
4
|
+
*
|
|
5
|
+
* Shown only when the active tab is a content-collection document (matched via
|
|
6
|
+
* `findContentTypeSchema`) in content mode with the effective canvas mode "edit". The fields sit
|
|
7
|
+
* inside a single collapsible accordion item so the panel can be dismissed to a slim header bar;
|
|
8
|
+
* the expanded/collapsed state persists per tab (`tab.session.ui.frontmatterOpen`).
|
|
9
|
+
*
|
|
10
|
+
* Unlike the Document-tab section (head-panel), no keys are reserved here — `title` renders as a
|
|
11
|
+
* regular property, matching Obsidian.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { html, nothing, render as litRender } from "lit-html";
|
|
15
|
+
import { frontmatterPanelEl, projectState } from "../store";
|
|
16
|
+
import { activeTab } from "../workspace/workspace";
|
|
17
|
+
import { effect, effectScope } from "../reactivity";
|
|
18
|
+
import { createPanelScheduler } from "./panel-scheduler";
|
|
19
|
+
import { collectFmFields, renderFmField } from "./frontmatter-fields";
|
|
20
|
+
|
|
21
|
+
import type { PanelScheduler } from "./panel-scheduler";
|
|
22
|
+
|
|
23
|
+
interface FrontmatterPanelCtx {
|
|
24
|
+
/** Effective canvas mode (composes the base mode with the preview toggle). */
|
|
25
|
+
getCanvasMode: () => string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NO_RESERVED_KEYS = new Set<string>();
|
|
29
|
+
|
|
30
|
+
let _ctx: FrontmatterPanelCtx | null = null;
|
|
31
|
+
let _scheduler: PanelScheduler | null = null;
|
|
32
|
+
let _scope: { stop: () => void; run: <T>(fn: () => T) => T | undefined } | null = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mount the frontmatter panel: bind the focus-aware scheduler to the shell host and re-render
|
|
36
|
+
* reactively on tab/mode/frontmatter changes.
|
|
37
|
+
*
|
|
38
|
+
* @param {FrontmatterPanelCtx} ctx
|
|
39
|
+
*/
|
|
40
|
+
export function mount(ctx: FrontmatterPanelCtx) {
|
|
41
|
+
if (!frontmatterPanelEl) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
_ctx = ctx;
|
|
45
|
+
_scheduler = createPanelScheduler({ render: _doRender, root: frontmatterPanelEl });
|
|
46
|
+
_scheduler.bindFocus();
|
|
47
|
+
_scope = effectScope();
|
|
48
|
+
_scope.run(() => {
|
|
49
|
+
effect(() => {
|
|
50
|
+
const tab = activeTab.value;
|
|
51
|
+
if (tab) {
|
|
52
|
+
// Track everything the panel reads: mode/ui flags plus the frontmatter object itself AND
|
|
53
|
+
// Its entries — field commits mutate keys in place while the source-mode round-trip swaps
|
|
54
|
+
// The whole object; both must re-fire this effect.
|
|
55
|
+
void tab.doc.mode;
|
|
56
|
+
void tab.session.ui.canvasMode;
|
|
57
|
+
void tab.session.ui.preview;
|
|
58
|
+
void tab.session.ui.frontmatterOpen;
|
|
59
|
+
const fm = tab.doc.content?.frontmatter;
|
|
60
|
+
if (fm) {
|
|
61
|
+
for (const key of Object.keys(fm)) {
|
|
62
|
+
void fm[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
render();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function unmount() {
|
|
72
|
+
_scope?.stop();
|
|
73
|
+
_scope = null;
|
|
74
|
+
_ctx = null;
|
|
75
|
+
_scheduler?.unbind();
|
|
76
|
+
_scheduler = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Request a render. Coalesced and deferred while a text input in the panel is focused, so
|
|
81
|
+
* re-renders never clobber a field mid-edit.
|
|
82
|
+
*/
|
|
83
|
+
export function render() {
|
|
84
|
+
_scheduler?.schedule();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _doRender() {
|
|
88
|
+
if (!_ctx || !frontmatterPanelEl) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tab = activeTab.value;
|
|
93
|
+
const eligible = Boolean(
|
|
94
|
+
tab && tab.doc.mode === "content" && _ctx.getCanvasMode() === "edit" && tab.documentPath,
|
|
95
|
+
);
|
|
96
|
+
const fieldSet = eligible
|
|
97
|
+
? collectFmFields(tab!, projectState?.projectConfig, NO_RESERVED_KEYS)
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
if (!fieldSet?.collection) {
|
|
101
|
+
// Lit leaves comment markers, so the host is hidden explicitly rather than relying on :empty.
|
|
102
|
+
litRender(nothing, frontmatterPanelEl);
|
|
103
|
+
frontmatterPanelEl.hidden = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { collection, fields, requiredFields } = fieldSet;
|
|
108
|
+
const open = tab!.session.ui.frontmatterOpen !== false;
|
|
109
|
+
|
|
110
|
+
litRender(
|
|
111
|
+
html`
|
|
112
|
+
<sp-accordion size="s">
|
|
113
|
+
<sp-accordion-item
|
|
114
|
+
label="Properties · ${collection.name}"
|
|
115
|
+
?open=${open}
|
|
116
|
+
@sp-accordion-item-toggle=${(e: Event) => {
|
|
117
|
+
const t = activeTab.value;
|
|
118
|
+
if (t) {
|
|
119
|
+
t.session.ui.frontmatterOpen = (e.target as HTMLElement & { open: boolean }).open;
|
|
120
|
+
}
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<div class="fm-panel-body">
|
|
124
|
+
${fields.map((f) => renderFmField(f.field, f.entry, f.value, requiredFields))}
|
|
125
|
+
</div>
|
|
126
|
+
</sp-accordion-item>
|
|
127
|
+
</sp-accordion>
|
|
128
|
+
`,
|
|
129
|
+
frontmatterPanelEl,
|
|
130
|
+
);
|
|
131
|
+
frontmatterPanelEl.hidden = false;
|
|
132
|
+
}
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -471,7 +471,7 @@ export function renderGitPanel(
|
|
|
471
471
|
.value=${live(S.ui.gitCommitMessage || "")}
|
|
472
472
|
@input=${(e: Event) => updateUi("gitCommitMessage", (e.target as HTMLInputElement).value)}
|
|
473
473
|
@keydown=${(e: KeyboardEvent) => {
|
|
474
|
-
if (e.ctrlKey && e.key === "Enter") {
|
|
474
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
475
475
|
e.preventDefault();
|
|
476
476
|
void doCommit();
|
|
477
477
|
}
|
package/src/panels/head-panel.ts
CHANGED
|
@@ -7,15 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { html, nothing } from "lit-html";
|
|
10
|
-
import { live } from "lit-html/directives/live.js";
|
|
11
10
|
import { renderFieldRow } from "../ui/field-row";
|
|
12
|
-
import {
|
|
11
|
+
import { spTextArea, spTextField } from "../ui/field-input";
|
|
13
12
|
import { renderMediaPicker } from "../ui/media-picker";
|
|
14
13
|
import { projectState, renderOnly } from "../store";
|
|
15
|
-
import type { DirEntry
|
|
14
|
+
import type { DirEntry } from "../types";
|
|
16
15
|
import { activeTab } from "../workspace/workspace";
|
|
17
|
-
import {
|
|
18
|
-
import { findContentTypeSchema } from "../utils/studio-utils";
|
|
16
|
+
import { collectFmFields, renderFmField } from "./frontmatter-fields";
|
|
19
17
|
import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts";
|
|
20
18
|
import { invalidateLayoutCache } from "../site-context";
|
|
21
19
|
import { getPlatform } from "../platform";
|
|
@@ -31,13 +29,6 @@ interface MetaField {
|
|
|
31
29
|
media?: boolean;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
interface FmSchemaEntry {
|
|
35
|
-
type?: string;
|
|
36
|
-
enum?: string[];
|
|
37
|
-
format?: string;
|
|
38
|
-
properties?: Record<string, unknown>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
32
|
// ─── Layout picker ──────────────────────────────────────────────────────────
|
|
42
33
|
|
|
43
34
|
/** @type {{ name: string; path: string }[] | null} */
|
|
@@ -564,49 +555,13 @@ function renderFrontmatterSection() {
|
|
|
564
555
|
return nothing;
|
|
565
556
|
}
|
|
566
557
|
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const schemaProps = schema?.properties;
|
|
573
|
-
const requiredFields = new Set(schema?.required || []);
|
|
574
|
-
|
|
575
|
-
/** @type {{ field: string; entry: FmSchemaEntry; value: JsonValue }[]} */
|
|
576
|
-
const fields = [];
|
|
577
|
-
if (schemaProps) {
|
|
578
|
-
for (const [field, fieldSchema] of Object.entries(
|
|
579
|
-
/** @type {Record<string, FmSchemaEntry>} */ schemaProps,
|
|
580
|
-
)) {
|
|
581
|
-
if (RESERVED_FM_KEYS.has(field)) {
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
fields.push({ entry: fieldSchema, field, value: fm[field] as JsonValue });
|
|
585
|
-
}
|
|
586
|
-
for (const [field, value] of Object.entries(fm)) {
|
|
587
|
-
if (schemaProps[field] || field.startsWith("$") || RESERVED_FM_KEYS.has(field)) {
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
fields.push({
|
|
591
|
-
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
592
|
-
field,
|
|
593
|
-
value: /** @type {JsonValue} */ value,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
} else {
|
|
597
|
-
for (const [field, value] of Object.entries(fm)) {
|
|
598
|
-
if (field.startsWith("$") || RESERVED_FM_KEYS.has(field)) {
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
fields.push({
|
|
602
|
-
entry: { type: typeof value === "boolean" ? "boolean" : "string" },
|
|
603
|
-
field,
|
|
604
|
-
value: /** @type {JsonValue} */ value,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
}
|
|
558
|
+
const { collection, fields, hasSchema, requiredFields } = collectFmFields(
|
|
559
|
+
tab,
|
|
560
|
+
projectState?.projectConfig,
|
|
561
|
+
RESERVED_FM_KEYS,
|
|
562
|
+
);
|
|
608
563
|
|
|
609
|
-
if (fields.length === 0 && !
|
|
564
|
+
if (fields.length === 0 && !hasSchema) {
|
|
610
565
|
return nothing;
|
|
611
566
|
}
|
|
612
567
|
|
|
@@ -614,131 +569,12 @@ function renderFrontmatterSection() {
|
|
|
614
569
|
<div class="imports-section">
|
|
615
570
|
<div class="imports-section-header">
|
|
616
571
|
<span class="imports-section-title"
|
|
617
|
-
>${
|
|
572
|
+
>${collection ? `Frontmatter (${collection.name})` : "Frontmatter"}</span
|
|
618
573
|
>
|
|
619
574
|
</div>
|
|
620
575
|
<div class="head-section-body">
|
|
621
|
-
${fields.map((f) => renderFmField(f.field, f.entry, f.value
|
|
576
|
+
${fields.map((f) => renderFmField(f.field, f.entry, f.value, requiredFields))}
|
|
622
577
|
</div>
|
|
623
578
|
</div>
|
|
624
579
|
`;
|
|
625
580
|
}
|
|
626
|
-
|
|
627
|
-
function renderFmField(
|
|
628
|
-
field: string,
|
|
629
|
-
entry: FmSchemaEntry,
|
|
630
|
-
value: JsonValue,
|
|
631
|
-
requiredFields: Set<string>,
|
|
632
|
-
) {
|
|
633
|
-
const isRequired = requiredFields.has(field);
|
|
634
|
-
const label = field.replaceAll(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
|
|
635
|
-
const displayLabel = label + (isRequired ? " *" : "");
|
|
636
|
-
const hasVal = value !== undefined && value !== "" && value !== false;
|
|
637
|
-
const onClear = () => transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field));
|
|
638
|
-
|
|
639
|
-
if (entry.type === "boolean") {
|
|
640
|
-
return renderFieldRow({
|
|
641
|
-
hasValue: hasVal,
|
|
642
|
-
label: displayLabel,
|
|
643
|
-
onClear,
|
|
644
|
-
prop: field,
|
|
645
|
-
widget: html`
|
|
646
|
-
<sp-checkbox
|
|
647
|
-
size="s"
|
|
648
|
-
.checked=${live(Boolean(value))}
|
|
649
|
-
@change=${(e: Event) =>
|
|
650
|
-
transactDoc(activeTab.value, (t) =>
|
|
651
|
-
mutateUpdateFrontmatter(
|
|
652
|
-
t,
|
|
653
|
-
field,
|
|
654
|
-
(e.target as HTMLInputElement).checked || undefined,
|
|
655
|
-
),
|
|
656
|
-
)}
|
|
657
|
-
></sp-checkbox>
|
|
658
|
-
`,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (entry.type === "array") {
|
|
663
|
-
const display = Array.isArray(value) ? value.join(", ") : (value as string) || "";
|
|
664
|
-
return renderFieldRow({
|
|
665
|
-
hasValue: hasVal,
|
|
666
|
-
label: displayLabel,
|
|
667
|
-
onClear,
|
|
668
|
-
prop: field,
|
|
669
|
-
widget: spTextField(
|
|
670
|
-
`fm:${field}`,
|
|
671
|
-
display,
|
|
672
|
-
(v: string) => {
|
|
673
|
-
const arr = v
|
|
674
|
-
? v
|
|
675
|
-
.split(",")
|
|
676
|
-
.map((s: string) => s.trim())
|
|
677
|
-
.filter(Boolean)
|
|
678
|
-
: undefined;
|
|
679
|
-
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
|
|
680
|
-
},
|
|
681
|
-
{ placeholder: "comma, separated" },
|
|
682
|
-
),
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (Array.isArray(entry.enum)) {
|
|
687
|
-
return renderFieldRow({
|
|
688
|
-
hasValue: hasVal,
|
|
689
|
-
label: displayLabel,
|
|
690
|
-
onClear,
|
|
691
|
-
prop: field,
|
|
692
|
-
widget: html`
|
|
693
|
-
<sp-picker
|
|
694
|
-
size="s"
|
|
695
|
-
.value=${live(value || "")}
|
|
696
|
-
@change=${(e: Event) =>
|
|
697
|
-
transactDoc(activeTab.value, (t) =>
|
|
698
|
-
mutateUpdateFrontmatter(t, field, (e.target as HTMLInputElement).value || undefined),
|
|
699
|
-
)}
|
|
700
|
-
>
|
|
701
|
-
${entry.enum.map((opt: string) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`)}
|
|
702
|
-
</sp-picker>
|
|
703
|
-
`,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (entry.format === "image") {
|
|
708
|
-
return renderFieldRow({
|
|
709
|
-
hasValue: hasVal,
|
|
710
|
-
label: displayLabel,
|
|
711
|
-
onClear,
|
|
712
|
-
prop: field,
|
|
713
|
-
widget: renderMediaPicker(field, value as string, (v: string) =>
|
|
714
|
-
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
|
|
715
|
-
),
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (entry.type === "number") {
|
|
720
|
-
return renderFieldRow({
|
|
721
|
-
hasValue: hasVal,
|
|
722
|
-
label: displayLabel,
|
|
723
|
-
onClear,
|
|
724
|
-
prop: field,
|
|
725
|
-
widget: spNumberField(value !== undefined ? Number(value) : undefined, (n) =>
|
|
726
|
-
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, n)),
|
|
727
|
-
),
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return renderFieldRow({
|
|
732
|
-
hasValue: hasVal,
|
|
733
|
-
label: displayLabel,
|
|
734
|
-
onClear,
|
|
735
|
-
prop: field,
|
|
736
|
-
widget: spTextField(
|
|
737
|
-
`fm:${field}`,
|
|
738
|
-
(value as string) || "",
|
|
739
|
-
(v: string) =>
|
|
740
|
-
transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
|
|
741
|
-
{ placeholder: entry.format === "date" ? "YYYY-MM-DD" : "" },
|
|
742
|
-
),
|
|
743
|
-
});
|
|
744
|
-
}
|