@assemora/pages 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/dist/module.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The `.blocks()` facet and the `pages()` module (SPEC.md §13, §114).
3
+ */
4
+ import { defineModuleFacet, module, registerRestorer } from '@assemora/core';
5
+ import { UNSPECIFIED_LOCALE } from '@assemora/data';
6
+ import { emptyTree } from '@assemora/schema';
7
+ import { describeBlock, registerBlock } from './block.js';
8
+ import { pageCommands } from './commands.js';
9
+ import { Page, pageModels } from './models.js';
10
+ import { pageQueries } from './queries.js';
11
+ let defined = false;
12
+ export const defineBlockFacet = () => {
13
+ if (defined)
14
+ return;
15
+ defineModuleFacet('blocks', (internals, args) => {
16
+ internals.addRegistration((context) => {
17
+ for (const candidate of args) {
18
+ const declared = candidate;
19
+ if (declared?.node !== 'block')
20
+ continue;
21
+ registerBlock(declared);
22
+ context.registry.register('blocks', describeBlock(declared, internals.name));
23
+ }
24
+ });
25
+ });
26
+ defined = true;
27
+ };
28
+ defineBlockFacet();
29
+ export const pages = (options = {}) => module('pages')
30
+ .models(...pageModels)
31
+ .commands(...pageCommands)
32
+ .queries(...pageQueries)
33
+ .blocks(...(options.blocks ?? []))
34
+ .boot(() => {
35
+ // How a page goes back to an earlier state. `@assemora/revisions` calls this
36
+ // and never learns what a page is (SPEC.md §65).
37
+ registerRestorer('pages', async (entityId, state) => {
38
+ const existing = await Page.find(entityId);
39
+ const replaced = existing === null ? null : existing.toJSON();
40
+ // `null` means the page did not exist then. Undoing a creation is an ordinary
41
+ // restore, and so is putting back something that was deleted (SPEC.md §65).
42
+ if (state === null || state === undefined) {
43
+ if (existing !== null)
44
+ await existing.delete();
45
+ return { replaced, version: 0 };
46
+ }
47
+ const snapshot = state;
48
+ if (existing === null) {
49
+ const recreated = await Page.create({
50
+ id: entityId,
51
+ slug: String(snapshot.slug ?? entityId),
52
+ title: String(snapshot.title ?? 'Restored page'),
53
+ status: (snapshot.status ?? 'draft'),
54
+ draftTree: (snapshot.draftTree ?? emptyTree()),
55
+ publishedTree: (snapshot.publishedTree ?? null),
56
+ meta: (snapshot.meta ?? {}),
57
+ version: 1,
58
+ // From the revision, not from the operation restoring it: a page put back is
59
+ // the page it was, in the language it was written in (SPEC.md §131).
60
+ locale: String(snapshot.locale ?? UNSPECIFIED_LOCALE),
61
+ translationOf: (snapshot.translationOf ?? null),
62
+ publishedAt: null,
63
+ });
64
+ return { replaced, version: recreated.version, tree: recreated.draftTree };
65
+ }
66
+ await existing.update({
67
+ title: String(snapshot.title ?? existing.title),
68
+ slug: String(snapshot.slug ?? existing.slug),
69
+ status: (snapshot.status ?? existing.status),
70
+ draftTree: (snapshot.draftTree ?? existing.draftTree),
71
+ publishedTree: (snapshot.publishedTree ?? existing.publishedTree),
72
+ meta: (snapshot.meta ?? existing.meta),
73
+ version: existing.version + 1,
74
+ });
75
+ // The caller needs both: its next command carries `expectedVersion`, and an
76
+ // editor has to draw what the undo produced without reading the page again.
77
+ return { replaced, version: existing.version, tree: existing.draftTree };
78
+ });
79
+ });
80
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,162 @@
1
+ export type PageMode = 'draft' | 'published';
2
+ export declare const ListPages: import("@assemora/core").QueryDefinition<{
3
+ status: import("@assemora/schema").OptionalSchema<"archived" | "draft" | "published">;
4
+ search: import("@assemora/schema").OptionalSchema<string>;
5
+ page: import("@assemora/schema").OptionalSchema<number>;
6
+ perPage: import("@assemora/schema").OptionalSchema<number>;
7
+ }, {
8
+ readonly data: readonly {
9
+ readonly id: string;
10
+ readonly locale: string;
11
+ readonly publishedAt: Date | null;
12
+ readonly slug: string;
13
+ readonly status: "archived" | "draft" | "published";
14
+ readonly title: string;
15
+ readonly translationOf: string | null;
16
+ readonly updatedAt: Date;
17
+ readonly version: number;
18
+ }[];
19
+ readonly lastPage: number;
20
+ readonly page: number;
21
+ readonly perPage: number;
22
+ readonly total: number;
23
+ }>;
24
+ export declare const GetPage: import("@assemora/core").QueryDefinition<{
25
+ id: import("@assemora/schema").OptionalSchema<string>;
26
+ slug: import("@assemora/schema").OptionalSchema<string>;
27
+ mode: import("@assemora/schema").OptionalSchema<"draft" | "published">;
28
+ }, {
29
+ readonly hasUnpublishedChanges: boolean;
30
+ readonly id: string;
31
+ readonly locale: string;
32
+ readonly meta: {
33
+ readonly title?: string;
34
+ readonly description?: string;
35
+ readonly image?: string;
36
+ readonly noIndex?: boolean;
37
+ };
38
+ readonly mode: "draft" | "published";
39
+ readonly publishedAt: Date | null;
40
+ readonly slug: string;
41
+ readonly status: "archived" | "draft" | "published";
42
+ readonly title: string;
43
+ readonly translationOf: string | null;
44
+ readonly tree: {
45
+ readonly blocks: readonly {
46
+ readonly id: string;
47
+ readonly type: string;
48
+ readonly version: number;
49
+ readonly props: {
50
+ readonly [x: string]: unknown;
51
+ };
52
+ readonly children: readonly /*elided*/ any[];
53
+ readonly hidden?: boolean;
54
+ readonly design?: {
55
+ readonly spacingTop?: import("@assemora/schema").SpacingScale;
56
+ readonly spacingBottom?: import("@assemora/schema").SpacingScale;
57
+ readonly width?: import("@assemora/schema").BlockWidth;
58
+ readonly align?: import("@assemora/schema").BlockAlignment;
59
+ readonly background?: string;
60
+ readonly backgroundImage?: string;
61
+ readonly container?: import("@assemora/schema").ContainerWidth;
62
+ readonly hiddenOn?: readonly ("desktop" | "mobile" | "tablet")[];
63
+ };
64
+ }[];
65
+ };
66
+ readonly updatedAt: Date;
67
+ readonly updatedBy: string | null;
68
+ readonly version: number;
69
+ }>;
70
+ export declare const ListPageTranslations: import("@assemora/core").QueryDefinition<{
71
+ id: import("@assemora/schema").StringSchema;
72
+ }, {
73
+ readonly translations: readonly {
74
+ readonly id: string;
75
+ readonly isOriginal: boolean;
76
+ readonly locale: string;
77
+ readonly slug: string;
78
+ readonly stale: boolean | null;
79
+ readonly status: "archived" | "draft" | "published";
80
+ readonly updatedAt: Date;
81
+ }[];
82
+ }>;
83
+ export declare const pageQueries: readonly [import("@assemora/core").QueryDefinition<{
84
+ status: import("@assemora/schema").OptionalSchema<"archived" | "draft" | "published">;
85
+ search: import("@assemora/schema").OptionalSchema<string>;
86
+ page: import("@assemora/schema").OptionalSchema<number>;
87
+ perPage: import("@assemora/schema").OptionalSchema<number>;
88
+ }, {
89
+ readonly data: readonly {
90
+ readonly id: string;
91
+ readonly locale: string;
92
+ readonly publishedAt: Date | null;
93
+ readonly slug: string;
94
+ readonly status: "archived" | "draft" | "published";
95
+ readonly title: string;
96
+ readonly translationOf: string | null;
97
+ readonly updatedAt: Date;
98
+ readonly version: number;
99
+ }[];
100
+ readonly lastPage: number;
101
+ readonly page: number;
102
+ readonly perPage: number;
103
+ readonly total: number;
104
+ }>, import("@assemora/core").QueryDefinition<{
105
+ id: import("@assemora/schema").OptionalSchema<string>;
106
+ slug: import("@assemora/schema").OptionalSchema<string>;
107
+ mode: import("@assemora/schema").OptionalSchema<"draft" | "published">;
108
+ }, {
109
+ readonly hasUnpublishedChanges: boolean;
110
+ readonly id: string;
111
+ readonly locale: string;
112
+ readonly meta: {
113
+ readonly title?: string;
114
+ readonly description?: string;
115
+ readonly image?: string;
116
+ readonly noIndex?: boolean;
117
+ };
118
+ readonly mode: "draft" | "published";
119
+ readonly publishedAt: Date | null;
120
+ readonly slug: string;
121
+ readonly status: "archived" | "draft" | "published";
122
+ readonly title: string;
123
+ readonly translationOf: string | null;
124
+ readonly tree: {
125
+ readonly blocks: readonly {
126
+ readonly id: string;
127
+ readonly type: string;
128
+ readonly version: number;
129
+ readonly props: {
130
+ readonly [x: string]: unknown;
131
+ };
132
+ readonly children: readonly /*elided*/ any[];
133
+ readonly hidden?: boolean;
134
+ readonly design?: {
135
+ readonly spacingTop?: import("@assemora/schema").SpacingScale;
136
+ readonly spacingBottom?: import("@assemora/schema").SpacingScale;
137
+ readonly width?: import("@assemora/schema").BlockWidth;
138
+ readonly align?: import("@assemora/schema").BlockAlignment;
139
+ readonly background?: string;
140
+ readonly backgroundImage?: string;
141
+ readonly container?: import("@assemora/schema").ContainerWidth;
142
+ readonly hiddenOn?: readonly ("desktop" | "mobile" | "tablet")[];
143
+ };
144
+ }[];
145
+ };
146
+ readonly updatedAt: Date;
147
+ readonly updatedBy: string | null;
148
+ readonly version: number;
149
+ }>, import("@assemora/core").QueryDefinition<{
150
+ id: import("@assemora/schema").StringSchema;
151
+ }, {
152
+ readonly translations: readonly {
153
+ readonly id: string;
154
+ readonly isOriginal: boolean;
155
+ readonly locale: string;
156
+ readonly slug: string;
157
+ readonly stale: boolean | null;
158
+ readonly status: "archived" | "draft" | "published";
159
+ readonly updatedAt: Date;
160
+ }[];
161
+ }>];
162
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,WAAW,CAAA;AAqC5C,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;EAkDpB,CAAA;AAEF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0DlB,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAoD/B,CAAA;AAEF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAAsD,CAAA"}
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Reading pages (SPEC.md §15, §53).
3
+ *
4
+ * A page is read through the Query Bus, so the same validation, authorization and
5
+ * naming apply as to everything else and no layer above needs to depend on this one
6
+ * (ADR-0014). A browser reaches these through `server.mountQueries()`.
7
+ */
8
+ import { NotFoundError, query } from '@assemora/core';
9
+ import { array, blockTree, boolean, emptyTree, enumOf, json, number, object, string, timestamp, uuid, } from '@assemora/schema';
10
+ import { Page } from './models.js';
11
+ /**
12
+ * What a reader gets: one tree, already chosen.
13
+ *
14
+ * Handing back both trees would make every caller decide which one it meant, and a
15
+ * visitor's renderer would be one mistake away from showing an unpublished draft.
16
+ */
17
+ const project = (page, mode) => ({
18
+ id: page.id,
19
+ slug: page.slug,
20
+ title: page.title,
21
+ status: page.status,
22
+ /**
23
+ * Which language this page is actually written in, and which page it is one language
24
+ * of (SPEC.md §131).
25
+ *
26
+ * Beside the id for the reason a resource read carries them: a reader that cannot tell
27
+ * a translation from a fallback has been handed the wrong answer without being told,
28
+ * and a link to another language needs the entry rather than the row.
29
+ */
30
+ locale: page.locale,
31
+ translationOf: page.translationOf,
32
+ mode,
33
+ tree: (mode === 'draft' ? page.draftTree : (page.publishedTree ?? emptyTree())),
34
+ meta: page.meta,
35
+ /** Every mutation echoes this back as `expectedVersion` (SPEC.md §66). */
36
+ version: page.version,
37
+ /** True when the draft has moved on from what visitors see. */
38
+ hasUnpublishedChanges: page.publishedTree === null ||
39
+ JSON.stringify(page.draftTree) !== JSON.stringify(page.publishedTree),
40
+ publishedAt: page.publishedAt,
41
+ updatedAt: page.updatedAt,
42
+ updatedBy: page.updatedBy,
43
+ });
44
+ export const ListPages = query('pages.list', {
45
+ description: 'A page of pages, most recently changed first',
46
+ input: {
47
+ status: enumOf('draft', 'published', 'archived').optional(),
48
+ search: string().optional(),
49
+ page: number().integer().optional(),
50
+ perPage: number().integer().optional(),
51
+ },
52
+ output: {
53
+ data: array(object({
54
+ id: uuid(),
55
+ slug: string(),
56
+ title: string(),
57
+ status: enumOf('draft', 'published', 'archived'),
58
+ locale: string(),
59
+ translationOf: uuid().nullable(),
60
+ version: number(),
61
+ publishedAt: timestamp().nullable(),
62
+ updatedAt: timestamp(),
63
+ })),
64
+ total: number(),
65
+ page: number(),
66
+ perPage: number(),
67
+ lastPage: number(),
68
+ },
69
+ handle: async ({ status, search, page, perPage }) => {
70
+ let found = Page.orderBy('updatedAt', 'desc');
71
+ if (status !== undefined)
72
+ found = found.where('status', status);
73
+ if (search !== undefined && search !== '')
74
+ found = found.whereLike('title', `%${search}%`);
75
+ const listed = await found.paginate(page ?? 1, Math.min(perPage ?? 20, 100));
76
+ return {
77
+ ...listed,
78
+ data: listed.data.map((item) => ({
79
+ id: item.id,
80
+ slug: item.slug,
81
+ title: item.title,
82
+ status: item.status,
83
+ locale: item.locale,
84
+ translationOf: item.translationOf,
85
+ version: item.version,
86
+ publishedAt: item.publishedAt,
87
+ updatedAt: item.updatedAt,
88
+ })),
89
+ };
90
+ },
91
+ });
92
+ export const GetPage = query('pages.get', {
93
+ description: 'One page, with the draft tree or the published one',
94
+ input: {
95
+ id: uuid().optional(),
96
+ slug: string().optional(),
97
+ mode: enumOf('draft', 'published').optional(),
98
+ },
99
+ output: {
100
+ id: uuid(),
101
+ slug: string(),
102
+ title: string(),
103
+ status: enumOf('draft', 'published', 'archived'),
104
+ locale: string(),
105
+ translationOf: uuid().nullable(),
106
+ mode: enumOf('draft', 'published'),
107
+ tree: blockTree(),
108
+ meta: json(),
109
+ version: number(),
110
+ hasUnpublishedChanges: boolean(),
111
+ publishedAt: timestamp().nullable(),
112
+ updatedAt: timestamp(),
113
+ updatedBy: uuid().nullable(),
114
+ },
115
+ handle: async ({ id, slug, mode }) => {
116
+ const found = id !== undefined
117
+ ? await Page.find(id)
118
+ : slug === undefined
119
+ ? null
120
+ : await Page.where('slug', slug).first();
121
+ if (found === null)
122
+ throw new NotFoundError('page', id ?? slug ?? '');
123
+ /**
124
+ * A translation nobody has published yet is not what a visitor gets (SPEC.md §131).
125
+ *
126
+ * `pages.translate` copies the original's blocks so a translator has something to
127
+ * work on, and leaves the copy unpublished — so between making it and finishing it,
128
+ * this row exists, has no published tree, and would answer a visitor with an empty
129
+ * page. Worse than the fallback it replaced: before the translation existed, the
130
+ * reader got the original.
131
+ *
132
+ * So a request for the published page steps back to the original, which is what the
133
+ * site showed a minute earlier and what it should go on showing until somebody says
134
+ * the translation is ready.
135
+ */
136
+ if ((mode ?? 'published') === 'published' && found.publishedTree === null) {
137
+ const original = found.translationOf === null ? null : await Page.find(found.translationOf);
138
+ if (original !== null && original.publishedTree !== null) {
139
+ return project(original, 'published');
140
+ }
141
+ }
142
+ // Published by default: a visitor's renderer must never show a draft because a
143
+ // parameter was forgotten.
144
+ return project(found, mode ?? 'published');
145
+ },
146
+ });
147
+ export const ListPageTranslations = query('pages.translations', {
148
+ description: 'Which languages a page is written in, and which of them are out of date',
149
+ input: { id: uuid() },
150
+ output: {
151
+ translations: array(object({
152
+ id: uuid(),
153
+ locale: string(),
154
+ slug: string(),
155
+ status: enumOf('draft', 'published', 'archived'),
156
+ isOriginal: boolean(),
157
+ updatedAt: timestamp(),
158
+ stale: boolean().nullable(),
159
+ })),
160
+ },
161
+ handle: async ({ id }, context) => {
162
+ const named = await Page.find(id);
163
+ if (named === null)
164
+ throw new NotFoundError('page', id);
165
+ // The page itself decides who may be told about its other languages, the way
166
+ // `revisions.list` asks about the entity it is the history of (ADR-0015).
167
+ await context.authorize('pages', 'read', named.toJSON());
168
+ const entry = named.translationOf ?? named.id;
169
+ const every = await Page.allLocales().get();
170
+ const rows = every.filter((one) => (one.translationOf ?? one.id) === entry);
171
+ const original = rows.find((one) => one.translationOf === null);
172
+ return {
173
+ translations: rows.map((one) => ({
174
+ id: one.id,
175
+ locale: one.locale,
176
+ slug: one.slug,
177
+ status: one.status,
178
+ isOriginal: one.translationOf === null,
179
+ updatedAt: one.updatedAt,
180
+ /**
181
+ * Written before the original last changed.
182
+ *
183
+ * A page stamps `updatedAt` on every write, so unlike a resource this is always
184
+ * answerable — and it is the question a translator's screen is made of: which of
185
+ * these has the original moved on from.
186
+ */
187
+ stale: one.translationOf === null || original === undefined
188
+ ? null
189
+ : new Date(one.updatedAt).getTime() < new Date(original.updatedAt).getTime(),
190
+ })),
191
+ };
192
+ },
193
+ });
194
+ export const pageQueries = [ListPages, GetPage, ListPageTranslations];
195
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"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"}
package/dist/tree.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { type BlockDesignPatch, type BlockTree, type Issue } from '@assemora/schema';
2
+ export type Placement = {
3
+ /** The block to nest under. Omitted means the top level. */
4
+ readonly parentId?: string;
5
+ /** Where among the siblings. Appended when omitted. */
6
+ readonly index?: number;
7
+ };
8
+ export type AddBlock = Placement & {
9
+ readonly type: string;
10
+ readonly props?: Readonly<Record<string, unknown>>;
11
+ };
12
+ export declare const addBlock: (tree: BlockTree, request: AddBlock) => {
13
+ tree: BlockTree;
14
+ id: string;
15
+ };
16
+ export declare const updateBlockProps: (tree: BlockTree, id: string, props: Readonly<Record<string, unknown>>) => BlockTree;
17
+ /**
18
+ * Sets the universal design controls of SPEC.md §61.
19
+ *
20
+ * They are merged, not replaced: a properties panel sends the one control that
21
+ * changed, not the whole set. `null` for a control clears it, because "no spacing"
22
+ * and "the theme decides" are different answers.
23
+ */
24
+ export declare const setBlockDesign: (tree: BlockTree, id: string, design: BlockDesignPatch) => BlockTree;
25
+ export declare const setBlockHidden: (tree: BlockTree, id: string, hidden: boolean) => BlockTree;
26
+ export declare const removeBlock: (tree: BlockTree, id: string) => BlockTree;
27
+ export declare const moveBlock: (tree: BlockTree, id: string, placement: Placement) => BlockTree;
28
+ export declare const duplicateBlock: (tree: BlockTree, id: string) => {
29
+ tree: BlockTree;
30
+ id: string;
31
+ };
32
+ /**
33
+ * Every block that is not yet fit to be seen (SPEC.md §55, §60).
34
+ *
35
+ * A draft may hold an unfinished block; a published page may not. This is what
36
+ * publishing checks, and it names the block so an editor can go and fix it.
37
+ */
38
+ export declare const unfinishedBlocks: (tree: BlockTree) => Issue[];
39
+ /** The id of the block that holds this one, or `null` at the top level. */
40
+ export declare const parentOf: (tree: BlockTree, id: string) => string | null;
41
+ /**
42
+ * Where a block sits: whose child it is, and where among that parent's children.
43
+ *
44
+ * The two travel together. A placement that carries the parent and forgets the index
45
+ * is not "beside" anything — it is the bottom of the list, which is the one place an
46
+ * editor never means.
47
+ */
48
+ export declare const positionOf: (tree: BlockTree, id: string) => {
49
+ readonly parentId: string | null;
50
+ readonly index: number;
51
+ };
52
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"AAaA,OAAO,EAEL,KAAK,gBAAgB,EAErB,KAAK,SAAS,EAGd,KAAK,KAAK,EACX,MAAM,kBAAkB,CAAA;AAIzB,MAAM,MAAM,SAAS,GAAG;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAiFD,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACnD,CAAA;AAED,eAAO,MAAM,QAAQ,SAAU,SAAS,WAAW,QAAQ,KAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAkB1F,CAAA;AAED,eAAO,MAAM,gBAAgB,SACrB,SAAS,MACX,MAAM,SACH,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KACvC,SAYF,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,SACnB,SAAS,MACX,MAAM,UACF,gBAAgB,KACvB,SAoBF,CAAA;AAED,eAAO,MAAM,cAAc,SAAU,SAAS,MAAM,MAAM,UAAU,OAAO,KAAG,SAM7E,CAAA;AAED,eAAO,MAAM,WAAW,SAAU,SAAS,MAAM,MAAM,KAAG,SAIzD,CAAA;AAED,eAAO,MAAM,SAAS,SAAU,SAAS,MAAM,MAAM,aAAa,SAAS,KAAG,SAU7E,CAAA;AASD,eAAO,MAAM,cAAc,SAAU,SAAS,MAAM,MAAM,KAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAoBzF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,SAAU,SAAS,KAAG,KAAK,EA0BvD,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,QAAQ,SAAU,SAAS,MAAM,MAAM,KAAG,MAAM,GAAG,IAiB/D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,SACf,SAAS,MACX,MAAM,KACT;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAK5D,CAAA"}