mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Add snippet metadata panel, duplicate, and immediate-load preview (M4.5)
This commit is contained in:
@@ -18,7 +18,12 @@
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { createSnippet, type CreateSnippetOptions, type Snippet } from '@core/snippet';
|
||||
import {
|
||||
createSnippet,
|
||||
duplicateSnippet as duplicateSnippetRecord,
|
||||
type CreateSnippetOptions,
|
||||
type Snippet,
|
||||
} from '@core/snippet';
|
||||
import { recomputeDatasetRefs, renameDatasetInSpec } from '@core/spec-refs';
|
||||
|
||||
/** Which version of the active snippet the editor is showing (spec §03D). */
|
||||
@@ -47,6 +52,27 @@ export interface SnippetState {
|
||||
selectSnippet: (id: string) => void;
|
||||
/** Remove a snippet; if it was active, fall back to the newest remaining one. */
|
||||
removeSnippet: (id: string) => void;
|
||||
/**
|
||||
* Rename a snippet (spec §02 metadata panel, inline name edit). Advances
|
||||
* `modified` (a name edit is a save, §02 → Sort), but never touches the editor
|
||||
* buffer — the name isn't part of the spec text. No-op for an unknown id or an
|
||||
* unchanged name. `now` injectable.
|
||||
*/
|
||||
renameSnippet: (id: string, name: string, now?: Date) => void;
|
||||
/**
|
||||
* Set a snippet's free-form comment (spec §02 metadata panel). Advances
|
||||
* `modified` like a rename; no editor-buffer effect. No-op for an unknown id or
|
||||
* an unchanged comment. `now` injectable.
|
||||
*/
|
||||
setComment: (id: string, comment: string, now?: Date) => void;
|
||||
/**
|
||||
* Duplicate the active snippet (spec §02 → Duplicate): flushes the live buffer
|
||||
* into the source draft first so the copy reflects in-progress edits, then
|
||||
* prepends an independent copy ("(copy)" name, fresh identity/timestamps) and
|
||||
* makes it active. Returns the new id, or null if no snippet is active. `now`
|
||||
* injectable; `id` injectable for deterministic tests.
|
||||
*/
|
||||
duplicateActiveSnippet: (now?: Date, id?: string) => string | null;
|
||||
/** Update the draft buffer only (no persistence; debounced commit follows). */
|
||||
updateDraft: (text: string) => void;
|
||||
/**
|
||||
@@ -158,6 +184,9 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
|
||||
set((s) => {
|
||||
const snippets = s.snippets.filter((x) => x.id !== id);
|
||||
if (s.activeSnippetId !== id) return { snippets };
|
||||
// Deleting the active snippet falls back to the newest remaining one, so the
|
||||
// editor and detail panel stay populated (spec §02 → Delete); null only when
|
||||
// none remain.
|
||||
const activeSnippetId = newestId(snippets);
|
||||
return {
|
||||
snippets,
|
||||
@@ -169,6 +198,48 @@ export const useSnippetStore = create<SnippetState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
renameSnippet: (id, name, now) => {
|
||||
set((s) => {
|
||||
const target = s.snippets.find((x) => x.id === id);
|
||||
if (!target || target.name === name) return s; // unknown id or no change
|
||||
const modified = (now ?? new Date()).toISOString();
|
||||
return {
|
||||
snippets: s.snippets.map((x) => (x.id === id ? { ...x, name, modified } : x)),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setComment: (id, comment, now) => {
|
||||
set((s) => {
|
||||
const target = s.snippets.find((x) => x.id === id);
|
||||
if (!target || target.comment === comment) return s; // unknown id or no change
|
||||
const modified = (now ?? new Date()).toISOString();
|
||||
return {
|
||||
snippets: s.snippets.map((x) => (x.id === id ? { ...x, comment, modified } : x)),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
duplicateActiveSnippet: (now, id) => {
|
||||
// Flush the live buffer into the source draft first, so the copy faithfully
|
||||
// mirrors what the user currently sees, not the last auto-saved draft.
|
||||
get().commitDraft(now);
|
||||
const { activeSnippetId, snippets } = get();
|
||||
if (!activeSnippetId) return null;
|
||||
const source = snippets.find((s) => s.id === activeSnippetId);
|
||||
if (!source) return null;
|
||||
|
||||
const copy = duplicateSnippetRecord(source, { now, id });
|
||||
set((s) => ({
|
||||
snippets: [copy, ...s.snippets],
|
||||
activeSnippetId: copy.id,
|
||||
draftText: copy.draftSpec,
|
||||
editorView: 'draft', // a fresh copy opens on its editable draft
|
||||
bufferEpoch: s.bufferEpoch + 1,
|
||||
}));
|
||||
return copy.id;
|
||||
},
|
||||
|
||||
updateDraft: (draftText) => set({ draftText }),
|
||||
|
||||
replaceActiveDraft: (text, now) => {
|
||||
|
||||
Reference in New Issue
Block a user