@w6w/embed 0.0.0-stage → 0.1.1

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.
Files changed (102) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +166 -2
  3. package/dist/W6WEmbedProvider.d.ts +29 -0
  4. package/dist/chrome/CallFromCodeButton.d.ts +155 -0
  5. package/dist/chrome/CallableSettingsModal.d.ts +17 -0
  6. package/dist/chrome/ConfirmModal.d.ts +20 -0
  7. package/dist/chrome/DangerZone.d.ts +19 -0
  8. package/dist/chrome/ExecutionProfileHeader.d.ts +69 -0
  9. package/dist/chrome/HistoryButton.d.ts +8 -0
  10. package/dist/chrome/HistoryModal.d.ts +9 -0
  11. package/dist/chrome/HistoryView.d.ts +15 -0
  12. package/dist/chrome/ImportSpecModal.d.ts +40 -0
  13. package/dist/chrome/JsonToggle.d.ts +51 -0
  14. package/dist/chrome/Modal.d.ts +20 -0
  15. package/dist/chrome/RightRail.d.ts +24 -0
  16. package/dist/chrome/TagEditor.d.ts +23 -0
  17. package/dist/chrome/VersionHistoryModal.d.ts +38 -0
  18. package/dist/chrome/history-open-run.d.ts +7 -0
  19. package/dist/chrome/index.d.ts +36 -0
  20. package/dist/chrome/workspace.d.ts +67 -0
  21. package/dist/client-context-CX61Bgw-.js +16 -0
  22. package/dist/client-context-CX61Bgw-.js.map +1 -0
  23. package/dist/draft/autosave-flush.d.ts +63 -0
  24. package/dist/draft/autosave.d.ts +68 -0
  25. package/dist/draft/client-context.d.ts +8 -0
  26. package/dist/draft/draft-registry.d.ts +14 -0
  27. package/dist/draft/index.d.ts +15 -0
  28. package/dist/draft/use-workflow-draft.d.ts +67 -0
  29. package/dist/draft/workflow-save.d.ts +48 -0
  30. package/dist/draft-registry-YF0tqNN0.js +49 -0
  31. package/dist/draft-registry-YF0tqNN0.js.map +1 -0
  32. package/dist/draft.d.ts +2 -0
  33. package/dist/draft.mjs +18 -0
  34. package/dist/draft.mjs.map +1 -0
  35. package/dist/editor/WorkflowEditor.d.ts +12 -0
  36. package/dist/editor/WorkflowSettingsModal.d.ts +17 -0
  37. package/dist/editor/definition.d.ts +31 -0
  38. package/dist/editor/editor-apps.d.ts +7 -0
  39. package/dist/editor/types.d.ts +34 -0
  40. package/dist/editor.d.ts +9 -0
  41. package/dist/editor.mjs +2170 -0
  42. package/dist/editor.mjs.map +1 -0
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.mjs +67 -0
  45. package/dist/index.mjs.map +1 -0
  46. package/dist/lib/callable-descriptors.d.ts +77 -0
  47. package/dist/lib/callable-kind.d.ts +13 -0
  48. package/dist/lib/code-rail.d.ts +45 -0
  49. package/dist/lib/document-sources.d.ts +26 -0
  50. package/dist/lib/error-handler-self.d.ts +25 -0
  51. package/dist/lib/history-query.d.ts +135 -0
  52. package/dist/lib/import-paste.d.ts +41 -0
  53. package/dist/lib/index.d.ts +17 -0
  54. package/dist/lib/new-id.d.ts +10 -0
  55. package/dist/lib/rail-settings.d.ts +196 -0
  56. package/dist/lib/save-invalidation.d.ts +24 -0
  57. package/dist/lib/sdk-snippets.d.ts +96 -0
  58. package/dist/lib/settings-rail.d.ts +68 -0
  59. package/dist/lib/slug.d.ts +69 -0
  60. package/dist/lib/spec-import-invalidation.d.ts +48 -0
  61. package/dist/lib/tag-editor.d.ts +36 -0
  62. package/dist/lib/trigger-preview.d.ts +89 -0
  63. package/dist/lib/workflow-live-run.d.ts +165 -0
  64. package/dist/lib/workflow-poll.d.ts +8 -0
  65. package/dist/lib/workflow-route.d.ts +26 -0
  66. package/dist/lib/workflow-settings.d.ts +72 -0
  67. package/dist/lib/workflow-validity.d.ts +92 -0
  68. package/dist/lib/workflow-variables.d.ts +106 -0
  69. package/dist/lib.d.ts +2 -0
  70. package/dist/lib.mjs +62 -0
  71. package/dist/lib.mjs.map +1 -0
  72. package/dist/project-context-Dbk0Gr1K.js +16 -0
  73. package/dist/project-context-Dbk0Gr1K.js.map +1 -0
  74. package/dist/project-context.d.ts +11 -0
  75. package/dist/rail/CodeTool.d.ts +2 -0
  76. package/dist/rail/ErrorHandlingTool.d.ts +22 -0
  77. package/dist/rail/ExpressionScope.d.ts +21 -0
  78. package/dist/rail/SettingsTool.d.ts +27 -0
  79. package/dist/rail/editor-tools.d.ts +2 -0
  80. package/dist/rail/rail-env.d.ts +18 -0
  81. package/dist/rail/settings-sections.d.ts +93 -0
  82. package/dist/repos/documents.d.ts +102 -0
  83. package/dist/repos/history.d.ts +24 -0
  84. package/dist/repos/index.d.ts +22 -0
  85. package/dist/repos/spec-import.d.ts +44 -0
  86. package/dist/repos/step-tests.d.ts +19 -0
  87. package/dist/repos/vars.d.ts +25 -0
  88. package/dist/repos/vault.d.ts +24 -0
  89. package/dist/repos/versions.d.ts +112 -0
  90. package/dist/repos.d.ts +2 -0
  91. package/dist/repos.mjs +94 -0
  92. package/dist/repos.mjs.map +1 -0
  93. package/dist/slug-ByKQgxrf.js +642 -0
  94. package/dist/slug-ByKQgxrf.js.map +1 -0
  95. package/dist/spec-import-HgO7ZkLZ.js +898 -0
  96. package/dist/spec-import-HgO7ZkLZ.js.map +1 -0
  97. package/dist/spec-import-invalidation-D3retX4Q.js +18 -0
  98. package/dist/spec-import-invalidation-D3retX4Q.js.map +1 -0
  99. package/dist/use-workflow-draft-oI02dsP-.js +314 -0
  100. package/dist/use-workflow-draft-oI02dsP-.js.map +1 -0
  101. package/package.json +84 -3
  102. package/src/styles.css +829 -0
