@stackone/malachite 2.68.1 → 2.70.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/COMPONENTS.md CHANGED
@@ -44,6 +44,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
44
44
  - **`UsageCard`** — A small metered card: a title (optionally a link via `titleLink`, drawn with a trailing arrow), a right-aligned `detail` such as "700/1,000", and a body, usually a `Progress`. Used by `Sidebar.Usage`, and fine on its own, e.g. on a billing page. _Key props:_ `title`, `titleLink`, `detail`, `children`, `aria-label` (required; names the group), `busy` (sets `aria-busy` only).
45
45
  - **`Progress`** — Progress bar.
46
46
  - **`LoadingBar`** — Indeterminate full-width loading line; the variant colour sweeps across a pale track for work of unknown duration. Toggle `active` rather than unmounting, so the reserved height prevents layout shift. _Key props:_ `active`, `LoadingBarVariant` (`default` | `warning` | `error`), `LoadingBarSize` (`small` | `medium` | `large`), `aria-label`, `aria-labelledby`, `className`, `style`.
47
+ - **`ToastProvider`** — Transient top-centre toasts for the outcome of an action. Wrap the app once in `ToastProvider` and call `useToast().show({ message, variant })` anywhere beneath it. Each toast auto-dismisses, pauses while hovered and dismisses on click; at most three show at once. Toasts are announced through always-mounted live regions (`status` for success, `alert` for error), and the pills themselves are hidden from assistive tech and never take focus, so they cannot fight a dialog's focus trap. The host mounts on the first toast and joins the top layer, so toasts paint above elevated side panels. _Key props:_ `ToastProvider`: `children`, `duration` (ms, default 3000). `useToast()` → `show({ message, variant })`, `ToastVariant` (`success` | `error`, default `success`).
47
48
  - **`Spinner`** — Loading spinner.
48
49
  - **`Skeleton`** — Loading placeholders. Includes `Skeleton.Avatar` and `Skeleton.Input`. The shimmer stops under `prefers-reduced-motion`, leaving the plain placeholder.
49
50
  - **`Tooltip`** — Hover/focus tooltip; also works on disabled controls (see CLAUDE.md). It describes its trigger once shown, so `describesTrigger={false}` where the same sentence is already on screen and referenced — otherwise a screen reader reads it twice. _Key props:_ `text`, `placement`, `delay`, `maxWidth`, `shouldShow`, `triggerAs`, `describesTrigger`, `isShrinkable`, `fill`, `wrapChildren`, `suppressed` (holds the tooltip back, e.g. while the popup its trigger opens is up), `showOnFocus` (false where script can move focus onto the trigger, e.g. an option a panel focuses on open), `focusableTrigger` (makes a trigger that cannot take focus — text, a static `Tag` — a tab stop, so the tooltip is not mouse-only; not for focusable children).
@@ -99,6 +100,7 @@ The catalog of components exported from `@stackone/malachite`. **Reach for one o
99
100
 
