@evinvest/uikit 0.16.0 → 0.17.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/README.md CHANGED
@@ -170,8 +170,8 @@ measuring needs host-only `web-sys`). Known gaps:
170
170
  - **Rust overlays** (dialog, popover, dropdown, select, menus, tooltip, …) render
171
171
  inline with `position:fixed` + a backdrop and CSS-only placement — no portal,
172
172
  no viewport-measured floating, native focus order (no trap). TS overlays use a
173
- real `Portal`, single-flip `useFloating`, `useDismissableLayer`, and
174
- `useFocusScope`.
173
+ real `Portal`, single-flip `useFloating` (absolute in the document, so it
174
+ scrolls with the page natively), `useDismissableLayer`, and `useFocusScope`.
175
175
  - **chart:** the recharts plotting engine is not bundled. `ChartContainer` is a
176
176
  themed SVG host (emits `--color-*` from its config); `ChartTooltipContent` /
177
177
  `ChartLegendContent` are presentational and take explicit items. Draw series
@@ -182,17 +182,21 @@ measuring needs host-only `web-sys`). Known gaps:
182
182
  submit's default — inputs, validation and the order itself are the
183
183
  consumer's. The open-orders pane reuses `Table` and `Tabs`.
184
184
  - **calendar:** single month, single-date selection (no range/multi-month, no
185
- dropdown captions). `min`/`max` bound the grid at day granularity (days outside
186
- render `disabled` + `data-disabled="true"`); `disabled` freezes the
187
- whole grid and the nav the same way; the nav buttons take label overrides. TS-only `locale` renders the caption and weekday headers through
185
+ dropdown captions). The grid is always six fixed weeks of 36px cells, so its
186
+ size never changes with the month or the locale's weekday labels, and it
187
+ paints no background of its own (the host does). `min`/`max` bound the grid
188
+ at day granularity (days outside render `disabled` + `data-disabled="true"`);
189
+ `disabled` freezes the whole grid and the nav the same way; the nav buttons
190
+ take label overrides. TS-only `locale` renders the caption and weekday headers through
188
191
  `Intl` (Monday-first); Rust has no `Intl`, so its captions stay English. Rust
189
192
  does manual date math; TS uses the built-in `Date`.
190
193
  - **date-time-picker:** the kit's own bricks only — an outline trigger, a
191
194
  `Popover` with the `Calendar` and two numeric 24-hour hours/minutes fields (no
192
195
  native `datetime-local` / `time` input, so the browser's locale popup never
193
196
  appears). Its ARIA and button strings are `labels` overrides with English
194
- defaults — the nav buttons, the hours/minutes `aria-label`s, the clear button
195
- and the popover's own accessible name (`labels.dialog`). TS formats the trigger label through `Intl` when `locale` is set,
197
+ defaults — the nav buttons, the hours/minutes `aria-label`s, the clear button,
198
+ the close button (`labels.close`, which keeps the value) and the popover's own
199
+ accessible name (`labels.dialog`). TS formats the trigger label through `Intl` when `locale` is set,
196
200
  else `YYYY-MM-DD HH:MM`; Rust has no `Intl`: the ISO-like label or the
197
201
  `format` callback, English month/weekday captions. The hidden form value is
198
202
  unix seconds in TS and `YYYY-MM-DDTHH:MM` in Rust (no zone there). The Rust
package/dist/index.d.ts CHANGED
@@ -80,11 +80,28 @@ interface FloatingResult {
80
80
  align: Align;
81
81
  }