@@ -0,0 +1,51 @@
1
+ export type JsonToggleMode = "synced" | "disconnected";
2
+ /** The header icon button: `IconButton` with the set's `json` glyph — the
3
+ * curly-brace icon, distinct from the `code` `</>` "Call from code" icon
4
+ * beside it, so the two stop reading as the same button. The local
5
+ * `JsonIcon` SVG this file used to carry is now `icon="json"` in the shared
6
+ * set. */
7
+ export declare function JsonToggleButton(props: {
8
+ onClick: () => void;
9
+ }): import("react").JSX.Element;
10
+ export interface JsonToggleProps {
11
+ /** Parent-owned; render nothing when false. */
12
+ open: boolean;
13
+ onClose: () => void;
14
+ /** Modal title. */
15
+ title: string;
16
+ mode: JsonToggleMode;
17
+ /** Parent-owned text; the component keeps NO copy of it. */
18
+ value: string;
19
+ /** Every JsonEditor change, both modes. */
20
+ onChange: (text: string) => void;
21
+ /** JsonEditor's, error ?? null. */
22
+ onValidityChange?: (v: {
23
+ valid: boolean;
24
+ error: string | null;
25
+ }) => void;
26
+ /** When non-null: "Invalid JSON — {invalidReason}" (Workflow's existing line). */
27
+ invalidReason?: string | null;
28
+ /** Evaluated on `value` every render; non-null → shown in
29
+ * <p className="muted small" data-testid="json-toggle-reason">. */
30
+ guard?: (text: string) => string | null;
31
+ /** disconnected only. */
32
+ onSave?: () => void;
33
+ /** disconnected only. */
34
+ saving?: boolean;
35
+ /** disconnected only; rendered as <div className="error"> inside the dialog. */
36
+ error?: string | null;
37
+ /** JsonEditor aria-label. */
38
+ editorLabel: string;
39
+ }
40
+ /**
41
+ * The JSON⇄editor toggle's chrome — the header icon button's dialog, extracted
42
+ * verbatim from `WorkflowsPage.tsx`'s raw-JSON modal (T2.1.1 D-2) so a second
43
+ * host can reuse it in `mode="disconnected"` (T2.1.2). Wraps `@w6w/ui`'s
44
+ * `JsonEditor` and studio's own `Modal` (not `@w6w/ui`'s) — both existing
45
+ * JSON-toggle-shaped precedents in this repo use studio's `Modal`.
46
+ *
47
+ * The component holds NO copy of `value`: every render shows exactly what the
48
+ * parent hands it, which is what keeps a reopened dialog free of a stale
49
+ * snapshot from the previous time it was open.
50
+ */
51
+ export declare function JsonToggle(props: JsonToggleProps): JSX.Element | null;
@@ -0,0 +1,20 @@
1
+ interface ModalProps {
2
+ title: string;
3
+ onClose: () => void;
4
+ children: React.ReactNode;
5
+ /** Widen the dialog (e.g. for a JSON editor that needs room). */
6
+ wide?: boolean;
7
+ /** Widen further still (e.g. for the plan comparison grid). */
8
+ xl?: boolean;
9
+ /** Trailing control on the title row (e.g. a close `IconButton`). Given,
10
+ * the title and this node share one `.modal-head` row; absent, the title
11
+ * renders as a direct child exactly as before. */
12
+ headerRight?: React.ReactNode;
13
+ }
14
+ /**
15
+ * Small dialog modal. Built on the native <dialog> element so it gets focus
16
+ * trapping, Esc-dismiss, and accessibility semantics for free. A button
17
+ * positioned over the backdrop catches outside-clicks to close.
18
+ */
19
+ export declare function Modal({ title, onClose, children, wide, xl, headerRight, }: ModalProps): import("react").JSX.Element;
20
+ export {};
@@ -0,0 +1,24 @@
1
+ import { ComponentType } from 'react';
2
+ import { WorkspaceCallable } from './workspace.tsx';
3
+ export interface RailTool {
4
+ /** Stable; used as the persisted `activeTool`. */
5
+ id: string;
6
+ /** Strip tooltip + panel heading. */
7
+ title: string;
8
+ /** The strip glyph, as a component — built-in tools render a 16×16 `<Icon name="…" />`
9
+ * from `@w6w/ui`'s shared set; a tool may bring its own glyph instead. A component,
10
+ * not an `IconName`, so this seam never narrows to the shared set. */
11
+ Icon: ComponentType;
12
+ isAvailable(ctx: WorkspaceCallable): boolean;
13
+ /** Panel renders its own heading when it includes section actions. */
14
+ rendersHeading?: boolean;
15
+ Panel: ComponentType<{
16
+ ctx: WorkspaceCallable;
17
+ }>;
18
+ }
19
+ /** `storageKey === null`: no key to persist under yet (pin 12) — the strip renders closed and
20
+ * nothing is written to localStorage. No available tool ⇒ no rail element at all (C/D-C3). */
21
+ export declare function RightRail({ tools, storageKey, }: {
22
+ tools: readonly RailTool[];
23
+ storageKey: string | null;
24
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A chip input for a workflow's free-form tags: type text, `,` commits the
3
+ * typed value as a chip (or paste a whole comma-separated list to commit
4
+ * several at once — `../lib/tag-editor.ts`'s `commitTag`), each chip carries
5
+ * an `×` to remove it, and a still-typable tail follows the chips — one
6
+ * control, not a stacked input-above-list.
7
+ *
8
+ * Built fresh (T2.2.1) rather than extended from the retiring
9
+ * `WorkflowsPage.tsx` tags-editing modal: that component was a submit-button
10
+ * "Add" form with its own inline add/remove branching, a different
11
+ * interaction shape from a comma-commits chip editor. The shared data
12
+ * contract (trim, dedupe-on-add, drop empties) now lives in
13
+ * `../lib/tag-editor.ts`'s
14
+ * `commitTag`/`removeTag`, so this component holds no branching of its own —
15
+ * it only calls them and renders the result. Controlled, mirroring
16
+ * `InputListEditor.tsx`'s shape: the caller owns `tags`, this component owns
17
+ * only the not-yet-committed typed tail.
18
+ */
19
+ export interface TagEditorProps {
20
+ tags: string[];
21
+ onChange: (next: string[]) => void;
22
+ }
23
+ export declare function TagEditor({ tags, onChange }: TagEditorProps): import("react").JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { VersionsKind } from '@w6w/embed/repos';
2
+ /**
3
+ * Version viewer for Function/Workflow/Endpoint (T3.1.1, extended by T5.3.1): one
4
+ * kind-parametrized component, mirroring `ImportSpecModal.tsx:12-46`'s established shape (a
5
+ * `kind` prop plus per-kind `TESTIDS`, one component mounted from more than one place rather than
6
+ * three near-copies). This project mounts it from `SettingsTool.tsx` only — the component
7
+ * boundary exists so a second mount point later costs one line.
8
+ *
9
+ * Renders the version's own read-only definition through the same two primitives
10
+ * `SettingsTool.tsx`'s existing Export modal already uses — the studio-local `Modal` and
11
+ * `@w6w/ui`'s `CodeBlock` — plus, since T5.3.1, two live actions:
12
+ *
13
+ * - **"Revert to this version"** — confirmed via the studio-local `ConfirmModal` (P2-D4:
14
+ * `./ConfirmModal.tsx`, never `@w6w/ui`'s), rendered as a SIBLING of `<Modal>` in
15
+ * the returned fragment, never nested inside it. Overwrites the LIVE resource's `definition`
16
+ * with this version's content, preserving its `key`/address (HITL-4's pinned default).
17
+ * - **"Invoke this version"** — one click, NO confirm (ruling #12: an invoke is a call, not a
18
+ * write to the resource, and the LIVE invoke action it mirrors carries no confirm either). Runs
19
+ * this version's own archived definition, never the live one, and renders the response body
20
+ * inline via `CodeBlock`, or a failure inline via the `ApiError` idiom (never a thrown error, a
21
+ * blank modal, or a browser dialog).
22
+ *
23
+ * Both actions read `kind`/`id`/`versionNumber` from props, and both mutations are fresh
24
+ * `useMutation` instances scoped to this component instance — since `SettingsTool.tsx`'s
25
+ * `openVersion` gate fully unmounts/remounts this component per version rather than swapping
26
+ * props on a surviving instance, any prior invoke result or error is gone the moment a different
27
+ * version is opened.
28
+ *
29
+ * Amendments #5/#6 (project.md, Phase 2) supersede the earlier pin that made both of the above
30
+ * out of scope (HITL-2's "invocation always runs the live definition"; Amendment #5's "restore is
31
+ * out of scope, not even a disabled button").
32
+ */
33
+ export declare function VersionHistoryModal(props: {
34
+ kind: VersionsKind;
35
+ id: string;
36
+ versionNumber: number;
37
+ onClose: () => void;
38
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { CallableKind, ExecutionSummary } from '../repos/index.ts';
2
+ /**
3
+ * The open-in-editor decision for a History view: only a workflow run has an editor to open, and
4
+ * only when the host supplied `onOpenRun`. The handler closes the modal BEFORE handing the run id
5
+ * to the host (the host's navigation then lands on an unobscured page).
6
+ */
7
+ export declare function historyOpenRun(kind: CallableKind, onOpenRun: ((runId: string) => void) | undefined, onClose: () => void): ((execution: ExecutionSummary) => void) | undefined;
@@ -0,0 +1,36 @@
1
+ export { CallableSettingsModal } from './CallableSettingsModal.tsx';
2
+ export { CallFromCodeBody, CallFromCodeButton, SNIPPET_TABS, snippetTabsFor, } from './CallFromCodeButton.tsx';
3
+ export type { SnippetTabId } from './CallFromCodeButton.tsx';
4
+ export { ConfirmModal } from './ConfirmModal.tsx';
5
+ export { DangerZone } from './DangerZone.tsx';
6
+ export type { DangerZoneProps } from './DangerZone.tsx';
7
+ export { ExecutionProfileHeader } from './ExecutionProfileHeader.tsx';
8
+ export type { EditableProfileTitle, ExecutionProfileHeaderProps, ExecutionProfileTitle, ReadOnlyProfileTitle, } from './ExecutionProfileHeader.tsx';
9
+ export { JsonToggle, JsonToggleButton } from './JsonToggle.tsx';
10
+ export type { JsonToggleMode, JsonToggleProps } from './JsonToggle.tsx';
11
+ export { Modal } from './Modal.tsx';
12
+ export { TagEditor } from './TagEditor.tsx';
13
+ export type { TagEditorProps } from './TagEditor.tsx';
14
+ export { HistoryButton } from './HistoryButton.tsx';
15
+ export type { HistoryButtonProps } from './HistoryButton.tsx';
16
+ export { HistoryModal } from './HistoryModal.tsx';
17
+ export type { HistoryModalProps } from './HistoryModal.tsx';
18
+ export { HistoryView } from './HistoryView.tsx';
19
+ export { historyOpenRun } from './history-open-run.ts';
20
+ export { ImportSpecModal } from './ImportSpecModal.tsx';
21
+ export type { ImportSpecModalProps } from './ImportSpecModal.tsx';
22
+ export { VersionHistoryModal } from './VersionHistoryModal.tsx';
23
+ export { RightRail } from './RightRail.tsx';
24
+ export type { RailTool } from './RightRail.tsx';
25
+ export { WorkspaceProvider, useWorkspace, useWorkspaceCallable } from './workspace.tsx';
26
+ export type { WorkspaceCallable, WorkspaceKind } from './workspace.tsx';
27
+ export { RailEnvProvider, useRailEnv } from '../rail/rail-env.tsx';
28
+ export type { RailEnv, RailNavigation } from '../rail/rail-env.tsx';
29
+ export { type SettingsOpenState, WorkflowSettingsPanel, settingsTool, } from '../rail/SettingsTool.tsx';
30
+ export { MetadataSection, SettingsDangerZone, SettingsHeading, SpecActionsTrigger, SpecChooserModal, VersionListModal, VersionSection, } from '../rail/settings-sections.tsx';
31
+ export type { SettingsKind } from '../rail/settings-sections.tsx';
32
+ export { codeTool } from '../rail/CodeTool.tsx';
33
+ export { ErrorHandlerSection, errorHandlingTool } from '../rail/ErrorHandlingTool.tsx';
34
+ export type { ErrorHandlerKind, ErrorHandlerSectionProps } from '../rail/ErrorHandlingTool.tsx';
35
+ export { ProjectExpressionScope, useProjectExpressionOptions } from '../rail/ExpressionScope.tsx';
36
+ export { editorRailTools } from '../rail/editor-tools.ts';
@@ -0,0 +1,67 @@
1
+ import { CallablePolicy, SnippetInput, TriggerPreview } from '@w6w/embed/lib';
2
+ import { ReactNode } from 'react';
3
+ /**
4
+ * T2.1.2: widened to a discriminated union, one arm per kind — every new
5
+ * field OPTIONAL, no new required member, so every existing producer keeps
6
+ * compiling and behaving identically (`{kind, id}` alone still satisfies any
7
+ * arm). The extra fields are what `CodeTool`'s snippet panel needs per kind
8
+ * (mirrors `lib/sdk-snippets.ts`'s `SnippetCallable`, field-for-field where
9
+ * the names match — see `lib/code-rail.ts`'s `toSnippetCallable` for the one
10
+ * field that doesn't, `label` -> `displayName`).
11
+ */
12
+ export type WorkspaceCallable = {
13
+ kind: "function";
14
+ id: string;
15
+ label?: string;
16
+ key?: string;
17
+ inputs?: readonly SnippetInput[];
18
+ alias?: string;
19
+ } | {
20
+ kind: "endpoint";
21
+ id: string;
22
+ label?: string;
23
+ key?: string;
24
+ invokeUrl?: string;
25
+ invokeUrlByKey?: string;
26
+ urlForm?: "id" | "key";
27
+ alias?: string;
28
+ inputs?: readonly SnippetInput[];
29
+ } | {
30
+ kind: "workflow";
31
+ id: string;
32
+ label?: string;
33
+ key?: string;
34
+ triggers?: TriggerPreview[];
35
+ /**
36
+ * The workflow's run-level error policy, and the writer that applies it
37
+ * (core Workflow RFC — *Amendment — 2026-08-21: the run-level error
38
+ * handler*). Published ONLY by the page that owns the definition buffer
39
+ * — the canvas editor — so the rail's Error handling tool edits the same
40
+ * text the editor is about to save instead of racing it over the wire.
41
+ * `ErrorHandlingTool` is the only reader; see `lib/settings-rail.ts`'s
42
+ * `isAvailable` for why the tool hides itself when they are absent.
43
+ *
44
+ * ⚠️ `onPolicyChange` MUST be referentially stable across renders (a
45
+ * `useCallback` reading through a ref). `useWorkspace` keys its publish
46
+ * effect on `JSON.stringify(resource)`, which drops functions — so the
47
+ * stored callback is whichever one was current when some OTHER field
48
+ * last changed, and a closure that captured stale editor state would
49
+ * write stale bytes.
50
+ */
51
+ policy?: CallablePolicy;
52
+ onPolicyChange?: (policy: CallablePolicy) => void;
53
+ } | {
54
+ kind: "connection";
55
+ id: string;
56
+ label?: string;
57
+ } | null;
58
+ /** Derived from {@link WorkspaceCallable}, never redeclared — the arm a
59
+ * `RailTool.isAvailable`/route wrapper narrows on. */
60
+ export type WorkspaceKind = Exclude<WorkspaceCallable, null>["kind"];
61
+ export declare function WorkspaceProvider({ children }: {
62
+ children: ReactNode;
63
+ }): import("react").JSX.Element;
64
+ /** Publisher. Compute during render, effect-write, clear on unmount. */
65
+ export declare function useWorkspace(resource: WorkspaceCallable): void;
66
+ /** Reader — used by the rail and by every tool `Panel`. Throws outside the provider. */
67
+ export declare function useWorkspaceCallable(): WorkspaceCallable;
@@ -0,0 +1,16 @@
1
+ import { createElement as n, useContext as r, createContext as i } from "react";
2
+ const t = i(null);
3
+ function l(e) {
4
+ return n(t.Provider, { value: e.client }, e.children);
5
+ }
6
+ function u() {
7
+ const e = r(t);
8
+ if (e === null)
9
+ throw new Error("useEmbedClient must be used inside an <EmbedClientProvider>.");
10
+ return e;
11
+ }
12
+ export {
13
+ l as E,
14
+ u
15
+ };
16
+ //# sourceMappingURL=client-context-CX61Bgw-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-context-CX61Bgw-.js","sources":["../src/draft/client-context.ts"],"sourcesContent":["/**\n * The embed-owned client context for the headless draft hook.\n *\n * The hook talks to the server through the SDK `W6WClient` (`workflows.get` / `workflows.update`),\n * never through `@w6w/ui`'s `W6WApi` — that interface has no save member and is not extended here.\n * `W6WEmbedProvider` supplies the client from `useW6WClient()`; Studio supplies its own from\n * `useApiClient()`. A plain `.ts` module (no JSX) so Node's type stripping can load it.\n */\nimport type { W6WClient } from \"@w6w/sdk\";\nimport { type ReactNode, createContext, createElement, useContext } from \"react\";\n\nconst EmbedClientContext = createContext<W6WClient | null>(null);\n\nexport function EmbedClientProvider(p: { client: W6WClient; children?: ReactNode }) {\n return createElement(EmbedClientContext.Provider, { value: p.client }, p.children);\n}\n\n/** The SDK client for the nearest {@link EmbedClientProvider}. Throws outside one. */\nexport function useEmbedClient(): W6WClient {\n const client = useContext(EmbedClientContext);\n if (client === null) {\n throw new Error(\"useEmbedClient must be used inside an <EmbedClientProvider>.\");\n }\n return client;\n}\n"],"names":["EmbedClientContext","createContext","EmbedClientProvider","p","createElement","useEmbedClient","client","useContext"],"mappings":";AAWA,MAAMA,IAAqBC,EAAgC,IAAI;AAExD,SAASC,EAAoBC,GAAgD;AAClF,SAAOC,EAAcJ,EAAmB,UAAU,EAAE,OAAOG,EAAE,OAAA,GAAUA,EAAE,QAAQ;AACnF;AAGO,SAASE,IAA4B;AAC1C,QAAMC,IAASC,EAAWP,CAAkB;AAC5C,MAAIM,MAAW;AACb,UAAM,IAAI,MAAM,8DAA8D;AAEhF,SAAOA;AACT;"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Flush a pending debounced write before the page actually goes away.
3
+ *
4
+ * ## Why this exists at all
5
+ *
6
+ * `autosave.ts`'s `createAutoSaver` coalesces edits behind an `AUTOSAVE_DEBOUNCE_MS`
7
+ * (1200ms) window timer (`autosave.ts:45`) — a `push()` inside that window never writes,
8
+ * it only re-arms the timer. Nothing in `autosave.ts` itself ever
9
+ * flushes on navigation, so a canvas pan (or any other edit) that lands less than the
10
+ * window's width before a refresh is lost: the tab is gone before the timer fires (I-3).
11
+ * A zoom gesture "works" only by accident — it takes longer than the window to finish.
12
+ *
13
+ * This module does not touch the debounce, the `AutoSaver` interface, or its save
14
+ * decision (`fire()`'s `enabled`/`canSave`/dedup checks in `autosave.ts:98-107` already
15
+ * apply to a flush exactly as they do to a natural fire) — it only decides WHEN to call
16
+ * the `flush()` the caller already owns.
17
+ *
18
+ * ## The rule
19
+ *
20
+ * Two signals, both wired here, neither replacing the other:
21
+ *
22
+ * - `visibilitychange` with `document.visibilityState === "hidden"` is the PRIMARY path.
23
+ * It fires on tab close, tab switch, and navigation alike, and — unlike `beforeunload`
24
+ * — a listener registered on it is not itself a signal to the browser that the page is
25
+ * unusual, so it carries no risk of suppressing the back/forward cache.
26
+ * - `beforeunload` is the BELT, wired unconditionally (it does not check
27
+ * `visibilityState`): some navigations do not reliably fire `visibilitychange` first in
28
+ * every browser, so this is a second chance at the same flush.
29
+ *
30
+ * **Known, accepted limit — do not claim this race is provably closed.** A `fetch`
31
+ * kicked off from either handler is not guaranteed to complete before the browser
32
+ * actually tears the page down (`beforeunload` in particular has no such guarantee in
33
+ * every browser). `visibilitychange`+`hidden` fires earlier in the common
34
+ * switch-tab/navigate-away cases, which is why it is the primary path and `beforeunload`
35
+ * only the belt — this module narrows the loss window, it does not eliminate it.
36
+ *
37
+ * ## Why the parameters are typed the way they are
38
+ *
39
+ * `WindowLike`/`DocumentLike` below name only the handful of members this module
40
+ * actually calls. That is what lets `node --test` — which cannot load `.tsx`, the same
41
+ * reason `autosave.ts` itself lives in a plain `.ts` module (`autosave.ts:37-41`) — pass
42
+ * a plain fake object in tests, while `tsc -b --noEmit` still proves the real
43
+ * `window`/`document` satisfy the same shape at the call site in `WorkflowsPage.tsx`.
44
+ */
45
+ /** The subset of `Window` this module needs: register/unregister `beforeunload`. */
46
+ export interface WindowLike {
47
+ addEventListener(type: "beforeunload", listener: () => void): void;
48
+ removeEventListener(type: "beforeunload", listener: () => void): void;
49
+ }
50
+ /**
51
+ * The subset of `Document` this module needs: register/unregister `visibilitychange`,
52
+ * and read the visibility state the moment the event fires.
53
+ */
54
+ export interface DocumentLike {
55
+ readonly visibilityState: string;
56
+ addEventListener(type: "visibilitychange", listener: () => void): void;
57
+ removeEventListener(type: "visibilitychange", listener: () => void): void;
58
+ }
59
+ /**
60
+ * Register both listeners; call `flush` when the page is going away. Returns a cleanup
61
+ * that removes both — callers wire this straight into a `useEffect`'s return.
62
+ */
63
+ export declare function registerFlushOnHide(windowTarget: WindowLike, documentTarget: DocumentLike, flush: () => void): () => void;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The debouncing, coalescing, payload-deduplicated saver behind auto-save.
3
+ *
4
+ * ## Why this exists at all
5
+ *
6
+ * Every edit on the canvas replaces the whole definition: a single keystroke in a
7
+ * step's param form travels `ParamsForm onChange → commit → StepEditModal onChange
8
+ * → updateStep → emitChange → onChange(flowToWorkflow(…))` and lands in
9
+ * `WorkflowsPage` as `setJson(JSON.stringify(next, null, 2))`. So a naive
10
+ * `useEffect(() => save(), [json])` is **one `POST /workflows` per character
11
+ * typed**. This module is the thing that stands between those two facts.
12
+ *
13
+ * ## The rule
14
+ *
15
+ * **Trailing debounce, single pending entry, decided at fire time.**
16
+ *
17
+ * - `push` never writes. It replaces the one pending entry and restarts a
18
+ * `delayMs` window. N pushes inside one window therefore produce exactly ONE
19
+ * `save`, carrying the LAST payload.
20
+ * - The decision to save is taken when the timer fires, not when the payload was
21
+ * pushed. That is what makes a transiently-invalid edit harmless: while the JSON
22
+ * modal round-trips text through `JsonEditor`, `canSave` flickers false
23
+ * mid-typing, and a push-time decision would discard the save the author is
24
+ * waiting for. An invalid push still re-arms the window; the next valid one wins.
25
+ * - A payload equal to the last saved one is dropped without a request. The
26
+ * comparison is on the serialised JSON **text**, the same structural-equality
27
+ * dirty signal the house already uses (`packages/ui/src/ActionTestForm.tsx`,
28
+ * *"Compared by structural JSON so a load→edit→revert cycle clears it"*), which
29
+ * is why an in-place mutation of some nested object can never fool it: nothing
30
+ * here holds a reference to a live object across ticks.
31
+ *
32
+ * Deliberately absent: retry/backoff. On a failed save the host stops and shows
33
+ * the existing error surface; a background retry against a server that just
34
+ * refused is worse than a visible failure (and, with a stale-write conflict on
35
+ * screen, it would retry into the dialog the author is reading).
36
+ *
37
+ * It lives here rather than in `@w6w/ui` because `@w6w/ui` owns no workflow write
38
+ * at all — the canvas emits `onChange`, the host decides when to persist — and in
39
+ * a plain `.ts` module rather than in `WorkflowsPage.tsx` so it is executable:
40
+ * that file is JSX, which Node 24's type-stripping cannot parse, so nothing in it
41
+ * can be loaded by `node --test`. Same shape as `workflow-save.ts`.
42
+ */
43
+ /** The debounce window. One quiet beat after the last keystroke, not per keystroke. */
44
+ export declare const AUTOSAVE_DEBOUNCE_MS = 1200;
45
+ export interface AutoSaverOpts {
46
+ /** May return a promise (awaited by `flush`); any other return value is ignored. */
47
+ save: (payload: string, ctx?: unknown) => unknown;
48
+ delayMs?: number;
49
+ /** Injectable for tests; defaults to the globals. */
50
+ setTimer?: (fn: () => void, ms: number) => number;
51
+ clearTimer?: (handle: number) => void;
52
+ }
53
+ export interface AutoSaver {
54
+ /** Consider a payload. (Re)starts the debounce window. */
55
+ push(payload: string, enabled: boolean, canSave: boolean, ctx?: unknown): void;
56
+ /**
57
+ * Fire the pending save now (the Save button). Resolves after the fired save settles
58
+ * (`Promise.resolve()` when nothing fired); rejects if the fired save rejects.
59
+ */
60
+ flush(): Promise<void>;
61
+ /** Drop any pending save without firing (unmount / workflow switch / stale conflict). */
62
+ cancel(): void;
63
+ /** This payload is now on the server. */
64
+ markSaved(payload: string): void;
65
+ /** Does `payload` diverge from the last-saved one? */
66
+ isDirty(payload: string): boolean;
67
+ }
68
+ export declare function createAutoSaver(opts: AutoSaverOpts): AutoSaver;
@@ -0,0 +1,8 @@
1
+ import { W6WClient } from '@w6w/sdk';
2
+ import { ReactNode } from 'react';
3
+ export declare function EmbedClientProvider(p: {
4
+ client: W6WClient;
5
+ children?: ReactNode;
6
+ }): import('react').FunctionComponentElement<import('react').ProviderProps<W6WClient | null>>;
7
+ /** The SDK client for the nearest {@link EmbedClientProvider}. Throws outside one. */
8
+ export declare function useEmbedClient(): W6WClient;
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from 'react';
2
+ export interface DraftRegistry {
3
+ /** Register a draft's flush. Returns the unregister function. */
4
+ register(flush: () => Promise<unknown>): () => void;
5
+ /** Await every registered flush, at most `timeoutMs` (default 5000). Never rejects. */
6
+ flushAll(timeoutMs?: number): Promise<void>;
7
+ }
8
+ export declare function createDraftRegistry(): DraftRegistry;
9
+ export declare function DraftRegistryProvider(p: {
10
+ registry: DraftRegistry;
11
+ children?: ReactNode;
12
+ }): import('react').FunctionComponentElement<import('react').ProviderProps<DraftRegistry | null>>;
13
+ /** The nearest registry, or `null` when there is no provider (the hook then skips registering). */
14
+ export declare function useDraftRegistry(): DraftRegistry | null;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@w6w/embed/draft` — the headless draft lifecycle (debounced autosave, flush-on-hide, the
3
+ * workflow save precondition). No UI and no `@w6w/ui`: it imports only `react`,
4
+ * `@tanstack/react-query` and type-only `@w6w/sdk`, so a host can use it without the editor.
5
+ */
6
+ export { AUTOSAVE_DEBOUNCE_MS, createAutoSaver } from './autosave.ts';
7
+ export type { AutoSaver, AutoSaverOpts } from './autosave.ts';
8
+ export { registerFlushOnHide } from './autosave-flush.ts';
9
+ export type { DocumentLike, WindowLike } from './autosave-flush.ts';
10
+ export { isStaleWrite, nextSaveToken } from './workflow-save.ts';
11
+ export { EmbedClientProvider, useEmbedClient } from './client-context.ts';
12
+ export { DraftRegistryProvider, createDraftRegistry, useDraftRegistry, } from './draft-registry.ts';
13
+ export type { DraftRegistry } from './draft-registry.ts';
14
+ export { useWorkflowDraft } from './use-workflow-draft.ts';
15
+ export type { FlushOutcome, UseWorkflowDraftOptions, WorkflowDraft, } from './use-workflow-draft.ts';
@@ -0,0 +1,67 @@
1
+ export type FlushOutcome = {
2
+ status: "clean";
3
+ } | {
4
+ status: "saved";
5
+ } | {
6
+ status: "blocked";
7
+ reason: string;
8
+ } | {
9
+ status: "failed";
10
+ error: unknown;
11
+ };
12
+ export interface UseWorkflowDraftOptions {
13
+ /** `null` ⇒ idle (the hook stays mounted above an early return). */
14
+ workflowId: string | null;
15
+ /** REQUIRED to save; empty/absent ⇒ the save is refused and no request is sent. */
16
+ project: string | null | undefined;
17
+ /** Default true; a debounced save follows the last edit. */
18
+ autoSave?: boolean;
19
+ /** Injected busy probe (Studio: expression modal open). Gates AUTOMATIC saves only. */
20
+ canAutoSave?: () => boolean;
21
+ /** Injected key-edit guard: a reason blocks saving. */
22
+ saveGuard?: (parsed: unknown) => string | null;
23
+ /** Injected (B/D-3): reported as `publishBlockedReason`, not enforced by save. */
24
+ publishBlockedReason?: (parsed: unknown) => string | null;
25
+ /**
26
+ * Called exactly once when a handover (client/project change) or an identity switch
27
+ * (`flushAll`) drops a dirty buffer it could not save. `blocked`: the save was refused before a
28
+ * request (invalid text, no project, a guard, busy); `failed`: the request was sent or the row
29
+ * was stale and the save did not succeed. No UI here: the host decides what to show.
30
+ */
31
+ onDiscard?: (info: {
32
+ workflowId: string;
33
+ text: string;
34
+ reason: "blocked" | "failed";
35
+ }) => void;
36
+ }
37
+ export interface WorkflowDraft {
38
+ json: string;
39
+ setJson(next: string): void;
40
+ parsed: unknown | null;
41
+ syntaxError: string | null;
42
+ loading: boolean;
43
+ loadError: unknown | null;
44
+ dirty: boolean;
45
+ saving: boolean;
46
+ canSave: boolean;
47
+ saveBlockedReason: string | null;
48
+ stale: boolean;
49
+ error: unknown | null;
50
+ publishBlockedReason: string | null;
51
+ /** Push + fire now (Ctrl/Cmd+S, the Save button). */
52
+ saveNow(): void;
53
+ flush(): Promise<FlushOutcome>;
54
+ /** Flush, then run `action` only when the buffer is clean or just saved. */
55
+ flushThen<T>(action: () => Promise<T>): Promise<{
56
+ ok: true;
57
+ value: T;
58
+ } | {
59
+ ok: false;
60
+ outcome: FlushOutcome;
61
+ }>;
62
+ /** `setJson(next)` + an immediate save (the saveMeta / onPolicyChange paths). */
63
+ commit(next: string): Promise<FlushOutcome>;
64
+ /** 409 recovery: GET, replace the buffer and token, clear `stale`. */
65
+ reload(): Promise<void>;
66
+ }
67
+ export declare function useWorkflowDraft(o: UseWorkflowDraftOptions): WorkflowDraft;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The two decisions behind the workflow save precondition (optimistic
3
+ * concurrency; server half in `packages/server` `packages/api/workflows.ts`).
4
+ *
5
+ * `POST /workflows` accepts an optional `x-w6w-if-unmodified-since` header
6
+ * carrying the exact ISO-8601 `updatedAt` the client last saw. If the stored row
7
+ * has moved on, the save is refused with **409 `workflow_stale`** instead of
8
+ * silently overwriting whoever saved in between.
9
+ *
10
+ * Both functions here are pure functions of their inputs, and they live in a
11
+ * `.ts` module rather than in `WorkflowsPage.tsx` so they are executable:
12
+ * `WorkflowsPage.tsx` is JSX, which Node 24's type-stripping cannot parse, so
13
+ * nothing in it can be loaded by `node --test`. Same shape as
14
+ * `src/lib/sdk-snippets.ts` + its `.test.ts`.
15
+ */
16
+ /**
17
+ * Is this the one save failure an author can fix by reloading?
18
+ *
19
+ * Keyed on the error **code**, never on the HTTP status. The server answers 409
20
+ * for two completely different situations and only one of them is recoverable:
21
+ *
22
+ * - `workflow_stale` — your copy is out of date. Reload and re-save. ← this one
23
+ * - `workflow_conflict` — another `(tenant, subject)` owns that workflow id.
24
+ * Reloading changes nothing; offering a reload here would be a dead end.
25
+ *
26
+ * Anything that carries no `code` (a thrown `TypeError`, a string, `null`) is
27
+ * not a server verdict at all, so it is not a stale write either.
28
+ *
29
+ * Duck-typed on the `code` property rather than `instanceof ApiError`: this module is
30
+ * headless and must not value-import the SDK, and two SDK copies would defeat `instanceof`.
31
+ */
32
+ export declare function isStaleWrite(e: unknown): boolean;
33
+ /**
34
+ * The precondition token to send with the NEXT save, given the token we sent
35
+ * with this one and the response that came back.
36
+ *
37
+ * The server rides `updatedAt` on both the `GET /workflows/:id` body and the
38
+ * `POST /workflows` 201, precisely so a client can advance without a re-GET.
39
+ *
40
+ * When the response carries no usable `updatedAt`, the answer is **`null`** (no
41
+ * precondition on the next save), never `prev`: `prev` is by then a value the
42
+ * server may already have moved past, so resending it would 409 forever with no
43
+ * way out. Dropping the guard for one save is the weaker failure — it is exactly
44
+ * the pre-existing last-write-wins behaviour, which the header is optional for.
45
+ */
46
+ export declare function nextSaveToken(prev: string | null, res: {
47
+ updatedAt?: unknown;
48
+ }): string | null;
@@ -0,0 +1,49 @@
1
+ import { createElement as c, useContext as u, createContext as l } from "react";
2
+ const m = 5e3;
3
+ function y() {
4
+ const e = /* @__PURE__ */ new Set();
5
+ return {
6
+ register(r) {
7
+ const t = () => r();
8
+ return e.add(t), () => {
9
+ e.delete(t);
10
+ };
11
+ },
12
+ async flushAll(r = m) {
13
+ const t = [...e].map(
14
+ (n) => new Promise((s) => {
15
+ try {
16
+ Promise.resolve(n()).then(
17
+ () => s(),
18
+ () => s()
19
+ );
20
+ } catch {
21
+ s();
22
+ }
23
+ })
24
+ );
25
+ let i;
26
+ const a = new Promise((n) => {
27
+ i = setTimeout(n, r);
28
+ });
29
+ try {
30
+ await Promise.race([Promise.all(t), a]);
31
+ } finally {
32
+ clearTimeout(i);
33
+ }
34
+ }
35
+ };
36
+ }
37
+ const o = l(null);
38
+ function d(e) {
39
+ return c(o.Provider, { value: e.registry }, e.children);
40
+ }
41
+ function P() {
42
+ return u(o);
43
+ }
44
+ export {
45
+ d as D,
46
+ y as c,
47
+ P as u
48
+ };
49
+ //# sourceMappingURL=draft-registry-YF0tqNN0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-registry-YF0tqNN0.js","sources":["../src/draft/draft-registry.ts"],"sourcesContent":["/**\n * The draft registry: every mounted `useWorkflowDraft` registers its `flush` here so a host can\n * settle all of them before an identity swap (logout, account switch) — see `flushAll`.\n *\n * `flushAll` NEVER rejects and is bounded: a flush that throws synchronously, rejects, or never\n * settles must not block or fail the swap. Each call is wrapped individually so one bad draft\n * cannot stop the others being awaited (`createAutoSaver().flush()` rejects when its save rejects\n * and lets a synchronous throw from `save` escape synchronously — both are absorbed here).\n */\nimport { type ReactNode, createContext, createElement, useContext } from \"react\";\n\nexport interface DraftRegistry {\n /** Register a draft's flush. Returns the unregister function. */\n register(flush: () => Promise<unknown>): () => void;\n /** Await every registered flush, at most `timeoutMs` (default 5000). Never rejects. */\n flushAll(timeoutMs?: number): Promise<void>;\n}\n\nconst DEFAULT_FLUSH_ALL_TIMEOUT_MS = 5000;\n\nexport function createDraftRegistry(): DraftRegistry {\n const flushes = new Set<() => Promise<unknown>>();\n\n return {\n register(flush) {\n // Wrapped so registering the same function twice yields two independent entries.\n const entry = () => flush();\n flushes.add(entry);\n return () => {\n flushes.delete(entry);\n };\n },\n async flushAll(timeoutMs = DEFAULT_FLUSH_ALL_TIMEOUT_MS) {\n const settled = [...flushes].map(\n (flush) =>\n new Promise<void>((resolve) => {\n try {\n Promise.resolve(flush()).then(\n () => resolve(),\n () => resolve(),\n );\n } catch {\n resolve();\n }\n }),\n );\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, timeoutMs);\n });\n try {\n await Promise.race([Promise.all(settled), timeout]);\n } finally {\n clearTimeout(timer);\n }\n },\n };\n}\n\nconst DraftRegistryContext = createContext<DraftRegistry | null>(null);\n\nexport function DraftRegistryProvider(p: { registry: DraftRegistry; children?: ReactNode }) {\n return createElement(DraftRegistryContext.Provider, { value: p.registry }, p.children);\n}\n\n/** The nearest registry, or `null` when there is no provider (the hook then skips registering). */\nexport function useDraftRegistry(): DraftRegistry | null {\n return useContext(DraftRegistryContext);\n}\n"],"names":["DEFAULT_FLUSH_ALL_TIMEOUT_MS","createDraftRegistry","flushes","flush","entry","timeoutMs","settled","resolve","timer","timeout","DraftRegistryContext","createContext","DraftRegistryProvider","p","createElement","useDraftRegistry","useContext"],"mappings":";AAkBA,MAAMA,IAA+B;AAE9B,SAASC,IAAqC;AACnD,QAAMC,wBAAc,IAAA;AAEpB,SAAO;AAAA,IACL,SAASC,GAAO;AAEd,YAAMC,IAAQ,MAAMD,EAAA;AACpB,aAAAD,EAAQ,IAAIE,CAAK,GACV,MAAM;AACX,QAAAF,EAAQ,OAAOE,CAAK;AAAA,MACtB;AAAA,IACF;AAAA,IACA,MAAM,SAASC,IAAYL,GAA8B;AACvD,YAAMM,IAAU,CAAC,GAAGJ,CAAO,EAAE;AAAA,QAC3B,CAACC,MACC,IAAI,QAAc,CAACI,MAAY;AAC7B,cAAI;AACF,oBAAQ,QAAQJ,EAAA,CAAO,EAAE;AAAA,cACvB,MAAMI,EAAA;AAAA,cACN,MAAMA,EAAA;AAAA,YAAQ;AAAA,UAElB,QAAQ;AACN,YAAAA,EAAA;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MAAA;AAEL,UAAIC;AACJ,YAAMC,IAAU,IAAI,QAAc,CAACF,MAAY;AAC7C,QAAAC,IAAQ,WAAWD,GAASF,CAAS;AAAA,MACvC,CAAC;AACD,UAAI;AACF,cAAM,QAAQ,KAAK,CAAC,QAAQ,IAAIC,CAAO,GAAGG,CAAO,CAAC;AAAA,MACpD,UAAA;AACE,qBAAaD,CAAK;AAAA,MACpB;AAAA,IACF;AAAA,EAAA;AAEJ;AAEA,MAAME,IAAuBC,EAAoC,IAAI;AAE9D,SAASC,EAAsBC,GAAsD;AAC1F,SAAOC,EAAcJ,EAAqB,UAAU,EAAE,OAAOG,EAAE,SAAA,GAAYA,EAAE,QAAQ;AACvF;AAGO,SAASE,IAAyC;AACvD,SAAOC,EAAWN,CAAoB;AACxC;"}