@carto/ps-react-ui 4.11.2 → 4.12.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 (82) hide show
  1. package/dist/chat.js +962 -733
  2. package/dist/chat.js.map +1 -1
  3. package/dist/csv-item-hH_Gt7ur.js +32 -0
  4. package/dist/csv-item-hH_Gt7ur.js.map +1 -0
  5. package/dist/png-item-9dNbB37T.js +57 -0
  6. package/dist/png-item-9dNbB37T.js.map +1 -0
  7. package/dist/table-B3ZWWhJt.js +383 -0
  8. package/dist/table-B3ZWWhJt.js.map +1 -0
  9. package/dist/types/chat/containers/chat-footer.d.ts +1 -1
  10. package/dist/types/chat/containers/styles.d.ts +79 -12
  11. package/dist/types/chat/index.d.ts +1 -1
  12. package/dist/types/chat/types.d.ts +21 -0
  13. package/dist/types/chat/use-typewriter.d.ts +5 -3
  14. package/dist/types/widgets-v2/actions/download/constants.d.ts +12 -0
  15. package/dist/types/widgets-v2/actions/download/csv-item.d.ts +38 -0
  16. package/dist/types/widgets-v2/actions/download/icons.d.ts +6 -0
  17. package/dist/types/widgets-v2/actions/download/index.d.ts +3 -1
  18. package/dist/types/widgets-v2/actions/index.d.ts +1 -1
  19. package/dist/types/widgets-v2/wrapper/style.d.ts +5 -12
  20. package/dist/widgets-v2/actions.js +40 -36
  21. package/dist/widgets-v2/actions.js.map +1 -1
  22. package/dist/widgets-v2/bar.js +77 -84
  23. package/dist/widgets-v2/bar.js.map +1 -1
  24. package/dist/widgets-v2/category.js +50 -55
  25. package/dist/widgets-v2/category.js.map +1 -1
  26. package/dist/widgets-v2/formula.js +37 -43
  27. package/dist/widgets-v2/formula.js.map +1 -1
  28. package/dist/widgets-v2/histogram.js +138 -144
  29. package/dist/widgets-v2/histogram.js.map +1 -1
  30. package/dist/widgets-v2/markdown.js +18 -17
  31. package/dist/widgets-v2/markdown.js.map +1 -1
  32. package/dist/widgets-v2/pie.js +67 -73
  33. package/dist/widgets-v2/pie.js.map +1 -1
  34. package/dist/widgets-v2/scatterplot.js +75 -81
  35. package/dist/widgets-v2/scatterplot.js.map +1 -1
  36. package/dist/widgets-v2/spread.js +36 -41
  37. package/dist/widgets-v2/spread.js.map +1 -1
  38. package/dist/widgets-v2/table.js +46 -55
  39. package/dist/widgets-v2/table.js.map +1 -1
  40. package/dist/widgets-v2/timeseries.js +81 -87
  41. package/dist/widgets-v2/timeseries.js.map +1 -1
  42. package/dist/widgets-v2.js +247 -243
  43. package/dist/widgets-v2.js.map +1 -1
  44. package/package.json +3 -3
  45. package/src/chat/bubbles/styles.ts +5 -1
  46. package/src/chat/containers/chat-content.tsx +4 -1
  47. package/src/chat/containers/chat-footer.test.tsx +59 -0
  48. package/src/chat/containers/chat-footer.tsx +124 -36
  49. package/src/chat/containers/styles.ts +107 -16
  50. package/src/chat/feedback/styles.ts +11 -4
  51. package/src/chat/index.ts +1 -0
  52. package/src/chat/types.ts +22 -0
  53. package/src/chat/use-typewriter.ts +32 -24
  54. package/src/widgets-v2/actions/download/constants.ts +14 -0
  55. package/src/widgets-v2/actions/download/csv-item.test.tsx +77 -0
  56. package/src/widgets-v2/actions/download/csv-item.tsx +71 -0
  57. package/src/widgets-v2/actions/download/icons.tsx +10 -1
  58. package/src/widgets-v2/actions/download/index.ts +3 -1
  59. package/src/widgets-v2/actions/download/png-item.tsx +2 -1
  60. package/src/widgets-v2/actions/index.ts +5 -0
  61. package/src/widgets-v2/bar/download.tsx +16 -22
  62. package/src/widgets-v2/category/download.test.ts +9 -0
  63. package/src/widgets-v2/category/download.ts +16 -20
  64. package/src/widgets-v2/formula/download.tsx +23 -29
  65. package/src/widgets-v2/histogram/download.ts +22 -26
  66. package/src/widgets-v2/markdown/{download.ts → download.tsx} +5 -2
  67. package/src/widgets-v2/pie/download.ts +16 -20
  68. package/src/widgets-v2/scatterplot/download.ts +16 -20
  69. package/src/widgets-v2/spread/download.ts +23 -27
  70. package/src/widgets-v2/table/download.test.ts +10 -0
  71. package/src/widgets-v2/table/download.ts +11 -15
  72. package/src/widgets-v2/table/helpers.test.ts +19 -0
  73. package/src/widgets-v2/table/helpers.ts +7 -12
  74. package/src/widgets-v2/timeseries/download.ts +36 -40
  75. package/src/widgets-v2/wrapper/style.ts +13 -18
  76. package/src/widgets-v2/wrapper/widget-wrapper.test.tsx +66 -0
  77. package/src/widgets-v2/wrapper/widget-wrapper.tsx +7 -4
  78. package/dist/png-item-BE9uEqlD.js +0 -45
  79. package/dist/png-item-BE9uEqlD.js.map +0 -1
  80. package/dist/table-C9IMbTr0.js +0 -385
  81. package/dist/table-C9IMbTr0.js.map +0 -1
  82. package/dist/types/chat/feedback/styles.d.ts +0 -211