82
82
  /**
83
- * Positions a floating element against an anchor with `position: fixed` and
84
- * `getBoundingClientRect` — the dep-light stand-in for `@radix-ui/react-popper`
85
- * / floating-ui. Supports a preferred `side`/`align`, an `offset`, and a single
86
- * collision flip onto the opposite side when the preferred side overflows the
87
- * viewport. Recomputes on scroll and resize while `open`.
83
+ * Positions a floating element against an anchor with `position: absolute` in
84
+ * the coordinate space of its containing block (the `Portal` puts it in
85
+ * `document.body`, so normally document coordinates) — the dep-light stand-in
86
+ * for `@radix-ui/react-popper` / floating-ui. Supports a preferred
87
+ * `side`/`align`, an `offset`, and a single collision flip onto the opposite
88
+ * side when the preferred side overflows the viewport.
89
+ *
90
+ * Two update modes:
91
+ * - `place()` — full pass (flip + clamp into the viewport) on open, on window
92
+ * `resize`, and whenever the anchor or the floating element changes size.
93
+ * - `sync()` — re-derives the coordinates from the anchor with the side and
94
+ * the clamp shift already chosen; runs on every `scroll` (captured on
95
+ * `window`, so nested scroll containers count too).
96
+ *
97
+ * Because the element is absolutely positioned in the document, a document
98
+ * scroll moves it natively with the page — `sync()` then computes the same
99
+ * numbers and is a no-op — so the overlay never lags behind the compositor.
100
+ * Only a nested scroll container changes the coordinates, and there a frame
101
+ * of lag is acceptable. The side is never revisited on scroll: no jumping.
102
+ * A page that scrolls `body` instead of the document (`html { overflow:
103
+ * hidden } body { overflow: auto }`) is such a nested scroller: correct, but
104
+ * with the frame of lag the `fixed` strategy had.
88
105
  *
89
106
  * Rust's overlays use CSS-only placement (`data-side`) and do not measure; see
90
107
  * the README "Limitations".
@@ -109,11 +126,22 @@ declare function useFloating(opts: {
109
126
  declare function useFocusScope(enabled: boolean): React.RefObject<HTMLDivElement | null>;
110
127
 
111
128
  /**
112
- * Tracks whether an overlay should be mounted. Mount/unmount steps in lockstep
113
- * with `present`: enter animations (`data-[state=open]:animate-in`) still play
114
- * on mount, but the kit intentionally does **not** defer unmount for an exit
115
- * animation — deferring made portaled overlays flicker on close. Pass `ref`
116
- * through to the animated node (kept for API stability / future use).
129
+ * Keeps an overlay mounted through its exit animation. Enter animations
130
+ * (`data-[state=open]:animate-in`) play on mount; once `present` turns false
131
+ * the consumer flips the node to `data-state="closed"` in the same render and
132
+ * the node stays until its exit animation ends (or the fallback timer fires).
133
+ * Re-opening mid-exit just flips the state back; the node is never remounted.
134
+ *
135
+ * The exit was dropped once (5728b40) because closing flickered: tw-animate's
136
+ * `animate-out` runs with `animation-fill-mode: none`, so after `animationend`
137
+ * the node snaps back to full opacity/scale for the frame React needs to
138
+ * process an async unmount. Hence `flushSync` inside the native listener —
139
+ * the unmount commits before the browser paints that frame (Radix Presence
140
+ * does the same). With no animation on the node (jsdom, overlays without an
141
+ * exit class) unmount is immediate, as before. The Rust mirror still does not
142
+ * defer unmount.
143
+ *
144
+ * Pass `ref` to the animated node — the one carrying `data-state`.
117
145
  */
