feat: registry
This commit is contained in:
@@ -224,15 +224,39 @@ const COMMANDS: CmdEntry[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Dynamic commands from /api/dsl-meta — merged into COMMANDS
|
||||
let dynamicCommands: CmdEntry[] = [];
|
||||
|
||||
/** Update slash commands from /api/dsl-meta response. */
|
||||
export function setDslCommands(
|
||||
commands: { label: string; detail: string; section: string; snippet: string }[],
|
||||
) {
|
||||
// Build dynamic commands from API data, only for entries not already in COMMANDS
|
||||
const existingLabels = new Set(COMMANDS.map((c) => c.label));
|
||||
dynamicCommands = commands
|
||||
.filter((c) => c.detail && c.snippet && !existingLabels.has(c.label))
|
||||
.map((c) => ({
|
||||
label: c.label,
|
||||
detail: c.detail,
|
||||
section: c.section,
|
||||
apply: c.snippet.includes("${")
|
||||
? slashSnippet(c.snippet)
|
||||
: insert(c.snippet),
|
||||
}));
|
||||
}
|
||||
|
||||
export function uframeCommandSource(
|
||||
ctx: CompletionContext,
|
||||
): CompletionResult | null {
|
||||
const match = ctx.matchBefore(/\/\w*/);
|
||||
if (!match || (match.from === match.to && !ctx.explicit)) return null;
|
||||
|
||||
const allCommands = [...COMMANDS, ...dynamicCommands];
|
||||
|
||||
return {
|
||||
from: match.from + 1,
|
||||
filter: true,
|
||||
options: COMMANDS.map((cmd) => ({
|
||||
options: allCommands.map((cmd) => ({
|
||||
label: cmd.label,
|
||||
detail: cmd.detail,
|
||||
section: cmd.section,
|
||||
|
||||
@@ -8,14 +8,13 @@ import { tags } from "@lezer/highlight";
|
||||
/**
|
||||
* CodeMirror 6 syntax highlighting for the µFrame .uf DSL.
|
||||
*
|
||||
* Keywords: page, box, row, col, spacer, pad, heading, text, label,
|
||||
* divider, link, list, item, gauge, sparkline, status,
|
||||
* table, columns, form, field, radio, checkbox, button,
|
||||
* source, let, if, elif, else, for, align, color, bg,
|
||||
* bold, italic, underline, cache, state, on_submit
|
||||
* Keywords and values can be updated dynamically via setDslKeywords()
|
||||
* which is called when the frontend fetches /api/dsl-meta.
|
||||
*/
|
||||
|
||||
const KEYWORDS = new Set([
|
||||
// Mutable sets — updated from /api/dsl-meta
|
||||
let KEYWORDS = new Set([
|
||||
// Fallback defaults (used before API response arrives)
|
||||
"page", "box", "row", "col", "spacer", "pad",
|
||||
"heading", "text", "label", "divider", "link",
|
||||
"list", "item", "gauge", "sparkline", "status",
|
||||
@@ -23,17 +22,24 @@ const KEYWORDS = new Set([
|
||||
"checkbox", "button", "source", "let", "if",
|
||||
"elif", "else", "for", "align", "color", "bg",
|
||||
"bold", "italic", "underline", "cache", "state",
|
||||
"on_submit", "meter", "bar_h", "bar_v", "bar",
|
||||
"heatmap", "component", "use",
|
||||
"on_submit", "theme", "component", "use",
|
||||
]);
|
||||
|
||||
const WEIGHT_VALS = new Set([
|
||||
let WEIGHT_VALS = new Set([
|
||||
"light", "heavy", "double", "rounded",
|
||||
"bullet", "dash", "number", "arrow",
|
||||
"left", "center", "right",
|
||||
"online", "offline", "degraded", "unknown",
|
||||
"shell", "file", "json", "python", "rns", "param",
|
||||
"default", "nouveau", "gothic", "bamboo", "circuit", "brutalist",
|
||||
]);
|
||||
|
||||
/** Update keywords and values from /api/dsl-meta response. */
|
||||
export function setDslKeywords(keywords: string[], values: string[]) {
|
||||
if (keywords.length > 0) KEYWORDS = new Set(keywords);
|
||||
if (values.length > 0) WEIGHT_VALS = new Set(values);
|
||||
}
|
||||
|
||||
const uframeLanguage = StreamLanguage.define({
|
||||
token(stream) {
|
||||
// Comments
|
||||
|
||||
35
frontend/src/hooks/useDslMeta.ts
Normal file
35
frontend/src/hooks/useDslMeta.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface DslMeta {
|
||||
keywords: string[];
|
||||
values: string[];
|
||||
commands: { label: string; detail: string; section: string; snippet: string }[];
|
||||
themes: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_META: DslMeta = {
|
||||
keywords: [],
|
||||
values: [],
|
||||
commands: [],
|
||||
themes: [],
|
||||
};
|
||||
|
||||
let cachedMeta: DslMeta | null = null;
|
||||
|
||||
export function useDslMeta(): DslMeta {
|
||||
const [meta, setMeta] = useState<DslMeta>(cachedMeta || DEFAULT_META);
|
||||
|
||||
useEffect(() => {
|
||||
if (cachedMeta) return;
|
||||
|
||||
fetch("/api/dsl-meta")
|
||||
.then((r) => r.json())
|
||||
.then((data: DslMeta) => {
|
||||
cachedMeta = data;
|
||||
setMeta(data);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return meta;
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import { useEditorStore } from "@/stores/editorStore";
|
||||
import { usePagesStore } from "@/stores/pagesStore";
|
||||
import { useUnsavedGuard } from "@/hooks/useUnsavedGuard";
|
||||
import { useCompile } from "@/hooks/useCompile";
|
||||
import { uframeHighlight } from "@/components/editor/uframeHighlight";
|
||||
import { uframeCommandSource } from "@/components/editor/uframeCommands";
|
||||
import { useDslMeta } from "@/hooks/useDslMeta";
|
||||
import { uframeHighlight, setDslKeywords } from "@/components/editor/uframeHighlight";
|
||||
import { uframeCommandSource, setDslCommands } from "@/components/editor/uframeCommands";
|
||||
import EditorPane from "@/components/editor/EditorPane";
|
||||
import PreviewPane from "@/components/editor/PreviewPane";
|
||||
import ToolBar from "@/components/editor/ToolBar";
|
||||
@@ -47,6 +48,15 @@ export default function EditorView() {
|
||||
[],
|
||||
);
|
||||
|
||||
// Load DSL metadata (keywords, values, commands) from backend
|
||||
const dslMeta = useDslMeta();
|
||||
useEffect(() => {
|
||||
if (dslMeta.keywords.length > 0) {
|
||||
setDslKeywords(dslMeta.keywords, dslMeta.values);
|
||||
setDslCommands(dslMeta.commands);
|
||||
}
|
||||
}, [dslMeta]);
|
||||
|
||||
// Auto-compile on source changes
|
||||
useCompile();
|
||||
useUnsavedGuard();
|
||||
|
||||
Reference in New Issue
Block a user