@@ -1,6 +1,6 @@
1
1
  import {
2
+ buildCsvDownloadItem,
2
3
  buildPngDownloadItem,
3
- downloadToCSV,
4
4
  type DownloadItem,
5
5
  } from '../actions/download'
6
6
  import type { TimeseriesWidgetData } from './types'
@@ -31,51 +31,47 @@ export function createTimeseriesDownloadConfig(args: {
31
31
  }),
32
32
  )
33
33
  }
34
- items.push({
35
- id: 'csv',
36
- label: 'Download as CSV',
37
- resolve: () => {
38
- const data = args.getData()
39
- const seriesCount = data.length
34
+ items.push(
35
+ buildCsvDownloadItem({
36
+ filename: args.filename,
37
+ getRows: () => {
38
+ const data = args.getData()
39
+ const seriesCount = data.length
40
40
 
41
- // Collect every unique time, preserving insertion order.
42
- const timeKeys: (Date | number | string)[] = []
43
- const seenKeys = new Set<string>()
44
- for (const series of data) {
45
- for (const point of series) {
46
- const key = String(point.name)
47
- if (!seenKeys.has(key)) {
48
- seenKeys.add(key)
49
- timeKeys.push(point.name)
41
+ // Collect every unique time, preserving insertion order.
42
+ const timeKeys: (Date | number | string)[] = []
43
+ const seenKeys = new Set<string>()
44
+ for (const series of data) {
45
+ for (const point of series) {
46
+ const key = String(point.name)
47
+ if (!seenKeys.has(key)) {
48
+ seenKeys.add(key)
49
+ timeKeys.push(point.name)
50
+ }
50
51
  }
51
52
  }
52
- }
53
53
 
54
- // Build a quick lookup per series for O(rows × series) emit.
55
- const lookups = data.map(
56
- (series) => new Map(series.map((p) => [String(p.name), p.value])),
57
- )
54
+ // Build a quick lookup per series for O(rows × series) emit.
55
+ const lookups = data.map(
56
+ (series) => new Map(series.map((p) => [String(p.name), p.value])),
57
+ )
58
58
 
59
- const header: unknown[] = ['time']
60
- for (let i = 0; i < seriesCount; i++) {
61
- header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)
62
- }
63
- const rows: unknown[][] = [header]
64
- for (const key of timeKeys) {
65
- const row: unknown[] = [formatTime(key)]
66
- const lookupKey = String(key)
67
- for (const lookup of lookups) row.push(lookup.get(lookupKey) ?? '')
68
- rows.push(row)
69
- }
59
+ const header: unknown[] = ['time']
60
+ for (let i = 0; i < seriesCount; i++) {
61
+ header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)
62
+ }
63
+ const rows: unknown[][] = [header]
64
+ for (const key of timeKeys) {
65
+ const row: unknown[] = [formatTime(key)]
66
+ const lookupKey = String(key)
67
+ for (const lookup of lookups) row.push(lookup.get(lookupKey) ?? '')
68
+ rows.push(row)
69
+ }
70
70
 
71
- const handle = downloadToCSV(rows)
72
- return Promise.resolve({
73
- url: handle.url,
74
- filename: `${args.filename}.csv`,
75
- revoke: handle.revoke,
76
- })
77
- },
78
- })
71
+ return rows
72
+ },
73
+ }),
74
+ )
79
75
  return items
