@usetheo/ui 0.12.0-next.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/{chunk-ZNILW4G5.js → chunk-DW247T3Q.js} +2 -2
- package/dist/{chunk-ZNILW4G5.js.map → chunk-DW247T3Q.js.map} +1 -1
- package/dist/composites/data-table/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/preset-v3-legacy.js.map +1 -1
- package/package.json +13 -3
- package/registry/r/data-table.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-05-28
|
|
11
|
+
|
|
12
|
+
**Stable release — promoted from `0.12.0-next.0` after dogfood validation + cross-repo contract gates landed.**
|
|
13
|
+
|
|
14
|
+
First `@usetheo/ui` release on the npm `latest` tag without a `-next.*` pre-release suffix. Content is the union of everything shipped in `0.12.0-next.0` (2026-05-25) plus the additions below from `[Unreleased]` consolidation. **Zero breaking changes vs `0.12.0-next.0`** — consumers on `^0.12.0-next.0` upgrade transparently.
|
|
15
|
+
|
|
16
|
+
### Added (dogfood-fixes-and-coverage-expansion T4.1, 2026-05-28)
|
|
17
|
+
|
|
18
|
+
- **`scripts/validate-exports.mjs`** (NEW) — FAANG-grade pre-publish gate com 6 runtime checks: (1) exports['.'] declared, (2) type:module consistency D13, (3) import condition file exists + dynamic import runtime, (4) require condition runtime check (skip se ESM-only), (5) ESM-only intentional notice, (6) TODOS subpath exports validados (não só `.`). Bloqueia `npm publish` se shape OR comportamento regridir.
|
|
19
|
+
- **`prepublishOnly`** hook estendido: `pnpm build && pnpm test:contract && node scripts/validate-exports.mjs`. EC-8 fix do edge case review FAANG-grade.
|
|
20
|
+
- **`validate:exports`** script standalone — pode rodar manual `pnpm validate:exports`.
|
|
21
|
+
|
|
22
|
+
### Added (cross-repo-integration-coesao, 2026-05-28)
|
|
23
|
+
|
|
24
|
+
- **Contract test mirror** — `tests/contract/theokit-consumer.test.ts` (6 BDD `it()`) validates `dist/vite-plugin.js` shape (default export factory + Plugin/Plugin[] return + `name: string`) and presence of `dist/preset.css`, `dist/styles.css`, `dist/fonts.css`. Roda via novo `pnpm test:contract`. Bloqueia publish via `prepublishOnly: pnpm build && pnpm test:contract`. Cumpre [ADR 0001](docs/adr/0001-vite-plugin-subpath-export-contract.md) (cross-repo contract). Plano coordenador no monorepo theokit-tools: [cross-repo-integration-coesao-plan.md](../.claude/knowledge-base/plans/cross-repo-integration-coesao-plan.md) T1.3.
|
|
25
|
+
- **`vitest.config.ts`** include estendido para cobrir `tests/**/*.{test,spec}.ts` (era só `src/`).
|
|
26
|
+
|
|
10
27
|
## [0.12.0-next.0] - 2026-05-25
|
|
11
28
|
|
|
12
29
|
Minor (additive, zero breaking change) — ships two LLM-facing artifacts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/composites/data-table/data-table.tsx"],"names":["_"],"mappings":"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","file":"chunk-ZNILW4G5.js","sourcesContent":["import { ChevronDown, ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { Fragment, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../../lib/cn.js\";\nimport { DropdownMenu } from \"../../primitives/dropdown-menu/index.js\";\nimport { EmptyState } from \"../../primitives/empty-state/index.js\";\nimport { Pagination } from \"../../primitives/pagination/index.js\";\nimport { Skeleton } from \"../../primitives/skeleton/index.js\";\nimport { Table } from \"../../primitives/table/index.js\";\n\n/**\n * DataTable — generic, sortable, expandable composite over `<Table>`.\n *\n * Adds operator-grade entity-list patterns on top of the plain Table\n * primitive: sortable headers, sticky header, expandable rows\n * (multi-row by default), row action menus (Dropdown), client-side\n * pagination, loading skeleton rows, empty state. Both sort and\n * pagination support controlled OR uncontrolled mode (consumer\n * passes onSortChange / onPageChange to take over state).\n *\n * @example\n * <DataTable\n * columns={[\n * { key: \"name\", label: \"Name\", sortable: true },\n * { key: \"status\", label: \"Status\" },\n * ]}\n * data={domains}\n * rowKey={(d) => d.id}\n * expandable={(d) => d.status === \"pending\" ? <DnsRecords domain={d} /> : null}\n * rowActions={(d) => (\n * <>\n * <DropdownMenu.Item onSelect={() => editDomain(d)}>Edit</DropdownMenu.Item>\n * <DropdownMenu.Item onSelect={() => deleteDomain(d)}>Delete</DropdownMenu.Item>\n * </>\n * )}\n * />\n */\nexport interface DataTableColumn<T> {\n key: string;\n label: ReactNode;\n align?: \"left\" | \"center\" | \"right\";\n sortable?: boolean;\n width?: string;\n render?: (row: T) => ReactNode;\n className?: string;\n}\n\nexport interface DataTableSort {\n key: string;\n direction: \"asc\" | \"desc\";\n}\n\nexport interface DataTableProps<T> {\n data: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string;\n stickyHeader?: boolean;\n expandable?: (row: T) => ReactNode | null;\n expandMode?: \"single\" | \"multiple\";\n rowActions?: (row: T) => ReactNode;\n pagination?: {\n pageSize: number;\n controlledPage?: number;\n onPageChange?: (page: number) => void;\n } | null;\n defaultSort?: DataTableSort;\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n loading?: boolean;\n emptyState?: ReactNode;\n className?: string;\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === null || a === undefined) return -1;\n if (b === null || b === undefined) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction DataTable<T>(props: DataTableProps<T>): ReactNode {\n const {\n data,\n columns,\n rowKey,\n stickyHeader = true,\n expandable,\n expandMode = \"multiple\",\n rowActions,\n pagination,\n defaultSort,\n sort: controlledSort,\n onSortChange,\n loading = false,\n emptyState,\n className,\n } = props;\n\n const isControlledSort = onSortChange !== undefined;\n const [uncontrolledSort, setUncontrolledSort] = useState<DataTableSort | null>(\n defaultSort ?? null,\n );\n const sort = isControlledSort ? (controlledSort ?? null) : uncontrolledSort;\n\n const isControlledPage = pagination?.controlledPage !== undefined;\n const [uncontrolledPage, setUncontrolledPage] = useState(0);\n const currentPage = isControlledPage ? (pagination?.controlledPage ?? 0) : uncontrolledPage;\n\n // EC-9: clamp pageSize to >= 1 to avoid divide-by-zero / infinite render\n const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);\n\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n\n function handleSort(columnKey: string) {\n // Cycle: none → asc → desc → none\n let nextSort: DataTableSort | null;\n if (sort?.key !== columnKey) {\n nextSort = { key: columnKey, direction: \"asc\" };\n } else if (sort.direction === \"asc\") {\n nextSort = { key: columnKey, direction: \"desc\" };\n } else {\n nextSort = null;\n }\n if (isControlledSort) {\n onSortChange?.(nextSort);\n } else {\n setUncontrolledSort(nextSort);\n // EC-8: sort change resets pagination to page 0\n if (!isControlledPage) setUncontrolledPage(0);\n }\n }\n\n function handlePageChange(page: number) {\n // Pagination uses 1-indexed; internal state 0-indexed\n const zeroIdx = page - 1;\n if (isControlledPage) {\n pagination?.onPageChange?.(zeroIdx);\n } else {\n setUncontrolledPage(zeroIdx);\n }\n }\n\n function toggleExpand(key: string) {\n if (expandMode === \"single\") {\n setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])));\n } else {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }\n }\n\n // Apply client-side sort in uncontrolled mode\n const sortedData = useMemo(() => {\n if (isControlledSort || sort === null) return data;\n const col = columns.find((c) => c.key === sort.key);\n if (!col) return data;\n const sorted = [...data].sort((a, b) => {\n const aVal = col.render\n ? null\n : (a as Record<string, unknown>)[sort.key as keyof T as string];\n const bVal = col.render\n ? null\n : (b as Record<string, unknown>)[sort.key as keyof T as string];\n const cmp = compareValues(aVal, bVal);\n return sort.direction === \"asc\" ? cmp : -cmp;\n });\n return sorted;\n }, [data, sort, isControlledSort, columns]);\n\n // Apply client-side pagination in uncontrolled mode\n const visibleData = useMemo(() => {\n if (!pagination) return sortedData;\n if (isControlledPage) return sortedData; // consumer pre-sliced\n const start = currentPage * effectivePageSize;\n return sortedData.slice(start, start + effectivePageSize);\n }, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);\n\n // EC-1 fix: compute colSpan accounting for chevron + actions columns\n const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);\n const expandedColSpan = columns.length + extraCols;\n const totalCols = columns.length + extraCols;\n\n // Loading state (EC-7: loading > empty)\n if (loading) {\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => (\n <Table.HeaderCell key={col.key} align={col.align}>\n {col.label}\n </Table.HeaderCell>\n ))}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {Array.from({ length: 5 }, (_, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders\n <Table.Row key={`skeleton-${i}`}>\n {Array.from({ length: totalCols }, (_, j) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders\n <Table.Cell key={`s-${i}-${j}`}>\n <Skeleton className=\"h-4 w-full\" />\n </Table.Cell>\n ))}\n </Table.Row>\n ))}\n </Table.Body>\n </Table>\n </div>\n );\n }\n\n // Empty state (after loading check)\n if (sortedData.length === 0) {\n return (\n <div className={cn(\"w-full\", className)}>\n {emptyState ?? <EmptyState title=\"No data\" description=\"There's nothing here yet.\" />}\n </div>\n );\n }\n\n const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;\n\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 z-10 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => {\n const isSortable = col.sortable === true;\n const isActive = sort?.key === col.key;\n return (\n <Table.HeaderCell\n key={col.key}\n align={col.align}\n onSort={isSortable ? () => handleSort(col.key) : undefined}\n sortDirection={isSortable ? (isActive ? sort?.direction : \"none\") : undefined}\n style={col.width ? { width: col.width } : undefined}\n >\n {col.label}\n </Table.HeaderCell>\n );\n })}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {visibleData.map((row) => {\n const key = rowKey(row);\n const expandedContent = expandable ? expandable(row) : null;\n const isExpandable = expandedContent !== null && expandedContent !== undefined;\n const isExpanded = expanded.has(key);\n return (\n <Fragment key={key}>\n <Table.Row>\n {expandable ? (\n <Table.Cell>\n {isExpandable ? (\n <button\n type=\"button\"\n onClick={() => toggleExpand(key)}\n aria-expanded={isExpanded}\n aria-controls={`expanded-${key}`}\n aria-label={isExpanded ? \"Collapse row\" : \"Expand row\"}\n className=\"inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted\"\n >\n {isExpanded ? (\n <ChevronDown aria-hidden=\"true\" className=\"size-4\" />\n ) : (\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n )}\n </button>\n ) : null}\n </Table.Cell>\n ) : null}\n {columns.map((col) => (\n <Table.Cell key={col.key} align={col.align} className={col.className}>\n {col.render\n ? col.render(row)\n : String((row as Record<string, unknown>)[col.key] ?? \"\")}\n </Table.Cell>\n ))}\n {rowActions ? (\n <Table.Cell align=\"right\">\n <DropdownMenu>\n <DropdownMenu.Trigger\n aria-label=\"Row actions\"\n className={cn(\n \"inline-flex size-7 items-center justify-center rounded-md\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">{rowActions(row)}</DropdownMenu.Content>\n </DropdownMenu>\n </Table.Cell>\n ) : null}\n </Table.Row>\n {isExpanded && isExpandable ? (\n <tr id={`expanded-${key}`}>\n <td colSpan={expandedColSpan} className=\"bg-muted/30 p-4\">\n {expandedContent}\n </td>\n </tr>\n ) : null}\n </Fragment>\n );\n })}\n </Table.Body>\n </Table>\n {pagination && totalPages > 1 ? (\n <div className=\"mt-4 flex items-center justify-end\">\n <Pagination\n currentPage={currentPage + 1}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/components/composites/data-table/data-table.tsx"],"names":["_"],"mappings":"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","file":"chunk-DW247T3Q.js","sourcesContent":["import { ChevronDown, ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { Fragment, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../../lib/cn.js\";\nimport { DropdownMenu } from \"../../primitives/dropdown-menu/index.js\";\nimport { EmptyState } from \"../../primitives/empty-state/index.js\";\nimport { Pagination } from \"../../primitives/pagination/index.js\";\nimport { Skeleton } from \"../../primitives/skeleton/index.js\";\nimport { Table } from \"../../primitives/table/index.js\";\n\n/**\n * DataTable — generic, sortable, expandable composite over `<Table>`.\n *\n * Adds operator-grade entity-list patterns on top of the plain Table\n * primitive: sortable headers, sticky header, expandable rows\n * (multi-row by default), row action menus (Dropdown), client-side\n * pagination, loading skeleton rows, empty state. Both sort and\n * pagination support controlled OR uncontrolled mode (consumer\n * passes onSortChange / onPageChange to take over state).\n *\n * @example\n * <DataTable\n * columns={[\n * { key: \"name\", label: \"Name\", sortable: true },\n * { key: \"status\", label: \"Status\" },\n * ]}\n * data={domains}\n * rowKey={(d) => d.id}\n * expandable={(d) => d.status === \"pending\" ? <DnsRecords domain={d} /> : null}\n * rowActions={(d) => (\n * <>\n * <DropdownMenu.Item onSelect={() => editDomain(d)}>Edit</DropdownMenu.Item>\n * <DropdownMenu.Item onSelect={() => deleteDomain(d)}>Delete</DropdownMenu.Item>\n * </>\n * )}\n * />\n */\nexport interface DataTableColumn<T> {\n key: string;\n label: ReactNode;\n align?: \"left\" | \"center\" | \"right\";\n sortable?: boolean;\n width?: string;\n render?: (row: T) => ReactNode;\n className?: string;\n}\n\nexport interface DataTableSort {\n key: string;\n direction: \"asc\" | \"desc\";\n}\n\nexport interface DataTableProps<T> {\n data: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string;\n stickyHeader?: boolean;\n expandable?: (row: T) => ReactNode | null;\n expandMode?: \"single\" | \"multiple\";\n rowActions?: (row: T) => ReactNode;\n pagination?: {\n pageSize: number;\n controlledPage?: number;\n onPageChange?: (page: number) => void;\n } | null;\n defaultSort?: DataTableSort;\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n loading?: boolean;\n emptyState?: ReactNode;\n className?: string;\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === null || a === undefined) return -1;\n if (b === null || b === undefined) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction DataTable<T>(props: DataTableProps<T>): ReactNode {\n const {\n data,\n columns,\n rowKey,\n stickyHeader = true,\n expandable,\n expandMode = \"multiple\",\n rowActions,\n pagination,\n defaultSort,\n sort: controlledSort,\n onSortChange,\n loading = false,\n emptyState,\n className,\n } = props;\n\n const isControlledSort = onSortChange !== undefined;\n const [uncontrolledSort, setUncontrolledSort] = useState<DataTableSort | null>(\n defaultSort ?? null,\n );\n const sort = isControlledSort ? (controlledSort ?? null) : uncontrolledSort;\n\n const isControlledPage = pagination?.controlledPage !== undefined;\n const [uncontrolledPage, setUncontrolledPage] = useState(0);\n const currentPage = isControlledPage ? (pagination?.controlledPage ?? 0) : uncontrolledPage;\n\n // EC-9: clamp pageSize to >= 1 to avoid divide-by-zero / infinite render\n const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);\n\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n\n function handleSort(columnKey: string) {\n // Cycle: none → asc → desc → none\n let nextSort: DataTableSort | null;\n if (sort?.key !== columnKey) {\n nextSort = { key: columnKey, direction: \"asc\" };\n } else if (sort.direction === \"asc\") {\n nextSort = { key: columnKey, direction: \"desc\" };\n } else {\n nextSort = null;\n }\n if (isControlledSort) {\n onSortChange?.(nextSort);\n } else {\n setUncontrolledSort(nextSort);\n // EC-8: sort change resets pagination to page 0\n if (!isControlledPage) setUncontrolledPage(0);\n }\n }\n\n function handlePageChange(page: number) {\n // Pagination uses 1-indexed; internal state 0-indexed\n const zeroIdx = page - 1;\n if (isControlledPage) {\n pagination?.onPageChange?.(zeroIdx);\n } else {\n setUncontrolledPage(zeroIdx);\n }\n }\n\n function toggleExpand(key: string) {\n if (expandMode === \"single\") {\n setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])));\n } else {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }\n }\n\n // Apply client-side sort in uncontrolled mode\n const sortedData = useMemo(() => {\n if (isControlledSort || sort === null) return data;\n const col = columns.find((c) => c.key === sort.key);\n if (!col) return data;\n const sorted = [...data].sort((a, b) => {\n const aVal = col.render\n ? null\n : (a as Record<string, unknown>)[sort.key as keyof T as string];\n const bVal = col.render\n ? null\n : (b as Record<string, unknown>)[sort.key as keyof T as string];\n const cmp = compareValues(aVal, bVal);\n return sort.direction === \"asc\" ? cmp : -cmp;\n });\n return sorted;\n }, [data, sort, isControlledSort, columns]);\n\n // Apply client-side pagination in uncontrolled mode\n const visibleData = useMemo(() => {\n if (!pagination) return sortedData;\n if (isControlledPage) return sortedData; // consumer pre-sliced\n const start = currentPage * effectivePageSize;\n return sortedData.slice(start, start + effectivePageSize);\n }, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);\n\n // EC-1 fix: compute colSpan accounting for chevron + actions columns\n const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);\n const expandedColSpan = columns.length + extraCols;\n const totalCols = columns.length + extraCols;\n\n // Loading state (EC-7: loading > empty)\n if (loading) {\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => (\n <Table.HeaderCell key={col.key} align={col.align}>\n {col.label}\n </Table.HeaderCell>\n ))}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {Array.from({ length: 5 }, (_, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders\n <Table.Row key={`skeleton-${i}`}>\n {Array.from({ length: totalCols }, (_, j) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders\n <Table.Cell key={`s-${i}-${j}`}>\n <Skeleton className=\"h-4 w-full\" />\n </Table.Cell>\n ))}\n </Table.Row>\n ))}\n </Table.Body>\n </Table>\n </div>\n );\n }\n\n // Empty state (after loading check)\n if (sortedData.length === 0) {\n return (\n <div className={cn(\"w-full\", className)}>\n {emptyState ?? <EmptyState title=\"No data\" description=\"There's nothing here yet.\" />}\n </div>\n );\n }\n\n const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;\n\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 z-10 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => {\n const isSortable = col.sortable === true;\n const isActive = sort?.key === col.key;\n return (\n <Table.HeaderCell\n key={col.key}\n align={col.align}\n onSort={isSortable ? () => handleSort(col.key) : undefined}\n sortDirection={isSortable ? (isActive ? sort?.direction : \"none\") : undefined}\n style={col.width ? { width: col.width } : undefined}\n >\n {col.label}\n </Table.HeaderCell>\n );\n })}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {visibleData.map((row) => {\n const key = rowKey(row);\n const expandedContent = expandable ? expandable(row) : null;\n const isExpandable = expandedContent !== null && expandedContent !== undefined;\n const isExpanded = expanded.has(key);\n return (\n <Fragment key={key}>\n <Table.Row>\n {expandable ? (\n <Table.Cell>\n {isExpandable ? (\n <button\n type=\"button\"\n onClick={() => toggleExpand(key)}\n aria-expanded={isExpanded}\n aria-controls={`expanded-${key}`}\n aria-label={isExpanded ? \"Collapse row\" : \"Expand row\"}\n className=\"inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted\"\n >\n {isExpanded ? (\n <ChevronDown aria-hidden=\"true\" className=\"size-4\" />\n ) : (\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n )}\n </button>\n ) : null}\n </Table.Cell>\n ) : null}\n {columns.map((col) => (\n <Table.Cell key={col.key} align={col.align} className={col.className}>\n {col.render\n ? col.render(row)\n : String((row as Record<string, unknown>)[col.key] ?? \"\")}\n </Table.Cell>\n ))}\n {rowActions ? (\n <Table.Cell align=\"right\">\n <DropdownMenu>\n <DropdownMenu.Trigger\n aria-label=\"Row actions\"\n className={cn(\n \"inline-flex size-7 items-center justify-center rounded-md\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">{rowActions(row)}</DropdownMenu.Content>\n </DropdownMenu>\n </Table.Cell>\n ) : null}\n </Table.Row>\n {isExpanded && isExpandable ? (\n <tr id={`expanded-${key}`}>\n <td colSpan={expandedColSpan} className=\"bg-muted/30 p-4\">\n {expandedContent}\n </td>\n </tr>\n ) : null}\n </Fragment>\n );\n })}\n </Table.Body>\n </Table>\n {pagination && totalPages > 1 ? (\n <div className=\"mt-4 flex items-center justify-end\">\n <Pagination\n currentPage={currentPage + 1}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable };\n"]}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { SkillEditor } from './chunk-IPEYGWA7.js';
|
|
|
6
6
|
export { SkillsList } from './chunk-AEVSVDT6.js';
|
|
7
7
|
export { TaskHeader } from './chunk-BPUQWMBD.js';
|
|
8
8
|
export { UsageMeter } from './chunk-AC4MGCXI.js';
|
|
9
|
-
export { DataTable } from './chunk-
|
|
9
|
+
export { DataTable } from './chunk-DW247T3Q.js';
|
|
10
10
|
export { DeploymentRow } from './chunk-W2PVSIW3.js';
|
|
11
11
|
export { DomainConfig } from './chunk-DFADMEJK.js';
|
|
12
12
|
export { EnvVarEditor } from './chunk-WVPDQMC2.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/styles/tailwind-preset.ts","../src/preset-v3-legacy.ts"],"names":[],"mappings":"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","file":"preset-v3-legacy.js","sourcesContent":["/**\n * Theo UI Tailwind preset — Violet Forge identity.\n *\n * Single source of truth for the design system's utility-level tokens:\n * - colors mapped to CSS variables (HSL split via `hsl(var(--x) / <alpha>)`)\n * - Geist-inspired typescale (display / title / body / label / code tiers)\n * - radii, shadows, motion timing & duration, keyframes\n * - tailwindcss-animate plugin\n *\n * Consumed by:\n * - `tailwind.config.ts` (this repo) via `presets: [theoUIPreset]`\n * - registry/r/tailwind-preset.json (shipped to consumers via shadcn CLI)\n *\n * Consumers integrate as:\n *\n * import type { Config } from \"tailwindcss\";\n * import { theoUIPreset } from \"./styles/tailwind-preset\";\n *\n * export default {\n * darkMode: \"class\",\n * content: [\"./src/**\\/*.{ts,tsx}\"],\n * presets: [theoUIPreset],\n * } satisfies Config;\n *\n * Note: `darkMode` and `content` are NOT in the preset — they are consumer\n * decisions. The preset only contains `theme.extend.*` and `plugins`.\n */\n\nimport type { Config } from \"tailwindcss\";\nimport animate from \"tailwindcss-animate\";\n\nconst hsl = (token: string) => `hsl(var(${token}) / <alpha-value>)`;\n\nexport const theoUIPreset: Partial<Config> = {\n theme: {\n container: {\n center: true,\n padding: \"1rem\",\n screens: {\n \"2xl\": \"1280px\",\n },\n },\n extend: {\n colors: {\n background: hsl(\"--background\"),\n foreground: hsl(\"--foreground\"),\n card: {\n DEFAULT: hsl(\"--card\"),\n foreground: hsl(\"--card-foreground\"),\n },\n popover: {\n DEFAULT: hsl(\"--popover\"),\n foreground: hsl(\"--popover-foreground\"),\n },\n primary: {\n DEFAULT: hsl(\"--primary\"),\n deep: hsl(\"--primary-deep\"),\n glow: hsl(\"--primary-glow\"),\n foreground: hsl(\"--primary-foreground\"),\n },\n secondary: {\n DEFAULT: hsl(\"--secondary\"),\n foreground: hsl(\"--secondary-foreground\"),\n },\n accent: {\n DEFAULT: hsl(\"--accent\"),\n deep: hsl(\"--accent-deep\"),\n foreground: hsl(\"--accent-foreground\"),\n },\n muted: {\n DEFAULT: hsl(\"--muted\"),\n foreground: hsl(\"--muted-foreground\"),\n },\n success: {\n DEFAULT: hsl(\"--success\"),\n foreground: hsl(\"--success-foreground\"),\n },\n warning: {\n DEFAULT: hsl(\"--warning\"),\n foreground: hsl(\"--warning-foreground\"),\n },\n destructive: {\n DEFAULT: hsl(\"--destructive\"),\n foreground: hsl(\"--destructive-foreground\"),\n },\n info: {\n DEFAULT: hsl(\"--info\"),\n foreground: hsl(\"--info-foreground\"),\n },\n border: hsl(\"--border\"),\n input: hsl(\"--input\"),\n ring: hsl(\"--ring\"),\n },\n fontFamily: {\n display: \"var(--font-display)\",\n sans: \"var(--font-body)\",\n mono: \"var(--font-mono)\",\n },\n /* Geist-inspired Violet Forge typescale.\n *\n * Three strict weights: 400 (body), 500 (UI), 600 (display/headings).\n * Letter-spacing scales with size — aggressive negative on display.\n * Mirrors the Vercel/Geist vocabulary while keeping Theo's identity.\n */\n fontSize: {\n // Display tier — aggressive compression, content-led headlines\n \"display-2xl\": [\"64px\", { lineHeight: \"1\", letterSpacing: \"-0.0464em\", fontWeight: \"600\" }],\n \"display-xl\": [\"48px\", { lineHeight: \"1.05\", letterSpacing: \"-0.05em\", fontWeight: \"600\" }],\n \"display-lg\": [\"40px\", { lineHeight: \"1.1\", letterSpacing: \"-0.05em\", fontWeight: \"600\" }],\n \"display-md\": [\"32px\", { lineHeight: \"1.2\", letterSpacing: \"-0.04em\", fontWeight: \"600\" }],\n headline: [\"28px\", { lineHeight: \"1.25\", letterSpacing: \"-0.035em\", fontWeight: \"600\" }],\n // Title tier — section / card heads\n \"title-lg\": [\"24px\", { lineHeight: \"1.33\", letterSpacing: \"-0.04em\", fontWeight: \"600\" }],\n \"title-md\": [\"20px\", { lineHeight: \"1.4\", letterSpacing: \"-0.03em\", fontWeight: \"600\" }],\n // Body tier — FAANG-density realignment 2026-05-22: body-md is the\n // industry-standard 14px (shadcn / Vercel Geist / Linear / Stripe /\n // Mantine). The previous 15px was idiosyncratic. body-sm (14px label\n // weight) remains separate via its line-height / weight signature.\n \"body-lg\": [\"18px\", { lineHeight: \"1.56\", letterSpacing: \"-0.01em\", fontWeight: \"400\" }],\n \"body-md\": [\"14px\", { lineHeight: \"1.43\", letterSpacing: \"0\", fontWeight: \"400\" }],\n \"body-sm\": [\"13px\", { lineHeight: \"1.46\", fontWeight: \"400\" }],\n // Label tier — used on buttons, nav, secondary actions\n label: [\"14px\", { lineHeight: \"1.43\", fontWeight: \"500\" }],\n \"label-caps\": [\"12px\", { lineHeight: \"1.33\", letterSpacing: \"0.04em\", fontWeight: \"500\" }],\n // Mono — code surfaces, technical labels\n \"code-md\": [\"14px\", { lineHeight: \"1.5\", fontWeight: \"400\" }],\n \"code-sm\": [\"13px\", { lineHeight: \"1.54\", fontWeight: \"500\" }],\n },\n borderRadius: {\n none: \"var(--radius-none)\",\n sm: \"var(--radius-sm)\",\n md: \"var(--radius-md)\",\n lg: \"var(--radius-lg)\",\n xl: \"var(--radius-xl)\",\n \"2xl\": \"var(--radius-2xl)\",\n full: \"var(--radius-full)\",\n },\n boxShadow: {\n sm: \"var(--shadow-sm)\",\n md: \"var(--shadow-md)\",\n lg: \"var(--shadow-lg)\",\n glow: \"var(--shadow-glow)\",\n \"glow-strong\": \"var(--shadow-glow-strong)\",\n },\n transitionTimingFunction: {\n \"out-soft\": \"var(--ease-out-soft)\",\n snap: \"var(--ease-snap)\",\n },\n transitionDuration: {\n fast: \"var(--duration-fast)\",\n base: \"var(--duration-base)\",\n slow: \"var(--duration-slow)\",\n },\n keyframes: {\n \"fade-in-up\": {\n \"0%\": { opacity: \"0\", transform: \"translateY(8px)\" },\n \"100%\": { opacity: \"1\", transform: \"translateY(0)\" },\n },\n \"pulse-glow\": {\n \"0%, 100%\": { boxShadow: \"0 0 0 0 hsl(var(--primary) / 0.5)\" },\n \"50%\": { boxShadow: \"0 0 0 8px hsl(var(--primary) / 0)\" },\n },\n },\n animation: {\n \"fade-in-up\": \"fade-in-up var(--duration-base) var(--ease-out-soft) both\",\n \"pulse-glow\": \"pulse-glow 1.5s var(--ease-in-out) infinite\",\n },\n },\n },\n plugins: [animate],\n};\n","/**\n * `@usetheo/ui/preset-v3-legacy` — Tailwind v3 JS preset (legacy path).\n *\n * Default-export a `Partial<Config>` mirroring the design tokens shipped\n * in `@usetheo/ui/tokens.css`. Adds a `content` field covering the\n * library's published artifact tree so v3-based Tailwind builds emit\n * the utilities used by `@usetheo/ui` components.\n *\n * **Tailwind v4 consumers MUST use `@usetheo/ui/preset.css`**, not this\n * file — Tailwind v4 dropped the JS preset format entirely. This file\n * exists for any remaining `tailwindcss@^3` consumer (notably the\n * shadcn-style copy-paste registry path); new code should not import\n * from this subpath.\n *\n * The token surface is delegated to `./styles/tailwind-preset.ts` (the\n * existing source of truth used by the local Ladle dev surface and the\n * shadcn registry).\n *\n * v3 consumer usage:\n *\n * // tailwind.config.ts (Tailwind v3 only)\n * import preset from \"@usetheo/ui/preset-v3-legacy\";\n * export default {\n * presets: [preset],\n * content: [\"./app/**\\/*.{ts,tsx}\"],\n * };\n *\n * See RFC 0008 follow-up.\n */\nimport type { Config } from \"tailwindcss\";\nimport { theoUIPreset } from \"./styles/tailwind-preset.js\";\n\nconst LIBRARY_CONTENT_GLOBS: string[] = [\n // Resolved relative to the consumer's `tailwind.config.{ts,js}`.\n \"./node_modules/@usetheo/ui/dist/**/*.{js,mjs,cjs}\",\n // Yarn PnP / pnpm hoist fallback — Tailwind's globbing tolerates both.\n \"./node_modules/@usetheo/ui/dist/**/*.{ts,tsx}\",\n];\n\nconst preset: Partial<Config> = {\n ...theoUIPreset,\n content: LIBRARY_CONTENT_GLOBS,\n};\n\nexport default preset;\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/styles/tailwind-preset.ts","../src/preset-v3-legacy.ts"],"names":[],"mappings":"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","file":"preset-v3-legacy.js","sourcesContent":["/**\n * Theo UI Tailwind preset — Violet Forge identity.\n *\n * Single source of truth for the design system's utility-level tokens:\n * - colors mapped to CSS variables (HSL split via `hsl(var(--x) / <alpha>)`)\n * - Geist-inspired typescale (display / title / body / label / code tiers)\n * - radii, shadows, motion timing & duration, keyframes\n * - tailwindcss-animate plugin\n *\n * Consumed by:\n * - `tailwind.config.ts` (this repo) via `presets: [theoUIPreset]`\n * - registry/r/tailwind-preset.json (shipped to consumers via shadcn CLI)\n *\n * Consumers integrate as:\n *\n * import type { Config } from \"tailwindcss\";\n * import { theoUIPreset } from \"./styles/tailwind-preset\";\n *\n * export default {\n * darkMode: \"class\",\n * content: [\"./src/**\\/*.{ts,tsx}\"],\n * presets: [theoUIPreset],\n * } satisfies Config;\n *\n * Note: `darkMode` and `content` are NOT in the preset — they are consumer\n * decisions. The preset only contains `theme.extend.*` and `plugins`.\n */\n\nimport type { Config } from \"tailwindcss\";\nimport animate from \"tailwindcss-animate\";\n\nconst hsl = (token: string) => `hsl(var(${token}) / <alpha-value>)`;\n\nexport const theoUIPreset: Partial<Config> = {\n theme: {\n container: {\n center: true,\n padding: \"1rem\",\n screens: {\n \"2xl\": \"1280px\",\n },\n },\n extend: {\n colors: {\n background: hsl(\"--background\"),\n foreground: hsl(\"--foreground\"),\n card: {\n DEFAULT: hsl(\"--card\"),\n foreground: hsl(\"--card-foreground\"),\n },\n popover: {\n DEFAULT: hsl(\"--popover\"),\n foreground: hsl(\"--popover-foreground\"),\n },\n primary: {\n DEFAULT: hsl(\"--primary\"),\n deep: hsl(\"--primary-deep\"),\n glow: hsl(\"--primary-glow\"),\n foreground: hsl(\"--primary-foreground\"),\n },\n secondary: {\n DEFAULT: hsl(\"--secondary\"),\n foreground: hsl(\"--secondary-foreground\"),\n },\n accent: {\n DEFAULT: hsl(\"--accent\"),\n deep: hsl(\"--accent-deep\"),\n foreground: hsl(\"--accent-foreground\"),\n },\n muted: {\n DEFAULT: hsl(\"--muted\"),\n foreground: hsl(\"--muted-foreground\"),\n },\n success: {\n DEFAULT: hsl(\"--success\"),\n foreground: hsl(\"--success-foreground\"),\n },\n warning: {\n DEFAULT: hsl(\"--warning\"),\n foreground: hsl(\"--warning-foreground\"),\n },\n destructive: {\n DEFAULT: hsl(\"--destructive\"),\n foreground: hsl(\"--destructive-foreground\"),\n },\n info: {\n DEFAULT: hsl(\"--info\"),\n foreground: hsl(\"--info-foreground\"),\n },\n border: hsl(\"--border\"),\n input: hsl(\"--input\"),\n ring: hsl(\"--ring\"),\n },\n fontFamily: {\n display: \"var(--font-display)\",\n sans: \"var(--font-body)\",\n mono: \"var(--font-mono)\",\n },\n /* Geist-inspired Violet Forge typescale.\n *\n * Three strict weights: 400 (body), 500 (UI), 600 (display/headings).\n * Letter-spacing scales with size — aggressive negative on display.\n * Mirrors the Vercel/Geist vocabulary while keeping Theo's identity.\n */\n fontSize: {\n // Display tier — aggressive compression, content-led headlines\n \"display-2xl\": [\"64px\", { lineHeight: \"1\", letterSpacing: \"-0.0464em\", fontWeight: \"600\" }],\n \"display-xl\": [\"48px\", { lineHeight: \"1.05\", letterSpacing: \"-0.05em\", fontWeight: \"600\" }],\n \"display-lg\": [\"40px\", { lineHeight: \"1.1\", letterSpacing: \"-0.05em\", fontWeight: \"600\" }],\n \"display-md\": [\"32px\", { lineHeight: \"1.2\", letterSpacing: \"-0.04em\", fontWeight: \"600\" }],\n headline: [\"28px\", { lineHeight: \"1.25\", letterSpacing: \"-0.035em\", fontWeight: \"600\" }],\n // Title tier — section / card heads\n \"title-lg\": [\"24px\", { lineHeight: \"1.33\", letterSpacing: \"-0.04em\", fontWeight: \"600\" }],\n \"title-md\": [\"20px\", { lineHeight: \"1.4\", letterSpacing: \"-0.03em\", fontWeight: \"600\" }],\n // Body tier — FAANG-density realignment 2026-05-22: body-md is the\n // industry-standard 14px (shadcn / Vercel Geist / Linear / Stripe /\n // Mantine). The previous 15px was idiosyncratic. body-sm (14px label\n // weight) remains separate via its line-height / weight signature.\n \"body-lg\": [\"18px\", { lineHeight: \"1.56\", letterSpacing: \"-0.01em\", fontWeight: \"400\" }],\n \"body-md\": [\"14px\", { lineHeight: \"1.43\", letterSpacing: \"0\", fontWeight: \"400\" }],\n \"body-sm\": [\"13px\", { lineHeight: \"1.46\", fontWeight: \"400\" }],\n // Label tier — used on buttons, nav, secondary actions\n label: [\"14px\", { lineHeight: \"1.43\", fontWeight: \"500\" }],\n \"label-caps\": [\"12px\", { lineHeight: \"1.33\", letterSpacing: \"0.04em\", fontWeight: \"500\" }],\n // Mono — code surfaces, technical labels\n \"code-md\": [\"14px\", { lineHeight: \"1.5\", fontWeight: \"400\" }],\n \"code-sm\": [\"13px\", { lineHeight: \"1.54\", fontWeight: \"500\" }],\n },\n borderRadius: {\n none: \"var(--radius-none)\",\n sm: \"var(--radius-sm)\",\n md: \"var(--radius-md)\",\n lg: \"var(--radius-lg)\",\n xl: \"var(--radius-xl)\",\n \"2xl\": \"var(--radius-2xl)\",\n full: \"var(--radius-full)\",\n },\n boxShadow: {\n sm: \"var(--shadow-sm)\",\n md: \"var(--shadow-md)\",\n lg: \"var(--shadow-lg)\",\n glow: \"var(--shadow-glow)\",\n \"glow-strong\": \"var(--shadow-glow-strong)\",\n },\n transitionTimingFunction: {\n \"out-soft\": \"var(--ease-out-soft)\",\n snap: \"var(--ease-snap)\",\n },\n transitionDuration: {\n fast: \"var(--duration-fast)\",\n base: \"var(--duration-base)\",\n slow: \"var(--duration-slow)\",\n },\n keyframes: {\n \"fade-in-up\": {\n \"0%\": { opacity: \"0\", transform: \"translateY(8px)\" },\n \"100%\": { opacity: \"1\", transform: \"translateY(0)\" },\n },\n \"pulse-glow\": {\n \"0%, 100%\": { boxShadow: \"0 0 0 0 hsl(var(--primary) / 0.5)\" },\n \"50%\": { boxShadow: \"0 0 0 8px hsl(var(--primary) / 0)\" },\n },\n },\n animation: {\n \"fade-in-up\": \"fade-in-up var(--duration-base) var(--ease-out-soft) both\",\n \"pulse-glow\": \"pulse-glow 1.5s var(--ease-in-out) infinite\",\n },\n },\n },\n plugins: [animate],\n};\n","/**\n * `@usetheo/ui/preset-v3-legacy` — Tailwind v3 JS preset (legacy path).\n *\n * Default-export a `Partial<Config>` mirroring the design tokens shipped\n * in `@usetheo/ui/tokens.css`. Adds a `content` field covering the\n * library's published artifact tree so v3-based Tailwind builds emit\n * the utilities used by `@usetheo/ui` components.\n *\n * **Tailwind v4 consumers MUST use `@usetheo/ui/preset.css`**, not this\n * file — Tailwind v4 dropped the JS preset format entirely. This file\n * exists for any remaining `tailwindcss@^3` consumer (notably the\n * shadcn-style copy-paste registry path); new code should not import\n * from this subpath.\n *\n * The token surface is delegated to `./styles/tailwind-preset.ts` (the\n * existing source of truth used by the local Ladle dev surface and the\n * shadcn registry).\n *\n * v3 consumer usage:\n *\n * // tailwind.config.ts (Tailwind v3 only)\n * import preset from \"@usetheo/ui/preset-v3-legacy\";\n * export default {\n * presets: [preset],\n * content: [\"./app/**\\/*.{ts,tsx}\"],\n * };\n *\n * See RFC 0008 follow-up.\n */\nimport type { Config } from \"tailwindcss\";\nimport { theoUIPreset } from \"./styles/tailwind-preset.js\";\n\nconst LIBRARY_CONTENT_GLOBS: string[] = [\n // Resolved relative to the consumer's `tailwind.config.{ts,js}`.\n \"./node_modules/@usetheo/ui/dist/**/*.{js,mjs,cjs}\",\n // Yarn PnP / pnpm hoist fallback — Tailwind's globbing tolerates both.\n \"./node_modules/@usetheo/ui/dist/**/*.{ts,tsx}\",\n];\n\n// `theoUIPreset` is shape-compatible with v3 `Partial<Config>` at runtime\n// (tokens, theme, plugins, darkMode, etc are all carried through), but the\n// pnpm hoist places `tailwindcss@3` types alongside `tailwindcss@4` types in\n// the workspace — `theoUIPreset`'s inferred shape uses the v4 `UserConfig`\n// presets array type, which TS sees as incompatible with v3's `Config.presets`\n// (different element type). This file is the v3 LEGACY entry; its runtime\n// works for v3 consumers regardless. The cast bridges the v3/v4 type seam.\nconst preset = {\n ...theoUIPreset,\n content: LIBRARY_CONTENT_GLOBS,\n} as unknown as Partial<Config>;\n\nexport default preset;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usetheo/ui",
|
|
3
|
-
"version": "0.12.0
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Theo UI — framework-agnostic React component library with the Violet Forge design system. Focused on AI-agent interfaces, cloud dashboards, and developer-tooling surfaces.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -559,6 +559,9 @@
|
|
|
559
559
|
"test": "vitest run",
|
|
560
560
|
"test:watch": "vitest",
|
|
561
561
|
"test:ui": "vitest --ui",
|
|
562
|
+
"test:contract": "vitest run tests/contract",
|
|
563
|
+
"prepublishOnly": "pnpm build && pnpm test:contract && node scripts/validate-exports.mjs",
|
|
564
|
+
"validate:exports": "node scripts/validate-exports.mjs",
|
|
562
565
|
"registry:build": "tsx scripts/build-registry.ts",
|
|
563
566
|
"registry:validate": "tsx scripts/validate-registry.ts",
|
|
564
567
|
"sync:readme": "tsx scripts/sync-readme.ts",
|
|
@@ -576,8 +579,12 @@
|
|
|
576
579
|
"dogfood:v4-zero-config": "tsx scripts/dogfood-v4-zero-config.ts",
|
|
577
580
|
"dogfood:v4-real-build": "bash scripts/dogfood-v4-real-build.sh",
|
|
578
581
|
"dogfood:precompiled-utilities": "tsx scripts/dogfood-precompiled-utilities.ts",
|
|
579
|
-
"quality:gates": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm test && pnpm build && pnpm registry:build && pnpm registry:validate && pnpm quality:structure && pnpm quality:bundle && pnpm quality:a11y && pnpm ladle:build && pnpm dogfood:whiteboard && pnpm dogfood:slide && pnpm dogfood:slide-deck && pnpm dogfood:slide-rich && pnpm dogfood:v4-zero-config && pnpm dogfood:precompiled-utilities",
|
|
580
|
-
"quality:gates:fast": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm registry:build && pnpm registry:validate && pnpm quality:structure"
|
|
582
|
+
"quality:gates": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm quality:knip && pnpm test && pnpm build && pnpm quality:publint && pnpm registry:build && pnpm registry:validate && pnpm quality:structure && pnpm quality:bundle && pnpm quality:a11y && pnpm ladle:build && pnpm dogfood:whiteboard && pnpm dogfood:slide && pnpm dogfood:slide-deck && pnpm dogfood:slide-rich && pnpm dogfood:v4-zero-config && pnpm dogfood:precompiled-utilities",
|
|
583
|
+
"quality:gates:fast": "pnpm format:check && pnpm lint:ci && pnpm typecheck && pnpm quality:knip && pnpm registry:build && pnpm registry:validate && pnpm quality:structure",
|
|
584
|
+
"quality:knip": "knip",
|
|
585
|
+
"quality:knip:fix": "knip --fix",
|
|
586
|
+
"quality:publint": "publint --strict",
|
|
587
|
+
"quality:attw": "attw --pack . --profile esm-only"
|
|
581
588
|
},
|
|
582
589
|
"peerDependencies": {
|
|
583
590
|
"@tailwindcss/vite": "^4.0.0",
|
|
@@ -688,6 +695,7 @@
|
|
|
688
695
|
"zod": "^4.4.3"
|
|
689
696
|
},
|
|
690
697
|
"devDependencies": {
|
|
698
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
691
699
|
"@biomejs/biome": "^1.9.4",
|
|
692
700
|
"@ladle/react": "^4.1.2",
|
|
693
701
|
"@tailwindcss/cli": "^4.3.0",
|
|
@@ -708,12 +716,14 @@
|
|
|
708
716
|
"hast-util-from-html": "^2.0.3",
|
|
709
717
|
"hast-util-sanitize": "^5.0.2",
|
|
710
718
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
719
|
+
"knip": "^6.14.2",
|
|
711
720
|
"mdast-util-from-markdown": "^2.0.3",
|
|
712
721
|
"mdast-util-gfm": "^3.1.0",
|
|
713
722
|
"mdast-util-to-hast": "^13.2.1",
|
|
714
723
|
"micromark-extension-gfm": "^3.0.0",
|
|
715
724
|
"perfect-freehand": "1.2.3",
|
|
716
725
|
"postcss": "^8.5.14",
|
|
726
|
+
"publint": "^0.3.21",
|
|
717
727
|
"react": "^18.3.1",
|
|
718
728
|
"react-dom": "^18.3.1",
|
|
719
729
|
"roughjs": "4.6.6",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"path": "components/composites/data-table/data-table.tsx",
|
|
22
22
|
"type": "registry:ui",
|
|
23
23
|
"target": "components/ui/data-table.tsx",
|
|
24
|
-
"content": "import { ChevronDown, ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { Fragment, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { DropdownMenu } from \"@/components/ui/dropdown-menu\";\nimport { EmptyState } from \"@/components/ui/empty-state\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Table } from \"@/components/ui/table\";\n\n/**\n * DataTable — generic, sortable, expandable composite over `<Table>`.\n *\n * Adds operator-grade entity-list patterns on top of the plain Table\n * primitive: sortable headers, sticky header, expandable rows\n * (multi-row by default), row action menus (Dropdown), client-side\n * pagination, loading skeleton rows, empty state. Both sort and\n * pagination support controlled OR uncontrolled mode (consumer\n * passes onSortChange / onPageChange to take over state).\n *\n * @example\n * <DataTable\n * columns={[\n * { key: \"name\", label: \"Name\", sortable: true },\n * { key: \"status\", label: \"Status\" },\n * ]}\n * data={domains}\n * rowKey={(d) => d.id}\n * expandable={(d) => d.status === \"pending\" ? <DnsRecords domain={d} /> : null}\n * rowActions={(d) => (\n * <>\n * <DropdownMenu.Item onSelect={() => editDomain(d)}>Edit</DropdownMenu.Item>\n * <DropdownMenu.Item onSelect={() => deleteDomain(d)}>Delete</DropdownMenu.Item>\n * </>\n * )}\n * />\n */\nexport interface DataTableColumn<T> {\n key: string;\n label: ReactNode;\n align?: \"left\" | \"center\" | \"right\";\n sortable?: boolean;\n width?: string;\n render?: (row: T) => ReactNode;\n className?: string;\n}\n\nexport interface DataTableSort {\n key: string;\n direction: \"asc\" | \"desc\";\n}\n\nexport interface DataTableProps<T> {\n data: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string;\n stickyHeader?: boolean;\n expandable?: (row: T) => ReactNode | null;\n expandMode?: \"single\" | \"multiple\";\n rowActions?: (row: T) => ReactNode;\n pagination?: {\n pageSize: number;\n controlledPage?: number;\n onPageChange?: (page: number) => void;\n } | null;\n defaultSort?: DataTableSort;\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n loading?: boolean;\n emptyState?: ReactNode;\n className?: string;\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === null || a === undefined) return -1;\n if (b === null || b === undefined) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction DataTable<T>(props: DataTableProps<T>): ReactNode {\n const {\n data,\n columns,\n rowKey,\n stickyHeader = true,\n expandable,\n expandMode = \"multiple\",\n rowActions,\n pagination,\n defaultSort,\n sort: controlledSort,\n onSortChange,\n loading = false,\n emptyState,\n className,\n } = props;\n\n const isControlledSort = onSortChange !== undefined;\n const [uncontrolledSort, setUncontrolledSort] = useState<DataTableSort | null>(\n defaultSort ?? null,\n );\n const sort = isControlledSort ? (controlledSort ?? null) : uncontrolledSort;\n\n const isControlledPage = pagination?.controlledPage !== undefined;\n const [uncontrolledPage, setUncontrolledPage] = useState(0);\n const currentPage = isControlledPage ? (pagination?.controlledPage ?? 0) : uncontrolledPage;\n\n // EC-9: clamp pageSize to >= 1 to avoid divide-by-zero / infinite render\n const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);\n\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n\n function handleSort(columnKey: string) {\n // Cycle: none → asc → desc → none\n let nextSort: DataTableSort | null;\n if (sort?.key !== columnKey) {\n nextSort = { key: columnKey, direction: \"asc\" };\n } else if (sort.direction === \"asc\") {\n nextSort = { key: columnKey, direction: \"desc\" };\n } else {\n nextSort = null;\n }\n if (isControlledSort) {\n onSortChange?.(nextSort);\n } else {\n setUncontrolledSort(nextSort);\n // EC-8: sort change resets pagination to page 0\n if (!isControlledPage) setUncontrolledPage(0);\n }\n }\n\n function handlePageChange(page: number) {\n // Pagination uses 1-indexed; internal state 0-indexed\n const zeroIdx = page - 1;\n if (isControlledPage) {\n pagination?.onPageChange?.(zeroIdx);\n } else {\n setUncontrolledPage(zeroIdx);\n }\n }\n\n function toggleExpand(key: string) {\n if (expandMode === \"single\") {\n setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])));\n } else {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }\n }\n\n // Apply client-side sort in uncontrolled mode\n const sortedData = useMemo(() => {\n if (isControlledSort || sort === null) return data;\n const col = columns.find((c) => c.key === sort.key);\n if (!col) return data;\n const sorted = [...data].sort((a, b) => {\n const aVal = col.render\n ? null\n : (a as Record<string, unknown>)[sort.key as keyof T as string];\n const bVal = col.render\n ? null\n : (b as Record<string, unknown>)[sort.key as keyof T as string];\n const cmp = compareValues(aVal, bVal);\n return sort.direction === \"asc\" ? cmp : -cmp;\n });\n return sorted;\n }, [data, sort, isControlledSort, columns]);\n\n // Apply client-side pagination in uncontrolled mode\n const visibleData = useMemo(() => {\n if (!pagination) return sortedData;\n if (isControlledPage) return sortedData; // consumer pre-sliced\n const start = currentPage * effectivePageSize;\n return sortedData.slice(start, start + effectivePageSize);\n }, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);\n\n // EC-1 fix: compute colSpan accounting for chevron + actions columns\n const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);\n const expandedColSpan = columns.length + extraCols;\n const totalCols = columns.length + extraCols;\n\n // Loading state (EC-7: loading > empty)\n if (loading) {\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? <Table.HeaderCell aria-label=\"Expand\" /> : null}\n {columns.map((col) => (\n <Table.HeaderCell key={col.key} align={col.align}>\n {col.label}\n </Table.HeaderCell>\n ))}\n {rowActions ? <Table.HeaderCell aria-label=\"Actions\" /> : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {Array.from({ length: 5 }, (_, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders\n <Table.Row key={`skeleton-${i}`}>\n {Array.from({ length: totalCols }, (_, j) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders\n <Table.Cell key={`s-${i}-${j}`}>\n <Skeleton className=\"h-4 w-full\" />\n </Table.Cell>\n ))}\n </Table.Row>\n ))}\n </Table.Body>\n </Table>\n </div>\n );\n }\n\n // Empty state (after loading check)\n if (sortedData.length === 0) {\n return (\n <div className={cn(\"w-full\", className)}>\n {emptyState ?? <EmptyState title=\"No data\" description=\"There's nothing here yet.\" />}\n </div>\n );\n }\n\n const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;\n\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 z-10 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? <Table.HeaderCell aria-label=\"Expand\" /> : null}\n {columns.map((col) => {\n const isSortable = col.sortable === true;\n const isActive = sort?.key === col.key;\n return (\n <Table.HeaderCell\n key={col.key}\n align={col.align}\n onSort={isSortable ? () => handleSort(col.key) : undefined}\n sortDirection={isSortable ? (isActive ? sort?.direction : \"none\") : undefined}\n style={col.width ? { width: col.width } : undefined}\n >\n {col.label}\n </Table.HeaderCell>\n );\n })}\n {rowActions ? <Table.HeaderCell aria-label=\"Actions\" /> : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {visibleData.map((row) => {\n const key = rowKey(row);\n const expandedContent = expandable ? expandable(row) : null;\n const isExpandable = expandedContent !== null && expandedContent !== undefined;\n const isExpanded = expanded.has(key);\n return (\n <Fragment key={key}>\n <Table.Row>\n {expandable ? (\n <Table.Cell>\n {isExpandable ? (\n <button\n type=\"button\"\n onClick={() => toggleExpand(key)}\n aria-expanded={isExpanded}\n aria-controls={`expanded-${key}`}\n aria-label={isExpanded ? \"Collapse row\" : \"Expand row\"}\n className=\"inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted\"\n >\n {isExpanded ? (\n <ChevronDown aria-hidden=\"true\" className=\"size-4\" />\n ) : (\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n )}\n </button>\n ) : null}\n </Table.Cell>\n ) : null}\n {columns.map((col) => (\n <Table.Cell key={col.key} align={col.align} className={col.className}>\n {col.render\n ? col.render(row)\n : String((row as Record<string, unknown>)[col.key] ?? \"\")}\n </Table.Cell>\n ))}\n {rowActions ? (\n <Table.Cell align=\"right\">\n <DropdownMenu>\n <DropdownMenu.Trigger\n aria-label=\"Row actions\"\n className={cn(\n \"inline-flex size-7 items-center justify-center rounded-md\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">{rowActions(row)}</DropdownMenu.Content>\n </DropdownMenu>\n </Table.Cell>\n ) : null}\n </Table.Row>\n {isExpanded && isExpandable ? (\n <tr id={`expanded-${key}`}>\n <td colSpan={expandedColSpan} className=\"bg-muted/30 p-4\">\n {expandedContent}\n </td>\n </tr>\n ) : null}\n </Fragment>\n );\n })}\n </Table.Body>\n </Table>\n {pagination && totalPages > 1 ? (\n <div className=\"mt-4 flex items-center justify-end\">\n <Pagination\n currentPage={currentPage + 1}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable };\n"
|
|
24
|
+
"content": "import { ChevronDown, ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport { Fragment, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { DropdownMenu } from \"@/components/ui/dropdown-menu\";\nimport { EmptyState } from \"@/components/ui/empty-state\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Table } from \"@/components/ui/table\";\n\n/**\n * DataTable — generic, sortable, expandable composite over `<Table>`.\n *\n * Adds operator-grade entity-list patterns on top of the plain Table\n * primitive: sortable headers, sticky header, expandable rows\n * (multi-row by default), row action menus (Dropdown), client-side\n * pagination, loading skeleton rows, empty state. Both sort and\n * pagination support controlled OR uncontrolled mode (consumer\n * passes onSortChange / onPageChange to take over state).\n *\n * @example\n * <DataTable\n * columns={[\n * { key: \"name\", label: \"Name\", sortable: true },\n * { key: \"status\", label: \"Status\" },\n * ]}\n * data={domains}\n * rowKey={(d) => d.id}\n * expandable={(d) => d.status === \"pending\" ? <DnsRecords domain={d} /> : null}\n * rowActions={(d) => (\n * <>\n * <DropdownMenu.Item onSelect={() => editDomain(d)}>Edit</DropdownMenu.Item>\n * <DropdownMenu.Item onSelect={() => deleteDomain(d)}>Delete</DropdownMenu.Item>\n * </>\n * )}\n * />\n */\nexport interface DataTableColumn<T> {\n key: string;\n label: ReactNode;\n align?: \"left\" | \"center\" | \"right\";\n sortable?: boolean;\n width?: string;\n render?: (row: T) => ReactNode;\n className?: string;\n}\n\nexport interface DataTableSort {\n key: string;\n direction: \"asc\" | \"desc\";\n}\n\nexport interface DataTableProps<T> {\n data: T[];\n columns: DataTableColumn<T>[];\n rowKey: (row: T) => string;\n stickyHeader?: boolean;\n expandable?: (row: T) => ReactNode | null;\n expandMode?: \"single\" | \"multiple\";\n rowActions?: (row: T) => ReactNode;\n pagination?: {\n pageSize: number;\n controlledPage?: number;\n onPageChange?: (page: number) => void;\n } | null;\n defaultSort?: DataTableSort;\n sort?: DataTableSort | null;\n onSortChange?: (sort: DataTableSort | null) => void;\n loading?: boolean;\n emptyState?: ReactNode;\n className?: string;\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === b) return 0;\n if (a === null || a === undefined) return -1;\n if (b === null || b === undefined) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n return String(a).localeCompare(String(b));\n}\n\nfunction DataTable<T>(props: DataTableProps<T>): ReactNode {\n const {\n data,\n columns,\n rowKey,\n stickyHeader = true,\n expandable,\n expandMode = \"multiple\",\n rowActions,\n pagination,\n defaultSort,\n sort: controlledSort,\n onSortChange,\n loading = false,\n emptyState,\n className,\n } = props;\n\n const isControlledSort = onSortChange !== undefined;\n const [uncontrolledSort, setUncontrolledSort] = useState<DataTableSort | null>(\n defaultSort ?? null,\n );\n const sort = isControlledSort ? (controlledSort ?? null) : uncontrolledSort;\n\n const isControlledPage = pagination?.controlledPage !== undefined;\n const [uncontrolledPage, setUncontrolledPage] = useState(0);\n const currentPage = isControlledPage ? (pagination?.controlledPage ?? 0) : uncontrolledPage;\n\n // EC-9: clamp pageSize to >= 1 to avoid divide-by-zero / infinite render\n const effectivePageSize = Math.max(1, pagination?.pageSize ?? 10);\n\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n\n function handleSort(columnKey: string) {\n // Cycle: none → asc → desc → none\n let nextSort: DataTableSort | null;\n if (sort?.key !== columnKey) {\n nextSort = { key: columnKey, direction: \"asc\" };\n } else if (sort.direction === \"asc\") {\n nextSort = { key: columnKey, direction: \"desc\" };\n } else {\n nextSort = null;\n }\n if (isControlledSort) {\n onSortChange?.(nextSort);\n } else {\n setUncontrolledSort(nextSort);\n // EC-8: sort change resets pagination to page 0\n if (!isControlledPage) setUncontrolledPage(0);\n }\n }\n\n function handlePageChange(page: number) {\n // Pagination uses 1-indexed; internal state 0-indexed\n const zeroIdx = page - 1;\n if (isControlledPage) {\n pagination?.onPageChange?.(zeroIdx);\n } else {\n setUncontrolledPage(zeroIdx);\n }\n }\n\n function toggleExpand(key: string) {\n if (expandMode === \"single\") {\n setExpanded((prev) => (prev.has(key) ? new Set() : new Set([key])));\n } else {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n return next;\n });\n }\n }\n\n // Apply client-side sort in uncontrolled mode\n const sortedData = useMemo(() => {\n if (isControlledSort || sort === null) return data;\n const col = columns.find((c) => c.key === sort.key);\n if (!col) return data;\n const sorted = [...data].sort((a, b) => {\n const aVal = col.render\n ? null\n : (a as Record<string, unknown>)[sort.key as keyof T as string];\n const bVal = col.render\n ? null\n : (b as Record<string, unknown>)[sort.key as keyof T as string];\n const cmp = compareValues(aVal, bVal);\n return sort.direction === \"asc\" ? cmp : -cmp;\n });\n return sorted;\n }, [data, sort, isControlledSort, columns]);\n\n // Apply client-side pagination in uncontrolled mode\n const visibleData = useMemo(() => {\n if (!pagination) return sortedData;\n if (isControlledPage) return sortedData; // consumer pre-sliced\n const start = currentPage * effectivePageSize;\n return sortedData.slice(start, start + effectivePageSize);\n }, [sortedData, pagination, isControlledPage, currentPage, effectivePageSize]);\n\n // EC-1 fix: compute colSpan accounting for chevron + actions columns\n const extraCols = (expandable ? 1 : 0) + (rowActions ? 1 : 0);\n const expandedColSpan = columns.length + extraCols;\n const totalCols = columns.length + extraCols;\n\n // Loading state (EC-7: loading > empty)\n if (loading) {\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => (\n <Table.HeaderCell key={col.key} align={col.align}>\n {col.label}\n </Table.HeaderCell>\n ))}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {Array.from({ length: 5 }, (_, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows are positional placeholders\n <Table.Row key={`skeleton-${i}`}>\n {Array.from({ length: totalCols }, (_, j) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: skeleton cells are positional placeholders\n <Table.Cell key={`s-${i}-${j}`}>\n <Skeleton className=\"h-4 w-full\" />\n </Table.Cell>\n ))}\n </Table.Row>\n ))}\n </Table.Body>\n </Table>\n </div>\n );\n }\n\n // Empty state (after loading check)\n if (sortedData.length === 0) {\n return (\n <div className={cn(\"w-full\", className)}>\n {emptyState ?? <EmptyState title=\"No data\" description=\"There's nothing here yet.\" />}\n </div>\n );\n }\n\n const totalPages = pagination ? Math.ceil(sortedData.length / effectivePageSize) : 1;\n\n return (\n <div className={cn(\"w-full\", className)}>\n <Table>\n <Table.Header className={stickyHeader ? \"sticky top-0 z-10 bg-card\" : undefined}>\n <Table.Row>\n {expandable ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Expand</span>\n </Table.HeaderCell>\n ) : null}\n {columns.map((col) => {\n const isSortable = col.sortable === true;\n const isActive = sort?.key === col.key;\n return (\n <Table.HeaderCell\n key={col.key}\n align={col.align}\n onSort={isSortable ? () => handleSort(col.key) : undefined}\n sortDirection={isSortable ? (isActive ? sort?.direction : \"none\") : undefined}\n style={col.width ? { width: col.width } : undefined}\n >\n {col.label}\n </Table.HeaderCell>\n );\n })}\n {rowActions ? (\n <Table.HeaderCell>\n <span className=\"sr-only\">Actions</span>\n </Table.HeaderCell>\n ) : null}\n </Table.Row>\n </Table.Header>\n <Table.Body>\n {visibleData.map((row) => {\n const key = rowKey(row);\n const expandedContent = expandable ? expandable(row) : null;\n const isExpandable = expandedContent !== null && expandedContent !== undefined;\n const isExpanded = expanded.has(key);\n return (\n <Fragment key={key}>\n <Table.Row>\n {expandable ? (\n <Table.Cell>\n {isExpandable ? (\n <button\n type=\"button\"\n onClick={() => toggleExpand(key)}\n aria-expanded={isExpanded}\n aria-controls={`expanded-${key}`}\n aria-label={isExpanded ? \"Collapse row\" : \"Expand row\"}\n className=\"inline-flex items-center justify-center rounded-md p-0.5 hover:bg-muted\"\n >\n {isExpanded ? (\n <ChevronDown aria-hidden=\"true\" className=\"size-4\" />\n ) : (\n <ChevronRight aria-hidden=\"true\" className=\"size-4\" />\n )}\n </button>\n ) : null}\n </Table.Cell>\n ) : null}\n {columns.map((col) => (\n <Table.Cell key={col.key} align={col.align} className={col.className}>\n {col.render\n ? col.render(row)\n : String((row as Record<string, unknown>)[col.key] ?? \"\")}\n </Table.Cell>\n ))}\n {rowActions ? (\n <Table.Cell align=\"right\">\n <DropdownMenu>\n <DropdownMenu.Trigger\n aria-label=\"Row actions\"\n className={cn(\n \"inline-flex size-7 items-center justify-center rounded-md\",\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />\n </DropdownMenu.Trigger>\n <DropdownMenu.Content align=\"end\">{rowActions(row)}</DropdownMenu.Content>\n </DropdownMenu>\n </Table.Cell>\n ) : null}\n </Table.Row>\n {isExpanded && isExpandable ? (\n <tr id={`expanded-${key}`}>\n <td colSpan={expandedColSpan} className=\"bg-muted/30 p-4\">\n {expandedContent}\n </td>\n </tr>\n ) : null}\n </Fragment>\n );\n })}\n </Table.Body>\n </Table>\n {pagination && totalPages > 1 ? (\n <div className=\"mt-4 flex items-center justify-end\">\n <Pagination\n currentPage={currentPage + 1}\n totalPages={totalPages}\n onPageChange={handlePageChange}\n />\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable };\n"
|
|
25
25
|
}
|
|
26
26
|
]
|
|
27
27
|
}
|