@blocksdiy/react-common 1.70.1 → 1.71.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 (57) hide show
  1. package/dist/comments/anchor.d.ts +74 -0
  2. package/dist/comments/anchor.d.ts.map +1 -0
  3. package/dist/comments/anchor.js +279 -0
  4. package/dist/comments/api.d.ts +38 -0
  5. package/dist/comments/api.d.ts.map +1 -0
  6. package/dist/comments/api.js +62 -0
  7. package/dist/comments/comment-agents.d.ts +28 -0
  8. package/dist/comments/comment-agents.d.ts.map +1 -0
  9. package/dist/comments/comment-agents.js +32 -0
  10. package/dist/comments/comment-mode.d.ts +97 -0
  11. package/dist/comments/comment-mode.d.ts.map +1 -0
  12. package/dist/comments/comment-mode.js +159 -0
  13. package/dist/comments/composer.d.ts +133 -0
  14. package/dist/comments/composer.d.ts.map +1 -0
  15. package/dist/comments/composer.js +273 -0
  16. package/dist/comments/index.d.ts +13 -0
  17. package/dist/comments/index.d.ts.map +1 -0
  18. package/dist/comments/index.js +17 -0
  19. package/dist/comments/layout.d.ts +25 -0
  20. package/dist/comments/layout.d.ts.map +1 -0
  21. package/dist/comments/layout.js +83 -0
  22. package/dist/comments/mentions.d.ts +32 -0
  23. package/dist/comments/mentions.d.ts.map +1 -0
  24. package/dist/comments/mentions.js +30 -0
  25. package/dist/comments/page-pick.d.ts +76 -0
  26. package/dist/comments/page-pick.d.ts.map +1 -0
  27. package/dist/comments/page-pick.js +267 -0
  28. package/dist/comments/pick-message.d.ts +11 -0
  29. package/dist/comments/pick-message.d.ts.map +1 -0
  30. package/dist/comments/pick-message.js +33 -0
  31. package/dist/comments/pins.d.ts +72 -0
  32. package/dist/comments/pins.d.ts.map +1 -0
  33. package/dist/comments/pins.js +125 -0
  34. package/dist/comments/text-pick.d.ts +59 -0
  35. package/dist/comments/text-pick.d.ts.map +1 -0
  36. package/dist/comments/text-pick.js +165 -0
  37. package/dist/comments/threads-store.d.ts +119 -0
  38. package/dist/comments/threads-store.d.ts.map +1 -0
  39. package/dist/comments/threads-store.js +286 -0
  40. package/dist/comments/types.d.ts +232 -0
  41. package/dist/comments/types.d.ts.map +1 -0
  42. package/dist/comments/types.js +109 -0
  43. package/dist/hooks/agent-conversation-context.d.ts +12 -1
  44. package/dist/hooks/agent-conversation-context.d.ts.map +1 -1
  45. package/dist/hooks/agent-conversation-context.js +12 -5
  46. package/dist/hooks/agent-thread-live-update.d.ts +5 -2
  47. package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
  48. package/dist/hooks/agent-thread-live-update.js +14 -0
  49. package/dist/hooks/use-agent-conversation.d.ts +8 -0
  50. package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
  51. package/dist/hooks/use-agent-conversation.js +10 -0
  52. package/dist/hooks/use-agent-threads.d.ts +1 -0
  53. package/dist/hooks/use-agent-threads.d.ts.map +1 -1
  54. package/dist/hooks/use-agent-threads.js +4 -1
  55. package/dist/iframe-communication/types.d.ts +24 -0
  56. package/dist/iframe-communication/types.d.ts.map +1 -1
  57. package/package.json +6 -3
