@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,216 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Formula chips — a horizontal presentation layer summarizing an expression tree as left-to-right
|
|
4
|
+
* chips (spec §19.9). The `target` chain unrolls deepest-first: the head chip is the innermost
|
|
5
|
+
* target operand (ref/literal), followed by each operator up to the root. Nested non-target
|
|
6
|
+
* operands render as parenthesized group chips. Pure presentation — no editing logic; clicking a
|
|
7
|
+
* chip reports the node path to the caller.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { html, nothing } from "lit-html";
|
|
11
|
+
import { isJsonObject, isRef } from "@jxsuite/schema/guards";
|
|
12
|
+
|
|
13
|
+
import type { TemplateResult } from "lit-html";
|
|
14
|
+
import type { EditorPreview } from "./expression-editor";
|
|
15
|
+
|
|
16
|
+
type NodePath = (string | number)[];
|
|
17
|
+
|
|
18
|
+
interface ChainLink {
|
|
19
|
+
node: Record<string, unknown>;
|
|
20
|
+
path: NodePath;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Chain {
|
|
24
|
+
/** The innermost target operand (ref/literal), absent for target-less roots. */
|
|
25
|
+
head: { value: unknown; path: NodePath } | null;
|
|
26
|
+
/** Operator nodes, deepest target first → outermost operator last. */
|
|
27
|
+
links: ChainLink[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isExprNode(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return isJsonObject(value) && typeof value.operator === "string";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Human label for a pointer ref: strips the #/state/ and window#/ prefixes. */
|
|
35
|
+
function refLabel(ref: string): string {
|
|
36
|
+
if (ref.startsWith("#/state/")) {
|
|
37
|
+
return ref.slice("#/state/".length);
|
|
38
|
+
}
|
|
39
|
+
if (ref.startsWith("window#/")) {
|
|
40
|
+
return ref.slice("window#/".length).replaceAll("/", ".");
|
|
41
|
+
}
|
|
42
|
+
return ref || "?";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Compact label for a non-node operand (ref or literal). */
|
|
46
|
+
function operandLabel(value: unknown): string {
|
|
47
|
+
if (isRef(value)) {
|
|
48
|
+
return refLabel(value.$ref);
|
|
49
|
+
}
|
|
50
|
+
if (isExprNode(value)) {
|
|
51
|
+
return `(${chipSummary(value)})`;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
}
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The chain path for `hops` target descents below `basePath`. */
|
|
60
|
+
function targetPath(basePath: NodePath, hops: number): NodePath {
|
|
61
|
+
return [...basePath, ...Array.from({ length: hops }, () => "target")];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Unroll a node's target chain into head operand + operator links (deepest first). */
|
|
65
|
+
function unrollChain(node: Record<string, unknown>, basePath: NodePath): Chain {
|
|
66
|
+
// Collect the chain root-first, then materialize each link's path from its descent depth.
|
|
67
|
+
const nodes: Record<string, unknown>[] = [];
|
|
68
|
+
let current: Record<string, unknown> = node;
|
|
69
|
+
for (;;) {
|
|
70
|
+
nodes.push(current);
|
|
71
|
+
const next = current.target;
|
|
72
|
+
if (isExprNode(next)) {
|
|
73
|
+
current = next;
|
|
74
|
+
} else {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const deepest = nodes.at(-1)!;
|
|
79
|
+
const head: Chain["head"] =
|
|
80
|
+
"target" in deepest || deepest.target !== undefined
|
|
81
|
+
? { path: targetPath(basePath, nodes.length), value: deepest.target }
|
|
82
|
+
: null;
|
|
83
|
+
const rootFirst = nodes.map((n, i) => ({ node: n, path: targetPath(basePath, i) }));
|
|
84
|
+
return { head, links: rootFirst.toReversed() };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Compact one-line text form of a node: head operand followed by the operator chain. */
|
|
88
|
+
export function chipSummary(node: unknown): string {
|
|
89
|
+
if (!isExprNode(node)) {
|
|
90
|
+
return operandLabel(node);
|
|
91
|
+
}
|
|
92
|
+
const { head, links } = unrollChain(node, []);
|
|
93
|
+
const parts: string[] = [];
|
|
94
|
+
if (head) {
|
|
95
|
+
parts.push(operandLabel(head.value));
|
|
96
|
+
}
|
|
97
|
+
for (const link of links) {
|
|
98
|
+
parts.push(String(link.node.operator));
|
|
99
|
+
}
|
|
100
|
+
return parts.join(" › ");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Rendering ──────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const CHIP_STYLE =
|
|
106
|
+
"display:inline-flex;align-items:center;gap:4px;max-width:180px;padding:1px 7px;" +
|
|
107
|
+
"border:1px solid var(--spectrum-gray-300, #3c3c3c);border-radius:10px;cursor:pointer;" +
|
|
108
|
+
"background:var(--spectrum-gray-100, #232323);color:var(--spectrum-gray-800, #d0d0d0);" +
|
|
109
|
+
"font-size:11px;font-family:var(--spectrum-code-font-family, monospace);line-height:18px";
|
|
110
|
+
|
|
111
|
+
/** Live value badge — same styling convention as the expression editor's `.expr-live-badge`. */
|
|
112
|
+
function renderChipBadge(preview: EditorPreview | null | undefined, pathKey: string) {
|
|
113
|
+
const text = preview?.values.get(pathKey);
|
|
114
|
+
if (text === undefined) {
|
|
115
|
+
return nothing;
|
|
116
|
+
}
|
|
117
|
+
return html`
|
|
118
|
+
<span
|
|
119
|
+
class="expr-live-badge"
|
|
120
|
+
title=${text}
|
|
121
|
+
style="font-family:var(--spectrum-code-font-family, monospace);font-size:10px;line-height:16px;padding:0 5px;border-radius:4px;background:var(--spectrum-gray-200, #323232);color:var(--spectrum-seafoam-900, #35a690);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px;flex-shrink:1"
|
|
122
|
+
>${text}</span
|
|
123
|
+
>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderChip(
|
|
128
|
+
label: string,
|
|
129
|
+
path: NodePath,
|
|
130
|
+
onSelect: (path: NodePath) => void,
|
|
131
|
+
preview: EditorPreview | null | undefined,
|
|
132
|
+
group = false,
|
|
133
|
+
) {
|
|
134
|
+
return html`
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class=${group ? "formula-chip formula-chip--group" : "formula-chip"}
|
|
138
|
+
data-path=${path.join("/")}
|
|
139
|
+
style=${CHIP_STYLE}
|
|
140
|
+
title=${label}
|
|
141
|
+
@click=${() => onSelect(path)}
|
|
142
|
+
>
|
|
143
|
+
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px"
|
|
144
|
+
>${label}</span
|
|
145
|
+
>
|
|
146
|
+
${renderChipBadge(preview, path.join("/"))}
|
|
147
|
+
</button>
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Collect group chips for a link's non-target expression operands (value/initial/cases). */
|
|
152
|
+
function groupChips(
|
|
153
|
+
link: ChainLink,
|
|
154
|
+
onSelect: (path: NodePath) => void,
|
|
155
|
+
preview: EditorPreview | null | undefined,
|
|
156
|
+
): TemplateResult[] {
|
|
157
|
+
const chips: TemplateResult[] = [];
|
|
158
|
+
const add = (operand: unknown, path: NodePath) => {
|
|
159
|
+
if (isExprNode(operand)) {
|
|
160
|
+
chips.push(renderChip(`(${chipSummary(operand)})`, path, onSelect, preview, true));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const { value, initial, cases } = link.node;
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
for (const [i, item] of value.entries()) {
|
|
166
|
+
add(item, [...link.path, "value", i]);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
add(value, [...link.path, "value"]);
|
|
170
|
+
}
|
|
171
|
+
add(initial, [...link.path, "initial"]);
|
|
172
|
+
if (isJsonObject(cases)) {
|
|
173
|
+
for (const [key, operand] of Object.entries(cases)) {
|
|
174
|
+
add(operand, [...link.path, "cases", key]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
add(link.node.default, [...link.path, "default"]);
|
|
178
|
+
return chips;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Render an expression node as a horizontal chip pipeline. Each chip carries the node's live value
|
|
183
|
+
* badge when a preview is supplied; clicking a chip calls `onSelect` with the node path.
|
|
184
|
+
*/
|
|
185
|
+
export function renderFormulaChips(
|
|
186
|
+
node: unknown,
|
|
187
|
+
onSelect: (path: NodePath) => void,
|
|
188
|
+
opts: { preview?: EditorPreview | null; path?: NodePath } = {},
|
|
189
|
+
): TemplateResult {
|
|
190
|
+
if (!isExprNode(node)) {
|
|
191
|
+
return html`${nothing}`;
|
|
192
|
+
}
|
|
193
|
+
const preview = opts.preview ?? null;
|
|
194
|
+
const basePath = opts.path ?? [];
|
|
195
|
+
const { head, links } = unrollChain(node, basePath);
|
|
196
|
+
|
|
197
|
+
const chips: TemplateResult[] = [];
|
|
198
|
+
if (head) {
|
|
199
|
+
chips.push(renderChip(operandLabel(head.value), head.path, onSelect, preview));
|
|
200
|
+
}
|
|
201
|
+
for (const link of links) {
|
|
202
|
+
chips.push(
|
|
203
|
+
renderChip(String(link.node.operator), link.path, onSelect, preview),
|
|
204
|
+
...groupChips(link, onSelect, preview),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return html`
|
|
209
|
+
<div
|
|
210
|
+
class="formula-chips"
|
|
211
|
+
style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;padding:2px 0 6px"
|
|
212
|
+
>
|
|
213
|
+
${chips}
|
|
214
|
+
</div>
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Formula palette — a command-palette overlay for browsing/searching the formula catalog (spec
|
|
4
|
+
* §19.9). Clones the quick-search pattern: a plain overlay div (no nested sp-overlay), a search
|
|
5
|
+
* field, grouped results, and keyboard navigation. Picking an entry calls `onPick` with it; the
|
|
6
|
+
* caller decides what to insert.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
10
|
+
import { classMap } from "lit-html/directives/class-map.js";
|
|
11
|
+
import { live } from "lit-html/directives/live.js";
|
|
12
|
+
import { ref } from "lit-html/directives/ref.js";
|
|
13
|
+
import { getLayerSlot } from "./layers";
|
|
14
|
+
import { rectOf } from "../utils/geometry";
|
|
15
|
+
|
|
16
|
+
import type { FormulaCatalogEntry } from "./formula-catalog";
|
|
17
|
+
|
|
18
|
+
export interface FormulaPaletteOpts {
|
|
19
|
+
entries: FormulaCatalogEntry[];
|
|
20
|
+
onPick: (entry: FormulaCatalogEntry) => void;
|
|
21
|
+
/** Optional anchor element the panel is positioned under; centered when absent. */
|
|
22
|
+
anchor?: HTMLElement | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let _open = false;
|
|
26
|
+
let _opts: FormulaPaletteOpts | null = null;
|
|
27
|
+
let _query = "";
|
|
28
|
+
let _selectedIndex = 0;
|
|
29
|
+
|
|
30
|
+
function getContainer() {
|
|
31
|
+
return getLayerSlot("popover", "formula-palette");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function openFormulaPalette(opts: FormulaPaletteOpts) {
|
|
35
|
+
_open = true;
|
|
36
|
+
_opts = opts;
|
|
37
|
+
_query = "";
|
|
38
|
+
_selectedIndex = 0;
|
|
39
|
+
renderOverlay();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function closeFormulaPalette() {
|
|
43
|
+
_open = false;
|
|
44
|
+
_opts = null;
|
|
45
|
+
renderOverlay();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Entries matching the current query (label, name, group, or description substring). */
|
|
49
|
+
function filteredEntries(): FormulaCatalogEntry[] {
|
|
50
|
+
const entries = _opts?.entries ?? [];
|
|
51
|
+
const needle = _query.trim().toLowerCase();
|
|
52
|
+
if (!needle) {
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
return entries.filter(
|
|
56
|
+
(e) =>
|
|
57
|
+
e.label.toLowerCase().includes(needle) ||
|
|
58
|
+
e.name.toLowerCase().includes(needle) ||
|
|
59
|
+
e.group.toLowerCase().includes(needle) ||
|
|
60
|
+
e.description.toLowerCase().includes(needle),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Group entries for display, preserving first-seen group order. */
|
|
65
|
+
function groupedEntries(entries: FormulaCatalogEntry[]): [string, FormulaCatalogEntry[]][] {
|
|
66
|
+
const groups = new Map<string, FormulaCatalogEntry[]>();
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const bucket = groups.get(entry.group);
|
|
69
|
+
if (bucket) {
|
|
70
|
+
bucket.push(entry);
|
|
71
|
+
} else {
|
|
72
|
+
groups.set(entry.group, [entry]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...groups.entries()];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pickEntry(entry: FormulaCatalogEntry) {
|
|
79
|
+
const opts = _opts;
|
|
80
|
+
closeFormulaPalette();
|
|
81
|
+
opts?.onPick(entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onInput(e: Event) {
|
|
85
|
+
_query = (e.target as HTMLInputElement).value;
|
|
86
|
+
_selectedIndex = 0;
|
|
87
|
+
renderOverlay();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onKeydown(e: KeyboardEvent) {
|
|
91
|
+
const entries = filteredEntries();
|
|
92
|
+
switch (e.key) {
|
|
93
|
+
case "ArrowDown": {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
_selectedIndex = Math.min(_selectedIndex + 1, entries.length - 1);
|
|
96
|
+
renderOverlay();
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "ArrowUp": {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
_selectedIndex = Math.max(_selectedIndex - 1, 0);
|
|
102
|
+
renderOverlay();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "Enter": {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
if (entries[_selectedIndex]) {
|
|
108
|
+
pickEntry(entries[_selectedIndex]!);
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "Escape": {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
closeFormulaPalette();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
default: {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Inline position style for the panel when anchored to an element. */
|
|
124
|
+
function panelStyle(): string {
|
|
125
|
+
const anchor = _opts?.anchor;
|
|
126
|
+
if (!anchor) {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
const rect = rectOf(anchor);
|
|
130
|
+
if (!rect || (rect.top === 0 && rect.left === 0 && rect.width === 0)) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const left = Math.max(8, Math.min(rect.left, (globalThis.innerWidth || 1200) - 568));
|
|
134
|
+
return `position:fixed;left:${left}px;top:${rect.bottom + 4}px;margin:0`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderOverlay() {
|
|
138
|
+
const container = getContainer();
|
|
139
|
+
if (!_open) {
|
|
140
|
+
litRender(nothing, container);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const entries = filteredEntries();
|
|
145
|
+
const groups = groupedEntries(entries);
|
|
146
|
+
const hasQuery = _query.trim().length > 0;
|
|
147
|
+
|
|
148
|
+
const tpl = html`
|
|
149
|
+
<div class="quick-search-overlay formula-palette-overlay" @click=${closeFormulaPalette}>
|
|
150
|
+
<div
|
|
151
|
+
class="quick-search-panel formula-palette"
|
|
152
|
+
style=${panelStyle()}
|
|
153
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
154
|
+
>
|
|
155
|
+
<input
|
|
156
|
+
class="quick-search-input formula-palette-input"
|
|
157
|
+
type="text"
|
|
158
|
+
placeholder="Search formulas, operators, globals…"
|
|
159
|
+
.value=${live(_query)}
|
|
160
|
+
@input=${onInput}
|
|
161
|
+
@keydown=${onKeydown}
|
|
162
|
+
${ref((el) => {
|
|
163
|
+
if (el) {
|
|
164
|
+
requestAnimationFrame(() => (el as HTMLInputElement).focus());
|
|
165
|
+
}
|
|
166
|
+
})}
|
|
167
|
+
/>
|
|
168
|
+
<div class="quick-search-results">
|
|
169
|
+
${entries.length === 0
|
|
170
|
+
? html`<div class="quick-search-empty">
|
|
171
|
+
${hasQuery ? "No results" : "No entries available"}
|
|
172
|
+
</div>`
|
|
173
|
+
: nothing}
|
|
174
|
+
${groups.map(
|
|
175
|
+
([group, groupEntries]) => html`
|
|
176
|
+
<div class="quick-search-section-label">${group}</div>
|
|
177
|
+
${groupEntries.map((entry) => {
|
|
178
|
+
const index = entries.indexOf(entry);
|
|
179
|
+
return html`
|
|
180
|
+
<div
|
|
181
|
+
class=${classMap({
|
|
182
|
+
"quick-search-item": true,
|
|
183
|
+
selected: index === _selectedIndex,
|
|
184
|
+
})}
|
|
185
|
+
@click=${() => pickEntry(entry)}
|
|
186
|
+
@mouseenter=${() => {
|
|
187
|
+
_selectedIndex = index;
|
|
188
|
+
renderOverlay();
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<span
|
|
192
|
+
class="quick-search-name"
|
|
193
|
+
style="font-family:var(--spectrum-code-font-family, monospace)"
|
|
194
|
+
>${entry.label}</span
|
|
195
|
+
>
|
|
196
|
+
<span class="quick-search-path" title=${entry.description}
|
|
197
|
+
>${entry.description}</span
|
|
198
|
+
>
|
|
199
|
+
<span class="quick-search-badge">${entry.kind}</span>
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
})}
|
|
203
|
+
`,
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
litRender(tpl, container);
|
|
211
|
+
}
|
package/src/ui/layers.ts
CHANGED
|
@@ -78,6 +78,46 @@ export function showConfirmDialog(
|
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Show a three-way Save / Discard / Cancel dialog. Resolves "save", "discard", or "cancel"
|
|
83
|
+
* (dismiss/close counts as cancel).
|
|
84
|
+
*
|
|
85
|
+
* @param {string} headline
|
|
86
|
+
* @param {string | import("lit-html").TemplateResult} message
|
|
87
|
+
* @param {{ saveLabel?: string; discardLabel?: string; cancelLabel?: string }} [opts]
|
|
88
|
+
* @returns {Promise<"save" | "discard" | "cancel">}
|
|
89
|
+
*/
|
|
90
|
+
export function showSaveDiscardDialog(
|
|
91
|
+
headline: string,
|
|
92
|
+
message: string | TemplateResult,
|
|
93
|
+
opts: {
|
|
94
|
+
saveLabel?: string;
|
|
95
|
+
discardLabel?: string;
|
|
96
|
+
cancelLabel?: string;
|
|
97
|
+
} = {},
|
|
98
|
+
): Promise<"save" | "discard" | "cancel"> {
|
|
99
|
+
const { saveLabel = "Save", discardLabel = "Discard", cancelLabel = "Cancel" } = opts;
|
|
100
|
+
return showDialog<"save" | "discard" | "cancel">(
|
|
101
|
+
(done) => html`
|
|
102
|
+
<sp-dialog-wrapper
|
|
103
|
+
open
|
|
104
|
+
underlay
|
|
105
|
+
headline=${headline}
|
|
106
|
+
confirm-label=${saveLabel}
|
|
107
|
+
secondary-label=${discardLabel}
|
|
108
|
+
cancel-label=${cancelLabel}
|
|
109
|
+
size="s"
|
|
110
|
+
@confirm=${() => done("save")}
|
|
111
|
+
@secondary=${() => done("discard")}
|
|
112
|
+
@cancel=${() => done("cancel")}
|
|
113
|
+
@close=${() => done("cancel")}
|
|
114
|
+
>
|
|
115
|
+
<p>${message}</p>
|
|
116
|
+
</sp-dialog-wrapper>
|
|
117
|
+
`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
/**
|
|
82
122
|
* Open a persistent modal. Returns a handle with update() and close() methods.
|
|
83
123
|
*
|
package/src/ui/media-picker.ts
CHANGED
|
@@ -93,7 +93,7 @@ async function loadMediaCache() {
|
|
|
93
93
|
// Re-render the host panels so the Browse button (gated on mediaCache.length) appears once the
|
|
94
94
|
// Async listing resolves — including when an image value is already set, so the current image can
|
|
95
95
|
// Be replaced. Mirrors loadLayoutEntries()'s renderOnly() in head-panel.
|
|
96
|
-
renderOnly("leftPanel", "rightPanel");
|
|
96
|
+
renderOnly("leftPanel", "rightPanel", "frontmatterPanel");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** Force media cache reload (e.g. after upload). */
|
package/src/ui/panel-resize.ts
CHANGED
|
@@ -13,6 +13,7 @@ const MIN_WIDTH = 160;
|
|
|
13
13
|
const MAX_RATIO = 0.5; // Max 50% of viewport
|
|
14
14
|
const DEFAULT_LEFT = 240;
|
|
15
15
|
const DEFAULT_RIGHT = 280;
|
|
16
|
+
const DEFAULT_CHAT = 320;
|
|
16
17
|
|
|
17
18
|
const root = document.documentElement;
|
|
18
19
|
|
|
@@ -27,8 +28,10 @@ try {
|
|
|
27
28
|
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}") as {
|
|
28
29
|
left?: number;
|
|
29
30
|
right?: number;
|
|
31
|
+
chat?: number;
|
|
30
32
|
leftCollapsed?: boolean;
|
|
31
33
|
rightCollapsed?: boolean;
|
|
34
|
+
chatCollapsed?: boolean;
|
|
32
35
|
};
|
|
33
36
|
if (saved.left) {
|
|
34
37
|
root.style.setProperty("--panel-w-left", `${saved.left}px`);
|
|
@@ -36,12 +39,18 @@ try {
|
|
|
36
39
|
if (saved.right) {
|
|
37
40
|
root.style.setProperty("--panel-w-right", `${saved.right}px`);
|
|
38
41
|
}
|
|
42
|
+
if (saved.chat) {
|
|
43
|
+
root.style.setProperty("--panel-w-chat", `${saved.chat}px`);
|
|
44
|
+
}
|
|
39
45
|
if (saved.leftCollapsed) {
|
|
40
46
|
view.leftPanelCollapsed = true;
|
|
41
47
|
}
|
|
42
48
|
if (saved.rightCollapsed) {
|
|
43
49
|
view.rightPanelCollapsed = true;
|
|
44
50
|
}
|
|
51
|
+
if (saved.chatCollapsed) {
|
|
52
|
+
view.chatPanelCollapsed = true;
|
|
53
|
+
}
|
|
45
54
|
applyPanelCollapse();
|
|
46
55
|
} catch {
|
|
47
56
|
// Ignore
|
|
@@ -111,8 +120,9 @@ function setupHandle(
|
|
|
111
120
|
function persistWidths() {
|
|
112
121
|
const left = readPxVar("--panel-w-left") || DEFAULT_LEFT;
|
|
113
122
|
const right = readPxVar("--panel-w-right") || DEFAULT_RIGHT;
|
|
123
|
+
const chat = readPxVar("--panel-w-chat") || DEFAULT_CHAT;
|
|
114
124
|
try {
|
|
115
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify({ left, right }));
|
|
125
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ chat, left, right }));
|
|
116
126
|
} catch {
|
|
117
127
|
// Storage full or unavailable
|
|
118
128
|
}
|
|
@@ -122,6 +132,7 @@ function persistWidths() {
|
|
|
122
132
|
|
|
123
133
|
const resizeLeft = document.querySelector<HTMLElement>("#resize-left");
|
|
124
134
|
const resizeRight = document.querySelector<HTMLElement>("#resize-right");
|
|
135
|
+
const resizeChat = document.querySelector<HTMLElement>("#resize-chat");
|
|
125
136
|
|
|
126
137
|
if (resizeLeft) {
|
|
127
138
|
setupHandle(resizeLeft, "--panel-w-left", "left", DEFAULT_LEFT);
|
|
@@ -129,3 +140,6 @@ if (resizeLeft) {
|
|
|
129
140
|
if (resizeRight) {
|
|
130
141
|
setupHandle(resizeRight, "--panel-w-right", "right", DEFAULT_RIGHT);
|
|
131
142
|
}
|
|
143
|
+
if (resizeChat) {
|
|
144
|
+
setupHandle(resizeChat, "--panel-w-chat", "right", DEFAULT_CHAT);
|
|
145
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display formatting for live expression-preview values, shared by the parent-side snapshot
|
|
3
|
+
* evaluator (services/preview-eval.ts) and the in-iframe live evaluator (canvas/iframe-eval.ts) so
|
|
4
|
+
* both realms badge values with identical truncation rules. Dependency-light and DOM-free (the
|
|
5
|
+
* strip-events pattern) so importing it never grows the iframe bundle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MAX_BADGE_LENGTH = 48;
|
|
9
|
+
|
|
10
|
+
/** Format a runtime value as a short badge string. */
|
|
11
|
+
export function formatPreviewValue(value?: unknown): string {
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
return "undefined";
|
|
14
|
+
}
|
|
15
|
+
let text: string;
|
|
16
|
+
try {
|
|
17
|
+
text =
|
|
18
|
+
typeof value === "string" ? JSON.stringify(value) : (JSON.stringify(value) ?? "undefined");
|
|
19
|
+
} catch {
|
|
20
|
+
text = String(value);
|
|
21
|
+
}
|
|
22
|
+
if (text.length > MAX_BADGE_LENGTH) {
|
|
23
|
+
text = `${text.slice(0, MAX_BADGE_LENGTH - 1)}…`;
|
|
24
|
+
}
|
|
25
|
+
return text;
|
|
26
|
+
}
|
package/src/view.ts
CHANGED
|
@@ -46,7 +46,7 @@ interface ViewState {
|
|
|
46
46
|
leftTab: string;
|
|
47
47
|
leftPanelCollapsed: boolean;
|
|
48
48
|
rightPanelCollapsed: boolean;
|
|
49
|
-
|
|
49
|
+
chatPanelCollapsed: boolean;
|
|
50
50
|
_layersCollapsed: Set<string> | null;
|
|
51
51
|
[key: string]: unknown;
|
|
52
52
|
}
|
|
@@ -104,9 +104,7 @@ export const view: ViewState = {
|
|
|
104
104
|
leftTab: "layers",
|
|
105
105
|
leftPanelCollapsed: false,
|
|
106
106
|
rightPanelCollapsed: false,
|
|
107
|
-
|
|
108
|
-
// Autosave
|
|
109
|
-
autosaveTimer: null,
|
|
107
|
+
chatPanelCollapsed: false,
|
|
110
108
|
|
|
111
109
|
// Layers panel collapsed state
|
|
112
110
|
_layersCollapsed: null,
|
|
@@ -121,6 +119,7 @@ export function applyPanelCollapse() {
|
|
|
121
119
|
}
|
|
122
120
|
app.classList.toggle("left-collapsed", view.leftPanelCollapsed);
|
|
123
121
|
app.classList.toggle("right-collapsed", view.rightPanelCollapsed);
|
|
122
|
+
app.classList.toggle("chat-collapsed", view.chatPanelCollapsed);
|
|
124
123
|
try {
|
|
125
124
|
const saved = JSON.parse(localStorage.getItem(COLLAPSE_STORAGE_KEY) || "{}") as Record<
|
|
126
125
|
string,
|
|
@@ -128,6 +127,7 @@ export function applyPanelCollapse() {
|
|
|
128
127
|
>;
|
|
129
128
|
saved.leftCollapsed = view.leftPanelCollapsed;
|
|
130
129
|
saved.rightCollapsed = view.rightPanelCollapsed;
|
|
130
|
+
saved.chatCollapsed = view.chatPanelCollapsed;
|
|
131
131
|
localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(saved));
|
|
132
132
|
} catch {
|
|
133
133
|
// Storage unavailable
|
|
@@ -57,6 +57,20 @@ export const activeTab = computed(() =>
|
|
|
57
57
|
workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
|
|
58
58
|
) as unknown as ComputedRef<Tab | null>;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Record the open project on the reactive workspace. `root` must be the absolute project root (the
|
|
62
|
+
* same value `createProject` returns and the pending-agent-prompt handoff is keyed by) — not the
|
|
63
|
+
* "."-relative root projectState sometimes holds. Consumers: AI session scoping, system-prompt
|
|
64
|
+
* project context, and the chat panel's pending-prompt effect.
|
|
65
|
+
*
|
|
66
|
+
* @param {string | null} root
|
|
67
|
+
* @param {object | null} [config]
|
|
68
|
+
*/
|
|
69
|
+
export function setWorkspaceProject(root: string | null, config: object | null = null) {
|
|
70
|
+
workspace.projectRoot = root;
|
|
71
|
+
workspace.projectConfig = config;
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
/**
|
|
61
75
|
* Whether `tab` is the active tab. Identity check survives reactive-proxy wrapping (activeTab.value
|
|
62
76
|
* is a proxy; callers may hold either the raw tab or a proxy of it).
|