@lablup/ui-common 0.1.0-alpha.0 → 0.1.0-alpha.10

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 (68) hide show
  1. package/README.md +14 -4
  2. package/dist/assets/components/BaseCard/BaseCard.css +1 -1
  3. package/dist/assets/components/Button/Button.css +1 -1
  4. package/dist/assets/components/DataTable/DataTable.css +1 -1
  5. package/dist/assets/components/Drawer/Drawer.css +1 -1
  6. package/dist/assets/components/EmptyState/EmptyState.css +1 -1
  7. package/dist/assets/components/ErrorState/ErrorState.css +1 -1
  8. package/dist/assets/components/StatCard/StatCard.css +1 -1
  9. package/dist/assets/components/Tabs/Tabs.css +1 -1
  10. package/dist/assets/components/Tooltip/Tooltip.css +1 -1
  11. package/dist/components/Badge/Badge2.js +2 -1
  12. package/dist/components/BaseCard/BaseCard2.js +2 -1
  13. package/dist/components/Button/Button2.js +2 -1
  14. package/dist/components/DataTable/DataTable.d.ts +16 -5
  15. package/dist/components/DataTable/DataTable2.js +33 -65
  16. package/dist/components/DataTable/DataTable2.js.map +1 -1
  17. package/dist/components/Drawer/Drawer2.js +2 -1
  18. package/dist/components/EmptyState/EmptyState.d.ts +10 -3
  19. package/dist/components/EmptyState/EmptyState2.js +33 -49
  20. package/dist/components/EmptyState/EmptyState2.js.map +1 -1
  21. package/dist/components/EmptyState/index.d.ts +6 -1
  22. package/dist/components/ErrorState/ErrorState.d.ts +17 -4
  23. package/dist/components/ErrorState/ErrorState2.js +22 -24
  24. package/dist/components/ErrorState/ErrorState2.js.map +1 -1
  25. package/dist/components/ErrorState/index.d.ts +1 -1
  26. package/dist/components/PageHeader/PageHeader2.js +2 -1
  27. package/dist/components/PageLayout/PageLayout2.js +2 -1
  28. package/dist/components/ProgressBar/ProgressBar2.js +2 -1
  29. package/dist/components/Skeleton/Skeleton2.js +2 -1
  30. package/dist/components/Skeleton/SkeletonCard.d.ts +3 -1
  31. package/dist/components/Skeleton/SkeletonCard2.js +14 -4
  32. package/dist/components/Skeleton/SkeletonCard2.js.map +1 -1
  33. package/dist/components/Skeleton/SkeletonChart2.js +2 -1
  34. package/dist/components/Skeleton/SkeletonRow.d.ts +3 -1
  35. package/dist/components/Skeleton/SkeletonRow2.js +10 -4
  36. package/dist/components/Skeleton/SkeletonRow2.js.map +1 -1
  37. package/dist/components/Skeleton/SkeletonText.d.ts +3 -1
  38. package/dist/components/Skeleton/SkeletonText2.js +8 -6
  39. package/dist/components/Skeleton/SkeletonText2.js.map +1 -1
  40. package/dist/components/SmoothHeight/SmoothHeight2.js +2 -1
  41. package/dist/components/StatCard/StatCard2.js +2 -1
  42. package/dist/components/StatusTag/StatusTag2.js +2 -1
  43. package/dist/components/Tabs/Tabs.d.ts +10 -23
  44. package/dist/components/Tabs/Tabs2.js +129 -147
  45. package/dist/components/Tabs/Tabs2.js.map +1 -1
  46. package/dist/components/Tabs/index.d.ts +2 -2
  47. package/dist/components/Tabs/index.js +2 -3
  48. package/dist/components/Tooltip/Tooltip2.js +2 -1
  49. package/dist/index.d.ts +5 -3
  50. package/dist/index.js +3 -4
  51. package/dist/styles/base.css +130 -48
  52. package/dist/styles/themes/orange-dark.css +3 -0
  53. package/dist/styles/themes/orange-light.css +6 -1
  54. package/package.json +1 -1
  55. package/dist/components/EmptyState/illustrations.d.ts +0 -51
  56. package/dist/components/EmptyState/illustrations.js +0 -571
  57. package/dist/components/EmptyState/illustrations.js.map +0 -1
  58. package/dist/components/Tabs/tabTags.d.ts +0 -9
  59. package/dist/components/Tabs/tabTags.js +0 -11
  60. package/dist/components/Tabs/tabTags.js.map +0 -1
  61. package/dist/styles/themes/bliss-dark.css +0 -123
  62. package/dist/styles/themes/bliss-light.css +0 -123
  63. package/dist/styles/themes/glass-dark.css +0 -124
  64. package/dist/styles/themes/glass-light.css +0 -125
  65. package/dist/styles/themes/reverie-dark.css +0 -104
  66. package/dist/styles/themes/reverie-light.css +0 -104
  67. package/dist/styles/themes/stained-dark.css +0 -80
  68. package/dist/styles/themes/stained-light.css +0 -80
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable2.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * DataTable Component (epic #2730 / issue #2738 · sort: epic #2859 / issue #2861)\n *\n * Generic, type-safe table primitive modeled after the Backend.AI WebUI\n * `BAITable`. Designed so future menus (Models, Sessions, Data, ...)\n * can share one tabular surface instead of inventing a new one each\n * time.\n *\n * Features (V1 — additional capabilities planned for follow-up issues):\n * - Type-safe column definitions with custom cell renderers\n * - Optional column-visibility persistence via `localStorage` when\n * `persistKey` is supplied\n * - Optional resizable columns (drag the right edge of a header cell)\n * with widths persisted alongside visibility\n * - Empty-state slot rendered when `rows` is empty\n * - Loading-state slot rendered when `loading` is `true`\n * - Stable `getRowKey` accessor so rows with the same identifier remain\n * referentially identical across event-driven updates\n *\n * Sorting (V2 — epic #2859 / issue #2861):\n * - Optional per-column sortable flag with sort value accessor or\n * custom comparator\n * - Optional defaultSortDirection per column\n * - Controlled (`sortColumnId` + `sortDirection` + `onSortChange`) and\n * uncontrolled (internal state) modes\n * - Accessible `aria-sort` attributes on header cells\n * - Keyboard activation (Enter / Space) for sortable headers\n * - Stable sort — equal-valued rows retain their original order\n * - Sorting never mutates the original `rows` prop\n *\n * The component is intentionally domain-agnostic — it MUST NOT carry\n * Session, Model, or Agent-specific knowledge. The first consumer is\n * `SessionListTable` (epic #2730 / #2738) but the API is generic.\n */\n\nimport {\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport \"./DataTable.css\";\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/** Direction a column is currently sorted. */\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableColumn<T> {\n /** Unique identifier for the column. Used for visibility + width persistence. */\n id: string;\n /** Header label. */\n header: ReactNode;\n /**\n * Cell renderer. Receives the row and returns a React node. Pure\n * functions are encouraged so memoization stays effective.\n */\n render: (row: T, index: number) => ReactNode;\n /** Optional minimum column width in pixels. Defaults to 80. */\n minWidth?: number;\n /** Optional initial width in pixels (only honored on the first paint). */\n initialWidth?: number;\n /** When `true`, the column cannot be resized. Defaults to `false`. */\n noResize?: boolean;\n /** When `true`, the column cannot be hidden by the user. Defaults to `false`. */\n alwaysVisible?: boolean;\n /** Extra `class` for the column's `<th>` and `<td>` cells. */\n className?: string;\n /**\n * When provided, the value is exposed via the `align` style on the\n * column's cells. Useful for right-aligned numeric columns.\n */\n align?: \"left\" | \"right\" | \"center\";\n\n // ---- Sorting API (added epic #2859 / issue #2861) -------------------------\n\n /**\n * When `true`, the column header renders a sort affordance and clicking\n * (or pressing Enter / Space) cycles through ascending → descending →\n * unsorted states. Defaults to `false`.\n */\n sortable?: boolean;\n /**\n * Extracts a primitive comparable value from a row for sorting purposes.\n * Used when the default ascending/descending behavior is sufficient.\n * Mutually exclusive with `sortComparator` — if both are provided,\n * `sortComparator` takes precedence.\n *\n * @example\n * ```tsx\n * sortValueAccessor: (row) => row.name.toLowerCase()\n * ```\n */\n sortValueAccessor?: (row: T) => string | number | boolean | null | undefined;\n /**\n * Custom comparator used for sorting. Receives two rows and the active\n * sort direction. Return a negative number if `a` should come first, a\n * positive number if `b` should come first, or `0` if they are equal.\n * The table applies a stable fallback on `0` to preserve original order.\n *\n * @example\n * ```tsx\n * sortComparator: (a, b, dir) =>\n * dir === \"asc\"\n * ? a.createdAt - b.createdAt\n * : b.createdAt - a.createdAt\n * ```\n */\n sortComparator?: (a: T, b: T, direction: SortDirection) => number;\n /**\n * The sort direction applied when this column is first activated.\n * Defaults to `\"asc\"`.\n */\n defaultSortDirection?: SortDirection;\n}\n\nexport interface DataTablePersistedState {\n /** column id → user-overridden width in pixels. */\n widths: Record<string, number>;\n /** column id → visibility flag. Missing keys count as visible. */\n visibility: Record<string, boolean>;\n}\n\nexport interface DataTableProps<T> {\n /** Column definitions, in display order. */\n columns: DataTableColumn<T>[];\n /** Row data. */\n rows: T[];\n /**\n * Stable identifier accessor — used as the React key for each row.\n * Must be deterministic for the lifetime of the row.\n */\n getRowKey: (row: T, index: number) => string;\n /** Render this node when `rows` is empty. */\n emptyState?: ReactNode;\n /** Render this node when `loading` is `true`. */\n loadingState?: ReactNode;\n /** When `true`, the loading slot replaces the table body. */\n loading?: boolean;\n /**\n * When provided, column widths and visibility settings are persisted\n * to `localStorage` under this key. Use a stable, namespaced string\n * (e.g. `\"sessions.activeTab\"`).\n */\n persistKey?: string;\n /** Extra class for the table's wrapping element. */\n className?: string;\n /** ARIA label for the table. Defaults to \"Data table\". */\n ariaLabel?: string;\n /** Test ID for testing harnesses. */\n testId?: string;\n /**\n * Callback invoked when a row is clicked. When provided, rows render\n * with a clickable affordance (cursor: pointer + keyboard binding).\n */\n onRowClick?: (row: T) => void;\n\n // ---- Sorting props (added epic #2859 / issue #2861) -----------------------\n\n /**\n * Controlled — the column currently being sorted. Pass `undefined` to\n * indicate no active sort. When this prop is present, the component\n * operates in controlled mode: the caller is responsible for updating\n * the sort state via `onSortChange`.\n */\n sortColumnId?: string;\n /**\n * Controlled — the direction of the active sort. Ignored when\n * `sortColumnId` is `undefined`.\n */\n sortDirection?: SortDirection;\n /**\n * Called when the user activates a sortable column header. In\n * controlled mode the caller must propagate this back as\n * `sortColumnId` / `sortDirection`. In uncontrolled mode this prop is\n * purely observational.\n *\n * @param columnId The column being sorted, or `undefined` when the\n * sort is cleared.\n * @param direction The new direction.\n */\n onSortChange?: (\n columnId: string | undefined,\n direction: SortDirection | undefined,\n ) => void;\n}\n\n// ============================================================================\n// Storage helpers\n// ============================================================================\n\nconst STORAGE_NAMESPACE = \"dataTable\";\n\nfunction buildStorageKey(persistKey: string): string {\n return `${STORAGE_NAMESPACE}.${persistKey}`;\n}\n\n/**\n * Load persisted state from `localStorage`. Returns an empty state on\n * any failure (missing storage, malformed JSON, schema drift) so the\n * component degrades gracefully.\n */\nfunction loadPersistedState(persistKey: string | undefined): DataTablePersistedState {\n if (!persistKey || typeof window === \"undefined\") {\n return { widths: {}, visibility: {} };\n }\n try {\n const raw = window.localStorage.getItem(buildStorageKey(persistKey));\n if (!raw) return { widths: {}, visibility: {} };\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== \"object\" || parsed === null) {\n return { widths: {}, visibility: {} };\n }\n const obj = parsed as { widths?: unknown; visibility?: unknown };\n const widths =\n typeof obj.widths === \"object\" && obj.widths !== null\n ? (obj.widths as Record<string, number>)\n : {};\n const visibility =\n typeof obj.visibility === \"object\" && obj.visibility !== null\n ? (obj.visibility as Record<string, boolean>)\n : {};\n return { widths, visibility };\n } catch {\n return { widths: {}, visibility: {} };\n }\n}\n\nfunction savePersistedState(\n persistKey: string | undefined,\n state: DataTablePersistedState,\n): void {\n if (!persistKey || typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(buildStorageKey(persistKey), JSON.stringify(state));\n } catch {\n // Quota exceeded / disabled storage — silently ignore.\n }\n}\n\n// ============================================================================\n// Sorting helpers\n// ============================================================================\n\n/**\n * Stable sort — rows with equal comparison result keep their original\n * relative order. We achieve stability by augmenting each row with its\n * original index and using that as a tie-breaker.\n *\n * The input array is never mutated.\n */\nfunction stableSort<T>(rows: T[], comparator: (a: T, b: T) => number): T[] {\n const indexed = rows.map((row, i) => ({ row, i }));\n indexed.sort((a, b) => {\n const result = comparator(a.row, b.row);\n return result !== 0 ? result : a.i - b.i;\n });\n return indexed.map(({ row }) => row);\n}\n\n/**\n * Builds a comparator from a `DataTableColumn` + active sort direction.\n * Falls back to string comparison when neither `sortComparator` nor\n * `sortValueAccessor` is provided on the column.\n */\nfunction buildComparator<T>(\n col: DataTableColumn<T>,\n direction: SortDirection,\n): (a: T, b: T) => number {\n const { sortComparator } = col;\n if (sortComparator) {\n return (a, b) => sortComparator(a, b, direction);\n }\n const accessor = col.sortValueAccessor;\n if (accessor) {\n return (a, b) => {\n const av = accessor(a);\n const bv = accessor(b);\n // Nullish values sort last regardless of direction.\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n let cmp: number;\n if (typeof av === \"number\" && typeof bv === \"number\") {\n cmp = av - bv;\n } else {\n cmp = String(av).localeCompare(String(bv));\n }\n return direction === \"asc\" ? cmp : -cmp;\n };\n }\n // No accessor — unsortable in practice (column should not be sortable).\n return () => 0;\n}\n\n/**\n * Returns the `aria-sort` attribute value for a column header cell.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort\n */\nfunction ariaSortValue(\n col: DataTableColumn<unknown>,\n activeSortColumnId: string | undefined,\n activeSortDirection: SortDirection | undefined,\n): \"ascending\" | \"descending\" | \"none\" | undefined {\n if (!col.sortable) return undefined;\n if (col.id === activeSortColumnId) {\n return activeSortDirection === \"asc\" ? \"ascending\" : \"descending\";\n }\n return \"none\";\n}\n\n// Sort indicator icons rendered as inline SVG so there is no external\n// asset dependency. They intentionally reuse design-token colours so they\n// automatically adapt to light / dark themes.\n\nfunction SortIcon({ direction }: { direction: SortDirection | \"none\" }) {\n if (direction === \"none\") {\n // Neutral chevron stack — both arrows in muted colour\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--none\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.35\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.35\" />\n </svg>\n </span>\n );\n }\n if (direction === \"asc\") {\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--asc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.25\" />\n </svg>\n </span>\n );\n }\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--desc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.25\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" />\n </svg>\n </span>\n );\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nfunction DataTableInner<T>({\n columns,\n rows,\n getRowKey,\n emptyState,\n loadingState,\n loading = false,\n persistKey,\n className = \"\",\n ariaLabel = \"Data table\",\n testId,\n onRowClick,\n // Sorting (controlled)\n sortColumnId: controlledSortColumnId,\n sortDirection: controlledSortDirection,\n onSortChange,\n}: DataTableProps<T>) {\n // ---- Persisted state (widths + visibility) -------------------------------\n const [persisted, setPersisted] = useState<DataTablePersistedState>(() =>\n loadPersistedState(persistKey),\n );\n\n // Refresh persisted state when the storage key changes (defensive — the\n // expected use case is a stable key).\n useEffect(() => {\n setPersisted(loadPersistedState(persistKey));\n }, [persistKey]);\n\n // ---- Sorting state (uncontrolled fallback) -------------------------------\n //\n // Controlled mode is detected by the presence of `sortColumnId` as a prop\n // (the caller explicitly manages the sort column). Using only this prop as\n // the signal (rather than OR-ing with `sortDirection`) avoids a\n // pathological state where `sortDirection` is set but `sortColumnId` is\n // not, which would enter controlled mode with no sort effect.\n //\n // Note: TypeScript cannot distinguish \"prop not passed\" from \"prop passed\n // as undefined\", so we rely on the convention that controlled callers\n // always pass both `sortColumnId` and `sortDirection` together.\n const isControlled =\n controlledSortColumnId !== undefined || controlledSortDirection !== undefined;\n\n const [internalSortColumnId, setInternalSortColumnId] = useState<string | undefined>(\n undefined,\n );\n const [internalSortDirection, setInternalSortDirection] = useState<\n SortDirection | undefined\n >(undefined);\n\n const activeSortColumnId = isControlled\n ? controlledSortColumnId\n : internalSortColumnId;\n const activeSortDirection = isControlled\n ? controlledSortDirection\n : internalSortDirection;\n\n /** Handle a click / keypress on a sortable column header. */\n const handleSortToggle = useCallback(\n (col: DataTableColumn<T>) => {\n if (!col.sortable) return;\n\n let nextColumnId: string | undefined;\n let nextDirection: SortDirection | undefined;\n\n if (activeSortColumnId !== col.id) {\n // Activate this column in its default direction.\n nextColumnId = col.id;\n nextDirection = col.defaultSortDirection ?? \"asc\";\n } else if (activeSortDirection === (col.defaultSortDirection ?? \"asc\")) {\n // Flip to the opposite direction.\n nextColumnId = col.id;\n nextDirection = activeSortDirection === \"asc\" ? \"desc\" : \"asc\";\n } else {\n // Already in the non-default direction — clear the sort.\n nextColumnId = undefined;\n nextDirection = undefined;\n }\n\n if (!isControlled) {\n setInternalSortColumnId(nextColumnId);\n setInternalSortDirection(nextDirection);\n }\n onSortChange?.(nextColumnId, nextDirection);\n },\n [activeSortColumnId, activeSortDirection, isControlled, onSortChange],\n );\n\n // ---- Visibility computation (column always-visible wins) -----------------\n const visibleColumns = useMemo(\n () =>\n columns.filter((col) => {\n if (col.alwaysVisible) return true;\n const flag = persisted.visibility[col.id];\n return flag !== false;\n }),\n [columns, persisted.visibility],\n );\n\n // ---- Width resolution ----------------------------------------------------\n const resolveWidth = useCallback(\n (col: DataTableColumn<T>): number | undefined => {\n const persistedWidth = persisted.widths[col.id];\n if (typeof persistedWidth === \"number\" && persistedWidth > 0) {\n return persistedWidth;\n }\n return col.initialWidth;\n },\n [persisted.widths],\n );\n\n // ---- Resize handling -----------------------------------------------------\n const resizeRef = useRef<{\n columnId: string;\n startX: number;\n startWidth: number;\n minWidth: number;\n } | null>(null);\n\n const beginResize = useCallback(\n (\n ev: React.MouseEvent<HTMLDivElement>,\n col: DataTableColumn<T>,\n currentWidth: number,\n ) => {\n if (col.noResize) return;\n ev.preventDefault();\n ev.stopPropagation();\n resizeRef.current = {\n columnId: col.id,\n startX: ev.clientX,\n startWidth: currentWidth,\n minWidth: col.minWidth ?? 80,\n };\n\n const onMove = (e: MouseEvent) => {\n const ref = resizeRef.current;\n if (!ref) return;\n const delta = e.clientX - ref.startX;\n const next = Math.max(ref.minWidth, ref.startWidth + delta);\n setPersisted((prev) => ({\n ...prev,\n widths: { ...prev.widths, [ref.columnId]: next },\n }));\n };\n const onUp = () => {\n resizeRef.current = null;\n window.removeEventListener(\"mousemove\", onMove);\n window.removeEventListener(\"mouseup\", onUp);\n };\n window.addEventListener(\"mousemove\", onMove);\n window.addEventListener(\"mouseup\", onUp);\n },\n [],\n );\n\n // Persist when state changes\n useEffect(() => {\n savePersistedState(persistKey, persisted);\n }, [persistKey, persisted]);\n\n // ---- Sorted rows --------------------------------------------------------\n const sortedRows = useMemo(() => {\n if (!activeSortColumnId || !activeSortDirection) return rows;\n const col = columns.find((c) => c.id === activeSortColumnId);\n if (!col?.sortable) return rows;\n return stableSort(rows, buildComparator(col, activeSortDirection));\n }, [rows, columns, activeSortColumnId, activeSortDirection]);\n\n // ---- Body rendering ------------------------------------------------------\n const composedClassName = [\"data-table\", className].filter(Boolean).join(\" \");\n\n const renderBody = () => {\n if (loading) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {loadingState ?? <div className=\"data-table__loading\" />}\n </td>\n </tr>\n );\n }\n if (sortedRows.length === 0) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {emptyState}\n </td>\n </tr>\n );\n }\n return sortedRows.map((row, index) => {\n const key = getRowKey(row, index);\n return (\n <tr\n key={key}\n className={[\"data-table__row\", onRowClick && \"data-table__row--clickable\"]\n .filter(Boolean)\n .join(\" \")}\n onClick={\n onRowClick\n ? () => {\n onRowClick(row);\n }\n : undefined\n }\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(row);\n }\n }\n : undefined\n }\n tabIndex={onRowClick ? 0 : undefined}\n role={onRowClick ? \"button\" : undefined}\n >\n {visibleColumns.map((col) => (\n <td\n key={col.id}\n className={[\n \"data-table__cell\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {col.render(row, index)}\n </td>\n ))}\n </tr>\n );\n });\n };\n\n return (\n <div className={composedClassName} data-testid={testId}>\n <table className=\"data-table__table\" aria-label={ariaLabel} role=\"table\">\n <thead className=\"data-table__head\">\n <tr className=\"data-table__row data-table__row--head\">\n {visibleColumns.map((col) => {\n const width = resolveWidth(col);\n const isSortActive = col.sortable && col.id === activeSortColumnId;\n\n return (\n <th\n key={col.id}\n scope=\"col\"\n className={[\n \"data-table__cell\",\n \"data-table__cell--head\",\n col.sortable && \"data-table__cell--sortable\",\n isSortActive && \"data-table__cell--sort-active\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={width ? { width: `${String(width)}px` } : undefined}\n aria-sort={ariaSortValue(\n col as DataTableColumn<unknown>,\n activeSortColumnId,\n activeSortDirection,\n )}\n onClick={\n col.sortable\n ? () => {\n handleSortToggle(col);\n }\n : undefined\n }\n onKeyDown={\n col.sortable\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleSortToggle(col);\n }\n }\n : undefined\n }\n tabIndex={col.sortable ? 0 : undefined}\n >\n <span className=\"data-table__head-label\">{col.header}</span>\n {col.sortable && (\n <SortIcon\n direction={\n isSortActive && activeSortDirection\n ? activeSortDirection\n : \"none\"\n }\n />\n )}\n {!col.noResize && (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n className=\"data-table__resize-handle\"\n onMouseDown={(e) => {\n beginResize(e, col, width ?? col.minWidth ?? 120);\n }}\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody className=\"data-table__body\">{renderBody()}</tbody>\n </table>\n </div>\n );\n}\n\n// Memoize while preserving the generic signature.\nexport const DataTable = memo(DataTableInner) as typeof DataTableInner;\n\nexport default DataTable;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable2.js","names":[],"sources":["../../../src/components/DataTable/DataTable.tsx"],"sourcesContent":["/**\n * DataTable Component (epic #2730 / issue #2738 · sort: epic #2859 / issue #2861)\n *\n * Generic, type-safe table primitive modeled after the Backend.AI WebUI\n * `BAITable`. Designed so future menus (Models, Sessions, Data, ...)\n * can share one tabular surface instead of inventing a new one each\n * time.\n *\n * Features (V1 — additional capabilities planned for follow-up issues):\n * - Type-safe column definitions with custom cell renderers\n * - Optional column-visibility persistence via `localStorage` when\n * `persistKey` is supplied\n * - Optional resizable columns (drag the right edge of a header cell)\n * with widths persisted alongside visibility\n * - Empty-state slot rendered when `rows` is empty\n * - Loading-state slot rendered when `loading` is `true`\n * - Stable `getRowKey` accessor so rows with the same identifier remain\n * referentially identical across event-driven updates\n *\n * Sorting (V2 — epic #2859 / issue #2861):\n * - Optional per-column sortable flag with sort value accessor or\n * custom comparator\n * - Optional defaultSortDirection per column\n * - Controlled (`sortColumnId` + `sortDirection` + `onSortChange`) and\n * uncontrolled (internal state) modes\n * - Accessible `aria-sort` attributes on header cells\n * - Keyboard activation (Enter / Space) for sortable headers\n * - Stable sort — equal-valued rows retain their original order\n * - Sorting never mutates the original `rows` prop\n *\n * The component is intentionally domain-agnostic — it MUST NOT carry\n * Session, Model, or Agent-specific knowledge. The first consumer is\n * `SessionListTable` (epic #2730 / #2738) but the API is generic.\n */\n\nimport {\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport \"./DataTable.css\";\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/** Direction a column is currently sorted. */\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface DataTableColumn<T> {\n /** Unique identifier for the column. Used for visibility + width persistence. */\n id: string;\n /** Header label. */\n header: ReactNode;\n /**\n * Cell renderer. Receives the row and returns a React node. Pure\n * functions are encouraged so memoization stays effective.\n */\n render: (row: T, index: number) => ReactNode;\n /** Optional minimum column width in pixels. Defaults to 80. */\n minWidth?: number;\n /** Optional initial width in pixels (only honored on the first paint). */\n initialWidth?: number;\n /** When `true`, the column cannot be resized. Defaults to `false`. */\n noResize?: boolean;\n /** When `true`, the column cannot be hidden by the user. Defaults to `false`. */\n alwaysVisible?: boolean;\n /** Extra `class` for the column's `<th>` and `<td>` cells. */\n className?: string;\n /**\n * When provided, the value is exposed via the `align` style on the\n * column's cells. Useful for right-aligned numeric columns.\n */\n align?: \"left\" | \"right\" | \"center\";\n\n // ---- Sorting API (added epic #2859 / issue #2861) -------------------------\n\n /**\n * When `true`, the column header renders a sort affordance and clicking\n * (or pressing Enter / Space) cycles through ascending → descending →\n * unsorted states. Defaults to `false`.\n */\n sortable?: boolean;\n /**\n * Extracts a primitive comparable value from a row for sorting purposes.\n * Used when the default ascending/descending behavior is sufficient.\n * Mutually exclusive with `sortComparator` — if both are provided,\n * `sortComparator` takes precedence.\n *\n * @example\n * ```tsx\n * sortValueAccessor: (row) => row.name.toLowerCase()\n * ```\n */\n sortValueAccessor?: (row: T) => string | number | boolean | null | undefined;\n /**\n * Custom comparator used for sorting. Receives two rows and the active\n * sort direction. Return a negative number if `a` should come first, a\n * positive number if `b` should come first, or `0` if they are equal.\n * The table applies a stable fallback on `0` to preserve original order.\n *\n * @example\n * ```tsx\n * sortComparator: (a, b, dir) =>\n * dir === \"asc\"\n * ? a.createdAt - b.createdAt\n * : b.createdAt - a.createdAt\n * ```\n */\n sortComparator?: (a: T, b: T, direction: SortDirection) => number;\n /**\n * The sort direction applied when this column is first activated.\n * Defaults to `\"asc\"`.\n */\n defaultSortDirection?: SortDirection;\n}\n\nexport interface DataTablePersistedState {\n /** column id → user-overridden width in pixels. */\n widths: Record<string, number>;\n /** column id → visibility flag. Missing keys count as visible. */\n visibility: Record<string, boolean>;\n}\n\nexport interface DataTableProps<T> {\n /** Column definitions, in display order. */\n columns: DataTableColumn<T>[];\n /** Row data. */\n rows: T[];\n /**\n * Stable identifier accessor — used as the React key for each row.\n * Must be deterministic for the lifetime of the row.\n */\n getRowKey: (row: T, index: number) => string;\n /** Render this node when `rows` is empty. */\n emptyState?: ReactNode;\n /** Render this node when `loading` is `true`. */\n loadingState?: ReactNode;\n /** When `true`, the loading slot replaces the table body. */\n loading?: boolean;\n /**\n * Column widths and visibility, controlled by the caller.\n *\n * This component used to take a `persistKey` and write to `localStorage`\n * itself. Storage is a policy decision that belongs to the consumer: where\n * it goes, under which key, whether it is per user or per workspace, and\n * whether it exists at all in a host that has no `localStorage`. A\n * rendering component that answers those questions on its own cannot be\n * reused by a consumer that answers them differently.\n */\n columnState?: DataTablePersistedState;\n /**\n * Called whenever the user resizes a column or toggles its visibility.\n * Pair it with `columnState` to persist wherever the consumer keeps\n * preferences; omit both to get a table that forgets on unmount.\n */\n onColumnStateChange?: (state: DataTablePersistedState) => void;\n /** Extra class for the table's wrapping element. */\n className?: string;\n /** ARIA label for the table. Defaults to \"Data table\". */\n ariaLabel?: string;\n /** Test ID for testing harnesses. */\n testId?: string;\n /**\n * Callback invoked when a row is clicked. When provided, rows render\n * with a clickable affordance (cursor: pointer + keyboard binding).\n */\n onRowClick?: (row: T) => void;\n\n // ---- Sorting props (added epic #2859 / issue #2861) -----------------------\n\n /**\n * Controlled — the column currently being sorted. Pass `undefined` to\n * indicate no active sort. When this prop is present, the component\n * operates in controlled mode: the caller is responsible for updating\n * the sort state via `onSortChange`.\n */\n sortColumnId?: string;\n /**\n * Controlled — the direction of the active sort. Ignored when\n * `sortColumnId` is `undefined`.\n */\n sortDirection?: SortDirection;\n /**\n * Called when the user activates a sortable column header. In\n * controlled mode the caller must propagate this back as\n * `sortColumnId` / `sortDirection`. In uncontrolled mode this prop is\n * purely observational.\n *\n * @param columnId The column being sorted, or `undefined` when the\n * sort is cleared.\n * @param direction The new direction.\n */\n onSortChange?: (\n columnId: string | undefined,\n direction: SortDirection | undefined,\n ) => void;\n}\n\nconst EMPTY_COLUMN_STATE: DataTablePersistedState = { widths: {}, visibility: {} };\n\n// ============================================================================\n// Sorting helpers\n// ============================================================================\n\n/**\n * Stable sort — rows with equal comparison result keep their original\n * relative order. We achieve stability by augmenting each row with its\n * original index and using that as a tie-breaker.\n *\n * The input array is never mutated.\n */\nfunction stableSort<T>(rows: T[], comparator: (a: T, b: T) => number): T[] {\n const indexed = rows.map((row, i) => ({ row, i }));\n indexed.sort((a, b) => {\n const result = comparator(a.row, b.row);\n return result !== 0 ? result : a.i - b.i;\n });\n return indexed.map(({ row }) => row);\n}\n\n/**\n * Builds a comparator from a `DataTableColumn` + active sort direction.\n * Falls back to string comparison when neither `sortComparator` nor\n * `sortValueAccessor` is provided on the column.\n */\nfunction buildComparator<T>(\n col: DataTableColumn<T>,\n direction: SortDirection,\n): (a: T, b: T) => number {\n const { sortComparator } = col;\n if (sortComparator) {\n return (a, b) => sortComparator(a, b, direction);\n }\n const accessor = col.sortValueAccessor;\n if (accessor) {\n return (a, b) => {\n const av = accessor(a);\n const bv = accessor(b);\n // Nullish values sort last regardless of direction.\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n let cmp: number;\n if (typeof av === \"number\" && typeof bv === \"number\") {\n cmp = av - bv;\n } else {\n cmp = String(av).localeCompare(String(bv));\n }\n return direction === \"asc\" ? cmp : -cmp;\n };\n }\n // No accessor — unsortable in practice (column should not be sortable).\n return () => 0;\n}\n\n/**\n * Returns the `aria-sort` attribute value for a column header cell.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-sort\n */\nfunction ariaSortValue(\n col: DataTableColumn<unknown>,\n activeSortColumnId: string | undefined,\n activeSortDirection: SortDirection | undefined,\n): \"ascending\" | \"descending\" | \"none\" | undefined {\n if (!col.sortable) return undefined;\n if (col.id === activeSortColumnId) {\n return activeSortDirection === \"asc\" ? \"ascending\" : \"descending\";\n }\n return \"none\";\n}\n\n// Sort indicator icons rendered as inline SVG so there is no external\n// asset dependency. They intentionally reuse design-token colours so they\n// automatically adapt to light / dark themes.\n\nfunction SortIcon({ direction }: { direction: SortDirection | \"none\" }) {\n if (direction === \"none\") {\n // Neutral chevron stack — both arrows in muted colour\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--none\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.35\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.35\" />\n </svg>\n </span>\n );\n }\n if (direction === \"asc\") {\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--asc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" opacity=\"0.25\" />\n </svg>\n </span>\n );\n }\n return (\n <span\n className=\"data-table__sort-icon data-table__sort-icon--desc\"\n aria-hidden=\"true\"\n >\n <svg\n width=\"10\"\n height=\"14\"\n viewBox=\"0 0 10 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 1L1 5H9L5 1Z\" fill=\"currentColor\" opacity=\"0.25\" />\n <path d=\"M5 13L9 9H1L5 13Z\" fill=\"currentColor\" />\n </svg>\n </span>\n );\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nfunction DataTableInner<T>({\n columns,\n rows,\n getRowKey,\n emptyState,\n loadingState,\n loading = false,\n columnState,\n onColumnStateChange,\n className = \"\",\n ariaLabel = \"Data table\",\n testId,\n onRowClick,\n // Sorting (controlled)\n sortColumnId: controlledSortColumnId,\n sortDirection: controlledSortDirection,\n onSortChange,\n}: DataTableProps<T>) {\n // ---- Column state (widths + visibility) ----------------------------------\n //\n // Held internally so a caller that does not care about persistence gets a\n // working table, and re-seeded whenever the caller supplies a new one.\n const [persisted, setPersisted] = useState<DataTablePersistedState>(\n () => columnState ?? EMPTY_COLUMN_STATE,\n );\n\n useEffect(() => {\n if (columnState) setPersisted(columnState);\n }, [columnState]);\n\n // ---- Sorting state (uncontrolled fallback) -------------------------------\n //\n // Controlled mode is detected by the presence of `sortColumnId` as a prop\n // (the caller explicitly manages the sort column). Using only this prop as\n // the signal (rather than OR-ing with `sortDirection`) avoids a\n // pathological state where `sortDirection` is set but `sortColumnId` is\n // not, which would enter controlled mode with no sort effect.\n //\n // Note: TypeScript cannot distinguish \"prop not passed\" from \"prop passed\n // as undefined\", so we rely on the convention that controlled callers\n // always pass both `sortColumnId` and `sortDirection` together.\n const isControlled =\n controlledSortColumnId !== undefined || controlledSortDirection !== undefined;\n\n const [internalSortColumnId, setInternalSortColumnId] = useState<string | undefined>(\n undefined,\n );\n const [internalSortDirection, setInternalSortDirection] = useState<\n SortDirection | undefined\n >(undefined);\n\n const activeSortColumnId = isControlled\n ? controlledSortColumnId\n : internalSortColumnId;\n const activeSortDirection = isControlled\n ? controlledSortDirection\n : internalSortDirection;\n\n /** Handle a click / keypress on a sortable column header. */\n const handleSortToggle = useCallback(\n (col: DataTableColumn<T>) => {\n if (!col.sortable) return;\n\n let nextColumnId: string | undefined;\n let nextDirection: SortDirection | undefined;\n\n if (activeSortColumnId !== col.id) {\n // Activate this column in its default direction.\n nextColumnId = col.id;\n nextDirection = col.defaultSortDirection ?? \"asc\";\n } else if (activeSortDirection === (col.defaultSortDirection ?? \"asc\")) {\n // Flip to the opposite direction.\n nextColumnId = col.id;\n nextDirection = activeSortDirection === \"asc\" ? \"desc\" : \"asc\";\n } else {\n // Already in the non-default direction — clear the sort.\n nextColumnId = undefined;\n nextDirection = undefined;\n }\n\n if (!isControlled) {\n setInternalSortColumnId(nextColumnId);\n setInternalSortDirection(nextDirection);\n }\n onSortChange?.(nextColumnId, nextDirection);\n },\n [activeSortColumnId, activeSortDirection, isControlled, onSortChange],\n );\n\n // ---- Visibility computation (column always-visible wins) -----------------\n const visibleColumns = useMemo(\n () =>\n columns.filter((col) => {\n if (col.alwaysVisible) return true;\n const flag = persisted.visibility[col.id];\n return flag !== false;\n }),\n [columns, persisted.visibility],\n );\n\n // ---- Width resolution ----------------------------------------------------\n const resolveWidth = useCallback(\n (col: DataTableColumn<T>): number | undefined => {\n const persistedWidth = persisted.widths[col.id];\n if (typeof persistedWidth === \"number\" && persistedWidth > 0) {\n return persistedWidth;\n }\n return col.initialWidth;\n },\n [persisted.widths],\n );\n\n // ---- Resize handling -----------------------------------------------------\n const resizeRef = useRef<{\n columnId: string;\n startX: number;\n startWidth: number;\n minWidth: number;\n } | null>(null);\n\n const beginResize = useCallback(\n (\n ev: React.MouseEvent<HTMLDivElement>,\n col: DataTableColumn<T>,\n currentWidth: number,\n ) => {\n if (col.noResize) return;\n ev.preventDefault();\n ev.stopPropagation();\n resizeRef.current = {\n columnId: col.id,\n startX: ev.clientX,\n startWidth: currentWidth,\n minWidth: col.minWidth ?? 80,\n };\n\n const onMove = (e: MouseEvent) => {\n const ref = resizeRef.current;\n if (!ref) return;\n const delta = e.clientX - ref.startX;\n const next = Math.max(ref.minWidth, ref.startWidth + delta);\n setPersisted((prev) => ({\n ...prev,\n widths: { ...prev.widths, [ref.columnId]: next },\n }));\n };\n const onUp = () => {\n resizeRef.current = null;\n window.removeEventListener(\"mousemove\", onMove);\n window.removeEventListener(\"mouseup\", onUp);\n };\n window.addEventListener(\"mousemove\", onMove);\n window.addEventListener(\"mouseup\", onUp);\n },\n [],\n );\n\n // Report changes so the caller can persist them. Skipped while `persisted`\n // still holds what the caller last handed in, so echoing the callback back\n // through `columnState` does not loop.\n const reported = useRef(persisted);\n useEffect(() => {\n if (reported.current === persisted) return;\n reported.current = persisted;\n onColumnStateChange?.(persisted);\n }, [persisted, onColumnStateChange]);\n\n // ---- Sorted rows --------------------------------------------------------\n const sortedRows = useMemo(() => {\n if (!activeSortColumnId || !activeSortDirection) return rows;\n const col = columns.find((c) => c.id === activeSortColumnId);\n if (!col?.sortable) return rows;\n return stableSort(rows, buildComparator(col, activeSortDirection));\n }, [rows, columns, activeSortColumnId, activeSortDirection]);\n\n // ---- Body rendering ------------------------------------------------------\n const composedClassName = [\"data-table\", className].filter(Boolean).join(\" \");\n\n const renderBody = () => {\n if (loading) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {loadingState ?? <div className=\"data-table__loading\" />}\n </td>\n </tr>\n );\n }\n if (sortedRows.length === 0) {\n return (\n <tr className=\"data-table__row data-table__row--state\">\n <td colSpan={visibleColumns.length || 1} className=\"data-table__state-cell\">\n {emptyState}\n </td>\n </tr>\n );\n }\n return sortedRows.map((row, index) => {\n const key = getRowKey(row, index);\n return (\n <tr\n key={key}\n className={[\"data-table__row\", onRowClick && \"data-table__row--clickable\"]\n .filter(Boolean)\n .join(\" \")}\n onClick={\n onRowClick\n ? () => {\n onRowClick(row);\n }\n : undefined\n }\n onKeyDown={\n onRowClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onRowClick(row);\n }\n }\n : undefined\n }\n tabIndex={onRowClick ? 0 : undefined}\n role={onRowClick ? \"button\" : undefined}\n >\n {visibleColumns.map((col) => (\n <td\n key={col.id}\n className={[\n \"data-table__cell\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {col.render(row, index)}\n </td>\n ))}\n </tr>\n );\n });\n };\n\n return (\n <div className={composedClassName} data-testid={testId}>\n <table className=\"data-table__table\" aria-label={ariaLabel} role=\"table\">\n <thead className=\"data-table__head\">\n <tr className=\"data-table__row data-table__row--head\">\n {visibleColumns.map((col) => {\n const width = resolveWidth(col);\n const isSortActive = col.sortable && col.id === activeSortColumnId;\n\n return (\n <th\n key={col.id}\n scope=\"col\"\n className={[\n \"data-table__cell\",\n \"data-table__cell--head\",\n col.sortable && \"data-table__cell--sortable\",\n isSortActive && \"data-table__cell--sort-active\",\n col.className,\n col.align && `data-table__cell--align-${col.align}`,\n ]\n .filter(Boolean)\n .join(\" \")}\n style={width ? { width: `${String(width)}px` } : undefined}\n aria-sort={ariaSortValue(\n col as DataTableColumn<unknown>,\n activeSortColumnId,\n activeSortDirection,\n )}\n onClick={\n col.sortable\n ? () => {\n handleSortToggle(col);\n }\n : undefined\n }\n onKeyDown={\n col.sortable\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleSortToggle(col);\n }\n }\n : undefined\n }\n tabIndex={col.sortable ? 0 : undefined}\n >\n <span className=\"data-table__head-label\">{col.header}</span>\n {col.sortable && (\n <SortIcon\n direction={\n isSortActive && activeSortDirection\n ? activeSortDirection\n : \"none\"\n }\n />\n )}\n {!col.noResize && (\n <div\n role=\"separator\"\n aria-orientation=\"vertical\"\n className=\"data-table__resize-handle\"\n onMouseDown={(e) => {\n beginResize(e, col, width ?? col.minWidth ?? 120);\n }}\n />\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody className=\"data-table__body\">{renderBody()}</tbody>\n </table>\n </div>\n );\n}\n\n// Memoize while preserving the generic signature.\nexport const DataTable = memo(DataTableInner) as typeof DataTableInner;\n\nexport default DataTable;\n"],"mappings":"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"}
@@ -103,4 +103,5 @@ function s({ isOpen: s, onClose: c, title: l, subtitle: u, width: d = "medium",
103
103
  //#endregion
104
104
  export { s as Drawer };
105
105
 
106
- //# sourceMappingURL=Drawer2.js.map
106
+ //# sourceMappingURL=Drawer2.js.map
107
+ import "../../assets/components/Drawer/Drawer.css";
@@ -1,5 +1,4 @@
1
1
  import { ReactNode } from 'react';
2
- export type IllustrationType = "chat" | "models" | "creations" | "text" | "benchmark" | "logs" | "statistics" | "error" | "schedule" | "generic";
3
2
  export interface EmptyStateAction {
4
3
  label: string;
5
4
  onClick: () => void;
@@ -10,8 +9,16 @@ export interface EmptyStateSecondaryAction {
10
9
  onClick?: () => void;
11
10
  }
12
11
  export interface EmptyStateProps {
13
- /** Type of illustration to display */
14
- illustration: IllustrationType;
12
+ /**
13
+ * The drawing shown above the text.
14
+ *
15
+ * This used to be one of ten names: chat, models, creations, benchmark,
16
+ * logs, statistics, schedule and so on, each resolving to an SVG shipped
17
+ * inside this package. Those names are one product's information
18
+ * architecture, and no other consumer has a "creations" screen to draw for.
19
+ * A consumer passes its own artwork and keeps its own vocabulary.
20
+ */
21
+ illustration?: ReactNode;
15
22
  /** Main heading */
16
23
  title: string;
17
24
  /** Descriptive text */
@@ -1,78 +1,62 @@
1
1
  import { Button as e } from "../Button/Button2.js";
2
- import { BenchmarkIllustration as t, ChatIllustration as n, CreationsIllustration as r, ErrorIllustration as i, GenericIllustration as a, LogsIllustration as o, ModelsIllustration as s, ScheduleIllustration as c, StatisticsIllustration as l, TextIllustration as u } from "./illustrations.js";
3
2
  /* empty css */
4
- import { memo as d, useCallback as f, useMemo as p } from "react";
5
- import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
3
+ import { memo as t, useCallback as n, useMemo as r } from "react";
4
+ import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
6
5
  //#region src/components/EmptyState/EmptyState.tsx
7
- var _ = {
8
- chat: n,
9
- models: s,
10
- creations: r,
11
- text: u,
12
- benchmark: t,
13
- logs: o,
14
- statistics: l,
15
- error: i,
16
- schedule: c,
17
- generic: a
18
- };
19
- function v({ illustration: t, title: n, description: r, primaryAction: i, secondaryAction: a, children: o, className: s = "", showIllustration: c = !0 }) {
20
- let l = i?.onClick, u = a?.onClick, d = f(() => {
21
- l?.();
22
- }, [l]), v = f(() => {
23
- u?.();
24
- }, [u]), y = p(() => _[t], [t]), b = p(() => [
25
- "empty-state",
26
- `empty-state--${t}`,
27
- s
28
- ].filter(Boolean).join(" "), [t, s]);
29
- return /* @__PURE__ */ g("div", {
30
- className: b,
6
+ function s({ illustration: t, title: s, description: c, primaryAction: l, secondaryAction: u, children: d, className: f = "", showIllustration: p = !0 }) {
7
+ let m = l?.onClick, h = u?.onClick, g = n(() => {
8
+ m?.();
9
+ }, [m]), _ = n(() => {
10
+ h?.();
11
+ }, [h]), v = r(() => ["empty-state", f].filter(Boolean).join(" "), [f]);
12
+ return /* @__PURE__ */ o("div", {
13
+ className: v,
31
14
  role: "status",
32
15
  "aria-live": "polite",
33
16
  children: [
34
- c && /* @__PURE__ */ h("div", {
17
+ p && t && /* @__PURE__ */ a("div", {
35
18
  className: "empty-state__illustration",
36
- children: /* @__PURE__ */ h(y, { className: "empty-state__icon" })
19
+ children: t
37
20
  }),
38
- /* @__PURE__ */ g("div", {
21
+ /* @__PURE__ */ o("div", {
39
22
  className: "empty-state__content",
40
23
  children: [
41
- /* @__PURE__ */ h("h3", {
24
+ /* @__PURE__ */ a("h3", {
42
25
  className: "empty-state__title",
43
- children: n
26
+ children: s
44
27
  }),
45
- /* @__PURE__ */ h("p", {
28
+ /* @__PURE__ */ a("p", {
46
29
  className: "empty-state__description",
47
- children: r
30
+ children: c
48
31
  }),
49
- o
32
+ d
50
33
  ]
51
34
  }),
52
- (i || a) && /* @__PURE__ */ g("div", {
35
+ (l || u) && /* @__PURE__ */ o("div", {
53
36
  className: "empty-state__actions",
54
- children: [i && /* @__PURE__ */ h(e, {
37
+ children: [l && /* @__PURE__ */ a(e, {
55
38
  variant: "primary",
56
- onClick: d,
39
+ onClick: g,
57
40
  className: "empty-state__action-btn empty-state__action-btn--primary",
58
- children: i.label
59
- }), a && /* @__PURE__ */ h(m, { children: a.href ? /* @__PURE__ */ h("a", {
60
- href: a.href,
41
+ children: l.label
42
+ }), u && /* @__PURE__ */ a(i, { children: u.href ? /* @__PURE__ */ a("a", {
43
+ href: u.href,
61
44
  className: "empty-state__action-link",
62
- onClick: v,
63
- children: a.label
64
- }) : /* @__PURE__ */ h(e, {
45
+ onClick: _,
46
+ children: u.label
47
+ }) : /* @__PURE__ */ a(e, {
65
48
  variant: "ghost",
66
- onClick: v,
49
+ onClick: _,
67
50
  className: "empty-state__action-btn empty-state__action-btn--secondary",
68
- children: a.label
51
+ children: u.label
69
52
  }) })]
70
53
  })
71
54
  ]
72
55
  });
73
56
  }
74
- var y = d(v);
57
+ var c = t(s);
75
58
  //#endregion
76
- export { y as EmptyState };
59
+ export { c as EmptyState };
77
60
 
78
- //# sourceMappingURL=EmptyState2.js.map
61
+ //# sourceMappingURL=EmptyState2.js.map
62
+ import "../../assets/components/EmptyState/EmptyState.css";
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState2.js","names":[],"sources":["../../../src/components/EmptyState/EmptyState.tsx"],"sourcesContent":["/**\n * EmptyState Component\n *\n * Unified empty state component with consistent layout and styling.\n * Provides configurable illustration, title, description, and call-to-action buttons.\n *\n * Features:\n * - Multiple illustration types for different contexts\n * - Primary and secondary action buttons, plus a `children` slot for an\n * onboarding state that needs more than two (issue #3903)\n * - Accessible (ARIA attributes, semantic HTML)\n * - Dark/light theme support via design tokens\n * - Fade-in animation with reduced motion support\n * - Responsive design\n * - Memoized for optimal performance\n */\n\nimport { memo, useCallback, useMemo, type ReactNode } from \"react\";\nimport { Button } from \"../Button\";\nimport {\n ChatIllustration,\n ModelsIllustration,\n CreationsIllustration,\n TextIllustration,\n BenchmarkIllustration,\n LogsIllustration,\n StatisticsIllustration,\n ErrorIllustration,\n ScheduleIllustration,\n GenericIllustration,\n} from \"./illustrations\";\nimport \"./EmptyState.css\";\n\nexport type IllustrationType =\n | \"chat\"\n | \"models\"\n | \"creations\"\n | \"text\"\n | \"benchmark\"\n | \"logs\"\n | \"statistics\"\n | \"error\"\n | \"schedule\"\n | \"generic\";\n\nexport interface EmptyStateAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface EmptyStateSecondaryAction {\n label: string;\n href?: string;\n onClick?: () => void;\n}\n\nexport interface EmptyStateProps {\n /** Type of illustration to display */\n illustration: IllustrationType;\n /** Main heading */\n title: string;\n /** Descriptive text */\n description: string;\n /** Primary action button */\n primaryAction?: EmptyStateAction;\n /** Secondary action (link or button) */\n secondaryAction?: EmptyStateSecondaryAction;\n /**\n * Extra content rendered under the description, inside the same text column.\n *\n * The two action props cover the common one-or-two-button case. An\n * onboarding state that has to offer more than that (e.g. the Data Hub's\n * three ingest paths, issue #3903) renders them here instead of growing a\n * page-local copy of this component.\n */\n children?: ReactNode;\n /** Additional CSS classes */\n className?: string;\n /** Whether to show illustration (default: true) */\n showIllustration?: boolean;\n}\n\n/**\n * Static map of illustration types to components.\n * Defined outside component to avoid recreation on each render.\n */\nconst illustrationMap: Record<\n IllustrationType,\n React.ComponentType<{ className?: string }>\n> = {\n chat: ChatIllustration,\n models: ModelsIllustration,\n creations: CreationsIllustration,\n text: TextIllustration,\n benchmark: BenchmarkIllustration,\n logs: LogsIllustration,\n statistics: StatisticsIllustration,\n error: ErrorIllustration,\n schedule: ScheduleIllustration,\n generic: GenericIllustration,\n};\n\n/**\n * EmptyState Component\n *\n * Displays an empty state with illustration, text, and optional actions.\n * Memoized to prevent unnecessary re-renders.\n */\nfunction EmptyStateComponent({\n illustration,\n title,\n description,\n primaryAction,\n secondaryAction,\n children,\n className = \"\",\n showIllustration = true,\n}: EmptyStateProps) {\n // Use stable callback references by extracting onClick functions\n const primaryOnClick = primaryAction?.onClick;\n const secondaryOnClick = secondaryAction?.onClick;\n\n const handlePrimaryAction = useCallback(() => {\n primaryOnClick?.();\n }, [primaryOnClick]);\n\n const handleSecondaryAction = useCallback(() => {\n secondaryOnClick?.();\n }, [secondaryOnClick]);\n\n // Memoize illustration component lookup\n const Illustration = useMemo(() => illustrationMap[illustration], [illustration]);\n\n // Memoize class name computation\n const containerClass = useMemo(\n () =>\n [\"empty-state\", `empty-state--${illustration}`, className]\n .filter(Boolean)\n .join(\" \"),\n [illustration, className],\n );\n\n return (\n <div className={containerClass} role=\"status\" aria-live=\"polite\">\n {showIllustration && (\n <div className=\"empty-state__illustration\">\n <Illustration className=\"empty-state__icon\" />\n </div>\n )}\n\n <div className=\"empty-state__content\">\n <h3 className=\"empty-state__title\">{title}</h3>\n <p className=\"empty-state__description\">{description}</p>\n {children}\n </div>\n\n {(primaryAction || secondaryAction) && (\n <div className=\"empty-state__actions\">\n {primaryAction && (\n <Button\n variant=\"primary\"\n onClick={handlePrimaryAction}\n className=\"empty-state__action-btn empty-state__action-btn--primary\"\n >\n {primaryAction.label}\n </Button>\n )}\n\n {secondaryAction && (\n <>\n {secondaryAction.href ? (\n <a\n href={secondaryAction.href}\n className=\"empty-state__action-link\"\n onClick={handleSecondaryAction}\n >\n {secondaryAction.label}\n </a>\n ) : (\n <Button\n variant=\"ghost\"\n onClick={handleSecondaryAction}\n className=\"empty-state__action-btn empty-state__action-btn--secondary\"\n >\n {secondaryAction.label}\n </Button>\n )}\n </>\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport const EmptyState = memo(EmptyStateComponent);\n"],"mappings":";;;;;;AAsFA,IAAM,IAGF;CACF,MAAM;CACN,QAAQ;CACR,WAAW;CACX,MAAM;CACN,WAAW;CACX,MAAM;CACN,YAAY;CACZ,OAAO;CACP,UAAU;CACV,SAAS;AACX;AAQA,SAAS,EAAoB,EAC3B,iBACA,UACA,gBACA,kBACA,oBACA,aACA,eAAY,IACZ,sBAAmB,MACD;CAElB,IAAM,IAAiB,GAAe,SAChC,IAAmB,GAAiB,SAEpC,IAAsB,QAAkB;EAC5C,IAAiB;CACnB,GAAG,CAAC,CAAc,CAAC,GAEb,IAAwB,QAAkB;EAC9C,IAAmB;CACrB,GAAG,CAAC,CAAgB,CAAC,GAGf,IAAe,QAAc,EAAgB,IAAe,CAAC,CAAY,CAAC,GAG1E,IAAiB,QAEnB;EAAC;EAAe,gBAAgB;EAAgB;CAAS,CAAC,CACvD,OAAO,OAAO,CAAC,CACf,KAAK,GAAG,GACb,CAAC,GAAc,CAAS,CAC1B;CAEA,OACE,kBAAC,OAAD;EAAK,WAAW;EAAgB,MAAK;EAAS,aAAU;EAAxD,UAAA;GACG,KACC,kBAAC,OAAD;IAAK,WAAU;IACb,UAAA,kBAAC,GAAD,EAAc,WAAU,oBAAqB,CAAA;GAC1C,CAAA;GAGP,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;MAAsB,UAAA;KAAU,CAAA;KAC9C,kBAAC,KAAD;MAAG,WAAU;MAA4B,UAAA;KAAe,CAAA;KACvD;IACE;;IAEH,KAAiB,MACjB,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAc;IACT,CAAA,GAGT,KACC,kBAAA,GAAA,EAAA,UACG,EAAgB,OACf,kBAAC,KAAD;KACE,MAAM,EAAgB;KACtB,WAAU;KACV,SAAS;KAER,UAAA,EAAgB;IAChB,CAAA,IAEH,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAgB;IACX,CAAA,EAEV,CAAA,CAED;;EAEJ;;AAET;AAEA,IAAa,IAAa,EAAK,CAAmB"}
1
+ {"version":3,"file":"EmptyState2.js","names":[],"sources":["../../../src/components/EmptyState/EmptyState.tsx"],"sourcesContent":["/**\n * EmptyState Component\n *\n * Unified empty state component with consistent layout and styling.\n * Provides configurable illustration, title, description, and call-to-action buttons.\n *\n * Features:\n * - Multiple illustration types for different contexts\n * - Primary and secondary action buttons, plus a `children` slot for an\n * onboarding state that needs more than two (issue #3903)\n * - Accessible (ARIA attributes, semantic HTML)\n * - Dark/light theme support via design tokens\n * - Fade-in animation with reduced motion support\n * - Responsive design\n * - Memoized for optimal performance\n */\n\nimport { memo, useCallback, useMemo, type ReactNode } from \"react\";\nimport { Button } from \"../Button\";\nimport \"./EmptyState.css\";\n\nexport interface EmptyStateAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface EmptyStateSecondaryAction {\n label: string;\n href?: string;\n onClick?: () => void;\n}\n\nexport interface EmptyStateProps {\n /**\n * The drawing shown above the text.\n *\n * This used to be one of ten names: chat, models, creations, benchmark,\n * logs, statistics, schedule and so on, each resolving to an SVG shipped\n * inside this package. Those names are one product's information\n * architecture, and no other consumer has a \"creations\" screen to draw for.\n * A consumer passes its own artwork and keeps its own vocabulary.\n */\n illustration?: ReactNode;\n /** Main heading */\n title: string;\n /** Descriptive text */\n description: string;\n /** Primary action button */\n primaryAction?: EmptyStateAction;\n /** Secondary action (link or button) */\n secondaryAction?: EmptyStateSecondaryAction;\n /**\n * Extra content rendered under the description, inside the same text column.\n *\n * The two action props cover the common one-or-two-button case. An\n * onboarding state that has to offer more than that (e.g. the Data Hub's\n * three ingest paths, issue #3903) renders them here instead of growing a\n * page-local copy of this component.\n */\n children?: ReactNode;\n /** Additional CSS classes */\n className?: string;\n /** Whether to show illustration (default: true) */\n showIllustration?: boolean;\n}\n\n/**\n * EmptyState Component\n *\n * Displays an empty state with illustration, text, and optional actions.\n * Memoized to prevent unnecessary re-renders.\n */\nfunction EmptyStateComponent({\n illustration,\n title,\n description,\n primaryAction,\n secondaryAction,\n children,\n className = \"\",\n showIllustration = true,\n}: EmptyStateProps) {\n // Use stable callback references by extracting onClick functions\n const primaryOnClick = primaryAction?.onClick;\n const secondaryOnClick = secondaryAction?.onClick;\n\n const handlePrimaryAction = useCallback(() => {\n primaryOnClick?.();\n }, [primaryOnClick]);\n\n const handleSecondaryAction = useCallback(() => {\n secondaryOnClick?.();\n }, [secondaryOnClick]);\n\n // Memoize class name computation\n const containerClass = useMemo(\n () => [\"empty-state\", className].filter(Boolean).join(\" \"),\n [className],\n );\n\n return (\n <div className={containerClass} role=\"status\" aria-live=\"polite\">\n {showIllustration && illustration && (\n <div className=\"empty-state__illustration\">{illustration}</div>\n )}\n\n <div className=\"empty-state__content\">\n <h3 className=\"empty-state__title\">{title}</h3>\n <p className=\"empty-state__description\">{description}</p>\n {children}\n </div>\n\n {(primaryAction || secondaryAction) && (\n <div className=\"empty-state__actions\">\n {primaryAction && (\n <Button\n variant=\"primary\"\n onClick={handlePrimaryAction}\n className=\"empty-state__action-btn empty-state__action-btn--primary\"\n >\n {primaryAction.label}\n </Button>\n )}\n\n {secondaryAction && (\n <>\n {secondaryAction.href ? (\n <a\n href={secondaryAction.href}\n className=\"empty-state__action-link\"\n onClick={handleSecondaryAction}\n >\n {secondaryAction.label}\n </a>\n ) : (\n <Button\n variant=\"ghost\"\n onClick={handleSecondaryAction}\n className=\"empty-state__action-btn empty-state__action-btn--secondary\"\n >\n {secondaryAction.label}\n </Button>\n )}\n </>\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport const EmptyState = memo(EmptyStateComponent);\n"],"mappings":";;;;;AAwEA,SAAS,EAAoB,EAC3B,iBACA,UACA,gBACA,kBACA,oBACA,aACA,eAAY,IACZ,sBAAmB,MACD;CAElB,IAAM,IAAiB,GAAe,SAChC,IAAmB,GAAiB,SAEpC,IAAsB,QAAkB;EAC5C,IAAiB;CACnB,GAAG,CAAC,CAAc,CAAC,GAEb,IAAwB,QAAkB;EAC9C,IAAmB;CACrB,GAAG,CAAC,CAAgB,CAAC,GAGf,IAAiB,QACf,CAAC,eAAe,CAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,GACzD,CAAC,CAAS,CACZ;CAEA,OACE,kBAAC,OAAD;EAAK,WAAW;EAAgB,MAAK;EAAS,aAAU;EAAxD,UAAA;GACG,KAAoB,KACnB,kBAAC,OAAD;IAAK,WAAU;IAA6B,UAAA;GAAkB,CAAA;GAGhE,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;MAAsB,UAAA;KAAU,CAAA;KAC9C,kBAAC,KAAD;MAAG,WAAU;MAA4B,UAAA;KAAe,CAAA;KACvD;IACE;;IAEH,KAAiB,MACjB,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAc;IACT,CAAA,GAGT,KACC,kBAAA,GAAA,EAAA,UACG,EAAgB,OACf,kBAAC,KAAD;KACE,MAAM,EAAgB;KACtB,WAAU;KACV,SAAS;KAER,UAAA,EAAgB;IAChB,CAAA,IAEH,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAgB;IACX,CAAA,EAEV,CAAA,CAED;;EAEJ;;AAET;AAEA,IAAa,IAAa,EAAK,CAAmB"}
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * EmptyState Component - Barrel Export
3
+ *
4
+ * The ten illustrations that used to ship here were one product's information
5
+ * architecture: chat, models, creations, benchmark, logs, statistics,
6
+ * schedule. No other consumer has a "creations" screen to draw for. A consumer
7
+ * passes its own artwork through `illustration` and keeps its own vocabulary.
3
8
  */
4
9
  export { EmptyState } from './EmptyState';
5
- export type { EmptyStateProps, EmptyStateAction, EmptyStateSecondaryAction, IllustrationType, } from './EmptyState';
10
+ export type { EmptyStateProps, EmptyStateAction, EmptyStateSecondaryAction, } from './EmptyState';
@@ -1,11 +1,24 @@
1
- export type ErrorType = "network" | "configuration" | "model" | "permission" | "generic";
1
+ import { ReactNode } from 'react';
2
+ /**
3
+ * How the error reads, not what it is about.
4
+ *
5
+ * This prop used to be a union of five names: network, configuration, model,
6
+ * permission, generic. Two of those, and "model" in particular, are one
7
+ * product's categories rather than anything a shared component can reason
8
+ * about, and all five resolved to three colours anyway: network and permission
9
+ * were the same amber, model and generic the same red. A consumer decides
10
+ * which of its own error categories reads as which tone.
11
+ */
12
+ export type ErrorTone = "danger" | "warning" | "accent";
2
13
  export interface ErrorAction {
3
14
  label: string;
4
15
  onClick: () => void;
5
16
  }
6
17
  export interface ErrorStateProps {
7
- /** Error type determines icon and default styling */
8
- type?: ErrorType;
18
+ /** How the error reads. Defaults to `danger`. */
19
+ tone?: ErrorTone;
20
+ /** Replaces the default alert icon. */
21
+ icon?: ReactNode;
9
22
  /** Error title - main heading */
10
23
  title: string;
11
24
  /** Detailed error message */
@@ -22,4 +35,4 @@ export interface ErrorStateProps {
22
35
  /**
23
36
  * ErrorState Component
24
37
  */
25
- export declare function ErrorState({ type, title, message, primaryAction, secondaryAction, className, showIcon, }: ErrorStateProps): import("react").JSX.Element;
38
+ export declare function ErrorState({ tone, icon, title, message, primaryAction, secondaryAction, className, showIcon, }: ErrorStateProps): import("react").JSX.Element;
@@ -4,55 +4,53 @@ import { AlertCircleIcon as t } from "../../icons/AlertCircleIcon.js";
4
4
  import { useCallback as n } from "react";
5
5
  import { jsx as r, jsxs as i } from "react/jsx-runtime";
6
6
  //#region src/components/ErrorState/ErrorState.tsx
7
- function a(e) {
8
- return /* @__PURE__ */ r(t, { size: 48 });
9
- }
10
- function o({ type: t = "generic", title: o, message: s, primaryAction: c, secondaryAction: l, className: u = "", showIcon: d = !0 }) {
11
- let f = n(() => {
12
- c?.onClick();
13
- }, [c]), p = n(() => {
7
+ function a({ tone: a = "danger", icon: o, title: s, message: c, primaryAction: l, secondaryAction: u, className: d = "", showIcon: f = !0 }) {
8
+ let p = n(() => {
14
9
  l?.onClick();
15
- }, [l]), m = [
10
+ }, [l]), m = n(() => {
11
+ u?.onClick();
12
+ }, [u]), h = [
16
13
  "error-state",
17
- `error-state--${t}`,
18
- u
14
+ `error-state--${a}`,
15
+ d
19
16
  ].filter(Boolean).join(" ");
20
17
  return /* @__PURE__ */ i("div", {
21
- className: m,
18
+ className: h,
22
19
  role: "alert",
23
20
  "aria-live": "polite",
24
21
  children: [
25
- d && /* @__PURE__ */ r("div", {
22
+ f && /* @__PURE__ */ r("div", {
26
23
  className: "error-state__icon",
27
24
  "aria-hidden": "true",
28
- children: a(t)
25
+ children: o ?? /* @__PURE__ */ r(t, { size: 48 })
29
26
  }),
30
27
  /* @__PURE__ */ r("h2", {
31
28
  className: "error-state__title",
32
- children: o
29
+ children: s
33
30
  }),
34
31
  /* @__PURE__ */ r("p", {
35
32
  className: "error-state__message",
36
- children: s
33
+ children: c
37
34
  }),
38
- (c || l) && /* @__PURE__ */ i("div", {
35
+ (l || u) && /* @__PURE__ */ i("div", {
39
36
  className: "error-state__actions",
40
- children: [c && /* @__PURE__ */ r(e, {
37
+ children: [l && /* @__PURE__ */ r(e, {
41
38
  variant: "primary",
42
- onClick: f,
39
+ onClick: p,
43
40
  className: "error-state__action-btn error-state__action-btn--primary",
44
- children: c.label
45
- }), l && /* @__PURE__ */ r(e, {
41
+ children: l.label
42
+ }), u && /* @__PURE__ */ r(e, {
46
43
  variant: "secondary",
47
- onClick: p,
44
+ onClick: m,
48
45
  className: "error-state__action-btn error-state__action-btn--secondary",
49
- children: l.label
46
+ children: u.label
50
47
  })]
51
48
  })
52
49
  ]
53
50
  });
54
51
  }
55
52
  //#endregion
56
- export { o as ErrorState };
53
+ export { a as ErrorState };
57
54
 
58
- //# sourceMappingURL=ErrorState2.js.map
55
+ //# sourceMappingURL=ErrorState2.js.map
56
+ import "../../assets/components/ErrorState/ErrorState.css";
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorState2.js","names":[],"sources":["../../../src/components/ErrorState/ErrorState.tsx"],"sourcesContent":["/**\n * ErrorState Component\n *\n * Unified error state component with recovery actions.\n * Provides consistent error presentation across all pages with actionable buttons.\n *\n * Features:\n * - Error type variants (network, configuration, model, permission, generic)\n * - Primary and secondary action buttons\n * - Accessible (ARIA attributes, focus management)\n * - Dark/light theme support\n */\n\nimport { useCallback } from \"react\";\nimport { AlertCircleIcon } from \"../../icons/AlertCircleIcon\";\nimport { Button } from \"../Button\";\nimport \"./ErrorState.css\";\n\nexport type ErrorType =\n \"network\" | \"configuration\" | \"model\" | \"permission\" | \"generic\";\n\nexport interface ErrorAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface ErrorStateProps {\n /** Error type determines icon and default styling */\n type?: ErrorType;\n /** Error title - main heading */\n title: string;\n /** Detailed error message */\n message: string;\n /** Primary action button (e.g., Retry, Go to Settings) */\n primaryAction?: ErrorAction;\n /** Secondary action button (e.g., View Logs, Report Issue) */\n secondaryAction?: ErrorAction;\n /** Additional CSS classes */\n className?: string;\n /** Whether to show error icon */\n showIcon?: boolean;\n}\n\n/**\n * Get icon for error type\n */\nfunction getErrorIcon(_type: ErrorType): React.ReactNode {\n // All error types use the same AlertCircleIcon for now\n // Can be extended with different icons per type in the future\n return <AlertCircleIcon size={48} />;\n}\n\n/**\n * ErrorState Component\n */\nexport function ErrorState({\n type = \"generic\",\n title,\n message,\n primaryAction,\n secondaryAction,\n className = \"\",\n showIcon = true,\n}: ErrorStateProps) {\n const handlePrimaryAction = useCallback(() => {\n primaryAction?.onClick();\n }, [primaryAction]);\n\n const handleSecondaryAction = useCallback(() => {\n secondaryAction?.onClick();\n }, [secondaryAction]);\n\n const containerClass = [\"error-state\", `error-state--${type}`, className]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <div className={containerClass} role=\"alert\" aria-live=\"polite\">\n {showIcon && (\n <div className=\"error-state__icon\" aria-hidden=\"true\">\n {getErrorIcon(type)}\n </div>\n )}\n\n <h2 className=\"error-state__title\">{title}</h2>\n\n <p className=\"error-state__message\">{message}</p>\n\n {(primaryAction || secondaryAction) && (\n <div className=\"error-state__actions\">\n {primaryAction && (\n <Button\n variant=\"primary\"\n onClick={handlePrimaryAction}\n className=\"error-state__action-btn error-state__action-btn--primary\"\n >\n {primaryAction.label}\n </Button>\n )}\n\n {secondaryAction && (\n <Button\n variant=\"secondary\"\n onClick={handleSecondaryAction}\n className=\"error-state__action-btn error-state__action-btn--secondary\"\n >\n {secondaryAction.label}\n </Button>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AA8CA,SAAS,EAAa,GAAmC;CAGvD,OAAO,kBAAC,GAAD,EAAiB,MAAM,GAAK,CAAA;AACrC;AAKA,SAAgB,EAAW,EACzB,UAAO,WACP,UACA,YACA,kBACA,oBACA,eAAY,IACZ,cAAW,MACO;CAClB,IAAM,IAAsB,QAAkB;EAC5C,GAAe,QAAQ;CACzB,GAAG,CAAC,CAAa,CAAC,GAEZ,IAAwB,QAAkB;EAC9C,GAAiB,QAAQ;CAC3B,GAAG,CAAC,CAAe,CAAC,GAEd,IAAiB;EAAC;EAAe,gBAAgB;EAAQ;CAAS,CAAC,CACtE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;CAEX,OACE,kBAAC,OAAD;EAAK,WAAW;EAAgB,MAAK;EAAQ,aAAU;EAAvD,UAAA;GACG,KACC,kBAAC,OAAD;IAAK,WAAU;IAAoB,eAAY;IAC5C,UAAA,EAAa,CAAI;GACf,CAAA;GAGP,kBAAC,MAAD;IAAI,WAAU;IAAsB,UAAA;GAAU,CAAA;GAE9C,kBAAC,KAAD;IAAG,WAAU;IAAwB,UAAA;GAAW,CAAA;IAE9C,KAAiB,MACjB,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAc;IACT,CAAA,GAGT,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAgB;IACX,CAAA,CAEP;;EAEJ;;AAET"}
1
+ {"version":3,"file":"ErrorState2.js","names":[],"sources":["../../../src/components/ErrorState/ErrorState.tsx"],"sourcesContent":["/**\n * ErrorState Component\n *\n * Unified error state component with recovery actions.\n * Provides consistent error presentation across all pages with actionable buttons.\n *\n * Features:\n * - Three visual tones\n * - Primary and secondary action buttons\n * - Accessible (ARIA attributes, focus management)\n * - Dark/light theme support\n */\n\nimport type { ReactNode } from \"react\";\nimport { useCallback } from \"react\";\nimport { AlertCircleIcon } from \"../../icons/AlertCircleIcon\";\nimport { Button } from \"../Button\";\nimport \"./ErrorState.css\";\n\n/**\n * How the error reads, not what it is about.\n *\n * This prop used to be a union of five names: network, configuration, model,\n * permission, generic. Two of those, and \"model\" in particular, are one\n * product's categories rather than anything a shared component can reason\n * about, and all five resolved to three colours anyway: network and permission\n * were the same amber, model and generic the same red. A consumer decides\n * which of its own error categories reads as which tone.\n */\nexport type ErrorTone = \"danger\" | \"warning\" | \"accent\";\n\nexport interface ErrorAction {\n label: string;\n onClick: () => void;\n}\n\nexport interface ErrorStateProps {\n /** How the error reads. Defaults to `danger`. */\n tone?: ErrorTone;\n /** Replaces the default alert icon. */\n icon?: ReactNode;\n /** Error title - main heading */\n title: string;\n /** Detailed error message */\n message: string;\n /** Primary action button (e.g., Retry, Go to Settings) */\n primaryAction?: ErrorAction;\n /** Secondary action button (e.g., View Logs, Report Issue) */\n secondaryAction?: ErrorAction;\n /** Additional CSS classes */\n className?: string;\n /** Whether to show error icon */\n showIcon?: boolean;\n}\n\n/**\n * ErrorState Component\n */\nexport function ErrorState({\n tone = \"danger\",\n icon,\n title,\n message,\n primaryAction,\n secondaryAction,\n className = \"\",\n showIcon = true,\n}: ErrorStateProps) {\n const handlePrimaryAction = useCallback(() => {\n primaryAction?.onClick();\n }, [primaryAction]);\n\n const handleSecondaryAction = useCallback(() => {\n secondaryAction?.onClick();\n }, [secondaryAction]);\n\n const containerClass = [\"error-state\", `error-state--${tone}`, className]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <div className={containerClass} role=\"alert\" aria-live=\"polite\">\n {showIcon && (\n <div className=\"error-state__icon\" aria-hidden=\"true\">\n {icon ?? <AlertCircleIcon size={48} />}\n </div>\n )}\n\n <h2 className=\"error-state__title\">{title}</h2>\n\n <p className=\"error-state__message\">{message}</p>\n\n {(primaryAction || secondaryAction) && (\n <div className=\"error-state__actions\">\n {primaryAction && (\n <Button\n variant=\"primary\"\n onClick={handlePrimaryAction}\n className=\"error-state__action-btn error-state__action-btn--primary\"\n >\n {primaryAction.label}\n </Button>\n )}\n\n {secondaryAction && (\n <Button\n variant=\"secondary\"\n onClick={handleSecondaryAction}\n className=\"error-state__action-btn error-state__action-btn--secondary\"\n >\n {secondaryAction.label}\n </Button>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AA0DA,SAAgB,EAAW,EACzB,UAAO,UACP,SACA,UACA,YACA,kBACA,oBACA,eAAY,IACZ,cAAW,MACO;CAClB,IAAM,IAAsB,QAAkB;EAC5C,GAAe,QAAQ;CACzB,GAAG,CAAC,CAAa,CAAC,GAEZ,IAAwB,QAAkB;EAC9C,GAAiB,QAAQ;CAC3B,GAAG,CAAC,CAAe,CAAC,GAEd,IAAiB;EAAC;EAAe,gBAAgB;EAAQ;CAAS,CAAC,CACtE,OAAO,OAAO,CAAC,CACf,KAAK,GAAG;CAEX,OACE,kBAAC,OAAD;EAAK,WAAW;EAAgB,MAAK;EAAQ,aAAU;EAAvD,UAAA;GACG,KACC,kBAAC,OAAD;IAAK,WAAU;IAAoB,eAAY;IAC5C,UAAA,KAAQ,kBAAC,GAAD,EAAiB,MAAM,GAAK,CAAA;GAClC,CAAA;GAGP,kBAAC,MAAD;IAAI,WAAU;IAAsB,UAAA;GAAU,CAAA;GAE9C,kBAAC,KAAD;IAAG,WAAU;IAAwB,UAAA;GAAW,CAAA;IAE9C,KAAiB,MACjB,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAc;IACT,CAAA,GAGT,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,SAAS;KACT,WAAU;KAET,UAAA,EAAgB;IACX,CAAA,CAEP;;EAEJ;;AAET"}
@@ -2,4 +2,4 @@
2
2
  * ErrorState Component Export
3
3
  */
4
4
  export { ErrorState } from './ErrorState';
5
- export type { ErrorStateProps, ErrorType, ErrorAction } from './ErrorState';
5
+ export type { ErrorStateProps, ErrorTone, ErrorAction } from './ErrorState';
@@ -39,4 +39,5 @@ function n({ title: n, description: r, actions: i, error: a, onErrorDismiss: o,
39
39
  //#endregion
40
40
  export { n as PageHeader };
41
41
 
42
- //# sourceMappingURL=PageHeader2.js.map
42
+ //# sourceMappingURL=PageHeader2.js.map
43
+ import "../../assets/components/PageHeader/PageHeader.css";
@@ -16,4 +16,5 @@ function t({ children: t, variant: n = "standard", className: r = "", ...i }) {
16
16
  //#endregion
17
17
  export { t as PageLayout };
18
18
 
19
- //# sourceMappingURL=PageLayout2.js.map
19
+ //# sourceMappingURL=PageLayout2.js.map
20
+ import "../../assets/components/PageLayout/PageLayout.css";
@@ -33,4 +33,5 @@ function n({ value: n, variant: r = "primary", size: i = "md", showLabel: a = !1
33
33
  //#endregion
34
34
  export { n as ProgressBar, n as default };
35
35
 
36
- //# sourceMappingURL=ProgressBar2.js.map
36
+ //# sourceMappingURL=ProgressBar2.js.map
37
+ import "../../assets/components/ProgressBar/ProgressBar.css";
@@ -23,4 +23,5 @@ function t({ width: t = "100%", height: n = "20px", variant: r = "rect", classNa
23
23
  //#endregion
24
24
  export { t as Skeleton, t as default };
25
25
 
26
- //# sourceMappingURL=Skeleton2.js.map
26
+ //# sourceMappingURL=Skeleton2.js.map
27
+ import "../../assets/components/Skeleton/Skeleton.css";
@@ -5,6 +5,8 @@ export interface SkeletonCardProps {
5
5
  className?: string;
6
6
  /** Test ID for testing */
7
7
  testId?: string;
8
+ /** Accessible name for each skeleton inside. Defaults to "Loading". */
9
+ loadingLabel?: string;
8
10
  }
9
11
  /**
10
12
  * SkeletonCard provides a loading placeholder for card components.
@@ -14,5 +16,5 @@ export interface SkeletonCardProps {
14
16
  * - compact: Smaller card with reduced padding
15
17
  * - stat: Statistics card layout
16
18
  */
17
- export declare function SkeletonCard({ variant, className, testId, }: SkeletonCardProps): import("react").JSX.Element;
19
+ export declare function SkeletonCard({ variant, className, testId, loadingLabel, }: SkeletonCardProps): import("react").JSX.Element;
18
20
  export default SkeletonCard;
@@ -2,29 +2,32 @@ import { Skeleton as e } from "./Skeleton2.js";
2
2
  /* empty css */
3
3
  import { Fragment as t, jsx as n, jsxs as r } from "react/jsx-runtime";
4
4
  //#region src/components/Skeleton/SkeletonCard.tsx
5
- function i({ variant: i = "default", className: a = "", testId: o }) {
6
- let s = [
5
+ function i({ variant: i = "default", className: a = "", testId: o, loadingLabel: s }) {
6
+ let c = [
7
7
  "skeleton-card",
8
8
  `skeleton-card--${i}`,
9
9
  a
10
10
  ].filter(Boolean).join(" ");
11
11
  return /* @__PURE__ */ n("div", {
12
- className: s,
12
+ className: c,
13
13
  "data-testid": o,
14
14
  role: "status",
15
15
  "aria-busy": "true",
16
16
  children: i === "stat" ? /* @__PURE__ */ n(t, { children: /* @__PURE__ */ r("div", {
17
17
  className: "skeleton-card__header",
18
18
  children: [/* @__PURE__ */ n(e, {
19
+ loadingLabel: s,
19
20
  width: "48px",
20
21
  height: "48px",
21
22
  variant: "circle"
22
23
  }), /* @__PURE__ */ r("div", {
23
24
  className: "skeleton-card__header-text",
24
25
  children: [/* @__PURE__ */ n(e, {
26
+ loadingLabel: s,
25
27
  width: "60%",
26
28
  height: "16px"
27
29
  }), /* @__PURE__ */ n(e, {
30
+ loadingLabel: s,
28
31
  width: "40%",
29
32
  height: "24px"
30
33
  })]
@@ -33,6 +36,7 @@ function i({ variant: i = "default", className: a = "", testId: o }) {
33
36
  /* @__PURE__ */ n("div", {
34
37
  className: "skeleton-card__header",
35
38
  children: /* @__PURE__ */ n(e, {
39
+ loadingLabel: s,
36
40
  width: "70%",
37
41
  height: "20px"
38
42
  })
@@ -41,14 +45,17 @@ function i({ variant: i = "default", className: a = "", testId: o }) {
41
45
  className: "skeleton-card__content",
42
46
  children: [
43
47
  /* @__PURE__ */ n(e, {
48
+ loadingLabel: s,
44
49
  width: "100%",
45
50
  height: "16px"
46
51
  }),
47
52
  /* @__PURE__ */ n(e, {
53
+ loadingLabel: s,
48
54
  width: "90%",
49
55
  height: "16px"
50
56
  }),
51
57
  /* @__PURE__ */ n(e, {
58
+ loadingLabel: s,
52
59
  width: "80%",
53
60
  height: "16px"
54
61
  })
@@ -57,9 +64,11 @@ function i({ variant: i = "default", className: a = "", testId: o }) {
57
64
  i !== "compact" && /* @__PURE__ */ r("div", {
58
65
  className: "skeleton-card__footer",
59
66
  children: [/* @__PURE__ */ n(e, {
67
+ loadingLabel: s,
60
68
  width: "80px",
61
69
  height: "32px"
62
70
  }), /* @__PURE__ */ n(e, {
71
+ loadingLabel: s,
63
72
  width: "80px",
64
73
  height: "32px"
65
74
  })]
@@ -70,4 +79,5 @@ function i({ variant: i = "default", className: a = "", testId: o }) {
70
79
  //#endregion
71
80
  export { i as SkeletonCard, i as default };
72
81
 
73
- //# sourceMappingURL=SkeletonCard2.js.map
82
+ //# sourceMappingURL=SkeletonCard2.js.map
83
+ import "../../assets/components/Skeleton/SkeletonCard.css";