@xemahq/ui-kernel 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/biome-host/biome-builders.d.ts +21 -0
  6. package/dist/lib/biome-host/biome-builders.d.ts.map +1 -0
  7. package/dist/lib/biome-host/biome-builders.js +25 -0
  8. package/dist/lib/biome-host/biome-builders.js.map +1 -0
  9. package/dist/lib/biome-host/biome-navigation.d.ts +3 -0
  10. package/dist/lib/biome-host/biome-navigation.d.ts.map +1 -0
  11. package/dist/lib/biome-host/biome-navigation.js +14 -0
  12. package/dist/lib/biome-host/biome-navigation.js.map +1 -0
  13. package/dist/lib/biome-host/biome-scope.d.ts +18 -0
  14. package/dist/lib/biome-host/biome-scope.d.ts.map +1 -0
  15. package/dist/lib/biome-host/biome-scope.js +42 -0
  16. package/dist/lib/biome-host/biome-scope.js.map +1 -0
  17. package/dist/lib/biome-host/biome-scoped-query.d.ts +17 -0
  18. package/dist/lib/biome-host/biome-scoped-query.d.ts.map +1 -0
  19. package/dist/lib/biome-host/biome-scoped-query.js +39 -0
  20. package/dist/lib/biome-host/biome-scoped-query.js.map +1 -0
  21. package/dist/lib/biome-host/host-bridge.d.ts +3 -0
  22. package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
  23. package/dist/lib/biome-host/host-bridge.js.map +1 -1
  24. package/dist/lib/biome-host/index.d.ts +4 -0
  25. package/dist/lib/biome-host/index.d.ts.map +1 -1
  26. package/dist/lib/biome-host/index.js +4 -0
  27. package/dist/lib/biome-host/index.js.map +1 -1
  28. package/dist/lib/capabilities/capability-provider.d.ts +15 -0
  29. package/dist/lib/capabilities/capability-provider.d.ts.map +1 -0
  30. package/dist/lib/capabilities/capability-provider.js +36 -0
  31. package/dist/lib/capabilities/capability-provider.js.map +1 -0
  32. package/dist/lib/capabilities/index.d.ts +4 -0
  33. package/dist/lib/capabilities/index.d.ts.map +1 -0
  34. package/dist/lib/capabilities/index.js +20 -0
  35. package/dist/lib/capabilities/index.js.map +1 -0
  36. package/dist/lib/capabilities/types.d.ts +18 -0
  37. package/dist/lib/capabilities/types.d.ts.map +1 -0
  38. package/dist/lib/capabilities/types.js +3 -0
  39. package/dist/lib/capabilities/types.js.map +1 -0
  40. package/dist/lib/capabilities/use-capability.d.ts +18 -0
  41. package/dist/lib/capabilities/use-capability.d.ts.map +1 -0
  42. package/dist/lib/capabilities/use-capability.js +21 -0
  43. package/dist/lib/capabilities/use-capability.js.map +1 -0
  44. package/dist/session/shell/SessionWorkspaceShell.js +1 -1
  45. package/dist/session/shell/SessionWorkspaceShell.js.map +1 -1
  46. package/dist/session-kit/display/ThinkingPanel.d.ts.map +1 -1
  47. package/dist/session-kit/display/ThinkingPanel.js +3 -0
  48. package/dist/session-kit/display/ThinkingPanel.js.map +1 -1
  49. package/dist/ui/chrome/AsyncBoundary.d.ts +22 -0
  50. package/dist/ui/chrome/AsyncBoundary.d.ts.map +1 -0
  51. package/dist/ui/chrome/AsyncBoundary.js +23 -0
  52. package/dist/ui/chrome/AsyncBoundary.js.map +1 -0
  53. package/dist/ui/chrome/EmptyState.d.ts +34 -0
  54. package/dist/ui/chrome/EmptyState.d.ts.map +1 -0
  55. package/dist/ui/chrome/EmptyState.js +27 -0
  56. package/dist/ui/chrome/EmptyState.js.map +1 -0
  57. package/dist/ui/chrome/ErrorCard.d.ts +11 -0
  58. package/dist/ui/chrome/ErrorCard.d.ts.map +1 -0
  59. package/dist/ui/chrome/ErrorCard.js +21 -0
  60. package/dist/ui/chrome/ErrorCard.js.map +1 -0
  61. package/dist/ui/chrome/LoadingState.d.ts +10 -0
  62. package/dist/ui/chrome/LoadingState.d.ts.map +1 -0
  63. package/dist/ui/chrome/LoadingState.js +17 -0
  64. package/dist/ui/chrome/LoadingState.js.map +1 -0
  65. package/dist/ui/chrome/PageHeader.d.ts +20 -0
  66. package/dist/ui/chrome/PageHeader.d.ts.map +1 -0
  67. package/dist/ui/chrome/PageHeader.js +26 -0
  68. package/dist/ui/chrome/PageHeader.js.map +1 -0
  69. package/dist/ui/chrome/StateCard.d.ts +24 -0
  70. package/dist/ui/chrome/StateCard.d.ts.map +1 -0
  71. package/dist/ui/chrome/StateCard.js +17 -0
  72. package/dist/ui/chrome/StateCard.js.map +1 -0
  73. package/dist/ui/cn.d.ts +3 -0
  74. package/dist/ui/cn.d.ts.map +1 -0
  75. package/dist/ui/cn.js +18 -0
  76. package/dist/ui/cn.js.map +1 -0
  77. package/dist/ui/index.d.ts +33 -0
  78. package/dist/ui/index.d.ts.map +1 -0
  79. package/dist/ui/index.js +61 -0
  80. package/dist/ui/index.js.map +1 -0
  81. package/dist/ui/primitives/alert-dialog.d.ts +21 -0
  82. package/dist/ui/primitives/alert-dialog.d.ts.map +1 -0
  83. package/dist/ui/primitives/alert-dialog.js +72 -0
  84. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  85. package/dist/ui/primitives/badge.d.ts +10 -0
  86. package/dist/ui/primitives/badge.d.ts.map +1 -0
  87. package/dist/ui/primitives/badge.js +60 -0
  88. package/dist/ui/primitives/badge.js.map +1 -0
  89. package/dist/ui/primitives/button.d.ts +12 -0
  90. package/dist/ui/primitives/button.d.ts.map +1 -0
  91. package/dist/ui/primitives/button.js +71 -0
  92. package/dist/ui/primitives/button.js.map +1 -0
  93. package/dist/ui/primitives/card.d.ts +9 -0
  94. package/dist/ui/primitives/card.d.ts.map +1 -0
  95. package/dist/ui/primitives/card.js +58 -0
  96. package/dist/ui/primitives/card.js.map +1 -0
  97. package/dist/ui/primitives/checkbox.d.ts +5 -0
  98. package/dist/ui/primitives/checkbox.d.ts.map +1 -0
  99. package/dist/ui/primitives/checkbox.js +45 -0
  100. package/dist/ui/primitives/checkbox.js.map +1 -0
  101. package/dist/ui/primitives/collapsible.d.ts +6 -0
  102. package/dist/ui/primitives/collapsible.d.ts.map +1 -0
  103. package/dist/ui/primitives/collapsible.js +44 -0
  104. package/dist/ui/primitives/collapsible.js.map +1 -0
  105. package/dist/ui/primitives/dialog.d.ts +22 -0
  106. package/dist/ui/primitives/dialog.d.ts.map +1 -0
  107. package/dist/ui/primitives/dialog.js +68 -0
  108. package/dist/ui/primitives/dialog.js.map +1 -0
  109. package/dist/ui/primitives/dropdown-menu.d.ts +28 -0
  110. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  111. package/dist/ui/primitives/dropdown-menu.js +83 -0
  112. package/dist/ui/primitives/dropdown-menu.js.map +1 -0
  113. package/dist/ui/primitives/input.d.ts +4 -0
  114. package/dist/ui/primitives/input.d.ts.map +1 -0
  115. package/dist/ui/primitives/input.js +45 -0
  116. package/dist/ui/primitives/input.js.map +1 -0
  117. package/dist/ui/primitives/label.d.ts +6 -0
  118. package/dist/ui/primitives/label.d.ts.map +1 -0
  119. package/dist/ui/primitives/label.js +46 -0
  120. package/dist/ui/primitives/label.js.map +1 -0
  121. package/dist/ui/primitives/overflow-tabs.d.ts +18 -0
  122. package/dist/ui/primitives/overflow-tabs.d.ts.map +1 -0
  123. package/dist/ui/primitives/overflow-tabs.js +84 -0
  124. package/dist/ui/primitives/overflow-tabs.js.map +1 -0
  125. package/dist/ui/primitives/popover.d.ts +9 -0
  126. package/dist/ui/primitives/popover.d.ts.map +1 -0
  127. package/dist/ui/primitives/popover.js +48 -0
  128. package/dist/ui/primitives/popover.js.map +1 -0
  129. package/dist/ui/primitives/radio-group.d.ts +6 -0
  130. package/dist/ui/primitives/radio-group.d.ts.map +1 -0
  131. package/dist/ui/primitives/radio-group.js +52 -0
  132. package/dist/ui/primitives/radio-group.js.map +1 -0
  133. package/dist/ui/primitives/resizable.d.ts +12 -0
  134. package/dist/ui/primitives/resizable.d.ts.map +1 -0
  135. package/dist/ui/primitives/resizable.js +18 -0
  136. package/dist/ui/primitives/resizable.js.map +1 -0
  137. package/dist/ui/primitives/scroll-area.d.ts +6 -0
  138. package/dist/ui/primitives/scroll-area.d.ts.map +1 -0
  139. package/dist/ui/primitives/scroll-area.js +47 -0
  140. package/dist/ui/primitives/scroll-area.js.map +1 -0
  141. package/dist/ui/primitives/select.d.ts +14 -0
  142. package/dist/ui/primitives/select.d.ts.map +1 -0
  143. package/dist/ui/primitives/select.js +71 -0
  144. package/dist/ui/primitives/select.js.map +1 -0
  145. package/dist/ui/primitives/separator.d.ts +5 -0
  146. package/dist/ui/primitives/separator.d.ts.map +1 -0
  147. package/dist/ui/primitives/separator.js +44 -0
  148. package/dist/ui/primitives/separator.js.map +1 -0
  149. package/dist/ui/primitives/sheet.d.ts +26 -0
  150. package/dist/ui/primitives/sheet.d.ts.map +1 -0
  151. package/dist/ui/primitives/sheet.js +82 -0
  152. package/dist/ui/primitives/sheet.js.map +1 -0
  153. package/dist/ui/primitives/skeleton.d.ts +13 -0
  154. package/dist/ui/primitives/skeleton.d.ts.map +1 -0
  155. package/dist/ui/primitives/skeleton.js +29 -0
  156. package/dist/ui/primitives/skeleton.js.map +1 -0
  157. package/dist/ui/primitives/switch.d.ts +5 -0
  158. package/dist/ui/primitives/switch.d.ts.map +1 -0
  159. package/dist/ui/primitives/switch.js +44 -0
  160. package/dist/ui/primitives/switch.js.map +1 -0
  161. package/dist/ui/primitives/table.d.ts +11 -0
  162. package/dist/ui/primitives/table.d.ts.map +1 -0
  163. package/dist/ui/primitives/table.js +64 -0
  164. package/dist/ui/primitives/table.js.map +1 -0
  165. package/dist/ui/primitives/tabs.d.ts +8 -0
  166. package/dist/ui/primitives/tabs.d.ts.map +1 -0
  167. package/dist/ui/primitives/tabs.js +52 -0
  168. package/dist/ui/primitives/tabs.js.map +1 -0
  169. package/dist/ui/primitives/tag-multi-select.d.ts +19 -0
  170. package/dist/ui/primitives/tag-multi-select.d.ts.map +1 -0
  171. package/dist/ui/primitives/tag-multi-select.js +92 -0
  172. package/dist/ui/primitives/tag-multi-select.js.map +1 -0
  173. package/dist/ui/primitives/textarea.d.ts +5 -0
  174. package/dist/ui/primitives/textarea.d.ts.map +1 -0
  175. package/dist/ui/primitives/textarea.js +45 -0
  176. package/dist/ui/primitives/textarea.js.map +1 -0
  177. package/dist/ui/primitives/tooltip.d.ts +8 -0
  178. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  179. package/dist/ui/primitives/tooltip.js +50 -0
  180. package/dist/ui/primitives/tooltip.js.map +1 -0
  181. package/package.json +27 -4
  182. package/src/index.ts +1 -0
  183. package/src/lib/biome-host/biome-builders.ts +109 -0
  184. package/src/lib/biome-host/biome-navigation.ts +37 -0
  185. package/src/lib/biome-host/biome-scope.tsx +119 -0
  186. package/src/lib/biome-host/biome-scoped-query.ts +130 -0
  187. package/src/lib/biome-host/host-bridge.ts +23 -0
  188. package/src/lib/biome-host/index.ts +4 -0
  189. package/src/lib/capabilities/capability-provider.tsx +95 -0
  190. package/src/lib/capabilities/index.ts +16 -0
  191. package/src/lib/capabilities/types.ts +69 -0
  192. package/src/lib/capabilities/use-capability.ts +72 -0
  193. package/src/session/shell/SessionWorkspaceShell.tsx +2 -2
  194. package/src/session-kit/display/ThinkingPanel.tsx +3 -0
  195. package/src/ui/chrome/AsyncBoundary.tsx +66 -0
  196. package/src/ui/chrome/EmptyState.tsx +184 -0
  197. package/src/ui/chrome/ErrorCard.tsx +68 -0
  198. package/src/ui/chrome/LoadingState.tsx +61 -0
  199. package/src/ui/chrome/PageHeader.tsx +137 -0
  200. package/src/ui/chrome/StateCard.tsx +150 -0
  201. package/src/ui/cn.ts +32 -0
  202. package/src/ui/index.ts +53 -0
  203. package/src/ui/primitives/alert-dialog.tsx +104 -0
  204. package/src/ui/primitives/badge.tsx +32 -0
  205. package/src/ui/primitives/button.tsx +47 -0
  206. package/src/ui/primitives/card.tsx +43 -0
  207. package/src/ui/primitives/checkbox.tsx +26 -0
  208. package/src/ui/primitives/collapsible.tsx +9 -0
  209. package/src/ui/primitives/dialog.tsx +103 -0
  210. package/src/ui/primitives/dropdown-menu.tsx +179 -0
  211. package/src/ui/primitives/input.tsx +22 -0
  212. package/src/ui/primitives/label.tsx +17 -0
  213. package/src/ui/primitives/overflow-tabs.tsx +281 -0
  214. package/src/ui/primitives/popover.tsx +33 -0
  215. package/src/ui/primitives/radio-group.tsx +36 -0
  216. package/src/ui/primitives/resizable.tsx +67 -0
  217. package/src/ui/primitives/scroll-area.tsx +38 -0
  218. package/src/ui/primitives/select.tsx +143 -0
  219. package/src/ui/primitives/separator.tsx +20 -0
  220. package/src/ui/primitives/sheet.tsx +107 -0
  221. package/src/ui/primitives/skeleton.tsx +99 -0
  222. package/src/ui/primitives/switch.tsx +27 -0
  223. package/src/ui/primitives/table.tsx +72 -0
  224. package/src/ui/primitives/tabs.tsx +53 -0
  225. package/src/ui/primitives/tag-multi-select.tsx +241 -0
  226. package/src/ui/primitives/textarea.tsx +21 -0
  227. package/src/ui/primitives/tooltip.tsx +30 -0
  228. package/dist/lib/biome-host/composition-validation.d.ts +0 -22
  229. package/dist/lib/biome-host/composition-validation.d.ts.map +0 -1
  230. package/dist/lib/biome-host/composition-validation.js +0 -127
  231. package/dist/lib/biome-host/composition-validation.js.map +0 -1
  232. package/dist/registry/lib/composition-validation-host.d.ts +0 -3
  233. package/dist/registry/lib/composition-validation-host.d.ts.map +0 -1
  234. package/dist/registry/lib/composition-validation-host.js +0 -10
  235. package/dist/registry/lib/composition-validation-host.js.map +0 -1