80
76
  }
81
77
 
@@ -48,24 +48,6 @@ export const styles = {
48
48
  opacity: 1,
49
49
  },
50
50
  },
51
-
52
- // Disabled state — only block interaction; keep visuals unchanged.
53
- // MUI's default `.Mui-disabled` rules dim the AccordionSummary's color
54
- // and add a faded background; we override them so the wrapper looks
55
- // identical to its enabled form. The single behavioral change is
56
- // `pointer-events: none`, which propagates to every descendant.
57
- '&.Mui-disabled': {
58
- pointerEvents: 'none',
59
- bgcolor: 'background.paper',
60
- '& .MuiAccordionSummary-root.Mui-disabled': {
61
- opacity: 1,
62
- color: 'inherit',
63
- backgroundColor: 'transparent',
64
- },
65
- '& .MuiAccordionSummary-expandIconWrapper': {
66
- display: 'none',
67
- },
68
- },
69
51
  },
70
52
  loading: {
71
53
  position: 'absolute',
@@ -83,6 +65,19 @@ export const styles = {
83
65
  alignItems: 'center',
84
66
  },
85
67
  },
68
+ // Disabled state — only the collapse toggle is inert (enforced by the
69
+ // guarded `onChange` handler, not `pointer-events`). Actions and Options in
70
+ // the summary row, and the details panel, all keep their events. We just
71
+ // drop the toggle affordance: hide the chevron (handled in the component)
72
+ // and clear the pointer cursor. MUI's AccordionSummary sets
73
+ // `cursor: pointer` via `&:hover:not(.Mui-disabled)` — a two-class selector
74
+ // that outranks a plain `cursor` rule — so we override at matching
75
+ // specificity. Action/Option buttons restore their own pointer cursor.
76
+ summaryDisabled: {
77
+ '&, &:hover:not(.Mui-disabled)': {
78
+ cursor: 'default',
79
+ },
80
+ },
86
81
  titleCell: {
87
82
  flexGrow: 1,
88
83
  flexShrink: 1,
@@ -186,4 +186,70 @@ describe('<Wrapper>', () => {
186
186
  )
187
187
  expect(screen.getByTestId('body')).toBeTruthy()
188
188
  })
