@medicine-wheel/episode-ui 0.0.0-stage → 0.26.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.
Files changed (64) hide show
  1. package/README.md +215 -2
  2. package/dist/address.d.ts +30 -0
  3. package/dist/address.d.ts.map +1 -0
  4. package/dist/address.js +64 -0
  5. package/dist/address.js.map +1 -0
  6. package/dist/browser/episode-files.js +89 -0
  7. package/dist/browser.d.ts +14 -0
  8. package/dist/browser.d.ts.map +1 -0
  9. package/dist/browser.js +14 -0
  10. package/dist/browser.js.map +1 -0
  11. package/dist/ceremony.d.ts +48 -0
  12. package/dist/ceremony.d.ts.map +1 -0
  13. package/dist/ceremony.js +52 -0
  14. package/dist/ceremony.js.map +1 -0
  15. package/dist/chronicle-theme.d.ts +60 -0
  16. package/dist/chronicle-theme.d.ts.map +1 -0
  17. package/dist/chronicle-theme.js +64 -0
  18. package/dist/chronicle-theme.js.map +1 -0
  19. package/dist/episode-files.d.ts +148 -0
  20. package/dist/episode-files.d.ts.map +1 -0
  21. package/dist/episode-files.js +997 -0
  22. package/dist/episode-files.js.map +1 -0
  23. package/dist/file-tree.d.ts +51 -0
  24. package/dist/file-tree.d.ts.map +1 -0
  25. package/dist/file-tree.js +112 -0
  26. package/dist/file-tree.js.map +1 -0
  27. package/dist/http.d.ts +31 -0
  28. package/dist/http.d.ts.map +1 -0
  29. package/dist/http.js +88 -0
  30. package/dist/http.js.map +1 -0
  31. package/dist/index.d.ts +29 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +29 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/react.d.ts +32 -0
  36. package/dist/react.d.ts.map +1 -0
  37. package/dist/react.js +58 -0
  38. package/dist/react.js.map +1 -0
  39. package/dist/room.d.ts +82 -0
  40. package/dist/room.d.ts.map +1 -0
  41. package/dist/room.js +48 -0
  42. package/dist/room.js.map +1 -0
  43. package/dist/shelf-filter.d.ts +22 -0
  44. package/dist/shelf-filter.d.ts.map +1 -0
  45. package/dist/shelf-filter.js +34 -0
  46. package/dist/shelf-filter.js.map +1 -0
  47. package/dist/types.d.ts +108 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +70 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +98 -4
  52. package/src/address.ts +61 -0
  53. package/src/browser.ts +13 -0
  54. package/src/ceremony.tsx +185 -0
  55. package/src/chronicle-theme.ts +82 -0
  56. package/src/episode-files.ts +1143 -0
  57. package/src/episode-ui.css +889 -0
  58. package/src/file-tree.ts +135 -0
  59. package/src/http.ts +123 -0
  60. package/src/index.ts +91 -0
  61. package/src/react.tsx +94 -0
  62. package/src/room.tsx +177 -0
  63. package/src/shelf-filter.tsx +98 -0
  64. package/src/types.ts +154 -0
