@miadi/episode-ui 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Miadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @miadi/episode-ui
2
+
3
+ The episode-vessel experience, as React.
4
+
5
+ Browse an episode's files, read one, edit one with a conflict that never loses
6
+ the draft, keep a ceremony's working notes, and name the artefacts a circle was
7
+ held about.
8
+
9
+ ```bash
10
+ npm i @miadi/episode-ui
11
+ ```
12
+
13
+ ```jsx
14
+ import { EpisodeFileBrowser, EpisodeTextEditor } from "@miadi/episode-ui"
15
+ import "@miadi/episode-ui/styles.css"
16
+
17
+ <EpisodeFileBrowser
18
+ files={files} // from @miadi/episode-vessel, however you fetched it
19
+ capabilities={capabilities} // from @miadi/episode-vessel/permissions
20
+ selected={open?.relativePath}
21
+ onSelect={(file) => open(file)}
22
+ />
23
+ ```
24
+
25
+ ## The contract
26
+
27
+ **Data and callbacks in, intent out.** No component fetches, holds a URL or a
28
+ token, knows a chronicle root, or decides what a person is allowed to do. The
29
+ host resolves capabilities and remains the authority; a capability here decides
30
+ only what is *offered*. `test/contract.test.mjs` asserts it — the sources
31
+ contain no `fetch(`, no URL, no `node:fs`, no `process.env`.
32
+
33
+ This is the pattern `@miadi/attention-ui` and `@miadi/capture-ui` already use.
34
+
35
+ ## Components
36
+
37
+ | component | what it is for |
38
+ |---|---|
39
+ | `EpisodeFileBrowser` | the vessel's files, filterable, with an optional attach control when a ceremony is in context |
40
+ | `EpisodeTextViewer` | read one artefact; pass `renderMarkdown` to render, omit it and the source is shown |
41
+ | `EpisodeTextEditor` | edit one; carries `expectedRevision` and handles the 409 |
42
+ | `CeremonyNoteEditor` | a ceremony's working notes |
43
+ | `CeremonyAttachments` | what a circle was held about |
44
+ | `EpisodeShelfFilter` | find an episode by number, title or slug |
45
+
46
+ ## The conflict path is the point
47
+
48
+ When someone else writes between load and save, the vessel refuses and hands
49
+ back the revision on disk. `EpisodeTextEditor` then keeps the draft in the
50
+ textarea and says so, shows what is on disk now rather than a status code, and
51
+ offers reload as a choice the person makes. **A save is never retried
52
+ automatically** — overwriting someone's work silently is the failure the guard
53
+ exists to prevent, and a retry is exactly that.
54
+
55
+ ## Markdown
56
+
57
+ The package ships no markdown engine. Miadi renders with `react-markdown`,
58
+ gmtermux with `marked` + `sanitize-html`, and a second engine here would be a
59
+ second sanitisation boundary to keep correct. Pass `renderMarkdown` or get the
60
+ source, which is always safe.
61
+
62
+ ## Styling
63
+
64
+ One stylesheet of plain class names over CSS custom properties, set from a
65
+ `EpisodeUITheme` through `themeVars(theme)`. Override the properties, override
66
+ the classes, or write your own stylesheet against the same names. Touch targets
67
+ are 40px because the first surface this runs on is a phone.
68
+
69
+ Ref jgwill/Miadi#651.
@@ -0,0 +1,48 @@
1
+ import type { CSSProperties } from "react";
2
+ import { type CeremonyAttachmentView, type EpisodeCapabilitiesView, type EpisodeUITheme } from "./types.js";
3
+ export interface CeremonyNoteEditorProps {
4
+ ceremonyId: string;
5
+ /** Where the note lives in the vessel, shown so a person can find it later. */
6
+ relativePath: string;
7
+ content: string;
8
+ exists: boolean;
9
+ capabilities: EpisodeCapabilitiesView;
10
+ onSave: (content: string) => Promise<{
11
+ ok: boolean;
12
+ message?: string;
13
+ }>;
14
+ label?: string;
15
+ placeholder?: string;
16
+ theme?: EpisodeUITheme;
17
+ className?: string;
18
+ style?: CSSProperties;
19
+ }
20
+ /**
21
+ * The working notes of one ceremony, kept in the episode vessel.
22
+ *
23
+ * The medicine wheel owns the ceremony record; this is what the circle wrote
24
+ * down, and it stays in the folder so it travels with the episode and survives
25
+ * a wheel that is offline. A person without `writeCeremonyNote` reads it.
26
+ */
27
+ export declare function CeremonyNoteEditor({ ceremonyId, relativePath, content, exists, capabilities, onSave, label, placeholder, theme, className, style, }: CeremonyNoteEditorProps): import("react").JSX.Element;
28
+ export interface CeremonyAttachmentsProps {
29
+ attachments: CeremonyAttachmentView[];
30
+ capabilities: EpisodeCapabilitiesView;
31
+ onOpen?: (relativePath: string) => void;
32
+ onDetach?: (relativePath: string) => void;
33
+ emptyLabel?: string;
34
+ title?: string;
35
+ theme?: EpisodeUITheme;
36
+ className?: string;
37
+ style?: CSSProperties;
38
+ }
39
+ /**
40
+ * What a circle was held about.
41
+ *
42
+ * A ceremony with no attachment shows nothing rather than an empty frame — an
43
+ * absence stated loudly is still noise. When there are attachments, each one
44
+ * says who put it on the table and when, because that is the part a reader
45
+ * cannot reconstruct afterwards.
46
+ */
47
+ export declare function CeremonyAttachments({ attachments, capabilities, onOpen, onDetach, emptyLabel, title, theme, className, style, }: CeremonyAttachmentsProps): import("react").JSX.Element | null;
48
+ //# sourceMappingURL=ceremony.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ceremony.d.ts","sourceRoot":"","sources":["../src/ceremony.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACpB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,YAAY,EAAE,uBAAuB,CAAA;IACrC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,UAAU,EACV,YAAY,EACZ,OAAO,EACP,MAAM,EACN,YAAY,EACZ,MAAM,EACN,KAAuB,EACvB,WAAsD,EACtD,KAAqB,EACrB,SAAS,EACT,KAAK,GACN,EAAE,uBAAuB,+BA+DzB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,sBAAsB,EAAE,CAAA;IACrC,YAAY,EAAE,uBAAuB,CAAA;IACrC,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,UAAU,EACV,KAAsB,EACtB,KAAqB,EACrB,SAAS,EACT,KAAK,GACN,EAAE,wBAAwB,sCA2C1B"}
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { DEFAULT_THEME, themeVars, } from "./types.js";
5
+ /**
6
+ * The working notes of one ceremony, kept in the episode vessel.
7
+ *
8
+ * The medicine wheel owns the ceremony record; this is what the circle wrote
9
+ * down, and it stays in the folder so it travels with the episode and survives
10
+ * a wheel that is offline. A person without `writeCeremonyNote` reads it.
11
+ */
12
+ export function CeremonyNoteEditor({ ceremonyId, relativePath, content, exists, capabilities, onSave, label = "Working notes", placeholder = "What should remain with this ceremony?", theme = DEFAULT_THEME, className, style, }) {
13
+ const [draft, setDraft] = useState(content);
14
+ const [state, setState] = useState("idle");
15
+ const [message, setMessage] = useState("");
16
+ useEffect(() => {
17
+ setDraft(content);
18
+ setState("idle");
19
+ }, [ceremonyId, content]);
20
+ const mayWrite = capabilities.writeCeremonyNote;
21
+ const save = async () => {
22
+ setState("saving");
23
+ try {
24
+ const outcome = await onSave(draft);
25
+ setState(outcome.ok ? "saved" : "error");
26
+ setMessage(outcome.message ?? (outcome.ok ? `Saved to ${relativePath}` : "It could not be saved."));
27
+ }
28
+ catch (error) {
29
+ setState("error");
30
+ setMessage(error instanceof Error ? error.message : "It could not be saved.");
31
+ }
32
+ };
33
+ return (_jsxs("div", { className: ["ceremony-note-editor", className].filter(Boolean).join(" "), style: { ...themeVars(theme), ...style }, children: [_jsxs("div", { className: "ceremony-note-head", children: [_jsx("label", { className: "ceremony-note-label", htmlFor: `ceremony-note-${ceremonyId}`, children: label }), _jsx("span", { className: `ceremony-note-state is-${exists ? "saved" : "pending"}`, title: relativePath, children: exists ? "notes.md" : "not saved yet" })] }), _jsx("textarea", { id: `ceremony-note-${ceremonyId}`, className: "ceremony-note-field", value: draft, maxLength: 65536, readOnly: !mayWrite, placeholder: mayWrite ? placeholder : undefined, onChange: (event) => {
34
+ setDraft(event.target.value);
35
+ if (state !== "idle")
36
+ setState("idle");
37
+ } }), mayWrite && (_jsxs("div", { className: "ceremony-note-actions", children: [_jsx("button", { type: "button", disabled: state === "saving" || draft === content, onClick: save, children: "Save notes" }), message && (_jsx("span", { className: `ceremony-note-message is-${state}`, role: "status", "aria-live": "polite", children: message }))] }))] }));
38
+ }
39
+ /**
40
+ * What a circle was held about.
41
+ *
42
+ * A ceremony with no attachment shows nothing rather than an empty frame — an
43
+ * absence stated loudly is still noise. When there are attachments, each one
44
+ * says who put it on the table and when, because that is the part a reader
45
+ * cannot reconstruct afterwards.
46
+ */
47
+ export function CeremonyAttachments({ attachments, capabilities, onOpen, onDetach, emptyLabel, title = "On the table", theme = DEFAULT_THEME, className, style, }) {
48
+ if (attachments.length === 0 && !emptyLabel)
49
+ return null;
50
+ return (_jsxs("div", { className: ["ceremony-attachments", className].filter(Boolean).join(" "), style: { ...themeVars(theme), ...style }, children: [_jsx("span", { className: "ceremony-attachments-title", children: title }), attachments.length === 0 ? (_jsx("p", { className: "ceremony-attachments-empty", children: emptyLabel })) : (_jsx("ul", { className: "ceremony-attachments-list", children: attachments.map((item) => (_jsxs("li", { className: "ceremony-attachment", children: [_jsx("button", { type: "button", className: "ceremony-attachment-open", disabled: !capabilities.read || !onOpen, onClick: () => onOpen?.(item.relativePath), children: item.relativePath }), _jsxs("span", { className: "ceremony-attachment-meta", children: [item.attachedBy, " \u00B7 ", item.attachedAt.slice(0, 16).replace("T", " ")] }), item.note && _jsx("span", { className: "ceremony-attachment-note", children: item.note }), onDetach && capabilities.attachToCeremony && (_jsx("button", { type: "button", className: "ceremony-attachment-detach", title: "Take this off the table", onClick: () => onDetach(item.relativePath), children: "remove" }))] }, item.relativePath))) }))] }));
51
+ }
52
+ //# sourceMappingURL=ceremony.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ceremony.js","sourceRoot":"","sources":["../src/ceremony.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { CSSProperties } from "react";
2
+ import { type EpisodeCapabilitiesView, type EpisodeFileView, type EpisodeUITheme } from "./types.js";
3
+ export interface EpisodeFileBrowserProps {
4
+ files: EpisodeFileView[];
5
+ capabilities: EpisodeCapabilitiesView;
6
+ /** The file currently open, by `relativePath`. */
7
+ selected?: string | null;
8
+ onSelect?: (file: EpisodeFileView) => void;
9
+ /** Offered per file when the caller can act on a ceremony. */
10
+ onAttach?: (file: EpisodeFileView) => void;
11
+ /** Already attached to the ceremony in context, by `relativePath`. */
12
+ attached?: string[];
13
+ theme?: EpisodeUITheme;
14
+ title?: string;
15
+ /** Shown when the vessel holds no listable file. */
16
+ emptyLabel?: string;
17
+ className?: string;
18
+ style?: CSSProperties;
19
+ }
20
+ /**
21
+ * The files in one vessel, as a list a person can act on.
22
+ *
23
+ * It fetches nothing. The host lists the files and says what this person may
24
+ * do; the browser decides only what to show and raises intent. A capability
25
+ * that is absent removes its control rather than disabling it — a button a
26
+ * person can never press is noise, not information.
27
+ */
28
+ export declare function EpisodeFileBrowser({ files, capabilities, selected, onSelect, onAttach, attached, theme, title, emptyLabel, className, style, }: EpisodeFileBrowserProps): import("react").JSX.Element | null;
29
+ //# sourceMappingURL=file-browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-browser.d.ts","sourceRoot":"","sources":["../src/file-browser.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,EAKL,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,YAAY,EAAE,uBAAuB,CAAA;IACrC,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,YAAY,EACZ,QAAe,EACf,QAAQ,EACR,QAAQ,EACR,QAAa,EACb,KAAqB,EACrB,KAA+B,EAC/B,UAA8D,EAC9D,SAAS,EACT,KAAK,GACN,EAAE,uBAAuB,sCAqGzB"}
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { DEFAULT_THEME, formatBytes, iconFor, themeVars, } from "./types.js";
5
+ /**
6
+ * The files in one vessel, as a list a person can act on.
7
+ *
8
+ * It fetches nothing. The host lists the files and says what this person may
9
+ * do; the browser decides only what to show and raises intent. A capability
10
+ * that is absent removes its control rather than disabling it — a button a
11
+ * person can never press is noise, not information.
12
+ */
13
+ export function EpisodeFileBrowser({ files, capabilities, selected = null, onSelect, onAttach, attached = [], theme = DEFAULT_THEME, title = "Files in this episode", emptyLabel = "This episode folder holds no readable file yet.", className, style, }) {
14
+ const [query, setQuery] = useState("");
15
+ const shown = useMemo(() => {
16
+ const needle = query.trim().toLowerCase();
17
+ if (!needle)
18
+ return files;
19
+ return files.filter((file) => file.relativePath.toLowerCase().includes(needle));
20
+ }, [files, query]);
21
+ if (!capabilities.browse)
22
+ return null;
23
+ const attachedSet = new Set(attached);
24
+ return (_jsxs("section", { className: ["episode-file-browser", className].filter(Boolean).join(" "), style: { ...themeVars(theme), ...style }, "aria-label": title, children: [_jsxs("header", { className: "episode-file-browser-head", children: [_jsx("h3", { className: "episode-file-browser-title", children: title }), _jsxs("span", { className: "episode-file-browser-count", children: [files.length, " ", files.length === 1 ? "file" : "files"] })] }), files.length > 6 && (_jsx("input", { className: "episode-file-browser-filter", type: "search", value: query, placeholder: "Filter by name", "aria-label": "Filter files", onChange: (event) => setQuery(event.target.value) })), files.length === 0 ? (_jsx("p", { className: "episode-file-browser-empty", children: emptyLabel })) : (_jsx("ul", { className: "episode-file-browser-list", children: shown.map((file) => {
25
+ const isSelected = file.relativePath === selected;
26
+ const canOpen = capabilities.read && (file.previewable || file.kind !== "text");
27
+ return (_jsxs("li", { className: [
28
+ "episode-file-row",
29
+ isSelected ? "is-selected" : "",
30
+ file.guidance ? "is-guidance" : "",
31
+ ]
32
+ .filter(Boolean)
33
+ .join(" "), children: [_jsxs("button", { type: "button", className: "episode-file-open", disabled: !canOpen, "aria-current": isSelected ? "true" : undefined, onClick: () => onSelect?.(file), children: [_jsx("span", { className: "episode-file-icon", "aria-hidden": "true", children: iconFor(file) }), _jsx("span", { className: "episode-file-name", children: file.relativePath }), _jsxs("span", { className: "episode-file-meta", children: [formatBytes(file.size), file.editable && capabilities.edit ? " · editable" : "", file.kind === "document" ? " · download" : "", file.guidance ? " · guidance" : ""] })] }), onAttach && capabilities.attachToCeremony && (_jsx("button", { type: "button", className: "episode-file-attach", "aria-pressed": attachedSet.has(file.relativePath), title: attachedSet.has(file.relativePath)
34
+ ? "On this ceremony's table"
35
+ : "Name this file as being on the ceremony's table", onClick: () => onAttach(file), children: attachedSet.has(file.relativePath) ? "on the table" : "attach" }))] }, file.relativePath));
36
+ }) })), !capabilities.read && (_jsx("p", { className: "episode-file-browser-notice", children: "Sign in to read these files. What is here is listed for everyone; what it says is not." }))] }));
37
+ }
38
+ //# sourceMappingURL=file-browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-browser.js","sourceRoot":"","sources":["../src/file-browser.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @miadi/episode-ui — the episode-vessel experience, as React.
3
+ *
4
+ * gmtermux proved this experience on a phone in vanilla JS
5
+ * (`web/lib/file-browser.js`, `web/lib/fullscreen-editor.js`,
6
+ * `web/lib/episode-shelf-filter.js`) where no React surface could reach it. The
7
+ * Miadi episode room meanwhile rendered its files as text it could not open.
8
+ * These components are that experience in a form both can hold.
9
+ *
10
+ * The contract, in one line: **data and callbacks in, intent out.** No
11
+ * component fetches, holds a URL or token, knows a chronicle root, or decides
12
+ * what a person is allowed to do. The host resolves capabilities (see
13
+ * `@miadi/episode-vessel/permissions`) and remains the authority; a capability
14
+ * here decides only what is offered.
15
+ *
16
+ * Styles are one stylesheet of plain classes over CSS custom properties:
17
+ * `import "@miadi/episode-ui/styles.css"`, or write your own against the same
18
+ * class names.
19
+ */
20
+ export { EpisodeFileBrowser, type EpisodeFileBrowserProps } from "./file-browser.js";
21
+ export { EpisodeTextEditor, EpisodeTextViewer, type EpisodeTextEditorProps, type EpisodeTextViewerProps, type SaveOutcome, } from "./text-editor.js";
22
+ export { CeremonyNoteEditor, CeremonyAttachments, type CeremonyNoteEditorProps, type CeremonyAttachmentsProps, } from "./ceremony.js";
23
+ export { EpisodeShelfFilter, type EpisodeShelfFilterProps } from "./shelf-filter.js";
24
+ export { DEFAULT_THEME, themeVars, formatBytes, iconFor, type FileKind, type EpisodeFileView, type FileRevisionView, type EpisodeTextView, type EpisodeCapabilitiesView, type CeremonyAttachmentView, type EpisodeShelfEntry, type EpisodeUITheme, } from "./types.js";
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AACpF,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AACpF,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,EACX,OAAO,EACP,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @miadi/episode-ui — the episode-vessel experience, as React.
3
+ *
4
+ * gmtermux proved this experience on a phone in vanilla JS
5
+ * (`web/lib/file-browser.js`, `web/lib/fullscreen-editor.js`,
6
+ * `web/lib/episode-shelf-filter.js`) where no React surface could reach it. The
7
+ * Miadi episode room meanwhile rendered its files as text it could not open.
8
+ * These components are that experience in a form both can hold.
9
+ *
10
+ * The contract, in one line: **data and callbacks in, intent out.** No
11
+ * component fetches, holds a URL or token, knows a chronicle root, or decides
12
+ * what a person is allowed to do. The host resolves capabilities (see
13
+ * `@miadi/episode-vessel/permissions`) and remains the authority; a capability
14
+ * here decides only what is offered.
15
+ *
16
+ * Styles are one stylesheet of plain classes over CSS custom properties:
17
+ * `import "@miadi/episode-ui/styles.css"`, or write your own against the same
18
+ * class names.
19
+ */
20
+ export { EpisodeFileBrowser } from "./file-browser.js";
21
+ export { EpisodeTextEditor, EpisodeTextViewer, } from "./text-editor.js";
22
+ export { CeremonyNoteEditor, CeremonyAttachments, } from "./ceremony.js";
23
+ export { EpisodeShelfFilter } from "./shelf-filter.js";
24
+ export { DEFAULT_THEME, themeVars, formatBytes, iconFor, } from "./types.js";
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,kBAAkB,EAAgC,MAAM,mBAAmB,CAAA;AACpF,OAAO,EACL,iBAAiB,EACjB,iBAAiB,GAIlB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GAGpB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,kBAAkB,EAAgC,MAAM,mBAAmB,CAAA;AACpF,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,EACX,OAAO,GASR,MAAM,YAAY,CAAA"}
@@ -0,0 +1,22 @@
1
+ import type { CSSProperties } from "react";
2
+ import { type EpisodeShelfEntry, type EpisodeUITheme } from "./types.js";
3
+ export interface EpisodeShelfFilterProps {
4
+ episodes: EpisodeShelfEntry[];
5
+ selected?: string | null;
6
+ onSelect?: (episode: EpisodeShelfEntry) => void;
7
+ /** Render each entry yourself when the host has its own card. */
8
+ renderEntry?: (episode: EpisodeShelfEntry, selected: boolean) => React.ReactNode;
9
+ placeholder?: string;
10
+ theme?: EpisodeUITheme;
11
+ className?: string;
12
+ style?: CSSProperties;
13
+ }
14
+ /**
15
+ * Find an episode by number, title or slug.
16
+ *
17
+ * `349`, `ep349`, `episode 349` and a word from the title all reach the same
18
+ * vessel — on a phone, typing the number is the fastest way there, and it was
19
+ * the first thing gmtermux's shelf learned.
20
+ */
21
+ export declare function EpisodeShelfFilter({ episodes, selected, onSelect, renderEntry, placeholder, theme, className, style, }: EpisodeShelfFilterProps): import("react").JSX.Element;
22
+ //# sourceMappingURL=shelf-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shelf-filter.d.ts","sourceRoot":"","sources":["../src/shelf-filter.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAC1C,OAAO,EAA4B,KAAK,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAElG,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,iBAAiB,EAAE,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC/C,iEAAiE;IACjE,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,QAAe,EACf,QAAQ,EACR,WAAW,EACX,WAAkD,EAClD,KAAqB,EACrB,SAAS,EACT,KAAK,GACN,EAAE,uBAAuB,+BA+DzB"}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { DEFAULT_THEME, themeVars } from "./types.js";
5
+ /**
6
+ * Find an episode by number, title or slug.
7
+ *
8
+ * `349`, `ep349`, `episode 349` and a word from the title all reach the same
9
+ * vessel — on a phone, typing the number is the fastest way there, and it was
10
+ * the first thing gmtermux's shelf learned.
11
+ */
12
+ export function EpisodeShelfFilter({ episodes, selected = null, onSelect, renderEntry, placeholder = "Find an episode by number or title", theme = DEFAULT_THEME, className, style, }) {
13
+ const [query, setQuery] = useState("");
14
+ const shown = useMemo(() => {
15
+ const needle = query.trim().toLowerCase().replace(/^(?:ep(?:isode)?\s*)/, "");
16
+ if (!needle)
17
+ return episodes;
18
+ return episodes.filter((episode) => [
19
+ String(episode.number),
20
+ episode.numberLabel,
21
+ episode.title,
22
+ episode.episodePath,
23
+ episode.goal ?? "",
24
+ ]
25
+ .join(" ")
26
+ .toLowerCase()
27
+ .includes(needle));
28
+ }, [episodes, query]);
29
+ return (_jsxs("div", { className: ["episode-shelf", className].filter(Boolean).join(" "), style: { ...themeVars(theme), ...style }, children: [_jsx("input", { className: "episode-shelf-filter", type: "search", value: query, placeholder: placeholder, "aria-label": placeholder, onChange: (event) => setQuery(event.target.value) }), _jsxs("span", { className: "episode-shelf-count", children: [shown.length, " of ", episodes.length] }), _jsx("ul", { className: "episode-shelf-list", children: shown.map((episode) => {
30
+ const isSelected = episode.episodePath === selected;
31
+ return (_jsx("li", { className: isSelected ? "is-selected" : undefined, children: renderEntry ? (renderEntry(episode, isSelected)) : (_jsxs("button", { type: "button", className: "episode-shelf-entry", onClick: () => onSelect?.(episode), children: [_jsx("span", { className: "episode-shelf-number", children: episode.numberLabel }), _jsxs("span", { className: "episode-shelf-body", children: [_jsx("strong", { children: episode.title }), episode.goal && _jsx("span", { children: episode.goal }), _jsxs("small", { children: [episode.date, episode.status ? ` · ${episode.status}` : "", episode.registered === false ? " · vessel only" : ""] })] })] })) }, episode.episodePath));
32
+ }) })] }));
33
+ }
34
+ //# sourceMappingURL=shelf-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shelf-filter.js","sourceRoot":"","sources":["../src/shelf-filter.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { type EpisodeTextView, type EpisodeUITheme, type FileRevisionView } from "./types.js";
3
+ export interface SaveOutcome {
4
+ ok: boolean;
5
+ /** Present when the file moved under the writer. */
6
+ conflict?: {
7
+ currentRevision: FileRevisionView;
8
+ currentContent?: string;
9
+ };
10
+ message?: string;
11
+ }
12
+ export interface EpisodeTextEditorProps {
13
+ /** Vessel-relative path, shown as the editor's identity. */
14
+ file: string;
15
+ /** What the host read. `revision.sha256` is the guard carried into the save. */
16
+ loaded: EpisodeTextView;
17
+ /**
18
+ * Save. Resolve `{ ok: true }` when it landed, or `{ ok: false, conflict }`
19
+ * when the vessel refused with 409. Throwing is also handled — the draft is
20
+ * kept either way.
21
+ */
22
+ onSave: (content: string, expectedRevision: string) => Promise<SaveOutcome>;
23
+ /** Re-read the file. Called when a person chooses to reload after a conflict. */
24
+ onReload?: () => void;
25
+ readOnly?: boolean;
26
+ theme?: EpisodeUITheme;
27
+ className?: string;
28
+ style?: CSSProperties;
29
+ }
30
+ /**
31
+ * Edit one text artefact of a vessel.
32
+ *
33
+ * The whole point of this component is the conflict path. When someone else
34
+ * writes the file between load and save, the vessel refuses and hands back the
35
+ * revision on disk. This editor then:
36
+ *
37
+ * - keeps the draft in the textarea, always, and says so;
38
+ * - shows what is on disk now rather than a status code;
39
+ * - offers reload as a choice the person makes, never as something that
40
+ * happens to their typing.
41
+ *
42
+ * A save is never retried automatically. Overwriting someone's work silently is
43
+ * the failure this guard exists to prevent, and a retry is exactly that.
44
+ */
45
+ export declare function EpisodeTextEditor({ file, loaded, onSave, onReload, readOnly, theme, className, style, }: EpisodeTextEditorProps): import("react").JSX.Element;
46
+ export interface EpisodeTextViewerProps {
47
+ file: string;
48
+ loaded: EpisodeTextView;
49
+ /**
50
+ * How to render markdown. The package ships no markdown engine: Miadi
51
+ * already renders with `react-markdown`, gmtermux with `marked` +
52
+ * `sanitize-html`, and a second one would be a second sanitisation boundary.
53
+ * Omit it and the source is shown as-is, which is always safe.
54
+ */
55
+ renderMarkdown?: (source: string) => ReactNode;
56
+ theme?: EpisodeUITheme;
57
+ className?: string;
58
+ style?: CSSProperties;
59
+ }
60
+ /** Read one text artefact — rendered when the host supplies a renderer, else source. */
61
+ export declare function EpisodeTextViewer({ file, loaded, renderMarkdown, theme, className, style, }: EpisodeTextViewerProps): import("react").JSX.Element;
62
+ //# sourceMappingURL=text-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-editor.d.ts","sourceRoot":"","sources":["../src/text-editor.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,oDAAoD;IACpD,QAAQ,CAAC,EAAE;QAAE,eAAe,EAAE,gBAAgB,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACzE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,MAAM,EAAE,eAAe,CAAA;IACvB;;;;OAIG;IACH,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3E,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,MAAM,EACN,MAAM,EACN,QAAQ,EACR,QAAgB,EAChB,KAAqB,EACrB,SAAS,EACT,KAAK,GACN,EAAE,sBAAsB,+BAqHxB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,CAAA;IAC9C,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,MAAM,EACN,cAAc,EACd,KAAqB,EACrB,SAAS,EACT,KAAK,GACN,EAAE,sBAAsB,+BAyBxB"}
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { DEFAULT_THEME, themeVars, } from "./types.js";
5
+ /**
6
+ * Edit one text artefact of a vessel.
7
+ *
8
+ * The whole point of this component is the conflict path. When someone else
9
+ * writes the file between load and save, the vessel refuses and hands back the
10
+ * revision on disk. This editor then:
11
+ *
12
+ * - keeps the draft in the textarea, always, and says so;
13
+ * - shows what is on disk now rather than a status code;
14
+ * - offers reload as a choice the person makes, never as something that
15
+ * happens to their typing.
16
+ *
17
+ * A save is never retried automatically. Overwriting someone's work silently is
18
+ * the failure this guard exists to prevent, and a retry is exactly that.
19
+ */
20
+ export function EpisodeTextEditor({ file, loaded, onSave, onReload, readOnly = false, theme = DEFAULT_THEME, className, style, }) {
21
+ const [draft, setDraft] = useState(loaded.content);
22
+ const [baseRevision, setBaseRevision] = useState(loaded.revision.sha256);
23
+ const [state, setState] = useState("idle");
24
+ const [message, setMessage] = useState("");
25
+ const [theirRevision, setTheirRevision] = useState(null);
26
+ const fileRef = useRef(file);
27
+ // A different file is a different draft; the same file re-read is a new base.
28
+ useEffect(() => {
29
+ if (fileRef.current !== file) {
30
+ fileRef.current = file;
31
+ setDraft(loaded.content);
32
+ setBaseRevision(loaded.revision.sha256);
33
+ setState("idle");
34
+ setMessage("");
35
+ setTheirRevision(null);
36
+ return;
37
+ }
38
+ if (loaded.revision.sha256 !== baseRevision && state !== "conflict") {
39
+ setDraft(loaded.content);
40
+ setBaseRevision(loaded.revision.sha256);
41
+ }
42
+ }, [file, loaded, baseRevision, state]);
43
+ const dirty = draft !== loaded.content || state === "conflict";
44
+ const save = async () => {
45
+ setState("saving");
46
+ setMessage("");
47
+ try {
48
+ const outcome = await onSave(draft, baseRevision);
49
+ if (outcome.ok) {
50
+ setState("saved");
51
+ setMessage(outcome.message ?? "Saved.");
52
+ setTheirRevision(null);
53
+ return;
54
+ }
55
+ if (outcome.conflict) {
56
+ setState("conflict");
57
+ setTheirRevision(outcome.conflict.currentRevision);
58
+ setMessage(outcome.message ??
59
+ "This file changed on disk while you were writing. Nothing was overwritten and your draft is still here.");
60
+ return;
61
+ }
62
+ setState("error");
63
+ setMessage(outcome.message ?? "It could not be saved.");
64
+ }
65
+ catch (error) {
66
+ setState("error");
67
+ setMessage(error instanceof Error ? error.message : "It could not be saved.");
68
+ }
69
+ };
70
+ const reload = () => {
71
+ setState("idle");
72
+ setMessage("");
73
+ setTheirRevision(null);
74
+ onReload?.();
75
+ };
76
+ return (_jsxs("section", { className: ["episode-text-editor", className].filter(Boolean).join(" "), style: { ...themeVars(theme), ...style }, "aria-label": `Editing ${file}`, children: [_jsxs("header", { className: "episode-text-editor-head", children: [_jsx("span", { className: "episode-text-editor-file", children: file }), _jsxs("span", { className: `episode-text-editor-state is-${state}`, role: "status", "aria-live": "polite", children: [state === "saving" && "Saving…", state === "saved" && "Saved", state === "conflict" && "Changed on disk", state === "error" && "Not saved", state === "idle" && (dirty ? "Unsaved changes" : "Up to date")] })] }), message && (_jsxs("p", { className: `episode-text-editor-message is-${state}`, children: [message, theirRevision && (_jsxs("span", { className: "episode-text-editor-their-revision", children: [" ", "On disk now: ", theirRevision.bytes, " bytes, ", theirRevision.modifiedAt, "."] }))] })), _jsx("textarea", { className: "episode-text-editor-field", value: draft, readOnly: readOnly, spellCheck: false, "aria-label": `Contents of ${file}`, onChange: (event) => {
77
+ setDraft(event.target.value);
78
+ if (state === "saved" || state === "error")
79
+ setState("idle");
80
+ } }), !readOnly && (_jsxs("div", { className: "episode-text-editor-actions", children: [_jsx("button", { type: "button", className: "episode-text-editor-save", disabled: state === "saving" || !dirty, onClick: save, children: "Save" }), state === "conflict" && onReload && (_jsx("button", { type: "button", className: "episode-text-editor-reload", onClick: reload, children: "Discard my draft and reload" }))] }))] }));
81
+ }
82
+ /** Read one text artefact — rendered when the host supplies a renderer, else source. */
83
+ export function EpisodeTextViewer({ file, loaded, renderMarkdown, theme = DEFAULT_THEME, className, style, }) {
84
+ const isMarkdown = loaded.contentType === ".md";
85
+ const [rich, setRich] = useState(Boolean(renderMarkdown) && isMarkdown);
86
+ return (_jsxs("section", { className: ["episode-text-viewer", className].filter(Boolean).join(" "), style: { ...themeVars(theme), ...style }, "aria-label": `Reading ${file}`, children: [_jsxs("header", { className: "episode-text-viewer-head", children: [_jsx("span", { className: "episode-text-viewer-file", children: file }), renderMarkdown && isMarkdown && (_jsx("button", { type: "button", className: "episode-text-viewer-toggle", onClick: () => setRich((value) => !value), children: rich ? "Source" : "Rendered" }))] }), rich && renderMarkdown ? (_jsx("div", { className: "episode-text-viewer-rich", children: renderMarkdown(loaded.content) })) : (_jsx("pre", { className: "episode-text-viewer-source", children: loaded.content }))] }));
87
+ }
88
+ //# sourceMappingURL=text-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-editor.js","sourceRoot":"","sources":["../src/text-editor.tsx"],"names":[],"mappings":"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"}