diff --git a/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx b/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx index e2b81a3ea..e74620888 100644 --- a/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx +++ b/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx @@ -9,6 +9,7 @@ export default function AggregateFunctionsDemo(props: { return ( row.id} rows={aggregateFunctionsConfig.rows} rowGrouping={aggregateFunctionsConfig.tableProps.rowGrouping} columnResizing diff --git a/packages/examples/solid/src/demos/aggregate-functions/aggregate-functions.demo-data.ts b/packages/examples/solid/src/demos/aggregate-functions/aggregate-functions.demo-data.ts index 06a8a1289..ede687a9d 100644 --- a/packages/examples/solid/src/demos/aggregate-functions/aggregate-functions.demo-data.ts +++ b/packages/examples/solid/src/demos/aggregate-functions/aggregate-functions.demo-data.ts @@ -1,8 +1,21 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +/** Platform → category → creator nesting used by rowGrouping. */ +export type AggregateFunctionsRow = { + id: number; + name: string; + status: string; + followers?: number; + revenue?: string; + rating?: number; + contentCount?: number; + avgViewTime?: number; + categories?: AggregateFunctionsRow[]; + creators?: AggregateFunctionsRow[]; +}; -export const aggregateFunctionsHeaders: SolidColumnDef[] = [ +export const aggregateFunctionsHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", width: 200, expandable: true, type: "string" }, { accessor: "followers", @@ -66,7 +79,7 @@ export const aggregateFunctionsHeaders: SolidColumnDef[] = [ { accessor: "status", label: "Status", width: 120, type: "string" }, ]; -export const aggregateFunctionsData = [ +export const aggregateFunctionsData: AggregateFunctionsRow[] = [ { id: 1, name: "StreamFlix", @@ -187,7 +200,7 @@ export const aggregateFunctionsConfig = { headers: aggregateFunctionsHeaders, rows: aggregateFunctionsData, tableProps: { - rowGrouping: ["categories", "creators"] as string[], + rowGrouping: ["categories", "creators"], columnResizing: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/analytics/AnalyticsDemo.tsx b/packages/examples/solid/src/demos/analytics/AnalyticsDemo.tsx index 4042d0e92..6fcfb55c9 100644 --- a/packages/examples/solid/src/demos/analytics/AnalyticsDemo.tsx +++ b/packages/examples/solid/src/demos/analytics/AnalyticsDemo.tsx @@ -1,7 +1,7 @@ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; import type { TableAPI, Theme } from "@simple-table/solid"; -import { analyticsDemoConfig, analyticsPresets } from "./analytics.demo-data"; +import { analyticsDemoConfig, analyticsPresets, type AnalyticsFactRow } from "./analytics.demo-data"; import "@simple-table/solid/styles.css"; function formatHeight(height?: string | number | null): string { @@ -17,7 +17,7 @@ export default function AnalyticsDemo(props: { const [activeId, setActiveId] = createSignal(analyticsPresets[0].id); const [tableHeightPx, setTableHeightPx] = createSignal(null); let tableHost: HTMLDivElement | undefined; - let tableApi: TableAPI | undefined; + let tableApi: TableAPI | undefined; const active = createMemo( () => analyticsPresets.find((p) => p.id === activeId()) ?? analyticsPresets[0] diff --git a/packages/examples/solid/src/demos/analytics/analytics.demo-data.ts b/packages/examples/solid/src/demos/analytics/analytics.demo-data.ts index d7e93e9b6..0754b78ea 100644 --- a/packages/examples/solid/src/demos/analytics/analytics.demo-data.ts +++ b/packages/examples/solid/src/demos/analytics/analytics.demo-data.ts @@ -1,6 +1,20 @@ -import type { PivotConfig, SolidColumnDef, Row } from "@simple-table/solid"; +import type { PivotConfig, SolidColumnDef } from "@simple-table/solid"; -export const analyticsHeaders: SolidColumnDef[] = [ +export interface AnalyticsFactRow { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const analyticsHeaders: SolidColumnDef[] = [ { accessor: "region", label: "Region", @@ -100,30 +114,28 @@ export const analyticsHeaders: SolidColumnDef[] = [ }, ]; -const REGIONS = ["West", "East", "North", "South"] as const; -const COUNTRIES: Record<(typeof REGIONS)[number], string[]> = { +const COUNTRIES = { West: ["USA", "Canada"], East: ["USA", "UK"], North: ["Canada", "Sweden"], South: ["Brazil", "Australia"], }; -const CATEGORIES = ["Hardware", "Software"] as const; -const PRODUCTS: Record<(typeof CATEGORIES)[number], string[]> = { +const PRODUCTS = { Hardware: ["Widget", "Gadget", "Sensor"], Software: ["License", "Subscription"], }; -const CHANNELS = ["Direct", "Partner", "Online"] as const; -const YEARS = [2024, 2025] as const; -const QUARTERS = ["Q1", "Q2", "Q3", "Q4"] as const; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; /** Sparse multi-dimension fact cube (~150–250 rows). */ -export function generateAnalyticsRows(): Row[] { - const rows: Row[] = []; +export function generateAnalyticsRows(): AnalyticsFactRow[] { + const rows: AnalyticsFactRow[] = []; let id = 1; - for (const region of REGIONS) { - for (const country of COUNTRIES[region]) { - for (const category of CATEGORIES) { - for (const product of PRODUCTS[category]) { + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { for (const channel of CHANNELS) { for (const year of YEARS) { for (const quarter of QUARTERS) { @@ -156,7 +168,7 @@ export function generateAnalyticsRows(): Row[] { return rows; } -export const analyticsRows: Row[] = generateAnalyticsRows(); +export const analyticsRows: AnalyticsFactRow[] = generateAnalyticsRows(); export type AnalyticsPreset = { id: string; diff --git a/packages/examples/solid/src/demos/animations/AnimationsDemo.tsx b/packages/examples/solid/src/demos/animations/AnimationsDemo.tsx index 1f10d2ef9..95b754aa0 100644 --- a/packages/examples/solid/src/demos/animations/AnimationsDemo.tsx +++ b/packages/examples/solid/src/demos/animations/AnimationsDemo.tsx @@ -1,22 +1,27 @@ import { createSignal } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { Theme } from "@simple-table/solid"; -import { animationsConfig } from "./animations.demo-data"; +import type { Theme, SolidColumnDef } from "@simple-table/solid"; +import { animationsConfig, type AnimationsCrewMember } from "./animations.demo-data"; import "@simple-table/solid/styles.css"; export default function AnimationsDemo(props: { height?: string | number; theme?: Theme }) { const [headers, setHeaders] = createSignal([...animationsConfig.headers]); + const handleColumnOrderChange = (newHeaders: SolidColumnDef[]) => { + setHeaders(newHeaders); + }; + return ( row.id} rows={animationsConfig.rows} height={props.height ?? "400px"} theme={props.theme} - onColumnOrderChange={setHeaders} + onColumnOrderChange={handleColumnOrderChange} /> ); } diff --git a/packages/examples/solid/src/demos/animations/animations.demo-data.ts b/packages/examples/solid/src/demos/animations/animations.demo-data.ts index fdbae5736..4556111fd 100644 --- a/packages/examples/solid/src/demos/animations/animations.demo-data.ts +++ b/packages/examples/solid/src/demos/animations/animations.demo-data.ts @@ -1,8 +1,15 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface AnimationsCrewMember { + id: number; + name: string; + age: number; + role: string; + department: string; +} -export const animationsHeaders: SolidColumnDef[] = [ +export const animationsHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, sortable: true, type: "number" }, { accessor: "name", label: "Name", minWidth: 140, width: "1fr", sortable: true, type: "string" }, { accessor: "age", label: "Age", width: 80, align: "right", sortable: true, type: "number" }, @@ -18,7 +25,7 @@ export const animationsHeaders: SolidColumnDef[] = [ }, ]; -export const animationsData = [ +export const animationsData: AnimationsCrewMember[] = [ { id: 1, name: "Captain Stella Vega", age: 38, role: "Mission Commander", department: "Flight Operations" }, { id: 2, name: "Dr. Cosmos Rivera", age: 34, role: "Astrophysicist", department: "Science" }, { id: 3, name: "Commander Nebula Johnson", age: 42, role: "Operations Director", department: "Mission Control" }, @@ -37,4 +44,4 @@ export const animationsConfig = { headers: animationsHeaders, rows: animationsData, tableProps: { columnReordering: true, enableColumnEditor: true, enableColumnEditorInitOpen: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/billing/BillingDemo.tsx b/packages/examples/solid/src/demos/billing/BillingDemo.tsx index 48779ec20..539f1b88b 100644 --- a/packages/examples/solid/src/demos/billing/BillingDemo.tsx +++ b/packages/examples/solid/src/demos/billing/BillingDemo.tsx @@ -1,17 +1,17 @@ -import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; import { billingConfig } from "./billing.demo-data"; import type { BillingRow } from "./billing.demo-data"; import "@simple-table/solid/styles.css"; export default function BillingDemo(props: { height?: string | number; theme?: Theme }) { - const headers: SolidColumnDef[] = billingConfig.headers.map((h) => { + const headers: SolidColumnDef[] = billingConfig.headers.map((h) => { if (h.accessor === "name") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as BillingRow; - return
{d.name}
; - }, + cellRenderer: ({ row }: CellRendererProps) => ( +
{row.name}
+ ), }; } return h; @@ -23,6 +23,7 @@ export default function BillingDemo(props: { height?: string | number; theme?: T columnResizing columns={headers} enableColumnEditor + getRowId={({ row }) => row.id} height={props.height ?? "400px"} initialSortColumn="amount" initialSortDirection="desc" diff --git a/packages/examples/solid/src/demos/billing/billing.demo-data.ts b/packages/examples/solid/src/demos/billing/billing.demo-data.ts index 3f0e639be..4aaab7eb7 100644 --- a/packages/examples/solid/src/demos/billing/billing.demo-data.ts +++ b/packages/examples/solid/src/demos/billing/billing.demo-data.ts @@ -14,10 +14,27 @@ export interface BillingRow { [key: `revenue_${string}`]: number; } - -const ACCOUNT_NAMES = ["Acme Corp", "Globex Inc", "Initech", "Soylent Corp", "Umbrella LLC", "Wayne Industries", "Stark Tech", "Oscorp", "LexCorp", "Cyberdyne"]; +const ACCOUNT_NAMES = [ + "Acme Corp", + "Globex Inc", + "Initech", + "Soylent Corp", + "Umbrella LLC", + "Wayne Industries", + "Stark Tech", + "Oscorp", + "LexCorp", + "Cyberdyne", +]; const INVOICE_PREFIXES = ["INV", "SUB", "REN"]; -const CHARGE_TYPES = ["Subscription", "API Usage", "Storage", "Support Premium", "Bandwidth", "Compute"]; +const CHARGE_TYPES = [ + "Subscription", + "API Usage", + "Storage", + "Support Premium", + "Bandwidth", + "Compute", +]; const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; @@ -85,8 +102,8 @@ export function generateBillingData(count: number = 30): BillingRow[] { export const billingData = generateBillingData(30); -function generateMonthHeaders(): SolidColumnDef[] { - const headers: SolidColumnDef[] = []; +function generateMonthHeaders(): SolidColumnDef[] { + const headers: SolidColumnDef[] = []; const year = 2024; for (let monthIndex = 11; monthIndex >= 0; monthIndex--) { const fullMonthName = new Date(year, monthIndex).toLocaleString("default", { month: "long" }); @@ -136,7 +153,7 @@ function generateMonthHeaders(): SolidColumnDef[] { return headers; } -export const billingHeaders: SolidColumnDef[] = [ +export const billingHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", @@ -196,4 +213,4 @@ export const billingHeaders: SolidColumnDef[] = [ export const billingConfig = { headers: billingHeaders, rows: billingData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/cell-clicking/CellClickingDemo.tsx b/packages/examples/solid/src/demos/cell-clicking/CellClickingDemo.tsx index 204a80ada..15b55b5ed 100644 --- a/packages/examples/solid/src/demos/cell-clicking/CellClickingDemo.tsx +++ b/packages/examples/solid/src/demos/cell-clicking/CellClickingDemo.tsx @@ -11,11 +11,11 @@ export default function CellClickingDemo(props: { height?: string | number; them const [selectedTask, setSelectedTask] = createSignal(null); const [rows, setRows] = createSignal([...cellClickingData]); - const headers: SolidColumnDef[] = cellClickingHeaders.map((h) => { + const headers: SolidColumnDef[] = cellClickingHeaders.map((h) => { if (h.accessor === "priority") { return { ...h, - cellRenderer: (cr: CellRendererProps) => { + cellRenderer: (cr: CellRendererProps) => { const p = String(cr.row.priority); const color = p === "High" ? "#ef4444" : p === "Medium" ? "#f59e0b" : "#10b981"; return ( @@ -29,7 +29,7 @@ export default function CellClickingDemo(props: { height?: string | number; them if (h.accessor === "status") { return { ...h, - cellRenderer: (cr: CellRendererProps) => { + cellRenderer: (cr: CellRendererProps) => { const s = String(cr.row.status); const bg = s === "Completed" ? "#dcfce7" : s === "In Progress" ? "#fef3c7" : "#fee2e2"; const c = s === "Completed" ? "#166534" : s === "In Progress" ? "#92400e" : "#991b1b"; @@ -79,8 +79,8 @@ export default function CellClickingDemo(props: { height?: string | number; them const isDark = createMemo(() => props.theme === "modern-dark" || props.theme === "dark"); - const handleCellClick = ({ accessor, rowIndex, value, row }: CellClickProps) => { - const task = row as ProjectTask; + const handleCellClick = ({ accessor, rowIndex, value, row }: CellClickProps) => { + const task = row; switch (accessor) { case "priority": setClickInfo(`Filtering by ${value} priority`); @@ -187,6 +187,7 @@ export default function CellClickingDemo(props: { height?: string | number; them row.id} height={props.height ?? "320px"} onCellClick={handleCellClick} rows={rows()} diff --git a/packages/examples/solid/src/demos/cell-clicking/cell-clicking.demo-data.ts b/packages/examples/solid/src/demos/cell-clicking/cell-clicking.demo-data.ts index 748a95c20..7ef1cdb59 100644 --- a/packages/examples/solid/src/demos/cell-clicking/cell-clicking.demo-data.ts +++ b/packages/examples/solid/src/demos/cell-clicking/cell-clicking.demo-data.ts @@ -1,6 +1,5 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; - +import type { SolidColumnDef } from "@simple-table/solid"; export type ProjectTask = { id: number; @@ -17,19 +16,106 @@ export type ProjectTask = { export const STATUSES = ["Not Started", "In Progress", "Completed"]; export const cellClickingData: ProjectTask[] = [ - { id: 1001, task: "Design login page mockups", assignee: "Sarah Chen", priority: "High", status: "In Progress", dueDate: "2024-02-15", estimatedHours: 8, completedHours: 5, details: "Create responsive login page designs with modern UI patterns" }, - { id: 1002, task: "Implement user authentication API", assignee: "Marcus Rodriguez", priority: "High", status: "Not Started", dueDate: "2024-02-20", estimatedHours: 16, completedHours: 0, details: "Build secure JWT-based authentication system with OAuth integration" }, - { id: 1003, task: "Write unit tests for payment module", assignee: "Luna Martinez", priority: "Medium", status: "Completed", dueDate: "2024-02-10", estimatedHours: 12, completedHours: 12, details: "Comprehensive test coverage for payment processing functionality" }, - { id: 1004, task: "Update documentation for API endpoints", assignee: "Kai Thompson", priority: "Low", status: "In Progress", dueDate: "2024-02-25", estimatedHours: 6, completedHours: 3, details: "Update Swagger documentation and add usage examples" }, - { id: 1005, task: "Performance optimization for dashboard", assignee: "Zara Kim", priority: "Medium", status: "Not Started", dueDate: "2024-03-01", estimatedHours: 20, completedHours: 0, details: "Optimize rendering performance and implement lazy loading" }, - { id: 1006, task: "Mobile responsiveness testing", assignee: "Tyler Anderson", priority: "High", status: "In Progress", dueDate: "2024-02-18", estimatedHours: 10, completedHours: 7, details: "Test application across various mobile devices and screen sizes" }, - { id: 1007, task: "Setup CI/CD pipeline", assignee: "Phoenix Lee", priority: "Medium", status: "Completed", dueDate: "2024-02-08", estimatedHours: 14, completedHours: 14, details: "Automated testing and deployment pipeline using GitHub Actions" }, - { id: 1008, task: "Database migration scripts", assignee: "River Jackson", priority: "Low", status: "Not Started", dueDate: "2024-02-28", estimatedHours: 8, completedHours: 0, details: "Create migration scripts for database schema updates" }, + { + id: 1001, + task: "Design login page mockups", + assignee: "Sarah Chen", + priority: "High", + status: "In Progress", + dueDate: "2024-02-15", + estimatedHours: 8, + completedHours: 5, + details: "Create responsive login page designs with modern UI patterns", + }, + { + id: 1002, + task: "Implement user authentication API", + assignee: "Marcus Rodriguez", + priority: "High", + status: "Not Started", + dueDate: "2024-02-20", + estimatedHours: 16, + completedHours: 0, + details: "Build secure JWT-based authentication system with OAuth integration", + }, + { + id: 1003, + task: "Write unit tests for payment module", + assignee: "Luna Martinez", + priority: "Medium", + status: "Completed", + dueDate: "2024-02-10", + estimatedHours: 12, + completedHours: 12, + details: "Comprehensive test coverage for payment processing functionality", + }, + { + id: 1004, + task: "Update documentation for API endpoints", + assignee: "Kai Thompson", + priority: "Low", + status: "In Progress", + dueDate: "2024-02-25", + estimatedHours: 6, + completedHours: 3, + details: "Update Swagger documentation and add usage examples", + }, + { + id: 1005, + task: "Performance optimization for dashboard", + assignee: "Zara Kim", + priority: "Medium", + status: "Not Started", + dueDate: "2024-03-01", + estimatedHours: 20, + completedHours: 0, + details: "Optimize rendering performance and implement lazy loading", + }, + { + id: 1006, + task: "Mobile responsiveness testing", + assignee: "Tyler Anderson", + priority: "High", + status: "In Progress", + dueDate: "2024-02-18", + estimatedHours: 10, + completedHours: 7, + details: "Test application across various mobile devices and screen sizes", + }, + { + id: 1007, + task: "Setup CI/CD pipeline", + assignee: "Phoenix Lee", + priority: "Medium", + status: "Completed", + dueDate: "2024-02-08", + estimatedHours: 14, + completedHours: 14, + details: "Automated testing and deployment pipeline using GitHub Actions", + }, + { + id: 1008, + task: "Database migration scripts", + assignee: "River Jackson", + priority: "Low", + status: "Not Started", + dueDate: "2024-02-28", + estimatedHours: 8, + completedHours: 0, + details: "Create migration scripts for database schema updates", + }, ]; -export const cellClickingHeaders: SolidColumnDef[] = [ +export const cellClickingHeaders: SolidColumnDef[] = [ { accessor: "id", label: "Task ID", width: 80, sortable: true, type: "number" }, - { accessor: "task", label: "Task Name", minWidth: 150, width: "1fr", sortable: true, type: "string" }, + { + accessor: "task", + label: "Task Name", + minWidth: 150, + width: "1fr", + sortable: true, + type: "string", + }, { accessor: "assignee", label: "Assignee", width: 120, sortable: true, type: "string" }, { accessor: "priority", label: "Priority", width: 100, sortable: true, type: "string" }, { accessor: "status", label: "Status", width: 120, sortable: true, type: "string" }, @@ -42,6 +128,6 @@ export const cellClickingHeaders: SolidColumnDef[] = [ export const cellClickingConfig = { headers: cellClickingHeaders, rows: cellClickingData, -} as const; +}; export { STATUSES as CELL_CLICKING_STATUSES }; diff --git a/packages/examples/solid/src/demos/cell-editing/CellEditingDemo.tsx b/packages/examples/solid/src/demos/cell-editing/CellEditingDemo.tsx index 810bb3036..668de5899 100644 --- a/packages/examples/solid/src/demos/cell-editing/CellEditingDemo.tsx +++ b/packages/examples/solid/src/demos/cell-editing/CellEditingDemo.tsx @@ -1,24 +1,24 @@ import { createSignal } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, CellChangeProps } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme } from "@simple-table/solid"; import { cellEditingConfig } from "./cell-editing.demo-data"; import "@simple-table/solid/styles.css"; export default function CellEditingDemo(props: { height?: string | number; theme?: Theme }) { const [data, setData] = createSignal([...cellEditingConfig.rows]); - const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { - setData((prev) => - prev.map((item) => (item.id === row.id ? { ...item, [accessor]: newValue } : item)) - ); - }; - return ( row.id} rows={data()} height={props.height ?? "400px"} theme={props.theme} - onCellEdit={handleCellEdit} + onCellEdit={({ accessor, newValue, row }) => { + setData((prev) => + prev.map((item) => (item.id === row.id ? { ...item, [accessor]: newValue } : item)), + ); + }} /> ); } diff --git a/packages/examples/solid/src/demos/cell-editing/cell-editing.demo-data.ts b/packages/examples/solid/src/demos/cell-editing/cell-editing.demo-data.ts index 554b543a0..46e04566b 100644 --- a/packages/examples/solid/src/demos/cell-editing/cell-editing.demo-data.ts +++ b/packages/examples/solid/src/demos/cell-editing/cell-editing.demo-data.ts @@ -1,8 +1,17 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface CellEditingEmployee { + id: number; + firstName: string; + lastName: string; + role: string; + hireDate: string; + isActive: boolean; + salary: number; +} -export const cellEditingHeaders: SolidColumnDef[] = [ +export const cellEditingHeaders: SolidColumnDef[] = [ { accessor: "firstName", label: "First Name", width: "1fr", minWidth: 100, editable: true, type: "string" }, { accessor: "lastName", label: "Last Name", width: 120, editable: true, type: "string" }, { accessor: "role", label: "Role", width: 120, editable: true, type: "enum", enumOptions: [ @@ -17,7 +26,7 @@ export const cellEditingHeaders: SolidColumnDef[] = [ { accessor: "salary", label: "Salary", width: 120, editable: true, type: "number" }, ]; -export const cellEditingData = [ +export const cellEditingData: CellEditingEmployee[] = [ { id: 1, firstName: "Ranger", lastName: "Wilde", role: "Manager", hireDate: "2019-03-12", isActive: true, salary: 89000 }, { id: 2, firstName: "Safari", lastName: "Brooks", role: "Designer", hireDate: "2021-07-18", isActive: true, salary: 74000 }, { id: 3, firstName: "Forest", lastName: "Rivers", role: "Manager", hireDate: "2018-11-08", isActive: true, salary: 94000 }, @@ -35,4 +44,4 @@ export const cellEditingData = [ export const cellEditingConfig = { headers: cellEditingHeaders, rows: cellEditingData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/cell-highlighting/CellHighlightingDemo.tsx b/packages/examples/solid/src/demos/cell-highlighting/CellHighlightingDemo.tsx index 8fca200de..d34e1a802 100644 --- a/packages/examples/solid/src/demos/cell-highlighting/CellHighlightingDemo.tsx +++ b/packages/examples/solid/src/demos/cell-highlighting/CellHighlightingDemo.tsx @@ -6,6 +6,7 @@ export default function CellHighlightingDemo(props: { height?: string | number; return ( row.id} rows={cellHighlightingConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/cell-highlighting/cell-highlighting.demo-data.ts b/packages/examples/solid/src/demos/cell-highlighting/cell-highlighting.demo-data.ts index 286950146..534d41fe2 100644 --- a/packages/examples/solid/src/demos/cell-highlighting/cell-highlighting.demo-data.ts +++ b/packages/examples/solid/src/demos/cell-highlighting/cell-highlighting.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface CellHighlightingEmployee { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; +} -export const cellHighlightingHeaders: SolidColumnDef[] = [ +export const cellHighlightingHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, sortable: true, type: "number" }, { accessor: "name", label: "Name", minWidth: 80, width: "1fr", sortable: true, type: "string" }, { accessor: "age", label: "Age", width: 100, sortable: true, type: "number" }, @@ -11,7 +19,7 @@ export const cellHighlightingHeaders: SolidColumnDef[] = [ { accessor: "startDate", label: "Start Date", width: 150, sortable: true, type: "date" }, ]; -export const cellHighlightingData = [ +export const cellHighlightingData: CellHighlightingEmployee[] = [ { id: 1, name: "Davi Thompson", age: 29, role: "Personal Trainer", department: "Fitness", startDate: "2021-03-15" }, { id: 2, name: "Paloma Martinez", age: 26, role: "Yoga Instructor", department: "Group Classes", startDate: "2022-01-10" }, { id: 3, name: "Jaxon Johnson", age: 34, role: "Fitness Manager", department: "Management", startDate: "2019-08-20" }, @@ -30,4 +38,4 @@ export const cellHighlightingConfig = { headers: cellHighlightingHeaders, rows: cellHighlightingData, tableProps: { selectableCells: true, selectableColumns: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/cell-renderer/CellRendererDemo.tsx b/packages/examples/solid/src/demos/cell-renderer/CellRendererDemo.tsx index f7c82cb55..044817dd2 100644 --- a/packages/examples/solid/src/demos/cell-renderer/CellRendererDemo.tsx +++ b/packages/examples/solid/src/demos/cell-renderer/CellRendererDemo.tsx @@ -16,8 +16,8 @@ const getInitials = (name: string) => .join("") .toUpperCase(); -const TeamCell = (props: CellRendererProps) => { - const members = (props.row as CellRendererEmployee).teamMembers; +const TeamCell = (props: CellRendererProps) => { + const members = props.row.teamMembers; return (
{members.map((m) => ( @@ -46,7 +46,7 @@ const TeamCell = (props: CellRendererProps) => { ); }; -const WebsiteCell = (props: CellRendererProps) => { +const WebsiteCell = (props: CellRendererProps) => { const url = String(props.value); return ( @@ -65,7 +65,7 @@ const WebsiteCell = (props: CellRendererProps) => { ); }; -const StatusCell = (props: CellRendererProps) => { +const StatusCell = (props: CellRendererProps) => { const status = String(props.value); const map: Record = { active: { icon: "✓", color: "#10B981" }, @@ -80,7 +80,7 @@ const StatusCell = (props: CellRendererProps) => { ); }; -const ProgressCell = (props: CellRendererProps) => { +const ProgressCell = (props: CellRendererProps) => { const pct = Number(props.value) || 0; const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981"; return ( @@ -108,7 +108,7 @@ const ProgressCell = (props: CellRendererProps) => { ); }; -const RatingCell = (props: CellRendererProps) => { +const RatingCell = (props: CellRendererProps) => { const rating = Number(props.value) || 0; const full = Math.floor(rating); const hasHalf = rating % 1 >= 0.25; @@ -125,7 +125,7 @@ const RatingCell = (props: CellRendererProps) => { ); }; -const VerifiedCell = (props: CellRendererProps) => { +const VerifiedCell = (props: CellRendererProps) => { const yes = Boolean(props.value); return ( @@ -134,8 +134,9 @@ const VerifiedCell = (props: CellRendererProps) => { ); }; -const TagsCell = (props: CellRendererProps) => { - const tags = Array.isArray(props.value) ? (props.value as string[]) : []; +const TagsCell = (props: CellRendererProps) => { + const raw = Array.isArray(props.value) ? props.value : []; + const tags = raw.filter((t): t is string => typeof t === "string"); return (
{tags.map((tag) => ( @@ -158,7 +159,7 @@ const TagsCell = (props: CellRendererProps) => { ); }; -const RENDERER_MAP: Record = { +const RENDERER_MAP: Partial>> = { teamMembers: TeamCell, website: WebsiteCell, status: StatusCell, @@ -168,15 +169,16 @@ const RENDERER_MAP: Record = { tags: TagsCell, }; -const HEADERS: SolidColumnDef[] = cellRendererConfig.headers.map((h) => { - const fn = RENDERER_MAP[String(h.accessor)]; - return fn !== undefined ? { ...h, cellRenderer: fn } : h; +const HEADERS: SolidColumnDef[] = cellRendererConfig.headers.map((h) => { + const cellRenderer = RENDERER_MAP[String(h.accessor)]; + return cellRenderer ? { ...h, cellRenderer } : h; }); export default function CellRendererDemo(props: { height?: string | number; theme?: Theme }) { return ( row.id} rows={cellRendererConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/cell-renderer/cell-renderer.demo-data.ts b/packages/examples/solid/src/demos/cell-renderer/cell-renderer.demo-data.ts index 2bc3563de..36f29c007 100644 --- a/packages/examples/solid/src/demos/cell-renderer/cell-renderer.demo-data.ts +++ b/packages/examples/solid/src/demos/cell-renderer/cell-renderer.demo-data.ts @@ -29,7 +29,7 @@ export const cellRendererData: CellRendererEmployee[] = [ { id: 12, name: "Atlas Johnson", website: "atlasjohnson.brand", status: "inactive", progress: 28, rating: 3.6, verified: false, tags: ["Brand Design", "Graphic Design"], teamMembers: [{ name: "Uma Patel", role: "Graphic Designer" }] }, ]; -export const cellRendererHeaders: SolidColumnDef[] = [ +export const cellRendererHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: 180, type: "string" }, { accessor: "teamMembers", label: "Team", width: 280, type: "string" }, @@ -48,4 +48,4 @@ export const cellRendererConfig = { selectableCells: true, customTheme: { rowHeight: 48 }, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/charts/ChartsDemo.tsx b/packages/examples/solid/src/demos/charts/ChartsDemo.tsx index 2ce51f3b9..db1dcb09d 100644 --- a/packages/examples/solid/src/demos/charts/ChartsDemo.tsx +++ b/packages/examples/solid/src/demos/charts/ChartsDemo.tsx @@ -8,6 +8,7 @@ export default function ChartsDemo(props: { height?: string | number; theme?: Th columnReordering={chartsConfig.tableProps.columnReordering} columnResizing={chartsConfig.tableProps.columnResizing} columns={chartsConfig.headers} + getRowId={({ row }) => row.id} rows={chartsConfig.rows} selectableCells={chartsConfig.tableProps.selectableCells} height={props.height ?? "400px"} diff --git a/packages/examples/solid/src/demos/charts/charts.demo-data.ts b/packages/examples/solid/src/demos/charts/charts.demo-data.ts index 12973e6b8..99f39e8cb 100644 --- a/packages/examples/solid/src/demos/charts/charts.demo-data.ts +++ b/packages/examples/solid/src/demos/charts/charts.demo-data.ts @@ -1,6 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface ChartsProduct { + id: number; + product: string; + category: string; + monthlySales: number[]; + dailyViews: number[]; + quarterlyRevenue: number[]; + weeklyOrders: number[]; + rating: number; +} const generateTrendData = (baseValue: number, volatility: number, length: number = 12): number[] => { const data: number[] = []; @@ -13,7 +23,7 @@ const generateTrendData = (baseValue: number, volatility: number, length: number return data; }; -export const chartsData = [ +export const chartsData: ChartsProduct[] = [ { id: 1, product: "Laptop Pro", category: "Electronics", monthlySales: generateTrendData(150, 30, 12), dailyViews: generateTrendData(500, 100, 30), quarterlyRevenue: [45000, 52000, 48000, 61000], weeklyOrders: [23, 28, 31, 25, 29, 35, 38], rating: 4.5 }, { id: 2, product: "Wireless Mouse", category: "Accessories", monthlySales: generateTrendData(300, 50, 12), dailyViews: generateTrendData(800, 150, 30), quarterlyRevenue: [12000, 15000, 18000, 21000], weeklyOrders: [45, 52, 48, 61, 58, 67, 71], rating: 4.2 }, { id: 3, product: "USB-C Cable", category: "Accessories", monthlySales: generateTrendData(500, 80, 12), dailyViews: generateTrendData(1200, 200, 30), quarterlyRevenue: [8000, 9000, 7500, 10000], weeklyOrders: [78, 82, 75, 88, 91, 85, 93], rating: 4.7 }, @@ -28,7 +38,7 @@ export const chartsData = [ { id: 12, product: "Smart Home Hub", category: "Electronics", monthlySales: generateTrendData(100, 25, 12), dailyViews: generateTrendData(400, 80, 30), quarterlyRevenue: [35000, 38000, 42000, 45000], weeklyOrders: [15, 18, 22, 19, 21, 24, 27], rating: 4.6 }, ]; -export const chartsHeaders: SolidColumnDef[] = [ +export const chartsHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 70, sortable: true, type: "number" }, { accessor: "product", label: "Product", width: 180, sortable: true, type: "string" }, { accessor: "category", label: "Category", width: 120, sortable: true, type: "string" }, @@ -47,4 +57,4 @@ export const chartsConfig = { columnResizing: true, selectableCells: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx b/packages/examples/solid/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx index d8febb734..3b80c2156 100644 --- a/packages/examples/solid/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx +++ b/packages/examples/solid/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx @@ -9,6 +9,7 @@ export default function CollapsibleColumnsDemo(props: { return ( row.id} rows={collapsibleColumnsConfig.rows} columnResizing enableColumnEditor diff --git a/packages/examples/solid/src/demos/collapsible-columns/collapsible-columns.demo-data.ts b/packages/examples/solid/src/demos/collapsible-columns/collapsible-columns.demo-data.ts index 944845fd2..4a48d7bb0 100644 --- a/packages/examples/solid/src/demos/collapsible-columns/collapsible-columns.demo-data.ts +++ b/packages/examples/solid/src/demos/collapsible-columns/collapsible-columns.demo-data.ts @@ -1,8 +1,38 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef } from "@simple-table/solid"; +import type { SolidCellRenderer, SolidColumnDef } from "@simple-table/solid"; +export interface CollapsibleSalesRep { + id: number; + name: string; + region: string; + q1Sales: number; + q2Sales: number; + q3Sales: number; + q4Sales: number; + totalSales: number; + avgQuarterly: number; + jan: number; + feb: number; + mar: number; + apr: number; + may: number; + jun: number; + jul: number; + aug: number; + sep: number; + oct: number; + nov: number; + dec: number; + avgMonthly: number; + bestMonth: number; + softwareSales: number; + hardwareSales: number; + servicesSales: number; + topCategory: string; + categoryCount: number; +} -export const collapsibleColumnsData = [ +export const collapsibleColumnsData: CollapsibleSalesRep[] = [ { id: 1, name: "Alice Thompson", region: "North America", q1Sales: 245000, q2Sales: 289000, q3Sales: 312000, q4Sales: 298000, totalSales: 1144000, avgQuarterly: 286000, jan: 78000, feb: 82000, mar: 85000, apr: 89000, may: 95000, jun: 105000, jul: 98000, aug: 102000, sep: 112000, oct: 108000, nov: 95000, dec: 95000, avgMonthly: 95333, bestMonth: 112000, softwareSales: 456000, hardwareSales: 342000, servicesSales: 346000, topCategory: "Software", categoryCount: 3 }, { id: 2, name: "Marcus Chen", region: "Asia Pacific", q1Sales: 189000, q2Sales: 234000, q3Sales: 287000, q4Sales: 276000, totalSales: 986000, avgQuarterly: 246500, jan: 58000, feb: 62000, mar: 69000, apr: 72000, may: 78000, jun: 84000, jul: 89000, aug: 95000, sep: 103000, oct: 98000, nov: 89000, dec: 89000, avgMonthly: 82166, bestMonth: 103000, softwareSales: 398000, hardwareSales: 298000, servicesSales: 290000, topCategory: "Software", categoryCount: 3 }, { id: 3, name: "Sofia Rodriguez", region: "Europe", q1Sales: 198000, q2Sales: 245000, q3Sales: 267000, q4Sales: 289000, totalSales: 999000, avgQuarterly: 249750, jan: 62000, feb: 66000, mar: 70000, apr: 78000, may: 82000, jun: 85000, jul: 85000, aug: 88000, sep: 94000, oct: 96000, nov: 97000, dec: 96000, avgMonthly: 83250, bestMonth: 97000, softwareSales: 389000, hardwareSales: 312000, servicesSales: 298000, topCategory: "Software", categoryCount: 3 }, @@ -17,21 +47,38 @@ export const collapsibleColumnsData = [ { id: 12, name: "Christopher Lee", region: "Europe", q1Sales: 167000, q2Sales: 189000, q3Sales: 212000, q4Sales: 234000, totalSales: 802000, avgQuarterly: 200500, jan: 52000, feb: 55000, mar: 60000, apr: 61000, may: 63000, jun: 65000, jul: 68000, aug: 71000, sep: 73000, oct: 76000, nov: 78000, dec: 80000, avgMonthly: 66833, bestMonth: 80000, softwareSales: 320000, hardwareSales: 241000, servicesSales: 241000, topCategory: "Software", categoryCount: 3 }, ]; -const fmt = (accessor: string) => ({ row }: { row: Record }) => - `$${((row[accessor] as number) || 0).toLocaleString()}`; +const fmtCurrency: SolidCellRenderer = ({ value }) => + `$${(value ?? 0).toLocaleString()}`; -const monthCol = (accessor: string, label: string): SolidColumnDef => ({ +type MonthKey = + | "jan" + | "feb" + | "mar" + | "apr" + | "may" + | "jun" + | "jul" + | "aug" + | "sep" + | "oct" + | "nov" + | "dec"; + +const monthCol = ( + accessor: MonthKey, + label: string, +): SolidColumnDef => ({ accessor, label, width: 100, - showWhen: "parentExpanded" as const, + showWhen: "parentExpanded", sortable: true, align: "right", type: "number", - cellRenderer: fmt(accessor), + cellRenderer: fmtCurrency, }); -export const collapsibleColumnsHeaders: SolidColumnDef[] = [ +export const collapsibleColumnsHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, sortable: true }, { accessor: "name", label: "Sales Rep", minWidth: 150, width: "1fr", sortable: true }, { accessor: "region", label: "Region", width: 140, sortable: true }, @@ -42,11 +89,11 @@ export const collapsibleColumnsHeaders: SolidColumnDef[] = [ collapsible: true, collapseDefault: true, children: [ - { accessor: "totalSales", label: "Total Sales", width: 140, showWhen: "parentCollapsed" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("totalSales") }, - { accessor: "q1Sales", label: "Q1", width: 120, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("q1Sales") }, - { accessor: "q2Sales", label: "Q2", width: 120, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("q2Sales") }, - { accessor: "q3Sales", label: "Q3", width: 120, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("q3Sales") }, - { accessor: "q4Sales", label: "Q4", width: 120, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("q4Sales") }, + { accessor: "totalSales", label: "Total Sales", width: 140, showWhen: "parentCollapsed", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "q1Sales", label: "Q1", width: 120, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "q2Sales", label: "Q2", width: 120, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "q3Sales", label: "Q3", width: 120, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "q4Sales", label: "Q4", width: 120, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, ], }, { @@ -56,8 +103,8 @@ export const collapsibleColumnsHeaders: SolidColumnDef[] = [ collapsible: true, collapseDefault: true, children: [ - { accessor: "avgMonthly", label: "Avg Monthly", width: 130, showWhen: "parentCollapsed" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("avgMonthly") }, - { accessor: "bestMonth", label: "Best Month", width: 130, showWhen: "parentCollapsed" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("bestMonth") }, + { accessor: "avgMonthly", label: "Avg Monthly", width: 130, showWhen: "parentCollapsed", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "bestMonth", label: "Best Month", width: 130, showWhen: "parentCollapsed", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, monthCol("jan", "Jan"), monthCol("feb", "Feb"), monthCol("mar", "Mar"), monthCol("apr", "Apr"), monthCol("may", "May"), monthCol("jun", "Jun"), monthCol("jul", "Jul"), monthCol("aug", "Aug"), monthCol("sep", "Sep"), @@ -71,10 +118,10 @@ export const collapsibleColumnsHeaders: SolidColumnDef[] = [ collapsible: true, collapseDefault: true, children: [ - { accessor: "topCategory", label: "Top Category", width: 140, showWhen: "parentCollapsed" as const, sortable: true, type: "string" }, - { accessor: "softwareSales", label: "Software", width: 130, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("softwareSales") }, - { accessor: "hardwareSales", label: "Hardware", width: 130, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("hardwareSales") }, - { accessor: "servicesSales", label: "Services", width: 130, showWhen: "parentExpanded" as const, sortable: true, align: "right", type: "number", cellRenderer: fmt("servicesSales") }, + { accessor: "topCategory", label: "Top Category", width: 140, showWhen: "parentCollapsed", sortable: true, type: "string" }, + { accessor: "softwareSales", label: "Software", width: 130, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "hardwareSales", label: "Hardware", width: 130, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, + { accessor: "servicesSales", label: "Services", width: 130, showWhen: "parentExpanded", sortable: true, align: "right", type: "number", cellRenderer: fmtCurrency }, ], }, ]; @@ -88,4 +135,4 @@ export const collapsibleColumnsConfig = { selectableCells: true, columnReordering: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-alignment/ColumnAlignmentDemo.tsx b/packages/examples/solid/src/demos/column-alignment/ColumnAlignmentDemo.tsx index 30db0acb8..083cd9abd 100644 --- a/packages/examples/solid/src/demos/column-alignment/ColumnAlignmentDemo.tsx +++ b/packages/examples/solid/src/demos/column-alignment/ColumnAlignmentDemo.tsx @@ -6,6 +6,7 @@ export default function ColumnAlignmentDemo(props: { height?: string | number; t return ( row.id} rows={columnAlignmentConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-alignment/column-alignment.demo-data.ts b/packages/examples/solid/src/demos/column-alignment/column-alignment.demo-data.ts index 19af4d8b1..663b00a76 100644 --- a/packages/examples/solid/src/demos/column-alignment/column-alignment.demo-data.ts +++ b/packages/examples/solid/src/demos/column-alignment/column-alignment.demo-data.ts @@ -1,8 +1,15 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface ColumnAlignmentPlayer { + id: number; + name: string; + score: number; + rating: number; + status: string; +} -export const columnAlignmentHeaders: SolidColumnDef[] = [ +export const columnAlignmentHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, align: "left", type: "number" }, { accessor: "name", label: "Name", minWidth: 100, width: "1fr", align: "center", type: "string" }, { accessor: "score", label: "Score", width: 120, align: "right", type: "number" }, @@ -10,7 +17,7 @@ export const columnAlignmentHeaders: SolidColumnDef[] = [ { accessor: "status", label: "Status", width: 120, align: "left", type: "string" }, ]; -export const columnAlignmentData = [ +export const columnAlignmentData: ColumnAlignmentPlayer[] = [ { id: 1, name: "Camila Rodriguez", score: 94, rating: 4.9, status: "Active" }, { id: 2, name: "Enzo Silva", score: 89, rating: 4.6, status: "Active" }, { id: 3, name: "Yuki Kim", score: 96, rating: 4.8, status: "Active" }, @@ -28,4 +35,4 @@ export const columnAlignmentData = [ export const columnAlignmentConfig = { headers: columnAlignmentHeaders, rows: columnAlignmentData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-editing/ColumnEditingDemo.tsx b/packages/examples/solid/src/demos/column-editing/ColumnEditingDemo.tsx index b4fc8d41b..45523f95f 100644 --- a/packages/examples/solid/src/demos/column-editing/ColumnEditingDemo.tsx +++ b/packages/examples/solid/src/demos/column-editing/ColumnEditingDemo.tsx @@ -1,16 +1,27 @@ import { createSignal, createMemo } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; import type { Theme, SolidColumnDef } from "@simple-table/solid"; -import { columnEditingData, columnEditingHeaders } from "./column-editing.demo-data"; +import { + columnEditingData, + columnEditingHeaders, + type ColumnEditingEmployee, +} from "./column-editing.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnEditingDemo(props: { height?: string | number; theme?: Theme }) { - const [additionalColumns, setAdditionalColumns] = createSignal([]); + const [additionalColumns, setAdditionalColumns] = createSignal< + SolidColumnDef[] + >([]); const [lastAdded, setLastAdded] = createSignal(""); const addColumn = () => { const n = additionalColumns().length + 1; - const col: SolidColumnDef = { accessor: `custom-${n}`, label: `Custom ${n}`, width: 120, type: "string" }; + const col: SolidColumnDef = { + accessor: `custom-${n}`, + label: `Custom ${n}`, + width: 120, + type: "string", + }; setAdditionalColumns([...additionalColumns(), col]); setLastAdded(col.label); }; @@ -41,12 +52,15 @@ export default function ColumnEditingDemo(props: { height?: string | number; the
row.id} rows={columnEditingData} height={props.height ?? "400px"} theme={props.theme} enableHeaderEditing selectableColumns - onHeaderEdit={(_header, newLabel) => setLastAdded(`Renamed to: ${newLabel}`)} + onHeaderEdit={(_header: SolidColumnDef, newLabel: string) => + setLastAdded(`Renamed to: ${newLabel}`) + } />
); diff --git a/packages/examples/solid/src/demos/column-editing/column-editing.demo-data.ts b/packages/examples/solid/src/demos/column-editing/column-editing.demo-data.ts index 1cec98f11..096e1d210 100644 --- a/packages/examples/solid/src/demos/column-editing/column-editing.demo-data.ts +++ b/packages/examples/solid/src/demos/column-editing/column-editing.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface ColumnEditingEmployee { + id: number; + name: string; + age: number; + role: string; + department: string; + email: string; +} -export const columnEditingHeaders: SolidColumnDef[] = [ +export const columnEditingHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 120, width: "1fr", type: "string" }, { accessor: "age", label: "Age", width: 100, type: "number" }, @@ -10,7 +18,7 @@ export const columnEditingHeaders: SolidColumnDef[] = [ { accessor: "department", label: "Department", width: 150, type: "string" }, ]; -export const columnEditingData = [ +export const columnEditingData: ColumnEditingEmployee[] = [ { id: 1, name: "Marcus Rodriguez", age: 29, role: "Frontend Developer", department: "Engineering", email: "marcus.rodriguez@company.com" }, { id: 2, name: "Sophia Chen", age: 27, role: "UX/UI Designer", department: "Design", email: "sophia.chen@company.com" }, { id: 3, name: "Raj Patel", age: 34, role: "Engineering Manager", department: "Management", email: "raj.patel@company.com" }, @@ -29,4 +37,4 @@ export const columnEditingConfig = { headers: columnEditingHeaders, rows: columnEditingData, tableProps: { enableHeaderEditing: true, selectableColumns: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx b/packages/examples/solid/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx index e021fcc1d..829c0b951 100644 --- a/packages/examples/solid/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx +++ b/packages/examples/solid/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx @@ -73,6 +73,7 @@ export default function ColumnEditorCustomRendererDemo(props: { return ( row.id} rows={columnEditorCustomRendererConfig.rows} enableColumnEditor columnEditorConfig={columnEditorConfig} diff --git a/packages/examples/solid/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts b/packages/examples/solid/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts index 2c2889369..f8101b0c7 100644 --- a/packages/examples/solid/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts +++ b/packages/examples/solid/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts @@ -1,19 +1,92 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface ColumnEditorCustomRendererEmployee { + id: number; + name: string; + email: string; + role: string; + salary: number; + department: string; + status: string; +} -export const columnEditorCustomRendererData: Row[] = [ - { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Engineer", salary: 125000, department: "Engineering", status: "active" }, - { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Designer", salary: 98000, department: "Design", status: "active" }, - { id: 3, name: "Clara Chen", email: "clara@example.com", role: "PM", salary: 115000, department: "Product", status: "inactive" }, - { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering", status: "active" }, - { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics", status: "active" }, - { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering", status: "inactive" }, - { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design", status: "active" }, - { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering", status: "active" }, +export const columnEditorCustomRendererData: ColumnEditorCustomRendererEmployee[] = [ + { + id: 1, + name: "Alice Johnson", + email: "alice@example.com", + role: "Engineer", + salary: 125000, + department: "Engineering", + status: "active", + }, + { + id: 2, + name: "Bob Martinez", + email: "bob@example.com", + role: "Designer", + salary: 98000, + department: "Design", + status: "active", + }, + { + id: 3, + name: "Clara Chen", + email: "clara@example.com", + role: "PM", + salary: 115000, + department: "Product", + status: "inactive", + }, + { + id: 4, + name: "David Kim", + email: "david@example.com", + role: "Engineer", + salary: 132000, + department: "Engineering", + status: "active", + }, + { + id: 5, + name: "Elena Rossi", + email: "elena@example.com", + role: "Analyst", + salary: 89000, + department: "Analytics", + status: "active", + }, + { + id: 6, + name: "Frank Müller", + email: "frank@example.com", + role: "Engineer", + salary: 118000, + department: "Engineering", + status: "inactive", + }, + { + id: 7, + name: "Grace Park", + email: "grace@example.com", + role: "Designer", + salary: 105000, + department: "Design", + status: "active", + }, + { + id: 8, + name: "Henry Patel", + email: "henry@example.com", + role: "Lead", + salary: 145000, + department: "Engineering", + status: "active", + }, ]; -export const columnEditorCustomRendererHeaders: SolidColumnDef[] = [ +export const columnEditorCustomRendererHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: 170, type: "string", sortable: true }, { accessor: "email", label: "Email", width: 200, type: "string" }, @@ -36,7 +109,7 @@ export const columnEditorCustomRendererConfig = { tableProps: { enableColumnEditor: true, }, -} as const; +}; export const COLUMN_EDITOR_TEXT = "Manage Columns"; export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…"; diff --git a/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx b/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx index b6a36f1c7..b3d50bede 100644 --- a/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx +++ b/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx @@ -9,6 +9,7 @@ export default function ColumnFilteringDemo(props: { return ( row.id} rows={columnFilteringConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-filtering/column-filtering.demo-data.ts b/packages/examples/solid/src/demos/column-filtering/column-filtering.demo-data.ts index fd6f2ece1..225625b19 100644 --- a/packages/examples/solid/src/demos/column-filtering/column-filtering.demo-data.ts +++ b/packages/examples/solid/src/demos/column-filtering/column-filtering.demo-data.ts @@ -1,8 +1,17 @@ // Self-contained demo table setup for this example. -import type { Row, SolidColumnDef } from "@simple-table/solid"; +import type { SolidColumnDef, CellRendererProps } from "@simple-table/solid"; +export interface ColumnFilteringEmployee { + id: number; + name: string; + department: string; + role: string; + salary: number; + startDate: string; + isActive: boolean; +} -export const COLUMN_FILTERING_DATA: Row[] = [ +export const COLUMN_FILTERING_DATA: ColumnFilteringEmployee[] = [ { id: 1, name: "Bianca Rossi", @@ -147,7 +156,7 @@ export const DEPARTMENT_OPTIONS = [ { label: "Quality Assurance", value: "Quality Assurance" }, ]; -export const columnFilteringHeaders: SolidColumnDef[] = [ +export const columnFilteringHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", @@ -191,7 +200,7 @@ export const columnFilteringHeaders: SolidColumnDef[] = [ type: "number", sortable: true, filterable: true, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const salary = row.salary as number; return `$${salary.toLocaleString()}`; }, @@ -218,4 +227,4 @@ export const columnFilteringHeaders: SolidColumnDef[] = [ export const columnFilteringConfig = { headers: columnFilteringHeaders, rows: COLUMN_FILTERING_DATA, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-pinning/ColumnPinningDemo.tsx b/packages/examples/solid/src/demos/column-pinning/ColumnPinningDemo.tsx index 2166cd4ef..5469be174 100644 --- a/packages/examples/solid/src/demos/column-pinning/ColumnPinningDemo.tsx +++ b/packages/examples/solid/src/demos/column-pinning/ColumnPinningDemo.tsx @@ -6,6 +6,7 @@ export default function ColumnPinningDemo(props: { height?: string | number; the return ( row.id} rows={columnPinningConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-pinning/column-pinning.demo-data.ts b/packages/examples/solid/src/demos/column-pinning/column-pinning.demo-data.ts index cef3e203c..da2938170 100644 --- a/packages/examples/solid/src/demos/column-pinning/column-pinning.demo-data.ts +++ b/packages/examples/solid/src/demos/column-pinning/column-pinning.demo-data.ts @@ -1,8 +1,21 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface ColumnPinningEmployee { + id: number; + name: string; + email: string; + role: string; + department: string; + location: string; + joinDate: string; + salary: number; + manager: string; + status: string; + projects: number; +} -export const columnPinningHeaders: SolidColumnDef[] = [ +export const columnPinningHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", width: 132, pinned: "left", type: "string" }, { accessor: "email", label: "Email", width: 220, type: "string" }, { accessor: "role", label: "Role", width: 150, type: "string" }, @@ -15,7 +28,7 @@ export const columnPinningHeaders: SolidColumnDef[] = [ { accessor: "projects", label: "Projects", width: 100, align: "right", pinned: "right", type: "number" }, ]; -export const columnPinningData = [ +export const columnPinningData: ColumnPinningEmployee[] = [ { id: 1, name: "Zara Nakamura", email: "zara.n@pixelstudio.game", role: "Lead Game Designer", department: "Game Design", location: "Tokyo", joinDate: "2019-03-12", salary: 145000, manager: "Hiroshi Tanaka", status: "Active", projects: 7 }, { id: 2, name: "Phoenix Rodriguez", email: "phoenix.r@pixelstudio.game", role: "3D Artist", department: "Art & Animation", location: "Montreal", joinDate: "2020-11-08", salary: 98000, manager: "Elena Volkov", status: "Active", projects: 4 }, { id: 3, name: "Kai Thompson", email: "kai.t@pixelstudio.game", role: "Senior Programmer", department: "Engineering", location: "San Francisco", joinDate: "2018-05-15", salary: 135000, manager: "Nova Singh", status: "Active", projects: 6 }, @@ -34,4 +47,4 @@ export const columnPinningConfig = { headers: columnPinningHeaders, rows: columnPinningData, tableProps: { columnResizing: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-reordering/ColumnReorderingDemo.tsx b/packages/examples/solid/src/demos/column-reordering/ColumnReorderingDemo.tsx index ffaafd7b3..80e71643f 100644 --- a/packages/examples/solid/src/demos/column-reordering/ColumnReorderingDemo.tsx +++ b/packages/examples/solid/src/demos/column-reordering/ColumnReorderingDemo.tsx @@ -1,20 +1,25 @@ import { createSignal } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { Theme } from "@simple-table/solid"; -import { columnReorderingConfig } from "./column-reordering.demo-data"; +import type { Theme, SolidColumnDef } from "@simple-table/solid"; +import { columnReorderingConfig, type CrewMember } from "./column-reordering.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnReorderingDemo(props: { height?: string | number; theme?: Theme }) { const [headers, setHeaders] = createSignal([...columnReorderingConfig.headers]); + const handleColumnOrderChange = (newHeaders: SolidColumnDef[]) => { + setHeaders(newHeaders); + }; + return ( row.id} rows={columnReorderingConfig.rows} height={props.height ?? "400px"} theme={props.theme} - onColumnOrderChange={setHeaders} + onColumnOrderChange={handleColumnOrderChange} /> ); } diff --git a/packages/examples/solid/src/demos/column-reordering/column-reordering.demo-data.ts b/packages/examples/solid/src/demos/column-reordering/column-reordering.demo-data.ts index a4d47df8c..716b29ebf 100644 --- a/packages/examples/solid/src/demos/column-reordering/column-reordering.demo-data.ts +++ b/packages/examples/solid/src/demos/column-reordering/column-reordering.demo-data.ts @@ -1,8 +1,15 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface CrewMember { + id: number; + name: string; + age: number; + role: string; + department: string; +} -export const columnReorderingHeaders: SolidColumnDef[] = [ +export const columnReorderingHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: "1fr", type: "string" }, { accessor: "age", label: "Age", width: 80, align: "right", type: "number" }, @@ -29,4 +36,4 @@ export const columnReorderingConfig = { headers: columnReorderingHeaders, rows: columnReorderingData, tableProps: { columnReordering: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-resizing/ColumnResizingDemo.tsx b/packages/examples/solid/src/demos/column-resizing/ColumnResizingDemo.tsx index 51bea84de..d7a542941 100644 --- a/packages/examples/solid/src/demos/column-resizing/ColumnResizingDemo.tsx +++ b/packages/examples/solid/src/demos/column-resizing/ColumnResizingDemo.tsx @@ -1,7 +1,12 @@ import { createSignal, onMount, onCleanup } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; import type { Theme, SolidColumnDef } from "@simple-table/solid"; -import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "./column-resizing.demo-data"; +import { + columnResizingHeaders, + columnResizingData, + COLUMN_RESIZING_STORAGE_KEY, + type OceanStaff, +} from "./column-resizing.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnResizingDemo(props: { height?: string | number; theme?: Theme }) { @@ -35,7 +40,7 @@ export default function ColumnResizingDemo(props: { height?: string | number; th clearMessageTimer(); }); - const handleColumnWidthChange = (updatedHeaders: SolidColumnDef[]) => { + const handleColumnWidthChange = (updatedHeaders: SolidColumnDef[]) => { try { const widthMap: Record = {}; for (const h of updatedHeaders) widthMap[h.accessor] = h.width; @@ -75,6 +80,7 @@ export default function ColumnResizingDemo(props: { height?: string | number; th row.id} rows={columnResizingData} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-resizing/column-resizing.demo-data.ts b/packages/examples/solid/src/demos/column-resizing/column-resizing.demo-data.ts index 6983ee0fd..e6dc1f7ad 100644 --- a/packages/examples/solid/src/demos/column-resizing/column-resizing.demo-data.ts +++ b/packages/examples/solid/src/demos/column-resizing/column-resizing.demo-data.ts @@ -1,10 +1,18 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface OceanStaff { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; +} export const COLUMN_RESIZING_STORAGE_KEY = "columnResizingDemo_widths"; -export const columnResizingHeaders: SolidColumnDef[] = [ +export const columnResizingHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "First Name", width: "1fr", minWidth: 100, type: "string" }, { accessor: "age", label: "Age", width: "1fr", minWidth: 50, type: "string" }, @@ -31,4 +39,4 @@ export const columnResizingData = [ export const columnResizingConfig = { headers: columnResizingHeaders, rows: columnResizingData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-selection/ColumnSelectionDemo.tsx b/packages/examples/solid/src/demos/column-selection/ColumnSelectionDemo.tsx index efb580cc5..052c76acf 100644 --- a/packages/examples/solid/src/demos/column-selection/ColumnSelectionDemo.tsx +++ b/packages/examples/solid/src/demos/column-selection/ColumnSelectionDemo.tsx @@ -6,6 +6,7 @@ export default function ColumnSelectionDemo(props: { height?: string | number; t return ( row.id} rows={columnSelectionConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-selection/column-selection.demo-data.ts b/packages/examples/solid/src/demos/column-selection/column-selection.demo-data.ts index 72c783876..05ebfe61d 100644 --- a/packages/examples/solid/src/demos/column-selection/column-selection.demo-data.ts +++ b/packages/examples/solid/src/demos/column-selection/column-selection.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface TeamMember { + id: number; + name: string; + age: number; + role: string; + department: string; + email: string; +} -export const columnSelectionHeaders: SolidColumnDef[] = [ +export const columnSelectionHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, sortable: true, type: "number" }, { accessor: "name", label: "Name", minWidth: 120, width: "1fr", sortable: true, type: "string" }, { accessor: "age", label: "Age", width: 100, sortable: true, type: "number" }, @@ -30,4 +38,4 @@ export const columnSelectionConfig = { headers: columnSelectionHeaders, rows: columnSelectionData, tableProps: { selectableColumns: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-sorting/ColumnSortingDemo.tsx b/packages/examples/solid/src/demos/column-sorting/ColumnSortingDemo.tsx index a5ab81ff6..b5754a6e2 100644 --- a/packages/examples/solid/src/demos/column-sorting/ColumnSortingDemo.tsx +++ b/packages/examples/solid/src/demos/column-sorting/ColumnSortingDemo.tsx @@ -9,6 +9,7 @@ export default function ColumnSortingDemo(props: { return ( row.id} rows={columnSortingConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-sorting/column-sorting.demo-data.ts b/packages/examples/solid/src/demos/column-sorting/column-sorting.demo-data.ts index d37125974..ef5af33d8 100644 --- a/packages/examples/solid/src/demos/column-sorting/column-sorting.demo-data.ts +++ b/packages/examples/solid/src/demos/column-sorting/column-sorting.demo-data.ts @@ -1,9 +1,16 @@ // Self-contained demo table setup for this example. -import type { Row } from "@simple-table/solid"; import type { SolidColumnDef } from "@simple-table/solid"; +export interface FacultyMember { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; +} -export const COLUMN_SORTING_DATA: Row[] = [ +export const COLUMN_SORTING_DATA: FacultyMember[] = [ { id: 1, name: "Dr. Elena Vasquez", @@ -103,7 +110,7 @@ export const COLUMN_SORTING_DATA: Row[] = [ ]; -export const columnSortingHeaders: SolidColumnDef[] = [ +export const columnSortingHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, sortable: true, type: "number" }, { accessor: "name", label: "Name", width: 180, sortable: true, type: "string" }, { accessor: "age", label: "Age", width: 80, sortable: true, type: "number" }, @@ -141,3 +148,4 @@ export const columnSortingConfig = { headers: columnSortingHeaders, rows: COLUMN_SORTING_DATA, }; + diff --git a/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx b/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx index 5103e2b12..e42d77033 100644 --- a/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx +++ b/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx @@ -22,6 +22,7 @@ export default function ColumnVisibilityDemo(props: { height?: string | number; return ( row.id} rows={columnVisibilityConfig.rows} enableColumnEditor enableColumnEditorInitOpen diff --git a/packages/examples/solid/src/demos/column-visibility/column-visibility.demo-data.ts b/packages/examples/solid/src/demos/column-visibility/column-visibility.demo-data.ts index 6c422fab7..74b7cf7e4 100644 --- a/packages/examples/solid/src/demos/column-visibility/column-visibility.demo-data.ts +++ b/packages/examples/solid/src/demos/column-visibility/column-visibility.demo-data.ts @@ -1,5 +1,17 @@ // Self-contained demo table setup for this example (aligned with simple-table-marketing column visibility demo). -import type { ColumnVisibilityState, SolidColumnDef, Row } from "@simple-table/solid"; +import type { ColumnVisibilityState, SolidColumnDef } from "@simple-table/solid"; + +export interface VisibilityEmployee { + id: number; + firstName: string; + lastName: string; + email: string; + phone: string; + role: string; + department: string; + location: string; + startDate: string; +} export const COLUMN_VISIBILITY_DEMO_STORAGE_KEY = "columnVisibilityDemo"; @@ -22,98 +34,18 @@ export function saveColumnVisibilityDemoState(state: ColumnVisibilityState): voi } } -export const columnVisibilityData: Row[] = [ - { - id: 1, - firstName: "Alice", - lastName: "Johnson", - email: "alice@example.com", - phone: "555-0101", - role: "Engineer", - department: "Engineering", - location: "NYC", - startDate: "2021-03-15", - }, - { - id: 2, - firstName: "Bob", - lastName: "Martinez", - email: "bob@example.com", - phone: "555-0102", - role: "Designer", - department: "Design", - location: "LA", - startDate: "2022-07-22", - }, - { - id: 3, - firstName: "Clara", - lastName: "Chen", - email: "clara@example.com", - phone: "555-0103", - role: "PM", - department: "Product", - location: "SF", - startDate: "2020-01-10", - }, - { - id: 4, - firstName: "David", - lastName: "Kim", - email: "david@example.com", - phone: "555-0104", - role: "Engineer", - department: "Engineering", - location: "CHI", - startDate: "2019-11-05", - }, - { - id: 5, - firstName: "Elena", - lastName: "Rossi", - email: "elena@example.com", - phone: "555-0105", - role: "Analyst", - department: "Analytics", - location: "BOS", - startDate: "2023-02-14", - }, - { - id: 6, - firstName: "Frank", - lastName: "Müller", - email: "frank@example.com", - phone: "555-0106", - role: "Engineer", - department: "Engineering", - location: "SEA", - startDate: "2021-09-30", - }, - { - id: 7, - firstName: "Grace", - lastName: "Park", - email: "grace@example.com", - phone: "555-0107", - role: "Designer", - department: "Design", - location: "AUS", - startDate: "2022-04-18", - }, - { - id: 8, - firstName: "Henry", - lastName: "Patel", - email: "henry@example.com", - phone: "555-0108", - role: "Lead", - department: "Engineering", - location: "DEN", - startDate: "2018-05-20", - }, +export const columnVisibilityData: VisibilityEmployee[] = [ + { id: 1, firstName: "Alice", lastName: "Johnson", email: "alice@example.com", phone: "555-0101", role: "Engineer", department: "Engineering", location: "NYC", startDate: "2021-03-15" }, + { id: 2, firstName: "Bob", lastName: "Martinez", email: "bob@example.com", phone: "555-0102", role: "Designer", department: "Design", location: "LA", startDate: "2022-07-22" }, + { id: 3, firstName: "Clara", lastName: "Chen", email: "clara@example.com", phone: "555-0103", role: "PM", department: "Product", location: "SF", startDate: "2020-01-10" }, + { id: 4, firstName: "David", lastName: "Kim", email: "david@example.com", phone: "555-0104", role: "Engineer", department: "Engineering", location: "CHI", startDate: "2019-11-05" }, + { id: 5, firstName: "Elena", lastName: "Rossi", email: "elena@example.com", phone: "555-0105", role: "Analyst", department: "Analytics", location: "BOS", startDate: "2023-02-14" }, + { id: 6, firstName: "Frank", lastName: "Müller", email: "frank@example.com", phone: "555-0106", role: "Engineer", department: "Engineering", location: "SEA", startDate: "2021-09-30" }, + { id: 7, firstName: "Grace", lastName: "Park", email: "grace@example.com", phone: "555-0107", role: "Designer", department: "Design", location: "AUS", startDate: "2022-04-18" }, + { id: 8, firstName: "Henry", lastName: "Patel", email: "henry@example.com", phone: "555-0108", role: "Lead", department: "Engineering", location: "DEN", startDate: "2018-05-20" }, ]; -export const columnVisibilityHeaders: SolidColumnDef[] = [ +export const columnVisibilityHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "firstName", label: "First Name", width: 120, type: "string" }, { accessor: "lastName", label: "Last Name", width: 120, type: "string" }, @@ -127,18 +59,14 @@ export const columnVisibilityHeaders: SolidColumnDef[] = [ label: "Start Date", width: 130, type: "date", - valueFormatter: ({ value }) => - new Date(value as string).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }), + valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), }, ]; +/** Applies saved visibility + marketing-style defaults (email hidden when unset). */ export function getColumnVisibilityDemoHeaders( savedVisibility: ColumnVisibilityState = loadColumnVisibilityDemoSaved(), -): SolidColumnDef[] { +): SolidColumnDef[] { return columnVisibilityHeaders.map((header) => ({ ...header, hide: @@ -153,11 +81,11 @@ export const columnVisibilityConfig = { rows: columnVisibilityData, tableProps: { enableColumnEditor: true, - enableColumnEditorInitOpen: true as const, + enableColumnEditorInitOpen: true, columnEditorConfig: { text: "Manage Columns", searchEnabled: true, searchPlaceholder: "Search columns…", }, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/column-width/ColumnWidthDemo.tsx b/packages/examples/solid/src/demos/column-width/ColumnWidthDemo.tsx index db934636c..274401c9b 100644 --- a/packages/examples/solid/src/demos/column-width/ColumnWidthDemo.tsx +++ b/packages/examples/solid/src/demos/column-width/ColumnWidthDemo.tsx @@ -22,6 +22,7 @@ export default function ColumnWidthDemo(props: { height?: string | number; theme autoExpandColumns={!isMobile()} columnResizing columns={columnWidthConfig.headers} + getRowId={({ row }) => row.id} height={props.height ?? "400px"} rows={columnWidthConfig.rows} theme={props.theme} diff --git a/packages/examples/solid/src/demos/column-width/column-width.demo-data.ts b/packages/examples/solid/src/demos/column-width/column-width.demo-data.ts index 524b70e7b..01efa9de6 100644 --- a/packages/examples/solid/src/demos/column-width/column-width.demo-data.ts +++ b/packages/examples/solid/src/demos/column-width/column-width.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface StartupEmployee { + id: number; + name: string; + email: string; + age: number; + department: string; + salary: string; +} -export const columnWidthHeaders: SolidColumnDef[] = [ +export const columnWidthHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: "1fr", minWidth: 120, type: "string" }, { accessor: "email", label: "Email", width: "1fr", minWidth: 180, type: "string" }, @@ -31,4 +39,4 @@ export const columnWidthConfig = { headers: columnWidthHeaders, rows: columnWidthData, tableProps: { columnResizing: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/crm/CRMDemo.tsx b/packages/examples/solid/src/demos/crm/CRMDemo.tsx index 22af380e2..e5d0aca5f 100644 --- a/packages/examples/solid/src/demos/crm/CRMDemo.tsx +++ b/packages/examples/solid/src/demos/crm/CRMDemo.tsx @@ -1,6 +1,6 @@ import { createSignal, For } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { SolidColumnDef, FooterRendererProps, CellChangeProps } from "@simple-table/solid"; +import type { SolidColumnDef, FooterRendererProps, CellChangeProps, CellRendererProps } from "@simple-table/solid"; import { crmData, CRM_THEME_COLORS_LIGHT, @@ -60,13 +60,13 @@ const FitButtons = (props: { colors: typeof CRM_THEME_COLORS_LIGHT }) => { ); }; -function getCRMHeaders(isDark: boolean): SolidColumnDef[] { +function getCRMHeaders(isDark: boolean): SolidColumnDef[] { const colors = isDark ? CRM_THEME_COLORS_DARK : CRM_THEME_COLORS_LIGHT; return [ { accessor: "name", label: "CONTACT", width: "2fr", minWidth: 290, sortable: true, editable: true, type: "string", - cellRenderer: ({ row }) => { - const d = row as unknown as CRMLead; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const initials = d.name.split(" ").map((n) => n[0]).join("").toUpperCase(); return (
@@ -82,16 +82,16 @@ function getCRMHeaders(isDark: boolean): SolidColumnDef[] { }, { accessor: "signal", label: "SIGNAL", width: "3fr", minWidth: 340, sortable: true, editable: true, type: "string", - cellRenderer: ({ row }) => ( + cellRenderer: ({ row }: CellRendererProps) => (
-
Keyword: {(row as unknown as CRMLead).signal}
+
Keyword: {row.signal}
), }, { accessor: "aiScore", label: "AI SCORE", width: "1fr", minWidth: 100, sortable: true, align: "center", type: "number", - cellRenderer: ({ row }) =>
{"🔥".repeat((row as unknown as CRMLead).aiScore)}
, + cellRenderer: ({ row }: CellRendererProps) =>
{"🔥".repeat(row.aiScore)}
, }, { accessor: "emailStatus", label: "EMAIL", width: "1.5fr", minWidth: 210, sortable: true, align: "center", type: "enum", @@ -100,13 +100,13 @@ function getCRMHeaders(isDark: boolean): SolidColumnDef[] { }, { accessor: "timeAgo", label: "IMPORT", width: "1fr", minWidth: 100, sortable: true, align: "center", type: "string", - cellRenderer: ({ row }) =>
{(row as unknown as CRMLead).timeAgo}
, + cellRenderer: ({ row }: CellRendererProps) =>
{row.timeAgo}
, }, { accessor: "list", label: "LIST", width: "1.2fr", minWidth: 160, sortable: true, align: "center", type: "enum", enumOptions: [{ label: "Leads", value: "Leads" }, { label: "Hot Leads", value: "Hot Leads" }, { label: "Warm Leads", value: "Warm Leads" }, { label: "Cold Leads", value: "Cold Leads" }, { label: "Enterprise", value: "Enterprise" }, { label: "SMB", value: "SMB" }, { label: "Nurture", value: "Nurture" }], valueGetter: ({ row }) => { const list = String(row.list); const m: Record = { "Hot Leads": 1, "Warm Leads": 2, Enterprise: 3, Leads: 4, SMB: 5, "Cold Leads": 6, Nurture: 7 }; return m[list] || 999; }, - cellRenderer: ({ row }) => e.preventDefault()} style={{ cursor: "pointer", "font-size": "0.875rem", color: colors.link, "text-decoration": "none", "font-weight": "600" }}>{(row as unknown as CRMLead).list}, + cellRenderer: ({ row }: CellRendererProps) => e.preventDefault()} style={{ cursor: "pointer", "font-size": "0.875rem", color: colors.link, "text-decoration": "none", "font-weight": "600" }}>{row.list}, }, { accessor: "_fit", label: "Fit", width: "1fr", align: "center", minWidth: 120, cellRenderer: () => }, { accessor: "_contactNow", label: "", width: "1.2fr", minWidth: 160, cellRenderer: () => e.preventDefault()} style={{ cursor: "pointer", "font-size": "0.875rem", color: colors.link, "text-decoration": "none", "font-weight": "600" }}>Contact Now }, @@ -119,7 +119,7 @@ export default function CRMDemo(props: { height?: string | number; theme?: CrmSh const [rowsPerPage, setRowsPerPage] = createSignal(100); const footerColors = () => isDark() ? CRM_FOOTER_COLORS_DARK : CRM_FOOTER_COLORS_LIGHT; - const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { + const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { setData((prev) => prev.map((item) => item.id === row.id ? { ...item, [accessor]: newValue } : item)); }; @@ -130,6 +130,7 @@ export default function CRMDemo(props: { height?: string | number; theme?: CrmSh columnResizing columns={getCRMHeaders(isDark())} enableRowSelection + getRowId={({ row }) => row.id} customTheme={{ headerHeight: 48, rowHeight: 92 }} height={props.height ?? "400px"} onCellEdit={handleCellEdit} diff --git a/packages/examples/solid/src/demos/crm/crm.demo-data.ts b/packages/examples/solid/src/demos/crm/crm.demo-data.ts index 986ee4ad0..2c8f2f5dc 100644 --- a/packages/examples/solid/src/demos/crm/crm.demo-data.ts +++ b/packages/examples/solid/src/demos/crm/crm.demo-data.ts @@ -42,7 +42,7 @@ export function generateCRMData(count: number = 100): CRMLead[] { export const crmData = generateCRMData(100); -export const crmHeaders: SolidColumnDef[] = [ +export const crmHeaders: SolidColumnDef[] = [ { accessor: "name", label: "CONTACT", @@ -174,4 +174,4 @@ export const CRM_THEME_COLORS_DARK = { export const crmConfig = { headers: crmHeaders, rows: crmData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/crypto/CryptoDemo.tsx b/packages/examples/solid/src/demos/crypto/CryptoDemo.tsx index 2a6d76011..259d16763 100644 --- a/packages/examples/solid/src/demos/crypto/CryptoDemo.tsx +++ b/packages/examples/solid/src/demos/crypto/CryptoDemo.tsx @@ -1,7 +1,7 @@ import { onMount, onCleanup } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { Theme, TableAPI, Row, CellValue } from "@simple-table/solid"; -import { cryptoConfig } from "./crypto.demo-data"; +import type { Theme, TableAPI, CellValue } from "@simple-table/solid"; +import { cryptoConfig, type CryptoCoin } from "./crypto.demo-data"; import "@simple-table/solid/styles.css"; const TICK_MS = 90; @@ -18,32 +18,32 @@ function pickRandomSubset(arr: T[], n: number): T[] { return copy.slice(0, Math.min(n, copy.length)); } -function applyRowPatch(api: TableAPI, rowId: string | number, patch: Partial) { - for (const accessor of Object.keys(patch)) { +function applyRowPatch(api: TableAPI, rowId: string, patch: Partial) { + for (const accessor of Object.keys(patch) as (keyof CryptoCoin)[]) { const newValue = patch[accessor]; if (newValue === undefined) continue; api.updateData({ accessor, rowId, newValue: newValue as CellValue }); } } -function runTick(getApi: () => TableAPI | null | undefined) { +function runTick(getApi: () => TableAPI | null | undefined) { const api = getApi(); if (!api) return; const visible = api.getVisibleRows(); if (!visible.length) return; for (const vr of pickRandomSubset(visible, ROWS_PER_TICK)) { - const rowId = vr.row.id as string | number | undefined; - if (rowId === undefined || rowId === null || rowId === "") continue; - const currentPrice = vr.row.price as number; + const rowId = vr.row.id; + if (typeof rowId !== "string" || rowId === "") continue; + const currentPrice = vr.row.price; if (typeof currentPrice !== "number") continue; const drift = (Math.random() - 0.5) * 0.012; const newPrice = Math.max(currentPrice * (1 + drift), currentPrice * 0.0001); const round = newPrice >= 1 ? 1e2 : 1e6; const newPriceRounded = Math.round(newPrice * round) / round; - const currentChange = (vr.row.change24h as number) ?? 0; + const currentChange = typeof vr.row.change24h === "number" ? vr.row.change24h : 0; const newChange = Math.round((currentChange + drift * 100) * 100) / 100; - const history = vr.row.priceHistory as number[]; - const patch: Partial = { price: newPriceRounded, change24h: newChange }; + const history = vr.row.priceHistory; + const patch: Partial = { price: newPriceRounded, change24h: newChange }; if (Array.isArray(history) && history.length > 0) { patch.priceHistory = [...history.slice(1), newPriceRounded]; } @@ -52,7 +52,7 @@ function runTick(getApi: () => TableAPI | null | undefined) { } export default function CryptoDemo(props: { height?: string | number; theme?: Theme }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; let cleanupFn: (() => void) | undefined; onMount(() => { @@ -66,7 +66,7 @@ export default function CryptoDemo(props: { height?: string | number; theme?: Th ref={(api) => (tableRef = api)} columns={cryptoConfig.headers} rows={cryptoConfig.rows} - getRowId={({ row }) => (row as { id: string | number }).id} + getRowId={({ row }) => row.id} height={props.height ?? "70dvh"} theme={props.theme} columnReordering diff --git a/packages/examples/solid/src/demos/crypto/crypto.demo-data.ts b/packages/examples/solid/src/demos/crypto/crypto.demo-data.ts index c18d98584..280c15aea 100644 --- a/packages/examples/solid/src/demos/crypto/crypto.demo-data.ts +++ b/packages/examples/solid/src/demos/crypto/crypto.demo-data.ts @@ -1,8 +1,7 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; -import type { Row } from "@simple-table/solid"; -export interface CryptoCoin extends Row { +export interface CryptoCoin { id: string; rank: number; name: string; @@ -177,7 +176,7 @@ export function generateCryptoData(count = 200): CryptoCoin[] { const pct = ({ value }: { value: unknown }) => typeof value === "number" ? formatSignedPercent(value) : ""; -export const cryptoHeaders: SolidColumnDef[] = [ +export const cryptoHeaders: SolidColumnDef[] = [ { accessor: "rank", label: "#", width: 56, align: "center", type: "number", pinned: "left", sortable: true, editable: false }, { accessor: "name", label: "Asset", width: 200, align: "left", type: "string", pinned: "left", sortable: true, editable: false, @@ -229,4 +228,4 @@ export const cryptoData = generateCryptoData(200); export const cryptoConfig = { headers: cryptoHeaders, rows: cryptoData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/csv-export/CsvExportDemo.tsx b/packages/examples/solid/src/demos/csv-export/CsvExportDemo.tsx index 8b76c876f..cc5c9c904 100644 --- a/packages/examples/solid/src/demos/csv-export/CsvExportDemo.tsx +++ b/packages/examples/solid/src/demos/csv-export/CsvExportDemo.tsx @@ -1,11 +1,12 @@ -import {SimpleTable} from "@simple-table/solid";import type { Theme, TableAPI, SolidColumnDef } from "@simple-table/solid"; -import { csvExportHeaders, csvExportData, csvExportConfig } from "./csv-export.demo-data"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, TableAPI, SolidColumnDef } from "@simple-table/solid"; +import { csvExportHeaders, csvExportData, csvExportConfig, type CsvProduct } from "./csv-export.demo-data"; import "@simple-table/solid/styles.css"; export default function CsvExportDemo(props: { height?: string | number; theme?: Theme }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; - const headers: SolidColumnDef[] = csvExportHeaders.map((h) => { + const headers: SolidColumnDef[] = csvExportHeaders.map((h) => { if (h.accessor === "actions") { return { ...h, @@ -39,10 +40,10 @@ export default function CsvExportDemo(props: { height?: string | number; theme?: if (!tableRef) return; const rows = tableRef.getAllRows(); const hdrs = tableRef.getHeaders(); - const totalRevenue = rows.reduce( - (sum, r) => sum + (Number((r.row as { revenue?: unknown }).revenue) || 0), - 0, - ); + const totalRevenue = rows.reduce((sum, r) => { + const revenue = r.row.revenue; + return sum + (typeof revenue === "number" ? revenue : Number(revenue) || 0); + }, 0); alert( `Table Info:\n• ${rows.length} rows\n• ${hdrs.length} columns\n• Columns: ${hdrs.map((h) => h.label).join(", ")}\n• Total Revenue: $${totalRevenue.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, ); @@ -61,6 +62,7 @@ export default function CsvExportDemo(props: { height?: string | number; theme?: (tableRef = api)} columns={headers} + getRowId={({ row }) => row.id} rows={csvExportData} enableColumnEditor={csvExportConfig.tableProps.enableColumnEditor} selectableCells={csvExportConfig.tableProps.selectableCells} diff --git a/packages/examples/solid/src/demos/csv-export/csv-export.demo-data.ts b/packages/examples/solid/src/demos/csv-export/csv-export.demo-data.ts index 3825ffa6d..c0e079270 100644 --- a/packages/examples/solid/src/demos/csv-export/csv-export.demo-data.ts +++ b/packages/examples/solid/src/demos/csv-export/csv-export.demo-data.ts @@ -1,6 +1,17 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface CsvProduct { + id: string; + sku: string; + product: string; + category: string; + price: number; + stock: number; + sold: number; + revenue: number; + actions: string; +} const CATEGORY_CODES: Record = { electronics: "ELEC", @@ -24,7 +35,7 @@ export const csvExportData = [ { id: "db-4003", sku: "PRD-4003", product: "Desk Lamp LED", category: "Appliances", price: 44.99, stock: 201, sold: 198, revenue: 8908.02, actions: "" }, ]; -export const csvExportHeaders: SolidColumnDef[] = [ +export const csvExportHeaders: SolidColumnDef[] = [ { accessor: "id", label: "Internal ID", width: 80, type: "string", excludeFromRender: true }, { accessor: "sku", label: "SKU", width: 100, sortable: true, type: "string" }, { accessor: "product", label: "Product Name", minWidth: 120, width: "1fr", sortable: true, type: "string" }, @@ -73,4 +84,4 @@ export const csvExportConfig = { headers: csvExportHeaders, rows: csvExportData, tableProps: { enableColumnEditor: true, selectableCells: true, customTheme: { rowHeight: 32 } }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/custom-icons/CustomIconsDemo.tsx b/packages/examples/solid/src/demos/custom-icons/CustomIconsDemo.tsx index b06044cff..21b9cf64b 100644 --- a/packages/examples/solid/src/demos/custom-icons/CustomIconsDemo.tsx +++ b/packages/examples/solid/src/demos/custom-icons/CustomIconsDemo.tsx @@ -1,8 +1,8 @@ -import {SimpleTable} from "@simple-table/solid";import type { Theme } from "@simple-table/solid"; +import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidIconsConfig } from "@simple-table/solid"; import { customIconsConfig } from "./custom-icons.demo-data"; import "@simple-table/solid/styles.css"; -const customIcons = { +const customIcons: SolidIconsConfig = { sortUp: ( @@ -39,6 +39,7 @@ export default function CustomIconsDemo(props: { height?: string | number; theme return ( row.id} rows={customIconsConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/custom-icons/custom-icons.demo-data.ts b/packages/examples/solid/src/demos/custom-icons/custom-icons.demo-data.ts index 96f975824..a007b4220 100644 --- a/packages/examples/solid/src/demos/custom-icons/custom-icons.demo-data.ts +++ b/packages/examples/solid/src/demos/custom-icons/custom-icons.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface SoftwareRelease { + id: number; + name: string; + version: string; + status: string; + downloads: number; + date: string; +} -export const customIconsData: Row[] = [ +export const customIconsData: SoftwareRelease[] = [ { id: 1, name: "Alpha Release", version: "1.0.0", status: "released", downloads: 15420, date: "2024-01-15" }, { id: 2, name: "Beta Release", version: "1.1.0", status: "released", downloads: 28300, date: "2024-03-22" }, { id: 3, name: "Hotfix", version: "1.1.1", status: "released", downloads: 31050, date: "2024-04-05" }, @@ -13,7 +21,7 @@ export const customIconsData: Row[] = [ { id: 8, name: "Next Release", version: "2.2.0", status: "planned", downloads: 0, date: "2025-01-20" }, ]; -export const customIconsHeaders: SolidColumnDef[] = [ +export const customIconsHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number", sortable: true }, { accessor: "name", label: "Release", width: 170, type: "string", sortable: true }, { accessor: "version", label: "Version", width: 100, type: "string", sortable: true }, @@ -39,4 +47,4 @@ export const customIconsHeaders: SolidColumnDef[] = [ export const customIconsConfig = { headers: customIconsHeaders, rows: customIconsData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx b/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx index f826cc835..e2391773c 100644 --- a/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx +++ b/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx @@ -7,6 +7,7 @@ export default function CustomThemeDemo(props: { height?: string | number; theme return ( row.id} rows={customThemeConfig.rows} height={props.height ?? "400px"} theme={props.theme ?? "custom"} diff --git a/packages/examples/solid/src/demos/custom-theme/custom-theme.demo-data.ts b/packages/examples/solid/src/demos/custom-theme/custom-theme.demo-data.ts index 9cef9c479..cb1bfc5a5 100644 --- a/packages/examples/solid/src/demos/custom-theme/custom-theme.demo-data.ts +++ b/packages/examples/solid/src/demos/custom-theme/custom-theme.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface ThemeContact { + id: number; + name: string; + phone: string; + email: string; + city: string; + status: string; +} -export const customThemeData: Row[] = [ +export const customThemeData: ThemeContact[] = [ { id: 1, name: "Alice Johnson", phone: "2125551234", email: "alice@corp.com", city: "New York", status: "active" }, { id: 2, name: "Bob Martinez", phone: "3105559876", email: "bob@corp.com", city: "Los Angeles", status: "active" }, { id: 3, name: "Clara Chen", phone: "4155553210", email: "clara@corp.com", city: "San Francisco", status: "inactive" }, @@ -20,7 +28,7 @@ function formatPhone(raw: string): string { return raw; } -export const customThemeHeaders: SolidColumnDef[] = [ +export const customThemeHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: 170, type: "string", sortable: true }, { @@ -39,10 +47,9 @@ export const customThemeConfig = { headers: customThemeHeaders, rows: customThemeData, tableProps: { - theme: "custom" as const, customTheme: { rowHeight: 40, headerHeight: 44, }, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx b/packages/examples/solid/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx index dde53400c..6a43eb7fc 100644 --- a/packages/examples/solid/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx +++ b/packages/examples/solid/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx @@ -1,5 +1,6 @@ import { createSignal } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid"; import { dynamicNestedTablesConfig, dynamicNestedTablesData, @@ -19,14 +20,13 @@ export default function DynamicNestedTablesDemo(props: { height?: string | numbe setLoading, setError, setEmpty, - }: OnRowGroupExpandProps) => { + }: OnRowGroupExpandProps) => { if (!isExpanded) return; try { if (groupingKey === "divisions") { - const company = row as DynamicCompany; - if (company.divisions && company.divisions.length > 0) return; + if (row.divisions && row.divisions.length > 0) return; setLoading(true); - const divisions = await fetchDivisionsForCompany(company.id); + const divisions = await fetchDivisionsForCompany(row.id); if (divisions.length === 0) { setEmpty(true, "No divisions found for this company"); return; @@ -50,7 +50,7 @@ export default function DynamicNestedTablesDemo(props: { height?: string | numbe expandAll={dynamicNestedTablesConfig.tableProps.expandAll} height={props.height ?? "500px"} rowGrouping={dynamicNestedTablesConfig.tableProps.rowGrouping} - getRowId={dynamicNestedTablesConfig.tableProps.getRowId} + getRowId={({ row }) => row.id} rows={rows()} onRowGroupExpand={handleCompanyExpand} theme={props.theme} diff --git a/packages/examples/solid/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts b/packages/examples/solid/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts index a5f08a948..857c59230 100644 --- a/packages/examples/solid/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts +++ b/packages/examples/solid/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts @@ -1,8 +1,7 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; - -export interface DynamicCompany extends Row { +export interface DynamicCompany { id: string; companyName: string; industry: string; @@ -11,7 +10,7 @@ export interface DynamicCompany extends Row { divisions?: DynamicDivision[]; } -export interface DynamicDivision extends Row { +export interface DynamicDivision { id: string; divisionName: string; revenue: string; @@ -25,8 +24,22 @@ const simulateDelay = (ms: number) => new Promise((resolve) => setTimeout(resolv export const fetchDivisionsForCompany = async (companyId: string): Promise => { await simulateDelay(800); const divisionCount = Math.floor(Math.random() * 3) + 2; - const divisionNames = ["Cloud Services", "AI Research", "Consumer Products", "Investment Banking", "Operations", "Engineering"]; - const locations = ["San Francisco, CA", "New York, NY", "Boston, MA", "Seattle, WA", "Austin, TX", "Chicago, IL"]; + const divisionNames = [ + "Cloud Services", + "AI Research", + "Consumer Products", + "Investment Banking", + "Operations", + "Engineering", + ]; + const locations = [ + "San Francisco, CA", + "New York, NY", + "Boston, MA", + "Seattle, WA", + "Austin, TX", + "Chicago, IL", + ]; return Array.from({ length: divisionCount }, (_, i) => ({ id: `${companyId}-div-${i}`, @@ -39,24 +52,114 @@ export const fetchDivisionsForCompany = async (companyId: string): Promise[] = [ { accessor: "divisionName", label: "Division", width: 200 }, { accessor: "revenue", label: "Revenue", width: 120 }, { accessor: "profitMargin", label: "Profit Margin", width: 130 }, @@ -64,7 +167,7 @@ export const dynamicNestedTablesDivisionHeaders: SolidColumnDef[] = [ { accessor: "location", label: "Location", width: 180 }, ]; -export const dynamicNestedTablesCompanyHeaders: SolidColumnDef[] = [ +export const dynamicNestedTablesCompanyHeaders: SolidColumnDef[] = [ { accessor: "companyName", label: "Company", @@ -85,9 +188,8 @@ export const dynamicNestedTablesConfig = { headers: dynamicNestedTablesCompanyHeaders, rows: dynamicNestedTablesData, tableProps: { - rowGrouping: ["divisions"] as string[], - getRowId: ({ row }: { row: Record }) => row.id as string, + rowGrouping: ["divisions"], expandAll: false, autoExpandColumns: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx b/packages/examples/solid/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx index 8d4dbba10..cddbc196b 100644 --- a/packages/examples/solid/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx +++ b/packages/examples/solid/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx @@ -1,12 +1,13 @@ import { createSignal } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid"; import { dynamicRowLoadingConfig, generateInitialRegions, fetchStoresForRegion, fetchProductsForStore, } from "./dynamic-row-loading.demo-data"; -import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data"; +import type { DynamicRegion, DynamicTreeRow } from "./dynamic-row-loading.demo-data"; import "@simple-table/solid/styles.css"; export default function DynamicRowLoadingDemo(props: { height?: string | number; theme?: Theme }) { @@ -21,17 +22,20 @@ export default function DynamicRowLoadingDemo(props: { height?: string | number; setError, setEmpty, rowIndexPath, - }: OnRowGroupExpandProps) => { + }: OnRowGroupExpandProps) => { if (!isExpanded) return; - if (groupingKey && row[groupingKey] && (row[groupingKey] as unknown[]).length > 0) return; + if (groupingKey && row[groupingKey as keyof DynamicTreeRow] != null) { + const nested = row[groupingKey as keyof DynamicTreeRow]; + if (Array.isArray(nested) && nested.length > 0) return; + } try { - if (depth === 0 && groupingKey === "stores") { + if (depth === 0 && groupingKey === "stores" && row.type === "region") { setLoading(true); - const stores = await fetchStoresForRegion((row as DynamicRegion).id); + const stores = await fetchStoresForRegion(row.id); setLoading(false); if (stores.length === 0) { - setEmpty(true, "No stores found"); + setEmpty(true, "No stores found for this region"); return; } setRows((prev) => { @@ -39,12 +43,12 @@ export default function DynamicRowLoadingDemo(props: { height?: string | number; newRows[rowIndexPath[0]].stores = stores; return newRows; }); - } else if (depth === 1 && groupingKey === "products") { + } else if (depth === 1 && groupingKey === "products" && row.type === "store") { setLoading(true); - const products = await fetchProductsForStore((row as DynamicStore).id); + const products = await fetchProductsForStore(row.id); setLoading(false); if (products.length === 0) { - setEmpty(true, "No products found"); + setEmpty(true, "No products found for this store"); return; } setRows((prev) => { @@ -71,7 +75,7 @@ export default function DynamicRowLoadingDemo(props: { height?: string | number; height={props.height ?? "400px"} onRowGroupExpand={handleRowExpand} rowGrouping={dynamicRowLoadingConfig.tableProps.rowGrouping} - getRowId={dynamicRowLoadingConfig.tableProps.getRowId} + getRowId={({ row }) => row.id} rows={rows()} selectableCells={dynamicRowLoadingConfig.tableProps.selectableCells} theme={props.theme} diff --git a/packages/examples/solid/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts b/packages/examples/solid/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts index fdb0a23f4..63d898c94 100644 --- a/packages/examples/solid/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts +++ b/packages/examples/solid/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts @@ -1,8 +1,7 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; - -export interface DynamicRegion extends Row { +export interface DynamicRegion { id: string; name: string; type: "region"; @@ -14,7 +13,7 @@ export interface DynamicRegion extends Row { stores?: DynamicStore[]; } -export interface DynamicStore extends Row { +export interface DynamicStore { id: string; name: string; type: "store"; @@ -26,7 +25,7 @@ export interface DynamicStore extends Row { products?: DynamicProduct[]; } -export interface DynamicProduct extends Row { +export interface DynamicProduct { id: string; name: string; type: "product"; @@ -37,7 +36,10 @@ export interface DynamicProduct extends Row { lastUpdate: string; } -export const dynamicRowLoadingHeaders: SolidColumnDef[] = [ +/** Row union across grouping depths (region → store → product). */ +export type DynamicTreeRow = DynamicRegion | DynamicStore | DynamicProduct; + +export const dynamicRowLoadingHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", width: 280, expandable: true, type: "string", pinned: "left" }, { accessor: "type", label: "Type", width: 100, type: "string" }, { @@ -130,7 +132,7 @@ const generateStoresForRegion = (regionId: string): DynamicStore[] => { return { id: storeId, name: STORE_NAMES[storeIndex % STORE_NAMES.length], - type: "store" as const, + type: "store", totalSales, totalRevenue: totalSales * avgPrice, avgRating: getRandomRating(storeId), @@ -150,7 +152,7 @@ const generateProductsForStore = (storeId: string): DynamicProduct[] => { return { id: productId, name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length], - type: "product" as const, + type: "product", totalSales, totalRevenue: totalSales * avgPrice, avgRating: getRandomRating(productId), @@ -181,7 +183,7 @@ export const generateInitialRegions = (): DynamicRegion[] => { return { id: regionId, name, - type: "region" as const, + type: "region", totalSales, totalRevenue, activeStores: getRandomInt(regionId, 3, 4), @@ -194,12 +196,11 @@ export const generateInitialRegions = (): DynamicRegion[] => { export const dynamicRowLoadingConfig = { headers: dynamicRowLoadingHeaders, tableProps: { - rowGrouping: ["stores", "products"] as string[], - getRowId: ({ row }: { row: Record }) => row.id as string, + rowGrouping: ["stores", "products"], expandAll: false, columnResizing: true, selectableCells: true, oddEvenRowBackground: true, enableColumnEditor: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/empty-state/EmptyStateDemo.tsx b/packages/examples/solid/src/demos/empty-state/EmptyStateDemo.tsx index d97d641b0..429962f28 100644 --- a/packages/examples/solid/src/demos/empty-state/EmptyStateDemo.tsx +++ b/packages/examples/solid/src/demos/empty-state/EmptyStateDemo.tsx @@ -14,6 +14,7 @@ export default function EmptyStateDemo(props: { height?: string | number; theme? return ( row.id} rows={emptyStateConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/empty-state/empty-state.demo-data.ts b/packages/examples/solid/src/demos/empty-state/empty-state.demo-data.ts index edb449f1a..10fb78700 100644 --- a/packages/examples/solid/src/demos/empty-state/empty-state.demo-data.ts +++ b/packages/examples/solid/src/demos/empty-state/empty-state.demo-data.ts @@ -1,10 +1,17 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface EmptyEmployee { + id: number; + name: string; + email: string; + role: string; + department: string; +} -export const emptyStateData: Row[] = []; +export const emptyStateData: EmptyEmployee[] = []; -export const emptyStateHeaders: SolidColumnDef[] = [ +export const emptyStateHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: 180, type: "string" }, { accessor: "email", label: "Email", width: 220, type: "string" }, @@ -15,4 +22,4 @@ export const emptyStateHeaders: SolidColumnDef[] = [ export const emptyStateConfig = { headers: emptyStateHeaders, rows: emptyStateData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/external-filter/ExternalFilterDemo.tsx b/packages/examples/solid/src/demos/external-filter/ExternalFilterDemo.tsx index 7c9bbe051..65e9b6606 100644 --- a/packages/examples/solid/src/demos/external-filter/ExternalFilterDemo.tsx +++ b/packages/examples/solid/src/demos/external-filter/ExternalFilterDemo.tsx @@ -1,6 +1,11 @@ import { createSignal, createMemo } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, TableFilterState } from "@simple-table/solid"; -import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, TableFilterState } from "@simple-table/solid"; +import { + externalFilterConfig, + matchesFilter, + type FilterableEmployee, +} from "./external-filter.demo-data"; import "@simple-table/solid/styles.css"; export default function ExternalFilterDemo(props: { height?: string | number; theme?: Theme }) { @@ -12,14 +17,15 @@ export default function ExternalFilterDemo(props: { height?: string | number; th return externalFilterConfig.rows.filter((row) => entries.every(([accessor, filter]) => - matchesFilter(row[accessor as keyof typeof row] as any, filter) - ) + matchesFilter(row[accessor as keyof FilterableEmployee], filter), + ), ); }); return ( row.id} rows={filteredData()} onFilterChange={setFilters} externalFilterHandling diff --git a/packages/examples/solid/src/demos/external-filter/external-filter.demo-data.ts b/packages/examples/solid/src/demos/external-filter/external-filter.demo-data.ts index 0164cba66..efc89d13f 100644 --- a/packages/examples/solid/src/demos/external-filter/external-filter.demo-data.ts +++ b/packages/examples/solid/src/demos/external-filter/external-filter.demo-data.ts @@ -1,6 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef, TableFilterState } from "@simple-table/solid"; +export interface FilterableEmployee { + id: number; + name: string; + age: number; + email: string; + salary: number; + department: string; + active: boolean; + location: string; +} type CellValue = string | number | boolean | null | undefined; @@ -75,7 +85,7 @@ const LOCATION_OPTIONS = [ { label: "Moscow", value: "Moscow" }, ]; -export const externalFilterData = [ +export const externalFilterData: FilterableEmployee[] = [ { id: 1, name: "Dr. Elena Vasquez", age: 42, email: "elena.vasquez@techcorp.com", salary: 145000, department: "AI Research", active: true, location: "San Francisco" }, { id: 2, name: "Kai Tanaka", age: 29, email: "k.tanaka@techcorp.com", salary: 95000, department: "UX Design", active: true, location: "Tokyo" }, { id: 3, name: "Amara Okafor", age: 35, email: "amara.okafor@techcorp.com", salary: 125000, department: "DevOps", active: false, location: "Lagos" }, @@ -90,7 +100,7 @@ export const externalFilterData = [ { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" }, ]; -export const externalFilterHeaders: SolidColumnDef[] = [ +export const externalFilterHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" }, { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" }, { @@ -125,4 +135,4 @@ export const externalFilterConfig = { headers: externalFilterHeaders, rows: externalFilterData, tableProps: { externalFilterHandling: true, columnResizing: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/external-sort/ExternalSortDemo.tsx b/packages/examples/solid/src/demos/external-sort/ExternalSortDemo.tsx index 9c5beaf3b..133892a91 100644 --- a/packages/examples/solid/src/demos/external-sort/ExternalSortDemo.tsx +++ b/packages/examples/solid/src/demos/external-sort/ExternalSortDemo.tsx @@ -1,42 +1,41 @@ -import {SimpleTable, asRows} from "@simple-table/solid";import type { Theme, SortColumn, Row } from "@simple-table/solid"; -import { externalSortConfig } from "./external-sort.demo-data"; import { createSignal, createMemo } from "solid-js"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, SortColumn } from "@simple-table/solid"; +import { externalSortConfig, type SortableEmployee } from "./external-sort.demo-data"; import "@simple-table/solid/styles.css"; export default function ExternalSortDemo(props: { height?: string | number; theme?: Theme; }) { - const [sortState, setSortState] = createSignal(null); + const [sortConfig, setSortConfig] = createSignal(null); - const sortedRows = createMemo((): Row[] => { - const sort = sortState(); - const rows = [...asRows(externalSortConfig.rows)]; - if (!sort) return rows; - const accessor = sort.key.accessor as string; - const type = sort.key.type; - const dir = sort.direction; - return rows.sort((a, b) => { - const aVal = a[accessor]; - const bVal = b[accessor]; + const sortedData = createMemo(() => { + const sort = sortConfig(); + if (!sort) return externalSortConfig.rows; + return [...externalSortConfig.rows].sort((a, b) => { + const key = sort.key.accessor as keyof SortableEmployee; + const aVal = a[key]; + const bVal = b[key]; if (aVal === bVal) return 0; const cmp = - type === "number" - ? (Number(aVal) || 0) - (Number(bVal) || 0) + sort.key.type === "number" + ? (aVal as number) - (bVal as number) : String(aVal).localeCompare(String(bVal)); - return dir === "asc" ? cmp : -cmp; + return sort.direction === "asc" ? cmp : -cmp; }); }); return ( row.id} + rows={sortedData()} + onSortChange={setSortConfig} + externalSortHandling + columnResizing height={props.height ?? "400px"} theme={props.theme} - externalSortHandling={true} - columnResizing={true} - onSortChange={(sort) => setSortState(sort)} /> ); } diff --git a/packages/examples/solid/src/demos/external-sort/external-sort.demo-data.ts b/packages/examples/solid/src/demos/external-sort/external-sort.demo-data.ts index 962e9d54f..0e0fc9d13 100644 --- a/packages/examples/solid/src/demos/external-sort/external-sort.demo-data.ts +++ b/packages/examples/solid/src/demos/external-sort/external-sort.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface SortableEmployee { + id: number; + name: string; + age: number; + email: string; + salary: number; + department: string; +} -export const externalSortData = [ +export const externalSortData: SortableEmployee[] = [ { id: 1, name: "Dr. Elena Vasquez", age: 42, email: "elena.vasquez@techcorp.com", salary: 145000, department: "AI Research" }, { id: 2, name: "Kai Tanaka", age: 29, email: "k.tanaka@techcorp.com", salary: 95000, department: "UX Design" }, { id: 3, name: "Amara Okafor", age: 35, email: "amara.okafor@techcorp.com", salary: 125000, department: "DevOps" }, @@ -17,7 +25,7 @@ export const externalSortData = [ { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps" }, ]; -export const externalSortHeaders: SolidColumnDef[] = [ +export const externalSortHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", width: "1fr", minWidth: 120, sortable: true, type: "string" }, { accessor: "age", label: "Age", width: 120, sortable: true, type: "number" }, { accessor: "department", label: "Department", width: 150, sortable: true, type: "string" }, @@ -37,4 +45,4 @@ export const externalSortConfig = { headers: externalSortHeaders, rows: externalSortData, tableProps: { externalSortHandling: true, columnResizing: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/footer-renderer/FooterRendererDemo.tsx b/packages/examples/solid/src/demos/footer-renderer/FooterRendererDemo.tsx index 59256acf0..90f758621 100644 --- a/packages/examples/solid/src/demos/footer-renderer/FooterRendererDemo.tsx +++ b/packages/examples/solid/src/demos/footer-renderer/FooterRendererDemo.tsx @@ -41,6 +41,7 @@ export default function FooterRendererDemo(props: { height?: string | number; th return ( row.id} rows={footerRendererConfig.rows} enablePagination={true} rowsPerPage={10} diff --git a/packages/examples/solid/src/demos/footer-renderer/footer-renderer.demo-data.ts b/packages/examples/solid/src/demos/footer-renderer/footer-renderer.demo-data.ts index 6eb96a557..176821e72 100644 --- a/packages/examples/solid/src/demos/footer-renderer/footer-renderer.demo-data.ts +++ b/packages/examples/solid/src/demos/footer-renderer/footer-renderer.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface CatalogProduct { + id: number; + product: string; + category: string; + price: number; + stock: number; + status: string; +} -export const footerRendererHeaders: SolidColumnDef[] = [ +export const footerRendererHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "product", label: "Product Name", width: 220, type: "string" }, { accessor: "category", label: "Category", width: 150, type: "string" }, @@ -11,7 +19,7 @@ export const footerRendererHeaders: SolidColumnDef[] = [ { accessor: "status", label: "Status", width: "1fr", type: "string" }, ]; -export const footerRendererData: Row[] = [ +export const footerRendererData: CatalogProduct[] = [ { id: 1, product: "MacBook Pro 16-inch M3 Max", category: "Laptops", price: 3499, stock: 28, status: "In Stock" }, { id: 2, product: "Dell XPS 15 OLED Touchscreen", category: "Laptops", price: 2299, stock: 42, status: "In Stock" }, { id: 3, product: "ThinkPad X1 Carbon Gen 11", category: "Laptops", price: 1899, stock: 35, status: "In Stock" }, @@ -66,4 +74,4 @@ export const footerRendererConfig = { enablePagination: true, rowsPerPage: 10, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/header-renderer/HeaderRendererDemo.tsx b/packages/examples/solid/src/demos/header-renderer/HeaderRendererDemo.tsx index 3b9d95fe9..5cb6a1b13 100644 --- a/packages/examples/solid/src/demos/header-renderer/HeaderRendererDemo.tsx +++ b/packages/examples/solid/src/demos/header-renderer/HeaderRendererDemo.tsx @@ -1,6 +1,6 @@ import { createSignal, createMemo } from "solid-js"; import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidColumnDef, HeaderRendererProps } from "@simple-table/solid"; -import { headerRendererConfig } from "./header-renderer.demo-data"; +import { headerRendererConfig, type HeaderEmployee } from "./header-renderer.demo-data"; import "@simple-table/solid/styles.css"; type SortDir = "asc" | "desc" | null; @@ -15,8 +15,9 @@ export default function HeaderRendererDemo(props: { height?: string | number; th const dir = sortDirection(); if (!acc || !dir) return [...headerRendererConfig.rows]; return [...headerRendererConfig.rows].sort((a, b) => { - const aVal = a[acc]; - const bVal = b[acc]; + const key = acc as keyof HeaderEmployee; + const aVal = a[key]; + const bVal = b[key]; if (aVal === bVal) return 0; const cmp = typeof aVal === "number" && typeof bVal === "number" ? aVal - bVal @@ -25,11 +26,11 @@ export default function HeaderRendererDemo(props: { height?: string | number; th }); }); - const headers = createMemo((): SolidColumnDef[] => + const headers = createMemo((): SolidColumnDef[] => headerRendererConfig.headers.map((h) => ({ ...h, sortable: false, - headerRenderer: ({ accessor }: HeaderRendererProps) => { + headerRenderer: ({ accessor }: HeaderRendererProps) => { const isSorted = sortAccessor() === accessor; const dir = isSorted ? sortDirection() : null; const indicator = dir === "asc" ? " ▲" : dir === "desc" ? " ▼" : ""; @@ -71,6 +72,7 @@ export default function HeaderRendererDemo(props: { height?: string | number; th return ( row.id} rows={sortedData()} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/header-renderer/header-renderer.demo-data.ts b/packages/examples/solid/src/demos/header-renderer/header-renderer.demo-data.ts index 9221bb264..54356a889 100644 --- a/packages/examples/solid/src/demos/header-renderer/header-renderer.demo-data.ts +++ b/packages/examples/solid/src/demos/header-renderer/header-renderer.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface HeaderEmployee { + id: number; + name: string; + email: string; + role: string; + salary: number; + department: string; +} -export const headerRendererData: Row[] = [ +export const headerRendererData: HeaderEmployee[] = [ { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Engineer", salary: 125000, department: "Engineering" }, { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Designer", salary: 98000, department: "Design" }, { id: 3, name: "Clara Chen", email: "clara@example.com", role: "PM", salary: 115000, department: "Product" }, @@ -13,7 +21,7 @@ export const headerRendererData: Row[] = [ { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" }, ]; -export const headerRendererHeaders: SolidColumnDef[] = [ +export const headerRendererHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number", sortable: true }, { accessor: "name", label: "Employee Name", width: 180, type: "string", sortable: true }, { accessor: "email", label: "Email Address", width: 200, type: "string" }, @@ -29,4 +37,4 @@ export const headerRendererConfig = { selectableCells: true, columnResizing: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/hr/HRDemo.tsx b/packages/examples/solid/src/demos/hr/HRDemo.tsx index adc7b2647..6a81ded95 100644 --- a/packages/examples/solid/src/demos/hr/HRDemo.tsx +++ b/packages/examples/solid/src/demos/hr/HRDemo.tsx @@ -4,13 +4,13 @@ import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "./hr.demo-data" import type { HREmployee, HRTagColorKey } from "./hr.demo-data"; import "@simple-table/solid/styles.css"; -function getHeaders(): SolidColumnDef[] { +function getHeaders(): SolidColumnDef[] { return hrConfig.headers.map((h) => { if (h.accessor === "fullName") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as HREmployee; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; const c = getHRThemeColors(theme); const initials = `${d.firstName?.charAt(0) || ""}${d.lastName?.charAt(0) || ""}`; return ( @@ -42,8 +42,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "performanceScore") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as HREmployee; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; const c = getHRThemeColors(theme); const color = d.performanceScore >= 90 @@ -89,8 +89,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "hireDate") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as HREmployee; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; if (!d.hireDate) return ""; const [year, month, day] = d.hireDate.split("-").map(Number); const date = new Date(year, month - 1, day); @@ -110,8 +110,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "yearsOfService") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as HREmployee; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; if (d.yearsOfService === null) return ""; const c = getHRThemeColors(theme); return {`${d.yearsOfService} yrs`}; @@ -121,8 +121,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "salary") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as HREmployee; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; const c = getHRThemeColors(theme); return {`$${d.salary.toLocaleString()}`}; }, @@ -131,8 +131,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as HREmployee; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; if (!d.status) return ""; const c = getHRThemeColors(theme); const colorKey: HRTagColorKey = HR_STATUS_COLOR_MAP[d.status] || "default"; @@ -165,7 +165,7 @@ export default function HRDemo(props: { height?: string | number; theme?: Theme const heightNum = () => (typeof props.height === "number" ? props.height : 400); const howManyRowsCanFit = () => Math.floor(heightNum() / rowHeight); - const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { + const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { setData((prev) => prev.map((item) => (item.id === row.id ? { ...item, [accessor]: newValue } : item)), ); @@ -176,6 +176,7 @@ export default function HRDemo(props: { height?: string | number; theme?: Theme columnReordering columnResizing columns={getHeaders()} + getRowId={({ row }) => row.id} onCellEdit={handleCellEdit} customTheme={{ rowHeight }} rows={data()} diff --git a/packages/examples/solid/src/demos/hr/hr.demo-data.ts b/packages/examples/solid/src/demos/hr/hr.demo-data.ts index c5b609627..e246768c0 100644 --- a/packages/examples/solid/src/demos/hr/hr.demo-data.ts +++ b/packages/examples/solid/src/demos/hr/hr.demo-data.ts @@ -55,7 +55,7 @@ export function generateHRData(count: number = 100): HREmployee[] { export const hrData = generateHRData(100); -export const hrHeaders: SolidColumnDef[] = [ +export const hrHeaders: SolidColumnDef[] = [ { accessor: "fullName", label: "Employee", width: 220, sortable: true, editable: false, align: "left", pinned: "left", type: "string" }, { accessor: "performanceScore", label: "Performance", width: 160, sortable: true, editable: true, align: "center", type: "number", @@ -123,4 +123,4 @@ export const HR_STATUS_COLOR_MAP: Record = { export const hrConfig = { headers: hrHeaders, rows: hrData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/infinite-scroll/InfiniteScrollDemo.tsx b/packages/examples/solid/src/demos/infinite-scroll/InfiniteScrollDemo.tsx index 61c4dc3b9..3352c6ea4 100644 --- a/packages/examples/solid/src/demos/infinite-scroll/InfiniteScrollDemo.tsx +++ b/packages/examples/solid/src/demos/infinite-scroll/InfiniteScrollDemo.tsx @@ -1,18 +1,23 @@ import { createSignal, createMemo } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, Row } from "@simple-table/solid"; -import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme } from "@simple-table/solid"; +import { + infiniteScrollConfig, + generateInfiniteScrollData, + type InfiniteScrollEmployee, +} from "./infinite-scroll.demo-data"; import "@simple-table/solid/styles.css"; const MAX_ROWS = 200; const BATCH_SIZE = 15; export default function InfiniteScrollDemo(props: { height?: string | number; theme?: Theme }) { - const [rows, setRows] = createSignal(generateInfiniteScrollData(0, 30) as Row[]); + const [rows, setRows] = createSignal(generateInfiniteScrollData(0, 30)); const [loading, setLoading] = createSignal(false); const [hasMore, setHasMore] = createSignal(true); const statusText = createMemo(() => - `${rows().length} rows loaded${hasMore() ? "" : " (all loaded)"}` + `${rows().length} rows loaded${hasMore() ? "" : " (all loaded)"}`, ); const handleLoadMore = () => { @@ -20,7 +25,7 @@ export default function InfiniteScrollDemo(props: { height?: string | number; th setLoading(true); setTimeout(() => { setRows((prev) => { - const newRows = generateInfiniteScrollData(prev.length, BATCH_SIZE) as Row[]; + const newRows = generateInfiniteScrollData(prev.length, BATCH_SIZE); const updated = [...prev, ...newRows]; if (updated.length >= MAX_ROWS) setHasMore(false); return updated; @@ -36,6 +41,7 @@ export default function InfiniteScrollDemo(props: { height?: string | number; th
row.id} rows={rows()} onLoadMore={handleLoadMore} isLoading={loading()} diff --git a/packages/examples/solid/src/demos/infinite-scroll/infinite-scroll.demo-data.ts b/packages/examples/solid/src/demos/infinite-scroll/infinite-scroll.demo-data.ts index 439795faa..8fc9ce78b 100644 --- a/packages/examples/solid/src/demos/infinite-scroll/infinite-scroll.demo-data.ts +++ b/packages/examples/solid/src/demos/infinite-scroll/infinite-scroll.demo-data.ts @@ -1,13 +1,20 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface InfiniteScrollEmployee { + id: number; + name: string; + email: string; + department: string; + salary: number; +} const FIRST_NAMES = ["Elena", "Kai", "Amara", "Santiago", "Priya", "Magnus", "Zara", "Luca", "Sarah", "Olumide", "Isabella", "Dmitri"]; const LAST_NAMES = ["Vasquez", "Tanaka", "Okafor", "Rodriguez", "Chakraborty", "Eriksson", "Al-Rashid", "Rossi", "Kim", "Adebayo", "Chen", "Volkov"]; const DEPARTMENTS = ["Engineering", "AI Research", "UX Design", "DevOps", "Marketing", "Product", "Sales", "Finance"]; -export function generateInfiniteScrollData(startIndex: number, count: number): Row[] { - const rows: Row[] = []; +export function generateInfiniteScrollData(startIndex: number, count: number): InfiniteScrollEmployee[] { + const rows: InfiniteScrollEmployee[] = []; for (let i = 0; i < count; i++) { const idx = startIndex + i; const first = FIRST_NAMES[idx % FIRST_NAMES.length]; @@ -23,7 +30,7 @@ export function generateInfiniteScrollData(startIndex: number, count: number): R return rows; } -export const infiniteScrollHeaders: SolidColumnDef[] = [ +export const infiniteScrollHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", width: "1fr", minWidth: 120 }, { accessor: "email", label: "Email", width: 250 }, @@ -41,4 +48,4 @@ export const infiniteScrollHeaders: SolidColumnDef[] = [ export const infiniteScrollConfig = { headers: infiniteScrollHeaders, rows: generateInfiniteScrollData(0, 30), -} as const; +}; diff --git a/packages/examples/solid/src/demos/infrastructure/InfrastructureDemo.tsx b/packages/examples/solid/src/demos/infrastructure/InfrastructureDemo.tsx index a8e372c7e..83ff8dc56 100644 --- a/packages/examples/solid/src/demos/infrastructure/InfrastructureDemo.tsx +++ b/packages/examples/solid/src/demos/infrastructure/InfrastructureDemo.tsx @@ -1,6 +1,6 @@ import { createSignal, createEffect, onMount, onCleanup } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { Theme, TableAPI, Row, CellValue, SolidColumnDef } from "@simple-table/solid"; +import type { Theme, TableAPI, CellValue, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; import { infrastructureData, getInfraMetricColorStyles, getInfraStatusColors } from "./infrastructure.demo-data"; import type { InfrastructureServer } from "./infrastructure.demo-data"; import "@simple-table/solid/styles.css"; @@ -20,15 +20,15 @@ function infraPickRandomSubset(arr: T[], n: number): T[] { return copy.slice(0, Math.min(n, copy.length)); } -function infraApplyRowPatch(api: TableAPI, rowId: string | number, patch: Partial) { - for (const accessor of Object.keys(patch)) { +function infraApplyRowPatch(api: TableAPI, rowId: string | number, patch: Partial) { + for (const accessor of Object.keys(patch) as (keyof InfrastructureServer)[]) { const newValue = patch[accessor]; if (newValue === undefined) continue; api.updateData({ accessor, rowId, newValue: newValue as CellValue }); } } -function infraComputeMetricPatch(row: Row, slot: InfraMetricSlot): Partial | null { +function infraComputeMetricPatch(row: InfrastructureServer, slot: InfraMetricSlot): Partial | null { switch (slot) { case 0: { const currentCpu = row.cpuUsage as number; @@ -84,7 +84,7 @@ function infraComputeMetricPatch(row: Row, slot: InfraMetricSlot): Partial } } -function startInfraDemoLiveUpdates(getApi: () => TableAPI | null | undefined): () => void { +function startInfraDemoLiveUpdates(getApi: () => TableAPI | null | undefined): () => void { let isActive = true; const tick = () => { if (!isActive) return; @@ -95,12 +95,13 @@ function startInfraDemoLiveUpdates(getApi: () => TableAPI | null | undefined): ( const picks = infraPickRandomSubset(visible, INFRA_ROWS_PER_TICK); let usedCpuSparkline = false; for (const vr of picks) { - const rowId = vr.row.id as string | number | undefined; - if (rowId === undefined || rowId === null || rowId === "") continue; + const rowId = vr.row.id; + if (typeof rowId !== "number") continue; + const server = vr.row as unknown as InfrastructureServer; let slot = Math.floor(Math.random() * 7) as InfraMetricSlot; if (slot === 0 && usedCpuSparkline) slot = (1 + Math.floor(Math.random() * 6)) as InfraMetricSlot; if (slot === 0) usedCpuSparkline = true; - const patch = infraComputeMetricPatch(vr.row, slot); + const patch = infraComputeMetricPatch(server, slot); if (patch) infraApplyRowPatch(api, rowId, patch); } }; @@ -112,10 +113,10 @@ function startInfraDemoLiveUpdates(getApi: () => TableAPI | null | undefined): ( }; } -function getHeaders(currentTheme?: Theme): SolidColumnDef[] { +function getHeaders(currentTheme?: Theme): SolidColumnDef[] { const t = currentTheme || "light"; return [ - { accessor: "serverId", align: "left", filterable: true, editable: false, sortable: true, label: "Server ID", minWidth: 180, pinned: "left", type: "string", width: "1.2fr", cellRenderer: ({ row }) => {(row as unknown as InfrastructureServer).serverId} }, + { accessor: "serverId", align: "left", filterable: true, editable: false, sortable: true, label: "Server ID", minWidth: 180, pinned: "left", type: "string", width: "1.2fr", cellRenderer: ({ row }: CellRendererProps) => {row.serverId} }, { accessor: "serverName", align: "left", filterable: true, editable: false, sortable: true, label: "Name", minWidth: 200, type: "string", width: "1.5fr" }, { accessor: "performance", label: "Performance Metrics", width: 690, sortable: false, @@ -123,16 +124,16 @@ function getHeaders(currentTheme?: Theme): SolidColumnDef[] { { accessor: "cpuHistory", label: "CPU History", width: 150, sortable: false, filterable: false, editable: false, align: "center", type: "lineAreaChart", tooltip: "CPU usage over the last 30 intervals" }, { accessor: "cpuUsage", label: "CPU %", width: 120, sortable: true, filterable: true, editable: true, align: "right", type: "number", - cellRenderer: ({ row, theme }) => { const d = row as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(d.cpuUsage, theme || t, "cpu"); return
{d.cpuUsage.toFixed(1)}%
; }, + cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const s = getInfraMetricColorStyles(d.cpuUsage, theme || t, "cpu"); return
{d.cpuUsage.toFixed(1)}%
; }, }, { accessor: "memoryUsage", label: "Memory %", width: 130, sortable: true, filterable: true, editable: true, align: "right", type: "number", - cellRenderer: ({ row, theme }) => { const d = row as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(d.memoryUsage, theme || t, "memory"); return
{d.memoryUsage.toFixed(1)}%
; }, + cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const s = getInfraMetricColorStyles(d.memoryUsage, theme || t, "memory"); return
{d.memoryUsage.toFixed(1)}%
; }, }, - { accessor: "diskUsage", label: "Disk %", width: 120, sortable: true, filterable: true, editable: true, align: "right", type: "number", cellRenderer: ({ row }) => `${(row as unknown as InfrastructureServer).diskUsage.toFixed(1)}%` }, + { accessor: "diskUsage", label: "Disk %", width: 120, sortable: true, filterable: true, editable: true, align: "right", type: "number", cellRenderer: ({ row }: CellRendererProps) => `${row.diskUsage.toFixed(1)}%` }, { accessor: "responseTime", label: "Response (ms)", width: 120, sortable: true, filterable: true, editable: true, align: "right", type: "number", - cellRenderer: ({ row, theme }) => { const d = row as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(d.responseTime, theme || t, "response"); return {d.responseTime.toFixed(1)}; }, + cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const s = getInfraMetricColorStyles(d.responseTime, theme || t, "response"); return {d.responseTime.toFixed(1)}; }, }, ], }, @@ -140,13 +141,13 @@ function getHeaders(currentTheme?: Theme): SolidColumnDef[] { accessor: "status", label: "Status", width: 130, sortable: true, filterable: true, editable: false, align: "center", type: "enum", enumOptions: [{ label: "Online", value: "online" }, { label: "Warning", value: "warning" }, { label: "Critical", value: "critical" }, { label: "Maintenance", value: "maintenance" }, { label: "Offline", value: "offline" }], valueGetter: ({ row }) => { const s = String(row.status); const m: Record = { critical: 1, offline: 2, warning: 3, maintenance: 4, online: 5 }; return m[s] || 999; }, - cellRenderer: ({ row, theme }) => { const d = row as unknown as InfrastructureServer; const s = getInfraStatusColors(d.status, theme || t); return
{d.status.charAt(0).toUpperCase() + d.status.slice(1)}
; }, + cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const s = getInfraStatusColors(d.status, theme || t); return
{d.status.charAt(0).toUpperCase() + d.status.slice(1)}
; }, }, ]; } export default function InfrastructureDemo(props: { height?: string | number; theme?: Theme }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; let cleanupFn: (() => void) | undefined; const [isMobile, setIsMobile] = createSignal(false); const data = infrastructureData; @@ -171,7 +172,7 @@ export default function InfrastructureDemo(props: { height?: string | number; th columnResizing columns={getHeaders(props.theme)} enableColumnEditor - getRowId={({ row }) => (row as { id: string | number }).id} + getRowId={({ row }) => row.id} height={props.height ?? "400px"} ref={(api) => (tableRef = api)} rows={data} diff --git a/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts b/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts index 2dbc629d7..425e20c67 100644 --- a/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts +++ b/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts @@ -1,5 +1,5 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; export interface InfrastructureServer { id: number; @@ -50,7 +50,7 @@ const STATUSES: Array = [ "offline", ]; -export function generateInfrastructureData(count: number = 50): Row[] { +export function generateInfrastructureData(count: number = 50): InfrastructureServer[] { return Array.from({ length: count }, (_, i) => { const prefix = SERVER_PREFIXES[i % SERVER_PREFIXES.length]; const num = String(i + 1).padStart(3, "0"); @@ -75,7 +75,7 @@ export function generateInfrastructureData(count: number = 50): Row[] { export const infrastructureData = generateInfrastructureData(50); -export const infrastructureHeaders: SolidColumnDef[] = [ +export const infrastructureHeaders: SolidColumnDef[] = [ { accessor: "serverId", align: "left", @@ -255,4 +255,4 @@ export function getInfraStatusColors(status: string, theme: string) { export const infrastructureConfig = { headers: infrastructureHeaders, rows: infrastructureData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/live-update/LiveUpdateDemo.tsx b/packages/examples/solid/src/demos/live-update/LiveUpdateDemo.tsx index 99e2cd3b3..52b27135f 100644 --- a/packages/examples/solid/src/demos/live-update/LiveUpdateDemo.tsx +++ b/packages/examples/solid/src/demos/live-update/LiveUpdateDemo.tsx @@ -1,45 +1,48 @@ import { onMount, onCleanup } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, TableAPI } from "@simple-table/solid"; -import { liveUpdateConfig, liveUpdateData } from "./live-update.demo-data"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, TableAPI } from "@simple-table/solid"; +import { + liveUpdateConfig, + liveUpdateData, + type LiveUpdateProduct, +} from "./live-update.demo-data"; import "@simple-table/solid/styles.css"; export default function LiveUpdateDemo(props: { height?: string | number; theme?: Theme }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; let cleanupFn: (() => void) | undefined; onMount(() => { if (!tableRef) return; - const currentData = JSON.parse(JSON.stringify(liveUpdateData)); - const timerMap = new Map>(); - const currentPeriodSales = new Map(); + const currentData: LiveUpdateProduct[] = JSON.parse(JSON.stringify(liveUpdateData)); + const timerMap = new Map>(); + const currentPeriodSales = new Map(); let isActive = true; - const createRowTimer = (rowId: string | number) => { + const createRowTimer = (rowId: number) => { const scheduleUpdate = () => { if (!isActive) return; const interval = 300 + Math.random() * 700; const timerId = setTimeout(() => { if (!isActive || !tableRef) return; - const idx = currentData.findIndex((r: (typeof liveUpdateData)[number]) => r.id === rowId); + const idx = currentData.findIndex((r) => r.id === rowId); if (idx === -1) return; const product = currentData[idx]; - if (typeof product.price === "number") { - const newPrice = parseFloat((product.price * (0.95 + Math.random() * 0.1)).toFixed(2)); - currentData[idx].price = newPrice; - tableRef.updateData({ accessor: "price", rowId, newValue: newPrice }); - } - if (typeof product.stock === "number") { - const newStock = Math.max(0, product.stock + Math.floor((Math.random() - 0.5) * 6)); - currentData[idx].stock = newStock; - tableRef.updateData({ accessor: "stock", rowId, newValue: newStock }); - if (Array.isArray(product.stockHistory)) { - const updated = [...product.stockHistory.slice(1), newStock]; - currentData[idx].stockHistory = updated; - tableRef.updateData({ accessor: "stockHistory", rowId, newValue: updated }); - } + const newPrice = parseFloat((product.price * (0.95 + Math.random() * 0.1)).toFixed(2)); + currentData[idx].price = newPrice; + tableRef.updateData({ accessor: "price", rowId, newValue: newPrice }); + + const newStock = Math.max(0, product.stock + Math.floor((Math.random() - 0.5) * 6)); + currentData[idx].stock = newStock; + tableRef.updateData({ accessor: "stock", rowId, newValue: newStock }); + if (product.stockHistory.length > 0) { + const updated = [...product.stockHistory.slice(1), newStock]; + currentData[idx].stockHistory = updated; + tableRef.updateData({ accessor: "stockHistory", rowId, newValue: updated }); } - if (Math.random() < 0.6 && typeof product.sales === "number") { + + if (Math.random() < 0.6) { const inc = Math.floor(Math.random() * 3) + 1; currentData[idx].sales = product.sales + inc; tableRef.updateData({ accessor: "sales", rowId, newValue: currentData[idx].sales }); @@ -55,7 +58,9 @@ export default function LiveUpdateDemo(props: { height?: string | number; theme? const syncTimers = () => { if (!tableRef) return; const visibleRows = tableRef.getVisibleRows(); - const visibleIds = new Set(visibleRows.map((vr) => vr.row.id as string | number)); + const visibleIds = new Set( + visibleRows.map((vr) => vr.row.id).filter((id): id is number => typeof id === "number"), + ); timerMap.forEach((tid, rid) => { if (!visibleIds.has(rid)) { clearTimeout(tid); @@ -63,15 +68,15 @@ export default function LiveUpdateDemo(props: { height?: string | number; theme? } }); visibleRows.forEach((vr) => { - const rid = vr.row.id as string | number; - if (!timerMap.has(rid)) createRowTimer(rid); + const rowId = vr.row.id; + if (typeof rowId === "number" && !timerMap.has(rowId)) createRowTimer(rowId); }); }; const salesRotate = setInterval(() => { if (!tableRef || !isActive) return; - currentData.forEach((row: (typeof liveUpdateData)[number], i: number) => { - if (Array.isArray(row.salesHistory)) { + currentData.forEach((row, i) => { + if (row.salesHistory.length > 0) { const rid = row.id; const sp = currentPeriodSales.get(rid) || 0; const updated = [...row.salesHistory.slice(1), sp]; @@ -100,8 +105,8 @@ export default function LiveUpdateDemo(props: { height?: string | number; theme? (tableRef = api)} columns={liveUpdateConfig.headers} + getRowId={({ row }) => row.id} rows={liveUpdateConfig.rows} - getRowId={({ row }) => (row as { id: number }).id} height={props.height ?? "400px"} theme={props.theme} /> diff --git a/packages/examples/solid/src/demos/live-update/live-update.demo-data.ts b/packages/examples/solid/src/demos/live-update/live-update.demo-data.ts index c141566aa..a2b93458a 100644 --- a/packages/examples/solid/src/demos/live-update/live-update.demo-data.ts +++ b/packages/examples/solid/src/demos/live-update/live-update.demo-data.ts @@ -1,8 +1,17 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface LiveUpdateProduct { + id: number; + product: string; + price: number; + stock: number; + sales: number; + stockHistory: number[]; + salesHistory: number[]; +} -export const liveUpdateHeaders: SolidColumnDef[] = [ +export const liveUpdateHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "product", label: "Product", width: 180, type: "string" }, { @@ -40,7 +49,7 @@ export const generateSalesHistory = (_currentSales: number, length = 12) => { return history; }; -export const liveUpdateData = [ +export const liveUpdateData: LiveUpdateProduct[] = [ { id: 1, product: "Organic Green Tea", price: 24.99, stock: 156, sales: 342, stockHistory: generateStockHistory(156), salesHistory: generateSalesHistory(342) }, { id: 2, product: "Bluetooth Headphones", price: 89.99, stock: 73, sales: 187, stockHistory: generateStockHistory(73), salesHistory: generateSalesHistory(187) }, { id: 3, product: "Bamboo Yoga Mat", price: 45.99, stock: 92, sales: 256, stockHistory: generateStockHistory(92), salesHistory: generateSalesHistory(256) }, @@ -58,4 +67,4 @@ export const liveUpdateData = [ export const liveUpdateConfig = { headers: liveUpdateHeaders, rows: liveUpdateData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/loading-state/LoadingStateDemo.tsx b/packages/examples/solid/src/demos/loading-state/LoadingStateDemo.tsx index a4428d012..4f02eee22 100644 --- a/packages/examples/solid/src/demos/loading-state/LoadingStateDemo.tsx +++ b/packages/examples/solid/src/demos/loading-state/LoadingStateDemo.tsx @@ -1,11 +1,12 @@ import { createSignal, onMount, onCleanup } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, Row } from "@simple-table/solid"; -import { loadingStateConfig } from "./loading-state.demo-data"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme } from "@simple-table/solid"; +import { loadingStateConfig, type LoadingStateEmployee } from "./loading-state.demo-data"; import "@simple-table/solid/styles.css"; export default function LoadingStateDemo(props: { height?: string | number; theme?: Theme }) { const [isLoading, setIsLoading] = createSignal(true); - const [data, setData] = createSignal([]); + const [data, setData] = createSignal([]); let timer: ReturnType | null = null; const loadData = () => { @@ -13,13 +14,15 @@ export default function LoadingStateDemo(props: { height?: string | number; them setData([]); if (timer) clearTimeout(timer); timer = setTimeout(() => { - setData(loadingStateConfig.rows as Row[]); + setData([...loadingStateConfig.rows]); setIsLoading(false); }, 2000); }; onMount(() => loadData()); - onCleanup(() => { if (timer) clearTimeout(timer); }); + onCleanup(() => { + if (timer) clearTimeout(timer); + }); return (
@@ -34,6 +37,7 @@ export default function LoadingStateDemo(props: { height?: string | number; them
row.id} rows={data()} isLoading={isLoading()} height={props.height ?? "400px"} diff --git a/packages/examples/solid/src/demos/loading-state/loading-state.demo-data.ts b/packages/examples/solid/src/demos/loading-state/loading-state.demo-data.ts index 17903cae7..c0703fa41 100644 --- a/packages/examples/solid/src/demos/loading-state/loading-state.demo-data.ts +++ b/packages/examples/solid/src/demos/loading-state/loading-state.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface LoadingStateEmployee { + id: number; + name: string; + age: number; + department: string; + salary: number; + status: string; +} -export const loadingStateData = [ +export const loadingStateData: LoadingStateEmployee[] = [ { id: 1, name: "Dr. Elena Vasquez", age: 42, department: "AI Research", salary: 145000, status: "Active" }, { id: 2, name: "Kai Tanaka", age: 29, department: "UX Design", salary: 95000, status: "Active" }, { id: 3, name: "Amara Okafor", age: 35, department: "DevOps", salary: 125000, status: "On Leave" }, @@ -13,7 +21,7 @@ export const loadingStateData = [ { id: 8, name: "Luca Rossi", age: 26, department: "Marketing", salary: 75000, status: "Active" }, ]; -export const loadingStateHeaders: SolidColumnDef[] = [ +export const loadingStateHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Name", width: "1fr", minWidth: 120 }, { accessor: "age", label: "Age", width: 80, type: "number" }, { accessor: "department", label: "Department", width: 150 }, @@ -31,4 +39,4 @@ export const loadingStateHeaders: SolidColumnDef[] = [ export const loadingStateConfig = { headers: loadingStateHeaders, rows: loadingStateData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/manufacturing/ManufacturingDemo.tsx b/packages/examples/solid/src/demos/manufacturing/ManufacturingDemo.tsx index c6897873c..8712e1d83 100644 --- a/packages/examples/solid/src/demos/manufacturing/ManufacturingDemo.tsx +++ b/packages/examples/solid/src/demos/manufacturing/ManufacturingDemo.tsx @@ -1,16 +1,16 @@ -import {SimpleTable, asRows} from "@simple-table/solid";import type { Theme, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; +import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; import { manufacturingConfig, getManufacturingStatusColors } from "./manufacturing.demo-data"; import type { ManufacturingRow } from "./manufacturing.demo-data"; import "@simple-table/solid/styles.css"; -function getHeaders(): SolidColumnDef[] { +function getHeaders(): SolidColumnDef[] { const baseHeaders = [...manufacturingConfig.headers]; return baseHeaders.map((h) => { if (h.accessor === "productLine") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); return hasChildren ? {d.productLine} : d.productLine; }, @@ -19,8 +19,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "station") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return {d.id}; return ( @@ -35,8 +35,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row, theme }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return "—"; const colors = getManufacturingStatusColors(d.status, theme); @@ -47,8 +47,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "outputRate" || h.accessor === "defectCount" || h.accessor === "energy") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); const value = d[h.accessor as keyof ManufacturingRow] as number; return
{value.toLocaleString()}
; @@ -58,8 +58,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "cycletime") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return {d.cycletime?.toFixed(1)}; return {String(d.cycletime)}; @@ -69,8 +69,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "efficiency") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); const color = d.efficiency >= 90 ? "#52c41a" : d.efficiency >= 75 ? "#1890ff" : "#ff4d4f"; return ( @@ -87,8 +87,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "defectRate") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); const rate = typeof d.defectRate === "number" ? d.defectRate : parseFloat(String(d.defectRate)); const color = rate < 1 ? "#16a34a" : rate < 3 ? "#f59e0b" : "#dc2626"; @@ -99,8 +99,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "downtime") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); const hours = typeof d.downtime === "number" ? d.downtime : parseFloat(String(d.downtime)); const color = hours < 1 ? "#16a34a" : hours < 2 ? "#f59e0b" : "#dc2626"; @@ -111,8 +111,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "utilization") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return {d.utilization?.toFixed(0)}%; return `${d.utilization}%`; @@ -122,8 +122,8 @@ function getHeaders(): SolidColumnDef[] { if (h.accessor === "maintenanceDate") { return { ...h, - cellRenderer: ({ row }: CellRendererProps) => { - const d = row as unknown as ManufacturingRow; + cellRenderer: ({ row }: CellRendererProps) => { + const d = row; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return "—"; const [year, month, day] = d.maintenanceDate.split("-").map(Number); @@ -153,6 +153,6 @@ function getHeaders(): SolidColumnDef[] { export default function ManufacturingDemo(props: { height?: string | number; theme?: Theme }) { return ( - + row.id} height={props.height ?? "400px"} rowGrouping={["stations"]} rows={manufacturingConfig.rows} selectableCells theme={props.theme} /> ); } diff --git a/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts b/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts index 079d60db4..8041dfc42 100644 --- a/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts +++ b/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts @@ -83,7 +83,7 @@ export function generateManufacturingData(count: number = 8): ManufacturingRow[] export const manufacturingData = generateManufacturingData(8); -export const manufacturingHeaders: SolidColumnDef[] = [ +export const manufacturingHeaders: SolidColumnDef[] = [ { accessor: "productLine", label: "Production Line", width: 180, expandable: true, sortable: true, editable: false, align: "left", type: "string" }, { accessor: "station", label: "Workstation", width: 150, sortable: true, editable: false, align: "left", type: "string" }, { accessor: "machineType", label: "Machine Type", width: 150, sortable: true, editable: false, align: "left", type: "string" }, @@ -122,4 +122,4 @@ export function getManufacturingStatusColors(status: string, theme?: string) { export const manufacturingConfig = { headers: manufacturingHeaders, rows: manufacturingData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/music/MusicDemo.tsx b/packages/examples/solid/src/demos/music/MusicDemo.tsx index f62a91776..e7df29b2b 100644 --- a/packages/examples/solid/src/demos/music/MusicDemo.tsx +++ b/packages/examples/solid/src/demos/music/MusicDemo.tsx @@ -1,10 +1,24 @@ import { SimpleTable } from "@simple-table/solid"; -import type { Theme, SolidColumnDef, TableAPI } from "@simple-table/solid"; +import type { Theme, SolidColumnDef, TableAPI, CellRendererProps } from "@simple-table/solid"; import { musicData, getMusicThemeColors } from "./music.demo-data"; import type { MusicArtist } from "./music.demo-data"; import "@simple-table/solid/styles.css"; import "./music-theme.css"; +function musicText(row: MusicArtist, key: string): string { + const value = row[key]; + if (typeof value === "string") return value; + if (typeof value === "number") return String(value); + return ""; +} + +function musicNumber(row: MusicArtist, key: string): number { + const value = row[key]; + if (typeof value === "number") return value; + if (typeof value === "string") return Number(value) || 0; + return 0; +} + const Tag = (props: { children: any; color?: string; theme?: string }) => { const c = () => getMusicThemeColors(props.theme); const colorMap = () => { @@ -36,13 +50,13 @@ const GrowthMetric = (props: { value: string | number; growthPercent: number; is ); }; -function getMusicHeaders(): SolidColumnDef[] { +function getMusicHeaders(): SolidColumnDef[] { return [ { accessor: "rank", label: "#", width: 60, sortable: true, editable: false, align: "center", type: "number", pinned: "left" }, { accessor: "artistName", label: "Artist", width: 330, sortable: true, editable: false, align: "left", type: "string", pinned: "left", - cellRenderer: ({ row, theme }) => { - const d = row as unknown as MusicArtist; + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row; const c = getMusicThemeColors(theme); let hash = 0; for (let i = 0; i < d.artistName.length; i++) hash = d.artistName.charCodeAt(i) + ((hash << 5) - hash); return ( @@ -51,8 +65,8 @@ function getMusicHeaders(): SolidColumnDef[] {
{d.artistName}
- {d.growthStatus} - {d.mood} + {musicText(d, "growthStatus")} + {musicText(d, "mood")} {d.genre}
@@ -62,55 +76,55 @@ function getMusicHeaders(): SolidColumnDef[] { }, { accessor: "artistType", label: "Identity", width: 280, sortable: false, editable: false, align: "left", type: "string", - cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); return
{d.artistType}, {d.pronouns}
{d.recordLabel}
Lyrics Language: {d.lyricsLanguage}
; }, + cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); return
{d.artistType}, {d.pronouns}
{d.recordLabel}
Lyrics Language: {musicText(d, "lyricsLanguage")}
; }, }, { accessor: "followersGroup", label: "Followers", width: 700, collapsible: true, children: [ - { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); return
{d.followersFormatted}
↑ +{d.followersGrowthFormatted} ({d.followersGrowthPercent.toFixed(2)}%)
; } }, - { accessor: "followers7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return ; } }, - { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return ; } }, - { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return ; } }, + { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); return
{musicText(d, "followersFormatted")}
↑ +{musicText(d, "followersGrowthFormatted")} ({musicNumber(d, "followersGrowthPercent").toFixed(2)}%)
; } }, + { accessor: "followers7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return ; } }, + { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return ; } }, + { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return ; } }, ], }, - { accessor: "popularity", label: "Popularity", width: 180, sortable: true, editable: false, align: "center", type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return
= 0} theme={theme} showSign={false} />
; } }, + { accessor: "popularity", label: "Popularity", width: 180, sortable: true, editable: false, align: "center", type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return
= 0} theme={theme} showSign={false} />
; } }, { accessor: "playlistReachGroup", label: "Playlist Reach", width: 700, collapsible: true, children: [ - { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); const isPos = d.playlistReachChange >= 0; return
{d.playlistReachFormatted}
{isPos ? "↑" : "↓"} {isPos ? "+" : ""}{d.playlistReachChangeFormatted} ({Math.abs(d.playlistReachChangePercent).toFixed(2)}%)
; } }, - { accessor: "playlistReach7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); const change = musicNumber(d, "playlistReachChange"); const isPos = change >= 0; return
{musicText(d, "playlistReachFormatted")}
{isPos ? "↑" : "↓"} {isPos ? "+" : ""}{musicText(d, "playlistReachChangeFormatted")} ({Math.abs(musicNumber(d, "playlistReachChangePercent")).toFixed(2)}%)
; } }, + { accessor: "playlistReach7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const growth = musicNumber(d, "playlistReach7DayGrowth"); return = 0} theme={theme} align="right" />; } }, + { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const growth = musicNumber(d, "playlistReach28DayGrowth"); return = 0} theme={theme} align="right" />; } }, + { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const growth = musicNumber(d, "playlistReach60DayGrowth"); return = 0} theme={theme} align="right" />; } }, ], }, { accessor: "playlistCountGroup", label: "Playlist Count", width: 700, collapsible: true, children: [ - { accessor: "playlistCount", label: "Total Count", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); return
{d.playlistCount.toLocaleString()}
↑ +{d.playlistCountGrowth} ({d.playlistCountGrowthPercent.toFixed(2)}%)
; } }, - { accessor: "playlistCount7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return ; } }, - { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return ; } }, - { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return ; } }, + { accessor: "playlistCount", label: "Total Count", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); return
{musicNumber(d, "playlistCount").toLocaleString()}
↑ +{musicNumber(d, "playlistCountGrowth")} ({musicNumber(d, "playlistCountGrowthPercent").toFixed(2)}%)
; } }, + { accessor: "playlistCount7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return ; } }, + { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return ; } }, + { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; return ; } }, ], }, { accessor: "monthlyListenersGroup", label: "Monthly Listeners", width: 700, collapsible: true, children: [ - { accessor: "monthlyListeners", label: "Total Listeners", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); const isPos = d.monthlyListenersChange >= 0; return
{d.monthlyListenersFormatted}
{isPos ? "↑" : "↓"} {isPos ? "+" : ""}{d.monthlyListenersChangeFormatted} ({Math.abs(d.monthlyListenersChangePercent).toFixed(2)}%)
; } }, - { accessor: "monthlyListeners7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "monthlyListeners", label: "Total Listeners", width: 180, showWhen: "always", sortable: true, editable: false, type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); const change = musicNumber(d, "monthlyListenersChange"); const isPos = change >= 0; return
{musicText(d, "monthlyListenersFormatted")}
{isPos ? "↑" : "↓"} {isPos ? "+" : ""}{musicText(d, "monthlyListenersChangeFormatted")} ({Math.abs(musicNumber(d, "monthlyListenersChangePercent")).toFixed(2)}%)
; } }, + { accessor: "monthlyListeners7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const growth = musicNumber(d, "monthlyListeners7DayGrowth"); return = 0} theme={theme} align="right" />; } }, + { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const growth = musicNumber(d, "monthlyListeners28DayGrowth"); return = 0} theme={theme} align="right" />; } }, + { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const growth = musicNumber(d, "monthlyListeners60DayGrowth"); return = 0} theme={theme} align="right" />; } }, ], }, - { accessor: "conversionRate", label: "Conversion Rate", width: 150, sortable: true, editable: false, align: "right", type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); return {d.conversionRate.toFixed(2)}%; } }, - { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, sortable: true, editable: false, align: "right", type: "number", cellRenderer: ({ row, theme }) => { const d = row as unknown as MusicArtist; const c = getMusicThemeColors(theme); return {d.reachFollowersRatio.toFixed(1)}x; } }, + { accessor: "conversionRate", label: "Conversion Rate", width: 150, sortable: true, editable: false, align: "right", type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); return {musicNumber(d, "conversionRate").toFixed(2)}%; } }, + { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, sortable: true, editable: false, align: "right", type: "number", cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row; const c = getMusicThemeColors(theme); return {musicNumber(d, "reachFollowersRatio").toFixed(1)}x; } }, ]; } export default function MusicDemo(props: { height?: string | number; theme?: Theme }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; return (
- (tableRef = api)} rows={musicData} selectableCells theme={props.theme} /> + row.id} height={props.height ?? "400px"} ref={(api) => (tableRef = api)} rows={musicData} selectableCells theme={props.theme} />
); } diff --git a/packages/examples/solid/src/demos/music/music.demo-data.ts b/packages/examples/solid/src/demos/music/music.demo-data.ts index ced1c41c7..07aa80430 100644 --- a/packages/examples/solid/src/demos/music/music.demo-data.ts +++ b/packages/examples/solid/src/demos/music/music.demo-data.ts @@ -1,202 +1,249 @@ -// Self-contained demo table setup for this example. -import type { SolidColumnDef } from "@simple-table/solid"; +// Self-contained demo data for the Music example. +// +// This produces a wide, real-world-shaped artist analytics dataset (~60 columns) +// that mirrors the kind of multi-platform reporting grid consumers build on top +// of Simple Table. Data is generated at runtime via a deterministic PRNG so the +// rows (and any diffs) stay stable across reloads. export interface MusicArtist { - id: number; + id: string; rank: number; + rankChange: number; artistName: string; artistType: string; pronouns: string; recordLabel: string; - lyricsLanguage: string; genre: string; - mood: string; - growthStatus: string; - followers: number; - followersFormatted: string; - followersGrowthFormatted: string; - followersGrowthPercent: number; - followers7DayGrowth: number; - followers7DayGrowthPercent: number; - followers28DayGrowth: number; - followers28DayGrowthPercent: number; - followers60DayGrowth: number; - followers60DayGrowthPercent: number; - popularity: number; - popularityChangePercent: number; - playlistReach: number; - playlistReachFormatted: string; - playlistReachChange: number; - playlistReachChangeFormatted: string; - playlistReachChangePercent: number; - playlistReach7DayGrowth: number; - playlistReach7DayGrowthPercent: number; - playlistReach28DayGrowth: number; - playlistReach28DayGrowthPercent: number; - playlistReach60DayGrowth: number; - playlistReach60DayGrowthPercent: number; - playlistCount: number; - playlistCountGrowth: number; - playlistCountGrowthPercent: number; - playlistCount7DayGrowth: number; - playlistCount7DayGrowthPercent: number; - playlistCount28DayGrowth: number; - playlistCount28DayGrowthPercent: number; - playlistCount60DayGrowth: number; - playlistCount60DayGrowthPercent: number; - monthlyListeners: number; - monthlyListenersFormatted: string; - monthlyListenersChange: number; - monthlyListenersChangeFormatted: string; - monthlyListenersChangePercent: number; - monthlyListeners7DayGrowth: number; - monthlyListeners7DayGrowthPercent: number; - monthlyListeners28DayGrowth: number; - monthlyListeners28DayGrowthPercent: number; - monthlyListeners60DayGrowth: number; - monthlyListeners60DayGrowthPercent: number; - conversionRate: number; - reachFollowersRatio: number; + country: string; + countryFlag: string; + continent: string; + score: number; + scoreChange: number; + earliestAlbumReleaseDate: string; + latestAlbumReleaseDate: string; + audienceAge: Record; + audienceGender: { f: number; m: number }; + // Single-value rates / ratios / scores + spotifyPopularity: number; + spotifyPopularityChangePercent: number; + spotifyFollowersToListenersRatio: number; + spotifyReachFollowersRatio: number; + youtubeEngagementRate: number; + instagramEngagementRate: number; + tiktokEngagementRate: number; + tiktokEngagementRateChange: number; + // Each platform metric expands to ``, `Formatted`, + // `ChangeFormatted`, `ChangePercent` (see METRICS below). + [key: string]: string | number | Record | undefined; } +// Deterministic PRNG so regenerating produces stable data (and stable diffs). +function createRng(seed: number) { + let state = seed; + return () => { + state = (state * 1664525 + 1013904223) % 4294967296; + return state / 4294967296; + }; +} -const ARTIST_NAMES = ["Luna Nova", "The Midnight Echo", "Astral Frequency", "Crimson Tide", "Echo Chamber", "Neon Pulse", "Celestial Drift", "Violet Storm", "Arctic Monkeys", "Glass Animals", "Tame Impala", "Beach House", "Radiohead", "Portishead", "Massive Attack", "Bonobo", "Four Tet", "Caribou", "Jamie xx", "Burial"]; -const ARTIST_TYPES = ["Solo Artist", "Band", "Duo", "Collective", "DJ/Producer"]; -const PRONOUNS = ["she/her", "he/him", "they/them", "she/they", "he/they"]; -const RECORD_LABELS = ["Universal", "Sony Music", "Warner", "Independent", "Sub Pop", "XL Recordings", "4AD", "Warp Records", "Ninja Tune", "Domino"]; -const LANGUAGES = ["English", "Spanish", "French", "Portuguese", "Korean", "Japanese", "Multilingual"]; -const GENRES = ["Pop", "Rock", "Electronic", "Hip Hop", "R&B", "Indie", "Alternative", "Jazz", "Folk", "Metal"]; -const MOODS = ["Energetic", "Chill", "Dark", "Uplifting", "Melancholic", "Dreamy", "Aggressive", "Romantic"]; -const GROWTH_STATUSES = ["Rising", "Established", "Viral", "Steady", "Declining", "Breakthrough"]; - -function formatNumber(n: number): string { - if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; - if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; - return n.toString(); +function formatCompact(n: number): string { + const abs = Math.abs(n); + if (abs >= 1e9) return `${(n / 1e9).toFixed(2)}B`; + if (abs >= 1e6) return `${(n / 1e6).toFixed(2)}M`; + if (abs >= 1e3) return `${(n / 1e3).toFixed(2)}K`; + return `${Math.round(n)}`; } -export function generateMusicData(count: number = 50): MusicArtist[] { - return Array.from({ length: count }, (_, i) => { - const followers = Math.floor(10000 + Math.random() * 5000000); - const followersGrowth = Math.floor(followers * (0.01 + Math.random() * 0.05)); - const playlistReach = Math.floor(followers * (0.5 + Math.random() * 3)); - const playlistReachChange = Math.floor(playlistReach * ((Math.random() - 0.3) * 0.1)); - const playlistCount = Math.floor(50 + Math.random() * 5000); - const playlistCountGrowth = Math.floor(playlistCount * (0.01 + Math.random() * 0.05)); - const monthlyListeners = Math.floor(followers * (1 + Math.random() * 5)); - const monthlyListenersChange = Math.floor(monthlyListeners * ((Math.random() - 0.3) * 0.08)); - const growthMultiplier = () => 0.01 + Math.random() * 0.03; - const randomGrowth = (base: number) => Math.floor(base * growthMultiplier()); - const randomGrowthPercent = () => Math.round((0.5 + Math.random() * 5) * 100) / 100; - const randomGrowthSigned = (base: number) => { const v = Math.floor(base * ((Math.random() - 0.3) * 0.05)); return v; }; - const randomGrowthSignedPercent = () => { const v = Math.round(((Math.random() - 0.3) * 5) * 100) / 100; return v; }; +const ARTISTS = [ + "Aurora Vale", "Neon Tigers", "Mika Sol", "The Velvet Echo", "Kairo", + "Lumen", "Sasha Frost", "Midnight Cartel", "Indigo Wave", "Rico Mendez", + "Halcyon", "Petra Lune", "The Drifters Club", "Yuki Mori", "Saint Cyprian", + "Nova Rae", "Echo Park Kids", "Dante Cruz", "Marisol", "The Paper Lanterns", + "Bly", "Cassius Gray", "Florae", "Tobias Wren", "Lola Sky", + "Phantom Bloom", "Ren Akiyama", "The Saltwater Saints", "Vivienne Cole", "Ozma", + "Calla Hart", "Brixton Row", "Sable", "The Northern Hum", "Adira", + "Felix Stone", "Marigold", "The Glasshouse", "Nadia Volkov", "Sunset 88", + "Caspian", "Wren & Wolf", "Lyric Monroe", "The Tidewater", "Esme", + "Kingsley Ace", "Pale Horizon", "Octavia", "The Brass Hearts", "Zaire", +]; + +const ARTIST_TYPES = ["Solo Artist", "Band", "Duo", "Collective", "Producer"]; +const PRONOUNS = ["She/Her", "He/Him", "They/Them"]; +const LABELS = [ + "UMG (Republic Records)", "Sony Music", "Warner Records", "Atlantic", "Columbia", + "Capitol", "Interscope", "Independent", "Def Jam", "RCA", +]; +const GENRES = ["Pop", "Hip-Hop", "Indie", "R&B", "Electronic", "Rock", "Latin", "K-Pop"]; +const REGIONS = [ + { country: "United States", flag: "🇺🇸", continent: "North America" }, + { country: "United Kingdom", flag: "🇬🇧", continent: "Europe" }, + { country: "South Korea", flag: "🇰🇷", continent: "Asia" }, + { country: "Brazil", flag: "🇧🇷", continent: "South America" }, + { country: "Canada", flag: "🇨🇦", continent: "North America" }, + { country: "Germany", flag: "🇩🇪", continent: "Europe" }, + { country: "Japan", flag: "🇯🇵", continent: "Asia" }, + { country: "France", flag: "🇫🇷", continent: "Europe" }, + { country: "Mexico", flag: "🇲🇽", continent: "North America" }, + { country: "Australia", flag: "🇦🇺", continent: "Oceania" }, +]; + +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +// Each metric describes a "stat" column: a big total + period-over-period change. +// `base` controls magnitude. +const METRICS: [string, number][] = [ + // Followers / fans across platforms + ["spotifyFollowers", 90e6], + ["youtubeSubscribers", 45e6], + ["instagramFollowers", 120e6], + ["tiktokFollowers", 28e6], + ["deezerFans", 9e6], + ["facebookFollowers", 60e6], + ["twitterFollowers", 55e6], + ["soundcloudFollowers", 900e3], + ["songkickFans", 4e6], + ["snapchatSubscribers", 1.2e6], + ["blueskyFollowers", 8e6], + ["twitchFollowers", 600e3], + ["lineMusicArtistLikes", 500e3], + ["melonArtistFans", 80e3], + // Spotify performance + ["spotifyMonthlyListeners", 70e6], + // YouTube performance + ["youtubeChannelViews", 30e9], + ["youtubeDailyVideoViews", 12e6], + ["youtubeMonthlyListeners", 250e6], + // Engagement / plays + ["tiktokLikes", 200e6], + ["wikipediaViews", 18e3], + ["pandoraLifetimeStreams", 10e9], + ["pandoraListeners28Day", 4e6], + ["pandoraLifetimeStationsAdded", 40e6], + ["facebookTalks", 40e3], + ["lineMusicLikes", 900e3], + ["lineMusicMvPlays", 28e3], + ["lineMusicPlays", 120e6], + ["melonVideoLikes", 7e3], + ["melonVideoViews", 900e3], + ["tiktokTopVideoViews", 200e9], + ["soundcloudPlays", 70e6], + ["boomplayFavorites", 300e3], + ["boomplayPlays", 5e6], + // Playlists + ["spotifyPlaylistReach", 900e6], + ["spotifyPlaylistCount", 1.5e6], + ["youtubePlaylistCount", 1e3], + ["deezerPlaylistCount", 1e3], + ["itunesPlaylistCount", 2.5e3], + ["amazonPlaylistCount", 1.5e3], + ["deezerPlaylistReach", 55e6], + // Airwaves / discovery + ["tiktokTrackPosts", 100e6], + ["airplayStreams", 8e6], + ["siriusxmStreams", 140e3], + ["geniusPageviews", 250e6], + ["shazamCount", 240e6], + ["twitterTweetCount", 800], +]; + +export function generateMusicData(count: number = 2000): MusicArtist[] { + const rand = createRng(1337); + const between = (min: number, max: number) => min + rand() * (max - min); + const intBetween = (min: number, max: number) => Math.round(between(min, max)); + const pick = (arr: T[]): T => arr[Math.floor(rand() * arr.length)]; + const formatDate = (year: number) => `${pick(MONTHS)} ${intBetween(1, 28)}, ${year}`; + + const buildMetric = (key: string, base: number): Record => { + const value = Math.round(base * between(0.35, 1.4)); + const changePercent = +between(-12, 18).toFixed(2); + const change = Math.round((value * changePercent) / 100); return { - id: i + 1, - rank: i + 1, - artistName: ARTIST_NAMES[i % ARTIST_NAMES.length], - artistType: ARTIST_TYPES[i % ARTIST_TYPES.length], - pronouns: PRONOUNS[i % PRONOUNS.length], - recordLabel: RECORD_LABELS[i % RECORD_LABELS.length], - lyricsLanguage: LANGUAGES[i % LANGUAGES.length], - genre: GENRES[i % GENRES.length], - mood: MOODS[i % MOODS.length], - growthStatus: GROWTH_STATUSES[i % GROWTH_STATUSES.length], - followers, - followersFormatted: formatNumber(followers), - followersGrowthFormatted: formatNumber(followersGrowth), - followersGrowthPercent: Math.round((followersGrowth / followers) * 10000) / 100, - followers7DayGrowth: randomGrowth(followers), - followers7DayGrowthPercent: randomGrowthPercent(), - followers28DayGrowth: randomGrowth(followers) * 3, - followers28DayGrowthPercent: randomGrowthPercent() * 2, - followers60DayGrowth: randomGrowth(followers) * 6, - followers60DayGrowthPercent: randomGrowthPercent() * 3, - popularity: Math.floor(30 + Math.random() * 70), - popularityChangePercent: Math.round(((Math.random() - 0.4) * 10) * 100) / 100, - playlistReach, - playlistReachFormatted: formatNumber(playlistReach), - playlistReachChange, - playlistReachChangeFormatted: formatNumber(Math.abs(playlistReachChange)), - playlistReachChangePercent: Math.round((playlistReachChange / playlistReach) * 10000) / 100, - playlistReach7DayGrowth: randomGrowthSigned(playlistReach), - playlistReach7DayGrowthPercent: randomGrowthSignedPercent(), - playlistReach28DayGrowth: randomGrowthSigned(playlistReach) * 3, - playlistReach28DayGrowthPercent: randomGrowthSignedPercent() * 2, - playlistReach60DayGrowth: randomGrowthSigned(playlistReach) * 5, - playlistReach60DayGrowthPercent: randomGrowthSignedPercent() * 3, - playlistCount, - playlistCountGrowth, - playlistCountGrowthPercent: Math.round((playlistCountGrowth / playlistCount) * 10000) / 100, - playlistCount7DayGrowth: randomGrowth(playlistCount), - playlistCount7DayGrowthPercent: randomGrowthPercent(), - playlistCount28DayGrowth: randomGrowth(playlistCount) * 3, - playlistCount28DayGrowthPercent: randomGrowthPercent() * 2, - playlistCount60DayGrowth: randomGrowth(playlistCount) * 5, - playlistCount60DayGrowthPercent: randomGrowthPercent() * 3, - monthlyListeners, - monthlyListenersFormatted: formatNumber(monthlyListeners), - monthlyListenersChange, - monthlyListenersChangeFormatted: formatNumber(Math.abs(monthlyListenersChange)), - monthlyListenersChangePercent: Math.round((monthlyListenersChange / monthlyListeners) * 10000) / 100, - monthlyListeners7DayGrowth: randomGrowthSigned(monthlyListeners), - monthlyListeners7DayGrowthPercent: randomGrowthSignedPercent(), - monthlyListeners28DayGrowth: randomGrowthSigned(monthlyListeners) * 3, - monthlyListeners28DayGrowthPercent: randomGrowthSignedPercent() * 2, - monthlyListeners60DayGrowth: randomGrowthSigned(monthlyListeners) * 5, - monthlyListeners60DayGrowthPercent: randomGrowthSignedPercent() * 3, - conversionRate: Math.round((1 + Math.random() * 15) * 100) / 100, - reachFollowersRatio: Math.round((playlistReach / followers) * 10) / 10, + [key]: value, + [`${key}Formatted`]: formatCompact(value), + [`${key}ChangeFormatted`]: formatCompact(change), + [`${key}ChangePercent`]: changePercent, + }; + }; + + const buildRates = () => ({ + spotifyPopularity: intBetween(60, 99), + spotifyPopularityChangePercent: +between(-3, 3).toFixed(2), + spotifyFollowersToListenersRatio: +between(40, 220).toFixed(1), + spotifyReachFollowersRatio: +between(1.2, 9).toFixed(2), + youtubeEngagementRate: +between(0.05, 0.6).toFixed(2), + instagramEngagementRate: +between(0.4, 3.2).toFixed(2), + tiktokEngagementRate: +between(1.5, 8).toFixed(2), + tiktokEngagementRateChange: +between(-0.6, 0.6).toFixed(2), + }); + + const buildAudienceAge = (): Record => { + // weighted toward 18-34, like a typical pop audience + const raw: Record = { + "13-17": between(4, 14), + "18-24": between(25, 45), + "25-34": between(20, 40), + "35-44": between(6, 18), + "45-59": between(3, 12), + "60+": between(1, 6), }; + const total = Object.values(raw).reduce((a, b) => a + b, 0); + const age: Record = {}; + for (const k of Object.keys(raw)) age[k] = Math.round((raw[k] / total) * 100); + return age; + }; + + const buildAudienceGender = () => { + const f = intBetween(35, 70); + return { f, m: 100 - f }; + }; + + const rows: MusicArtist[] = Array.from({ length: count }, (_, i) => { + // Cycle through the base name pool, adding a suffix once we wrap around so + // every row stays unique even past the 50 seed names. + const baseName = ARTISTS[i % ARTISTS.length]; + const cycle = Math.floor(i / ARTISTS.length); + const artistName = cycle === 0 ? baseName : `${baseName} ${cycle + 1}`; + + const region = pick(REGIONS); + const earliestYear = intBetween(2003, 2018); + const row = { + id: `artist-${i + 1}`, + rank: i + 1, + rankChange: intBetween(-6, 6), + artistName, + artistType: pick(ARTIST_TYPES), + pronouns: pick(PRONOUNS), + recordLabel: pick(LABELS), + genre: pick(GENRES), + country: region.country, + countryFlag: region.flag, + continent: region.continent, + score: +between(45, 99.9).toFixed(1), + scoreChange: +between(-2.5, 2.5).toFixed(1), + earliestAlbumReleaseDate: formatDate(earliestYear), + latestAlbumReleaseDate: formatDate(intBetween(earliestYear + 1, 2026)), + audienceAge: buildAudienceAge(), + audienceGender: buildAudienceGender(), + ...buildRates(), + } as MusicArtist; + + for (const [key, base] of METRICS) { + Object.assign(row, buildMetric(key, base)); + } + + return row; + }); + + // Sort by score desc and re-rank so #1 is the top performer. + rows.sort((a, b) => b.score - a.score); + rows.forEach((row, idx) => { + row.rank = idx + 1; }); + + return rows; } -export const musicData = generateMusicData(50); - -export const musicHeaders: SolidColumnDef[] = [ - { accessor: "rank", label: "#", width: 60, sortable: true, editable: false, align: "center", type: "number", pinned: "left" }, - { accessor: "artistName", label: "Artist", width: 330, sortable: true, editable: false, align: "left", type: "string", pinned: "left" }, - { accessor: "artistType", label: "Identity", width: 280, sortable: false, editable: false, align: "left", type: "string" }, - { - accessor: "followersGroup", label: "Followers", width: 700, collapsible: true, - children: [ - { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", sortable: true, editable: false, type: "number" }, - { accessor: "followers7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - ], - }, - { accessor: "popularity", label: "Popularity", width: 180, sortable: true, editable: false, align: "center", type: "number" }, - { - accessor: "playlistReachGroup", label: "Playlist Reach", width: 700, collapsible: true, - children: [ - { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", sortable: true, editable: false, type: "number" }, - { accessor: "playlistReach7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - ], - }, - { - accessor: "playlistCountGroup", label: "Playlist Count", width: 700, collapsible: true, - children: [ - { accessor: "playlistCount", label: "Total Count", width: 180, showWhen: "always", sortable: true, editable: false, type: "number" }, - { accessor: "playlistCount7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - ], - }, - { - accessor: "monthlyListenersGroup", label: "Monthly Listeners", width: 700, collapsible: true, - children: [ - { accessor: "monthlyListeners", label: "Total Listeners", width: 180, showWhen: "always", sortable: true, editable: false, type: "number" }, - { accessor: "monthlyListeners7DayGrowth", label: "7-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, sortable: true, editable: false, align: "right", type: "number", showWhen: "parentExpanded" }, - ], - }, - { accessor: "conversionRate", label: "Conversion Rate", width: 150, sortable: true, editable: false, align: "right", type: "number" }, - { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, sortable: true, editable: false, align: "right", type: "number" }, -]; +export const musicData = generateMusicData(2000); export const MUSIC_THEME_COLORS: Record> = { "modern-light": { gray: "#374151", grayMuted: "#9ca3af", success: "#16a34a", successBg: "#f0fdf4", error: "#dc2626", errorBg: "#fef2f2", primary: "#2563eb", primaryBg: "#eff6ff", warning: "#d97706", warningBg: "#fffbeb", tagBorder: "#e5e7eb", tagBg: "#ffffff", tagText: "#000000", highScore: "#16a34a", mediumScore: "#2563eb", lowScore: "#f59e0b", veryLowScore: "#ef4444" }, @@ -208,8 +255,3 @@ export const MUSIC_THEME_COLORS: Record> = { export function getMusicThemeColors(theme?: string): Record { return MUSIC_THEME_COLORS[theme || "modern-light"] || MUSIC_THEME_COLORS["modern-light"]; } - -export const musicConfig = { - headers: musicHeaders, - rows: musicData, -} as const; diff --git a/packages/examples/solid/src/demos/nested-headers/NestedHeadersDemo.tsx b/packages/examples/solid/src/demos/nested-headers/NestedHeadersDemo.tsx index 86ae40356..916bb4a44 100644 --- a/packages/examples/solid/src/demos/nested-headers/NestedHeadersDemo.tsx +++ b/packages/examples/solid/src/demos/nested-headers/NestedHeadersDemo.tsx @@ -6,6 +6,7 @@ export default function NestedHeadersDemo(props: { height?: string | number; the return ( row.id} rows={nestedHeadersConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/nested-headers/nested-headers.demo-data.ts b/packages/examples/solid/src/demos/nested-headers/nested-headers.demo-data.ts index 6db8a5761..5b49b9540 100644 --- a/packages/examples/solid/src/demos/nested-headers/nested-headers.demo-data.ts +++ b/packages/examples/solid/src/demos/nested-headers/nested-headers.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface StudentScores { + id: number; + name: string; + mathScore: number; + scienceScore: number; + historyScore: number; + grade: string; +} -export const nestedHeadersHeaders: SolidColumnDef[] = [ +export const nestedHeadersHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, sortable: true, type: "number" }, { accessor: "name", label: "Name", width: "1fr", sortable: true, type: "string" }, { @@ -20,7 +28,7 @@ export const nestedHeadersHeaders: SolidColumnDef[] = [ { accessor: "grade", label: "Overall Grade", width: 120, sortable: true, type: "string", align: "center" }, ]; -export const nestedHeadersData = [ +export const nestedHeadersData: StudentScores[] = [ { id: 1, name: "Aria Chen", mathScore: 94, scienceScore: 89, historyScore: 92, grade: "A" }, { id: 2, name: "Kai Rodriguez", mathScore: 81, scienceScore: 85, historyScore: 78, grade: "B" }, { id: 3, name: "Luna Nakamura", mathScore: 96, scienceScore: 94, historyScore: 93, grade: "A" }, @@ -38,4 +46,4 @@ export const nestedHeadersConfig = { headers: nestedHeadersHeaders, rows: nestedHeadersData, tableProps: { columnResizing: true }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/nested-tables/NestedTablesDemo.tsx b/packages/examples/solid/src/demos/nested-tables/NestedTablesDemo.tsx index 0b2d9d575..7f2749a6c 100644 --- a/packages/examples/solid/src/demos/nested-tables/NestedTablesDemo.tsx +++ b/packages/examples/solid/src/demos/nested-tables/NestedTablesDemo.tsx @@ -1,5 +1,6 @@ import { createMemo } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme } from "@simple-table/solid"; import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data"; import "@simple-table/solid/styles.css"; @@ -12,7 +13,7 @@ export default function NestedTablesDemo(props: { height?: string | number; them columns={nestedTablesConfig.headers} rows={sampleData()} rowGrouping={nestedTablesConfig.tableProps.rowGrouping} - getRowId={nestedTablesConfig.tableProps.getRowId} + getRowId={({ row }) => row.id} expandAll={nestedTablesConfig.tableProps.expandAll} columnResizing={nestedTablesConfig.tableProps.columnResizing} height={props.height ?? "500px"} diff --git a/packages/examples/solid/src/demos/nested-tables/nested-tables.demo-data.ts b/packages/examples/solid/src/demos/nested-tables/nested-tables.demo-data.ts index 1aeb9a94c..952b329b1 100644 --- a/packages/examples/solid/src/demos/nested-tables/nested-tables.demo-data.ts +++ b/packages/examples/solid/src/demos/nested-tables/nested-tables.demo-data.ts @@ -1,6 +1,28 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface NestedDivision { + divisionId: string; + divisionName: string; + revenue: string; + profitMargin: string; + headcount: number; + location: string; +} + +export interface NestedCompany { + id: number; + companyName: string; + industry: string; + founded: number; + headquarters: string; + stockSymbol: string; + marketCap: string; + ceo: string; + revenue: string; + employees: number; + divisions: NestedDivision[]; +} const industries = ["Technology", "Financial Services", "Healthcare", "Manufacturing", "Retail", "Energy", "Telecommunications", "Pharmaceuticals", "Automotive", "Aerospace", "Biotechnology", "E-commerce"]; const cities = ["San Francisco, CA", "New York, NY", "Boston, MA", "Seattle, WA", "Austin, TX", "Chicago, IL", "Los Angeles, CA", "Denver, CO", "Miami, FL", "Atlanta, GA", "Portland, OR", "Dallas, TX"]; @@ -13,7 +35,7 @@ const suffixes = ["Global", "Inc", "Solutions", "Systems", "Ventures", "Group", const randomElement = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; const randomInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min + 1)) + min; -const generateDivision = (divisionIndex: number, companyIndex: number) => ({ +const generateDivision = (divisionIndex: number, companyIndex: number): NestedDivision => ({ divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`, divisionName: randomElement(divisionTypes), revenue: `$${randomInt(5, 25)}B`, @@ -22,7 +44,7 @@ const generateDivision = (divisionIndex: number, companyIndex: number) => ({ location: randomElement(cities), }); -const generateCompany = (companyIndex: number) => { +const generateCompany = (companyIndex: number): NestedCompany => { const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex)); return { id: companyIndex + 1, @@ -39,9 +61,10 @@ const generateCompany = (companyIndex: number) => { }; }; -export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i)); +export const generateNestedTablesData = (count: number = 25): NestedCompany[] => + Array.from({ length: count }, (_, i) => generateCompany(i)); -export const nestedTablesDivisionHeaders: SolidColumnDef[] = [ +export const nestedTablesDivisionHeaders: SolidColumnDef[] = [ { accessor: "divisionId", label: "Division ID", width: 120 }, { accessor: "revenue", label: "Revenue", width: 120 }, { accessor: "profitMargin", label: "Profit Margin", width: 130 }, @@ -49,13 +72,15 @@ export const nestedTablesDivisionHeaders: SolidColumnDef[] = [ { accessor: "location", label: "Location", width: "1fr" }, ]; -export const nestedTablesHeaders: SolidColumnDef[] = [ +export const nestedTablesHeaders: SolidColumnDef[] = [ { accessor: "companyName", label: "Company", width: 200, expandable: true, - nestedTable: { columns: nestedTablesDivisionHeaders }, + nestedTable: { + columns: nestedTablesDivisionHeaders, + }, }, { accessor: "stockSymbol", label: "Symbol", width: 100 }, { accessor: "marketCap", label: "Market Cap", width: 120 }, @@ -66,10 +91,9 @@ export const nestedTablesHeaders: SolidColumnDef[] = [ export const nestedTablesConfig = { headers: nestedTablesHeaders, tableProps: { - rowGrouping: ["divisions"] as string[], - getRowId: ({ row }: { row: Record }) => row.id as string, + rowGrouping: ["divisions"], expandAll: false, columnResizing: true, autoExpandColumns: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/pagination/PaginationDemo.tsx b/packages/examples/solid/src/demos/pagination/PaginationDemo.tsx index 4a443de9e..53a26da93 100644 --- a/packages/examples/solid/src/demos/pagination/PaginationDemo.tsx +++ b/packages/examples/solid/src/demos/pagination/PaginationDemo.tsx @@ -1,13 +1,15 @@ import { createSignal } from "solid-js"; import {SimpleTable} from "@simple-table/solid";import type { Theme } from "@simple-table/solid"; -import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "./pagination.demo-data"; +import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE, type HotelStaff } from "./pagination.demo-data"; import "@simple-table/solid/styles.css"; export default function PaginationDemo(props: { height?: string | number; theme?: Theme; }) { - const [rows, setRows] = createSignal(paginationData.slice(0, PAGINATION_ROWS_PER_PAGE)); + const [rows, setRows] = createSignal( + paginationData.slice(0, PAGINATION_ROWS_PER_PAGE), + ); const [isLoading, setIsLoading] = createSignal(false); const onNextPage = async (pageIndex: number) => { @@ -34,6 +36,7 @@ export default function PaginationDemo(props: { height={props.height ?? "auto"} isLoading={isLoading()} onNextPage={onNextPage} + getRowId={({ row }) => row.id} rows={rows()} rowsPerPage={PAGINATION_ROWS_PER_PAGE} enablePagination={true} diff --git a/packages/examples/solid/src/demos/pagination/pagination.demo-data.ts b/packages/examples/solid/src/demos/pagination/pagination.demo-data.ts index 6c15d90fb..759f559e7 100644 --- a/packages/examples/solid/src/demos/pagination/pagination.demo-data.ts +++ b/packages/examples/solid/src/demos/pagination/pagination.demo-data.ts @@ -1,10 +1,18 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface HotelStaff { + id: number; + name: string; + email: string; + role: string; + department: string; + status: string; +} export const PAGINATION_ROWS_PER_PAGE = 9; -export const paginationHeaders: SolidColumnDef[] = [ +export const paginationHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: "1fr", minWidth: 100, type: "string" }, { accessor: "email", label: "Email", width: 200, type: "string" }, @@ -13,7 +21,7 @@ export const paginationHeaders: SolidColumnDef[] = [ { accessor: "status", label: "Status", width: 110, type: "string" }, ]; -export const paginationData: Row[] = [ +export const paginationData: HotelStaff[] = [ { id: 1, name: "Miguel Santos", email: "miguel.santos@grandhotel.com", role: "Guest Relations Manager", department: "Front Office", status: "Active" }, { id: 2, name: "Carmen Delacroix", email: "carmen.d@grandhotel.com", role: "Head Concierge", department: "Concierge", status: "Active" }, { id: 3, name: "Dimitri Petrov", email: "dimitri.p@grandhotel.com", role: "Executive Chef", department: "Culinary", status: "Active" }, @@ -50,4 +58,4 @@ export const paginationConfig = { rowsPerPage: PAGINATION_ROWS_PER_PAGE, enablePagination: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/pivot/PivotDemo.tsx b/packages/examples/solid/src/demos/pivot/PivotDemo.tsx index fa5dc138c..a709a0eda 100644 --- a/packages/examples/solid/src/demos/pivot/PivotDemo.tsx +++ b/packages/examples/solid/src/demos/pivot/PivotDemo.tsx @@ -43,6 +43,7 @@ export default function PivotDemo(props: { row.id} rows={pivotDemoConfig.rows} pivot={active().pivot} columnResizing diff --git a/packages/examples/solid/src/demos/pivot/pivot.demo-data.ts b/packages/examples/solid/src/demos/pivot/pivot.demo-data.ts index 22330ab2b..027ee5269 100644 --- a/packages/examples/solid/src/demos/pivot/pivot.demo-data.ts +++ b/packages/examples/solid/src/demos/pivot/pivot.demo-data.ts @@ -1,6 +1,20 @@ -import type { PivotConfig, SolidColumnDef, Row } from "@simple-table/solid"; +import type { PivotConfig, SolidColumnDef } from "@simple-table/solid"; -export const pivotHeaders: SolidColumnDef[] = [ +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const pivotHeaders: SolidColumnDef[] = [ { accessor: "region", label: "Region", width: 110, type: "string" }, { accessor: "country", label: "Country", width: 100, type: "string" }, { accessor: "category", label: "Category", width: 110, type: "string" }, @@ -35,33 +49,32 @@ export const pivotHeaders: SolidColumnDef[] = [ }, ]; -const REGIONS = ["West", "East", "North", "South"] as const; -const COUNTRIES: Record<(typeof REGIONS)[number], string[]> = { +const COUNTRIES = { West: ["USA", "Canada"], East: ["USA", "UK"], North: ["Canada", "Sweden"], South: ["Brazil", "Australia"], }; -const CATEGORIES = ["Hardware", "Software"] as const; -const PRODUCTS: Record<(typeof CATEGORIES)[number], string[]> = { +const PRODUCTS = { Hardware: ["Widget", "Gadget", "Sensor"], Software: ["License", "Subscription"], }; -const CHANNELS = ["Direct", "Partner", "Online"] as const; -const YEARS = [2024, 2025] as const; -const QUARTERS = ["Q1", "Q2", "Q3", "Q4"] as const; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; /** Sparse multi-dimension fact cube (~150–250 rows). */ -export function generatePivotRows(): Row[] { - const rows: Row[] = []; +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; let id = 1; - for (const region of REGIONS) { - for (const country of COUNTRIES[region]) { - for (const category of CATEGORIES) { - for (const product of PRODUCTS[category]) { + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { for (const channel of CHANNELS) { for (const year of YEARS) { for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { id++; continue; @@ -91,7 +104,7 @@ export function generatePivotRows(): Row[] { return rows; } -export const pivotRows: Row[] = generatePivotRows(); +export const pivotRows: PivotFact[] = generatePivotRows(); export type PivotPreset = { id: string; diff --git a/packages/examples/solid/src/demos/programmatic-control/ProgrammaticControlDemo.tsx b/packages/examples/solid/src/demos/programmatic-control/ProgrammaticControlDemo.tsx index dd9dd7b7f..29f006074 100644 --- a/packages/examples/solid/src/demos/programmatic-control/ProgrammaticControlDemo.tsx +++ b/packages/examples/solid/src/demos/programmatic-control/ProgrammaticControlDemo.tsx @@ -1,8 +1,10 @@ import { createSignal } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, TableAPI, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, TableAPI, SolidColumnDef, CellRendererProps } from "@simple-table/solid"; import { programmaticControlConfig, PROGRAMMATIC_CONTROL_STATUS_COLORS, + type ProgrammaticControlProduct, } from "./programmatic-control.demo-data"; import "@simple-table/solid/styles.css"; @@ -10,14 +12,14 @@ export default function ProgrammaticControlDemo(props: { height?: string | number; theme?: Theme; }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; const [statusMessage, setStatusMessage] = createSignal("No status message"); - const headers: SolidColumnDef[] = programmaticControlConfig.headers.map((h) => { + const headers: SolidColumnDef[] = programmaticControlConfig.headers.map((h) => { if (h.accessor === "status") { return { ...h, - cellRenderer: (cr: CellRendererProps) => { + cellRenderer: (cr: CellRendererProps) => { const s = String(cr.row.status); const colors = PROGRAMMATIC_CONTROL_STATUS_COLORS[s] ?? { bg: "#f3f4f6", @@ -70,8 +72,9 @@ export default function ProgrammaticControlDemo(props: { const sortState = tableRef.getSortState(); const filterState = tableRef.getFilterState(); const totalValue = allRows.reduce((sum, r) => { - const data = r.row as { price?: number; stock?: number }; - return sum + (Number(data.price) || 0) * (Number(data.stock) || 0); + const price = typeof r.row.price === "number" ? r.row.price : 0; + const stock = typeof r.row.stock === "number" ? r.row.stock : 0; + return sum + price * stock; }, 0); const sortInfo = sortState ? `${sortState.key.label} (${sortState.direction})` : "None"; alert( @@ -105,6 +108,7 @@ export default function ProgrammaticControlDemo(props: { (tableRef = api)} columns={headers} + getRowId={({ row }) => row.id} rows={programmaticControlConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/programmatic-control/programmatic-control.demo-data.ts b/packages/examples/solid/src/demos/programmatic-control/programmatic-control.demo-data.ts index ef58748ed..320218f18 100644 --- a/packages/examples/solid/src/demos/programmatic-control/programmatic-control.demo-data.ts +++ b/packages/examples/solid/src/demos/programmatic-control/programmatic-control.demo-data.ts @@ -1,6 +1,14 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface ProgrammaticControlProduct { + id: number; + name: string; + category: string; + price: number; + stock: number; + status: string; +} export const STATUS_COLORS: Record = { Available: { bg: "#dcfce7", color: "#166534" }, @@ -8,7 +16,7 @@ export const STATUS_COLORS: Record = { "Out of Stock": { bg: "#fee2e2", color: "#991b1b" }, }; -export const programmaticControlData = [ +export const programmaticControlData: ProgrammaticControlProduct[] = [ { id: 1, name: "Wireless Keyboard", category: "Electronics", price: 49.99, stock: 145, status: "Available" }, { id: 2, name: "Ergonomic Mouse", category: "Electronics", price: 29.99, stock: 12, status: "Low Stock" }, { id: 3, name: "USB-C Hub", category: "Electronics", price: 39.99, stock: 234, status: "Available" }, @@ -23,7 +31,7 @@ export const programmaticControlData = [ { id: 12, name: "Desk Lamp LED", category: "Appliances", price: 44.99, stock: 0, status: "Out of Stock" }, ]; -export const programmaticControlHeaders: SolidColumnDef[] = [ +export const programmaticControlHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 70, type: "number", sortable: true, filterable: true }, { accessor: "name", label: "Product Name", width: "1fr", minWidth: 150, type: "string", sortable: true, filterable: true }, { @@ -51,6 +59,6 @@ export const programmaticControlHeaders: SolidColumnDef[] = [ export const programmaticControlConfig = { headers: programmaticControlHeaders, rows: programmaticControlData, -} as const; +}; export { STATUS_COLORS as PROGRAMMATIC_CONTROL_STATUS_COLORS }; diff --git a/packages/examples/solid/src/demos/quick-filter/QuickFilterDemo.tsx b/packages/examples/solid/src/demos/quick-filter/QuickFilterDemo.tsx index d1b657f05..48b2a41f6 100644 --- a/packages/examples/solid/src/demos/quick-filter/QuickFilterDemo.tsx +++ b/packages/examples/solid/src/demos/quick-filter/QuickFilterDemo.tsx @@ -59,6 +59,7 @@ export default function QuickFilterDemo(props: { height?: string | number; theme row.id} rows={quickFilterConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/quick-filter/quick-filter.demo-data.ts b/packages/examples/solid/src/demos/quick-filter/quick-filter.demo-data.ts index 8693a754b..414602cc5 100644 --- a/packages/examples/solid/src/demos/quick-filter/quick-filter.demo-data.ts +++ b/packages/examples/solid/src/demos/quick-filter/quick-filter.demo-data.ts @@ -1,8 +1,17 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface QuickFilterEmployee { + id: number; + name: string; + age: number; + department: string; + salary: number; + status: string; + location: string; +} -export const quickFilterHeaders: SolidColumnDef[] = [ +export const quickFilterHeaders: SolidColumnDef[] = [ { accessor: "name", label: "Employee Name", width: 180, type: "string" }, { accessor: "age", label: "Age", width: 80, type: "number" }, { accessor: "department", label: "Department", width: 140, type: "string" }, @@ -11,7 +20,7 @@ export const quickFilterHeaders: SolidColumnDef[] = [ { accessor: "location", label: "Location", width: 140, type: "string" }, ]; -export const quickFilterData = [ +export const quickFilterData: QuickFilterEmployee[] = [ { id: 1, name: "Alice Johnson", age: 28, department: "Engineering", salary: 95000, status: "Active", location: "New York" }, { id: 2, name: "Bob Smith", age: 35, department: "Sales", salary: 75000, status: "Active", location: "Los Angeles" }, { id: 3, name: "Charlie Davis", age: 42, department: "Engineering", salary: 110000, status: "Active", location: "San Francisco" }, @@ -29,5 +38,4 @@ export const quickFilterData = [ export const quickFilterConfig = { headers: quickFilterHeaders, rows: quickFilterData, - tableProps: { quickFilter: { text: "", mode: "simple" as const, caseSensitive: false } }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/quick-start/QuickStartDemo.tsx b/packages/examples/solid/src/demos/quick-start/QuickStartDemo.tsx index 6d154f764..c4a5ab4d1 100644 --- a/packages/examples/solid/src/demos/quick-start/QuickStartDemo.tsx +++ b/packages/examples/solid/src/demos/quick-start/QuickStartDemo.tsx @@ -9,6 +9,7 @@ export default function QuickStartDemo(props: { return ( row.id} rows={quickStartConfig.rows} height={props.height ?? "300px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/quick-start/quick-start.demo-data.ts b/packages/examples/solid/src/demos/quick-start/quick-start.demo-data.ts index c3ad2e871..cad33d174 100644 --- a/packages/examples/solid/src/demos/quick-start/quick-start.demo-data.ts +++ b/packages/examples/solid/src/demos/quick-start/quick-start.demo-data.ts @@ -1,9 +1,16 @@ // Self-contained demo table setup for this example. -import type { Row } from "@simple-table/solid"; import type { SolidColumnDef } from "@simple-table/solid"; +export interface QuickStartEmployee { + id: number; + name: string; + age: number; + role: string; + department: string; + startDate: string; +} -export const QUICK_START_DATA: Row[] = [ +export const QUICK_START_DATA: QuickStartEmployee[] = [ { id: 1, name: "Marcus Rodriguez", @@ -103,7 +110,7 @@ export const QUICK_START_DATA: Row[] = [ ]; -export const quickStartHeaders: SolidColumnDef[] = [ +export const quickStartHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, sortable: true, type: "number" }, { accessor: "name", @@ -127,4 +134,4 @@ export const quickStartConfig = { selectableCells: true, customTheme: { rowHeight: 32 }, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/row-grouping/RowGroupingDemo.tsx b/packages/examples/solid/src/demos/row-grouping/RowGroupingDemo.tsx index d2f7fd09c..c03e9dab8 100644 --- a/packages/examples/solid/src/demos/row-grouping/RowGroupingDemo.tsx +++ b/packages/examples/solid/src/demos/row-grouping/RowGroupingDemo.tsx @@ -1,5 +1,6 @@ -import {SimpleTable} from "@simple-table/solid";import type { Theme, TableAPI } from "@simple-table/solid"; -import { rowGroupingConfig } from "./row-grouping.demo-data"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, TableAPI } from "@simple-table/solid"; +import { rowGroupingConfig, type OrgUnit } from "./row-grouping.demo-data"; import "@simple-table/solid/styles.css"; const btnStyle = (color: string) => ({ @@ -14,7 +15,7 @@ const btnStyle = (color: string) => ({ }); export default function RowGroupingDemo(props: { height?: string | number; theme?: Theme }) { - let tableRef: TableAPI | undefined; + let tableRef: TableAPI | undefined; return (
@@ -31,8 +32,8 @@ export default function RowGroupingDemo(props: { height?: string | number; theme columns={rowGroupingConfig.headers} rows={rowGroupingConfig.rows} rowGrouping={rowGroupingConfig.tableProps.rowGrouping} - enableStickyParents={true} - getRowId={rowGroupingConfig.tableProps.getRowId} + enableStickyParents={rowGroupingConfig.tableProps.enableStickyParents} + getRowId={({ row }) => row.id} columnResizing height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/row-grouping/row-grouping.demo-data.ts b/packages/examples/solid/src/demos/row-grouping/row-grouping.demo-data.ts index 0a38da30a..c9d1e4209 100644 --- a/packages/examples/solid/src/demos/row-grouping/row-grouping.demo-data.ts +++ b/packages/examples/solid/src/demos/row-grouping/row-grouping.demo-data.ts @@ -1,8 +1,22 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +/** Shared metrics at every grouping depth; nesting uses rowGrouping keys. */ +export interface OrgUnit { + id: string; + organization: string; + employees: number; + budget: string; + performance: string; + location: string; + growthRate: string; + status: string; + established: string; + divisions?: OrgUnit[]; + departments?: OrgUnit[]; +} -export const rowGroupingHeaders: SolidColumnDef[] = [ +export const rowGroupingHeaders: SolidColumnDef[] = [ { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" }, { accessor: "employees", label: "Employees", width: 100, type: "number" }, { accessor: "budget", label: "Annual Budget", width: 140, type: "string" }, @@ -13,7 +27,7 @@ export const rowGroupingHeaders: SolidColumnDef[] = [ { accessor: "established", label: "Est. Date", width: 110, type: "date" }, ]; -export const rowGroupingData = [ +export const rowGroupingData: OrgUnit[] = [ { id: "company-1", organization: "TechSolutions Inc.", @@ -197,9 +211,8 @@ export const rowGroupingConfig = { headers: rowGroupingHeaders, rows: rowGroupingData, tableProps: { - rowGrouping: ["divisions", "departments"] as string[], + rowGrouping: ["divisions", "departments"], enableStickyParents: true, - getRowId: ({ row }: { row: Record }) => String(row.id), columnResizing: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/row-height/RowHeightDemo.tsx b/packages/examples/solid/src/demos/row-height/RowHeightDemo.tsx index 21fda04cb..bd2e87b4e 100644 --- a/packages/examples/solid/src/demos/row-height/RowHeightDemo.tsx +++ b/packages/examples/solid/src/demos/row-height/RowHeightDemo.tsx @@ -6,6 +6,7 @@ export default function RowHeightDemo(props: { height?: string | number; theme?: return ( row.id} rows={rowHeightConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/row-height/row-height.demo-data.ts b/packages/examples/solid/src/demos/row-height/row-height.demo-data.ts index 94a43659c..b44e7573f 100644 --- a/packages/examples/solid/src/demos/row-height/row-height.demo-data.ts +++ b/packages/examples/solid/src/demos/row-height/row-height.demo-data.ts @@ -1,8 +1,15 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface ArchitectStaff { + id: number; + name: string; + age: number; + role: string; + department: string; +} -export const rowHeightHeaders: SolidColumnDef[] = [ +export const rowHeightHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 150, width: "1fr", type: "string" }, { accessor: "age", label: "Age", width: 100, type: "string" }, @@ -10,7 +17,7 @@ export const rowHeightHeaders: SolidColumnDef[] = [ { accessor: "department", label: "Department", minWidth: 180, width: "1fr", type: "string" }, ]; -export const rowHeightData = [ +export const rowHeightData: ArchitectStaff[] = [ { id: 1, name: "Valentina Romano", age: 34, role: "Principal Architect", department: "Design" }, { id: 2, name: "Mateo Fernandez", age: 29, role: "Project Architect", department: "Design" }, { id: 3, name: "Amira Okafor", age: 41, role: "Design Director", department: "Leadership" }, @@ -29,4 +36,4 @@ export const rowHeightConfig = { headers: rowHeightHeaders, rows: rowHeightData, tableProps: { customTheme: { rowHeight: 32 } }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/row-selection/RowSelectionDemo.tsx b/packages/examples/solid/src/demos/row-selection/RowSelectionDemo.tsx index 4c553058e..789c930a8 100644 --- a/packages/examples/solid/src/demos/row-selection/RowSelectionDemo.tsx +++ b/packages/examples/solid/src/demos/row-selection/RowSelectionDemo.tsx @@ -1,5 +1,6 @@ import { createSignal, createMemo } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, SolidColumnDef, CellRendererProps, @@ -20,11 +21,11 @@ export default function RowSelectionDemo(props: { return books.length > 0 ? books.map((b) => b.title).join(", ") : "None"; }); - const headers: SolidColumnDef[] = rowSelectionConfig.headers.map((h) => { + const headers: SolidColumnDef[] = rowSelectionConfig.headers.map((h) => { if (h.accessor === "status") { return { ...h, - cellRenderer: (cr: CellRendererProps) => { + cellRenderer: (cr: CellRendererProps) => { const s = String(cr.row.status); const color = s === "Available" ? "#16a34a" : s === "Checked Out" ? "#ea580c" : "#dc2626"; return {s}; @@ -34,7 +35,7 @@ export default function RowSelectionDemo(props: { return h; }); - const handleSelectionChange = (selection: RowSelectionChangeProps) => { + const handleSelectionChange = (selection: RowSelectionChangeProps) => { const selected = rowSelectionData.filter((book) => selection.selectedRows.has(String(book.id)), ); @@ -65,6 +66,7 @@ export default function RowSelectionDemo(props: { row.id} rows={rowSelectionConfig.rows} height={props.height ?? "348px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/row-selection/row-selection.demo-data.ts b/packages/examples/solid/src/demos/row-selection/row-selection.demo-data.ts index 8c7a54c8a..fab165624 100644 --- a/packages/examples/solid/src/demos/row-selection/row-selection.demo-data.ts +++ b/packages/examples/solid/src/demos/row-selection/row-selection.demo-data.ts @@ -31,7 +31,7 @@ export const rowSelectionData: LibraryBook[] = [ { id: 1012, isbn: "978-0593229439", title: "Climate Solutions for Tomorrow", author: "Dr. Hassan Al-Rashid", genre: "Environmental Science", yearPublished: 2022, pages: 398, rating: 4.8, status: "Available", librarySection: "Science" }, ]; -export const rowSelectionHeaders: SolidColumnDef[] = [ +export const rowSelectionHeaders: SolidColumnDef[] = [ { accessor: "id", label: "Book ID", width: 80, sortable: true, type: "number" }, { accessor: "isbn", label: "ISBN", width: 120, sortable: true, type: "string" }, { accessor: "title", label: "Title", minWidth: 150, width: "1fr", sortable: true, type: "string" }, @@ -53,4 +53,4 @@ export const rowSelectionConfig = { columnReordering: true, selectableCells: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/sales/SalesDemo.tsx b/packages/examples/solid/src/demos/sales/SalesDemo.tsx index c5ed17b49..ebd7681a1 100644 --- a/packages/examples/solid/src/demos/sales/SalesDemo.tsx +++ b/packages/examples/solid/src/demos/sales/SalesDemo.tsx @@ -1,5 +1,6 @@ import { createEffect, createSignal, onCleanup } from "solid-js"; -import {SimpleTable} from "@simple-table/solid";import type { Theme, SolidColumnDef, CellChangeProps } from "@simple-table/solid"; +import { SimpleTable } from "@simple-table/solid"; +import type { Theme, SolidColumnDef, CellChangeProps, CellRendererProps } from "@simple-table/solid"; import { getThemeColors, salesHeadersCore, salesSampleRows, type SalesRow } from "./sales.demo-data"; import "@simple-table/solid/styles.css"; @@ -9,15 +10,14 @@ function formatTableHeight(height?: string | number | null): string { return height; } -function getHeaders(): SolidColumnDef[] { - const headers = JSON.parse(JSON.stringify(salesHeadersCore)); +function getHeaders(): SolidColumnDef[] { + const headers = JSON.parse(JSON.stringify(salesHeadersCore)) as SolidColumnDef[]; - const addRenderers = (hdrs: SolidColumnDef[]) => { + const addRenderers = (hdrs: SolidColumnDef[]) => { for (const h of hdrs) { if (h.accessor === "dealValue") { - h.cellRenderer = ({ row, theme }) => { - if (row.dealValue === "—") return "—"; - const value = row.dealValue as number; + h.cellRenderer = ({ row, theme }: CellRendererProps) => { + const value = row.dealValue; const colors = getThemeColors(theme); let style: Record = { color: colors.gray }; if (value > 100000) style = { ...colors.success.high }; @@ -32,10 +32,8 @@ function getHeaders(): SolidColumnDef[] { }; } if (h.accessor === "isWon") { - h.cellRenderer = ({ row }) => { - if (row.isWon === "—") return "—"; - const d = row as unknown as SalesRow; - const s = d.isWon + h.cellRenderer = ({ row }: CellRendererProps) => { + const s = row.isWon ? { bg: "#f6ffed", text: "#2a6a0d" } : { bg: "#fff1f0", text: "#a8071a" }; return ( @@ -50,24 +48,22 @@ function getHeaders(): SolidColumnDef[] { display: "inline-block", }} > - {d.isWon ? "Won" : "Lost"} + {row.isWon ? "Won" : "Lost"} ); }; } if (h.accessor === "commission") { - h.cellRenderer = ({ row, theme }) => { - if (row.commission === "—") return "—"; - const value = row.commission as number; + h.cellRenderer = ({ row, theme }: CellRendererProps) => { + const value = row.commission; const colors = getThemeColors(theme); if (value === 0) return $0.00; return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; } if (h.accessor === "profitMargin") { - h.cellRenderer = ({ row, theme }) => { - if (row.profitMargin === "—") return "—"; - const value = row.profitMargin as number; + h.cellRenderer = ({ row, theme }: CellRendererProps) => { + const value = row.profitMargin; const colors = getThemeColors(theme); let colorStyle: Record = { color: colors.gray }; if (value >= 0.7) colorStyle = { ...colors.success.high }; @@ -109,9 +105,8 @@ function getHeaders(): SolidColumnDef[] { }; } if (h.accessor === "dealProfit") { - h.cellRenderer = ({ row, theme }) => { - if (row.dealProfit === "—") return "—"; - const value = row.dealProfit as number; + h.cellRenderer = ({ row, theme }: CellRendererProps) => { + const value = row.dealProfit; const colors = getThemeColors(theme); if (value === 0) return $0.00; let style: Record = { color: colors.gray }; @@ -126,7 +121,7 @@ function getHeaders(): SolidColumnDef[] { ); }; } - if (h.children) addRenderers(h.children as SolidColumnDef[]); + if (h.children) addRenderers(h.children as SolidColumnDef[]); } }; addRenderers(headers); @@ -146,7 +141,7 @@ export default function SalesDemo(props: { height?: string | number | null; them onCleanup(() => window.removeEventListener("resize", check)); }); - const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { + const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { setRows((prev) => prev.map((item) => (item.id === row.id ? { ...item, [accessor]: newValue } : item)), ); @@ -159,6 +154,7 @@ export default function SalesDemo(props: { height?: string | number | null; them columnReordering columns={HEADERS} enableColumnEditor + getRowId={({ row }) => row.id} height={formatTableHeight(props.height ?? null)} initialSortColumn="dealValue" initialSortDirection="desc" diff --git a/packages/examples/solid/src/demos/sales/sales.demo-data.ts b/packages/examples/solid/src/demos/sales/sales.demo-data.ts index 932febf83..4a3e4181b 100644 --- a/packages/examples/solid/src/demos/sales/sales.demo-data.ts +++ b/packages/examples/solid/src/demos/sales/sales.demo-data.ts @@ -143,7 +143,7 @@ const SALES_SAMPLE_INBOUND: SalesInboundRow[] = [ export const salesSampleRows: SalesRow[] = processSalesData(SALES_SAMPLE_INBOUND); -export const salesHeadersCore: SolidColumnDef[] = [ +export const salesHeadersCore: SolidColumnDef[] = [ { accessor: "repName", label: "Sales Representative", @@ -292,8 +292,8 @@ export const salesHeadersCore: SolidColumnDef[] = [ { label: "Training", value: "Training" }, { label: "Support", value: "Support" }, ], - valueGetter: ({ row }: ValueGetterProps) => { - const category = row.category as string; + valueGetter: ({ row }: ValueGetterProps) => { + const category = row.category; const priorityMap: Record = { Software: 1, Consulting: 2, @@ -308,3 +308,44 @@ export const salesHeadersCore: SolidColumnDef[] = [ ], }, ]; +const FIRST_NAMES = [ + "Sophie", "Akira", "Thomas", "Valentina", "Isabella", "Emily", "Olivia", "Marcus", + "Nina", "James", "Elena", "Chen", "Priya", "Lars", "Amélie", "Diego", "Fatima", + "Henrik", "Yuki", "Grace", "Liam", "Noah", "Mia", "Lucas", "Aria", +]; + +const LAST_NAMES = [ + "Dubois", "Tanaka", "Müller", "Diaz", "Fernandez", "Davis", "Bennett", "Webb", + "Kowalski", "Okafor", "Rossi", "Wei", "Sharma", "Hansen", "Laurent", "Alvarez", + "Al-Farsi", "Berg", "Sato", "O'Malley", "Nguyen", "Schmidt", "Costa", "Ivanova", "Park", +]; + +const CATEGORIES = ["Software", "Hardware", "Services", "Consulting", "Training", "Support"]; + +const pick = (items: readonly T[]): T => items[Math.floor(Math.random() * items.length)]; + +const randomBetween = (min: number, max: number) => min + Math.random() * (max - min); + +const randomCloseDate = () => { + const start = new Date(2026, 0, 1).getTime(); + const end = new Date(2026, 11, 31).getTime(); + const date = new Date(start + Math.random() * (end - start)); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + +/** Generates `count` randomized inbound sales rows, then processes derived fields. */ +export function generateSalesData(count = 240): SalesRow[] { + const inbound: SalesInboundRow[] = Array.from({ length: count }, (_, index) => ({ + id: `SALE-${index}`, + repName: `${pick(FIRST_NAMES)} ${pick(LAST_NAMES)}`, + dealSize: parseFloat(randomBetween(100, 250000).toFixed(2)), + isWon: Math.random() > 0.35, + profitMargin: parseFloat(randomBetween(0.25, 0.75).toFixed(2)), + closeDate: randomCloseDate(), + category: pick(CATEGORIES), + })); + return processSalesData(inbound); +} diff --git a/packages/examples/solid/src/demos/single-row-children/SingleRowChildrenDemo.tsx b/packages/examples/solid/src/demos/single-row-children/SingleRowChildrenDemo.tsx index 9c8fc902a..9752b0b1f 100644 --- a/packages/examples/solid/src/demos/single-row-children/SingleRowChildrenDemo.tsx +++ b/packages/examples/solid/src/demos/single-row-children/SingleRowChildrenDemo.tsx @@ -6,6 +6,7 @@ export default function SingleRowChildrenDemo(props: { height?: string | number; return ( row.id} rows={singleRowChildrenConfig.rows} columnResizing={singleRowChildrenConfig.tableProps.columnResizing} selectableCells={singleRowChildrenConfig.tableProps.selectableCells} diff --git a/packages/examples/solid/src/demos/single-row-children/single-row-children.demo-data.ts b/packages/examples/solid/src/demos/single-row-children/single-row-children.demo-data.ts index b9f7f28eb..312157966 100644 --- a/packages/examples/solid/src/demos/single-row-children/single-row-children.demo-data.ts +++ b/packages/examples/solid/src/demos/single-row-children/single-row-children.demo-data.ts @@ -1,8 +1,19 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface StudentRecord { + id: number; + studentId: string; + studentName: string; + gradeLevel: string; + overallGPA: number; + mathGrade: number; + scienceGrade: number; + englishGrade: number; + historyGrade: number; +} -export const singleRowChildrenData = [ +export const singleRowChildrenData: StudentRecord[] = [ { id: 1, studentId: "STU-2024-001", studentName: "Alexandra Martinez", gradeLevel: "10th Grade", overallGPA: 3.85, mathGrade: 92, scienceGrade: 88, englishGrade: 91, historyGrade: 89 }, { id: 2, studentId: "STU-2024-002", studentName: "Benjamin Foster", gradeLevel: "11th Grade", overallGPA: 3.65, mathGrade: 85, scienceGrade: 87, englishGrade: 89, historyGrade: 86 }, { id: 3, studentId: "STU-2024-003", studentName: "Chloe Nakamura", gradeLevel: "12th Grade", overallGPA: 3.95, mathGrade: 96, scienceGrade: 94, englishGrade: 95, historyGrade: 93 }, @@ -16,7 +27,7 @@ export const singleRowChildrenData = [ { id: 11, studentId: "STU-2024-011", studentName: "Keiko Tanaka", gradeLevel: "12th Grade", overallGPA: 3.9, mathGrade: 94, scienceGrade: 93, englishGrade: 96, historyGrade: 92 }, ]; -export const singleRowChildrenHeaders: SolidColumnDef[] = [ +export const singleRowChildrenHeaders: SolidColumnDef[] = [ { accessor: "studentId", label: "Student ID", width: 160, sortable: true, type: "string" }, { accessor: "gradeLevel", label: "Grade Level", width: 150, sortable: true, type: "string" }, { @@ -44,4 +55,4 @@ export const singleRowChildrenConfig = { columnResizing: true, selectableCells: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/soccer/SoccerDemo.tsx b/packages/examples/solid/src/demos/soccer/SoccerDemo.tsx index 2e6899a58..13e23229a 100644 --- a/packages/examples/solid/src/demos/soccer/SoccerDemo.tsx +++ b/packages/examples/solid/src/demos/soccer/SoccerDemo.tsx @@ -7,6 +7,7 @@ export default function SoccerDemo(props: { height?: string | number; theme?: Th return ( row.id} rows={soccerConfig.rows} height={props.height ?? "70dvh"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/soccer/soccer.demo-data.ts b/packages/examples/solid/src/demos/soccer/soccer.demo-data.ts index e85824712..6e8b12967 100644 --- a/packages/examples/solid/src/demos/soccer/soccer.demo-data.ts +++ b/packages/examples/solid/src/demos/soccer/soccer.demo-data.ts @@ -1,10 +1,9 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; -import type { Row } from "@simple-table/solid"; export type SoccerPosition = "GK" | "DEF" | "MID" | "FWD"; -export interface SoccerPlayer extends Row { +export interface SoccerPlayer { id: string; rank: number; rankChange: number; @@ -125,7 +124,7 @@ export function generateSoccerData(count = 200): SoccerPlayer[] { return players; } -export const soccerHeaders: SolidColumnDef[] = [ +export const soccerHeaders: SolidColumnDef[] = [ { accessor: "rank", label: "#", width: 56, align: "center", type: "number", pinned: "left", sortable: true, editable: false }, { accessor: "name", label: "Player", width: 220, align: "left", type: "string", pinned: "left", sortable: true, editable: false, @@ -185,4 +184,4 @@ export const soccerData = generateSoccerData(200); export const soccerConfig = { headers: soccerHeaders, rows: soccerData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/table-height/TableHeightDemo.tsx b/packages/examples/solid/src/demos/table-height/TableHeightDemo.tsx index 50786df1a..be0236b26 100644 --- a/packages/examples/solid/src/demos/table-height/TableHeightDemo.tsx +++ b/packages/examples/solid/src/demos/table-height/TableHeightDemo.tsx @@ -33,6 +33,7 @@ export default function TableHeightDemo(props: { height?: string | number; theme
row.id} rows={tableHeightConfig.rows} height={selectedHeight()} theme={props.theme} diff --git a/packages/examples/solid/src/demos/table-height/table-height.demo-data.ts b/packages/examples/solid/src/demos/table-height/table-height.demo-data.ts index 077221602..8475601c7 100644 --- a/packages/examples/solid/src/demos/table-height/table-height.demo-data.ts +++ b/packages/examples/solid/src/demos/table-height/table-height.demo-data.ts @@ -1,8 +1,16 @@ // Self-contained demo table setup for this example. import type { SolidColumnDef } from "@simple-table/solid"; +export interface HeightDemoEmployee { + id: number; + name: string; + email: string; + role: string; + department: string; + status: string; +} -export const tableHeightHeaders: SolidColumnDef[] = [ +export const tableHeightHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 150, width: "1fr", type: "string" }, { accessor: "email", label: "Email", minWidth: 200, width: "1fr", type: "string" }, @@ -11,7 +19,7 @@ export const tableHeightHeaders: SolidColumnDef[] = [ { accessor: "status", label: "Status", width: 120, type: "string" }, ]; -export const tableHeightData = [ +export const tableHeightData: HeightDemoEmployee[] = [ { id: 1, name: "Alice Chen", email: "alice@example.com", role: "Senior Engineer", department: "Engineering", status: "Active" }, { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Product Manager", department: "Product", status: "Active" }, { id: 3, name: "Carol Williams", email: "carol@example.com", role: "Designer", department: "Design", status: "Active" }, @@ -32,4 +40,4 @@ export const tableHeightData = [ export const tableHeightConfig = { headers: tableHeightHeaders, rows: tableHeightData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/themes/ThemesDemo.tsx b/packages/examples/solid/src/demos/themes/ThemesDemo.tsx index 7ee1eb1ba..64b62a374 100644 --- a/packages/examples/solid/src/demos/themes/ThemesDemo.tsx +++ b/packages/examples/solid/src/demos/themes/ThemesDemo.tsx @@ -31,6 +31,7 @@ export default function ThemesDemo(props: { height?: string | number; theme?: Th row.id} rows={themesConfig.rows} height={props.height ?? "400px"} theme={selectedTheme()} diff --git a/packages/examples/solid/src/demos/themes/themes.demo-data.ts b/packages/examples/solid/src/demos/themes/themes.demo-data.ts index 6ba5321d8..0c831b75f 100644 --- a/packages/examples/solid/src/demos/themes/themes.demo-data.ts +++ b/packages/examples/solid/src/demos/themes/themes.demo-data.ts @@ -10,7 +10,15 @@ export const AVAILABLE_THEMES: { value: Theme; label: string }[] = [ { value: "neutral", label: "Neutral" }, ]; -export const themesHeaders: SolidColumnDef[] = [ +export interface ThemeDemoStaff { + id: number; + name: string; + email: string; + department: string; + status: string; +} + +export const themesHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 100, width: "1fr", type: "string" }, { accessor: "email", label: "Email", width: 220, type: "string" }, @@ -18,7 +26,7 @@ export const themesHeaders: SolidColumnDef[] = [ { accessor: "status", label: "Status", width: 120, type: "string" }, ]; -export const themesData = [ +export const themesData: ThemeDemoStaff[] = [ { id: 1, name: "Dr. Sarah Mitchell", email: "s.mitchell@cityhospital.org", department: "Cardiology", status: "Active" }, { id: 2, name: "Nurse Emily Chen", email: "e.chen@cityhospital.org", department: "Emergency", status: "Active" }, { id: 3, name: "Dr. Marcus Williams", email: "m.williams@cityhospital.org", department: "Neurology", status: "Active" }, @@ -36,4 +44,4 @@ export const themesData = [ export const themesConfig = { headers: themesHeaders, rows: themesData, -} as const; +}; diff --git a/packages/examples/solid/src/demos/tooltip/TooltipDemo.tsx b/packages/examples/solid/src/demos/tooltip/TooltipDemo.tsx index 8005513a4..f7da2603d 100644 --- a/packages/examples/solid/src/demos/tooltip/TooltipDemo.tsx +++ b/packages/examples/solid/src/demos/tooltip/TooltipDemo.tsx @@ -6,6 +6,7 @@ export default function TooltipDemo(props: { height?: string | number; theme?: T return ( row.id} rows={tooltipConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/tooltip/tooltip.demo-data.ts b/packages/examples/solid/src/demos/tooltip/tooltip.demo-data.ts index bc3c61bd0..a346860ed 100644 --- a/packages/examples/solid/src/demos/tooltip/tooltip.demo-data.ts +++ b/packages/examples/solid/src/demos/tooltip/tooltip.demo-data.ts @@ -1,8 +1,17 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface TooltipProduct { + id: number; + productName: string; + category: string; + price: number; + stock: number; + rating: number; + lastUpdated: string; +} -export const tooltipData: Row[] = [ +export const tooltipData: TooltipProduct[] = [ { id: 1, productName: "MacBook Pro M3 Max", category: "Laptops", price: 3299.99, stock: 12, rating: 4.8, lastUpdated: "2024-01-15" }, { id: 2, productName: "Logitech MX Master 3S", category: "Peripherals", price: 99.99, stock: 45, rating: 4.6, lastUpdated: "2024-01-18" }, { id: 3, productName: "Samsung 49-inch Ultrawide Monitor", category: "Displays", price: 899.99, stock: 8, rating: 4.7, lastUpdated: "2024-01-20" }, @@ -20,7 +29,7 @@ export const tooltipData: Row[] = [ { id: 15, productName: "Corsair K95 RGB Platinum", category: "Peripherals", price: 199.99, stock: 16, rating: 4.5, lastUpdated: "2024-02-18" }, ]; -export const tooltipHeaders: SolidColumnDef[] = [ +export const tooltipHeaders: SolidColumnDef[] = [ { accessor: "productName", label: "Product", width: 200, sortable: true, tooltip: "Complete product name including model specifications and key features" }, { accessor: "category", label: "Category", width: 150, sortable: true, filterable: true, tooltip: "Product classification: Laptops, Displays, or Peripherals for easy filtering" }, { @@ -53,4 +62,4 @@ export const tooltipConfig = { columnReordering: true, selectableCells: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/value-formatter/ValueFormatterDemo.tsx b/packages/examples/solid/src/demos/value-formatter/ValueFormatterDemo.tsx index 90ef44c25..d75bcdcfe 100644 --- a/packages/examples/solid/src/demos/value-formatter/ValueFormatterDemo.tsx +++ b/packages/examples/solid/src/demos/value-formatter/ValueFormatterDemo.tsx @@ -9,6 +9,7 @@ export default function ValueFormatterDemo(props: { return ( row.id} rows={valueFormatterConfig.rows} height={props.height ?? "400px"} theme={props.theme} diff --git a/packages/examples/solid/src/demos/value-formatter/value-formatter.demo-data.ts b/packages/examples/solid/src/demos/value-formatter/value-formatter.demo-data.ts index e88181dbd..6b36484b5 100644 --- a/packages/examples/solid/src/demos/value-formatter/value-formatter.demo-data.ts +++ b/packages/examples/solid/src/demos/value-formatter/value-formatter.demo-data.ts @@ -1,8 +1,18 @@ // Self-contained demo table setup for this example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; +export interface FormattedEmployee { + id: number; + firstName: string; + lastName: string; + salary: number; + joinDate: string; + performanceScore: number; + balance: number; + department: string; +} -export const valueFormatterData: Row[] = [ +export const valueFormatterData: FormattedEmployee[] = [ { id: 1, firstName: "Isabella", lastName: "Romano", salary: 125000, joinDate: "2021-03-15", performanceScore: 0.945, balance: 1250.50, department: "Engineering" }, { id: 2, firstName: "Ethan", lastName: "McKenzie", salary: 98500, joinDate: "2022-07-22", performanceScore: 0.875, balance: -150.00, department: "Marketing" }, { id: 3, firstName: "Zoe", lastName: "Patterson", salary: 110000, joinDate: "2020-01-10", performanceScore: 0.923, balance: 0, department: "Sales" }, @@ -26,7 +36,7 @@ const DEPARTMENT_CODES: Record = { operations: "OPS", }; -export const valueFormatterHeaders: SolidColumnDef[] = [ +export const valueFormatterHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "firstName", @@ -34,7 +44,7 @@ export const valueFormatterHeaders: SolidColumnDef[] = [ width: 180, type: "string", valueFormatter: ({ value, row }) => { - return `${value as string} ${row.lastName as string}`; + return `${value as string} ${row.lastName}`; }, }, { @@ -102,4 +112,4 @@ export const valueFormatterConfig = { tableProps: { selectableCells: true, }, -} as const; +}; diff --git a/packages/examples/solid/src/demos/window-infinite-scroll/WindowInfiniteScrollDemo.tsx b/packages/examples/solid/src/demos/window-infinite-scroll/WindowInfiniteScrollDemo.tsx index db605edb5..c40450ec8 100644 --- a/packages/examples/solid/src/demos/window-infinite-scroll/WindowInfiniteScrollDemo.tsx +++ b/packages/examples/solid/src/demos/window-infinite-scroll/WindowInfiniteScrollDemo.tsx @@ -1,9 +1,10 @@ import { createMemo, createSignal } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { Row, Theme } from "@simple-table/solid"; +import type { Theme } from "@simple-table/solid"; import { generateWindowScrollRows, windowScrollHeaders, + type WindowScrollEmployee, } from "./window-infinite-scroll.demo-data"; import "@simple-table/solid/styles.css"; @@ -12,26 +13,15 @@ const BATCH_SIZE = 50; const MAX_ROWS = 5_000; const LOAD_DELAY_MS = 350; -/** - * Window-style infinite scroll demo. - * - * The table has no `height` / `maxHeight` — it grows to its natural size inside - * whatever scroll parent surrounds it. `scrollParent` is a getter that resolves - * to the nearest scrollable ancestor at render time (the demo shell wraps - * everything in a scrollable `
`, so `window` itself doesn't scroll inside - * this preview). In a typical app you'd just pass `scrollParent="window"`. - * - * As the user scrolls toward the bottom, `onLoadMore` appends a batch of rows - * until the dataset cap is reached. - */ export default function WindowInfiniteScrollDemo(_props: { - // `height` is intentionally ignored — this demo is about *not* setting one. height?: string | number; theme?: Theme; }) { let wrapperRef: HTMLDivElement | undefined; - const [rows, setRows] = createSignal(generateWindowScrollRows(0, INITIAL_ROWS)); + const [rows, setRows] = createSignal( + generateWindowScrollRows(0, INITIAL_ROWS), + ); const [loading, setLoading] = createSignal(false); const [hasMore, setHasMore] = createSignal(true); @@ -43,12 +33,8 @@ export default function WindowInfiniteScrollDemo(_props: { return `${rows().length.toLocaleString()} rows loaded · end of dataset`; }); - // Getter form so we don't capture the wrapper before Solid attaches the ref. - // In a regular app outside this preview shell, pass scrollParent="window". const getScrollParent = () => wrapperRef?.parentElement ?? null; - const getRowId = (p: { row: Row }) => String((p.row as { id?: number })?.id); - const handleLoadMore = () => { if (loading() || !hasMore()) return; setLoading(true); @@ -117,7 +103,7 @@ export default function WindowInfiniteScrollDemo(_props: { columns={windowScrollHeaders} rows={rows()} theme={_props.theme} - getRowId={getRowId} + getRowId={({ row }) => String(row.id)} scrollParent={getScrollParent} infiniteScrollThreshold={400} onLoadMore={handleLoadMore} diff --git a/packages/examples/solid/src/demos/window-infinite-scroll/window-infinite-scroll.demo-data.ts b/packages/examples/solid/src/demos/window-infinite-scroll/window-infinite-scroll.demo-data.ts index 041df1238..b473f0857 100644 --- a/packages/examples/solid/src/demos/window-infinite-scroll/window-infinite-scroll.demo-data.ts +++ b/packages/examples/solid/src/demos/window-infinite-scroll/window-infinite-scroll.demo-data.ts @@ -1,5 +1,15 @@ // Self-contained data + headers for the window-scroll infinite scroll example. -import type { SolidColumnDef, Row } from "@simple-table/solid"; +import type { SolidColumnDef } from "@simple-table/solid"; + +export interface WindowScrollEmployee { + id: number; + name: string; + email: string; + department: string; + status: string; + tenureYears: number; + salary: number; +} const FIRST_NAMES = [ "Elena", @@ -52,10 +62,13 @@ const DEPARTMENTS = [ "Customer Success", ]; -const STATUSES = ["Active", "On Leave", "Remote", "Onsite"] as const; +const STATUSES = ["Active", "On Leave", "Remote", "Onsite"]; -export function generateWindowScrollRows(startIndex: number, count: number): Row[] { - const rows: Row[] = []; +export function generateWindowScrollRows( + startIndex: number, + count: number, +): WindowScrollEmployee[] { + const rows: WindowScrollEmployee[] = []; for (let i = 0; i < count; i++) { const idx = startIndex + i; const first = FIRST_NAMES[idx % FIRST_NAMES.length]; @@ -73,7 +86,7 @@ export function generateWindowScrollRows(startIndex: number, count: number): Row return rows; } -export const windowScrollHeaders: SolidColumnDef[] = [ +export const windowScrollHeaders: SolidColumnDef[] = [ { accessor: "id", label: "ID", width: 80, type: "number", align: "right" }, { accessor: "name", label: "Name", width: "1fr", minWidth: 160 }, { accessor: "email", label: "Email", width: 260 }, diff --git a/packages/solid/src/SimpleTable.tsx b/packages/solid/src/SimpleTable.tsx index 2700d084e..e7be4bc4f 100644 --- a/packages/solid/src/SimpleTable.tsx +++ b/packages/solid/src/SimpleTable.tsx @@ -7,7 +7,11 @@ import { import type { SimpleTableConfig, TableAPI } from "simple-table-core"; import { buildVanillaConfig, resolveSolidColumns } from "./buildVanillaConfig"; import { MountRegistry } from "./MountRegistry"; -import type { SimpleTableSolidProps, TableInstance } from "./types"; +import type { + SimpleTableSolidProps, + SolidDefaultRowData, + TableInstance, +} from "./types"; /** * SimpleTable — Solid.js adapter for simple-table-core. @@ -15,25 +19,32 @@ import type { SimpleTableSolidProps, TableInstance } from "./types"; * Accepts the same props as SimpleTableProps (the vanilla user-facing API) but * with Solid component types for all renderer props. * - * Pass a callback `ref` prop to receive the full TableAPI imperative interface: + * `TData` is inferred from `rows` / `columns` — no explicit type argument needed + * for typical demos. Pass a callback `ref` prop to receive the TableAPI once mounted: * * @example - * let tableApi: TableAPI | undefined; + * let tableApi: TableAPI | undefined; * * (tableApi = api)} * rows={rows()} * columns={headers} + * getRowId={({ row }) => row.id} * /> */ -export function SimpleTable(props: SimpleTableSolidProps) { +// No default on `TData`: TypeScript should infer it from `rows` / `columns` +// so callers can write `` without ``. +// (`SimpleTableSolidProps` still defaults for untyped prop bags / helpers.) +export function SimpleTable( + props: SimpleTableSolidProps, +) { let containerEl!: HTMLDivElement; let instance: TableInstance | null = null; const registry = new MountRegistry(); let syncedDefaultHeaders: ReadonlyArray< - NonNullable[number] + NonNullable["columns"]>[number] > | undefined; - let syncedRows: SimpleTableSolidProps["rows"] | undefined; + let syncedRows: SimpleTableSolidProps["rows"] | undefined; onMount(() => { instance = new SimpleTableVanilla( @@ -45,7 +56,8 @@ export function SimpleTable(props: SimpleTableSolidProps) { syncedRows = props.rows; if (props.ref) { - props.ref(instance.getAPI() as TableAPI); + // Runtime API is Row-shaped; expose as TableAPI at the boundary. + props.ref(instance.getAPI() as unknown as TableAPI); } }); @@ -70,7 +82,7 @@ export function SimpleTable(props: SimpleTableSolidProps) { const rowsUnchanged = rowsShallowUnchanged( syncedRows as ReadonlyArray | undefined, props.rows as ReadonlyArray, - props.getRowId, + props.getRowId as Parameters[2], ); syncedRows = props.rows; if (rowsUnchanged) { diff --git a/packages/solid/src/__tests__/genericRowData.types.test.ts b/packages/solid/src/__tests__/genericRowData.types.test.ts new file mode 100644 index 000000000..97a22ac38 --- /dev/null +++ b/packages/solid/src/__tests__/genericRowData.types.test.ts @@ -0,0 +1,126 @@ +/** + * Compile-time smoke test: SimpleTable / SolidColumnDef / TableAPI accept a + * domain row type end-to-end (no asRows, no row casts in callbacks). + */ +import { describe, it, expect } from "vitest"; +import { SimpleTable } from "../index"; +import type { + TableAPI, + SolidColumnDef, + NestedSolidColumnDef, + CellChangeProps, + SimpleTableSolidProps, +} from "../index"; + +interface HREmployee { + id: number; + firstName: string; + lastName: string; + fullName: string; + performanceScore: number; +} + +const columns: SolidColumnDef[] = [ + { + accessor: "fullName", + label: "Employee", + width: 220, + cellRenderer: ({ row }) => { + const name: string = row.firstName; + return name; + }, + }, +]; + +const rows: HREmployee[] = [ + { + id: 1, + firstName: "Ada", + lastName: "Lovelace", + fullName: "Ada Lovelace", + performanceScore: 99, + }, +]; + +const props: SimpleTableSolidProps = { + columns, + rows, + getRowId: ({ row }) => row.id, + onCellEdit: ({ row }: CellChangeProps) => { + const id: number = row.id; + void id; + }, + ref: (api: TableAPI) => { + void api; + }, +}; +void props; + +// Explicit type arg still works when you want it: +void SimpleTable; + +// Untyped / default path still accepts open records (prior object[] escape). +const loose: SimpleTableSolidProps = { + columns: [{ accessor: "x", label: "X", width: 40 }], + rows: [{ x: 1 }], +}; +void loose; + +// Nested grid: child columns typed as Division assign under a Company column +// with no cast / helper (nestedTable.columns is open at the boundary). +interface NestCompany { + id: string; + name: string; + divisions?: NestDivision[]; +} +interface NestDivision { + id: string; + divisionName: string; +} + +const divisionColumns: SolidColumnDef[] = [ + { + accessor: "divisionName", + label: "Division", + width: 120, + cellRenderer: ({ row }) => { + const name: string = row.divisionName; + // @ts-expect-error NestDivision has no company-only fields + const missing: string = row.companyName; + void missing; + return name; + }, + }, +]; + +const companyColumns: SolidColumnDef[] = [ + { + accessor: "name", + label: "Company", + width: 160, + expandable: true, + nestedTable: { + columns: divisionColumns, + expandAll: false, + }, + }, +]; +void companyColumns; + +// Non-column objects are rejected at the nest boundary. +const notAColumn = { foo: 1 }; +const badNestedColumns: NestedSolidColumnDef[] = [ + // @ts-expect-error nest columns require column metadata (accessor, label, …) + notAColumn, +]; +void badNestedColumns; + +describe("generic row data types", () => { + it("compiles typed SimpleTable props and ref", () => { + expect(columns[0]?.accessor).toBe("fullName"); + }); + + it("accepts differently typed nested columns without casts", () => { + expect(companyColumns[0]?.nestedTable?.columns?.[0]?.accessor).toBe("divisionName"); + }); +}); diff --git a/packages/solid/src/__tests__/inferRowData.types.test.ts b/packages/solid/src/__tests__/inferRowData.types.test.ts new file mode 100644 index 000000000..f69644d1b --- /dev/null +++ b/packages/solid/src/__tests__/inferRowData.types.test.ts @@ -0,0 +1,62 @@ +/** + * Compile-time probe: TData should infer from `rows` without `` on SimpleTable. + * Uses a direct call (Solid is a generic function component) — same inference as JSX. + * Calls are behind `false &&` so the component body never runs. + */ +import { describe, it, expect } from "vitest"; +import { SimpleTable } from "../index"; +import type { SolidColumnDef, TableAPI } from "../index"; + +interface AnalyticsFactRow { + id: number; + metric: string; + value: number; +} + +const columns: SolidColumnDef[] = [ + { accessor: "metric", label: "Metric", width: 120 }, + { accessor: "value", label: "Value", width: 100, type: "number" }, +]; + +const rows: AnalyticsFactRow[] = [{ id: 1, metric: "revenue", value: 10 }]; + +let tableApi: TableAPI | undefined; + +false && + SimpleTable({ + ref: (api) => { + tableApi = api; + }, + columns, + rows, + getRowId: ({ row }) => { + const id: number = row.id; + const metric: string = row.metric; + void metric; + return id; + }, + }); +void tableApi; + +false && + SimpleTable({ + columns, + rows, + getRowId: ({ row }) => { + // @ts-expect-error — AnalyticsFactRow has no `missing` + return row.missing; + }, + }); + +false && + SimpleTable({ + columns: [{ accessor: "x", label: "X", width: 40 }], + rows: [{ x: 1 }], + getRowId: ({ row }) => row.x, + }); + +describe("SimpleTable TData inference", () => { + it("compiles without an explicit type argument", () => { + expect(rows[0]?.id).toBe(1); + }); +}); diff --git a/packages/solid/src/buildVanillaConfig.ts b/packages/solid/src/buildVanillaConfig.ts index fde2af5b1..57f54cac3 100644 --- a/packages/solid/src/buildVanillaConfig.ts +++ b/packages/solid/src/buildVanillaConfig.ts @@ -1,9 +1,10 @@ -import type { SimpleTableConfig, ColumnDef, ColumnEditorConfig, Row } from "simple-table-core"; -import { collectHeaderAccessors } from "simple-table-core"; +import type { SimpleTableConfig, ColumnDef, ColumnEditorConfig } from "simple-table-core"; +import { asRows, collectHeaderAccessors } from "simple-table-core"; import type { SimpleTableSolidProps, SolidColumnDef, SolidColumnEditorConfig, + SolidDefaultRowData, SolidIconsConfig, } from "./types"; import type { MountRegistry } from "./MountRegistry"; @@ -44,8 +45,8 @@ function transformColumnEditorConfig( }; } -function transformHeader( - header: ColumnDef | SolidColumnDef, +function transformHeader( + header: SolidColumnDef, registry: MountRegistry, ): ColumnDef { const { cellRenderer, headerRenderer, children, nestedTable, ...rest } = header; @@ -76,6 +77,8 @@ function transformHeader( } if (nestedTable) { + // Recursively convert the nested table config. Rows are provided at + // render time by the vanilla core, so we supply an empty placeholder. const nestedConfig = { ...nestedTable, rows: [] } as unknown as SimpleTableSolidProps; transformed.nestedTable = buildVanillaConfig(nestedConfig, registry) as any; } @@ -84,9 +87,9 @@ function transformHeader( } /** Resolve column definitions. */ -export function resolveSolidColumns( - config: Pick, -): ReadonlyArray { +export function resolveSolidColumns( + config: Pick, "columns">, +): ReadonlyArray> { const headers = config.columns; if (!headers) { throw new Error("SimpleTable requires `columns`"); @@ -94,8 +97,8 @@ export function resolveSolidColumns( return headers; } -export function buildVanillaConfig( - config: SimpleTableSolidProps, +export function buildVanillaConfig( + config: SimpleTableSolidProps, registry: MountRegistry, ): SimpleTableConfig { const { @@ -129,9 +132,13 @@ export function buildVanillaConfig( registry.pruneRendererCaches(collectHeaderAccessors(columns)); + // `rest` still carries TData-bound callbacks (getRowId, onCellEdit, …). + // Widen once here — the vanilla runtime is Row-shaped. + const shared = rest as SimpleTableConfig; + const vanillaConfig: SimpleTableConfig = { - ...rest, - rows: rows as Row[], + ...shared, + rows: asRows(rows), columns: columns.map((header) => transformHeader(header, registry)), enableColumnEditor, enableColumnEditorInitOpen, @@ -144,32 +151,25 @@ export function buildVanillaConfig( // discards any host element, so the registry disposes exactly the affected // Solid trees (including portals / floating UI). onRendererHostDiscard: registry.disposeHost, - ...(onColumnOrderChange - ? { - onColumnOrderChange: (headers: ColumnDef[]) => - onColumnOrderChange(headers as unknown as SolidColumnDef[]), - } - : {}), - ...(onColumnWidthChange - ? { - onColumnWidthChange: (headers: ColumnDef[]) => - onColumnWidthChange(headers as unknown as SolidColumnDef[]), - } - : {}), - ...(onHeaderEdit - ? { - onHeaderEdit: (header: ColumnDef, newLabel: string) => - onHeaderEdit(header as unknown as SolidColumnDef, newLabel), - } - : {}), - ...(onColumnSelect - ? { - onColumnSelect: (header: ColumnDef) => - onColumnSelect(header as unknown as SolidColumnDef), - } - : {}), }; + if (onColumnOrderChange) { + vanillaConfig.onColumnOrderChange = (headers) => + onColumnOrderChange(headers as unknown as SolidColumnDef[]); + } + if (onColumnWidthChange) { + vanillaConfig.onColumnWidthChange = (headers) => + onColumnWidthChange(headers as unknown as SolidColumnDef[]); + } + if (onHeaderEdit) { + vanillaConfig.onHeaderEdit = (header, newLabel) => + onHeaderEdit(header as unknown as SolidColumnDef, newLabel); + } + if (onColumnSelect) { + vanillaConfig.onColumnSelect = (header) => + onColumnSelect(header as unknown as SolidColumnDef); + } + if (footerRenderer !== undefined) { vanillaConfig.footerRenderer = wrapSolidRenderer(registry, footerRenderer) as any; } diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index 863b50162..dd205d0f3 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -7,6 +7,8 @@ export type { SimpleTableSolidProps, TableInstance, SolidColumnDef, + NestedSolidColumnDef, + NestedTableSolidConfig, SolidColumnEditorConfig, SolidIconsConfig, SolidIconElement, @@ -20,10 +22,12 @@ export type { SolidLoadingStateRenderer, SolidErrorStateRenderer, SolidEmptyStateRenderer, + SolidDefaultRowData, } from "./types"; -// Re-export vanilla types consumers need when building column definitions, -// callbacks, or using the imperative ref API. +// Re-export vanilla types for callbacks and the imperative ref API. For +// column definitions and column-related callbacks on `SimpleTable`, use +// `SolidColumnDef` (exported above). export type { Accessor, AggregationConfig, diff --git a/packages/solid/src/types.ts b/packages/solid/src/types.ts index 745e50b71..df69f49ee 100644 --- a/packages/solid/src/types.ts +++ b/packages/solid/src/types.ts @@ -1,10 +1,10 @@ -import type { Component, JSX } from "solid-js"; +import type { JSX } from "solid-js"; import type { SimpleTableProps, SimpleTableConfig, ColumnDef, - Row, TableAPI, + CellValue, CellRendererProps, HeaderRendererProps, FooterRendererProps, @@ -18,6 +18,16 @@ import type { RowButtonProps, } from "simple-table-core"; +/** + * Default `TData` for Solid props bags / helpers: open records so untyped + * object arrays keep assigning to `rows` without `asRows`. + * + * Prefer letting `` infer `TData` from + * those props — an explicit `SimpleTable` is only needed when inference + * can't see a typed `rows`/`columns` (or for a typed `ref` callback). + */ +export type SolidDefaultRowData = Record; + // ─── Internal instance contract ─────────────────────────────────────────────── export interface TableInstance { mount(): void; @@ -45,19 +55,38 @@ export interface SolidIconsConfig { } // ─── Renderer overrides ─────────────────────────────────────────────────────── -export type SolidCellRenderer = Component; -export type SolidHeaderRenderer = Component; -export type SolidFooterRenderer = Component; -export type SolidHeaderDropdown = Component; -export type SolidColumnEditorRowRenderer = Component; -export type SolidColumnEditorCustomRenderer = Component; -// Per-row action buttons. Each entry is a Solid component receiving RowButtonProps. -export type SolidRowButton = Component; +// Function types (not `Component<…>`) so inline renderers get contextual typing +// — e.g. `cellRenderer: (props) => …` infers props from TData. Solid components +// still assign to these call signatures. +export type SolidCellRenderer< + TData extends SolidDefaultRowData = SolidDefaultRowData, + TValue = CellValue, +> = (props: CellRendererProps) => JSX.Element; +export type SolidHeaderRenderer = ( + props: HeaderRendererProps, +) => JSX.Element; +export type SolidFooterRenderer = (props: FooterRendererProps) => JSX.Element; +export type SolidHeaderDropdown = (props: HeaderDropdownProps) => JSX.Element; +export type SolidColumnEditorRowRenderer = ( + props: ColumnEditorRowRendererProps, +) => JSX.Element; +export type SolidColumnEditorCustomRenderer = ( + props: ColumnEditorCustomRendererProps, +) => JSX.Element; +export type SolidRowButton = ( + props: RowButtonProps, +) => JSX.Element; -// State renderers can be a component (receives props) or a plain JSX.Element (static markup) -export type SolidLoadingStateRenderer = Component | JSX.Element; -export type SolidErrorStateRenderer = Component | JSX.Element; -export type SolidEmptyStateRenderer = Component | JSX.Element; +// State renderers can be a function (receives props) or a plain JSX.Element (static markup) +export type SolidLoadingStateRenderer = + | ((props: LoadingStateRendererProps) => JSX.Element) + | JSX.Element; +export type SolidErrorStateRenderer = + | ((props: ErrorStateRendererProps) => JSX.Element) + | JSX.Element; +export type SolidEmptyStateRenderer = + | ((props: EmptyStateRendererProps) => JSX.Element) + | JSX.Element; // ─── Column editor config override ─────────────────────────────────────────── export interface SolidColumnEditorConfig @@ -68,73 +97,124 @@ export interface SolidColumnEditorConfig // ─── ColumnDef override ──────────────────────────────────────────────────── /** - * Column definition for `columns`: core column metadata with - * Solid-only renderer fields. `columns` also accept plain - * `ColumnDef[]` from shared configs. + * Column definition for `columns`: same column metadata as core + * columns, but `cellRenderer` / `headerRenderer` / `children` / `nestedTable` are Solid-only. */ -export interface SolidColumnDef - extends Omit { - cellRenderer?: SolidCellRenderer; - headerRenderer?: SolidHeaderRenderer; - children?: SolidColumnDef[]; - nestedTable?: Omit< - SimpleTableSolidProps, - | "rows" - | "loadingStateRenderer" - | "errorStateRenderer" - | "emptyStateRenderer" - | "tableEmptyStateRenderer" - >; +export interface SolidColumnDef< + TData extends SolidDefaultRowData = SolidDefaultRowData, + TValue = CellValue, +> extends Omit< + ColumnDef, + "cellRenderer" | "headerRenderer" | "children" | "nestedTable" +> { + cellRenderer?: SolidCellRenderer; + headerRenderer?: SolidHeaderRenderer; + children?: ReadonlyArray>; + /** Nested grid; child columns may use a different row type than `TData`. */ + nestedTable?: NestedTableSolidConfig; } - // ─── Top-level props ────────────────────────────────────────────────────────── -// Mirrors SimpleTableProps with Solid-specific overrides. Pass `ref` to receive -// the TableAPI once mounted (callback ref). +// Mirrors SimpleTableProps with Solid-specific renderer/icon types. +// `SimpleTable` infers `TData` from `rows`/`columns`; use a typed `ref` callback +// when you need a typed imperative handle. // // Overridden to Solid equivalents: -// - columns → ReadonlyArray -export interface SimpleTableSolidProps - extends Omit< - SimpleTableProps, - | "rows" - | "columns" - | "footerRenderer" - | "emptyStateRenderer" - | "errorStateRenderer" - | "loadingStateRenderer" - | "tableEmptyStateRenderer" - | "headerDropdown" - | "columnEditorConfig" - | "icons" - | "rowButtons" - | "onColumnOrderChange" - | "onColumnWidthChange" - | "onHeaderEdit" - | "onColumnSelect" - > { +// - columns → ReadonlyArray> +// - footerRenderer → SolidFooterRenderer +// - loadingStateRenderer → SolidLoadingStateRenderer | JSX.Element +// - errorStateRenderer → SolidErrorStateRenderer | JSX.Element +// - emptyStateRenderer → SolidEmptyStateRenderer | JSX.Element +// - tableEmptyStateRenderer → JSX.Element +// - headerDropdown → SolidHeaderDropdown +// - columnEditorConfig → SolidColumnEditorConfig +// - icons → SolidIconsConfig +// - rowButtons → SolidRowButton[] +export interface SimpleTableSolidProps< + TData extends SolidDefaultRowData = SolidDefaultRowData, +> extends Omit< + SimpleTableProps, + // Overridden below with Solid types + | "columns" + | "footerRenderer" + | "emptyStateRenderer" + | "errorStateRenderer" + | "loadingStateRenderer" + | "tableEmptyStateRenderer" + | "headerDropdown" + | "columnEditorConfig" + | "icons" + | "rows" + | "rowButtons" + | "onColumnOrderChange" + | "onColumnWidthChange" + | "onHeaderEdit" + | "onColumnSelect" +> { /** Column definitions. */ - columns?: ReadonlyArray; - /** Row data: domain objects or core `Row[]`; cast inside the adapter. */ - rows: ReadonlyArray | ReadonlyArray; - onColumnOrderChange?: (newHeaders: SolidColumnDef[]) => void; - onColumnWidthChange?: (headers: SolidColumnDef[]) => void; - onHeaderEdit?: (header: SolidColumnDef, newLabel: string) => void; - onColumnSelect?: (header: SolidColumnDef) => void; + columns?: ReadonlyArray>; + onColumnOrderChange?: (newHeaders: SolidColumnDef[]) => void; + onColumnWidthChange?: (headers: SolidColumnDef[]) => void; + onHeaderEdit?: (header: SolidColumnDef, newLabel: string) => void; + onColumnSelect?: (header: SolidColumnDef) => void; + /** Row data; cast to vanilla `Row[]` inside the adapter. */ + rows: readonly TData[]; footerRenderer?: SolidFooterRenderer; - loadingStateRenderer?: SolidLoadingStateRenderer; - errorStateRenderer?: SolidErrorStateRenderer; - emptyStateRenderer?: SolidEmptyStateRenderer; + loadingStateRenderer?: SolidLoadingStateRenderer; + errorStateRenderer?: SolidErrorStateRenderer; + emptyStateRenderer?: SolidEmptyStateRenderer; tableEmptyStateRenderer?: JSX.Element; headerDropdown?: SolidHeaderDropdown; columnEditorConfig?: SolidColumnEditorConfig; icons?: SolidIconsConfig; - /** Per-row action buttons; each entry is a Solid component rendered into the row's selection column. */ - rowButtons?: SolidRowButton[]; + /** Per-row action buttons; each entry returns JSX rendered into the row's selection column. */ + rowButtons?: SolidRowButton[]; /** Callback ref — receives the TableAPI once the table is mounted. */ - ref?: (api: TableAPI) => void; + ref?: (api: TableAPI) => void; } +// ─── Nested table config ───────────────────────────────────────────────────── +/** + * TData-bound call signatures omitted at nest boundaries so + * `SolidColumnDef[]` assigns under a parent column without casts/`any`. + * Child columns stay fully checked at their own declaration site. + */ +type SolidColumnDefCallbacks = + | "cellRenderer" + | "headerRenderer" + | "children" + | "nestedTable" + | "comparator" + | "exportValueGetter" + | "valueFormatter" + | "valueGetter" + | "quickFilterGetter"; + +/** Column shape accepted on nested grids — metadata only, no TData-bound callbacks. */ +export type NestedSolidColumnDef = Omit< + SolidColumnDef, + SolidColumnDefCallbacks +> & { + children?: ReadonlyArray; + nestedTable?: NestedTableSolidConfig; +}; + +/** + * Nested grid props (no `rows` / inherited state renderers). + * Child row shape may differ from the parent column's `TData`. + */ +export type NestedTableSolidConfig = Omit< + SimpleTableSolidProps, + | "rows" + | "columns" + | "loadingStateRenderer" + | "errorStateRenderer" + | "emptyStateRenderer" + | "tableEmptyStateRenderer" +> & { + columns?: ReadonlyArray; +}; + // Re-export vanilla prop types that consumers still need directly export type { CellRendererProps,