package/README.md CHANGED
@@ -1,3 +1,216 @@
1
- # Temporary Holding Version
1
+ # @medicine-wheel/episode-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Browse, read and edit the files of a Miadi episode in the browser.
4
+
5
+ In the Miadi stack, an episode is the folder a piece of work is filed under, and this package
6
+ is how a person opens one: browse its files, read one, preview its markdown, edit it in
7
+ CodeMirror, go full screen with only Save and Back, and survive a conflict without losing the
8
+ draft. It also has React components for a ceremony's working notes, its attachments, and the
9
+ episode shelf. It is the browser half of `@medicine-wheel/episode-vessel`, which holds the rules for
10
+ the folder on the server side.
11
+
12
+ ## Where it lives
13
+
14
+ Up to 0.6.0 (jgwill/Miadi e4e1b141) this package was `@miadi/episode-ui`, published from jgwill/Miadi. It moved to
15
+ jgwill/medicine-wheel in 0.25.0 so that dependencies run one way: the medicine-wheel suite is
16
+ the foundation, and Miadi consumes it. From 0.7.0, `@miadi/episode-ui` re-exports this package with the
17
+ same entry points, so code that imports it keeps working. Ref jgwill/medicine-wheel#166.
18
+
19
+ ```bash
20
+ npm i @medicine-wheel/episode-ui
21
+ ```
22
+
23
+ ## One files workspace, two kinds of host
24
+
25
+ `mountEpisodeFiles` is framework-free, so a plain page and a React app mount the
26
+ same implementation.
27
+
28
+ React:
29
+
30
+ ```jsx
31
+ import { EpisodeFiles, httpEpisodeFiles } from "@medicine-wheel/episode-ui"
32
+ import "@medicine-wheel/episode-ui/styles.css"
33
+
34
+ const service = httpEpisodeFiles({ fileUrl: (file) => `/api/files/${file.relativePath}` })
35
+ <EpisodeFiles files={files} capabilities={capabilities} service={service} />
36
+ ```
37
+
38
+ A page with no bundler: serve `dist/browser/episode-files.js`
39
+ (CodeMirror, marked and DOMPurify inlined) and `src/episode-ui.css`.
40
+
41
+ ```html
42
+ <div id="files"></div>
43
+ <script type="module">
44
+ import { mountEpisodeFiles, httpEpisodeFiles } from "/vendor/episode-ui/episode-files.js"
45
+ mountEpisodeFiles(document.getElementById("files"), {
46
+ files,
47
+ service: httpEpisodeFiles({
48
+ fileUrl: (file) => `/api/episode-file?file=${encodeURIComponent(file.relativePath)}`,
49
+ mediaUrl: (file) => `/api/episode-media?file=${encodeURIComponent(file.relativePath)}`,
50
+ }),
51
+ })
52
+ </script>
53
+ ```
54
+
55
+ ## What it does
56
+
57
+ - A filterable file tree: the folders the paths name, every one open, so a file's
58
+ folder is plain to see. Guidance files are dimmed and media and documents marked.
59
+ - Markdown opens in a sanitised preview (`marked` + DOMPurify); `Source` shows CodeMirror.
60
+ - CodeMirror 6 with markdown, JSON and YAML colouring, undo, search (Ctrl/Cmd-F),
61
+ line wrapping, and Ctrl/Cmd-S to save.
62
+ - `⛶` opens the viewer full screen with only the file name, its state, Save and `×`.
63
+ Esc returns.
64
+ - Audio and video play inline; images show inline; documents are a download.
65
+ - A markdown preview shows the episode's own images: `![](images/still.png)` is
66
+ resolved from the file's folder and shown from the host's `mediaUrl`.
67
+ - **Add images** (button, or drop files on the workspace) appears when the
68
+ capabilities include `upload` and the service has `upload` (`httpEpisodeFiles`
69
+ has it only when the host passes `uploadUrl`). Each image is sent
70
+ to `images/<name>`, the name reduced to safe characters and suffixed `-2`, `-3`
71
+ when taken; the new files are listed at once, `onFilesAdded` tells the host, and
72
+ the last one opens.
73
+ - An open image shows the record beside it: caption, role and, at the bottom, the
74
+ credit (`service.readRecord`). A role holding `edit` gets **Describe** (role,
75
+ caption, alt text, origin, tool, made by, chosen by; `service.saveRecord` over
76
+ the revision read) and **Use as cover** (`service.setCover`). `httpEpisodeFiles`
77
+ offers them when the host passes `recordUrl` and `coverUrl`.
78
+ - Leaving a file or the page with unsaved changes asks first.
79
+
80
+ ## The folders
81
+
82
+ The vessel lists files, each with its path, and no folders. The workspace builds
83
+ the folders from the paths and draws them as a tree:
84
+
85
+ - Every folder is open to begin with. A folder closes and opens from its row, and
86
+ **Collapse all** / **Expand all** does every folder at once. Only a closed folder
87
+ is remembered, so a folder that arrives later opens.
88
+ - A row says the file's name; its whole path is the row's tooltip, and
89
+ `data-path` carries it for a host or a test.
90
+ - At each level a `README.md` comes first, then the folders by name, then the
91
+ files in the vessel's order (text, documents, guidance, images, media). Names
92
+ compare numerically: `chapter-2` comes before `chapter-10`.
93
+ - A folder shows how many files are beneath it, at any depth.
94
+ - Typing in the filter shows each match in its folders, every one open whatever was
95
+ closed before; clearing it brings back the folders as the reader left them.
96
+ - A file opened from a link or an address opens the folders it sits in.
97
+ - A list with no folder in it looks as it always did.
98
+
99
+ `listing="flat"` keeps the old list of whole paths, for a host that wants it.
100
+
101
+ The grouping is also exported, framework-free, for a host that draws its own:
102
+ `buildFileTree(files)` returns the root folder (`children`, each a `file` or a
103
+ `folder` with its `path` and `fileCount`), `ancestorFolders(path)` names the
104
+ folders above a file and `folderPaths(tree)` every folder in the tree.
105
+
106
+ ## The open file as an address
107
+
108
+ The workspace holds no URL, so a host that wants the open file in its address
109
+ passes `open` and listens to `onSelectionChange`:
110
+
111
+ ```jsx
112
+ <EpisodeFiles
113
+ files={files}
114
+ service={service}
115
+ open={{ path: "notes.md", mode: "source" }} // from the address; null closes
116
+ onSelectionChange={(selection, { push }) => {
117
+ // selection: { path, mode? } or null. mode is "preview" | "source" for a text file.
118
+ // push is true when a person opened another file, or kept a draft instead of
119
+ // following the address: add a history entry. Otherwise replace the current one.
120
+ }}
121
+ />
122
+ ```
123
+
124
+ - A requested path waits for `files` to arrive and for the right to read, then
125
+ opens and scrolls the viewer into view. `open={{ path, reveal: false }}` opens
126
+ it without scrolling, for a room that opens on its script by default and should
127
+ not carry a phone past its header. A file a person clicks still scrolls into
128
+ view on a narrow screen.
129
+ - `open` is acted on only when its value changes, so re-rendering with the same
130
+ one does nothing, and a click the address has not caught up with is kept.
131
+ - A path the episode does not hold, or a file too large to open, says so. A
132
+ mode that does not fit the file is corrected, and the corrected selection is
133
+ reported back.
134
+ - An unsaved draft is never dropped by an address change: the person is asked.
135
+ A refusal reports the file still shown with `push`, so the entry Back or
136
+ Forward moved to is kept rather than overwritten.
137
+ - A relative link in a markdown preview is read from that file's folder. A plain
138
+ click on a link to another file of the episode opens it here in preview, with
139
+ `push`. Pass `fileHref={(selection) => url}` and those links also carry the
140
+ file's address, so a new tab or a copied link lands on the same file. A link
141
+ that leaves the episode (a scheme, a root path, a climb above the root) is
142
+ left as written. `episodeLinkTarget(fromPath, href)` is the same resolution.
143
+
144
+ The address rule is also exported on its own: `selectionFromSearch(search)`
145
+ reads `?file=&mode=`, `addressWithSelection(href, selection)` writes it into an
146
+ address and keeps every other parameter, and `writeSelectionToAddress(selection,
147
+ { push })` writes it into the browser's history. In React, one hook does all of it:
148
+
149
+ ```jsx
150
+ const address = useEpisodeFilesAddress() // or { search: useSearchParams().toString() } in Next.js
151
+ <EpisodeFiles {...address} files={files} service={service} />
152
+ ```
153
+
154
+ ## The room's masthead, lineage and source issues
155
+
156
+ `EpisodeMasthead`, `LineageList` and `SourceIssueList` draw the top of an
157
+ episode room. They hold no state and fetch nothing, so a server page renders them
158
+ and passes a link callback:
159
+
160
+ ```jsx
161
+ <EpisodeMasthead numberLabel="551" title={summary.title} goal={summary.goal}
162
+ date={summary.date} status={summary.status} type={summary.type}
163
+ direction={{ label: "West", oj: "Epangishmok", glyph: "🌊", color: "#5b78b4" }}
164
+ coverUrl={coverUrl} theme={CHRONICLE_THEME} />
165
+ <LineageList edges={await readEpisodeLineage(root, episode)} href={(folder) => `/episodes/${folder}`} />
166
+ <SourceIssueList issues={summary.references.map((ref) => ({ ref, url: issueUrl(ref) }))} />
167
+ ```
168
+
169
+ An edge whose number two folders claim lists both, each linked.
170
+
171
+ ## The chronicle theme
172
+
173
+ `@medicine-wheel/episode-ui/chronicle-theme` is the Chronicle's dark surface as data:
174
+ `CHRONICLE_PALETTE`, `CHRONICLE_THEME` (an `EpisodeUITheme`, which
175
+ `@miadi/attention-ui`'s panel also takes), `DIRECTIONS` (the four directions with
176
+ their names, colours and glyphs), `FONT_SANS` and `FONT_MONO`. Every text colour
177
+ of `CHRONICLE_THEME` reads at 4.5:1 or better on its background and its card.
178
+
179
+ ## One file, no folder
180
+
181
+ A host that opens a single file it names itself, such as Miadi's `/e` editor, passes
182
+ that file in `files` and in `open` with `layout: "single"`: the list, filter and
183
+ count are hidden and the viewer takes the width. `outsideFileHref(path)` gives a
184
+ link in the preview to a file the workspace does not hold an address of its own;
185
+ without it, such a link says the file is not here.
186
+
187
+ ## The conflict path
188
+
189
+ A save carries the revision the file was read at. When someone else wrote in
190
+ between, the host answers 409 with `currentRevision`: the draft stays in the
191
+ editor, the message says what is on disk now, and Reload is a choice the person
192
+ makes. **A save is never retried automatically.**
193
+
194
+ ## The contract
195
+
196
+ No component holds a URL or a token, knows a chronicle root, or decides what a
197
+ person is allowed to do. The host passes a `service`; `httpEpisodeFiles` is the
198
+ one module that fetches, and only from URLs the host gives it. The host resolves
199
+ capabilities (`@medicine-wheel/episode-vessel/permissions`) and stays the authority; a
200
+ capability here decides only what is offered. A test holds this contract.
201
+
202
+ | export | what it is for |
203
+ |---|---|
204
+ | `mountEpisodeFiles(el, options)` | the files workspace; returns `{ update, destroy }` |
205
+ | `EpisodeFiles` | the same, as a React component |
206
+ | `httpEpisodeFiles({ fileUrl, mediaUrl?, headers?, uploadUrl?, recordUrl?, coverUrl? })` | the service for a host that answers GET/PUT JSON; with `uploadUrl` it POSTs a new image's bytes, with `recordUrl` it reads and writes an image's record, with `coverUrl` it names the cover |
207
+ | `imageUploadPath(name, type, taken)` | the `images/` path a chosen image is added at |
208
+ | `CeremonyNoteEditor` | a ceremony's working notes (React) |
209
+ | `CeremonyAttachments` | what a circle was held about (React) |
210
+ | `EpisodeShelfFilter` | find an episode by number, title or slug (React) |
211
+
212
+ ## Styling
213
+
214
+ One stylesheet of plain class names over CSS custom properties, set from an
215
+ `EpisodeUITheme`. Touch targets are 44px because the first surface this runs on
216
+ is a phone.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The open file as the page's address: `?file=<path>&mode=preview|source`.
3
+ *
4
+ * A reload, a shared link and Back all land on the same file because the
5
+ * address carries it. These functions are that rule, with no framework: read a
6
+ * selection from a query string, and write one into an address. Miadi's room
7
+ * and eury's room each wrote them by hand; `useEpisodeFilesAddress` is the
8
+ * React form.
9
+ */
10
+ import type { EpisodeFileSelection } from "./episode-files.js";
11
+ export declare const FILE_PARAM = "file";
12
+ export declare const MODE_PARAM = "mode";
13
+ /** The selection a query string names (`?file=&mode=`), or null when it names none. A mode it does not know is dropped. */
14
+ export declare function selectionFromSearch(search: string | URLSearchParams): EpisodeFileSelection | null;
15
+ /**
16
+ * `href` with the selection written into its query, every other parameter and
17
+ * the fragment kept. `null` removes the file and its mode. The address keeps
18
+ * its form: absolute stays absolute, `/episodes/551` stays rooted, and
19
+ * `551?x=1`, `?x=1` or `#top` stay relative to the page.
20
+ */
21
+ export declare function addressWithSelection(href: string, selection: EpisodeFileSelection | null): string;
22
+ /**
23
+ * Write the selection into the browser's address: a new history entry when
24
+ * `push` (a person opened another file), otherwise a replacement. Nothing is
25
+ * written when the address already says it.
26
+ */
27
+ export declare function writeSelectionToAddress(selection: EpisodeFileSelection | null, { push }: {
28
+ push: boolean;
29
+ }): void;
30
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAY,MAAM,oBAAoB,CAAA;AAExE,eAAO,MAAM,UAAU,SAAS,CAAA;AAChC,eAAO,MAAM,UAAU,SAAS,CAAA;AAIhC,2HAA2H;AAC3H,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,oBAAoB,GAAG,IAAI,CAMjG;AAKD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,GAAG,IAAI,GAAG,MAAM,CAWjG;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAOjH"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The open file as the page's address: `?file=<path>&mode=preview|source`.
3
+ *
4
+ * A reload, a shared link and Back all land on the same file because the
5
+ * address carries it. These functions are that rule, with no framework: read a
6
+ * selection from a query string, and write one into an address. Miadi's room
7
+ * and eury's room each wrote them by hand; `useEpisodeFilesAddress` is the
8
+ * React form.
9
+ */
10
+ export const FILE_PARAM = "file";
11
+ export const MODE_PARAM = "mode";
12
+ const isMode = (value) => value === "preview" || value === "source";
13
+ /** The selection a query string names (`?file=&mode=`), or null when it names none. A mode it does not know is dropped. */
14
+ export function selectionFromSearch(search) {
15
+ const params = typeof search === "string" ? new URLSearchParams(search) : search;
16
+ const path = params.get(FILE_PARAM);
17
+ if (!path)
18
+ return null;
19
+ const mode = params.get(MODE_PARAM);
20
+ return isMode(mode) ? { path, mode } : { path };
21
+ }
22
+ /** A base for reading a relative href: no scheme a browser fetches, and a folder to strip off again. */
23
+ const RELATIVE_BASE = "address:/~/";
24
+ /**
25
+ * `href` with the selection written into its query, every other parameter and
26
+ * the fragment kept. `null` removes the file and its mode. The address keeps
27
+ * its form: absolute stays absolute, `/episodes/551` stays rooted, and
28
+ * `551?x=1`, `?x=1` or `#top` stay relative to the page.
29
+ */
30
+ export function addressWithSelection(href, selection) {
31
+ const absolute = /^[a-z][a-z0-9+.-]*:/i.test(href);
32
+ const url = new URL(href, RELATIVE_BASE);
33
+ if (selection)
34
+ url.searchParams.set(FILE_PARAM, selection.path);
35
+ else
36
+ url.searchParams.delete(FILE_PARAM);
37
+ if (selection?.mode)
38
+ url.searchParams.set(MODE_PARAM, selection.mode);
39
+ else
40
+ url.searchParams.delete(MODE_PARAM);
41
+ if (absolute)
42
+ return url.href;
43
+ const rooted = href.startsWith("/");
44
+ const path = !rooted && url.pathname.startsWith("/~/") ? url.pathname.slice(3) : url.pathname;
45
+ return `${path}${url.search}${url.hash}`;
46
+ }
47
+ /**
48
+ * Write the selection into the browser's address: a new history entry when
49
+ * `push` (a person opened another file), otherwise a replacement. Nothing is
50
+ * written when the address already says it.
51
+ */
52
+ export function writeSelectionToAddress(selection, { push }) {
53
+ if (typeof window === "undefined")
54
+ return;
55
+ const next = addressWithSelection(window.location.href, selection);
56
+ if (next === window.location.href)
57
+ return;
58
+ // `null` state, as Next.js's app router documents for its own pushState sync.
59
+ if (push)
60
+ window.history.pushState(null, "", next);
61
+ else
62
+ window.history.replaceState(null, "", next);
63
+ }
64
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAA;AAChC,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAA;AAEhC,MAAM,MAAM,GAAG,CAAC,KAAoB,EAAqB,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,QAAQ,CAAA;AAErG,2HAA2H;AAC3H,MAAM,UAAU,mBAAmB,CAAC,MAAgC;IAClE,MAAM,MAAM,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAChF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACnC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;AACjD,CAAC;AAED,wGAAwG;AACxG,MAAM,aAAa,GAAG,aAAa,CAAA;AAEnC;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,SAAsC;IACvF,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,CAAA;IACxC,IAAI,SAAS;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;;QAC1D,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxC,IAAI,SAAS,EAAE,IAAI;QAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;;QAChE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;IACxC,IAAI,QAAQ;QAAE,OAAO,GAAG,CAAC,IAAI,CAAA;IAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;IACnC,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAA;IAC7F,OAAO,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,SAAsC,EAAE,EAAE,IAAI,EAAqB;IACzG,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAM;IACzC,MAAM,IAAI,GAAG,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAClE,IAAI,IAAI,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAM;IACzC,8EAA8E;IAC9E,IAAI,IAAI;QAAE,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;;QAC7C,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;AAClD,CAAC"}