189
+
190
+ it('disables only the summary, leaving the details panel interactive', () => {
191
+ let clicks = 0
192
+ render(
193
+ withProvider(
194
+ 'wrap-10',
195
+ <Wrapper title='t' disabled>
196
+ <Content>
197
+ <button data-testid='body-btn' onClick={() => (clicks += 1)}>
198
+ click
199
+ </button>
200
+ </Content>
201
+ </Wrapper>,
202
+ ),
203
+ )
204
+ // The details panel keeps its events even while the wrapper is disabled.
205
+ fireEvent.click(screen.getByTestId('body-btn'))
206
+ expect(clicks).toBe(1)
207
+ })
208
+
209
+ it('does not toggle when the disabled summary is clicked', () => {
210
+ let lastCollapsed: boolean | undefined
211
+ render(
212
+ withProvider(
213
+ 'wrap-11',
214
+ <Wrapper
215
+ title='t'
216
+ disabled
217
+ onCollapseChange={(c) => (lastCollapsed = c)}
218
+ >
219
+ <Content>body</Content>
220
+ </Wrapper>,
221
+ ),
222
+ )
223
+ fireEvent.click(screen.getByText('t'))
224
+ expect(lastCollapsed).toBeUndefined()
225
+ })
226
+
227
+ it('keeps summary actions clickable while the wrapper is disabled', () => {
228
+ let actionClicks = 0
229
+ let lastCollapsed: boolean | undefined
230
+ render(
231
+ withProvider(
232
+ 'wrap-12',
233
+ <Wrapper
234
+ title='t'
235
+ disabled
236
+ onCollapseChange={(c) => (lastCollapsed = c)}
237
+ >
238
+ <Actions>
239
+ <button
240
+ data-testid='action-btn'
241
+ onClick={() => (actionClicks += 1)}
242
+ >
243
+ A
244
+ </button>
245
+ </Actions>
246
+ <Content>body</Content>
247
+ </Wrapper>,
248
+ ),
249
+ )
250
+ fireEvent.click(screen.getByTestId('action-btn'))
251
+ // The action fires, and clicking it must not toggle the disabled wrapper.
252
+ expect(actionClicks).toBe(1)
253
+ expect(lastCollapsed).toBeUndefined()
254
+ })
189
255
  })