@@ -0,0 +1,137 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── PageHeader — INVISIBLE topbar registration ──
3
+ //
4
+ // PageHeader does NOT render a visible header bar. It registers the page's
5
+ // title, description, eyebrow, help-actions, status pill, count, and
6
+ // primary action buttons into the shared page-meta store. The global
7
+ // `AppTopbar` reads from that store and renders them in the topbar
8
+ // (mac-style — the topbar IS the page chrome).
9
+ //
10
+ // Why invisible: stacking a second header row beneath the topbar wastes
11
+ // vertical space and competes with the topbar for attention. Pages get
12
+ // the canvas back; the topbar carries context.
13
+ //
14
+ // When you need a thin in-page row (filter dropdowns, search input,
15
+ // selection-mode toolbar), use `<PageToolbar>` from this same file —
16
+ // that's a real visible row but carries NO title (the topbar still owns
17
+ // it), so there's no visual duplication.
18
+ //
19
+ // Host-agnostic: reaches the page-meta store through the kernel `HostBridge`
20
+ // singleton (NOT a host import), so it bundles cleanly into any biome remote.
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+
23
+ import { useMemo, type ReactNode } from 'react';
24
+
25
+ import { useHostBridge, type PageBackTarget } from '../../lib/biome-host';
26
+ import { cn } from '../cn';
27
+
28
+ interface PageHeaderProps {
29
+ readonly title: string;
30
+ /**
31
+ * Short secondary line. Renders in the topbar info popover (long-form
32
+ * `topbarDescription` wins for the popover if both are set) AND
33
+ * inline next to the title as `topbarMeta` when it's a string.
34
+ */
35
+ readonly description?: string;
36
+ /**
37
+ * Long-form text rendered ONLY in the topbar's info popover (the
38
+ * (i) button). Use when `description` is a counts line and you still
39
+ * want a richer popover blurb.
40
+ */
41
+ readonly topbarDescription?: string;
42
+ /** Mono caption rendered before the title in the topbar info popover. */
43
+ readonly eyebrow?: string;
44
+ /** Bullet list of "what you can do here" shown in the topbar info popover. */
45
+ readonly helpActions?: readonly string[];
46
+ /**
47
+ * Right-aligned topbar action cluster. Use small h-7 buttons / icon
48
+ * buttons. Anything that needs more space (multi-input filter rows,
49
+ * search inputs) belongs in a `<PageToolbar>` inside the page body
50
+ * — not here.
51
+ */
52
+ readonly actions?: ReactNode;
53
+ /**
54
+ * Leftmost back affordance. Renders as an icon-only button before
55
+ * the title in the topbar. Pages with a parent context (a detail
56
+ * page under a list, etc.) set this so the user can return to where
57
+ * they came from. See `app/page-meta.ts` for the full layout
58
+ * contract — back always lives on the left, never inside the page
59
+ * body.
60
+ */
61
+ readonly backTo?: PageBackTarget;
62
+ }
63
+
64
+ /**
65
+ * Invisible primitive — registers page meta with the topbar. Renders
66
+ * nothing visible. Mount once at the top of a page component.
67
+ */
68
+ export default function PageHeader({
69
+ title,
70
+ description,
71
+ topbarDescription,
72
+ eyebrow,
73
+ helpActions,
74
+ actions,
75
+ backTo,
76
+ }: Readonly<PageHeaderProps>) {
77
+ // `topbarActions` is a ReactNode — wrap in useMemo so React doesn't see
78
+ // a new identity every render and re-notify the store needlessly. Same
79
+ // for the meta line.
80
+ const topbarActions = useMemo(() => actions, [actions]);
81
+ const topbarMeta = useMemo<ReactNode>(
82
+ () => (description ? <span>{description}</span> : null),
83
+ [description],
84
+ );
85
+
86
+ // Reach the host page-meta store through the shared bridge singleton — NOT
87
+ // a module-level import — so this component bundles cleanly into an MF
88
+ // remote and still drives the one host topbar store.
89
+ useHostBridge().pageMeta.usePageMeta({
90
+ title,
91
+ eyebrow,
92
+ description: topbarDescription ?? description,
93
+ actions: helpActions,
94
+ topbarActions,
95
+ topbarMeta,
96
+ backTo,
97
+ });
98
+ return null;
99
+ }
100
+
101
+ // ═══════════════════════════════════════════════════════════════════════════
102
+ // ── PageToolbar — thin in-page row for filters / search / sub-tabs ──
103
+ //
104
+ // Use when the page needs a horizontal row of controls that don't fit
105
+ // in the topbar (multi-select filter, search input, bulk-selection
106
+ // toolbar). NEVER include the page title — that's the topbar's job.
107
+ //
108
+ // Default height is `h-9` to match the topbar visual rhythm without
109
+ // looking like a duplicated header. Background is the same paper tone
110
+ // as the page so it reads as a tool strip, not a banner.
111
+ // ═══════════════════════════════════════════════════════════════════════════
112
+
113
+ interface PageToolbarProps {
114
+ /** Left cluster — filters, search, etc. */
115
+ readonly children?: ReactNode;
116
+ /** Right cluster — usually `null`; the primary action lives in the topbar. */
117
+ readonly trailing?: ReactNode;
118
+ readonly className?: string;
119
+ }
120
+
121
+ export function PageToolbar({
122
+ children,
123
+ trailing,
124
+ className,
125
+ }: Readonly<PageToolbarProps>) {
126
+ return (
127
+ <div
128
+ className={cn(
129
+ 'flex shrink-0 items-center gap-2 border-b border-rule/50 bg-paper px-4 py-1.5 sm:px-6',
130
+ className,
131
+ )}
132
+ >
133
+ <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">{children}</div>
134
+ {trailing && <div className="flex shrink-0 items-center gap-1.5">{trailing}</div>}
135
+ </div>
136
+ );
137
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Unified state display card for empty, error-simple, and error-detailed states.
3
+ * Replaces ErrorCard, ErrorDisplay, and EmptyState components.
4
+ * Supports collapsible error details and structured field extraction.
5
+ */
6
+ import { ChevronDown, ChevronRight } from 'lucide-react';
7
+ import React, { useState } from 'react';
8
+
9
+
10
+ import { cn } from '../cn';
11
+
12
+ interface FieldEntry {
13
+ label: string;
14
+ value: string | React.ReactNode;
15
+ }
16
+
17
+ interface StateCardProps {
18
+ /** State variant: 'empty' | 'error-simple' | 'error-detailed' */
19
+ variant: 'empty' | 'error-simple' | 'error-detailed';
20
+ /** Icon React element */
21
+ icon: React.ReactNode;
22
+ /** Primary title */
23
+ title: string;
24
+ /** Description or error message */
25
+ description?: string;
26
+ /** Optional action button */
27
+ action?: {
28
+ label: string;
29
+ onClick: () => void;
30
+ };
31
+ /** Optional error details (for error-detailed variant) */
32
+ errorDetails?: {
33
+ fields?: FieldEntry[];
34
+ stackTrace?: string;
35
+ requestId?: string;
36
+ };
37
+ /** Optional CSS class */
38
+ className?: string;
39
+ }
40
+
41
+ /**
42
+ * Renders a state card (empty/error) with consistent appearance.
43
+ * Supports collapsible error details for debugging.
44
+ */
45
+ export const StateCard: React.FC<StateCardProps> = ({
46
+ variant,
47
+ icon,
48
+ title,
49
+ description,
50
+ action,
51
+ errorDetails,
52
+ className = '',
53
+ }) => {
54
+ const [detailsExpanded, setDetailsExpanded] = useState(false);
55
+
56
+ const isError = variant.startsWith('error');
57
+ const isDetailed = variant === 'error-detailed';
58
+
59
+ return (
60
+ <div className={cn(
61
+ 'flex flex-col items-center justify-center py-14 px-6 rounded-xl border-2',
62
+ isError ? 'border-destructive/20 bg-destructive/[0.03]' : 'border-dashed border-border/50 bg-paper-elev/60',
63
+ className,
64
+ )}>
65
+ <div className={cn(
66
+ 'h-14 w-14 rounded-2xl flex items-center justify-center mb-5',
67
+ isError ? 'bg-destructive/10' : 'bg-primary/10',
68
+ )}>
69
+ <span className={isError ? 'text-destructive' : 'text-primary'}>{icon}</span>
70
+ </div>
71
+
72
+ <h2 className="text-subtitle font-semibold text-ink mb-2 text-center">
73
+ {title}
74
+ </h2>
75
+
76
+ {description && (
77
+ <p className="text-body-1 text-ink-3 max-w-md text-center mb-4 leading-relaxed">
78
+ {description}
79
+ </p>
80
+ )}
81
+
82
+ {/* Error details section */}
83
+ {isDetailed && errorDetails && (
84
+ <div className="mt-4 w-full max-w-md">
85
+ <button
86
+ type="button"
87
+ onClick={() => setDetailsExpanded(!detailsExpanded)}
88
+ className="text-body-1 font-medium text-destructive hover:text-destructive flex items-center gap-1.5 mb-2"
89
+ >
90
+ {detailsExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
91
+ Error Details
92
+ </button>
93
+
94
+ {detailsExpanded && (
95
+ <div className="p-4 bg-card border border-destructive/20 rounded-xl space-y-3">
96
+ {/* Structured fields */}
97
+ {errorDetails.fields && errorDetails.fields.length > 0 && (
98
+ <div className="space-y-1.5">
99
+ {errorDetails.fields.map((field, idx) => (
100
+ <div key={idx} className="text-body-1">
101
+ <span className="font-semibold text-destructive">{field.label}</span>
102
+ <span className="text-ink/70">: {field.value}</span>
103
+ </div>
104
+ ))}
105
+ </div>
106
+ )}
107
+
108
+ {/* Stack trace */}
109
+ {errorDetails.stackTrace && (
110
+ <details className="text-body-1">
111
+ <summary className="font-medium text-destructive cursor-pointer mb-1">
112
+ Stack Trace
113
+ </summary>
114
+ <pre className="bg-destructive/5 p-3 rounded-lg overflow-auto max-h-32 text-destructive whitespace-pre-wrap break-words text-body-1 font-mono">
115
+ {errorDetails.stackTrace}
116
+ </pre>
117
+ </details>
118
+ )}
119
+
120
+ {/* Request ID */}
121
+ {errorDetails.requestId && (
122
+ <div className="text-body-1 pt-2 border-t border-destructive/15">
123
+ <span className="font-medium text-ink-3">Request ID</span>
124
+ <br />
125
+ <code className="text-body-1 text-ink/70 font-mono break-all">{errorDetails.requestId}</code>
126
+ </div>
127
+ )}
128
+ </div>
129
+ )}
130
+ </div>
131
+ )}
132
+
133
+ {/* Action button */}
134
+ {action && (
135
+ <button
136
+ type="button"
137
+ onClick={action.onClick}
138
+ className={cn(
139
+ 'mt-6 px-5 py-2.5 rounded-lg font-medium transition-colors h-10',
140
+ isError
141
+ ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
142
+ : 'bg-primary text-primary-foreground hover:bg-primary/90',
143
+ )}
144
+ >
145
+ {action.label}
146
+ </button>
147
+ )}
148
+ </div>
149
+ );
150
+ };
package/src/ui/cn.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { extendTailwindMerge } from 'tailwind-merge';
3
+
4
+ // The Xema design system defines a custom typography scale in the host
5
+ // Tailwind config (`fontSize` keys: caption / body-1 / body-2 / subtitle /
6
+ // title / display). tailwind-merge doesn't know these keys, so it
7
+ // misclassifies `text-body-1`, `text-caption`, … as text-*color* utilities
8
+ // and silently strips a real `text-*` colour when both land on one element
9
+ // via cn(). Registering them in the `font-size` group keeps conflict
10
+ // resolution correct.
11
+ //
12
+ // This is the SAME config the host shell ships in `@/lib/utils`. The shared
13
+ // primitives moved into this package MUST merge classes identically, so the
14
+ // config is duplicated here (one trivial block) rather than imported from the
15
+ // host — this package stays host-framework-agnostic.
16
+ const twMerge = extendTailwindMerge({
17
+ extend: {
18
+ classGroups: {
19
+ 'font-size': [
20
+ { text: ['caption', 'body-1', 'body-2', 'subtitle', 'title', 'display'] },
21
+ ],
22
+ },
23
+ },
24
+ });
25
+
26
+ /**
27
+ * Tailwind-aware class merge used by every shared UI primitive. Identical in
28
+ * behaviour to the host shell's `cn` (including the custom typography scale).
29
+ */
30
+ export function cn(...inputs: ClassValue[]): string {
31
+ return twMerge(clsx(inputs));
32
+ }
@@ -0,0 +1,53 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // @xemahq/ui-kernel/ui — shared UI surface for biomes
3
+ //
4
+ // Canonical shadcn primitives + Xema chrome, owned by the kernel so biomes
5
+ // stop deep-importing the host shell's `@/components/ui/*` and re-implementing
6
+ // chrome (ErrorCard / EmptyState / StateCard / PageHeader). Host-framework-
7
+ // agnostic: React + radix + cva + lucide only, no Next/Vite/React-Router.
8
+ //
9
+ // The class-merge helper (`cn`) carries the Xema typography scale so merging
10
+ // matches the host shell exactly.
11
+ // ═══════════════════════════════════════════════════════════════════════════
12
+
13
+ export { cn } from './cn';
14
+
15
+ // ── shadcn primitives ──
16
+ export * from './primitives/button';
17
+ export * from './primitives/badge';
18
+ export * from './primitives/input';
19
+ export * from './primitives/label';
20
+ export * from './primitives/select';
21
+ export * from './primitives/dialog';
22
+ export * from './primitives/card';
23
+ export * from './primitives/textarea';
24
+ export * from './primitives/tabs';
25
+ export * from './primitives/dropdown-menu';
26
+ export * from './primitives/skeleton';
27
+ export * from './primitives/checkbox';
28
+ export * from './primitives/table';
29
+ export * from './primitives/tooltip';
30
+ export * from './primitives/switch';
31
+ export * from './primitives/separator';
32
+ export * from './primitives/scroll-area';
33
+ export * from './primitives/alert-dialog';
34
+ export * from './primitives/popover';
35
+ export * from './primitives/collapsible';
36
+ export * from './primitives/resizable';
37
+ export * from './primitives/radio-group';
38
+ export * from './primitives/overflow-tabs';
39
+ export * from './primitives/tag-multi-select';
40
+ export * from './primitives/sheet';
41
+
42
+ // ── Xema chrome ──
43
+ export { default as ErrorCard, type ErrorMessageFormatter } from './chrome/ErrorCard';
44
+ export {
45
+ default as EmptyState,
46
+ type EmptyStateVariant,
47
+ type EmptyStateSize,
48
+ type EmptyStateLinkComponent,
49
+ } from './chrome/EmptyState';
50
+ export { StateCard } from './chrome/StateCard';
51
+ export { default as PageHeader, PageToolbar } from './chrome/PageHeader';
52
+ export { default as LoadingState, type LoadingStateVariant } from './chrome/LoadingState';
53
+ export { default as AsyncBoundary } from './chrome/AsyncBoundary';
@@ -0,0 +1,104 @@
1
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
2
+ import * as React from "react";
3
+
4
+ import { buttonVariants } from './button';
5
+ import { cn } from '../cn';
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root;
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "modal-scrim fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className,
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ));
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "modal-surface fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 p-6 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ));
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
+
46
+ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
48
+ );
49
+ AlertDialogHeader.displayName = "AlertDialogHeader";
50
+
51
+ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
53
+ );
54
+ AlertDialogFooter.displayName = "AlertDialogFooter";
55
+
56
+ const AlertDialogTitle = React.forwardRef<
57
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
58
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
59
+ >(({ className, ...props }, ref) => (
60
+ <AlertDialogPrimitive.Title ref={ref} className={cn("text-subtitle font-semibold", className)} {...props} />
61
+ ));
62
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
63
+
64
+ const AlertDialogDescription = React.forwardRef<
65
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
66
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
67
+ >(({ className, ...props }, ref) => (
68
+ <AlertDialogPrimitive.Description ref={ref} className={cn("text-body-1 text-ink-3", className)} {...props} />
69
+ ));
70
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
71
+
72
+ const AlertDialogAction = React.forwardRef<
73
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
74
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
75
+ >(({ className, ...props }, ref) => (
76
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
77
+ ));
78
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
79
+
80
+ const AlertDialogCancel = React.forwardRef<
81
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
82
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
83
+ >(({ className, ...props }, ref) => (
84
+ <AlertDialogPrimitive.Cancel
85
+ ref={ref}
86
+ className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
87
+ {...props}
88
+ />
89
+ ));
90
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
91
+
92
+ export {
93
+ AlertDialog,
94
+ AlertDialogPortal,
95
+ AlertDialogOverlay,
96
+ AlertDialogTrigger,
97
+ AlertDialogContent,
98
+ AlertDialogHeader,
99
+ AlertDialogFooter,
100
+ AlertDialogTitle,
101
+ AlertDialogDescription,
102
+ AlertDialogAction,
103
+ AlertDialogCancel,
104
+ };
@@ -0,0 +1,32 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+
4
+ import { cn } from '../cn';
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-body-1 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
13
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
14
+ outline: "border-rule bg-paper text-ink",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ },
21
+ );
22
+
23
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
24
+
25
+ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
26
+ ({ className, variant, ...props }, ref) => {
27
+ return <span ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />;
28
+ },
29
+ );
30
+ Badge.displayName = "Badge";
31
+
32
+ export { Badge, badgeVariants };
@@ -0,0 +1,47 @@
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import * as React from "react";
4
+
5
+ import { cn } from '../cn';
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-body-1 font-medium ring-offset-background transition-[background-color,color,border-color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14
+ outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
15
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
16
+ ghost: "text-ink-2 hover:bg-paper-elev hover:text-ink",
17
+ link: "text-primary underline-offset-4 hover:underline",
18
+ },
19
+ size: {
20
+ default: "h-10 px-4 py-2",
21
+ sm: "h-9 rounded-md px-3",
22
+ lg: "h-11 rounded-md px-8",
23
+ icon: "h-10 w-10",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: "default",
28
+ size: "default",
29
+ },
30
+ },
31
+ );
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : "button";
42
+ return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
43
+ },
44
+ );
45
+ Button.displayName = "Button";
46
+
47
+ export { Button, buttonVariants };
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from '../cn';
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
6
+ <div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
7
+ ));
8
+ Card.displayName = "Card";
9
+
10
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
11
+ ({ className, ...props }, ref) => (
12
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
13
+ ),
14
+ );
15
+ CardHeader.displayName = "CardHeader";
16
+
17
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
18
+ ({ className, children, ...props }, ref) => (
19
+ <h3 ref={ref} className={cn("text-display font-semibold leading-none tracking-tight", className)} {...props}>{children}</h3>
20
+ ),
21
+ );
22
+ CardTitle.displayName = "CardTitle";
23
+
24
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
25
+ ({ className, ...props }, ref) => (
26
+ <p ref={ref} className={cn("text-body-1 text-ink-3", className)} {...props} />
27
+ ),
28
+ );
29
+ CardDescription.displayName = "CardDescription";
30
+
31
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
32
+ ({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
33
+ );
34
+ CardContent.displayName = "CardContent";
35
+
36
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
37
+ ({ className, ...props }, ref) => (
38
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
39
+ ),
40
+ );
41
+ CardFooter.displayName = "CardFooter";
42
+
43
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,26 @@
1
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
2
+ import { Check } from "lucide-react";
3
+ import * as React from "react";
4
+
5
+ import { cn } from '../cn';
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
20
+ <Check className="h-4 w-4" />
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ ));
24
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25
+
26
+ export { Checkbox };
@@ -0,0 +1,9 @@
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root;
4
+
5
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
6
+
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };