@xenosystem/elements-react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +41 -0
  3. package/dist/XenoElement.d.ts +39 -0
  4. package/dist/XenoElement.js +25 -0
  5. package/dist/containers/Avatar.d.ts +42 -0
  6. package/dist/containers/Avatar.js +29 -0
  7. package/dist/containers/Badges.d.ts +61 -0
  8. package/dist/containers/Badges.js +43 -0
  9. package/dist/containers/Card.d.ts +24 -0
  10. package/dist/containers/Card.js +5 -0
  11. package/dist/containers/Chip.d.ts +34 -0
  12. package/dist/containers/Chip.js +17 -0
  13. package/dist/containers/DataTable.d.ts +28 -0
  14. package/dist/containers/DataTable.js +13 -0
  15. package/dist/containers/ListRow.d.ts +37 -0
  16. package/dist/containers/ListRow.js +40 -0
  17. package/dist/containers/MessageBubble.d.ts +33 -0
  18. package/dist/containers/MessageBubble.js +9 -0
  19. package/dist/containers/Panel.d.ts +24 -0
  20. package/dist/containers/Panel.js +6 -0
  21. package/dist/containers/Table.d.ts +39 -0
  22. package/dist/containers/Table.js +20 -0
  23. package/dist/containers/Tile.d.ts +19 -0
  24. package/dist/containers/Tile.js +7 -0
  25. package/dist/content/Callout.d.ts +32 -0
  26. package/dist/content/Callout.js +10 -0
  27. package/dist/content/Caret.d.ts +9 -0
  28. package/dist/content/Caret.js +5 -0
  29. package/dist/content/CodeBlock.d.ts +36 -0
  30. package/dist/content/CodeBlock.js +50 -0
  31. package/dist/content/Collapsible.d.ts +24 -0
  32. package/dist/content/Collapsible.js +19 -0
  33. package/dist/content/InlineCode.d.ts +12 -0
  34. package/dist/content/InlineCode.js +5 -0
  35. package/dist/content/ModelPicker.d.ts +45 -0
  36. package/dist/content/ModelPicker.js +124 -0
  37. package/dist/content/SourceCard.d.ts +23 -0
  38. package/dist/content/SourceCard.js +18 -0
  39. package/dist/content/SourcesDisclosure.d.ts +26 -0
  40. package/dist/content/SourcesDisclosure.js +20 -0
  41. package/dist/controls/Button.d.ts +39 -0
  42. package/dist/controls/Button.js +16 -0
  43. package/dist/controls/ConfirmButton.d.ts +22 -0
  44. package/dist/controls/ConfirmButton.js +22 -0
  45. package/dist/controls/IconButton.d.ts +45 -0
  46. package/dist/controls/IconButton.js +15 -0
  47. package/dist/controls/Switch.d.ts +11 -0
  48. package/dist/controls/Switch.js +23 -0
  49. package/dist/controls/TextInput.d.ts +13 -0
  50. package/dist/controls/TextInput.js +6 -0
  51. package/dist/controls/Textarea.d.ts +8 -0
  52. package/dist/controls/Textarea.js +5 -0
  53. package/dist/controls/ToggleButton.d.ts +23 -0
  54. package/dist/controls/ToggleButton.js +23 -0
  55. package/dist/controls/util.d.ts +48 -0
  56. package/dist/controls/util.js +23 -0
  57. package/dist/forms/Checkbox.d.ts +18 -0
  58. package/dist/forms/Checkbox.js +28 -0
  59. package/dist/forms/RadioGroup.d.ts +51 -0
  60. package/dist/forms/RadioGroup.js +83 -0
  61. package/dist/index.d.ts +75 -0
  62. package/dist/index.js +81 -0
  63. package/dist/layout/ResizablePanel.d.ts +40 -0
  64. package/dist/layout/ResizablePanel.js +108 -0
  65. package/dist/nav/Sidebar.d.ts +60 -0
  66. package/dist/nav/Sidebar.js +84 -0
  67. package/dist/overlays/DateTimePicker.d.ts +71 -0
  68. package/dist/overlays/DateTimePicker.js +191 -0
  69. package/dist/overlays/Menu.d.ts +28 -0
  70. package/dist/overlays/Menu.js +32 -0
  71. package/dist/overlays/MenuItem.d.ts +71 -0
  72. package/dist/overlays/MenuItem.js +15 -0
  73. package/dist/overlays/Modal.d.ts +36 -0
  74. package/dist/overlays/Modal.js +27 -0
  75. package/dist/overlays/PickerField.d.ts +32 -0
  76. package/dist/overlays/PickerField.js +40 -0
  77. package/dist/overlays/PillFilter.d.ts +25 -0
  78. package/dist/overlays/PillFilter.js +41 -0
  79. package/dist/overlays/Popover.d.ts +32 -0
  80. package/dist/overlays/Popover.js +38 -0
  81. package/dist/overlays/Reveal.d.ts +20 -0
  82. package/dist/overlays/Reveal.js +45 -0
  83. package/dist/overlays/SegmentedControl.d.ts +23 -0
  84. package/dist/overlays/SegmentedControl.js +43 -0
  85. package/dist/overlays/Tabs.d.ts +32 -0
  86. package/dist/overlays/Tabs.js +142 -0
  87. package/dist/overlays/Tooltip.d.ts +23 -0
  88. package/dist/overlays/Tooltip.js +61 -0
  89. package/dist/status/ProgressBar.d.ts +20 -0
  90. package/dist/status/ProgressBar.js +11 -0
  91. package/dist/status/Spinner.d.ts +16 -0
  92. package/dist/status/Spinner.js +12 -0
  93. package/dist/status/StatusIndicator.d.ts +19 -0
  94. package/dist/status/StatusIndicator.js +11 -0
  95. package/dist/status/StepTimeline.d.ts +26 -0
  96. package/dist/status/StepTimeline.js +16 -0
  97. package/dist/status/ThinkingCube.d.ts +24 -0
  98. package/dist/status/ThinkingCube.js +11 -0
  99. package/dist/useDialog.d.ts +45 -0
  100. package/dist/useDialog.js +122 -0
  101. package/dist/useGooPill.d.ts +57 -0
  102. package/dist/useGooPill.js +76 -0
  103. package/dist/useMenu.d.ts +50 -0
  104. package/dist/useMenu.js +119 -0
  105. package/dist/useTabs.d.ts +88 -0
  106. package/dist/useTabs.js +93 -0
  107. package/package.json +58 -0
  108. package/src/chrome-separated.css +132 -0
  109. package/src/chrome-unified.css +72 -0
  110. package/src/containers/Badges.css +116 -0
  111. package/src/containers/avatar.css +70 -0
  112. package/src/containers/card.css +75 -0
  113. package/src/containers/chip.css +103 -0
  114. package/src/containers/list-row.css +132 -0
  115. package/src/containers/message-bubble.css +112 -0
  116. package/src/containers/panel.css +120 -0
  117. package/src/containers/table.css +102 -0
  118. package/src/containers/tile.css +17 -0
  119. package/src/content/callout.css +77 -0
  120. package/src/content/caret.css +30 -0
  121. package/src/content/code-block.css +159 -0
  122. package/src/content/collapsible.css +80 -0
  123. package/src/content/inline-code.css +14 -0
  124. package/src/content/model-picker.css +219 -0
  125. package/src/content/source-card.css +99 -0
  126. package/src/content/sources-disclosure.css +118 -0
  127. package/src/controls.css +453 -0
  128. package/src/fonts.css +34 -0
  129. package/src/forms/Checkbox.css +114 -0
  130. package/src/forms/RadioGroup.css +115 -0
  131. package/src/goo.css +97 -0
  132. package/src/icon-motion.css +1909 -0
  133. package/src/layout/ResizablePanel.css +79 -0
  134. package/src/nav/sidebar.css +296 -0
  135. package/src/overlays/date-time-picker.css +238 -0
  136. package/src/overlays/menu.css +195 -0
  137. package/src/overlays/modal.css +167 -0
  138. package/src/overlays/picker-field.css +81 -0
  139. package/src/overlays/pill-filter.css +129 -0
  140. package/src/overlays/reveal.css +56 -0
  141. package/src/overlays/segmented-control.css +185 -0
  142. package/src/overlays/tabs.css +143 -0
  143. package/src/overlays/tooltip.css +126 -0
  144. package/src/scrollbar.css +184 -0
  145. package/src/size.css +179 -0
  146. package/src/status/progress-bar.css +88 -0
  147. package/src/status/spinner.css +33 -0
  148. package/src/status/statusindicator.css +65 -0
  149. package/src/status/steptimeline.css +137 -0
  150. package/src/status/thinkingcube.css +75 -0
  151. package/src/xeno-element.css +60 -0
  152. package/src/xeno-elements.css +71 -0
  153. package/src/xeno-theme.css +99 -0
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2026 XENO Corporation. All rights reserved.
2
+
3
+ PROPRIETARY AND CONFIDENTIAL.
4
+
5
+ This software and its source code are the confidential and proprietary property
6
+ of XENO Corporation. No licence, express or implied, is granted to any person to
7
+ use, copy, modify, merge, publish, distribute, sublicense, or sell copies of this
8
+ software, in whole or in part, without the prior written permission of XENO
9
+ Corporation.
10
+
11
+ Unauthorised copying of this file, via any medium, is strictly prohibited.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
17
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @xenosystem/elements-react
2
+
3
+ **Web + Electron renderer**
4
+
5
+ > Status: 🔵 **planned** — see [`../../SPEC.md`](../../SPEC.md) §13 for the build order.
6
+
7
+ Interprets `@xenosystem/elements` declarations as React components and web components.
8
+
9
+ **Not built yet** — Phase 2. The contract lands first, deliberately (`SPEC.md` §13).
10
+
11
+ ## When it is built, it must
12
+
13
+ 1. **Honour every declared axis.** Silently ignoring one produces an element that looks
14
+ interactive and isn't — the worst failure in the set.
15
+ 2. **Never invent state.** No `pressed` binding in the declaration means pressed looks like idle.
16
+ 3. **Map input modalities** per `docs/STATE-MODEL.md` §3 — including the **agent** row, or
17
+ agent-driven UI diverges from what a human would have seen.
18
+ 4. **Be visually indistinguishable** from every other renderer given the same declaration. Testable, and tested.
19
+ 5. **Add nothing.** No renderer-local colours, timings or easing.
20
+
21
+ ## Hard constraints
22
+
23
+ - **CSS-first, 0 kb runtime.** Axes become `data-*` attributes; bindings become CSS custom
24
+ properties and transitions. This diverges from the 2026 norm (@animateicons, LivelyIcons,
25
+ Itshover all build on `motion/react`) because XENO elements must run inside **sandboxed
26
+ iframes** and survive **web export**, where a JS animation runtime is weight in every sandbox
27
+ and a dependency that cannot be removed later.
28
+ - **`motion` is an opt-in entry point only** (`/motion`), for genuinely orchestrated cases —
29
+ multi-step morphs, physics. ~4.6 kb with `m` + `LazyMotion` vs 34 kb naive. **Never a
30
+ dependency of the base renderer.**
31
+ - **No barrel.** Per-element entry points, `sideEffects: false`. A single index importing
32
+ everything is why `lucide-react` is 2.4 MB — and `lucide-react/dynamic` collapses to an empty
33
+ chunk in production builds.
34
+ - **Base UI is an implementation detail here**, used for composite behaviour and accessibility.
35
+ It is never exposed as the system's foundation. (Radix stalled after the WorkOS acquisition;
36
+ Base UI reached v1.0 in Dec 2025 with full-time MUI engineering.)
37
+ - **Components are generated**, never hand-written. Editing one to fix a glyph means the
38
+ declaration is wrong.
39
+
40
+ ---
41
+ Part of [`xeno-elements`](../../README.md). Visual authority: [`DESIGN_SYSTEM.md`](../../DESIGN_SYSTEM.md) — LOCKED.
@@ -0,0 +1,39 @@
1
+ import type { ReactElement, SVGProps } from 'react';
2
+ import type { ElementDeclaration, ElementState } from '@xenosystem/elements/schema';
3
+ /**
4
+ * `<XenoElement>` — the XENO web renderer.
5
+ *
6
+ * It interprets a declaration (pure DATA) into SVG via the SHARED {@link interpret} core, so it draws
7
+ * byte-for-byte what the reference `renderSvg` serializer draws (SPEC §6: two renderers must be
8
+ * visually indistinguishable). It adds nothing of its own — no colours, no timings.
9
+ *
10
+ * ## Why there is no per-icon component (and no barrel)
11
+ * This is ONE component parameterised by data, not 300 generated components. The failure mode the spec
12
+ * pre-registers — lucide's 2.4 MB barrel — comes from a module that imports every icon. Here the glyph
13
+ * is `decl`, imported per-id from `@xenosystem/elements` (which itself forbids barrelling
14
+ * declarations), so a product pulls in exactly the elements it names and nothing else.
15
+ *
16
+ * ## CSS-first, 0 kb
17
+ * State is not animated in JS. Every axis the element declares becomes a `data-<axis>` attribute on the
18
+ * root; a stylesheet (`./xeno-element.css`) is where token-driven transitions attach. A geometry
19
+ * variant swap (e.g. bookmark off↔on) is re-rendered from `state` — no animation runtime, ever.
20
+ */
21
+ /**
22
+ * Everything an `<svg>` accepts, plus the element's own four. The passthrough matters for one case
23
+ * above all: a glyph rendered NEXT TO its own label is decorative, and a decorative icon must be
24
+ * `aria-hidden` or a screen reader announces the same thing twice. The declaration cannot know which
25
+ * it is — only the call site can — so the caller has to be able to say.
26
+ */
27
+ export interface XenoElementProps extends Omit<SVGProps<SVGSVGElement>, 'ref' | 'strokeWidth' | 'className'> {
28
+ /** The element to render. Import it per-id from `@xenosystem/elements`. */
29
+ readonly decl: ElementDeclaration;
30
+ /** Discrete state. Missing axes default to enabled / off / idle. */
31
+ readonly state?: Partial<ElementState>;
32
+ /** Pixel width/height. Default 24; the viewBox is always the declared one. */
33
+ readonly size?: number;
34
+ /** Override the weight-derived stroke width. */
35
+ readonly strokeWidth?: number;
36
+ /** Extra class on the root `<svg>`. `xeno-element` is always present. */
37
+ readonly className?: string;
38
+ }
39
+ export declare function XenoElement({ decl, state, size, strokeWidth, className, ...rest }: XenoElementProps): ReactElement;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { interpret } from '@xenosystem/generate';
3
+ const Shape = (s, i, morph) => {
4
+ // A solid part is painted and explicitly un-stroked; an outline part omits both and inherits the
5
+ // root's `fill:none; stroke:currentColor`.
6
+ const paint = s.fill !== undefined ? { fill: s.fill, stroke: 'none' } : {};
7
+ if (s.kind === 'rect') {
8
+ return _jsx("rect", { className: "xeno-part", "data-part": i, x: s.x, y: s.y, width: s.w, height: s.h, rx: s.rx, ...paint }, i);
9
+ }
10
+ // A morphable path carries its `d` a SECOND time, as a custom property. The attribute stays the
11
+ // fallback and stays authoritative wherever CSS `d` is unsupported; the property is the thing a
12
+ // stylesheet can interpolate, because changing a custom property is a computed-value change and `d`
13
+ // transitions. React swaps the value on re-render and the browser does the rest — still no runtime.
14
+ const morphStyle = morph ? { '--part-d': `path('${s.d}')` } : undefined;
15
+ return _jsx("path", { className: "xeno-part", "data-part": i, d: s.d, fillRule: s.fillRule, style: morphStyle, ...paint }, i);
16
+ };
17
+ export function XenoElement({ decl, state, size, strokeWidth, className, ...rest }) {
18
+ const scene = interpret(decl, {
19
+ ...(state !== undefined ? { state } : {}),
20
+ ...(size !== undefined ? { size } : {}),
21
+ ...(strokeWidth !== undefined ? { strokeWidth } : {}),
22
+ ...(className !== undefined ? { className } : {}),
23
+ });
24
+ return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: scene.size, height: scene.size, viewBox: scene.viewBox, fill: "none", stroke: "currentColor", strokeWidth: scene.strokeWidth, strokeLinecap: "butt", strokeLinejoin: "round", role: scene.role, "aria-label": scene.label, "data-glyph": scene.glyph, ...(scene.morph ? { 'data-morph': 'on' } : {}), "data-xeno-element": "", className: scene.className ? `xeno-element ${scene.className}` : 'xeno-element', ...scene.data, ...rest, children: scene.shapes.map((s, i) => Shape(s, i, scene.morph)) }));
25
+ }
@@ -0,0 +1,42 @@
1
+ import type { HTMLAttributes, ReactElement } from 'react';
2
+ import type { ElementDeclaration } from '@xenosystem/elements/schema';
3
+ /**
4
+ * `<Avatar>` — a person/agent tile. Grammar-consistent: a rounded SQUARE (`radius.xs`), NEVER a circle.
5
+ * Content resolves by priority — an `<img>` when `src` is given (object-fit: cover), else a glyph
6
+ * (declaration) drawn by the shared renderer when `icon` is given, else uppercase INITIALS derived from
7
+ * `name` (muted ink on the control surface). The root is `role="img"` with an accessible label so a
8
+ * screen reader announces the person once (ARIA hides the leaf content), whatever the visual fallback.
9
+ */
10
+ export interface AvatarProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
11
+ /** Pixel edge length of the square. Default 24. */
12
+ readonly size?: number;
13
+ /** Image source — highest priority; rendered object-fit: cover. */
14
+ readonly src?: string;
15
+ /** Display name — initials fallback, and the accessible label when `alt` is absent. */
16
+ readonly name?: string;
17
+ /** Glyph declaration — the middle fallback, drawn in muted ink. */
18
+ readonly icon?: ElementDeclaration;
19
+ /** Accessible label / image alt. Falls back to `name`, then a generic label. */
20
+ readonly alt?: string;
21
+ }
22
+ export declare function Avatar({ size, src, name, icon, alt, className, style, ...rest }: AvatarProps): ReactElement;
23
+ /** One entry in an {@link AvatarStack} — the same content priority as {@link Avatar}. */
24
+ export interface AvatarStackItem {
25
+ readonly src?: string;
26
+ readonly name?: string;
27
+ readonly icon?: ElementDeclaration;
28
+ }
29
+ /**
30
+ * `<AvatarStack>` — overlapping avatar squares. Each tile is pulled left and carries a 2px `--xeno-canvas`
31
+ * ring so the overlap reads as a stack; earlier tiles sit on top. When there are more items than `max`,
32
+ * a trailing `+N` overflow tile stands in for the remainder.
33
+ */
34
+ export interface AvatarStackProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
35
+ /** People/agents to stack. */
36
+ readonly items: readonly AvatarStackItem[];
37
+ /** Maximum tiles shown before collapsing into a `+N` overflow tile. Default 4. */
38
+ readonly max?: number;
39
+ /** Pixel edge length of every tile. Default 24. */
40
+ readonly size?: number;
41
+ }
42
+ export declare function AvatarStack({ items, max, size, className, style, ...rest }: AvatarStackProps): ReactElement;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { XenoElement } from '../XenoElement.js';
3
+ import { cx } from '../controls/util.js';
4
+ /** First letters of the first and last word (uppercased); a single word yields its first two letters. */
5
+ const initialsOf = (name) => {
6
+ const parts = name.trim().split(/\s+/).filter(Boolean);
7
+ if (parts.length === 0)
8
+ return '';
9
+ if (parts.length === 1) {
10
+ const only = parts[0] ?? '';
11
+ return only.slice(0, 2).toUpperCase();
12
+ }
13
+ const first = parts[0] ?? '';
14
+ const last = parts[parts.length - 1] ?? '';
15
+ return (first.charAt(0) + last.charAt(0)).toUpperCase();
16
+ };
17
+ export function Avatar({ size = 24, src, name, icon, alt, className, style, ...rest }) {
18
+ const variant = src !== undefined ? 'image' : icon !== undefined ? 'icon' : name !== undefined ? 'initials' : 'empty';
19
+ const label = alt ?? name ?? 'Avatar';
20
+ const rootStyle = { ['--xeno-avatar-size']: `${size}px`, ...style };
21
+ const content = src !== undefined ? (_jsx("img", { className: "xeno-avatar-img", src: src, alt: "", draggable: false })) : icon !== undefined ? (_jsx(XenoElement, { decl: icon, size: Math.round(size * 0.58) })) : name !== undefined ? (_jsx("span", { className: "xeno-avatar-initials", "aria-hidden": "true", children: initialsOf(name) })) : null;
22
+ return (_jsx("span", { className: cx('xeno-avatar', className), "data-variant": variant, role: "img", "aria-label": label, style: rootStyle, ...rest, children: content }));
23
+ }
24
+ export function AvatarStack({ items, max = 4, size = 24, className, style, ...rest }) {
25
+ const visible = items.slice(0, max);
26
+ const overflow = items.length - visible.length;
27
+ const rootStyle = { ['--xeno-avatar-size']: `${size}px`, ...style };
28
+ return (_jsxs("div", { className: cx('xeno-avatar-stack', className), role: "group", style: rootStyle, ...rest, children: [visible.map((item, i) => (_jsx(Avatar, { size: size, style: { zIndex: visible.length - i }, ...(item.src !== undefined ? { src: item.src } : {}), ...(item.name !== undefined ? { name: item.name } : {}), ...(item.icon !== undefined ? { icon: item.icon } : {}) }, i))), overflow > 0 ? (_jsx("span", { className: "xeno-avatar xeno-avatar-more", "data-variant": "more", role: "img", "aria-label": `${overflow} more`, style: { ['--xeno-avatar-size']: `${size}px`, zIndex: 0 }, children: _jsxs("span", { className: "xeno-avatar-initials", "aria-hidden": "true", children: ["+", overflow] }) })) : null] }));
29
+ }
@@ -0,0 +1,61 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * Badges — a family of inline, non-interactive markers (SPEC: containers/Badges).
4
+ *
5
+ * These are STATIC glyphs of text: they carry no availability/selection axis (nothing to press or
6
+ * toggle), so the only `data-*` seam is `data-tone` on the tonal members. They stay inside the
7
+ * monochrome shell — surface/border/muted greys — with ONE sanctioned exception: `StatusPill` may
8
+ * carry a status hue. `success`/`warning` arrive as an inline `--xeno-pill-tone` var straight from
9
+ * the status TOKEN (so the value can never drift from the data); `danger` resolves to the theme's
10
+ * `--xeno-danger`. Grammar holds throughout: rounded squares, never circles.
11
+ */
12
+ export interface BadgeProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
13
+ /** The label. Rendered uppercase, mono, 10px. */
14
+ readonly children: ReactNode;
15
+ /** Badge is the neutral base of the family; `StatusPill` adds the semantic tones. */
16
+ readonly tone?: 'neutral';
17
+ }
18
+ /**
19
+ * `<Badge>` — a mono uppercase micro-label on a surface chip (10px / .05em / radius-sm, hairline
20
+ * border, muted foreground). The neutral base of the family.
21
+ */
22
+ export declare function Badge({ children, tone, className, ...rest }: BadgeProps): ReactElement;
23
+ export type StatusTone = 'neutral' | 'success' | 'warning' | 'danger';
24
+ export interface StatusPillProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'style'> {
25
+ readonly children: ReactNode;
26
+ /** Colours the text + border. `neutral` stays greyscale; the rest carry a status hue. */
27
+ readonly tone?: StatusTone;
28
+ /** Prepend a small rounded-square status dot echoing the tone (via `currentColor`). */
29
+ readonly dot?: boolean;
30
+ }
31
+ /**
32
+ * `<StatusPill>` — like {@link Badge}, but a `tone` paints the text + border. `success`/`warning`
33
+ * inject their status token as the inline `--xeno-pill-tone` var; `danger` uses `--xeno-danger`.
34
+ * `data-tone` on the root is the state seam the CSS reads.
35
+ */
36
+ export declare function StatusPill({ children, tone, dot, className, ...rest }: StatusPillProps): ReactElement;
37
+ export interface CountBadgeProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
38
+ /** The number to show. Hidden entirely when `<= 0` unless `showZero`. */
39
+ readonly count: number;
40
+ /** Keep the badge (showing `0`) at a zero count. */
41
+ readonly showZero?: boolean;
42
+ /** Cap the display — anything above renders as `{max}+`. Defaults to 99. */
43
+ readonly max?: number;
44
+ }
45
+ /**
46
+ * `<CountBadge>` — a tiny numeric chip (min-width square, radius-sm, tabular figures) for counts and
47
+ * unread indicators. Renders nothing at zero unless `showZero`, and caps large values at `{max}+`.
48
+ */
49
+ export declare function CountBadge({ count, showZero, max, className, ...rest }: CountBadgeProps): ReactElement | null;
50
+ export interface CitationBadgeProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
51
+ /** The citation number. */
52
+ readonly index: number;
53
+ /** The source label before the number — the `XS` in `[XS:n]`. Defaults to `XS`. */
54
+ readonly label?: string;
55
+ }
56
+ /**
57
+ * `<CitationBadge>` — an inline monospace source marker in the `[XS:n]` shape, subtle by default
58
+ * (surface chip, hairline border, muted brackets) with the index emphasised to text colour. Sized to
59
+ * sit on a line of running text.
60
+ */
61
+ export declare function CitationBadge({ index, label, className, ...rest }: CitationBadgeProps): ReactElement;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { status } from '@xenosystem/elements/tokens';
3
+ import { cx } from '../controls/util.js';
4
+ /**
5
+ * `<Badge>` — a mono uppercase micro-label on a surface chip (10px / .05em / radius-sm, hairline
6
+ * border, muted foreground). The neutral base of the family.
7
+ */
8
+ export function Badge({ children, tone = 'neutral', className, ...rest }) {
9
+ return (_jsx("span", { className: cx('xeno-badge', className), "data-tone": tone, ...rest, children: children }));
10
+ }
11
+ /**
12
+ * `<StatusPill>` — like {@link Badge}, but a `tone` paints the text + border. `success`/`warning`
13
+ * inject their status token as the inline `--xeno-pill-tone` var; `danger` uses `--xeno-danger`.
14
+ * `data-tone` on the root is the state seam the CSS reads.
15
+ */
16
+ export function StatusPill({ children, tone = 'neutral', dot = false, className, ...rest }) {
17
+ // success/warning are not theme vars — resolve them from the status TOKEN into an inline CSS var.
18
+ // neutral/danger need no inline var (neutral = the base greys, danger = --xeno-danger in CSS).
19
+ const toneStyle = tone === 'success'
20
+ ? { ['--xeno-pill-tone']: status.success }
21
+ : tone === 'warning'
22
+ ? { ['--xeno-pill-tone']: status.warning }
23
+ : undefined;
24
+ return (_jsxs("span", { className: cx('xeno-statuspill', className), "data-tone": tone, ...(toneStyle ? { style: toneStyle } : {}), ...rest, children: [dot && _jsx("span", { className: "xeno-statuspill-dot", "aria-hidden": "true" }), children] }));
25
+ }
26
+ /**
27
+ * `<CountBadge>` — a tiny numeric chip (min-width square, radius-sm, tabular figures) for counts and
28
+ * unread indicators. Renders nothing at zero unless `showZero`, and caps large values at `{max}+`.
29
+ */
30
+ export function CountBadge({ count, showZero = false, max = 99, className, ...rest }) {
31
+ if (count <= 0 && !showZero)
32
+ return null;
33
+ const label = count > max ? `${max}+` : String(count);
34
+ return (_jsx("span", { className: cx('xeno-countbadge', className), ...rest, children: label }));
35
+ }
36
+ /**
37
+ * `<CitationBadge>` — an inline monospace source marker in the `[XS:n]` shape, subtle by default
38
+ * (surface chip, hairline border, muted brackets) with the index emphasised to text colour. Sized to
39
+ * sit on a line of running text.
40
+ */
41
+ export function CitationBadge({ index, label = 'XS', className, ...rest }) {
42
+ return (_jsxs("span", { className: cx('xeno-citation', className), ...rest, children: ["[", label, ":", _jsx("span", { className: "xeno-citation-index", children: index }), "]"] }));
43
+ }
@@ -0,0 +1,24 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * `<Card>` — the base surface box (SPEC: containers). A monochrome pane on `--xeno-surface` with a
4
+ * hairline border and card radius. It is NOT a control: it carries no availability/selection axis. Its
5
+ * one stateful seam is `interactive` — the ONLY case XENO permits a hover-lift (the shell otherwise kills
6
+ * hover motion; lifts are reserved for artifact-like affordances), exposed as `data-interactive` so the
7
+ * lift lives entirely in CSS behind `prefers-reduced-motion`.
8
+ */
9
+ export type CardVariant = 'flat' | 'elevated';
10
+ export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
11
+ /** `flat` sits on `--xeno-surface`; `elevated` swaps to `--xeno-elevated` + a soft shadow. */
12
+ readonly variant?: CardVariant;
13
+ /** Opt in to the hover-lift (translateY + border brighten). Off by default — XENO kills hover motion. */
14
+ readonly interactive?: boolean;
15
+ /** Lift strength when interactive: `subtle` (default, -1px) or `strong` (-3px + soft shadow, the
16
+ * artifact-card affordance). */
17
+ readonly lift?: 'subtle' | 'strong';
18
+ /** Optional slot above the body, split by a hairline. */
19
+ readonly header?: ReactNode;
20
+ /** Optional slot below the body, split by a hairline. */
21
+ readonly footer?: ReactNode;
22
+ readonly children?: ReactNode;
23
+ }
24
+ export declare function Card({ variant, interactive, lift, header, footer, className, children, ...rest }: CardProps): ReactElement;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../controls/util.js';
3
+ export function Card({ variant = 'flat', interactive = false, lift = 'subtle', header, footer, className, children, ...rest }) {
4
+ return (_jsxs("div", { className: cx('xeno-card', className), "data-variant": variant, "data-interactive": interactive ? 'true' : 'false', "data-lift": lift, ...rest, children: [header !== undefined && _jsx("div", { className: "xeno-card-header", children: header }), _jsx("div", { className: "xeno-card-body", children: children }), footer !== undefined && _jsx("div", { className: "xeno-card-footer", children: footer })] }));
5
+ }
@@ -0,0 +1,34 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ import type { ElementDeclaration } from '@xenosystem/elements/schema';
3
+ /**
4
+ * `<Chip>` — a small SQUARE token (radius-control, 1px border) that carries a label with an optional
5
+ * leading glyph and an optional remove `x`. It honours the `selection` axis via `data-selection`
6
+ * (`on` fills with `var(--xeno-control)`) — the SAME axis the bookmark icon and ToggleButton use.
7
+ *
8
+ * Two shapes fall out of one element:
9
+ * - a display/removable token — the body is a plain `<span>`; `onRemove` adds a bare `x` button;
10
+ * - a filter/toggle chip — supply `onSelectedChange` and the body becomes a `role`-correct toggle
11
+ * `<button aria-pressed>` (native Enter/Space), with the remove `x` a SIBLING button so no
12
+ * interactive element is ever nested inside another.
13
+ *
14
+ * Sizes come from the design tokens (`sizeAttr` + `size.css`), so a chip's metrics can never drift
15
+ * from the data.
16
+ */
17
+ export type ChipSize = 'sm' | 'md';
18
+ export interface ChipProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'style' | 'children'> {
19
+ /** The chip label. */
20
+ readonly children?: ReactNode;
21
+ /** A glyph before the label — imported per-id from `@xenosystem/elements`. */
22
+ readonly leadingIcon?: ElementDeclaration;
23
+ /** Presentational selection — drives `data-selection`. */
24
+ readonly selected?: boolean;
25
+ /** Provide to make the chip a controlled toggle (the body becomes a `button aria-pressed`). */
26
+ readonly onSelectedChange?: (selected: boolean) => void;
27
+ /** Provide to render a trailing remove `x`. */
28
+ readonly onRemove?: () => void;
29
+ /** Accessible name for the remove button. */
30
+ readonly removeLabel?: string;
31
+ readonly size?: ChipSize;
32
+ readonly disabled?: boolean;
33
+ }
34
+ export declare function Chip({ children, leadingIcon, selected, onSelectedChange, onRemove, removeLabel, size, disabled, className, ...rest }: ChipProps): ReactElement;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { XenoElement } from '../XenoElement.js';
3
+ import { sizeAttr, iconPx, cx } from '../controls/util.js';
4
+ import X from '@xenosystem/elements/elements/x';
5
+ export function Chip({ children, leadingIcon, selected = false, onSelectedChange, onRemove, removeLabel = 'Remove', size = 'md', disabled = false, className, ...rest }) {
6
+ const availability = disabled ? 'disabled' : 'enabled';
7
+ const glyph = iconPx(size);
8
+ const selectable = onSelectedChange !== undefined;
9
+ const inner = (_jsxs(_Fragment, { children: [leadingIcon && _jsx(XenoElement, { decl: leadingIcon, size: glyph }), children != null && _jsx("span", { className: "xeno-chip-label", children: children })] }));
10
+ return (_jsxs("span", { className: cx('xeno-chip', className), "data-selection": selected ? 'on' : 'off', "data-availability": availability, "data-removable": onRemove ? 'true' : undefined, ...sizeAttr(size), ...rest, children: [selectable ? (_jsx("button", { type: "button", className: "xeno-chip-body xeno-chip-select", "aria-pressed": selected, disabled: disabled, onClick: (e) => {
11
+ if (!e.defaultPrevented)
12
+ onSelectedChange?.(!selected);
13
+ }, children: inner })) : (_jsx("span", { className: "xeno-chip-body", children: inner })), onRemove && (_jsx("button", { type: "button", className: "xeno-chip-remove", "aria-label": removeLabel, disabled: disabled, onClick: (e) => {
14
+ e.stopPropagation();
15
+ onRemove?.();
16
+ }, children: _jsx(XenoElement, { decl: X, size: Math.max(12, glyph - 2) }) }))] }));
17
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactElement, ReactNode, TableHTMLAttributes } from 'react';
2
+ import { type ColumnAlign } from './Table.js';
3
+ /**
4
+ * `<DataTable>` — the column/row convenience over the {@link Table} primitives. Purely presentational:
5
+ * you hand it `columns` (a header, an optional `render`, an optional `align`) and `rows`; it draws the
6
+ * locked dense table for you. A column with no `render` reads `row[column.key]` verbatim. Row identity
7
+ * comes from `getRowKey` (falls back to the index). When `rows` is empty and `empty` is supplied, a
8
+ * single muted, column-spanning cell is drawn instead of an empty body.
9
+ */
10
+ export interface DataTableColumn<T> {
11
+ /** Stable column id; also the property read from the row when `render` is absent. */
12
+ readonly key: string;
13
+ /** Header cell content. */
14
+ readonly header: ReactNode;
15
+ /** Custom cell renderer — overrides the default `row[key]` lookup. */
16
+ readonly render?: (row: T, rowIndex: number) => ReactNode;
17
+ /** Cell text alignment for both the header and the column's body cells. */
18
+ readonly align?: ColumnAlign;
19
+ }
20
+ export interface DataTableProps<T> extends Omit<TableHTMLAttributes<HTMLTableElement>, 'children'> {
21
+ readonly columns: readonly DataTableColumn<T>[];
22
+ readonly rows: readonly T[];
23
+ /** Row identity — defaults to the row index. */
24
+ readonly getRowKey?: (row: T, index: number) => string | number;
25
+ /** Shown (column-spanning, muted) when `rows` is empty. */
26
+ readonly empty?: ReactNode;
27
+ }
28
+ export declare function DataTable<T>({ columns, rows, getRowKey, empty, ...rest }: DataTableProps<T>): ReactElement;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Table, THead, TBody, Tr, Th, Td } from './Table.js';
3
+ export function DataTable({ columns, rows, getRowKey, empty, ...rest }) {
4
+ return (_jsxs(Table, { ...rest, children: [_jsx(THead, { children: _jsx(Tr, { children: columns.map((col) => (_jsx(Th, { ...(col.align !== undefined ? { align: col.align } : {}), children: col.header }, col.key))) }) }), _jsx(TBody, { children: rows.length === 0 && empty !== undefined ? (_jsx(Tr, { children: _jsx(Td, { className: "xeno-table-empty", colSpan: columns.length, children: empty }) })) : (rows.map((row, rowIndex) => {
5
+ const key = getRowKey ? getRowKey(row, rowIndex) : rowIndex;
6
+ return (_jsx(Tr, { children: columns.map((col) => {
7
+ const content = col.render
8
+ ? col.render(row, rowIndex)
9
+ : row[col.key];
10
+ return (_jsx(Td, { ...(col.align !== undefined ? { align: col.align } : {}), children: content }, col.key));
11
+ }) }, key));
12
+ })) })] }));
13
+ }
@@ -0,0 +1,37 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * `<ListRow>` — a selectable / actionable row in a list. It is the container counterpart to the Tier-1
4
+ * controls: appearance is CSS-first, driven by the `data-selection` and `data-availability` axes, and
5
+ * behaviour is hand-rolled (no Base UI). A row has three slots — a `leading` glyph/avatar, a middle
6
+ * that stacks a `title` over an optional `subtitle` (both truncate), and a `trailing` slot for
7
+ * meta / actions.
8
+ *
9
+ * ## Element & interaction
10
+ * A row is a passive `div` by default. Give it an `onSelect` (or `as="button"`) and it becomes a
11
+ * full-width `button`: hover paints `--xeno-hover`, and the `selection` axis (`data-selection="on"`)
12
+ * paints `--xeno-control`. A native button already fires on Enter/Space; when a row is forced to a
13
+ * `div` while still carrying an `onSelect` (`as="div"`) it gets `role="button"`, a tab stop, and a
14
+ * hand-rolled Enter/Space handler so the keyboard path is identical. Set `option` for listbox rows —
15
+ * the root then reports `role="option"` + `aria-selected`.
16
+ */
17
+ export interface ListRowProps extends Omit<HTMLAttributes<HTMLElement>, 'title' | 'onSelect' | 'children'> {
18
+ /** Leading slot — a glyph, avatar, or any node. Presentational (aria-hidden). */
19
+ readonly leading?: ReactNode;
20
+ /** The primary line. Truncates with an ellipsis. */
21
+ readonly title: ReactNode;
22
+ /** An optional secondary line under the title. Truncates with an ellipsis. */
23
+ readonly subtitle?: ReactNode;
24
+ /** Trailing slot — meta text or an actions cluster, pinned to the end. */
25
+ readonly trailing?: ReactNode;
26
+ /** Persistent selection — paints `--xeno-control` and reflects on the `selection` axis. */
27
+ readonly selected?: boolean;
28
+ /** Activate handler. Its presence makes the row a full-width button unless `as="div"`. */
29
+ readonly onSelect?: () => void;
30
+ /** Force the rendered element. Defaults to `button` when `onSelect` is set, else `div`. */
31
+ readonly as?: 'div' | 'button';
32
+ /** Report the row as a listbox `option` (`role="option"` + `aria-selected`). */
33
+ readonly option?: boolean;
34
+ /** Maps to the `disabled` availability; blocks activation and removes the tab stop. */
35
+ readonly disabled?: boolean;
36
+ }
37
+ export declare function ListRow({ leading, title, subtitle, trailing, selected, onSelect, as, option, disabled, className, onClick, onKeyDown, ...rest }: ListRowProps): ReactElement;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { cx } from '../controls/util.js';
3
+ export function ListRow({ leading, title, subtitle, trailing, selected = false, onSelect, as, option = false, disabled = false, className, onClick, onKeyDown, ...rest }) {
4
+ const interactive = onSelect !== undefined;
5
+ const tag = as ?? (interactive ? 'button' : 'div');
6
+ const availability = disabled ? 'disabled' : 'enabled';
7
+ const activate = () => {
8
+ if (!disabled)
9
+ onSelect?.();
10
+ };
11
+ const body = (_jsxs(_Fragment, { children: [leading !== undefined && (_jsx("span", { className: "xeno-list-row-leading", "aria-hidden": "true", children: leading })), _jsxs("span", { className: "xeno-list-row-body", children: [_jsx("span", { className: "xeno-list-row-title", children: title }), subtitle !== undefined && _jsx("span", { className: "xeno-list-row-subtitle", children: subtitle })] }), trailing !== undefined && _jsx("span", { className: "xeno-list-row-trailing", children: trailing })] }));
12
+ if (tag === 'button') {
13
+ // `onKeyDown` is destructured out of props, so unless it is handed back explicitly it is not in
14
+ // `...rest` either and a caller's handler is silently swallowed — on the DEFAULT branch, since this
15
+ // is the tag chosen whenever `onSelect` is given. A button needs no key handling of its own (Enter
16
+ // and Space already click it), so it is simply passed through.
17
+ return (_jsx("button", { type: "button", className: cx('xeno-list-row', className), "data-selection": selected ? 'on' : 'off', "data-availability": availability, role: option ? 'option' : undefined, "aria-selected": option ? selected : undefined, disabled: disabled, onClick: (e) => {
18
+ onClick?.(e);
19
+ if (!e.defaultPrevented)
20
+ activate();
21
+ }, onKeyDown: onKeyDown, ...rest, children: body }));
22
+ }
23
+ return (_jsx("div", { className: cx('xeno-list-row', className), "data-selection": selected ? 'on' : 'off', "data-availability": availability, role: option ? 'option' : interactive ? 'button' : undefined, "aria-selected": option ? selected : undefined, "aria-disabled": disabled || undefined, tabIndex: interactive && !disabled ? 0 : undefined, onClick: interactive
24
+ ? (e) => {
25
+ onClick?.(e);
26
+ if (!e.defaultPrevented)
27
+ activate();
28
+ }
29
+ : onClick, onKeyDown: interactive
30
+ ? (e) => {
31
+ onKeyDown?.(e);
32
+ if (e.defaultPrevented)
33
+ return;
34
+ if (e.key === 'Enter' || e.key === ' ') {
35
+ e.preventDefault();
36
+ activate();
37
+ }
38
+ }
39
+ : onKeyDown, ...rest, children: body }));
40
+ }
@@ -0,0 +1,33 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * `<MessageBubble>` — the chat message container. One element renders both sides of a conversation and
4
+ * the difference is the locked design, not a variant: a `user` turn is a right-aligned bubble (surface
5
+ * fill, hairline border, card radius with the bottom-right corner squared back to `radius-xs`), while an
6
+ * `assistant` turn is a bare left-aligned prose block — no bubble, monochrome shell, coloured content
7
+ * lives inside `children`. The `actions` slot is a hover action bar that fades in on `:hover`/`:focus`
8
+ * and stays hidden while the turn is `streaming`. The role rides the root as `data-role`.
9
+ */
10
+ export type MessageRole = 'user' | 'assistant';
11
+ export interface MessageBubbleProps extends Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'children'> {
12
+ /** Which side of the conversation this turn is — drives `data-role` and the whole visual treatment. */
13
+ readonly role: MessageRole;
14
+ /** The message body: plain text for a user turn, rendered prose for an assistant turn. */
15
+ readonly children?: ReactNode;
16
+ /**
17
+ * What was sent WITH the message — images, files — shown above the body and OUTSIDE the bubble.
18
+ *
19
+ * Not `children`. The body is the fill: a bubble with a photo inside it puts a hairline border and
20
+ * 12px of surface around a picture that already has its own edges, and a turn that is only an image
21
+ * renders as an empty bubble wearing one. Attachments are their own row, sharing the turn's
22
+ * alignment and nothing else — which is also why a turn with attachments and no text draws no
23
+ * bubble at all.
24
+ */
25
+ readonly attachments?: ReactNode;
26
+ /** A hover action bar (copy / regenerate / …) revealed on hover or focus; hidden while streaming. */
27
+ readonly actions?: ReactNode;
28
+ /** A display timestamp shown in the meta row (e.g. `"2:14 PM"`). */
29
+ readonly timestamp?: string;
30
+ /** While true the turn is still being written — the action bar is suppressed via `data-streaming`. */
31
+ readonly streaming?: boolean;
32
+ }
33
+ export declare function MessageBubble({ role, children, attachments, actions, timestamp, streaming, className, ...rest }: MessageBubbleProps): ReactElement;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../controls/util.js';
3
+ export function MessageBubble({ role, children, attachments, actions, timestamp, streaming = false, className, ...rest }) {
4
+ const hasMeta = actions !== undefined || timestamp !== undefined;
5
+ // An empty bubble is a border and a fill around nothing. A turn that is only an attachment has no
6
+ // body, and should draw none — so the element itself is conditional, not just its contents.
7
+ const hasBody = children !== undefined && children !== null && children !== false && children !== '';
8
+ return (_jsxs("div", { className: cx('xeno-message', className), "data-role": role, ...(streaming ? { 'data-streaming': '' } : {}), ...rest, children: [attachments !== undefined && (_jsx("div", { className: "xeno-message-attachments", children: attachments })), hasBody && _jsx("div", { className: "xeno-message-body", children: children }), hasMeta && (_jsxs("div", { className: "xeno-message-meta", children: [timestamp !== undefined && _jsx("time", { className: "xeno-message-time", children: timestamp }), actions !== undefined && (_jsx("div", { className: "xeno-message-actions", role: "group", "aria-label": "Message actions", children: actions }))] }))] }));
9
+ }
@@ -0,0 +1,24 @@
1
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * `<Panel>` — a section container built to `DESIGN_SYSTEM.md` §3.1 "Panel Structure — Separated
4
+ * Header + Body": a layout-only wrapper holding standalone heading, body and footer slabs, each with
5
+ * its own surface and border, separated by a 4px gap. §3.1 is explicit that they "are NOT merged into
6
+ * a single container."
7
+ *
8
+ * Give the panel a bounded height and the body scrolls on its own (it is a flex column with a
9
+ * `min-height: 0` body).
10
+ *
11
+ * The footer is `space-between`, so two children land dismiss-left / confirm-right — the arrangement
12
+ * the products already use for their dialogs. One child sits left; wrap it in a `<span />` first if it
13
+ * should sit right.
14
+ */
15
+ export interface PanelProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
16
+ /** Heading label (a node, so a badge or glyph can ride along). Rendered uppercase per §4. */
17
+ readonly title?: ReactNode;
18
+ /** Trailing heading slot — buttons, toggles, a menu. */
19
+ readonly actions?: ReactNode;
20
+ /** Footer slab. Symmetric with the heading (§4); two children read as left/right. */
21
+ readonly footer?: ReactNode;
22
+ readonly children?: ReactNode;
23
+ }
24
+ export declare function Panel({ title, actions, footer, className, children, ...rest }: PanelProps): ReactElement;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../controls/util.js';
3
+ export function Panel({ title, actions, footer, className, children, ...rest }) {
4
+ const hasHeader = title !== undefined || actions !== undefined;
5
+ return (_jsxs("div", { className: cx('xeno-panel', className), ...rest, children: [hasHeader && (_jsxs("div", { className: "xeno-panel-header", children: [title !== undefined && _jsx("div", { className: "xeno-panel-title", children: title }), actions !== undefined && _jsx("div", { className: "xeno-panel-actions", children: actions })] })), _jsx("div", { className: "xeno-panel-body", children: children }), footer !== undefined && _jsx("div", { className: "xeno-panel-footer", children: footer })] }));
6
+ }