@@ -103,11 +103,12 @@ export function Wrapper({
103
103
  const effectiveCollapsed = isControlled ? collapsed : internalCollapsed
104
104
  const handleAccordionToggle = useCallback(
105
105
  (_: unknown, expanded: boolean) => {
106
+ if (disabled) return
106
107
  const next = !expanded
107
108
  onCollapseChange?.(next)
108
109
  setInternalCollapsed(next)
109
110
  },
110
- [onCollapseChange],
111
+ [disabled, onCollapseChange],
111
112
  )
112
113
  const _labels = { ...DEFAULT_WRAPPER_LABELS, ...labels }
113
114
 
@@ -121,7 +122,6 @@ export function Wrapper({
121
122
  data-collapsed={effectiveCollapsed ? 'true' : undefined}
122
123
  expanded={!effectiveCollapsed}
123
124
  onChange={handleAccordionToggle}
124
- disabled={disabled}
125
125
  disableGutters
126
126
  elevation={0}
127
127
  variant={variant}
@@ -136,9 +136,12 @@ export function Wrapper({
136
136
  ) : null}
137
137
 
138
138
  <AccordionSummary
139
- expandIcon={<ExpandIcon fontSize='small' {...iconProps} />}
139
+ expandIcon={
140
+ disabled ? null : <ExpandIcon fontSize='small' {...iconProps} />
141
+ }
140
142
  aria-label={ariaLabel}
141
- sx={styles.summary}
143
+ aria-disabled={disabled || undefined}
144
+ sx={{ ...styles.summary, ...(disabled ? styles.summaryDisabled : null) }}
142
145
  >
143
146
  <SmartTooltip title={title}>
144
147
  {({ ref }) => (
@@ -1,45 +0,0 @@
1
- import { jsx as n } from "react/jsx-runtime";
2
- import { d as a } from "./exports-Cx-f6m6U.js";
3
- import { c as r } from "react/compiler-runtime";
4
- import { SvgIcon as i } from "@mui/material";
5
- import { ImageOutlined as c } from "@mui/icons-material";
6
- function m(o) {
7
- const e = r(2);
8
- let l;
9
- return e[0] !== o ? (l = /* @__PURE__ */ n(c, { ...o }), e[0] = o, e[1] = l) : l = e[1], l;
10
- }
11
- function s(o) {
12
- const e = r(3);
13
- let l;
14
- e[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (l = /* @__PURE__ */ n("path", { fill: "currentColor", d: "M4.313 11.25h2.25v-1.125H4.688v-2.25h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v3c0 .213.072.39.216.534a.726.726 0 0 0 .534.216Zm2.925 0h2.25c.212 0 .39-.072.534-.216a.726.726 0 0 0 .216-.534V9.375a.931.931 0 0 0-.216-.59.658.658 0 0 0-.534-.273H8.363v-.637h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v1.125c0 .213.072.403.216.572a.675.675 0 0 0 .534.253h1.126v.675H7.238v1.125Zm4.95 0h1.124l1.313-4.5H13.5l-.75 2.588L12 6.75h-1.125l1.313 4.5ZM3 15c-.413 0-.766-.147-1.06-.44a1.445 1.445 0 0 1-.44-1.06v-9c0-.412.147-.766.44-1.06C2.235 3.148 2.588 3 3 3h12c.412 0 .766.147 1.06.44.293.294.44.648.44 1.06v9c0 .412-.147.766-.44 1.06-.294.293-.647.44-1.06.44H3Zm0-1.5h12v-9H3v9Z" }), e[0] = l) : l = e[0];
15
- let t;
16
- return e[1] !== o ? (t = /* @__PURE__ */ n(i, { viewBox: "0 0 18 18", ...o, children: l }), e[1] = o, e[2] = t) : t = e[2], t;
17
- }
18
- function b(o) {
19
- return {
20
- id: "png",
21
- label: o.label ?? "PNG",
22
- icon: /* @__PURE__ */ n(m, { fontSize: "small" }),
23
- resolve: async () => {
24
- const e = o.getCaptureEl();
25
- if (!e)
26
- throw new Error("[widgets-v2] No PNG capture element available");
27
- const l = await a({
28
- element: e,
29
- pixelRatio: o.pixelRatio,
30
- backgroundColor: o.backgroundColor
31
- });
32
- return {
33
- url: l.url,
34
- filename: `${o.filename}.png`,
35
- revoke: l.revoke
36
- };
37
- }
38
- };
39
- }
40
- export {
41
- s as C,
42
- m as P,
43
- b
44
- };
45
- //# sourceMappingURL=png-item-BE9uEqlD.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"png-item-BE9uEqlD.js","sources":["../src/widgets-v2/actions/download/icons.tsx","../src/widgets-v2/actions/download/png-item.tsx"],"sourcesContent":["import { SvgIcon, type SvgIconProps } from '@mui/material'\nimport { ImageOutlined } from '@mui/icons-material'\n\n/**\n * Generic \"image\" glyph used for the PNG download item. Wraps MUI's\n * `ImageOutlined` so callers can swap it via the per-widget download\n * config's `icon` override without pulling MUI directly.\n */\nexport function PNGIcon(props: SvgIconProps) {\n return <ImageOutlined {...props} />\n}\n\n/**\n * \"CSV\" rectangle with the letters spelled inside — matches v1 visual and is\n * easier to recognise in a download menu than a generic table glyph.\n */\nexport function CSVIcon(props: SvgIconProps) {\n return (\n <SvgIcon viewBox='0 0 18 18' {...props}>\n <path\n fill='currentColor'\n d='M4.313 11.25h2.25v-1.125H4.688v-2.25h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v3c0 .213.072.39.216.534a.726.726 0 0 0 .534.216Zm2.925 0h2.25c.212 0 .39-.072.534-.216a.726.726 0 0 0 .216-.534V9.375a.931.931 0 0 0-.216-.59.658.658 0 0 0-.534-.273H8.363v-.637h1.875V6.75h-2.25a.726.726 0 0 0-.535.216.726.726 0 0 0-.216.534v1.125c0 .213.072.403.216.572a.675.675 0 0 0 .534.253h1.126v.675H7.238v1.125Zm4.95 0h1.124l1.313-4.5H13.5l-.75 2.588L12 6.75h-1.125l1.313 4.5ZM3 15c-.413 0-.766-.147-1.06-.44a1.445 1.445 0 0 1-.44-1.06v-9c0-.412.147-.766.44-1.06C2.235 3.148 2.588 3 3 3h12c.412 0 .766.147 1.06.44.293.294.44.648.44 1.06v9c0 .412-.147.766-.44 1.06-.294.293-.647.44-1.06.44H3Zm0-1.5h12v-9H3v9Z'\n />\n </SvgIcon>\n )\n}\n","import { downloadDOMToPNG } from './exports'\nimport { PNGIcon } from './icons'\nimport type { DownloadItem } from './types'\n\nexport interface BuildPngDownloadItemArgs {\n /** Base filename (without extension). The item appends `.png`. */\n filename: string\n /**\n * Reads the capture element to rasterise. Called at click time so the\n * download config doesn't capture a stale reference. Wire it to\n * `() => getCaptureEl(id)` from `widgets-v2/stores`.\n */\n getCaptureEl: () => HTMLElement | null\n /** html2canvas `scale`. Default 2. */\n pixelRatio?: number\n /** html2canvas `backgroundColor`. Default transparent (`null`). */\n backgroundColor?: string | null\n /** Override the menu label. Default `'PNG'`. */\n label?: string\n}\n\n/**\n * Builds the standard PNG `DownloadItem` used by every per-widget download\n * config. Centralised so the menu label, icon, error message, and filename\n * suffix stay consistent across widgets without each `create*DownloadConfig`\n * re-deriving the same shape.\n */\nexport function buildPngDownloadItem(\n args: BuildPngDownloadItemArgs,\n): DownloadItem {\n return {\n id: 'png',\n label: args.label ?? 'PNG',\n icon: <PNGIcon fontSize='small' />,\n resolve: async () => {\n const el = args.getCaptureEl()\n if (!el) {\n throw new Error('[widgets-v2] No PNG capture element available')\n }\n const handle = await downloadDOMToPNG({\n element: el,\n pixelRatio: args.pixelRatio,\n backgroundColor: args.backgroundColor,\n })\n return {\n url: handle.url,\n filename: `${args.filename}.png`,\n revoke: handle.revoke,\n }\n },\n }\n}\n"],"names":["PNGIcon","props","$","_c","t0","jsx","ImageOutlined","CSVIcon","Symbol","for","t1","SvgIcon","buildPngDownloadItem","args","id","label","icon","resolve","el","getCaptureEl","Error","handle","downloadDOMToPNG","element","pixelRatio","backgroundColor","url","filename","revoke"],"mappings":";;;;;AAQO,SAAAA,EAAAC,GAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA;AAAA,MAAAC;AAAA,SAAAF,SAAAD,KACEG,IAAA,gBAAAC,EAACC,GAAA,EAAa,GAAKL,EAAAA,CAAK,GAAIC,OAAAD,GAAAC,OAAAE,KAAAA,IAAAF,EAAA,CAAA,GAA5BE;AAA4B;AAO9B,SAAAG,EAAAN,GAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA;AAAA,MAAAC;AAAA,EAAAF,EAAA,CAAA,MAAAM,uBAAAC,IAAA,2BAAA,KAGDL,IAAA,gBAAAC,EAAA,QAAA,EACO,MAAA,gBACH,GAAA,gtBAA8sB,GAChtBH,OAAAE,KAAAA,IAAAF,EAAA,CAAA;AAAA,MAAAQ;AAAA,SAAAR,SAAAD,KAJJS,sBAACC,GAAA,EAAgB,SAAA,aAAW,GAAKV,GAC/BG,UAAAA,GAIF,GAAUF,OAAAD,GAAAC,OAAAQ,KAAAA,IAAAR,EAAA,CAAA,GALVQ;AAKU;ACIP,SAASE,EACdC,GACc;AACd,SAAO;AAAA,IACLC,IAAI;AAAA,IACJC,OAAOF,EAAKE,SAAS;AAAA,IACrBC,MAAM,gBAAAX,EAACL,GAAA,EAAQ,UAAS,QAAA,CAAO;AAAA,IAC/BiB,SAAS,YAAY;AACnB,YAAMC,IAAKL,EAAKM,aAAAA;AAChB,UAAI,CAACD;AACH,cAAM,IAAIE,MAAM,+CAA+C;AAEjE,YAAMC,IAAS,MAAMC,EAAiB;AAAA,QACpCC,SAASL;AAAAA,QACTM,YAAYX,EAAKW;AAAAA,QACjBC,iBAAiBZ,EAAKY;AAAAA,MAAAA,CACvB;AACD,aAAO;AAAA,QACLC,KAAKL,EAAOK;AAAAA,QACZC,UAAU,GAAGd,EAAKc,QAAQ;AAAA,QAC1BC,QAAQP,EAAOO;AAAAA,MAAAA;AAAAA,IAEnB;AAAA,EAAA;AAEJ;"}