@antadesign/anta 0.3.13 → 0.3.15

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.
@@ -29,9 +29,17 @@ export declare function wrapLabel(kids: React.ReactNode, tag: string): React.Rea
29
29
  * `currentTarget` is still live. (Read as `true` for the falsy-event guard so a
30
30
  * missing event never spuriously reads as bubbled.)
31
31
  */
32
- export declare function nativeStateChange<D>(e: CustomEvent<D> | {
32
+ /**
33
+ * The event a cross-renderer `statechange` handler receives: React wraps the native
34
+ * `CustomEvent` in a synthetic event (`.nativeEvent`); Preact passes it directly. `D`
35
+ * is the per-component `detail` vocabulary. Pair with `nativeStateChange` to unwrap.
36
+ * Exported so the stateful wrappers (`Banner`, and future ones) don't each re-declare
37
+ * this union — the CLAUDE.md "reuse shared wrapper helpers" rule.
38
+ */
39
+ export type StateChangeEvent<D> = CustomEvent<D> | {
33
40
  nativeEvent: CustomEvent<D>;
34
- }): {
41
+ };
42
+ export declare function nativeStateChange<D>(e: StateChangeEvent<D>): {
35
43
  event: CustomEvent<D>;
36
44
  detail?: D;
37
45
  isOwn: boolean;
@@ -46,6 +54,16 @@ export declare const ISOLATE_HINT: string;
46
54
  * `'open'` is `'closed'`, so an absent / malformed attribute reads closed. Kept
47
55
  * here so the two elements can't drift on how the token is parsed. */
48
56
  export declare function parseOpenState(v: string | null): 'open' | 'closed';
57
+ /** Install a delegated "dismiss from an action" click handler: any element inside
58
+ * `host` carrying `[attr]` runs `onDismiss` on click, scoped to the nearest host
59
+ * of `host`'s own tag (so a control inside a nested same-tag element doesn't also
60
+ * dismiss the outer one). A read-only light-DOM walk — no host / tree mutation —
61
+ * that catches any activated control (native `<button>`, `<a>`, `<a-button>`,
62
+ * `<a-menu-item>`) with keyboard activation for free. Shared by `a-toast`
63
+ * (`data-toast-dismiss`) and `a-banner` (`data-banner-dismiss`), mirroring
64
+ * `a-dialog`'s `data-dialog-close` family; keep the one copy here so a future fix
65
+ * (e.g. skipping `[disabled]` triggers) lands for every element at once. */
66
+ export declare function installDismissTrigger(host: HTMLElement, attr: string, onDismiss: () => void): void;
49
67
  /** The six named tones every toned component shares. Anything else is a literal
50
68
  * CSS colour the element resolves through its `--{component}-tone-source` var. */
51
69
  export declare const NAMED_TONES: Set<string>;
@@ -21,6 +21,14 @@ const ISOLATE_HINT = IS_MAC ? "\u2325+Click to select only this" : "Alt+Click to
21
21
  function parseOpenState(v) {
22
22
  return v === "open" ? "open" : "closed";
23
23
  }
24
+ function installDismissTrigger(host, attr, onDismiss) {
25
+ host.addEventListener("click", (e) => {
26
+ const target = e.target;
27
+ if (!(target instanceof Element)) return;
28
+ const trigger = target.closest(`[${attr}]`);
29
+ if (trigger && trigger.closest(host.localName) === host) onDismiss();
30
+ });
31
+ }
24
32
  const NAMED_TONES = /* @__PURE__ */ new Set([
25
33
  "brand",
26
34
  "neutral",
@@ -138,6 +146,7 @@ export {
138
146
  SelectableChildElement,
139
147
  anchorRect,
140
148
  hasChildren,
149
+ installDismissTrigger,
141
150
  isInsideOpenMenu,
142
151
  isMenuOpen,
143
152
  nativeStateChange,
@@ -0,0 +1,79 @@
1
+ import type { BaseProps } from "../general_types";
2
+ /** Public props for the `<Banner>` message strip. `message` is the leading
3
+ * content, `children` sit between it and `actions`, all centered in one row. */
4
+ export interface BannerProps extends BaseProps {
5
+ /** Leading message — a string (rendered at the banner type scale) or any node.
6
+ * Centered in the bar, followed by `children` and then `actions`. */
7
+ message?: React.ReactNode;
8
+ /** Trailing controls (buttons, links) rendered as a compact row after the
9
+ * message and `children`. */
10
+ actions?: React.ReactNode;
11
+ /** Free content placed BETWEEN the message and the actions. */
12
+ children?: React.ReactNode;
13
+ /** Semantic tone, or any literal CSS color (`'#ff1493'`, `'rebeccapurple'`) for
14
+ * a one-off custom tone. Named tones re-point the surface, text, and border
15
+ * colour (used if you opt into a border); a custom color keeps its hue while
16
+ * lightness/chroma are pinned. `'neutral'` (the default) is the same as omitting it.
17
+ * @defaultValue 'neutral' */
18
+ tone?: "neutral" | "brand" | "info" | "success" | "warning" | "critical" | (string & {});
19
+ /** Rounded corners for a standalone, floating banner (`border-radius: 999px`,
20
+ * which clamps to a stadium) instead of the edge-to-edge strip. Still borderless
21
+ * by default. Pass a `number` (px) or a CSS length string for a custom radius. */
22
+ round?: boolean | number | string;
23
+ /** Whether the trailing ✕ dismiss button is present. `false` removes it — drive
24
+ * dismissal yourself (a controlled `dismissed`, or your own control) instead.
25
+ * It doesn't make the banner un-dismissible, it just drops the built-in ✕.
26
+ * @defaultValue true */
27
+ closable?: boolean;
28
+ /** ARIA role for the strip host. `'status'` (a polite live region, so the notice
29
+ * reaches assistive tech) by default; pass `'alert'` for an urgent notice that
30
+ * should interrupt, or a landmark role.
31
+ * @defaultValue 'status' */
32
+ role?: string;
33
+ /** Controlled dismissed state. When provided, the consumer owns visibility: the
34
+ * banner follows this prop only, and clicking ✕ *requests* dismissal (reject by
35
+ * not updating). **Requires `onDismiss`** — controlled mode never self-hides, so
36
+ * without a handler to set `dismissed` the ✕ can't close the banner. Leave
37
+ * undefined for uncontrolled. */
38
+ dismissed?: boolean;
39
+ /** Initial dismissed state for the uncontrolled case (read once on mount). */
40
+ defaultDismissed?: boolean;
41
+ /** Fired when the user dismisses the banner (clicks ✕). Uncontrolled, the banner
42
+ * hides itself and this just notifies. Controlled, it's how you accept the
43
+ * request — set `dismissed` to `true` (or ignore to reject); pair it with
44
+ * `dismissed`, or the banner can't be closed. */
45
+ onDismiss?: () => void;
46
+ }
47
+ /**
48
+ * `<Banner>` — a full-width, dismissible message strip with a bottom border.
49
+ *
50
+ * A close cousin of `<Card>` with a small, centered horizontal payload:
51
+ * `message` leads, `children` sit in the middle, `actions` trail, and (when
52
+ * `closable`) a 40px-wide, full-height ✕ fills the right edge without shifting the centered group.
53
+ *
54
+ * A pure, stateless pass-through to `<a-banner>`: the element owns dismissal and
55
+ * visibility, so the wrapper holds no state and grabs no ref — it maps props to
56
+ * attributes (safe wherever the host DOM is reconciled, incl. off the UI thread).
57
+ *
58
+ * Uncontrolled (`defaultDismissed`): the element owns visibility and the ✕
59
+ * self-dismisses. Controlled (`dismissed` + `onDismiss`): the wrapper emits
60
+ * `state="open"|"closed"`; the element treats the attribute as the source of
61
+ * truth and the ✕ only *requests* dismissal. The element uses Anta's shared
62
+ * open/closed state contract under the hood — the wrapper presents it as the
63
+ * banner-natural `dismissed` (closed = dismissed). See STATEFUL-COMPONENTS.md.
64
+ *
65
+ * Requires `@antadesign/anta/elements` to be imported (client-side only) to
66
+ * register the underlying custom element.
67
+ *
68
+ * @example Basic usage
69
+ * ```tsx
70
+ * <Banner tone="info" message="A new release is available." actions={<Button size="small" label="Update" />} />
71
+ * ```
72
+ *
73
+ * @example Controlled
74
+ * ```tsx
75
+ * const [hidden, setHidden] = useState(false)
76
+ * <Banner message="Saved." dismissed={hidden} onDismiss={() => setHidden(true)} />
77
+ * ```
78
+ */
79
+ export declare const Banner: ({ message, actions, tone, round, closable, role, dismissed, defaultDismissed, onDismiss, className, style, children, ...rest }: BannerProps) => any;
@@ -0,0 +1,69 @@
1
+ import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import { nativeStateChange, toneStyle, roundStyle, roundAttr } from "../anta_helpers";
3
+ import { Button } from "./Button";
4
+ const Banner = ({
5
+ message,
6
+ actions,
7
+ tone,
8
+ round,
9
+ closable,
10
+ role = "status",
11
+ dismissed,
12
+ defaultDismissed,
13
+ onDismiss,
14
+ className,
15
+ style,
16
+ children,
17
+ ...rest
18
+ }) => {
19
+ const controlled = dismissed != null;
20
+ const computedStyle = roundStyle(
21
+ round,
22
+ "--banner-round",
23
+ toneStyle(tone, "--banner-tone-source", style)
24
+ );
25
+ const messageNode = message == null ? null : typeof message === "string" || typeof message === "number" ? /* @__PURE__ */ jsx("a-banner-message", { slot: "message", children: message }) : /* @__PURE__ */ jsx("span", { slot: "message", style: { display: "contents" }, children: message });
26
+ return /* @__PURE__ */ jsxs(
27
+ "a-banner",
28
+ {
29
+ state: controlled ? dismissed ? "closed" : "open" : void 0,
30
+ "default-state": !controlled ? defaultDismissed ? "closed" : "open" : void 0,
31
+ tone: tone && tone !== "neutral" ? tone : void 0,
32
+ round: roundAttr(round),
33
+ role,
34
+ onstatechange: onDismiss ? (e) => {
35
+ const { detail, isOwn } = nativeStateChange(e);
36
+ if (isOwn && detail?.next === "closed") onDismiss();
37
+ } : void 0,
38
+ class: className,
39
+ style: computedStyle,
40
+ ...rest,
41
+ children: [
42
+ messageNode,
43
+ children,
44
+ actions != null && /* @__PURE__ */ jsx("span", { slot: "actions", style: { display: "contents" }, children: actions }),
45
+ closable !== false && // A real <a-button> (light DOM → fully styled, keyboard-focusable) in the
46
+ // element's `close` slot. It fires the bubbling `dismissrequest` event via
47
+ // a-button's global listener, so dismissing works even without framework
48
+ // hydration; the element turns that into a dismiss request.
49
+ // CONTRACT: `data-custom-event` below MUST match `DISMISS_TRIGGER` in the
50
+ // element (src/elements/a-banner.ts). Duplicated, not shared — importing
51
+ // the element module here would self-register it and break the
52
+ // wrapper/element decoupling. Rename in both places.
53
+ /* @__PURE__ */ jsx("span", { slot: "close", style: { display: "contents" }, children: /* @__PURE__ */ jsx(
54
+ Button,
55
+ {
56
+ priority: "quaternary",
57
+ icon: "x",
58
+ tone,
59
+ "aria-label": "Dismiss",
60
+ "data-custom-event": "dismissrequest"
61
+ }
62
+ ) })
63
+ ]
64
+ }
65
+ );
66
+ };
67
+ export {
68
+ Banner
69
+ };
@@ -227,6 +227,24 @@ export interface SelectCommonProps<V extends OptionValue = string> extends Omit<
227
227
  * `indicator` (`'check'` / `'radio'`) or `selection="multiple"` for the
228
228
  * semantics. Composes with `renderOption`. */
229
229
  renderIndicator?: (state: OptionState<V>) => React.ReactNode;
230
+ /** `multiple` only: spell the picks out in the count summary — `3 selected:
231
+ * A, B, C` (labels comma-joined) in place of the bare `3 selected`. Applies
232
+ * to the multi-count case only: `All` stays `All`, a single pick stays its
233
+ * own label, and an empty selection stays the `placeholder`. The list flows
234
+ * into the read-only field, so it ellipsizes at the field's width when long
235
+ * (`3 selected: Engineering, Des… `). `renderSummary` overrides this. */
236
+ verbose?: boolean;
237
+ /** `multiple` only: build the trigger's selection summary text yourself,
238
+ * replacing the built-in "`All` / one label / `N selected`" logic. Receives
239
+ * the resolved selected options (`selected.length` is the count) and runs only
240
+ * while something is selected — an empty selection still shows the
241
+ * `placeholder`. Return a **string**: it flows into the default trigger's
242
+ * read-only field, so a long summary ellipsizes at the field's width just
243
+ * like a long value (`Engineering, Design, … `). Return `undefined` to fall
244
+ * back to the default for that case (e.g. customize only the count, keeping
245
+ * the single-label case built-in). For rich content (chips, multiple nodes)
246
+ * use `renderTrigger`, which replaces the whole field. */
247
+ renderSummary?: (selected: SelectOption<V>[]) => string | undefined;
230
248
  /** Render your own trigger in place of the default field. Receives a
231
249
  * `TriggerState` (`open` / `value` / `selected` / `disabled` / `icon`) to drive
232
250
  * its look. **Return exactly one focusable element** (an Anta `Button`, say) —
@@ -62,6 +62,8 @@ const Select = (props) => {
62
62
  clearLabel = "Clear",
63
63
  renderOption,
64
64
  renderIndicator,
65
+ verbose,
66
+ renderSummary,
65
67
  renderTrigger,
66
68
  renderEmpty,
67
69
  className,
@@ -112,13 +114,22 @@ const Select = (props) => {
112
114
  const selectedValues = Array.isArray(currentRaw) ? currentRaw : currentRaw != null ? [currentRaw] : [];
113
115
  const isSelected = (v) => selectedValues.includes(v);
114
116
  const selectedOptions = selectedValues.map((v) => byValue.get(v)).filter(Boolean);
117
+ const allEnabledValues = allLeaves.filter((l) => !l.disabled).map((l) => l.opt.value);
118
+ const everythingSelected = multiple && allEnabledValues.length > 1 && allEnabledValues.every((v) => selectedValues.includes(v));
119
+ const labelOf = (o) => o.label ?? String(o.value);
115
120
  let display = "";
116
121
  if (multiple) {
117
- if (selectedOptions.length === 1) display = selectedOptions[0].label ?? String(selectedOptions[0].value);
118
- else if (selectedOptions.length > 1) display = `${selectedOptions.length} selected`;
122
+ const custom = selectedOptions.length > 0 ? renderSummary?.(selectedOptions) : void 0;
123
+ if (custom != null && custom !== "") display = custom;
124
+ else if (everythingSelected) display = "All";
125
+ else if (selectedOptions.length === 1) display = labelOf(selectedOptions[0]);
126
+ else if (selectedOptions.length > 1) {
127
+ const count = `${selectedOptions.length} selected`;
128
+ display = verbose ? `${count}: ${selectedOptions.map(labelOf).join(", ")}` : count;
129
+ }
119
130
  } else if (currentRaw != null) {
120
131
  const o = byValue.get(currentRaw);
121
- display = o ? o.label ?? String(o.value) : "";
132
+ display = o ? labelOf(o) : "";
122
133
  }
123
134
  const choose = (o, e) => {
124
135
  if (multiple) {
@@ -1 +1 @@
1
- .chevron{margin-inline-end:7px;transition:transform .15s ease}.chevron.open{transform:rotate(180deg)}.filter{padding:4px 4px 0}.footer{padding:var(--menu-padding, 4px);border-top:1px solid var(--border-4)}
1
+ .chevron{transition:transform .15s ease}.chevron.open{transform:rotate(180deg)}.filter{padding:4px 4px 0}.footer{padding:var(--menu-padding, 4px);border-top:1px solid var(--border-4)}
@@ -196,7 +196,7 @@ const SelectFaceted = (props) => {
196
196
  const applied = current[facet.key] ?? "";
197
197
  const draft = drafts[facet.key] ?? applied;
198
198
  const apply = () => {
199
- const next = draft.trim() || void 0;
199
+ const next = draft.trim() === "" ? void 0 : draft;
200
200
  if (next !== (applied || void 0)) setFacet(facet, next);
201
201
  };
202
202
  return (
@@ -312,7 +312,7 @@ const SelectFaceted = (props) => {
312
312
  searchable && rootQuery.trim() ? renderFlatResults() : facets.map((facet) => /* @__PURE__ */ jsxs(MenuItem, { submenu: true, icon: facet.icon, label: facet.label, children: [
313
313
  (() => {
314
314
  const s = summaryOf(facet);
315
- return s != null ? /* @__PURE__ */ jsx(Tag, { size: "small", tone: "brand", children: s }) : null;
315
+ return s != null ? /* @__PURE__ */ jsx(Tag, { size: "small", tone: "brand", children: /* @__PURE__ */ jsx("span", { className: styles.summary, children: s }) }) : null;
316
316
  })(),
317
317
  renderEditor(facet)
318
318
  ] }, facet.key)),
@@ -1 +1 @@
1
- .search{padding:var(--menu-padding, 4px)}.footer{padding:var(--menu-padding, 4px);border-top:1px solid var(--border-4)}
1
+ .search{padding:var(--menu-padding, 4px)}.summary{display:block;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.footer{padding:var(--menu-padding, 4px);border-top:1px solid var(--border-4)}
@@ -0,0 +1,48 @@
1
+ import type { BaseProps } from "../general_types";
2
+ import { type Toaster as ToasterStore } from "../toaster";
3
+ export interface ToasterProps extends BaseProps {
4
+ /** The store this region renders. Omit to bind the default store driven by
5
+ * `Toaster.manager`; pass a `createToaster()` for an isolated region. */
6
+ toaster?: ToasterStore;
7
+ /** Accessible label for the region landmark.
8
+ * @defaultValue 'Notifications' */
9
+ label?: string;
10
+ }
11
+ /**
12
+ * `<Toaster>` — the mounted notification region. Keep one mounted somewhere in
13
+ * your app (a portal is fine); toasts are added imperatively through the store
14
+ * (`Toaster.manager`, or a `createToaster()` you pass as `toaster`). It
15
+ * subscribes to the store and renders each toast through the reconciler, so
16
+ * React/Preact owns the toast nodes.
17
+ *
18
+ * Announcement is opt-in **per toast** (`add(render, { politeness })`), not a
19
+ * blanket live region on the whole toaster.
20
+ *
21
+ * Requires `@antadesign/anta/elements` to be imported (client-side only) to
22
+ * register the underlying custom elements.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <Toaster /> // mount once, keep it mounted
27
+ *
28
+ * // A bare string is auto-wrapped in a dismissible Banner:
29
+ * Toaster.manager.add(() => 'Saved', { placement: 'bottom-right' })
30
+ *
31
+ * // Or toast a Banner yourself and wire its dismiss to the toast:
32
+ * Toaster.manager.add((id) => (
33
+ * <Banner tone="success" message="Saved" onDismiss={() => Toaster.manager.dismiss(id)} />
34
+ * ))
35
+ * ```
36
+ */
37
+ declare const ToasterImpl: ({ toaster, label, className, style, children: _children, ...rest }: ToasterProps) => any;
38
+ /**
39
+ * The `<Toaster>` component, carrying the default toast store as a static:
40
+ * `Toaster.manager.add(render, opts)`. A function-with-a-static (like
41
+ * `Component.displayName`) — no class component, which would couple to a renderer.
42
+ */
43
+ export declare const Toaster: typeof ToasterImpl & {
44
+ /** The default toast store, bound to a `<Toaster>` mounted with no `toaster`. */
45
+ readonly manager: ToasterStore;
46
+ };
47
+ export { createToaster } from "../toaster";
48
+ export type { Toaster as ToasterManager, ToastOptions, ToastPlacement, ToastContent, ToastRender, } from "../toaster";
@@ -0,0 +1,56 @@
1
+ import { jsx } from "@antadesign/anta/jsx-runtime";
2
+ import { useMemo, useSyncExternalStore } from "../jsx-runtime";
3
+ import { createToaster } from "../toaster";
4
+ import { Banner } from "./Banner";
5
+ const ToastItem = ({ entry, store }) => {
6
+ const content = useMemo(() => entry.render(entry.id), [entry.id, entry.rev]);
7
+ const isNode = typeof Node !== "undefined" && content instanceof Node;
8
+ const isText = typeof content === "string" || typeof content === "number";
9
+ const common = {
10
+ slot: entry.placement,
11
+ duration: entry.duration,
12
+ leaving: entry.leaving ? "" : void 0,
13
+ rev: entry.rev,
14
+ // Opt-in per-toast announcement. role=status/alert (not a bare aria-live) so
15
+ // it announces when inserted — a plain aria-live region must pre-exist before
16
+ // its content changes, which a freshly-added toast can't satisfy. Omitted (no
17
+ // announcement) unless requested.
18
+ role: entry.politeness === "assertive" ? "alert" : entry.politeness === "polite" ? "status" : void 0,
19
+ // Fires after the exit animation; that's when the entry actually leaves.
20
+ ondismiss: () => store.remove(entry.id)
21
+ };
22
+ if (isNode) return /* @__PURE__ */ jsx("a-toast", { ...common, content });
23
+ if (isText)
24
+ return /* @__PURE__ */ jsx("a-toast", { ...common, children: /* @__PURE__ */ jsx(
25
+ Banner,
26
+ {
27
+ round: true,
28
+ message: content,
29
+ dismissed: false,
30
+ onDismiss: () => store.dismiss(entry.id)
31
+ }
32
+ ) });
33
+ return /* @__PURE__ */ jsx("a-toast", { ...common, children: content });
34
+ };
35
+ const ToasterImpl = ({
36
+ toaster,
37
+ label = "Notifications",
38
+ className,
39
+ style,
40
+ // The region's children come from the store, not from JSX.
41
+ children: _children,
42
+ ...rest
43
+ }) => {
44
+ const store = toaster ?? getDefaultStore();
45
+ const entries = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
46
+ return /* @__PURE__ */ jsx("a-toaster", { role: "region", "aria-label": label, class: className, style, ...rest, children: entries.map((entry) => /* @__PURE__ */ jsx(ToastItem, { entry, store }, entry.id)) });
47
+ };
48
+ let defaultStore;
49
+ const getDefaultStore = () => defaultStore ??= createToaster();
50
+ const Toaster = ToasterImpl;
51
+ Object.defineProperty(Toaster, "manager", { get: getDefaultStore, enumerable: true });
52
+ import { createToaster as createToaster2 } from "../toaster";
53
+ export {
54
+ Toaster,
55
+ createToaster2 as createToaster
56
+ };
@@ -0,0 +1 @@
1
+ @layer anta{a-banner{--banner-bg: var(--bg-4);--banner-border: var(--border-4);--banner-text: var(--text-2);display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:4px 1ch;position:relative;box-sizing:border-box;width:100%;min-height:40px;padding:6px 52px;color:var(--banner-text);background:var(--banner-bg);border:0 solid var(--banner-border);border-radius:0;overflow:clip;overflow-clip-margin:3px;interpolate-size:allow-keywords;transition:opacity .16s ease,block-size .2s ease,padding-block .2s ease,border-block-width .2s ease,display .2s allow-discrete}a-banner[round]{--banner-round: attr(round type(<length>));border-radius:var(--banner-round, 999px)}a-banner [slot=close] a-button,a-banner a-button[slot=close]{height:100%;width:40px}a-banner-message{font-size:14px;line-height:20px;font-weight:475;text-align:center}a-banner[tone=brand]{--banner-bg: var(--bg-4-brand);--banner-border: var(--border-4-brand);--banner-text: var(--text-2-brand)}a-banner[tone=info]{--banner-bg: var(--bg-4-info);--banner-border: var(--border-4-info);--banner-text: var(--text-2-info)}a-banner[tone=success]{--banner-bg: var(--bg-4-success);--banner-border: var(--border-4-success);--banner-text: var(--text-2-success)}a-banner[tone=warning]{--banner-bg: var(--bg-4-warning);--banner-border: var(--border-4-warning);--banner-text: var(--text-2-warning)}a-banner[tone=critical]{--banner-bg: var(--bg-4-critical);--banner-border: var(--border-4-critical);--banner-text: var(--text-2-critical)}a-banner[tone]:not([tone=""],[tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--banner-tone-source: attr(tone type(<color>), currentColor);--banner-text: oklch(from var(--banner-tone-source) .5 .125 h);--banner-bg: oklch(from var(--banner-tone-source) .955 .02 h);--banner-border: oklch(from var(--banner-tone-source) .9 .04 h);.dark &{--banner-text: oklch(from var(--banner-tone-source) .77 .11 h);--banner-bg: oklch(from var(--banner-tone-source) .185 .05 h);--banner-border: oklch(from var(--banner-tone-source) .26 .06 h)}}a-banner:not(:defined){opacity:0}a-banner[state=closed],a-banner[default-state=closed],a-banner:state(closed){display:none;block-size:0;min-block-size:0;padding-block:0;border-block-width:0;opacity:0}@media(prefers-reduced-motion:reduce){a-banner{transition:none}}}
@@ -0,0 +1,10 @@
1
+ import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-banner.css';
3
+ export declare class ABannerElement extends HTMLElementBase {
4
+ #private;
5
+ static observedAttributes: string[];
6
+ constructor();
7
+ connectedCallback(): void;
8
+ attributeChangedCallback(name: string): void;
9
+ }
10
+ export declare function register_a_banner(): void;
@@ -0,0 +1,136 @@
1
+ import { HTMLElementBase, installDismissTrigger, parseOpenState } from "../anta_helpers";
2
+ import "./a-banner.css";
3
+ const DISMISS_TRIGGER = "dismissrequest";
4
+ const DISMISS_ATTR = "data-banner-dismiss";
5
+ const SHADOW_STYLE = `
6
+ slot[name="message"], slot:not([name]) { display: contents; }
7
+
8
+ slot[name="actions"] {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 4px;
12
+ flex-shrink: 0;
13
+ }
14
+
15
+ slot[name="close"] {
16
+ position: absolute;
17
+ inset-inline-end: 0;
18
+ inset-block: 0;
19
+ display: grid;
20
+ place-items: center;
21
+ opacity: 0.65;
22
+ transition: opacity 150ms ease;
23
+ }
24
+ slot[name="close"]:hover,
25
+ slot[name="close"]:focus-within {
26
+ opacity: 1;
27
+ }
28
+ `;
29
+ const parseState = parseOpenState;
30
+ class ABannerElement extends HTMLElementBase {
31
+ static observedAttributes = ["state"];
32
+ // Visibility is held here as a custom state (`:state(open)` / `:state(closed)`),
33
+ // NOT a host attribute — declarative-DOM safe (no host mutation, no reconcile
34
+ // churn). This is the ONLY declarative-safe self-hide channel, so a runtime
35
+ // (uncontrolled) ✕ dismiss needs CustomStateSet + the CSS `:state()` selector
36
+ // (Chrome 90+, Safari 17.4+, Firefox 126+); below that the ✕ can't self-hide (an
37
+ // attribute fallback would violate the no-host-mutation rule). Controlled mode
38
+ // degrades everywhere — the `state="closed"` attribute the wrapper sets has the
39
+ // `[state="closed"]` CSS fallback, which needs no custom-state support.
40
+ #internals;
41
+ // `default-state` seeds the uncontrolled intent ONCE, on the first connect; a
42
+ // re-parent (disconnect → reconnect) then keeps the applied state (which lives
43
+ // off-DOM in #internals and survives the move) instead of re-seeding `open` over a
44
+ // banner the user already dismissed. Mirrors a-dialog's #seeded.
45
+ #seeded = false;
46
+ constructor() {
47
+ super();
48
+ this.#internals = typeof this.attachInternals === "function" ? this.attachInternals() : void 0;
49
+ const shadow = this.attachShadow({ mode: "open" });
50
+ const style = document.createElement("style");
51
+ style.textContent = SHADOW_STYLE;
52
+ const messageSlot = document.createElement("slot");
53
+ messageSlot.name = "message";
54
+ messageSlot.setAttribute("part", "message");
55
+ const contentSlot = document.createElement("slot");
56
+ contentSlot.setAttribute("part", "content");
57
+ const actionsSlot = document.createElement("slot");
58
+ actionsSlot.name = "actions";
59
+ actionsSlot.setAttribute("part", "actions");
60
+ const closeSlot = document.createElement("slot");
61
+ closeSlot.name = "close";
62
+ closeSlot.setAttribute("part", "close");
63
+ shadow.append(style, messageSlot, contentSlot, actionsSlot, closeSlot);
64
+ this.addEventListener(DISMISS_TRIGGER, (e) => {
65
+ e.stopPropagation();
66
+ this.#requestDismiss();
67
+ });
68
+ installDismissTrigger(this, DISMISS_ATTR, () => this.#requestDismiss());
69
+ }
70
+ /** Controlled mode: the `state` attribute is present and owns visibility. */
71
+ get #controlled() {
72
+ return this.hasAttribute("state");
73
+ }
74
+ /** The currently *applied* state (read from the custom state — the source of
75
+ * truth for what's painted). Reads open until the element has applied a state.
76
+ * Both `?.` guard engines with ElementInternals but no CustomStateSet (`.states`
77
+ * undefined), where reading it would otherwise throw inside the ✕ click. */
78
+ get #current() {
79
+ return this.#internals?.states?.has("closed") ? "closed" : "open";
80
+ }
81
+ connectedCallback() {
82
+ if (this.#controlled) {
83
+ this.#applyState(parseState(this.getAttribute("state")));
84
+ } else if (!this.#seeded) {
85
+ this.#applyState(this.#seedState());
86
+ this.#seeded = true;
87
+ }
88
+ }
89
+ /** The uncontrolled initial state. A banner is shown by DEFAULT, so `open` unless
90
+ * `default-state="closed"` is explicit. (Unlike a-dialog / a-expander, whose
91
+ * absent default-state means closed — a banner is the inverse, so it can't share
92
+ * `parseState` here. A hand-authored `<a-banner>` with no attributes shows.) */
93
+ #seedState() {
94
+ return this.getAttribute("default-state") === "closed" ? "closed" : "open";
95
+ }
96
+ attributeChangedCallback(name) {
97
+ if (name === "state" && this.#controlled) this.#applyState(parseState(this.getAttribute("state")));
98
+ }
99
+ /** Mirror the state to `:state(open)` / `:state(closed)` (idempotent). The
100
+ * external sheet collapses the host on `:state(closed)`. Off-DOM only. */
101
+ #applyState(state) {
102
+ if (!this.#internals) return;
103
+ try {
104
+ if (state === "closed") {
105
+ this.#internals.states.add("closed");
106
+ this.#internals.states.delete("open");
107
+ } else {
108
+ this.#internals.states.add("open");
109
+ this.#internals.states.delete("closed");
110
+ }
111
+ } catch {
112
+ }
113
+ }
114
+ /** Announce a requested dismiss (cancelable, before applying), then — uncontrolled
115
+ * and un-vetoed — apply it. Controlled: never self-apply; the consumer answers
116
+ * via `state`. See STATEFUL-COMPONENTS.md. */
117
+ #requestDismiss() {
118
+ if (this.#current === "closed") return;
119
+ const ok = this.dispatchEvent(
120
+ new CustomEvent("statechange", { cancelable: true, detail: { next: "closed", prev: "open" } })
121
+ );
122
+ if (this.#controlled) return;
123
+ if (ok) this.#applyState("closed");
124
+ }
125
+ }
126
+ function register_a_banner() {
127
+ if (typeof customElements === "undefined") return;
128
+ if (!customElements.get("a-banner")) {
129
+ customElements.define("a-banner", ABannerElement);
130
+ }
131
+ }
132
+ register_a_banner();
133
+ export {
134
+ ABannerElement,
135
+ register_a_banner
136
+ };
@@ -1 +1 @@
1
- @layer anta{a-input:not(:defined){display:grid;grid-template-columns:minmax(0,1fr);row-gap:4px}a-input:not(:defined)>[slot=leading],a-input:not(:defined)>[slot=trailing],a-input:not(:defined)>[slot=clear],a-input:not(:defined)>[slot=hint]{display:none!important}a-input:not(:defined)>[slot=label]{display:block;color:var(--input-label);font-family:var(--sans-serif);font-size:15px;line-height:20px;font-weight:500}a-input[size=small]:not(:defined)>[slot=label]{font-size:13px;line-height:16px}a-input[size=large]:not(:defined)>[slot=label]{font-size:17px;line-height:22px}a-input:not(:defined):after{content:attr(placeholder);display:block;min-height:28px;line-height:28px;padding-inline:7px;border-radius:4px;box-shadow:inset 0 0 0 .5px var(--input-border);color:var(--input-placeholder);font-family:var(--sans-serif);font-size:15px;font-weight:400;font-feature-settings:"ss02","ss05","tnum";font-variation-settings:"wdth" 100,"slnt" 0,"ital" 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}a-input[value]:not([value=""]):not([type=password]):not(:defined):after{content:attr(value);color:var(--input-text)}a-input[size=small]:not(:defined):after{min-height:24px;line-height:24px;font-size:13px}a-input[size=large]:not(:defined):after{min-height:32px;line-height:32px;font-size:17px}a-input[round]:not(:defined):after{border-radius:var(--input-round, 999px)}a-input{transition:opacity .28s ease-out 40ms}a-input:not(:defined){opacity:.5}@media(prefers-reduced-motion:reduce){a-input{transition:none}}a-input{--input-bg: var(--bg-1);--input-border: var(--border-2);--input-text: var(--text-1);--input-label: var(--text-3);--input-placeholder: var(--text-4);--input-adornment: var(--text-3);--input-hint: var(--text-3);width:100%}a-input[round]{--input-round: attr(round type(<length>), 999px)}a-input[tone]:not([tone=""]):not([status]){--input-tone-source: attr(tone type(<color>), var(--border-2));--input-border: oklch(from var(--input-tone-source) .7 calc(c*.8) h);.dark &{--input-border: oklch(from var(--input-tone-source) .5 calc(c*.8) h)}}a-input[status=critical]{--input-status-source: var(--anta-seed-critical);--input-bg: var(--bg-2-critical);--input-hint: var(--text-2-critical)}a-input[status=warning]{--input-status-source: var(--anta-seed-warning);--input-bg: var(--bg-2-warning);--input-hint: var(--text-2-warning)}a-input[status=success]{--input-status-source: var(--anta-seed-success);--input-bg: var(--bg-2-success);--input-hint: var(--text-2-success)}a-input[status=info]{--input-status-source: var(--anta-seed-info);--input-bg: var(--bg-2-info);--input-hint: var(--text-2-info)}a-input[status=brand]{--input-status-source: var(--anta-seed-brand);--input-bg: var(--bg-2-brand);--input-hint: var(--text-2-brand)}a-input[status]{--input-border: oklch(from var(--input-status-source) .58 c h)}.dark a-input[status]{--input-border: oklch(from var(--input-status-source) .52 c h)}a-input:disabled{--input-bg: #44374b12;--input-border: #44374b26;--input-text: var(--text-4);--input-label: var(--text-4);--input-hint: var(--text-5);cursor:not-allowed}a-input[status]:disabled{--input-bg: #44374b12;--input-border: #44374b26;--input-hint: var(--text-5)}.dark a-input:disabled{--input-bg: #e4d1ef0d;--input-border: #e4d1ef1f}.dark a-input[status]:disabled{--input-bg: #e4d1ef0d;--input-border: #e4d1ef1f;--input-hint: var(--text-5)}a-input[size=small] a-icon{--icon-size: 14px}a-input[size=large] a-icon{--icon-size: 18px}a-input [slot=hint]>a-icon{margin-block-start:1px}a-input[size=small] [slot=hint]>a-icon{margin-block-start:.5px}a-input[size=large] [slot=hint]>a-icon{margin-block-start:1.5px}}
1
+ @layer anta{a-input:not(:defined){display:grid;grid-template-columns:minmax(0,1fr);row-gap:4px}a-input:not(:defined)>[slot=leading],a-input:not(:defined)>[slot=trailing],a-input:not(:defined)>[slot=clear],a-input:not(:defined)>[slot=hint]{display:none!important}a-input:not(:defined)>[slot=label]{display:block;color:var(--input-label);font-family:var(--sans-serif);font-size:15px;line-height:20px;font-weight:500}a-input[size=small]:not(:defined)>[slot=label]{font-size:13px;line-height:16px}a-input[size=large]:not(:defined)>[slot=label]{font-size:17px;line-height:22px}a-input:not(:defined):after{content:attr(placeholder);display:block;min-height:28px;line-height:28px;padding-inline:7px;border-radius:4px;box-shadow:inset 0 0 0 .5px var(--input-border);color:var(--input-placeholder);font-family:var(--sans-serif);font-size:15px;font-weight:400;font-feature-settings:"ss02","ss05","tnum";font-variation-settings:"wdth" 100,"slnt" 0,"ital" 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}a-input[value]:not([value=""]):not([type=password]):not(:defined):after{content:attr(value);color:var(--input-text)}a-input[size=small]:not(:defined):after{min-height:24px;line-height:24px;font-size:13px}a-input[size=large]:not(:defined):after{min-height:32px;line-height:32px;font-size:17px}a-input[round]:not(:defined):after{border-radius:var(--input-round, 999px)}a-input{transition:opacity .28s ease-out 40ms}a-input:not(:defined){opacity:.5}@media(prefers-reduced-motion:reduce){a-input{transition:none}}a-input{--input-bg: var(--bg-1);--input-border: var(--border-2);--input-text: var(--text-1);--input-label: var(--text-3);--input-placeholder: var(--text-4);--input-adornment: var(--text-3);--input-hint: var(--text-3);--input-trailing-inset: 7px;width:100%}a-input:has([slot=trailing] :is(a-button,button)),a-input:has([slot=trailing]:is(a-button,button)){--input-trailing-inset: 0}a-input[round]{--input-round: attr(round type(<length>), 999px)}a-input[tone]:not([tone=""]):not([status]){--input-tone-source: attr(tone type(<color>), var(--border-2));--input-border: oklch(from var(--input-tone-source) .7 calc(c*.8) h);.dark &{--input-border: oklch(from var(--input-tone-source) .5 calc(c*.8) h)}}a-input[status=critical]{--input-status-source: var(--anta-seed-critical);--input-bg: var(--bg-2-critical);--input-hint: var(--text-2-critical)}a-input[status=warning]{--input-status-source: var(--anta-seed-warning);--input-bg: var(--bg-2-warning);--input-hint: var(--text-2-warning)}a-input[status=success]{--input-status-source: var(--anta-seed-success);--input-bg: var(--bg-2-success);--input-hint: var(--text-2-success)}a-input[status=info]{--input-status-source: var(--anta-seed-info);--input-bg: var(--bg-2-info);--input-hint: var(--text-2-info)}a-input[status=brand]{--input-status-source: var(--anta-seed-brand);--input-bg: var(--bg-2-brand);--input-hint: var(--text-2-brand)}a-input[status]{--input-border: oklch(from var(--input-status-source) .58 c h)}.dark a-input[status]{--input-border: oklch(from var(--input-status-source) .52 c h)}a-input:disabled{--input-bg: #44374b12;--input-border: #44374b26;--input-text: var(--text-4);--input-label: var(--text-4);--input-hint: var(--text-5);cursor:not-allowed}a-input[status]:disabled{--input-bg: #44374b12;--input-border: #44374b26;--input-hint: var(--text-5)}.dark a-input:disabled{--input-bg: #e4d1ef0d;--input-border: #e4d1ef1f}.dark a-input[status]:disabled{--input-bg: #e4d1ef0d;--input-border: #e4d1ef1f;--input-hint: var(--text-5)}a-input[size=small] a-icon{--icon-size: 14px}a-input[size=large] a-icon{--icon-size: 18px}a-input [slot=hint]>a-icon{margin-block-start:1px}a-input[size=small] [slot=hint]>a-icon{margin-block-start:.5px}a-input[size=large] [slot=hint]>a-icon{margin-block-start:1.5px}}
@@ -173,6 +173,7 @@ const SHADOW_STYLE = `
173
173
  :host([readonly]) slot[name="clear"] { display: none; }
174
174
 
175
175
  .field.has-leading slot[name="leading"] { margin-inline-start: 7px; }
176
+ .field.has-trailing slot[name="trailing"] { margin-inline-end: var(--input-trailing-inset, 7px); }
176
177
  .field.has-leading input,
177
178
  .field.has-leading textarea { padding-inline-start: 5px; }
178
179
  :host([multiline]) .field.has-leading slot[name="leading"],
@@ -31,7 +31,12 @@ function ensureMenuItemKeyListener(doc) {
31
31
  if (doc.hasKeyListenerForAMenuItem) return;
32
32
  installKeyActivation(doc, {
33
33
  keys: ["Enter", " "],
34
- resolve: (t) => t?.closest?.(MENU_ITEM_SELECTOR),
34
+ resolve: (t) => {
35
+ const el = t;
36
+ if (el?.closest?.('input, textarea, select, a-input, a-input-time, [contenteditable]:not([contenteditable="false"])'))
37
+ return null;
38
+ return el?.closest?.(MENU_ITEM_SELECTOR);
39
+ },
35
40
  blocked: (el) => el.hasAttribute("disabled"),
36
41
  activate: (el) => el.click()
37
42
  });
@@ -1 +1 @@
1
- @layer anta{a-menu:not(:defined){display:none}a-menu{--menu-bg: color-mix(in oklch, var(--bg-1) 90%, transparent);--menu-radius: var(--radius-md, 8px);--menu-border: 1px solid var(--border-3);--menu-shadow: 0 2px 6px color-mix(in oklch, var(--text-1) 8%, transparent), 0 8px 24px color-mix(in oklch, var(--text-1) 10%, transparent);--menu-padding: 4px;--menu-min-width: 88px;--menu-backdrop-filter: blur(20px)}a-menu[round]{--menu-radius: attr(round type(<length>), 20px)}.dark a-menu{--menu-bg: color-mix(in oklch, var(--bg-1) 78%, transparent);--menu-shadow: 0 2px 8px color-mix(in oklch, black 50%, transparent), 0 12px 32px color-mix(in oklch, black 40%, transparent), inset 0 0 0 1px color-mix(in oklch, white 8%, transparent)}}
1
+ @layer anta{a-menu:not(:defined){display:none}a-menu{--menu-bg: color-mix(in oklch, var(--bg-1) 90%, transparent);--menu-radius: var(--radius-md, 8px);--menu-border: 1px solid var(--border-3);--menu-shadow: 0 2px 6px color-mix(in oklch, var(--text-1) 8%, transparent), 0 8px 24px color-mix(in oklch, var(--text-1) 10%, transparent);--menu-padding: 4px;--menu-min-width: 88px;--menu-backdrop-filter: blur(20px);--menu-width-duration: .16s}@media(prefers-reduced-motion:reduce){a-menu{--menu-width-duration: 0}}a-menu[round]{--menu-radius: attr(round type(<length>), 20px)}.dark a-menu{--menu-bg: color-mix(in oklch, var(--bg-1) 78%, transparent);--menu-shadow: 0 2px 8px color-mix(in oklch, black 50%, transparent), 0 12px 32px color-mix(in oklch, black 40%, transparent), inset 0 0 0 1px color-mix(in oklch, white 8%, transparent)}}
@@ -216,6 +216,7 @@ class AMenuElement extends HTMLElementBase {
216
216
  transition:
217
217
  opacity 140ms ease-out,
218
218
  translate 140ms ease-out,
219
+ width var(--menu-width-duration, 160ms) ease,
219
220
  display 140ms allow-discrete,
220
221
  overlay 140ms allow-discrete;
221
222
  }
@@ -227,6 +228,7 @@ class AMenuElement extends HTMLElementBase {
227
228
  transition:
228
229
  opacity 100ms ease-out,
229
230
  translate 100ms ease-out,
231
+ width var(--menu-width-duration, 160ms) ease,
230
232
  display 100ms allow-discrete,
231
233
  overlay 100ms allow-discrete;
232
234
  }
@@ -0,0 +1 @@
1
+ @property --toast-remaining{syntax: "<number>"; inherits: true; initial-value: 1;}@layer anta{a-toast{--toast-dur: .22s}a-toast:not(:defined){display:none}}