@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 +21 -0
- package/README.md +69 -0
- package/dist/ceremony.d.ts +48 -0
- package/dist/ceremony.d.ts.map +1 -0
- package/dist/ceremony.js +52 -0
- package/dist/ceremony.js.map +1 -0
- package/dist/file-browser.d.ts +29 -0
- package/dist/file-browser.d.ts.map +1 -0
- package/dist/file-browser.js +38 -0
- package/dist/file-browser.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/shelf-filter.d.ts +22 -0
- package/dist/shelf-filter.d.ts.map +1 -0
- package/dist/shelf-filter.js +34 -0
- package/dist/shelf-filter.js.map +1 -0
- package/dist/text-editor.d.ts +62 -0
- package/dist/text-editor.d.ts.map +1 -0
- package/dist/text-editor.js +88 -0
- package/dist/text-editor.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +63 -0
- package/dist/types.js.map +1 -0
- package/package.json +70 -0
- package/src/ceremony.tsx +185 -0
- package/src/episode-ui.css +400 -0
- package/src/file-browser.tsx +154 -0
- package/src/index.ts +49 -0
- package/src/shelf-filter.tsx +98 -0
- package/src/text-editor.tsx +230 -0
- package/src/types.ts +125 -0
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"}
|
package/dist/ceremony.js
ADDED
|
@@ -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":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,EACL,aAAa,EACb,SAAS,GAIV,MAAM,YAAY,CAAA;AAiBnB;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,UAAU,EACV,YAAY,EACZ,OAAO,EACP,MAAM,EACN,YAAY,EACZ,MAAM,EACN,KAAK,GAAG,eAAe,EACvB,WAAW,GAAG,wCAAwC,EACtD,KAAK,GAAG,aAAa,EACrB,SAAS,EACT,KAAK,GACmB;IACxB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;IAC3C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAwC,MAAM,CAAC,CAAA;IACjF,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAE1C,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,OAAO,CAAC,CAAA;QACjB,QAAQ,CAAC,MAAM,CAAC,CAAA;IAClB,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAA;IAEzB,MAAM,QAAQ,GAAG,YAAY,CAAC,iBAAiB,CAAA;IAE/C,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,QAAQ,CAAC,QAAQ,CAAC,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAA;YACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YACxC,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,YAAY,EAAE,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAA;QACrG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,OAAO,CAAC,CAAA;YACjB,UAAU,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAA;QAC/E,CAAC;IACH,CAAC,CAAA;IAED,OAAO,CACL,eACE,SAAS,EAAE,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EACxE,KAAK,EAAE,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,EAAmB,aAEzD,eAAK,SAAS,EAAC,oBAAoB,aACjC,gBAAO,SAAS,EAAC,qBAAqB,EAAC,OAAO,EAAE,iBAAiB,UAAU,EAAE,YAC1E,KAAK,GACA,EACR,eAAM,SAAS,EAAE,0BAA0B,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,YAAY,YAC3F,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,GACjC,IACH,EACN,mBACE,EAAE,EAAE,iBAAiB,UAAU,EAAE,EACjC,SAAS,EAAC,qBAAqB,EAC/B,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,KAAK,EAChB,QAAQ,EAAE,CAAC,QAAQ,EACnB,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,EAC/C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAClB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;oBAC5B,IAAI,KAAK,KAAK,MAAM;wBAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;gBACxC,CAAC,GACD,EACD,QAAQ,IAAI,CACX,eAAK,SAAS,EAAC,uBAAuB,aACpC,iBAAQ,IAAI,EAAC,QAAQ,EAAC,QAAQ,EAAE,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,OAAO,EAAE,OAAO,EAAE,IAAI,2BAE7E,EACR,OAAO,IAAI,CACV,eAAM,SAAS,EAAE,4BAA4B,KAAK,EAAE,EAAE,IAAI,EAAC,QAAQ,eAAW,QAAQ,YACnF,OAAO,GACH,CACR,IACG,CACP,IACG,CACP,CAAA;AACH,CAAC;AAcD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,WAAW,EACX,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,UAAU,EACV,KAAK,GAAG,cAAc,EACtB,KAAK,GAAG,aAAa,EACrB,SAAS,EACT,KAAK,GACoB;IACzB,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAA;IAExD,OAAO,CACL,eACE,SAAS,EAAE,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EACxE,KAAK,EAAE,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,EAAmB,aAEzD,eAAM,SAAS,EAAC,4BAA4B,YAAE,KAAK,GAAQ,EAC1D,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAC1B,YAAG,SAAS,EAAC,4BAA4B,YAAE,UAAU,GAAK,CAC3D,CAAC,CAAC,CAAC,CACF,aAAI,SAAS,EAAC,2BAA2B,YACtC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACzB,cAA4B,SAAS,EAAC,qBAAqB,aACzD,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,0BAA0B,EACpC,QAAQ,EAAE,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,MAAM,EACvC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,YAEzC,IAAI,CAAC,YAAY,GACX,EACT,gBAAM,SAAS,EAAC,0BAA0B,aACvC,IAAI,CAAC,UAAU,cAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,IAC9D,EACN,IAAI,CAAC,IAAI,IAAI,eAAM,SAAS,EAAC,0BAA0B,YAAE,IAAI,CAAC,IAAI,GAAQ,EAC1E,QAAQ,IAAI,YAAY,CAAC,gBAAgB,IAAI,CAC5C,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,4BAA4B,EACtC,KAAK,EAAC,yBAAyB,EAC/B,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,uBAGnC,CACV,KAtBM,IAAI,CAAC,YAAY,CAuBrB,CACN,CAAC,GACC,CACN,IACG,CACP,CAAA;AACH,CAAC"}
|
|
@@ -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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"}
|
package/dist/index.d.ts
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, 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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"}
|