118
146
  declare function usePresence(present: boolean): {
119
147
  isPresent: boolean;
@@ -1444,6 +1472,8 @@ interface DateTimePickerLabels {
1444
1472
  clear?: string;
1445
1473
  /** `aria-label` of the popover dialog; "Choose date and time". */
1446
1474
  dialog?: string;
1475
+ /** `aria-label` of the close button; "Close". */
1476
+ close?: string;
1447
1477
  }
1448
1478
  interface DateTimePickerProps extends React.AriaAttributes, Pick<React.ComponentProps<"button">, "onFocus" | "onBlur"> {
1449
1479
  /** Controlled value; `null` = empty. Local-zone `Date` (seconds/ms are zeroed on every change). */
@@ -1489,10 +1519,11 @@ interface DateTimePickerProps extends React.AriaAttributes, Pick<React.Component
1489
1519
  * `datetime-local`, so the browser's locale popup never appears.
1490
1520
  *
1491
1521
  * Every edit reports through `onChange`; the popover stays open after a day
1492
- * click (the operator still sets the time) and closes on clear. Opening moves
1493
- * focus into the hours field; closing hands it back to the trigger unless the
1494
- * operator already focused something else. Any other `aria-*` prop, `onFocus`
1495
- * and `onBlur` land on the trigger.
1522
+ * click (the operator still sets the time) and closes on clear or on the close
1523
+ * button (which keeps the value). Opening moves focus into the hours field;
1524
+ * closing hands it back to the trigger unless the operator already focused
1525
+ * something else. Any other `aria-*` prop, `onFocus` and `onBlur` land on the
1526
+ * trigger.
1496
1527
  */
1497
1528
  declare function DateTimePicker({ value: valueProp, defaultValue, onChange, min, max, locale, format, placeholder, labels, disabled, name, today, open, defaultOpen, onOpenChange, align, className, id, ...rest }: DateTimePickerProps): React.JSX.Element;
1498
1529
 
package/dist/index.js CHANGED
@@ -109,37 +109,46 @@ function useDismissableLayer(opts) {
109
109
 
110
110
  // src/primitives/use-floating.ts
111
111
  import * as React5 from "react";
112
+ var ABSOLUTE = Object.freeze({ position: "absolute", top: 0, left: 0 });
113
+ var POSITIONED = /* @__PURE__ */ new Set(["relative", "absolute", "fixed", "sticky"]);
114
+ function toContainingBlock(floating, x, y) {
115
+ const parent = floating.offsetParent;
116
+ const doc = floating.ownerDocument;
117
+ const root = parent === doc.body || parent === doc.documentElement || parent === null;
118
+ const positioned = parent instanceof HTMLElement && (!root || POSITIONED.has(doc.defaultView?.getComputedStyle(parent).position ?? ""));
119
+ if (!positioned) {
120
+ const win = doc.defaultView;
121
+ return { x: x + (win?.scrollX ?? 0), y: y + (win?.scrollY ?? 0), root: true };
122
+ }
123
+ const p = parent.getBoundingClientRect();
124
+ return {
125
+ x: x - p.left - parent.clientLeft + parent.scrollLeft,
126
+ y: y - p.top - parent.clientTop + parent.scrollTop,
127
+ root: false
128
+ };
129
+ }
130
+ function layoutSize(el) {
131
+ return { width: el.offsetWidth, height: el.offsetHeight };
132
+ }
133
+ function viewport() {
134
+ const el = document.documentElement;
135
+ return {
136
+ vw: el.clientWidth || window.innerWidth,
137
+ vh: el.clientHeight || window.innerHeight
138
+ };
139
+ }
112
140
  function useFloating(opts) {
113
141
  const { anchorRef, open, side = "bottom", align = "center", offset = 4 } = opts;
114
142
  const floatingRef = React5.useRef(null);
115
- const [style, setStyle] = React5.useState({
116
- position: "fixed",
117
- top: 0,
118
- left: 0
119
- });
120
- const [resolved, setResolved] = React5.useState({
143
+ const [placedSide, setPlacedSide] = React5.useState(side);
144
+ const placement = React5.useRef({
121
145
  side,
122
- align
146
+ shiftX: 0,
147
+ shiftY: 0
123
148
  });
124
149
  React5.useLayoutEffect(() => {
125
150
  if (!open) return;
126
- function update() {
127
- const anchor = anchorRef.current;
128
- const floating = floatingRef.current;
129
- if (!anchor || !floating) return;
130
- const a = anchor.getBoundingClientRect();
131
- const f = floating.getBoundingClientRect();
132
- const vw = window.innerWidth;
133
- const vh = window.innerHeight;
134
- let placed = side;
135
- if (side === "bottom" && a.bottom + offset + f.height > vh && a.top - offset - f.height > 0)
136
- placed = "top";
137
- else if (side === "top" && a.top - offset - f.height < 0 && a.bottom + offset + f.height < vh)
138
- placed = "bottom";
139
- else if (side === "right" && a.right + offset + f.width > vw && a.left - offset - f.width > 0)
140
- placed = "left";
141
- else if (side === "left" && a.left - offset - f.width < 0 && a.right + offset + f.width < vw)
142
- placed = "right";
151
+ function ideal(a, f, placed) {
143
152
  let top = 0;
144
153
  let left = 0;
145
154
  const vertical = placed === "top" || placed === "bottom";
@@ -156,29 +165,71 @@ function useFloating(opts) {
156
165
  else if (align === "end") top = a.bottom - f.height;
157
166
  else top = a.top + a.height / 2 - f.height / 2;
158
167
  }
159
- left = Math.max(offset, Math.min(left, vw - f.width - offset));
160
- top = Math.max(offset, Math.min(top, vh - f.height - offset));
161
- setStyle({ position: "fixed", top: Math.round(top), left: Math.round(left) });
162
- setResolved({ side: placed, align });
163
- }
164
- let frame = 0;
165
- function schedule() {
166
- if (frame) return;
167
- frame = requestAnimationFrame(() => {
168
- frame = 0;
169
- update();
170
- });
171
- }
172
- update();
173
- window.addEventListener("scroll", schedule, true);
174
- window.addEventListener("resize", schedule);
168
+ return { top, left };
169
+ }
170
+ function write(floating, f, top, left, contain) {
171
+ const p = toContainingBlock(floating, left, top);
172
+ let { x, y } = p;
173
+ if (contain && p.root) {
174
+ const icbWidth = floating.ownerDocument.documentElement.clientWidth || window.innerWidth;
175
+ x = Math.max(0, Math.min(x, icbWidth - f.width));
176
+ y = Math.max(0, y);
177
+ }
178
+ floating.style.translate = `${Math.round(x)}px ${Math.round(y)}px`;
179
+ }
180
+ function place() {
181
+ const anchor = anchorRef.current;
182
+ const floating = floatingRef.current;
183
+ if (!anchor || !floating) return;
184
+ const a = anchor.getBoundingClientRect();
185
+ const f = layoutSize(floating);
186
+ const { vw, vh } = viewport();
187
+ let placed = side;
188
+ if (side === "bottom" && a.bottom + offset + f.height > vh && a.top - offset - f.height > 0)
189
+ placed = "top";
190
+ else if (side === "top" && a.top - offset - f.height < 0 && a.bottom + offset + f.height < vh)
191
+ placed = "bottom";
192
+ else if (side === "right" && a.right + offset + f.width > vw && a.left - offset - f.width > 0)
193
+ placed = "left";
194
+ else if (side === "left" && a.left - offset - f.width < 0 && a.right + offset + f.width < vw)
195
+ placed = "right";
196
+ const { top, left } = ideal(a, f, placed);
197
+ const clampedLeft = Math.max(offset, Math.min(left, vw - f.width - offset));
198
+ const clampedTop = Math.max(offset, Math.min(top, vh - f.height - offset));
199
+ placement.current = {
200
+ side: placed,
201
+ shiftX: clampedLeft - left,
202
+ shiftY: clampedTop - top
203
+ };
204
+ write(floating, f, clampedTop, clampedLeft, false);
205
+ setPlacedSide((prev) => prev === placed ? prev : placed);
206
+ }
207
+ function sync() {
208
+ const anchor = anchorRef.current;
209
+ const floating = floatingRef.current;
210
+ if (!anchor || !floating) return;
211
+ const a = anchor.getBoundingClientRect();
212
+ const f = layoutSize(floating);
213
+ const { side: placed, shiftX, shiftY } = placement.current;
214
+ const { top, left } = ideal(a, f, placed);
215
+ write(floating, f, top + shiftY, left + shiftX, true);
216
+ }
217
+ place();
218
+ window.addEventListener("scroll", sync, true);
219
+ window.addEventListener("resize", place);
220
+ let observer;
221
+ if (typeof ResizeObserver !== "undefined") {
222
+ observer = new ResizeObserver(place);
223
+ if (anchorRef.current) observer.observe(anchorRef.current);
224
+ if (floatingRef.current) observer.observe(floatingRef.current);
225
+ }
175
226
  return () => {
176
- if (frame) cancelAnimationFrame(frame);
177
- window.removeEventListener("scroll", schedule, true);
178
- window.removeEventListener("resize", schedule);
227
+ window.removeEventListener("scroll", sync, true);
228
+ window.removeEventListener("resize", place);
229
+ observer?.disconnect();
179
230
  };
180
231
  }, [open, side, align, offset, anchorRef]);
181
- return { floatingRef, style, side: resolved.side, align: resolved.align };
232
+ return { floatingRef, style: ABSOLUTE, side: placedSide, align };
182
233
  }
183
234
 
184
235
  // src/primitives/focus-scope.tsx
@@ -227,9 +278,44 @@ function useFocusScope(enabled) {
227
278
 
228
279
  // src/primitives/presence.ts
229
280
  import * as React7 from "react";
281
+ import { flushSync } from "react-dom";
282
+ var EXIT_FALLBACK_MS = 1e3;
230
283
  function usePresence(present) {
231
284
  const ref = React7.useRef(null);
232
- return { isPresent: present, ref };
285
+ const [mounted, setMounted] = React7.useState(present);
286
+ React7.useLayoutEffect(() => {
287
+ if (present) {
288
+ setMounted(true);
289
+ return;
290
+ }
291
+ const node = ref.current;
292
+ if (!node) {
293
+ setMounted(false);
294
+ return;
295
+ }
296
+ const name = getComputedStyle(node).animationName;
297
+ if (name === "" || name === "none") {
298
+ setMounted(false);
299
+ return;
300
+ }
301
+ const prevPointerEvents = node.style.pointerEvents;
302
+ node.style.pointerEvents = "none";
303
+ const onEnd = (e) => {
304
+ if (e.target !== node) return;
305
+ if (e.animationName && !name.includes(e.animationName)) return;
306
+ flushSync(() => setMounted(false));
307
+ };
308
+ node.addEventListener("animationend", onEnd);
309
+ node.addEventListener("animationcancel", onEnd);
310
+ const timer = setTimeout(() => setMounted(false), EXIT_FALLBACK_MS);
311
+ return () => {
312
+ clearTimeout(timer);
313
+ node.removeEventListener("animationend", onEnd);
314
+ node.removeEventListener("animationcancel", onEnd);
315
+ node.style.pointerEvents = prevPointerEvents;
316
+ };
317
+ }, [present]);
318
+ return { isPresent: present || mounted, ref };
233
319
  }
234
320
 
235
321
  // src/primitives/use-roving-focus.ts
@@ -5197,14 +5283,14 @@ function useItem() {
5197
5283
  function NavigationMenu({
5198
5284
  className,
5199
5285
  children,
5200
- viewport = true,
5286
+ viewport: viewport2 = true,
5201
5287
  ...props
5202
5288
  }) {
5203
5289
  return /* @__PURE__ */ jsxs18(
5204
5290
  "nav",
5205
5291
  {
5206
5292
  "data-slot": "navigation-menu",
5207
- "data-viewport": viewport,
5293
+ "data-viewport": viewport2,
5208
5294
  role: "navigation",
5209
5295
  className: cn(
5210
5296
  NAVIGATION_MENU,
@@ -5213,7 +5299,7 @@ function NavigationMenu({
5213
5299
  ...props,
5214
5300
  children: [
5215
5301
  children,
5216
- viewport && /* @__PURE__ */ jsx48(NavigationMenuViewport, {})
5302
+ viewport2 && /* @__PURE__ */ jsx48(NavigationMenuViewport, {})
5217
5303
  ]
5218
5304
  }
5219
5305
  );
@@ -5911,7 +5997,7 @@ import { jsx as jsx52, jsxs as jsxs22 } from "react/jsx-runtime";
5911
5997
  var CLOSE_THRESHOLD = 0.25;
5912
5998
  var VELOCITY_THRESHOLD = 0.4;
5913
5999
  var DRAG_AFTER_OPEN_MS = 500;
5914
- var EXIT_FALLBACK_MS = 600;
6000
+ var EXIT_FALLBACK_MS2 = 600;
5915
6001
  var DrawerContext = React62.createContext(null);
5916
6002
  function useDrawer() {
5917
6003
  const ctx = React62.useContext(DrawerContext);
@@ -6118,7 +6204,7 @@ function DrawerContent({ className, children, ...props }) {
6118
6204
  }, [open]);
6119
6205
  React62.useEffect(() => {
6120
6206
  if (!closing) return;
6121
- const id = setTimeout(() => setPresent(false), EXIT_FALLBACK_MS);
6207
+ const id = setTimeout(() => setPresent(false), EXIT_FALLBACK_MS2);
6122
6208
  return () => clearTimeout(id);
6123
6209
  }, [closing]);
6124
6210
  const drag = useDrawerDrag({
@@ -6878,17 +6964,17 @@ function ChartLegendContent({
6878
6964
  import * as React66 from "react";
6879
6965
 
6880
6966
  // src/generated/calendar.ts
6881
- var CALENDAR_ROOT = "bg-background p-3 w-fit";
6967
+ var CALENDAR_ROOT = "p-3 w-fit";
6882
6968
  var CALENDAR_NAV_BUTTON = "size-8 p-0 select-none";
6883
6969
  var CALENDAR_NAV = "flex items-center justify-between gap-1 w-full px-1";
6884
6970
  var CALENDAR_CAPTION = "text-sm font-medium select-none";
6885
6971
  var CALENDAR_GRID = "w-full border-collapse mt-4";
6886
6972
  var CALENDAR_WEEKDAY_ROW = "flex";
6887
- var CALENDAR_WEEKDAY = "text-ink-soft rounded-md flex-1 font-normal text-[0.8rem] select-none";
6973
+ var CALENDAR_WEEKDAY = "text-ink-soft rounded-md w-9 shrink-0 font-normal text-[0.8rem] select-none";
6888
6974
  var CALENDAR_WEEK = "flex w-full mt-2";
6889
- var CALENDAR_DAY_EMPTY = "relative w-full h-full p-0 aspect-square select-none";
6890
- var CALENDAR_DAY_CELL = "relative w-full h-full p-0 text-center aspect-square select-none";
6891
- var CALENDAR_DAY = "size-auto w-full aspect-square font-normal leading-none";
6975
+ var CALENDAR_DAY_EMPTY = "relative size-9 shrink-0 p-0 select-none";
6976
+ var CALENDAR_DAY_CELL = "relative size-9 shrink-0 p-0 text-center select-none";
6977
+ var CALENDAR_DAY = "size-auto w-full aspect-square font-normal leading-none tabular-nums";
6892
6978
  var CALENDAR_DAY_SELECTED = "bg-primary text-on-primary";
6893
6979
  var CALENDAR_DAY_TODAY = "bg-hover text-ink rounded-md";
6894
6980
 
@@ -6987,7 +7073,7 @@ function Calendar({
6987
7073
  const cells = [];
6988
7074
  for (let i = 0; i < lead; i += 1) cells.push(null);
6989
7075
  for (let day = 1; day <= total; day += 1) cells.push(day);
6990
- while (cells.length % 7 !== 0) cells.push(null);
7076
+ while (cells.length < 42) cells.push(null);
6991
7077
  const weeks = [];
6992
7078
  for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
6993
7079
  const navClass = buttonVariants({
@@ -7129,6 +7215,25 @@ function CalendarIcon() {
7129
7215
  }
7130
7216
  );
7131
7217
  }
7218
+ function XIcon3() {
7219
+ return /* @__PURE__ */ jsx57(
7220
+ "svg",
7221
+ {
7222
+ className: "size-4",
7223
+ xmlns: "http://www.w3.org/2000/svg",
7224
+ width: "24",
7225
+ height: "24",
7226
+ viewBox: "0 0 24 24",
7227
+ fill: "none",
7228
+ stroke: "currentColor",
7229
+ strokeWidth: "2",
7230
+ strokeLinecap: "round",
7231
+ strokeLinejoin: "round",
7232
+ "aria-hidden": true,
7233
+ children: /* @__PURE__ */ jsx57("path", { d: "M18 6 6 18M6 6l12 12" })
7234
+ }
7235
+ );
7236
+ }
7132
7237
  var pad = (n) => String(n).padStart(2, "0");
7133
7238
  function atMinute(d) {
7134
7239
  return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes());
@@ -7336,6 +7441,18 @@ function DateTimePicker({
7336
7441
  onClick: onClear,
7337
7442
  children: labels.clear ?? "Clear"
7338
7443
  }
7444
+ ),
7445
+ /* @__PURE__ */ jsx57(
7446
+ "button",
7447
+ {
7448
+ type: "button",
7449
+ className: buttonVariants({ variant: "ghost", size: "sm", icon: true }),
7450
+ "data-slot": "date-time-picker-close",
7451
+ "aria-label": labels.close ?? "Close",
7452
+ disabled,
7453
+ onClick: () => setOpen(false),
7454
+ children: /* @__PURE__ */ jsx57(XIcon3, {})
7455
+ }
7339
7456
  )
7340
7457
  ] })
7341
7458
  ]