100
101
  - **`Button`** — Primary button. Variants: `filled`, `outline`, `surface` (outline's geometry on a ground of its own, for a control sitting on a tinted surface), `assistant` (the violet the assistant is named in — provenance, not a severity: it says who is being asked, not that anything is wrong), `destructive`, `success`, `warning`, `error`, `ghost`, `no-style`. Supports `iconOnly`, `loading`, `tooltip`. `leading` puts something that is not an icon, such as an avatar, before the label at its own size; `contentAlign="start"` starts the label at the leading edge and pins an end `icon` to the far edge, which with `flexGrow` makes a full-width switcher trigger — a project pill whose width comes from its container (e.g. a box `SIDEBAR_POPOVER_WIDTH_PX` wide, so it lines up with the `Sidebar.Panel` it opens). _Key props:_ `variant`, `size`, `icon`, `iconPosition`, `leading`, `contentAlign` (`center` | `start`), `flexGrow` (fills the container), `iconOnly`, `loading`, `disabled`, `fill`, `tooltip`.
101
102
  - **`CloseButton`** — The bare X that dismisses a screen, panel or wizard: secondary at rest, primary on hover and keyboard focus, with no box or background at either. Use it instead of a `ghost` `Button` (which draws a hover box) or `no-style` (which has no disabled look). `surface="inverse"` is for a panel that stays dark in every theme, such as `SplitScreenLayout`’s, and reads the `closeButton` theme tokens. `loading` swaps the X for a spinner and sets `aria-busy`. Disabled and loading both use `aria-disabled` rather than `disabled`, so they ignore clicks but stay focusable and can still trigger a `Tooltip` explaining why. Named "Close" unless `aria-label` says otherwise. _Key props:_ `onClick`, `aria-label`, `size` (`small` 24px | `medium` 32px, 12px glyph), `surface` (`default` | `inverse`), `disabled`, `loading`.
103
+ - **`AccessPills`** — Three toggle pills, R / W / D, saying which effects an account grants (Read, Write, Delete) — on a linked-account row and in the header above the rows. Each pill is a button with `aria-pressed`, named in full by its state (`Write: some actions off`), and a tooltip on hover and focus spells that name out, since a letter alone is cryptic. On paints the effect’s tint (Read in `Tag`’s primary, Write in warning, Delete in error, the colours the dashboard already gives these effects), `"mixed"` (some, not all, of its actions on) an empty pill with a dashed border, off a neutral one. The colours come from the theme and there is no colour prop. It reports only which pill was pressed through `onToggle`: any cascade between effects (Read off turning Write and Delete off, say) and what pressing a mixed pill means belong to the caller. A pill in `disabled` stays focusable with `aria-disabled`, ignores clicks and keys, and its tooltip shows `disabledReason` instead, which is also its accessible description. `size="dense"` draws 24×20 pills in place of 26×24, for a narrow screen. _Key props:_ `value` (`{ read, write, delete }`, each `true` | `false` | `"mixed"`), `onToggle(effect)`, `disabled` (per effect), `disabledReason` (per effect, or one string), `size` (`default` | `dense`), `aria-label`, `aria-labelledby`.
102
104
  - **`PillButton`** — Pill-shaped button, often used for filters/toggleable chips. `shape="tab"` squares the top corners so a row of them reads as attached to whatever sits above — the controls under a chat composer. Pass `href` and an interactive, non-disabled pill renders a real anchor (cmd-click, copy-link) instead of navigating from `onClick`; a disabled or `interactive={false}` pill stays static, and an unsafe scheme such as `javascript:` is dropped. `target="_blank"` always carries `noopener noreferrer`. _Key props:_ `label`, `icon`, `trailingIcon`, `selected`, `disabled`, `interactive`, `size`, `variant`, `shape` (`pill` | `tab`), `onClick`, `href`, `target`, `rel`.
103
105
  - **`Link`** — Styled anchor/link text. _Key props:_ `LinkProps`.
104
106
  - **`InlineConfirm`** — A confirmation asked on the control that triggered it rather than in a modal, for a reversible bulk edit where a dialog would be heavier than the change it guards. Drawn to the design's chip: 33px tall, 12px radius, its confirm cell running to the pill's own edge between two rules. Reads its own `inlineConfirm` theme tokens — a shade off the `SelectionBar` it appears beside, by design. Carries a spinning glyph by default — its own, so it takes the pill's colour rather than the sheet's dimmed grey — standing for a change waiting on an answer; pass `icon={null}` to drop it or a node to replace it. Anchored placement is in viewport coordinates, so the chip must not sit inside an ancestor carrying `transform`, `filter` or `contain`. It takes focus on open so `Enter` confirms, dismisses on `Escape` (through the shared escape-layer stack, so the topmost overlay answers) or a click outside, and returns focus to whatever opened it. `role="alertdialog"` named by the message itself, so the confirm button is never announced bare. Only one can be open at a time: opening a second dismisses the first, so it is never ambiguous what Yes answers. Pass `anchorTo` (a ref to the control it answers for) and it pins itself to that control's left, follows it while the page or a sticky header scrolls, dismisses once it scrolls out of view, and treats clicks on it as inside — without which the same click would dismiss and reopen. Left unanchored it renders in flow for the consumer to place, and must then not sit in a container that clips overflow (`Table` header cells among them). Nothing renders while `open` is false, and its listeners are attached only while it is open. _Key props:_ `open`, `message`, `confirmLabel`, `onConfirm`, `onDismiss`, `anchorTo`, `icon`, `className`, `style`.
@@ -0,0 +1,99 @@
1
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
2
+ import { useId as _ } from "react";
3
+ import w from "./Tooltip.js";
4
+ import { FOCUS_RING as y } from "./focusRing.js";
5
+ const S = ["read", "write", "delete"], s = {
6
+ read: { full: "Read", letter: "R" },
7
+ write: { full: "Write", letter: "W" },
8
+ delete: { full: "Delete", letter: "D" }
9
+ }, k = "inline-flex shrink-0 items-center justify-center border rounded-[var(--malachite-radius-md,6px)] bg-[var(--malachite-card-background,rgb(255,255,255))] font-semibold leading-none text-[length:var(--malachite-font-size-xsmall,11px)] whitespace-nowrap transition-colors duration-150 ease-in-out", E = {
10
+ default: "h-6 w-[26px]",
11
+ dense: "h-5 w-6"
12
+ }, $ = {
13
+ read: {
14
+ on: "border-solid text-[var(--malachite-tag-primary-foreground)] [background-image:linear-gradient(var(--malachite-tag-primary-background),var(--malachite-tag-primary-background))] border-[var(--malachite-tag-primary-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-primary-border)_35%,transparent)]",
15
+ mixed: "border-dashed text-[var(--malachite-tag-primary-foreground)] border-[var(--malachite-tag-primary-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-primary-border)_65%,transparent)]"
16
+ },
17
+ write: {
18
+ on: "border-solid text-[var(--malachite-tag-warning-foreground)] [background-image:linear-gradient(var(--malachite-tag-warning-background),var(--malachite-tag-warning-background))] border-[var(--malachite-tag-warning-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-warning-border)_35%,transparent)]",
19
+ mixed: "border-dashed text-[var(--malachite-tag-warning-foreground)] border-[var(--malachite-tag-warning-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-warning-border)_65%,transparent)]"
20
+ },
21
+ delete: {
22
+ on: "border-solid text-[var(--malachite-tag-error-foreground)] [background-image:linear-gradient(var(--malachite-tag-error-background),var(--malachite-tag-error-background))] border-[var(--malachite-tag-error-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-error-border)_35%,transparent)]",
23
+ mixed: "border-dashed text-[var(--malachite-tag-error-foreground)] border-[var(--malachite-tag-error-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-error-border)_65%,transparent)]"
24
+ }
25
+ }, C = "border-solid text-[var(--malachite-text-muted,rgb(76,75,75))] border-[var(--malachite-card-border,rgb(232,232,232))]", A = {
26
+ read: {
27
+ on: "hover:border-[var(--malachite-tag-primary-border)]",
28
+ mixed: "hover:border-[var(--malachite-tag-primary-border)]",
29
+ off: "hover:border-[var(--malachite-text-dimmed,rgb(161,161,161))] hover:text-[var(--malachite-text-primary,rgb(10,10,10))]"
30
+ },
31
+ write: {
32
+ on: "hover:border-[var(--malachite-tag-warning-border)]",
33
+ mixed: "hover:border-[var(--malachite-tag-warning-border)]",
34
+ off: "hover:border-[var(--malachite-text-dimmed,rgb(161,161,161))] hover:text-[var(--malachite-text-primary,rgb(10,10,10))]"
35
+ },
36
+ delete: {
37
+ on: "hover:border-[var(--malachite-tag-error-border)]",
38
+ mixed: "hover:border-[var(--malachite-tag-error-border)]",
39
+ off: "hover:border-[var(--malachite-text-dimmed,rgb(161,161,161))] hover:text-[var(--malachite-text-primary,rgb(10,10,10))]"
40
+ }
41
+ }, L = {
42
+ on: "all actions on",
43
+ mixed: "some actions off",
44
+ off: "off"
45
+ }, T = (r) => r === "mixed" ? "mixed" : r ? "on" : "off", F = (r, i) => typeof r == "string" ? r : r == null ? void 0 : r[i], O = (r) => {
46
+ r.preventDefault(), r.stopPropagation();
47
+ }, P = ({
48
+ value: r,
49
+ onToggle: i,
50
+ disabled: n,
51
+ disabledReason: g,
52
+ size: b = "default",
53
+ "aria-label": h,
54
+ "aria-labelledby": v
55
+ }) => {
56
+ const p = _();
57
+ return /* @__PURE__ */ d(
58
+ "div",
59
+ {
60
+ role: "group",
61
+ "aria-label": h,
62
+ "aria-labelledby": v,
63
+ className: "inline-flex shrink-0 items-center gap-1",
64
+ children: S.map((a) => {
65
+ const l = r[a], e = T(l), o = !!(n != null && n[a]), t = o ? F(g, a) : void 0, c = `${p}-${a}-reason`, x = e === "off" ? C : $[a][e], u = o ? "cursor-not-allowed opacity-50" : `cursor-pointer ${A[a][e]}`, m = `${s[a].full}: ${L[e]}`;
66
+ return /* @__PURE__ */ d(
67
+ w,
68
+ {
69
+ text: t ?? m,
70
+ delay: 0.2,
71
+ describesTrigger: !1,
72
+ children: /* @__PURE__ */ f(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ "aria-label": m,
77
+ "aria-pressed": l,
78
+ "aria-disabled": o || void 0,
79
+ "aria-describedby": t ? c : void 0,
80
+ onClick: o ? O : () => i(a),
81
+ className: `${y} ${k} ${E[b]} ${x} ${u}`,
82
+ children: [
83
+ s[a].letter,
84
+ t && /* @__PURE__ */ d("span", { id: c, hidden: !0, children: t })
85
+ ]
86
+ }
87
+ )
88
+ },
89
+ a
90
+ );
91
+ })
92
+ }
93
+ );
94
+ };
95
+ export {
96
+ P as AccessPills,
97
+ P as default
98
+ };
99
+ //# sourceMappingURL=AccessPills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AccessPills.js","sources":["../../src/components/AccessPills.tsx"],"sourcesContent":["import React, { useId } from 'react';\nimport Tooltip from './Tooltip';\nimport { FOCUS_RING } from './focusRing';\n\nexport type AccessEffect = 'read' | 'write' | 'delete';\n\n/** `'mixed'` is partly on: some, not all, of the effect's actions are allowed. */\nexport type AccessPillState = boolean | 'mixed';\n\n/** `dense` draws the pills smaller, for a narrow screen. */\nexport type AccessPillsSize = 'default' | 'dense';\n\nexport interface AccessPillsProps {\n value: Record<AccessEffect, AccessPillState>;\n /** Reports the pill pressed. Cascading the change to the other effects is the caller's call. */\n onToggle: (effect: AccessEffect) => void;\n /** An effect with nothing to grant, e.g. a connector with no delete actions. */\n disabled?: Partial<Record<AccessEffect, boolean>>;\n /** Why a pill is disabled, per effect or one sentence for all of them. */\n disabledReason?: string | Partial<Record<AccessEffect, string>>;\n size?: AccessPillsSize;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\nconst EFFECTS: AccessEffect[] = ['read', 'write', 'delete'];\n\nconst LABELS: Record<AccessEffect, { full: string; letter: string }> = {\n read: { full: 'Read', letter: 'R' },\n write: { full: 'Write', letter: 'W' },\n delete: { full: 'Delete', letter: 'D' },\n};\n\nconst BASE =\n 'inline-flex shrink-0 items-center justify-center border rounded-[var(--malachite-radius-md,6px)] bg-[var(--malachite-card-background,rgb(255,255,255))] font-semibold leading-none text-[length:var(--malachite-font-size-xsmall,11px)] whitespace-nowrap transition-colors duration-150 ease-in-out';\n\nconst SIZE_CLASSES: Record<AccessPillsSize, string> = {\n default: 'h-6 w-[26px]',\n dense: 'h-5 w-6',\n};\n\ntype PillLook = 'on' | 'mixed' | 'off';\n\n// Every look names its border style: a bare `border` paints `var(--tw-border-style)`, which a\n// shadow root inherits from whatever ancestor set it. The tints read the same tokens as `Tag`'s\n// primary / warning / error, the colours the dashboard already gives these effects.\nconst LOOK_CLASSES: Record<AccessEffect, Record<Exclude<PillLook, 'off'>, string>> = {\n read: {\n on: 'border-solid text-[var(--malachite-tag-primary-foreground)] [background-image:linear-gradient(var(--malachite-tag-primary-background),var(--malachite-tag-primary-background))] border-[var(--malachite-tag-primary-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-primary-border)_35%,transparent)]',\n mixed: 'border-dashed text-[var(--malachite-tag-primary-foreground)] border-[var(--malachite-tag-primary-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-primary-border)_65%,transparent)]',\n },\n write: {\n on: 'border-solid text-[var(--malachite-tag-warning-foreground)] [background-image:linear-gradient(var(--malachite-tag-warning-background),var(--malachite-tag-warning-background))] border-[var(--malachite-tag-warning-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-warning-border)_35%,transparent)]',\n mixed: 'border-dashed text-[var(--malachite-tag-warning-foreground)] border-[var(--malachite-tag-warning-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-warning-border)_65%,transparent)]',\n },\n delete: {\n on: 'border-solid text-[var(--malachite-tag-error-foreground)] [background-image:linear-gradient(var(--malachite-tag-error-background),var(--malachite-tag-error-background))] border-[var(--malachite-tag-error-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-error-border)_35%,transparent)]',\n mixed: 'border-dashed text-[var(--malachite-tag-error-foreground)] border-[var(--malachite-tag-error-border)] supports-[color:color-mix(in_srgb,red,red)]:border-[color-mix(in_srgb,var(--malachite-tag-error-border)_65%,transparent)]',\n },\n};\n\n// `text-muted`, not `text-secondary`: secondary is 4.2:1 on a light card, short of AA at 11px.\nconst OFF_CLASSES =\n 'border-solid text-[var(--malachite-text-muted,rgb(76,75,75))] border-[var(--malachite-card-border,rgb(232,232,232))]';\n\nconst HOVER_CLASSES: Record<AccessEffect, Record<PillLook, string>> = {\n read: {\n on: 'hover:border-[var(--malachite-tag-primary-border)]',\n mixed: 'hover:border-[var(--malachite-tag-primary-border)]',\n off: 'hover:border-[var(--malachite-text-dimmed,rgb(161,161,161))] hover:text-[var(--malachite-text-primary,rgb(10,10,10))]',\n },\n write: {\n on: 'hover:border-[var(--malachite-tag-warning-border)]',\n mixed: 'hover:border-[var(--malachite-tag-warning-border)]',\n off: 'hover:border-[var(--malachite-text-dimmed,rgb(161,161,161))] hover:text-[var(--malachite-text-primary,rgb(10,10,10))]',\n },\n delete: {\n on: 'hover:border-[var(--malachite-tag-error-border)]',\n mixed: 'hover:border-[var(--malachite-tag-error-border)]',\n off: 'hover:border-[var(--malachite-text-dimmed,rgb(161,161,161))] hover:text-[var(--malachite-text-primary,rgb(10,10,10))]',\n },\n};\n\nconst STATE_TEXT: Record<PillLook, string> = {\n on: 'all actions on',\n mixed: 'some actions off',\n off: 'off',\n};\n\nconst lookOf = (state: AccessPillState): PillLook =>\n state === 'mixed' ? 'mixed' : state ? 'on' : 'off';\n\nconst reasonFor = (\n disabledReason: AccessPillsProps['disabledReason'],\n effect: AccessEffect,\n): string | undefined =>\n typeof disabledReason === 'string' ? disabledReason : disabledReason?.[effect];\n\n// An aria-disabled button still dispatches clicks, so stop one before a clickable row sees it.\nconst swallowClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n event.preventDefault();\n event.stopPropagation();\n};\n\nconst AccessPills: React.FC<AccessPillsProps> = ({\n value,\n onToggle,\n disabled,\n disabledReason,\n size = 'default',\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n}) => {\n const idPrefix = useId();\n\n return (\n <div\n role=\"group\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n className=\"inline-flex shrink-0 items-center gap-1\"\n >\n {EFFECTS.map((effect) => {\n const state = value[effect];\n const look = lookOf(state);\n const isDisabled = !!disabled?.[effect];\n const reason = isDisabled ? reasonFor(disabledReason, effect) : undefined;\n const reasonId = `${idPrefix}-${effect}-reason`;\n const lookClasses = look === 'off' ? OFF_CLASSES : LOOK_CLASSES[effect][look];\n const interaction = isDisabled\n ? 'cursor-not-allowed opacity-50'\n : `cursor-pointer ${HOVER_CLASSES[effect][look]}`;\n\n const name = `${LABELS[effect].full}: ${STATE_TEXT[look]}`;\n\n // The tooltip repeats the name (or the reason, already linked below), so it does\n // not describe the trigger. Always mounted, so toggling `disabled` keeps focus.\n return (\n <Tooltip\n key={effect}\n text={reason ?? name}\n delay={0.2}\n describesTrigger={false}\n >\n <button\n type=\"button\"\n aria-label={name}\n aria-pressed={state}\n aria-disabled={isDisabled || undefined}\n aria-describedby={reason ? reasonId : undefined}\n onClick={isDisabled ? swallowClick : () => onToggle(effect)}\n className={`${FOCUS_RING} ${BASE} ${SIZE_CLASSES[size]} ${lookClasses} ${interaction}`}\n >\n {LABELS[effect].letter}\n {reason && (\n <span id={reasonId} hidden>\n {reason}\n </span>\n )}\n </button>\n </Tooltip>\n );\n })}\n </div>\n );\n};\n\nexport { AccessPills };\nexport default AccessPills;\n"],"names":["EFFECTS","LABELS","BASE","SIZE_CLASSES","LOOK_CLASSES","OFF_CLASSES","HOVER_CLASSES","STATE_TEXT","lookOf","state","reasonFor","disabledReason","effect","swallowClick","event","AccessPills","value","onToggle","disabled","size","ariaLabel","ariaLabelledBy","idPrefix","useId","jsx","look","isDisabled","reason","reasonId","lookClasses","interaction","name","Tooltip","jsxs","FOCUS_RING"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import { jsx as i } from "react/jsx-runtime";
2
+ import { useRef as a, useLayoutEffect as n } from "react";
3
+ import { createPortal as c } from "react-dom";
4
+ import { ShadowRootWrapper as l } from "../ShadowRootWrapper.js";
5
+ import d from "./ToastItem.js";
6
+ import { keepAboveOverlayStack as u } from "./overlayBehaviour.js";
7
+ import { ensureToastStyles as h, overlayIdentity as v } from "./portalOverlay.js";
8
+ import { leaveTopLayer as x, raiseToTopLayer as y } from "./topLayer.js";
9
+ const T = {
10
+ position: "fixed",
11
+ top: 0,
12
+ left: 0,
13
+ right: 0,
14
+ pointerEvents: "none"
15
+ }, w = ({ toasts: t, duration: m, onDismiss: f }) => {
16
+ var p;
17
+ const e = a(null), r = (p = t[t.length - 1]) == null ? void 0 : p.id;
18
+ n(() => {
19
+ h();
20
+ }, []), n(() => {
21
+ const o = e.current;
22
+ if (o) {
23
+ if (r === void 0) {
24
+ x(o);
25
+ return;
26
+ }
27
+ y(o);
28
+ }
29
+ }, [r]);
30
+ const s = r !== void 0;
31
+ return n(() => {
32
+ const o = e.current;
33
+ if (!(!o || !s))
34
+ return u(o);
35
+ }, [s]), c(
36
+ /* @__PURE__ */ i(
37
+ l,
38
+ {
39
+ hostRef: e,
40
+ hostStyle: T,
41
+ hostProps: v("toast"),
42
+ children: /* @__PURE__ */ i("div", { className: "flex flex-col items-center gap-[8px] px-[16px] pt-[24px]", children: t.map((o) => /* @__PURE__ */ i(
43
+ d,
44
+ {
45
+ toast: o,
46
+ duration: m,
47
+ onDismiss: f
48
+ },
49
+ o.id
50
+ )) })
51
+ }
52
+ ),
53
+ document.body
54
+ );
55
+ };
56
+ export {
57
+ w as default
58
+ };
59
+ //# sourceMappingURL=ToastHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToastHost.js","sources":["../../src/components/ToastHost.tsx"],"sourcesContent":["import { useLayoutEffect, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { ShadowRootWrapper } from '../ShadowRootWrapper';\nimport ToastItem from './ToastItem';\nimport { keepAboveOverlayStack } from './overlayBehaviour';\nimport { ensureToastStyles, overlayIdentity } from './portalOverlay';\nimport type { QueuedToast } from './toastTypes';\nimport { leaveTopLayer, raiseToTopLayer } from './topLayer';\n\ninterface ToastHostProps {\n toasts: QueuedToast[];\n duration: number;\n onDismiss: (id: number) => void;\n}\n\nconst HOST_STYLE: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n pointerEvents: 'none',\n};\n\nconst ToastHost: React.FC<ToastHostProps> = ({ toasts, duration, onDismiss }) => {\n const hostRef = useRef<HTMLDivElement | null>(null);\n const newestId = toasts[toasts.length - 1]?.id;\n\n useLayoutEffect(() => {\n ensureToastStyles();\n }, []);\n\n useLayoutEffect(() => {\n const host = hostRef.current;\n if (!host) {\n return;\n }\n if (newestId === undefined) {\n leaveTopLayer(host);\n return;\n }\n raiseToTopLayer(host);\n }, [newestId]);\n\n const visible = newestId !== undefined;\n useLayoutEffect(() => {\n const host = hostRef.current;\n if (!host || !visible) {\n return;\n }\n return keepAboveOverlayStack(host);\n }, [visible]);\n\n return createPortal(\n <ShadowRootWrapper\n hostRef={hostRef}\n hostStyle={HOST_STYLE}\n hostProps={overlayIdentity('toast')}\n >\n <div className=\"flex flex-col items-center gap-[8px] px-[16px] pt-[24px]\">\n {toasts.map((toast) => (\n <ToastItem\n key={toast.id}\n toast={toast}\n duration={duration}\n onDismiss={onDismiss}\n />\n ))}\n </div>\n </ShadowRootWrapper>,\n document.body,\n );\n};\n\nexport default ToastHost;\n"],"names":["HOST_STYLE","ToastHost","toasts","duration","onDismiss","hostRef","useRef","newestId","_a","useLayoutEffect","ensureToastStyles","host","leaveTopLayer","raiseToTopLayer","visible","keepAboveOverlayStack","createPortal","jsx","ShadowRootWrapper","overlayIdentity","toast","ToastItem"],"mappings":";;;;;;;;AAeA,MAAMA,IAAkC;AAAA,EACpC,UAAU;AAAA,EACV,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,eAAe;AACnB,GAEMC,IAAsC,CAAC,EAAE,QAAAC,GAAQ,UAAAC,GAAU,WAAAC,QAAgB;;AAC7E,QAAMC,IAAUC,EAA8B,IAAI,GAC5CC,KAAWC,IAAAN,EAAOA,EAAO,SAAS,CAAC,MAAxB,gBAAAM,EAA2B;AAE5C,EAAAC,EAAgB,MAAM;AAClB,IAAAC,EAAA;AAAA,EACJ,GAAG,CAAA,CAAE,GAELD,EAAgB,MAAM;AAClB,UAAME,IAAON,EAAQ;AACrB,QAAKM,GAGL;AAAA,UAAIJ,MAAa,QAAW;AACxB,QAAAK,EAAcD,CAAI;AAClB;AAAA,MACJ;AACA,MAAAE,EAAgBF,CAAI;AAAA;AAAA,EACxB,GAAG,CAACJ,CAAQ,CAAC;AAEb,QAAMO,IAAUP,MAAa;AAC7B,SAAAE,EAAgB,MAAM;AAClB,UAAME,IAAON,EAAQ;AACrB,QAAI,GAACM,KAAQ,CAACG;AAGd,aAAOC,EAAsBJ,CAAI;AAAA,EACrC,GAAG,CAACG,CAAO,CAAC,GAELE;AAAA,IACH,gBAAAC;AAAA,MAACC;AAAA,MAAA;AAAA,QACG,SAAAb;AAAA,QACA,WAAWL;AAAA,QACX,WAAWmB,EAAgB,OAAO;AAAA,QAElC,4BAAC,OAAA,EAAI,WAAU,4DACV,UAAAjB,EAAO,IAAI,CAACkB,MACT,gBAAAH;AAAA,UAACI;AAAA,UAAA;AAAA,YAEG,OAAAD;AAAA,YACA,UAAAjB;AAAA,YACA,WAAAC;AAAA,UAAA;AAAA,UAHKgB,EAAM;AAAA,QAAA,CAKlB,EAAA,CACL;AAAA,MAAA;AAAA,IAAA;AAAA,IAEJ,SAAS;AAAA,EAAA;AAEjB;"}
@@ -0,0 +1,51 @@
1
+ import { jsxs as x, jsx as l } from "react/jsx-runtime";
2
+ import { useState as v, useRef as m, useEffect as p } from "react";
3
+ import g from "./CustomIcons/CheckCircleFilled.js";
4
+ import h from "./CustomIcons/ErrorCircleFilled.js";
5
+ const f = {
6
+ success: "text-[var(--malachite-text-success,rgb(2,122,66))] supports-[color:color-mix(in_srgb,red,red)]:text-[color-mix(in_srgb,var(--malachite-text-success,rgb(2,122,66))_70%,var(--malachite-tooltip-foreground,rgb(255,255,255)))]",
7
+ error: "text-[var(--malachite-text-destructive,rgb(237,53,54))] supports-[color:color-mix(in_srgb,red,red)]:text-[color-mix(in_srgb,var(--malachite-text-destructive,rgb(237,53,54))_70%,var(--malachite-tooltip-foreground,rgb(255,255,255)))]"
8
+ }, E = ({ toast: r, duration: u, onDismiss: a }) => {
9
+ const [i, n] = v(!1), s = m(null), o = m(u);
10
+ p(() => {
11
+ const e = s.current;
12
+ if (!e)
13
+ return;
14
+ const t = () => n(!0), c = () => n(!1);
15
+ return e.addEventListener("pointerenter", t), e.addEventListener("pointerleave", c), () => {
16
+ e.removeEventListener("pointerenter", t), e.removeEventListener("pointerleave", c);
17
+ };
18
+ }, []), p(() => {
19
+ if (i)
20
+ return;
21
+ const e = Date.now(), t = setTimeout(() => a(r.id), o.current);
22
+ return () => {
23
+ clearTimeout(t), o.current = Math.max(0, o.current - (Date.now() - e));
24
+ };
25
+ }, [i, a, r.id]);
26
+ const d = r.variant === "error" ? h : g;
27
+ return (
28
+ // aria-hidden: the provider's live regions are the announced copy.
29
+ /* @__PURE__ */ x(
30
+ "div",
31
+ {
32
+ ref: s,
33
+ "aria-hidden": "true",
34
+ "data-toast": r.variant,
35
+ onClick: (e) => {
36
+ e.stopPropagation(), a(r.id);
37
+ },
38
+ className: "pointer-events-auto flex max-w-[480px] cursor-pointer items-start gap-[8px] rounded-[var(--malachite-radius-xxxl,16px)] bg-[var(--malachite-tooltip-background,rgb(59,59,59))] px-[12px] py-[8px] text-[length:var(--malachite-font-size-medium,14px)] leading-[20px] text-[var(--malachite-tooltip-foreground,rgb(255,255,255))] [box-shadow:var(--malachite-tooltip-shadow,0px_0px_10px_0px_rgba(0,0,0,0.2))] animate-[malachite-toast-in_150ms_ease-out] motion-reduce:animate-none",
39
+ style: { overflowWrap: "anywhere" },
40
+ children: [
41
+ /* @__PURE__ */ l(d, { size: 16, className: `mt-[2px] shrink-0 ${f[r.variant]}` }),
42
+ /* @__PURE__ */ l("span", { children: r.message })
43
+ ]
44
+ }
45
+ )
46
+ );
47
+ };
48
+ export {
49
+ E as default
50
+ };
51
+ //# sourceMappingURL=ToastItem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToastItem.js","sources":["../../src/components/ToastItem.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport CheckCircleFilled from './CustomIcons/CheckCircleFilled';\nimport ErrorCircleFilled from './CustomIcons/ErrorCircleFilled';\nimport type { QueuedToast, ToastVariant } from './toastTypes';\n\n// The plain status tokens fall under 3:1 on the pill; the mix clears it in both themes.\nconst ICON_COLOR: Record<ToastVariant, string> = {\n success:\n 'text-[var(--malachite-text-success,rgb(2,122,66))] supports-[color:color-mix(in_srgb,red,red)]:text-[color-mix(in_srgb,var(--malachite-text-success,rgb(2,122,66))_70%,var(--malachite-tooltip-foreground,rgb(255,255,255)))]',\n error: 'text-[var(--malachite-text-destructive,rgb(237,53,54))] supports-[color:color-mix(in_srgb,red,red)]:text-[color-mix(in_srgb,var(--malachite-text-destructive,rgb(237,53,54))_70%,var(--malachite-tooltip-foreground,rgb(255,255,255)))]',\n};\n\ninterface ToastItemProps {\n toast: QueuedToast;\n duration: number;\n onDismiss: (id: number) => void;\n}\n\nconst ToastItem: React.FC<ToastItemProps> = ({ toast, duration, onDismiss }) => {\n const [paused, setPaused] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n const remaining = useRef(duration);\n\n // React's onPointerEnter never fires across this portal's shadow boundary.\n useEffect(() => {\n const el = ref.current;\n if (!el) {\n return;\n }\n const pause = () => setPaused(true);\n const resume = () => setPaused(false);\n el.addEventListener('pointerenter', pause);\n el.addEventListener('pointerleave', resume);\n return () => {\n el.removeEventListener('pointerenter', pause);\n el.removeEventListener('pointerleave', resume);\n };\n }, []);\n\n useEffect(() => {\n if (paused) {\n return;\n }\n const startedAt = Date.now();\n const timeout = setTimeout(() => onDismiss(toast.id), remaining.current);\n return () => {\n clearTimeout(timeout);\n remaining.current = Math.max(0, remaining.current - (Date.now() - startedAt));\n };\n }, [paused, onDismiss, toast.id]);\n\n const Icon = toast.variant === 'error' ? ErrorCircleFilled : CheckCircleFilled;\n\n return (\n // aria-hidden: the provider's live regions are the announced copy.\n <div\n ref={ref}\n aria-hidden=\"true\"\n data-toast={toast.variant}\n onClick={(event) => {\n // A portal bubbles through the React tree, to the provider's ancestors.\n event.stopPropagation();\n onDismiss(toast.id);\n }}\n className=\"pointer-events-auto flex max-w-[480px] cursor-pointer items-start gap-[8px] rounded-[var(--malachite-radius-xxxl,16px)] bg-[var(--malachite-tooltip-background,rgb(59,59,59))] px-[12px] py-[8px] text-[length:var(--malachite-font-size-medium,14px)] leading-[20px] text-[var(--malachite-tooltip-foreground,rgb(255,255,255))] [box-shadow:var(--malachite-tooltip-shadow,0px_0px_10px_0px_rgba(0,0,0,0.2))] animate-[malachite-toast-in_150ms_ease-out] motion-reduce:animate-none\"\n style={{ overflowWrap: 'anywhere' }}\n >\n <Icon size={16} className={`mt-[2px] shrink-0 ${ICON_COLOR[toast.variant]}`} />\n <span>{toast.message}</span>\n </div>\n );\n};\n\nexport default ToastItem;\n"],"names":["ICON_COLOR","ToastItem","toast","duration","onDismiss","paused","setPaused","useState","ref","useRef","remaining","useEffect","el","pause","resume","startedAt","timeout","Icon","ErrorCircleFilled","CheckCircleFilled","jsxs","event","jsx"],"mappings":";;;;AAMA,MAAMA,IAA2C;AAAA,EAC7C,SACI;AAAA,EACJ,OAAO;AACX,GAQMC,IAAsC,CAAC,EAAE,OAAAC,GAAO,UAAAC,GAAU,WAAAC,QAAgB;AAC5E,QAAM,CAACC,GAAQC,CAAS,IAAIC,EAAS,EAAK,GACpCC,IAAMC,EAAuB,IAAI,GACjCC,IAAYD,EAAON,CAAQ;AAGjC,EAAAQ,EAAU,MAAM;AACZ,UAAMC,IAAKJ,EAAI;AACf,QAAI,CAACI;AACD;AAEJ,UAAMC,IAAQ,MAAMP,EAAU,EAAI,GAC5BQ,IAAS,MAAMR,EAAU,EAAK;AACpC,WAAAM,EAAG,iBAAiB,gBAAgBC,CAAK,GACzCD,EAAG,iBAAiB,gBAAgBE,CAAM,GACnC,MAAM;AACT,MAAAF,EAAG,oBAAoB,gBAAgBC,CAAK,GAC5CD,EAAG,oBAAoB,gBAAgBE,CAAM;AAAA,IACjD;AAAA,EACJ,GAAG,CAAA,CAAE,GAELH,EAAU,MAAM;AACZ,QAAIN;AACA;AAEJ,UAAMU,IAAY,KAAK,IAAA,GACjBC,IAAU,WAAW,MAAMZ,EAAUF,EAAM,EAAE,GAAGQ,EAAU,OAAO;AACvE,WAAO,MAAM;AACT,mBAAaM,CAAO,GACpBN,EAAU,UAAU,KAAK,IAAI,GAAGA,EAAU,WAAW,KAAK,IAAA,IAAQK,EAAU;AAAA,IAChF;AAAA,EACJ,GAAG,CAACV,GAAQD,GAAWF,EAAM,EAAE,CAAC;AAEhC,QAAMe,IAAOf,EAAM,YAAY,UAAUgB,IAAoBC;AAE7D;AAAA;AAAA,IAEI,gBAAAC;AAAA,MAAC;AAAA,MAAA;AAAA,QACG,KAAAZ;AAAA,QACA,eAAY;AAAA,QACZ,cAAYN,EAAM;AAAA,QAClB,SAAS,CAACmB,MAAU;AAEhB,UAAAA,EAAM,gBAAA,GACNjB,EAAUF,EAAM,EAAE;AAAA,QACtB;AAAA,QACA,WAAU;AAAA,QACV,OAAO,EAAE,cAAc,WAAA;AAAA,QAEvB,UAAA;AAAA,UAAA,gBAAAoB,EAACL,GAAA,EAAK,MAAM,IAAI,WAAW,qBAAqBjB,EAAWE,EAAM,OAAO,CAAC,GAAA,CAAI;AAAA,UAC7E,gBAAAoB,EAAC,QAAA,EAAM,UAAApB,EAAM,QAAA,CAAQ;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA;AAGjC;"}
@@ -0,0 +1,54 @@
1
+ import { jsxs as m, jsx as n } from "react/jsx-runtime";
2
+ import { useState as i, useRef as v, useCallback as u, useEffect as D, useMemo as E } from "react";
3
+ import _ from "./ToastHost.js";
4
+ import { ToastContext as x } from "./toastContext.js";
5
+ import { TOAST_ANNOUNCEMENT_LIFETIME_MS as I, TOAST_DEFAULT_DURATION_MS as M } from "./toastTypes.js";
6
+ const N = {
7
+ position: "absolute",
8
+ width: 1,
9
+ height: 1,
10
+ margin: -1,
11
+ padding: 0,
12
+ border: 0,
13
+ overflow: "hidden",
14
+ clip: "rect(0 0 0 0)",
15
+ clipPath: "inset(50%)",
16
+ whiteSpace: "nowrap"
17
+ }, C = ({
18
+ children: p,
19
+ duration: f = M
20
+ }) => {
21
+ const [h, r] = i([]), [a, c] = i(
22
+ []
23
+ ), [T, w] = i(!1), l = v(0), d = u(({ message: t, variant: s = "success" }) => {
24
+ l.current += 1;
25
+ const e = { id: l.current, message: t, variant: s };
26
+ r((o) => [...o, e].slice(-3)), c((o) => [...o, { ...e, announcedAt: Date.now() }]), w(!0);
27
+ }, []), A = u((t) => {
28
+ r((s) => s.filter((e) => e.id !== t));
29
+ }, []);
30
+ D(() => {
31
+ const t = a[0];
32
+ if (!t)
33
+ return;
34
+ const s = setTimeout(
35
+ () => c((e) => e.filter((o) => o.id !== t.id)),
36
+ Math.max(0, t.announcedAt + I - Date.now())
37
+ );
38
+ return () => clearTimeout(s);
39
+ }, [a]);
40
+ const S = E(() => ({ show: d }), [d]);
41
+ return /* @__PURE__ */ m(x.Provider, { value: S, children: [
42
+ p,
43
+ /* @__PURE__ */ m("div", { style: N, children: [
44
+ /* @__PURE__ */ n("span", { role: "status", "aria-atomic": "false", children: a.filter((t) => t.variant === "success").map((t) => /* @__PURE__ */ n("span", { children: t.message }, t.id)) }),
45
+ /* @__PURE__ */ n("span", { role: "alert", "aria-atomic": "false", children: a.filter((t) => t.variant === "error").map((t) => /* @__PURE__ */ n("span", { children: t.message }, t.id)) })
46
+ ] }),
47
+ T && /* @__PURE__ */ n(_, { toasts: h, duration: f, onDismiss: A })
48
+ ] });
49
+ };
50
+ export {
51
+ C as ToastProvider,
52
+ C as default
53
+ };
54
+ //# sourceMappingURL=ToastProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToastProvider.js","sources":["../../src/components/ToastProvider.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport ToastHost from './ToastHost';\nimport { ToastContext } from './toastContext';\nimport {\n type QueuedToast,\n TOAST_ANNOUNCEMENT_LIFETIME_MS,\n TOAST_DEFAULT_DURATION_MS,\n TOAST_MAX_VISIBLE,\n type ToastOptions,\n} from './toastTypes';\n\n// Outside any shadow root, so Tailwind's sr-only is not available.\nconst VISUALLY_HIDDEN: React.CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n margin: -1,\n padding: 0,\n border: 0,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n clipPath: 'inset(50%)',\n whiteSpace: 'nowrap',\n};\n\nexport interface ToastProviderProps {\n children?: React.ReactNode;\n /** How long each toast stays, in ms. Defaults to 3000. */\n duration?: number;\n}\n\nconst ToastProvider: React.FC<ToastProviderProps> = ({\n children,\n duration = TOAST_DEFAULT_DURATION_MS,\n}) => {\n const [toasts, setToasts] = useState<QueuedToast[]>([]);\n const [announcements, setAnnouncements] = useState<(QueuedToast & { announcedAt: number })[]>(\n [],\n );\n const [hasShown, setHasShown] = useState(false);\n const nextId = useRef(0);\n\n const show = useCallback(({ message, variant = 'success' }: ToastOptions) => {\n nextId.current += 1;\n const toast: QueuedToast = { id: nextId.current, message, variant };\n setToasts((previous) => [...previous, toast].slice(-TOAST_MAX_VISIBLE));\n setAnnouncements((previous) => [...previous, { ...toast, announcedAt: Date.now() }]);\n setHasShown(true);\n }, []);\n\n const dismiss = useCallback((id: number) => {\n setToasts((previous) => previous.filter((toast) => toast.id !== id));\n }, []);\n\n useEffect(() => {\n const oldest = announcements[0];\n if (!oldest) {\n return;\n }\n const timeout = setTimeout(\n () =>\n setAnnouncements((previous) => previous.filter((entry) => entry.id !== oldest.id)),\n Math.max(0, oldest.announcedAt + TOAST_ANNOUNCEMENT_LIFETIME_MS - Date.now()),\n );\n return () => clearTimeout(timeout);\n }, [announcements]);\n\n const api = useMemo(() => ({ show }), [show]);\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n {/* A region added with its text is not announced; status and alert default to atomic. */}\n <div style={VISUALLY_HIDDEN}>\n <span role=\"status\" aria-atomic=\"false\">\n {announcements\n .filter((entry) => entry.variant === 'success')\n .map((entry) => (\n <span key={entry.id}>{entry.message}</span>\n ))}\n </span>\n <span role=\"alert\" aria-atomic=\"false\">\n {announcements\n .filter((entry) => entry.variant === 'error')\n .map((entry) => (\n <span key={entry.id}>{entry.message}</span>\n ))}\n </span>\n </div>\n {hasShown && <ToastHost toasts={toasts} duration={duration} onDismiss={dismiss} />}\n </ToastContext.Provider>\n );\n};\n\nexport { ToastProvider };\nexport default ToastProvider;\n"],"names":["VISUALLY_HIDDEN","ToastProvider","children","duration","TOAST_DEFAULT_DURATION_MS","toasts","setToasts","useState","announcements","setAnnouncements","hasShown","setHasShown","nextId","useRef","show","useCallback","message","variant","toast","previous","dismiss","id","useEffect","oldest","timeout","entry","TOAST_ANNOUNCEMENT_LIFETIME_MS","api","useMemo","jsxs","ToastContext","jsx","ToastHost"],"mappings":";;;;;AAYA,MAAMA,IAAuC;AAAA,EACzC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,MAAM;AAAA,EACN,UAAU;AAAA,EACV,YAAY;AAChB,GAQMC,IAA8C,CAAC;AAAA,EACjD,UAAAC;AAAA,EACA,UAAAC,IAAWC;AACf,MAAM;AACF,QAAM,CAACC,GAAQC,CAAS,IAAIC,EAAwB,CAAA,CAAE,GAChD,CAACC,GAAeC,CAAgB,IAAIF;AAAA,IACtC,CAAA;AAAA,EAAC,GAEC,CAACG,GAAUC,CAAW,IAAIJ,EAAS,EAAK,GACxCK,IAASC,EAAO,CAAC,GAEjBC,IAAOC,EAAY,CAAC,EAAE,SAAAC,GAAS,SAAAC,IAAU,gBAA8B;AACzE,IAAAL,EAAO,WAAW;AAClB,UAAMM,IAAqB,EAAE,IAAIN,EAAO,SAAS,SAAAI,GAAS,SAAAC,EAAA;AAC1D,IAAAX,EAAU,CAACa,MAAa,CAAC,GAAGA,GAAUD,CAAK,EAAE,MAAM,EAAkB,CAAC,GACtET,EAAiB,CAACU,MAAa,CAAC,GAAGA,GAAU,EAAE,GAAGD,GAAO,aAAa,KAAK,IAAA,EAAI,CAAG,CAAC,GACnFP,EAAY,EAAI;AAAA,EACpB,GAAG,CAAA,CAAE,GAECS,IAAUL,EAAY,CAACM,MAAe;AACxC,IAAAf,EAAU,CAACa,MAAaA,EAAS,OAAO,CAACD,MAAUA,EAAM,OAAOG,CAAE,CAAC;AAAA,EACvE,GAAG,CAAA,CAAE;AAEL,EAAAC,EAAU,MAAM;AACZ,UAAMC,IAASf,EAAc,CAAC;AAC9B,QAAI,CAACe;AACD;AAEJ,UAAMC,IAAU;AAAA,MACZ,MACIf,EAAiB,CAACU,MAAaA,EAAS,OAAO,CAACM,MAAUA,EAAM,OAAOF,EAAO,EAAE,CAAC;AAAA,MACrF,KAAK,IAAI,GAAGA,EAAO,cAAcG,IAAiC,KAAK,KAAK;AAAA,IAAA;AAEhF,WAAO,MAAM,aAAaF,CAAO;AAAA,EACrC,GAAG,CAAChB,CAAa,CAAC;AAElB,QAAMmB,IAAMC,EAAQ,OAAO,EAAE,MAAAd,MAAS,CAACA,CAAI,CAAC;AAE5C,SACI,gBAAAe,EAACC,EAAa,UAAb,EAAsB,OAAOH,GACzB,UAAA;AAAA,IAAAzB;AAAA,IAED,gBAAA2B,EAAC,OAAA,EAAI,OAAO7B,GACR,UAAA;AAAA,MAAA,gBAAA+B,EAAC,QAAA,EAAK,MAAK,UAAS,eAAY,SAC3B,UAAAvB,EACI,OAAO,CAACiB,MAAUA,EAAM,YAAY,SAAS,EAC7C,IAAI,CAACA,MACF,gBAAAM,EAAC,QAAA,EAAqB,YAAM,QAAA,GAAjBN,EAAM,EAAmB,CACvC,EAAA,CACT;AAAA,MACA,gBAAAM,EAAC,QAAA,EAAK,MAAK,SAAQ,eAAY,SAC1B,UAAAvB,EACI,OAAO,CAACiB,MAAUA,EAAM,YAAY,OAAO,EAC3C,IAAI,CAACA,MACF,gBAAAM,EAAC,QAAA,EAAqB,YAAM,QAAA,GAAjBN,EAAM,EAAmB,CACvC,EAAA,CACT;AAAA,IAAA,GACJ;AAAA,IACCf,KAAY,gBAAAqB,EAACC,GAAA,EAAU,QAAA3B,GAAgB,UAAAF,GAAoB,WAAWiB,EAAA,CAAS;AAAA,EAAA,GACpF;AAER;"}