@@ -0,0 +1,74 @@
1
+ import { type CommentAnchor, type CommentAnchorItem } from "./types.js";
2
+ /**
3
+ * Where a comment (or a pick from the chat) points, and finding that place again on the page as it
4
+ * is now. An anchor keeps several signals — the row, the source location, its position among the
5
+ * same source's elements, a DOM path and the text itself — because none of them alone survives an
6
+ * edit to the app or a re-sorted list.
7
+ */
8
+ /** An app marks the element that shows one row with `data-blocks-item="<table>:<rowId>"`. */
9
+ export declare const ITEM_ATTRIBUTE = "data-blocks-item";
10
+ /** What `fieldAnchorProps` (blocks-client-sdk) tags a column's element with. */
11
+ export declare const FIELD_ATTRIBUTE = "data-blocks-field";
12
+ /**
13
+ * Inside an element with this attribute nothing is picked, and the picker blocks no click: the
14
+ * comments' own UI, the agent chat (the registry's `pagePickIgnoreProps` writes it).
15
+ */
16
+ export declare const PICK_IGNORE_ATTRIBUTE = "data-blocks-pick-ignore";
17
+ /** An element's text as it reads, whitespace collapsed. */
18
+ export declare const textOf: (element: HTMLElement) => string;
19
+ /**
20
+ * `"<table>:<rowId>"` as a row. Split on the last colon: the table part is what the app's code
21
+ * calls the table (generated code uses its title), and row ids are digits.
22
+ */
23
+ export declare const parseItemAttribute: (value: string | null | undefined) => CommentAnchorItem | undefined;
24
+ /** True when `node` is inside UI the picker leaves alone. */
25
+ export declare const isPickIgnored: (node: Node | null) => boolean;
26
+ /**
27
+ * The element a click or selection is about: the nearest one the app's own markup tagged, else the
28
+ * element itself. Null inside ignored UI.
29
+ */
30
+ export declare const pickTarget: (node: Node | null) => HTMLElement | null;
31
+ /**
32
+ * Describes where a comment points, from the element picked and, when text was highlighted inside
33
+ * it, the selection. `clientX`/`clientY` (the click) keep where in the element it was dropped.
34
+ */
35
+ export declare const captureAnchor: (options: {
36
+ routePath: string;
37
+ element: HTMLElement;
38
+ range?: Range | null;
39
+ clientX?: number;
40
+ clientY?: number;
41
+ }) => CommentAnchor;
42
+ export type AnchorState = "anchored" | "outdated" | "missing";
43
+ export interface ResolvedAnchor {
44
+ state: AnchorState;
45
+ element: HTMLElement | null;
46
+ /** The highlighted text, found again: where a quote's pin and highlight go. */
47
+ range: Range | null;
48
+ /** What the anchored text says now, when it changed. */
49
+ currentText?: string;
50
+ }
51
+ /** The row's element an anchor names, or null when it has none or the row is not on the page. */
52
+ export declare const findAnchorItemElement: (anchor: CommentAnchor, root?: ParentNode) => Element | null;
53
+ /**
54
+ * Finds an anchor on the page as it is now. `outdated`: the element is there but the text the
55
+ * comment was about changed, so the thread shows what it said.
56
+ */
57
+ export declare const resolveAnchor: (anchor: CommentAnchor) => ResolvedAnchor;
58
+ /** Whether `element` still says what the anchor was about. */
59
+ export declare const checkText: (anchor: CommentAnchor, element: HTMLElement) => ResolvedAnchor;
60
+ /** The words the thread is about: the highlighted text, else the element's text when short. */
61
+ export declare const anchorQuote: (anchor: CommentAnchor) => string | undefined;
62
+ /**
63
+ * A component's name as the app's code gives it (`TaskCard`): a `devName` that starts with a
64
+ * capital. A plain element's `devName` is its tag (`div`), jargon to the app's people: none then.
65
+ */
66
+ export declare const componentNameOf: (devName: string | null | undefined) => string | undefined;
67
+ /** The component an anchor's element is (`componentNameOf`), when it has a name. */
68
+ export declare const anchorComponentName: (anchor: CommentAnchor) => string | undefined;
69
+ /**
70
+ * A short name for what an anchor points at, never a tag: its quote, else its element's text,
71
+ * clipped, else its component's name, else its page.
72
+ */
73
+ export declare const anchorLabel: (anchor: CommentAnchor) => string;
74
+ //# sourceMappingURL=anchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../src/comments/anchor.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAElB,KAAK,iBAAiB,EACvB,MAAM,SAAS,CAAC;AAEjB;;;;;GAKG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,cAAc,qBAAqB,CAAC;AACjD,gFAAgF;AAChF,eAAO,MAAM,eAAe,sBAAsB,CAAC;AAEnD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAQ/D,2DAA2D;AAC3D,eAAO,MAAM,MAAM,GAAI,SAAS,WAAW,WAA8D,CAAC;AAO1G;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,iBAAiB,GAAG,SAMzF,CAAC;AAyBF,6DAA6D;AAC7D,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,GAAG,IAAI,KAAG,OAGjD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,IAAI,GAAG,IAAI,KAAG,WAAW,GAAG,IAS5D,CAAC;AAmCF;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,KAAG,aAgDH,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,+EAA+E;IAC/E,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAsED,iGAAiG;AACjG,eAAO,MAAM,qBAAqB,GAAI,QAAQ,aAAa,EAAE,OAAM,UAAqB,KAAG,OAAO,GAAG,IACjC,CAAC;AAErE;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,aAAa,KAAG,cAcrD,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,SAAS,GAAI,QAAQ,aAAa,EAAE,SAAS,WAAW,KAAG,cAavE,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,WAAW,GAAI,QAAQ,aAAa,KAAG,MAAM,GAAG,SAM5D,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,MAAM,GAAG,SACrB,CAAC;AAE1D,oFAAoF;AACpF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,aAAa,KAAG,MAAM,GAAG,SAC3B,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,aAAa,KAAG,MAMnD,CAAC"}
@@ -0,0 +1,279 @@
1
+ import { COMMENT_ANCHOR_CONTEXT_MAX_LENGTH, COMMENT_ANCHOR_FIELD_MAX_LENGTH, COMMENT_ANCHOR_TEXT_MAX_LENGTH, } from "./types.js";
2
+ /**
3
+ * Where a comment (or a pick from the chat) points, and finding that place again on the page as it
4
+ * is now. An anchor keeps several signals — the row, the source location, its position among the
5
+ * same source's elements, a DOM path and the text itself — because none of them alone survives an
6
+ * edit to the app or a re-sorted list.
7
+ */
8
+ /** An app marks the element that shows one row with `data-blocks-item="<table>:<rowId>"`. */
9
+ export const ITEM_ATTRIBUTE = "data-blocks-item";
10
+ /** What `fieldAnchorProps` (blocks-client-sdk) tags a column's element with. */
11
+ export const FIELD_ATTRIBUTE = "data-blocks-field";
12
+ /**
13
+ * Inside an element with this attribute nothing is picked, and the picker blocks no click: the
14
+ * comments' own UI, the agent chat (the registry's `pagePickIgnoreProps` writes it).
15
+ */
16
+ export const PICK_IGNORE_ATTRIBUTE = "data-blocks-pick-ignore";
17
+ /** An element comment shows "was …" when its text changed, if the text was this short. */
18
+ const SHORT_TEXT_MAX = 200;
19
+ const DOM_PATH_MAX_DEPTH = 12;
20
+ const normalize = (text) => text.replace(/\s+/g, " ").trim();
21
+ /** An element's text as it reads, whitespace collapsed. */
22
+ export const textOf = (element) => normalize(element.innerText ?? element.textContent ?? "");
23
+ const escape = (value) => typeof CSS !== "undefined" && typeof CSS.escape === "function"
24
+ ? CSS.escape(value)
25
+ : value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26
+ /**
27
+ * `"<table>:<rowId>"` as a row. Split on the last colon: the table part is what the app's code
28
+ * calls the table (generated code uses its title), and row ids are digits.
29
+ */
30
+ export const parseItemAttribute = (value) => {
31
+ const separator = value?.lastIndexOf(":") ?? -1;
32
+ if (!value || separator <= 0 || separator === value.length - 1) {
33
+ return undefined;
34
+ }
35
+ return { tableBlockId: value.slice(0, separator), itemId: value.slice(separator + 1) };
36
+ };
37
+ const itemSelector = (item) => `[${ITEM_ATTRIBUTE}="${escape(`${item.tableBlockId}:${item.itemId}`)}"]`;
38
+ const sameSourceSelector = (devPath, devName) => `[data-dev-path="${escape(devPath)}"][data-dev-name="${escape(devName)}"]`;
39
+ /** `:scope > div:nth-of-type(2) > span:nth-of-type(1)` from `root` down to `element`. */
40
+ const domPathFrom = (root, element) => {
41
+ const parts = [];
42
+ let current = element;
43
+ while (current && current !== root && parts.length < DOM_PATH_MAX_DEPTH) {
44
+ const parent = current.parentElement;
45
+ if (!parent) {
46
+ return undefined;
47
+ }
48
+ const tag = current.tagName;
49
+ const sameTag = Array.from(parent.children).filter((child) => child.tagName === tag);
50
+ parts.unshift(`${tag.toLowerCase()}:nth-of-type(${sameTag.indexOf(current) + 1})`);
51
+ current = parent;
52
+ }
53
+ return current === root && parts.length > 0 ? `:scope > ${parts.join(" > ")}` : undefined;
54
+ };
55
+ /** True when `node` is inside UI the picker leaves alone. */
56
+ export const isPickIgnored = (node) => {
57
+ const element = node instanceof Element ? node : (node?.parentElement ?? null);
58
+ return !!element?.closest(`[${PICK_IGNORE_ATTRIBUTE}]`);
59
+ };
60
+ /**
61
+ * The element a click or selection is about: the nearest one the app's own markup tagged, else the
62
+ * element itself. Null inside ignored UI.
63
+ */
64
+ export const pickTarget = (node) => {
65
+ if (!node || isPickIgnored(node)) {
66
+ return null;
67
+ }
68
+ const element = node instanceof HTMLElement ? node : node.parentElement;
69
+ if (!element) {
70
+ return null;
71
+ }
72
+ return element.closest("[data-dev-id]") ?? element;
73
+ };
74
+ /** The text of a selection inside `element`, with context on both sides; undefined when none. */
75
+ const selectedText = (element, range) => {
76
+ if (!range || range.collapsed || !element.contains(range.commonAncestorContainer)) {
77
+ return undefined;
78
+ }
79
+ const selected = range.toString();
80
+ const trimmed = selected.trim();
81
+ if (!trimmed) {
82
+ return undefined;
83
+ }
84
+ // A double or triple click selects the space or line break around the words too: that whitespace
85
+ // goes to the context, so prefix + exact + suffix is still the element's text.
86
+ const leading = selected.slice(0, selected.length - selected.trimStart().length);
87
+ const exact = trimmed.slice(0, COMMENT_ANCHOR_TEXT_MAX_LENGTH);
88
+ const trailing = trimmed.slice(exact.length) + selected.slice(selected.trimEnd().length);
89
+ const before = document.createRange();
90
+ before.selectNodeContents(element);
91
+ before.setEnd(range.startContainer, range.startOffset);
92
+ const after = document.createRange();
93
+ after.selectNodeContents(element);
94
+ after.setStart(range.endContainer, range.endOffset);
95
+ return {
96
+ exact,
97
+ prefix: (before.toString() + leading).slice(-COMMENT_ANCHOR_CONTEXT_MAX_LENGTH),
98
+ suffix: (trailing + after.toString()).slice(0, COMMENT_ANCHOR_CONTEXT_MAX_LENGTH),
99
+ };
100
+ };
101
+ const clamp01 = (value) => Math.min(1, Math.max(0, Number.isFinite(value) ? value : 0));
102
+ const stripUndefined = (value) => Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
103
+ /**
104
+ * Describes where a comment points, from the element picked and, when text was highlighted inside
105
+ * it, the selection. `clientX`/`clientY` (the click) keep where in the element it was dropped.
106
+ */
107
+ export const captureAnchor = (options) => {
108
+ const { element } = options;
109
+ const itemElement = element.closest(`[${ITEM_ATTRIBUTE}]`);
110
+ const item = parseItemAttribute(itemElement?.getAttribute(ITEM_ATTRIBUTE));
111
+ const root = (item && itemElement) || document.body;
112
+ const devPath = element.getAttribute("data-dev-path") ?? undefined;
113
+ const devName = element.getAttribute("data-dev-name") ?? undefined;
114
+ const anchorElement = {
115
+ devId: element.getAttribute("data-dev-id") ?? undefined,
116
+ devPath,
117
+ devName,
118
+ tagName: element.tagName,
119
+ domPath: domPathFrom(root, element),
120
+ };
121
+ if (devPath && devName) {
122
+ const ordinal = Array.from(root.querySelectorAll(sameSourceSelector(devPath, devName))).indexOf(element);
123
+ if (ordinal >= 0) {
124
+ anchorElement.ordinal = ordinal;
125
+ }
126
+ }
127
+ const text = textOf(element);
128
+ if (text) {
129
+ anchorElement.textSnapshot = text.slice(0, COMMENT_ANCHOR_TEXT_MAX_LENGTH);
130
+ }
131
+ const anchor = { v: 1, routePath: options.routePath, element: stripUndefined(anchorElement) };
132
+ if (item) {
133
+ // The column the element shows: the nearest tag at or above it, inside the row's element.
134
+ const fieldElement = element.closest(`[${FIELD_ATTRIBUTE}]`);
135
+ const field = fieldElement && itemElement?.contains(fieldElement) ? fieldElement.getAttribute(FIELD_ATTRIBUTE) : null;
136
+ anchor.item = field ? { ...item, field: field.slice(0, COMMENT_ANCHOR_FIELD_MAX_LENGTH) } : item;
137
+ }
138
+ const quote = selectedText(element, options.range);
139
+ if (quote) {
140
+ anchor.text = quote;
141
+ }
142
+ if (options.clientX !== undefined && options.clientY !== undefined) {
143
+ const rect = element.getBoundingClientRect();
144
+ if (rect.width > 0 && rect.height > 0) {
145
+ anchor.point = {
146
+ x: clamp01((options.clientX - rect.left) / rect.width),
147
+ y: clamp01((options.clientY - rect.top) / rect.height),
148
+ };
149
+ }
150
+ }
151
+ return anchor;
152
+ };
153
+ const MISSING = { state: "missing", element: null, range: null };
154
+ const findElement = (anchor, root) => {
155
+ const element = anchor.element;
156
+ if (!element) {
157
+ return null;
158
+ }
159
+ if (element.devPath && element.devName && element.ordinal !== undefined) {
160
+ // No ordinal: the element was the row's own element, which its descendants never match.
161
+ const byOrdinal = root.querySelectorAll(sameSourceSelector(element.devPath, element.devName))[element.ordinal];
162
+ if (byOrdinal) {
163
+ return byOrdinal;
164
+ }
165
+ }
166
+ if (element.devId) {
167
+ const byId = root.querySelector(`[data-dev-id="${escape(element.devId)}"]`);
168
+ if (byId) {
169
+ return byId;
170
+ }
171
+ }
172
+ if (element.domPath) {
173
+ try {
174
+ const byPath = root.querySelector(element.domPath);
175
+ if (byPath && (!element.tagName || byPath.tagName === element.tagName)) {
176
+ return byPath;
177
+ }
178
+ }
179
+ catch {
180
+ // A path the current DOM can't parse: no match.
181
+ }
182
+ }
183
+ return null;
184
+ };
185
+ /** A Range over the first occurrence of `exact` in `element`'s text, preferring one in context. */
186
+ const findTextRange = (element, text) => {
187
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
188
+ const nodes = [];
189
+ let content = "";
190
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
191
+ nodes.push(node);
192
+ content += node.data;
193
+ }
194
+ const inContext = content.indexOf(text.prefix + text.exact + text.suffix);
195
+ const start = inContext >= 0 ? inContext + text.prefix.length : content.indexOf(text.exact);
196
+ if (start < 0) {
197
+ return null;
198
+ }
199
+ const end = start + text.exact.length;
200
+ const range = document.createRange();
201
+ let offset = 0;
202
+ let startSet = false;
203
+ for (const node of nodes) {
204
+ const next = offset + node.data.length;
205
+ if (!startSet && start < next) {
206
+ range.setStart(node, start - offset);
207
+ startSet = true;
208
+ }
209
+ if (startSet && end <= next) {
210
+ range.setEnd(node, end - offset);
211
+ return range;
212
+ }
213
+ offset = next;
214
+ }
215
+ return null;
216
+ };
217
+ /** The row's element an anchor names, or null when it has none or the row is not on the page. */
218
+ export const findAnchorItemElement = (anchor, root = document) => anchor.item ? root.querySelector(itemSelector(anchor.item)) : null;
219
+ /**
220
+ * Finds an anchor on the page as it is now. `outdated`: the element is there but the text the
221
+ * comment was about changed, so the thread shows what it said.
222
+ */
223
+ export const resolveAnchor = (anchor) => {
224
+ let root = document.body;
225
+ if (anchor.item) {
226
+ const itemElement = findAnchorItemElement(anchor);
227
+ if (!itemElement) {
228
+ return MISSING;
229
+ }
230
+ root = itemElement;
231
+ }
232
+ const element = findElement(anchor, root) ?? (anchor.item ? root : null);
233
+ if (!element) {
234
+ return MISSING;
235
+ }
236
+ return checkText(anchor, element);
237
+ };
238
+ /** Whether `element` still says what the anchor was about. */
239
+ export const checkText = (anchor, element) => {
240
+ const current = textOf(element);
241
+ if (anchor.text) {
242
+ const range = findTextRange(element, anchor.text);
243
+ return range
244
+ ? { state: "anchored", element, range }
245
+ : { state: "outdated", element, range: null, currentText: current.slice(0, SHORT_TEXT_MAX) };
246
+ }
247
+ const snapshot = anchor.element?.textSnapshot;
248
+ if (snapshot && snapshot.length <= SHORT_TEXT_MAX && current !== snapshot) {
249
+ return { state: "outdated", element, range: null, currentText: current.slice(0, SHORT_TEXT_MAX) };
250
+ }
251
+ return { state: "anchored", element, range: null };
252
+ };
253
+ /** The words the thread is about: the highlighted text, else the element's text when short. */
254
+ export const anchorQuote = (anchor) => {
255
+ if (anchor.text) {
256
+ return anchor.text.exact;
257
+ }
258
+ const snapshot = anchor.element?.textSnapshot;
259
+ return snapshot && snapshot.length <= SHORT_TEXT_MAX ? snapshot : undefined;
260
+ };
261
+ const LABEL_MAX = 40;
262
+ /**
263
+ * A component's name as the app's code gives it (`TaskCard`): a `devName` that starts with a
264
+ * capital. A plain element's `devName` is its tag (`div`), jargon to the app's people: none then.
265
+ */
266
+ export const componentNameOf = (devName) => devName && /^[A-Z]/.test(devName) ? devName : undefined;
267
+ /** The component an anchor's element is (`componentNameOf`), when it has a name. */
268
+ export const anchorComponentName = (anchor) => componentNameOf(anchor.element?.devName);
269
+ /**
270
+ * A short name for what an anchor points at, never a tag: its quote, else its element's text,
271
+ * clipped, else its component's name, else its page.
272
+ */
273
+ export const anchorLabel = (anchor) => {
274
+ const text = anchorQuote(anchor) ?? anchor.element?.textSnapshot;
275
+ if (text) {
276
+ return text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}…` : text;
277
+ }
278
+ return anchorComponentName(anchor) ?? anchor.routePath;
279
+ };
@@ -0,0 +1,38 @@
1
+ import type { CommentAnchor, CommentThreadDto, CommentThreadsPageDto, MentionableUsersDto } from "./types.js";
2
+ /**
3
+ * The in-app comments API, through the platform's proxy (`/data-api/apps/:appId/comments/**`), as
4
+ * the signed-in app user. The token is read at call time, so a refreshed token needs no new client.
5
+ */
6
+ /** A non-2xx answer, with its status: a 404 means "not there, or not yours to see". */
7
+ export declare class CommentsRequestError extends Error {
8
+ readonly status: number;
9
+ constructor(status: number);
10
+ }
11
+ export interface CommentsApi {
12
+ /** One page of a page's threads, newest activity first. */
13
+ listThreads(routePath: string, options?: {
14
+ cursor?: string;
15
+ }): Promise<CommentThreadsPageDto>;
16
+ getThread(threadId: number): Promise<CommentThreadDto>;
17
+ createThread(request: {
18
+ anchor: CommentAnchor;
19
+ body: string;
20
+ }): Promise<CommentThreadDto>;
21
+ addComment(threadId: number, body: string): Promise<CommentThreadDto>;
22
+ updateComment(commentId: number, body: string): Promise<CommentThreadDto>;
23
+ /** The thread without the comment; null when it was the thread's last: the thread is gone. */
24
+ deleteComment(commentId: number): Promise<CommentThreadDto | null>;
25
+ resolveThread(threadId: number): Promise<CommentThreadDto>;
26
+ reopenThread(threadId: number): Promise<CommentThreadDto>;
27
+ mentionableUsers(): Promise<MentionableUsersDto>;
28
+ }
29
+ export interface CreateCommentsApiOptions {
30
+ appId: string;
31
+ /** The app user's token, read on every call. */
32
+ getToken: () => string | undefined;
33
+ /** Defaults to the platform host (`getApiHost`). */
34
+ baseUrl?: string;
35
+ fetch?: typeof fetch;
36
+ }
37
+ export declare const createCommentsApi: ({ appId, getToken, baseUrl, fetch: fetchImpl, }: CreateCommentsApiOptions) => CommentsApi;
38
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/comments/api.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE3G;;;GAGG;AAEH,uFAAuF;AACvF,qBAAa,oBAAqB,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAIpC;AAED,MAAM,WAAW,WAAW;IAC1B,2DAA2D;IAC3D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC9F,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvD,YAAY,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1F,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtE,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E,8FAA8F;IAC9F,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC3D,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1D,gBAAgB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACnC,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,eAAO,MAAM,iBAAiB,GAAI,iDAK/B,wBAAwB,KAAG,WAiD7B,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { getBuilderContextHeaders } from "@blocksdiy/blocks-client-api";
2
+ import { getApiHost } from "@blocksdiy/blocks-client-api/envService";
3
+ import { websocketsService } from "@blocksdiy/blocks-client-api/websocketService";
4
+ /**
5
+ * The in-app comments API, through the platform's proxy (`/data-api/apps/:appId/comments/**`), as
6
+ * the signed-in app user. The token is read at call time, so a refreshed token needs no new client.
7
+ */
8
+ /** A non-2xx answer, with its status: a 404 means "not there, or not yours to see". */
9
+ export class CommentsRequestError extends Error {
10
+ status;
11
+ constructor(status) {
12
+ super(`Comments request failed with status ${status}`);
13
+ this.status = status;
14
+ this.name = "CommentsRequestError";
15
+ }
16
+ }
17
+ export const createCommentsApi = ({ appId, getToken, baseUrl, fetch: fetchImpl = (...args) => fetch(...args), }) => {
18
+ const request = async (path, init = {}) => {
19
+ const token = getToken();
20
+ const socketId = websocketsService.getSocketId();
21
+ const response = await fetchImpl(`${baseUrl ?? getApiHost()}/data-api/apps/${encodeURIComponent(appId)}/comments/${path}`, {
22
+ method: init.method ?? "GET",
23
+ headers: {
24
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
25
+ ...(init.body === undefined ? {} : { "Content-Type": "application/json" }),
26
+ // A person's change pings every tab but theirs: their write returned it. An agent's
27
+ // events (picking a mention up, its progress, its answer) reach every tab.
28
+ ...(socketId ? { "x-socket-id": socketId } : {}),
29
+ "x-app-id": appId,
30
+ ...getBuilderContextHeaders(appId),
31
+ },
32
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
33
+ });
34
+ if (!response.ok) {
35
+ throw new CommentsRequestError(response.status);
36
+ }
37
+ // No content: what was asked for is gone (a thread whose last comment was deleted).
38
+ if (response.status === 204) {
39
+ return null;
40
+ }
41
+ return (await response.json());
42
+ };
43
+ const post = (path, body = {}) => request(path, { method: "POST", body });
44
+ return {
45
+ listThreads: (routePath, options = {}) => {
46
+ const query = new URLSearchParams({ routePath });
47
+ if (options.cursor) {
48
+ query.set("cursor", options.cursor);
49
+ }
50
+ return request(`threads?${query}`);
51
+ },
52
+ getThread: (threadId) => request(`threads/${threadId}`),
53
+ createThread: (body) => post("threads", body),
54
+ addComment: (threadId, body) => post(`threads/${threadId}/comments`, { body }),
55
+ // POST only: the platform answers an app cross-origin, and its CORS allows GET and POST.
56
+ updateComment: (commentId, body) => post(`${commentId}/edit`, { body }),
57
+ deleteComment: (commentId) => post(`${commentId}/delete`),
58
+ resolveThread: (threadId) => post(`threads/${threadId}/resolve`),
59
+ reopenThread: (threadId) => post(`threads/${threadId}/reopen`),
60
+ mentionableUsers: () => request("mentionable-users"),
61
+ };
62
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The agents a comment can mention: the app's floating-chat agents (one per agent mini-app), as
3
+ * the floating chat's provider registers them while it is mounted, with the way to open one's
4
+ * conversation in its panel. The comments surface reads them; it never imports the chat itself. An
5
+ * app without the floating chat offers people only.
6
+ */
7
+ export interface CommentAgent {
8
+ /** The agent block's name, what a mention stores (`@[agent:<id>]`). */
9
+ id: string;
10
+ /** The persona's name. */
11
+ title: string;
12
+ photoUrl?: string;
13
+ }
14
+ /** Opens the floating chat on an agent's conversation (`chatThreadId`). */
15
+ export type OpenCommentAgentThread = (agentId: string, chatThreadId: string) => void;
16
+ export interface CommentAgentsSnapshot {
17
+ agents: readonly CommentAgent[];
18
+ openThread: OpenCommentAgentThread | null;
19
+ }
20
+ /** Registers the app's agents; returns the unregister. The latest registration wins. */
21
+ export declare const registerCommentAgents: (agents: readonly CommentAgent[], openThread: OpenCommentAgentThread) => (() => void);
22
+ /** The agents registered now (what `useCommentAgents` reads). */
23
+ export declare const readCommentAgents: () => CommentAgentsSnapshot;
24
+ /** The agents comments can mention, and how to open one's conversation. */
25
+ export declare const useCommentAgents: () => CommentAgentsSnapshot;
26
+ /** Registers `agents` while the calling component is mounted (the floating chat's provider). */
27
+ export declare const useRegisterCommentAgents: (agents: readonly CommentAgent[], openThread: OpenCommentAgentThread) => void;
28
+ //# sourceMappingURL=comment-agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-agents.d.ts","sourceRoot":"","sources":["../../src/comments/comment-agents.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2EAA2E;AAC3E,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;AAErF,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IAChC,UAAU,EAAE,sBAAsB,GAAG,IAAI,CAAC;CAC3C;AASD,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,GAChC,QAAQ,SAAS,YAAY,EAAE,EAC/B,YAAY,sBAAsB,KACjC,CAAC,MAAM,IAAI,CAUb,CAAC;AAQF,iEAAiE;AACjE,eAAO,MAAM,iBAAiB,QAAO,qBAAgC,CAAC;AAEtE,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,QAAO,qBACmC,CAAC;AAExE,gGAAgG;AAChG,eAAO,MAAM,wBAAwB,GAAI,QAAQ,SAAS,YAAY,EAAE,EAAE,YAAY,sBAAsB,KAAG,IAE9G,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { useEffect, useSyncExternalStore } from "react";
2
+ const NONE = { agents: [], openThread: null };
3
+ let current = NONE;
4
+ let registration = 0;
5
+ const listeners = new Set();
6
+ const emit = () => listeners.forEach((listener) => listener());
7
+ /** Registers the app's agents; returns the unregister. The latest registration wins. */
8
+ export const registerCommentAgents = (agents, openThread) => {
9
+ const id = ++registration;
10
+ current = { agents, openThread };
11
+ emit();
12
+ return () => {
13
+ if (registration === id) {
14
+ current = NONE;
15
+ emit();
16
+ }
17
+ };
18
+ };
19
+ const subscribe = (listener) => {
20
+ listeners.add(listener);
21
+ return () => {
22
+ listeners.delete(listener);
23
+ };
24
+ };
25
+ /** The agents registered now (what `useCommentAgents` reads). */
26
+ export const readCommentAgents = () => current;
27
+ /** The agents comments can mention, and how to open one's conversation. */
28
+ export const useCommentAgents = () => useSyncExternalStore(subscribe, readCommentAgents, readCommentAgents);
29
+ /** Registers `agents` while the calling component is mounted (the floating chat's provider). */
30
+ export const useRegisterCommentAgents = (agents, openThread) => {
31
+ useEffect(() => registerCommentAgents(agents, openThread), [agents, openThread]);
32
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Comment mode: whether the app shows its comments right now. The platform turns it on and off
3
+ * (its Comment button, over the iframe bridge: app-render calls `setCommentMode`); the app's
4
+ * comments surface (the registry `comments` item) reads it and can turn it off itself (Esc), which
5
+ * the platform hears through `onCommentModeExit`. Host and app share this module: it is a UMD
6
+ * external app-render provides, so no window global is needed.
7
+ *
8
+ * Turning it on is a handshake: it resolves `true` once a surface is mounted, waiting a little for
9
+ * one (the bundle may still be loading). An app without the comments component never answers, so
10
+ * the platform learns it can't show comments and the mode stays off.
11
+ *
12
+ * The app can also open one thread itself (a comment's link in the agent chat): `openCommentThread`
13
+ * asks the host for comment mode, as its Comment button would, and the surface goes to the thread.
14
+ */
15
+ /**
16
+ * What the app's comments show, as the platform's Comment button sets it. `mentionableAgents`: the
17
+ * block names of the agents this person may mention (their roles' agent mini-apps); all of the
18
+ * app's agents when absent (the platform hasn't sent its options).
19
+ */
20
+ export interface CommentOptions {
21
+ /** Pins of resolved threads and outdated ones (their text changed) show too. */
22
+ showResolved: boolean;
23
+ mentionableAgents?: readonly string[];
24
+ }
25
+ export declare const DEFAULT_COMMENT_OPTIONS: CommentOptions;
26
+ /** A thread to open in the app: on its page, scrolled to, open. */
27
+ export interface CommentThreadOpenRequest {
28
+ threadId: number;
29
+ /** The page the thread is on (`CommentThreadSummaryDto.routePath`). */
30
+ routePath: string;
31
+ }
32
+ /** How long turning comment mode on waits for a comments surface to mount. */
33
+ export declare const COMMENT_MODE_CLAIM_TIMEOUT_MS = 5000;
34
+ export interface CommentModeTimers {
35
+ setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
36
+ clearTimeout(id: ReturnType<typeof setTimeout>): void;
37
+ }
38
+ export interface CommentModeStore {
39
+ /** The host: turn comment mode on or off. On resolves false when no surface mounted in time. */
40
+ set(isOn: boolean): Promise<boolean>;
41
+ isOn(): boolean;
42
+ subscribe(listener: () => void): () => void;
43
+ /** The app: a comments surface is mounted while the returned function hasn't been called. */
44
+ registerSurface(): () => void;
45
+ /** The app: the surface turned comment mode off itself (Esc). The host hears it. */
46
+ exit(): void;
47
+ /** The host: hears when the app turned comment mode off itself. */
48
+ onExit(listener: () => void): () => void;
49
+ /** The host: what the comments show (resolved and outdated pins, the agents to mention). */
50
+ setOptions(options: CommentOptions): void;
51
+ options(): CommentOptions;
52
+ /** The app's surface: opens the threads asked for while the returned function hasn't been called. */
53
+ handleOpenThread(handler: (request: CommentThreadOpenRequest) => void): () => void;
54
+ /** The host: hears the app ask for comment mode (to show a thread it opens). */
55
+ onRequestOn(listener: () => void): () => void;
56
+ /** Whether a thread can be opened from inside the app: a host listens and a surface opens it. */
57
+ canOpenThread(): boolean;
58
+ /** The app: asks the host for comment mode and the surface for the thread. False when it can't here. */
59
+ openThread(request: CommentThreadOpenRequest): boolean;
60
+ }
61
+ export declare const createCommentModeStore: (timers?: CommentModeTimers) => CommentModeStore;
62
+ /** The one store the host and the app share. */
63
+ export declare const commentMode: CommentModeStore;
64
+ /** The host: turn comment mode on or off (resolves false when the app has no comments surface). */
65
+ export declare const setCommentMode: (isOn: boolean) => Promise<boolean>;
66
+ /** The host: hears when the app turned comment mode off itself. Returns the unsubscribe. */
67
+ export declare const onCommentModeExit: (listener: () => void) => (() => void);
68
+ /** The host: what the app's comments show (the Comment button's options). */
69
+ export declare const setCommentOptions: (next: CommentOptions) => void;
70
+ /** The host: hears the app ask for comment mode. Returns the unsubscribe. */
71
+ export declare const onCommentModeRequest: (listener: () => void) => (() => void);
72
+ /**
73
+ * The app (the agent chat's comment message): opens a thread — its page, comment mode on, the
74
+ * thread open. False when it can't here: outside the platform, or without the comments component.
75
+ */
76
+ export declare const openCommentThread: (request: CommentThreadOpenRequest, store?: CommentModeStore) => boolean;
77
+ /** Whether `openCommentThread` can open a thread here: the link back to it shows only then. */
78
+ export declare const canOpenCommentThread: (store?: CommentModeStore) => boolean;
79
+ export interface UseCommentsSurfaceOptions {
80
+ /**
81
+ * Opens a thread `openCommentThread` asked for: navigate to its page with the thread's link
82
+ * (`?comment=<id>`), which the comments then open.
83
+ */
84
+ onOpenThread?: (request: CommentThreadOpenRequest) => void;
85
+ store?: CommentModeStore;
86
+ }
87
+ /**
88
+ * The app's comments surface: registers itself while mounted and reads the mode. `exit` turns it off
89
+ * from inside the app and tells the host. It opens the threads the app asks for (a comment's link in
90
+ * the agent chat) and reads the host's options.
91
+ */
92
+ export declare const useCommentsSurface: ({ onOpenThread, store }?: UseCommentsSurfaceOptions) => {
93
+ isCommentMode: boolean;
94
+ options: CommentOptions;
95
+ exit: () => void;
96
+ };
97
+ //# sourceMappingURL=comment-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-mode.d.ts","sourceRoot":"","sources":["../../src/comments/comment-mode.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,eAAO,MAAM,uBAAuB,EAAE,cAAwC,CAAC;AAK/E,mEAAmE;AACnE,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC5E,YAAY,CAAC,EAAE,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAAC;CACvD;AAOD,MAAM,WAAW,gBAAgB;IAC/B,gGAAgG;IAChG,GAAG,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,IAAI,OAAO,CAAC;IAChB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,6FAA6F;IAC7F,eAAe,IAAI,MAAM,IAAI,CAAC;IAC9B,oFAAoF;IACpF,IAAI,IAAI,IAAI,CAAC;IACb,mEAAmE;IACnE,MAAM,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACzC,4FAA4F;IAC5F,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,OAAO,IAAI,cAAc,CAAC;IAC1B,qGAAqG;IACrG,gBAAgB,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnF,gFAAgF;IAChF,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC9C,iGAAiG;IACjG,aAAa,IAAI,OAAO,CAAC;IACzB,wGAAwG;IACxG,UAAU,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC;CACxD;AAED,eAAO,MAAM,sBAAsB,GAAI,SAAQ,iBAAiC,KAAG,gBAqHlF,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,WAAW,EAAE,gBAA2C,CAAC;AAEtE,mGAAmG;AACnG,eAAO,MAAM,cAAc,GAAI,MAAM,OAAO,KAAG,OAAO,CAAC,OAAO,CAA0B,CAAC;AAEzF,4FAA4F;AAC5F,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAiC,CAAC;AAEtG,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,GAAI,MAAM,cAAc,KAAG,IAAoC,CAAC;AAE9F,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAsC,CAAC;AAE9G;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,wBAAwB,EAAE,QAAO,gBAA8B,KAAG,OAClF,CAAC;AAE5B,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB,GAAI,QAAO,gBAA8B,KAAG,OAAgC,CAAC;AAE9G,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,CAAC;IAC3D,KAAK,CAAC,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,0BAAuC,yBAA8B,KAAG;IACzG,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,MAAM,IAAI,CAAC;CAgBlB,CAAC"}