From 5aba6cfefd1f724349bde24d6a38cd02db1075ad Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:19:02 -0500 Subject: [PATCH 1/3] Reorder fix --- packages/core/src/core/SimpleTableVanilla.ts | 1 + .../src/core/rendering/RenderOrchestrator.ts | 2 + .../core/src/core/rendering/TableRenderer.ts | 3 +- packages/examples/shared/src/types/crm.ts | 4 +- .../examples/vanilla/src/demos/crm/CRMDemo.ts | 294 ++++++++++++++---- 5 files changed, 248 insertions(+), 56 deletions(-) diff --git a/packages/core/src/core/SimpleTableVanilla.ts b/packages/core/src/core/SimpleTableVanilla.ts index 96b9352e6..d310103ee 100644 --- a/packages/core/src/core/SimpleTableVanilla.ts +++ b/packages/core/src/core/SimpleTableVanilla.ts @@ -518,6 +518,7 @@ export class SimpleTableVanilla { getCollapsedRows: () => this.collapsedRows, getCollapsedHeaders: () => this.collapsedHeaders, getExpandedRows: () => this.expandedRows, + getHeaders: () => this.headers, getRowStateMap: () => this.rowStateMap, setColumnEditorOpen: (open: boolean) => { this.columnEditorOpen = open; diff --git a/packages/core/src/core/rendering/RenderOrchestrator.ts b/packages/core/src/core/rendering/RenderOrchestrator.ts index 13be18603..72e13d14f 100644 --- a/packages/core/src/core/rendering/RenderOrchestrator.ts +++ b/packages/core/src/core/rendering/RenderOrchestrator.ts @@ -42,6 +42,7 @@ export interface RenderContext { getCollapsedRows: () => Map; getCollapsedHeaders?: () => Set; getExpandedRows: () => Map; + getHeaders: () => HeaderObject[]; getRowStateMap: () => Map; headerRegistry: Map; headers: HeaderObject[]; @@ -655,6 +656,7 @@ export class RenderOrchestrator { getCollapsedRows: context.getCollapsedRows, getCollapsedHeaders: context.getCollapsedHeaders, getExpandedRows: context.getExpandedRows, + getHeaders: context.getHeaders, getRowStateMap: context.getRowStateMap, positionOnlyBody: context.positionOnlyBody, essentialAccessors: context.essentialAccessors, diff --git a/packages/core/src/core/rendering/TableRenderer.ts b/packages/core/src/core/rendering/TableRenderer.ts index f57c9606c..e3a306275 100644 --- a/packages/core/src/core/rendering/TableRenderer.ts +++ b/packages/core/src/core/rendering/TableRenderer.ts @@ -40,6 +40,7 @@ export interface TableRendererDeps { getCollapsedHeaders?: () => Set; getCollapsedRows: () => Map; getExpandedRows: () => Map; + getHeaders: () => HeaderObject[]; getRowStateMap: () => Map; headerRegistry: Map; headers: HeaderObject[]; @@ -205,7 +206,7 @@ export class TableRenderer { }, setHeaders: (value: any) => { if (typeof value === "function") { - deps.setHeaders(value(deps.headers)); + deps.setHeaders(value(deps.getHeaders())); } else { deps.setHeaders(value); } diff --git a/packages/examples/shared/src/types/crm.ts b/packages/examples/shared/src/types/crm.ts index c017840f7..0ba6ed241 100644 --- a/packages/examples/shared/src/types/crm.ts +++ b/packages/examples/shared/src/types/crm.ts @@ -1,4 +1,4 @@ -export interface CRMLead { +export type CRMLead = { id: number; name: string; title: string; @@ -9,4 +9,4 @@ export interface CRMLead { emailStatus: string; timeAgo: string; list: string; -} +}; diff --git a/packages/examples/vanilla/src/demos/crm/CRMDemo.ts b/packages/examples/vanilla/src/demos/crm/CRMDemo.ts index e9bc313c7..add2a9536 100644 --- a/packages/examples/vanilla/src/demos/crm/CRMDemo.ts +++ b/packages/examples/vanilla/src/demos/crm/CRMDemo.ts @@ -20,18 +20,22 @@ function el(tag: string, styles?: Partial, text?: string): } function createEmailEnrich(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { - const wrapper = el("span", { - cursor: "pointer", - alignItems: "center", - columnGap: "6px", - borderRadius: "9999px", - backgroundColor: "color-mix(in oklab, oklch(62.3% .214 259.815) 10%, transparent)", - paddingInline: "8px", - paddingBlock: "4px", - fontSize: "12px", - fontWeight: "500", - color: colors.tagText, - }, "Enrich"); + const wrapper = el( + "span", + { + cursor: "pointer", + alignItems: "center", + columnGap: "6px", + borderRadius: "9999px", + backgroundColor: "color-mix(in oklab, oklch(62.3% .214 259.815) 10%, transparent)", + paddingInline: "8px", + paddingBlock: "4px", + fontSize: "12px", + fontWeight: "500", + color: colors.tagText, + }, + "Enrich", + ); let isLoading = false; let email: string | null = null; @@ -86,10 +90,28 @@ function createFitButtons(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { color: colors.buttonText, }; - const buttons: Array<{ key: string; label: string; activeBg: string; normalBg: string; radius?: Partial }> = [ - { key: "fit", label: "✓", activeBg: "oklch(62.7% .194 149.214)", normalBg: "oklch(92.5% .084 155.995)", radius: { borderTopLeftRadius: "6px", borderBottomLeftRadius: "6px" } }, + const buttons: Array<{ + key: string; + label: string; + activeBg: string; + normalBg: string; + radius?: Partial; + }> = [ + { + key: "fit", + label: "✓", + activeBg: "oklch(62.7% .194 149.214)", + normalBg: "oklch(92.5% .084 155.995)", + radius: { borderTopLeftRadius: "6px", borderBottomLeftRadius: "6px" }, + }, { key: "partial", label: "?", activeBg: colors.buttonHoverBg, normalBg: colors.buttonBg }, - { key: "no", label: "X", activeBg: "oklch(64.6% .222 41.116)", normalBg: "oklch(90.1% .076 70.697)", radius: { borderTopRightRadius: "6px", borderBottomRightRadius: "6px" } }, + { + key: "no", + label: "X", + activeBg: "oklch(64.6% .222 41.116)", + normalBg: "oklch(90.1% .076 70.697)", + radius: { borderTopRightRadius: "6px", borderBottomRightRadius: "6px" }, + }, ]; const btnEls: HTMLButtonElement[] = []; @@ -101,7 +123,8 @@ function createFitButtons(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { btn.addEventListener("click", () => { selected = selected === b.key ? null : b.key; btnEls.forEach((be, i) => { - be.style.backgroundColor = selected === buttons[i].key ? buttons[i].activeBg : buttons[i].normalBg; + be.style.backgroundColor = + selected === buttons[i].key ? buttons[i].activeBg : buttons[i].normalBg; }); }); btnEls.push(btn); @@ -116,18 +139,37 @@ function getCRMHeaders(isDark: boolean): HeaderObject[] { const contactRenderer: CellRenderer = ({ row }) => { const d = row as unknown as CRMLead; - const initials = d.name.split(" ").map((n) => n[0]).join("").toUpperCase(); + const initials = d.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase(); const wrapper = el("div", { display: "flex", alignItems: "center", gap: "12px" }); - const avatar = el("div", { - width: "40px", height: "40px", borderRadius: "50%", - background: "linear-gradient(to right, oklch(75% .183 55.934), oklch(70.4% .191 22.216))", - color: "white", display: "flex", alignItems: "center", justifyContent: "center", - fontSize: "12px", fontWeight: "600", flexShrink: "0", - }, initials); + const avatar = el( + "div", + { + width: "40px", + height: "40px", + borderRadius: "50%", + background: "linear-gradient(to right, oklch(75% .183 55.934), oklch(70.4% .191 22.216))", + color: "white", + display: "flex", + alignItems: "center", + justifyContent: "center", + fontSize: "12px", + fontWeight: "600", + flexShrink: "0", + }, + initials, + ); const info = el("div", { display: "flex", flexDirection: "column", gap: "2px" }); - const nameEl = el("span", { cursor: "pointer", fontSize: "14px", fontWeight: "600", color: colors.link }, d.name); + const nameEl = el( + "span", + { cursor: "pointer", fontSize: "14px", fontWeight: "600", color: colors.link }, + d.name, + ); const titleEl = el("div", { fontSize: "12px", color: colors.textSecondary }, d.title); const companyEl = el("div", { fontSize: "12px", color: colors.textSecondary }); const atSign = el("span", { fontSize: "12px", color: colors.textTertiary }, "@"); @@ -142,9 +184,17 @@ function getCRMHeaders(isDark: boolean): HeaderObject[] { const signalRenderer: CellRenderer = ({ row }) => { const d = row as unknown as CRMLead; const wrapper = el("div"); - const line1 = el("div", { color: colors.textSecondary, marginBottom: "4px", fontSize: "0.875rem" }); + const line1 = el("div", { + color: colors.textSecondary, + marginBottom: "4px", + fontSize: "0.875rem", + }); line1.textContent = "🧠 Just engaged with a "; - const link = el("a", { color: "#0077b5", textDecoration: "underline", cursor: "pointer" }, "post"); + const link = el( + "a", + { color: "#0077b5", textDecoration: "underline", cursor: "pointer" }, + "post", + ); (link as HTMLAnchorElement).href = "#"; link.addEventListener("click", (e) => e.preventDefault()); line1.appendChild(link); @@ -170,7 +220,17 @@ function getCRMHeaders(isDark: boolean): HeaderObject[] { const listRenderer: CellRenderer = ({ row }) => { const d = row as unknown as CRMLead; - const link = el("a", { cursor: "pointer", fontSize: "0.875rem", color: colors.link, textDecoration: "none", fontWeight: "600" }, d.list); + const link = el( + "a", + { + cursor: "pointer", + fontSize: "0.875rem", + color: colors.link, + textDecoration: "none", + fontWeight: "600", + }, + d.list, + ); (link as HTMLAnchorElement).href = "#"; link.addEventListener("click", (e) => e.preventDefault()); return link; @@ -179,42 +239,143 @@ function getCRMHeaders(isDark: boolean): HeaderObject[] { const fitRenderer: CellRenderer = () => createFitButtons(colors); const contactNowRenderer: CellRenderer = () => { - const link = el("a", { cursor: "pointer", fontSize: "0.875rem", color: colors.link, textDecoration: "none", fontWeight: "600" }, "Contact Now"); + const link = el( + "a", + { + cursor: "pointer", + fontSize: "0.875rem", + color: colors.link, + textDecoration: "none", + fontWeight: "600", + }, + "Contact Now", + ); (link as HTMLAnchorElement).href = "#"; link.addEventListener("click", (e) => e.preventDefault()); return link; }; return [ - { accessor: "name", label: "CONTACT", width: "2fr", minWidth: 290, isSortable: true, isEditable: true, type: "string", cellRenderer: contactRenderer }, - { accessor: "signal", label: "SIGNAL", width: "3fr", minWidth: 340, isSortable: true, isEditable: true, type: "string", cellRenderer: signalRenderer }, - { accessor: "aiScore", label: "AI SCORE", width: "1fr", minWidth: 100, isSortable: true, align: "center", type: "number", cellRenderer: aiScoreRenderer }, { - accessor: "emailStatus", label: "EMAIL", width: "1.5fr", minWidth: 210, isSortable: true, align: "center", type: "enum", - enumOptions: [{ label: "Enrich", value: "Enrich" }, { label: "Verified", value: "Verified" }, { label: "Pending", value: "Pending" }, { label: "Bounced", value: "Bounced" }], + accessor: "name", + label: "CONTACT", + width: "2fr", + minWidth: 290, + isSortable: true, + isEditable: true, + type: "string", + cellRenderer: contactRenderer, + }, + { + accessor: "signal", + label: "SIGNAL", + width: "3fr", + minWidth: 340, + isSortable: true, + isEditable: true, + type: "string", + cellRenderer: signalRenderer, + }, + { + accessor: "aiScore", + label: "AI SCORE", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "number", + cellRenderer: aiScoreRenderer, + }, + { + accessor: "emailStatus", + label: "EMAIL", + width: "1.5fr", + minWidth: 210, + isSortable: true, + align: "center", + type: "enum", + enumOptions: [ + { label: "Enrich", value: "Enrich" }, + { label: "Verified", value: "Verified" }, + { label: "Pending", value: "Pending" }, + { label: "Bounced", value: "Bounced" }, + ], cellRenderer: emailRenderer, }, - { accessor: "timeAgo", label: "IMPORT", width: "1fr", minWidth: 100, isSortable: true, align: "center", type: "string", cellRenderer: timeAgoRenderer }, { - accessor: "list", label: "LIST", width: "1.2fr", minWidth: 160, isSortable: 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" }], + accessor: "timeAgo", + label: "IMPORT", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "string", + cellRenderer: timeAgoRenderer, + }, + { + accessor: "list", + label: "LIST", + width: "1.2fr", + minWidth: 160, + isSortable: 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 }; + 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: listRenderer, }, - { accessor: "_fit", label: "Fit", width: "1fr", align: "center", minWidth: 120, cellRenderer: fitRenderer }, - { accessor: "_contactNow", label: "", width: "1.2fr", minWidth: 160, cellRenderer: contactNowRenderer }, + { + accessor: "_fit", + label: "Fit", + width: "1fr", + align: "center", + minWidth: 120, + cellRenderer: fitRenderer, + }, + { + accessor: "_contactNow", + label: "", + width: "1.2fr", + minWidth: 160, + cellRenderer: contactNowRenderer, + }, ]; } -function createCRMFooter(props: FooterRendererProps, footerColors: typeof CRM_FOOTER_COLORS_LIGHT, rowsPerPage: number, onRowsPerPageChange: (n: number) => void): HTMLElement { +function createCRMFooter( + props: FooterRendererProps, + footerColors: typeof CRM_FOOTER_COLORS_LIGHT, + rowsPerPage: number, + onRowsPerPageChange: (n: number) => void, +): HTMLElement { const c = footerColors; const wrapper = el("div", { - display: "flex", alignItems: "center", justifyContent: "space-between", - padding: "12px 16px", borderTop: `1px solid ${c.border}`, backgroundColor: c.bg, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px 16px", + borderTop: `1px solid ${c.border}`, + backgroundColor: c.bg, }); const info = el("p", { fontSize: "14px", color: c.text, margin: "0" }); @@ -228,8 +389,13 @@ function createCRMFooter(props: FooterRendererProps, footerColors: typeof CRM_FO const select = document.createElement("select"); Object.assign(select.style, { - border: `1px solid ${c.inputBorder}`, borderRadius: "6px", padding: "4px 8px", - fontSize: "14px", backgroundColor: c.inputBg, color: c.text, cursor: "pointer", + border: `1px solid ${c.inputBorder}`, + borderRadius: "6px", + padding: "4px 8px", + fontSize: "14px", + backgroundColor: c.inputBg, + color: c.text, + cursor: "pointer", }); for (const opt of [25, 50, 100, 200, 10000]) { const option = document.createElement("option"); @@ -246,16 +412,26 @@ function createCRMFooter(props: FooterRendererProps, footerColors: typeof CRM_FO perPageContainer.appendChild(el("span", { fontSize: "14px", color: c.text }, "per page")); right.appendChild(perPageContainer); - const nav = el("nav", { display: "inline-flex", borderRadius: "6px", boxShadow: "0 1px 2px 0 rgba(0,0,0,0.05)" }); + const nav = el("nav", { + display: "inline-flex", + borderRadius: "6px", + boxShadow: "0 1px 2px 0 rgba(0,0,0,0.05)", + }); const makePageBtn = (label: string, onClick: () => void, disabled: boolean, active = false) => { const btn = document.createElement("button"); btn.textContent = label; Object.assign(btn.style, { - display: "inline-flex", alignItems: "center", padding: "8px", - border: `1px solid ${c.buttonBorder}`, backgroundColor: active ? c.activeBg : c.buttonBg, - fontSize: "14px", fontWeight: "500", color: active ? c.activeText : disabled ? c.buttonText : c.text, - cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? "0.5" : "1", + display: "inline-flex", + alignItems: "center", + padding: "8px", + border: `1px solid ${c.buttonBorder}`, + backgroundColor: active ? c.activeBg : c.buttonBg, + fontSize: "14px", + fontWeight: "500", + color: active ? c.activeText : disabled ? c.buttonText : c.text, + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? "0.5" : "1", }); btn.disabled = disabled; if (!disabled) btn.addEventListener("click", onClick); @@ -268,14 +444,23 @@ function createCRMFooter(props: FooterRendererProps, footerColors: typeof CRM_FO const visiblePages = generateVisiblePages(props.currentPage, props.totalPages); for (const page of visiblePages) { - const btn = makePageBtn(String(page), () => props.onPageChange(page), false, page === props.currentPage); + const btn = makePageBtn( + String(page), + () => props.onPageChange(page), + false, + page === props.currentPage, + ); btn.style.padding = "8px 16px"; btn.style.marginLeft = "-1px"; nav.appendChild(btn); } const nextBtn = makePageBtn("›", () => props.onNextPage(), !props.hasNextPage); - Object.assign(nextBtn.style, { borderTopRightRadius: "6px", borderBottomRightRadius: "6px", marginLeft: "-1px" }); + Object.assign(nextBtn.style, { + borderTopRightRadius: "6px", + borderBottomRightRadius: "6px", + marginLeft: "-1px", + }); nav.appendChild(nextBtn); right.appendChild(nav); @@ -287,7 +472,10 @@ export function renderCRMDemo( container: HTMLElement, options?: { height?: string | number; theme?: Theme }, ): SimpleTableVanilla { - const isDark = options?.theme === "custom-dark" || options?.theme === "dark" || options?.theme === "modern-dark"; + const isDark = + options?.theme === "custom" || + options?.theme === "dark" || + options?.theme === "modern-dark"; const footerColors = isDark ? CRM_FOOTER_COLORS_DARK : CRM_FOOTER_COLORS_LIGHT; const themeContainer = el("div"); @@ -310,7 +498,7 @@ export function renderCRMDemo( footerRenderer: (props) => createCRMFooter(props, footerColors, rowsPerPage, (newVal) => { rowsPerPage = newVal; - table.getAPI().setRowsPerPage(newVal); + table.update({ rowsPerPage: newVal }); }), }); From c409e1f60520dafec1065f94fa854fc0e570c5ad Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:31:41 -0500 Subject: [PATCH 2/3] Icon improvement --- packages/core/src/utils/bodyCell/expansion.ts | 23 ++++++- .../columnEditor/createColumnEditorPopout.ts | 20 +++--- .../columnEditor/createColumnEditorRow.ts | 65 +++++++++++++++---- .../src/utils/footer/createTableFooter.ts | 2 + .../core/src/utils/headerCell/collapsing.ts | 25 ++----- 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/packages/core/src/utils/bodyCell/expansion.ts b/packages/core/src/utils/bodyCell/expansion.ts index 6118827cb..b3a09dd6e 100644 --- a/packages/core/src/utils/bodyCell/expansion.ts +++ b/packages/core/src/utils/bodyCell/expansion.ts @@ -152,20 +152,39 @@ export const createExpandIcon = ( return outerContainer; }; +export type UpdateExpandIconStateOptions = { + /** aria-label when the group is expanded (chevron shows collapse action). */ + ariaLabelWhenExpanded?: string; + /** aria-label when the group is collapsed (chevron shows expand action). */ + ariaLabelWhenCollapsed?: string; + /** When true, sets aria-expanded to match isExpanded after the toggle. */ + syncAriaExpanded?: boolean; +}; + /** Update expand/collapse icon direction on an existing cell (e.g. after expand state changes for nested grids). */ -export const updateExpandIconState = (cellElement: HTMLElement, isExpanded: boolean): void => { +export const updateExpandIconState = ( + cellElement: HTMLElement, + isExpanded: boolean, + options?: UpdateExpandIconStateOptions, +): void => { const iconContainer = cellElement.querySelector(".st-expand-icon-container"); if (!iconContainer || !(iconContainer instanceof HTMLElement)) return; const currentlyExpanded = iconContainer.classList.contains("expanded"); if (currentlyExpanded === isExpanded) return; + const labelExpanded = options?.ariaLabelWhenExpanded ?? "Collapse row"; + const labelCollapsed = options?.ariaLabelWhenCollapsed ?? "Expand row"; + // Defer class toggle so the browser paints the current state first, then we apply the new state // and the CSS transition runs. Use double rAF so the first paint has committed. requestAnimationFrame(() => { requestAnimationFrame(() => { iconContainer.classList.toggle("expanded", isExpanded); iconContainer.classList.toggle("collapsed", !isExpanded); - iconContainer.setAttribute("aria-label", isExpanded ? "Collapse row" : "Expand row"); + iconContainer.setAttribute("aria-label", isExpanded ? labelExpanded : labelCollapsed); + if (options?.syncAriaExpanded) { + iconContainer.setAttribute("aria-expanded", String(isExpanded)); + } }); }); }; diff --git a/packages/core/src/utils/columnEditor/createColumnEditorPopout.ts b/packages/core/src/utils/columnEditor/createColumnEditorPopout.ts index 24e1e04be..6d3a81184 100644 --- a/packages/core/src/utils/columnEditor/createColumnEditorPopout.ts +++ b/packages/core/src/utils/columnEditor/createColumnEditorPopout.ts @@ -235,8 +235,9 @@ export const createColumnEditorPopout = (initialOptions: CreateColumnEditorPopou }; const setExpandedHeaders = (newHeaders: Set) => { + const previousExpandedHeaders = expandedHeaders; expandedHeaders = newHeaders; - render(); + render(previousExpandedHeaders); }; const updateSeparatorVisibility = () => { @@ -309,6 +310,7 @@ export const createColumnEditorPopout = (initialOptions: CreateColumnEditorPopou panelSection: PanelSection, label: string | null, targetContainer: HTMLElement, + previousExpandedHeaders?: ReadonlySet, ) => { if (sectionHeaders.length === 0) return; @@ -336,7 +338,7 @@ export const createColumnEditorPopout = (initialOptions: CreateColumnEditorPopou const hasChildren = doesAnyHeaderHaveChildren(sectionHeaders); flattenedHeaders.forEach((flatItem) => { - const rowFragment = createColumnEditorRow({ + const rowResult = createColumnEditorRow({ allHeaders: headers, clearHoverSeparator, depth: flatItem.depth, @@ -360,25 +362,27 @@ export const createColumnEditorPopout = (initialOptions: CreateColumnEditorPopou setHeaders, onColumnVisibilityChange, onColumnOrderChange, + previousExpandedHeaders, }); - listEl.appendChild(rowFragment); + listEl.appendChild(rowResult.fragment); + rowResult.scheduleExpandIconAnimation?.(); }); }; - const render = () => { + const render = (previousExpandedHeaders?: ReadonlySet) => { listsContainer.innerHTML = ""; const allowColumnPinning = columnEditorConfig.allowColumnPinning !== false; const { pinnedLeft, unpinned, pinnedRight } = partitionRootHeadersByPin(headers); if (allowColumnPinning) { - renderSection(pinnedLeft, "left", "Pinned Left", listsContainer); - renderSection(unpinned, "main", null, listsContainer); - renderSection(pinnedRight, "right", "Pinned Right", listsContainer); + renderSection(pinnedLeft, "left", "Pinned Left", listsContainer, previousExpandedHeaders); + renderSection(unpinned, "main", null, listsContainer, previousExpandedHeaders); + renderSection(pinnedRight, "right", "Pinned Right", listsContainer, previousExpandedHeaders); } else { const allHeaders = [...pinnedLeft, ...unpinned, ...pinnedRight]; - renderSection(allHeaders, "main", null, listsContainer); + renderSection(allHeaders, "main", null, listsContainer, previousExpandedHeaders); } }; diff --git a/packages/core/src/utils/columnEditor/createColumnEditorRow.ts b/packages/core/src/utils/columnEditor/createColumnEditorRow.ts index cdcd549c8..9f5b324cd 100644 --- a/packages/core/src/utils/columnEditor/createColumnEditorRow.ts +++ b/packages/core/src/utils/columnEditor/createColumnEditorRow.ts @@ -19,6 +19,8 @@ import { validateFullHeaderTreeEssentialOrder, PanelSection, } from "../pinnedColumnUtils"; +import { createAngleRightIcon } from "../../icons"; +import { updateExpandIconState } from "../bodyCell/expansion"; const DRAG_ICON_SVG = ` `; -const EXPAND_ICON_SVG = ` - -`; - export interface CreateColumnEditorRowOptions { allHeaders: HeaderObject[]; clearHoverSeparator?: () => void; @@ -68,9 +61,17 @@ export interface CreateColumnEditorRowOptions { setHeaders: (headers: HeaderObject[]) => void; onColumnVisibilityChange?: (state: ColumnVisibilityState) => void; onColumnOrderChange?: (headers: HeaderObject[]) => void; + /** When set (e.g. after expand toggle), used with updateExpandIconState so the chevron animates like table cells. */ + previousExpandedHeaders?: ReadonlySet; } -export const createColumnEditorRow = (options: CreateColumnEditorRowOptions) => { +export interface CreateColumnEditorRowResult { + fragment: DocumentFragment; + /** Run after the row fragment is connected to the document (e.g. listEl.appendChild). */ + scheduleExpandIconAnimation?: () => void; +} + +export const createColumnEditorRow = (options: CreateColumnEditorRowOptions): CreateColumnEditorRowResult => { const { allHeaders, clearHoverSeparator, @@ -93,6 +94,7 @@ export const createColumnEditorRow = (options: CreateColumnEditorRowOptions) => setHeaders, onColumnVisibilityChange, onColumnOrderChange, + previousExpandedHeaders, } = options; const essentialAccessors: ReadonlySet = options.essentialAccessors ?? new Set(); @@ -112,6 +114,8 @@ export const createColumnEditorRow = (options: CreateColumnEditorRowOptions) => const isExpanded = expandedHeaders.has(header.accessor); const shouldExpand = forceExpanded || isExpanded; + let scheduleExpandIconAnimation: (() => void) | undefined; + if (rowIndex === 0) { const topSeparator = document.createElement("div"); topSeparator.className = "st-column-editor-drag-separator"; @@ -314,14 +318,49 @@ export const createColumnEditorRow = (options: CreateColumnEditorRowOptions) => iconContainer.className = "st-header-icon-container"; if (hasChildren) { + const wasExpandedForIcon = + previousExpandedHeaders !== undefined + ? forceExpanded || previousExpandedHeaders.has(header.accessor) + : shouldExpand; + const shouldAnimateExpandIcon = + !forceExpanded && + previousExpandedHeaders !== undefined && + wasExpandedForIcon !== shouldExpand; + + const iconShowsExpanded = shouldAnimateExpandIcon ? wasExpandedForIcon : shouldExpand; + const expandIcon = document.createElement("div"); expandIcon.className = `st-collapsible-header-icon st-expand-icon-container ${ - shouldExpand ? "expanded" : "collapsed" + iconShowsExpanded ? "expanded" : "collapsed" }`; - expandIcon.innerHTML = EXPAND_ICON_SVG; + expandIcon.setAttribute("role", "button"); + expandIcon.setAttribute("tabindex", "0"); + expandIcon.setAttribute("aria-expanded", String(iconShowsExpanded)); + expandIcon.setAttribute( + "aria-label", + iconShowsExpanded ? `Collapse ${header.label} column` : `Expand ${header.label} column`, + ); + expandIcon.appendChild(createAngleRightIcon("st-expand-icon")); expandIcon.addEventListener("click", toggleExpanded); + expandIcon.addEventListener("keydown", (event: Event) => { + const keyEvent = event as KeyboardEvent; + if (keyEvent.key === "Enter" || keyEvent.key === " ") { + keyEvent.preventDefault(); + toggleExpanded(event); + } + }); iconContainer.appendChild(expandIcon); expandIconEl = expandIcon; + + if (shouldAnimateExpandIcon) { + scheduleExpandIconAnimation = () => { + updateExpandIconState(rowContainer, shouldExpand, { + ariaLabelWhenExpanded: `Collapse ${header.label} column`, + ariaLabelWhenCollapsed: `Expand ${header.label} column`, + syncAriaExpanded: true, + }); + }; + } } if (!options.columnEditorConfig.rowRenderer) { @@ -457,5 +496,5 @@ export const createColumnEditorRow = (options: CreateColumnEditorRowOptions) => bottomSeparator.style.opacity = hoveredSeparatorIndex === rowIndex ? "1" : "0"; fragment.appendChild(bottomSeparator); - return fragment; + return { fragment, scheduleExpandIconAnimation }; }; diff --git a/packages/core/src/utils/footer/createTableFooter.ts b/packages/core/src/utils/footer/createTableFooter.ts index 5d12b39c7..32e0d2c7a 100644 --- a/packages/core/src/utils/footer/createTableFooter.ts +++ b/packages/core/src/utils/footer/createTableFooter.ts @@ -1,6 +1,7 @@ import OnNextPage from "../../types/OnNextPage"; const PREV_ICON_SVG = ``; const NEXT_ICON_SVG = ` { - const iconContainer = cellElement.querySelector(".st-expand-icon-container"); - if (!iconContainer || !(iconContainer instanceof HTMLElement)) return; - const currentlyCollapsed = iconContainer.classList.contains("collapsed"); - if (currentlyCollapsed === isCollapsed) return; - - const ariaLabel = label - ? `${isCollapsed ? "Expand" : "Collapse"} ${label} column` - : isCollapsed - ? "Expand column" - : "Collapse column"; - - requestAnimationFrame(() => { - requestAnimationFrame(() => { - iconContainer.classList.toggle("expanded", !isCollapsed); - iconContainer.classList.toggle("collapsed", isCollapsed); - iconContainer.setAttribute("aria-label", ariaLabel); - iconContainer.setAttribute("aria-expanded", String(!isCollapsed)); - }); + updateExpandIconState(cellElement, !isCollapsed, { + ariaLabelWhenExpanded: label ? `Collapse ${label} column` : "Collapse column", + ariaLabelWhenCollapsed: label ? `Expand ${label} column` : "Expand column", + syncAriaExpanded: true, }); }; From e7428bf0f7c25a378d7e5302f9c0fe5333661675 Mon Sep 17 00:00:00 2001 From: peter <20213436+petera2c@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:32:43 -0500 Subject: [PATCH 3/3] Type improvements --- packages/angular/package.json | 2 +- packages/angular/src/buildVanillaConfig.ts | 25 +- .../angular/src/defaultHeadersFromCore.ts | 14 + packages/angular/src/index.ts | 9 + .../angular/src/lib/SimpleTableComponent.ts | 3 +- packages/angular/src/types.ts | 56 ++- packages/core/package.json | 2 +- packages/core/src/core/SimpleTableVanilla.ts | 8 +- .../core/initialization/TableInitializer.ts | 5 +- packages/core/src/index.ts | 3 +- packages/core/src/styles/base.css | 5 + .../core/src/styles/themes/modern-dark.css | 12 - .../core/src/styles/themes/modern-light.css | 12 - packages/core/src/types/HeaderObject.ts | 8 - packages/core/src/types/SimpleTableConfig.ts | 2 - packages/core/src/types/SimpleTableProps.ts | 11 - packages/core/src/types/TableRefType.ts | 77 ----- packages/core/src/utils/asRows.ts | 10 + .../core/src/utils/deprecatedPropsWarnings.ts | 100 ------ .../src/utils/footer/createTableFooter.ts | 28 +- .../tests/37-TableRefMethodsTests.stories.ts | 2 +- packages/examples/angular/package.json | 1 - .../examples/angular/src/app.component.ts | 2 +- .../index.ts => angular/src/demo-list.ts} | 0 .../aggregate-functions-demo.component.ts | 6 +- .../aggregate-functions.demo-data.ts | 193 +++++++++++ .../demos/billing/billing-demo.component.ts | 14 +- .../src/demos/billing/billing.demo-data.ts | 199 +++++++++++ .../cell-clicking-demo.component.ts | 10 +- .../cell-clicking/cell-clicking.demo-data.ts | 47 +++ .../cell-editing-demo.component.ts | 6 +- .../cell-editing/cell-editing.demo-data.ts | 38 ++ .../cell-highlighting-demo.component.ts | 6 +- .../cell-highlighting.demo-data.ts | 33 ++ .../cell-renderer-demo.component.ts | 16 +- .../cell-renderer/cell-renderer.demo-data.ts | 51 +++ .../src/demos/charts/charts-demo.component.ts | 6 +- .../src/demos/charts/charts.demo-data.ts | 50 +++ .../collapsible-columns-demo.component.ts | 6 +- .../collapsible-columns.demo-data.ts | 91 +++++ .../column-alignment-demo.component.ts | 6 +- .../column-alignment.demo-data.ts | 31 ++ .../column-editing-demo.component.ts | 6 +- .../column-editing.demo-data.ts | 32 ++ ...n-editor-custom-renderer-demo.component.ts | 6 +- ...column-editor-custom-renderer.demo-data.ts | 83 +++++ .../column-filtering-demo.component.ts | 6 +- .../column-filtering.demo-data.ts | 222 ++++++++++++ .../column-pinning-demo.component.ts | 6 +- .../column-pinning.demo-data.ts | 37 ++ .../column-reordering-demo.component.ts | 12 +- .../column-reordering.demo-data.ts | 32 ++ .../column-resizing-demo.component.ts | 12 +- .../column-resizing.demo-data.ts | 34 ++ .../column-selection-demo.component.ts | 6 +- .../column-selection.demo-data.ts | 33 ++ .../column-sorting-demo.component.ts | 6 +- .../column-sorting.demo-data.ts | 147 ++++++++ .../column-visibility-demo.component.ts | 6 +- .../column-visibility.demo-data.ts | 45 +++ .../column-width-demo.component.ts | 6 +- .../column-width/column-width.demo-data.ts | 34 ++ .../src/demos/crm}/crm-custom-theme.css | 0 .../src/demos/crm/crm-demo.component.ts | 327 ++++++++++++++---- .../angular/src/demos/crm/crm.demo-data.ts | 178 ++++++++++ .../csv-export/csv-export-demo.component.ts | 28 +- .../demos/csv-export/csv-export.demo-data.ts | 76 ++++ .../custom-icons-demo.component.ts | 10 +- .../custom-icons/custom-icons.demo-data.ts | 78 +++++ .../custom-theme-demo.component.ts | 8 +- .../src/demos/custom-theme}/custom-theme.css | 0 .../custom-theme/custom-theme.demo-data.ts | 48 +++ .../dynamic-nested-tables-demo.component.ts | 8 +- .../dynamic-nested-tables.demo-data.ts | 93 +++++ .../dynamic-row-loading-demo.component.ts | 8 +- .../dynamic-row-loading.demo-data.ts | 205 +++++++++++ .../empty-state/empty-state-demo.component.ts | 6 +- .../empty-state/empty-state.demo-data.ts | 68 ++++ .../external-filter-demo.component.ts | 6 +- .../external-filter.demo-data.ts | 128 +++++++ .../external-sort-demo.component.ts | 8 +- .../external-sort/external-sort.demo-data.ts | 40 +++ .../footer-renderer-demo.component.ts | 8 +- .../footer-renderer.demo-data.ts | 69 ++++ .../header-renderer-demo.component.ts | 16 +- .../header-renderer.demo-data.ts | 32 ++ .../angular/src/demos/hr/hr-demo.component.ts | 16 +- .../angular/src/demos/hr/hr.demo-data.ts | 126 +++++++ .../infinite-scroll-demo.component.ts | 6 +- .../infinite-scroll.demo-data.ts | 44 +++ .../infrastructure-demo.component.ts | 14 +- .../infrastructure.demo-data.ts | 263 ++++++++++++++ .../live-update/live-update-demo.component.ts | 6 +- .../live-update/live-update.demo-data.ts | 61 ++++ .../loading-state-demo.component.ts | 6 +- .../loading-state/loading-state.demo-data.ts | 34 ++ .../manufacturing-demo.component.ts | 20 +- .../manufacturing/manufacturing.demo-data.ts | 125 +++++++ .../src/demos/music/music-demo.component.ts | 36 +- .../src/demos/music}/music-theme.css | 0 .../src/demos/music/music.demo-data.ts | 215 ++++++++++++ .../nested-headers-demo.component.ts | 6 +- .../nested-headers.demo-data.ts | 41 +++ .../nested-tables-demo.component.ts | 6 +- .../nested-tables/nested-tables.demo-data.ts | 75 ++++ .../pagination/pagination-demo.component.ts | 6 +- .../demos/pagination/pagination.demo-data.ts | 53 +++ .../programmatic-control-demo.component.ts | 37 +- .../programmatic-control.demo-data.ts | 56 +++ .../quick-filter-demo.component.ts | 6 +- .../quick-filter/quick-filter.demo-data.ts | 33 ++ .../quick-start/quick-start-demo.component.ts | 6 +- .../quick-start/quick-start.demo-data.ts | 130 +++++++ .../row-grouping-demo.component.ts | 6 +- .../row-grouping/row-grouping.demo-data.ts | 205 +++++++++++ .../row-height/row-height-demo.component.ts | 6 +- .../demos/row-height/row-height.demo-data.ts | 32 ++ .../row-selection-demo.component.ts | 34 +- .../row-selection/row-selection.demo-data.ts | 56 +++ .../src/demos/sales/sales-demo.component.ts | 8 +- .../src/demos/sales/sales.demo-data.ts | 114 ++++++ .../single-row-children-demo.component.ts | 6 +- .../single-row-children.demo-data.ts | 47 +++ .../demos/spreadsheet}/spreadsheet-custom.css | 0 .../spreadsheet/spreadsheet-demo.component.ts | 13 +- .../spreadsheet/spreadsheet.demo-data.ts | 92 +++++ .../table-height-demo.component.ts | 6 +- .../table-height/table-height.demo-data.ts | 35 ++ .../src/demos/themes/themes-demo.component.ts | 6 +- .../src/demos/themes/themes.demo-data.ts | 42 +++ .../demos/tooltip/tooltip-demo.component.ts | 6 +- .../src/demos/tooltip/tooltip.demo-data.ts | 56 +++ .../value-formatter-demo.component.ts | 6 +- .../value-formatter.demo-data.ts | 105 ++++++ packages/examples/angular/src/main.ts | 2 +- .../{shared => angular}/src/styles/shell.css | 0 packages/examples/angular/vite.config.ts | 2 - packages/examples/react/package.json | 3 +- packages/examples/react/src/demo-list.ts | 57 +++ .../AggregateFunctionsDemo.tsx | 6 +- .../aggregate-functions.demo-data.ts | 193 +++++++++++ .../react/src/demos/billing/BillingDemo.tsx | 34 +- .../src/demos/billing/billing.demo-data.ts | 199 +++++++++++ .../demos/cell-clicking/CellClickingDemo.tsx | 24 +- .../cell-clicking/cell-clicking.demo-data.ts | 47 +++ .../demos/cell-editing/CellEditingDemo.tsx | 6 +- .../cell-editing/cell-editing.demo-data.ts | 38 ++ .../CellHighlightingDemo.tsx | 6 +- .../cell-highlighting.demo-data.ts | 33 ++ .../demos/cell-renderer/CellRendererDemo.tsx | 39 ++- .../cell-renderer/cell-renderer.demo-data.ts | 51 +++ .../react/src/demos/charts/ChartsDemo.tsx | 6 +- .../src/demos/charts/charts.demo-data.ts | 50 +++ .../CollapsibleColumnsDemo.tsx | 6 +- .../collapsible-columns.demo-data.ts | 91 +++++ .../column-alignment/ColumnAlignmentDemo.tsx | 6 +- .../column-alignment.demo-data.ts | 31 ++ .../column-editing/ColumnEditingDemo.tsx | 6 +- .../column-editing.demo-data.ts | 32 ++ .../ColumnEditorCustomRendererDemo.tsx | 6 +- ...column-editor-custom-renderer.demo-data.ts | 83 +++++ .../column-filtering/ColumnFilteringDemo.tsx | 6 +- .../column-filtering.demo-data.ts | 222 ++++++++++++ .../column-pinning/ColumnPinningDemo.tsx | 6 +- .../column-pinning.demo-data.ts | 37 ++ .../ColumnReorderingDemo.tsx | 10 +- .../column-reordering.demo-data.ts | 32 ++ .../column-resizing/ColumnResizingDemo.tsx | 18 +- .../column-resizing.demo-data.ts | 34 ++ .../column-selection/ColumnSelectionDemo.tsx | 6 +- .../column-selection.demo-data.ts | 33 ++ .../column-sorting/ColumnSortingDemo.tsx | 6 +- .../column-sorting.demo-data.ts | 147 ++++++++ .../ColumnVisibilityDemo.tsx | 6 +- .../column-visibility.demo-data.ts | 45 +++ .../demos/column-width/ColumnWidthDemo.tsx | 6 +- .../column-width/column-width.demo-data.ts | 34 ++ .../examples/react/src/demos/crm/CRMDemo.tsx | 20 +- .../react/src/demos/crm/crm-custom-theme.css | 161 +++++++++ .../react/src/demos/crm/crm.demo-data.ts | 177 ++++++++++ .../src/demos/csv-export/CsvExportDemo.tsx | 17 +- .../demos/csv-export/csv-export.demo-data.ts | 76 ++++ .../demos/custom-icons/CustomIconsDemo.tsx | 6 +- .../custom-icons/custom-icons.demo-data.ts | 78 +++++ .../demos/custom-theme/CustomThemeDemo.tsx | 8 +- .../src/demos/custom-theme/custom-theme.css | 90 +++++ .../custom-theme/custom-theme.demo-data.ts | 48 +++ .../DynamicNestedTablesDemo.tsx | 8 +- .../dynamic-nested-tables.demo-data.ts | 93 +++++ .../DynamicRowLoadingDemo.tsx | 8 +- .../dynamic-row-loading.demo-data.ts | 205 +++++++++++ .../src/demos/empty-state/EmptyStateDemo.tsx | 6 +- .../empty-state/empty-state.demo-data.ts | 68 ++++ .../external-filter/ExternalFilterDemo.tsx | 6 +- .../external-filter.demo-data.ts | 128 +++++++ .../demos/external-sort/ExternalSortDemo.tsx | 6 +- .../external-sort/external-sort.demo-data.ts | 40 +++ .../footer-renderer/FooterRendererDemo.tsx | 6 +- .../footer-renderer.demo-data.ts | 69 ++++ .../header-renderer/HeaderRendererDemo.tsx | 8 +- .../header-renderer.demo-data.ts | 32 ++ .../examples/react/src/demos/hr/HRDemo.tsx | 32 +- .../react/src/demos/hr/hr.demo-data.ts | 126 +++++++ .../infinite-scroll/InfiniteScrollDemo.tsx | 6 +- .../infinite-scroll.demo-data.ts | 44 +++ .../infrastructure/InfrastructureDemo.tsx | 18 +- .../infrastructure.demo-data.ts | 263 ++++++++++++++ .../src/demos/live-update/LiveUpdateDemo.tsx | 8 +- .../live-update/live-update.demo-data.ts | 61 ++++ .../demos/loading-state/LoadingStateDemo.tsx | 6 +- .../loading-state/loading-state.demo-data.ts | 34 ++ .../demos/manufacturing/ManufacturingDemo.tsx | 34 +- .../manufacturing/manufacturing.demo-data.ts | 125 +++++++ .../react/src/demos/music/MusicDemo.tsx | 50 +-- .../react/src/demos/music/music-theme.css | 13 + .../react/src/demos/music/music.demo-data.ts | 215 ++++++++++++ .../nested-headers/NestedHeadersDemo.tsx | 6 +- .../nested-headers.demo-data.ts | 41 +++ .../demos/nested-tables/NestedTablesDemo.tsx | 6 +- .../nested-tables/nested-tables.demo-data.ts | 75 ++++ .../src/demos/pagination/PaginationDemo.tsx | 6 +- .../demos/pagination/pagination.demo-data.ts | 53 +++ .../ProgrammaticControlDemo.tsx | 21 +- .../programmatic-control.demo-data.ts | 56 +++ .../demos/quick-filter/QuickFilterDemo.tsx | 6 +- .../quick-filter/quick-filter.demo-data.ts | 33 ++ .../src/demos/quick-start/QuickStartDemo.tsx | 6 +- .../quick-start/quick-start.demo-data.ts | 130 +++++++ .../demos/row-grouping/RowGroupingDemo.tsx | 6 +- .../row-grouping/row-grouping.demo-data.ts | 205 +++++++++++ .../src/demos/row-height/RowHeightDemo.tsx | 6 +- .../demos/row-height/row-height.demo-data.ts | 32 ++ .../demos/row-selection/RowSelectionDemo.tsx | 18 +- .../row-selection/row-selection.demo-data.ts | 56 +++ .../react/src/demos/sales/SalesDemo.tsx | 18 +- .../react/src/demos/sales/sales.demo-data.ts | 114 ++++++ .../SingleRowChildrenDemo.tsx | 6 +- .../single-row-children.demo-data.ts | 47 +++ .../src/demos/spreadsheet/SpreadsheetDemo.tsx | 14 +- .../demos/spreadsheet/spreadsheet-custom.css | 28 ++ .../spreadsheet/spreadsheet.demo-data.ts | 92 +++++ .../demos/table-height/TableHeightDemo.tsx | 6 +- .../table-height/table-height.demo-data.ts | 35 ++ .../react/src/demos/themes/ThemesDemo.tsx | 6 +- .../src/demos/themes/themes.demo-data.ts | 42 +++ .../react/src/demos/tooltip/TooltipDemo.tsx | 6 +- .../src/demos/tooltip/tooltip.demo-data.ts | 56 +++ .../value-formatter/ValueFormatterDemo.tsx | 6 +- .../value-formatter.demo-data.ts | 105 ++++++ packages/examples/react/src/main.tsx | 4 +- packages/examples/react/src/styles/shell.css | 63 ++++ packages/examples/react/vite.config.ts | 2 - packages/examples/shared/package.json | 22 -- .../src/configs/column-filtering-config.ts | 90 ----- .../src/configs/column-sorting-config.ts | 45 --- packages/examples/shared/src/configs/index.ts | 59 ---- .../shared/src/configs/quick-start-config.ts | 28 -- packages/examples/shared/src/data/index.ts | 3 - packages/examples/shared/src/index.ts | 4 - packages/examples/shared/src/types/billing.ts | 12 - packages/examples/shared/src/types/crm.ts | 12 - .../examples/shared/src/types/employee.ts | 10 - packages/examples/shared/src/types/hr.ts | 16 - packages/examples/shared/src/types/index.ts | 9 - .../shared/src/types/infrastructure.ts | 15 - .../shared/src/types/manufacturing.ts | 17 - packages/examples/shared/src/types/music.ts | 57 --- packages/examples/shared/src/types/sales.ts | 12 - .../examples/shared/src/types/spreadsheet.ts | 9 - packages/examples/shared/tsconfig.json | 16 - packages/examples/solid/package.json | 1 - packages/examples/solid/src/demo-list.ts | 57 +++ .../AggregateFunctionsDemo.tsx | 6 +- .../aggregate-functions.demo-data.ts | 193 +++++++++++ .../solid/src/demos/billing/BillingDemo.tsx | 34 +- .../src/demos/billing/billing.demo-data.ts | 199 +++++++++++ .../demos/cell-clicking/CellClickingDemo.tsx | 12 +- .../cell-clicking/cell-clicking.demo-data.ts | 47 +++ .../demos/cell-editing/CellEditingDemo.tsx | 6 +- .../cell-editing/cell-editing.demo-data.ts | 38 ++ .../CellHighlightingDemo.tsx | 6 +- .../cell-highlighting.demo-data.ts | 33 ++ .../demos/cell-renderer/CellRendererDemo.tsx | 35 +- .../cell-renderer/cell-renderer.demo-data.ts | 51 +++ .../solid/src/demos/charts/ChartsDemo.tsx | 6 +- .../src/demos/charts/charts.demo-data.ts | 50 +++ .../CollapsibleColumnsDemo.tsx | 6 +- .../collapsible-columns.demo-data.ts | 91 +++++ .../column-alignment/ColumnAlignmentDemo.tsx | 6 +- .../column-alignment.demo-data.ts | 31 ++ .../column-editing/ColumnEditingDemo.tsx | 12 +- .../column-editing.demo-data.ts | 32 ++ .../ColumnEditorCustomRendererDemo.tsx | 6 +- ...column-editor-custom-renderer.demo-data.ts | 83 +++++ .../column-filtering/ColumnFilteringDemo.tsx | 6 +- .../column-filtering.demo-data.ts | 222 ++++++++++++ .../column-pinning/ColumnPinningDemo.tsx | 6 +- .../column-pinning.demo-data.ts | 37 ++ .../ColumnReorderingDemo.tsx | 8 +- .../column-reordering.demo-data.ts | 32 ++ .../column-resizing/ColumnResizingDemo.tsx | 12 +- .../column-resizing.demo-data.ts | 34 ++ .../column-selection/ColumnSelectionDemo.tsx | 6 +- .../column-selection.demo-data.ts | 33 ++ .../column-sorting/ColumnSortingDemo.tsx | 6 +- .../column-sorting.demo-data.ts | 147 ++++++++ .../ColumnVisibilityDemo.tsx | 6 +- .../column-visibility.demo-data.ts | 45 +++ .../demos/column-width/ColumnWidthDemo.tsx | 6 +- .../column-width/column-width.demo-data.ts | 34 ++ .../examples/solid/src/demos/crm/CRMDemo.tsx | 15 +- .../solid/src/demos/crm/crm-custom-theme.css | 161 +++++++++ .../solid/src/demos/crm/crm.demo-data.ts | 177 ++++++++++ .../src/demos/csv-export/CsvExportDemo.tsx | 66 ++-- .../demos/csv-export/csv-export.demo-data.ts | 76 ++++ .../demos/custom-icons/CustomIconsDemo.tsx | 6 +- .../custom-icons/custom-icons.demo-data.ts | 78 +++++ .../demos/custom-theme/CustomThemeDemo.tsx | 8 +- .../src/demos/custom-theme/custom-theme.css | 90 +++++ .../custom-theme/custom-theme.demo-data.ts | 48 +++ .../DynamicNestedTablesDemo.tsx | 8 +- .../dynamic-nested-tables.demo-data.ts | 93 +++++ .../DynamicRowLoadingDemo.tsx | 8 +- .../dynamic-row-loading.demo-data.ts | 205 +++++++++++ .../src/demos/empty-state/EmptyStateDemo.tsx | 6 +- .../empty-state/empty-state.demo-data.ts | 68 ++++ .../external-filter/ExternalFilterDemo.tsx | 6 +- .../external-filter.demo-data.ts | 128 +++++++ .../demos/external-sort/ExternalSortDemo.tsx | 22 +- .../external-sort/external-sort.demo-data.ts | 40 +++ .../footer-renderer/FooterRendererDemo.tsx | 6 +- .../footer-renderer.demo-data.ts | 69 ++++ .../header-renderer/HeaderRendererDemo.tsx | 8 +- .../header-renderer.demo-data.ts | 32 ++ .../examples/solid/src/demos/hr/HRDemo.tsx | 266 +++++++++----- .../solid/src/demos/hr/hr.demo-data.ts | 126 +++++++ .../infinite-scroll/InfiniteScrollDemo.tsx | 6 +- .../infinite-scroll.demo-data.ts | 44 +++ .../infrastructure/InfrastructureDemo.tsx | 4 +- .../infrastructure.demo-data.ts | 263 ++++++++++++++ .../src/demos/live-update/LiveUpdateDemo.tsx | 6 +- .../live-update/live-update.demo-data.ts | 61 ++++ .../demos/loading-state/LoadingStateDemo.tsx | 6 +- .../loading-state/loading-state.demo-data.ts | 34 ++ .../demos/manufacturing/ManufacturingDemo.tsx | 36 +- .../manufacturing/manufacturing.demo-data.ts | 125 +++++++ .../solid/src/demos/music/MusicDemo.tsx | 6 +- .../solid/src/demos/music/music-theme.css | 13 + .../solid/src/demos/music/music.demo-data.ts | 215 ++++++++++++ .../nested-headers/NestedHeadersDemo.tsx | 6 +- .../nested-headers.demo-data.ts | 41 +++ .../demos/nested-tables/NestedTablesDemo.tsx | 6 +- .../nested-tables/nested-tables.demo-data.ts | 75 ++++ .../src/demos/pagination/PaginationDemo.tsx | 6 +- .../demos/pagination/pagination.demo-data.ts | 53 +++ .../ProgrammaticControlDemo.tsx | 69 ++-- .../programmatic-control.demo-data.ts | 56 +++ .../demos/quick-filter/QuickFilterDemo.tsx | 6 +- .../quick-filter/quick-filter.demo-data.ts | 33 ++ .../src/demos/quick-start/QuickStartDemo.tsx | 6 +- .../quick-start/quick-start.demo-data.ts | 130 +++++++ .../demos/row-grouping/RowGroupingDemo.tsx | 6 +- .../row-grouping/row-grouping.demo-data.ts | 205 +++++++++++ .../src/demos/row-height/RowHeightDemo.tsx | 6 +- .../demos/row-height/row-height.demo-data.ts | 32 ++ .../demos/row-selection/RowSelectionDemo.tsx | 12 +- .../row-selection/row-selection.demo-data.ts | 56 +++ .../solid/src/demos/sales/SalesDemo.tsx | 8 +- .../solid/src/demos/sales/sales.demo-data.ts | 114 ++++++ .../SingleRowChildrenDemo.tsx | 6 +- .../single-row-children.demo-data.ts | 47 +++ .../src/demos/spreadsheet/SpreadsheetDemo.tsx | 16 +- .../demos/spreadsheet/spreadsheet-custom.css | 28 ++ .../spreadsheet/spreadsheet.demo-data.ts | 92 +++++ .../demos/table-height/TableHeightDemo.tsx | 6 +- .../table-height/table-height.demo-data.ts | 35 ++ .../solid/src/demos/themes/ThemesDemo.tsx | 6 +- .../src/demos/themes/themes.demo-data.ts | 42 +++ .../solid/src/demos/tooltip/TooltipDemo.tsx | 6 +- .../src/demos/tooltip/tooltip.demo-data.ts | 56 +++ .../value-formatter/ValueFormatterDemo.tsx | 6 +- .../value-formatter.demo-data.ts | 105 ++++++ packages/examples/solid/src/main.tsx | 4 +- packages/examples/solid/src/styles/shell.css | 63 ++++ packages/examples/solid/vite.config.ts | 2 - packages/examples/svelte/package.json | 1 - packages/examples/svelte/src/App.svelte | 2 +- packages/examples/svelte/src/demo-list.ts | 57 +++ .../AggregateFunctionsDemo.svelte | 6 +- .../aggregate-functions.demo-data.ts | 193 +++++++++++ .../src/demos/billing/BillingDemo.svelte | 4 +- .../src/demos/billing/billing.demo-data.ts | 199 +++++++++++ .../cell-clicking/CellClickingDemo.svelte | 4 +- .../cell-clicking/cell-clicking.demo-data.ts | 47 +++ .../demos/cell-editing/CellEditingDemo.svelte | 6 +- .../cell-editing/cell-editing.demo-data.ts | 38 ++ .../CellHighlightingDemo.svelte | 6 +- .../cell-highlighting.demo-data.ts | 33 ++ .../cell-renderer/CellRendererDemo.svelte | 4 +- .../cell-renderer/cell-renderer.demo-data.ts | 51 +++ .../svelte/src/demos/charts/ChartsDemo.svelte | 6 +- .../src/demos/charts/charts.demo-data.ts | 50 +++ .../CollapsibleColumnsDemo.svelte | 6 +- .../collapsible-columns.demo-data.ts | 91 +++++ .../ColumnAlignmentDemo.svelte | 6 +- .../column-alignment.demo-data.ts | 31 ++ .../column-editing/ColumnEditingDemo.svelte | 2 +- .../column-editing.demo-data.ts | 32 ++ .../ColumnEditorCustomRendererDemo.svelte | 6 +- ...column-editor-custom-renderer.demo-data.ts | 83 +++++ .../ColumnFilteringDemo.svelte | 6 +- .../column-filtering.demo-data.ts | 222 ++++++++++++ .../column-pinning/ColumnPinningDemo.svelte | 6 +- .../column-pinning.demo-data.ts | 37 ++ .../ColumnReorderingDemo.svelte | 2 +- .../column-reordering.demo-data.ts | 32 ++ .../column-resizing/ColumnResizingDemo.svelte | 2 +- .../column-resizing.demo-data.ts | 34 ++ .../ColumnSelectionDemo.svelte | 6 +- .../column-selection.demo-data.ts | 33 ++ .../column-sorting/ColumnSortingDemo.svelte | 6 +- .../column-sorting.demo-data.ts | 147 ++++++++ .../ColumnVisibilityDemo.svelte | 6 +- .../column-visibility.demo-data.ts | 45 +++ .../demos/column-width/ColumnWidthDemo.svelte | 6 +- .../column-width/column-width.demo-data.ts | 34 ++ .../svelte/src/demos/crm/CRMDemo.svelte | 10 +- .../svelte/src/demos/crm/crm-custom-theme.css | 161 +++++++++ .../svelte/src/demos/crm/crm.demo-data.ts | 177 ++++++++++ .../src/demos/csv-export/CsvExportDemo.svelte | 2 +- .../demos/csv-export/csv-export.demo-data.ts | 76 ++++ .../demos/custom-icons/CustomIconsDemo.svelte | 6 +- .../custom-icons/custom-icons.demo-data.ts | 78 +++++ .../demos/custom-theme/CustomThemeDemo.svelte | 8 +- .../src/demos/custom-theme/custom-theme.css | 90 +++++ .../custom-theme/custom-theme.demo-data.ts | 48 +++ .../DynamicNestedTablesDemo.svelte | 8 +- .../dynamic-nested-tables.demo-data.ts | 93 +++++ .../DynamicRowLoadingDemo.svelte | 8 +- .../dynamic-row-loading.demo-data.ts | 205 +++++++++++ .../demos/empty-state/EmptyStateDemo.svelte | 6 +- .../empty-state/empty-state.demo-data.ts | 68 ++++ .../external-filter/ExternalFilterDemo.svelte | 6 +- .../external-filter.demo-data.ts | 128 +++++++ .../external-sort/ExternalSortDemo.svelte | 6 +- .../external-sort/external-sort.demo-data.ts | 40 +++ .../footer-renderer/FooterRendererDemo.svelte | 6 +- .../footer-renderer.demo-data.ts | 69 ++++ .../header-renderer/HeaderRendererDemo.svelte | 2 +- .../header-renderer.demo-data.ts | 32 ++ .../svelte/src/demos/hr/HRDemo.svelte | 4 +- .../svelte/src/demos/hr/hr.demo-data.ts | 126 +++++++ .../infinite-scroll/InfiniteScrollDemo.svelte | 6 +- .../infinite-scroll.demo-data.ts | 44 +++ .../infrastructure/InfrastructureDemo.svelte | 4 +- .../infrastructure.demo-data.ts | 263 ++++++++++++++ .../demos/live-update/LiveUpdateDemo.svelte | 6 +- .../live-update/live-update.demo-data.ts | 61 ++++ .../loading-state/LoadingStateDemo.svelte | 6 +- .../loading-state/loading-state.demo-data.ts | 34 ++ .../manufacturing/ManufacturingDemo.svelte | 4 +- .../manufacturing/manufacturing.demo-data.ts | 125 +++++++ .../svelte/src/demos/music/MusicDemo.svelte | 6 +- .../svelte/src/demos/music/music-theme.css | 13 + .../svelte/src/demos/music/music.demo-data.ts | 215 ++++++++++++ .../nested-headers/NestedHeadersDemo.svelte | 6 +- .../nested-headers.demo-data.ts | 41 +++ .../nested-tables/NestedTablesDemo.svelte | 6 +- .../nested-tables/nested-tables.demo-data.ts | 75 ++++ .../demos/pagination/PaginationDemo.svelte | 6 +- .../demos/pagination/pagination.demo-data.ts | 53 +++ .../ProgrammaticControlDemo.svelte | 2 +- .../programmatic-control.demo-data.ts | 56 +++ .../demos/quick-filter/QuickFilterDemo.svelte | 6 +- .../quick-filter/quick-filter.demo-data.ts | 33 ++ .../demos/quick-start/QuickStartDemo.svelte | 6 +- .../quick-start/quick-start.demo-data.ts | 130 +++++++ .../demos/row-grouping/RowGroupingDemo.svelte | 6 +- .../row-grouping/row-grouping.demo-data.ts | 205 +++++++++++ .../src/demos/row-height/RowHeightDemo.svelte | 6 +- .../demos/row-height/row-height.demo-data.ts | 32 ++ .../row-selection/RowSelectionDemo.svelte | 4 +- .../row-selection/row-selection.demo-data.ts | 56 +++ .../svelte/src/demos/sales/SalesDemo.svelte | 4 +- .../svelte/src/demos/sales/sales.demo-data.ts | 114 ++++++ .../SingleRowChildrenDemo.svelte | 6 +- .../single-row-children.demo-data.ts | 47 +++ .../demos/spreadsheet/SpreadsheetDemo.svelte | 6 +- .../demos/spreadsheet/spreadsheet-custom.css | 28 ++ .../spreadsheet/spreadsheet.demo-data.ts | 92 +++++ .../demos/table-height/TableHeightDemo.svelte | 6 +- .../table-height/table-height.demo-data.ts | 35 ++ .../svelte/src/demos/themes/ThemesDemo.svelte | 6 +- .../src/demos/themes/themes.demo-data.ts | 42 +++ .../src/demos/tooltip/TooltipDemo.svelte | 6 +- .../src/demos/tooltip/tooltip.demo-data.ts | 56 +++ .../value-formatter/ValueFormatterDemo.svelte | 6 +- .../value-formatter.demo-data.ts | 105 ++++++ packages/examples/svelte/src/main.ts | 2 +- packages/examples/svelte/src/styles/shell.css | 63 ++++ packages/examples/svelte/vite.config.ts | 2 - packages/examples/vanilla/package.json | 3 +- packages/examples/vanilla/src/demo-list.ts | 57 +++ .../AggregateFunctionsDemo.ts | 2 +- .../aggregate-functions.demo-data.ts} | 5 +- .../vanilla/src/demos/billing/BillingDemo.ts | 4 +- .../src/demos/billing/billing.demo-data.ts} | 16 +- .../demos/cell-clicking/CellClickingDemo.ts | 4 +- .../cell-clicking/cell-clicking.demo-data.ts} | 4 + .../src/demos/cell-editing/CellEditingDemo.ts | 2 +- .../cell-editing/cell-editing.demo-data.ts} | 2 + .../cell-highlighting/CellHighlightingDemo.ts | 2 +- .../cell-highlighting.demo-data.ts} | 2 + .../demos/cell-renderer/CellRendererDemo.ts | 4 +- .../cell-renderer/cell-renderer.demo-data.ts} | 2 + .../vanilla/src/demos/charts/ChartsDemo.ts | 2 +- .../src/demos/charts/charts.demo-data.ts} | 2 + .../CollapsibleColumnsDemo.ts | 2 +- .../collapsible-columns.demo-data.ts} | 2 + .../column-alignment/ColumnAlignmentDemo.ts | 2 +- .../column-alignment.demo-data.ts} | 2 + .../demos/column-editing/ColumnEditingDemo.ts | 2 +- .../column-editing.demo-data.ts} | 2 + .../ColumnEditorCustomRendererDemo.ts | 2 +- ...olumn-editor-custom-renderer.demo-data.ts} | 2 + .../column-filtering/ColumnFilteringDemo.ts | 2 +- .../column-filtering.demo-data.ts} | 92 +++++ .../demos/column-pinning/ColumnPinningDemo.ts | 2 +- .../column-pinning.demo-data.ts} | 2 + .../column-reordering/ColumnReorderingDemo.ts | 2 +- .../column-reordering.demo-data.ts} | 2 + .../column-resizing/ColumnResizingDemo.ts | 2 +- .../column-resizing.demo-data.ts} | 2 + .../column-selection/ColumnSelectionDemo.ts | 2 +- .../column-selection.demo-data.ts} | 2 + .../demos/column-sorting/ColumnSortingDemo.ts | 2 +- .../column-sorting.demo-data.ts} | 47 +++ .../column-visibility/ColumnVisibilityDemo.ts | 2 +- .../column-visibility.demo-data.ts} | 2 + .../src/demos/column-width/ColumnWidthDemo.ts | 2 +- .../column-width/column-width.demo-data.ts} | 2 + .../examples/vanilla/src/demos/crm/CRMDemo.ts | 12 +- .../src/demos/crm/crm-custom-theme.css | 161 +++++++++ .../src/demos/crm/crm.demo-data.ts} | 20 +- .../src/demos/csv-export/CsvExportDemo.ts | 7 +- .../demos/csv-export/csv-export.demo-data.ts} | 2 + .../src/demos/custom-icons/CustomIconsDemo.ts | 2 +- .../custom-icons/custom-icons.demo-data.ts} | 4 +- .../src/demos/custom-theme/CustomThemeDemo.ts | 4 +- .../src/demos/custom-theme/custom-theme.css | 90 +++++ .../custom-theme/custom-theme.demo-data.ts} | 2 + .../DynamicNestedTablesDemo.ts | 4 +- .../dynamic-nested-tables.demo-data.ts} | 2 + .../DynamicRowLoadingDemo.ts | 4 +- .../dynamic-row-loading.demo-data.ts} | 2 + .../src/demos/empty-state/EmptyStateDemo.ts | 2 +- .../empty-state/empty-state.demo-data.ts} | 2 + .../external-filter/ExternalFilterDemo.ts | 2 +- .../external-filter.demo-data.ts} | 2 + .../demos/external-sort/ExternalSortDemo.ts | 19 +- .../external-sort/external-sort.demo-data.ts} | 2 + .../footer-renderer/FooterRendererDemo.ts | 2 +- .../footer-renderer.demo-data.ts} | 2 + .../header-renderer/HeaderRendererDemo.ts | 2 +- .../header-renderer.demo-data.ts} | 2 + .../examples/vanilla/src/demos/hr/HRDemo.ts | 27 +- .../src/demos/hr/hr.demo-data.ts} | 20 +- .../infinite-scroll/InfiniteScrollDemo.ts | 2 +- .../infinite-scroll.demo-data.ts} | 2 + .../infrastructure/InfrastructureDemo.ts | 4 +- .../infrastructure.demo-data.ts} | 19 +- .../src/demos/live-update/LiveUpdateDemo.ts | 2 +- .../live-update/live-update.demo-data.ts} | 2 + .../demos/loading-state/LoadingStateDemo.ts | 2 +- .../loading-state/loading-state.demo-data.ts} | 2 + .../demos/manufacturing/ManufacturingDemo.ts | 30 +- .../manufacturing/manufacturing.demo-data.ts} | 21 +- .../vanilla/src/demos/music/MusicDemo.ts | 32 +- .../vanilla/src/demos/music/music-theme.css | 13 + .../src/demos/music/music.demo-data.ts} | 61 +++- .../demos/nested-headers/NestedHeadersDemo.ts | 2 +- .../nested-headers.demo-data.ts} | 2 + .../demos/nested-tables/NestedTablesDemo.ts | 2 +- .../nested-tables/nested-tables.demo-data.ts} | 2 + .../src/demos/pagination/PaginationDemo.ts | 2 +- .../demos/pagination/pagination.demo-data.ts} | 2 + .../ProgrammaticControlDemo.ts | 2 +- .../programmatic-control.demo-data.ts} | 24 +- .../src/demos/quick-filter/QuickFilterDemo.ts | 2 +- .../quick-filter/quick-filter.demo-data.ts} | 2 + .../src/demos/quick-start/QuickStartDemo.ts | 2 +- .../quick-start/quick-start.demo-data.ts} | 30 ++ .../src/demos/row-grouping/RowGroupingDemo.ts | 2 +- .../row-grouping/row-grouping.demo-data.ts} | 2 + .../src/demos/row-height/RowHeightDemo.ts | 2 +- .../demos/row-height/row-height.demo-data.ts} | 2 + .../demos/row-selection/RowSelectionDemo.ts | 4 +- .../row-selection/row-selection.demo-data.ts} | 2 + .../vanilla/src/demos/sales/SalesDemo.ts | 4 +- .../src/demos/sales/sales.demo-data.ts} | 16 +- .../SingleRowChildrenDemo.ts | 2 +- .../single-row-children.demo-data.ts} | 2 + .../src/demos/spreadsheet/SpreadsheetDemo.ts | 18 +- .../demos/spreadsheet/spreadsheet-custom.css | 28 ++ .../spreadsheet/spreadsheet.demo-data.ts} | 13 +- .../src/demos/table-height/TableHeightDemo.ts | 2 +- .../table-height/table-height.demo-data.ts} | 2 + .../vanilla/src/demos/themes/ThemesDemo.ts | 2 +- .../src/demos/themes/themes.demo-data.ts} | 2 + .../vanilla/src/demos/tooltip/TooltipDemo.ts | 2 +- .../src/demos/tooltip/tooltip.demo-data.ts} | 2 + .../value-formatter/ValueFormatterDemo.ts | 2 +- .../value-formatter.demo-data.ts} | 2 + packages/examples/vanilla/src/main.ts | 4 +- .../examples/vanilla/src/styles/shell.css | 63 ++++ packages/examples/vanilla/vite.config.ts | 2 - packages/examples/vue/package.json | 1 - packages/examples/vue/src/App.vue | 2 +- packages/examples/vue/src/demo-list.ts | 57 +++ .../AggregateFunctionsDemo.vue | 6 +- .../aggregate-functions.demo-data.ts | 193 +++++++++++ .../vue/src/demos/billing/BillingDemo.vue | 4 +- .../src/demos/billing/billing.demo-data.ts | 199 +++++++++++ .../demos/cell-clicking/CellClickingDemo.vue | 4 +- .../cell-clicking/cell-clicking.demo-data.ts | 47 +++ .../demos/cell-editing/CellEditingDemo.vue | 6 +- .../cell-editing/cell-editing.demo-data.ts | 38 ++ .../CellHighlightingDemo.vue | 6 +- .../cell-highlighting.demo-data.ts | 33 ++ .../demos/cell-renderer/CellRendererDemo.vue | 4 +- .../cell-renderer/cell-renderer.demo-data.ts | 51 +++ .../vue/src/demos/charts/ChartsDemo.vue | 6 +- .../vue/src/demos/charts/charts.demo-data.ts | 50 +++ .../CollapsibleColumnsDemo.vue | 6 +- .../collapsible-columns.demo-data.ts | 91 +++++ .../column-alignment/ColumnAlignmentDemo.vue | 6 +- .../column-alignment.demo-data.ts | 31 ++ .../column-editing/ColumnEditingDemo.vue | 2 +- .../column-editing.demo-data.ts | 32 ++ .../ColumnEditorCustomRendererDemo.vue | 6 +- ...column-editor-custom-renderer.demo-data.ts | 83 +++++ .../column-filtering/ColumnFilteringDemo.vue | 6 +- .../column-filtering.demo-data.ts | 222 ++++++++++++ .../column-pinning/ColumnPinningDemo.vue | 6 +- .../column-pinning.demo-data.ts | 37 ++ .../ColumnReorderingDemo.vue | 2 +- .../column-reordering.demo-data.ts | 32 ++ .../column-resizing/ColumnResizingDemo.vue | 2 +- .../column-resizing.demo-data.ts | 34 ++ .../column-selection/ColumnSelectionDemo.vue | 6 +- .../column-selection.demo-data.ts | 33 ++ .../column-sorting/ColumnSortingDemo.vue | 6 +- .../column-sorting.demo-data.ts | 147 ++++++++ .../ColumnVisibilityDemo.vue | 6 +- .../column-visibility.demo-data.ts | 45 +++ .../demos/column-width/ColumnWidthDemo.vue | 6 +- .../column-width/column-width.demo-data.ts | 34 ++ .../examples/vue/src/demos/crm/CRMDemo.vue | 16 +- .../vue/src/demos/crm/crm-custom-theme.css | 161 +++++++++ .../vue/src/demos/crm/crm.demo-data.ts | 177 ++++++++++ .../src/demos/csv-export/CsvExportDemo.vue | 2 +- .../demos/csv-export/csv-export.demo-data.ts | 76 ++++ .../demos/custom-icons/CustomIconsDemo.vue | 6 +- .../custom-icons/custom-icons.demo-data.ts | 78 +++++ .../demos/custom-theme/CustomThemeDemo.vue | 8 +- .../src/demos/custom-theme/custom-theme.css | 90 +++++ .../custom-theme/custom-theme.demo-data.ts | 48 +++ .../DynamicNestedTablesDemo.vue | 8 +- .../dynamic-nested-tables.demo-data.ts | 93 +++++ .../DynamicRowLoadingDemo.vue | 8 +- .../dynamic-row-loading.demo-data.ts | 205 +++++++++++ .../src/demos/empty-state/EmptyStateDemo.vue | 6 +- .../empty-state/empty-state.demo-data.ts | 68 ++++ .../external-filter/ExternalFilterDemo.vue | 6 +- .../external-filter.demo-data.ts | 128 +++++++ .../demos/external-sort/ExternalSortDemo.vue | 6 +- .../external-sort/external-sort.demo-data.ts | 40 +++ .../footer-renderer/FooterRendererDemo.vue | 6 +- .../footer-renderer.demo-data.ts | 69 ++++ .../header-renderer/HeaderRendererDemo.vue | 2 +- .../header-renderer.demo-data.ts | 32 ++ packages/examples/vue/src/demos/hr/HRDemo.vue | 4 +- .../examples/vue/src/demos/hr/hr.demo-data.ts | 126 +++++++ .../infinite-scroll/InfiniteScrollDemo.vue | 6 +- .../infinite-scroll.demo-data.ts | 44 +++ .../infrastructure/InfrastructureDemo.vue | 4 +- .../infrastructure.demo-data.ts | 263 ++++++++++++++ .../src/demos/live-update/LiveUpdateDemo.vue | 6 +- .../live-update/live-update.demo-data.ts | 61 ++++ .../demos/loading-state/LoadingStateDemo.vue | 6 +- .../loading-state/loading-state.demo-data.ts | 34 ++ .../demos/manufacturing/ManufacturingDemo.vue | 4 +- .../manufacturing/manufacturing.demo-data.ts | 125 +++++++ .../vue/src/demos/music/MusicDemo.vue | 6 +- .../vue/src/demos/music/music-theme.css | 13 + .../vue/src/demos/music/music.demo-data.ts | 215 ++++++++++++ .../nested-headers/NestedHeadersDemo.vue | 6 +- .../nested-headers.demo-data.ts | 41 +++ .../demos/nested-tables/NestedTablesDemo.vue | 6 +- .../nested-tables/nested-tables.demo-data.ts | 75 ++++ .../src/demos/pagination/PaginationDemo.vue | 6 +- .../demos/pagination/pagination.demo-data.ts | 53 +++ .../ProgrammaticControlDemo.vue | 2 +- .../programmatic-control.demo-data.ts | 56 +++ .../demos/quick-filter/QuickFilterDemo.vue | 6 +- .../quick-filter/quick-filter.demo-data.ts | 33 ++ .../src/demos/quick-start/QuickStartDemo.vue | 6 +- .../quick-start/quick-start.demo-data.ts | 130 +++++++ .../demos/row-grouping/RowGroupingDemo.vue | 6 +- .../row-grouping/row-grouping.demo-data.ts | 205 +++++++++++ .../src/demos/row-height/RowHeightDemo.vue | 6 +- .../demos/row-height/row-height.demo-data.ts | 32 ++ .../demos/row-selection/RowSelectionDemo.vue | 4 +- .../row-selection/row-selection.demo-data.ts | 56 +++ .../vue/src/demos/sales/SalesDemo.vue | 4 +- .../vue/src/demos/sales/sales.demo-data.ts | 114 ++++++ .../SingleRowChildrenDemo.vue | 6 +- .../single-row-children.demo-data.ts | 47 +++ .../src/demos/spreadsheet/SpreadsheetDemo.vue | 6 +- .../demos/spreadsheet/spreadsheet-custom.css | 28 ++ .../spreadsheet/spreadsheet.demo-data.ts | 92 +++++ .../demos/table-height/TableHeightDemo.vue | 6 +- .../table-height/table-height.demo-data.ts | 35 ++ .../vue/src/demos/themes/ThemesDemo.vue | 6 +- .../vue/src/demos/themes/themes.demo-data.ts | 42 +++ .../vue/src/demos/tooltip/TooltipDemo.vue | 6 +- .../src/demos/tooltip/tooltip.demo-data.ts | 56 +++ .../value-formatter/ValueFormatterDemo.vue | 6 +- .../value-formatter.demo-data.ts | 105 ++++++ packages/examples/vue/src/env.d.ts | 7 + packages/examples/vue/src/main.ts | 2 +- packages/examples/vue/src/styles/shell.css | 63 ++++ packages/examples/vue/tsconfig.json | 2 +- packages/examples/vue/vite.config.ts | 2 - packages/react/package.json | 2 +- packages/react/src/buildVanillaConfig.ts | 32 +- packages/react/src/defaultHeadersFromCore.ts | 21 ++ packages/react/src/index.ts | 6 + packages/react/src/types.ts | 116 ++++--- packages/solid/package.json | 2 +- packages/solid/src/buildVanillaConfig.ts | 4 +- packages/solid/src/defaultHeadersFromCore.ts | 14 + packages/solid/src/index.ts | 7 + packages/solid/src/types.ts | 34 +- packages/svelte/package.json | 6 +- packages/svelte/src/buildVanillaConfig.ts | 31 +- packages/svelte/src/defaultHeadersFromCore.ts | 14 + packages/svelte/src/index.ts | 9 + packages/svelte/src/types.ts | 53 ++- packages/vue/package.json | 2 +- packages/vue/src/buildVanillaConfig.ts | 4 +- packages/vue/src/defaultHeadersFromCore.ts | 14 + packages/vue/src/index.ts | 7 + packages/vue/src/types.ts | 35 +- pnpm-lock.yaml | 43 --- scripts/generate-stackblitz.mjs | 64 +--- scripts/new-demo.mjs | 84 +++-- 757 files changed, 27190 insertions(+), 2406 deletions(-) create mode 100644 packages/angular/src/defaultHeadersFromCore.ts delete mode 100644 packages/core/src/types/TableRefType.ts create mode 100644 packages/core/src/utils/asRows.ts delete mode 100644 packages/core/src/utils/deprecatedPropsWarnings.ts rename packages/examples/{shared/src/utils/index.ts => angular/src/demo-list.ts} (100%) create mode 100644 packages/examples/angular/src/demos/aggregate-functions/aggregate-functions.demo-data.ts create mode 100644 packages/examples/angular/src/demos/billing/billing.demo-data.ts create mode 100644 packages/examples/angular/src/demos/cell-clicking/cell-clicking.demo-data.ts create mode 100644 packages/examples/angular/src/demos/cell-editing/cell-editing.demo-data.ts create mode 100644 packages/examples/angular/src/demos/cell-highlighting/cell-highlighting.demo-data.ts create mode 100644 packages/examples/angular/src/demos/cell-renderer/cell-renderer.demo-data.ts create mode 100644 packages/examples/angular/src/demos/charts/charts.demo-data.ts create mode 100644 packages/examples/angular/src/demos/collapsible-columns/collapsible-columns.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-alignment/column-alignment.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-editing/column-editing.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-filtering/column-filtering.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-pinning/column-pinning.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-reordering/column-reordering.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-resizing/column-resizing.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-selection/column-selection.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-sorting/column-sorting.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-visibility/column-visibility.demo-data.ts create mode 100644 packages/examples/angular/src/demos/column-width/column-width.demo-data.ts rename packages/examples/{shared/src/styles => angular/src/demos/crm}/crm-custom-theme.css (100%) create mode 100644 packages/examples/angular/src/demos/crm/crm.demo-data.ts create mode 100644 packages/examples/angular/src/demos/csv-export/csv-export.demo-data.ts create mode 100644 packages/examples/angular/src/demos/custom-icons/custom-icons.demo-data.ts rename packages/examples/{shared/src/styles => angular/src/demos/custom-theme}/custom-theme.css (100%) create mode 100644 packages/examples/angular/src/demos/custom-theme/custom-theme.demo-data.ts create mode 100644 packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts create mode 100644 packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts create mode 100644 packages/examples/angular/src/demos/empty-state/empty-state.demo-data.ts create mode 100644 packages/examples/angular/src/demos/external-filter/external-filter.demo-data.ts create mode 100644 packages/examples/angular/src/demos/external-sort/external-sort.demo-data.ts create mode 100644 packages/examples/angular/src/demos/footer-renderer/footer-renderer.demo-data.ts create mode 100644 packages/examples/angular/src/demos/header-renderer/header-renderer.demo-data.ts create mode 100644 packages/examples/angular/src/demos/hr/hr.demo-data.ts create mode 100644 packages/examples/angular/src/demos/infinite-scroll/infinite-scroll.demo-data.ts create mode 100644 packages/examples/angular/src/demos/infrastructure/infrastructure.demo-data.ts create mode 100644 packages/examples/angular/src/demos/live-update/live-update.demo-data.ts create mode 100644 packages/examples/angular/src/demos/loading-state/loading-state.demo-data.ts create mode 100644 packages/examples/angular/src/demos/manufacturing/manufacturing.demo-data.ts rename packages/examples/{shared/src/styles => angular/src/demos/music}/music-theme.css (100%) create mode 100644 packages/examples/angular/src/demos/music/music.demo-data.ts create mode 100644 packages/examples/angular/src/demos/nested-headers/nested-headers.demo-data.ts create mode 100644 packages/examples/angular/src/demos/nested-tables/nested-tables.demo-data.ts create mode 100644 packages/examples/angular/src/demos/pagination/pagination.demo-data.ts create mode 100644 packages/examples/angular/src/demos/programmatic-control/programmatic-control.demo-data.ts create mode 100644 packages/examples/angular/src/demos/quick-filter/quick-filter.demo-data.ts create mode 100644 packages/examples/angular/src/demos/quick-start/quick-start.demo-data.ts create mode 100644 packages/examples/angular/src/demos/row-grouping/row-grouping.demo-data.ts create mode 100644 packages/examples/angular/src/demos/row-height/row-height.demo-data.ts create mode 100644 packages/examples/angular/src/demos/row-selection/row-selection.demo-data.ts create mode 100644 packages/examples/angular/src/demos/sales/sales.demo-data.ts create mode 100644 packages/examples/angular/src/demos/single-row-children/single-row-children.demo-data.ts rename packages/examples/{shared/src/styles => angular/src/demos/spreadsheet}/spreadsheet-custom.css (100%) create mode 100644 packages/examples/angular/src/demos/spreadsheet/spreadsheet.demo-data.ts create mode 100644 packages/examples/angular/src/demos/table-height/table-height.demo-data.ts create mode 100644 packages/examples/angular/src/demos/themes/themes.demo-data.ts create mode 100644 packages/examples/angular/src/demos/tooltip/tooltip.demo-data.ts create mode 100644 packages/examples/angular/src/demos/value-formatter/value-formatter.demo-data.ts rename packages/examples/{shared => angular}/src/styles/shell.css (100%) create mode 100644 packages/examples/react/src/demo-list.ts create mode 100644 packages/examples/react/src/demos/aggregate-functions/aggregate-functions.demo-data.ts create mode 100644 packages/examples/react/src/demos/billing/billing.demo-data.ts create mode 100644 packages/examples/react/src/demos/cell-clicking/cell-clicking.demo-data.ts create mode 100644 packages/examples/react/src/demos/cell-editing/cell-editing.demo-data.ts create mode 100644 packages/examples/react/src/demos/cell-highlighting/cell-highlighting.demo-data.ts create mode 100644 packages/examples/react/src/demos/cell-renderer/cell-renderer.demo-data.ts create mode 100644 packages/examples/react/src/demos/charts/charts.demo-data.ts create mode 100644 packages/examples/react/src/demos/collapsible-columns/collapsible-columns.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-alignment/column-alignment.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-editing/column-editing.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-filtering/column-filtering.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-pinning/column-pinning.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-reordering/column-reordering.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-resizing/column-resizing.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-selection/column-selection.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-sorting/column-sorting.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-visibility/column-visibility.demo-data.ts create mode 100644 packages/examples/react/src/demos/column-width/column-width.demo-data.ts create mode 100644 packages/examples/react/src/demos/crm/crm-custom-theme.css create mode 100644 packages/examples/react/src/demos/crm/crm.demo-data.ts create mode 100644 packages/examples/react/src/demos/csv-export/csv-export.demo-data.ts create mode 100644 packages/examples/react/src/demos/custom-icons/custom-icons.demo-data.ts create mode 100644 packages/examples/react/src/demos/custom-theme/custom-theme.css create mode 100644 packages/examples/react/src/demos/custom-theme/custom-theme.demo-data.ts create mode 100644 packages/examples/react/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts create mode 100644 packages/examples/react/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts create mode 100644 packages/examples/react/src/demos/empty-state/empty-state.demo-data.ts create mode 100644 packages/examples/react/src/demos/external-filter/external-filter.demo-data.ts create mode 100644 packages/examples/react/src/demos/external-sort/external-sort.demo-data.ts create mode 100644 packages/examples/react/src/demos/footer-renderer/footer-renderer.demo-data.ts create mode 100644 packages/examples/react/src/demos/header-renderer/header-renderer.demo-data.ts create mode 100644 packages/examples/react/src/demos/hr/hr.demo-data.ts create mode 100644 packages/examples/react/src/demos/infinite-scroll/infinite-scroll.demo-data.ts create mode 100644 packages/examples/react/src/demos/infrastructure/infrastructure.demo-data.ts create mode 100644 packages/examples/react/src/demos/live-update/live-update.demo-data.ts create mode 100644 packages/examples/react/src/demos/loading-state/loading-state.demo-data.ts create mode 100644 packages/examples/react/src/demos/manufacturing/manufacturing.demo-data.ts create mode 100644 packages/examples/react/src/demos/music/music-theme.css create mode 100644 packages/examples/react/src/demos/music/music.demo-data.ts create mode 100644 packages/examples/react/src/demos/nested-headers/nested-headers.demo-data.ts create mode 100644 packages/examples/react/src/demos/nested-tables/nested-tables.demo-data.ts create mode 100644 packages/examples/react/src/demos/pagination/pagination.demo-data.ts create mode 100644 packages/examples/react/src/demos/programmatic-control/programmatic-control.demo-data.ts create mode 100644 packages/examples/react/src/demos/quick-filter/quick-filter.demo-data.ts create mode 100644 packages/examples/react/src/demos/quick-start/quick-start.demo-data.ts create mode 100644 packages/examples/react/src/demos/row-grouping/row-grouping.demo-data.ts create mode 100644 packages/examples/react/src/demos/row-height/row-height.demo-data.ts create mode 100644 packages/examples/react/src/demos/row-selection/row-selection.demo-data.ts create mode 100644 packages/examples/react/src/demos/sales/sales.demo-data.ts create mode 100644 packages/examples/react/src/demos/single-row-children/single-row-children.demo-data.ts create mode 100644 packages/examples/react/src/demos/spreadsheet/spreadsheet-custom.css create mode 100644 packages/examples/react/src/demos/spreadsheet/spreadsheet.demo-data.ts create mode 100644 packages/examples/react/src/demos/table-height/table-height.demo-data.ts create mode 100644 packages/examples/react/src/demos/themes/themes.demo-data.ts create mode 100644 packages/examples/react/src/demos/tooltip/tooltip.demo-data.ts create mode 100644 packages/examples/react/src/demos/value-formatter/value-formatter.demo-data.ts create mode 100644 packages/examples/react/src/styles/shell.css delete mode 100644 packages/examples/shared/package.json delete mode 100644 packages/examples/shared/src/configs/column-filtering-config.ts delete mode 100644 packages/examples/shared/src/configs/column-sorting-config.ts delete mode 100644 packages/examples/shared/src/configs/index.ts delete mode 100644 packages/examples/shared/src/configs/quick-start-config.ts delete mode 100644 packages/examples/shared/src/data/index.ts delete mode 100644 packages/examples/shared/src/index.ts delete mode 100644 packages/examples/shared/src/types/billing.ts delete mode 100644 packages/examples/shared/src/types/crm.ts delete mode 100644 packages/examples/shared/src/types/employee.ts delete mode 100644 packages/examples/shared/src/types/hr.ts delete mode 100644 packages/examples/shared/src/types/index.ts delete mode 100644 packages/examples/shared/src/types/infrastructure.ts delete mode 100644 packages/examples/shared/src/types/manufacturing.ts delete mode 100644 packages/examples/shared/src/types/music.ts delete mode 100644 packages/examples/shared/src/types/sales.ts delete mode 100644 packages/examples/shared/src/types/spreadsheet.ts delete mode 100644 packages/examples/shared/tsconfig.json create mode 100644 packages/examples/solid/src/demo-list.ts create mode 100644 packages/examples/solid/src/demos/aggregate-functions/aggregate-functions.demo-data.ts create mode 100644 packages/examples/solid/src/demos/billing/billing.demo-data.ts create mode 100644 packages/examples/solid/src/demos/cell-clicking/cell-clicking.demo-data.ts create mode 100644 packages/examples/solid/src/demos/cell-editing/cell-editing.demo-data.ts create mode 100644 packages/examples/solid/src/demos/cell-highlighting/cell-highlighting.demo-data.ts create mode 100644 packages/examples/solid/src/demos/cell-renderer/cell-renderer.demo-data.ts create mode 100644 packages/examples/solid/src/demos/charts/charts.demo-data.ts create mode 100644 packages/examples/solid/src/demos/collapsible-columns/collapsible-columns.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-alignment/column-alignment.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-editing/column-editing.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-filtering/column-filtering.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-pinning/column-pinning.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-reordering/column-reordering.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-resizing/column-resizing.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-selection/column-selection.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-sorting/column-sorting.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-visibility/column-visibility.demo-data.ts create mode 100644 packages/examples/solid/src/demos/column-width/column-width.demo-data.ts create mode 100644 packages/examples/solid/src/demos/crm/crm-custom-theme.css create mode 100644 packages/examples/solid/src/demos/crm/crm.demo-data.ts create mode 100644 packages/examples/solid/src/demos/csv-export/csv-export.demo-data.ts create mode 100644 packages/examples/solid/src/demos/custom-icons/custom-icons.demo-data.ts create mode 100644 packages/examples/solid/src/demos/custom-theme/custom-theme.css create mode 100644 packages/examples/solid/src/demos/custom-theme/custom-theme.demo-data.ts create mode 100644 packages/examples/solid/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts create mode 100644 packages/examples/solid/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts create mode 100644 packages/examples/solid/src/demos/empty-state/empty-state.demo-data.ts create mode 100644 packages/examples/solid/src/demos/external-filter/external-filter.demo-data.ts create mode 100644 packages/examples/solid/src/demos/external-sort/external-sort.demo-data.ts create mode 100644 packages/examples/solid/src/demos/footer-renderer/footer-renderer.demo-data.ts create mode 100644 packages/examples/solid/src/demos/header-renderer/header-renderer.demo-data.ts create mode 100644 packages/examples/solid/src/demos/hr/hr.demo-data.ts create mode 100644 packages/examples/solid/src/demos/infinite-scroll/infinite-scroll.demo-data.ts create mode 100644 packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts create mode 100644 packages/examples/solid/src/demos/live-update/live-update.demo-data.ts create mode 100644 packages/examples/solid/src/demos/loading-state/loading-state.demo-data.ts create mode 100644 packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts create mode 100644 packages/examples/solid/src/demos/music/music-theme.css create mode 100644 packages/examples/solid/src/demos/music/music.demo-data.ts create mode 100644 packages/examples/solid/src/demos/nested-headers/nested-headers.demo-data.ts create mode 100644 packages/examples/solid/src/demos/nested-tables/nested-tables.demo-data.ts create mode 100644 packages/examples/solid/src/demos/pagination/pagination.demo-data.ts create mode 100644 packages/examples/solid/src/demos/programmatic-control/programmatic-control.demo-data.ts create mode 100644 packages/examples/solid/src/demos/quick-filter/quick-filter.demo-data.ts create mode 100644 packages/examples/solid/src/demos/quick-start/quick-start.demo-data.ts create mode 100644 packages/examples/solid/src/demos/row-grouping/row-grouping.demo-data.ts create mode 100644 packages/examples/solid/src/demos/row-height/row-height.demo-data.ts create mode 100644 packages/examples/solid/src/demos/row-selection/row-selection.demo-data.ts create mode 100644 packages/examples/solid/src/demos/sales/sales.demo-data.ts create mode 100644 packages/examples/solid/src/demos/single-row-children/single-row-children.demo-data.ts create mode 100644 packages/examples/solid/src/demos/spreadsheet/spreadsheet-custom.css create mode 100644 packages/examples/solid/src/demos/spreadsheet/spreadsheet.demo-data.ts create mode 100644 packages/examples/solid/src/demos/table-height/table-height.demo-data.ts create mode 100644 packages/examples/solid/src/demos/themes/themes.demo-data.ts create mode 100644 packages/examples/solid/src/demos/tooltip/tooltip.demo-data.ts create mode 100644 packages/examples/solid/src/demos/value-formatter/value-formatter.demo-data.ts create mode 100644 packages/examples/solid/src/styles/shell.css create mode 100644 packages/examples/svelte/src/demo-list.ts create mode 100644 packages/examples/svelte/src/demos/aggregate-functions/aggregate-functions.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/billing/billing.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/cell-clicking/cell-clicking.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/cell-editing/cell-editing.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/cell-highlighting/cell-highlighting.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/cell-renderer/cell-renderer.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/charts/charts.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/collapsible-columns/collapsible-columns.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-alignment/column-alignment.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-editing/column-editing.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-filtering/column-filtering.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-pinning/column-pinning.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-reordering/column-reordering.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-resizing/column-resizing.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-selection/column-selection.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-sorting/column-sorting.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-visibility/column-visibility.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/column-width/column-width.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/crm/crm-custom-theme.css create mode 100644 packages/examples/svelte/src/demos/crm/crm.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/csv-export/csv-export.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/custom-icons/custom-icons.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/custom-theme/custom-theme.css create mode 100644 packages/examples/svelte/src/demos/custom-theme/custom-theme.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/empty-state/empty-state.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/external-filter/external-filter.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/external-sort/external-sort.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/footer-renderer/footer-renderer.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/header-renderer/header-renderer.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/hr/hr.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/infinite-scroll/infinite-scroll.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/infrastructure/infrastructure.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/live-update/live-update.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/loading-state/loading-state.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/manufacturing/manufacturing.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/music/music-theme.css create mode 100644 packages/examples/svelte/src/demos/music/music.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/nested-headers/nested-headers.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/nested-tables/nested-tables.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/pagination/pagination.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/programmatic-control/programmatic-control.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/quick-filter/quick-filter.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/quick-start/quick-start.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/row-grouping/row-grouping.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/row-height/row-height.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/row-selection/row-selection.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/sales/sales.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/single-row-children/single-row-children.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/spreadsheet/spreadsheet-custom.css create mode 100644 packages/examples/svelte/src/demos/spreadsheet/spreadsheet.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/table-height/table-height.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/themes/themes.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/tooltip/tooltip.demo-data.ts create mode 100644 packages/examples/svelte/src/demos/value-formatter/value-formatter.demo-data.ts create mode 100644 packages/examples/svelte/src/styles/shell.css create mode 100644 packages/examples/vanilla/src/demo-list.ts rename packages/examples/{shared/src/configs/aggregate-functions-config.ts => vanilla/src/demos/aggregate-functions/aggregate-functions.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/billing-config.ts => vanilla/src/demos/billing/billing.demo-data.ts} (94%) rename packages/examples/{shared/src/configs/cell-clicking-config.ts => vanilla/src/demos/cell-clicking/cell-clicking.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/cell-editing-config.ts => vanilla/src/demos/cell-editing/cell-editing.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/cell-highlighting-config.ts => vanilla/src/demos/cell-highlighting/cell-highlighting.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/cell-renderer-config.ts => vanilla/src/demos/cell-renderer/cell-renderer.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/charts-config.ts => vanilla/src/demos/charts/charts.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/collapsible-columns-config.ts => vanilla/src/demos/collapsible-columns/collapsible-columns.demo-data.ts} (99%) rename packages/examples/{shared/src/configs/column-alignment-config.ts => vanilla/src/demos/column-alignment/column-alignment.demo-data.ts} (96%) rename packages/examples/{shared/src/configs/column-editing-config.ts => vanilla/src/demos/column-editing/column-editing.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/column-editor-custom-renderer-config.ts => vanilla/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts} (98%) rename packages/examples/{shared/src/data/column-filtering-data.ts => vanilla/src/demos/column-filtering/column-filtering.demo-data.ts} (54%) rename packages/examples/{shared/src/configs/column-pinning-config.ts => vanilla/src/demos/column-pinning/column-pinning.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/column-reordering-config.ts => vanilla/src/demos/column-reordering/column-reordering.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/column-resizing-config.ts => vanilla/src/demos/column-resizing/column-resizing.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/column-selection-config.ts => vanilla/src/demos/column-selection/column-selection.demo-data.ts} (97%) rename packages/examples/{shared/src/data/column-sorting-data.ts => vanilla/src/demos/column-sorting/column-sorting.demo-data.ts} (59%) rename packages/examples/{shared/src/configs/column-visibility-config.ts => vanilla/src/demos/column-visibility/column-visibility.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/column-width-config.ts => vanilla/src/demos/column-width/column-width.demo-data.ts} (97%) create mode 100644 packages/examples/vanilla/src/demos/crm/crm-custom-theme.css rename packages/examples/{shared/src/configs/crm-config.ts => vanilla/src/demos/crm/crm.demo-data.ts} (93%) rename packages/examples/{shared/src/configs/csv-export-config.ts => vanilla/src/demos/csv-export/csv-export.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/custom-icons-config.ts => vanilla/src/demos/custom-icons/custom-icons.demo-data.ts} (97%) create mode 100644 packages/examples/vanilla/src/demos/custom-theme/custom-theme.css rename packages/examples/{shared/src/configs/custom-theme-config.ts => vanilla/src/demos/custom-theme/custom-theme.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/dynamic-nested-tables-config.ts => vanilla/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/dynamic-row-loading-config.ts => vanilla/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts} (99%) rename packages/examples/{shared/src/configs/empty-state-config.ts => vanilla/src/demos/empty-state/empty-state.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/external-filter-config.ts => vanilla/src/demos/external-filter/external-filter.demo-data.ts} (99%) rename packages/examples/{shared/src/configs/external-sort-config.ts => vanilla/src/demos/external-sort/external-sort.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/footer-renderer-config.ts => vanilla/src/demos/footer-renderer/footer-renderer.demo-data.ts} (99%) rename packages/examples/{shared/src/configs/header-renderer-config.ts => vanilla/src/demos/header-renderer/header-renderer.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/hr-config.ts => vanilla/src/demos/hr/hr.demo-data.ts} (94%) rename packages/examples/{shared/src/configs/infinite-scroll-config.ts => vanilla/src/demos/infinite-scroll/infinite-scroll.demo-data.ts} (96%) rename packages/examples/{shared/src/configs/infrastructure-config.ts => vanilla/src/demos/infrastructure/infrastructure.demo-data.ts} (94%) rename packages/examples/{shared/src/configs/live-update-config.ts => vanilla/src/demos/live-update/live-update.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/loading-state-config.ts => vanilla/src/demos/loading-state/loading-state.demo-data.ts} (96%) rename packages/examples/{shared/src/configs/manufacturing-config.ts => vanilla/src/demos/manufacturing/manufacturing.demo-data.ts} (94%) create mode 100644 packages/examples/vanilla/src/demos/music/music-theme.css rename packages/examples/{shared/src/configs/music-config.ts => vanilla/src/demos/music/music.demo-data.ts} (86%) rename packages/examples/{shared/src/configs/nested-headers-config.ts => vanilla/src/demos/nested-headers/nested-headers.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/nested-tables-config.ts => vanilla/src/demos/nested-tables/nested-tables.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/pagination-config.ts => vanilla/src/demos/pagination/pagination.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/programmatic-control-config.ts => vanilla/src/demos/programmatic-control/programmatic-control.demo-data.ts} (79%) rename packages/examples/{shared/src/configs/quick-filter-config.ts => vanilla/src/demos/quick-filter/quick-filter.demo-data.ts} (97%) rename packages/examples/{shared/src/data/quick-start-data.ts => vanilla/src/demos/quick-start/quick-start.demo-data.ts} (66%) rename packages/examples/{shared/src/configs/row-grouping-config.ts => vanilla/src/demos/row-grouping/row-grouping.demo-data.ts} (99%) rename packages/examples/{shared/src/configs/row-height-config.ts => vanilla/src/demos/row-height/row-height.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/row-selection-config.ts => vanilla/src/demos/row-selection/row-selection.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/sales-config.ts => vanilla/src/demos/sales/sales.demo-data.ts} (95%) rename packages/examples/{shared/src/configs/single-row-children-config.ts => vanilla/src/demos/single-row-children/single-row-children.demo-data.ts} (98%) create mode 100644 packages/examples/vanilla/src/demos/spreadsheet/spreadsheet-custom.css rename packages/examples/{shared/src/configs/spreadsheet-config.ts => vanilla/src/demos/spreadsheet/spreadsheet.demo-data.ts} (94%) rename packages/examples/{shared/src/configs/table-height-config.ts => vanilla/src/demos/table-height/table-height.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/themes-config.ts => vanilla/src/demos/themes/themes.demo-data.ts} (97%) rename packages/examples/{shared/src/configs/tooltip-config.ts => vanilla/src/demos/tooltip/tooltip.demo-data.ts} (98%) rename packages/examples/{shared/src/configs/value-formatter-config.ts => vanilla/src/demos/value-formatter/value-formatter.demo-data.ts} (98%) create mode 100644 packages/examples/vanilla/src/styles/shell.css create mode 100644 packages/examples/vue/src/demo-list.ts create mode 100644 packages/examples/vue/src/demos/aggregate-functions/aggregate-functions.demo-data.ts create mode 100644 packages/examples/vue/src/demos/billing/billing.demo-data.ts create mode 100644 packages/examples/vue/src/demos/cell-clicking/cell-clicking.demo-data.ts create mode 100644 packages/examples/vue/src/demos/cell-editing/cell-editing.demo-data.ts create mode 100644 packages/examples/vue/src/demos/cell-highlighting/cell-highlighting.demo-data.ts create mode 100644 packages/examples/vue/src/demos/cell-renderer/cell-renderer.demo-data.ts create mode 100644 packages/examples/vue/src/demos/charts/charts.demo-data.ts create mode 100644 packages/examples/vue/src/demos/collapsible-columns/collapsible-columns.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-alignment/column-alignment.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-editing/column-editing.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-filtering/column-filtering.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-pinning/column-pinning.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-reordering/column-reordering.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-resizing/column-resizing.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-selection/column-selection.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-sorting/column-sorting.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-visibility/column-visibility.demo-data.ts create mode 100644 packages/examples/vue/src/demos/column-width/column-width.demo-data.ts create mode 100644 packages/examples/vue/src/demos/crm/crm-custom-theme.css create mode 100644 packages/examples/vue/src/demos/crm/crm.demo-data.ts create mode 100644 packages/examples/vue/src/demos/csv-export/csv-export.demo-data.ts create mode 100644 packages/examples/vue/src/demos/custom-icons/custom-icons.demo-data.ts create mode 100644 packages/examples/vue/src/demos/custom-theme/custom-theme.css create mode 100644 packages/examples/vue/src/demos/custom-theme/custom-theme.demo-data.ts create mode 100644 packages/examples/vue/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts create mode 100644 packages/examples/vue/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts create mode 100644 packages/examples/vue/src/demos/empty-state/empty-state.demo-data.ts create mode 100644 packages/examples/vue/src/demos/external-filter/external-filter.demo-data.ts create mode 100644 packages/examples/vue/src/demos/external-sort/external-sort.demo-data.ts create mode 100644 packages/examples/vue/src/demos/footer-renderer/footer-renderer.demo-data.ts create mode 100644 packages/examples/vue/src/demos/header-renderer/header-renderer.demo-data.ts create mode 100644 packages/examples/vue/src/demos/hr/hr.demo-data.ts create mode 100644 packages/examples/vue/src/demos/infinite-scroll/infinite-scroll.demo-data.ts create mode 100644 packages/examples/vue/src/demos/infrastructure/infrastructure.demo-data.ts create mode 100644 packages/examples/vue/src/demos/live-update/live-update.demo-data.ts create mode 100644 packages/examples/vue/src/demos/loading-state/loading-state.demo-data.ts create mode 100644 packages/examples/vue/src/demos/manufacturing/manufacturing.demo-data.ts create mode 100644 packages/examples/vue/src/demos/music/music-theme.css create mode 100644 packages/examples/vue/src/demos/music/music.demo-data.ts create mode 100644 packages/examples/vue/src/demos/nested-headers/nested-headers.demo-data.ts create mode 100644 packages/examples/vue/src/demos/nested-tables/nested-tables.demo-data.ts create mode 100644 packages/examples/vue/src/demos/pagination/pagination.demo-data.ts create mode 100644 packages/examples/vue/src/demos/programmatic-control/programmatic-control.demo-data.ts create mode 100644 packages/examples/vue/src/demos/quick-filter/quick-filter.demo-data.ts create mode 100644 packages/examples/vue/src/demos/quick-start/quick-start.demo-data.ts create mode 100644 packages/examples/vue/src/demos/row-grouping/row-grouping.demo-data.ts create mode 100644 packages/examples/vue/src/demos/row-height/row-height.demo-data.ts create mode 100644 packages/examples/vue/src/demos/row-selection/row-selection.demo-data.ts create mode 100644 packages/examples/vue/src/demos/sales/sales.demo-data.ts create mode 100644 packages/examples/vue/src/demos/single-row-children/single-row-children.demo-data.ts create mode 100644 packages/examples/vue/src/demos/spreadsheet/spreadsheet-custom.css create mode 100644 packages/examples/vue/src/demos/spreadsheet/spreadsheet.demo-data.ts create mode 100644 packages/examples/vue/src/demos/table-height/table-height.demo-data.ts create mode 100644 packages/examples/vue/src/demos/themes/themes.demo-data.ts create mode 100644 packages/examples/vue/src/demos/tooltip/tooltip.demo-data.ts create mode 100644 packages/examples/vue/src/demos/value-formatter/value-formatter.demo-data.ts create mode 100644 packages/examples/vue/src/env.d.ts create mode 100644 packages/examples/vue/src/styles/shell.css create mode 100644 packages/react/src/defaultHeadersFromCore.ts create mode 100644 packages/solid/src/defaultHeadersFromCore.ts create mode 100644 packages/svelte/src/defaultHeadersFromCore.ts create mode 100644 packages/vue/src/defaultHeadersFromCore.ts diff --git a/packages/angular/package.json b/packages/angular/package.json index 4dfd7b5e1..b0b48d8f8 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@simple-table/angular", - "version": "3.0.0-beta.16", + "version": "3.0.0-beta.17", "main": "dist/cjs/index.js", "module": "dist/index.es.js", "types": "dist/types/angular/src/index.d.ts", diff --git a/packages/angular/src/buildVanillaConfig.ts b/packages/angular/src/buildVanillaConfig.ts index 13d161f86..ec7a19d2a 100644 --- a/packages/angular/src/buildVanillaConfig.ts +++ b/packages/angular/src/buildVanillaConfig.ts @@ -1,9 +1,10 @@ import type { ApplicationRef, EnvironmentInjector } from "@angular/core"; -import type { SimpleTableConfig, HeaderObject, ColumnEditorConfig } from "simple-table-core"; +import type { SimpleTableConfig, HeaderObject, ColumnEditorConfig, Row } from "simple-table-core"; import type { SimpleTableAngularProps, AngularHeaderObject, AngularColumnEditorConfig, + AngularIconsConfig, } from "./types"; import { wrapAngularRenderer } from "./utils/wrapAngularRenderer"; @@ -14,6 +15,7 @@ export function buildVanillaConfig( ): SimpleTableConfig { const { defaultHeaders, + rows, footerRenderer, emptyStateRenderer, errorStateRenderer, @@ -21,12 +23,28 @@ export function buildVanillaConfig( tableEmptyStateRenderer, headerDropdown, columnEditorConfig, + icons, ...rest } = config; const wrap =

(component: any) => wrapAngularRenderer

(component, appRef, injector); + function transformIcons(icons: AngularIconsConfig): NonNullable { + const result: NonNullable = {}; + for (const [key, value] of Object.entries(icons)) { + if (value == null) continue; + if (typeof value === "string" || value instanceof HTMLElement || value instanceof SVGSVGElement) { + (result as any)[key] = value; + } else if ((value as any).ɵcmp) { + (result as any)[key] = wrap(value as any)({}); + } else { + (result as any)[key] = value; + } + } + return result; + } + function transformColumnEditorConfig(cfg: AngularColumnEditorConfig): ColumnEditorConfig { const { rowRenderer, customRenderer, ...cfgRest } = cfg; return { @@ -66,6 +84,7 @@ export function buildVanillaConfig( const vanillaConfig: SimpleTableConfig = { ...rest, + rows: rows as Row[], defaultHeaders: defaultHeaders.map(transformHeader), }; @@ -113,5 +132,9 @@ export function buildVanillaConfig( vanillaConfig.columnEditorConfig = transformColumnEditorConfig(columnEditorConfig); } + if (icons !== undefined) { + vanillaConfig.icons = transformIcons(icons); + } + return vanillaConfig; } diff --git a/packages/angular/src/defaultHeadersFromCore.ts b/packages/angular/src/defaultHeadersFromCore.ts new file mode 100644 index 000000000..f33527e9a --- /dev/null +++ b/packages/angular/src/defaultHeadersFromCore.ts @@ -0,0 +1,14 @@ +import type { HeaderObject } from "simple-table-core"; +import type { AngularHeaderObject } from "./types"; + +export function defaultHeaderFromCore(header: HeaderObject): AngularHeaderObject { + return header as unknown as AngularHeaderObject; +} + +export function defaultHeadersFromCore(headers: readonly HeaderObject[]): AngularHeaderObject[] { + return headers as unknown as AngularHeaderObject[]; +} + +export function mapToAngularHeaderObjects(columns: readonly object[]): AngularHeaderObject[] { + return columns as unknown as AngularHeaderObject[]; +} diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index e8e49e839..c95bf5e9a 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -1,5 +1,11 @@ // Component export { SimpleTableComponent } from "./lib/SimpleTableComponent"; +export { asRows } from "simple-table-core"; +export { + defaultHeaderFromCore, + defaultHeadersFromCore, + mapToAngularHeaderObjects, +} from "./defaultHeadersFromCore"; // Provider helper export { provideSimpleTable } from "./lib/provideSimpleTable"; @@ -9,6 +15,8 @@ export type { SimpleTableAngularProps, TableInstance, AngularHeaderObject, + AngularIconsConfig, + AngularIconSlot, AngularColumnEditorConfig, AngularCellRenderer, AngularHeaderRenderer, @@ -31,6 +39,7 @@ export type { Cell, CellChangeProps, CellClickProps, + CellRenderer, CellRendererProps, CellValue, ChartOptions, diff --git a/packages/angular/src/lib/SimpleTableComponent.ts b/packages/angular/src/lib/SimpleTableComponent.ts index d1a76435d..ccf49767c 100644 --- a/packages/angular/src/lib/SimpleTableComponent.ts +++ b/packages/angular/src/lib/SimpleTableComponent.ts @@ -15,7 +15,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { TableAPI } from "simple-table-core"; import { buildVanillaConfig } from "../buildVanillaConfig"; import type { SimpleTableAngularProps, TableInstance, AngularHeaderObject } from "../types"; -import type { Row } from "simple-table-core"; /** * SimpleTable — Angular adapter for simple-table-core. @@ -36,7 +35,7 @@ import type { Row } from "simple-table-core"; styles: [":host { display: block; }"], }) export class SimpleTableComponent implements OnInit, OnChanges, OnDestroy { - @Input({ required: true }) rows!: Row[]; + @Input({ required: true }) rows!: SimpleTableAngularProps["rows"]; @Input({ required: true }) defaultHeaders!: AngularHeaderObject[]; // All optional SimpleTableAngularProps inputs diff --git a/packages/angular/src/types.ts b/packages/angular/src/types.ts index 20d802a32..115bfa7ca 100644 --- a/packages/angular/src/types.ts +++ b/packages/angular/src/types.ts @@ -3,6 +3,7 @@ import type { SimpleTableProps, SimpleTableConfig, HeaderObject, + Row, TableAPI, CellRendererProps, HeaderRendererProps, @@ -14,7 +15,6 @@ import type { ColumnEditorRowRendererProps, ColumnEditorCustomRendererProps, ColumnEditorConfig, - IconsConfig, } from "simple-table-core"; // ─── Internal instance contract ─────────────────────────────────────────────── @@ -37,6 +37,23 @@ export type AngularLoadingStateRenderer = Type; export type AngularErrorStateRenderer = Type; export type AngularEmptyStateRenderer = Type; +/** Per-slot icon: Angular component or vanilla element/string (pass-through). */ +export type AngularIconSlot = Type | SVGSVGElement | HTMLElement | string; + +export interface AngularIconsConfig { + drag?: AngularIconSlot; + expand?: AngularIconSlot; + filter?: AngularIconSlot; + headerCollapse?: AngularIconSlot; + headerExpand?: AngularIconSlot; + next?: AngularIconSlot; + prev?: AngularIconSlot; + sortDown?: AngularIconSlot; + sortUp?: AngularIconSlot; + pinnedLeftIcon?: AngularIconSlot; + pinnedRightIcon?: AngularIconSlot; +} + // ─── Column editor config override ─────────────────────────────────────────── export interface AngularColumnEditorConfig extends Omit { @@ -45,33 +62,33 @@ export interface AngularColumnEditorConfig } // ─── HeaderObject override ──────────────────────────────────────────────────── +/** + * Column definition for `defaultHeaders`: core column metadata with Angular-only + * renderer fields. For trees from `simple-table-core`, use `defaultHeadersFromCore` / + * `mapToAngularHeaderObjects`. + */ export interface AngularHeaderObject extends Omit { cellRenderer?: AngularCellRenderer; headerRenderer?: AngularHeaderRenderer; children?: AngularHeaderObject[]; - nestedTable?: Omit; + nestedTable?: Omit< + SimpleTableAngularProps, + | "rows" + | "loadingStateRenderer" + | "errorStateRenderer" + | "emptyStateRenderer" + | "tableEmptyStateRenderer" + >; } // ─── Top-level props ────────────────────────────────────────────────────────── -// Mirrors SimpleTableProps with Angular-specific overrides. -// `tableRef` is omitted — consumers use Angular's @ViewChild decorator instead: -// @ViewChild(SimpleTableComponent) tableRef!: SimpleTableComponent; -// then: this.tableRef.getAPI()?.sort(...) +// Mirrors SimpleTableProps with Angular-specific overrides. Use @ViewChild on the +// table component and `getAPI()` for the imperative TableAPI. export interface SimpleTableAngularProps extends Omit< SimpleTableProps, - | "tableRef" - | "allowAnimations" - | "expandIcon" - | "filterIcon" - | "headerCollapseIcon" - | "headerExpandIcon" - | "nextIcon" - | "prevIcon" - | "sortDownIcon" - | "sortUpIcon" - | "columnEditorText" + | "rows" | "defaultHeaders" | "footerRenderer" | "emptyStateRenderer" @@ -80,8 +97,11 @@ export interface SimpleTableAngularProps | "tableEmptyStateRenderer" | "headerDropdown" | "columnEditorConfig" + | "icons" > { defaultHeaders: AngularHeaderObject[]; + /** Row data: domain objects or core `Row[]`; cast inside the adapter. */ + rows: ReadonlyArray | ReadonlyArray; footerRenderer?: AngularFooterRenderer; loadingStateRenderer?: AngularLoadingStateRenderer; errorStateRenderer?: AngularErrorStateRenderer; @@ -89,7 +109,7 @@ export interface SimpleTableAngularProps tableEmptyStateRenderer?: HTMLElement | string | null; headerDropdown?: AngularHeaderDropdown; columnEditorConfig?: AngularColumnEditorConfig; - icons?: IconsConfig; + icons?: AngularIconsConfig; } // Re-export vanilla prop types that consumers still need directly diff --git a/packages/core/package.json b/packages/core/package.json index e14bb97ae..713edd104 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "simple-table-core", - "version": "3.0.0-beta.16", + "version": "3.0.0-beta.17", "main": "dist/cjs/index.js", "module": "dist/index.es.js", "types": "dist/src/index.d.ts", diff --git a/packages/core/src/core/SimpleTableVanilla.ts b/packages/core/src/core/SimpleTableVanilla.ts index d310103ee..eb12778a8 100644 --- a/packages/core/src/core/SimpleTableVanilla.ts +++ b/packages/core/src/core/SimpleTableVanilla.ts @@ -21,7 +21,6 @@ import AriaAnnouncementManager from "../hooks/ariaAnnouncements"; import { calculateScrollbarWidth } from "../hooks/scrollbarWidth"; import { generateRowId, rowIdToString } from "../utils/rowUtils"; -import { checkDeprecatedProps } from "../utils/deprecatedPropsWarnings"; import { deepClone } from "../utils/generalUtils"; import { @@ -106,8 +105,6 @@ export class SimpleTableVanilla { this.container = container; this.config = config; - checkDeprecatedProps(config); - this.customTheme = TableInitializer.mergeCustomTheme(config); this.mergedColumnEditorConfig = TableInitializer.mergeColumnEditorConfig(config); @@ -618,6 +615,11 @@ export class SimpleTableVanilla { this.render("update"); } + /** @deprecated Use {@link update} — same behavior. */ + updateConfig(config: Partial): void { + this.update(config); + } + destroy(): void { this.mounted = false; this.firstRenderDone = false; diff --git a/packages/core/src/core/initialization/TableInitializer.ts b/packages/core/src/core/initialization/TableInitializer.ts index e2841b955..deda092f2 100644 --- a/packages/core/src/core/initialization/TableInitializer.ts +++ b/packages/core/src/core/initialization/TableInitializer.ts @@ -73,10 +73,7 @@ export class TableInitializer { static mergeColumnEditorConfig(config: SimpleTableConfig): MergedColumnEditorConfig { return { - text: - config.columnEditorConfig?.text ?? - config.columnEditorText ?? - DEFAULT_COLUMN_EDITOR_CONFIG.text, + text: config.columnEditorConfig?.text ?? DEFAULT_COLUMN_EDITOR_CONFIG.text, searchEnabled: config.columnEditorConfig?.searchEnabled ?? DEFAULT_COLUMN_EDITOR_CONFIG.searchEnabled, searchPlaceholder: diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 92857c59f..2b296c2d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,7 +29,6 @@ import type SharedTableProps from "./types/SharedTableProps"; import type SortColumn from "./types/SortColumn"; import type TableHeaderProps from "./types/TableHeaderProps"; import type { TableAPI, SetHeaderRenameProps, ExportToCSVProps } from "./types/TableAPI"; -import type TableRefType from "./types/TableRefType"; import type TableRowProps from "./types/TableRowProps"; import type Theme from "./types/Theme"; import type UpdateDataProps from "./types/UpdateCellProps"; @@ -81,6 +80,7 @@ import type { RowId } from "./types/RowId"; import type { PinnedSectionsState } from "./types/PinnedSectionsState"; export { SimpleTableVanilla }; +export { asRows } from "./utils/asRows"; export type { Accessor, @@ -149,7 +149,6 @@ export type { TableAPI, TableFilterState, TableHeaderProps, - TableRefType, TableRowProps, Theme, PinnedSectionsState, diff --git a/packages/core/src/styles/base.css b/packages/core/src/styles/base.css index 78f23fd37..7ca554ab0 100644 --- a/packages/core/src/styles/base.css +++ b/packages/core/src/styles/base.css @@ -967,6 +967,11 @@ input { color: white; } +/* Set by createTableFooter when page controls exceed MAX_COMPACT_VISIBLE_PAGE_BUTTONS */ +.st-footer-pagination .st-page-btn.st-page-btn--compact-hidden { + display: none; +} + .st-page-ellipsis { display: inline-flex; align-items: center; diff --git a/packages/core/src/styles/themes/modern-dark.css b/packages/core/src/styles/themes/modern-dark.css index c3fce9443..164d99d87 100644 --- a/packages/core/src/styles/themes/modern-dark.css +++ b/packages/core/src/styles/themes/modern-dark.css @@ -226,18 +226,6 @@ font-weight: 600; } -/* Hide buttons beyond the 9th one to prevent overflow */ -.theme-modern-dark .st-page-btn:nth-child(n+10):not(.active) { - display: none; -} - -/* Always show active button and adjacent buttons */ -.theme-modern-dark .st-page-btn.active, -.theme-modern-dark .st-page-btn.active + .st-page-btn, -.theme-modern-dark .st-page-btn:has(+ .st-page-btn.active) { - display: inline-flex; -} - /* Next/Prev buttons */ .theme-modern-dark .st-next-prev-btn { padding: 6px 8px; diff --git a/packages/core/src/styles/themes/modern-light.css b/packages/core/src/styles/themes/modern-light.css index 14b5627fc..8a8baeb5b 100644 --- a/packages/core/src/styles/themes/modern-light.css +++ b/packages/core/src/styles/themes/modern-light.css @@ -226,18 +226,6 @@ font-weight: 600; } -/* Hide buttons beyond the 9th one to prevent overflow */ -.theme-modern-light .st-page-btn:nth-child(n + 10):not(.active) { - display: none; -} - -/* Always show active button and adjacent buttons */ -.theme-modern-light .st-page-btn.active, -.theme-modern-light .st-page-btn.active + .st-page-btn, -.theme-modern-light .st-page-btn:has(+ .st-page-btn.active) { - display: inline-flex; -} - /* Next/Prev buttons */ .theme-modern-light .st-next-prev-btn { padding: 6px 8px; diff --git a/packages/core/src/types/HeaderObject.ts b/packages/core/src/types/HeaderObject.ts index f76097e47..cd33763e7 100644 --- a/packages/core/src/types/HeaderObject.ts +++ b/packages/core/src/types/HeaderObject.ts @@ -114,14 +114,6 @@ type HeaderObject = { | "errorStateRenderer" | "emptyStateRenderer" | "tableEmptyStateRenderer" - | "expandIcon" - | "filterIcon" - | "sortUpIcon" - | "sortDownIcon" - | "nextIcon" - | "prevIcon" - | "headerCollapseIcon" - | "headerExpandIcon" >; pinned?: Pinned; quickFilterable?: boolean; // Default: true - whether column is searchable via quick filter diff --git a/packages/core/src/types/SimpleTableConfig.ts b/packages/core/src/types/SimpleTableConfig.ts index 71c94779f..30492fc27 100644 --- a/packages/core/src/types/SimpleTableConfig.ts +++ b/packages/core/src/types/SimpleTableConfig.ts @@ -24,14 +24,12 @@ import { VanillaIconsConfig } from "./IconsConfig"; import { QuickFilterConfig } from "./QuickFilterTypes"; export interface SimpleTableConfig { - allowAnimations?: boolean; autoExpandColumns?: boolean; canExpandRowGroup?: (row: Row) => boolean; cellUpdateFlash?: boolean; className?: string; columnBorders?: boolean; columnEditorConfig?: ColumnEditorConfig; - columnEditorText?: string; columnReordering?: boolean; columnResizing?: boolean; copyHeadersToClipboard?: boolean; diff --git a/packages/core/src/types/SimpleTableProps.ts b/packages/core/src/types/SimpleTableProps.ts index 9d7f26205..c31ca480c 100644 --- a/packages/core/src/types/SimpleTableProps.ts +++ b/packages/core/src/types/SimpleTableProps.ts @@ -17,7 +17,6 @@ import OnNextPage from "./OnNextPage"; import OnRowGroupExpandProps from "./OnRowGroupExpandProps"; import RowSelectionChangeProps from "./RowSelectionChangeProps"; import { RowButton } from "./RowButton"; -import TableRefType from "./TableRefType"; import Theme from "./Theme"; import { CustomThemeProps } from "./CustomTheme"; import { GetRowId } from "./GetRowId"; @@ -32,7 +31,6 @@ export interface SimpleTableProps { className?: string; // Class name for the table columnBorders?: boolean; // Flag for showing column borders columnEditorConfig?: ColumnEditorConfig; // Configuration for the column editor drawer - columnEditorText?: string; // @deprecated Use columnEditorConfig.text instead columnReordering?: boolean; // Flag for column reordering columnResizing?: boolean; // Flag for column resizing copyHeadersToClipboard?: boolean; // Flag for including column headers when copying cells to clipboard (default: false) @@ -46,14 +44,10 @@ export interface SimpleTableProps { enableStickyParents?: boolean; // Flag for enabling sticky parent rows during scrolling in grouped tables (default: false) errorStateRenderer?: ErrorStateRenderer; // Custom renderer for error states expandAll?: boolean; // Flag for expanding all rows by default - expandIcon?: IconsConfig["expand"]; // @deprecated Use icons.expand instead externalFilterHandling?: boolean; // Flag to let consumer handle filter logic completely externalSortHandling?: boolean; // Flag to let consumer handle sort logic completely - filterIcon?: IconsConfig["filter"]; // @deprecated Use icons.filter instead footerRenderer?: (props: FooterRendererProps) => HTMLElement | string | null; // Custom footer renderer - headerCollapseIcon?: IconsConfig["headerCollapse"]; // @deprecated Use icons.headerCollapse instead headerDropdown?: HeaderDropdown; // Custom dropdown component for headers - headerExpandIcon?: IconsConfig["headerExpand"]; // @deprecated Use icons.headerExpand instead height?: string | number; // Height of the table hideFooter?: boolean; // Flag for hiding the footer hideHeader?: boolean; // Flag for hiding the header @@ -64,7 +58,6 @@ export interface SimpleTableProps { isLoading?: boolean; // Flag for showing loading skeleton state loadingStateRenderer?: LoadingStateRenderer; // Custom renderer for loading states maxHeight?: string | number; // Maximum height of the table (enables adaptive height with virtualization) - nextIcon?: IconsConfig["next"]; // @deprecated Use icons.next instead onCellClick?: (props: CellClickProps) => void; onCellEdit?: (props: CellChangeProps) => void; onColumnOrderChange?: (newHeaders: HeaderObject[]) => void; @@ -80,7 +73,6 @@ export interface SimpleTableProps { onRowGroupExpand?: (props: OnRowGroupExpandProps) => void | Promise; // Callback when a row is expanded/collapsed onRowSelectionChange?: (props: RowSelectionChangeProps) => void; // Callback when row selection changes onSortChange?: (sort: SortColumn | null) => void; // Callback when sort is applied - prevIcon?: IconsConfig["prev"]; // @deprecated Use icons.prev instead quickFilter?: QuickFilterConfig; // Global search configuration across all columns rowButtons?: RowButton[]; // Array of buttons to show in each row rowGrouping?: Accessor[]; // Array of property names that define row grouping hierarchy @@ -91,10 +83,7 @@ export interface SimpleTableProps { selectableColumns?: boolean; // Flag for selectable column headers serverSidePagination?: boolean; // Flag to disable internal pagination slicing (for server-side pagination) shouldPaginate?: boolean; // Flag for pagination - sortDownIcon?: IconsConfig["sortDown"]; // @deprecated Use icons.sortDown instead - sortUpIcon?: IconsConfig["sortUp"]; // @deprecated Use icons.sortUp instead tableEmptyStateRenderer?: HTMLElement | string | null; // Custom empty state component when table has no rows - tableRef?: { current: TableRefType | null }; theme?: Theme; // Theme totalRowCount?: number; // Total number of rows on server (for server-side pagination) useHoverRowBackground?: boolean; // Flag for using hover row background diff --git a/packages/core/src/types/TableRefType.ts b/packages/core/src/types/TableRefType.ts deleted file mode 100644 index fca86b438..000000000 --- a/packages/core/src/types/TableRefType.ts +++ /dev/null @@ -1,77 +0,0 @@ -import UpdateDataProps from "./UpdateCellProps"; -import HeaderObject, { Accessor } from "./HeaderObject"; -import TableRow from "./TableRow"; -import SortColumn, { SortDirection } from "./SortColumn"; -import { TableFilterState, FilterCondition } from "./FilterTypes"; -import type { PinnedSectionsState } from "./PinnedSectionsState"; - -interface SetHeaderRenameProps { - accessor: Accessor; -} - -interface ExportToCSVProps { - filename?: string; -} - -type TableRefType = { - updateData: (props: UpdateDataProps) => void; - setHeaderRename: (props: SetHeaderRenameProps) => void; - /** Returns the currently visible rows (e.g., current page when paginated) */ - getVisibleRows: () => TableRow[]; - /** Returns all rows (flattened, including nested/grouped rows) */ - getAllRows: () => TableRow[]; - /** Returns the table's header/column definitions */ - getHeaders: () => HeaderObject[]; - exportToCSV: (props?: ExportToCSVProps) => void; - /** Returns the current sort state */ - getSortState: () => SortColumn | null; - /** Applies a new sort state to the table. Pass null to clear sort. Direction defaults to cycling through asc -> desc -> null */ - applySortState: (props?: { accessor: Accessor; direction?: SortDirection }) => Promise; - /** Ordered root accessors per pin section (left, main/unpinned, right) */ - getPinnedState: () => PinnedSectionsState; - /** Reorder root columns and set pinned flags; lists must include every root accessor exactly once. Essential order is clamped per section. */ - applyPinnedState: (state: PinnedSectionsState) => Promise; - /** Returns the current filter state */ - getFilterState: () => TableFilterState; - /** Applies a filter to a specific column */ - applyFilter: (filter: FilterCondition) => Promise; - /** Clears a filter for a specific column */ - clearFilter: (accessor: Accessor) => Promise; - /** Clears all filters */ - clearAllFilters: () => Promise; - /** Returns the current page number (1-indexed) */ - getCurrentPage: () => number; - /** Returns the total number of pages */ - getTotalPages: () => number; - /** Sets the current page (1-indexed) and triggers onPageChange callback */ - setPage: (page: number) => Promise; - /** Expand all rows at all depths */ - expandAll: () => void; - /** Collapse all rows at all depths */ - collapseAll: () => void; - /** Expand all rows at a specific depth (0-indexed) */ - expandDepth: (depth: number) => void; - /** Collapse all rows at a specific depth (0-indexed) */ - collapseDepth: (depth: number) => void; - /** Toggle expansion for a specific depth */ - toggleDepth: (depth: number) => void; - /** Set which depths are expanded (replaces current state) */ - setExpandedDepths: (depths: Set) => void; - /** Get currently expanded depths */ - getExpandedDepths: () => Set; - /** Get the grouping property name for a depth index */ - getGroupingProperty: (depth: number) => Accessor | undefined; - /** Get the depth index for a grouping property name */ - getGroupingDepth: (property: Accessor) => number; - /** Toggle the column editor menu. Pass true to open, false to close, or no argument to toggle. Only works if editColumns prop is enabled. */ - toggleColumnEditor: (open?: boolean) => void; - /** Apply column visibility changes. Pass an object with column accessors as keys and boolean visibility as values. */ - applyColumnVisibility: (visibility: { [accessor: string]: boolean }) => Promise; - /** Reset columns to default order and visibility (restores defaultHeaders state). */ - resetColumns: () => void; - /** Set the quick filter text programmatically. Triggers the onChange callback if provided in quickFilter config. */ - setQuickFilter: (text: string) => void; -}; - -export default TableRefType; -export type { SetHeaderRenameProps, ExportToCSVProps }; diff --git a/packages/core/src/utils/asRows.ts b/packages/core/src/utils/asRows.ts new file mode 100644 index 000000000..ad0a1538a --- /dev/null +++ b/packages/core/src/utils/asRows.ts @@ -0,0 +1,10 @@ +import type Row from "../types/Row"; + +/** + * Structural typing bridge: domain row interfaces often do not assign to {@link Row}[] + * because `Row` uses an index signature. Use when passing typed data to `rows` and + * similar APIs. + */ +export function asRows(rows: readonly T[]): Row[] { + return rows as unknown as Row[]; +} diff --git a/packages/core/src/utils/deprecatedPropsWarnings.ts b/packages/core/src/utils/deprecatedPropsWarnings.ts deleted file mode 100644 index a5f4ddcea..000000000 --- a/packages/core/src/utils/deprecatedPropsWarnings.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { SimpleTableConfig } from "../types/SimpleTableConfig"; -import { SimpleTableProps } from "../types/SimpleTableProps"; - -/** - * Configuration for deprecated props - */ -interface DeprecatedProp { - /** The deprecated prop name */ - propName: keyof SimpleTableProps; - /** The replacement prop or configuration */ - replacement: string; - /** Optional additional message */ - message?: string; -} - -/** - * List of deprecated props with their replacements - */ -const DEPRECATED_PROPS: DeprecatedProp[] = [ - { - propName: "columnEditorText", - replacement: "columnEditorConfig.text", - message: - "Use the columnEditorConfig object instead for better organization of column editor settings.", - }, - { - propName: "expandIcon", - replacement: "icons.expand", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "filterIcon", - replacement: "icons.filter", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "headerCollapseIcon", - replacement: "icons.headerCollapse", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "headerExpandIcon", - replacement: "icons.headerExpand", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "nextIcon", - replacement: "icons.next", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "prevIcon", - replacement: "icons.prev", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "sortDownIcon", - replacement: "icons.sortDown", - message: "Use the icons object instead for better organization of icon settings.", - }, - { - propName: "sortUpIcon", - replacement: "icons.sortUp", - message: "Use the icons object instead for better organization of icon settings.", - }, -]; - -/** - * Checks for deprecated props and logs console errors with helpful migration messages - * @param props - The SimpleTable props to check - */ -export const checkDeprecatedProps = (props: SimpleTableConfig): void => { - // Only run in development mode - if (process.env.NODE_ENV === "production") { - return; - } - - DEPRECATED_PROPS.forEach(({ propName, replacement, message }) => { - if (props[propName] !== undefined) { - const baseMessage = `SimpleTable: The "${propName}" prop is deprecated and will be removed in a future version.`; - const replacementMessage = `Please use "${replacement}" instead.`; - const additionalMessage = message ? `\n${message}` : ""; - - const [objectName, propertyName] = replacement.split("."); - const exampleValue = - typeof props[propName] === "string" ? JSON.stringify(props[propName]) : ""; - - console.error( - `${baseMessage}\n${replacementMessage}${additionalMessage}\n\nExample:\n`, - ); - } - }); -}; - -/** - * Export the list of deprecated props for testing or documentation purposes - */ -export const getDeprecatedPropNames = (): string[] => { - return DEPRECATED_PROPS.map((prop) => prop.propName); -}; diff --git a/packages/core/src/utils/footer/createTableFooter.ts b/packages/core/src/utils/footer/createTableFooter.ts index 32e0d2c7a..3fa124b43 100644 --- a/packages/core/src/utils/footer/createTableFooter.ts +++ b/packages/core/src/utils/footer/createTableFooter.ts @@ -36,6 +36,9 @@ export interface CreateTableFooterOptions { nextIcon?: string | HTMLElement | SVGSVGElement; } +/** Page controls beyond this count are hidden unless current, jump-after-ellipsis, or ±1 neighbor of current. */ +const MAX_COMPACT_VISIBLE_PAGE_BUTTONS = 9; + const getVisiblePages = (currentPage: number, totalPages: number): number[] => { if (totalPages <= 15) { return Array.from({ length: totalPages }, (_, i) => i + 1); @@ -187,6 +190,8 @@ export const createTableFooter = (options: CreateTableFooterOptions) => { const visiblePages = getVisiblePages(currentPage, totalPages); + const pageButtonMetas: Array<{ button: HTMLButtonElement; page: number; afterEllipsis: boolean }> = []; + visiblePages.forEach((page, index) => { if (page < 0) { const ellipsis = document.createElement("span"); @@ -195,7 +200,8 @@ export const createTableFooter = (options: CreateTableFooterOptions) => { paginationDiv.appendChild(ellipsis); } else { const pageBtn = document.createElement("button"); - pageBtn.className = `st-page-btn ${currentPage === page ? "active" : ""}`; + const afterEllipsis = index > 0 && visiblePages[index - 1]! < 0; + pageBtn.className = ["st-page-btn", currentPage === page ? "active" : ""].filter(Boolean).join(" "); pageBtn.textContent = page.toString(); pageBtn.setAttribute("aria-label", `Go to page ${page}`); if (currentPage === page) { @@ -203,6 +209,26 @@ export const createTableFooter = (options: CreateTableFooterOptions) => { } pageBtn.addEventListener("click", () => handlePageChange(page)); paginationDiv.appendChild(pageBtn); + pageButtonMetas.push({ button: pageBtn, page, afterEllipsis }); + } + }); + + pageButtonMetas.forEach((meta, i) => { + const { button, page, afterEllipsis } = meta; + const ordinal = i + 1; + const isActive = currentPage === page; + const prev = button.previousElementSibling; + const next = button.nextElementSibling; + const nextToActive = + (prev !== null && + prev.classList.contains("st-page-btn") && + prev.classList.contains("active")) || + (next !== null && + next.classList.contains("st-page-btn") && + next.classList.contains("active")); + const forceVisible = isActive || afterEllipsis || nextToActive; + if (ordinal > MAX_COMPACT_VISIBLE_PAGE_BUTTONS && !forceVisible) { + button.classList.add("st-page-btn--compact-hidden"); } }); diff --git a/packages/core/stories/tests/37-TableRefMethodsTests.stories.ts b/packages/core/stories/tests/37-TableRefMethodsTests.stories.ts index 521f81141..f50b43d4b 100644 --- a/packages/core/stories/tests/37-TableRefMethodsTests.stories.ts +++ b/packages/core/stories/tests/37-TableRefMethodsTests.stories.ts @@ -1,6 +1,6 @@ /** * TABLE REF METHODS TESTS - * Tests for tableRef / getAPI() methods: getVisibleRows, getAllRows, getHeaders, + * Tests for TableAPI (getAPI()) methods: getVisibleRows, getAllRows, getHeaders, * getSortState/applySortState, getFilterState/applyFilter/clearFilter, * getCurrentPage/setPage, setQuickFilter, toggleColumnEditor/applyColumnVisibility, * expandAll/collapseAll/getExpandedDepths. diff --git a/packages/examples/angular/package.json b/packages/examples/angular/package.json index 89c2e0239..9090acb7c 100644 --- a/packages/examples/angular/package.json +++ b/packages/examples/angular/package.json @@ -16,7 +16,6 @@ "@angular/platform-browser": "^19.0.0", "rxjs": "^7.0.0", "@simple-table/angular": "workspace:*", - "@simple-table/examples-shared": "workspace:*", "zone.js": "^0.15.0" }, "devDependencies": { diff --git a/packages/examples/angular/src/app.component.ts b/packages/examples/angular/src/app.component.ts index 367316e7a..5d7bcf9ad 100644 --- a/packages/examples/angular/src/app.component.ts +++ b/packages/examples/angular/src/app.component.ts @@ -5,7 +5,7 @@ import { OnInit, OnDestroy, } from "@angular/core"; -import { DEMO_LIST } from "@simple-table/examples-shared"; +import { DEMO_LIST } from "./demo-list"; import { QuickStartDemoComponent } from "./demos/quick-start/quick-start-demo.component"; import { ColumnFilteringDemoComponent } from "./demos/column-filtering/column-filtering-demo.component"; import { ColumnSortingDemoComponent } from "./demos/column-sorting/column-sorting-demo.component"; diff --git a/packages/examples/shared/src/utils/index.ts b/packages/examples/angular/src/demo-list.ts similarity index 100% rename from packages/examples/shared/src/utils/index.ts rename to packages/examples/angular/src/demo-list.ts diff --git a/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions-demo.component.ts b/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions-demo.component.ts index 5e8d8861d..b4956c585 100644 --- a/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions-demo.component.ts +++ b/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { aggregateFunctionsConfig } from "@simple-table/examples-shared"; +import { aggregateFunctionsConfig } from "./aggregate-functions.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,6 +24,6 @@ export class AggregateFunctionsDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = aggregateFunctionsConfig.rows; - readonly headers: AngularHeaderObject[] = aggregateFunctionsConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(aggregateFunctionsConfig.headers); readonly grouping = aggregateFunctionsConfig.tableProps.rowGrouping; } diff --git a/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions.demo-data.ts b/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions.demo-data.ts new file mode 100644 index 000000000..f271ffdc8 --- /dev/null +++ b/packages/examples/angular/src/demos/aggregate-functions/aggregate-functions.demo-data.ts @@ -0,0 +1,193 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const aggregateFunctionsHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: 200, expandable: true, type: "string" }, + { + accessor: "followers", + label: "Followers", + width: 120, + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value === "number") { + return value >= 1000000 + ? `${(value / 1000000).toFixed(1)}M` + : value >= 1000 + ? `${(value / 1000).toFixed(0)}K` + : value.toString(); + } + return ""; + }, + }, + { + accessor: "revenue", + label: "Monthly Revenue", + width: 140, + type: "string", + aggregation: { + type: "sum", + parseValue: (value) => { + if (typeof value !== "string") return 0; + const numericValue = parseFloat(value.replace(/[$K]/g, "")); + return isNaN(numericValue) ? 0 : numericValue; + }, + }, + valueFormatter: ({ value }) => { + if (typeof value === "number") return `$${value.toFixed(1)}K`; + if (typeof value === "string") return value; + return ""; + }, + }, + { + accessor: "rating", + label: "Rating", + width: 100, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${value.toFixed(1)} ⭐` : ""), + }, + { + accessor: "contentCount", + label: "Content", + width: 90, + type: "number", + aggregation: { type: "sum" }, + }, + { + accessor: "avgViewTime", + label: "Avg Watch Time", + width: 130, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${Math.round(value)}min` : ""), + }, + { accessor: "status", label: "Status", width: 120, type: "string" }, +]; + +export const aggregateFunctionsData = [ + { + id: 1, + name: "StreamFlix", + status: "Leading Platform", + categories: [ + { + id: 101, + name: "Gaming", + status: "Trending", + creators: [ + { id: 1001, name: "PixelMaster", followers: 2800000, revenue: "$45.2K", rating: 4.8, contentCount: 328, avgViewTime: 45, status: "Partner" }, + { id: 1002, name: "RetroGamer93", followers: 1200000, revenue: "$28.5K", rating: 4.6, contentCount: 156, avgViewTime: 52, status: "Partner" }, + { id: 1003, name: "SpeedrunQueen", followers: 890000, revenue: "$22.1K", rating: 4.9, contentCount: 89, avgViewTime: 38, status: "Partner" }, + ], + }, + { + id: 102, + name: "Music & Arts", + status: "Growing", + creators: [ + { id: 1101, name: "MelodyMaker", followers: 1650000, revenue: "$31.8K", rating: 4.7, contentCount: 203, avgViewTime: 28, status: "Partner" }, + { id: 1102, name: "DigitalArtist", followers: 720000, revenue: "$18.9K", rating: 4.5, contentCount: 127, avgViewTime: 35, status: "Affiliate" }, + { id: 1103, name: "JazzVibez", followers: 430000, revenue: "$12.4K", rating: 4.8, contentCount: 78, avgViewTime: 42, status: "Affiliate" }, + ], + }, + { + id: 103, + name: "Cooking & Lifestyle", + status: "Stable", + creators: [ + { id: 1201, name: "ChefExtraordinaire", followers: 3200000, revenue: "$58.7K", rating: 4.9, contentCount: 245, avgViewTime: 22, status: "Partner" }, + { id: 1202, name: "HomeDecorGuru", followers: 980000, revenue: "$19.3K", rating: 4.4, contentCount: 134, avgViewTime: 18, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 2, + name: "WatchNow", + status: "Competitor", + categories: [ + { + id: 201, + name: "Tech Reviews", + status: "Hot", + creators: [ + { id: 2001, name: "TechGuru2024", followers: 2100000, revenue: "$42.6K", rating: 4.6, contentCount: 189, avgViewTime: 35, status: "Partner" }, + { id: 2002, name: "GadgetWhisperer", followers: 1450000, revenue: "$29.1K", rating: 4.7, contentCount: 156, avgViewTime: 31, status: "Partner" }, + { id: 2003, name: "CodeReviewer", followers: 680000, revenue: "$16.8K", rating: 4.8, contentCount: 94, avgViewTime: 48, status: "Affiliate" }, + ], + }, + { + id: 202, + name: "Fitness & Health", + status: "Growing", + creators: [ + { id: 2101, name: "FitnessPhenom", followers: 1890000, revenue: "$35.4K", rating: 4.5, contentCount: 312, avgViewTime: 25, status: "Partner" }, + { id: 2102, name: "YogaMaster", followers: 1100000, revenue: "$21.7K", rating: 4.9, contentCount: 178, avgViewTime: 33, status: "Partner" }, + ], + }, + ], + }, + { + id: 3, + name: "CreativeSpace", + status: "Emerging", + categories: [ + { + id: 301, + name: "Photography", + status: "Niche", + creators: [ + { id: 3001, name: "LensArtist", followers: 750000, revenue: "$18.2K", rating: 4.7, contentCount: 145, avgViewTime: 27, status: "Partner" }, + { id: 3002, name: "NatureShooter", followers: 520000, revenue: "$13.5K", rating: 4.6, contentCount: 98, avgViewTime: 29, status: "Affiliate" }, + { id: 3003, name: "PortraitPro", followers: 390000, revenue: "$9.8K", rating: 4.8, contentCount: 67, avgViewTime: 24, status: "Affiliate" }, + ], + }, + { + id: 302, + name: "Animation & VFX", + status: "Specialized", + creators: [ + { id: 3101, name: "3DAnimator", followers: 640000, revenue: "$15.9K", rating: 4.9, contentCount: 58, avgViewTime: 41, status: "Partner" }, + { id: 3102, name: "VFXWizard", followers: 480000, revenue: "$12.3K", rating: 4.7, contentCount: 42, avgViewTime: 38, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 4, + name: "EduStream", + status: "Educational Focus", + categories: [ + { + id: 401, + name: "Science & Math", + status: "Educational", + creators: [ + { id: 4001, name: "MathExplainer", followers: 1340000, revenue: "$26.8K", rating: 4.8, contentCount: 234, avgViewTime: 36, status: "Partner" }, + { id: 4002, name: "PhysicsPhun", followers: 890000, revenue: "$19.4K", rating: 4.6, contentCount: 167, avgViewTime: 42, status: "Partner" }, + { id: 4003, name: "ChemistryLab", followers: 560000, revenue: "$14.2K", rating: 4.7, contentCount: 89, avgViewTime: 33, status: "Affiliate" }, + ], + }, + { + id: 402, + name: "History & Culture", + status: "Informative", + creators: [ + { id: 4101, name: "HistoryBuff", followers: 920000, revenue: "$18.6K", rating: 4.5, contentCount: 145, avgViewTime: 39, status: "Partner" }, + { id: 4102, name: "CultureExplorer", followers: 670000, revenue: "$15.1K", rating: 4.8, contentCount: 112, avgViewTime: 45, status: "Affiliate" }, + ], + }, + ], + }, +]; + +export const aggregateFunctionsConfig = { + headers: aggregateFunctionsHeaders, + rows: aggregateFunctionsData, + tableProps: { + rowGrouping: ["categories", "creators"] as string[], + columnResizing: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/billing/billing-demo.component.ts b/packages/examples/angular/src/demos/billing/billing-demo.component.ts index 11dfa66fd..615c91edc 100644 --- a/packages/examples/angular/src/demos/billing/billing-demo.component.ts +++ b/packages/examples/angular/src/demos/billing/billing-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, CellRenderer, Row, Theme } from "@simple-table/angular"; -import { billingConfig } from "@simple-table/examples-shared"; -import type { BillingRow } from "@simple-table/examples-shared"; +import { billingConfig } from "./billing.demo-data"; +import type { BillingRow } from "./billing.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -33,7 +33,8 @@ export class BillingDemoComponent { readonly grouping = ["invoices", "charges"]; readonly rows: Row[] = billingConfig.rows as unknown as Row[]; - readonly headers: AngularHeaderObject[] = billingConfig.headers.map((h) => { + readonly headers: AngularHeaderObject[] = mapToAngularHeaderObjects( + billingConfig.headers.map((h) => { if (h.accessor === "name") { const nameRenderer: CellRenderer = ({ row }) => { const d = row as unknown as BillingRow; @@ -47,6 +48,7 @@ export class BillingDemoComponent { }; return { ...h, cellRenderer: nameRenderer }; } - return { ...h }; - }); + return h; + }), + ); } diff --git a/packages/examples/angular/src/demos/billing/billing.demo-data.ts b/packages/examples/angular/src/demos/billing/billing.demo-data.ts new file mode 100644 index 000000000..c0c5fcb63 --- /dev/null +++ b/packages/examples/angular/src/demos/billing/billing.demo-data.ts @@ -0,0 +1,199 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + +export interface BillingRow { + id: string | number; + name: string; + type: string; + amount: number; + deferredRevenue: number; + recognizedRevenue: number; + invoices?: BillingRow[]; + charges?: BillingRow[]; + [key: `balance_${string}`]: number; + [key: `revenue_${string}`]: number; +} + + +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 months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +function generateMonthlyData(): Record { + const data: Record = {}; + const year = 2024; + for (let m = 0; m < 12; m++) { + const mo = months[m]; + const base = Math.round((1000 + Math.random() * 50000) * 100) / 100; + data[`balance_${mo}_${year}`] = base; + data[`revenue_${mo}_${year}`] = Math.round(base * (0.6 + Math.random() * 0.3) * 100) / 100; + } + return data; +} + +export function generateBillingData(count: number = 30): BillingRow[] { + const rows: BillingRow[] = []; + for (let i = 0; i < count; i++) { + const accountName = ACCOUNT_NAMES[i % ACCOUNT_NAMES.length]; + const totalAmount = Math.round((5000 + Math.random() * 200000) * 100) / 100; + const recognized = Math.round(totalAmount * (0.3 + Math.random() * 0.5) * 100) / 100; + const invoices: BillingRow[] = []; + const invoiceCount = 2 + Math.floor(Math.random() * 3); + for (let j = 0; j < invoiceCount; j++) { + const invAmount = Math.round((totalAmount / invoiceCount) * 100) / 100; + const invRecognized = Math.round(invAmount * (0.4 + Math.random() * 0.4) * 100) / 100; + const charges: BillingRow[] = []; + const chargeCount = 1 + Math.floor(Math.random() * 3); + for (let k = 0; k < chargeCount; k++) { + const chargeAmount = Math.round((invAmount / chargeCount) * 100) / 100; + charges.push({ + id: `${i + 1}-inv${j + 1}-chg${k + 1}`, + name: CHARGE_TYPES[k % CHARGE_TYPES.length], + type: "charge", + amount: chargeAmount, + deferredRevenue: Math.round(chargeAmount * 0.3 * 100) / 100, + recognizedRevenue: Math.round(chargeAmount * 0.7 * 100) / 100, + ...generateMonthlyData(), + }); + } + invoices.push({ + id: `${i + 1}-inv${j + 1}`, + name: `${INVOICE_PREFIXES[j % 3]}-${String(i * 10 + j + 1).padStart(4, "0")}`, + type: "invoice", + amount: invAmount, + deferredRevenue: Math.round((invAmount - invRecognized) * 100) / 100, + recognizedRevenue: invRecognized, + charges, + ...generateMonthlyData(), + }); + } + rows.push({ + id: i + 1, + name: accountName, + type: "account", + amount: totalAmount, + deferredRevenue: Math.round((totalAmount - recognized) * 100) / 100, + recognizedRevenue: recognized, + invoices, + ...generateMonthlyData(), + }); + } + return rows; +} + +export const billingData = generateBillingData(30); + +function generateMonthHeaders(): HeaderObject[] { + const headers: HeaderObject[] = []; + const year = 2024; + for (let monthIndex = 11; monthIndex >= 0; monthIndex--) { + const fullMonthName = new Date(year, monthIndex).toLocaleString("default", { month: "long" }); + const mo = months[monthIndex]; + headers.push({ + accessor: `month_${mo}_${year}`, + label: `${fullMonthName} ${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + children: [ + { + disableReorder: true, + label: "Balance", + accessor: `balance_${mo}_${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + disableReorder: true, + label: "Revenue", + accessor: `revenue_${mo}_${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + ], + }); + } + return headers; +} + +export const billingHeaders: HeaderObject[] = [ + { + accessor: "name", + label: "Name", + width: 250, + expandable: true, + isSortable: true, + isEditable: false, + align: "left", + pinned: "left", + type: "string", + }, + { + accessor: "amount", + label: "Total Amount", + width: 130, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + accessor: "deferredRevenue", + label: "Deferred Revenue", + width: 180, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + accessor: "recognizedRevenue", + label: "Recognized Revenue", + width: 180, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + ...generateMonthHeaders(), +]; + +export const billingConfig = { + headers: billingHeaders, + rows: billingData, +} as const; diff --git a/packages/examples/angular/src/demos/cell-clicking/cell-clicking-demo.component.ts b/packages/examples/angular/src/demos/cell-clicking/cell-clicking-demo.component.ts index 074db8e17..dad6d6a2a 100644 --- a/packages/examples/angular/src/demos/cell-clicking/cell-clicking-demo.component.ts +++ b/packages/examples/angular/src/demos/cell-clicking/cell-clicking-demo.component.ts @@ -1,9 +1,9 @@ import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, CellClickProps, Theme } from "@simple-table/angular"; -import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "@simple-table/examples-shared"; -import type { ProjectTask } from "@simple-table/examples-shared"; +import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "./cell-clicking.demo-data"; +import type { ProjectTask } from "./cell-clicking.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -50,7 +50,7 @@ export class CellClickingDemoComponent { get isDark() { return this.theme === "modern-dark" || this.theme === "dark"; } - headers: AngularHeaderObject[] = cellClickingHeaders.map((h) => { + headers: AngularHeaderObject[] = mapToAngularHeaderObjects(cellClickingHeaders.map((h) => { if (h.accessor === "priority") { return { ...h, cellRenderer: ({ row }: { row: Record }) => { const p = String(row.priority); @@ -83,7 +83,7 @@ export class CellClickingDemoComponent { }}; } return { ...h }; - }); + })); handleCellClick = ({ accessor, rowIndex, value, row }: CellClickProps) => { const task = row as ProjectTask; diff --git a/packages/examples/angular/src/demos/cell-clicking/cell-clicking.demo-data.ts b/packages/examples/angular/src/demos/cell-clicking/cell-clicking.demo-data.ts new file mode 100644 index 000000000..44db4d52c --- /dev/null +++ b/packages/examples/angular/src/demos/cell-clicking/cell-clicking.demo-data.ts @@ -0,0 +1,47 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export type ProjectTask = { + id: number; + task: string; + assignee: string; + priority: string; + status: string; + dueDate: string; + estimatedHours: number; + completedHours: number; + details: string; +}; + +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" }, +]; + +export const cellClickingHeaders: HeaderObject[] = [ + { accessor: "id", label: "Task ID", width: 80, isSortable: true, type: "number" }, + { accessor: "task", label: "Task Name", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "assignee", label: "Assignee", width: 120, isSortable: true, type: "string" }, + { accessor: "priority", label: "Priority", width: 100, isSortable: true, type: "string" }, + { accessor: "status", label: "Status", width: 120, isSortable: true, type: "string" }, + { accessor: "dueDate", label: "Due Date", width: 120, isSortable: true, type: "date" }, + { accessor: "estimatedHours", label: "Est. Hours", width: 100, isSortable: true, type: "number" }, + { accessor: "completedHours", label: "Done Hours", width: 100, isSortable: true, type: "number" }, + { accessor: "details", label: "View Details", width: 120, type: "other" }, +]; + +export const cellClickingConfig = { + headers: cellClickingHeaders, + rows: cellClickingData, +} as const; + +export { STATUSES as CELL_CLICKING_STATUSES }; diff --git a/packages/examples/angular/src/demos/cell-editing/cell-editing-demo.component.ts b/packages/examples/angular/src/demos/cell-editing/cell-editing-demo.component.ts index 0e2f1baf4..785ed8c0c 100644 --- a/packages/examples/angular/src/demos/cell-editing/cell-editing-demo.component.ts +++ b/packages/examples/angular/src/demos/cell-editing/cell-editing-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, CellChangeProps, Theme } from "@simple-table/angular"; -import { cellEditingConfig } from "@simple-table/examples-shared"; +import { cellEditingConfig } from "./cell-editing.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -22,7 +22,7 @@ export class CellEditingDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = cellEditingConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(cellEditingConfig.headers); data = [...cellEditingConfig.rows]; onCellEdit({ accessor, newValue, row }: CellChangeProps): void { diff --git a/packages/examples/angular/src/demos/cell-editing/cell-editing.demo-data.ts b/packages/examples/angular/src/demos/cell-editing/cell-editing.demo-data.ts new file mode 100644 index 000000000..43bde2a8e --- /dev/null +++ b/packages/examples/angular/src/demos/cell-editing/cell-editing.demo-data.ts @@ -0,0 +1,38 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const cellEditingHeaders: HeaderObject[] = [ + { accessor: "firstName", label: "First Name", width: "1fr", minWidth: 100, isEditable: true, type: "string" }, + { accessor: "lastName", label: "Last Name", width: 120, isEditable: true, type: "string" }, + { accessor: "role", label: "Role", width: 120, isEditable: true, type: "enum", enumOptions: [ + { label: "Developer", value: "Developer" }, + { label: "Designer", value: "Designer" }, + { label: "Manager", value: "Manager" }, + { label: "Marketing", value: "Marketing" }, + { label: "QA", value: "QA" }, + ]}, + { accessor: "hireDate", label: "Hire Date", width: 120, isEditable: true, type: "date" }, + { accessor: "isActive", label: "Active", width: 100, isEditable: true, type: "boolean" }, + { accessor: "salary", label: "Salary", width: 120, isEditable: true, type: "number" }, +]; + +export const cellEditingData = [ + { 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 }, + { id: 4, firstName: "Savanna", lastName: "Fields", role: "Developer", hireDate: "2022-02-14", isActive: false, salary: 81000 }, + { id: 5, firstName: "Canyon", lastName: "Stone", role: "Marketing", hireDate: "2021-09-20", isActive: true, salary: 73000 }, + { id: 6, firstName: "Meadow", lastName: "Vale", role: "QA", hireDate: "2020-06-25", isActive: true, salary: 79000 }, + { id: 7, firstName: "Ridge", lastName: "Peak", role: "Manager", hireDate: "2019-01-20", isActive: true, salary: 92000 }, + { id: 8, firstName: "Tundra", lastName: "Frost", role: "Developer", hireDate: "2022-05-03", isActive: false, salary: 85000 }, + { id: 9, firstName: "Prairie", lastName: "Wind", role: "Designer", hireDate: "2021-10-12", isActive: true, salary: 77000 }, + { id: 10, firstName: "Delta", lastName: "Flow", role: "Developer", hireDate: "2020-08-17", isActive: true, salary: 83000 }, + { id: 11, firstName: "Grove", lastName: "Shade", role: "Designer", hireDate: "2022-01-09", isActive: true, salary: 76000 }, + { id: 12, firstName: "Cliff", lastName: "Edge", role: "QA", hireDate: "2019-12-04", isActive: false, salary: 82000 }, +]; + +export const cellEditingConfig = { + headers: cellEditingHeaders, + rows: cellEditingData, +} as const; diff --git a/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting-demo.component.ts b/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting-demo.component.ts index eba9b8ef0..4f99e1503 100644 --- a/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting-demo.component.ts +++ b/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { cellHighlightingConfig } from "@simple-table/examples-shared"; +import { cellHighlightingConfig } from "./cell-highlighting.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,7 +24,7 @@ export class CellHighlightingDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = cellHighlightingConfig.rows; - readonly headers: AngularHeaderObject[] = cellHighlightingConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(cellHighlightingConfig.headers); readonly selectableCells = cellHighlightingConfig.tableProps.selectableCells; readonly selectableColumns = cellHighlightingConfig.tableProps.selectableColumns; } diff --git a/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting.demo-data.ts b/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting.demo-data.ts new file mode 100644 index 000000000..c9f13c916 --- /dev/null +++ b/packages/examples/angular/src/demos/cell-highlighting/cell-highlighting.demo-data.ts @@ -0,0 +1,33 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const cellHighlightingHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { accessor: "name", label: "Name", minWidth: 80, width: "1fr", isSortable: true, type: "string" }, + { accessor: "age", label: "Age", width: 100, isSortable: true, type: "number" }, + { accessor: "role", label: "Role", width: 150, isSortable: true, type: "string" }, + { accessor: "department", label: "Department", width: 150, isSortable: true, type: "string" }, + { accessor: "startDate", label: "Start Date", width: 150, isSortable: true, type: "date" }, +]; + +export const cellHighlightingData = [ + { 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" }, + { id: 4, name: "Cleo Silva", age: 22, role: "Front Desk Associate", department: "Customer Service", startDate: "2023-05-12" }, + { id: 5, name: "Bodhi Rodriguez", age: 31, role: "Nutritionist", department: "Wellness", startDate: "2020-11-08" }, + { id: 6, name: "Indie Chen", age: 28, role: "Swim Instructor", department: "Aquatics", startDate: "2021-07-14" }, + { id: 7, name: "Skye Williams", age: 25, role: "Group Fitness Instructor", department: "Group Classes", startDate: "2022-04-03" }, + { id: 8, name: "Rio Garcia", age: 33, role: "Equipment Specialist", department: "Maintenance", startDate: "2020-02-17" }, + { id: 9, name: "Wren Kumar", age: 27, role: "Wellness Coach", department: "Wellness", startDate: "2021-09-25" }, + { id: 10, name: "Storm Lee", age: 30, role: "Pilates Instructor", department: "Group Classes", startDate: "2020-12-01" }, + { id: 11, name: "Vale Davis", age: 24, role: "Membership Coordinator", department: "Sales", startDate: "2023-02-18" }, + { id: 12, name: "Cruz Martinez", age: 36, role: "Head Trainer", department: "Fitness", startDate: "2018-06-12" }, +]; + +export const cellHighlightingConfig = { + headers: cellHighlightingHeaders, + rows: cellHighlightingData, + tableProps: { selectableCells: true, selectableColumns: true }, +} as const; diff --git a/packages/examples/angular/src/demos/cell-renderer/cell-renderer-demo.component.ts b/packages/examples/angular/src/demos/cell-renderer/cell-renderer-demo.component.ts index 47d9bb9b3..5ce61905b 100644 --- a/packages/examples/angular/src/demos/cell-renderer/cell-renderer-demo.component.ts +++ b/packages/examples/angular/src/demos/cell-renderer/cell-renderer-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, CellRenderer, Row, Theme } from "@simple-table/angular"; -import { cellRendererConfig } from "@simple-table/examples-shared"; -import type { CellRendererEmployee } from "@simple-table/examples-shared"; +import { cellRendererConfig } from "./cell-renderer.demo-data"; +import type { CellRendererEmployee } from "./cell-renderer.demo-data"; import "@simple-table/angular/styles.css"; const html = (str: string): Node => { @@ -106,8 +106,10 @@ export class CellRendererDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = cellRendererConfig.rows; - readonly headers: AngularHeaderObject[] = cellRendererConfig.headers.map((h) => { - const renderer = RENDERERS[h.accessor as string]; - return renderer ? { ...h, cellRenderer: renderer as any } : h; - }); + readonly headers: AngularHeaderObject[] = mapToAngularHeaderObjects( + cellRendererConfig.headers.map((h) => { + const renderer = RENDERERS[h.accessor as string]; + return renderer ? { ...h, cellRenderer: renderer as any } : h; + }), + ); } diff --git a/packages/examples/angular/src/demos/cell-renderer/cell-renderer.demo-data.ts b/packages/examples/angular/src/demos/cell-renderer/cell-renderer.demo-data.ts new file mode 100644 index 000000000..2df861639 --- /dev/null +++ b/packages/examples/angular/src/demos/cell-renderer/cell-renderer.demo-data.ts @@ -0,0 +1,51 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export type CellRendererEmployee = { + id: number; + name: string; + website: string; + status: string; + progress: number; + rating: number; + verified: boolean; + tags: string[]; + teamMembers: { name: string; role: string }[]; +}; + +export const cellRendererData: CellRendererEmployee[] = [ + { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] }, + { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] }, + { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] }, + { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] }, + { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] }, + { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] }, + { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] }, + { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] }, + { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] }, + { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] }, + { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 180, type: "string" }, + { accessor: "teamMembers", label: "Team", width: 280, type: "string" }, + { accessor: "website", label: "Website", width: 180, type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, + { accessor: "progress", label: "Progress", width: 150, type: "number" }, + { accessor: "rating", label: "Rating", width: 150, type: "number" }, + { accessor: "verified", label: "Verified", width: 100, type: "boolean" }, + { accessor: "tags", label: "Tags", width: 250, type: "string" }, +]; + +export const cellRendererConfig = { + headers: cellRendererHeaders, + rows: cellRendererData, + tableProps: { + selectableCells: true, + customTheme: { rowHeight: 48 }, + }, +} as const; diff --git a/packages/examples/angular/src/demos/charts/charts-demo.component.ts b/packages/examples/angular/src/demos/charts/charts-demo.component.ts index 0768ddf43..a96e98c5d 100644 --- a/packages/examples/angular/src/demos/charts/charts-demo.component.ts +++ b/packages/examples/angular/src/demos/charts/charts-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { chartsConfig } from "@simple-table/examples-shared"; +import { chartsConfig } from "./charts.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,6 +24,6 @@ export class ChartsDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = chartsConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(chartsConfig.headers); readonly rows: Row[] = chartsConfig.rows; } diff --git a/packages/examples/angular/src/demos/charts/charts.demo-data.ts b/packages/examples/angular/src/demos/charts/charts.demo-data.ts new file mode 100644 index 000000000..3d35fe38a --- /dev/null +++ b/packages/examples/angular/src/demos/charts/charts.demo-data.ts @@ -0,0 +1,50 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +const generateTrendData = (baseValue: number, volatility: number, length: number = 12): number[] => { + const data: number[] = []; + let current = baseValue; + for (let i = 0; i < length; i++) { + const change = (Math.random() - 0.5) * volatility; + current = Math.max(0, current + change); + data.push(Math.round(current * 100) / 100); + } + return data; +}; + +export const chartsData = [ + { 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 }, + { id: 4, product: 'Monitor 27"', 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 }, + { id: 5, product: "Keyboard Mechanical", category: "Accessories", monthlySales: generateTrendData(200, 40, 12), dailyViews: generateTrendData(600, 120, 30), quarterlyRevenue: [18000, 22000, 25000, 28000], weeklyOrders: [32, 38, 35, 42, 45, 48, 52], rating: 4.8 }, + { id: 6, product: "Webcam HD", category: "Electronics", monthlySales: generateTrendData(120, 30, 12), dailyViews: generateTrendData(450, 90, 30), quarterlyRevenue: [15000, 17000, 16000, 19000], weeklyOrders: [18, 22, 20, 25, 28, 31, 29], rating: 4.3 }, + { id: 7, product: "Headphones Bluetooth", category: "Audio", monthlySales: generateTrendData(250, 60, 12), dailyViews: generateTrendData(900, 180, 30), quarterlyRevenue: [28000, 32000, 35000, 38000], weeklyOrders: [42, 48, 45, 52, 55, 58, 62], rating: 4.4 }, + { id: 8, product: "Phone Case", category: "Accessories", monthlySales: generateTrendData(600, 100, 12), dailyViews: generateTrendData(1500, 250, 30), quarterlyRevenue: [5000, 6000, 7000, 8500], weeklyOrders: [95, 102, 98, 108, 115, 120, 125], rating: 4.1 }, + { id: 9, product: "Smartwatch", 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 }, + { id: 10, product: "Tablet", 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 }, + { id: 11, product: "TV", 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 }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, isSortable: true, type: "number" }, + { accessor: "product", label: "Product", width: 180, isSortable: true, type: "string" }, + { accessor: "category", label: "Category", width: 120, isSortable: true, type: "string" }, + { accessor: "monthlySales", label: "Monthly Sales (12mo)", width: 150, type: "lineAreaChart", tooltip: "Sales trend over the past 12 months", align: "center" }, + { accessor: "dailyViews", label: "Daily Views (30d)", width: 150, type: "lineAreaChart", tooltip: "Daily page views for the past 30 days", align: "center" }, + { accessor: "quarterlyRevenue", label: "Quarterly Revenue", width: 140, type: "barChart", tooltip: "Revenue by quarter", align: "center" }, + { accessor: "weeklyOrders", label: "Weekly Orders", width: 130, type: "barChart", tooltip: "Orders per week over the past 7 weeks", align: "center" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number", align: "center" }, +]; + +export const chartsConfig = { + headers: chartsHeaders, + rows: chartsData, + tableProps: { + columnReordering: true, + columnResizing: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns-demo.component.ts b/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns-demo.component.ts index 3a3995aaf..2b479eaae 100644 --- a/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns-demo.component.ts +++ b/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { collapsibleColumnsConfig } from "@simple-table/examples-shared"; +import { collapsibleColumnsConfig } from "./collapsible-columns.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -26,5 +26,5 @@ export class CollapsibleColumnsDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = collapsibleColumnsConfig.rows; - readonly headers: AngularHeaderObject[] = collapsibleColumnsConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(collapsibleColumnsConfig.headers); } diff --git a/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns.demo-data.ts b/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns.demo-data.ts new file mode 100644 index 000000000..bfd2a2e6f --- /dev/null +++ b/packages/examples/angular/src/demos/collapsible-columns/collapsible-columns.demo-data.ts @@ -0,0 +1,91 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const collapsibleColumnsData = [ + { 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 }, + { id: 4, name: "David Kim", region: "North America", q1Sales: 167000, q2Sales: 198000, q3Sales: 234000, q4Sales: 267000, totalSales: 866000, avgQuarterly: 216500, jan: 52000, feb: 55000, mar: 60000, apr: 63000, may: 66000, jun: 69000, jul: 75000, aug: 78000, sep: 81000, oct: 87000, nov: 89000, dec: 91000, avgMonthly: 72166, bestMonth: 91000, softwareSales: 346000, hardwareSales: 267000, servicesSales: 253000, topCategory: "Software", categoryCount: 3 }, + { id: 5, name: "Emma Wilson", region: "Australia", q1Sales: 134000, q2Sales: 167000, q3Sales: 198000, q4Sales: 234000, totalSales: 733000, avgQuarterly: 183250, jan: 42000, feb: 45000, mar: 47000, apr: 52000, may: 55000, jun: 60000, jul: 63000, aug: 66000, sep: 69000, oct: 75000, nov: 78000, dec: 81000, avgMonthly: 61083, bestMonth: 81000, softwareSales: 293000, hardwareSales: 220000, servicesSales: 220000, topCategory: "Software", categoryCount: 3 }, + { id: 6, name: "James Anderson", region: "South America", q1Sales: 156000, q2Sales: 178000, q3Sales: 201000, q4Sales: 223000, totalSales: 758000, avgQuarterly: 189500, jan: 48000, feb: 52000, mar: 56000, apr: 58000, may: 60000, jun: 60000, jul: 65000, aug: 67000, sep: 69000, oct: 72000, nov: 75000, dec: 76000, avgMonthly: 63166, bestMonth: 76000, softwareSales: 303000, hardwareSales: 227000, servicesSales: 228000, topCategory: "Software", categoryCount: 3 }, + { id: 7, name: "Lisa Chang", region: "Asia Pacific", q1Sales: 145000, q2Sales: 178000, q3Sales: 201000, q4Sales: 234000, totalSales: 758000, avgQuarterly: 189500, jan: 45000, feb: 48000, mar: 52000, apr: 58000, may: 60000, jun: 60000, jul: 65000, aug: 67000, sep: 69000, oct: 75000, nov: 78000, dec: 81000, avgMonthly: 63166, bestMonth: 81000, softwareSales: 303000, hardwareSales: 227000, servicesSales: 228000, topCategory: "Software", categoryCount: 3 }, + { id: 8, name: "Michael Brown", region: "Europe", q1Sales: 178000, q2Sales: 201000, q3Sales: 234000, q4Sales: 256000, totalSales: 869000, avgQuarterly: 217250, jan: 56000, feb: 60000, mar: 62000, apr: 65000, may: 67000, jun: 69000, jul: 75000, aug: 78000, sep: 81000, oct: 83000, nov: 86000, dec: 87000, avgMonthly: 72416, bestMonth: 87000, softwareSales: 347000, hardwareSales: 260000, servicesSales: 262000, topCategory: "Software", categoryCount: 3 }, + { id: 9, name: "Sarah Johnson", region: "North America", q1Sales: 201000, q2Sales: 234000, q3Sales: 267000, q4Sales: 289000, totalSales: 991000, avgQuarterly: 247750, jan: 63000, feb: 67000, mar: 71000, apr: 75000, may: 78000, jun: 81000, jul: 85000, aug: 89000, sep: 93000, oct: 95000, nov: 97000, dec: 97000, avgMonthly: 82583, bestMonth: 97000, softwareSales: 396000, hardwareSales: 297000, servicesSales: 298000, topCategory: "Software", categoryCount: 3 }, + { id: 10, name: "Robert Davis", region: "Australia", q1Sales: 123000, q2Sales: 145000, q3Sales: 167000, q4Sales: 189000, totalSales: 624000, avgQuarterly: 156000, jan: 38000, feb: 42000, mar: 43000, apr: 46000, may: 48000, jun: 51000, jul: 53000, aug: 56000, sep: 58000, oct: 60000, nov: 63000, dec: 66000, avgMonthly: 52000, bestMonth: 66000, softwareSales: 249000, hardwareSales: 187000, servicesSales: 188000, topCategory: "Software", categoryCount: 3 }, + { id: 11, name: "Jennifer Martinez", region: "South America", q1Sales: 134000, q2Sales: 156000, q3Sales: 178000, q4Sales: 201000, totalSales: 669000, avgQuarterly: 167250, jan: 42000, feb: 45000, mar: 47000, apr: 50000, may: 52000, jun: 54000, jul: 57000, aug: 59000, sep: 62000, oct: 65000, nov: 67000, dec: 69000, avgMonthly: 55750, bestMonth: 69000, softwareSales: 267000, hardwareSales: 201000, servicesSales: 201000, topCategory: "Software", categoryCount: 3 }, + { 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 monthCol = (accessor: string, label: string): HeaderObject => ({ + accessor, + label, + width: 100, + showWhen: "parentExpanded" as const, + isSortable: true, + align: "right", + type: "number", + cellRenderer: fmt(accessor), +}); + +export const collapsibleColumnsHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, isSortable: true }, + { accessor: "name", label: "Sales Rep", minWidth: 150, width: "1fr", isSortable: true }, + { accessor: "region", label: "Region", width: 140, isSortable: true }, + { + accessor: "quarterlySales", + label: "Quarterly Sales", + width: 500, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "totalSales", label: "Total Sales", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("totalSales") }, + { accessor: "q1Sales", label: "Q1", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q1Sales") }, + { accessor: "q2Sales", label: "Q2", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q2Sales") }, + { accessor: "q3Sales", label: "Q3", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q3Sales") }, + { accessor: "q4Sales", label: "Q4", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q4Sales") }, + ], + }, + { + accessor: "monthlyPerformance", + label: "Monthly Performance", + width: 800, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "avgMonthly", label: "Avg Monthly", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("avgMonthly") }, + { accessor: "bestMonth", label: "Best Month", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("bestMonth") }, + 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"), + monthCol("oct", "Oct"), monthCol("nov", "Nov"), monthCol("dec", "Dec"), + ], + }, + { + accessor: "productCategories", + label: "Product Categories", + width: 450, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "topCategory", label: "Top Category", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, type: "string" }, + { accessor: "softwareSales", label: "Software", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("softwareSales") }, + { accessor: "hardwareSales", label: "Hardware", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("hardwareSales") }, + { accessor: "servicesSales", label: "Services", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("servicesSales") }, + ], + }, +]; + +export const collapsibleColumnsConfig = { + headers: collapsibleColumnsHeaders, + rows: collapsibleColumnsData, + tableProps: { + columnResizing: true, + editColumns: true, + selectableCells: true, + columnReordering: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/column-alignment/column-alignment-demo.component.ts b/packages/examples/angular/src/demos/column-alignment/column-alignment-demo.component.ts index 14d8f5d6d..5138df423 100644 --- a/packages/examples/angular/src/demos/column-alignment/column-alignment-demo.component.ts +++ b/packages/examples/angular/src/demos/column-alignment/column-alignment-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnAlignmentConfig } from "@simple-table/examples-shared"; +import { columnAlignmentConfig } from "./column-alignment.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -22,5 +22,5 @@ export class ColumnAlignmentDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnAlignmentConfig.rows; - readonly headers: AngularHeaderObject[] = columnAlignmentConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnAlignmentConfig.headers); } diff --git a/packages/examples/angular/src/demos/column-alignment/column-alignment.demo-data.ts b/packages/examples/angular/src/demos/column-alignment/column-alignment.demo-data.ts new file mode 100644 index 000000000..1ddc05ce6 --- /dev/null +++ b/packages/examples/angular/src/demos/column-alignment/column-alignment.demo-data.ts @@ -0,0 +1,31 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const columnAlignmentHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "rating", label: "Rating", width: 120, align: "right", type: "number" }, + { accessor: "status", label: "Status", width: 120, align: "left", type: "string" }, +]; + +export const columnAlignmentData = [ + { 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" }, + { id: 4, name: "Leandro Nakamura", score: 81, rating: 4.2, status: "Injured" }, + { id: 5, name: "Nadia Petrov", score: 87, rating: 4.4, status: "Active" }, + { id: 6, name: "Taj Chen", score: 92, rating: 4.7, status: "Active" }, + { id: 7, name: "Mira Thompson", score: 90, rating: 4.5, status: "Active" }, + { id: 8, name: "Juno Garcia", score: 84, rating: 4.3, status: "Inactive" }, + { id: 9, name: "Caspian Williams", score: 95, rating: 4.9, status: "Active" }, + { id: 10, name: "Vera Martinez", score: 88, rating: 4.4, status: "Active" }, + { id: 11, name: "Zion Hassan", score: 93, rating: 4.8, status: "Active" }, + { id: 12, name: "Kira Kumar", score: 91, rating: 4.6, status: "Resting" }, +]; + +export const columnAlignmentConfig = { + headers: columnAlignmentHeaders, + rows: columnAlignmentData, +} as const; diff --git a/packages/examples/angular/src/demos/column-editing/column-editing-demo.component.ts b/packages/examples/angular/src/demos/column-editing/column-editing-demo.component.ts index c6688bae8..6672c20a5 100644 --- a/packages/examples/angular/src/demos/column-editing/column-editing-demo.component.ts +++ b/packages/examples/angular/src/demos/column-editing/column-editing-demo.component.ts @@ -1,8 +1,8 @@ import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, defaultHeadersFromCore } from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnEditingData, columnEditingHeaders } from "@simple-table/examples-shared"; +import { columnEditingData, columnEditingHeaders } from "./column-editing.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -43,7 +43,7 @@ export class ColumnEditingDemoComponent { lastAdded = ""; get headers(): AngularHeaderObject[] { - return [...columnEditingHeaders, ...this.additionalColumns]; + return [...defaultHeadersFromCore(columnEditingHeaders), ...this.additionalColumns]; } addColumn() { diff --git a/packages/examples/angular/src/demos/column-editing/column-editing.demo-data.ts b/packages/examples/angular/src/demos/column-editing/column-editing.demo-data.ts new file mode 100644 index 000000000..11a5c7368 --- /dev/null +++ b/packages/examples/angular/src/demos/column-editing/column-editing.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const columnEditingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, type: "string" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, +]; + +export const columnEditingData = [ + { 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" }, + { id: 4, name: "Luna Martinez", age: 23, role: "Junior Developer", department: "Engineering", email: "luna.martinez@company.com" }, + { id: 5, name: "Tyler Anderson", age: 31, role: "DevOps Engineer", department: "Operations", email: "tyler.anderson@company.com" }, + { id: 6, name: "Zara Kim", age: 28, role: "Product Designer", department: "Design", email: "zara.kim@company.com" }, + { id: 7, name: "Kai Thompson", age: 26, role: "Full Stack Developer", department: "Engineering", email: "kai.thompson@company.com" }, + { id: 8, name: "Ava Singh", age: 33, role: "Product Manager", department: "Product", email: "ava.singh@company.com" }, + { id: 9, name: "Jordan Walsh", age: 25, role: "Marketing Specialist", department: "Growth", email: "jordan.walsh@company.com" }, + { id: 10, name: "Phoenix Lee", age: 30, role: "Backend Developer", department: "Engineering", email: "phoenix.lee@company.com" }, + { id: 11, name: "River Jackson", age: 24, role: "Growth Designer", department: "Design", email: "river.jackson@company.com" }, + { id: 12, name: "Atlas Morgan", age: 32, role: "Tech Lead", department: "Engineering", email: "atlas.morgan@company.com" }, +]; + +export const columnEditingConfig = { + headers: columnEditingHeaders, + rows: columnEditingData, + tableProps: { enableHeaderEditing: true, selectableColumns: true }, +} as const; diff --git a/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer-demo.component.ts b/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer-demo.component.ts index 0f1c4fd86..543d2f07e 100644 --- a/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer-demo.component.ts +++ b/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer-demo.component.ts @@ -1,12 +1,12 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, ColumnEditorConfig, Row, Theme } from "@simple-table/angular"; import { columnEditorCustomRendererConfig, COLUMN_EDITOR_TEXT, COLUMN_EDITOR_SEARCH_PLACEHOLDER, buildVanillaColumnEditorRowRenderer, -} from "@simple-table/examples-shared"; +} from "./column-editor-custom-renderer.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -29,7 +29,7 @@ export class ColumnEditorCustomRendererDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnEditorCustomRendererConfig.rows; - readonly headers: AngularHeaderObject[] = columnEditorCustomRendererConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnEditorCustomRendererConfig.headers); readonly editorConfig: ColumnEditorConfig = { text: COLUMN_EDITOR_TEXT, searchEnabled: true, diff --git a/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts b/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts new file mode 100644 index 000000000..ff266e543 --- /dev/null +++ b/packages/examples/angular/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts @@ -0,0 +1,83 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row, ColumnEditorRowRendererProps } from "@simple-table/angular"; + + +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 columnEditorCustomRendererHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 170, type: "string", isSortable: true }, + { accessor: "email", label: "Email", width: 200, type: "string" }, + { accessor: "role", label: "Role", width: 130, type: "string", isSortable: true }, + { + accessor: "salary", + label: "Salary", + width: 130, + type: "number", + isSortable: true, + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, + { accessor: "department", label: "Department", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const columnEditorCustomRendererConfig = { + headers: columnEditorCustomRendererHeaders, + rows: columnEditorCustomRendererData, + tableProps: { + editColumns: true, + }, +} as const; + +export const COLUMN_EDITOR_TEXT = "Manage Columns"; +export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…"; + +export function buildVanillaColumnEditorRowRenderer(props: ColumnEditorRowRendererProps): HTMLElement { + const row = document.createElement("div"); + Object.assign(row.style, { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "6px 8px", + borderRadius: "6px", + background: "#f8fafc", + marginBottom: "4px", + }); + + if (props.components.checkbox) { + const span = document.createElement("span"); + if (typeof props.components.checkbox === "string") { + span.innerHTML = props.components.checkbox; + } else { + span.appendChild(props.components.checkbox as Node); + } + row.appendChild(span); + } + + const label = document.createElement("span"); + Object.assign(label.style, { flex: "1", fontSize: "13px", fontWeight: "500" }); + label.textContent = props.header.label; + row.appendChild(label); + + if (props.components.dragIcon) { + const span = document.createElement("span"); + Object.assign(span.style, { cursor: "grab", opacity: "0.5" }); + if (typeof props.components.dragIcon === "string") { + span.innerHTML = props.components.dragIcon; + } else { + span.appendChild(props.components.dragIcon as Node); + } + row.appendChild(span); + } + + return row; +} diff --git a/packages/examples/angular/src/demos/column-filtering/column-filtering-demo.component.ts b/packages/examples/angular/src/demos/column-filtering/column-filtering-demo.component.ts index f4889b3ac..ea8079f6c 100644 --- a/packages/examples/angular/src/demos/column-filtering/column-filtering-demo.component.ts +++ b/packages/examples/angular/src/demos/column-filtering/column-filtering-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnFilteringConfig } from "@simple-table/examples-shared"; +import { columnFilteringConfig } from "./column-filtering.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -22,5 +22,5 @@ export class ColumnFilteringDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnFilteringConfig.rows; - readonly headers: AngularHeaderObject[] = columnFilteringConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnFilteringConfig.headers); } diff --git a/packages/examples/angular/src/demos/column-filtering/column-filtering.demo-data.ts b/packages/examples/angular/src/demos/column-filtering/column-filtering.demo-data.ts new file mode 100644 index 000000000..66471aaf4 --- /dev/null +++ b/packages/examples/angular/src/demos/column-filtering/column-filtering.demo-data.ts @@ -0,0 +1,222 @@ +// Self-contained demo table setup for this example. +import type { Row } from "@simple-table/angular"; +import type { HeaderObject } from "@simple-table/angular"; + + +export const COLUMN_FILTERING_DATA: Row[] = [ + { + id: 1, + name: "Bianca Rossi", + department: "Editorial", + role: "Senior Editor", + salary: 82000, + startDate: "2020-02-14", + isActive: true, + }, + { + id: 2, + name: "Axel Chen", + department: "Production", + role: "Art Director", + salary: 75000, + startDate: "2021-06-18", + isActive: true, + }, + { + id: 3, + name: "Emilia Nakamura", + department: "Editorial", + role: "Managing Editor", + salary: 95000, + startDate: "2019-04-22", + isActive: true, + }, + { + id: 4, + name: "Luca Martinez", + department: "Marketing", + role: "Content Strategist", + salary: 68000, + startDate: "2022-01-12", + isActive: false, + }, + { + id: 5, + name: "Delia Kumar", + department: "Production", + role: "Layout Designer", + salary: 72000, + startDate: "2020-09-07", + isActive: true, + }, + { + id: 6, + name: "Cian O'Sullivan", + department: "Sales", + role: "Sales Representative", + salary: 65000, + startDate: "2021-11-03", + isActive: true, + }, + { + id: 7, + name: "Amara Okafor", + department: "Human Resources", + role: "HR Manager", + salary: 78000, + startDate: "2019-08-15", + isActive: true, + }, + { + id: 8, + name: "Rowan Thompson", + department: "Production", + role: "Cover Designer", + salary: 69000, + startDate: "2021-12-20", + isActive: false, + }, + { + id: 9, + name: "Celeste Petrov", + department: "Marketing", + role: "PR Specialist", + salary: 63000, + startDate: "2022-03-08", + isActive: true, + }, + { + id: 10, + name: "Quinn Hassan", + department: "Sales", + role: "Sales Manager", + salary: 89000, + startDate: "2020-05-11", + isActive: true, + }, + { + id: 11, + name: "Isla Williams", + department: "Editorial", + role: "Copy Editor", + salary: 58000, + startDate: "2021-10-25", + isActive: true, + }, + { + id: 12, + name: "Dax Silva", + department: "Finance", + role: "Financial Analyst", + salary: 64000, + startDate: "2022-07-14", + isActive: false, + }, + { + id: 13, + name: "Maya Patel", + department: "IT Support", + role: "Systems Administrator", + salary: 71000, + startDate: "2021-03-15", + isActive: true, + }, + { + id: 14, + name: "Jordan Lee", + department: "Quality Assurance", + role: "QA Engineer", + salary: 67000, + startDate: "2020-11-08", + isActive: true, + }, +]; + + +export const DEPARTMENT_OPTIONS = [ + { label: "Editorial", value: "Editorial" }, + { label: "Production", value: "Production" }, + { label: "Marketing", value: "Marketing" }, + { label: "Sales", value: "Sales" }, + { label: "Operations", value: "Operations" }, + { label: "Human Resources", value: "Human Resources" }, + { label: "Finance", value: "Finance" }, + { label: "Legal", value: "Legal" }, + { label: "IT Support", value: "IT Support" }, + { label: "Customer Service", value: "Customer Service" }, + { label: "Research & Development", value: "Research & Development" }, + { label: "Quality Assurance", value: "Quality Assurance" }, +]; + +export const columnFilteringHeaders: HeaderObject[] = [ + { + accessor: "id", + label: "ID", + width: 80, + type: "number", + isSortable: true, + filterable: true, + }, + { + accessor: "name", + label: "Employee Name", + width: "1fr", + minWidth: 150, + type: "string", + isSortable: true, + filterable: true, + }, + { + accessor: "department", + label: "Department", + width: "1fr", + minWidth: 120, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: DEPARTMENT_OPTIONS, + }, + { + accessor: "role", + label: "Role", + width: 140, + type: "string", + isSortable: true, + filterable: true, + }, + { + accessor: "salary", + label: "Salary", + width: 120, + align: "right", + type: "number", + isSortable: true, + filterable: true, + cellRenderer: ({ row }) => { + const salary = row.salary as number; + return `$${salary.toLocaleString()}`; + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 130, + type: "date", + isSortable: true, + filterable: true, + }, + { + accessor: "isActive", + label: "Active", + width: 100, + align: "center", + type: "boolean", + isSortable: true, + filterable: true, + }, +]; + +export const columnFilteringConfig = { + headers: columnFilteringHeaders, + rows: COLUMN_FILTERING_DATA, +} as const; diff --git a/packages/examples/angular/src/demos/column-pinning/column-pinning-demo.component.ts b/packages/examples/angular/src/demos/column-pinning/column-pinning-demo.component.ts index d016f3c8e..10f0f0966 100644 --- a/packages/examples/angular/src/demos/column-pinning/column-pinning-demo.component.ts +++ b/packages/examples/angular/src/demos/column-pinning/column-pinning-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnPinningConfig } from "@simple-table/examples-shared"; +import { columnPinningConfig } from "./column-pinning.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class ColumnPinningDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnPinningConfig.rows; - readonly headers: AngularHeaderObject[] = columnPinningConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnPinningConfig.headers); readonly columnResizing = columnPinningConfig.tableProps.columnResizing; } diff --git a/packages/examples/angular/src/demos/column-pinning/column-pinning.demo-data.ts b/packages/examples/angular/src/demos/column-pinning/column-pinning.demo-data.ts new file mode 100644 index 000000000..9eafd5a1f --- /dev/null +++ b/packages/examples/angular/src/demos/column-pinning/column-pinning.demo-data.ts @@ -0,0 +1,37 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const columnPinningHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, + { accessor: "location", label: "Location", width: 150, type: "string" }, + { accessor: "joinDate", label: "Join Date", width: 120, type: "date" }, + { accessor: "salary", label: "Salary", width: 120, align: "right", type: "number" }, + { accessor: "manager", label: "Manager", width: 180, type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, + { accessor: "projects", label: "Projects", width: 100, align: "right", pinned: "right", type: "number" }, +]; + +export const columnPinningData = [ + { 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 }, + { id: 4, name: "Luna Martinez", email: "luna.m@pixelstudio.game", role: "UI/UX Designer", department: "User Experience", location: "Barcelona", joinDate: "2021-08-23", salary: 89000, manager: "River Chen", status: "Active", projects: 3 }, + { id: 5, name: "Atlas Williams", email: "atlas.w@pixelstudio.game", role: "Audio Engineer", department: "Sound Design", location: "Nashville", joinDate: "2020-01-17", salary: 92000, manager: "Echo Davis", status: "Active", projects: 5 }, + { id: 6, name: "Sage Kumar", email: "sage.k@pixelstudio.game", role: "QA Lead", department: "Quality Assurance", location: "Bangalore", joinDate: "2019-09-30", salary: 78000, manager: "Orion Lee", status: "Active", projects: 8 }, + { id: 7, name: "River Petrov", email: "river.p@pixelstudio.game", role: "Producer", department: "Production", location: "London", joinDate: "2018-12-05", salary: 125000, manager: "Nova Singh", status: "Active", projects: 4 }, + { id: 8, name: "Nova Hassan", email: "nova.h@pixelstudio.game", role: "Community Manager", department: "Marketing", location: "Los Angeles", joinDate: "2022-04-14", salary: 75000, manager: "Zara Nakamura", status: "Active", projects: 2 }, + { id: 9, name: "Echo Fernandez", email: "echo.f@pixelstudio.game", role: "Narrative Designer", department: "Storytelling", location: "Prague", joinDate: "2021-02-28", salary: 87000, manager: "Atlas Williams", status: "Active", projects: 3 }, + { id: 10, name: "Orion Silva", email: "orion.s@pixelstudio.game", role: "Backend Developer", department: "Engineering", location: "São Paulo", joinDate: "2020-07-11", salary: 101000, manager: "Kai Thompson", status: "Active", projects: 5 }, + { id: 11, name: "Aria Kim", email: "aria.k@pixelstudio.game", role: "Character Artist", department: "Art & Animation", location: "Seoul", joinDate: "2022-01-19", salary: 94000, manager: "Phoenix Rodriguez", status: "Active", projects: 2 }, + { id: 12, name: "Zenith Okafor", email: "zenith.o@pixelstudio.game", role: "Technical Director", department: "Engineering", location: "Stockholm", joinDate: "2017-10-02", salary: 165000, manager: "CEO", status: "Active", projects: 9 }, +]; + +export const columnPinningConfig = { + headers: columnPinningHeaders, + rows: columnPinningData, + tableProps: { columnResizing: true }, +} as const; diff --git a/packages/examples/angular/src/demos/column-reordering/column-reordering-demo.component.ts b/packages/examples/angular/src/demos/column-reordering/column-reordering-demo.component.ts index 3d399d975..969db81df 100644 --- a/packages/examples/angular/src/demos/column-reordering/column-reordering-demo.component.ts +++ b/packages/examples/angular/src/demos/column-reordering/column-reordering-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnReorderingConfig } from "@simple-table/examples-shared"; +import { SimpleTableComponent, defaultHeadersFromCore } from "@simple-table/angular"; +import type { AngularHeaderObject, HeaderObject, Row, Theme } from "@simple-table/angular"; +import { columnReorderingConfig } from "./column-reordering.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,9 +24,9 @@ export class ColumnReorderingDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnReorderingConfig.rows; - headers: AngularHeaderObject[] = [...columnReorderingConfig.headers]; + headers: AngularHeaderObject[] = defaultHeadersFromCore([...columnReorderingConfig.headers]); - onColumnOrderChange(newHeaders: AngularHeaderObject[]): void { - this.headers = newHeaders; + onColumnOrderChange(newHeaders: HeaderObject[]): void { + this.headers = defaultHeadersFromCore(newHeaders); } } diff --git a/packages/examples/angular/src/demos/column-reordering/column-reordering.demo-data.ts b/packages/examples/angular/src/demos/column-reordering/column-reordering.demo-data.ts new file mode 100644 index 000000000..152d15c8e --- /dev/null +++ b/packages/examples/angular/src/demos/column-reordering/column-reordering.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const columnReorderingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", minWidth: 100, width: "1fr", type: "string" }, + { accessor: "department", disableReorder: true, label: "Department", width: "1fr", type: "string" }, +]; + +export const columnReorderingData = [ + { 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" }, + { id: 4, name: "Cadet Orbit Williams", age: 26, role: "Flight Engineer", department: "Engineering" }, + { id: 5, name: "Dr. Galaxy Chen", age: 31, role: "Life Support Specialist", department: "Engineering" }, + { id: 6, name: "Lt. Meteor Lee", age: 29, role: "Navigation Officer", department: "Flight Operations" }, + { id: 7, name: "Dr. Comet Hassan", age: 33, role: "Mission Planner", department: "Planning" }, + { id: 8, name: "Major Pulsar White", age: 36, role: "Communications Director", department: "Communications" }, + { id: 9, name: "Specialist Quasar Black", age: 28, role: "Systems Analyst", department: "Technology" }, + { id: 10, name: "Engineer Supernova Blue", age: 35, role: "Propulsion Engineer", department: "Engineering" }, + { id: 11, name: "Dr. Aurora Kumar", age: 30, role: "Planetary Geologist", department: "Science" }, + { id: 12, name: "Admiral Cosmos Silver", age: 45, role: "Program Director", department: "Leadership" }, +]; + +export const columnReorderingConfig = { + headers: columnReorderingHeaders, + rows: columnReorderingData, + tableProps: { columnReordering: true }, +} as const; diff --git a/packages/examples/angular/src/demos/column-resizing/column-resizing-demo.component.ts b/packages/examples/angular/src/demos/column-resizing/column-resizing-demo.component.ts index 4cc80c795..08f1c287c 100644 --- a/packages/examples/angular/src/demos/column-resizing/column-resizing-demo.component.ts +++ b/packages/examples/angular/src/demos/column-resizing/column-resizing-demo.component.ts @@ -1,8 +1,8 @@ import { NgIf } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, defaultHeadersFromCore } from "@simple-table/angular"; import type { AngularHeaderObject, HeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "@simple-table/examples-shared"; +import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "./column-resizing.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -33,7 +33,7 @@ export class ColumnResizingDemoComponent implements OnInit { @Input() theme?: Theme; readonly rows: Row[] = columnResizingData; - headers: AngularHeaderObject[] = [...columnResizingHeaders]; + headers: AngularHeaderObject[] = defaultHeadersFromCore([...columnResizingHeaders]); saveMessage = ""; handleColumnWidthChange = (updatedHeaders: HeaderObject[]) => { @@ -41,7 +41,7 @@ export class ColumnResizingDemoComponent implements OnInit { const widthMap: Record = {}; for (const h of updatedHeaders) widthMap[h.accessor] = h.width; localStorage.setItem(COLUMN_RESIZING_STORAGE_KEY, JSON.stringify(widthMap)); - this.headers = updatedHeaders; + this.headers = defaultHeadersFromCore(updatedHeaders); this.saveMessage = "Column widths saved!"; setTimeout(() => { this.saveMessage = ""; }, 2000); } catch { @@ -55,7 +55,9 @@ export class ColumnResizingDemoComponent implements OnInit { const saved = localStorage.getItem(COLUMN_RESIZING_STORAGE_KEY); if (saved) { const widthMap = JSON.parse(saved); - this.headers = columnResizingHeaders.map((h) => ({ ...h, width: widthMap[h.accessor] ?? h.width })); + this.headers = defaultHeadersFromCore( + columnResizingHeaders.map((h) => ({ ...h, width: widthMap[h.accessor] ?? h.width })), + ); } } catch { /* ignore */ } } diff --git a/packages/examples/angular/src/demos/column-resizing/column-resizing.demo-data.ts b/packages/examples/angular/src/demos/column-resizing/column-resizing.demo-data.ts new file mode 100644 index 000000000..a1a5fa30e --- /dev/null +++ b/packages/examples/angular/src/demos/column-resizing/column-resizing.demo-data.ts @@ -0,0 +1,34 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const COLUMN_RESIZING_STORAGE_KEY = "columnResizingDemo_widths"; + +export const columnResizingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, align: "right", type: "number" }, + { accessor: "department", label: "Department", width: "1fr", minWidth: 100, type: "string" }, + { accessor: "startDate", label: "Start Date", width: 150, type: "date" }, +]; + +export const columnResizingData = [ + { id: 1, name: "Dr. Marina Silva", age: 38, role: "Marine Biologist", department: "Research", startDate: "2019-03-15" }, + { id: 2, name: "Captain Alex Torres", age: 45, role: "Research Vessel Captain", department: "Operations", startDate: "2017-08-20" }, + { id: 3, name: "Dr. Coral Chen", age: 34, role: "Oceanographer", department: "Research", startDate: "2020-01-12" }, + { id: 4, name: "Finn O'Brien", age: 27, role: "Research Assistant", department: "Research", startDate: "2022-06-08" }, + { id: 5, name: "Reef Nakamura", age: 31, role: "Dive Safety Officer", department: "Safety", startDate: "2021-02-14" }, + { id: 6, name: "Tide Rodriguez", age: 29, role: "Equipment Specialist", department: "Technical", startDate: "2021-09-03" }, + { id: 7, name: "Dr. Ocean Williams", age: 42, role: "Research Director", department: "Leadership", startDate: "2016-05-10" }, + { id: 8, name: "Wave Petrov", age: 26, role: "Data Analyst", department: "Analysis", startDate: "2022-11-22" }, + { id: 9, name: "Pearl Kim", age: 33, role: "Laboratory Manager", department: "Laboratory", startDate: "2020-07-18" }, + { id: 10, name: "Current Hassan", age: 28, role: "Field Coordinator", department: "Operations", startDate: "2021-12-05" }, + { id: 11, name: "Abyss Thompson", age: 30, role: "ROV Operator", department: "Technical", startDate: "2021-04-20" }, + { id: 12, name: "Dr. Depth Martinez", age: 39, role: "Senior Researcher", department: "Research", startDate: "2018-10-14" }, +]; + +export const columnResizingConfig = { + headers: columnResizingHeaders, + rows: columnResizingData, +} as const; diff --git a/packages/examples/angular/src/demos/column-selection/column-selection-demo.component.ts b/packages/examples/angular/src/demos/column-selection/column-selection-demo.component.ts index b2f35d2b8..3901a5d43 100644 --- a/packages/examples/angular/src/demos/column-selection/column-selection-demo.component.ts +++ b/packages/examples/angular/src/demos/column-selection/column-selection-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnSelectionConfig } from "@simple-table/examples-shared"; +import { columnSelectionConfig } from "./column-selection.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class ColumnSelectionDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnSelectionConfig.rows; - readonly headers: AngularHeaderObject[] = columnSelectionConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnSelectionConfig.headers); readonly selectableColumns = columnSelectionConfig.tableProps.selectableColumns; } diff --git a/packages/examples/angular/src/demos/column-selection/column-selection.demo-data.ts b/packages/examples/angular/src/demos/column-selection/column-selection.demo-data.ts new file mode 100644 index 000000000..0c1388925 --- /dev/null +++ b/packages/examples/angular/src/demos/column-selection/column-selection.demo-data.ts @@ -0,0 +1,33 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const columnSelectionHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { accessor: "name", label: "Name", minWidth: 120, width: "1fr", isSortable: true, type: "string" }, + { accessor: "age", label: "Age", width: 100, isSortable: true, type: "number" }, + { accessor: "role", label: "Role", width: 150, isSortable: true, type: "string" }, + { accessor: "department", label: "Department", width: 150, isSortable: true, type: "string" }, + { accessor: "email", label: "Email", width: 200, isSortable: true, type: "string" }, +]; + +export const columnSelectionData = [ + { 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" }, + { id: 4, name: "Luna Martinez", age: 23, role: "Junior Developer", department: "Engineering", email: "luna.martinez@company.com" }, + { id: 5, name: "Tyler Anderson", age: 31, role: "DevOps Engineer", department: "Operations", email: "tyler.anderson@company.com" }, + { id: 6, name: "Zara Kim", age: 28, role: "Product Designer", department: "Design", email: "zara.kim@company.com" }, + { id: 7, name: "Kai Thompson", age: 26, role: "Full Stack Developer", department: "Engineering", email: "kai.thompson@company.com" }, + { id: 8, name: "Ava Singh", age: 33, role: "Product Manager", department: "Product", email: "ava.singh@company.com" }, + { id: 9, name: "Jordan Walsh", age: 25, role: "Marketing Specialist", department: "Growth", email: "jordan.walsh@company.com" }, + { id: 10, name: "Phoenix Lee", age: 30, role: "Backend Developer", department: "Engineering", email: "phoenix.lee@company.com" }, + { id: 11, name: "River Jackson", age: 24, role: "Growth Designer", department: "Design", email: "river.jackson@company.com" }, + { id: 12, name: "Atlas Morgan", age: 32, role: "Tech Lead", department: "Engineering", email: "atlas.morgan@company.com" }, +]; + +export const columnSelectionConfig = { + headers: columnSelectionHeaders, + rows: columnSelectionData, + tableProps: { selectableColumns: true }, +} as const; diff --git a/packages/examples/angular/src/demos/column-sorting/column-sorting-demo.component.ts b/packages/examples/angular/src/demos/column-sorting/column-sorting-demo.component.ts index 62441849f..d2c0f3a24 100644 --- a/packages/examples/angular/src/demos/column-sorting/column-sorting-demo.component.ts +++ b/packages/examples/angular/src/demos/column-sorting/column-sorting-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnSortingConfig } from "@simple-table/examples-shared"; +import { columnSortingConfig } from "./column-sorting.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,7 +24,7 @@ export class ColumnSortingDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnSortingConfig.rows; - readonly headers: AngularHeaderObject[] = columnSortingConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnSortingConfig.headers); readonly initialSortColumn = columnSortingConfig.tableProps.initialSortColumn; readonly initialSortDirection = columnSortingConfig.tableProps.initialSortDirection; } diff --git a/packages/examples/angular/src/demos/column-sorting/column-sorting.demo-data.ts b/packages/examples/angular/src/demos/column-sorting/column-sorting.demo-data.ts new file mode 100644 index 000000000..c45fc040f --- /dev/null +++ b/packages/examples/angular/src/demos/column-sorting/column-sorting.demo-data.ts @@ -0,0 +1,147 @@ +// Self-contained demo table setup for this example. +import type { Row } from "@simple-table/angular"; +import type { HeaderObject } from "@simple-table/angular"; + + +export const COLUMN_SORTING_DATA: Row[] = [ + { + id: 1, + name: "Dr. Elena Vasquez", + age: 42, + role: "Computer Science Professor", + department: "Computer Science", + startDate: "2015-08-15", + }, + { + id: 2, + name: "Prof. Michael Chang", + age: 38, + role: "Mathematics Professor", + department: "Mathematics", + startDate: "2017-01-10", + }, + { + id: 3, + name: "Dr. Sarah Mitchell", + age: 45, + role: "Dean of Engineering", + department: "Administration", + startDate: "2012-09-01", + }, + { + id: 4, + name: "Alex Parker", + age: 22, + role: "Graduate Student", + department: "Computer Science", + startDate: "2023-09-01", + }, + { + id: 5, + name: "Dr. James Wilson", + age: 51, + role: "Physics Professor", + department: "Physics", + startDate: "2008-03-15", + }, + { + id: 6, + name: "Maria Santos", + age: 24, + role: "Research Assistant", + department: "Biology", + startDate: "2022-06-01", + }, + { + id: 7, + name: "Prof. David Kumar", + age: 39, + role: "Biology Professor", + department: "Biology", + startDate: "2018-02-14", + }, + { + id: 8, + name: "Rachel Green", + age: 28, + role: "Lab Coordinator", + department: "Chemistry", + startDate: "2020-11-05", + }, + { + id: 9, + name: "Dr. Lisa Chen", + age: 47, + role: "Psychology Professor", + department: "Psychology", + startDate: "2014-08-20", + }, + { + id: 10, + name: "Ben Taylor", + age: 23, + role: "Teaching Assistant", + department: "Mathematics", + startDate: "2023-01-15", + }, + { + id: 11, + name: "Dr. Anna Rodriguez", + age: 35, + role: "Chemistry Professor", + department: "Chemistry", + startDate: "2019-07-01", + }, + { + id: 12, + name: "Prof. Robert Kim", + age: 44, + role: "Department Head", + department: "Engineering", + startDate: "2011-04-12", + }, +]; + + +export const columnSortingHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { accessor: "name", label: "Name", width: 180, isSortable: true, type: "string" }, + { accessor: "age", label: "Age", width: 80, isSortable: true, type: "number" }, + { accessor: "role", label: "Role", width: 200, isSortable: true, type: "string" }, + { + accessor: "department", + label: "Department", + width: 180, + isSortable: true, + type: "string", + valueFormatter: ({ value }) => { + return (value as string).charAt(0).toUpperCase() + (value as string).slice(1); + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 140, + isSortable: true, + type: "date", + valueFormatter: ({ value }) => { + if (typeof value === "string") { + return new Date(value).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + return String(value); + }, + }, +]; + +export const columnSortingConfig = { + headers: columnSortingHeaders, + rows: COLUMN_SORTING_DATA, + tableProps: { + initialSortColumn: "age", + initialSortDirection: "desc" as const, + }, +} as const; diff --git a/packages/examples/angular/src/demos/column-visibility/column-visibility-demo.component.ts b/packages/examples/angular/src/demos/column-visibility/column-visibility-demo.component.ts index a476e86b8..b20e11813 100644 --- a/packages/examples/angular/src/demos/column-visibility/column-visibility-demo.component.ts +++ b/packages/examples/angular/src/demos/column-visibility/column-visibility-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnVisibilityConfig } from "@simple-table/examples-shared"; +import { columnVisibilityConfig } from "./column-visibility.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,6 +24,6 @@ export class ColumnVisibilityDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = columnVisibilityConfig.rows; - readonly headers: AngularHeaderObject[] = columnVisibilityConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnVisibilityConfig.headers); readonly tableProps = columnVisibilityConfig.tableProps; } diff --git a/packages/examples/angular/src/demos/column-visibility/column-visibility.demo-data.ts b/packages/examples/angular/src/demos/column-visibility/column-visibility.demo-data.ts new file mode 100644 index 000000000..465732cb3 --- /dev/null +++ b/packages/examples/angular/src/demos/column-visibility/column-visibility.demo-data.ts @@ -0,0 +1,45 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +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 columnVisibilityHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "email", label: "Email", width: 200, type: "string" }, + { accessor: "phone", label: "Phone", width: 120, type: "string", hide: true }, + { accessor: "role", label: "Role", width: 130, type: "string" }, + { accessor: "department", label: "Department", width: 140, type: "string" }, + { accessor: "location", label: "Location", width: 100, type: "string", hide: true }, + { + accessor: "startDate", + label: "Start Date", + width: 130, + type: "date", + valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const columnVisibilityConfig = { + headers: columnVisibilityHeaders, + rows: columnVisibilityData, + tableProps: { + editColumns: true, + columnEditorConfig: { + text: "Manage Columns", + searchEnabled: true, + searchPlaceholder: "Search columns…", + }, + }, +} as const; diff --git a/packages/examples/angular/src/demos/column-width/column-width-demo.component.ts b/packages/examples/angular/src/demos/column-width/column-width-demo.component.ts index 66c59b381..3e8df1921 100644 --- a/packages/examples/angular/src/demos/column-width/column-width-demo.component.ts +++ b/packages/examples/angular/src/demos/column-width/column-width-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit, OnDestroy } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { columnWidthConfig } from "@simple-table/examples-shared"; +import { columnWidthConfig } from "./column-width.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,7 +24,7 @@ export class ColumnWidthDemoComponent implements OnInit, OnDestroy { @Input() theme?: Theme; readonly rows: Row[] = columnWidthConfig.rows; - readonly headers: AngularHeaderObject[] = columnWidthConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(columnWidthConfig.headers); isMobile = false; private checkMobile = () => { this.isMobile = window.innerWidth < 768; }; diff --git a/packages/examples/angular/src/demos/column-width/column-width.demo-data.ts b/packages/examples/angular/src/demos/column-width/column-width.demo-data.ts new file mode 100644 index 000000000..70682447b --- /dev/null +++ b/packages/examples/angular/src/demos/column-width/column-width.demo-data.ts @@ -0,0 +1,34 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const columnWidthHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "age", label: "Age", width: 80, type: "number" }, + { accessor: "department", label: "Department", width: "1fr", minWidth: 100, type: "string" }, + { accessor: "salary", label: "Salary", width: 120, align: "right", type: "number" }, +]; + +export const columnWidthData = [ + { id: 1, name: "Alexandra Reeves", email: "alex.reeves@techstartup.io", age: 32, department: "AI Research", salary: "$145,000" }, + { id: 2, name: "Zephyr Kim", email: "zephyr.kim@techstartup.io", age: 28, department: "Product Design", salary: "$125,000" }, + { id: 3, name: "Phoenix Okafor", email: "phoenix.okafor@techstartup.io", age: 35, department: "Platform Engineering", salary: "$160,000" }, + { id: 4, name: "Luna Tanaka", email: "luna.tanaka@techstartup.io", age: 29, department: "DevOps & Infrastructure", salary: "$138,000" }, + { id: 5, name: "River Stone", email: "river.stone@techstartup.io", age: 31, department: "Growth Engineering", salary: "$142,000" }, + { id: 6, name: "Sage Morrison", email: "sage.morrison@techstartup.io", age: 27, department: "Customer Success", salary: "$95,000" }, + { id: 7, name: "Atlas Chen", email: "atlas.chen@techstartup.io", age: 33, department: "Security & Compliance", salary: "$155,000" }, + { id: 8, name: "Nova Patel", email: "nova.patel@techstartup.io", age: 30, department: "Data Science", salary: "$148,000" }, + { id: 9, name: "Isabella Patel", email: "isabella.patel@techstartup.io", age: 29, department: "Marketing", salary: "$130,000" }, + { id: 10, name: "Leo Patel", email: "leo.patel@techstartup.io", age: 30, department: "Sales", salary: "$125,000" }, + { id: 11, name: "Oliver Patel", email: "oliver.patel@techstartup.io", age: 31, department: "HR", salary: "$115,000" }, + { id: 12, name: "Sophia Patel", email: "sophia.patel@techstartup.io", age: 28, department: "Finance", salary: "$120,000" }, + { id: 13, name: "William Patel", email: "william.patel@techstartup.io", age: 32, department: "Marketing", salary: "$135,000" }, +]; + +export const columnWidthConfig = { + headers: columnWidthHeaders, + rows: columnWidthData, + tableProps: { columnResizing: true }, +} as const; diff --git a/packages/examples/shared/src/styles/crm-custom-theme.css b/packages/examples/angular/src/demos/crm/crm-custom-theme.css similarity index 100% rename from packages/examples/shared/src/styles/crm-custom-theme.css rename to packages/examples/angular/src/demos/crm/crm-custom-theme.css diff --git a/packages/examples/angular/src/demos/crm/crm-demo.component.ts b/packages/examples/angular/src/demos/crm/crm-demo.component.ts index 07d23fea5..bd83a51dd 100644 --- a/packages/examples/angular/src/demos/crm/crm-demo.component.ts +++ b/packages/examples/angular/src/demos/crm/crm-demo.component.ts @@ -1,6 +1,12 @@ import { Component, Input, ViewChild, OnInit } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularHeaderObject, CellChangeProps, CellRenderer, FooterRendererProps, Theme } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; +import type { + AngularHeaderObject, + CellChangeProps, + CellRenderer, + FooterRendererProps, + ValueGetterProps, +} from "@simple-table/angular"; import { crmData, CRM_THEME_COLORS_LIGHT, @@ -8,10 +14,10 @@ import { CRM_FOOTER_COLORS_LIGHT, CRM_FOOTER_COLORS_DARK, generateVisiblePages, -} from "@simple-table/examples-shared"; -import type { CRMLead } from "@simple-table/examples-shared"; +} from "./crm.demo-data"; +import type { CRMLead, CrmShellTheme } from "./crm.demo-data"; import "@simple-table/angular/styles.css"; -import "../../../../shared/src/styles/crm-custom-theme.css"; +import "./crm-custom-theme.css"; function el(tag: string, styles?: Partial, text?: string): HTMLElement { const e = document.createElement(tag); @@ -21,11 +27,22 @@ function el(tag: string, styles?: Partial, text?: string): } function createEmailEnrich(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { - const wrapper = el("span", { - cursor: "pointer", alignItems: "center", columnGap: "6px", borderRadius: "9999px", - backgroundColor: "color-mix(in oklab, oklch(62.3% .214 259.815) 10%, transparent)", - paddingInline: "8px", paddingBlock: "4px", fontSize: "12px", fontWeight: "500", color: colors.tagText, - }, "Enrich"); + const wrapper = el( + "span", + { + cursor: "pointer", + alignItems: "center", + columnGap: "6px", + borderRadius: "9999px", + backgroundColor: "color-mix(in oklab, oklch(62.3% .214 259.815) 10%, transparent)", + paddingInline: "8px", + paddingBlock: "4px", + fontSize: "12px", + fontWeight: "500", + color: colors.tagText, + }, + "Enrich", + ); let isLoading = false; let email: string | null = null; @@ -35,10 +52,15 @@ function createEmailEnrich(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { isLoading = true; wrapper.textContent = ""; const spinner = el("div", { - width: "12px", height: "12px", - border: `2px solid ${colors.buttonHoverBg}`, borderTop: `2px solid ${colors.accent}`, - borderRadius: "50%", animation: "spin 1s linear infinite", - display: "inline-block", verticalAlign: "middle", marginRight: "6px", + width: "12px", + height: "12px", + border: `2px solid ${colors.buttonHoverBg}`, + borderTop: `2px solid ${colors.accent}`, + borderRadius: "50%", + animation: "spin 1s linear infinite", + display: "inline-block", + verticalAlign: "middle", + marginRight: "6px", }); wrapper.appendChild(spinner); wrapper.appendChild(document.createTextNode("Enriching...")); @@ -62,15 +84,41 @@ function createFitButtons(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { let selected: string | null = null; const btnBase: Partial = { - flex: "1", padding: "4px 8px", fontSize: "0.75rem", fontWeight: "500", - border: "none", cursor: "pointer", display: "flex", alignItems: "center", - justifyContent: "center", transition: "background-color 0.2s", color: colors.buttonText, + flex: "1", + padding: "4px 8px", + fontSize: "0.75rem", + fontWeight: "500", + border: "none", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "background-color 0.2s", + color: colors.buttonText, }; - const buttons: Array<{ key: string; label: string; activeBg: string; normalBg: string; radius?: Partial }> = [ - { key: "fit", label: "✓", activeBg: "oklch(62.7% .194 149.214)", normalBg: "oklch(92.5% .084 155.995)", radius: { borderTopLeftRadius: "6px", borderBottomLeftRadius: "6px" } }, + const buttons: Array<{ + key: string; + label: string; + activeBg: string; + normalBg: string; + radius?: Partial; + }> = [ + { + key: "fit", + label: "✓", + activeBg: "oklch(62.7% .194 149.214)", + normalBg: "oklch(92.5% .084 155.995)", + radius: { borderTopLeftRadius: "6px", borderBottomLeftRadius: "6px" }, + }, { key: "partial", label: "?", activeBg: colors.buttonHoverBg, normalBg: colors.buttonBg }, - { key: "no", label: "X", activeBg: "oklch(64.6% .222 41.116)", normalBg: "oklch(90.1% .076 70.697)", radius: { borderTopRightRadius: "6px", borderBottomRightRadius: "6px" } }, + { + key: "no", + label: "X", + activeBg: "oklch(64.6% .222 41.116)", + normalBg: "oklch(90.1% .076 70.697)", + radius: { borderTopRightRadius: "6px", borderBottomRightRadius: "6px" }, + }, ]; const btnEls: HTMLButtonElement[] = []; @@ -82,7 +130,8 @@ function createFitButtons(colors: typeof CRM_THEME_COLORS_LIGHT): HTMLElement { btn.addEventListener("click", () => { selected = selected === b.key ? null : b.key; btnEls.forEach((be, i) => { - be.style.backgroundColor = selected === buttons[i].key ? buttons[i].activeBg : buttons[i].normalBg; + be.style.backgroundColor = + selected === buttons[i].key ? buttons[i].activeBg : buttons[i].normalBg; }); }); btnEls.push(btn); @@ -97,18 +146,37 @@ function getCRMHeaders(isDark: boolean): AngularHeaderObject[] { const contactRenderer: CellRenderer = ({ row }) => { const d = row as unknown as CRMLead; - const initials = d.name.split(" ").map((n) => n[0]).join("").toUpperCase(); + const initials = d.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase(); const wrapper = el("div", { display: "flex", alignItems: "center", gap: "12px" }); - const avatar = el("div", { - width: "40px", height: "40px", borderRadius: "50%", - background: "linear-gradient(to right, oklch(75% .183 55.934), oklch(70.4% .191 22.216))", - color: "white", display: "flex", alignItems: "center", justifyContent: "center", - fontSize: "12px", fontWeight: "600", flexShrink: "0", - }, initials); + const avatar = el( + "div", + { + width: "40px", + height: "40px", + borderRadius: "50%", + background: "linear-gradient(to right, oklch(75% .183 55.934), oklch(70.4% .191 22.216))", + color: "white", + display: "flex", + alignItems: "center", + justifyContent: "center", + fontSize: "12px", + fontWeight: "600", + flexShrink: "0", + }, + initials, + ); const info = el("div", { display: "flex", flexDirection: "column", gap: "2px" }); - const nameEl = el("span", { cursor: "pointer", fontSize: "14px", fontWeight: "600", color: colors.link }, d.name); + const nameEl = el( + "span", + { cursor: "pointer", fontSize: "14px", fontWeight: "600", color: colors.link }, + d.name, + ); const titleEl = el("div", { fontSize: "12px", color: colors.textSecondary }, d.title); const companyEl = el("div", { fontSize: "12px", color: colors.textSecondary }); const atSign = el("span", { fontSize: "12px", color: colors.textTertiary }, "@"); @@ -123,9 +191,17 @@ function getCRMHeaders(isDark: boolean): AngularHeaderObject[] { const signalRenderer: CellRenderer = ({ row }) => { const d = row as unknown as CRMLead; const wrapper = el("div"); - const line1 = el("div", { color: colors.textSecondary, marginBottom: "4px", fontSize: "0.875rem" }); + const line1 = el("div", { + color: colors.textSecondary, + marginBottom: "4px", + fontSize: "0.875rem", + }); line1.textContent = "🧠 Just engaged with a "; - const link = el("a", { color: "#0077b5", textDecoration: "underline", cursor: "pointer" }, "post"); + const link = el( + "a", + { color: "#0077b5", textDecoration: "underline", cursor: "pointer" }, + "post", + ); (link as HTMLAnchorElement).href = "#"; link.addEventListener("click", (e) => e.preventDefault()); line1.appendChild(link); @@ -149,7 +225,17 @@ function getCRMHeaders(isDark: boolean): AngularHeaderObject[] { const listRenderer: CellRenderer = ({ row }) => { const d = row as unknown as CRMLead; - const link = el("a", { cursor: "pointer", fontSize: "0.875rem", color: colors.link, textDecoration: "none", fontWeight: "600" }, d.list); + const link = el( + "a", + { + cursor: "pointer", + fontSize: "0.875rem", + color: colors.link, + textDecoration: "none", + fontWeight: "600", + }, + d.list, + ); (link as HTMLAnchorElement).href = "#"; link.addEventListener("click", (e) => e.preventDefault()); return link; @@ -158,34 +244,126 @@ function getCRMHeaders(isDark: boolean): AngularHeaderObject[] { const fitRenderer: CellRenderer = () => createFitButtons(colors); const contactNowRenderer: CellRenderer = () => { - const link = el("a", { cursor: "pointer", fontSize: "0.875rem", color: colors.link, textDecoration: "none", fontWeight: "600" }, "Contact Now"); + const link = el( + "a", + { + cursor: "pointer", + fontSize: "0.875rem", + color: colors.link, + textDecoration: "none", + fontWeight: "600", + }, + "Contact Now", + ); (link as HTMLAnchorElement).href = "#"; link.addEventListener("click", (e) => e.preventDefault()); return link; }; - return [ - { accessor: "name", label: "CONTACT", width: "2fr", minWidth: 290, isSortable: true, isEditable: true, type: "string", cellRenderer: contactRenderer }, - { accessor: "signal", label: "SIGNAL", width: "3fr", minWidth: 340, isSortable: true, isEditable: true, type: "string", cellRenderer: signalRenderer }, - { accessor: "aiScore", label: "AI SCORE", width: "1fr", minWidth: 100, isSortable: true, align: "center", type: "number", cellRenderer: aiScoreRenderer }, + return mapToAngularHeaderObjects([ { - accessor: "emailStatus", label: "EMAIL", width: "1.5fr", minWidth: 210, isSortable: true, align: "center", type: "enum", - enumOptions: [{ label: "Enrich", value: "Enrich" }, { label: "Verified", value: "Verified" }, { label: "Pending", value: "Pending" }, { label: "Bounced", value: "Bounced" }], + accessor: "name", + label: "CONTACT", + width: "2fr", + minWidth: 290, + isSortable: true, + isEditable: true, + type: "string", + cellRenderer: contactRenderer, + }, + { + accessor: "signal", + label: "SIGNAL", + width: "3fr", + minWidth: 340, + isSortable: true, + isEditable: true, + type: "string", + cellRenderer: signalRenderer, + }, + { + accessor: "aiScore", + label: "AI SCORE", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "number", + cellRenderer: aiScoreRenderer, + }, + { + accessor: "emailStatus", + label: "EMAIL", + width: "1.5fr", + minWidth: 210, + isSortable: true, + align: "center", + type: "enum", + enumOptions: [ + { label: "Enrich", value: "Enrich" }, + { label: "Verified", value: "Verified" }, + { label: "Pending", value: "Pending" }, + { label: "Bounced", value: "Bounced" }, + ], cellRenderer: emailRenderer, }, - { accessor: "timeAgo", label: "IMPORT", width: "1fr", minWidth: 100, isSortable: true, align: "center", type: "string", cellRenderer: timeAgoRenderer }, { - accessor: "list", label: "LIST", width: "1.2fr", minWidth: 160, isSortable: 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 m: Record = { "Hot Leads": 1, "Warm Leads": 2, Enterprise: 3, Leads: 4, SMB: 5, "Cold Leads": 6, Nurture: 7 }; + accessor: "timeAgo", + label: "IMPORT", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "string", + cellRenderer: timeAgoRenderer, + }, + { + accessor: "list", + label: "LIST", + width: "1.2fr", + minWidth: 160, + isSortable: 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 }: ValueGetterProps) => { + const m: Record = { + "Hot Leads": 1, + "Warm Leads": 2, + Enterprise: 3, + Leads: 4, + SMB: 5, + "Cold Leads": 6, + Nurture: 7, + }; return m[String(row.list)] || 999; }, cellRenderer: listRenderer, }, - { accessor: "_fit", label: "Fit", width: "1fr", align: "center", minWidth: 120, cellRenderer: fitRenderer }, - { accessor: "_contactNow", label: "", width: "1.2fr", minWidth: 160, cellRenderer: contactNowRenderer }, - ]; + { + accessor: "_fit", + label: "Fit", + width: "1fr", + align: "center", + minWidth: 120, + cellRenderer: fitRenderer, + }, + { + accessor: "_contactNow", + label: "", + width: "1.2fr", + minWidth: 160, + cellRenderer: contactNowRenderer, + }, + ]); } function createCRMFooter( @@ -196,8 +374,12 @@ function createCRMFooter( ): HTMLElement { const c = footerColors; const wrapper = el("div", { - display: "flex", alignItems: "center", justifyContent: "space-between", - padding: "12px 16px", borderTop: `1px solid ${c.border}`, backgroundColor: c.bg, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px 16px", + borderTop: `1px solid ${c.border}`, + backgroundColor: c.bg, }); const info = el("p", { fontSize: "14px", color: c.text, margin: "0" }); @@ -211,8 +393,13 @@ function createCRMFooter( const select = document.createElement("select"); Object.assign(select.style, { - border: `1px solid ${c.inputBorder}`, borderRadius: "6px", padding: "4px 8px", - fontSize: "14px", backgroundColor: c.inputBg, color: c.text, cursor: "pointer", + border: `1px solid ${c.inputBorder}`, + borderRadius: "6px", + padding: "4px 8px", + fontSize: "14px", + backgroundColor: c.inputBg, + color: c.text, + cursor: "pointer", }); for (const opt of [25, 50, 100, 200, 10000]) { const option = document.createElement("option"); @@ -229,16 +416,26 @@ function createCRMFooter( perPageContainer.appendChild(el("span", { fontSize: "14px", color: c.text }, "per page")); right.appendChild(perPageContainer); - const nav = el("nav", { display: "inline-flex", borderRadius: "6px", boxShadow: "0 1px 2px 0 rgba(0,0,0,0.05)" }); + const nav = el("nav", { + display: "inline-flex", + borderRadius: "6px", + boxShadow: "0 1px 2px 0 rgba(0,0,0,0.05)", + }); const makePageBtn = (label: string, onClick: () => void, disabled: boolean, active = false) => { const btn = document.createElement("button"); btn.textContent = label; Object.assign(btn.style, { - display: "inline-flex", alignItems: "center", padding: "8px", - border: `1px solid ${c.buttonBorder}`, backgroundColor: active ? c.activeBg : c.buttonBg, - fontSize: "14px", fontWeight: "500", color: active ? c.activeText : disabled ? c.buttonText : c.text, - cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? "0.5" : "1", + display: "inline-flex", + alignItems: "center", + padding: "8px", + border: `1px solid ${c.buttonBorder}`, + backgroundColor: active ? c.activeBg : c.buttonBg, + fontSize: "14px", + fontWeight: "500", + color: active ? c.activeText : disabled ? c.buttonText : c.text, + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? "0.5" : "1", }); btn.disabled = disabled; if (!disabled) btn.addEventListener("click", onClick); @@ -251,14 +448,23 @@ function createCRMFooter( const visiblePages = generateVisiblePages(props.currentPage, props.totalPages); for (const page of visiblePages) { - const btn = makePageBtn(String(page), () => props.onPageChange(page), false, page === props.currentPage); + const btn = makePageBtn( + String(page), + () => props.onPageChange(page), + false, + page === props.currentPage, + ); btn.style.padding = "8px 16px"; btn.style.marginLeft = "-1px"; nav.appendChild(btn); } const nextBtn = makePageBtn("›", () => props.onNextPage(), !props.hasNextPage); - Object.assign(nextBtn.style, { borderTopRightRadius: "6px", borderBottomRightRadius: "6px", marginLeft: "-1px" }); + Object.assign(nextBtn.style, { + borderTopRightRadius: "6px", + borderBottomRightRadius: "6px", + marginLeft: "-1px", + }); nav.appendChild(nextBtn); right.appendChild(nav); @@ -293,7 +499,7 @@ function createCRMFooter( export class CRMDemoComponent implements OnInit { @ViewChild("simpleTable") tableRef!: SimpleTableComponent; @Input() height: string | number = "400px"; - @Input() theme?: Theme; + @Input() theme?: CrmShellTheme; isDark = false; data = [...crmData]; @@ -308,7 +514,8 @@ export class CRMDemoComponent implements OnInit { }; ngOnInit(): void { - this.isDark = this.theme === "custom-dark" || this.theme === "dark" || this.theme === "modern-dark"; + this.isDark = + this.theme === "custom-dark" || this.theme === "dark" || this.theme === "modern-dark"; this.headers = getCRMHeaders(this.isDark); } diff --git a/packages/examples/angular/src/demos/crm/crm.demo-data.ts b/packages/examples/angular/src/demos/crm/crm.demo-data.ts new file mode 100644 index 000000000..8115a6ed7 --- /dev/null +++ b/packages/examples/angular/src/demos/crm/crm.demo-data.ts @@ -0,0 +1,178 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Theme } from "@simple-table/angular"; + +/** Shell/wrapper theme from the demo app (not the same as the table's `theme` input). */ +export type CrmShellTheme = Theme | "custom-light" | "custom-dark"; + +export type CRMLead = { + id: number; + name: string; + title: string; + company: string; + linkedin: boolean; + signal: string; + aiScore: number; + emailStatus: string; + timeAgo: string; + list: string; +}; + + +const FIRST_NAMES = ["Emma", "Liam", "Sophia", "Noah", "Olivia", "James", "Ava", "William", "Isabella", "Oliver", "Mia", "Benjamin", "Charlotte", "Elijah", "Amelia", "Lucas", "Harper", "Mason", "Evelyn", "Logan"]; +const LAST_NAMES = ["Chen", "Rodriguez", "Kim", "Thompson", "Martinez", "Anderson", "Taylor", "Brown", "Wilson", "Johnson", "Lee", "Garcia", "Davis", "Miller", "Moore", "Jackson", "White", "Harris", "Martin", "Clark"]; +const TITLES = ["VP of Engineering", "Head of Marketing", "CTO", "Product Manager", "Director of Sales", "CEO", "CFO", "Head of Operations", "Engineering Manager", "Growth Lead", "CMO", "Head of Product", "Director of Engineering", "VP of Sales", "Head of Design"]; +const COMPANIES = ["TechCorp", "InnovateLabs", "CloudBase", "DataFlow", "NexGen", "Quantum AI", "CyberPulse", "MetaVision", "ByteForge", "CodeStream", "PixelPerfect", "LogicGate", "CircuitMind", "NetSphere", "DigiCore"]; +const SIGNALS = ["cloud infrastructure", "enterprise SaaS", "AI/ML tools", "developer platform", "security solutions", "data analytics", "API management", "DevOps automation", "microservices", "serverless computing"]; +const LISTS = ["Hot Leads", "Warm Leads", "Cold Leads", "Enterprise", "SMB", "Leads", "Nurture"]; +const TIME_AGOS = ["2 min ago", "5 min ago", "15 min ago", "1 hour ago", "3 hours ago", "6 hours ago", "1 day ago", "2 days ago", "3 days ago", "1 week ago"]; + +export function generateCRMData(count: number = 100): CRMLead[] { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + name: `${FIRST_NAMES[i % FIRST_NAMES.length]} ${LAST_NAMES[i % LAST_NAMES.length]}`, + title: TITLES[i % TITLES.length], + company: COMPANIES[i % COMPANIES.length], + linkedin: i % 3 !== 0, + signal: SIGNALS[i % SIGNALS.length], + aiScore: Math.min(5, Math.max(1, Math.floor(Math.random() * 5) + 1)), + emailStatus: ["Enrich", "Verified", "Pending", "Bounced"][i % 4], + timeAgo: TIME_AGOS[i % TIME_AGOS.length], + list: LISTS[i % LISTS.length], + })); +} + +export const crmData = generateCRMData(100); + +export const crmHeaders: HeaderObject[] = [ + { + accessor: "name", + label: "CONTACT", + width: "2fr", + minWidth: 290, + isSortable: true, + isEditable: true, + type: "string", + }, + { + accessor: "signal", + label: "SIGNAL", + width: "3fr", + minWidth: 340, + isSortable: true, + isEditable: true, + type: "string", + }, + { + accessor: "aiScore", + label: "AI SCORE", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "number", + }, + { + accessor: "emailStatus", + label: "EMAIL", + width: "1.5fr", + minWidth: 210, + isSortable: true, + align: "center", + type: "enum", + enumOptions: [ + { label: "Enrich", value: "Enrich" }, + { label: "Verified", value: "Verified" }, + { label: "Pending", value: "Pending" }, + { label: "Bounced", value: "Bounced" }, + ], + }, + { + accessor: "timeAgo", + label: "IMPORT", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "string", + }, + { + accessor: "list", + label: "LIST", + width: "1.2fr", + minWidth: 160, + isSortable: 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 priorityMap: Record = { + "Hot Leads": 1, "Warm Leads": 2, Enterprise: 3, Leads: 4, SMB: 5, "Cold Leads": 6, Nurture: 7, + }; + return priorityMap[String(row.list)] || 999; + }, + }, + { + accessor: "_fit", + label: "Fit", + width: "1fr", + align: "center", + minWidth: 120, + }, + { + accessor: "_contactNow", + label: "", + width: "1.2fr", + minWidth: 160, + }, +]; + +export const CRM_FOOTER_COLORS_LIGHT = { + bg: "white", border: "#e5e7eb", text: "#374151", textBold: "#374151", + inputBg: "white", inputBorder: "#d1d5db", buttonBg: "white", buttonBorder: "#d1d5db", + buttonText: "#6b7280", activeBg: "#fff7ed", activeText: "#ea580c", +}; + +export const CRM_FOOTER_COLORS_DARK = { + bg: "#0f172a", border: "#334155", text: "#cbd5e1", textBold: "#e2e8f0", + inputBg: "#1e293b", inputBorder: "#475569", buttonBg: "#1e293b", buttonBorder: "#475569", + buttonText: "#cbd5e1", activeBg: "#334155", activeText: "#ea580c", +}; + +export function generateVisiblePages(currentPage: number, totalPages: number): number[] { + const maxVisible = 5; + if (totalPages <= maxVisible) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + let start = currentPage - 2; + let end = currentPage + 2; + if (start < 1) { start = 1; end = Math.min(maxVisible, totalPages); } + if (end > totalPages) { end = totalPages; start = Math.max(1, totalPages - maxVisible + 1); } + return Array.from({ length: end - start + 1 }, (_, i) => start + i); +} + +export const CRM_THEME_COLORS_LIGHT = { + text: "oklch(21% .034 264.665)", textSecondary: "oklch(44.6% .03 256.802)", + textTertiary: "oklch(55.1% .027 264.364)", link: "oklch(64.6% .222 41.116)", + accent: "#ea580c", bg: "white", tagBg: "oklch(96.7% .003 264.542)", + tagText: "oklch(21% .034 264.665)", buttonBg: "oklch(92.8% .006 264.531)", + buttonText: "oklch(70.7% .022 261.325)", buttonHoverBg: "oklch(87.2% .01 258.338)", +}; + +export const CRM_THEME_COLORS_DARK = { + text: "#cbd5e1", textSecondary: "#94a3b8", textTertiary: "#64748b", + link: "#60a5fa", accent: "#ea580c", bg: "#0f172a", tagBg: "#1e293b", + tagText: "#cbd5e1", buttonBg: "#1e293b", buttonText: "#cbd5e1", buttonHoverBg: "#334155", +}; + +export const crmConfig = { + headers: crmHeaders, + rows: crmData, +} as const; diff --git a/packages/examples/angular/src/demos/csv-export/csv-export-demo.component.ts b/packages/examples/angular/src/demos/csv-export/csv-export-demo.component.ts index 198b56514..00ed639d5 100644 --- a/packages/examples/angular/src/demos/csv-export/csv-export-demo.component.ts +++ b/packages/examples/angular/src/demos/csv-export/csv-export-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input, ViewChild } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { csvExportHeaders, csvExportData, csvExportConfig } from "@simple-table/examples-shared"; +import { csvExportHeaders, csvExportData, csvExportConfig } from "./csv-export.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -33,16 +33,18 @@ export class CsvExportDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = csvExportData; - readonly headers: AngularHeaderObject[] = csvExportHeaders.map((h) => { - if (h.accessor === "actions") { - return { - ...h, - cellRenderer: () => - ``, - }; - } - return { ...h }; - }); + readonly headers: AngularHeaderObject[] = mapToAngularHeaderObjects( + csvExportHeaders.map((h) => { + if (h.accessor === "actions") { + return { + ...h, + cellRenderer: () => + ``, + }; + } + return { ...h }; + }), + ); handleExport(): void { this.tableRef.getAPI()?.exportToCSV(); @@ -53,7 +55,7 @@ export class CsvExportDemoComponent { if (!api) return; const rows = api.getAllRows(); const hdrs = api.getHeaders(); - const totalRevenue = rows.reduce((sum, r) => sum + (Number(r.revenue) || 0), 0); + const totalRevenue = rows.reduce((sum, r) => sum + (Number((r.row as { revenue?: unknown }).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 })}`, ); diff --git a/packages/examples/angular/src/demos/csv-export/csv-export.demo-data.ts b/packages/examples/angular/src/demos/csv-export/csv-export.demo-data.ts new file mode 100644 index 000000000..c6930ace9 --- /dev/null +++ b/packages/examples/angular/src/demos/csv-export/csv-export.demo-data.ts @@ -0,0 +1,76 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +const CATEGORY_CODES: Record = { + electronics: "ELEC", + furniture: "FURN", + stationery: "STAT", + appliances: "APPL", +}; + +export const csvExportData = [ + { id: "db-1001", sku: "PRD-1001", product: "Wireless Keyboard", category: "Electronics", price: 49.99, stock: 145, sold: 234, revenue: 11697.66, actions: "" }, + { id: "db-1002", sku: "PRD-1002", product: "Ergonomic Mouse", category: "Electronics", price: 29.99, stock: 89, sold: 456, revenue: 13675.44, actions: "" }, + { id: "db-1003", sku: "PRD-1003", product: "USB-C Hub", category: "Electronics", price: 39.99, stock: 234, sold: 178, revenue: 7118.22, actions: "" }, + { id: "db-2001", sku: "PRD-2001", product: "Standing Desk", category: "Furniture", price: 399.99, stock: 23, sold: 67, revenue: 26799.33, actions: "" }, + { id: "db-2002", sku: "PRD-2002", product: "Office Chair", category: "Furniture", price: 249.99, stock: 56, sold: 123, revenue: 30748.77, actions: "" }, + { id: "db-2003", sku: "PRD-2003", product: "Monitor Stand", category: "Furniture", price: 79.99, stock: 167, sold: 89, revenue: 7119.11, actions: "" }, + { id: "db-3001", sku: "PRD-3001", product: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, sold: 678, revenue: 8807.22, actions: "" }, + { id: "db-3002", sku: "PRD-3002", product: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, sold: 534, revenue: 10674.66, actions: "" }, + { id: "db-3003", sku: "PRD-3003", product: "Desk Organizer", category: "Stationery", price: 24.99, stock: 198, sold: 289, revenue: 7222.11, actions: "" }, + { id: "db-4001", sku: "PRD-4001", product: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, sold: 156, revenue: 14038.44, actions: "" }, + { id: "db-4002", sku: "PRD-4002", product: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, sold: 267, revenue: 9342.33, actions: "" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Internal ID", width: 80, type: "string", excludeFromRender: true }, + { accessor: "sku", label: "SKU", width: 100, isSortable: true, type: "string" }, + { accessor: "product", label: "Product Name", minWidth: 120, width: "1fr", isSortable: true, type: "string" }, + { + accessor: "category", + label: "Category", + width: 130, + isSortable: true, + type: "string", + valueFormatter: ({ value }) => { + const s = String(value); + return s.charAt(0).toUpperCase() + s.slice(1); + }, + exportValueGetter: ({ value }) => { + const code = CATEGORY_CODES[String(value).toLowerCase()] ?? String(value).toUpperCase(); + return `${value} (${code})`; + }, + }, + { + accessor: "price", + label: "Price", + width: 100, + isSortable: true, + type: "number", + valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}`, + useFormattedValueForCSV: true, + useFormattedValueForClipboard: true, + }, + { accessor: "stock", label: "In Stock", width: 100, isSortable: true, type: "number" }, + { accessor: "sold", label: "Units Sold", width: 110, isSortable: true, type: "number" }, + { + accessor: "revenue", + label: "Revenue", + width: 120, + isSortable: true, + type: "number", + valueFormatter: ({ value }) => + `$${(value as number).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + useFormattedValueForCSV: true, + useFormattedValueForClipboard: true, + }, + { accessor: "actions", label: "Actions", width: 100, type: "string", excludeFromCsv: true }, +]; + +export const csvExportConfig = { + headers: csvExportHeaders, + rows: csvExportData, + tableProps: { editColumns: true, selectableCells: true, customTheme: { rowHeight: 32 } }, +} as const; diff --git a/packages/examples/angular/src/demos/custom-icons/custom-icons-demo.component.ts b/packages/examples/angular/src/demos/custom-icons/custom-icons-demo.component.ts index 8cd58fa23..ecdc3c58e 100644 --- a/packages/examples/angular/src/demos/custom-icons/custom-icons-demo.component.ts +++ b/packages/examples/angular/src/demos/custom-icons/custom-icons-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularHeaderObject, IconsConfig, Row, Theme } from "@simple-table/angular"; -import { customIconsConfig, buildVanillaCustomIcons } from "@simple-table/examples-shared"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; +import type { AngularHeaderObject, AngularIconsConfig, Row, Theme } from "@simple-table/angular"; +import { customIconsConfig, buildVanillaCustomIcons } from "./custom-icons.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class CustomIconsDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = customIconsConfig.rows; - readonly headers: AngularHeaderObject[] = customIconsConfig.headers; - readonly icons: IconsConfig = buildVanillaCustomIcons(); + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(customIconsConfig.headers); + readonly icons: AngularIconsConfig = buildVanillaCustomIcons(); } diff --git a/packages/examples/angular/src/demos/custom-icons/custom-icons.demo-data.ts b/packages/examples/angular/src/demos/custom-icons/custom-icons.demo-data.ts new file mode 100644 index 000000000..8a7955c9f --- /dev/null +++ b/packages/examples/angular/src/demos/custom-icons/custom-icons.demo-data.ts @@ -0,0 +1,78 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const customIconsData: Row[] = [ + { 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" }, + { id: 4, name: "Feature Update", version: "1.2.0", status: "released", downloads: 42100, date: "2024-06-10" }, + { id: 5, name: "Security Patch", version: "1.2.1", status: "released", downloads: 45800, date: "2024-07-18" }, + { id: 6, name: "Major Release", version: "2.0.0", status: "released", downloads: 67200, date: "2024-09-01" }, + { id: 7, name: "Minor Update", version: "2.1.0", status: "beta", downloads: 8900, date: "2024-11-12" }, + { id: 8, name: "Next Release", version: "2.2.0", status: "planned", downloads: 0, date: "2025-01-20" }, +]; + +export const customIconsHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number", isSortable: true }, + { accessor: "name", label: "Release", width: 170, type: "string", isSortable: true }, + { accessor: "version", label: "Version", width: 100, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 110, type: "string", isSortable: true }, + { + accessor: "downloads", + label: "Downloads", + width: 130, + type: "number", + isSortable: true, + valueFormatter: ({ value }) => (value as number).toLocaleString(), + }, + { + accessor: "date", + label: "Date", + width: 130, + type: "date", + isSortable: true, + valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const customIconsConfig = { + headers: customIconsHeaders, + rows: customIconsData, +} as const; + +export function createSvgIcon(pathD: string, color = "#3b82f6", size = 14): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", String(size)); + svg.setAttribute("height", String(size)); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", color); + svg.setAttribute("stroke-width", "2.5"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathD); + svg.appendChild(path); + return svg; +} + +export const ICON_PATHS = { + sortUp: "M12 19V5M5 12l7-7 7 7", + sortDown: "M12 5v14M19 12l-7 7-7-7", + filter: "M3 4h18l-7 8.5V18l-4 2V12.5L3 4z", + expand: "M9 5l7 7-7 7", + next: "M9 5l7 7-7 7", + prev: "M15 19l-7-7 7-7", +} as const; + +export function buildVanillaCustomIcons() { + return { + sortUp: createSvgIcon(ICON_PATHS.sortUp, "#6366f1"), + sortDown: createSvgIcon(ICON_PATHS.sortDown, "#6366f1"), + filter: createSvgIcon(ICON_PATHS.filter, "#8b5cf6"), + expand: createSvgIcon(ICON_PATHS.expand, "#6366f1"), + next: createSvgIcon(ICON_PATHS.next, "#2563eb"), + prev: createSvgIcon(ICON_PATHS.prev, "#2563eb"), + }; +} diff --git a/packages/examples/angular/src/demos/custom-theme/custom-theme-demo.component.ts b/packages/examples/angular/src/demos/custom-theme/custom-theme-demo.component.ts index 524c135d6..8f4d5da88 100644 --- a/packages/examples/angular/src/demos/custom-theme/custom-theme-demo.component.ts +++ b/packages/examples/angular/src/demos/custom-theme/custom-theme-demo.component.ts @@ -1,9 +1,9 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { customThemeConfig } from "@simple-table/examples-shared"; +import { customThemeConfig } from "./custom-theme.demo-data"; import "@simple-table/angular/styles.css"; -import "../../../../shared/src/styles/custom-theme.css"; +import "./custom-theme.css"; @Component({ selector: "custom-theme-demo", @@ -26,7 +26,7 @@ export class CustomThemeDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = customThemeConfig.rows; - readonly headers: AngularHeaderObject[] = customThemeConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(customThemeConfig.headers); readonly customThemeOverrides = customThemeConfig.tableProps.customTheme; get resolvedTheme(): Theme { diff --git a/packages/examples/shared/src/styles/custom-theme.css b/packages/examples/angular/src/demos/custom-theme/custom-theme.css similarity index 100% rename from packages/examples/shared/src/styles/custom-theme.css rename to packages/examples/angular/src/demos/custom-theme/custom-theme.css diff --git a/packages/examples/angular/src/demos/custom-theme/custom-theme.demo-data.ts b/packages/examples/angular/src/demos/custom-theme/custom-theme.demo-data.ts new file mode 100644 index 000000000..ff65a34df --- /dev/null +++ b/packages/examples/angular/src/demos/custom-theme/custom-theme.demo-data.ts @@ -0,0 +1,48 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const customThemeData: Row[] = [ + { 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" }, + { id: 4, name: "David Kim", phone: "3125557654", email: "david@corp.com", city: "Chicago", status: "active" }, + { id: 5, name: "Elena Rossi", phone: "6175554321", email: "elena@corp.com", city: "Boston", status: "active" }, + { id: 6, name: "Frank Müller", phone: "2065558765", email: "frank@corp.com", city: "Seattle", status: "inactive" }, + { id: 7, name: "Grace Park", phone: "5125552468", email: "grace@corp.com", city: "Austin", status: "active" }, + { id: 8, name: "Henry Patel", phone: "3035551357", email: "henry@corp.com", city: "Denver", status: "active" }, +]; + +function formatPhone(raw: string): string { + if (raw.length === 10) { + return `(${raw.slice(0, 3)}) ${raw.slice(3, 6)}-${raw.slice(6)}`; + } + return raw; +} + +export const customThemeHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 170, type: "string", isSortable: true }, + { + accessor: "phone", + label: "Phone", + width: 150, + type: "string", + valueFormatter: ({ value }) => formatPhone(value as string), + }, + { accessor: "email", label: "Email", width: 180, type: "string" }, + { accessor: "city", label: "City", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const customThemeConfig = { + headers: customThemeHeaders, + rows: customThemeData, + tableProps: { + theme: "custom" as const, + customTheme: { + rowHeight: 40, + headerHeight: 44, + }, + }, +} as const; diff --git a/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables-demo.component.ts b/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables-demo.component.ts index 76eb3514b..4ec938e53 100644 --- a/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables-demo.component.ts +++ b/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables-demo.component.ts @@ -1,12 +1,12 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, OnRowGroupExpandProps, Theme } from "@simple-table/angular"; import { dynamicNestedTablesConfig, dynamicNestedTablesData, fetchDivisionsForCompany, -} from "@simple-table/examples-shared"; -import type { DynamicCompany } from "@simple-table/examples-shared"; +} from "./dynamic-nested-tables.demo-data"; +import type { DynamicCompany } from "./dynamic-nested-tables.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -31,7 +31,7 @@ export class DynamicNestedTablesDemoComponent { @Input() height: string | number = "500px"; @Input() theme?: Theme; - headers: AngularHeaderObject[] = dynamicNestedTablesConfig.headers; + headers: AngularHeaderObject[] = defaultHeadersFromCore(dynamicNestedTablesConfig.headers); rows: DynamicCompany[] = [...dynamicNestedTablesData]; readonly tableProps = dynamicNestedTablesConfig.tableProps; diff --git a/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts b/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts new file mode 100644 index 000000000..894bec85e --- /dev/null +++ b/packages/examples/angular/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts @@ -0,0 +1,93 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export interface DynamicCompany extends Row { + id: string; + companyName: string; + industry: string; + revenue: string; + employees: number; + divisions?: DynamicDivision[]; +} + +export interface DynamicDivision extends Row { + id: string; + divisionName: string; + revenue: string; + profitMargin: string; + headcount: number; + location: string; +} + +const simulateDelay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +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"]; + + return Array.from({ length: divisionCount }, (_, i) => ({ + id: `${companyId}-div-${i}`, + divisionName: divisionNames[i % divisionNames.length], + revenue: `$${Math.floor(Math.random() * 50) + 10}M`, + profitMargin: `${Math.floor(Math.random() * 30) + 10}%`, + headcount: Math.floor(Math.random() * 400) + 50, + location: locations[i % locations.length], + })); +}; + +export const dynamicNestedTablesData: DynamicCompany[] = [ + { id: "comp-1", companyName: "TechCorp Global", industry: "Technology", revenue: "$250M", employees: 1200 }, + { id: "comp-2", companyName: "FinanceHub Inc", industry: "Financial Services", revenue: "$180M", employees: 850 }, + { id: "comp-3", companyName: "HealthTech Solutions", industry: "Healthcare", revenue: "$320M", employees: 1500 }, + { id: "comp-4", companyName: "RetailMax Corporation", industry: "Retail", revenue: "$420M", employees: 2100 }, + { id: "comp-5", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-6", companyName: "MediaVision Studios", industry: "Entertainment", revenue: "$290M", employees: 950 }, + { id: "comp-7", companyName: "AutoDrive Industries", industry: "Automotive", revenue: "$680M", employees: 3200 }, + { id: "comp-8", companyName: "CloudNet Services", industry: "Technology", revenue: "$195M", employees: 720 }, + { id: "comp-9", companyName: "HealthCare Solutions", industry: "Healthcare", revenue: "$380M", employees: 1300 }, + { id: "comp-10", companyName: "EducationTech Innovations", industry: "Education", revenue: "$240M", employees: 1050 }, + { id: "comp-11", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-12", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-13", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-14", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-15", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, +]; + +export const dynamicNestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionName", label: "Division", width: 200 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: 180 }, +]; + +export const dynamicNestedTablesCompanyHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { + defaultHeaders: dynamicNestedTablesDivisionHeaders, + expandAll: false, + autoExpandColumns: true, + }, + }, + { accessor: "industry", label: "Industry", width: 150 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const dynamicNestedTablesConfig = { + headers: dynamicNestedTablesCompanyHeaders, + rows: dynamicNestedTablesData, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + autoExpandColumns: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading-demo.component.ts b/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading-demo.component.ts index 2967a027a..f6f1e54d0 100644 --- a/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading-demo.component.ts +++ b/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading-demo.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, OnRowGroupExpandProps, Theme } from "@simple-table/angular"; import { dynamicRowLoadingConfig, generateInitialRegions, fetchStoresForRegion, fetchProductsForStore, -} from "@simple-table/examples-shared"; -import type { DynamicRegion, DynamicStore } from "@simple-table/examples-shared"; +} from "./dynamic-row-loading.demo-data"; +import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -35,7 +35,7 @@ export class DynamicRowLoadingDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - headers: AngularHeaderObject[] = dynamicRowLoadingConfig.headers; + headers: AngularHeaderObject[] = defaultHeadersFromCore(dynamicRowLoadingConfig.headers); rows: DynamicRegion[] = generateInitialRegions(); readonly grouping = ["stores", "products"]; readonly getRowId = ({ row }: { row: Record }) => row["id"] as string; diff --git a/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts b/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts new file mode 100644 index 000000000..6e1d60ec8 --- /dev/null +++ b/packages/examples/angular/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts @@ -0,0 +1,205 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export interface DynamicRegion extends Row { + id: string; + name: string; + type: "region"; + totalSales: number; + totalRevenue: number; + activeStores: number; + avgRating: string; + lastUpdate: string; + stores?: DynamicStore[]; +} + +export interface DynamicStore extends Row { + id: string; + name: string; + type: "store"; + totalSales: number; + totalRevenue: number; + activeStores?: number; + avgRating: string; + lastUpdate: string; + products?: DynamicProduct[]; +} + +export interface DynamicProduct extends Row { + id: string; + name: string; + type: "product"; + totalSales: number; + totalRevenue: number; + activeStores?: number; + avgRating: string; + lastUpdate: string; +} + +export const dynamicRowLoadingHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: 280, expandable: true, type: "string", pinned: "left" }, + { accessor: "type", label: "Type", width: 100, type: "string" }, + { + accessor: "totalSales", label: "Total Sales", width: 120, type: "number", align: "right", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(), + }, + { + accessor: "totalRevenue", label: "Revenue", width: 140, type: "number", align: "right", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + }, + { + accessor: "activeStores", label: "Stores", width: 100, type: "number", align: "right", + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(), + }, + { accessor: "avgRating", label: "Avg Rating", width: 120, type: "string", align: "center" }, + { accessor: "lastUpdate", label: "Last Updated", width: 130, type: "date" }, +]; + +const REGION_NAMES = [ + "North America - East", "North America - West", "Europe - North", "Europe - South", + "Asia Pacific - East", "Asia Pacific - Southeast", "Middle East", + "Latin America - North", "Latin America - South", "Africa - North", "Africa - South", "Oceania", +]; + +const STORE_NAMES = [ + "Manhattan Flagship", "Brooklyn Heights", "Boston Downtown", "Miami Beach", + "Los Angeles Beverly Hills", "San Francisco Union Square", "Seattle Downtown", "Portland Pearl District", + "London Oxford Street", "Stockholm Gamla Stan", "Copenhagen Strøget", "Amsterdam Central", + "Paris Champs-Élysées", "Madrid Gran Vía", "Rome Via del Corso", "Barcelona La Rambla", + "Tokyo Shibuya", "Shanghai Nanjing Road", "Hong Kong Central", "Seoul Gangnam", + "Singapore Orchard", "Bangkok Siam", "Kuala Lumpur Bukit Bintang", "Jakarta Grand Indonesia", + "Dubai Mall", "Abu Dhabi Marina", "Riyadh Kingdom Centre", + "Mexico City Reforma", "Monterrey Valle", "Guadalajara Centro", + "São Paulo Paulista", "Buenos Aires Palermo", "Santiago Providencia", + "Cairo City Stars", "Casablanca Morocco Mall", "Tunis Centre Urbain", + "Johannesburg Sandton", "Cape Town V&A Waterfront", + "Sydney Pitt Street", "Melbourne Bourke Street", "Auckland Queen Street", +]; + +const PRODUCT_NAMES = [ + "Wireless Headphones Pro", "Smart Watch Elite", "USB-C Hub Deluxe", "Mechanical Keyboard RGB", + "Ergonomic Mouse", "Webcam 4K", "Portable SSD 2TB", "Wireless Charger Pad", + "Phone Stand Aluminum", "Bluetooth Speaker Mini", "Laptop Stand Pro", "Cable Organizer Set", + "Gaming Mouse Elite", "Noise Cancelling Headset", "RGB Desk Mat XL", "Wireless Presenter", + "Document Camera", "Smart Pen Digital", "Monitor Arm Dual", "Docking Station Pro", + "Microphone USB Studio", "Tablet Stand Adjustable", "HDMI Switch 4K", "Laptop Cooling Pad", + "Blue Light Blocking Glasses", "Anti-Glare Screen Protector", "Laptop Privacy Filter", + "Wireless Charging Pad Trio", "MagSafe Car Mount", "Charging Cable Braided 10ft", + "Ergonomic Vertical Mouse", "Trackball Mouse Wireless", "Gaming Mouse Pad XXL", + "Keyboard Wrist Rest", "Monitor Privacy Filter", "Laptop Sleeve Premium", + "Desktop Mic Arm", "Cable Management Box", "USB Hub 7-Port", "Ergonomic Chair Cushion", + "Footrest Adjustable", "Desk Lamp LED Smart", "Portable Monitor 15.6", "Screen Cleaning Kit", + "Desk Organizer Bamboo", "Wireless Trackpad", "Numeric Keypad Wireless", + "Presentation Clicker", "Gaming Controller Pro", "Racing Wheel Set", +]; + +const seededRandom = (seed: string) => { + let hash = 0; + for (let i = 0; i < seed.length; i++) { + hash = (hash << 5) - hash + seed.charCodeAt(i); + hash = hash & hash; + } + const x = Math.sin(hash) * 10000; + return x - Math.floor(x); +}; + +const getRandomInt = (seed: string, min: number, max: number) => + Math.floor(seededRandom(seed) * (max - min + 1)) + min; + +const getRandomRating = (seed: string) => (4.0 + seededRandom(seed + "rating") * 1.0).toFixed(1); + +const getRandomDate = (seed: string) => { + const daysAgo = getRandomInt(seed + "date", 0, 5); + const date = new Date(); + date.setDate(date.getDate() - daysAgo); + return date.toISOString().split("T")[0]; +}; + +const generateStoresForRegion = (regionId: string): DynamicStore[] => { + const regionIndex = parseInt(regionId.split("-")[1]); + const numStores = getRandomInt(regionId, 3, 4); + const startIndex = (regionIndex - 1) * 3; + return Array.from({ length: numStores }, (_, i) => { + const storeId = `STORE-${regionIndex}${String(i + 1).padStart(2, "0")}`; + const storeIndex = startIndex + i; + const totalSales = getRandomInt(storeId, 10000, 25000); + const avgPrice = getRandomInt(storeId + "price", 25, 35); + return { + id: storeId, + name: STORE_NAMES[storeIndex % STORE_NAMES.length], + type: "store" as const, + totalSales, + totalRevenue: totalSales * avgPrice, + avgRating: getRandomRating(storeId), + lastUpdate: getRandomDate(storeId), + }; + }); +}; + +const generateProductsForStore = (storeId: string): DynamicProduct[] => { + const numProducts = getRandomInt(storeId, 3, 5); + const storeNumber = parseInt(storeId.split("-")[1]); + const startIndex = storeNumber * 3; + return Array.from({ length: numProducts }, (_, i) => { + const productId = `PROD-${storeId.split("-")[1]}-${i + 1}`; + const totalSales = getRandomInt(productId, 2000, 8000); + const avgPrice = getRandomInt(productId + "price", 20, 40); + return { + id: productId, + name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length], + type: "product" as const, + totalSales, + totalRevenue: totalSales * avgPrice, + avgRating: getRandomRating(productId), + lastUpdate: getRandomDate(productId), + }; + }); +}; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const fetchStoresForRegion = async (regionId: string): Promise => { + await delay(1500); + return generateStoresForRegion(regionId); +}; + +export const fetchProductsForStore = async (storeId: string): Promise => { + await delay(1000); + return generateProductsForStore(storeId); +}; + +export const generateInitialRegions = (): DynamicRegion[] => { + return REGION_NAMES.map((name, index) => { + const regionId = `REG-${index + 1}`; + const stores = generateStoresForRegion(regionId); + const totalSales = stores.reduce((sum, s) => sum + s.totalSales, 0); + const totalRevenue = stores.reduce((sum, s) => sum + s.totalRevenue, 0); + const avgRating = (stores.reduce((sum, s) => sum + parseFloat(s.avgRating), 0) / stores.length).toFixed(1); + return { + id: regionId, + name, + type: "region" as const, + totalSales, + totalRevenue, + activeStores: getRandomInt(regionId, 3, 4), + avgRating, + lastUpdate: getRandomDate(regionId), + }; + }); +}; + +export const dynamicRowLoadingConfig = { + headers: dynamicRowLoadingHeaders, + tableProps: { + rowGrouping: ["stores", "products"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + columnResizing: true, + selectableCells: true, + useOddEvenRowBackground: true, + editColumns: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/empty-state/empty-state-demo.component.ts b/packages/examples/angular/src/demos/empty-state/empty-state-demo.component.ts index 2f6a86aef..3ca830cb7 100644 --- a/packages/examples/angular/src/demos/empty-state/empty-state-demo.component.ts +++ b/packages/examples/angular/src/demos/empty-state/empty-state-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { emptyStateConfig, buildEmptyStateElement } from "@simple-table/examples-shared"; +import { emptyStateConfig, buildEmptyStateElement } from "./empty-state.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class EmptyStateDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = emptyStateConfig.rows; - readonly headers: AngularHeaderObject[] = emptyStateConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(emptyStateConfig.headers); readonly emptyStateEl = buildEmptyStateElement(); } diff --git a/packages/examples/angular/src/demos/empty-state/empty-state.demo-data.ts b/packages/examples/angular/src/demos/empty-state/empty-state.demo-data.ts new file mode 100644 index 000000000..6e24b098d --- /dev/null +++ b/packages/examples/angular/src/demos/empty-state/empty-state.demo-data.ts @@ -0,0 +1,68 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const emptyStateData: Row[] = []; + +export const emptyStateHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 180, type: "string" }, + { accessor: "email", label: "Email", width: 220, type: "string" }, + { accessor: "role", label: "Role", width: 140, type: "string" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, +]; + +export const emptyStateConfig = { + headers: emptyStateHeaders, + rows: emptyStateData, +} as const; + +export function buildEmptyStateElement(): HTMLElement { + const wrapper = document.createElement("div"); + Object.assign(wrapper.style, { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + padding: "48px 24px", + color: "#64748b", + gap: "12px", + }); + + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", "48"); + svg.setAttribute("height", "48"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "#94a3b8"); + svg.setAttribute("stroke-width", "1.5"); + + const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path1.setAttribute("d", "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7"); + svg.appendChild(path1); + + const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path2.setAttribute("d", "M16 3H8L3 7h18l-5-4z"); + svg.appendChild(path2); + + const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); + line.setAttribute("x1", "10"); + line.setAttribute("y1", "12"); + line.setAttribute("x2", "14"); + line.setAttribute("y2", "12"); + svg.appendChild(line); + + wrapper.appendChild(svg); + + const title = document.createElement("div"); + Object.assign(title.style, { fontSize: "16px", fontWeight: "600" }); + title.textContent = "No data available"; + wrapper.appendChild(title); + + const sub = document.createElement("div"); + Object.assign(sub.style, { fontSize: "13px" }); + sub.textContent = "Try adjusting your filters or adding new records."; + wrapper.appendChild(sub); + + return wrapper; +} diff --git a/packages/examples/angular/src/demos/external-filter/external-filter-demo.component.ts b/packages/examples/angular/src/demos/external-filter/external-filter-demo.component.ts index d5ce67222..36c012127 100644 --- a/packages/examples/angular/src/demos/external-filter/external-filter-demo.component.ts +++ b/packages/examples/angular/src/demos/external-filter/external-filter-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, TableFilterState, Theme } from "@simple-table/angular"; -import { externalFilterConfig, matchesFilter } from "@simple-table/examples-shared"; +import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,7 +24,7 @@ export class ExternalFilterDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = externalFilterConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(externalFilterConfig.headers); private filters: TableFilterState = {}; handleFilterChange = (newFilters: TableFilterState) => { diff --git a/packages/examples/angular/src/demos/external-filter/external-filter.demo-data.ts b/packages/examples/angular/src/demos/external-filter/external-filter.demo-data.ts new file mode 100644 index 000000000..a4849da26 --- /dev/null +++ b/packages/examples/angular/src/demos/external-filter/external-filter.demo-data.ts @@ -0,0 +1,128 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, TableFilterState } from "@simple-table/angular"; + + +type CellValue = string | number | boolean | null | undefined; + +export function matchesFilter( + value: CellValue, + filter: TableFilterState[string] +): boolean { + const { operator } = filter; + + switch (operator) { + case "equals": + return value === filter.value; + case "notEquals": + return value !== filter.value; + case "contains": + return String(value).toLowerCase().includes(String(filter.value).toLowerCase()); + case "notContains": + return !String(value).toLowerCase().includes(String(filter.value).toLowerCase()); + case "startsWith": + return String(value).toLowerCase().startsWith(String(filter.value).toLowerCase()); + case "endsWith": + return String(value).toLowerCase().endsWith(String(filter.value).toLowerCase()); + case "greaterThan": + return Number(value) > Number(filter.value); + case "lessThan": + return Number(value) < Number(filter.value); + case "greaterThanOrEqual": + return Number(value) >= Number(filter.value); + case "lessThanOrEqual": + return Number(value) <= Number(filter.value); + case "between": + return ( + filter.values != null && + Number(value) >= Number(filter.values[0]) && + Number(value) <= Number(filter.values[1]) + ); + case "in": + return filter.values != null && filter.values.includes(value); + case "notIn": + return filter.values != null && !filter.values.includes(value); + case "isEmpty": + return value == null || value === ""; + case "isNotEmpty": + return value != null && value !== ""; + default: + return true; + } +} + +const DEPARTMENT_OPTIONS = [ + { label: "AI Research", value: "AI Research" }, + { label: "UX Design", value: "UX Design" }, + { label: "DevOps", value: "DevOps" }, + { label: "Marketing", value: "Marketing" }, + { label: "Engineering", value: "Engineering" }, + { label: "Product", value: "Product" }, + { label: "Sales", value: "Sales" }, +]; + +const LOCATION_OPTIONS = [ + { label: "San Francisco", value: "San Francisco" }, + { label: "Tokyo", value: "Tokyo" }, + { label: "Lagos", value: "Lagos" }, + { label: "Mexico City", value: "Mexico City" }, + { label: "Kolkata", value: "Kolkata" }, + { label: "Stockholm", value: "Stockholm" }, + { label: "Dubai", value: "Dubai" }, + { label: "Milan", value: "Milan" }, + { label: "Seoul", value: "Seoul" }, + { label: "Austin", value: "Austin" }, + { label: "London", value: "London" }, + { label: "Moscow", value: "Moscow" }, +]; + +export const externalFilterData = [ + { 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" }, + { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing", active: true, location: "Mexico City" }, + { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering", active: true, location: "Kolkata" }, + { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product", active: false, location: "Stockholm" }, + { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales", active: true, location: "Dubai" }, + { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing", active: true, location: "Milan" }, + { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research", active: true, location: "Seoul" }, + { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering", active: false, location: "Austin" }, + { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design", active: true, location: "London" }, + { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" }, +]; + +export const externalFilterHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" }, + { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" }, + { + accessor: "department", + label: "Department", + width: 150, + filterable: true, + type: "enum", + enumOptions: DEPARTMENT_OPTIONS, + }, + { + accessor: "location", + label: "Location", + width: 150, + filterable: true, + type: "enum", + enumOptions: LOCATION_OPTIONS, + }, + { accessor: "active", label: "Active", width: 120, filterable: true, type: "boolean" }, + { + accessor: "salary", + label: "Salary", + width: 120, + filterable: true, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, +]; + +export const externalFilterConfig = { + headers: externalFilterHeaders, + rows: externalFilterData, + tableProps: { externalFilterHandling: true, columnResizing: true }, +} as const; diff --git a/packages/examples/angular/src/demos/external-sort/external-sort-demo.component.ts b/packages/examples/angular/src/demos/external-sort/external-sort-demo.component.ts index f37a721de..827a24841 100644 --- a/packages/examples/angular/src/demos/external-sort/external-sort-demo.component.ts +++ b/packages/examples/angular/src/demos/external-sort/external-sort-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, asRows, defaultHeadersFromCore } from "@simple-table/angular"; import type { AngularHeaderObject, Row, SortColumn, Theme } from "@simple-table/angular"; -import { externalSortConfig } from "@simple-table/examples-shared"; +import { externalSortConfig } from "./external-sort.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -24,7 +24,7 @@ export class ExternalSortDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = externalSortConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(externalSortConfig.headers); private sortState: SortColumn | null = null; handleSortChange = (sort: SortColumn | null): void => { @@ -32,7 +32,7 @@ export class ExternalSortDemoComponent { }; get sortedRows(): Row[] { - const rows = [...externalSortConfig.rows]; + const rows = [...asRows(externalSortConfig.rows)]; if (!this.sortState) return rows; const accessor = this.sortState.key.accessor as string; const type = this.sortState.key.type; diff --git a/packages/examples/angular/src/demos/external-sort/external-sort.demo-data.ts b/packages/examples/angular/src/demos/external-sort/external-sort.demo-data.ts new file mode 100644 index 000000000..011030edf --- /dev/null +++ b/packages/examples/angular/src/demos/external-sort/external-sort.demo-data.ts @@ -0,0 +1,40 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const externalSortData = [ + { 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" }, + { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing" }, + { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering" }, + { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product" }, + { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales" }, + { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing" }, + { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research" }, + { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering" }, + { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design" }, + { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps" }, +]; + +export const externalSortHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: "1fr", minWidth: 120, isSortable: true, type: "string" }, + { accessor: "age", label: "Age", width: 120, isSortable: true, type: "number" }, + { accessor: "department", label: "Department", width: 150, isSortable: true, type: "string" }, + { accessor: "email", label: "Email", width: 200, isSortable: true, type: "string" }, + { + accessor: "salary", + label: "Salary", + width: 120, + isSortable: true, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, +]; + +export const externalSortConfig = { + headers: externalSortHeaders, + rows: externalSortData, + tableProps: { externalSortHandling: true, columnResizing: true }, +} as const; diff --git a/packages/examples/angular/src/demos/footer-renderer/footer-renderer-demo.component.ts b/packages/examples/angular/src/demos/footer-renderer/footer-renderer-demo.component.ts index 146aa4112..e82029085 100644 --- a/packages/examples/angular/src/demos/footer-renderer/footer-renderer-demo.component.ts +++ b/packages/examples/angular/src/demos/footer-renderer/footer-renderer-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { FooterRendererProps, Row, Theme } from "@simple-table/angular"; -import { footerRendererConfig } from "@simple-table/examples-shared"; +import { SimpleTableComponent, defaultHeadersFromCore } from "@simple-table/angular"; +import type { AngularHeaderObject, FooterRendererProps, Row, Theme } from "@simple-table/angular"; +import { footerRendererConfig } from "./footer-renderer.demo-data"; import "@simple-table/angular/styles.css"; function getFooterColors(theme?: Theme) { @@ -125,6 +125,6 @@ export class FooterRendererDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = footerRendererConfig.rows; - readonly headers = footerRendererConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(footerRendererConfig.headers); readonly footerFn = ((props: FooterRendererProps) => createFooter(props, this.theme)) as any; } diff --git a/packages/examples/angular/src/demos/footer-renderer/footer-renderer.demo-data.ts b/packages/examples/angular/src/demos/footer-renderer/footer-renderer.demo-data.ts new file mode 100644 index 000000000..258764394 --- /dev/null +++ b/packages/examples/angular/src/demos/footer-renderer/footer-renderer.demo-data.ts @@ -0,0 +1,69 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const footerRendererHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "price", label: "Price", width: 100, type: "number" }, + { accessor: "stock", label: "Stock", width: 100, type: "number" }, + { accessor: "status", label: "Status", width: "1fr", type: "string" }, +]; + +export const footerRendererData: Row[] = [ + { 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" }, + { id: 4, product: "HP Spectre x360 Convertible", category: "Laptops", price: 1649, stock: 51, status: "In Stock" }, + { id: 5, product: "ASUS ROG Strix Gaming Laptop", category: "Laptops", price: 2199, stock: 19, status: "In Stock" }, + { id: 6, product: "Logitech MX Master 3S Wireless", category: "Accessories", price: 99, stock: 342, status: "In Stock" }, + { id: 7, product: "Apple Magic Mouse Black", category: "Accessories", price: 89, stock: 218, status: "In Stock" }, + { id: 8, product: "Razer DeathAdder V3 Pro", category: "Accessories", price: 149, stock: 167, status: "In Stock" }, + { id: 9, product: "Microsoft Surface Precision Mouse", category: "Accessories", price: 79, stock: 203, status: "In Stock" }, + { id: 10, product: "Corsair K95 RGB Platinum XT", category: "Keyboards", price: 199, stock: 89, status: "In Stock" }, + { id: 11, product: "Keychron Q1 Pro Mechanical", category: "Keyboards", price: 189, stock: 134, status: "In Stock" }, + { id: 12, product: "Ducky One 3 TKL RGB", category: "Keyboards", price: 159, stock: 76, status: "In Stock" }, + { id: 13, product: "Leopold FC900R PD Cherry MX", category: "Keyboards", price: 169, stock: 54, status: "In Stock" }, + { id: 14, product: "LG UltraGear 27-inch 4K 144Hz", category: "Monitors", price: 799, stock: 31, status: "In Stock" }, + { id: 15, product: "Samsung Odyssey G9 Curved", category: "Monitors", price: 1299, stock: 18, status: "In Stock" }, + { id: 16, product: "Dell UltraSharp U2723DE 27in", category: "Monitors", price: 649, stock: 47, status: "In Stock" }, + { id: 17, product: "BenQ PD3220U Designer 32in", category: "Monitors", price: 1099, stock: 23, status: "In Stock" }, + { id: 18, product: "ASUS ProArt Display PA279CRV", category: "Monitors", price: 549, stock: 39, status: "In Stock" }, + { id: 19, product: "Sony WH-1000XM5 Noise Cancelling", category: "Audio", price: 399, stock: 127, status: "In Stock" }, + { id: 20, product: "Bose QuietComfort Ultra", category: "Audio", price: 429, stock: 98, status: "In Stock" }, + { id: 21, product: "Apple AirPods Max Space Gray", category: "Audio", price: 549, stock: 82, status: "In Stock" }, + { id: 22, product: "Sennheiser Momentum 4 Wireless", category: "Audio", price: 349, stock: 104, status: "In Stock" }, + { id: 23, product: "Logitech C920 HD Pro Webcam", category: "Video", price: 79, stock: 245, status: "In Stock" }, + { id: 24, product: "Elgato Facecam Pro 4K60", category: "Video", price: 299, stock: 67, status: "In Stock" }, + { id: 25, product: "Razer Kiyo Pro Ultra 4K", category: "Video", price: 329, stock: 41, status: "In Stock" }, + { id: 26, product: "Herman Miller Aeron Ergonomic", category: "Furniture", price: 1695, stock: 12, status: "Low Stock" }, + { id: 27, product: "Steelcase Leap V2 Office Chair", category: "Furniture", price: 1299, stock: 15, status: "In Stock" }, + { id: 28, product: "Autonomous SmartDesk Pro", category: "Furniture", price: 899, stock: 28, status: "In Stock" }, + { id: 29, product: "Uplift V2 Standing Desk Frame", category: "Furniture", price: 749, stock: 34, status: "In Stock" }, + { id: 30, product: "FlexiSpot E7 Plus Adjustable", category: "Furniture", price: 649, stock: 45, status: "In Stock" }, + { id: 31, product: "Anker PowerCore 20000mAh", category: "Power", price: 49, stock: 412, status: "In Stock" }, + { id: 32, product: "RAVPower 60W USB-C Charger", category: "Power", price: 39, stock: 387, status: "In Stock" }, + { id: 33, product: "Belkin BoostCharge Pro 3-in-1", category: "Power", price: 149, stock: 156, status: "In Stock" }, + { id: 34, product: "Samsung T7 Shield 2TB SSD", category: "Storage", price: 199, stock: 234, status: "In Stock" }, + { id: 35, product: "SanDisk Extreme Pro 1TB Portable", category: "Storage", price: 159, stock: 189, status: "In Stock" }, + { id: 36, product: "WD My Passport 5TB External", category: "Storage", price: 139, stock: 276, status: "In Stock" }, + { id: 37, product: "Crucial X9 Pro 4TB Rugged", category: "Storage", price: 289, stock: 143, status: "In Stock" }, + { id: 38, product: "CalDigit TS4 Thunderbolt 4 Dock", category: "Hubs & Docks", price: 399, stock: 52, status: "In Stock" }, + { id: 39, product: "Anker 577 Thunderbolt Docking", category: "Hubs & Docks", price: 299, stock: 78, status: "In Stock" }, + { id: 40, product: "HyperDrive Gen2 16-Port USB-C", category: "Hubs & Docks", price: 249, stock: 91, status: "In Stock" }, + { id: 41, product: "Blue Yeti X Professional USB", category: "Audio", price: 169, stock: 123, status: "In Stock" }, + { id: 42, product: "Shure MV7 Podcast Microphone", category: "Audio", price: 249, stock: 87, status: "In Stock" }, + { id: 43, product: "Elgato Wave:3 Premium USB", category: "Audio", price: 159, stock: 104, status: "In Stock" }, + { id: 44, product: "Rode NT-USB Mini Studio", category: "Audio", price: 99, stock: 167, status: "In Stock" }, + { id: 45, product: "Audio-Technica AT2020USB+", category: "Audio", price: 149, stock: 145, status: "In Stock" }, +]; + +export const footerRendererConfig = { + headers: footerRendererHeaders, + rows: footerRendererData, + tableProps: { + shouldPaginate: true, + rowsPerPage: 10, + }, +} as const; diff --git a/packages/examples/angular/src/demos/header-renderer/header-renderer-demo.component.ts b/packages/examples/angular/src/demos/header-renderer/header-renderer-demo.component.ts index a71253308..da59bf1e2 100644 --- a/packages/examples/angular/src/demos/header-renderer/header-renderer-demo.component.ts +++ b/packages/examples/angular/src/demos/header-renderer/header-renderer-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, HeaderObject, Row, Theme } from "@simple-table/angular"; -import { headerRendererConfig } from "@simple-table/examples-shared"; +import { headerRendererConfig } from "./header-renderer.demo-data"; import "@simple-table/angular/styles.css"; type SortDir = "asc" | "desc" | null; @@ -43,11 +43,13 @@ export class HeaderRendererDemoComponent { } get headers(): AngularHeaderObject[] { - return headerRendererConfig.headers.map((h) => ({ - ...h, - isSortable: false, - headerRenderer: this.buildHeaderRenderer(h), - })); + return mapToAngularHeaderObjects( + headerRendererConfig.headers.map((h) => ({ + ...h, + isSortable: false, + headerRenderer: this.buildHeaderRenderer(h), + })), + ); } private buildHeaderRenderer(h: HeaderObject) { diff --git a/packages/examples/angular/src/demos/header-renderer/header-renderer.demo-data.ts b/packages/examples/angular/src/demos/header-renderer/header-renderer.demo-data.ts new file mode 100644 index 000000000..4c3939ebc --- /dev/null +++ b/packages/examples/angular/src/demos/header-renderer/header-renderer.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const headerRendererData: Row[] = [ + { 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" }, + { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering" }, + { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics" }, + { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering" }, + { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design" }, + { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" }, +]; + +export const headerRendererHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number", isSortable: true }, + { accessor: "name", label: "Employee Name", width: 180, type: "string", isSortable: true }, + { accessor: "email", label: "Email Address", width: 200, type: "string" }, + { accessor: "role", label: "Job Role", width: 130, type: "string", isSortable: true }, + { accessor: "salary", label: "Annual Salary", width: 140, type: "number", isSortable: true }, + { accessor: "department", label: "Department", width: 150, type: "string", isSortable: true }, +]; + +export const headerRendererConfig = { + headers: headerRendererHeaders, + rows: headerRendererData, + tableProps: { + selectableCells: true, + columnResizing: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/hr/hr-demo.component.ts b/packages/examples/angular/src/demos/hr/hr-demo.component.ts index 4de3d32ef..e49b4b329 100644 --- a/packages/examples/angular/src/demos/hr/hr-demo.component.ts +++ b/packages/examples/angular/src/demos/hr/hr-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, CellChangeProps, CellRenderer, Theme } from "@simple-table/angular"; -import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "@simple-table/examples-shared"; -import type { HREmployee, HRTagColorKey } from "@simple-table/examples-shared"; +import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "./hr.demo-data"; +import type { HREmployee, HRTagColorKey } from "./hr.demo-data"; import "@simple-table/angular/styles.css"; function el(tag: string, styles?: Partial, children?: (Node | string)[]): HTMLElement { @@ -97,10 +97,12 @@ function buildHRHeaders(): AngularHeaderObject[] { }, }; - return hrConfig.headers.map((h) => { - const renderer = renderers[String(h.accessor)]; - return renderer ? { ...h, cellRenderer: renderer } : { ...h }; - }); + return mapToAngularHeaderObjects( + hrConfig.headers.map((h) => { + const renderer = renderers[String(h.accessor)]; + return renderer ? { ...h, cellRenderer: renderer } : { ...h }; + }), + ); } @Component({ diff --git a/packages/examples/angular/src/demos/hr/hr.demo-data.ts b/packages/examples/angular/src/demos/hr/hr.demo-data.ts new file mode 100644 index 000000000..573443b1e --- /dev/null +++ b/packages/examples/angular/src/demos/hr/hr.demo-data.ts @@ -0,0 +1,126 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + +export interface HREmployee { + id: number; + firstName: string; + lastName: string; + fullName: string; + position: string; + performanceScore: number; + department: string; + email: string; + location: string; + hireDate: string; + yearsOfService: number; + salary: number; + status: string; + isRemoteEligible: boolean; +} + + +const HR_FIRST_NAMES = ["James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda", "David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah", "Charles", "Karen"]; +const HR_LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"]; +const POSITIONS = ["Software Engineer", "Senior Engineer", "Tech Lead", "Engineering Manager", "Product Manager", "Designer", "Data Scientist", "DevOps Engineer", "QA Engineer", "Solutions Architect"]; +const DEPARTMENTS = ["Engineering", "Marketing", "Sales", "Finance", "HR", "Operations", "Customer Support"]; +const LOCATIONS = ["New York", "Los Angeles", "Chicago", "San Francisco", "Austin", "Boston", "Seattle", "Remote"]; +const HR_STATUSES = ["Active", "On Leave", "Probation", "Contract", "Terminated"]; + +export function generateHRData(count: number = 100): HREmployee[] { + return Array.from({ length: count }, (_, i) => { + const firstName = HR_FIRST_NAMES[i % HR_FIRST_NAMES.length]; + const lastName = HR_LAST_NAMES[i % HR_LAST_NAMES.length]; + const yearsOfService = Math.floor(Math.random() * 15); + const hireYear = 2024 - yearsOfService; + const hireMonth = String(1 + Math.floor(Math.random() * 12)).padStart(2, "0"); + const hireDay = String(1 + Math.floor(Math.random() * 28)).padStart(2, "0"); + return { + id: i + 1, + firstName, + lastName, + fullName: `${firstName} ${lastName}`, + position: POSITIONS[i % POSITIONS.length], + performanceScore: Math.floor(40 + Math.random() * 60), + department: DEPARTMENTS[i % DEPARTMENTS.length], + email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@company.com`, + location: LOCATIONS[i % LOCATIONS.length], + hireDate: `${hireYear}-${hireMonth}-${hireDay}`, + yearsOfService, + salary: Math.floor(50000 + Math.random() * 150000), + status: HR_STATUSES[i % 10 === 9 ? 4 : i % 7 === 0 ? 1 : i % 11 === 0 ? 2 : i % 13 === 0 ? 3 : 0], + isRemoteEligible: Math.random() > 0.3, + }; + }); +} + +export const hrData = generateHRData(100); + +export const hrHeaders: HeaderObject[] = [ + { accessor: "fullName", label: "Employee", width: 220, isSortable: true, isEditable: false, align: "left", pinned: "left", type: "string" }, + { + accessor: "performanceScore", label: "Performance", width: 160, isSortable: true, isEditable: true, align: "center", type: "number", + valueFormatter: ({ value }) => `${value}/100`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${value}%`, + }, + { + accessor: "department", label: "Department", width: 150, isSortable: true, isEditable: true, align: "left", type: "enum", + enumOptions: [ + { label: "Engineering", value: "Engineering" }, { label: "Marketing", value: "Marketing" }, + { label: "Sales", value: "Sales" }, { label: "Finance", value: "Finance" }, + { label: "HR", value: "HR" }, { label: "Operations", value: "Operations" }, + { label: "Customer Support", value: "Customer Support" }, + ], + }, + { accessor: "email", label: "Email", width: 280, isSortable: true, isEditable: true, align: "left", type: "string" }, + { + accessor: "location", label: "Location", width: 130, isSortable: true, isEditable: true, align: "left", type: "enum", + enumOptions: LOCATIONS.map((l) => ({ label: l, value: l })), + }, + { accessor: "hireDate", label: "Hire Date", width: 120, isSortable: true, isEditable: true, align: "left", type: "date" }, + { accessor: "yearsOfService", label: "Service", width: 100, isSortable: true, isEditable: false, align: "center", type: "number" }, + { accessor: "salary", label: "Salary", width: 130, isSortable: true, isEditable: true, align: "right", type: "number", valueFormatter: ({ value }) => { if (typeof value !== "number") return ""; return `$${value.toLocaleString()}`; }, useFormattedValueForClipboard: true, useFormattedValueForCSV: true }, + { + accessor: "status", label: "Status", width: 120, isSortable: true, isEditable: true, align: "center", pinned: "right", type: "enum", + enumOptions: [ + { label: "Active", value: "Active" }, { label: "On Leave", value: "On Leave" }, + { label: "Probation", value: "Probation" }, { label: "Contract", value: "Contract" }, + { label: "Terminated", value: "Terminated" }, + ], + valueGetter: ({ row }) => { + const priorityMap: Record = { Terminated: 1, Probation: 2, Contract: 3, "On Leave": 4, Active: 5 }; + return priorityMap[String(row.status)] || 999; + }, + }, + { accessor: "isRemoteEligible", label: "Remote Eligible", width: 140, isSortable: true, isEditable: true, align: "center", type: "boolean" }, +]; + +export function getHRThemeColors(theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const tagColors: Record = isDark + ? { green: { bg: "#065f46", text: "#86efac" }, orange: { bg: "#9a3412", text: "#fed7aa" }, blue: { bg: "#1e3a8a", text: "#93c5fd" }, purple: { bg: "#581c87", text: "#c4b5fd" }, red: { bg: "#991b1b", text: "#fca5a5" }, default: { bg: "#374151", text: "#e5e7eb" } } + : { green: { bg: "#f6ffed", text: "#2a6a0d" }, orange: { bg: "#fff7e6", text: "#ad4e00" }, blue: { bg: "#e6f7ff", text: "#0050b3" }, purple: { bg: "#f9f0ff", text: "#391085" }, red: { bg: "#fff1f0", text: "#a8071a" }, default: { bg: "#f0f0f0", text: "rgba(0, 0, 0, 0.85)" } }; + return { + gray: isDark ? "#f3f4f6" : "#1f2937", + grayMuted: isDark ? "#f3f4f6" : "#6b7280", + avatarBg: isDark ? "#3b82f6" : "#1890ff", + avatarText: "#ffffff", + progressSuccess: isDark ? "#34d399" : "#52c41a", + progressNormal: isDark ? "#60a5fa" : "#1890ff", + progressException: isDark ? "#f87171" : "#ff4d4f", + progressBg: isDark ? "#374151" : "#f5f5f5", + progressText: isDark ? "#d1d5db" : "rgba(0, 0, 0, 0.65)", + tagColors, + }; +} + +export type HRTagColorKey = "green" | "orange" | "blue" | "purple" | "red" | "default"; + +export const HR_STATUS_COLOR_MAP: Record = { + Active: "green", "On Leave": "orange", Probation: "blue", Contract: "purple", Terminated: "red", +}; + +export const hrConfig = { + headers: hrHeaders, + rows: hrData, +} as const; diff --git a/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll-demo.component.ts b/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll-demo.component.ts index 014ade190..469cb3a51 100644 --- a/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll-demo.component.ts +++ b/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { infiniteScrollConfig, generateInfiniteScrollData } from "@simple-table/examples-shared"; +import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data"; import "@simple-table/angular/styles.css"; const MAX_ROWS = 200; @@ -31,7 +31,7 @@ export class InfiniteScrollDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = infiniteScrollConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(infiniteScrollConfig.headers); rows: Row[] = generateInfiniteScrollData(0, 30) as Row[]; loading = false; hasMore = true; diff --git a/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll.demo-data.ts b/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll.demo-data.ts new file mode 100644 index 000000000..6e1329119 --- /dev/null +++ b/packages/examples/angular/src/demos/infinite-scroll/infinite-scroll.demo-data.ts @@ -0,0 +1,44 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +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[] = []; + for (let i = 0; i < count; i++) { + const idx = startIndex + i; + const first = FIRST_NAMES[idx % FIRST_NAMES.length]; + const last = LAST_NAMES[idx % LAST_NAMES.length]; + rows.push({ + id: idx + 1, + name: `${first} ${last}`, + email: `${first.toLowerCase()}.${last.toLowerCase()}@techcorp.com`, + department: DEPARTMENTS[idx % DEPARTMENTS.length], + salary: 60000 + Math.floor(((idx * 7919) % 100000)), + }); + } + return rows; +} + +export const infiniteScrollHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, type: "number" }, + { accessor: "name", label: "Name", width: "1fr", minWidth: 120 }, + { accessor: "email", label: "Email", width: 250 }, + { accessor: "department", label: "Department", width: 150 }, + { + accessor: "salary", + label: "Salary", + width: 120, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, +]; + +export const infiniteScrollConfig = { + headers: infiniteScrollHeaders, + rows: generateInfiniteScrollData(0, 30), +} as const; diff --git a/packages/examples/angular/src/demos/infrastructure/infrastructure-demo.component.ts b/packages/examples/angular/src/demos/infrastructure/infrastructure-demo.component.ts index e8ea8a3bc..fe5d9d2ca 100644 --- a/packages/examples/angular/src/demos/infrastructure/infrastructure-demo.component.ts +++ b/packages/examples/angular/src/demos/infrastructure/infrastructure-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input, ViewChild, AfterViewInit, OnDestroy } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularHeaderObject, CellRenderer, Row, Theme } from "@simple-table/angular"; -import { infrastructureData, INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors } from "@simple-table/examples-shared"; -import type { InfrastructureServer } from "@simple-table/examples-shared"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; +import type { AngularHeaderObject, CellRenderer, Row, Theme, ValueGetterProps } from "@simple-table/angular"; +import { infrastructureData, INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors } from "./infrastructure.demo-data"; +import type { InfrastructureServer } from "./infrastructure.demo-data"; import "@simple-table/angular/styles.css"; function getHeaders(currentTheme?: Theme): AngularHeaderObject[] { @@ -69,7 +69,7 @@ function getHeaders(currentTheme?: Theme): AngularHeaderObject[] { return div; }; - return [ + return mapToAngularHeaderObjects([ { accessor: "serverId", align: "left", filterable: true, isEditable: false, isSortable: true, label: "Server ID", minWidth: 180, pinned: "left", type: "string", width: "1.2fr", cellRenderer: serverIdRenderer }, { accessor: "serverName", align: "left", filterable: true, isEditable: false, isSortable: true, label: "Name", minWidth: 200, type: "string", width: "1.5fr" }, { @@ -85,13 +85,13 @@ function getHeaders(currentTheme?: Theme): AngularHeaderObject[] { { accessor: "status", label: "Status", width: 130, isSortable: true, filterable: true, isEditable: 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 }) => { + valueGetter: ({ row }: ValueGetterProps) => { const m: Record = { critical: 1, offline: 2, warning: 3, maintenance: 4, online: 5 }; return m[String(row.status)] || 999; }, cellRenderer: statusRenderer, }, - ]; + ]); } @Component({ diff --git a/packages/examples/angular/src/demos/infrastructure/infrastructure.demo-data.ts b/packages/examples/angular/src/demos/infrastructure/infrastructure.demo-data.ts new file mode 100644 index 000000000..563899352 --- /dev/null +++ b/packages/examples/angular/src/demos/infrastructure/infrastructure.demo-data.ts @@ -0,0 +1,263 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + +export interface InfrastructureServer { + id: number; + serverId: string; + serverName: string; + cpuUsage: number; + cpuHistory: number[]; + memoryUsage: number; + diskUsage: number; + responseTime: number; + networkIn: number; + networkOut: number; + activeConnections: number; + requestsPerSec: number; + status: "online" | "warning" | "critical" | "maintenance" | "offline"; +} + + +const SERVER_PREFIXES = [ + "web", + "api", + "db", + "cache", + "worker", + "proxy", + "auth", + "search", + "queue", + "storage", +]; +const SERVER_NAMES = [ + "Production Primary", + "Production Replica", + "Staging", + "Development", + "Analytics", + "Load Balancer", + "CDN Edge", + "Backup Primary", + "Monitoring", + "Gateway", +]; +const STATUSES: Array = [ + "online", + "warning", + "critical", + "maintenance", + "offline", +]; + +export function generateInfrastructureData(count: number = 50): Row[] { + return Array.from({ length: count }, (_, i) => { + const prefix = SERVER_PREFIXES[i % SERVER_PREFIXES.length]; + const num = String(i + 1).padStart(3, "0"); + const cpu = Math.round((20 + Math.random() * 70) * 10) / 10; + return { + id: i + 1, + serverId: `${prefix}-${num}`, + serverName: SERVER_NAMES[i % SERVER_NAMES.length], + cpuUsage: cpu, + cpuHistory: Array.from({ length: 30 }, () => Math.round((20 + Math.random() * 70) * 10) / 10), + memoryUsage: Math.round((30 + Math.random() * 60) * 10) / 10, + diskUsage: Math.round((10 + Math.random() * 80) * 10) / 10, + responseTime: Math.round((20 + Math.random() * 400) * 10) / 10, + networkIn: Math.round(Math.random() * 1000 * 100) / 100, + networkOut: Math.round(Math.random() * 600 * 100) / 100, + activeConnections: Math.floor(Math.random() * 5000), + requestsPerSec: Math.floor(Math.random() * 10000), + status: STATUSES[i % 5 === 4 ? 4 : i % 7 === 0 ? 2 : i % 5 === 0 ? 1 : i % 9 === 0 ? 3 : 0], + }; + }); +} + +export const infrastructureData = generateInfrastructureData(50); + +export const infrastructureHeaders: HeaderObject[] = [ + { + accessor: "serverId", + align: "left", + filterable: true, + isEditable: false, + isSortable: true, + label: "Server ID", + minWidth: 180, + pinned: "left", + type: "string", + width: "1.2fr", + }, + { + accessor: "serverName", + align: "left", + filterable: true, + isEditable: false, + isSortable: true, + label: "Name", + minWidth: 200, + type: "string", + width: "1.5fr", + }, + { + accessor: "performance", + label: "Performance Metrics", + width: 690, + isSortable: false, + children: [ + { + accessor: "cpuHistory", + label: "CPU History", + width: 150, + isSortable: false, + filterable: false, + isEditable: false, + align: "center", + type: "lineAreaChart", + tooltip: "CPU usage over the last 30 intervals", + }, + { + accessor: "cpuUsage", + label: "CPU %", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "memoryUsage", + label: "Memory %", + width: 130, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "diskUsage", + label: "Disk %", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "responseTime", + label: "Response (ms)", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + ], + }, + { + accessor: "status", + label: "Status", + width: 130, + isSortable: true, + filterable: true, + isEditable: 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 severityMap: Record = { + critical: 1, + offline: 2, + warning: 3, + maintenance: 4, + online: 5, + }; + return severityMap[String(row.status)] || 999; + }, + }, +]; + +export const INFRA_UPDATE_CONFIG = { + minInterval: 300, + maxInterval: 1000, +}; + +export function getInfraMetricColorStyles( + value: number, + theme: string, + metric: "cpu" | "memory" | "response" | "status", + statusValue?: string, +) { + const getLevel = (val: number, thresholds: [number, number, number]) => { + if (val >= thresholds[0]) return "critical"; + if (val >= thresholds[1]) return "warning"; + if (val >= thresholds[2]) return "moderate"; + return "good"; + }; + + let level: string; + if (metric === "cpu") level = getLevel(value, [90, 80, 60]); + else if (metric === "memory") level = getLevel(value, [95, 85, 70]); + else if (metric === "response") level = getLevel(value, [400, 200, 100]); + else level = statusValue || "good"; + + const isDark = theme === "dark" || theme === "modern-dark"; + const colorMap: Record = isDark + ? { + critical: { color: "#fca5a5", backgroundColor: "rgba(127, 29, 29, 0.4)" }, + warning: { color: "#fcd34d", backgroundColor: "rgba(146, 64, 14, 0.4)" }, + moderate: { color: "#60a5fa", backgroundColor: "rgba(30, 64, 175, 0.3)" }, + good: { color: "#4ade80", backgroundColor: "rgba(21, 128, 61, 0.3)" }, + } + : { + critical: { color: "#dc2626", backgroundColor: "#fef2f2" }, + warning: { color: "#d97706", backgroundColor: "#fffbeb" }, + moderate: { color: "#2563eb", backgroundColor: "#eff6ff" }, + good: { color: "#16a34a", backgroundColor: "#f0fdf4" }, + }; + + return colorMap[level] || colorMap.good; +} + +export function getInfraStatusColors(status: string, theme: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const map: Record = isDark + ? { + online: { color: "#6ee7b7", backgroundColor: "rgba(6, 95, 70, 0.4)", fontWeight: "600" }, + warning: { color: "#fcd34d", backgroundColor: "rgba(146, 64, 14, 0.4)", fontWeight: "600" }, + critical: { + color: "#fca5a5", + backgroundColor: "rgba(153, 27, 27, 0.4)", + fontWeight: "600", + }, + maintenance: { + color: "#93c5fd", + backgroundColor: "rgba(30, 64, 175, 0.4)", + fontWeight: "600", + }, + offline: { color: "#d1d5db", backgroundColor: "rgba(75, 85, 99, 0.4)", fontWeight: "600" }, + } + : { + online: { color: "#16a34a", backgroundColor: "#f0fdf4", fontWeight: "600" }, + warning: { color: "#d97706", backgroundColor: "#fffbeb", fontWeight: "600" }, + critical: { color: "#dc2626", backgroundColor: "#fef2f2", fontWeight: "600" }, + maintenance: { color: "#2563eb", backgroundColor: "#eff6ff", fontWeight: "600" }, + offline: { color: "#4b5563", backgroundColor: "#f9fafb", fontWeight: "600" }, + }; + return map[status] || map.offline; +} + +export const infrastructureConfig = { + headers: infrastructureHeaders, + rows: infrastructureData, +} as const; diff --git a/packages/examples/angular/src/demos/live-update/live-update-demo.component.ts b/packages/examples/angular/src/demos/live-update/live-update-demo.component.ts index a828401c3..de344c80b 100644 --- a/packages/examples/angular/src/demos/live-update/live-update-demo.component.ts +++ b/packages/examples/angular/src/demos/live-update/live-update-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input, ViewChild, AfterViewInit, OnDestroy } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { liveUpdateConfig, liveUpdateData } from "@simple-table/examples-shared"; +import { liveUpdateConfig, liveUpdateData } from "./live-update.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,7 +23,7 @@ export class LiveUpdateDemoComponent implements AfterViewInit, OnDestroy { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = liveUpdateConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(liveUpdateConfig.headers); readonly rows: Row[] = liveUpdateConfig.rows; private cleanupFn?: () => void; diff --git a/packages/examples/angular/src/demos/live-update/live-update.demo-data.ts b/packages/examples/angular/src/demos/live-update/live-update.demo-data.ts new file mode 100644 index 000000000..0edc0165d --- /dev/null +++ b/packages/examples/angular/src/demos/live-update/live-update.demo-data.ts @@ -0,0 +1,61 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const liveUpdateHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "product", label: "Product", width: 180, type: "string" }, + { + accessor: "price", label: "Price", width: "1fr", type: "number", + valueFormatter: ({ value }) => typeof value === "number" ? `$${value.toFixed(2)}` : "$0.00", + }, + { accessor: "stock", label: "In Stock", width: 100, type: "number" }, + { + accessor: "stockHistory", label: "Stock Trend", width: 140, type: "lineAreaChart", align: "center", + tooltip: "Stock levels over the last 20 updates", + chartOptions: { color: "#10b981", fillColor: "#34d399", fillOpacity: 0.2, strokeWidth: 2, height: 35 }, + }, + { accessor: "sales", label: "Sales", width: 100, type: "number" }, + { + accessor: "salesHistory", label: "Sales Trend", width: 140, type: "barChart", align: "center", + tooltip: "Sales activity over the last 12 updates", + chartOptions: { color: "#f59e0b", gap: 2, height: 35 }, + }, +]; + +export const generateStockHistory = (currentStock: number, length = 20) => { + const history: number[] = []; + for (let i = 0; i < length; i++) { + const variation = (Math.random() - 0.5) * 30; + history.push(Math.max(0, Math.round(currentStock + variation))); + } + return history; +}; + +export const generateSalesHistory = (_currentSales: number, length = 12) => { + const history: number[] = []; + for (let i = 0; i < length; i++) { + history.push(Math.floor(Math.random() * 4)); + } + return history; +}; + +export const liveUpdateData = [ + { 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) }, + { id: 4, product: "Smart Water Bottle", price: 34.99, stock: 48, sales: 134, stockHistory: generateStockHistory(48), salesHistory: generateSalesHistory(134) }, + { id: 5, product: "Ceramic Coffee Mug", price: 18.99, stock: 124, sales: 298, stockHistory: generateStockHistory(124), salesHistory: generateSalesHistory(298) }, + { id: 6, product: "Wireless Phone Charger", price: 29.99, stock: 67, sales: 156, stockHistory: generateStockHistory(67), salesHistory: generateSalesHistory(156) }, + { id: 7, product: "Essential Oil Diffuser", price: 52.99, stock: 89, sales: 203, stockHistory: generateStockHistory(89), salesHistory: generateSalesHistory(203) }, + { id: 8, product: "Stainless Steel Tumbler", price: 22.99, stock: 134, sales: 267, stockHistory: generateStockHistory(134), salesHistory: generateSalesHistory(267) }, + { id: 9, product: "LED Desk Lamp", price: 39.99, stock: 95, sales: 176, stockHistory: generateStockHistory(95), salesHistory: generateSalesHistory(176) }, + { id: 10, product: "Organic Cotton Towel", price: 26.99, stock: 87, sales: 145, stockHistory: generateStockHistory(87), salesHistory: generateSalesHistory(145) }, + { id: 11, product: "Portable Phone Stand", price: 15.99, stock: 203, sales: 387, stockHistory: generateStockHistory(203), salesHistory: generateSalesHistory(387) }, + { id: 12, product: "Aromatherapy Candle", price: 31.99, stock: 56, sales: 112, stockHistory: generateStockHistory(56), salesHistory: generateSalesHistory(112) }, +]; + +export const liveUpdateConfig = { + headers: liveUpdateHeaders, + rows: liveUpdateData, +} as const; diff --git a/packages/examples/angular/src/demos/loading-state/loading-state-demo.component.ts b/packages/examples/angular/src/demos/loading-state/loading-state-demo.component.ts index 60c634ff6..60d392586 100644 --- a/packages/examples/angular/src/demos/loading-state/loading-state-demo.component.ts +++ b/packages/examples/angular/src/demos/loading-state/loading-state-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input, OnInit, OnDestroy } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { loadingStateConfig } from "@simple-table/examples-shared"; +import { loadingStateConfig } from "./loading-state.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -34,7 +34,7 @@ export class LoadingStateDemoComponent implements OnInit, OnDestroy { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = loadingStateConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(loadingStateConfig.headers); data: Row[] = []; isLoading = true; private timer: ReturnType | null = null; diff --git a/packages/examples/angular/src/demos/loading-state/loading-state.demo-data.ts b/packages/examples/angular/src/demos/loading-state/loading-state.demo-data.ts new file mode 100644 index 000000000..3403b8c58 --- /dev/null +++ b/packages/examples/angular/src/demos/loading-state/loading-state.demo-data.ts @@ -0,0 +1,34 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const loadingStateData = [ + { 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" }, + { id: 4, name: "Santiago Rodriguez", age: 27, department: "Marketing", salary: 82000, status: "Active" }, + { id: 5, name: "Priya Chakraborty", age: 33, department: "Engineering", salary: 118000, status: "Active" }, + { id: 6, name: "Magnus Eriksson", age: 38, department: "Product", salary: 110000, status: "Inactive" }, + { id: 7, name: "Zara Al-Rashid", age: 31, department: "Sales", salary: 98000, status: "Active" }, + { id: 8, name: "Luca Rossi", age: 26, department: "Marketing", salary: 75000, status: "Active" }, +]; + +export const loadingStateHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: "1fr", minWidth: 120 }, + { accessor: "age", label: "Age", width: 80, type: "number" }, + { accessor: "department", label: "Department", width: 150 }, + { + accessor: "salary", + label: "Salary", + width: 120, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, + { accessor: "status", label: "Status", width: 120 }, +]; + +export const loadingStateConfig = { + headers: loadingStateHeaders, + rows: loadingStateData, +} as const; diff --git a/packages/examples/angular/src/demos/manufacturing/manufacturing-demo.component.ts b/packages/examples/angular/src/demos/manufacturing/manufacturing-demo.component.ts index fcef1139d..f13d5fbe2 100644 --- a/packages/examples/angular/src/demos/manufacturing/manufacturing-demo.component.ts +++ b/packages/examples/angular/src/demos/manufacturing/manufacturing-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularHeaderObject, CellRenderer, Row, Theme } from "@simple-table/angular"; -import { manufacturingConfig, getManufacturingStatusColors } from "@simple-table/examples-shared"; -import type { ManufacturingRow } from "@simple-table/examples-shared"; +import { SimpleTableComponent, mapToAngularHeaderObjects, asRows } from "@simple-table/angular"; +import type { AngularHeaderObject, CellRenderer, Theme } from "@simple-table/angular"; +import { manufacturingConfig, getManufacturingStatusColors } from "./manufacturing.demo-data"; +import type { ManufacturingRow } from "./manufacturing.demo-data"; import "@simple-table/angular/styles.css"; function hasStations(row: Record): boolean { @@ -187,10 +187,12 @@ function getHeaders(): AngularHeaderObject[] { maintenanceDate: maintenanceDateRenderer, }; - return manufacturingConfig.headers.map((h) => { - const renderer = rendererMap[String(h.accessor)]; - return renderer ? { ...h, cellRenderer: renderer } : { ...h }; - }); + return mapToAngularHeaderObjects( + manufacturingConfig.headers.map((h) => { + const renderer = rendererMap[String(h.accessor)]; + return renderer ? { ...h, cellRenderer: renderer } : { ...h }; + }), + ); } @Component({ @@ -215,6 +217,6 @@ export class ManufacturingDemoComponent { @Input() theme?: Theme; readonly grouping = ["stations"]; - readonly rows: Row[] = manufacturingConfig.rows; + readonly rows = asRows(manufacturingConfig.rows); readonly headers: AngularHeaderObject[] = getHeaders(); } diff --git a/packages/examples/angular/src/demos/manufacturing/manufacturing.demo-data.ts b/packages/examples/angular/src/demos/manufacturing/manufacturing.demo-data.ts new file mode 100644 index 000000000..d6fcb51e0 --- /dev/null +++ b/packages/examples/angular/src/demos/manufacturing/manufacturing.demo-data.ts @@ -0,0 +1,125 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + +export interface ManufacturingRow { + id: string; + productLine: string; + station: string; + machineType: string; + status: string; + outputRate: number; + cycletime: number; + efficiency: number; + defectRate: number; + defectCount: number; + downtime: number; + utilization: number; + energy: number; + maintenanceDate: string; + stations?: ManufacturingRow[]; +} + + +const PRODUCT_LINES = ["Assembly Line A", "Assembly Line B", "Welding Station", "Paint Shop", "Quality Control", "Packaging Unit", "CNC Machining", "Injection Molding"]; +const STATIONS = ["Station Alpha", "Station Beta", "Station Gamma", "Station Delta", "Station Epsilon"]; +const MACHINE_TYPES = ["CNC Mill", "Lathe", "Welder", "Press", "Robot Arm", "Conveyor", "Inspector", "Dryer"]; +const MANUFACTURING_STATUSES = ["Running", "Scheduled Maintenance", "Unplanned Downtime", "Idle", "Setup"]; + +export function generateManufacturingData(count: number = 8): ManufacturingRow[] { + return Array.from({ length: count }, (_, i) => { + const stationCount = 3 + Math.floor(Math.random() * 3); + const stations: ManufacturingRow[] = Array.from({ length: stationCount }, (_, j) => { + const efficiency = Math.floor(70 + Math.random() * 28); + const defectRate = Math.round((0.1 + Math.random() * 4) * 100) / 100; + const downtime = Math.round((0.1 + Math.random() * 3) * 100) / 100; + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + Math.floor(Math.random() * 30)); + return { + id: `${PRODUCT_LINES[i % PRODUCT_LINES.length]}-S${j + 1}`, + productLine: PRODUCT_LINES[i % PRODUCT_LINES.length], + station: STATIONS[j % STATIONS.length], + machineType: MACHINE_TYPES[(i + j) % MACHINE_TYPES.length], + status: MANUFACTURING_STATUSES[j % MANUFACTURING_STATUSES.length], + outputRate: Math.floor(500 + Math.random() * 2000), + cycletime: Math.round((10 + Math.random() * 50) * 10) / 10, + efficiency, + defectRate, + defectCount: Math.floor(defectRate * 10 + Math.random() * 20), + downtime, + utilization: Math.floor(60 + Math.random() * 38), + energy: Math.floor(100 + Math.random() * 900), + maintenanceDate: futureDate.toISOString().split("T")[0], + }; + }); + + const totalOutput = stations.reduce((s, st) => s + st.outputRate, 0); + const avgEfficiency = Math.round(stations.reduce((s, st) => s + st.efficiency, 0) / stations.length); + const avgCycletime = Math.round((stations.reduce((s, st) => s + st.cycletime, 0) / stations.length) * 10) / 10; + const avgDefectRate = Math.round((stations.reduce((s, st) => s + st.defectRate, 0) / stations.length) * 100) / 100; + const totalDefects = stations.reduce((s, st) => s + st.defectCount, 0); + const totalDowntime = Math.round(stations.reduce((s, st) => s + st.downtime, 0) * 100) / 100; + const avgUtilization = Math.round(stations.reduce((s, st) => s + st.utilization, 0) / stations.length); + const totalEnergy = stations.reduce((s, st) => s + st.energy, 0); + + return { + id: PRODUCT_LINES[i % PRODUCT_LINES.length], + productLine: PRODUCT_LINES[i % PRODUCT_LINES.length], + station: "", + machineType: "", + status: "", + outputRate: totalOutput, + cycletime: avgCycletime, + efficiency: avgEfficiency, + defectRate: avgDefectRate, + defectCount: totalDefects, + downtime: totalDowntime, + utilization: avgUtilization, + energy: totalEnergy, + maintenanceDate: "", + stations, + }; + }); +} + +export const manufacturingData = generateManufacturingData(8); + +export const manufacturingHeaders: HeaderObject[] = [ + { accessor: "productLine", label: "Production Line", width: 180, expandable: true, isSortable: true, isEditable: false, align: "left", type: "string" }, + { accessor: "station", label: "Workstation", width: 150, isSortable: true, isEditable: false, align: "left", type: "string" }, + { accessor: "machineType", label: "Machine Type", width: 150, isSortable: true, isEditable: false, align: "left", type: "string" }, + { + accessor: "status", label: "Status", width: 180, isSortable: true, isEditable: false, align: "center", type: "string", + valueGetter: ({ row }) => { + if (row.stations && Array.isArray(row.stations)) return 999; + const priorityMap: Record = { "Unplanned Downtime": 1, Idle: 2, Setup: 3, "Scheduled Maintenance": 4, Running: 5 }; + return priorityMap[String(row.status)] || 999; + }, + }, + { accessor: "outputRate", label: "Output (units/shift)", width: 200, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "cycletime", label: "Cycle Time (s)", width: 140, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "efficiency", label: "Efficiency", width: 150, isSortable: true, isEditable: false, align: "center", type: "number", aggregation: { type: "average" } }, + { accessor: "defectRate", label: "Defect Rate", width: 120, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "defectCount", label: "Defects", width: 120, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "downtime", label: "Downtime (h)", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "utilization", label: "Utilization", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "energy", label: "Energy (kWh)", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "maintenanceDate", label: "Next Maintenance", width: 200, isSortable: true, isEditable: false, align: "center", type: "date" }, +]; + +export function getManufacturingStatusColors(status: string, theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const isLight = theme === "light" || theme === "modern-light"; + const colorMaps: Record = { + Running: isDark ? { bg: "rgba(6, 95, 70, 0.4)", text: "#6ee7b7" } : isLight ? { bg: "#dcfce7", text: "#16a34a" } : { bg: "#f6ffed", text: "#2a6a0d" }, + "Scheduled Maintenance": isDark ? { bg: "rgba(30, 64, 175, 0.4)", text: "#93c5fd" } : isLight ? { bg: "#dbeafe", text: "#3b82f6" } : { bg: "#e6f7ff", text: "#0050b3" }, + "Unplanned Downtime": isDark ? { bg: "rgba(153, 27, 27, 0.4)", text: "#fca5a5" } : isLight ? { bg: "#fee2e2", text: "#dc2626" } : { bg: "#fff1f0", text: "#a8071a" }, + Idle: isDark ? { bg: "rgba(146, 64, 14, 0.4)", text: "#fcd34d" } : isLight ? { bg: "#fef3c7", text: "#d97706" } : { bg: "#fff7e6", text: "#ad4e00" }, + Setup: isDark ? { bg: "rgba(109, 40, 217, 0.4)", text: "#c4b5fd" } : isLight ? { bg: "#e9d5ff", text: "#9333ea" } : { bg: "#f9f0ff", text: "#391085" }, + }; + return colorMaps[status] || (isDark ? { bg: "rgba(75, 85, 99, 0.4)", text: "#d1d5db" } : isLight ? { bg: "#f3f4f6", text: "#6b7280" } : { bg: "#f0f0f0", text: "rgba(0, 0, 0, 0.85)" }); +} + +export const manufacturingConfig = { + headers: manufacturingHeaders, + rows: manufacturingData, +} as const; diff --git a/packages/examples/angular/src/demos/music/music-demo.component.ts b/packages/examples/angular/src/demos/music/music-demo.component.ts index b8d6bdb5b..d8b3c41cd 100644 --- a/packages/examples/angular/src/demos/music/music-demo.component.ts +++ b/packages/examples/angular/src/demos/music/music-demo.component.ts @@ -1,10 +1,10 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularHeaderObject, CellRenderer, Row, Theme } from "@simple-table/angular"; -import { musicData, getMusicThemeColors } from "@simple-table/examples-shared"; -import type { MusicArtist } from "@simple-table/examples-shared"; +import { SimpleTableComponent, asRows, mapToAngularHeaderObjects } from "@simple-table/angular"; +import type { AngularHeaderObject, CellRenderer, CellRendererProps, Theme } from "@simple-table/angular"; +import { musicData, getMusicThemeColors } from "./music.demo-data"; +import type { MusicArtist } from "./music.demo-data"; import "@simple-table/angular/styles.css"; -import "../../../../shared/src/styles/music-theme.css"; +import "./music-theme.css"; function el(tag: string, styles?: Partial, children?: (Node | string)[]): HTMLElement { const e = document.createElement(tag); @@ -57,7 +57,7 @@ function growthMetric( function buildMusicHeaders(theme?: string): AngularHeaderObject[] { const c = getMusicThemeColors(theme); - const artistRenderer: CellRenderer = ({ row }) => { + const artistRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; let hash = 0; for (let i = 0; i < d.artistName.length; i++) hash = d.artistName.charCodeAt(i) + ((hash << 5) - hash); @@ -82,7 +82,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { return el("div", { display: "flex", alignItems: "center", gap: "12px" }, [avatar, info]); }; - const artistTypeRenderer: CellRenderer = ({ row }) => { + const artistTypeRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("div", { display: "flex", flexDirection: "column", gap: "4px" }, [ el("div", { fontSize: "13px", color: c.gray }, [`${d.artistType}, ${d.pronouns}`]), @@ -91,7 +91,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { ]); }; - const followersRenderer: CellRenderer = ({ row }) => { + const followersRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("div", { display: "flex", flexDirection: "column", gap: "4px", alignItems: "flex-start" }, [ el("div", { fontSize: "14px", color: c.gray }, [d.followersFormatted]), @@ -99,7 +99,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { ]); }; - const playlistReachRenderer: CellRenderer = ({ row }) => { + const playlistReachRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const growth = d.playlistReachChange; const isPos = growth >= 0; @@ -110,7 +110,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { ]); }; - const playlistCountRenderer: CellRenderer = ({ row }) => { + const playlistCountRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("div", { display: "flex", flexDirection: "column", gap: "4px", alignItems: "flex-start" }, [ el("div", { fontSize: "14px", color: c.gray }, [d.playlistCount.toLocaleString()]), @@ -118,7 +118,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { ]); }; - const monthlyListenersRenderer: CellRenderer = ({ row }) => { + const monthlyListenersRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const growth = d.monthlyListenersChange; const isPos = growth >= 0; @@ -129,7 +129,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { ]); }; - const popularityRenderer: CellRenderer = ({ row }) => { + const popularityRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const pct = d.popularityChangePercent; const isPos = pct >= 0; @@ -138,24 +138,24 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { return wrapper; }; - const conversionRateRenderer: CellRenderer = ({ row }) => { + const conversionRateRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("span", { color: c.gray }, [`${d.conversionRate.toFixed(2)}%`]); }; - const ratioRenderer: CellRenderer = ({ row }) => { + const ratioRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("span", { color: c.gray }, [`${d.reachFollowersRatio.toFixed(1)}x`]); }; - const growthCell = (valueKey: string, pctKey: string, signed: boolean): CellRenderer => ({ row }) => { + const growthCell = (valueKey: string, pctKey: string, signed: boolean): CellRenderer => ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const val = d[valueKey as keyof MusicArtist] as number; const pct = d[pctKey as keyof MusicArtist] as number; return growthMetric(val, pct, c, { isPositive: signed ? val >= 0 : true, align: "right" }); }; - return [ + return mapToAngularHeaderObjects([ { accessor: "rank", label: "#", width: 60, isSortable: true, isEditable: false, align: "center", type: "number", pinned: "left" }, { accessor: "artistName", label: "Artist", width: 330, isSortable: true, isEditable: false, align: "left", type: "string", pinned: "left", cellRenderer: artistRenderer }, { accessor: "artistType", label: "Identity", width: 280, isSortable: false, isEditable: false, align: "left", type: "string", cellRenderer: artistTypeRenderer }, @@ -198,7 +198,7 @@ function buildMusicHeaders(theme?: string): AngularHeaderObject[] { }, { accessor: "conversionRate", label: "Conversion Rate", width: 150, isSortable: true, isEditable: false, align: "right", type: "number", cellRenderer: conversionRateRenderer }, { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, isSortable: true, isEditable: false, align: "right", type: "number", cellRenderer: ratioRenderer }, - ]; + ]); } @Component({ @@ -224,7 +224,7 @@ export class MusicDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly rows: Row[] = [...musicData]; + readonly rows = asRows([...musicData]); headers: AngularHeaderObject[] = buildMusicHeaders(); ngOnInit(): void { diff --git a/packages/examples/shared/src/styles/music-theme.css b/packages/examples/angular/src/demos/music/music-theme.css similarity index 100% rename from packages/examples/shared/src/styles/music-theme.css rename to packages/examples/angular/src/demos/music/music-theme.css diff --git a/packages/examples/angular/src/demos/music/music.demo-data.ts b/packages/examples/angular/src/demos/music/music.demo-data.ts new file mode 100644 index 000000000..62868bfaa --- /dev/null +++ b/packages/examples/angular/src/demos/music/music.demo-data.ts @@ -0,0 +1,215 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + +export interface MusicArtist { + id: number; + rank: 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; +} + + +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(); +} + +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; }; + + 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, + }; + }); +} + +export const musicData = generateMusicData(50); + +export const musicHeaders: HeaderObject[] = [ + { accessor: "rank", label: "#", width: 60, isSortable: true, isEditable: false, align: "center", type: "number", pinned: "left" }, + { accessor: "artistName", label: "Artist", width: 330, isSortable: true, isEditable: false, align: "left", type: "string", pinned: "left" }, + { accessor: "artistType", label: "Identity", width: 280, isSortable: false, isEditable: false, align: "left", type: "string" }, + { + accessor: "followersGroup", label: "Followers", width: 700, collapsible: true, + children: [ + { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number" }, + { accessor: "followers7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + ], + }, + { accessor: "popularity", label: "Popularity", width: 180, isSortable: true, isEditable: false, align: "center", type: "number" }, + { + accessor: "playlistReachGroup", label: "Playlist Reach", width: 700, collapsible: true, + children: [ + { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number" }, + { accessor: "playlistReach7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: 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", isSortable: true, isEditable: false, type: "number" }, + { accessor: "playlistCount7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: 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", isSortable: true, isEditable: false, type: "number" }, + { accessor: "monthlyListeners7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + ], + }, + { accessor: "conversionRate", label: "Conversion Rate", width: 150, isSortable: true, isEditable: false, align: "right", type: "number" }, + { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, isSortable: true, isEditable: false, align: "right", type: "number" }, +]; + +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" }, + 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" }, + "modern-dark": { gray: "#e5e7eb", grayMuted: "#9ca3af", success: "#22c55e", successBg: "#052e16", error: "#ef4444", errorBg: "#450a0a", primary: "#60a5fa", primaryBg: "#1e3a8a", warning: "#f59e0b", warningBg: "#451a03", tagBorder: "#4b5563", tagBg: "#111827", tagText: "#f9fafb", highScore: "#22c55e", mediumScore: "#60a5fa", lowScore: "#fbbf24", veryLowScore: "#f87171" }, + dark: { gray: "#e5e7eb", grayMuted: "#9ca3af", success: "#22c55e", successBg: "#052e16", error: "#ef4444", errorBg: "#450a0a", primary: "#60a5fa", primaryBg: "#1e3a8a", warning: "#f59e0b", warningBg: "#451a03", tagBorder: "#4b5563", tagBg: "#111827", tagText: "#f9fafb", highScore: "#22c55e", mediumScore: "#60a5fa", lowScore: "#fbbf24", veryLowScore: "#f87171" }, +}; + +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/angular/src/demos/nested-headers/nested-headers-demo.component.ts b/packages/examples/angular/src/demos/nested-headers/nested-headers-demo.component.ts index a9bc0e169..a2c944092 100644 --- a/packages/examples/angular/src/demos/nested-headers/nested-headers-demo.component.ts +++ b/packages/examples/angular/src/demos/nested-headers/nested-headers-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { nestedHeadersConfig } from "@simple-table/examples-shared"; +import { nestedHeadersConfig } from "./nested-headers.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class NestedHeadersDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = nestedHeadersConfig.rows; - readonly headers: AngularHeaderObject[] = nestedHeadersConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(nestedHeadersConfig.headers); readonly columnResizing = nestedHeadersConfig.tableProps.columnResizing; } diff --git a/packages/examples/angular/src/demos/nested-headers/nested-headers.demo-data.ts b/packages/examples/angular/src/demos/nested-headers/nested-headers.demo-data.ts new file mode 100644 index 000000000..a2fb43982 --- /dev/null +++ b/packages/examples/angular/src/demos/nested-headers/nested-headers.demo-data.ts @@ -0,0 +1,41 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const nestedHeadersHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { accessor: "name", label: "Name", width: "1fr", isSortable: true, type: "string" }, + { + accessor: "score", + label: "Test Scores", + width: 300, + isSortable: false, + type: "number", + children: [ + { accessor: "mathScore", label: "Math", width: 100, isSortable: true, type: "number", align: "right" }, + { accessor: "scienceScore", label: "Science", width: 100, isSortable: true, type: "number", align: "right" }, + { accessor: "historyScore", label: "History", width: 100, isSortable: true, type: "number", align: "right" }, + ], + }, + { accessor: "grade", label: "Overall Grade", width: 120, isSortable: true, type: "string", align: "center" }, +]; + +export const nestedHeadersData = [ + { 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" }, + { id: 4, name: "Phoenix Williams", mathScore: 72, scienceScore: 75, historyScore: 69, grade: "C" }, + { id: 5, name: "River Martinez", mathScore: 87, scienceScore: 91, historyScore: 83, grade: "B" }, + { id: 6, name: "Sage Thompson", mathScore: 79, scienceScore: 74, historyScore: 82, grade: "B" }, + { id: 7, name: "Nova Patel", mathScore: 93, scienceScore: 88, historyScore: 95, grade: "A" }, + { id: 8, name: "Atlas Kim", mathScore: 86, scienceScore: 82, historyScore: 89, grade: "B" }, + { id: 9, name: "Zara Hassan", mathScore: 91, scienceScore: 97, historyScore: 87, grade: "A" }, + { id: 10, name: "Orion Singh", mathScore: 77, scienceScore: 73, historyScore: 80, grade: "B" }, + { id: 11, name: "Echo Volkov", mathScore: 95, scienceScore: 92, historyScore: 98, grade: "A" }, +]; + +export const nestedHeadersConfig = { + headers: nestedHeadersHeaders, + rows: nestedHeadersData, + tableProps: { columnResizing: true }, +} as const; diff --git a/packages/examples/angular/src/demos/nested-tables/nested-tables-demo.component.ts b/packages/examples/angular/src/demos/nested-tables/nested-tables-demo.component.ts index bc44375a4..d876c3d05 100644 --- a/packages/examples/angular/src/demos/nested-tables/nested-tables-demo.component.ts +++ b/packages/examples/angular/src/demos/nested-tables/nested-tables-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Theme } from "@simple-table/angular"; -import { nestedTablesConfig, generateNestedTablesData } from "@simple-table/examples-shared"; +import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -26,7 +26,7 @@ export class NestedTablesDemoComponent { @Input() height: string | number = "500px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = nestedTablesConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(nestedTablesConfig.headers); readonly sampleData = generateNestedTablesData(25); readonly grouping = ["divisions"]; readonly getRowId = ({ row }: { row: Record }) => row["id"] as string; diff --git a/packages/examples/angular/src/demos/nested-tables/nested-tables.demo-data.ts b/packages/examples/angular/src/demos/nested-tables/nested-tables.demo-data.ts new file mode 100644 index 000000000..857e55861 --- /dev/null +++ b/packages/examples/angular/src/demos/nested-tables/nested-tables.demo-data.ts @@ -0,0 +1,75 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +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"]; +const firstNames = ["Jane", "John", "Emily", "Michael", "Sarah", "David", "Lisa", "Robert", "Maria", "James", "Jennifer", "William", "Patricia", "Richard", "Linda"]; +const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Anderson", "Taylor", "Thomas", "Moore"]; +const divisionTypes = ["Cloud Services", "AI Research", "Consumer Products", "Investment Banking", "Retail Banking", "Research & Development", "Operations", "Sales & Marketing", "Customer Success", "Engineering", "Product Development", "Analytics", "Infrastructure", "Security", "Data Science"]; +const companyNames = ["TechCorp", "FinanceHub", "HealthTech", "GlobalSystems", "InnovateLabs", "FutureTech", "DataWorks", "CloudFirst", "SmartSolutions", "NextGen", "PrimeVentures", "AlphaGroup", "BetaSystems", "GammaIndustries", "DeltaCorp"]; +const suffixes = ["Global", "Inc", "Solutions", "Systems", "Ventures", "Group", "Industries", "Technologies"]; + +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) => ({ + divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`, + divisionName: randomElement(divisionTypes), + revenue: `$${randomInt(5, 25)}B`, + profitMargin: `${randomInt(15, 50)}%`, + headcount: randomInt(50, 500), + location: randomElement(cities), +}); + +const generateCompany = (companyIndex: number) => { + const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex)); + return { + id: companyIndex + 1, + companyName: `${randomElement(companyNames)} ${randomElement(suffixes)}`, + industry: randomElement(industries), + founded: randomInt(1985, 2020), + headquarters: randomElement(cities), + stockSymbol: Array.from({ length: 4 }, () => String.fromCharCode(65 + randomInt(0, 25))).join(""), + marketCap: `$${randomInt(10, 200)}B`, + ceo: `${randomElement(firstNames)} ${randomElement(lastNames)}`, + revenue: `$${randomInt(5, 60)}B`, + employees: randomInt(5000, 100000), + divisions, + }; +}; + +export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i)); + +export const nestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionId", label: "Division ID", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: "1fr" }, +]; + +export const nestedTablesHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { defaultHeaders: nestedTablesDivisionHeaders }, + }, + { accessor: "stockSymbol", label: "Symbol", width: 100 }, + { accessor: "marketCap", label: "Market Cap", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const nestedTablesConfig = { + headers: nestedTablesHeaders, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + columnResizing: true, + autoExpandColumns: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/pagination/pagination-demo.component.ts b/packages/examples/angular/src/demos/pagination/pagination-demo.component.ts index 1e09d4938..99feecf48 100644 --- a/packages/examples/angular/src/demos/pagination/pagination-demo.component.ts +++ b/packages/examples/angular/src/demos/pagination/pagination-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "@simple-table/examples-shared"; +import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "./pagination.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -25,7 +25,7 @@ export class PaginationDemoComponent { @Input() height: string | number = "auto"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = paginationConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(paginationConfig.headers); readonly rowsPerPage = PAGINATION_ROWS_PER_PAGE; rows: Row[] = paginationData.slice(0, PAGINATION_ROWS_PER_PAGE); isLoading = false; diff --git a/packages/examples/angular/src/demos/pagination/pagination.demo-data.ts b/packages/examples/angular/src/demos/pagination/pagination.demo-data.ts new file mode 100644 index 000000000..1bf43548d --- /dev/null +++ b/packages/examples/angular/src/demos/pagination/pagination.demo-data.ts @@ -0,0 +1,53 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const PAGINATION_ROWS_PER_PAGE = 9; + +export const paginationHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 140, type: "string" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, + { accessor: "status", label: "Status", width: 110, type: "string" }, +]; + +export const paginationData: Row[] = [ + { 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" }, + { id: 4, name: "Priya Sharma", email: "priya.sharma@grandhotel.com", role: "Spa Director", department: "Wellness", status: "On Leave" }, + { id: 5, name: "Giovanni Rossi", email: "giovanni.r@grandhotel.com", role: "Banquet Manager", department: "Events", status: "Active" }, + { id: 6, name: "Anastasia Volkov", email: "anastasia.v@grandhotel.com", role: "Housekeeping Supervisor", department: "Housekeeping", status: "Active" }, + { id: 7, name: "Omar Hassan", email: "omar.hassan@grandhotel.com", role: "Night Auditor", department: "Front Office", status: "Active" }, + { id: 8, name: "Lucia Fernandez", email: "lucia.f@grandhotel.com", role: "Restaurant Manager", department: "Food & Beverage", status: "Active" }, + { id: 9, name: "Kenji Nakamura", email: "kenji.n@grandhotel.com", role: "Guest Services Coordinator", department: "Guest Services", status: "Active" }, + { id: 10, name: "Victoria Sterling", email: "victoria.s@grandhotel.com", role: "Sales Director", department: "Sales & Marketing", status: "Active" }, + { id: 11, name: "Rafael Martinez", email: "rafael.m@grandhotel.com", role: "Security Chief", department: "Security", status: "Active" }, + { id: 12, name: "Ingrid Larsson", email: "ingrid.l@grandhotel.com", role: "Event Coordinator", department: "Events", status: "Active" }, + { id: 13, name: "Hassan Al-Rashid", email: "hassan.a@grandhotel.com", role: "Maintenance Supervisor", department: "Engineering", status: "Active" }, + { id: 14, name: "Chloe Bennett", email: "chloe.b@grandhotel.com", role: "Front Desk Agent", department: "Front Office", status: "Active" }, + { id: 15, name: "Akira Tanaka", email: "akira.t@grandhotel.com", role: "Sous Chef", department: "Culinary", status: "Active" }, + { id: 16, name: "Isabella Costa", email: "isabella.c@grandhotel.com", role: "HR Specialist", department: "Human Resources", status: "Active" }, + { id: 17, name: "Yuki Sato", email: "yuki.sato@grandhotel.com", role: "Guest Experience Manager", department: "Guest Services", status: "Active" }, + { id: 18, name: "Marco Benedetti", email: "marco.b@grandhotel.com", role: "Sommelier", department: "Food & Beverage", status: "Active" }, + { id: 19, name: "Fatima Al-Zahra", email: "fatima.a@grandhotel.com", role: "Revenue Manager", department: "Finance", status: "Active" }, + { id: 20, name: "Sebastian Wagner", email: "sebastian.w@grandhotel.com", role: "Bell Captain", department: "Guest Services", status: "Active" }, + { id: 21, name: "Mei Lin Chen", email: "mei.chen@grandhotel.com", role: "Pastry Chef", department: "Culinary", status: "Active" }, + { id: 22, name: "Diego Morales", email: "diego.m@grandhotel.com", role: "Pool Attendant", department: "Recreation", status: "Active" }, + { id: 23, name: "Zara Khan", email: "zara.khan@grandhotel.com", role: "Business Center Manager", department: "Business Services", status: "Active" }, + { id: 24, name: "Matteo Ricci", email: "matteo.r@grandhotel.com", role: "Valet Manager", department: "Guest Services", status: "Active" }, + { id: 25, name: "Camila Gonzalez", email: "camila.g@grandhotel.com", role: "Laundry Supervisor", department: "Housekeeping", status: "Active" }, + { id: 26, name: "Bjorn Larsson", email: "bjorn.l@grandhotel.com", role: "IT Support Specialist", department: "Technology", status: "Active" }, + { id: 27, name: "Amara Okafor", email: "amara.o@grandhotel.com", role: "Training Coordinator", department: "Human Resources", status: "On Leave" }, +]; + +export const paginationConfig = { + headers: paginationHeaders, + rows: paginationData, + tableProps: { + rowsPerPage: PAGINATION_ROWS_PER_PAGE, + shouldPaginate: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/programmatic-control/programmatic-control-demo.component.ts b/packages/examples/angular/src/demos/programmatic-control/programmatic-control-demo.component.ts index fd7f183dd..e0aaf9d98 100644 --- a/packages/examples/angular/src/demos/programmatic-control/programmatic-control-demo.component.ts +++ b/packages/examples/angular/src/demos/programmatic-control/programmatic-control-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input, ViewChild } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { programmaticControlConfig, PROGRAMMATIC_CONTROL_STATUS_COLORS } from "@simple-table/examples-shared"; +import { programmaticControlConfig, PROGRAMMATIC_CONTROL_STATUS_COLORS } from "./programmatic-control.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -37,19 +37,21 @@ export class ProgrammaticControlDemoComponent { statusMessage = "No status message"; readonly rows: Row[] = programmaticControlConfig.rows; - readonly headers: AngularHeaderObject[] = programmaticControlConfig.headers.map((h) => { - if (h.accessor === "status") { - return { - ...h, - cellRenderer: ({ row }: { row: Record }) => { - const s = String(row.status); - const colors = PROGRAMMATIC_CONTROL_STATUS_COLORS[s] ?? { bg: "#f3f4f6", color: "#374151" }; - return `${s}`; - }, - }; - } - return { ...h }; - }); + readonly headers: AngularHeaderObject[] = mapToAngularHeaderObjects( + programmaticControlConfig.headers.map((h) => { + if (h.accessor === "status") { + return { + ...h, + cellRenderer: ({ row }: { row: Record }) => { + const s = String(row.status); + const colors = PROGRAMMATIC_CONTROL_STATUS_COLORS[s] ?? { bg: "#f3f4f6", color: "#374151" }; + return `${s}`; + }, + }; + } + return { ...h }; + }), + ); sortByName(): void { this.tableRef.getAPI()?.applySortState({ accessor: "name", direction: "asc" }); @@ -78,7 +80,10 @@ export class ProgrammaticControlDemoComponent { const hdrs = api.getHeaders(); const sortState = api.getSortState(); const filterState = api.getFilterState(); - const totalValue = allRows.reduce((sum, r) => sum + (r.price as number) * (r.stock as number), 0); + 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); + }, 0); const sortInfo = sortState ? `${sortState.key.label} (${sortState.direction})` : "None"; alert( `Table Info:\n• Rows: ${allRows.length}\n• Columns: ${hdrs.length}\n• Active filters: ${Object.keys(filterState).length}\n• Sort: ${sortInfo}\n• Total inventory value: $${totalValue.toFixed(2)}`, diff --git a/packages/examples/angular/src/demos/programmatic-control/programmatic-control.demo-data.ts b/packages/examples/angular/src/demos/programmatic-control/programmatic-control.demo-data.ts new file mode 100644 index 000000000..691f14bda --- /dev/null +++ b/packages/examples/angular/src/demos/programmatic-control/programmatic-control.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const STATUS_COLORS: Record = { + Available: { bg: "#dcfce7", color: "#166534" }, + "Low Stock": { bg: "#fef3c7", color: "#92400e" }, + "Out of Stock": { bg: "#fee2e2", color: "#991b1b" }, +}; + +export const programmaticControlData = [ + { 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" }, + { id: 4, name: "Standing Desk", category: "Furniture", price: 399.99, stock: 0, status: "Out of Stock" }, + { id: 5, name: "Office Chair", category: "Furniture", price: 249.99, stock: 56, status: "Available" }, + { id: 6, name: "Monitor Stand", category: "Furniture", price: 79.99, stock: 8, status: "Low Stock" }, + { id: 7, name: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, status: "Available" }, + { id: 8, name: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, status: "Available" }, + { id: 9, name: "Desk Organizer", category: "Stationery", price: 24.99, stock: 5, status: "Low Stock" }, + { id: 10, name: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, status: "Available" }, + { id: 11, name: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, status: "Available" }, + { id: 12, name: "Desk Lamp LED", category: "Appliances", price: 44.99, stock: 0, status: "Out of Stock" }, +]; + +export const programmaticControlHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, type: "number", isSortable: true, filterable: true }, + { accessor: "name", label: "Product Name", width: "1fr", minWidth: 150, type: "string", isSortable: true, filterable: true }, + { + accessor: "category", + label: "Category", + width: 140, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Electronics", "Furniture", "Stationery", "Appliances"].map((v) => ({ label: v, value: v })), + }, + { accessor: "price", label: "Price", width: 110, align: "right", type: "number", isSortable: true, filterable: true, valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}` }, + { accessor: "stock", label: "Stock", width: 100, align: "right", type: "number", isSortable: true, filterable: true }, + { + accessor: "status", + label: "Status", + width: 110, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Available", "Low Stock", "Out of Stock"].map((v) => ({ label: v, value: v })), + }, +]; + +export const programmaticControlConfig = { + headers: programmaticControlHeaders, + rows: programmaticControlData, +} as const; + +export { STATUS_COLORS as PROGRAMMATIC_CONTROL_STATUS_COLORS }; diff --git a/packages/examples/angular/src/demos/quick-filter/quick-filter-demo.component.ts b/packages/examples/angular/src/demos/quick-filter/quick-filter-demo.component.ts index b96393fea..4cb6418ca 100644 --- a/packages/examples/angular/src/demos/quick-filter/quick-filter-demo.component.ts +++ b/packages/examples/angular/src/demos/quick-filter/quick-filter-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; import { FormsModule } from "@angular/forms"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, QuickFilterMode, Row, Theme } from "@simple-table/angular"; -import { quickFilterConfig } from "@simple-table/examples-shared"; +import { quickFilterConfig } from "./quick-filter.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -73,7 +73,7 @@ export class QuickFilterDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = quickFilterConfig.rows; - readonly headers: AngularHeaderObject[] = quickFilterConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(quickFilterConfig.headers); searchText = ""; filterMode: QuickFilterMode = "simple"; caseSensitive = false; diff --git a/packages/examples/angular/src/demos/quick-filter/quick-filter.demo-data.ts b/packages/examples/angular/src/demos/quick-filter/quick-filter.demo-data.ts new file mode 100644 index 000000000..8f206ec6e --- /dev/null +++ b/packages/examples/angular/src/demos/quick-filter/quick-filter.demo-data.ts @@ -0,0 +1,33 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const quickFilterHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "salary", label: "Salary", width: 120, type: "number", valueFormatter: ({ value }) => `$${(value || 0).toLocaleString()}`, align: "right" }, + { accessor: "status", label: "Status", width: 100, type: "string" }, + { accessor: "location", label: "Location", width: 140, type: "string" }, +]; + +export const quickFilterData = [ + { 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" }, + { id: 4, name: "Diana Prince", age: 31, department: "Marketing", salary: 82000, status: "Inactive", location: "Chicago" }, + { id: 5, name: "Ethan Hunt", age: 29, department: "Sales", salary: 78000, status: "Active", location: "Boston" }, + { id: 6, name: "Fiona Green", age: 38, department: "Engineering", salary: 105000, status: "Active", location: "Seattle" }, + { id: 7, name: "George Wilson", age: 26, department: "Marketing", salary: 68000, status: "Active", location: "Austin" }, + { id: 8, name: "Hannah Lee", age: 33, department: "Sales", salary: 88000, status: "Inactive", location: "Denver" }, + { id: 9, name: "Isaac Chen", age: 27, department: "Engineering", salary: 92000, status: "Active", location: "San Diego" }, + { id: 10, name: "Julia Brown", age: 30, department: "Marketing", salary: 72000, status: "Inactive", location: "Miami" }, + { id: 11, name: "Kevin Davis", age: 28, department: "Sales", salary: 85000, status: "Active", location: "Phoenix" }, + { id: 12, name: "Laura Garcia", age: 32, department: "Engineering", salary: 102000, status: "Active", location: "San Antonio" }, +]; + +export const quickFilterConfig = { + headers: quickFilterHeaders, + rows: quickFilterData, + tableProps: { quickFilter: { text: "", mode: "simple" as const, caseSensitive: false } }, +} as const; diff --git a/packages/examples/angular/src/demos/quick-start/quick-start-demo.component.ts b/packages/examples/angular/src/demos/quick-start/quick-start-demo.component.ts index 63e378acf..64ecf0bd5 100644 --- a/packages/examples/angular/src/demos/quick-start/quick-start-demo.component.ts +++ b/packages/examples/angular/src/demos/quick-start/quick-start-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { quickStartConfig } from "@simple-table/examples-shared"; +import { quickStartConfig } from "./quick-start.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -25,7 +25,7 @@ export class QuickStartDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = quickStartConfig.rows; - readonly headers: AngularHeaderObject[] = quickStartConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(quickStartConfig.headers); readonly editColumnsProp = quickStartConfig.tableProps.editColumns; readonly selectableCellsProp = quickStartConfig.tableProps.selectableCells; readonly customTheme = quickStartConfig.tableProps.customTheme; diff --git a/packages/examples/angular/src/demos/quick-start/quick-start.demo-data.ts b/packages/examples/angular/src/demos/quick-start/quick-start.demo-data.ts new file mode 100644 index 000000000..2b8658754 --- /dev/null +++ b/packages/examples/angular/src/demos/quick-start/quick-start.demo-data.ts @@ -0,0 +1,130 @@ +// Self-contained demo table setup for this example. +import type { Row } from "@simple-table/angular"; +import type { HeaderObject } from "@simple-table/angular"; + + +export const QUICK_START_DATA: Row[] = [ + { + id: 1, + name: "Marcus Rodriguez", + age: 29, + role: "Frontend Developer", + department: "Engineering", + startDate: "2022-03-15", + }, + { + id: 2, + name: "Sophia Chen", + age: 27, + role: "UX/UI Designer", + department: "Design", + startDate: "2021-11-08", + }, + { + id: 3, + name: "Raj Patel", + age: 34, + role: "Engineering Manager", + department: "Engineering", + startDate: "2020-01-20", + }, + { + id: 4, + name: "Luna Martinez", + age: 23, + role: "Junior Developer", + department: "Engineering", + startDate: "2023-06-12", + }, + { + id: 5, + name: "Tyler Anderson", + age: 31, + role: "DevOps Engineer", + department: "Infrastructure", + startDate: "2021-08-03", + }, + { + id: 6, + name: "Zara Kim", + age: 28, + role: "Product Designer", + department: "Design", + startDate: "2022-01-17", + }, + { + id: 7, + name: "Kai Thompson", + age: 26, + role: "Full Stack Developer", + department: "Engineering", + startDate: "2022-09-05", + }, + { + id: 8, + name: "Ava Singh", + age: 33, + role: "Product Manager", + department: "Product", + startDate: "2020-07-14", + }, + { + id: 9, + name: "Jordan Walsh", + age: 25, + role: "Marketing Specialist", + department: "Growth", + startDate: "2023-02-28", + }, + { + id: 10, + name: "Phoenix Lee", + age: 30, + role: "Backend Developer", + department: "Engineering", + startDate: "2021-05-11", + }, + { + id: 11, + name: "River Jackson", + age: 24, + role: "Growth Designer", + department: "Design", + startDate: "2023-01-09", + }, + { + id: 12, + name: "Atlas Morgan", + age: 32, + role: "Tech Lead", + department: "Engineering", + startDate: "2019-12-02", + }, +]; + + +export const quickStartHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { + accessor: "name", + label: "Name", + minWidth: 80, + width: "1fr", + isSortable: true, + type: "string", + }, + { accessor: "age", label: "Age", width: 100, isSortable: true, type: "number" }, + { accessor: "role", label: "Role", width: 150, isSortable: true, type: "string" }, + { accessor: "department", label: "Department", width: 150, isSortable: true, type: "string" }, + { accessor: "startDate", label: "Start Date", width: 150, isSortable: true, type: "date" }, +]; + +export const quickStartConfig = { + headers: quickStartHeaders, + rows: QUICK_START_DATA, + tableProps: { + editColumns: true, + selectableCells: true, + customTheme: { rowHeight: 32 }, + }, +} as const; diff --git a/packages/examples/angular/src/demos/row-grouping/row-grouping-demo.component.ts b/packages/examples/angular/src/demos/row-grouping/row-grouping-demo.component.ts index 479a7332a..272a955d9 100644 --- a/packages/examples/angular/src/demos/row-grouping/row-grouping-demo.component.ts +++ b/packages/examples/angular/src/demos/row-grouping/row-grouping-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input, ViewChild } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { rowGroupingConfig } from "@simple-table/examples-shared"; +import { rowGroupingConfig } from "./row-grouping.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -38,7 +38,7 @@ export class RowGroupingDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = rowGroupingConfig.rows; - readonly headers: AngularHeaderObject[] = rowGroupingConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(rowGroupingConfig.headers); readonly grouping = rowGroupingConfig.tableProps.rowGrouping; readonly getRowId = rowGroupingConfig.tableProps.getRowId; diff --git a/packages/examples/angular/src/demos/row-grouping/row-grouping.demo-data.ts b/packages/examples/angular/src/demos/row-grouping/row-grouping.demo-data.ts new file mode 100644 index 000000000..938ebf160 --- /dev/null +++ b/packages/examples/angular/src/demos/row-grouping/row-grouping.demo-data.ts @@ -0,0 +1,205 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const rowGroupingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "performance", label: "Performance", width: 120, type: "string" }, + { accessor: "location", label: "Location", width: 130, type: "string" }, + { accessor: "growthRate", label: "Growth", width: 90, type: "string" }, + { accessor: "status", label: "Status", width: 110, type: "string" }, + { accessor: "established", label: "Est. Date", width: 110, type: "date" }, +]; + +export const rowGroupingData = [ + { + id: "company-1", + organization: "TechSolutions Inc.", + employees: 137, + budget: "$15.0M", + performance: "Exceeding", + location: "San Francisco", + growthRate: "+9%", + status: "Expanding", + established: "2018-01-01", + divisions: [ + { + id: "div-100", + organization: "Engineering Division", + employees: 97, + budget: "$10.6M", + performance: "Exceeding", + location: "Multiple", + growthRate: "+11%", + status: "Expanding", + established: "2018-01-15", + departments: [ + { id: "dept-1001", organization: "Frontend", employees: 28, budget: "$2.8M", performance: "Exceeding", location: "San Francisco", growthRate: "+12%", status: "Hiring", established: "2019-05-16" }, + { id: "dept-1002", organization: "Backend", employees: 32, budget: "$3.4M", performance: "Meeting", location: "Seattle", growthRate: "+8%", status: "Stable", established: "2018-03-22" }, + { id: "dept-1003", organization: "DevOps", employees: 15, budget: "$1.9M", performance: "Exceeding", location: "Remote", growthRate: "+15%", status: "Hiring", established: "2020-11-05" }, + { id: "dept-1004", organization: "Mobile", employees: 22, budget: "$2.5M", performance: "Meeting", location: "Austin", growthRate: "+10%", status: "Restructuring", established: "2019-08-12" }, + ], + }, + { + id: "div-101", + organization: "Product Division", + employees: 40, + budget: "$4.4M", + performance: "Meeting", + location: "Multiple", + growthRate: "+5%", + status: "Stable", + established: "2019-01-10", + departments: [ + { id: "dept-1101", organization: "Design", employees: 17, budget: "$1.8M", performance: "Meeting", location: "Portland", growthRate: "+6%", status: "Stable", established: "2019-02-28" }, + { id: "dept-1102", organization: "Research", employees: 9, budget: "$1.4M", performance: "Below Target", location: "Boston", growthRate: "+3%", status: "Reviewing", established: "2020-07-15" }, + { id: "dept-1103", organization: "QA Testing", employees: 14, budget: "$1.2M", performance: "Meeting", location: "Chicago", growthRate: "+5%", status: "Stable", established: "2019-11-01" }, + ], + }, + ], + }, + { + id: "company-2", + organization: "HealthFirst Group", + employees: 138, + budget: "$22.4M", + performance: "Meeting", + location: "Boston", + growthRate: "+8%", + status: "Stable", + established: "2010-01-01", + divisions: [ + { + id: "div-200", + organization: "Hospital Operations", + employees: 106, + budget: "$13.1M", + performance: "Meeting", + location: "Multiple", + growthRate: "+6%", + status: "Expanding", + established: "2010-01-05", + departments: [ + { id: "dept-2001", organization: "Emergency", employees: 48, budget: "$5.2M", performance: "Meeting", location: "New York", growthRate: "+4%", status: "Critical", established: "2010-06-14" }, + { id: "dept-2002", organization: "Cardiology", employees: 32, budget: "$4.8M", performance: "Exceeding", location: "Chicago", growthRate: "+9%", status: "Expanding", established: "2012-03-25" }, + { id: "dept-2003", organization: "Pediatrics", employees: 26, budget: "$3.1M", performance: "Meeting", location: "Boston", growthRate: "+7%", status: "Stable", established: "2014-08-30" }, + ], + }, + { + id: "div-201", + organization: "Research & Development", + employees: 32, + budget: "$9.3M", + performance: "Exceeding", + location: "Multiple", + growthRate: "+15%", + status: "Hiring", + established: "2017-01-10", + departments: [ + { id: "dept-2101", organization: "Clinical Trials", employees: 18, budget: "$4.2M", performance: "Exceeding", location: "San Diego", growthRate: "+12%", status: "Expanding", established: "2017-04-18" }, + { id: "dept-2102", organization: "Genomics", employees: 14, budget: "$5.1M", performance: "Exceeding", location: "Cambridge", growthRate: "+18%", status: "Hiring", established: "2019-02-21" }, + ], + }, + ], + }, + { + id: "company-3", + organization: "Global Finance", + employees: 121, + budget: "$15.5M", + performance: "Meeting", + location: "New York", + growthRate: "+3%", + status: "Restructuring", + established: "2005-01-01", + divisions: [ + { + id: "div-300", + organization: "Banking Operations", + employees: 121, + budget: "$15.5M", + performance: "Meeting", + location: "Multiple", + growthRate: "+3%", + status: "Stable", + established: "2005-01-15", + departments: [ + { id: "dept-3001", organization: "Retail Banking", employees: 56, budget: "$4.8M", performance: "Meeting", location: "New York", growthRate: "+2%", status: "Stable", established: "2005-11-08" }, + { id: "dept-3002", organization: "Investment", employees: 38, budget: "$7.2M", performance: "Exceeding", location: "Chicago", growthRate: "+11%", status: "Hiring", established: "2008-05-12" }, + { id: "dept-3003", organization: "Loans", employees: 27, budget: "$3.5M", performance: "Below Target", location: "Dallas", growthRate: "-3%", status: "Restructuring", established: "2010-03-17" }, + ], + }, + ], + }, + { + id: "company-4", + organization: "Apex University", + employees: 115, + budget: "$13.4M", + performance: "Meeting", + location: "Cambridge", + growthRate: "+6%", + status: "Stable", + established: "1992-01-01", + divisions: [ + { + id: "div-400", + organization: "Academic Departments", + employees: 115, + budget: "$13.4M", + performance: "Meeting", + location: "Multiple", + growthRate: "+6%", + status: "Stable", + established: "1992-01-15", + departments: [ + { id: "dept-4001", organization: "Computer Science", employees: 35, budget: "$3.8M", performance: "Meeting", location: "Boston", growthRate: "+8%", status: "Expanding", established: "1998-08-24" }, + { id: "dept-4002", organization: "Business", employees: 42, budget: "$4.5M", performance: "Exceeding", location: "Chicago", growthRate: "+6%", status: "Stable", established: "1995-09-15" }, + { id: "dept-4003", organization: "Engineering", employees: 38, budget: "$5.1M", performance: "Meeting", location: "San Francisco", growthRate: "+4%", status: "Stable", established: "1992-02-11" }, + ], + }, + ], + }, + { + id: "company-5", + organization: "Industrial Systems", + employees: 152, + budget: "$12.9M", + performance: "Meeting", + location: "Detroit", + growthRate: "+3%", + status: "Stable", + established: "2001-01-01", + divisions: [ + { + id: "div-500", + organization: "Production", + employees: 152, + budget: "$12.9M", + performance: "Meeting", + location: "Multiple", + growthRate: "+3%", + status: "Stable", + established: "2001-01-10", + departments: [ + { id: "dept-5001", organization: "Assembly", employees: 78, budget: "$6.2M", performance: "Meeting", location: "Detroit", growthRate: "+2%", status: "Stable", established: "2001-05-18" }, + { id: "dept-5002", organization: "Quality Control", employees: 32, budget: "$2.8M", performance: "Exceeding", location: "Pittsburgh", growthRate: "+5%", status: "Hiring", established: "2003-11-24" }, + { id: "dept-5003", organization: "Logistics", employees: 42, budget: "$3.9M", performance: "Meeting", location: "Indianapolis", growthRate: "+3%", status: "Stable", established: "2005-02-08" }, + ], + }, + ], + }, +]; + +export const rowGroupingConfig = { + headers: rowGroupingHeaders, + rows: rowGroupingData, + tableProps: { + rowGrouping: ["divisions", "departments"] as string[], + enableStickyParents: true, + getRowId: ({ row }: { row: Record }) => String(row.id), + columnResizing: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/row-height/row-height-demo.component.ts b/packages/examples/angular/src/demos/row-height/row-height-demo.component.ts index 2916e036e..50eef5003 100644 --- a/packages/examples/angular/src/demos/row-height/row-height-demo.component.ts +++ b/packages/examples/angular/src/demos/row-height/row-height-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { rowHeightConfig } from "@simple-table/examples-shared"; +import { rowHeightConfig } from "./row-height.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class RowHeightDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = rowHeightConfig.rows; - readonly headers: AngularHeaderObject[] = rowHeightConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(rowHeightConfig.headers); readonly customTheme = rowHeightConfig.tableProps.customTheme; } diff --git a/packages/examples/angular/src/demos/row-height/row-height.demo-data.ts b/packages/examples/angular/src/demos/row-height/row-height.demo-data.ts new file mode 100644 index 000000000..5e6c6340c --- /dev/null +++ b/packages/examples/angular/src/demos/row-height/row-height.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const rowHeightHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", minWidth: 180, width: "1fr", type: "string" }, + { accessor: "department", label: "Department", minWidth: 180, width: "1fr", type: "string" }, +]; + +export const rowHeightData = [ + { 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" }, + { id: 4, name: "Ravi Nakamura", age: 26, role: "Junior Architect", department: "Design" }, + { id: 5, name: "Layla Hassan", age: 32, role: "Structural Engineer", department: "Engineering" }, + { id: 6, name: "Cosmo Chen", age: 30, role: "Interior Designer", department: "Interiors" }, + { id: 7, name: "Stella Petrov", age: 28, role: "Urban Planner", department: "Planning" }, + { id: 8, name: "Dante Kim", age: 35, role: "Project Manager", department: "Operations" }, + { id: 9, name: "Indigo Martinez", age: 27, role: "Environmental Designer", department: "Sustainability" }, + { id: 10, name: "Blaze Williams", age: 33, role: "Construction Manager", department: "Construction" }, + { id: 11, name: "Iris Silva", age: 31, role: "Landscape Architect", department: "Landscape" }, + { id: 12, name: "Neo Thompson", age: 36, role: "Technical Coordinator", department: "Technical" }, +]; + +export const rowHeightConfig = { + headers: rowHeightHeaders, + rows: rowHeightData, + tableProps: { customTheme: { rowHeight: 32 } }, +} as const; diff --git a/packages/examples/angular/src/demos/row-selection/row-selection-demo.component.ts b/packages/examples/angular/src/demos/row-selection/row-selection-demo.component.ts index f099dddac..4eee36f55 100644 --- a/packages/examples/angular/src/demos/row-selection/row-selection-demo.component.ts +++ b/packages/examples/angular/src/demos/row-selection/row-selection-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, Row, RowSelectionChangeProps, Theme } from "@simple-table/angular"; -import { rowSelectionConfig, rowSelectionData } from "@simple-table/examples-shared"; -import type { LibraryBook } from "@simple-table/examples-shared"; +import { rowSelectionConfig, rowSelectionData } from "./row-selection.demo-data"; +import type { LibraryBook } from "./row-selection.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -42,19 +42,21 @@ export class RowSelectionDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = rowSelectionConfig.rows; - readonly headers: AngularHeaderObject[] = rowSelectionConfig.headers.map((h) => { - if (h.accessor === "status") { - return { - ...h, - cellRenderer: ({ row }: { row: Record }) => { - const s = String(row.status); - const color = s === "Available" ? "#16a34a" : s === "Checked Out" ? "#ea580c" : "#dc2626"; - return `${s}`; - }, - }; - } - return { ...h }; - }); + readonly headers: AngularHeaderObject[] = mapToAngularHeaderObjects( + rowSelectionConfig.headers.map((h) => { + if (h.accessor === "status") { + return { + ...h, + cellRenderer: ({ row }: { row: Record }) => { + const s = String(row.status); + const color = s === "Available" ? "#16a34a" : s === "Checked Out" ? "#ea580c" : "#dc2626"; + return `${s}`; + }, + }; + } + return { ...h }; + }), + ); selectedBooks: LibraryBook[] = []; diff --git a/packages/examples/angular/src/demos/row-selection/row-selection.demo-data.ts b/packages/examples/angular/src/demos/row-selection/row-selection.demo-data.ts new file mode 100644 index 000000000..e728bf637 --- /dev/null +++ b/packages/examples/angular/src/demos/row-selection/row-selection.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export type LibraryBook = { + id: number; + isbn: string; + title: string; + author: string; + genre: string; + yearPublished: number; + pages: number; + rating: number; + status: string; + librarySection: string; + borrowedBy?: string; +}; + +export const rowSelectionData: LibraryBook[] = [ + { id: 1001, isbn: "978-0553418026", title: "The Quantum Chronicles", author: "Dr. Elena Vasquez", genre: "Science Fiction", yearPublished: 2019, pages: 324, rating: 4.7, status: "Available", librarySection: "Fiction A-L" }, + { id: 1002, isbn: "978-0316769488", title: "Digital Renaissance", author: "Marcus Chen", genre: "Technology", yearPublished: 2021, pages: 287, rating: 4.2, status: "Checked Out", librarySection: "Technology", borrowedBy: "Sarah Williams" }, + { id: 1003, isbn: "978-1400079179", title: "Echoes of Ancient Wisdom", author: "Prof. Amara Okafor", genre: "Philosophy", yearPublished: 2018, pages: 456, rating: 4.9, status: "Available", librarySection: "Philosophy" }, + { id: 1004, isbn: "978-0062315007", title: "The Midnight Observatory", author: "Luna Rodriguez", genre: "Mystery", yearPublished: 2020, pages: 298, rating: 4.4, status: "Reserved", librarySection: "Fiction M-Z" }, + { id: 1005, isbn: "978-0544003415", title: "Sustainable Architecture Now", author: "Kai Nakamura", genre: "Architecture", yearPublished: 2022, pages: 368, rating: 4.6, status: "Available", librarySection: "Architecture" }, + { id: 1006, isbn: "978-0147516466", title: "Neural Networks Simplified", author: "Dr. Priya Sharma", genre: "Computer Science", yearPublished: 2021, pages: 412, rating: 4.8, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Alex Thompson" }, + { id: 1007, isbn: "978-0547928227", title: "Culinary Traditions of the World", author: "Isabella Fontana", genre: "Cooking", yearPublished: 2019, pages: 276, rating: 4.3, status: "Available", librarySection: "Lifestyle" }, + { id: 1008, isbn: "978-0525509288", title: "The Biomimicry Revolution", author: "Dr. James Whitfield", genre: "Biology", yearPublished: 2020, pages: 345, rating: 4.5, status: "Available", librarySection: "Science" }, + { id: 1009, isbn: "978-0345391803", title: "Symphonies in Code", author: "Aria Blackwood", genre: "Programming", yearPublished: 2022, pages: 423, rating: 4.7, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Emma Davis" }, + { id: 1010, isbn: "978-0812988407", title: "Urban Gardens & Green Spaces", author: "Miguel Santos", genre: "Gardening", yearPublished: 2021, pages: 189, rating: 4.1, status: "Available", librarySection: "Lifestyle" }, + { id: 1011, isbn: "978-0374533557", title: "The Psychology of Innovation", author: "Dr. Rachel Kim", genre: "Psychology", yearPublished: 2019, pages: 312, rating: 4.6, status: "Reserved", librarySection: "Psychology" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Book ID", width: 80, isSortable: true, type: "number" }, + { accessor: "isbn", label: "ISBN", width: 120, isSortable: true, type: "string" }, + { accessor: "title", label: "Title", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "author", label: "Author", width: 140, isSortable: true, type: "string" }, + { accessor: "genre", label: "Genre", width: 120, isSortable: true, type: "string" }, + { accessor: "yearPublished", label: "Year", width: 80, isSortable: true, type: "number" }, + { accessor: "pages", label: "Pages", width: 80, isSortable: true, type: "number" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number" }, + { accessor: "status", label: "Status", width: 100, isSortable: true, type: "string" }, + { accessor: "librarySection", label: "Section", width: 120, isSortable: true, type: "string" }, +]; + +export const rowSelectionConfig = { + headers: rowSelectionHeaders, + rows: rowSelectionData, + tableProps: { + enableRowSelection: true, + columnResizing: true, + columnReordering: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/sales/sales-demo.component.ts b/packages/examples/angular/src/demos/sales/sales-demo.component.ts index a7edeba36..e37697fed 100644 --- a/packages/examples/angular/src/demos/sales/sales-demo.component.ts +++ b/packages/examples/angular/src/demos/sales/sales-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, CellChangeProps, CellRenderer, HeaderObject, Row, Theme } from "@simple-table/angular"; -import { salesConfig, getSalesThemeColors } from "@simple-table/examples-shared"; -import type { SalesRow } from "@simple-table/examples-shared"; +import { salesConfig, getSalesThemeColors } from "./sales.demo-data"; +import type { SalesRow } from "./sales.demo-data"; import "@simple-table/angular/styles.css"; function el(tag: string, styles?: Partial, children?: (Node | string)[]): HTMLElement { @@ -108,7 +108,7 @@ function buildSalesHeaders(): AngularHeaderObject[] { } }; applyRenderers(headers); - return headers as AngularHeaderObject[]; + return mapToAngularHeaderObjects(headers); } @Component({ diff --git a/packages/examples/angular/src/demos/sales/sales.demo-data.ts b/packages/examples/angular/src/demos/sales/sales.demo-data.ts new file mode 100644 index 000000000..481346df3 --- /dev/null +++ b/packages/examples/angular/src/demos/sales/sales.demo-data.ts @@ -0,0 +1,114 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + +export interface SalesRow { + id: number; + repName: string; + dealSize: number; + dealValue: number; + isWon: boolean; + closeDate: string; + commission: number; + profitMargin: number; + dealProfit: number; + category: string; +} + + +const SALES_REP_NAMES = ["Alex Morgan", "Sam Rivera", "Jordan Lee", "Taylor Kim", "Casey Chen", "Riley Park", "Morgan Wells", "Avery Quinn", "Devon Blake", "Harper Cole", "Quinn Foster", "Sage Turner", "Cameron Reed", "Jesse Nash", "Blake Palmer"]; +const CATEGORIES = ["Software", "Hardware", "Services", "Consulting", "Training", "Support"]; + +export function generateSalesData(count: number = 100): SalesRow[] { + return Array.from({ length: count }, (_, i) => { + const dealSize = Math.round((5000 + Math.random() * 200000) * 100) / 100; + const profitMargin = Math.round((0.15 + Math.random() * 0.7) * 1000) / 1000; + const dealValue = Math.round((dealSize / profitMargin) * 100) / 100; + const commission = Math.round(dealValue * 0.1 * 100) / 100; + const dealProfit = Math.round((dealSize - commission) * 100) / 100; + const closeYear = 2023 + Math.floor(Math.random() * 2); + const closeMonth = String(1 + Math.floor(Math.random() * 12)).padStart(2, "0"); + const closeDay = String(1 + Math.floor(Math.random() * 28)).padStart(2, "0"); + + return { + id: i + 1, + repName: SALES_REP_NAMES[i % SALES_REP_NAMES.length], + dealSize, + dealValue, + isWon: Math.random() > 0.35, + closeDate: `${closeYear}-${closeMonth}-${closeDay}`, + commission, + profitMargin, + dealProfit, + category: CATEGORIES[i % CATEGORIES.length], + }; + }); +} + +export const salesData = generateSalesData(100); + +export const salesHeaders: HeaderObject[] = [ + { accessor: "repName", label: "Sales Representative", width: "2fr", minWidth: 200, isSortable: true, isEditable: true, type: "string", tooltip: "Name of the sales representative" }, + { + accessor: "salesMetrics", label: "Sales Metrics", width: 600, isSortable: false, + children: [ + { + accessor: "dealSize", label: "Deal Size", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The size of the deal in dollars", + valueFormatter: ({ value }) => { if (typeof value !== "number") return "—"; return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }, + useFormattedValueForClipboard: true, useFormattedValueForCSV: true, + }, + { accessor: "dealValue", label: "Deal Value", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The value of the deal in dollars" }, + { accessor: "isWon", label: "Status", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "boolean", tooltip: "Whether the deal was won or lost" }, + { + accessor: "closeDate", label: "Close Date", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "date", tooltip: "The date the deal was closed", + valueFormatter: ({ value }) => { + if (typeof value !== "string") return "—"; + const [year, month, day] = value.split("-").map(Number); + const date = new Date(year, month - 1, day); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + ], + }, + { + accessor: "financialMetrics", label: "Financial Metrics", width: "1fr", minWidth: 140, isSortable: false, + children: [ + { accessor: "commission", label: "Commission", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The commission earned from the deal in dollars" }, + { + accessor: "profitMargin", label: "Profit Margin", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The profit margin of the deal", + valueFormatter: ({ value }) => { if (typeof value !== "number") return "—"; return `${(value * 100).toFixed(1)}%`; }, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => { if (typeof value !== "number") return "—"; return `${Math.round(value * 100)}%`; }, + }, + { accessor: "dealProfit", label: "Deal Profit", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The profit of the deal in dollars" }, + { + accessor: "category", label: "Category", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "enum", tooltip: "The category of the deal", + enumOptions: CATEGORIES.map((c) => ({ label: c, value: c })), + valueGetter: ({ row }) => { + const priorityMap: Record = { Software: 1, Consulting: 2, Services: 3, Hardware: 4, Training: 5, Support: 6 }; + return priorityMap[String(row.category)] || 999; + }, + }, + ], + }, +]; + +export function getSalesThemeColors(theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + return { + gray: isDark ? "#f3f4f6" : "#374151", + grayMuted: isDark ? "#f3f4f6" : "#9ca3af", + successHigh: { color: isDark ? "#86efac" : "#15803d", fontWeight: "bold" as const }, + successMedium: isDark ? "#4ade80" : "#16a34a", + successLow: isDark ? "#22c55e" : "#22c55e", + info: isDark ? "#60a5fa" : "#3b82f6", + warning: isDark ? "#facc15" : "#ca8a04", + progressHigh: isDark ? "#34D399" : "#10B981", + progressMedium: isDark ? "#60A5FA" : "#3B82F6", + progressLow: isDark ? "#FBBF24" : "#D97706", + }; +} + +export const salesConfig = { + headers: salesHeaders, + rows: salesData, +} as const; diff --git a/packages/examples/angular/src/demos/single-row-children/single-row-children-demo.component.ts b/packages/examples/angular/src/demos/single-row-children/single-row-children-demo.component.ts index 967befaad..b5c7c0ab8 100644 --- a/packages/examples/angular/src/demos/single-row-children/single-row-children-demo.component.ts +++ b/packages/examples/angular/src/demos/single-row-children/single-row-children-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { singleRowChildrenConfig } from "@simple-table/examples-shared"; +import { singleRowChildrenConfig } from "./single-row-children.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class SingleRowChildrenDemoComponent { @Input() height: string | number = "400px"; @Input() theme?: Theme; - readonly headers: AngularHeaderObject[] = singleRowChildrenConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(singleRowChildrenConfig.headers); readonly rows: Row[] = singleRowChildrenConfig.rows; } diff --git a/packages/examples/angular/src/demos/single-row-children/single-row-children.demo-data.ts b/packages/examples/angular/src/demos/single-row-children/single-row-children.demo-data.ts new file mode 100644 index 000000000..f4bbac6fe --- /dev/null +++ b/packages/examples/angular/src/demos/single-row-children/single-row-children.demo-data.ts @@ -0,0 +1,47 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const singleRowChildrenData = [ + { 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 }, + { id: 4, studentId: "STU-2024-004", studentName: "Diego Ramirez", gradeLevel: "9th Grade", overallGPA: 3.45, mathGrade: 82, scienceGrade: 84, englishGrade: 85, historyGrade: 83 }, + { id: 5, studentId: "STU-2024-005", studentName: "Emma Lindberg", gradeLevel: "10th Grade", overallGPA: 3.75, mathGrade: 88, scienceGrade: 90, englishGrade: 92, historyGrade: 87 }, + { id: 6, studentId: "STU-2024-006", studentName: "Finn O'Connor", gradeLevel: "11th Grade", overallGPA: 3.55, mathGrade: 84, scienceGrade: 85, englishGrade: 87, historyGrade: 86 }, + { id: 7, studentId: "STU-2024-007", studentName: "Grace Wellington", gradeLevel: "12th Grade", overallGPA: 4.0, mathGrade: 98, scienceGrade: 97, englishGrade: 99, historyGrade: 98 }, + { id: 8, studentId: "STU-2024-008", studentName: "Hassan Al-Rashid", gradeLevel: "9th Grade", overallGPA: 3.35, mathGrade: 80, scienceGrade: 82, englishGrade: 83, historyGrade: 81 }, + { id: 9, studentId: "STU-2024-009", studentName: "Isabella Kowalski", gradeLevel: "10th Grade", overallGPA: 3.8, mathGrade: 90, scienceGrade: 89, englishGrade: 93, historyGrade: 88 }, + { id: 10, studentId: "STU-2024-010", studentName: "Jackson Mbele", gradeLevel: "11th Grade", overallGPA: 3.7, mathGrade: 87, scienceGrade: 88, englishGrade: 90, historyGrade: 89 }, + { 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: HeaderObject[] = [ + { accessor: "studentId", label: "Student ID", width: 160, isSortable: true, type: "string" }, + { accessor: "gradeLevel", label: "Grade Level", width: 150, isSortable: true, type: "string" }, + { + accessor: "studentName", + label: "Student Name", + width: 200, + collapsible: true, + type: "string", + isSortable: true, + singleRowChildren: true, + children: [ + { accessor: "overallGPA", label: "GPA", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentCollapsed", valueFormatter: ({ value }) => (value as number).toFixed(2) }, + { accessor: "mathGrade", label: "Math", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "scienceGrade", label: "Science", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "englishGrade", label: "English", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "historyGrade", label: "History", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + ], + }, +]; + +export const singleRowChildrenConfig = { + headers: singleRowChildrenHeaders, + rows: singleRowChildrenData, + tableProps: { + columnResizing: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/shared/src/styles/spreadsheet-custom.css b/packages/examples/angular/src/demos/spreadsheet/spreadsheet-custom.css similarity index 100% rename from packages/examples/shared/src/styles/spreadsheet-custom.css rename to packages/examples/angular/src/demos/spreadsheet/spreadsheet-custom.css diff --git a/packages/examples/angular/src/demos/spreadsheet/spreadsheet-demo.component.ts b/packages/examples/angular/src/demos/spreadsheet/spreadsheet-demo.component.ts index 077b81140..806b6f194 100644 --- a/packages/examples/angular/src/demos/spreadsheet/spreadsheet-demo.component.ts +++ b/packages/examples/angular/src/demos/spreadsheet/spreadsheet-demo.component.ts @@ -1,10 +1,10 @@ import { Component, Input, ViewChild } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import { SimpleTableComponent, mapToAngularHeaderObjects } from "@simple-table/angular"; import type { AngularHeaderObject, CellChangeProps, HeaderObject, HeaderRenderer, Theme } from "@simple-table/angular"; -import { spreadsheetConfig, recalculateAmortization } from "@simple-table/examples-shared"; -import type { SpreadsheetRow } from "@simple-table/examples-shared"; +import { spreadsheetConfig, recalculateAmortization } from "./spreadsheet.demo-data"; +import type { SpreadsheetRow } from "./spreadsheet.demo-data"; import "@simple-table/angular/styles.css"; -import "../../../../shared/src/styles/spreadsheet-custom.css"; +import "./spreadsheet-custom.css"; @Component({ selector: "spreadsheet-demo", @@ -77,14 +77,13 @@ export class SpreadsheetDemoComponent { aggregation: { type: "sum" }, }; this.additionalColumns = [...this.additionalColumns, newCol]; - this.tableRef?.getAPI()?.updateHeaders(this.buildHeaders() as any); }); div.appendChild(btn); return div; }; - return [ + return mapToAngularHeaderObjects([ ...baseHeaders, ...this.additionalColumns, { @@ -97,7 +96,7 @@ export class SpreadsheetDemoComponent { disableReorder: true, headerRenderer: addColumnHeaderRenderer as any, }, - ] as AngularHeaderObject[]; + ]); } onCellEdit({ accessor, newValue, row }: CellChangeProps): void { diff --git a/packages/examples/angular/src/demos/spreadsheet/spreadsheet.demo-data.ts b/packages/examples/angular/src/demos/spreadsheet/spreadsheet.demo-data.ts new file mode 100644 index 000000000..c50faa149 --- /dev/null +++ b/packages/examples/angular/src/demos/spreadsheet/spreadsheet.demo-data.ts @@ -0,0 +1,92 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + +export interface SpreadsheetRow { + id: number; + principal: number; + interestRate: number; + monthlyPayment: number; + remainingBalance: number; + totalPaid: number; + interestPaid: number; +} + + +export function generateSpreadsheetData(count: number = 100): SpreadsheetRow[] { + return Array.from({ length: count }, (_, i) => { + const principal = Math.round((50000 + Math.random() * 450000) * 100) / 100; + const interestRate = Math.round((2.5 + Math.random() * 5) * 100) / 100; + const monthlyRate = interestRate / 100 / 12; + const numMonths = 360; + const monthlyPayment = monthlyRate > 0 + ? Math.round(((principal * monthlyRate * Math.pow(1 + monthlyRate, numMonths)) / (Math.pow(1 + monthlyRate, numMonths) - 1)) * 100) / 100 + : Math.round((principal / numMonths) * 100) / 100; + const paymentsMade = Math.floor(Math.random() * 120); + const totalPaid = Math.round(monthlyPayment * paymentsMade * 100) / 100; + let remainingBalance = principal; + if (monthlyRate > 0) { + remainingBalance = Math.round(principal * ((Math.pow(1 + monthlyRate, numMonths) - Math.pow(1 + monthlyRate, paymentsMade)) / (Math.pow(1 + monthlyRate, numMonths) - 1)) * 100) / 100; + } + const principalReduction = principal - Math.max(0, remainingBalance); + const interestPaid = Math.round(Math.max(0, totalPaid - principalReduction) * 100) / 100; + + return { + id: i + 1, + principal, + interestRate, + monthlyPayment, + remainingBalance: Math.max(0, remainingBalance), + totalPaid, + interestPaid, + }; + }); +} + +export const spreadsheetData = generateSpreadsheetData(100); + +export const spreadsheetHeaders: HeaderObject[] = [ + { accessor: "principal", label: "Principal", width: "1fr", minWidth: 100, align: "right", isEditable: true, type: "number", aggregation: { type: "sum" } }, + { accessor: "interestRate", label: "Interest Rate %", width: "1fr", minWidth: 110, align: "right", isEditable: true, type: "number", aggregation: { type: "average" } }, + { accessor: "monthlyPayment", label: "Monthly Payment", width: "1fr", minWidth: 120, align: "right", isEditable: true, type: "number", aggregation: { type: "sum" } }, + { accessor: "remainingBalance", label: "Remaining Balance", width: "1fr", minWidth: 130, align: "right", isEditable: true, type: "number", aggregation: { type: "sum" } }, + { accessor: "totalPaid", label: "Total Paid", width: "1fr", minWidth: 110, align: "right", isEditable: true, type: "number", aggregation: { type: "sum" } }, + { accessor: "interestPaid", label: "Interest Paid", width: "1fr", minWidth: 110, align: "right", isEditable: true, type: "number", aggregation: { type: "sum" } }, +]; + +export function recalculateAmortization(item: SpreadsheetRow, accessor: string, newValue: string | number): SpreadsheetRow { + const updatedItem = { ...item, [accessor]: newValue }; + if (!["principal", "interestRate", "monthlyPayment"].includes(accessor)) return updatedItem; + + const principal = accessor === "principal" ? parseFloat(String(newValue)) || 0 : item.principal; + const interestRate = accessor === "interestRate" ? parseFloat(String(newValue)) || 0 : item.interestRate; + let monthlyPayment = accessor === "monthlyPayment" ? parseFloat(String(newValue)) || 0 : item.monthlyPayment; + const monthlyRate = interestRate / 100 / 12; + const numMonths = 360; + + if (accessor === "principal" || accessor === "interestRate") { + if (monthlyRate > 0 && principal > 0) { + monthlyPayment = parseFloat(((principal * monthlyRate * Math.pow(1 + monthlyRate, numMonths)) / (Math.pow(1 + monthlyRate, numMonths) - 1)).toFixed(2)); + updatedItem.monthlyPayment = monthlyPayment; + } + } + + const totalPaidValue = typeof item.totalPaid === "number" ? item.totalPaid : 0; + const estimatedPaymentsMade = Math.max(0, Math.min(120, Math.floor(totalPaidValue / monthlyPayment))); + let remainingBalance = principal; + if (monthlyRate > 0 && monthlyPayment > 0) { + remainingBalance = principal * ((Math.pow(1 + monthlyRate, numMonths) - Math.pow(1 + monthlyRate, estimatedPaymentsMade)) / (Math.pow(1 + monthlyRate, numMonths) - 1)); + } + const totalPaid = monthlyPayment * estimatedPaymentsMade; + const principalReduction = principal - Math.max(0, remainingBalance); + const interestPaid = totalPaid - principalReduction; + + updatedItem.remainingBalance = parseFloat(Math.max(0, remainingBalance).toFixed(2)); + updatedItem.totalPaid = parseFloat(totalPaid.toFixed(2)); + updatedItem.interestPaid = parseFloat(Math.max(0, interestPaid).toFixed(2)); + return updatedItem; +} + +export const spreadsheetConfig = { + headers: spreadsheetHeaders, + rows: spreadsheetData, +} as const; diff --git a/packages/examples/angular/src/demos/table-height/table-height-demo.component.ts b/packages/examples/angular/src/demos/table-height/table-height-demo.component.ts index 6c02ceec4..7ff68e3ba 100644 --- a/packages/examples/angular/src/demos/table-height/table-height-demo.component.ts +++ b/packages/examples/angular/src/demos/table-height/table-height-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { tableHeightConfig } from "@simple-table/examples-shared"; +import { tableHeightConfig } from "./table-height.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -41,7 +41,7 @@ export class TableHeightDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = tableHeightConfig.rows; - readonly headers: AngularHeaderObject[] = tableHeightConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(tableHeightConfig.headers); readonly heights = ["200px", "300px", "400px"]; selectedHeight = "400px"; } diff --git a/packages/examples/angular/src/demos/table-height/table-height.demo-data.ts b/packages/examples/angular/src/demos/table-height/table-height.demo-data.ts new file mode 100644 index 000000000..d06091b14 --- /dev/null +++ b/packages/examples/angular/src/demos/table-height/table-height.demo-data.ts @@ -0,0 +1,35 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/angular"; + + +export const tableHeightHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", minWidth: 150, width: "1fr", type: "string" }, + { accessor: "department", label: "Department", minWidth: 150, width: "1fr", type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, +]; + +export const tableHeightData = [ + { 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" }, + { id: 4, name: "David Kim", email: "david@example.com", role: "Data Analyst", department: "Analytics", status: "Active" }, + { id: 5, name: "Eva Patel", email: "eva@example.com", role: "DevOps Engineer", department: "Engineering", status: "On Leave" }, + { id: 6, name: "Frank Johnson", email: "frank@example.com", role: "QA Lead", department: "Engineering", status: "Active" }, + { id: 7, name: "Grace Lee", email: "grace@example.com", role: "UX Researcher", department: "Design", status: "Active" }, + { id: 8, name: "Henry Brown", email: "henry@example.com", role: "Backend Developer", department: "Engineering", status: "Active" }, + { id: 9, name: "Iris Davis", email: "iris@example.com", role: "Frontend Developer", department: "Engineering", status: "Active" }, + { id: 10, name: "Jack Wilson", email: "jack@example.com", role: "Technical Writer", department: "Documentation", status: "Active" }, + { id: 11, name: "Kate Thompson", email: "kate@example.com", role: "Security Engineer", department: "Engineering", status: "Active" }, + { id: 12, name: "Leo Garcia", email: "leo@example.com", role: "ML Engineer", department: "AI", status: "Active" }, + { id: 13, name: "Mia Anderson", email: "mia@example.com", role: "Project Manager", department: "Operations", status: "Active" }, + { id: 14, name: "Noah Taylor", email: "noah@example.com", role: "Solutions Architect", department: "Engineering", status: "Active" }, + { id: 15, name: "Olivia Moore", email: "olivia@example.com", role: "HR Manager", department: "Human Resources", status: "Active" }, +]; + +export const tableHeightConfig = { + headers: tableHeightHeaders, + rows: tableHeightData, +} as const; diff --git a/packages/examples/angular/src/demos/themes/themes-demo.component.ts b/packages/examples/angular/src/demos/themes/themes-demo.component.ts index e194f45bd..c486a3d12 100644 --- a/packages/examples/angular/src/demos/themes/themes-demo.component.ts +++ b/packages/examples/angular/src/demos/themes/themes-demo.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from "@angular/core"; import { NgFor } from "@angular/common"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { themesConfig, AVAILABLE_THEMES } from "@simple-table/examples-shared"; +import { themesConfig, AVAILABLE_THEMES } from "./themes.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -41,7 +41,7 @@ export class ThemesDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = themesConfig.rows; - readonly headers: AngularHeaderObject[] = themesConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(themesConfig.headers); readonly themes = AVAILABLE_THEMES; selectedTheme: Theme = "light"; diff --git a/packages/examples/angular/src/demos/themes/themes.demo-data.ts b/packages/examples/angular/src/demos/themes/themes.demo-data.ts new file mode 100644 index 000000000..8cd581680 --- /dev/null +++ b/packages/examples/angular/src/demos/themes/themes.demo-data.ts @@ -0,0 +1,42 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Theme } from "@simple-table/angular"; + + +export const AVAILABLE_THEMES: { value: Theme; label: string }[] = [ + { value: "light", label: "Light" }, + { value: "dark", label: "Dark" }, + { value: "modern-light", label: "Modern Light" }, + { value: "modern-dark", label: "Modern Dark" }, + { value: "sky", label: "Sky" }, + { value: "violet", label: "Violet" }, + { value: "neutral", label: "Neutral" }, + { value: "frost", label: "Frost" }, +]; + +export const themesHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, +]; + +export const themesData = [ + { 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" }, + { id: 4, name: "Therapist Ana Rodriguez", email: "a.rodriguez@cityhospital.org", department: "Physical Therapy", status: "Active" }, + { id: 5, name: "Dr. Yuki Tanaka", email: "y.tanaka@cityhospital.org", department: "Pediatrics", status: "On Call" }, + { id: 6, name: "Technician Omar Hassan", email: "o.hassan@cityhospital.org", department: "Radiology", status: "Active" }, + { id: 7, name: "Dr. Priya Patel", email: "p.patel@cityhospital.org", department: "Oncology", status: "Active" }, + { id: 8, name: "Coordinator Lisa Kim", email: "l.kim@cityhospital.org", department: "Patient Care", status: "Active" }, + { id: 9, name: "Dr. Giovanni Rossi", email: "g.rossi@cityhospital.org", department: "Surgery", status: "Active" }, + { id: 10, name: "Pharmacist David Lee", email: "d.lee@cityhospital.org", department: "Pharmacy", status: "Active" }, + { id: 11, name: "Nurse Zara Singh", email: "z.singh@cityhospital.org", department: "ICU", status: "Active" }, + { id: 12, name: "Dr. Felix Martinez", email: "f.martinez@cityhospital.org", department: "Orthopedics", status: "Active" }, +]; + +export const themesConfig = { + headers: themesHeaders, + rows: themesData, +} as const; diff --git a/packages/examples/angular/src/demos/tooltip/tooltip-demo.component.ts b/packages/examples/angular/src/demos/tooltip/tooltip-demo.component.ts index 51fd4d473..4fbd45477 100644 --- a/packages/examples/angular/src/demos/tooltip/tooltip-demo.component.ts +++ b/packages/examples/angular/src/demos/tooltip/tooltip-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { tooltipConfig } from "@simple-table/examples-shared"; +import { tooltipConfig } from "./tooltip.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -25,5 +25,5 @@ export class TooltipDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = tooltipConfig.rows; - readonly headers: AngularHeaderObject[] = tooltipConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(tooltipConfig.headers); } diff --git a/packages/examples/angular/src/demos/tooltip/tooltip.demo-data.ts b/packages/examples/angular/src/demos/tooltip/tooltip.demo-data.ts new file mode 100644 index 000000000..e33829b9e --- /dev/null +++ b/packages/examples/angular/src/demos/tooltip/tooltip.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const tooltipData: Row[] = [ + { 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" }, + { id: 4, productName: "Mechanical Gaming Keyboard RGB", category: "Peripherals", price: 189.99, stock: 23, rating: 4.4, lastUpdated: "2024-01-22" }, + { id: 5, productName: "Dell XPS 13 OLED", category: "Laptops", price: 1299.99, stock: 15, rating: 4.5, lastUpdated: "2024-01-25" }, + { id: 6, productName: "Apple Magic Trackpad", category: "Peripherals", price: 149.99, stock: 67, rating: 4.3, lastUpdated: "2024-01-28" }, + { id: 7, productName: "LG 32-inch 4K Monitor", category: "Displays", price: 449.99, stock: 19, rating: 4.6, lastUpdated: "2024-02-01" }, + { id: 8, productName: "ThinkPad X1 Carbon Gen 11", category: "Laptops", price: 1899.99, stock: 6, rating: 4.7, lastUpdated: "2024-02-03" }, + { id: 9, productName: "Razer DeathAdder V3 Pro", category: "Peripherals", price: 149.99, stock: 34, rating: 4.8, lastUpdated: "2024-02-05" }, + { id: 10, productName: "ASUS ROG Swift 27-inch", category: "Displays", price: 699.99, stock: 11, rating: 4.5, lastUpdated: "2024-02-08" }, + { id: 11, productName: "Surface Laptop Studio 2", category: "Laptops", price: 2199.99, stock: 4, rating: 4.4, lastUpdated: "2024-02-10" }, + { id: 12, productName: "Keychron K8 Wireless Keyboard", category: "Peripherals", price: 89.99, stock: 28, rating: 4.6, lastUpdated: "2024-02-12" }, + { id: 13, productName: "HP EliteBook 850 G10", category: "Laptops", price: 1599.99, stock: 9, rating: 4.3, lastUpdated: "2024-02-14" }, + { id: 14, productName: "BenQ PD3200U 32-inch", category: "Displays", price: 799.99, stock: 7, rating: 4.7, lastUpdated: "2024-02-16" }, + { id: 15, productName: "Corsair K95 RGB Platinum", category: "Peripherals", price: 199.99, stock: 16, rating: 4.5, lastUpdated: "2024-02-18" }, +]; + +export const tooltipHeaders: HeaderObject[] = [ + { accessor: "productName", label: "Product", width: 200, isSortable: true, tooltip: "Complete product name including model specifications and key features" }, + { accessor: "category", label: "Category", width: 150, isSortable: true, filterable: true, tooltip: "Product classification: Laptops, Displays, or Peripherals for easy filtering" }, + { + accessor: "price", + label: "Price", + width: 120, + align: "right", + isSortable: true, + tooltip: "Current retail price in US dollars (USD) including all standard features", + valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}`, + }, + { accessor: "stock", label: "Stock", width: 100, align: "right", isSortable: true, tooltip: "Available inventory count - number of units currently in warehouse stock" }, + { + accessor: "rating", + label: "Rating", + width: 100, + align: "center", + isSortable: true, + tooltip: "Customer satisfaction rating based on verified purchase reviews (scale: 1-5 stars)", + valueFormatter: ({ value }) => `${value}/5`, + }, + { accessor: "lastUpdated", label: "Last Updated", width: 150, isSortable: true, tooltip: "Most recent inventory update date in YYYY-MM-DD format" }, +]; + +export const tooltipConfig = { + headers: tooltipHeaders, + rows: tooltipData, + tableProps: { + columnResizing: true, + columnReordering: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/angular/src/demos/value-formatter/value-formatter-demo.component.ts b/packages/examples/angular/src/demos/value-formatter/value-formatter-demo.component.ts index b9db923d9..a2d89f2ba 100644 --- a/packages/examples/angular/src/demos/value-formatter/value-formatter-demo.component.ts +++ b/packages/examples/angular/src/demos/value-formatter/value-formatter-demo.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from "@angular/core"; -import { SimpleTableComponent } from "@simple-table/angular"; +import {SimpleTableComponent, defaultHeadersFromCore} from "@simple-table/angular"; import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular"; -import { valueFormatterConfig } from "@simple-table/examples-shared"; +import { valueFormatterConfig } from "./value-formatter.demo-data"; import "@simple-table/angular/styles.css"; @Component({ @@ -23,6 +23,6 @@ export class ValueFormatterDemoComponent { @Input() theme?: Theme; readonly rows: Row[] = valueFormatterConfig.rows; - readonly headers: AngularHeaderObject[] = valueFormatterConfig.headers; + readonly headers: AngularHeaderObject[] = defaultHeadersFromCore(valueFormatterConfig.headers); readonly selectableCellsProp = valueFormatterConfig.tableProps.selectableCells; } diff --git a/packages/examples/angular/src/demos/value-formatter/value-formatter.demo-data.ts b/packages/examples/angular/src/demos/value-formatter/value-formatter.demo-data.ts new file mode 100644 index 000000000..3ac6670ab --- /dev/null +++ b/packages/examples/angular/src/demos/value-formatter/value-formatter.demo-data.ts @@ -0,0 +1,105 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/angular"; + + +export const valueFormatterData: Row[] = [ + { 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" }, + { id: 4, firstName: "Felix", lastName: "Chang", salary: 135000, joinDate: "2019-11-05", performanceScore: 0.967, balance: 3450.75, department: "Engineering" }, + { id: 5, firstName: "Aria", lastName: "Gonzalez", salary: 92000, joinDate: "2023-02-14", performanceScore: 0.834, balance: 780.25, department: "Design" }, + { id: 6, firstName: "Jasper", lastName: "Flynn", salary: 118000, joinDate: "2021-09-30", performanceScore: 0.891, balance: -425.50, department: "Product" }, + { id: 7, firstName: "Nova", lastName: "Sterling", salary: 105000, joinDate: "2022-04-18", performanceScore: 0.912, balance: 1850.00, department: "Marketing" }, + { id: 8, firstName: "Cruz", lastName: "Martinez", salary: 88500, joinDate: "2023-08-07", performanceScore: 0.798, balance: 245.75, department: "Operations" }, + { id: 9, firstName: "Sage", lastName: "Thompson", salary: 142000, joinDate: "2018-05-20", performanceScore: 0.978, balance: 5620.30, department: "Engineering" }, + { id: 10, firstName: "River", lastName: "Davis", salary: 95000, joinDate: "2022-11-12", performanceScore: 0.856, balance: 0, department: "Sales" }, + { id: 11, firstName: "Phoenix", lastName: "Williams", salary: 128000, joinDate: "2020-06-25", performanceScore: 0.934, balance: 2340.50, department: "Product" }, + { id: 12, firstName: "Atlas", lastName: "Johnson", salary: 102000, joinDate: "2023-01-09", performanceScore: 0.867, balance: -89.25, department: "Design" }, +]; + +const DEPARTMENT_CODES: Record = { + engineering: "ENG", + marketing: "MKT", + sales: "SLS", + product: "PRD", + design: "DSN", + operations: "OPS", +}; + +export const valueFormatterHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { + accessor: "firstName", + label: "Name", + width: 180, + type: "string", + valueFormatter: ({ value, row }) => { + return `${value as string} ${row.lastName as string}`; + }, + }, + { + accessor: "salary", + label: "Salary", + width: 140, + type: "number", + valueFormatter: ({ value }) => { + return `$${(value as number).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; + }, + useFormattedValueForClipboard: true, + useFormattedValueForCSV: true, + }, + { + accessor: "joinDate", + label: "Join Date", + width: 140, + type: "date", + valueFormatter: ({ value }) => { + const date = new Date(value as string); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + { + accessor: "performanceScore", + label: "Performance", + width: 130, + type: "number", + valueFormatter: ({ value }) => `${((value as number) * 100).toFixed(1)}%`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${Math.round((value as number) * 100)}%`, + }, + { + accessor: "balance", + label: "Balance", + width: 120, + type: "number", + valueFormatter: ({ value }) => { + const balance = value as number; + if (balance === 0) return "\u2014"; + if (balance < 0) return `($${Math.abs(balance).toFixed(2)})`; + return `$${balance.toFixed(2)}`; + }, + }, + { + accessor: "department", + label: "Department", + width: 150, + type: "string", + valueFormatter: ({ value }) => (value as string).toUpperCase(), + exportValueGetter: ({ value }) => { + const str = (value as string).toLowerCase(); + const code = DEPARTMENT_CODES[str] || "OTH"; + return `${(value as string).toUpperCase()} (${code})`; + }, + }, +]; + +export const valueFormatterConfig = { + headers: valueFormatterHeaders, + rows: valueFormatterData, + tableProps: { + selectableCells: true, + }, +} as const; diff --git a/packages/examples/angular/src/main.ts b/packages/examples/angular/src/main.ts index 9f88ecb1f..af184f5de 100644 --- a/packages/examples/angular/src/main.ts +++ b/packages/examples/angular/src/main.ts @@ -1,6 +1,6 @@ import "@angular/compiler"; import "zone.js"; -import "../../shared/src/styles/shell.css"; +import "./styles/shell.css"; import { bootstrapApplication } from "@angular/platform-browser"; import { AppComponent } from "./app.component"; diff --git a/packages/examples/shared/src/styles/shell.css b/packages/examples/angular/src/styles/shell.css similarity index 100% rename from packages/examples/shared/src/styles/shell.css rename to packages/examples/angular/src/styles/shell.css diff --git a/packages/examples/angular/vite.config.ts b/packages/examples/angular/vite.config.ts index 2d201adac..bc830bbb8 100644 --- a/packages/examples/angular/vite.config.ts +++ b/packages/examples/angular/vite.config.ts @@ -14,8 +14,6 @@ export default defineConfig({ { find: "@simple-table/angular/styles.css", replacement: path.resolve(__dirname, "../../core/src/styles/base.css") }, { find: "@simple-table/angular", replacement: path.resolve(__dirname, "../../angular/src/index.ts") }, { find: "simple-table-core", replacement: path.resolve(__dirname, "../../core/src/index.ts") }, - { find: /^@simple-table\/examples-shared\/(.*)$/, replacement: path.resolve(__dirname, "../shared/src/$1") }, - { find: "@simple-table/examples-shared", replacement: path.resolve(__dirname, "../shared/src/index.ts") }, ], }, optimizeDeps: { diff --git a/packages/examples/react/package.json b/packages/examples/react/package.json index 12ba6720d..262be19bf 100644 --- a/packages/examples/react/package.json +++ b/packages/examples/react/package.json @@ -11,8 +11,7 @@ "dependencies": { "react": "^18.0.0", "react-dom": "^18.0.0", - "@simple-table/react": "workspace:*", - "@simple-table/examples-shared": "workspace:*" + "@simple-table/react": "workspace:*" }, "devDependencies": { "@types/react": "^18.0.0", diff --git a/packages/examples/react/src/demo-list.ts b/packages/examples/react/src/demo-list.ts new file mode 100644 index 000000000..7c1935b68 --- /dev/null +++ b/packages/examples/react/src/demo-list.ts @@ -0,0 +1,57 @@ +export const DEMO_LIST = [ + { id: "quick-start", label: "Quick Start" }, + { id: "column-filtering", label: "Column Filtering" }, + { id: "column-sorting", label: "Column Sorting" }, + { id: "value-formatter", label: "Value Formatter" }, + { id: "pagination", label: "Pagination" }, + { id: "column-pinning", label: "Column Pinning" }, + { id: "column-alignment", label: "Column Alignment" }, + { id: "column-width", label: "Column Width" }, + { id: "column-resizing", label: "Column Resizing" }, + { id: "column-reordering", label: "Column Reordering" }, + { id: "column-selection", label: "Column Selection" }, + { id: "column-editing", label: "Column Editing" }, + { id: "cell-editing", label: "Cell Editing" }, + { id: "cell-highlighting", label: "Cell Highlighting" }, + { id: "themes", label: "Themes" }, + { id: "row-height", label: "Row Height" }, + { id: "table-height", label: "Table Height" }, + { id: "quick-filter", label: "Quick Filter" }, + { id: "nested-headers", label: "Nested Headers" }, + { id: "external-sort", label: "External Sort" }, + { id: "external-filter", label: "External Filter" }, + { id: "loading-state", label: "Loading State" }, + { id: "infinite-scroll", label: "Infinite Scroll" }, + { id: "row-selection", label: "Row Selection" }, + { id: "csv-export", label: "CSV Export" }, + { id: "programmatic-control", label: "Programmatic Control" }, + { id: "row-grouping", label: "Row Grouping" }, + { id: "aggregate-functions", label: "Aggregate Functions" }, + { id: "collapsible-columns", label: "Collapsible Columns" }, + { id: "cell-renderer", label: "Cell Renderer" }, + { id: "header-renderer", label: "Header Renderer" }, + { id: "footer-renderer", label: "Footer Renderer" }, + { id: "cell-clicking", label: "Cell Clicking" }, + { id: "tooltip", label: "Tooltip" }, + { id: "custom-theme", label: "Custom Theme" }, + { id: "custom-icons", label: "Custom Icons" }, + { id: "empty-state", label: "Empty State" }, + { id: "column-visibility", label: "Column Visibility" }, + { id: "column-editor-custom-renderer", label: "Column Editor Custom Renderer" }, + { id: "single-row-children", label: "Single Row Children" }, + { id: "nested-tables", label: "Nested Tables" }, + { id: "dynamic-nested-tables", label: "Dynamic Nested Tables" }, + { id: "dynamic-row-loading", label: "Dynamic Row Loading" }, + { id: "charts", label: "Charts" }, + { id: "live-update", label: "Live Update" }, + { id: "crm", label: "CRM" }, + { id: "infrastructure", label: "Infrastructure" }, + { id: "music", label: "Music" }, + { id: "billing", label: "Billing" }, + { id: "manufacturing", label: "Manufacturing" }, + { id: "hr", label: "HR" }, + { id: "sales", label: "Sales" }, + { id: "spreadsheet", label: "Spreadsheet" }, +] as const; + +export type DemoId = (typeof DEMO_LIST)[number]["id"]; diff --git a/packages/examples/react/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx b/packages/examples/react/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx index 78e2451a3..23cc0bfa6 100644 --- a/packages/examples/react/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx +++ b/packages/examples/react/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { aggregateFunctionsConfig } from "@simple-table/examples-shared"; +import { aggregateFunctionsConfig } from "./aggregate-functions.demo-data"; import "@simple-table/react/styles.css"; const AggregateFunctionsDemo = ({ @@ -12,7 +12,7 @@ const AggregateFunctionsDemo = ({ }) => { return ( { + if (typeof value === "number") { + return value >= 1000000 + ? `${(value / 1000000).toFixed(1)}M` + : value >= 1000 + ? `${(value / 1000).toFixed(0)}K` + : value.toString(); + } + return ""; + }, + }, + { + accessor: "revenue", + label: "Monthly Revenue", + width: 140, + type: "string", + aggregation: { + type: "sum", + parseValue: (value) => { + if (typeof value !== "string") return 0; + const numericValue = parseFloat(value.replace(/[$K]/g, "")); + return isNaN(numericValue) ? 0 : numericValue; + }, + }, + valueFormatter: ({ value }) => { + if (typeof value === "number") return `$${value.toFixed(1)}K`; + if (typeof value === "string") return value; + return ""; + }, + }, + { + accessor: "rating", + label: "Rating", + width: 100, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${value.toFixed(1)} ⭐` : ""), + }, + { + accessor: "contentCount", + label: "Content", + width: 90, + type: "number", + aggregation: { type: "sum" }, + }, + { + accessor: "avgViewTime", + label: "Avg Watch Time", + width: 130, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${Math.round(value)}min` : ""), + }, + { accessor: "status", label: "Status", width: 120, type: "string" }, +]; + +export const aggregateFunctionsData = [ + { + id: 1, + name: "StreamFlix", + status: "Leading Platform", + categories: [ + { + id: 101, + name: "Gaming", + status: "Trending", + creators: [ + { id: 1001, name: "PixelMaster", followers: 2800000, revenue: "$45.2K", rating: 4.8, contentCount: 328, avgViewTime: 45, status: "Partner" }, + { id: 1002, name: "RetroGamer93", followers: 1200000, revenue: "$28.5K", rating: 4.6, contentCount: 156, avgViewTime: 52, status: "Partner" }, + { id: 1003, name: "SpeedrunQueen", followers: 890000, revenue: "$22.1K", rating: 4.9, contentCount: 89, avgViewTime: 38, status: "Partner" }, + ], + }, + { + id: 102, + name: "Music & Arts", + status: "Growing", + creators: [ + { id: 1101, name: "MelodyMaker", followers: 1650000, revenue: "$31.8K", rating: 4.7, contentCount: 203, avgViewTime: 28, status: "Partner" }, + { id: 1102, name: "DigitalArtist", followers: 720000, revenue: "$18.9K", rating: 4.5, contentCount: 127, avgViewTime: 35, status: "Affiliate" }, + { id: 1103, name: "JazzVibez", followers: 430000, revenue: "$12.4K", rating: 4.8, contentCount: 78, avgViewTime: 42, status: "Affiliate" }, + ], + }, + { + id: 103, + name: "Cooking & Lifestyle", + status: "Stable", + creators: [ + { id: 1201, name: "ChefExtraordinaire", followers: 3200000, revenue: "$58.7K", rating: 4.9, contentCount: 245, avgViewTime: 22, status: "Partner" }, + { id: 1202, name: "HomeDecorGuru", followers: 980000, revenue: "$19.3K", rating: 4.4, contentCount: 134, avgViewTime: 18, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 2, + name: "WatchNow", + status: "Competitor", + categories: [ + { + id: 201, + name: "Tech Reviews", + status: "Hot", + creators: [ + { id: 2001, name: "TechGuru2024", followers: 2100000, revenue: "$42.6K", rating: 4.6, contentCount: 189, avgViewTime: 35, status: "Partner" }, + { id: 2002, name: "GadgetWhisperer", followers: 1450000, revenue: "$29.1K", rating: 4.7, contentCount: 156, avgViewTime: 31, status: "Partner" }, + { id: 2003, name: "CodeReviewer", followers: 680000, revenue: "$16.8K", rating: 4.8, contentCount: 94, avgViewTime: 48, status: "Affiliate" }, + ], + }, + { + id: 202, + name: "Fitness & Health", + status: "Growing", + creators: [ + { id: 2101, name: "FitnessPhenom", followers: 1890000, revenue: "$35.4K", rating: 4.5, contentCount: 312, avgViewTime: 25, status: "Partner" }, + { id: 2102, name: "YogaMaster", followers: 1100000, revenue: "$21.7K", rating: 4.9, contentCount: 178, avgViewTime: 33, status: "Partner" }, + ], + }, + ], + }, + { + id: 3, + name: "CreativeSpace", + status: "Emerging", + categories: [ + { + id: 301, + name: "Photography", + status: "Niche", + creators: [ + { id: 3001, name: "LensArtist", followers: 750000, revenue: "$18.2K", rating: 4.7, contentCount: 145, avgViewTime: 27, status: "Partner" }, + { id: 3002, name: "NatureShooter", followers: 520000, revenue: "$13.5K", rating: 4.6, contentCount: 98, avgViewTime: 29, status: "Affiliate" }, + { id: 3003, name: "PortraitPro", followers: 390000, revenue: "$9.8K", rating: 4.8, contentCount: 67, avgViewTime: 24, status: "Affiliate" }, + ], + }, + { + id: 302, + name: "Animation & VFX", + status: "Specialized", + creators: [ + { id: 3101, name: "3DAnimator", followers: 640000, revenue: "$15.9K", rating: 4.9, contentCount: 58, avgViewTime: 41, status: "Partner" }, + { id: 3102, name: "VFXWizard", followers: 480000, revenue: "$12.3K", rating: 4.7, contentCount: 42, avgViewTime: 38, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 4, + name: "EduStream", + status: "Educational Focus", + categories: [ + { + id: 401, + name: "Science & Math", + status: "Educational", + creators: [ + { id: 4001, name: "MathExplainer", followers: 1340000, revenue: "$26.8K", rating: 4.8, contentCount: 234, avgViewTime: 36, status: "Partner" }, + { id: 4002, name: "PhysicsPhun", followers: 890000, revenue: "$19.4K", rating: 4.6, contentCount: 167, avgViewTime: 42, status: "Partner" }, + { id: 4003, name: "ChemistryLab", followers: 560000, revenue: "$14.2K", rating: 4.7, contentCount: 89, avgViewTime: 33, status: "Affiliate" }, + ], + }, + { + id: 402, + name: "History & Culture", + status: "Informative", + creators: [ + { id: 4101, name: "HistoryBuff", followers: 920000, revenue: "$18.6K", rating: 4.5, contentCount: 145, avgViewTime: 39, status: "Partner" }, + { id: 4102, name: "CultureExplorer", followers: 670000, revenue: "$15.1K", rating: 4.8, contentCount: 112, avgViewTime: 45, status: "Affiliate" }, + ], + }, + ], + }, +]; + +export const aggregateFunctionsConfig = { + headers: aggregateFunctionsHeaders, + rows: aggregateFunctionsData, + tableProps: { + rowGrouping: ["categories", "creators"] as string[], + columnResizing: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/billing/BillingDemo.tsx b/packages/examples/react/src/demos/billing/BillingDemo.tsx index 77104309f..5b316a87c 100644 --- a/packages/examples/react/src/demos/billing/BillingDemo.tsx +++ b/packages/examples/react/src/demos/billing/BillingDemo.tsx @@ -1,22 +1,24 @@ -import { SimpleTable } from "@simple-table/react"; -import type { Theme, ReactHeaderObject } from "@simple-table/react"; -import { billingConfig } from "@simple-table/examples-shared"; -import type { BillingRow } from "@simple-table/examples-shared"; +import { SimpleTable, mapToReactHeaderObjects } from "@simple-table/react"; +import type { Theme, ReactHeaderObject, CellRendererProps } from "@simple-table/react"; +import { billingConfig } from "./billing.demo-data"; +import type { BillingRow } from "./billing.demo-data"; import "@simple-table/react/styles.css"; const BillingDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { - const headers: ReactHeaderObject[] = billingConfig.headers.map((h) => { - if (h.accessor === "name") { - return { - ...h, - cellRenderer: ({ row: r }) => { - const d = r as unknown as BillingRow; - return
{d.name}
; - }, - }; - } - return h; - }); + const headers: ReactHeaderObject[] = mapToReactHeaderObjects( + billingConfig.headers.map((h) => { + if (h.accessor === "name") { + return { + ...h, + cellRenderer: ({ row: r }: CellRendererProps) => { + const d = r as unknown as BillingRow; + return
{d.name}
; + }, + }; + } + return h; + }), + ); return ( { + const data: Record = {}; + const year = 2024; + for (let m = 0; m < 12; m++) { + const mo = months[m]; + const base = Math.round((1000 + Math.random() * 50000) * 100) / 100; + data[`balance_${mo}_${year}`] = base; + data[`revenue_${mo}_${year}`] = Math.round(base * (0.6 + Math.random() * 0.3) * 100) / 100; + } + return data; +} + +export function generateBillingData(count: number = 30): BillingRow[] { + const rows: BillingRow[] = []; + for (let i = 0; i < count; i++) { + const accountName = ACCOUNT_NAMES[i % ACCOUNT_NAMES.length]; + const totalAmount = Math.round((5000 + Math.random() * 200000) * 100) / 100; + const recognized = Math.round(totalAmount * (0.3 + Math.random() * 0.5) * 100) / 100; + const invoices: BillingRow[] = []; + const invoiceCount = 2 + Math.floor(Math.random() * 3); + for (let j = 0; j < invoiceCount; j++) { + const invAmount = Math.round((totalAmount / invoiceCount) * 100) / 100; + const invRecognized = Math.round(invAmount * (0.4 + Math.random() * 0.4) * 100) / 100; + const charges: BillingRow[] = []; + const chargeCount = 1 + Math.floor(Math.random() * 3); + for (let k = 0; k < chargeCount; k++) { + const chargeAmount = Math.round((invAmount / chargeCount) * 100) / 100; + charges.push({ + id: `${i + 1}-inv${j + 1}-chg${k + 1}`, + name: CHARGE_TYPES[k % CHARGE_TYPES.length], + type: "charge", + amount: chargeAmount, + deferredRevenue: Math.round(chargeAmount * 0.3 * 100) / 100, + recognizedRevenue: Math.round(chargeAmount * 0.7 * 100) / 100, + ...generateMonthlyData(), + }); + } + invoices.push({ + id: `${i + 1}-inv${j + 1}`, + name: `${INVOICE_PREFIXES[j % 3]}-${String(i * 10 + j + 1).padStart(4, "0")}`, + type: "invoice", + amount: invAmount, + deferredRevenue: Math.round((invAmount - invRecognized) * 100) / 100, + recognizedRevenue: invRecognized, + charges, + ...generateMonthlyData(), + }); + } + rows.push({ + id: i + 1, + name: accountName, + type: "account", + amount: totalAmount, + deferredRevenue: Math.round((totalAmount - recognized) * 100) / 100, + recognizedRevenue: recognized, + invoices, + ...generateMonthlyData(), + }); + } + return rows; +} + +export const billingData = generateBillingData(30); + +function generateMonthHeaders(): HeaderObject[] { + const headers: HeaderObject[] = []; + const year = 2024; + for (let monthIndex = 11; monthIndex >= 0; monthIndex--) { + const fullMonthName = new Date(year, monthIndex).toLocaleString("default", { month: "long" }); + const mo = months[monthIndex]; + headers.push({ + accessor: `month_${mo}_${year}`, + label: `${fullMonthName} ${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + children: [ + { + disableReorder: true, + label: "Balance", + accessor: `balance_${mo}_${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + disableReorder: true, + label: "Revenue", + accessor: `revenue_${mo}_${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + ], + }); + } + return headers; +} + +export const billingHeaders: HeaderObject[] = [ + { + accessor: "name", + label: "Name", + width: 250, + expandable: true, + isSortable: true, + isEditable: false, + align: "left", + pinned: "left", + type: "string", + }, + { + accessor: "amount", + label: "Total Amount", + width: 130, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + accessor: "deferredRevenue", + label: "Deferred Revenue", + width: 180, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + accessor: "recognizedRevenue", + label: "Recognized Revenue", + width: 180, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + ...generateMonthHeaders(), +]; + +export const billingConfig = { + headers: billingHeaders, + rows: billingData, +} as const; diff --git a/packages/examples/react/src/demos/cell-clicking/CellClickingDemo.tsx b/packages/examples/react/src/demos/cell-clicking/CellClickingDemo.tsx index de3b59206..80f2c7069 100644 --- a/packages/examples/react/src/demos/cell-clicking/CellClickingDemo.tsx +++ b/packages/examples/react/src/demos/cell-clicking/CellClickingDemo.tsx @@ -1,8 +1,8 @@ import { useState, useMemo } from "react"; -import { SimpleTable } from "@simple-table/react"; -import type { Theme, ReactHeaderObject, CellClickProps } from "@simple-table/react"; -import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "@simple-table/examples-shared"; -import type { ProjectTask } from "@simple-table/examples-shared"; +import { SimpleTable, mapToReactHeaderObjects } from "@simple-table/react"; +import type { Theme, ReactHeaderObject, CellRendererProps, CellClickProps } from "@simple-table/react"; +import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "./cell-clicking.demo-data"; +import type { ProjectTask } from "./cell-clicking.demo-data"; import "@simple-table/react/styles.css"; const CellClickingDemo = ({ @@ -18,11 +18,11 @@ const CellClickingDemo = ({ const headers: ReactHeaderObject[] = useMemo( () => - cellClickingHeaders.map((h) => { + mapToReactHeaderObjects(cellClickingHeaders.map((h) => { if (h.accessor === "priority") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const p = String(row.priority); return ( ); }, - } as ReactHeaderObject; + }; } if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const s = String(row.status); const bg = s === "Completed" ? "#dcfce7" : s === "In Progress" ? "#fef3c7" : "#fee2e2"; const color = s === "Completed" ? "#166534" : s === "In Progress" ? "#92400e" : "#991b1b"; @@ -63,7 +63,7 @@ const CellClickingDemo = ({ ); }, - } as ReactHeaderObject; + }; } if (h.accessor === "details") { return { @@ -85,10 +85,10 @@ const CellClickingDemo = ({ View Details ), - } as ReactHeaderObject; + }; } - return { ...h } as ReactHeaderObject; - }), + return h; + })), [], ); diff --git a/packages/examples/react/src/demos/cell-clicking/cell-clicking.demo-data.ts b/packages/examples/react/src/demos/cell-clicking/cell-clicking.demo-data.ts new file mode 100644 index 000000000..57a92ac9a --- /dev/null +++ b/packages/examples/react/src/demos/cell-clicking/cell-clicking.demo-data.ts @@ -0,0 +1,47 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/react"; + + +export type ProjectTask = { + id: number; + task: string; + assignee: string; + priority: string; + status: string; + dueDate: string; + estimatedHours: number; + completedHours: number; + details: string; +}; + +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" }, +]; + +export const cellClickingHeaders: HeaderObject[] = [ + { accessor: "id", label: "Task ID", width: 80, isSortable: true, type: "number" }, + { accessor: "task", label: "Task Name", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "assignee", label: "Assignee", width: 120, isSortable: true, type: "string" }, + { accessor: "priority", label: "Priority", width: 100, isSortable: true, type: "string" }, + { accessor: "status", label: "Status", width: 120, isSortable: true, type: "string" }, + { accessor: "dueDate", label: "Due Date", width: 120, isSortable: true, type: "date" }, + { accessor: "estimatedHours", label: "Est. Hours", width: 100, isSortable: true, type: "number" }, + { accessor: "completedHours", label: "Done Hours", width: 100, isSortable: true, type: "number" }, + { accessor: "details", label: "View Details", width: 120, type: "other" }, +]; + +export const cellClickingConfig = { + headers: cellClickingHeaders, + rows: cellClickingData, +} as const; + +export { STATUSES as CELL_CLICKING_STATUSES }; diff --git a/packages/examples/react/src/demos/cell-editing/CellEditingDemo.tsx b/packages/examples/react/src/demos/cell-editing/CellEditingDemo.tsx index 9896ac127..3c9000d99 100644 --- a/packages/examples/react/src/demos/cell-editing/CellEditingDemo.tsx +++ b/packages/examples/react/src/demos/cell-editing/CellEditingDemo.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, CellChangeProps } from "@simple-table/react"; -import { cellEditingConfig } from "@simple-table/examples-shared"; +import { cellEditingConfig } from "./cell-editing.demo-data"; import "@simple-table/react/styles.css"; const CellEditingDemo = ({ @@ -23,7 +23,7 @@ const CellEditingDemo = ({ return ( { return ( @@ -120,10 +120,11 @@ const VerifiedCell = ({ value }: CellRendererProps) => { }; const TagsCell = ({ value }: CellRendererProps) => { - const tags = Array.isArray(value) ? value : []; + const raw = Array.isArray(value) ? value : []; + const tags = raw.filter((t): t is string => typeof t === "string"); return (
- {tags.map((tag: string) => ( + {tags.map((tag) => ( { const headers: ReactHeaderObject[] = useMemo( () => - cellRendererConfig.headers.map((h) => { - const renderers: Record> = { - teamMembers: TeamCell, - website: WebsiteCell, - status: StatusCell, - progress: ProgressCell, - rating: RatingCell, - verified: VerifiedCell, - tags: TagsCell, - }; - const cellRenderer = renderers[h.accessor as string]; - return cellRenderer ? { ...h, cellRenderer } : { ...h }; - }), + mapToReactHeaderObjects( + cellRendererConfig.headers.map((h) => { + const renderers: Record> = { + teamMembers: TeamCell, + website: WebsiteCell, + status: StatusCell, + progress: ProgressCell, + rating: RatingCell, + verified: VerifiedCell, + tags: TagsCell, + }; + const cellRenderer = renderers[h.accessor as string]; + return cellRenderer ? { ...h, cellRenderer } : h; + }), + ), [], ); diff --git a/packages/examples/react/src/demos/cell-renderer/cell-renderer.demo-data.ts b/packages/examples/react/src/demos/cell-renderer/cell-renderer.demo-data.ts new file mode 100644 index 000000000..875271c26 --- /dev/null +++ b/packages/examples/react/src/demos/cell-renderer/cell-renderer.demo-data.ts @@ -0,0 +1,51 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +export type CellRendererEmployee = { + id: number; + name: string; + website: string; + status: string; + progress: number; + rating: number; + verified: boolean; + tags: string[]; + teamMembers: { name: string; role: string }[]; +}; + +export const cellRendererData: CellRendererEmployee[] = [ + { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] }, + { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] }, + { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] }, + { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] }, + { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] }, + { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] }, + { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] }, + { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] }, + { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] }, + { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] }, + { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 180, type: "string" }, + { accessor: "teamMembers", label: "Team", width: 280, type: "string" }, + { accessor: "website", label: "Website", width: 180, type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, + { accessor: "progress", label: "Progress", width: 150, type: "number" }, + { accessor: "rating", label: "Rating", width: 150, type: "number" }, + { accessor: "verified", label: "Verified", width: 100, type: "boolean" }, + { accessor: "tags", label: "Tags", width: 250, type: "string" }, +]; + +export const cellRendererConfig = { + headers: cellRendererHeaders, + rows: cellRendererData, + tableProps: { + selectableCells: true, + customTheme: { rowHeight: 48 }, + }, +} as const; diff --git a/packages/examples/react/src/demos/charts/ChartsDemo.tsx b/packages/examples/react/src/demos/charts/ChartsDemo.tsx index b38e2ab6e..1541c4f69 100644 --- a/packages/examples/react/src/demos/charts/ChartsDemo.tsx +++ b/packages/examples/react/src/demos/charts/ChartsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { chartsConfig } from "@simple-table/examples-shared"; +import { chartsConfig } from "./charts.demo-data"; import "@simple-table/react/styles.css"; const ChartsDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { @@ -8,7 +8,7 @@ const ChartsDemo = ({ height = "400px", theme }: { height?: string | number; the { + const data: number[] = []; + let current = baseValue; + for (let i = 0; i < length; i++) { + const change = (Math.random() - 0.5) * volatility; + current = Math.max(0, current + change); + data.push(Math.round(current * 100) / 100); + } + return data; +}; + +export const chartsData = [ + { 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 }, + { id: 4, product: 'Monitor 27"', 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 }, + { id: 5, product: "Keyboard Mechanical", category: "Accessories", monthlySales: generateTrendData(200, 40, 12), dailyViews: generateTrendData(600, 120, 30), quarterlyRevenue: [18000, 22000, 25000, 28000], weeklyOrders: [32, 38, 35, 42, 45, 48, 52], rating: 4.8 }, + { id: 6, product: "Webcam HD", category: "Electronics", monthlySales: generateTrendData(120, 30, 12), dailyViews: generateTrendData(450, 90, 30), quarterlyRevenue: [15000, 17000, 16000, 19000], weeklyOrders: [18, 22, 20, 25, 28, 31, 29], rating: 4.3 }, + { id: 7, product: "Headphones Bluetooth", category: "Audio", monthlySales: generateTrendData(250, 60, 12), dailyViews: generateTrendData(900, 180, 30), quarterlyRevenue: [28000, 32000, 35000, 38000], weeklyOrders: [42, 48, 45, 52, 55, 58, 62], rating: 4.4 }, + { id: 8, product: "Phone Case", category: "Accessories", monthlySales: generateTrendData(600, 100, 12), dailyViews: generateTrendData(1500, 250, 30), quarterlyRevenue: [5000, 6000, 7000, 8500], weeklyOrders: [95, 102, 98, 108, 115, 120, 125], rating: 4.1 }, + { id: 9, product: "Smartwatch", 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 }, + { id: 10, product: "Tablet", 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 }, + { id: 11, product: "TV", 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 }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, isSortable: true, type: "number" }, + { accessor: "product", label: "Product", width: 180, isSortable: true, type: "string" }, + { accessor: "category", label: "Category", width: 120, isSortable: true, type: "string" }, + { accessor: "monthlySales", label: "Monthly Sales (12mo)", width: 150, type: "lineAreaChart", tooltip: "Sales trend over the past 12 months", align: "center" }, + { accessor: "dailyViews", label: "Daily Views (30d)", width: 150, type: "lineAreaChart", tooltip: "Daily page views for the past 30 days", align: "center" }, + { accessor: "quarterlyRevenue", label: "Quarterly Revenue", width: 140, type: "barChart", tooltip: "Revenue by quarter", align: "center" }, + { accessor: "weeklyOrders", label: "Weekly Orders", width: 130, type: "barChart", tooltip: "Orders per week over the past 7 weeks", align: "center" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number", align: "center" }, +]; + +export const chartsConfig = { + headers: chartsHeaders, + rows: chartsData, + tableProps: { + columnReordering: true, + columnResizing: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx b/packages/examples/react/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx index 3b0710e6e..35036e670 100644 --- a/packages/examples/react/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx +++ b/packages/examples/react/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { collapsibleColumnsConfig } from "@simple-table/examples-shared"; +import { collapsibleColumnsConfig } from "./collapsible-columns.demo-data"; import "@simple-table/react/styles.css"; const CollapsibleColumnsDemo = ({ @@ -12,7 +12,7 @@ const CollapsibleColumnsDemo = ({ }) => { return ( ({ row }: { row: Record }) => + `$${((row[accessor] as number) || 0).toLocaleString()}`; + +const monthCol = (accessor: string, label: string): HeaderObject => ({ + accessor, + label, + width: 100, + showWhen: "parentExpanded" as const, + isSortable: true, + align: "right", + type: "number", + cellRenderer: fmt(accessor), +}); + +export const collapsibleColumnsHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, isSortable: true }, + { accessor: "name", label: "Sales Rep", minWidth: 150, width: "1fr", isSortable: true }, + { accessor: "region", label: "Region", width: 140, isSortable: true }, + { + accessor: "quarterlySales", + label: "Quarterly Sales", + width: 500, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "totalSales", label: "Total Sales", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("totalSales") }, + { accessor: "q1Sales", label: "Q1", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q1Sales") }, + { accessor: "q2Sales", label: "Q2", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q2Sales") }, + { accessor: "q3Sales", label: "Q3", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q3Sales") }, + { accessor: "q4Sales", label: "Q4", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q4Sales") }, + ], + }, + { + accessor: "monthlyPerformance", + label: "Monthly Performance", + width: 800, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "avgMonthly", label: "Avg Monthly", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("avgMonthly") }, + { accessor: "bestMonth", label: "Best Month", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("bestMonth") }, + 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"), + monthCol("oct", "Oct"), monthCol("nov", "Nov"), monthCol("dec", "Dec"), + ], + }, + { + accessor: "productCategories", + label: "Product Categories", + width: 450, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "topCategory", label: "Top Category", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, type: "string" }, + { accessor: "softwareSales", label: "Software", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("softwareSales") }, + { accessor: "hardwareSales", label: "Hardware", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("hardwareSales") }, + { accessor: "servicesSales", label: "Services", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("servicesSales") }, + ], + }, +]; + +export const collapsibleColumnsConfig = { + headers: collapsibleColumnsHeaders, + rows: collapsibleColumnsData, + tableProps: { + columnResizing: true, + editColumns: true, + selectableCells: true, + columnReordering: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/column-alignment/ColumnAlignmentDemo.tsx b/packages/examples/react/src/demos/column-alignment/ColumnAlignmentDemo.tsx index b71865ec3..339a1a3b2 100644 --- a/packages/examples/react/src/demos/column-alignment/ColumnAlignmentDemo.tsx +++ b/packages/examples/react/src/demos/column-alignment/ColumnAlignmentDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { columnAlignmentConfig } from "@simple-table/examples-shared"; +import { columnAlignmentConfig } from "./column-alignment.demo-data"; import "@simple-table/react/styles.css"; const ColumnAlignmentDemo = ({ @@ -12,7 +12,7 @@ const ColumnAlignmentDemo = ({ }) => { return ( [...columnEditingHeaders, ...additionalColumns], + () => [...defaultHeadersFromCore(columnEditingHeaders), ...additionalColumns], [additionalColumns], ); diff --git a/packages/examples/react/src/demos/column-editing/column-editing.demo-data.ts b/packages/examples/react/src/demos/column-editing/column-editing.demo-data.ts new file mode 100644 index 000000000..d786eca6a --- /dev/null +++ b/packages/examples/react/src/demos/column-editing/column-editing.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +export const columnEditingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, type: "string" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, +]; + +export const columnEditingData = [ + { 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" }, + { id: 4, name: "Luna Martinez", age: 23, role: "Junior Developer", department: "Engineering", email: "luna.martinez@company.com" }, + { id: 5, name: "Tyler Anderson", age: 31, role: "DevOps Engineer", department: "Operations", email: "tyler.anderson@company.com" }, + { id: 6, name: "Zara Kim", age: 28, role: "Product Designer", department: "Design", email: "zara.kim@company.com" }, + { id: 7, name: "Kai Thompson", age: 26, role: "Full Stack Developer", department: "Engineering", email: "kai.thompson@company.com" }, + { id: 8, name: "Ava Singh", age: 33, role: "Product Manager", department: "Product", email: "ava.singh@company.com" }, + { id: 9, name: "Jordan Walsh", age: 25, role: "Marketing Specialist", department: "Growth", email: "jordan.walsh@company.com" }, + { id: 10, name: "Phoenix Lee", age: 30, role: "Backend Developer", department: "Engineering", email: "phoenix.lee@company.com" }, + { id: 11, name: "River Jackson", age: 24, role: "Growth Designer", department: "Design", email: "river.jackson@company.com" }, + { id: 12, name: "Atlas Morgan", age: 32, role: "Tech Lead", department: "Engineering", email: "atlas.morgan@company.com" }, +]; + +export const columnEditingConfig = { + headers: columnEditingHeaders, + rows: columnEditingData, + tableProps: { enableHeaderEditing: true, selectableColumns: true }, +} as const; diff --git a/packages/examples/react/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx b/packages/examples/react/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx index dfcd24ce5..34b404f54 100644 --- a/packages/examples/react/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx +++ b/packages/examples/react/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, ReactColumnEditorConfig, ColumnEditorRowRendererProps } from "@simple-table/react"; -import { columnEditorCustomRendererConfig } from "@simple-table/examples-shared"; +import { columnEditorCustomRendererConfig } from "./column-editor-custom-renderer.demo-data"; import "@simple-table/react/styles.css"; const CustomRowRenderer = ({ header, components }: ColumnEditorRowRendererProps) => ( @@ -46,7 +46,7 @@ const ColumnEditorCustomRendererDemo = ({ }) => { return ( `$${(value as number).toLocaleString()}`, + }, + { accessor: "department", label: "Department", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const columnEditorCustomRendererConfig = { + headers: columnEditorCustomRendererHeaders, + rows: columnEditorCustomRendererData, + tableProps: { + editColumns: true, + }, +} as const; + +export const COLUMN_EDITOR_TEXT = "Manage Columns"; +export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…"; + +export function buildVanillaColumnEditorRowRenderer(props: ColumnEditorRowRendererProps): HTMLElement { + const row = document.createElement("div"); + Object.assign(row.style, { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "6px 8px", + borderRadius: "6px", + background: "#f8fafc", + marginBottom: "4px", + }); + + if (props.components.checkbox) { + const span = document.createElement("span"); + if (typeof props.components.checkbox === "string") { + span.innerHTML = props.components.checkbox; + } else { + span.appendChild(props.components.checkbox as Node); + } + row.appendChild(span); + } + + const label = document.createElement("span"); + Object.assign(label.style, { flex: "1", fontSize: "13px", fontWeight: "500" }); + label.textContent = props.header.label; + row.appendChild(label); + + if (props.components.dragIcon) { + const span = document.createElement("span"); + Object.assign(span.style, { cursor: "grab", opacity: "0.5" }); + if (typeof props.components.dragIcon === "string") { + span.innerHTML = props.components.dragIcon; + } else { + span.appendChild(props.components.dragIcon as Node); + } + row.appendChild(span); + } + + return row; +} diff --git a/packages/examples/react/src/demos/column-filtering/ColumnFilteringDemo.tsx b/packages/examples/react/src/demos/column-filtering/ColumnFilteringDemo.tsx index 9557a57ce..b10de7051 100644 --- a/packages/examples/react/src/demos/column-filtering/ColumnFilteringDemo.tsx +++ b/packages/examples/react/src/demos/column-filtering/ColumnFilteringDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { columnFilteringConfig } from "@simple-table/examples-shared"; +import { columnFilteringConfig } from "./column-filtering.demo-data"; import "@simple-table/react/styles.css"; const ColumnFilteringDemo = ({ @@ -12,7 +12,7 @@ const ColumnFilteringDemo = ({ }) => { return ( { + const salary = row.salary as number; + return `$${salary.toLocaleString()}`; + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 130, + type: "date", + isSortable: true, + filterable: true, + }, + { + accessor: "isActive", + label: "Active", + width: 100, + align: "center", + type: "boolean", + isSortable: true, + filterable: true, + }, +]; + +export const columnFilteringConfig = { + headers: columnFilteringHeaders, + rows: COLUMN_FILTERING_DATA, +} as const; diff --git a/packages/examples/react/src/demos/column-pinning/ColumnPinningDemo.tsx b/packages/examples/react/src/demos/column-pinning/ColumnPinningDemo.tsx index 3ce1f64ff..d3a987578 100644 --- a/packages/examples/react/src/demos/column-pinning/ColumnPinningDemo.tsx +++ b/packages/examples/react/src/demos/column-pinning/ColumnPinningDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { columnPinningConfig } from "@simple-table/examples-shared"; +import { columnPinningConfig } from "./column-pinning.demo-data"; import "@simple-table/react/styles.css"; const ColumnPinningDemo = ({ @@ -12,7 +12,7 @@ const ColumnPinningDemo = ({ }) => { return ( { - const [headers, setHeaders] = useState([...columnReorderingConfig.headers]); + const [headers, setHeaders] = useState(() => + defaultHeadersFromCore([...columnReorderingConfig.headers]), + ); const handleColumnOrderChange = (newHeaders: HeaderObject[]) => { - setHeaders(newHeaders); + setHeaders(defaultHeadersFromCore(newHeaders)); }; return ( diff --git a/packages/examples/react/src/demos/column-reordering/column-reordering.demo-data.ts b/packages/examples/react/src/demos/column-reordering/column-reordering.demo-data.ts new file mode 100644 index 000000000..0e0691208 --- /dev/null +++ b/packages/examples/react/src/demos/column-reordering/column-reordering.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +export const columnReorderingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", minWidth: 100, width: "1fr", type: "string" }, + { accessor: "department", disableReorder: true, label: "Department", width: "1fr", type: "string" }, +]; + +export const columnReorderingData = [ + { 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" }, + { id: 4, name: "Cadet Orbit Williams", age: 26, role: "Flight Engineer", department: "Engineering" }, + { id: 5, name: "Dr. Galaxy Chen", age: 31, role: "Life Support Specialist", department: "Engineering" }, + { id: 6, name: "Lt. Meteor Lee", age: 29, role: "Navigation Officer", department: "Flight Operations" }, + { id: 7, name: "Dr. Comet Hassan", age: 33, role: "Mission Planner", department: "Planning" }, + { id: 8, name: "Major Pulsar White", age: 36, role: "Communications Director", department: "Communications" }, + { id: 9, name: "Specialist Quasar Black", age: 28, role: "Systems Analyst", department: "Technology" }, + { id: 10, name: "Engineer Supernova Blue", age: 35, role: "Propulsion Engineer", department: "Engineering" }, + { id: 11, name: "Dr. Aurora Kumar", age: 30, role: "Planetary Geologist", department: "Science" }, + { id: 12, name: "Admiral Cosmos Silver", age: 45, role: "Program Director", department: "Leadership" }, +]; + +export const columnReorderingConfig = { + headers: columnReorderingHeaders, + rows: columnReorderingData, + tableProps: { columnReordering: true }, +} as const; diff --git a/packages/examples/react/src/demos/column-resizing/ColumnResizingDemo.tsx b/packages/examples/react/src/demos/column-resizing/ColumnResizingDemo.tsx index 1b87eaa22..fd7f58dae 100644 --- a/packages/examples/react/src/demos/column-resizing/ColumnResizingDemo.tsx +++ b/packages/examples/react/src/demos/column-resizing/ColumnResizingDemo.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, HeaderObject } from "@simple-table/react"; -import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "@simple-table/examples-shared"; +import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "./column-resizing.demo-data"; import "@simple-table/react/styles.css"; const ColumnResizingDemo = ({ @@ -11,7 +11,7 @@ const ColumnResizingDemo = ({ height?: string | number; theme?: Theme; }) => { - const [headers, setHeaders] = useState(columnResizingHeaders); + const [headers, setHeaders] = useState(() => defaultHeadersFromCore(columnResizingHeaders)); const [saveMessage, setSaveMessage] = useState(""); useEffect(() => { @@ -20,10 +20,12 @@ const ColumnResizingDemo = ({ if (saved) { const widthMap = JSON.parse(saved); setHeaders( - columnResizingHeaders.map((h) => ({ - ...h, - width: widthMap[h.accessor] ?? h.width, - })), + defaultHeadersFromCore( + columnResizingHeaders.map((h) => ({ + ...h, + width: widthMap[h.accessor] ?? h.width, + })), + ), ); } } catch { @@ -41,7 +43,7 @@ const ColumnResizingDemo = ({ {} as Record, ); localStorage.setItem(COLUMN_RESIZING_STORAGE_KEY, JSON.stringify(widthMap)); - setHeaders(updatedHeaders); + setHeaders(defaultHeadersFromCore(updatedHeaders)); setSaveMessage("Column widths saved!"); setTimeout(() => setSaveMessage(""), 2000); } catch { diff --git a/packages/examples/react/src/demos/column-resizing/column-resizing.demo-data.ts b/packages/examples/react/src/demos/column-resizing/column-resizing.demo-data.ts new file mode 100644 index 000000000..d57c4ab51 --- /dev/null +++ b/packages/examples/react/src/demos/column-resizing/column-resizing.demo-data.ts @@ -0,0 +1,34 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +export const COLUMN_RESIZING_STORAGE_KEY = "columnResizingDemo_widths"; + +export const columnResizingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, align: "right", type: "number" }, + { accessor: "department", label: "Department", width: "1fr", minWidth: 100, type: "string" }, + { accessor: "startDate", label: "Start Date", width: 150, type: "date" }, +]; + +export const columnResizingData = [ + { id: 1, name: "Dr. Marina Silva", age: 38, role: "Marine Biologist", department: "Research", startDate: "2019-03-15" }, + { id: 2, name: "Captain Alex Torres", age: 45, role: "Research Vessel Captain", department: "Operations", startDate: "2017-08-20" }, + { id: 3, name: "Dr. Coral Chen", age: 34, role: "Oceanographer", department: "Research", startDate: "2020-01-12" }, + { id: 4, name: "Finn O'Brien", age: 27, role: "Research Assistant", department: "Research", startDate: "2022-06-08" }, + { id: 5, name: "Reef Nakamura", age: 31, role: "Dive Safety Officer", department: "Safety", startDate: "2021-02-14" }, + { id: 6, name: "Tide Rodriguez", age: 29, role: "Equipment Specialist", department: "Technical", startDate: "2021-09-03" }, + { id: 7, name: "Dr. Ocean Williams", age: 42, role: "Research Director", department: "Leadership", startDate: "2016-05-10" }, + { id: 8, name: "Wave Petrov", age: 26, role: "Data Analyst", department: "Analysis", startDate: "2022-11-22" }, + { id: 9, name: "Pearl Kim", age: 33, role: "Laboratory Manager", department: "Laboratory", startDate: "2020-07-18" }, + { id: 10, name: "Current Hassan", age: 28, role: "Field Coordinator", department: "Operations", startDate: "2021-12-05" }, + { id: 11, name: "Abyss Thompson", age: 30, role: "ROV Operator", department: "Technical", startDate: "2021-04-20" }, + { id: 12, name: "Dr. Depth Martinez", age: 39, role: "Senior Researcher", department: "Research", startDate: "2018-10-14" }, +]; + +export const columnResizingConfig = { + headers: columnResizingHeaders, + rows: columnResizingData, +} as const; diff --git a/packages/examples/react/src/demos/column-selection/ColumnSelectionDemo.tsx b/packages/examples/react/src/demos/column-selection/ColumnSelectionDemo.tsx index 7c5539fa6..082bd1416 100644 --- a/packages/examples/react/src/demos/column-selection/ColumnSelectionDemo.tsx +++ b/packages/examples/react/src/demos/column-selection/ColumnSelectionDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { columnSelectionConfig } from "@simple-table/examples-shared"; +import { columnSelectionConfig } from "./column-selection.demo-data"; import "@simple-table/react/styles.css"; const ColumnSelectionDemo = ({ @@ -12,7 +12,7 @@ const ColumnSelectionDemo = ({ }) => { return ( { return ( { + return (value as string).charAt(0).toUpperCase() + (value as string).slice(1); + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 140, + isSortable: true, + type: "date", + valueFormatter: ({ value }) => { + if (typeof value === "string") { + return new Date(value).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + return String(value); + }, + }, +]; + +export const columnSortingConfig = { + headers: columnSortingHeaders, + rows: COLUMN_SORTING_DATA, + tableProps: { + initialSortColumn: "age", + initialSortDirection: "desc" as const, + }, +} as const; diff --git a/packages/examples/react/src/demos/column-visibility/ColumnVisibilityDemo.tsx b/packages/examples/react/src/demos/column-visibility/ColumnVisibilityDemo.tsx index 482e19e63..70001cb3c 100644 --- a/packages/examples/react/src/demos/column-visibility/ColumnVisibilityDemo.tsx +++ b/packages/examples/react/src/demos/column-visibility/ColumnVisibilityDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { columnVisibilityConfig } from "@simple-table/examples-shared"; +import { columnVisibilityConfig } from "./column-visibility.demo-data"; import "@simple-table/react/styles.css"; const ColumnVisibilityDemo = ({ @@ -12,7 +12,7 @@ const ColumnVisibilityDemo = ({ }) => { return ( new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const columnVisibilityConfig = { + headers: columnVisibilityHeaders, + rows: columnVisibilityData, + tableProps: { + editColumns: true, + columnEditorConfig: { + text: "Manage Columns", + searchEnabled: true, + searchPlaceholder: "Search columns…", + }, + }, +} as const; diff --git a/packages/examples/react/src/demos/column-width/ColumnWidthDemo.tsx b/packages/examples/react/src/demos/column-width/ColumnWidthDemo.tsx index 17bb2ae03..45cf3d13b 100644 --- a/packages/examples/react/src/demos/column-width/ColumnWidthDemo.tsx +++ b/packages/examples/react/src/demos/column-width/ColumnWidthDemo.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { columnWidthConfig } from "@simple-table/examples-shared"; +import { columnWidthConfig } from "./column-width.demo-data"; import "@simple-table/react/styles.css"; const ColumnWidthDemo = ({ @@ -24,7 +24,7 @@ const ColumnWidthDemo = ({ { const [isLoading, setIsLoading] = useState(false); @@ -67,7 +67,7 @@ function getCRMHeaders(isDark: boolean): ReactHeaderObject[] { return [ { accessor: "name", label: "CONTACT", width: "2fr", minWidth: 290, isSortable: true, isEditable: true, type: "string", - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const { name, title, company } = r as unknown as CRMLead; const initials = name.split(" ").map((n) => n[0]).join("").toUpperCase(); return ( @@ -84,7 +84,7 @@ function getCRMHeaders(isDark: boolean): ReactHeaderObject[] { }, { accessor: "signal", label: "SIGNAL", width: "3fr", minWidth: 340, isSortable: true, isEditable: true, type: "string", - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const { signal } = r as unknown as CRMLead; return (
@@ -96,7 +96,7 @@ function getCRMHeaders(isDark: boolean): ReactHeaderObject[] { }, { accessor: "aiScore", label: "AI SCORE", width: "1fr", minWidth: 100, isSortable: true, align: "center", type: "number", - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const { aiScore } = r as unknown as CRMLead; return
{"🔥".repeat(aiScore)}
; }, @@ -108,7 +108,7 @@ function getCRMHeaders(isDark: boolean): ReactHeaderObject[] { }, { accessor: "timeAgo", label: "IMPORT", width: "1fr", minWidth: 100, isSortable: true, align: "center", type: "string", - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const { timeAgo } = r as unknown as CRMLead; return
{timeAgo}
; }, @@ -117,7 +117,7 @@ function getCRMHeaders(isDark: boolean): ReactHeaderObject[] { accessor: "list", label: "LIST", width: "1.2fr", minWidth: 160, isSortable: 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 m: Record = { "Hot Leads": 1, "Warm Leads": 2, Enterprise: 3, Leads: 4, SMB: 5, "Cold Leads": 6, Nurture: 7 }; return m[String(row.list)] || 999; }, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const { list } = r as unknown as CRMLead; return e.preventDefault()} style={{ cursor: "pointer", fontSize: "0.875rem", color: colors.link, textDecoration: "none", fontWeight: "600" }}>{list}; }, @@ -127,7 +127,7 @@ function getCRMHeaders(isDark: boolean): ReactHeaderObject[] { ]; } -const CRMDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { +const CRMDemo = ({ height = "400px", theme }: { height?: string | number; theme?: CrmShellTheme }) => { const isDark = theme === "custom-dark" || theme === "dark" || theme === "modern-dark"; const [data, setData] = useState([...crmData]); const [rowsPerPage, setRowsPerPage] = useState(100); diff --git a/packages/examples/react/src/demos/crm/crm-custom-theme.css b/packages/examples/react/src/demos/crm/crm-custom-theme.css new file mode 100644 index 000000000..8ed3366f4 --- /dev/null +++ b/packages/examples/react/src/demos/crm/crm-custom-theme.css @@ -0,0 +1,161 @@ +@import url("https://fonts.googleapis.com/css2?family=BBH+Sans+Hegarty&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"); + +.custom-theme-container .st-wrapper { + border-radius: 0; +} +.custom-theme-container .st-header-label-text { + font-size: 12px; +} + +.custom-theme-container * { + font-family: Plus Jakarta Sans, sans-serif; + font-optical-sizing: auto; + font-style: normal; +} +.custom-theme-container .st-header-resize-handle { + height: 100%; + width: 4px; +} +.custom-theme-container.theme-custom-light { + --st-border-radius: 4px; + --st-cell-padding: 12px; + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + --st-scrollbar-bg-color: #f1f5f9; + --st-scrollbar-thumb-color: #cbd5e1; + --st-border-color: none; + --st-odd-row-background-color: white; + --st-even-row-background-color: white; + --st-odd-column-background-color: white; + --st-even-column-background-color: oklch(98.5% 0.002 247.839); + --st-hover-row-background-color: oklch(96.7% 0.003 264.542); + --st-selected-row-background-color: oklch(96.7% 0.003 264.542); + --st-header-background-color: rgb(249 250 251); + --st-header-label-color: oklch(55.1% 0.027 264.364); + --st-header-icon-color: oklch(55.1% 0.027 264.364); + --st-dragging-background-color: oklch(96.7% 0.003 264.542); + --st-selected-cell-background-color: oklch(96.7% 0.003 264.542); + --st-selected-first-cell-background-color: oklch(92.8% 0.006 264.531); + --st-footer-background-color: oklch(98.5% 0.002 247.839); + --st-cell-color: oklch(37.3% 0.034 259.733); + --st-cell-odd-row-color: oklch(37.3% 0.034 259.733); + --st-edit-cell-shadow: 0 4px 6px -1px rgba(106, 114, 130, 0.1), + 0 2px 4px -1px rgba(106, 114, 130, 0.06); + --st-selected-cell-color: oklch(27.8% 0.033 256.848); + --st-selected-first-cell-color: oklch(27.8% 0.033 256.848); + --st-resize-handle-color: oklch(96.7% 0.003 264.542); + --st-separator-border-color: oklch(92.8% 0.006 264.531); + --st-last-group-row-separator-border-color: oklch(87.2% 0.01 258.338); + --st-selected-border-color: #6a7282; + --st-editable-cell-focus-border-color: #6a7282; + --st-checkbox-checked-background-color: #6a7282; + --st-checkbox-checked-border-color: #6a7282; + --st-column-editor-background-color: white; + --st-column-editor-popout-background-color: white; + --st-button-hover-background-color: oklch(96.7% 0.003 264.542); + --st-button-active-background-color: #6a7282; + --st-cell-flash-color: oklch(92.8% 0.006 264.531); + --st-copy-flash-color: #6a7282; + --st-warning-flash-color: oklch(80.8% 0.114 19.571); + --st-header-selected-background-color: #6a7282; + --st-header-selected-label-color: #6a7282; + --st-header-selected-icon-color: #6a7282; + --st-header-highlight-indicator-color: oklch(87.2% 0.01 258.338); + --st-selection-highlight-indicator-color: oklch(87.2% 0.01 258.338); + --st-drag-separator-color: #6a7282; + --st-next-prev-btn-color: oklch(44.6% 0.03 256.802); + --st-next-prev-btn-disabled-color: oklch(70.7% 0.022 261.325); + --st-page-btn-color: oklch(44.6% 0.03 256.802); + --st-page-btn-hover-background-color: oklch(96.7% 0.003 264.542); + --st-column-editor-text-color: oklch(55.1% 0.027 264.364); + --st-checkbox-border-color: oklch(87.2% 0.01 258.338); + --st-dropdown-group-label-color: oklch(55.1% 0.027 264.364); + --st-datepicker-weekday-color: oklch(55.1% 0.027 264.364); + --st-datepicker-other-month-color: oklch(70.7% 0.022 261.325); + --st-filter-button-disabled-background-color: oklch(87.2% 0.01 258.338); + --st-filter-button-disabled-text-color: oklch(55.1% 0.027 264.364); +} + +.custom-theme-container.theme-custom-light .st-wrapper { + background-color: white; +} + +.custom-theme-container.theme-custom-light .st-header-resize-handle { + background-color: oklch(96.7% 0.003 264.542); +} + +.custom-theme-container.theme-custom-dark { + --st-border-radius: 4px; + --st-cell-padding: 12px; + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + --st-scrollbar-bg-color: #1e293b; + --st-scrollbar-thumb-color: #475569; + --st-border-color: none; + --st-odd-row-background-color: #0f172a; + --st-even-row-background-color: #0f172a; + --st-odd-column-background-color: #0f172a; + --st-even-column-background-color: #1e293b; + --st-hover-row-background-color: #1e293b; + --st-selected-row-background-color: #1e293b; + --st-header-background-color: #0a0f1a; + --st-header-label-color: #94a3b8; + --st-header-icon-color: #94a3b8; + --st-dragging-background-color: #1e293b; + --st-selected-cell-background-color: #1e293b; + --st-selected-first-cell-background-color: #334155; + --st-footer-background-color: #0f172a; + --st-cell-color: #cbd5e1; + --st-cell-odd-row-color: #cbd5e1; + --st-edit-cell-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + --st-selected-cell-color: #e2e8f0; + --st-selected-first-cell-color: #e2e8f0; + --st-resize-handle-color: #1e293b; + --st-separator-border-color: #334155; + --st-last-group-row-separator-border-color: #475569; + --st-selected-border-color: #94a3b8; + --st-editable-cell-focus-border-color: #94a3b8; + --st-checkbox-checked-background-color: #94a3b8; + --st-checkbox-checked-border-color: #94a3b8; + --st-column-editor-background-color: #1e293b; + --st-column-editor-popout-background-color: #1e293b; + --st-button-hover-background-color: #334155; + --st-button-active-background-color: #94a3b8; + --st-cell-flash-color: #334155; + --st-copy-flash-color: #94a3b8; + --st-warning-flash-color: #ef4444; + --st-header-selected-background-color: #94a3b8; + --st-header-selected-label-color: #94a3b8; + --st-header-selected-icon-color: #94a3b8; + --st-header-highlight-indicator-color: #475569; + --st-selection-highlight-indicator-color: #475569; + --st-drag-separator-color: #94a3b8; + --st-next-prev-btn-color: #cbd5e1; + --st-next-prev-btn-disabled-color: #64748b; + --st-page-btn-color: #cbd5e1; + --st-page-btn-hover-background-color: #334155; + --st-column-editor-text-color: #94a3b8; + --st-checkbox-border-color: #475569; + --st-dropdown-group-label-color: #94a3b8; + --st-datepicker-weekday-color: #94a3b8; + --st-datepicker-other-month-color: #64748b; + --st-filter-button-disabled-background-color: #334155; + --st-filter-button-disabled-text-color: #64748b; +} + +.custom-theme-container.theme-custom-dark .st-wrapper { + background-color: #0f172a; +} + +.custom-theme-container.theme-custom-dark .st-header-resize-handle { + background-color: #475569; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/examples/react/src/demos/crm/crm.demo-data.ts b/packages/examples/react/src/demos/crm/crm.demo-data.ts new file mode 100644 index 000000000..b848fb9aa --- /dev/null +++ b/packages/examples/react/src/demos/crm/crm.demo-data.ts @@ -0,0 +1,177 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Theme } from "@simple-table/react"; + +export type CrmShellTheme = Theme | "custom-light" | "custom-dark"; + +export type CRMLead = { + id: number; + name: string; + title: string; + company: string; + linkedin: boolean; + signal: string; + aiScore: number; + emailStatus: string; + timeAgo: string; + list: string; +}; + + +const FIRST_NAMES = ["Emma", "Liam", "Sophia", "Noah", "Olivia", "James", "Ava", "William", "Isabella", "Oliver", "Mia", "Benjamin", "Charlotte", "Elijah", "Amelia", "Lucas", "Harper", "Mason", "Evelyn", "Logan"]; +const LAST_NAMES = ["Chen", "Rodriguez", "Kim", "Thompson", "Martinez", "Anderson", "Taylor", "Brown", "Wilson", "Johnson", "Lee", "Garcia", "Davis", "Miller", "Moore", "Jackson", "White", "Harris", "Martin", "Clark"]; +const TITLES = ["VP of Engineering", "Head of Marketing", "CTO", "Product Manager", "Director of Sales", "CEO", "CFO", "Head of Operations", "Engineering Manager", "Growth Lead", "CMO", "Head of Product", "Director of Engineering", "VP of Sales", "Head of Design"]; +const COMPANIES = ["TechCorp", "InnovateLabs", "CloudBase", "DataFlow", "NexGen", "Quantum AI", "CyberPulse", "MetaVision", "ByteForge", "CodeStream", "PixelPerfect", "LogicGate", "CircuitMind", "NetSphere", "DigiCore"]; +const SIGNALS = ["cloud infrastructure", "enterprise SaaS", "AI/ML tools", "developer platform", "security solutions", "data analytics", "API management", "DevOps automation", "microservices", "serverless computing"]; +const LISTS = ["Hot Leads", "Warm Leads", "Cold Leads", "Enterprise", "SMB", "Leads", "Nurture"]; +const TIME_AGOS = ["2 min ago", "5 min ago", "15 min ago", "1 hour ago", "3 hours ago", "6 hours ago", "1 day ago", "2 days ago", "3 days ago", "1 week ago"]; + +export function generateCRMData(count: number = 100): CRMLead[] { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + name: `${FIRST_NAMES[i % FIRST_NAMES.length]} ${LAST_NAMES[i % LAST_NAMES.length]}`, + title: TITLES[i % TITLES.length], + company: COMPANIES[i % COMPANIES.length], + linkedin: i % 3 !== 0, + signal: SIGNALS[i % SIGNALS.length], + aiScore: Math.min(5, Math.max(1, Math.floor(Math.random() * 5) + 1)), + emailStatus: ["Enrich", "Verified", "Pending", "Bounced"][i % 4], + timeAgo: TIME_AGOS[i % TIME_AGOS.length], + list: LISTS[i % LISTS.length], + })); +} + +export const crmData = generateCRMData(100); + +export const crmHeaders: HeaderObject[] = [ + { + accessor: "name", + label: "CONTACT", + width: "2fr", + minWidth: 290, + isSortable: true, + isEditable: true, + type: "string", + }, + { + accessor: "signal", + label: "SIGNAL", + width: "3fr", + minWidth: 340, + isSortable: true, + isEditable: true, + type: "string", + }, + { + accessor: "aiScore", + label: "AI SCORE", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "number", + }, + { + accessor: "emailStatus", + label: "EMAIL", + width: "1.5fr", + minWidth: 210, + isSortable: true, + align: "center", + type: "enum", + enumOptions: [ + { label: "Enrich", value: "Enrich" }, + { label: "Verified", value: "Verified" }, + { label: "Pending", value: "Pending" }, + { label: "Bounced", value: "Bounced" }, + ], + }, + { + accessor: "timeAgo", + label: "IMPORT", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "string", + }, + { + accessor: "list", + label: "LIST", + width: "1.2fr", + minWidth: 160, + isSortable: 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 priorityMap: Record = { + "Hot Leads": 1, "Warm Leads": 2, Enterprise: 3, Leads: 4, SMB: 5, "Cold Leads": 6, Nurture: 7, + }; + return priorityMap[String(row.list)] || 999; + }, + }, + { + accessor: "_fit", + label: "Fit", + width: "1fr", + align: "center", + minWidth: 120, + }, + { + accessor: "_contactNow", + label: "", + width: "1.2fr", + minWidth: 160, + }, +]; + +export const CRM_FOOTER_COLORS_LIGHT = { + bg: "white", border: "#e5e7eb", text: "#374151", textBold: "#374151", + inputBg: "white", inputBorder: "#d1d5db", buttonBg: "white", buttonBorder: "#d1d5db", + buttonText: "#6b7280", activeBg: "#fff7ed", activeText: "#ea580c", +}; + +export const CRM_FOOTER_COLORS_DARK = { + bg: "#0f172a", border: "#334155", text: "#cbd5e1", textBold: "#e2e8f0", + inputBg: "#1e293b", inputBorder: "#475569", buttonBg: "#1e293b", buttonBorder: "#475569", + buttonText: "#cbd5e1", activeBg: "#334155", activeText: "#ea580c", +}; + +export function generateVisiblePages(currentPage: number, totalPages: number): number[] { + const maxVisible = 5; + if (totalPages <= maxVisible) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + let start = currentPage - 2; + let end = currentPage + 2; + if (start < 1) { start = 1; end = Math.min(maxVisible, totalPages); } + if (end > totalPages) { end = totalPages; start = Math.max(1, totalPages - maxVisible + 1); } + return Array.from({ length: end - start + 1 }, (_, i) => start + i); +} + +export const CRM_THEME_COLORS_LIGHT = { + text: "oklch(21% .034 264.665)", textSecondary: "oklch(44.6% .03 256.802)", + textTertiary: "oklch(55.1% .027 264.364)", link: "oklch(64.6% .222 41.116)", + accent: "#ea580c", bg: "white", tagBg: "oklch(96.7% .003 264.542)", + tagText: "oklch(21% .034 264.665)", buttonBg: "oklch(92.8% .006 264.531)", + buttonText: "oklch(70.7% .022 261.325)", buttonHoverBg: "oklch(87.2% .01 258.338)", +}; + +export const CRM_THEME_COLORS_DARK = { + text: "#cbd5e1", textSecondary: "#94a3b8", textTertiary: "#64748b", + link: "#60a5fa", accent: "#ea580c", bg: "#0f172a", tagBg: "#1e293b", + tagText: "#cbd5e1", buttonBg: "#1e293b", buttonText: "#cbd5e1", buttonHoverBg: "#334155", +}; + +export const crmConfig = { + headers: crmHeaders, + rows: crmData, +} as const; diff --git a/packages/examples/react/src/demos/csv-export/CsvExportDemo.tsx b/packages/examples/react/src/demos/csv-export/CsvExportDemo.tsx index e1a877713..f7ba8d270 100644 --- a/packages/examples/react/src/demos/csv-export/CsvExportDemo.tsx +++ b/packages/examples/react/src/demos/csv-export/CsvExportDemo.tsx @@ -1,7 +1,7 @@ import { useRef, useMemo } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, mapToReactHeaderObjects } from "@simple-table/react"; import type { Theme, TableAPI, ReactHeaderObject } from "@simple-table/react"; -import { csvExportHeaders, csvExportData, csvExportConfig } from "@simple-table/examples-shared"; +import { csvExportHeaders, csvExportData, csvExportConfig } from "./csv-export.demo-data"; import "@simple-table/react/styles.css"; const CsvExportDemo = ({ @@ -15,7 +15,7 @@ const CsvExportDemo = ({ const headers: ReactHeaderObject[] = useMemo( () => - csvExportHeaders.map((h) => { + mapToReactHeaderObjects(csvExportHeaders.map((h) => { if (h.accessor === "actions") { return { ...h, @@ -35,10 +35,10 @@ const CsvExportDemo = ({ View ), - } as ReactHeaderObject; + }; } - return { ...h } as ReactHeaderObject; - }), + return h; + })), [], ); @@ -51,7 +51,10 @@ const CsvExportDemo = ({ if (!api) return; const rows = api.getAllRows(); const hdrs = api.getHeaders(); - const totalRevenue = rows.reduce((sum, r) => sum + (Number(r.revenue) || 0), 0); + const totalRevenue = rows.reduce((sum, r) => { + const row = r as Record; + return sum + (Number(row.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 })}`, ); diff --git a/packages/examples/react/src/demos/csv-export/csv-export.demo-data.ts b/packages/examples/react/src/demos/csv-export/csv-export.demo-data.ts new file mode 100644 index 000000000..d2bb1cc11 --- /dev/null +++ b/packages/examples/react/src/demos/csv-export/csv-export.demo-data.ts @@ -0,0 +1,76 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +const CATEGORY_CODES: Record = { + electronics: "ELEC", + furniture: "FURN", + stationery: "STAT", + appliances: "APPL", +}; + +export const csvExportData = [ + { id: "db-1001", sku: "PRD-1001", product: "Wireless Keyboard", category: "Electronics", price: 49.99, stock: 145, sold: 234, revenue: 11697.66, actions: "" }, + { id: "db-1002", sku: "PRD-1002", product: "Ergonomic Mouse", category: "Electronics", price: 29.99, stock: 89, sold: 456, revenue: 13675.44, actions: "" }, + { id: "db-1003", sku: "PRD-1003", product: "USB-C Hub", category: "Electronics", price: 39.99, stock: 234, sold: 178, revenue: 7118.22, actions: "" }, + { id: "db-2001", sku: "PRD-2001", product: "Standing Desk", category: "Furniture", price: 399.99, stock: 23, sold: 67, revenue: 26799.33, actions: "" }, + { id: "db-2002", sku: "PRD-2002", product: "Office Chair", category: "Furniture", price: 249.99, stock: 56, sold: 123, revenue: 30748.77, actions: "" }, + { id: "db-2003", sku: "PRD-2003", product: "Monitor Stand", category: "Furniture", price: 79.99, stock: 167, sold: 89, revenue: 7119.11, actions: "" }, + { id: "db-3001", sku: "PRD-3001", product: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, sold: 678, revenue: 8807.22, actions: "" }, + { id: "db-3002", sku: "PRD-3002", product: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, sold: 534, revenue: 10674.66, actions: "" }, + { id: "db-3003", sku: "PRD-3003", product: "Desk Organizer", category: "Stationery", price: 24.99, stock: 198, sold: 289, revenue: 7222.11, actions: "" }, + { id: "db-4001", sku: "PRD-4001", product: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, sold: 156, revenue: 14038.44, actions: "" }, + { id: "db-4002", sku: "PRD-4002", product: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, sold: 267, revenue: 9342.33, actions: "" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Internal ID", width: 80, type: "string", excludeFromRender: true }, + { accessor: "sku", label: "SKU", width: 100, isSortable: true, type: "string" }, + { accessor: "product", label: "Product Name", minWidth: 120, width: "1fr", isSortable: true, type: "string" }, + { + accessor: "category", + label: "Category", + width: 130, + isSortable: true, + type: "string", + valueFormatter: ({ value }) => { + const s = String(value); + return s.charAt(0).toUpperCase() + s.slice(1); + }, + exportValueGetter: ({ value }) => { + const code = CATEGORY_CODES[String(value).toLowerCase()] ?? String(value).toUpperCase(); + return `${value} (${code})`; + }, + }, + { + accessor: "price", + label: "Price", + width: 100, + isSortable: true, + type: "number", + valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}`, + useFormattedValueForCSV: true, + useFormattedValueForClipboard: true, + }, + { accessor: "stock", label: "In Stock", width: 100, isSortable: true, type: "number" }, + { accessor: "sold", label: "Units Sold", width: 110, isSortable: true, type: "number" }, + { + accessor: "revenue", + label: "Revenue", + width: 120, + isSortable: true, + type: "number", + valueFormatter: ({ value }) => + `$${(value as number).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + useFormattedValueForCSV: true, + useFormattedValueForClipboard: true, + }, + { accessor: "actions", label: "Actions", width: 100, type: "string", excludeFromCsv: true }, +]; + +export const csvExportConfig = { + headers: csvExportHeaders, + rows: csvExportData, + tableProps: { editColumns: true, selectableCells: true, customTheme: { rowHeight: 32 } }, +} as const; diff --git a/packages/examples/react/src/demos/custom-icons/CustomIconsDemo.tsx b/packages/examples/react/src/demos/custom-icons/CustomIconsDemo.tsx index 20139f7f9..3cfd7a57a 100644 --- a/packages/examples/react/src/demos/custom-icons/CustomIconsDemo.tsx +++ b/packages/examples/react/src/demos/custom-icons/CustomIconsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, ReactIconsConfig } from "@simple-table/react"; -import { customIconsConfig } from "@simple-table/examples-shared"; +import { customIconsConfig } from "./custom-icons.demo-data"; import "@simple-table/react/styles.css"; const customIcons: ReactIconsConfig = { @@ -45,7 +45,7 @@ const CustomIconsDemo = ({ }) => { return ( (value as number).toLocaleString(), + }, + { + accessor: "date", + label: "Date", + width: 130, + type: "date", + isSortable: true, + valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const customIconsConfig = { + headers: customIconsHeaders, + rows: customIconsData, +} as const; + +export function createSvgIcon(pathD: string, color = "#3b82f6", size = 14): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", String(size)); + svg.setAttribute("height", String(size)); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", color); + svg.setAttribute("stroke-width", "2.5"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathD); + svg.appendChild(path); + return svg; +} + +export const ICON_PATHS = { + sortUp: "M12 19V5M5 12l7-7 7 7", + sortDown: "M12 5v14M19 12l-7 7-7-7", + filter: "M3 4h18l-7 8.5V18l-4 2V12.5L3 4z", + expand: "M9 5l7 7-7 7", + next: "M9 5l7 7-7 7", + prev: "M15 19l-7-7 7-7", +} as const; + +export function buildVanillaCustomIcons() { + return { + sortUp: createSvgIcon(ICON_PATHS.sortUp, "#6366f1"), + sortDown: createSvgIcon(ICON_PATHS.sortDown, "#6366f1"), + filter: createSvgIcon(ICON_PATHS.filter, "#8b5cf6"), + expand: createSvgIcon(ICON_PATHS.expand, "#6366f1"), + next: createSvgIcon(ICON_PATHS.next, "#2563eb"), + prev: createSvgIcon(ICON_PATHS.prev, "#2563eb"), + }; +} diff --git a/packages/examples/react/src/demos/custom-theme/CustomThemeDemo.tsx b/packages/examples/react/src/demos/custom-theme/CustomThemeDemo.tsx index 197fae72d..fa03cd209 100644 --- a/packages/examples/react/src/demos/custom-theme/CustomThemeDemo.tsx +++ b/packages/examples/react/src/demos/custom-theme/CustomThemeDemo.tsx @@ -1,8 +1,8 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { customThemeConfig } from "@simple-table/examples-shared"; +import { customThemeConfig } from "./custom-theme.demo-data"; import "@simple-table/react/styles.css"; -import "../../../../shared/src/styles/custom-theme.css"; +import "./custom-theme.css"; const CustomThemeDemo = ({ height = "400px", @@ -13,7 +13,7 @@ const CustomThemeDemo = ({ }) => { return ( formatPhone(value as string), + }, + { accessor: "email", label: "Email", width: 180, type: "string" }, + { accessor: "city", label: "City", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const customThemeConfig = { + headers: customThemeHeaders, + rows: customThemeData, + tableProps: { + theme: "custom" as const, + customTheme: { + rowHeight: 40, + headerHeight: 44, + }, + }, +} as const; diff --git a/packages/examples/react/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx b/packages/examples/react/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx index 1ca6f6326..afd3a4edd 100644 --- a/packages/examples/react/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx +++ b/packages/examples/react/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx @@ -1,12 +1,12 @@ import { useState, useCallback } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, OnRowGroupExpandProps } from "@simple-table/react"; import { dynamicNestedTablesConfig, dynamicNestedTablesData, fetchDivisionsForCompany, -} from "@simple-table/examples-shared"; -import type { DynamicCompany } from "@simple-table/examples-shared"; +} from "./dynamic-nested-tables.demo-data"; +import type { DynamicCompany } from "./dynamic-nested-tables.demo-data"; import "@simple-table/react/styles.css"; const DynamicNestedTablesDemo = ({ height = "500px", theme }: { height?: string | number; theme?: Theme }) => { @@ -43,7 +43,7 @@ const DynamicNestedTablesDemo = ({ height = "500px", theme }: { height?: string return ( new Promise((resolve) => setTimeout(resolve, ms)); + +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"]; + + return Array.from({ length: divisionCount }, (_, i) => ({ + id: `${companyId}-div-${i}`, + divisionName: divisionNames[i % divisionNames.length], + revenue: `$${Math.floor(Math.random() * 50) + 10}M`, + profitMargin: `${Math.floor(Math.random() * 30) + 10}%`, + headcount: Math.floor(Math.random() * 400) + 50, + location: locations[i % locations.length], + })); +}; + +export const dynamicNestedTablesData: DynamicCompany[] = [ + { id: "comp-1", companyName: "TechCorp Global", industry: "Technology", revenue: "$250M", employees: 1200 }, + { id: "comp-2", companyName: "FinanceHub Inc", industry: "Financial Services", revenue: "$180M", employees: 850 }, + { id: "comp-3", companyName: "HealthTech Solutions", industry: "Healthcare", revenue: "$320M", employees: 1500 }, + { id: "comp-4", companyName: "RetailMax Corporation", industry: "Retail", revenue: "$420M", employees: 2100 }, + { id: "comp-5", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-6", companyName: "MediaVision Studios", industry: "Entertainment", revenue: "$290M", employees: 950 }, + { id: "comp-7", companyName: "AutoDrive Industries", industry: "Automotive", revenue: "$680M", employees: 3200 }, + { id: "comp-8", companyName: "CloudNet Services", industry: "Technology", revenue: "$195M", employees: 720 }, + { id: "comp-9", companyName: "HealthCare Solutions", industry: "Healthcare", revenue: "$380M", employees: 1300 }, + { id: "comp-10", companyName: "EducationTech Innovations", industry: "Education", revenue: "$240M", employees: 1050 }, + { id: "comp-11", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-12", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-13", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-14", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-15", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, +]; + +export const dynamicNestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionName", label: "Division", width: 200 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: 180 }, +]; + +export const dynamicNestedTablesCompanyHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { + defaultHeaders: dynamicNestedTablesDivisionHeaders, + expandAll: false, + autoExpandColumns: true, + }, + }, + { accessor: "industry", label: "Industry", width: 150 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const dynamicNestedTablesConfig = { + headers: dynamicNestedTablesCompanyHeaders, + rows: dynamicNestedTablesData, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + autoExpandColumns: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx b/packages/examples/react/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx index 971f9f94e..b2808772a 100644 --- a/packages/examples/react/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx +++ b/packages/examples/react/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx @@ -1,13 +1,13 @@ import { useState, useCallback } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, OnRowGroupExpandProps } from "@simple-table/react"; import { dynamicRowLoadingConfig, generateInitialRegions, fetchStoresForRegion, fetchProductsForStore, -} from "@simple-table/examples-shared"; -import type { DynamicRegion, DynamicStore } from "@simple-table/examples-shared"; +} from "./dynamic-row-loading.demo-data"; +import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data"; import "@simple-table/react/styles.css"; const DynamicRowLoadingDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { @@ -56,7 +56,7 @@ const DynamicRowLoadingDemo = ({ height = "400px", theme }: { height?: string | return ( typeof value !== "number" ? "—" : value.toLocaleString(), + }, + { + accessor: "totalRevenue", label: "Revenue", width: 140, type: "number", align: "right", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + }, + { + accessor: "activeStores", label: "Stores", width: 100, type: "number", align: "right", + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(), + }, + { accessor: "avgRating", label: "Avg Rating", width: 120, type: "string", align: "center" }, + { accessor: "lastUpdate", label: "Last Updated", width: 130, type: "date" }, +]; + +const REGION_NAMES = [ + "North America - East", "North America - West", "Europe - North", "Europe - South", + "Asia Pacific - East", "Asia Pacific - Southeast", "Middle East", + "Latin America - North", "Latin America - South", "Africa - North", "Africa - South", "Oceania", +]; + +const STORE_NAMES = [ + "Manhattan Flagship", "Brooklyn Heights", "Boston Downtown", "Miami Beach", + "Los Angeles Beverly Hills", "San Francisco Union Square", "Seattle Downtown", "Portland Pearl District", + "London Oxford Street", "Stockholm Gamla Stan", "Copenhagen Strøget", "Amsterdam Central", + "Paris Champs-Élysées", "Madrid Gran Vía", "Rome Via del Corso", "Barcelona La Rambla", + "Tokyo Shibuya", "Shanghai Nanjing Road", "Hong Kong Central", "Seoul Gangnam", + "Singapore Orchard", "Bangkok Siam", "Kuala Lumpur Bukit Bintang", "Jakarta Grand Indonesia", + "Dubai Mall", "Abu Dhabi Marina", "Riyadh Kingdom Centre", + "Mexico City Reforma", "Monterrey Valle", "Guadalajara Centro", + "São Paulo Paulista", "Buenos Aires Palermo", "Santiago Providencia", + "Cairo City Stars", "Casablanca Morocco Mall", "Tunis Centre Urbain", + "Johannesburg Sandton", "Cape Town V&A Waterfront", + "Sydney Pitt Street", "Melbourne Bourke Street", "Auckland Queen Street", +]; + +const PRODUCT_NAMES = [ + "Wireless Headphones Pro", "Smart Watch Elite", "USB-C Hub Deluxe", "Mechanical Keyboard RGB", + "Ergonomic Mouse", "Webcam 4K", "Portable SSD 2TB", "Wireless Charger Pad", + "Phone Stand Aluminum", "Bluetooth Speaker Mini", "Laptop Stand Pro", "Cable Organizer Set", + "Gaming Mouse Elite", "Noise Cancelling Headset", "RGB Desk Mat XL", "Wireless Presenter", + "Document Camera", "Smart Pen Digital", "Monitor Arm Dual", "Docking Station Pro", + "Microphone USB Studio", "Tablet Stand Adjustable", "HDMI Switch 4K", "Laptop Cooling Pad", + "Blue Light Blocking Glasses", "Anti-Glare Screen Protector", "Laptop Privacy Filter", + "Wireless Charging Pad Trio", "MagSafe Car Mount", "Charging Cable Braided 10ft", + "Ergonomic Vertical Mouse", "Trackball Mouse Wireless", "Gaming Mouse Pad XXL", + "Keyboard Wrist Rest", "Monitor Privacy Filter", "Laptop Sleeve Premium", + "Desktop Mic Arm", "Cable Management Box", "USB Hub 7-Port", "Ergonomic Chair Cushion", + "Footrest Adjustable", "Desk Lamp LED Smart", "Portable Monitor 15.6", "Screen Cleaning Kit", + "Desk Organizer Bamboo", "Wireless Trackpad", "Numeric Keypad Wireless", + "Presentation Clicker", "Gaming Controller Pro", "Racing Wheel Set", +]; + +const seededRandom = (seed: string) => { + let hash = 0; + for (let i = 0; i < seed.length; i++) { + hash = (hash << 5) - hash + seed.charCodeAt(i); + hash = hash & hash; + } + const x = Math.sin(hash) * 10000; + return x - Math.floor(x); +}; + +const getRandomInt = (seed: string, min: number, max: number) => + Math.floor(seededRandom(seed) * (max - min + 1)) + min; + +const getRandomRating = (seed: string) => (4.0 + seededRandom(seed + "rating") * 1.0).toFixed(1); + +const getRandomDate = (seed: string) => { + const daysAgo = getRandomInt(seed + "date", 0, 5); + const date = new Date(); + date.setDate(date.getDate() - daysAgo); + return date.toISOString().split("T")[0]; +}; + +const generateStoresForRegion = (regionId: string): DynamicStore[] => { + const regionIndex = parseInt(regionId.split("-")[1]); + const numStores = getRandomInt(regionId, 3, 4); + const startIndex = (regionIndex - 1) * 3; + return Array.from({ length: numStores }, (_, i) => { + const storeId = `STORE-${regionIndex}${String(i + 1).padStart(2, "0")}`; + const storeIndex = startIndex + i; + const totalSales = getRandomInt(storeId, 10000, 25000); + const avgPrice = getRandomInt(storeId + "price", 25, 35); + return { + id: storeId, + name: STORE_NAMES[storeIndex % STORE_NAMES.length], + type: "store" as const, + totalSales, + totalRevenue: totalSales * avgPrice, + avgRating: getRandomRating(storeId), + lastUpdate: getRandomDate(storeId), + }; + }); +}; + +const generateProductsForStore = (storeId: string): DynamicProduct[] => { + const numProducts = getRandomInt(storeId, 3, 5); + const storeNumber = parseInt(storeId.split("-")[1]); + const startIndex = storeNumber * 3; + return Array.from({ length: numProducts }, (_, i) => { + const productId = `PROD-${storeId.split("-")[1]}-${i + 1}`; + const totalSales = getRandomInt(productId, 2000, 8000); + const avgPrice = getRandomInt(productId + "price", 20, 40); + return { + id: productId, + name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length], + type: "product" as const, + totalSales, + totalRevenue: totalSales * avgPrice, + avgRating: getRandomRating(productId), + lastUpdate: getRandomDate(productId), + }; + }); +}; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const fetchStoresForRegion = async (regionId: string): Promise => { + await delay(1500); + return generateStoresForRegion(regionId); +}; + +export const fetchProductsForStore = async (storeId: string): Promise => { + await delay(1000); + return generateProductsForStore(storeId); +}; + +export const generateInitialRegions = (): DynamicRegion[] => { + return REGION_NAMES.map((name, index) => { + const regionId = `REG-${index + 1}`; + const stores = generateStoresForRegion(regionId); + const totalSales = stores.reduce((sum, s) => sum + s.totalSales, 0); + const totalRevenue = stores.reduce((sum, s) => sum + s.totalRevenue, 0); + const avgRating = (stores.reduce((sum, s) => sum + parseFloat(s.avgRating), 0) / stores.length).toFixed(1); + return { + id: regionId, + name, + type: "region" as const, + totalSales, + totalRevenue, + activeStores: getRandomInt(regionId, 3, 4), + avgRating, + lastUpdate: getRandomDate(regionId), + }; + }); +}; + +export const dynamicRowLoadingConfig = { + headers: dynamicRowLoadingHeaders, + tableProps: { + rowGrouping: ["stores", "products"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + columnResizing: true, + selectableCells: true, + useOddEvenRowBackground: true, + editColumns: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/empty-state/EmptyStateDemo.tsx b/packages/examples/react/src/demos/empty-state/EmptyStateDemo.tsx index a36261e40..eb17477f2 100644 --- a/packages/examples/react/src/demos/empty-state/EmptyStateDemo.tsx +++ b/packages/examples/react/src/demos/empty-state/EmptyStateDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { emptyStateConfig } from "@simple-table/examples-shared"; +import { emptyStateConfig } from "./empty-state.demo-data"; import "@simple-table/react/styles.css"; const EmptyIcon = () => ( @@ -38,7 +38,7 @@ const EmptyStateDemo = ({ }) => { return ( Number(filter.value); + case "lessThan": + return Number(value) < Number(filter.value); + case "greaterThanOrEqual": + return Number(value) >= Number(filter.value); + case "lessThanOrEqual": + return Number(value) <= Number(filter.value); + case "between": + return ( + filter.values != null && + Number(value) >= Number(filter.values[0]) && + Number(value) <= Number(filter.values[1]) + ); + case "in": + return filter.values != null && filter.values.includes(value); + case "notIn": + return filter.values != null && !filter.values.includes(value); + case "isEmpty": + return value == null || value === ""; + case "isNotEmpty": + return value != null && value !== ""; + default: + return true; + } +} + +const DEPARTMENT_OPTIONS = [ + { label: "AI Research", value: "AI Research" }, + { label: "UX Design", value: "UX Design" }, + { label: "DevOps", value: "DevOps" }, + { label: "Marketing", value: "Marketing" }, + { label: "Engineering", value: "Engineering" }, + { label: "Product", value: "Product" }, + { label: "Sales", value: "Sales" }, +]; + +const LOCATION_OPTIONS = [ + { label: "San Francisco", value: "San Francisco" }, + { label: "Tokyo", value: "Tokyo" }, + { label: "Lagos", value: "Lagos" }, + { label: "Mexico City", value: "Mexico City" }, + { label: "Kolkata", value: "Kolkata" }, + { label: "Stockholm", value: "Stockholm" }, + { label: "Dubai", value: "Dubai" }, + { label: "Milan", value: "Milan" }, + { label: "Seoul", value: "Seoul" }, + { label: "Austin", value: "Austin" }, + { label: "London", value: "London" }, + { label: "Moscow", value: "Moscow" }, +]; + +export const externalFilterData = [ + { 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" }, + { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing", active: true, location: "Mexico City" }, + { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering", active: true, location: "Kolkata" }, + { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product", active: false, location: "Stockholm" }, + { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales", active: true, location: "Dubai" }, + { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing", active: true, location: "Milan" }, + { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research", active: true, location: "Seoul" }, + { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering", active: false, location: "Austin" }, + { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design", active: true, location: "London" }, + { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" }, +]; + +export const externalFilterHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" }, + { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" }, + { + accessor: "department", + label: "Department", + width: 150, + filterable: true, + type: "enum", + enumOptions: DEPARTMENT_OPTIONS, + }, + { + accessor: "location", + label: "Location", + width: 150, + filterable: true, + type: "enum", + enumOptions: LOCATION_OPTIONS, + }, + { accessor: "active", label: "Active", width: 120, filterable: true, type: "boolean" }, + { + accessor: "salary", + label: "Salary", + width: 120, + filterable: true, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, +]; + +export const externalFilterConfig = { + headers: externalFilterHeaders, + rows: externalFilterData, + tableProps: { externalFilterHandling: true, columnResizing: true }, +} as const; diff --git a/packages/examples/react/src/demos/external-sort/ExternalSortDemo.tsx b/packages/examples/react/src/demos/external-sort/ExternalSortDemo.tsx index c1eb87544..bc1803628 100644 --- a/packages/examples/react/src/demos/external-sort/ExternalSortDemo.tsx +++ b/packages/examples/react/src/demos/external-sort/ExternalSortDemo.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, SortColumn } from "@simple-table/react"; -import { externalSortConfig } from "@simple-table/examples-shared"; +import { externalSortConfig } from "./external-sort.demo-data"; import "@simple-table/react/styles.css"; const ExternalSortDemo = ({ @@ -31,7 +31,7 @@ const ExternalSortDemo = ({ return ( `$${(value as number).toLocaleString()}`, + }, +]; + +export const externalSortConfig = { + headers: externalSortHeaders, + rows: externalSortData, + tableProps: { externalSortHandling: true, columnResizing: true }, +} as const; diff --git a/packages/examples/react/src/demos/footer-renderer/FooterRendererDemo.tsx b/packages/examples/react/src/demos/footer-renderer/FooterRendererDemo.tsx index 03d84cc41..cbcea85fc 100644 --- a/packages/examples/react/src/demos/footer-renderer/FooterRendererDemo.tsx +++ b/packages/examples/react/src/demos/footer-renderer/FooterRendererDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, FooterRendererProps } from "@simple-table/react"; -import { footerRendererConfig } from "@simple-table/examples-shared"; +import { footerRendererConfig } from "./footer-renderer.demo-data"; import "@simple-table/react/styles.css"; function getFooterColors(theme?: Theme) { @@ -45,7 +45,7 @@ const FooterRendererDemo = ({ return ( - headerRendererConfig.headers.map((h) => ({ + mapToReactHeaderObjects(headerRendererConfig.headers.map((h) => ({ ...h, isSortable: false, headerRenderer: ({ accessor }: HeaderRendererProps) => { @@ -74,7 +74,7 @@ const HeaderRendererDemo = ({
); }, - })), + }))), [sortState], ); diff --git a/packages/examples/react/src/demos/header-renderer/header-renderer.demo-data.ts b/packages/examples/react/src/demos/header-renderer/header-renderer.demo-data.ts new file mode 100644 index 000000000..372550744 --- /dev/null +++ b/packages/examples/react/src/demos/header-renderer/header-renderer.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/react"; + + +export const headerRendererData: Row[] = [ + { 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" }, + { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering" }, + { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics" }, + { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering" }, + { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design" }, + { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" }, +]; + +export const headerRendererHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number", isSortable: true }, + { accessor: "name", label: "Employee Name", width: 180, type: "string", isSortable: true }, + { accessor: "email", label: "Email Address", width: 200, type: "string" }, + { accessor: "role", label: "Job Role", width: 130, type: "string", isSortable: true }, + { accessor: "salary", label: "Annual Salary", width: 140, type: "number", isSortable: true }, + { accessor: "department", label: "Department", width: 150, type: "string", isSortable: true }, +]; + +export const headerRendererConfig = { + headers: headerRendererHeaders, + rows: headerRendererData, + tableProps: { + selectableCells: true, + columnResizing: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/hr/HRDemo.tsx b/packages/examples/react/src/demos/hr/HRDemo.tsx index 341ccf7dc..33b9ed7f4 100644 --- a/packages/examples/react/src/demos/hr/HRDemo.tsx +++ b/packages/examples/react/src/demos/hr/HRDemo.tsx @@ -1,16 +1,17 @@ -import { useState } from "react"; -import { SimpleTable } from "@simple-table/react"; -import type { Theme, ReactHeaderObject, CellChangeProps } from "@simple-table/react"; -import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "@simple-table/examples-shared"; -import type { HREmployee } from "@simple-table/examples-shared"; +import { useMemo, useState } from "react"; +import { SimpleTable, mapToReactHeaderObjects } from "@simple-table/react"; +import type { Theme, CellChangeProps, CellRendererProps, ReactHeaderObject } from "@simple-table/react"; +import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "./hr.demo-data"; +import type { HREmployee } from "./hr.demo-data"; import "@simple-table/react/styles.css"; function getHeaders(): ReactHeaderObject[] { - return hrConfig.headers.map((h) => { + return mapToReactHeaderObjects( + hrConfig.headers.map((h) => { if (h.accessor === "fullName") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as HREmployee; const c = getHRThemeColors(theme); const initials = `${d.firstName.charAt(0)}${d.lastName.charAt(0)}`; @@ -43,7 +44,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "performanceScore") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as HREmployee; const c = getHRThemeColors(theme); const color = @@ -85,7 +86,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "hireDate") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as HREmployee; if (!d.hireDate) return ""; const [year, month, day] = d.hireDate.split("-").map(Number); @@ -106,7 +107,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "yearsOfService") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as HREmployee; const c = getHRThemeColors(theme); return {`${d.yearsOfService} yrs`}; @@ -116,7 +117,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "salary") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as HREmployee; const c = getHRThemeColors(theme); return {`$${d.salary.toLocaleString()}`}; @@ -126,7 +127,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as HREmployee; if (!d.status) return ""; const c = getHRThemeColors(theme); @@ -151,7 +152,8 @@ function getHeaders(): ReactHeaderObject[] { }; } return h; - }); + }), + ); } const HRDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { @@ -166,11 +168,13 @@ const HRDemo = ({ height = "400px", theme }: { height?: string | number; theme?: ); }; + const headers = useMemo(() => getHeaders(), []); + return ( { + const firstName = HR_FIRST_NAMES[i % HR_FIRST_NAMES.length]; + const lastName = HR_LAST_NAMES[i % HR_LAST_NAMES.length]; + const yearsOfService = Math.floor(Math.random() * 15); + const hireYear = 2024 - yearsOfService; + const hireMonth = String(1 + Math.floor(Math.random() * 12)).padStart(2, "0"); + const hireDay = String(1 + Math.floor(Math.random() * 28)).padStart(2, "0"); + return { + id: i + 1, + firstName, + lastName, + fullName: `${firstName} ${lastName}`, + position: POSITIONS[i % POSITIONS.length], + performanceScore: Math.floor(40 + Math.random() * 60), + department: DEPARTMENTS[i % DEPARTMENTS.length], + email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@company.com`, + location: LOCATIONS[i % LOCATIONS.length], + hireDate: `${hireYear}-${hireMonth}-${hireDay}`, + yearsOfService, + salary: Math.floor(50000 + Math.random() * 150000), + status: HR_STATUSES[i % 10 === 9 ? 4 : i % 7 === 0 ? 1 : i % 11 === 0 ? 2 : i % 13 === 0 ? 3 : 0], + isRemoteEligible: Math.random() > 0.3, + }; + }); +} + +export const hrData = generateHRData(100); + +export const hrHeaders: HeaderObject[] = [ + { accessor: "fullName", label: "Employee", width: 220, isSortable: true, isEditable: false, align: "left", pinned: "left", type: "string" }, + { + accessor: "performanceScore", label: "Performance", width: 160, isSortable: true, isEditable: true, align: "center", type: "number", + valueFormatter: ({ value }) => `${value}/100`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${value}%`, + }, + { + accessor: "department", label: "Department", width: 150, isSortable: true, isEditable: true, align: "left", type: "enum", + enumOptions: [ + { label: "Engineering", value: "Engineering" }, { label: "Marketing", value: "Marketing" }, + { label: "Sales", value: "Sales" }, { label: "Finance", value: "Finance" }, + { label: "HR", value: "HR" }, { label: "Operations", value: "Operations" }, + { label: "Customer Support", value: "Customer Support" }, + ], + }, + { accessor: "email", label: "Email", width: 280, isSortable: true, isEditable: true, align: "left", type: "string" }, + { + accessor: "location", label: "Location", width: 130, isSortable: true, isEditable: true, align: "left", type: "enum", + enumOptions: LOCATIONS.map((l) => ({ label: l, value: l })), + }, + { accessor: "hireDate", label: "Hire Date", width: 120, isSortable: true, isEditable: true, align: "left", type: "date" }, + { accessor: "yearsOfService", label: "Service", width: 100, isSortable: true, isEditable: false, align: "center", type: "number" }, + { accessor: "salary", label: "Salary", width: 130, isSortable: true, isEditable: true, align: "right", type: "number", valueFormatter: ({ value }) => { if (typeof value !== "number") return ""; return `$${value.toLocaleString()}`; }, useFormattedValueForClipboard: true, useFormattedValueForCSV: true }, + { + accessor: "status", label: "Status", width: 120, isSortable: true, isEditable: true, align: "center", pinned: "right", type: "enum", + enumOptions: [ + { label: "Active", value: "Active" }, { label: "On Leave", value: "On Leave" }, + { label: "Probation", value: "Probation" }, { label: "Contract", value: "Contract" }, + { label: "Terminated", value: "Terminated" }, + ], + valueGetter: ({ row }) => { + const priorityMap: Record = { Terminated: 1, Probation: 2, Contract: 3, "On Leave": 4, Active: 5 }; + return priorityMap[String(row.status)] || 999; + }, + }, + { accessor: "isRemoteEligible", label: "Remote Eligible", width: 140, isSortable: true, isEditable: true, align: "center", type: "boolean" }, +]; + +export function getHRThemeColors(theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const tagColors: Record = isDark + ? { green: { bg: "#065f46", text: "#86efac" }, orange: { bg: "#9a3412", text: "#fed7aa" }, blue: { bg: "#1e3a8a", text: "#93c5fd" }, purple: { bg: "#581c87", text: "#c4b5fd" }, red: { bg: "#991b1b", text: "#fca5a5" }, default: { bg: "#374151", text: "#e5e7eb" } } + : { green: { bg: "#f6ffed", text: "#2a6a0d" }, orange: { bg: "#fff7e6", text: "#ad4e00" }, blue: { bg: "#e6f7ff", text: "#0050b3" }, purple: { bg: "#f9f0ff", text: "#391085" }, red: { bg: "#fff1f0", text: "#a8071a" }, default: { bg: "#f0f0f0", text: "rgba(0, 0, 0, 0.85)" } }; + return { + gray: isDark ? "#f3f4f6" : "#1f2937", + grayMuted: isDark ? "#f3f4f6" : "#6b7280", + avatarBg: isDark ? "#3b82f6" : "#1890ff", + avatarText: "#ffffff", + progressSuccess: isDark ? "#34d399" : "#52c41a", + progressNormal: isDark ? "#60a5fa" : "#1890ff", + progressException: isDark ? "#f87171" : "#ff4d4f", + progressBg: isDark ? "#374151" : "#f5f5f5", + progressText: isDark ? "#d1d5db" : "rgba(0, 0, 0, 0.65)", + tagColors, + }; +} + +export type HRTagColorKey = "green" | "orange" | "blue" | "purple" | "red" | "default"; + +export const HR_STATUS_COLOR_MAP: Record = { + Active: "green", "On Leave": "orange", Probation: "blue", Contract: "purple", Terminated: "red", +}; + +export const hrConfig = { + headers: hrHeaders, + rows: hrData, +} as const; diff --git a/packages/examples/react/src/demos/infinite-scroll/InfiniteScrollDemo.tsx b/packages/examples/react/src/demos/infinite-scroll/InfiniteScrollDemo.tsx index c80faf09d..db31aaf32 100644 --- a/packages/examples/react/src/demos/infinite-scroll/InfiniteScrollDemo.tsx +++ b/packages/examples/react/src/demos/infinite-scroll/InfiniteScrollDemo.tsx @@ -1,10 +1,10 @@ import { useState, useCallback } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, Row } from "@simple-table/react"; import { infiniteScrollConfig, generateInfiniteScrollData, -} from "@simple-table/examples-shared"; +} from "./infinite-scroll.demo-data"; import "@simple-table/react/styles.css"; const MAX_ROWS = 200; @@ -46,7 +46,7 @@ const InfiniteScrollDemo = ({ {rows.length} rows loaded{hasMore ? "" : " (all loaded)"}
`$${(value as number).toLocaleString()}`, + }, +]; + +export const infiniteScrollConfig = { + headers: infiniteScrollHeaders, + rows: generateInfiniteScrollData(0, 30), +} as const; diff --git a/packages/examples/react/src/demos/infrastructure/InfrastructureDemo.tsx b/packages/examples/react/src/demos/infrastructure/InfrastructureDemo.tsx index ca0ab353e..c244b0eaa 100644 --- a/packages/examples/react/src/demos/infrastructure/InfrastructureDemo.tsx +++ b/packages/examples/react/src/demos/infrastructure/InfrastructureDemo.tsx @@ -1,14 +1,14 @@ import { useRef, useEffect, useState } from "react"; import { SimpleTable } from "@simple-table/react"; -import type { Theme, TableAPI, ReactHeaderObject } from "@simple-table/react"; -import { infrastructureData, INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors } from "@simple-table/examples-shared"; -import type { InfrastructureServer } from "@simple-table/examples-shared"; +import type { Theme, TableAPI, ReactHeaderObject, CellRendererProps } from "@simple-table/react"; +import { infrastructureData, INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors } from "./infrastructure.demo-data"; +import type { InfrastructureServer } from "./infrastructure.demo-data"; import "@simple-table/react/styles.css"; function getHeaders(currentTheme?: Theme): ReactHeaderObject[] { const t = currentTheme || "light"; return [ - { accessor: "serverId", align: "left", filterable: true, isEditable: false, isSortable: true, label: "Server ID", minWidth: 180, pinned: "left", type: "string", width: "1.2fr", cellRenderer: ({ row: r }) => { const { serverId } = r as unknown as InfrastructureServer; return {serverId}; } }, + { accessor: "serverId", align: "left", filterable: true, isEditable: false, isSortable: true, label: "Server ID", minWidth: 180, pinned: "left", type: "string", width: "1.2fr", cellRenderer: ({ row: r }: CellRendererProps) => { const { serverId } = r as unknown as InfrastructureServer; return {serverId}; } }, { accessor: "serverName", align: "left", filterable: true, isEditable: false, isSortable: true, label: "Name", minWidth: 200, type: "string", width: "1.5fr" }, { accessor: "performance", label: "Performance Metrics", width: 690, isSortable: false, @@ -16,16 +16,16 @@ function getHeaders(currentTheme?: Theme): ReactHeaderObject[] { { accessor: "cpuHistory", label: "CPU History", width: 150, isSortable: false, filterable: false, isEditable: false, align: "center", type: "lineAreaChart", tooltip: "CPU usage over the last 30 intervals" }, { accessor: "cpuUsage", label: "CPU %", width: 120, isSortable: true, filterable: true, isEditable: true, align: "right", type: "number", - cellRenderer: ({ row: r, theme }) => { const { cpuUsage } = r as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(cpuUsage, theme || t, "cpu"); return
{cpuUsage.toFixed(1)}%
; }, + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const { cpuUsage } = r as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(cpuUsage, theme || t, "cpu"); return
{cpuUsage.toFixed(1)}%
; }, }, { accessor: "memoryUsage", label: "Memory %", width: 130, isSortable: true, filterable: true, isEditable: true, align: "right", type: "number", - cellRenderer: ({ row: r, theme }) => { const { memoryUsage } = r as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(memoryUsage, theme || t, "memory"); return
{memoryUsage.toFixed(1)}%
; }, + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const { memoryUsage } = r as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(memoryUsage, theme || t, "memory"); return
{memoryUsage.toFixed(1)}%
; }, }, - { accessor: "diskUsage", label: "Disk %", width: 120, isSortable: true, filterable: true, isEditable: true, align: "right", type: "number", cellRenderer: ({ row: r }) => { const { diskUsage } = r as unknown as InfrastructureServer; return `${diskUsage.toFixed(1)}%`; } }, + { accessor: "diskUsage", label: "Disk %", width: 120, isSortable: true, filterable: true, isEditable: true, align: "right", type: "number", cellRenderer: ({ row: r }: CellRendererProps) => { const { diskUsage } = r as unknown as InfrastructureServer; return `${diskUsage.toFixed(1)}%`; } }, { accessor: "responseTime", label: "Response (ms)", width: 120, isSortable: true, filterable: true, isEditable: true, align: "right", type: "number", - cellRenderer: ({ row: r, theme }) => { const { responseTime } = r as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(responseTime, theme || t, "response"); return {responseTime.toFixed(1)}; }, + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const { responseTime } = r as unknown as InfrastructureServer; const s = getInfraMetricColorStyles(responseTime, theme || t, "response"); return {responseTime.toFixed(1)}; }, }, ], }, @@ -33,7 +33,7 @@ function getHeaders(currentTheme?: Theme): ReactHeaderObject[] { accessor: "status", label: "Status", width: 130, isSortable: true, filterable: true, isEditable: 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 m: Record = { critical: 1, offline: 2, warning: 3, maintenance: 4, online: 5 }; return m[String(row.status)] || 999; }, - cellRenderer: ({ row: r, theme }) => { const { status } = r as unknown as InfrastructureServer; const s = getInfraStatusColors(status, theme || t); return
{status.charAt(0).toUpperCase() + status.slice(1)}
; }, + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const { status } = r as unknown as InfrastructureServer; const s = getInfraStatusColors(status, theme || t); return
{status.charAt(0).toUpperCase() + status.slice(1)}
; }, }, ]; } diff --git a/packages/examples/react/src/demos/infrastructure/infrastructure.demo-data.ts b/packages/examples/react/src/demos/infrastructure/infrastructure.demo-data.ts new file mode 100644 index 000000000..a8ca5b108 --- /dev/null +++ b/packages/examples/react/src/demos/infrastructure/infrastructure.demo-data.ts @@ -0,0 +1,263 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/react"; + +export interface InfrastructureServer { + id: number; + serverId: string; + serverName: string; + cpuUsage: number; + cpuHistory: number[]; + memoryUsage: number; + diskUsage: number; + responseTime: number; + networkIn: number; + networkOut: number; + activeConnections: number; + requestsPerSec: number; + status: "online" | "warning" | "critical" | "maintenance" | "offline"; +} + + +const SERVER_PREFIXES = [ + "web", + "api", + "db", + "cache", + "worker", + "proxy", + "auth", + "search", + "queue", + "storage", +]; +const SERVER_NAMES = [ + "Production Primary", + "Production Replica", + "Staging", + "Development", + "Analytics", + "Load Balancer", + "CDN Edge", + "Backup Primary", + "Monitoring", + "Gateway", +]; +const STATUSES: Array = [ + "online", + "warning", + "critical", + "maintenance", + "offline", +]; + +export function generateInfrastructureData(count: number = 50): Row[] { + return Array.from({ length: count }, (_, i) => { + const prefix = SERVER_PREFIXES[i % SERVER_PREFIXES.length]; + const num = String(i + 1).padStart(3, "0"); + const cpu = Math.round((20 + Math.random() * 70) * 10) / 10; + return { + id: i + 1, + serverId: `${prefix}-${num}`, + serverName: SERVER_NAMES[i % SERVER_NAMES.length], + cpuUsage: cpu, + cpuHistory: Array.from({ length: 30 }, () => Math.round((20 + Math.random() * 70) * 10) / 10), + memoryUsage: Math.round((30 + Math.random() * 60) * 10) / 10, + diskUsage: Math.round((10 + Math.random() * 80) * 10) / 10, + responseTime: Math.round((20 + Math.random() * 400) * 10) / 10, + networkIn: Math.round(Math.random() * 1000 * 100) / 100, + networkOut: Math.round(Math.random() * 600 * 100) / 100, + activeConnections: Math.floor(Math.random() * 5000), + requestsPerSec: Math.floor(Math.random() * 10000), + status: STATUSES[i % 5 === 4 ? 4 : i % 7 === 0 ? 2 : i % 5 === 0 ? 1 : i % 9 === 0 ? 3 : 0], + }; + }); +} + +export const infrastructureData = generateInfrastructureData(50); + +export const infrastructureHeaders: HeaderObject[] = [ + { + accessor: "serverId", + align: "left", + filterable: true, + isEditable: false, + isSortable: true, + label: "Server ID", + minWidth: 180, + pinned: "left", + type: "string", + width: "1.2fr", + }, + { + accessor: "serverName", + align: "left", + filterable: true, + isEditable: false, + isSortable: true, + label: "Name", + minWidth: 200, + type: "string", + width: "1.5fr", + }, + { + accessor: "performance", + label: "Performance Metrics", + width: 690, + isSortable: false, + children: [ + { + accessor: "cpuHistory", + label: "CPU History", + width: 150, + isSortable: false, + filterable: false, + isEditable: false, + align: "center", + type: "lineAreaChart", + tooltip: "CPU usage over the last 30 intervals", + }, + { + accessor: "cpuUsage", + label: "CPU %", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "memoryUsage", + label: "Memory %", + width: 130, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "diskUsage", + label: "Disk %", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "responseTime", + label: "Response (ms)", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + ], + }, + { + accessor: "status", + label: "Status", + width: 130, + isSortable: true, + filterable: true, + isEditable: 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 severityMap: Record = { + critical: 1, + offline: 2, + warning: 3, + maintenance: 4, + online: 5, + }; + return severityMap[String(row.status)] || 999; + }, + }, +]; + +export const INFRA_UPDATE_CONFIG = { + minInterval: 300, + maxInterval: 1000, +}; + +export function getInfraMetricColorStyles( + value: number, + theme: string, + metric: "cpu" | "memory" | "response" | "status", + statusValue?: string, +) { + const getLevel = (val: number, thresholds: [number, number, number]) => { + if (val >= thresholds[0]) return "critical"; + if (val >= thresholds[1]) return "warning"; + if (val >= thresholds[2]) return "moderate"; + return "good"; + }; + + let level: string; + if (metric === "cpu") level = getLevel(value, [90, 80, 60]); + else if (metric === "memory") level = getLevel(value, [95, 85, 70]); + else if (metric === "response") level = getLevel(value, [400, 200, 100]); + else level = statusValue || "good"; + + const isDark = theme === "dark" || theme === "modern-dark"; + const colorMap: Record = isDark + ? { + critical: { color: "#fca5a5", backgroundColor: "rgba(127, 29, 29, 0.4)" }, + warning: { color: "#fcd34d", backgroundColor: "rgba(146, 64, 14, 0.4)" }, + moderate: { color: "#60a5fa", backgroundColor: "rgba(30, 64, 175, 0.3)" }, + good: { color: "#4ade80", backgroundColor: "rgba(21, 128, 61, 0.3)" }, + } + : { + critical: { color: "#dc2626", backgroundColor: "#fef2f2" }, + warning: { color: "#d97706", backgroundColor: "#fffbeb" }, + moderate: { color: "#2563eb", backgroundColor: "#eff6ff" }, + good: { color: "#16a34a", backgroundColor: "#f0fdf4" }, + }; + + return colorMap[level] || colorMap.good; +} + +export function getInfraStatusColors(status: string, theme: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const map: Record = isDark + ? { + online: { color: "#6ee7b7", backgroundColor: "rgba(6, 95, 70, 0.4)", fontWeight: "600" }, + warning: { color: "#fcd34d", backgroundColor: "rgba(146, 64, 14, 0.4)", fontWeight: "600" }, + critical: { + color: "#fca5a5", + backgroundColor: "rgba(153, 27, 27, 0.4)", + fontWeight: "600", + }, + maintenance: { + color: "#93c5fd", + backgroundColor: "rgba(30, 64, 175, 0.4)", + fontWeight: "600", + }, + offline: { color: "#d1d5db", backgroundColor: "rgba(75, 85, 99, 0.4)", fontWeight: "600" }, + } + : { + online: { color: "#16a34a", backgroundColor: "#f0fdf4", fontWeight: "600" }, + warning: { color: "#d97706", backgroundColor: "#fffbeb", fontWeight: "600" }, + critical: { color: "#dc2626", backgroundColor: "#fef2f2", fontWeight: "600" }, + maintenance: { color: "#2563eb", backgroundColor: "#eff6ff", fontWeight: "600" }, + offline: { color: "#4b5563", backgroundColor: "#f9fafb", fontWeight: "600" }, + }; + return map[status] || map.offline; +} + +export const infrastructureConfig = { + headers: infrastructureHeaders, + rows: infrastructureData, +} as const; diff --git a/packages/examples/react/src/demos/live-update/LiveUpdateDemo.tsx b/packages/examples/react/src/demos/live-update/LiveUpdateDemo.tsx index d99f04b66..9f7a71991 100644 --- a/packages/examples/react/src/demos/live-update/LiveUpdateDemo.tsx +++ b/packages/examples/react/src/demos/live-update/LiveUpdateDemo.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, TableAPI } from "@simple-table/react"; -import { liveUpdateConfig, liveUpdateData } from "@simple-table/examples-shared"; +import { liveUpdateConfig, liveUpdateData } from "./live-update.demo-data"; import "@simple-table/react/styles.css"; const LiveUpdateDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { @@ -9,7 +9,7 @@ const LiveUpdateDemo = ({ height = "400px", theme }: { height?: string | number; useEffect(() => { const currentData = JSON.parse(JSON.stringify(liveUpdateData)); - const timerMap = new Map(); + const timerMap = new Map>(); const currentPeriodSales = new Map(); let isActive = true; @@ -106,7 +106,7 @@ const LiveUpdateDemo = ({ height = "400px", theme }: { height?: string | number; return ( typeof value === "number" ? `$${value.toFixed(2)}` : "$0.00", + }, + { accessor: "stock", label: "In Stock", width: 100, type: "number" }, + { + accessor: "stockHistory", label: "Stock Trend", width: 140, type: "lineAreaChart", align: "center", + tooltip: "Stock levels over the last 20 updates", + chartOptions: { color: "#10b981", fillColor: "#34d399", fillOpacity: 0.2, strokeWidth: 2, height: 35 }, + }, + { accessor: "sales", label: "Sales", width: 100, type: "number" }, + { + accessor: "salesHistory", label: "Sales Trend", width: 140, type: "barChart", align: "center", + tooltip: "Sales activity over the last 12 updates", + chartOptions: { color: "#f59e0b", gap: 2, height: 35 }, + }, +]; + +export const generateStockHistory = (currentStock: number, length = 20) => { + const history: number[] = []; + for (let i = 0; i < length; i++) { + const variation = (Math.random() - 0.5) * 30; + history.push(Math.max(0, Math.round(currentStock + variation))); + } + return history; +}; + +export const generateSalesHistory = (_currentSales: number, length = 12) => { + const history: number[] = []; + for (let i = 0; i < length; i++) { + history.push(Math.floor(Math.random() * 4)); + } + return history; +}; + +export const liveUpdateData = [ + { 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) }, + { id: 4, product: "Smart Water Bottle", price: 34.99, stock: 48, sales: 134, stockHistory: generateStockHistory(48), salesHistory: generateSalesHistory(134) }, + { id: 5, product: "Ceramic Coffee Mug", price: 18.99, stock: 124, sales: 298, stockHistory: generateStockHistory(124), salesHistory: generateSalesHistory(298) }, + { id: 6, product: "Wireless Phone Charger", price: 29.99, stock: 67, sales: 156, stockHistory: generateStockHistory(67), salesHistory: generateSalesHistory(156) }, + { id: 7, product: "Essential Oil Diffuser", price: 52.99, stock: 89, sales: 203, stockHistory: generateStockHistory(89), salesHistory: generateSalesHistory(203) }, + { id: 8, product: "Stainless Steel Tumbler", price: 22.99, stock: 134, sales: 267, stockHistory: generateStockHistory(134), salesHistory: generateSalesHistory(267) }, + { id: 9, product: "LED Desk Lamp", price: 39.99, stock: 95, sales: 176, stockHistory: generateStockHistory(95), salesHistory: generateSalesHistory(176) }, + { id: 10, product: "Organic Cotton Towel", price: 26.99, stock: 87, sales: 145, stockHistory: generateStockHistory(87), salesHistory: generateSalesHistory(145) }, + { id: 11, product: "Portable Phone Stand", price: 15.99, stock: 203, sales: 387, stockHistory: generateStockHistory(203), salesHistory: generateSalesHistory(387) }, + { id: 12, product: "Aromatherapy Candle", price: 31.99, stock: 56, sales: 112, stockHistory: generateStockHistory(56), salesHistory: generateSalesHistory(112) }, +]; + +export const liveUpdateConfig = { + headers: liveUpdateHeaders, + rows: liveUpdateData, +} as const; diff --git a/packages/examples/react/src/demos/loading-state/LoadingStateDemo.tsx b/packages/examples/react/src/demos/loading-state/LoadingStateDemo.tsx index 97f3397c9..65dad7c58 100644 --- a/packages/examples/react/src/demos/loading-state/LoadingStateDemo.tsx +++ b/packages/examples/react/src/demos/loading-state/LoadingStateDemo.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, Row } from "@simple-table/react"; -import { loadingStateConfig } from "@simple-table/examples-shared"; +import { loadingStateConfig } from "./loading-state.demo-data"; import "@simple-table/react/styles.css"; const LoadingStateDemo = ({ @@ -44,7 +44,7 @@ const LoadingStateDemo = ({ `$${(value as number).toLocaleString()}`, + }, + { accessor: "status", label: "Status", width: 120 }, +]; + +export const loadingStateConfig = { + headers: loadingStateHeaders, + rows: loadingStateData, +} as const; diff --git a/packages/examples/react/src/demos/manufacturing/ManufacturingDemo.tsx b/packages/examples/react/src/demos/manufacturing/ManufacturingDemo.tsx index 81120f008..c9a4102a3 100644 --- a/packages/examples/react/src/demos/manufacturing/ManufacturingDemo.tsx +++ b/packages/examples/react/src/demos/manufacturing/ManufacturingDemo.tsx @@ -1,16 +1,17 @@ -import { SimpleTable } from "@simple-table/react"; -import type { Theme, ReactHeaderObject } from "@simple-table/react"; -import { manufacturingConfig, getManufacturingStatusColors } from "@simple-table/examples-shared"; -import type { ManufacturingRow } from "@simple-table/examples-shared"; +import { SimpleTable, mapToReactHeaderObjects } from "@simple-table/react"; +import type { Theme, ReactHeaderObject, CellRendererProps } from "@simple-table/react"; +import { manufacturingConfig, getManufacturingStatusColors } from "./manufacturing.demo-data"; +import type { ManufacturingRow } from "./manufacturing.demo-data"; import "@simple-table/react/styles.css"; function getHeaders(): ReactHeaderObject[] { const baseHeaders = [...manufacturingConfig.headers]; - return baseHeaders.map((h) => { + return mapToReactHeaderObjects( + baseHeaders.map((h) => { if (h.accessor === "productLine") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; return d.stations ? {d.productLine} : d.productLine; }, @@ -19,7 +20,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "station") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; if (d.stations) return {d.id}; return ( @@ -34,7 +35,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; if (d.stations) return "—"; const colors = getManufacturingStatusColors(d.status, theme); @@ -45,7 +46,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "outputRate" || h.accessor === "defectCount" || h.accessor === "energy") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; const value = d[h.accessor as keyof ManufacturingRow] as number; return
{value.toLocaleString()}
; @@ -55,7 +56,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "cycletime") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; if (d.stations) return {d.cycletime.toFixed(1)}; return {d.cycletime}; @@ -65,7 +66,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "efficiency") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; const color = d.efficiency >= 90 ? "#52c41a" : d.efficiency >= 75 ? "#1890ff" : "#ff4d4f"; return ( @@ -82,7 +83,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "defectRate") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; const color = d.defectRate < 1 ? "#16a34a" : d.defectRate < 3 ? "#f59e0b" : "#dc2626"; return {d.defectRate.toFixed(2)}%; @@ -92,7 +93,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "downtime") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; const color = d.downtime < 1 ? "#16a34a" : d.downtime < 2 ? "#f59e0b" : "#dc2626"; return {d.downtime.toFixed(2)}; @@ -102,7 +103,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "utilization") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; if (d.stations) return {d.utilization.toFixed(0)}%; return `${d.utilization}%`; @@ -112,7 +113,7 @@ function getHeaders(): ReactHeaderObject[] { if (h.accessor === "maintenanceDate") { return { ...h, - cellRenderer: ({ row: r }) => { + cellRenderer: ({ row: r }: CellRendererProps) => { const d = r as unknown as ManufacturingRow; if (d.stations) return "—"; const [year, month, day] = d.maintenanceDate.split("-").map(Number); @@ -132,7 +133,8 @@ function getHeaders(): ReactHeaderObject[] { }; } return h; - }); + }), + ); } const ManufacturingDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => ( diff --git a/packages/examples/react/src/demos/manufacturing/manufacturing.demo-data.ts b/packages/examples/react/src/demos/manufacturing/manufacturing.demo-data.ts new file mode 100644 index 000000000..fb75ba21d --- /dev/null +++ b/packages/examples/react/src/demos/manufacturing/manufacturing.demo-data.ts @@ -0,0 +1,125 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + +export interface ManufacturingRow { + id: string; + productLine: string; + station: string; + machineType: string; + status: string; + outputRate: number; + cycletime: number; + efficiency: number; + defectRate: number; + defectCount: number; + downtime: number; + utilization: number; + energy: number; + maintenanceDate: string; + stations?: ManufacturingRow[]; +} + + +const PRODUCT_LINES = ["Assembly Line A", "Assembly Line B", "Welding Station", "Paint Shop", "Quality Control", "Packaging Unit", "CNC Machining", "Injection Molding"]; +const STATIONS = ["Station Alpha", "Station Beta", "Station Gamma", "Station Delta", "Station Epsilon"]; +const MACHINE_TYPES = ["CNC Mill", "Lathe", "Welder", "Press", "Robot Arm", "Conveyor", "Inspector", "Dryer"]; +const MANUFACTURING_STATUSES = ["Running", "Scheduled Maintenance", "Unplanned Downtime", "Idle", "Setup"]; + +export function generateManufacturingData(count: number = 8): ManufacturingRow[] { + return Array.from({ length: count }, (_, i) => { + const stationCount = 3 + Math.floor(Math.random() * 3); + const stations: ManufacturingRow[] = Array.from({ length: stationCount }, (_, j) => { + const efficiency = Math.floor(70 + Math.random() * 28); + const defectRate = Math.round((0.1 + Math.random() * 4) * 100) / 100; + const downtime = Math.round((0.1 + Math.random() * 3) * 100) / 100; + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + Math.floor(Math.random() * 30)); + return { + id: `${PRODUCT_LINES[i % PRODUCT_LINES.length]}-S${j + 1}`, + productLine: PRODUCT_LINES[i % PRODUCT_LINES.length], + station: STATIONS[j % STATIONS.length], + machineType: MACHINE_TYPES[(i + j) % MACHINE_TYPES.length], + status: MANUFACTURING_STATUSES[j % MANUFACTURING_STATUSES.length], + outputRate: Math.floor(500 + Math.random() * 2000), + cycletime: Math.round((10 + Math.random() * 50) * 10) / 10, + efficiency, + defectRate, + defectCount: Math.floor(defectRate * 10 + Math.random() * 20), + downtime, + utilization: Math.floor(60 + Math.random() * 38), + energy: Math.floor(100 + Math.random() * 900), + maintenanceDate: futureDate.toISOString().split("T")[0], + }; + }); + + const totalOutput = stations.reduce((s, st) => s + st.outputRate, 0); + const avgEfficiency = Math.round(stations.reduce((s, st) => s + st.efficiency, 0) / stations.length); + const avgCycletime = Math.round((stations.reduce((s, st) => s + st.cycletime, 0) / stations.length) * 10) / 10; + const avgDefectRate = Math.round((stations.reduce((s, st) => s + st.defectRate, 0) / stations.length) * 100) / 100; + const totalDefects = stations.reduce((s, st) => s + st.defectCount, 0); + const totalDowntime = Math.round(stations.reduce((s, st) => s + st.downtime, 0) * 100) / 100; + const avgUtilization = Math.round(stations.reduce((s, st) => s + st.utilization, 0) / stations.length); + const totalEnergy = stations.reduce((s, st) => s + st.energy, 0); + + return { + id: PRODUCT_LINES[i % PRODUCT_LINES.length], + productLine: PRODUCT_LINES[i % PRODUCT_LINES.length], + station: "", + machineType: "", + status: "", + outputRate: totalOutput, + cycletime: avgCycletime, + efficiency: avgEfficiency, + defectRate: avgDefectRate, + defectCount: totalDefects, + downtime: totalDowntime, + utilization: avgUtilization, + energy: totalEnergy, + maintenanceDate: "", + stations, + }; + }); +} + +export const manufacturingData = generateManufacturingData(8); + +export const manufacturingHeaders: HeaderObject[] = [ + { accessor: "productLine", label: "Production Line", width: 180, expandable: true, isSortable: true, isEditable: false, align: "left", type: "string" }, + { accessor: "station", label: "Workstation", width: 150, isSortable: true, isEditable: false, align: "left", type: "string" }, + { accessor: "machineType", label: "Machine Type", width: 150, isSortable: true, isEditable: false, align: "left", type: "string" }, + { + accessor: "status", label: "Status", width: 180, isSortable: true, isEditable: false, align: "center", type: "string", + valueGetter: ({ row }) => { + if (row.stations && Array.isArray(row.stations)) return 999; + const priorityMap: Record = { "Unplanned Downtime": 1, Idle: 2, Setup: 3, "Scheduled Maintenance": 4, Running: 5 }; + return priorityMap[String(row.status)] || 999; + }, + }, + { accessor: "outputRate", label: "Output (units/shift)", width: 200, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "cycletime", label: "Cycle Time (s)", width: 140, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "efficiency", label: "Efficiency", width: 150, isSortable: true, isEditable: false, align: "center", type: "number", aggregation: { type: "average" } }, + { accessor: "defectRate", label: "Defect Rate", width: 120, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "defectCount", label: "Defects", width: 120, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "downtime", label: "Downtime (h)", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "utilization", label: "Utilization", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "energy", label: "Energy (kWh)", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "maintenanceDate", label: "Next Maintenance", width: 200, isSortable: true, isEditable: false, align: "center", type: "date" }, +]; + +export function getManufacturingStatusColors(status: string, theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const isLight = theme === "light" || theme === "modern-light"; + const colorMaps: Record = { + Running: isDark ? { bg: "rgba(6, 95, 70, 0.4)", text: "#6ee7b7" } : isLight ? { bg: "#dcfce7", text: "#16a34a" } : { bg: "#f6ffed", text: "#2a6a0d" }, + "Scheduled Maintenance": isDark ? { bg: "rgba(30, 64, 175, 0.4)", text: "#93c5fd" } : isLight ? { bg: "#dbeafe", text: "#3b82f6" } : { bg: "#e6f7ff", text: "#0050b3" }, + "Unplanned Downtime": isDark ? { bg: "rgba(153, 27, 27, 0.4)", text: "#fca5a5" } : isLight ? { bg: "#fee2e2", text: "#dc2626" } : { bg: "#fff1f0", text: "#a8071a" }, + Idle: isDark ? { bg: "rgba(146, 64, 14, 0.4)", text: "#fcd34d" } : isLight ? { bg: "#fef3c7", text: "#d97706" } : { bg: "#fff7e6", text: "#ad4e00" }, + Setup: isDark ? { bg: "rgba(109, 40, 217, 0.4)", text: "#c4b5fd" } : isLight ? { bg: "#e9d5ff", text: "#9333ea" } : { bg: "#f9f0ff", text: "#391085" }, + }; + return colorMaps[status] || (isDark ? { bg: "rgba(75, 85, 99, 0.4)", text: "#d1d5db" } : isLight ? { bg: "#f3f4f6", text: "#6b7280" } : { bg: "#f0f0f0", text: "rgba(0, 0, 0, 0.85)" }); +} + +export const manufacturingConfig = { + headers: manufacturingHeaders, + rows: manufacturingData, +} as const; diff --git a/packages/examples/react/src/demos/music/MusicDemo.tsx b/packages/examples/react/src/demos/music/MusicDemo.tsx index 3255ef1b6..5d1f25e0b 100644 --- a/packages/examples/react/src/demos/music/MusicDemo.tsx +++ b/packages/examples/react/src/demos/music/MusicDemo.tsx @@ -1,10 +1,10 @@ import { useRef } from "react"; import { SimpleTable } from "@simple-table/react"; -import type { Theme, TableAPI, ReactHeaderObject } from "@simple-table/react"; -import { musicData, getMusicThemeColors } from "@simple-table/examples-shared"; -import type { MusicArtist } from "@simple-table/examples-shared"; +import type { Theme, TableAPI, ReactHeaderObject, CellRendererProps } from "@simple-table/react"; +import { musicData, getMusicThemeColors } from "./music.demo-data"; +import type { MusicArtist } from "./music.demo-data"; import "@simple-table/react/styles.css"; -import "@simple-table/examples-shared/styles/music-theme.css"; +import "./music-theme.css"; const Tag = ({ children, color, theme }: { children: React.ReactNode; color?: string; theme?: string }) => { const c = getMusicThemeColors(theme); @@ -33,7 +33,7 @@ function getMusicHeaders(): ReactHeaderObject[] { { accessor: "rank", label: "#", width: 60, isSortable: true, isEditable: false, align: "center", type: "number", pinned: "left" }, { accessor: "artistName", label: "Artist", width: 330, isSortable: true, isEditable: false, align: "left", type: "string", pinned: "left", - cellRenderer: ({ row: r, theme }) => { + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); let hash = 0; for (let i = 0; i < d.artistName.length; i++) hash = d.artistName.charCodeAt(i) + ((hash << 5) - hash); @@ -54,47 +54,47 @@ function getMusicHeaders(): ReactHeaderObject[] { }, { accessor: "artistType", label: "Identity", width: 280, isSortable: false, isEditable: false, align: "left", type: "string", - cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); return
{d.artistType}, {d.pronouns}
{d.recordLabel}
Lyrics Language: {d.lyricsLanguage}
; }, + cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); return
{d.artistType}, {d.pronouns}
{d.recordLabel}
Lyrics Language: {d.lyricsLanguage}
; }, }, { accessor: "followersGroup", label: "Followers", width: 700, collapsible: true, children: [ - { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return ; } }, - { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return ; } }, - { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return ; } }, + { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return ; } }, + { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return ; } }, + { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return ; } }, ], }, - { accessor: "popularity", label: "Popularity", width: 180, isSortable: true, isEditable: false, align: "center", type: "number", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return
= 0} theme={theme} showSign={false} />
; } }, + { accessor: "popularity", label: "Popularity", width: 180, isSortable: true, isEditable: false, align: "center", type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; 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", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; 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", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return ; } }, - { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return ; } }, - { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return ; } }, + { accessor: "playlistCount", label: "Total Count", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return ; } }, + { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return ; } }, + { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return ; } }, ], }, { accessor: "monthlyListenersGroup", label: "Monthly Listeners", width: 700, collapsible: true, children: [ - { accessor: "monthlyListeners", label: "Total Listeners", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, - { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "monthlyListeners", label: "Total Listeners", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r 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, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, + { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; return = 0} theme={theme} align="right" />; } }, ], }, - { accessor: "conversionRate", label: "Conversion Rate", width: 150, isSortable: true, isEditable: false, align: "right", type: "number", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); return {d.conversionRate.toFixed(2)}%; } }, - { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, isSortable: true, isEditable: false, align: "right", type: "number", cellRenderer: ({ row: r, theme }) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); return {d.reachFollowersRatio.toFixed(1)}x; } }, + { accessor: "conversionRate", label: "Conversion Rate", width: 150, isSortable: true, isEditable: false, align: "right", type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); return {d.conversionRate.toFixed(2)}%; } }, + { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, isSortable: true, isEditable: false, align: "right", type: "number", cellRenderer: ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as MusicArtist; const c = getMusicThemeColors(theme); return {d.reachFollowersRatio.toFixed(1)}x; } }, ]; } diff --git a/packages/examples/react/src/demos/music/music-theme.css b/packages/examples/react/src/demos/music/music-theme.css new file mode 100644 index 000000000..f2538a958 --- /dev/null +++ b/packages/examples/react/src/demos/music/music-theme.css @@ -0,0 +1,13 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +.music-theme-container * { + font-family: "Inter"; +} + +.music-theme-container .st-header-label-text { + font-weight: 500; + font-size: 12px; +} +.music-theme-container .st-cell-content { + font-size: 12px; +} diff --git a/packages/examples/react/src/demos/music/music.demo-data.ts b/packages/examples/react/src/demos/music/music.demo-data.ts new file mode 100644 index 000000000..438b8f7f0 --- /dev/null +++ b/packages/examples/react/src/demos/music/music.demo-data.ts @@ -0,0 +1,215 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + +export interface MusicArtist { + id: number; + rank: 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; +} + + +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(); +} + +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; }; + + 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, + }; + }); +} + +export const musicData = generateMusicData(50); + +export const musicHeaders: HeaderObject[] = [ + { accessor: "rank", label: "#", width: 60, isSortable: true, isEditable: false, align: "center", type: "number", pinned: "left" }, + { accessor: "artistName", label: "Artist", width: 330, isSortable: true, isEditable: false, align: "left", type: "string", pinned: "left" }, + { accessor: "artistType", label: "Identity", width: 280, isSortable: false, isEditable: false, align: "left", type: "string" }, + { + accessor: "followersGroup", label: "Followers", width: 700, collapsible: true, + children: [ + { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number" }, + { accessor: "followers7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + ], + }, + { accessor: "popularity", label: "Popularity", width: 180, isSortable: true, isEditable: false, align: "center", type: "number" }, + { + accessor: "playlistReachGroup", label: "Playlist Reach", width: 700, collapsible: true, + children: [ + { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number" }, + { accessor: "playlistReach7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: 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", isSortable: true, isEditable: false, type: "number" }, + { accessor: "playlistCount7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: 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", isSortable: true, isEditable: false, type: "number" }, + { accessor: "monthlyListeners7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + ], + }, + { accessor: "conversionRate", label: "Conversion Rate", width: 150, isSortable: true, isEditable: false, align: "right", type: "number" }, + { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, isSortable: true, isEditable: false, align: "right", type: "number" }, +]; + +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" }, + 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" }, + "modern-dark": { gray: "#e5e7eb", grayMuted: "#9ca3af", success: "#22c55e", successBg: "#052e16", error: "#ef4444", errorBg: "#450a0a", primary: "#60a5fa", primaryBg: "#1e3a8a", warning: "#f59e0b", warningBg: "#451a03", tagBorder: "#4b5563", tagBg: "#111827", tagText: "#f9fafb", highScore: "#22c55e", mediumScore: "#60a5fa", lowScore: "#fbbf24", veryLowScore: "#f87171" }, + dark: { gray: "#e5e7eb", grayMuted: "#9ca3af", success: "#22c55e", successBg: "#052e16", error: "#ef4444", errorBg: "#450a0a", primary: "#60a5fa", primaryBg: "#1e3a8a", warning: "#f59e0b", warningBg: "#451a03", tagBorder: "#4b5563", tagBg: "#111827", tagText: "#f9fafb", highScore: "#22c55e", mediumScore: "#60a5fa", lowScore: "#fbbf24", veryLowScore: "#f87171" }, +}; + +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/react/src/demos/nested-headers/NestedHeadersDemo.tsx b/packages/examples/react/src/demos/nested-headers/NestedHeadersDemo.tsx index a3d63a290..54d23756d 100644 --- a/packages/examples/react/src/demos/nested-headers/NestedHeadersDemo.tsx +++ b/packages/examples/react/src/demos/nested-headers/NestedHeadersDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { nestedHeadersConfig } from "@simple-table/examples-shared"; +import { nestedHeadersConfig } from "./nested-headers.demo-data"; import "@simple-table/react/styles.css"; const NestedHeadersDemo = ({ @@ -12,7 +12,7 @@ const NestedHeadersDemo = ({ }) => { return ( { @@ -10,7 +10,7 @@ const NestedTablesDemo = ({ height = "500px", theme }: { height?: string | numbe return ( (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) => ({ + divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`, + divisionName: randomElement(divisionTypes), + revenue: `$${randomInt(5, 25)}B`, + profitMargin: `${randomInt(15, 50)}%`, + headcount: randomInt(50, 500), + location: randomElement(cities), +}); + +const generateCompany = (companyIndex: number) => { + const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex)); + return { + id: companyIndex + 1, + companyName: `${randomElement(companyNames)} ${randomElement(suffixes)}`, + industry: randomElement(industries), + founded: randomInt(1985, 2020), + headquarters: randomElement(cities), + stockSymbol: Array.from({ length: 4 }, () => String.fromCharCode(65 + randomInt(0, 25))).join(""), + marketCap: `$${randomInt(10, 200)}B`, + ceo: `${randomElement(firstNames)} ${randomElement(lastNames)}`, + revenue: `$${randomInt(5, 60)}B`, + employees: randomInt(5000, 100000), + divisions, + }; +}; + +export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i)); + +export const nestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionId", label: "Division ID", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: "1fr" }, +]; + +export const nestedTablesHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { defaultHeaders: nestedTablesDivisionHeaders }, + }, + { accessor: "stockSymbol", label: "Symbol", width: 100 }, + { accessor: "marketCap", label: "Market Cap", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const nestedTablesConfig = { + headers: nestedTablesHeaders, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + columnResizing: true, + autoExpandColumns: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/pagination/PaginationDemo.tsx b/packages/examples/react/src/demos/pagination/PaginationDemo.tsx index 57194c7ac..b697a91c7 100644 --- a/packages/examples/react/src/demos/pagination/PaginationDemo.tsx +++ b/packages/examples/react/src/demos/pagination/PaginationDemo.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "@simple-table/examples-shared"; +import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "./pagination.demo-data"; import "@simple-table/react/styles.css"; const PaginationDemo = ({ @@ -34,7 +34,7 @@ const PaginationDemo = ({ return ( - programmaticControlConfig.headers.map((h) => { + mapToReactHeaderObjects(programmaticControlConfig.headers.map((h) => { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const s = String(row.status); const colors = PROGRAMMATIC_CONTROL_STATUS_COLORS[s] ?? { bg: "#f3f4f6", color: "#374151" }; return ( @@ -38,10 +38,10 @@ const ProgrammaticControlDemo = ({ ); }, - } as ReactHeaderObject; + }; } - return { ...h } as ReactHeaderObject; - }), + return h; + })), [], ); @@ -72,7 +72,10 @@ const ProgrammaticControlDemo = ({ const hdrs = api.getHeaders(); const sortState = api.getSortState(); const filterState = api.getFilterState(); - const totalValue = allRows.reduce((sum, r) => sum + (r.price as number) * (r.stock as number), 0); + const totalValue = allRows.reduce((sum, r) => { + const row = r as Record; + return sum + (Number(row.price) || 0) * (Number(row.stock) || 0); + }, 0); const sortInfo = sortState ? `${sortState.key.label} (${sortState.direction})` : "None"; alert( `Table Info:\n• Rows: ${allRows.length}\n• Columns: ${hdrs.length}\n• Active filters: ${Object.keys(filterState).length}\n• Sort: ${sortInfo}\n• Total inventory value: $${totalValue.toFixed(2)}`, diff --git a/packages/examples/react/src/demos/programmatic-control/programmatic-control.demo-data.ts b/packages/examples/react/src/demos/programmatic-control/programmatic-control.demo-data.ts new file mode 100644 index 000000000..69a6de254 --- /dev/null +++ b/packages/examples/react/src/demos/programmatic-control/programmatic-control.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +export const STATUS_COLORS: Record = { + Available: { bg: "#dcfce7", color: "#166534" }, + "Low Stock": { bg: "#fef3c7", color: "#92400e" }, + "Out of Stock": { bg: "#fee2e2", color: "#991b1b" }, +}; + +export const programmaticControlData = [ + { 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" }, + { id: 4, name: "Standing Desk", category: "Furniture", price: 399.99, stock: 0, status: "Out of Stock" }, + { id: 5, name: "Office Chair", category: "Furniture", price: 249.99, stock: 56, status: "Available" }, + { id: 6, name: "Monitor Stand", category: "Furniture", price: 79.99, stock: 8, status: "Low Stock" }, + { id: 7, name: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, status: "Available" }, + { id: 8, name: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, status: "Available" }, + { id: 9, name: "Desk Organizer", category: "Stationery", price: 24.99, stock: 5, status: "Low Stock" }, + { id: 10, name: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, status: "Available" }, + { id: 11, name: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, status: "Available" }, + { id: 12, name: "Desk Lamp LED", category: "Appliances", price: 44.99, stock: 0, status: "Out of Stock" }, +]; + +export const programmaticControlHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, type: "number", isSortable: true, filterable: true }, + { accessor: "name", label: "Product Name", width: "1fr", minWidth: 150, type: "string", isSortable: true, filterable: true }, + { + accessor: "category", + label: "Category", + width: 140, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Electronics", "Furniture", "Stationery", "Appliances"].map((v) => ({ label: v, value: v })), + }, + { accessor: "price", label: "Price", width: 110, align: "right", type: "number", isSortable: true, filterable: true, valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}` }, + { accessor: "stock", label: "Stock", width: 100, align: "right", type: "number", isSortable: true, filterable: true }, + { + accessor: "status", + label: "Status", + width: 110, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Available", "Low Stock", "Out of Stock"].map((v) => ({ label: v, value: v })), + }, +]; + +export const programmaticControlConfig = { + headers: programmaticControlHeaders, + rows: programmaticControlData, +} as const; + +export { STATUS_COLORS as PROGRAMMATIC_CONTROL_STATUS_COLORS }; diff --git a/packages/examples/react/src/demos/quick-filter/QuickFilterDemo.tsx b/packages/examples/react/src/demos/quick-filter/QuickFilterDemo.tsx index 76dd312bb..3634fc993 100644 --- a/packages/examples/react/src/demos/quick-filter/QuickFilterDemo.tsx +++ b/packages/examples/react/src/demos/quick-filter/QuickFilterDemo.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme, QuickFilterMode } from "@simple-table/react"; -import { quickFilterConfig } from "@simple-table/examples-shared"; +import { quickFilterConfig } from "./quick-filter.demo-data"; import "@simple-table/react/styles.css"; const QuickFilterDemo = ({ @@ -78,7 +78,7 @@ const QuickFilterDemo = ({ `$${(value || 0).toLocaleString()}`, align: "right" }, + { accessor: "status", label: "Status", width: 100, type: "string" }, + { accessor: "location", label: "Location", width: 140, type: "string" }, +]; + +export const quickFilterData = [ + { 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" }, + { id: 4, name: "Diana Prince", age: 31, department: "Marketing", salary: 82000, status: "Inactive", location: "Chicago" }, + { id: 5, name: "Ethan Hunt", age: 29, department: "Sales", salary: 78000, status: "Active", location: "Boston" }, + { id: 6, name: "Fiona Green", age: 38, department: "Engineering", salary: 105000, status: "Active", location: "Seattle" }, + { id: 7, name: "George Wilson", age: 26, department: "Marketing", salary: 68000, status: "Active", location: "Austin" }, + { id: 8, name: "Hannah Lee", age: 33, department: "Sales", salary: 88000, status: "Inactive", location: "Denver" }, + { id: 9, name: "Isaac Chen", age: 27, department: "Engineering", salary: 92000, status: "Active", location: "San Diego" }, + { id: 10, name: "Julia Brown", age: 30, department: "Marketing", salary: 72000, status: "Inactive", location: "Miami" }, + { id: 11, name: "Kevin Davis", age: 28, department: "Sales", salary: 85000, status: "Active", location: "Phoenix" }, + { id: 12, name: "Laura Garcia", age: 32, department: "Engineering", salary: 102000, status: "Active", location: "San Antonio" }, +]; + +export const quickFilterConfig = { + headers: quickFilterHeaders, + rows: quickFilterData, + tableProps: { quickFilter: { text: "", mode: "simple" as const, caseSensitive: false } }, +} as const; diff --git a/packages/examples/react/src/demos/quick-start/QuickStartDemo.tsx b/packages/examples/react/src/demos/quick-start/QuickStartDemo.tsx index e0c420ecf..5635d1523 100644 --- a/packages/examples/react/src/demos/quick-start/QuickStartDemo.tsx +++ b/packages/examples/react/src/demos/quick-start/QuickStartDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { quickStartConfig } from "@simple-table/examples-shared"; +import { quickStartConfig } from "./quick-start.demo-data"; import "@simple-table/react/styles.css"; const QuickStartDemo = ({ @@ -12,7 +12,7 @@ const QuickStartDemo = ({ }) => { return ( ({ @@ -54,7 +54,7 @@ const RowGroupingDemo = ({ }) => String(row.id), + columnResizing: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/row-height/RowHeightDemo.tsx b/packages/examples/react/src/demos/row-height/RowHeightDemo.tsx index 465d1bfaa..56860dcfb 100644 --- a/packages/examples/react/src/demos/row-height/RowHeightDemo.tsx +++ b/packages/examples/react/src/demos/row-height/RowHeightDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { rowHeightConfig } from "@simple-table/examples-shared"; +import { rowHeightConfig } from "./row-height.demo-data"; import "@simple-table/react/styles.css"; const RowHeightDemo = ({ @@ -12,7 +12,7 @@ const RowHeightDemo = ({ }) => { return ( - rowSelectionConfig.headers.map((h) => { + mapToReactHeaderObjects(rowSelectionConfig.headers.map((h) => { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const s = String(row.status); const color = s === "Available" ? "#16a34a" : s === "Checked Out" ? "#ea580c" : "#dc2626"; return ( {s} ); }, - } as ReactHeaderObject; + }; } - return { ...h } as ReactHeaderObject; - }), + return h; + })), [], ); diff --git a/packages/examples/react/src/demos/row-selection/row-selection.demo-data.ts b/packages/examples/react/src/demos/row-selection/row-selection.demo-data.ts new file mode 100644 index 000000000..4b81b93b8 --- /dev/null +++ b/packages/examples/react/src/demos/row-selection/row-selection.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + + +export type LibraryBook = { + id: number; + isbn: string; + title: string; + author: string; + genre: string; + yearPublished: number; + pages: number; + rating: number; + status: string; + librarySection: string; + borrowedBy?: string; +}; + +export const rowSelectionData: LibraryBook[] = [ + { id: 1001, isbn: "978-0553418026", title: "The Quantum Chronicles", author: "Dr. Elena Vasquez", genre: "Science Fiction", yearPublished: 2019, pages: 324, rating: 4.7, status: "Available", librarySection: "Fiction A-L" }, + { id: 1002, isbn: "978-0316769488", title: "Digital Renaissance", author: "Marcus Chen", genre: "Technology", yearPublished: 2021, pages: 287, rating: 4.2, status: "Checked Out", librarySection: "Technology", borrowedBy: "Sarah Williams" }, + { id: 1003, isbn: "978-1400079179", title: "Echoes of Ancient Wisdom", author: "Prof. Amara Okafor", genre: "Philosophy", yearPublished: 2018, pages: 456, rating: 4.9, status: "Available", librarySection: "Philosophy" }, + { id: 1004, isbn: "978-0062315007", title: "The Midnight Observatory", author: "Luna Rodriguez", genre: "Mystery", yearPublished: 2020, pages: 298, rating: 4.4, status: "Reserved", librarySection: "Fiction M-Z" }, + { id: 1005, isbn: "978-0544003415", title: "Sustainable Architecture Now", author: "Kai Nakamura", genre: "Architecture", yearPublished: 2022, pages: 368, rating: 4.6, status: "Available", librarySection: "Architecture" }, + { id: 1006, isbn: "978-0147516466", title: "Neural Networks Simplified", author: "Dr. Priya Sharma", genre: "Computer Science", yearPublished: 2021, pages: 412, rating: 4.8, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Alex Thompson" }, + { id: 1007, isbn: "978-0547928227", title: "Culinary Traditions of the World", author: "Isabella Fontana", genre: "Cooking", yearPublished: 2019, pages: 276, rating: 4.3, status: "Available", librarySection: "Lifestyle" }, + { id: 1008, isbn: "978-0525509288", title: "The Biomimicry Revolution", author: "Dr. James Whitfield", genre: "Biology", yearPublished: 2020, pages: 345, rating: 4.5, status: "Available", librarySection: "Science" }, + { id: 1009, isbn: "978-0345391803", title: "Symphonies in Code", author: "Aria Blackwood", genre: "Programming", yearPublished: 2022, pages: 423, rating: 4.7, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Emma Davis" }, + { id: 1010, isbn: "978-0812988407", title: "Urban Gardens & Green Spaces", author: "Miguel Santos", genre: "Gardening", yearPublished: 2021, pages: 189, rating: 4.1, status: "Available", librarySection: "Lifestyle" }, + { id: 1011, isbn: "978-0374533557", title: "The Psychology of Innovation", author: "Dr. Rachel Kim", genre: "Psychology", yearPublished: 2019, pages: 312, rating: 4.6, status: "Reserved", librarySection: "Psychology" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Book ID", width: 80, isSortable: true, type: "number" }, + { accessor: "isbn", label: "ISBN", width: 120, isSortable: true, type: "string" }, + { accessor: "title", label: "Title", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "author", label: "Author", width: 140, isSortable: true, type: "string" }, + { accessor: "genre", label: "Genre", width: 120, isSortable: true, type: "string" }, + { accessor: "yearPublished", label: "Year", width: 80, isSortable: true, type: "number" }, + { accessor: "pages", label: "Pages", width: 80, isSortable: true, type: "number" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number" }, + { accessor: "status", label: "Status", width: 100, isSortable: true, type: "string" }, + { accessor: "librarySection", label: "Section", width: 120, isSortable: true, type: "string" }, +]; + +export const rowSelectionConfig = { + headers: rowSelectionHeaders, + rows: rowSelectionData, + tableProps: { + enableRowSelection: true, + columnResizing: true, + columnReordering: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/sales/SalesDemo.tsx b/packages/examples/react/src/demos/sales/SalesDemo.tsx index f83f08c08..4ec874158 100644 --- a/packages/examples/react/src/demos/sales/SalesDemo.tsx +++ b/packages/examples/react/src/demos/sales/SalesDemo.tsx @@ -1,17 +1,17 @@ import { useState, useEffect } from "react"; import { SimpleTable } from "@simple-table/react"; -import type { Theme, ReactHeaderObject, CellChangeProps } from "@simple-table/react"; -import { salesConfig, getSalesThemeColors } from "@simple-table/examples-shared"; -import type { SalesRow } from "@simple-table/examples-shared"; +import type { Theme, CellChangeProps, CellRendererProps, ReactHeaderObject } from "@simple-table/react"; +import { salesConfig, getSalesThemeColors } from "./sales.demo-data"; +import type { SalesRow } from "./sales.demo-data"; import "@simple-table/react/styles.css"; function getHeaders(): ReactHeaderObject[] { - const headers: ReactHeaderObject[] = JSON.parse(JSON.stringify(salesConfig.headers)); + const headers = JSON.parse(JSON.stringify(salesConfig.headers)) as ReactHeaderObject[]; const addRenderers = (hdrs: ReactHeaderObject[]) => { for (const h of hdrs) { if (h.accessor === "dealValue") { - h.cellRenderer = ({ row: r, theme }) => { + h.cellRenderer = ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as SalesRow; const c = getSalesThemeColors(theme); let style: React.CSSProperties = { color: c.gray }; @@ -26,7 +26,7 @@ function getHeaders(): ReactHeaderObject[] { }; } if (h.accessor === "isWon") { - h.cellRenderer = ({ row: r }) => { + h.cellRenderer = ({ row: r }: CellRendererProps) => { const d = r as unknown as SalesRow; const s = d.isWon ? { bg: "#f6ffed", text: "#2a6a0d" } : { bg: "#fff1f0", text: "#a8071a" }; return ( @@ -37,7 +37,7 @@ function getHeaders(): ReactHeaderObject[] { }; } if (h.accessor === "commission") { - h.cellRenderer = ({ row: r, theme }) => { + h.cellRenderer = ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as SalesRow; const c = getSalesThemeColors(theme); if (d.commission === 0) return $0.00; @@ -45,7 +45,7 @@ function getHeaders(): ReactHeaderObject[] { }; } if (h.accessor === "profitMargin") { - h.cellRenderer = ({ row: r, theme }) => { + h.cellRenderer = ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as SalesRow; const c = getSalesThemeColors(theme); let colorStyle: React.CSSProperties = { color: c.gray }; @@ -68,7 +68,7 @@ function getHeaders(): ReactHeaderObject[] { }; } if (h.accessor === "dealProfit") { - h.cellRenderer = ({ row: r, theme }) => { + h.cellRenderer = ({ row: r, theme }: CellRendererProps) => { const d = r as unknown as SalesRow; const c = getSalesThemeColors(theme); if (d.dealProfit === 0) return $0.00; diff --git a/packages/examples/react/src/demos/sales/sales.demo-data.ts b/packages/examples/react/src/demos/sales/sales.demo-data.ts new file mode 100644 index 000000000..870cc2bab --- /dev/null +++ b/packages/examples/react/src/demos/sales/sales.demo-data.ts @@ -0,0 +1,114 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/react"; + +export interface SalesRow { + id: number; + repName: string; + dealSize: number; + dealValue: number; + isWon: boolean; + closeDate: string; + commission: number; + profitMargin: number; + dealProfit: number; + category: string; +} + + +const SALES_REP_NAMES = ["Alex Morgan", "Sam Rivera", "Jordan Lee", "Taylor Kim", "Casey Chen", "Riley Park", "Morgan Wells", "Avery Quinn", "Devon Blake", "Harper Cole", "Quinn Foster", "Sage Turner", "Cameron Reed", "Jesse Nash", "Blake Palmer"]; +const CATEGORIES = ["Software", "Hardware", "Services", "Consulting", "Training", "Support"]; + +export function generateSalesData(count: number = 100): SalesRow[] { + return Array.from({ length: count }, (_, i) => { + const dealSize = Math.round((5000 + Math.random() * 200000) * 100) / 100; + const profitMargin = Math.round((0.15 + Math.random() * 0.7) * 1000) / 1000; + const dealValue = Math.round((dealSize / profitMargin) * 100) / 100; + const commission = Math.round(dealValue * 0.1 * 100) / 100; + const dealProfit = Math.round((dealSize - commission) * 100) / 100; + const closeYear = 2023 + Math.floor(Math.random() * 2); + const closeMonth = String(1 + Math.floor(Math.random() * 12)).padStart(2, "0"); + const closeDay = String(1 + Math.floor(Math.random() * 28)).padStart(2, "0"); + + return { + id: i + 1, + repName: SALES_REP_NAMES[i % SALES_REP_NAMES.length], + dealSize, + dealValue, + isWon: Math.random() > 0.35, + closeDate: `${closeYear}-${closeMonth}-${closeDay}`, + commission, + profitMargin, + dealProfit, + category: CATEGORIES[i % CATEGORIES.length], + }; + }); +} + +export const salesData = generateSalesData(100); + +export const salesHeaders: HeaderObject[] = [ + { accessor: "repName", label: "Sales Representative", width: "2fr", minWidth: 200, isSortable: true, isEditable: true, type: "string", tooltip: "Name of the sales representative" }, + { + accessor: "salesMetrics", label: "Sales Metrics", width: 600, isSortable: false, + children: [ + { + accessor: "dealSize", label: "Deal Size", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The size of the deal in dollars", + valueFormatter: ({ value }) => { if (typeof value !== "number") return "—"; return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }, + useFormattedValueForClipboard: true, useFormattedValueForCSV: true, + }, + { accessor: "dealValue", label: "Deal Value", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The value of the deal in dollars" }, + { accessor: "isWon", label: "Status", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "boolean", tooltip: "Whether the deal was won or lost" }, + { + accessor: "closeDate", label: "Close Date", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "date", tooltip: "The date the deal was closed", + valueFormatter: ({ value }) => { + if (typeof value !== "string") return "—"; + const [year, month, day] = value.split("-").map(Number); + const date = new Date(year, month - 1, day); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + ], + }, + { + accessor: "financialMetrics", label: "Financial Metrics", width: "1fr", minWidth: 140, isSortable: false, + children: [ + { accessor: "commission", label: "Commission", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The commission earned from the deal in dollars" }, + { + accessor: "profitMargin", label: "Profit Margin", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The profit margin of the deal", + valueFormatter: ({ value }) => { if (typeof value !== "number") return "—"; return `${(value * 100).toFixed(1)}%`; }, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => { if (typeof value !== "number") return "—"; return `${Math.round(value * 100)}%`; }, + }, + { accessor: "dealProfit", label: "Deal Profit", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The profit of the deal in dollars" }, + { + accessor: "category", label: "Category", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "enum", tooltip: "The category of the deal", + enumOptions: CATEGORIES.map((c) => ({ label: c, value: c })), + valueGetter: ({ row }) => { + const priorityMap: Record = { Software: 1, Consulting: 2, Services: 3, Hardware: 4, Training: 5, Support: 6 }; + return priorityMap[String(row.category)] || 999; + }, + }, + ], + }, +]; + +export function getSalesThemeColors(theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + return { + gray: isDark ? "#f3f4f6" : "#374151", + grayMuted: isDark ? "#f3f4f6" : "#9ca3af", + successHigh: { color: isDark ? "#86efac" : "#15803d", fontWeight: "bold" as const }, + successMedium: isDark ? "#4ade80" : "#16a34a", + successLow: isDark ? "#22c55e" : "#22c55e", + info: isDark ? "#60a5fa" : "#3b82f6", + warning: isDark ? "#facc15" : "#ca8a04", + progressHigh: isDark ? "#34D399" : "#10B981", + progressMedium: isDark ? "#60A5FA" : "#3B82F6", + progressLow: isDark ? "#FBBF24" : "#D97706", + }; +} + +export const salesConfig = { + headers: salesHeaders, + rows: salesData, +} as const; diff --git a/packages/examples/react/src/demos/single-row-children/SingleRowChildrenDemo.tsx b/packages/examples/react/src/demos/single-row-children/SingleRowChildrenDemo.tsx index 29d4bfe67..55e008a87 100644 --- a/packages/examples/react/src/demos/single-row-children/SingleRowChildrenDemo.tsx +++ b/packages/examples/react/src/demos/single-row-children/SingleRowChildrenDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { singleRowChildrenConfig } from "@simple-table/examples-shared"; +import { singleRowChildrenConfig } from "./single-row-children.demo-data"; import "@simple-table/react/styles.css"; const SingleRowChildrenDemo = ({ height = "400px", theme }: { height?: string | number; theme?: Theme }) => { return ( (value as number).toFixed(2) }, + { accessor: "mathGrade", label: "Math", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "scienceGrade", label: "Science", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "englishGrade", label: "English", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "historyGrade", label: "History", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + ], + }, +]; + +export const singleRowChildrenConfig = { + headers: singleRowChildrenHeaders, + rows: singleRowChildrenData, + tableProps: { + columnResizing: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/spreadsheet/SpreadsheetDemo.tsx b/packages/examples/react/src/demos/spreadsheet/SpreadsheetDemo.tsx index fdebb49b5..630a4e2ac 100644 --- a/packages/examples/react/src/demos/spreadsheet/SpreadsheetDemo.tsx +++ b/packages/examples/react/src/demos/spreadsheet/SpreadsheetDemo.tsx @@ -1,16 +1,16 @@ import { useState, useMemo } from "react"; -import { SimpleTable } from "@simple-table/react"; -import type { Theme, ReactHeaderObject, CellChangeProps, HeaderObject } from "@simple-table/react"; -import { spreadsheetConfig, recalculateAmortization } from "@simple-table/examples-shared"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; +import type { Theme, ReactHeaderObject, CellChangeProps } from "@simple-table/react"; +import { spreadsheetConfig, recalculateAmortization } from "./spreadsheet.demo-data"; import "@simple-table/react/styles.css"; -import "@simple-table/examples-shared/styles/spreadsheet-custom.css"; +import "./spreadsheet-custom.css"; const SpreadsheetDemo = ({ height = "400px", theme = "light" }: { height?: string | number; theme?: Theme }) => { const [data, setData] = useState([...spreadsheetConfig.rows]); - const [additionalColumns, setAdditionalColumns] = useState([]); + const [additionalColumns, setAdditionalColumns] = useState([]); const headers = useMemo((): ReactHeaderObject[] => { - const baseHeaders: ReactHeaderObject[] = [...spreadsheetConfig.headers]; + const baseHeaders = defaultHeadersFromCore(spreadsheetConfig.headers); return [ ...baseHeaders, ...additionalColumns, @@ -26,7 +26,7 @@ const SpreadsheetDemo = ({ height = "400px", theme = "light" }: { height?: strin
{ return ( `$${(value as number).toFixed(2)}`, + }, + { accessor: "stock", label: "Stock", width: 100, align: "right", isSortable: true, tooltip: "Available inventory count - number of units currently in warehouse stock" }, + { + accessor: "rating", + label: "Rating", + width: 100, + align: "center", + isSortable: true, + tooltip: "Customer satisfaction rating based on verified purchase reviews (scale: 1-5 stars)", + valueFormatter: ({ value }) => `${value}/5`, + }, + { accessor: "lastUpdated", label: "Last Updated", width: 150, isSortable: true, tooltip: "Most recent inventory update date in YYYY-MM-DD format" }, +]; + +export const tooltipConfig = { + headers: tooltipHeaders, + rows: tooltipData, + tableProps: { + columnResizing: true, + columnReordering: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/react/src/demos/value-formatter/ValueFormatterDemo.tsx b/packages/examples/react/src/demos/value-formatter/ValueFormatterDemo.tsx index 378bde300..8c8a6589a 100644 --- a/packages/examples/react/src/demos/value-formatter/ValueFormatterDemo.tsx +++ b/packages/examples/react/src/demos/value-formatter/ValueFormatterDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/react"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/react"; import type { Theme } from "@simple-table/react"; -import { valueFormatterConfig } from "@simple-table/examples-shared"; +import { valueFormatterConfig } from "./value-formatter.demo-data"; import "@simple-table/react/styles.css"; const ValueFormatterDemo = ({ @@ -12,7 +12,7 @@ const ValueFormatterDemo = ({ }) => { return ( = { + engineering: "ENG", + marketing: "MKT", + sales: "SLS", + product: "PRD", + design: "DSN", + operations: "OPS", +}; + +export const valueFormatterHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { + accessor: "firstName", + label: "Name", + width: 180, + type: "string", + valueFormatter: ({ value, row }) => { + return `${value as string} ${row.lastName as string}`; + }, + }, + { + accessor: "salary", + label: "Salary", + width: 140, + type: "number", + valueFormatter: ({ value }) => { + return `$${(value as number).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; + }, + useFormattedValueForClipboard: true, + useFormattedValueForCSV: true, + }, + { + accessor: "joinDate", + label: "Join Date", + width: 140, + type: "date", + valueFormatter: ({ value }) => { + const date = new Date(value as string); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + { + accessor: "performanceScore", + label: "Performance", + width: 130, + type: "number", + valueFormatter: ({ value }) => `${((value as number) * 100).toFixed(1)}%`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${Math.round((value as number) * 100)}%`, + }, + { + accessor: "balance", + label: "Balance", + width: 120, + type: "number", + valueFormatter: ({ value }) => { + const balance = value as number; + if (balance === 0) return "\u2014"; + if (balance < 0) return `($${Math.abs(balance).toFixed(2)})`; + return `$${balance.toFixed(2)}`; + }, + }, + { + accessor: "department", + label: "Department", + width: 150, + type: "string", + valueFormatter: ({ value }) => (value as string).toUpperCase(), + exportValueGetter: ({ value }) => { + const str = (value as string).toLowerCase(); + const code = DEPARTMENT_CODES[str] || "OTH"; + return `${(value as string).toUpperCase()} (${code})`; + }, + }, +]; + +export const valueFormatterConfig = { + headers: valueFormatterHeaders, + rows: valueFormatterData, + tableProps: { + selectableCells: true, + }, +} as const; diff --git a/packages/examples/react/src/main.tsx b/packages/examples/react/src/main.tsx index 02389465a..b14cb934b 100644 --- a/packages/examples/react/src/main.tsx +++ b/packages/examples/react/src/main.tsx @@ -1,9 +1,9 @@ import React, { Suspense, lazy, useState, useEffect, useCallback } from "react"; import { createRoot } from "react-dom/client"; -import { DEMO_LIST } from "@simple-table/examples-shared"; +import { DEMO_LIST } from "./demo-list"; import { registry } from "./registry"; import type { DemoProps } from "./registry"; -import "../../shared/src/styles/shell.css"; +import "./styles/shell.css"; const lazyComponents = Object.fromEntries( Object.entries(registry).map(([key, loader]) => [key, lazy(loader)]) diff --git a/packages/examples/react/src/styles/shell.css b/packages/examples/react/src/styles/shell.css new file mode 100644 index 000000000..e19fa2fe2 --- /dev/null +++ b/packages/examples/react/src/styles/shell.css @@ -0,0 +1,63 @@ +.examples-shell { + display: flex; + height: 100vh; + overflow: hidden; +} + +.examples-sidebar { + width: 240px; + min-width: 240px; + background: #1e293b; + border-right: 1px solid #334155; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.examples-sidebar-header { + padding: 20px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748b; + border-bottom: 1px solid #334155; +} + +.examples-sidebar-nav { + list-style: none; + padding: 8px 0; + margin: 0; +} + +.examples-sidebar-link { + display: block; + width: 100%; + padding: 10px 20px; + background: none; + border: none; + color: #94a3b8; + text-decoration: none; + font-size: 14px; + font-family: inherit; + text-align: left; + cursor: pointer; + transition: background-color 0.15s, color 0.15s; +} + +.examples-sidebar-link:hover { + background-color: rgba(51, 65, 85, 0.5); + color: #e2e8f0; +} + +.examples-sidebar-link.active { + background-color: rgba(59, 130, 246, 0.15); + color: #60a5fa; + font-weight: 500; +} + +.examples-content { + flex: 1; + overflow-y: auto; + padding: 24px; +} diff --git a/packages/examples/react/vite.config.ts b/packages/examples/react/vite.config.ts index defe49b5d..9bdb6dd02 100644 --- a/packages/examples/react/vite.config.ts +++ b/packages/examples/react/vite.config.ts @@ -13,8 +13,6 @@ export default defineConfig({ { find: "@simple-table/react/styles.css", replacement: path.resolve(__dirname, "../../core/src/styles/base.css") }, { find: "@simple-table/react", replacement: path.resolve(__dirname, "../../react/src/index.ts") }, { find: "simple-table-core", replacement: path.resolve(__dirname, "../../core/src/index.ts") }, - { find: /^@simple-table\/examples-shared\/(.*)$/, replacement: path.resolve(__dirname, "../shared/src/$1") }, - { find: "@simple-table/examples-shared", replacement: path.resolve(__dirname, "../shared/src/index.ts") }, ], }, }); diff --git a/packages/examples/shared/package.json b/packages/examples/shared/package.json deleted file mode 100644 index d43aa7088..000000000 --- a/packages/examples/shared/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@simple-table/examples-shared", - "private": true, - "version": "0.0.1", - "type": "module", - "main": "src/index.ts", - "types": "src/index.ts", - "exports": { - ".": "./src/index.ts", - "./data/*": "./src/data/*", - "./configs/*": "./src/configs/*", - "./types/*": "./src/types/*", - "./utils/*": "./src/utils/*", - "./styles/*": "./src/styles/*" - }, - "dependencies": { - "simple-table-core": "workspace:*" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} diff --git a/packages/examples/shared/src/configs/column-filtering-config.ts b/packages/examples/shared/src/configs/column-filtering-config.ts deleted file mode 100644 index 75a8bb8a6..000000000 --- a/packages/examples/shared/src/configs/column-filtering-config.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { HeaderObject } from "simple-table-core"; -import { COLUMN_FILTERING_DATA } from "../data/column-filtering-data"; - -export const DEPARTMENT_OPTIONS = [ - { label: "Editorial", value: "Editorial" }, - { label: "Production", value: "Production" }, - { label: "Marketing", value: "Marketing" }, - { label: "Sales", value: "Sales" }, - { label: "Operations", value: "Operations" }, - { label: "Human Resources", value: "Human Resources" }, - { label: "Finance", value: "Finance" }, - { label: "Legal", value: "Legal" }, - { label: "IT Support", value: "IT Support" }, - { label: "Customer Service", value: "Customer Service" }, - { label: "Research & Development", value: "Research & Development" }, - { label: "Quality Assurance", value: "Quality Assurance" }, -]; - -export const columnFilteringHeaders: HeaderObject[] = [ - { - accessor: "id", - label: "ID", - width: 80, - type: "number", - isSortable: true, - filterable: true, - }, - { - accessor: "name", - label: "Employee Name", - width: "1fr", - minWidth: 150, - type: "string", - isSortable: true, - filterable: true, - }, - { - accessor: "department", - label: "Department", - width: "1fr", - minWidth: 120, - type: "enum", - isSortable: true, - filterable: true, - enumOptions: DEPARTMENT_OPTIONS, - }, - { - accessor: "role", - label: "Role", - width: 140, - type: "string", - isSortable: true, - filterable: true, - }, - { - accessor: "salary", - label: "Salary", - width: 120, - align: "right", - type: "number", - isSortable: true, - filterable: true, - cellRenderer: ({ row }) => { - const salary = row.salary as number; - return `$${salary.toLocaleString()}`; - }, - }, - { - accessor: "startDate", - label: "Start Date", - width: 130, - type: "date", - isSortable: true, - filterable: true, - }, - { - accessor: "isActive", - label: "Active", - width: 100, - align: "center", - type: "boolean", - isSortable: true, - filterable: true, - }, -]; - -export const columnFilteringConfig = { - headers: columnFilteringHeaders, - rows: COLUMN_FILTERING_DATA, -} as const; diff --git a/packages/examples/shared/src/configs/column-sorting-config.ts b/packages/examples/shared/src/configs/column-sorting-config.ts deleted file mode 100644 index 53ba4def9..000000000 --- a/packages/examples/shared/src/configs/column-sorting-config.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { HeaderObject } from "simple-table-core"; -import { COLUMN_SORTING_DATA } from "../data/column-sorting-data"; - -export const columnSortingHeaders: HeaderObject[] = [ - { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, - { accessor: "name", label: "Name", width: 180, isSortable: true, type: "string" }, - { accessor: "age", label: "Age", width: 80, isSortable: true, type: "number" }, - { accessor: "role", label: "Role", width: 200, isSortable: true, type: "string" }, - { - accessor: "department", - label: "Department", - width: 180, - isSortable: true, - type: "string", - valueFormatter: ({ value }) => { - return (value as string).charAt(0).toUpperCase() + (value as string).slice(1); - }, - }, - { - accessor: "startDate", - label: "Start Date", - width: 140, - isSortable: true, - type: "date", - valueFormatter: ({ value }) => { - if (typeof value === "string") { - return new Date(value).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - } - return String(value); - }, - }, -]; - -export const columnSortingConfig = { - headers: columnSortingHeaders, - rows: COLUMN_SORTING_DATA, - tableProps: { - initialSortColumn: "age", - initialSortDirection: "desc" as const, - }, -} as const; diff --git a/packages/examples/shared/src/configs/index.ts b/packages/examples/shared/src/configs/index.ts deleted file mode 100644 index 68eb4fb49..000000000 --- a/packages/examples/shared/src/configs/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -export { quickStartConfig, quickStartHeaders } from "./quick-start-config"; -export { columnFilteringConfig, columnFilteringHeaders, DEPARTMENT_OPTIONS } from "./column-filtering-config"; -export { columnSortingConfig, columnSortingHeaders } from "./column-sorting-config"; -export { valueFormatterConfig, valueFormatterHeaders, valueFormatterData } from "./value-formatter-config"; -export { paginationConfig, paginationHeaders, paginationData, PAGINATION_ROWS_PER_PAGE } from "./pagination-config"; -export { columnPinningConfig, columnPinningHeaders, columnPinningData } from "./column-pinning-config"; -export { columnAlignmentConfig, columnAlignmentHeaders, columnAlignmentData } from "./column-alignment-config"; -export { columnWidthConfig, columnWidthHeaders, columnWidthData } from "./column-width-config"; -export { columnResizingConfig, columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "./column-resizing-config"; -export { columnReorderingConfig, columnReorderingHeaders, columnReorderingData } from "./column-reordering-config"; -export { columnSelectionConfig, columnSelectionHeaders, columnSelectionData } from "./column-selection-config"; -export { columnEditingConfig, columnEditingHeaders, columnEditingData } from "./column-editing-config"; -export { cellEditingConfig, cellEditingHeaders, cellEditingData } from "./cell-editing-config"; -export { cellHighlightingConfig, cellHighlightingHeaders, cellHighlightingData } from "./cell-highlighting-config"; -export { themesConfig, themesHeaders, themesData, AVAILABLE_THEMES } from "./themes-config"; -export { rowHeightConfig, rowHeightHeaders, rowHeightData } from "./row-height-config"; -export { tableHeightConfig, tableHeightHeaders, tableHeightData } from "./table-height-config"; -export { quickFilterConfig, quickFilterHeaders, quickFilterData } from "./quick-filter-config"; -export { nestedHeadersConfig, nestedHeadersHeaders, nestedHeadersData } from "./nested-headers-config"; -export { aggregateFunctionsConfig, aggregateFunctionsHeaders, aggregateFunctionsData } from "./aggregate-functions-config"; -export { collapsibleColumnsConfig, collapsibleColumnsHeaders, collapsibleColumnsData } from "./collapsible-columns-config"; -export { externalSortConfig, externalSortHeaders, externalSortData } from "./external-sort-config"; -export { externalFilterConfig, externalFilterHeaders, externalFilterData, matchesFilter } from "./external-filter-config"; -export { loadingStateConfig, loadingStateHeaders, loadingStateData } from "./loading-state-config"; -export { infiniteScrollConfig, infiniteScrollHeaders, generateInfiniteScrollData } from "./infinite-scroll-config"; -export { rowSelectionConfig, rowSelectionHeaders, rowSelectionData } from "./row-selection-config"; -export type { LibraryBook } from "./row-selection-config"; -export { csvExportConfig, csvExportHeaders, csvExportData } from "./csv-export-config"; -export { programmaticControlConfig, programmaticControlHeaders, programmaticControlData, STATUS_COLORS as PROGRAMMATIC_CONTROL_STATUS_COLORS } from "./programmatic-control-config"; -export { rowGroupingConfig, rowGroupingHeaders, rowGroupingData } from "./row-grouping-config"; -export { cellRendererConfig, cellRendererHeaders, cellRendererData } from "./cell-renderer-config"; -export type { CellRendererEmployee } from "./cell-renderer-config"; -export { headerRendererConfig, headerRendererHeaders, headerRendererData } from "./header-renderer-config"; -export { footerRendererConfig, footerRendererHeaders, footerRendererData } from "./footer-renderer-config"; -export { cellClickingConfig, cellClickingHeaders, cellClickingData, STATUSES as CELL_CLICKING_STATUSES } from "./cell-clicking-config"; -export type { ProjectTask } from "./cell-clicking-config"; -export { tooltipConfig, tooltipHeaders, tooltipData } from "./tooltip-config"; -export { customThemeConfig, customThemeHeaders, customThemeData } from "./custom-theme-config"; -export { customIconsConfig, customIconsHeaders, customIconsData, createSvgIcon, ICON_PATHS, buildVanillaCustomIcons } from "./custom-icons-config"; -export { emptyStateConfig, emptyStateHeaders, emptyStateData, buildEmptyStateElement } from "./empty-state-config"; -export { columnVisibilityConfig, columnVisibilityHeaders, columnVisibilityData } from "./column-visibility-config"; -export { columnEditorCustomRendererConfig, columnEditorCustomRendererHeaders, columnEditorCustomRendererData, COLUMN_EDITOR_TEXT, COLUMN_EDITOR_SEARCH_PLACEHOLDER, buildVanillaColumnEditorRowRenderer } from "./column-editor-custom-renderer-config"; -export { singleRowChildrenConfig, singleRowChildrenHeaders, singleRowChildrenData } from "./single-row-children-config"; -export { nestedTablesConfig, nestedTablesHeaders, nestedTablesDivisionHeaders, generateNestedTablesData } from "./nested-tables-config"; -export { dynamicNestedTablesConfig, dynamicNestedTablesCompanyHeaders, dynamicNestedTablesDivisionHeaders, dynamicNestedTablesData, fetchDivisionsForCompany } from "./dynamic-nested-tables-config"; -export type { DynamicCompany, DynamicDivision } from "./dynamic-nested-tables-config"; -export { dynamicRowLoadingConfig, dynamicRowLoadingHeaders, generateInitialRegions, fetchStoresForRegion, fetchProductsForStore } from "./dynamic-row-loading-config"; -export type { DynamicRegion, DynamicStore, DynamicProduct } from "./dynamic-row-loading-config"; -export { chartsConfig, chartsHeaders, chartsData } from "./charts-config"; -export { liveUpdateConfig, liveUpdateHeaders, liveUpdateData, generateStockHistory, generateSalesHistory } from "./live-update-config"; -export { crmConfig, crmHeaders, crmData, generateCRMData, CRM_FOOTER_COLORS_LIGHT, CRM_FOOTER_COLORS_DARK, CRM_THEME_COLORS_LIGHT, CRM_THEME_COLORS_DARK, generateVisiblePages } from "./crm-config"; -export { infrastructureConfig, infrastructureHeaders, infrastructureData, generateInfrastructureData, INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors } from "./infrastructure-config"; -export { musicConfig, musicHeaders, musicData, generateMusicData, getMusicThemeColors, MUSIC_THEME_COLORS } from "./music-config"; -export { billingConfig, billingHeaders, billingData, generateBillingData } from "./billing-config"; -export { manufacturingConfig, manufacturingHeaders, manufacturingData, generateManufacturingData, getManufacturingStatusColors } from "./manufacturing-config"; -export { hrConfig, hrHeaders, hrData, generateHRData, getHRThemeColors, HR_STATUS_COLOR_MAP } from "./hr-config"; -export type { HRTagColorKey } from "./hr-config"; -export { salesConfig, salesHeaders, salesData, generateSalesData, getSalesThemeColors } from "./sales-config"; -export { spreadsheetConfig, spreadsheetHeaders, spreadsheetData, generateSpreadsheetData, recalculateAmortization } from "./spreadsheet-config"; diff --git a/packages/examples/shared/src/configs/quick-start-config.ts b/packages/examples/shared/src/configs/quick-start-config.ts deleted file mode 100644 index b26ae1c70..000000000 --- a/packages/examples/shared/src/configs/quick-start-config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { HeaderObject } from "simple-table-core"; -import { QUICK_START_DATA } from "../data/quick-start-data"; - -export const quickStartHeaders: HeaderObject[] = [ - { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, - { - accessor: "name", - label: "Name", - minWidth: 80, - width: "1fr", - isSortable: true, - type: "string", - }, - { accessor: "age", label: "Age", width: 100, isSortable: true, type: "number" }, - { accessor: "role", label: "Role", width: 150, isSortable: true, type: "string" }, - { accessor: "department", label: "Department", width: 150, isSortable: true, type: "string" }, - { accessor: "startDate", label: "Start Date", width: 150, isSortable: true, type: "date" }, -]; - -export const quickStartConfig = { - headers: quickStartHeaders, - rows: QUICK_START_DATA, - tableProps: { - editColumns: true, - selectableCells: true, - customTheme: { rowHeight: 32 }, - }, -} as const; diff --git a/packages/examples/shared/src/data/index.ts b/packages/examples/shared/src/data/index.ts deleted file mode 100644 index 9b63ed1de..000000000 --- a/packages/examples/shared/src/data/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { QUICK_START_DATA } from "./quick-start-data"; -export { COLUMN_FILTERING_DATA } from "./column-filtering-data"; -export { COLUMN_SORTING_DATA } from "./column-sorting-data"; diff --git a/packages/examples/shared/src/index.ts b/packages/examples/shared/src/index.ts deleted file mode 100644 index c4790dbec..000000000 --- a/packages/examples/shared/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./data"; -export * from "./configs"; -export * from "./types"; -export * from "./utils"; diff --git a/packages/examples/shared/src/types/billing.ts b/packages/examples/shared/src/types/billing.ts deleted file mode 100644 index 0792e9f2f..000000000 --- a/packages/examples/shared/src/types/billing.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface BillingRow { - id: string | number; - name: string; - type: string; - amount: number; - deferredRevenue: number; - recognizedRevenue: number; - invoices?: BillingRow[]; - charges?: BillingRow[]; - [key: `balance_${string}`]: number; - [key: `revenue_${string}`]: number; -} diff --git a/packages/examples/shared/src/types/crm.ts b/packages/examples/shared/src/types/crm.ts deleted file mode 100644 index 0ba6ed241..000000000 --- a/packages/examples/shared/src/types/crm.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type CRMLead = { - id: number; - name: string; - title: string; - company: string; - linkedin: boolean; - signal: string; - aiScore: number; - emailStatus: string; - timeAgo: string; - list: string; -}; diff --git a/packages/examples/shared/src/types/employee.ts b/packages/examples/shared/src/types/employee.ts deleted file mode 100644 index 46526756d..000000000 --- a/packages/examples/shared/src/types/employee.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface Employee { - id: number; - name: string; - age: number; - role: string; - department: string; - startDate: string; - salary?: number; - isActive?: boolean; -} diff --git a/packages/examples/shared/src/types/hr.ts b/packages/examples/shared/src/types/hr.ts deleted file mode 100644 index 2be3b3a9d..000000000 --- a/packages/examples/shared/src/types/hr.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface HREmployee { - id: number; - firstName: string; - lastName: string; - fullName: string; - position: string; - performanceScore: number; - department: string; - email: string; - location: string; - hireDate: string; - yearsOfService: number; - salary: number; - status: string; - isRemoteEligible: boolean; -} diff --git a/packages/examples/shared/src/types/index.ts b/packages/examples/shared/src/types/index.ts deleted file mode 100644 index 5787475b8..000000000 --- a/packages/examples/shared/src/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type { Employee } from "./employee"; -export type { CRMLead } from "./crm"; -export type { InfrastructureServer } from "./infrastructure"; -export type { MusicArtist } from "./music"; -export type { BillingRow } from "./billing"; -export type { ManufacturingRow } from "./manufacturing"; -export type { HREmployee } from "./hr"; -export type { SalesRow } from "./sales"; -export type { SpreadsheetRow } from "./spreadsheet"; diff --git a/packages/examples/shared/src/types/infrastructure.ts b/packages/examples/shared/src/types/infrastructure.ts deleted file mode 100644 index 5116daee2..000000000 --- a/packages/examples/shared/src/types/infrastructure.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface InfrastructureServer { - id: number; - serverId: string; - serverName: string; - cpuUsage: number; - cpuHistory: number[]; - memoryUsage: number; - diskUsage: number; - responseTime: number; - networkIn: number; - networkOut: number; - activeConnections: number; - requestsPerSec: number; - status: "online" | "warning" | "critical" | "maintenance" | "offline"; -} diff --git a/packages/examples/shared/src/types/manufacturing.ts b/packages/examples/shared/src/types/manufacturing.ts deleted file mode 100644 index 843fde8c5..000000000 --- a/packages/examples/shared/src/types/manufacturing.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface ManufacturingRow { - id: string; - productLine: string; - station: string; - machineType: string; - status: string; - outputRate: number; - cycletime: number; - efficiency: number; - defectRate: number; - defectCount: number; - downtime: number; - utilization: number; - energy: number; - maintenanceDate: string; - stations?: ManufacturingRow[]; -} diff --git a/packages/examples/shared/src/types/music.ts b/packages/examples/shared/src/types/music.ts deleted file mode 100644 index 3a82a349a..000000000 --- a/packages/examples/shared/src/types/music.ts +++ /dev/null @@ -1,57 +0,0 @@ -export interface MusicArtist { - id: number; - rank: 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; -} diff --git a/packages/examples/shared/src/types/sales.ts b/packages/examples/shared/src/types/sales.ts deleted file mode 100644 index d9fb0b718..000000000 --- a/packages/examples/shared/src/types/sales.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface SalesRow { - id: number; - repName: string; - dealSize: number; - dealValue: number; - isWon: boolean; - closeDate: string; - commission: number; - profitMargin: number; - dealProfit: number; - category: string; -} diff --git a/packages/examples/shared/src/types/spreadsheet.ts b/packages/examples/shared/src/types/spreadsheet.ts deleted file mode 100644 index ede445bd3..000000000 --- a/packages/examples/shared/src/types/spreadsheet.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface SpreadsheetRow { - id: number; - principal: number; - interestRate: number; - monthlyPayment: number; - remainingBalance: number; - totalPaid: number; - interestPaid: number; -} diff --git a/packages/examples/shared/tsconfig.json b/packages/examples/shared/tsconfig.json deleted file mode 100644 index fe6fb539c..000000000 --- a/packages/examples/shared/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"] -} diff --git a/packages/examples/solid/package.json b/packages/examples/solid/package.json index 898844611..dbc14310d 100644 --- a/packages/examples/solid/package.json +++ b/packages/examples/solid/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@simple-table/solid": "workspace:*", - "@simple-table/examples-shared": "workspace:*", "solid-js": "^1.0.0" }, "devDependencies": { diff --git a/packages/examples/solid/src/demo-list.ts b/packages/examples/solid/src/demo-list.ts new file mode 100644 index 000000000..7c1935b68 --- /dev/null +++ b/packages/examples/solid/src/demo-list.ts @@ -0,0 +1,57 @@ +export const DEMO_LIST = [ + { id: "quick-start", label: "Quick Start" }, + { id: "column-filtering", label: "Column Filtering" }, + { id: "column-sorting", label: "Column Sorting" }, + { id: "value-formatter", label: "Value Formatter" }, + { id: "pagination", label: "Pagination" }, + { id: "column-pinning", label: "Column Pinning" }, + { id: "column-alignment", label: "Column Alignment" }, + { id: "column-width", label: "Column Width" }, + { id: "column-resizing", label: "Column Resizing" }, + { id: "column-reordering", label: "Column Reordering" }, + { id: "column-selection", label: "Column Selection" }, + { id: "column-editing", label: "Column Editing" }, + { id: "cell-editing", label: "Cell Editing" }, + { id: "cell-highlighting", label: "Cell Highlighting" }, + { id: "themes", label: "Themes" }, + { id: "row-height", label: "Row Height" }, + { id: "table-height", label: "Table Height" }, + { id: "quick-filter", label: "Quick Filter" }, + { id: "nested-headers", label: "Nested Headers" }, + { id: "external-sort", label: "External Sort" }, + { id: "external-filter", label: "External Filter" }, + { id: "loading-state", label: "Loading State" }, + { id: "infinite-scroll", label: "Infinite Scroll" }, + { id: "row-selection", label: "Row Selection" }, + { id: "csv-export", label: "CSV Export" }, + { id: "programmatic-control", label: "Programmatic Control" }, + { id: "row-grouping", label: "Row Grouping" }, + { id: "aggregate-functions", label: "Aggregate Functions" }, + { id: "collapsible-columns", label: "Collapsible Columns" }, + { id: "cell-renderer", label: "Cell Renderer" }, + { id: "header-renderer", label: "Header Renderer" }, + { id: "footer-renderer", label: "Footer Renderer" }, + { id: "cell-clicking", label: "Cell Clicking" }, + { id: "tooltip", label: "Tooltip" }, + { id: "custom-theme", label: "Custom Theme" }, + { id: "custom-icons", label: "Custom Icons" }, + { id: "empty-state", label: "Empty State" }, + { id: "column-visibility", label: "Column Visibility" }, + { id: "column-editor-custom-renderer", label: "Column Editor Custom Renderer" }, + { id: "single-row-children", label: "Single Row Children" }, + { id: "nested-tables", label: "Nested Tables" }, + { id: "dynamic-nested-tables", label: "Dynamic Nested Tables" }, + { id: "dynamic-row-loading", label: "Dynamic Row Loading" }, + { id: "charts", label: "Charts" }, + { id: "live-update", label: "Live Update" }, + { id: "crm", label: "CRM" }, + { id: "infrastructure", label: "Infrastructure" }, + { id: "music", label: "Music" }, + { id: "billing", label: "Billing" }, + { id: "manufacturing", label: "Manufacturing" }, + { id: "hr", label: "HR" }, + { id: "sales", label: "Sales" }, + { id: "spreadsheet", label: "Spreadsheet" }, +] as const; + +export type DemoId = (typeof DEMO_LIST)[number]["id"]; diff --git a/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx b/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx index 090d5c511..4bb6ba5f4 100644 --- a/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx +++ b/packages/examples/solid/src/demos/aggregate-functions/AggregateFunctionsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { aggregateFunctionsConfig } from "@simple-table/examples-shared"; +import { aggregateFunctionsConfig } from "./aggregate-functions.demo-data"; import "@simple-table/solid/styles.css"; export default function AggregateFunctionsDemo(props: { @@ -9,7 +9,7 @@ export default function AggregateFunctionsDemo(props: { }) { return ( { + if (typeof value === "number") { + return value >= 1000000 + ? `${(value / 1000000).toFixed(1)}M` + : value >= 1000 + ? `${(value / 1000).toFixed(0)}K` + : value.toString(); + } + return ""; + }, + }, + { + accessor: "revenue", + label: "Monthly Revenue", + width: 140, + type: "string", + aggregation: { + type: "sum", + parseValue: (value) => { + if (typeof value !== "string") return 0; + const numericValue = parseFloat(value.replace(/[$K]/g, "")); + return isNaN(numericValue) ? 0 : numericValue; + }, + }, + valueFormatter: ({ value }) => { + if (typeof value === "number") return `$${value.toFixed(1)}K`; + if (typeof value === "string") return value; + return ""; + }, + }, + { + accessor: "rating", + label: "Rating", + width: 100, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${value.toFixed(1)} ⭐` : ""), + }, + { + accessor: "contentCount", + label: "Content", + width: 90, + type: "number", + aggregation: { type: "sum" }, + }, + { + accessor: "avgViewTime", + label: "Avg Watch Time", + width: 130, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${Math.round(value)}min` : ""), + }, + { accessor: "status", label: "Status", width: 120, type: "string" }, +]; + +export const aggregateFunctionsData = [ + { + id: 1, + name: "StreamFlix", + status: "Leading Platform", + categories: [ + { + id: 101, + name: "Gaming", + status: "Trending", + creators: [ + { id: 1001, name: "PixelMaster", followers: 2800000, revenue: "$45.2K", rating: 4.8, contentCount: 328, avgViewTime: 45, status: "Partner" }, + { id: 1002, name: "RetroGamer93", followers: 1200000, revenue: "$28.5K", rating: 4.6, contentCount: 156, avgViewTime: 52, status: "Partner" }, + { id: 1003, name: "SpeedrunQueen", followers: 890000, revenue: "$22.1K", rating: 4.9, contentCount: 89, avgViewTime: 38, status: "Partner" }, + ], + }, + { + id: 102, + name: "Music & Arts", + status: "Growing", + creators: [ + { id: 1101, name: "MelodyMaker", followers: 1650000, revenue: "$31.8K", rating: 4.7, contentCount: 203, avgViewTime: 28, status: "Partner" }, + { id: 1102, name: "DigitalArtist", followers: 720000, revenue: "$18.9K", rating: 4.5, contentCount: 127, avgViewTime: 35, status: "Affiliate" }, + { id: 1103, name: "JazzVibez", followers: 430000, revenue: "$12.4K", rating: 4.8, contentCount: 78, avgViewTime: 42, status: "Affiliate" }, + ], + }, + { + id: 103, + name: "Cooking & Lifestyle", + status: "Stable", + creators: [ + { id: 1201, name: "ChefExtraordinaire", followers: 3200000, revenue: "$58.7K", rating: 4.9, contentCount: 245, avgViewTime: 22, status: "Partner" }, + { id: 1202, name: "HomeDecorGuru", followers: 980000, revenue: "$19.3K", rating: 4.4, contentCount: 134, avgViewTime: 18, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 2, + name: "WatchNow", + status: "Competitor", + categories: [ + { + id: 201, + name: "Tech Reviews", + status: "Hot", + creators: [ + { id: 2001, name: "TechGuru2024", followers: 2100000, revenue: "$42.6K", rating: 4.6, contentCount: 189, avgViewTime: 35, status: "Partner" }, + { id: 2002, name: "GadgetWhisperer", followers: 1450000, revenue: "$29.1K", rating: 4.7, contentCount: 156, avgViewTime: 31, status: "Partner" }, + { id: 2003, name: "CodeReviewer", followers: 680000, revenue: "$16.8K", rating: 4.8, contentCount: 94, avgViewTime: 48, status: "Affiliate" }, + ], + }, + { + id: 202, + name: "Fitness & Health", + status: "Growing", + creators: [ + { id: 2101, name: "FitnessPhenom", followers: 1890000, revenue: "$35.4K", rating: 4.5, contentCount: 312, avgViewTime: 25, status: "Partner" }, + { id: 2102, name: "YogaMaster", followers: 1100000, revenue: "$21.7K", rating: 4.9, contentCount: 178, avgViewTime: 33, status: "Partner" }, + ], + }, + ], + }, + { + id: 3, + name: "CreativeSpace", + status: "Emerging", + categories: [ + { + id: 301, + name: "Photography", + status: "Niche", + creators: [ + { id: 3001, name: "LensArtist", followers: 750000, revenue: "$18.2K", rating: 4.7, contentCount: 145, avgViewTime: 27, status: "Partner" }, + { id: 3002, name: "NatureShooter", followers: 520000, revenue: "$13.5K", rating: 4.6, contentCount: 98, avgViewTime: 29, status: "Affiliate" }, + { id: 3003, name: "PortraitPro", followers: 390000, revenue: "$9.8K", rating: 4.8, contentCount: 67, avgViewTime: 24, status: "Affiliate" }, + ], + }, + { + id: 302, + name: "Animation & VFX", + status: "Specialized", + creators: [ + { id: 3101, name: "3DAnimator", followers: 640000, revenue: "$15.9K", rating: 4.9, contentCount: 58, avgViewTime: 41, status: "Partner" }, + { id: 3102, name: "VFXWizard", followers: 480000, revenue: "$12.3K", rating: 4.7, contentCount: 42, avgViewTime: 38, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 4, + name: "EduStream", + status: "Educational Focus", + categories: [ + { + id: 401, + name: "Science & Math", + status: "Educational", + creators: [ + { id: 4001, name: "MathExplainer", followers: 1340000, revenue: "$26.8K", rating: 4.8, contentCount: 234, avgViewTime: 36, status: "Partner" }, + { id: 4002, name: "PhysicsPhun", followers: 890000, revenue: "$19.4K", rating: 4.6, contentCount: 167, avgViewTime: 42, status: "Partner" }, + { id: 4003, name: "ChemistryLab", followers: 560000, revenue: "$14.2K", rating: 4.7, contentCount: 89, avgViewTime: 33, status: "Affiliate" }, + ], + }, + { + id: 402, + name: "History & Culture", + status: "Informative", + creators: [ + { id: 4101, name: "HistoryBuff", followers: 920000, revenue: "$18.6K", rating: 4.5, contentCount: 145, avgViewTime: 39, status: "Partner" }, + { id: 4102, name: "CultureExplorer", followers: 670000, revenue: "$15.1K", rating: 4.8, contentCount: 112, avgViewTime: 45, status: "Affiliate" }, + ], + }, + ], + }, +]; + +export const aggregateFunctionsConfig = { + headers: aggregateFunctionsHeaders, + rows: aggregateFunctionsData, + tableProps: { + rowGrouping: ["categories", "creators"] as string[], + columnResizing: true, + }, +} as const; diff --git a/packages/examples/solid/src/demos/billing/BillingDemo.tsx b/packages/examples/solid/src/demos/billing/BillingDemo.tsx index 1b8108941..746dc7006 100644 --- a/packages/examples/solid/src/demos/billing/BillingDemo.tsx +++ b/packages/examples/solid/src/demos/billing/BillingDemo.tsx @@ -1,22 +1,24 @@ -import { SimpleTable } from "@simple-table/solid"; -import type { Theme, SolidHeaderObject } from "@simple-table/solid"; -import { billingConfig } from "@simple-table/examples-shared"; -import type { BillingRow } from "@simple-table/examples-shared"; +import { SimpleTable, mapToSolidHeaderObjects } from "@simple-table/solid"; +import type { Theme, SolidHeaderObject, 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: SolidHeaderObject[] = billingConfig.headers.map((h) => { - if (h.accessor === "name") { - return { - ...h, - cellRenderer: ({ row }) => { - const d = row as unknown as BillingRow; - return
{d.name}
; - }, - }; - } - return h; - }); + const headers: SolidHeaderObject[] = mapToSolidHeaderObjects( + billingConfig.headers.map((h) => { + if (h.accessor === "name") { + return { + ...h, + cellRenderer: ({ row }: CellRendererProps) => { + const d = row as unknown as BillingRow; + return
{d.name}
; + }, + }; + } + return h; + }), + ); return ( { + const data: Record = {}; + const year = 2024; + for (let m = 0; m < 12; m++) { + const mo = months[m]; + const base = Math.round((1000 + Math.random() * 50000) * 100) / 100; + data[`balance_${mo}_${year}`] = base; + data[`revenue_${mo}_${year}`] = Math.round(base * (0.6 + Math.random() * 0.3) * 100) / 100; + } + return data; +} + +export function generateBillingData(count: number = 30): BillingRow[] { + const rows: BillingRow[] = []; + for (let i = 0; i < count; i++) { + const accountName = ACCOUNT_NAMES[i % ACCOUNT_NAMES.length]; + const totalAmount = Math.round((5000 + Math.random() * 200000) * 100) / 100; + const recognized = Math.round(totalAmount * (0.3 + Math.random() * 0.5) * 100) / 100; + const invoices: BillingRow[] = []; + const invoiceCount = 2 + Math.floor(Math.random() * 3); + for (let j = 0; j < invoiceCount; j++) { + const invAmount = Math.round((totalAmount / invoiceCount) * 100) / 100; + const invRecognized = Math.round(invAmount * (0.4 + Math.random() * 0.4) * 100) / 100; + const charges: BillingRow[] = []; + const chargeCount = 1 + Math.floor(Math.random() * 3); + for (let k = 0; k < chargeCount; k++) { + const chargeAmount = Math.round((invAmount / chargeCount) * 100) / 100; + charges.push({ + id: `${i + 1}-inv${j + 1}-chg${k + 1}`, + name: CHARGE_TYPES[k % CHARGE_TYPES.length], + type: "charge", + amount: chargeAmount, + deferredRevenue: Math.round(chargeAmount * 0.3 * 100) / 100, + recognizedRevenue: Math.round(chargeAmount * 0.7 * 100) / 100, + ...generateMonthlyData(), + }); + } + invoices.push({ + id: `${i + 1}-inv${j + 1}`, + name: `${INVOICE_PREFIXES[j % 3]}-${String(i * 10 + j + 1).padStart(4, "0")}`, + type: "invoice", + amount: invAmount, + deferredRevenue: Math.round((invAmount - invRecognized) * 100) / 100, + recognizedRevenue: invRecognized, + charges, + ...generateMonthlyData(), + }); + } + rows.push({ + id: i + 1, + name: accountName, + type: "account", + amount: totalAmount, + deferredRevenue: Math.round((totalAmount - recognized) * 100) / 100, + recognizedRevenue: recognized, + invoices, + ...generateMonthlyData(), + }); + } + return rows; +} + +export const billingData = generateBillingData(30); + +function generateMonthHeaders(): HeaderObject[] { + const headers: HeaderObject[] = []; + const year = 2024; + for (let monthIndex = 11; monthIndex >= 0; monthIndex--) { + const fullMonthName = new Date(year, monthIndex).toLocaleString("default", { month: "long" }); + const mo = months[monthIndex]; + headers.push({ + accessor: `month_${mo}_${year}`, + label: `${fullMonthName} ${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + children: [ + { + disableReorder: true, + label: "Balance", + accessor: `balance_${mo}_${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + disableReorder: true, + label: "Revenue", + accessor: `revenue_${mo}_${year}`, + width: 200, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + ], + }); + } + return headers; +} + +export const billingHeaders: HeaderObject[] = [ + { + accessor: "name", + label: "Name", + width: 250, + expandable: true, + isSortable: true, + isEditable: false, + align: "left", + pinned: "left", + type: "string", + }, + { + accessor: "amount", + label: "Total Amount", + width: 130, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + accessor: "deferredRevenue", + label: "Deferred Revenue", + width: 180, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + { + accessor: "recognizedRevenue", + label: "Recognized Revenue", + width: 180, + isSortable: true, + isEditable: false, + align: "right", + type: "number", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => { + if (typeof value !== "number" || value === 0) return "—"; + return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + }, + ...generateMonthHeaders(), +]; + +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 38d87aee3..f1d8d0de8 100644 --- a/packages/examples/solid/src/demos/cell-clicking/CellClickingDemo.tsx +++ b/packages/examples/solid/src/demos/cell-clicking/CellClickingDemo.tsx @@ -1,8 +1,8 @@ import { createSignal, createMemo, Show, For } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import { SimpleTable, mapToSolidHeaderObjects } from "@simple-table/solid"; import type { Theme, SolidHeaderObject, CellRendererProps, CellClickProps } from "@simple-table/solid"; -import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "@simple-table/examples-shared"; -import type { ProjectTask } from "@simple-table/examples-shared"; +import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "./cell-clicking.demo-data"; +import type { ProjectTask } from "./cell-clicking.demo-data"; import "@simple-table/solid/styles.css"; const DETAIL_KEYS = ["task", "details", "assignee", "status", "priority"] as const; @@ -12,7 +12,7 @@ export default function CellClickingDemo(props: { height?: string | number; them const [selectedTask, setSelectedTask] = createSignal(null); const [rows, setRows] = createSignal([...cellClickingData]); - const headers: SolidHeaderObject[] = cellClickingHeaders.map((h) => { + const headers: SolidHeaderObject[] = mapToSolidHeaderObjects(cellClickingHeaders.map((h) => { if (h.accessor === "priority") { return { ...h, @@ -75,8 +75,8 @@ export default function CellClickingDemo(props: { height?: string | number; them ), }; } - return { ...h }; - }); + return h; + })); const isDark = createMemo(() => props.theme === "modern-dark" || props.theme === "dark"); 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 new file mode 100644 index 000000000..4271dbe0c --- /dev/null +++ b/packages/examples/solid/src/demos/cell-clicking/cell-clicking.demo-data.ts @@ -0,0 +1,47 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/solid"; + + +export type ProjectTask = { + id: number; + task: string; + assignee: string; + priority: string; + status: string; + dueDate: string; + estimatedHours: number; + completedHours: number; + details: string; +}; + +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" }, +]; + +export const cellClickingHeaders: HeaderObject[] = [ + { accessor: "id", label: "Task ID", width: 80, isSortable: true, type: "number" }, + { accessor: "task", label: "Task Name", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "assignee", label: "Assignee", width: 120, isSortable: true, type: "string" }, + { accessor: "priority", label: "Priority", width: 100, isSortable: true, type: "string" }, + { accessor: "status", label: "Status", width: 120, isSortable: true, type: "string" }, + { accessor: "dueDate", label: "Due Date", width: 120, isSortable: true, type: "date" }, + { accessor: "estimatedHours", label: "Est. Hours", width: 100, isSortable: true, type: "number" }, + { accessor: "completedHours", label: "Done Hours", width: 100, isSortable: true, type: "number" }, + { accessor: "details", label: "View Details", width: 120, type: "other" }, +]; + +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 3d17ed6d9..a12054fd8 100644 --- a/packages/examples/solid/src/demos/cell-editing/CellEditingDemo.tsx +++ b/packages/examples/solid/src/demos/cell-editing/CellEditingDemo.tsx @@ -1,7 +1,7 @@ import { createSignal } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, CellChangeProps } from "@simple-table/solid"; -import { cellEditingConfig } from "@simple-table/examples-shared"; +import { cellEditingConfig } from "./cell-editing.demo-data"; import "@simple-table/solid/styles.css"; export default function CellEditingDemo(props: { height?: string | number; theme?: Theme }) { @@ -15,7 +15,7 @@ export default function CellEditingDemo(props: { height?: string | number; theme return ( @@ -142,19 +142,22 @@ const TagsCell = (props: CellRendererProps) => { ); }; -const HEADERS: SolidHeaderObject[] = cellRendererConfig.headers.map((h) => { - const renderers: Record any> = { - teamMembers: TeamCell, - website: WebsiteCell, - status: StatusCell, - progress: ProgressCell, - rating: RatingCell, - verified: VerifiedCell, - tags: TagsCell, - }; - const cellRenderer = renderers[h.accessor as string]; - return cellRenderer ? { ...h, cellRenderer } : { ...h }; -}); +const RENDERER_MAP: Record unknown> = { + teamMembers: TeamCell, + website: WebsiteCell, + status: StatusCell, + progress: ProgressCell, + rating: RatingCell, + verified: VerifiedCell, + tags: TagsCell, +}; + +const HEADERS: SolidHeaderObject[] = mapToSolidHeaderObjects( + cellRendererConfig.headers.map((h) => { + const fn = RENDERER_MAP[String(h.accessor)]; + return fn !== undefined ? { ...h, cellRenderer: fn } : h; + }), +); export default function CellRendererDemo(props: { height?: string | number; theme?: Theme }) { return ( 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 new file mode 100644 index 000000000..9b18a3cd3 --- /dev/null +++ b/packages/examples/solid/src/demos/cell-renderer/cell-renderer.demo-data.ts @@ -0,0 +1,51 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export type CellRendererEmployee = { + id: number; + name: string; + website: string; + status: string; + progress: number; + rating: number; + verified: boolean; + tags: string[]; + teamMembers: { name: string; role: string }[]; +}; + +export const cellRendererData: CellRendererEmployee[] = [ + { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] }, + { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] }, + { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] }, + { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] }, + { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] }, + { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] }, + { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] }, + { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] }, + { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] }, + { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] }, + { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 180, type: "string" }, + { accessor: "teamMembers", label: "Team", width: 280, type: "string" }, + { accessor: "website", label: "Website", width: 180, type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, + { accessor: "progress", label: "Progress", width: 150, type: "number" }, + { accessor: "rating", label: "Rating", width: 150, type: "number" }, + { accessor: "verified", label: "Verified", width: 100, type: "boolean" }, + { accessor: "tags", label: "Tags", width: 250, type: "string" }, +]; + +export const cellRendererConfig = { + headers: cellRendererHeaders, + rows: cellRendererData, + tableProps: { + 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 85baea5a7..1c35bd5b4 100644 --- a/packages/examples/solid/src/demos/charts/ChartsDemo.tsx +++ b/packages/examples/solid/src/demos/charts/ChartsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { chartsConfig } from "@simple-table/examples-shared"; +import { chartsConfig } from "./charts.demo-data"; import "@simple-table/solid/styles.css"; export default function ChartsDemo(props: { height?: string | number; theme?: Theme }) { @@ -8,7 +8,7 @@ export default function ChartsDemo(props: { height?: string | number; theme?: Th { + const data: number[] = []; + let current = baseValue; + for (let i = 0; i < length; i++) { + const change = (Math.random() - 0.5) * volatility; + current = Math.max(0, current + change); + data.push(Math.round(current * 100) / 100); + } + return data; +}; + +export const chartsData = [ + { 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 }, + { id: 4, product: 'Monitor 27"', 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 }, + { id: 5, product: "Keyboard Mechanical", category: "Accessories", monthlySales: generateTrendData(200, 40, 12), dailyViews: generateTrendData(600, 120, 30), quarterlyRevenue: [18000, 22000, 25000, 28000], weeklyOrders: [32, 38, 35, 42, 45, 48, 52], rating: 4.8 }, + { id: 6, product: "Webcam HD", category: "Electronics", monthlySales: generateTrendData(120, 30, 12), dailyViews: generateTrendData(450, 90, 30), quarterlyRevenue: [15000, 17000, 16000, 19000], weeklyOrders: [18, 22, 20, 25, 28, 31, 29], rating: 4.3 }, + { id: 7, product: "Headphones Bluetooth", category: "Audio", monthlySales: generateTrendData(250, 60, 12), dailyViews: generateTrendData(900, 180, 30), quarterlyRevenue: [28000, 32000, 35000, 38000], weeklyOrders: [42, 48, 45, 52, 55, 58, 62], rating: 4.4 }, + { id: 8, product: "Phone Case", category: "Accessories", monthlySales: generateTrendData(600, 100, 12), dailyViews: generateTrendData(1500, 250, 30), quarterlyRevenue: [5000, 6000, 7000, 8500], weeklyOrders: [95, 102, 98, 108, 115, 120, 125], rating: 4.1 }, + { id: 9, product: "Smartwatch", 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 }, + { id: 10, product: "Tablet", 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 }, + { id: 11, product: "TV", 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 }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, isSortable: true, type: "number" }, + { accessor: "product", label: "Product", width: 180, isSortable: true, type: "string" }, + { accessor: "category", label: "Category", width: 120, isSortable: true, type: "string" }, + { accessor: "monthlySales", label: "Monthly Sales (12mo)", width: 150, type: "lineAreaChart", tooltip: "Sales trend over the past 12 months", align: "center" }, + { accessor: "dailyViews", label: "Daily Views (30d)", width: 150, type: "lineAreaChart", tooltip: "Daily page views for the past 30 days", align: "center" }, + { accessor: "quarterlyRevenue", label: "Quarterly Revenue", width: 140, type: "barChart", tooltip: "Revenue by quarter", align: "center" }, + { accessor: "weeklyOrders", label: "Weekly Orders", width: 130, type: "barChart", tooltip: "Orders per week over the past 7 weeks", align: "center" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number", align: "center" }, +]; + +export const chartsConfig = { + headers: chartsHeaders, + rows: chartsData, + tableProps: { + columnReordering: true, + 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 d432bddb2..f6964a7c4 100644 --- a/packages/examples/solid/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx +++ b/packages/examples/solid/src/demos/collapsible-columns/CollapsibleColumnsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { collapsibleColumnsConfig } from "@simple-table/examples-shared"; +import { collapsibleColumnsConfig } from "./collapsible-columns.demo-data"; import "@simple-table/solid/styles.css"; export default function CollapsibleColumnsDemo(props: { @@ -9,7 +9,7 @@ export default function CollapsibleColumnsDemo(props: { }) { return ( ({ row }: { row: Record }) => + `$${((row[accessor] as number) || 0).toLocaleString()}`; + +const monthCol = (accessor: string, label: string): HeaderObject => ({ + accessor, + label, + width: 100, + showWhen: "parentExpanded" as const, + isSortable: true, + align: "right", + type: "number", + cellRenderer: fmt(accessor), +}); + +export const collapsibleColumnsHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, isSortable: true }, + { accessor: "name", label: "Sales Rep", minWidth: 150, width: "1fr", isSortable: true }, + { accessor: "region", label: "Region", width: 140, isSortable: true }, + { + accessor: "quarterlySales", + label: "Quarterly Sales", + width: 500, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "totalSales", label: "Total Sales", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("totalSales") }, + { accessor: "q1Sales", label: "Q1", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q1Sales") }, + { accessor: "q2Sales", label: "Q2", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q2Sales") }, + { accessor: "q3Sales", label: "Q3", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q3Sales") }, + { accessor: "q4Sales", label: "Q4", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q4Sales") }, + ], + }, + { + accessor: "monthlyPerformance", + label: "Monthly Performance", + width: 800, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "avgMonthly", label: "Avg Monthly", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("avgMonthly") }, + { accessor: "bestMonth", label: "Best Month", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("bestMonth") }, + 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"), + monthCol("oct", "Oct"), monthCol("nov", "Nov"), monthCol("dec", "Dec"), + ], + }, + { + accessor: "productCategories", + label: "Product Categories", + width: 450, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "topCategory", label: "Top Category", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, type: "string" }, + { accessor: "softwareSales", label: "Software", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("softwareSales") }, + { accessor: "hardwareSales", label: "Hardware", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("hardwareSales") }, + { accessor: "servicesSales", label: "Services", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("servicesSales") }, + ], + }, +]; + +export const collapsibleColumnsConfig = { + headers: collapsibleColumnsHeaders, + rows: collapsibleColumnsData, + tableProps: { + columnResizing: true, + editColumns: true, + 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 316394f5c..7d87a98c7 100644 --- a/packages/examples/solid/src/demos/column-alignment/ColumnAlignmentDemo.tsx +++ b/packages/examples/solid/src/demos/column-alignment/ColumnAlignmentDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { columnAlignmentConfig } from "@simple-table/examples-shared"; +import { columnAlignmentConfig } from "./column-alignment.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnAlignmentDemo(props: { height?: string | number; theme?: Theme }) { return ( ([]); + const [additionalColumns, setAdditionalColumns] = createSignal([]); const [lastAdded, setLastAdded] = createSignal(""); const addColumn = () => { const n = additionalColumns().length + 1; - const col: HeaderObject = { accessor: `custom-${n}`, label: `Custom ${n}`, width: 120, type: "string" }; + const col: SolidHeaderObject = { accessor: `custom-${n}`, label: `Custom ${n}`, width: 120, type: "string" }; setAdditionalColumns([...additionalColumns(), col]); setLastAdded(col.label); }; - const headers = createMemo(() => [...columnEditingHeaders, ...additionalColumns()]); + const headers = createMemo(() => [...defaultHeadersFromCore(columnEditingHeaders), ...additionalColumns()]); return (
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 new file mode 100644 index 000000000..d9585691d --- /dev/null +++ b/packages/examples/solid/src/demos/column-editing/column-editing.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export const columnEditingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, type: "string" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, +]; + +export const columnEditingData = [ + { 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" }, + { id: 4, name: "Luna Martinez", age: 23, role: "Junior Developer", department: "Engineering", email: "luna.martinez@company.com" }, + { id: 5, name: "Tyler Anderson", age: 31, role: "DevOps Engineer", department: "Operations", email: "tyler.anderson@company.com" }, + { id: 6, name: "Zara Kim", age: 28, role: "Product Designer", department: "Design", email: "zara.kim@company.com" }, + { id: 7, name: "Kai Thompson", age: 26, role: "Full Stack Developer", department: "Engineering", email: "kai.thompson@company.com" }, + { id: 8, name: "Ava Singh", age: 33, role: "Product Manager", department: "Product", email: "ava.singh@company.com" }, + { id: 9, name: "Jordan Walsh", age: 25, role: "Marketing Specialist", department: "Growth", email: "jordan.walsh@company.com" }, + { id: 10, name: "Phoenix Lee", age: 30, role: "Backend Developer", department: "Engineering", email: "phoenix.lee@company.com" }, + { id: 11, name: "River Jackson", age: 24, role: "Growth Designer", department: "Design", email: "river.jackson@company.com" }, + { id: 12, name: "Atlas Morgan", age: 32, role: "Tech Lead", department: "Engineering", email: "atlas.morgan@company.com" }, +]; + +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 1926d8ac8..37500792f 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 @@ -1,10 +1,10 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, SolidColumnEditorConfig, ColumnEditorRowRendererProps } from "@simple-table/solid"; import { columnEditorCustomRendererConfig, COLUMN_EDITOR_TEXT, COLUMN_EDITOR_SEARCH_PLACEHOLDER, -} from "@simple-table/examples-shared"; +} from "./column-editor-custom-renderer.demo-data"; import "@simple-table/solid/styles.css"; const CustomRowRenderer = ({ header, components }: ColumnEditorRowRendererProps) => ( @@ -42,7 +42,7 @@ const columnEditorConfig: SolidColumnEditorConfig = { export default function ColumnEditorCustomRendererDemo(props: { height?: string | number; theme?: Theme }) { return ( `$${(value as number).toLocaleString()}`, + }, + { accessor: "department", label: "Department", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const columnEditorCustomRendererConfig = { + headers: columnEditorCustomRendererHeaders, + rows: columnEditorCustomRendererData, + tableProps: { + editColumns: true, + }, +} as const; + +export const COLUMN_EDITOR_TEXT = "Manage Columns"; +export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…"; + +export function buildVanillaColumnEditorRowRenderer(props: ColumnEditorRowRendererProps): HTMLElement { + const row = document.createElement("div"); + Object.assign(row.style, { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "6px 8px", + borderRadius: "6px", + background: "#f8fafc", + marginBottom: "4px", + }); + + if (props.components.checkbox) { + const span = document.createElement("span"); + if (typeof props.components.checkbox === "string") { + span.innerHTML = props.components.checkbox; + } else { + span.appendChild(props.components.checkbox as Node); + } + row.appendChild(span); + } + + const label = document.createElement("span"); + Object.assign(label.style, { flex: "1", fontSize: "13px", fontWeight: "500" }); + label.textContent = props.header.label; + row.appendChild(label); + + if (props.components.dragIcon) { + const span = document.createElement("span"); + Object.assign(span.style, { cursor: "grab", opacity: "0.5" }); + if (typeof props.components.dragIcon === "string") { + span.innerHTML = props.components.dragIcon; + } else { + span.appendChild(props.components.dragIcon as Node); + } + row.appendChild(span); + } + + return row; +} diff --git a/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx b/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx index 6e12c3919..aedfb3813 100644 --- a/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx +++ b/packages/examples/solid/src/demos/column-filtering/ColumnFilteringDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { columnFilteringConfig } from "@simple-table/examples-shared"; +import { columnFilteringConfig } from "./column-filtering.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnFilteringDemo(props: { @@ -9,7 +9,7 @@ export default function ColumnFilteringDemo(props: { }) { return ( { + const salary = row.salary as number; + return `$${salary.toLocaleString()}`; + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 130, + type: "date", + isSortable: true, + filterable: true, + }, + { + accessor: "isActive", + label: "Active", + width: 100, + align: "center", + type: "boolean", + isSortable: true, + filterable: true, + }, +]; + +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 3f860c851..60bb2994d 100644 --- a/packages/examples/solid/src/demos/column-pinning/ColumnPinningDemo.tsx +++ b/packages/examples/solid/src/demos/column-pinning/ColumnPinningDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { columnPinningConfig } from "@simple-table/examples-shared"; +import { columnPinningConfig } from "./column-pinning.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnPinningDemo(props: { height?: string | number; theme?: Theme }) { return ( ([...columnReorderingConfig.headers]); + const [headers, setHeaders] = createSignal(defaultHeadersFromCore([...columnReorderingConfig.headers])); return ( setHeaders(h)} + onColumnOrderChange={(h: HeaderObject[]) => setHeaders(defaultHeadersFromCore(h))} /> ); } 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 new file mode 100644 index 000000000..6b4299cd5 --- /dev/null +++ b/packages/examples/solid/src/demos/column-reordering/column-reordering.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export const columnReorderingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", minWidth: 100, width: "1fr", type: "string" }, + { accessor: "department", disableReorder: true, label: "Department", width: "1fr", type: "string" }, +]; + +export const columnReorderingData = [ + { 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" }, + { id: 4, name: "Cadet Orbit Williams", age: 26, role: "Flight Engineer", department: "Engineering" }, + { id: 5, name: "Dr. Galaxy Chen", age: 31, role: "Life Support Specialist", department: "Engineering" }, + { id: 6, name: "Lt. Meteor Lee", age: 29, role: "Navigation Officer", department: "Flight Operations" }, + { id: 7, name: "Dr. Comet Hassan", age: 33, role: "Mission Planner", department: "Planning" }, + { id: 8, name: "Major Pulsar White", age: 36, role: "Communications Director", department: "Communications" }, + { id: 9, name: "Specialist Quasar Black", age: 28, role: "Systems Analyst", department: "Technology" }, + { id: 10, name: "Engineer Supernova Blue", age: 35, role: "Propulsion Engineer", department: "Engineering" }, + { id: 11, name: "Dr. Aurora Kumar", age: 30, role: "Planetary Geologist", department: "Science" }, + { id: 12, name: "Admiral Cosmos Silver", age: 45, role: "Program Director", department: "Leadership" }, +]; + +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 552467857..c5d5ddf25 100644 --- a/packages/examples/solid/src/demos/column-resizing/ColumnResizingDemo.tsx +++ b/packages/examples/solid/src/demos/column-resizing/ColumnResizingDemo.tsx @@ -1,11 +1,11 @@ import { createSignal, onMount, onCleanup } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/solid"; import type { Theme, HeaderObject } from "@simple-table/solid"; -import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "@simple-table/examples-shared"; +import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "./column-resizing.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnResizingDemo(props: { height?: string | number; theme?: Theme }) { - const [headers, setHeaders] = createSignal([...columnResizingHeaders]); + const [headers, setHeaders] = createSignal(defaultHeadersFromCore([...columnResizingHeaders])); const [saveMessage, setSaveMessage] = createSignal(""); let messageTimer: ReturnType | undefined; @@ -22,7 +22,9 @@ export default function ColumnResizingDemo(props: { height?: string | number; th const saved = localStorage.getItem(COLUMN_RESIZING_STORAGE_KEY); if (saved) { const widthMap = JSON.parse(saved) as Record; - setHeaders(columnResizingHeaders.map((h) => ({ ...h, width: widthMap[h.accessor] ?? h.width }))); + setHeaders( + defaultHeadersFromCore(columnResizingHeaders.map((h) => ({ ...h, width: widthMap[h.accessor] ?? h.width }))), + ); } } catch { /* ignore */ @@ -38,7 +40,7 @@ export default function ColumnResizingDemo(props: { height?: string | number; th const widthMap: Record = {}; for (const h of updatedHeaders) widthMap[h.accessor] = h.width; localStorage.setItem(COLUMN_RESIZING_STORAGE_KEY, JSON.stringify(widthMap)); - setHeaders(updatedHeaders); + setHeaders(defaultHeadersFromCore(updatedHeaders)); setSaveMessage("Column widths saved!"); clearMessageTimer(); messageTimer = setTimeout(() => setSaveMessage(""), 2000); 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 new file mode 100644 index 000000000..7fbdf6b5d --- /dev/null +++ b/packages/examples/solid/src/demos/column-resizing/column-resizing.demo-data.ts @@ -0,0 +1,34 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export const COLUMN_RESIZING_STORAGE_KEY = "columnResizingDemo_widths"; + +export const columnResizingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, align: "right", type: "number" }, + { accessor: "department", label: "Department", width: "1fr", minWidth: 100, type: "string" }, + { accessor: "startDate", label: "Start Date", width: 150, type: "date" }, +]; + +export const columnResizingData = [ + { id: 1, name: "Dr. Marina Silva", age: 38, role: "Marine Biologist", department: "Research", startDate: "2019-03-15" }, + { id: 2, name: "Captain Alex Torres", age: 45, role: "Research Vessel Captain", department: "Operations", startDate: "2017-08-20" }, + { id: 3, name: "Dr. Coral Chen", age: 34, role: "Oceanographer", department: "Research", startDate: "2020-01-12" }, + { id: 4, name: "Finn O'Brien", age: 27, role: "Research Assistant", department: "Research", startDate: "2022-06-08" }, + { id: 5, name: "Reef Nakamura", age: 31, role: "Dive Safety Officer", department: "Safety", startDate: "2021-02-14" }, + { id: 6, name: "Tide Rodriguez", age: 29, role: "Equipment Specialist", department: "Technical", startDate: "2021-09-03" }, + { id: 7, name: "Dr. Ocean Williams", age: 42, role: "Research Director", department: "Leadership", startDate: "2016-05-10" }, + { id: 8, name: "Wave Petrov", age: 26, role: "Data Analyst", department: "Analysis", startDate: "2022-11-22" }, + { id: 9, name: "Pearl Kim", age: 33, role: "Laboratory Manager", department: "Laboratory", startDate: "2020-07-18" }, + { id: 10, name: "Current Hassan", age: 28, role: "Field Coordinator", department: "Operations", startDate: "2021-12-05" }, + { id: 11, name: "Abyss Thompson", age: 30, role: "ROV Operator", department: "Technical", startDate: "2021-04-20" }, + { id: 12, name: "Dr. Depth Martinez", age: 39, role: "Senior Researcher", department: "Research", startDate: "2018-10-14" }, +]; + +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 98c534dcf..23f3fa267 100644 --- a/packages/examples/solid/src/demos/column-selection/ColumnSelectionDemo.tsx +++ b/packages/examples/solid/src/demos/column-selection/ColumnSelectionDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { columnSelectionConfig } from "@simple-table/examples-shared"; +import { columnSelectionConfig } from "./column-selection.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnSelectionDemo(props: { height?: string | number; theme?: Theme }) { return ( { + return (value as string).charAt(0).toUpperCase() + (value as string).slice(1); + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 140, + isSortable: true, + type: "date", + valueFormatter: ({ value }) => { + if (typeof value === "string") { + return new Date(value).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + return String(value); + }, + }, +]; + +export const columnSortingConfig = { + headers: columnSortingHeaders, + rows: COLUMN_SORTING_DATA, + tableProps: { + initialSortColumn: "age", + initialSortDirection: "desc" as const, + }, +} as const; diff --git a/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx b/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx index 9f44b9431..d2fdadb98 100644 --- a/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx +++ b/packages/examples/solid/src/demos/column-visibility/ColumnVisibilityDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { columnVisibilityConfig } from "@simple-table/examples-shared"; +import { columnVisibilityConfig } from "./column-visibility.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnVisibilityDemo(props: { height?: string | number; theme?: Theme }) { return ( new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const columnVisibilityConfig = { + headers: columnVisibilityHeaders, + rows: columnVisibilityData, + tableProps: { + editColumns: 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 d7b807202..4e0990c1c 100644 --- a/packages/examples/solid/src/demos/column-width/ColumnWidthDemo.tsx +++ b/packages/examples/solid/src/demos/column-width/ColumnWidthDemo.tsx @@ -1,7 +1,7 @@ import { createSignal, onMount, onCleanup } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { columnWidthConfig } from "@simple-table/examples-shared"; +import { columnWidthConfig } from "./column-width.demo-data"; import "@simple-table/solid/styles.css"; export default function ColumnWidthDemo(props: { height?: string | number; theme?: Theme }) { @@ -22,7 +22,7 @@ export default function ColumnWidthDemo(props: { height?: string | number; theme { const [isLoading, setIsLoading] = createSignal(false); @@ -118,7 +113,7 @@ function getCRMHeaders(isDark: boolean): SolidHeaderObject[] { ]; } -export default function CRMDemo(props: { height?: string | number; theme?: Theme }) { +export default function CRMDemo(props: { height?: string | number; theme?: CrmShellTheme }) { const isDark = () => props.theme === "custom-dark" || props.theme === "dark" || props.theme === "modern-dark"; const [data, setData] = createSignal([...crmData]); const [rowsPerPage, setRowsPerPage] = createSignal(100); diff --git a/packages/examples/solid/src/demos/crm/crm-custom-theme.css b/packages/examples/solid/src/demos/crm/crm-custom-theme.css new file mode 100644 index 000000000..8ed3366f4 --- /dev/null +++ b/packages/examples/solid/src/demos/crm/crm-custom-theme.css @@ -0,0 +1,161 @@ +@import url("https://fonts.googleapis.com/css2?family=BBH+Sans+Hegarty&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"); + +.custom-theme-container .st-wrapper { + border-radius: 0; +} +.custom-theme-container .st-header-label-text { + font-size: 12px; +} + +.custom-theme-container * { + font-family: Plus Jakarta Sans, sans-serif; + font-optical-sizing: auto; + font-style: normal; +} +.custom-theme-container .st-header-resize-handle { + height: 100%; + width: 4px; +} +.custom-theme-container.theme-custom-light { + --st-border-radius: 4px; + --st-cell-padding: 12px; + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + --st-scrollbar-bg-color: #f1f5f9; + --st-scrollbar-thumb-color: #cbd5e1; + --st-border-color: none; + --st-odd-row-background-color: white; + --st-even-row-background-color: white; + --st-odd-column-background-color: white; + --st-even-column-background-color: oklch(98.5% 0.002 247.839); + --st-hover-row-background-color: oklch(96.7% 0.003 264.542); + --st-selected-row-background-color: oklch(96.7% 0.003 264.542); + --st-header-background-color: rgb(249 250 251); + --st-header-label-color: oklch(55.1% 0.027 264.364); + --st-header-icon-color: oklch(55.1% 0.027 264.364); + --st-dragging-background-color: oklch(96.7% 0.003 264.542); + --st-selected-cell-background-color: oklch(96.7% 0.003 264.542); + --st-selected-first-cell-background-color: oklch(92.8% 0.006 264.531); + --st-footer-background-color: oklch(98.5% 0.002 247.839); + --st-cell-color: oklch(37.3% 0.034 259.733); + --st-cell-odd-row-color: oklch(37.3% 0.034 259.733); + --st-edit-cell-shadow: 0 4px 6px -1px rgba(106, 114, 130, 0.1), + 0 2px 4px -1px rgba(106, 114, 130, 0.06); + --st-selected-cell-color: oklch(27.8% 0.033 256.848); + --st-selected-first-cell-color: oklch(27.8% 0.033 256.848); + --st-resize-handle-color: oklch(96.7% 0.003 264.542); + --st-separator-border-color: oklch(92.8% 0.006 264.531); + --st-last-group-row-separator-border-color: oklch(87.2% 0.01 258.338); + --st-selected-border-color: #6a7282; + --st-editable-cell-focus-border-color: #6a7282; + --st-checkbox-checked-background-color: #6a7282; + --st-checkbox-checked-border-color: #6a7282; + --st-column-editor-background-color: white; + --st-column-editor-popout-background-color: white; + --st-button-hover-background-color: oklch(96.7% 0.003 264.542); + --st-button-active-background-color: #6a7282; + --st-cell-flash-color: oklch(92.8% 0.006 264.531); + --st-copy-flash-color: #6a7282; + --st-warning-flash-color: oklch(80.8% 0.114 19.571); + --st-header-selected-background-color: #6a7282; + --st-header-selected-label-color: #6a7282; + --st-header-selected-icon-color: #6a7282; + --st-header-highlight-indicator-color: oklch(87.2% 0.01 258.338); + --st-selection-highlight-indicator-color: oklch(87.2% 0.01 258.338); + --st-drag-separator-color: #6a7282; + --st-next-prev-btn-color: oklch(44.6% 0.03 256.802); + --st-next-prev-btn-disabled-color: oklch(70.7% 0.022 261.325); + --st-page-btn-color: oklch(44.6% 0.03 256.802); + --st-page-btn-hover-background-color: oklch(96.7% 0.003 264.542); + --st-column-editor-text-color: oklch(55.1% 0.027 264.364); + --st-checkbox-border-color: oklch(87.2% 0.01 258.338); + --st-dropdown-group-label-color: oklch(55.1% 0.027 264.364); + --st-datepicker-weekday-color: oklch(55.1% 0.027 264.364); + --st-datepicker-other-month-color: oklch(70.7% 0.022 261.325); + --st-filter-button-disabled-background-color: oklch(87.2% 0.01 258.338); + --st-filter-button-disabled-text-color: oklch(55.1% 0.027 264.364); +} + +.custom-theme-container.theme-custom-light .st-wrapper { + background-color: white; +} + +.custom-theme-container.theme-custom-light .st-header-resize-handle { + background-color: oklch(96.7% 0.003 264.542); +} + +.custom-theme-container.theme-custom-dark { + --st-border-radius: 4px; + --st-cell-padding: 12px; + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + --st-scrollbar-bg-color: #1e293b; + --st-scrollbar-thumb-color: #475569; + --st-border-color: none; + --st-odd-row-background-color: #0f172a; + --st-even-row-background-color: #0f172a; + --st-odd-column-background-color: #0f172a; + --st-even-column-background-color: #1e293b; + --st-hover-row-background-color: #1e293b; + --st-selected-row-background-color: #1e293b; + --st-header-background-color: #0a0f1a; + --st-header-label-color: #94a3b8; + --st-header-icon-color: #94a3b8; + --st-dragging-background-color: #1e293b; + --st-selected-cell-background-color: #1e293b; + --st-selected-first-cell-background-color: #334155; + --st-footer-background-color: #0f172a; + --st-cell-color: #cbd5e1; + --st-cell-odd-row-color: #cbd5e1; + --st-edit-cell-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + --st-selected-cell-color: #e2e8f0; + --st-selected-first-cell-color: #e2e8f0; + --st-resize-handle-color: #1e293b; + --st-separator-border-color: #334155; + --st-last-group-row-separator-border-color: #475569; + --st-selected-border-color: #94a3b8; + --st-editable-cell-focus-border-color: #94a3b8; + --st-checkbox-checked-background-color: #94a3b8; + --st-checkbox-checked-border-color: #94a3b8; + --st-column-editor-background-color: #1e293b; + --st-column-editor-popout-background-color: #1e293b; + --st-button-hover-background-color: #334155; + --st-button-active-background-color: #94a3b8; + --st-cell-flash-color: #334155; + --st-copy-flash-color: #94a3b8; + --st-warning-flash-color: #ef4444; + --st-header-selected-background-color: #94a3b8; + --st-header-selected-label-color: #94a3b8; + --st-header-selected-icon-color: #94a3b8; + --st-header-highlight-indicator-color: #475569; + --st-selection-highlight-indicator-color: #475569; + --st-drag-separator-color: #94a3b8; + --st-next-prev-btn-color: #cbd5e1; + --st-next-prev-btn-disabled-color: #64748b; + --st-page-btn-color: #cbd5e1; + --st-page-btn-hover-background-color: #334155; + --st-column-editor-text-color: #94a3b8; + --st-checkbox-border-color: #475569; + --st-dropdown-group-label-color: #94a3b8; + --st-datepicker-weekday-color: #94a3b8; + --st-datepicker-other-month-color: #64748b; + --st-filter-button-disabled-background-color: #334155; + --st-filter-button-disabled-text-color: #64748b; +} + +.custom-theme-container.theme-custom-dark .st-wrapper { + background-color: #0f172a; +} + +.custom-theme-container.theme-custom-dark .st-header-resize-handle { + background-color: #475569; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/examples/solid/src/demos/crm/crm.demo-data.ts b/packages/examples/solid/src/demos/crm/crm.demo-data.ts new file mode 100644 index 000000000..1ca1a4ded --- /dev/null +++ b/packages/examples/solid/src/demos/crm/crm.demo-data.ts @@ -0,0 +1,177 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Theme } from "@simple-table/solid"; + +export type CrmShellTheme = Theme | "custom-light" | "custom-dark"; + +export type CRMLead = { + id: number; + name: string; + title: string; + company: string; + linkedin: boolean; + signal: string; + aiScore: number; + emailStatus: string; + timeAgo: string; + list: string; +}; + + +const FIRST_NAMES = ["Emma", "Liam", "Sophia", "Noah", "Olivia", "James", "Ava", "William", "Isabella", "Oliver", "Mia", "Benjamin", "Charlotte", "Elijah", "Amelia", "Lucas", "Harper", "Mason", "Evelyn", "Logan"]; +const LAST_NAMES = ["Chen", "Rodriguez", "Kim", "Thompson", "Martinez", "Anderson", "Taylor", "Brown", "Wilson", "Johnson", "Lee", "Garcia", "Davis", "Miller", "Moore", "Jackson", "White", "Harris", "Martin", "Clark"]; +const TITLES = ["VP of Engineering", "Head of Marketing", "CTO", "Product Manager", "Director of Sales", "CEO", "CFO", "Head of Operations", "Engineering Manager", "Growth Lead", "CMO", "Head of Product", "Director of Engineering", "VP of Sales", "Head of Design"]; +const COMPANIES = ["TechCorp", "InnovateLabs", "CloudBase", "DataFlow", "NexGen", "Quantum AI", "CyberPulse", "MetaVision", "ByteForge", "CodeStream", "PixelPerfect", "LogicGate", "CircuitMind", "NetSphere", "DigiCore"]; +const SIGNALS = ["cloud infrastructure", "enterprise SaaS", "AI/ML tools", "developer platform", "security solutions", "data analytics", "API management", "DevOps automation", "microservices", "serverless computing"]; +const LISTS = ["Hot Leads", "Warm Leads", "Cold Leads", "Enterprise", "SMB", "Leads", "Nurture"]; +const TIME_AGOS = ["2 min ago", "5 min ago", "15 min ago", "1 hour ago", "3 hours ago", "6 hours ago", "1 day ago", "2 days ago", "3 days ago", "1 week ago"]; + +export function generateCRMData(count: number = 100): CRMLead[] { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + name: `${FIRST_NAMES[i % FIRST_NAMES.length]} ${LAST_NAMES[i % LAST_NAMES.length]}`, + title: TITLES[i % TITLES.length], + company: COMPANIES[i % COMPANIES.length], + linkedin: i % 3 !== 0, + signal: SIGNALS[i % SIGNALS.length], + aiScore: Math.min(5, Math.max(1, Math.floor(Math.random() * 5) + 1)), + emailStatus: ["Enrich", "Verified", "Pending", "Bounced"][i % 4], + timeAgo: TIME_AGOS[i % TIME_AGOS.length], + list: LISTS[i % LISTS.length], + })); +} + +export const crmData = generateCRMData(100); + +export const crmHeaders: HeaderObject[] = [ + { + accessor: "name", + label: "CONTACT", + width: "2fr", + minWidth: 290, + isSortable: true, + isEditable: true, + type: "string", + }, + { + accessor: "signal", + label: "SIGNAL", + width: "3fr", + minWidth: 340, + isSortable: true, + isEditable: true, + type: "string", + }, + { + accessor: "aiScore", + label: "AI SCORE", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "number", + }, + { + accessor: "emailStatus", + label: "EMAIL", + width: "1.5fr", + minWidth: 210, + isSortable: true, + align: "center", + type: "enum", + enumOptions: [ + { label: "Enrich", value: "Enrich" }, + { label: "Verified", value: "Verified" }, + { label: "Pending", value: "Pending" }, + { label: "Bounced", value: "Bounced" }, + ], + }, + { + accessor: "timeAgo", + label: "IMPORT", + width: "1fr", + minWidth: 100, + isSortable: true, + align: "center", + type: "string", + }, + { + accessor: "list", + label: "LIST", + width: "1.2fr", + minWidth: 160, + isSortable: 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 priorityMap: Record = { + "Hot Leads": 1, "Warm Leads": 2, Enterprise: 3, Leads: 4, SMB: 5, "Cold Leads": 6, Nurture: 7, + }; + return priorityMap[String(row.list)] || 999; + }, + }, + { + accessor: "_fit", + label: "Fit", + width: "1fr", + align: "center", + minWidth: 120, + }, + { + accessor: "_contactNow", + label: "", + width: "1.2fr", + minWidth: 160, + }, +]; + +export const CRM_FOOTER_COLORS_LIGHT = { + bg: "white", border: "#e5e7eb", text: "#374151", textBold: "#374151", + inputBg: "white", inputBorder: "#d1d5db", buttonBg: "white", buttonBorder: "#d1d5db", + buttonText: "#6b7280", activeBg: "#fff7ed", activeText: "#ea580c", +}; + +export const CRM_FOOTER_COLORS_DARK = { + bg: "#0f172a", border: "#334155", text: "#cbd5e1", textBold: "#e2e8f0", + inputBg: "#1e293b", inputBorder: "#475569", buttonBg: "#1e293b", buttonBorder: "#475569", + buttonText: "#cbd5e1", activeBg: "#334155", activeText: "#ea580c", +}; + +export function generateVisiblePages(currentPage: number, totalPages: number): number[] { + const maxVisible = 5; + if (totalPages <= maxVisible) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + let start = currentPage - 2; + let end = currentPage + 2; + if (start < 1) { start = 1; end = Math.min(maxVisible, totalPages); } + if (end > totalPages) { end = totalPages; start = Math.max(1, totalPages - maxVisible + 1); } + return Array.from({ length: end - start + 1 }, (_, i) => start + i); +} + +export const CRM_THEME_COLORS_LIGHT = { + text: "oklch(21% .034 264.665)", textSecondary: "oklch(44.6% .03 256.802)", + textTertiary: "oklch(55.1% .027 264.364)", link: "oklch(64.6% .222 41.116)", + accent: "#ea580c", bg: "white", tagBg: "oklch(96.7% .003 264.542)", + tagText: "oklch(21% .034 264.665)", buttonBg: "oklch(92.8% .006 264.531)", + buttonText: "oklch(70.7% .022 261.325)", buttonHoverBg: "oklch(87.2% .01 258.338)", +}; + +export const CRM_THEME_COLORS_DARK = { + text: "#cbd5e1", textSecondary: "#94a3b8", textTertiary: "#64748b", + link: "#60a5fa", accent: "#ea580c", bg: "#0f172a", tagBg: "#1e293b", + tagText: "#cbd5e1", buttonBg: "#1e293b", buttonText: "#cbd5e1", buttonHoverBg: "#334155", +}; + +export const crmConfig = { + headers: crmHeaders, + rows: crmData, +} 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 b785d3276..355abd16c 100644 --- a/packages/examples/solid/src/demos/csv-export/CsvExportDemo.tsx +++ b/packages/examples/solid/src/demos/csv-export/CsvExportDemo.tsx @@ -1,39 +1,38 @@ -import { SimpleTable } from "@simple-table/solid"; +import { SimpleTable, mapToSolidHeaderObjects } from "@simple-table/solid"; import type { Theme, TableAPI, SolidHeaderObject } from "@simple-table/solid"; -import { csvExportHeaders, csvExportData, csvExportConfig } from "@simple-table/examples-shared"; +import { csvExportHeaders, csvExportData, csvExportConfig } from "./csv-export.demo-data"; import "@simple-table/solid/styles.css"; -export default function CsvExportDemo(props: { - height?: string | number; - theme?: Theme; -}) { +export default function CsvExportDemo(props: { height?: string | number; theme?: Theme }) { let tableRef: TableAPI | undefined; - const headers: SolidHeaderObject[] = csvExportHeaders.map((h) => { - if (h.accessor === "actions") { - return { - ...h, - cellRenderer: () => ( - - ), - }; - } - return { ...h }; - }); + const headers: SolidHeaderObject[] = mapToSolidHeaderObjects( + csvExportHeaders.map((h) => { + if (h.accessor === "actions") { + return { + ...h, + cellRenderer: () => ( + + ), + }; + } + return h; + }), + ); const handleExport = () => { tableRef?.exportToCSV(); @@ -43,7 +42,10 @@ export default function CsvExportDemo(props: { if (!tableRef) return; const rows = tableRef.getAllRows(); const hdrs = tableRef.getHeaders(); - const totalRevenue = rows.reduce((sum, r) => sum + (Number(r.revenue) || 0), 0); + const totalRevenue = rows.reduce( + (sum, r) => sum + (Number((r.row as { revenue?: unknown }).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 })}`, ); 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 new file mode 100644 index 000000000..4c5331e04 --- /dev/null +++ b/packages/examples/solid/src/demos/csv-export/csv-export.demo-data.ts @@ -0,0 +1,76 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +const CATEGORY_CODES: Record = { + electronics: "ELEC", + furniture: "FURN", + stationery: "STAT", + appliances: "APPL", +}; + +export const csvExportData = [ + { id: "db-1001", sku: "PRD-1001", product: "Wireless Keyboard", category: "Electronics", price: 49.99, stock: 145, sold: 234, revenue: 11697.66, actions: "" }, + { id: "db-1002", sku: "PRD-1002", product: "Ergonomic Mouse", category: "Electronics", price: 29.99, stock: 89, sold: 456, revenue: 13675.44, actions: "" }, + { id: "db-1003", sku: "PRD-1003", product: "USB-C Hub", category: "Electronics", price: 39.99, stock: 234, sold: 178, revenue: 7118.22, actions: "" }, + { id: "db-2001", sku: "PRD-2001", product: "Standing Desk", category: "Furniture", price: 399.99, stock: 23, sold: 67, revenue: 26799.33, actions: "" }, + { id: "db-2002", sku: "PRD-2002", product: "Office Chair", category: "Furniture", price: 249.99, stock: 56, sold: 123, revenue: 30748.77, actions: "" }, + { id: "db-2003", sku: "PRD-2003", product: "Monitor Stand", category: "Furniture", price: 79.99, stock: 167, sold: 89, revenue: 7119.11, actions: "" }, + { id: "db-3001", sku: "PRD-3001", product: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, sold: 678, revenue: 8807.22, actions: "" }, + { id: "db-3002", sku: "PRD-3002", product: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, sold: 534, revenue: 10674.66, actions: "" }, + { id: "db-3003", sku: "PRD-3003", product: "Desk Organizer", category: "Stationery", price: 24.99, stock: 198, sold: 289, revenue: 7222.11, actions: "" }, + { id: "db-4001", sku: "PRD-4001", product: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, sold: 156, revenue: 14038.44, actions: "" }, + { id: "db-4002", sku: "PRD-4002", product: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, sold: 267, revenue: 9342.33, actions: "" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Internal ID", width: 80, type: "string", excludeFromRender: true }, + { accessor: "sku", label: "SKU", width: 100, isSortable: true, type: "string" }, + { accessor: "product", label: "Product Name", minWidth: 120, width: "1fr", isSortable: true, type: "string" }, + { + accessor: "category", + label: "Category", + width: 130, + isSortable: true, + type: "string", + valueFormatter: ({ value }) => { + const s = String(value); + return s.charAt(0).toUpperCase() + s.slice(1); + }, + exportValueGetter: ({ value }) => { + const code = CATEGORY_CODES[String(value).toLowerCase()] ?? String(value).toUpperCase(); + return `${value} (${code})`; + }, + }, + { + accessor: "price", + label: "Price", + width: 100, + isSortable: true, + type: "number", + valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}`, + useFormattedValueForCSV: true, + useFormattedValueForClipboard: true, + }, + { accessor: "stock", label: "In Stock", width: 100, isSortable: true, type: "number" }, + { accessor: "sold", label: "Units Sold", width: 110, isSortable: true, type: "number" }, + { + accessor: "revenue", + label: "Revenue", + width: 120, + isSortable: true, + type: "number", + valueFormatter: ({ value }) => + `$${(value as number).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + useFormattedValueForCSV: true, + useFormattedValueForClipboard: true, + }, + { accessor: "actions", label: "Actions", width: 100, type: "string", excludeFromCsv: true }, +]; + +export const csvExportConfig = { + headers: csvExportHeaders, + rows: csvExportData, + tableProps: { editColumns: 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 77e55942a..8f62ba293 100644 --- a/packages/examples/solid/src/demos/custom-icons/CustomIconsDemo.tsx +++ b/packages/examples/solid/src/demos/custom-icons/CustomIconsDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { customIconsConfig } from "@simple-table/examples-shared"; +import { customIconsConfig } from "./custom-icons.demo-data"; import "@simple-table/solid/styles.css"; const customIcons = { @@ -39,7 +39,7 @@ const customIcons = { export default function CustomIconsDemo(props: { height?: string | number; theme?: Theme }) { return ( (value as number).toLocaleString(), + }, + { + accessor: "date", + label: "Date", + width: 130, + type: "date", + isSortable: true, + valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const customIconsConfig = { + headers: customIconsHeaders, + rows: customIconsData, +} as const; + +export function createSvgIcon(pathD: string, color = "#3b82f6", size = 14): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", String(size)); + svg.setAttribute("height", String(size)); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", color); + svg.setAttribute("stroke-width", "2.5"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathD); + svg.appendChild(path); + return svg; +} + +export const ICON_PATHS = { + sortUp: "M12 19V5M5 12l7-7 7 7", + sortDown: "M12 5v14M19 12l-7 7-7-7", + filter: "M3 4h18l-7 8.5V18l-4 2V12.5L3 4z", + expand: "M9 5l7 7-7 7", + next: "M9 5l7 7-7 7", + prev: "M15 19l-7-7 7-7", +} as const; + +export function buildVanillaCustomIcons() { + return { + sortUp: createSvgIcon(ICON_PATHS.sortUp, "#6366f1"), + sortDown: createSvgIcon(ICON_PATHS.sortDown, "#6366f1"), + filter: createSvgIcon(ICON_PATHS.filter, "#8b5cf6"), + expand: createSvgIcon(ICON_PATHS.expand, "#6366f1"), + next: createSvgIcon(ICON_PATHS.next, "#2563eb"), + prev: createSvgIcon(ICON_PATHS.prev, "#2563eb"), + }; +} diff --git a/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx b/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx index 746cec890..6783566a1 100644 --- a/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx +++ b/packages/examples/solid/src/demos/custom-theme/CustomThemeDemo.tsx @@ -1,13 +1,13 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { customThemeConfig } from "@simple-table/examples-shared"; +import { customThemeConfig } from "./custom-theme.demo-data"; import "@simple-table/solid/styles.css"; -import "../../../../shared/src/styles/custom-theme.css"; +import "./custom-theme.css"; export default function CustomThemeDemo(props: { height?: string | number; theme?: Theme }) { return ( formatPhone(value as string), + }, + { accessor: "email", label: "Email", width: 180, type: "string" }, + { accessor: "city", label: "City", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +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 9fa3a34c2..aa2e47d41 100644 --- a/packages/examples/solid/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx +++ b/packages/examples/solid/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.tsx @@ -1,12 +1,12 @@ import { createSignal } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid"; import { dynamicNestedTablesConfig, dynamicNestedTablesData, fetchDivisionsForCompany, -} from "@simple-table/examples-shared"; -import type { DynamicCompany } from "@simple-table/examples-shared"; +} from "./dynamic-nested-tables.demo-data"; +import type { DynamicCompany } from "./dynamic-nested-tables.demo-data"; import "@simple-table/solid/styles.css"; export default function DynamicNestedTablesDemo(props: { height?: string | number; theme?: Theme }) { @@ -47,7 +47,7 @@ export default function DynamicNestedTablesDemo(props: { height?: string | numbe return ( new Promise((resolve) => setTimeout(resolve, ms)); + +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"]; + + return Array.from({ length: divisionCount }, (_, i) => ({ + id: `${companyId}-div-${i}`, + divisionName: divisionNames[i % divisionNames.length], + revenue: `$${Math.floor(Math.random() * 50) + 10}M`, + profitMargin: `${Math.floor(Math.random() * 30) + 10}%`, + headcount: Math.floor(Math.random() * 400) + 50, + location: locations[i % locations.length], + })); +}; + +export const dynamicNestedTablesData: DynamicCompany[] = [ + { id: "comp-1", companyName: "TechCorp Global", industry: "Technology", revenue: "$250M", employees: 1200 }, + { id: "comp-2", companyName: "FinanceHub Inc", industry: "Financial Services", revenue: "$180M", employees: 850 }, + { id: "comp-3", companyName: "HealthTech Solutions", industry: "Healthcare", revenue: "$320M", employees: 1500 }, + { id: "comp-4", companyName: "RetailMax Corporation", industry: "Retail", revenue: "$420M", employees: 2100 }, + { id: "comp-5", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-6", companyName: "MediaVision Studios", industry: "Entertainment", revenue: "$290M", employees: 950 }, + { id: "comp-7", companyName: "AutoDrive Industries", industry: "Automotive", revenue: "$680M", employees: 3200 }, + { id: "comp-8", companyName: "CloudNet Services", industry: "Technology", revenue: "$195M", employees: 720 }, + { id: "comp-9", companyName: "HealthCare Solutions", industry: "Healthcare", revenue: "$380M", employees: 1300 }, + { id: "comp-10", companyName: "EducationTech Innovations", industry: "Education", revenue: "$240M", employees: 1050 }, + { id: "comp-11", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-12", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-13", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-14", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, + { id: "comp-15", companyName: "EnergyFlow Systems", industry: "Energy", revenue: "$560M", employees: 1800 }, +]; + +export const dynamicNestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionName", label: "Division", width: 200 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: 180 }, +]; + +export const dynamicNestedTablesCompanyHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { + defaultHeaders: dynamicNestedTablesDivisionHeaders, + expandAll: false, + autoExpandColumns: true, + }, + }, + { accessor: "industry", label: "Industry", width: 150 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const dynamicNestedTablesConfig = { + headers: dynamicNestedTablesCompanyHeaders, + rows: dynamicNestedTablesData, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + 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 19b60c750..bf1b3c9e5 100644 --- a/packages/examples/solid/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx +++ b/packages/examples/solid/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.tsx @@ -1,13 +1,13 @@ import { createSignal } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, OnRowGroupExpandProps } from "@simple-table/solid"; import { dynamicRowLoadingConfig, generateInitialRegions, fetchStoresForRegion, fetchProductsForStore, -} from "@simple-table/examples-shared"; -import type { DynamicRegion, DynamicStore } from "@simple-table/examples-shared"; +} from "./dynamic-row-loading.demo-data"; +import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data"; import "@simple-table/solid/styles.css"; export default function DynamicRowLoadingDemo(props: { height?: string | number; theme?: Theme }) { @@ -66,7 +66,7 @@ export default function DynamicRowLoadingDemo(props: { height?: string | number; return ( typeof value !== "number" ? "—" : value.toLocaleString(), + }, + { + accessor: "totalRevenue", label: "Revenue", width: 140, type: "number", align: "right", + aggregation: { type: "sum" }, + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`, + }, + { + accessor: "activeStores", label: "Stores", width: 100, type: "number", align: "right", + valueFormatter: ({ value }) => typeof value !== "number" ? "—" : value.toLocaleString(), + }, + { accessor: "avgRating", label: "Avg Rating", width: 120, type: "string", align: "center" }, + { accessor: "lastUpdate", label: "Last Updated", width: 130, type: "date" }, +]; + +const REGION_NAMES = [ + "North America - East", "North America - West", "Europe - North", "Europe - South", + "Asia Pacific - East", "Asia Pacific - Southeast", "Middle East", + "Latin America - North", "Latin America - South", "Africa - North", "Africa - South", "Oceania", +]; + +const STORE_NAMES = [ + "Manhattan Flagship", "Brooklyn Heights", "Boston Downtown", "Miami Beach", + "Los Angeles Beverly Hills", "San Francisco Union Square", "Seattle Downtown", "Portland Pearl District", + "London Oxford Street", "Stockholm Gamla Stan", "Copenhagen Strøget", "Amsterdam Central", + "Paris Champs-Élysées", "Madrid Gran Vía", "Rome Via del Corso", "Barcelona La Rambla", + "Tokyo Shibuya", "Shanghai Nanjing Road", "Hong Kong Central", "Seoul Gangnam", + "Singapore Orchard", "Bangkok Siam", "Kuala Lumpur Bukit Bintang", "Jakarta Grand Indonesia", + "Dubai Mall", "Abu Dhabi Marina", "Riyadh Kingdom Centre", + "Mexico City Reforma", "Monterrey Valle", "Guadalajara Centro", + "São Paulo Paulista", "Buenos Aires Palermo", "Santiago Providencia", + "Cairo City Stars", "Casablanca Morocco Mall", "Tunis Centre Urbain", + "Johannesburg Sandton", "Cape Town V&A Waterfront", + "Sydney Pitt Street", "Melbourne Bourke Street", "Auckland Queen Street", +]; + +const PRODUCT_NAMES = [ + "Wireless Headphones Pro", "Smart Watch Elite", "USB-C Hub Deluxe", "Mechanical Keyboard RGB", + "Ergonomic Mouse", "Webcam 4K", "Portable SSD 2TB", "Wireless Charger Pad", + "Phone Stand Aluminum", "Bluetooth Speaker Mini", "Laptop Stand Pro", "Cable Organizer Set", + "Gaming Mouse Elite", "Noise Cancelling Headset", "RGB Desk Mat XL", "Wireless Presenter", + "Document Camera", "Smart Pen Digital", "Monitor Arm Dual", "Docking Station Pro", + "Microphone USB Studio", "Tablet Stand Adjustable", "HDMI Switch 4K", "Laptop Cooling Pad", + "Blue Light Blocking Glasses", "Anti-Glare Screen Protector", "Laptop Privacy Filter", + "Wireless Charging Pad Trio", "MagSafe Car Mount", "Charging Cable Braided 10ft", + "Ergonomic Vertical Mouse", "Trackball Mouse Wireless", "Gaming Mouse Pad XXL", + "Keyboard Wrist Rest", "Monitor Privacy Filter", "Laptop Sleeve Premium", + "Desktop Mic Arm", "Cable Management Box", "USB Hub 7-Port", "Ergonomic Chair Cushion", + "Footrest Adjustable", "Desk Lamp LED Smart", "Portable Monitor 15.6", "Screen Cleaning Kit", + "Desk Organizer Bamboo", "Wireless Trackpad", "Numeric Keypad Wireless", + "Presentation Clicker", "Gaming Controller Pro", "Racing Wheel Set", +]; + +const seededRandom = (seed: string) => { + let hash = 0; + for (let i = 0; i < seed.length; i++) { + hash = (hash << 5) - hash + seed.charCodeAt(i); + hash = hash & hash; + } + const x = Math.sin(hash) * 10000; + return x - Math.floor(x); +}; + +const getRandomInt = (seed: string, min: number, max: number) => + Math.floor(seededRandom(seed) * (max - min + 1)) + min; + +const getRandomRating = (seed: string) => (4.0 + seededRandom(seed + "rating") * 1.0).toFixed(1); + +const getRandomDate = (seed: string) => { + const daysAgo = getRandomInt(seed + "date", 0, 5); + const date = new Date(); + date.setDate(date.getDate() - daysAgo); + return date.toISOString().split("T")[0]; +}; + +const generateStoresForRegion = (regionId: string): DynamicStore[] => { + const regionIndex = parseInt(regionId.split("-")[1]); + const numStores = getRandomInt(regionId, 3, 4); + const startIndex = (regionIndex - 1) * 3; + return Array.from({ length: numStores }, (_, i) => { + const storeId = `STORE-${regionIndex}${String(i + 1).padStart(2, "0")}`; + const storeIndex = startIndex + i; + const totalSales = getRandomInt(storeId, 10000, 25000); + const avgPrice = getRandomInt(storeId + "price", 25, 35); + return { + id: storeId, + name: STORE_NAMES[storeIndex % STORE_NAMES.length], + type: "store" as const, + totalSales, + totalRevenue: totalSales * avgPrice, + avgRating: getRandomRating(storeId), + lastUpdate: getRandomDate(storeId), + }; + }); +}; + +const generateProductsForStore = (storeId: string): DynamicProduct[] => { + const numProducts = getRandomInt(storeId, 3, 5); + const storeNumber = parseInt(storeId.split("-")[1]); + const startIndex = storeNumber * 3; + return Array.from({ length: numProducts }, (_, i) => { + const productId = `PROD-${storeId.split("-")[1]}-${i + 1}`; + const totalSales = getRandomInt(productId, 2000, 8000); + const avgPrice = getRandomInt(productId + "price", 20, 40); + return { + id: productId, + name: PRODUCT_NAMES[(startIndex + i) % PRODUCT_NAMES.length], + type: "product" as const, + totalSales, + totalRevenue: totalSales * avgPrice, + avgRating: getRandomRating(productId), + lastUpdate: getRandomDate(productId), + }; + }); +}; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const fetchStoresForRegion = async (regionId: string): Promise => { + await delay(1500); + return generateStoresForRegion(regionId); +}; + +export const fetchProductsForStore = async (storeId: string): Promise => { + await delay(1000); + return generateProductsForStore(storeId); +}; + +export const generateInitialRegions = (): DynamicRegion[] => { + return REGION_NAMES.map((name, index) => { + const regionId = `REG-${index + 1}`; + const stores = generateStoresForRegion(regionId); + const totalSales = stores.reduce((sum, s) => sum + s.totalSales, 0); + const totalRevenue = stores.reduce((sum, s) => sum + s.totalRevenue, 0); + const avgRating = (stores.reduce((sum, s) => sum + parseFloat(s.avgRating), 0) / stores.length).toFixed(1); + return { + id: regionId, + name, + type: "region" as const, + totalSales, + totalRevenue, + activeStores: getRandomInt(regionId, 3, 4), + avgRating, + lastUpdate: getRandomDate(regionId), + }; + }); +}; + +export const dynamicRowLoadingConfig = { + headers: dynamicRowLoadingHeaders, + tableProps: { + rowGrouping: ["stores", "products"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + columnResizing: true, + selectableCells: true, + useOddEvenRowBackground: true, + editColumns: 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 d93ff53a7..a3cdcc281 100644 --- a/packages/examples/solid/src/demos/empty-state/EmptyStateDemo.tsx +++ b/packages/examples/solid/src/demos/empty-state/EmptyStateDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { emptyStateConfig } from "@simple-table/examples-shared"; +import { emptyStateConfig } from "./empty-state.demo-data"; import "@simple-table/solid/styles.css"; const EmptyIcon = () => ( @@ -14,7 +14,7 @@ const EmptyIcon = () => ( export default function EmptyStateDemo(props: { height?: string | number; theme?: Theme }) { return ( Number(filter.value); + case "lessThan": + return Number(value) < Number(filter.value); + case "greaterThanOrEqual": + return Number(value) >= Number(filter.value); + case "lessThanOrEqual": + return Number(value) <= Number(filter.value); + case "between": + return ( + filter.values != null && + Number(value) >= Number(filter.values[0]) && + Number(value) <= Number(filter.values[1]) + ); + case "in": + return filter.values != null && filter.values.includes(value); + case "notIn": + return filter.values != null && !filter.values.includes(value); + case "isEmpty": + return value == null || value === ""; + case "isNotEmpty": + return value != null && value !== ""; + default: + return true; + } +} + +const DEPARTMENT_OPTIONS = [ + { label: "AI Research", value: "AI Research" }, + { label: "UX Design", value: "UX Design" }, + { label: "DevOps", value: "DevOps" }, + { label: "Marketing", value: "Marketing" }, + { label: "Engineering", value: "Engineering" }, + { label: "Product", value: "Product" }, + { label: "Sales", value: "Sales" }, +]; + +const LOCATION_OPTIONS = [ + { label: "San Francisco", value: "San Francisco" }, + { label: "Tokyo", value: "Tokyo" }, + { label: "Lagos", value: "Lagos" }, + { label: "Mexico City", value: "Mexico City" }, + { label: "Kolkata", value: "Kolkata" }, + { label: "Stockholm", value: "Stockholm" }, + { label: "Dubai", value: "Dubai" }, + { label: "Milan", value: "Milan" }, + { label: "Seoul", value: "Seoul" }, + { label: "Austin", value: "Austin" }, + { label: "London", value: "London" }, + { label: "Moscow", value: "Moscow" }, +]; + +export const externalFilterData = [ + { 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" }, + { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing", active: true, location: "Mexico City" }, + { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering", active: true, location: "Kolkata" }, + { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product", active: false, location: "Stockholm" }, + { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales", active: true, location: "Dubai" }, + { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing", active: true, location: "Milan" }, + { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research", active: true, location: "Seoul" }, + { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering", active: false, location: "Austin" }, + { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design", active: true, location: "London" }, + { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" }, +]; + +export const externalFilterHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" }, + { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" }, + { + accessor: "department", + label: "Department", + width: 150, + filterable: true, + type: "enum", + enumOptions: DEPARTMENT_OPTIONS, + }, + { + accessor: "location", + label: "Location", + width: 150, + filterable: true, + type: "enum", + enumOptions: LOCATION_OPTIONS, + }, + { accessor: "active", label: "Active", width: 120, filterable: true, type: "boolean" }, + { + accessor: "salary", + label: "Salary", + width: 120, + filterable: true, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, +]; + +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 fac04220b..8b1365a68 100644 --- a/packages/examples/solid/src/demos/external-sort/ExternalSortDemo.tsx +++ b/packages/examples/solid/src/demos/external-sort/ExternalSortDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; -import type { Theme, SortColumn } from "@simple-table/solid"; -import { externalSortConfig } from "@simple-table/examples-shared"; +import { SimpleTable, asRows, defaultHeadersFromCore } 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 "@simple-table/solid/styles.css"; @@ -10,22 +10,28 @@ export default function ExternalSortDemo(props: { }) { const [sortState, setSortState] = createSignal(null); - const sortedRows = createMemo(() => { + const sortedRows = createMemo((): Row[] => { const sort = sortState(); - const rows = [...externalSortConfig.rows]; + 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 cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; - return sort.direction === "asc" ? cmp : -cmp; + if (aVal === bVal) return 0; + const cmp = + type === "number" + ? (Number(aVal) || 0) - (Number(bVal) || 0) + : String(aVal).localeCompare(String(bVal)); + return dir === "asc" ? cmp : -cmp; }); }); return ( `$${(value as number).toLocaleString()}`, + }, +]; + +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 e6325a918..234089b23 100644 --- a/packages/examples/solid/src/demos/footer-renderer/FooterRendererDemo.tsx +++ b/packages/examples/solid/src/demos/footer-renderer/FooterRendererDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, FooterRendererProps } from "@simple-table/solid"; -import { footerRendererConfig } from "@simple-table/examples-shared"; +import { footerRendererConfig } from "./footer-renderer.demo-data"; import { For } from "solid-js"; import "@simple-table/solid/styles.css"; @@ -41,7 +41,7 @@ export default function FooterRendererDemo(props: { height?: string | number; th return ( - headerRendererConfig.headers.map((h) => ({ + mapToSolidHeaderObjects(headerRendererConfig.headers.map((h) => ({ ...h, isSortable: false, headerRenderer: ({ accessor }: HeaderRendererProps) => { @@ -66,7 +66,7 @@ export default function HeaderRendererDemo(props: { height?: string | number; th
); }, - })) + }))) ); return ( 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 new file mode 100644 index 000000000..8f1096672 --- /dev/null +++ b/packages/examples/solid/src/demos/header-renderer/header-renderer.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/solid"; + + +export const headerRendererData: Row[] = [ + { 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" }, + { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering" }, + { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics" }, + { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering" }, + { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design" }, + { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" }, +]; + +export const headerRendererHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number", isSortable: true }, + { accessor: "name", label: "Employee Name", width: 180, type: "string", isSortable: true }, + { accessor: "email", label: "Email Address", width: 200, type: "string" }, + { accessor: "role", label: "Job Role", width: 130, type: "string", isSortable: true }, + { accessor: "salary", label: "Annual Salary", width: 140, type: "number", isSortable: true }, + { accessor: "department", label: "Department", width: 150, type: "string", isSortable: true }, +]; + +export const headerRendererConfig = { + headers: headerRendererHeaders, + rows: headerRendererData, + tableProps: { + 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 e5e68ff79..b05985187 100644 --- a/packages/examples/solid/src/demos/hr/HRDemo.tsx +++ b/packages/examples/solid/src/demos/hr/HRDemo.tsx @@ -1,112 +1,192 @@ import { createSignal } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; -import type { Theme, SolidHeaderObject, CellChangeProps } from "@simple-table/solid"; -import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "@simple-table/examples-shared"; -import type { HREmployee, HRTagColorKey } from "@simple-table/examples-shared"; +import { SimpleTable, mapToSolidHeaderObjects } from "@simple-table/solid"; +import type { Theme, SolidHeaderObject, CellChangeProps, CellRendererProps } from "@simple-table/solid"; +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(): SolidHeaderObject[] { - return hrConfig.headers.map((h) => { - if (h.accessor === "fullName") { - return { - ...h, - cellRenderer: ({ row, theme }) => { - const d = row as unknown as HREmployee; - const c = getHRThemeColors(theme); - const initials = `${d.firstName?.charAt(0) || ""}${d.lastName?.charAt(0) || ""}`; - return ( -
-
- {initials} + return mapToSolidHeaderObjects( + hrConfig.headers.map((h) => { + if (h.accessor === "fullName") { + return { + ...h, + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + const c = getHRThemeColors(theme); + const initials = `${d.firstName?.charAt(0) || ""}${d.lastName?.charAt(0) || ""}`; + return ( +
+
+ {initials} +
+
+
{d.fullName}
+
{d.position}
+
-
-
{d.fullName}
-
{d.position}
+ ); + }, + }; + } + if (h.accessor === "performanceScore") { + return { + ...h, + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + const c = getHRThemeColors(theme); + const color = + d.performanceScore >= 90 + ? c.progressSuccess + : d.performanceScore >= 65 + ? c.progressNormal + : c.progressException; + return ( +
+
+
+
+
+ {d.performanceScore}/100 +
-
- ); - }, - }; - } - if (h.accessor === "performanceScore") { - return { - ...h, - cellRenderer: ({ row, theme }) => { - const d = row as unknown as HREmployee; - const c = getHRThemeColors(theme); - const color = d.performanceScore >= 90 ? c.progressSuccess : d.performanceScore >= 65 ? c.progressNormal : c.progressException; - return ( -
-
-
-
-
{d.performanceScore}/100
-
- ); - }, - }; - } - if (h.accessor === "hireDate") { - return { - ...h, - cellRenderer: ({ row, theme }) => { - const d = row as unknown as HREmployee; - if (!d.hireDate) return ""; - const [year, month, day] = d.hireDate.split("-").map(Number); - const date = new Date(year, month - 1, day); - const c = getHRThemeColors(theme); - return {date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}; - }, - }; - } - if (h.accessor === "yearsOfService") { - return { - ...h, - cellRenderer: ({ row, theme }) => { - if (row.yearsOfService === null) return ""; - const c = getHRThemeColors(theme); - return {`${row.yearsOfService} yrs`}; - }, - }; - } - if (h.accessor === "salary") { - return { - ...h, - cellRenderer: ({ row, theme }) => { - const d = row as unknown as HREmployee; - const c = getHRThemeColors(theme); - return {`$${d.salary.toLocaleString()}`}; - }, - }; - } - if (h.accessor === "status") { - return { - ...h, - cellRenderer: ({ row, theme }) => { - const d = row as unknown as HREmployee; - if (!d.status) return ""; - const c = getHRThemeColors(theme); - const colorKey: HRTagColorKey = HR_STATUS_COLOR_MAP[d.status] || "default"; - const tagColors = c.tagColors[colorKey] || c.tagColors.default; - return {d.status}; - }, - }; - } - return h; - }); + ); + }, + }; + } + if (h.accessor === "hireDate") { + return { + ...h, + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + if (!d.hireDate) return ""; + const [year, month, day] = d.hireDate.split("-").map(Number); + const date = new Date(year, month - 1, day); + const c = getHRThemeColors(theme); + return ( + + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + ); + }, + }; + } + if (h.accessor === "yearsOfService") { + return { + ...h, + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + if (d.yearsOfService === null) return ""; + const c = getHRThemeColors(theme); + return {`${d.yearsOfService} yrs`}; + }, + }; + } + if (h.accessor === "salary") { + return { + ...h, + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + const c = getHRThemeColors(theme); + return {`$${d.salary.toLocaleString()}`}; + }, + }; + } + if (h.accessor === "status") { + return { + ...h, + cellRenderer: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + if (!d.status) return ""; + const c = getHRThemeColors(theme); + const colorKey: HRTagColorKey = HR_STATUS_COLOR_MAP[d.status] || "default"; + const tagColors = c.tagColors[colorKey] || c.tagColors.default; + return ( + + {d.status} + + ); + }, + }; + } + return h; + }), + ); } export default function HRDemo(props: { height?: string | number; theme?: Theme }) { const [data, setData] = createSignal([...hrConfig.rows]); const rowHeight = 48; - const heightNum = () => typeof props.height === "number" ? props.height : 400; + const heightNum = () => (typeof props.height === "number" ? props.height : 400); const howManyRowsCanFit = () => Math.floor(heightNum() / rowHeight); const handleCellEdit = ({ accessor, newValue, row }: CellChangeProps) => { - setData((prev) => prev.map((item) => (item.id === row.id ? { ...item, [accessor]: newValue } : item))); + setData((prev) => + prev.map((item) => (item.id === row.id ? { ...item, [accessor]: newValue } : item)), + ); }; return ( - + ); } diff --git a/packages/examples/solid/src/demos/hr/hr.demo-data.ts b/packages/examples/solid/src/demos/hr/hr.demo-data.ts new file mode 100644 index 000000000..16df4f1e7 --- /dev/null +++ b/packages/examples/solid/src/demos/hr/hr.demo-data.ts @@ -0,0 +1,126 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + +export interface HREmployee { + id: number; + firstName: string; + lastName: string; + fullName: string; + position: string; + performanceScore: number; + department: string; + email: string; + location: string; + hireDate: string; + yearsOfService: number; + salary: number; + status: string; + isRemoteEligible: boolean; +} + + +const HR_FIRST_NAMES = ["James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda", "David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah", "Charles", "Karen"]; +const HR_LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"]; +const POSITIONS = ["Software Engineer", "Senior Engineer", "Tech Lead", "Engineering Manager", "Product Manager", "Designer", "Data Scientist", "DevOps Engineer", "QA Engineer", "Solutions Architect"]; +const DEPARTMENTS = ["Engineering", "Marketing", "Sales", "Finance", "HR", "Operations", "Customer Support"]; +const LOCATIONS = ["New York", "Los Angeles", "Chicago", "San Francisco", "Austin", "Boston", "Seattle", "Remote"]; +const HR_STATUSES = ["Active", "On Leave", "Probation", "Contract", "Terminated"]; + +export function generateHRData(count: number = 100): HREmployee[] { + return Array.from({ length: count }, (_, i) => { + const firstName = HR_FIRST_NAMES[i % HR_FIRST_NAMES.length]; + const lastName = HR_LAST_NAMES[i % HR_LAST_NAMES.length]; + const yearsOfService = Math.floor(Math.random() * 15); + const hireYear = 2024 - yearsOfService; + const hireMonth = String(1 + Math.floor(Math.random() * 12)).padStart(2, "0"); + const hireDay = String(1 + Math.floor(Math.random() * 28)).padStart(2, "0"); + return { + id: i + 1, + firstName, + lastName, + fullName: `${firstName} ${lastName}`, + position: POSITIONS[i % POSITIONS.length], + performanceScore: Math.floor(40 + Math.random() * 60), + department: DEPARTMENTS[i % DEPARTMENTS.length], + email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@company.com`, + location: LOCATIONS[i % LOCATIONS.length], + hireDate: `${hireYear}-${hireMonth}-${hireDay}`, + yearsOfService, + salary: Math.floor(50000 + Math.random() * 150000), + status: HR_STATUSES[i % 10 === 9 ? 4 : i % 7 === 0 ? 1 : i % 11 === 0 ? 2 : i % 13 === 0 ? 3 : 0], + isRemoteEligible: Math.random() > 0.3, + }; + }); +} + +export const hrData = generateHRData(100); + +export const hrHeaders: HeaderObject[] = [ + { accessor: "fullName", label: "Employee", width: 220, isSortable: true, isEditable: false, align: "left", pinned: "left", type: "string" }, + { + accessor: "performanceScore", label: "Performance", width: 160, isSortable: true, isEditable: true, align: "center", type: "number", + valueFormatter: ({ value }) => `${value}/100`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${value}%`, + }, + { + accessor: "department", label: "Department", width: 150, isSortable: true, isEditable: true, align: "left", type: "enum", + enumOptions: [ + { label: "Engineering", value: "Engineering" }, { label: "Marketing", value: "Marketing" }, + { label: "Sales", value: "Sales" }, { label: "Finance", value: "Finance" }, + { label: "HR", value: "HR" }, { label: "Operations", value: "Operations" }, + { label: "Customer Support", value: "Customer Support" }, + ], + }, + { accessor: "email", label: "Email", width: 280, isSortable: true, isEditable: true, align: "left", type: "string" }, + { + accessor: "location", label: "Location", width: 130, isSortable: true, isEditable: true, align: "left", type: "enum", + enumOptions: LOCATIONS.map((l) => ({ label: l, value: l })), + }, + { accessor: "hireDate", label: "Hire Date", width: 120, isSortable: true, isEditable: true, align: "left", type: "date" }, + { accessor: "yearsOfService", label: "Service", width: 100, isSortable: true, isEditable: false, align: "center", type: "number" }, + { accessor: "salary", label: "Salary", width: 130, isSortable: true, isEditable: true, align: "right", type: "number", valueFormatter: ({ value }) => { if (typeof value !== "number") return ""; return `$${value.toLocaleString()}`; }, useFormattedValueForClipboard: true, useFormattedValueForCSV: true }, + { + accessor: "status", label: "Status", width: 120, isSortable: true, isEditable: true, align: "center", pinned: "right", type: "enum", + enumOptions: [ + { label: "Active", value: "Active" }, { label: "On Leave", value: "On Leave" }, + { label: "Probation", value: "Probation" }, { label: "Contract", value: "Contract" }, + { label: "Terminated", value: "Terminated" }, + ], + valueGetter: ({ row }) => { + const priorityMap: Record = { Terminated: 1, Probation: 2, Contract: 3, "On Leave": 4, Active: 5 }; + return priorityMap[String(row.status)] || 999; + }, + }, + { accessor: "isRemoteEligible", label: "Remote Eligible", width: 140, isSortable: true, isEditable: true, align: "center", type: "boolean" }, +]; + +export function getHRThemeColors(theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const tagColors: Record = isDark + ? { green: { bg: "#065f46", text: "#86efac" }, orange: { bg: "#9a3412", text: "#fed7aa" }, blue: { bg: "#1e3a8a", text: "#93c5fd" }, purple: { bg: "#581c87", text: "#c4b5fd" }, red: { bg: "#991b1b", text: "#fca5a5" }, default: { bg: "#374151", text: "#e5e7eb" } } + : { green: { bg: "#f6ffed", text: "#2a6a0d" }, orange: { bg: "#fff7e6", text: "#ad4e00" }, blue: { bg: "#e6f7ff", text: "#0050b3" }, purple: { bg: "#f9f0ff", text: "#391085" }, red: { bg: "#fff1f0", text: "#a8071a" }, default: { bg: "#f0f0f0", text: "rgba(0, 0, 0, 0.85)" } }; + return { + gray: isDark ? "#f3f4f6" : "#1f2937", + grayMuted: isDark ? "#f3f4f6" : "#6b7280", + avatarBg: isDark ? "#3b82f6" : "#1890ff", + avatarText: "#ffffff", + progressSuccess: isDark ? "#34d399" : "#52c41a", + progressNormal: isDark ? "#60a5fa" : "#1890ff", + progressException: isDark ? "#f87171" : "#ff4d4f", + progressBg: isDark ? "#374151" : "#f5f5f5", + progressText: isDark ? "#d1d5db" : "rgba(0, 0, 0, 0.65)", + tagColors, + }; +} + +export type HRTagColorKey = "green" | "orange" | "blue" | "purple" | "red" | "default"; + +export const HR_STATUS_COLOR_MAP: Record = { + Active: "green", "On Leave": "orange", Probation: "blue", Contract: "purple", Terminated: "red", +}; + +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 835ffb793..5e778e840 100644 --- a/packages/examples/solid/src/demos/infinite-scroll/InfiniteScrollDemo.tsx +++ b/packages/examples/solid/src/demos/infinite-scroll/InfiniteScrollDemo.tsx @@ -1,7 +1,7 @@ import { createSignal, createMemo } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, Row } from "@simple-table/solid"; -import { infiniteScrollConfig, generateInfiniteScrollData } from "@simple-table/examples-shared"; +import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data"; import "@simple-table/solid/styles.css"; const MAX_ROWS = 200; @@ -36,7 +36,7 @@ export default function InfiniteScrollDemo(props: { height?: string | number; th {statusText()}
`$${(value as number).toLocaleString()}`, + }, +]; + +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 1f7813d5e..bef148917 100644 --- a/packages/examples/solid/src/demos/infrastructure/InfrastructureDemo.tsx +++ b/packages/examples/solid/src/demos/infrastructure/InfrastructureDemo.tsx @@ -6,8 +6,8 @@ import { INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors, -} from "@simple-table/examples-shared"; -import type { InfrastructureServer } from "@simple-table/examples-shared"; +} from "./infrastructure.demo-data"; +import type { InfrastructureServer } from "./infrastructure.demo-data"; import "@simple-table/solid/styles.css"; function getHeaders(currentTheme?: Theme): SolidHeaderObject[] { diff --git a/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts b/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts new file mode 100644 index 000000000..0c1a78a9e --- /dev/null +++ b/packages/examples/solid/src/demos/infrastructure/infrastructure.demo-data.ts @@ -0,0 +1,263 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/solid"; + +export interface InfrastructureServer { + id: number; + serverId: string; + serverName: string; + cpuUsage: number; + cpuHistory: number[]; + memoryUsage: number; + diskUsage: number; + responseTime: number; + networkIn: number; + networkOut: number; + activeConnections: number; + requestsPerSec: number; + status: "online" | "warning" | "critical" | "maintenance" | "offline"; +} + + +const SERVER_PREFIXES = [ + "web", + "api", + "db", + "cache", + "worker", + "proxy", + "auth", + "search", + "queue", + "storage", +]; +const SERVER_NAMES = [ + "Production Primary", + "Production Replica", + "Staging", + "Development", + "Analytics", + "Load Balancer", + "CDN Edge", + "Backup Primary", + "Monitoring", + "Gateway", +]; +const STATUSES: Array = [ + "online", + "warning", + "critical", + "maintenance", + "offline", +]; + +export function generateInfrastructureData(count: number = 50): Row[] { + return Array.from({ length: count }, (_, i) => { + const prefix = SERVER_PREFIXES[i % SERVER_PREFIXES.length]; + const num = String(i + 1).padStart(3, "0"); + const cpu = Math.round((20 + Math.random() * 70) * 10) / 10; + return { + id: i + 1, + serverId: `${prefix}-${num}`, + serverName: SERVER_NAMES[i % SERVER_NAMES.length], + cpuUsage: cpu, + cpuHistory: Array.from({ length: 30 }, () => Math.round((20 + Math.random() * 70) * 10) / 10), + memoryUsage: Math.round((30 + Math.random() * 60) * 10) / 10, + diskUsage: Math.round((10 + Math.random() * 80) * 10) / 10, + responseTime: Math.round((20 + Math.random() * 400) * 10) / 10, + networkIn: Math.round(Math.random() * 1000 * 100) / 100, + networkOut: Math.round(Math.random() * 600 * 100) / 100, + activeConnections: Math.floor(Math.random() * 5000), + requestsPerSec: Math.floor(Math.random() * 10000), + status: STATUSES[i % 5 === 4 ? 4 : i % 7 === 0 ? 2 : i % 5 === 0 ? 1 : i % 9 === 0 ? 3 : 0], + }; + }); +} + +export const infrastructureData = generateInfrastructureData(50); + +export const infrastructureHeaders: HeaderObject[] = [ + { + accessor: "serverId", + align: "left", + filterable: true, + isEditable: false, + isSortable: true, + label: "Server ID", + minWidth: 180, + pinned: "left", + type: "string", + width: "1.2fr", + }, + { + accessor: "serverName", + align: "left", + filterable: true, + isEditable: false, + isSortable: true, + label: "Name", + minWidth: 200, + type: "string", + width: "1.5fr", + }, + { + accessor: "performance", + label: "Performance Metrics", + width: 690, + isSortable: false, + children: [ + { + accessor: "cpuHistory", + label: "CPU History", + width: 150, + isSortable: false, + filterable: false, + isEditable: false, + align: "center", + type: "lineAreaChart", + tooltip: "CPU usage over the last 30 intervals", + }, + { + accessor: "cpuUsage", + label: "CPU %", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "memoryUsage", + label: "Memory %", + width: 130, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "diskUsage", + label: "Disk %", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + { + accessor: "responseTime", + label: "Response (ms)", + width: 120, + isSortable: true, + filterable: true, + isEditable: true, + align: "right", + type: "number", + }, + ], + }, + { + accessor: "status", + label: "Status", + width: 130, + isSortable: true, + filterable: true, + isEditable: 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 severityMap: Record = { + critical: 1, + offline: 2, + warning: 3, + maintenance: 4, + online: 5, + }; + return severityMap[String(row.status)] || 999; + }, + }, +]; + +export const INFRA_UPDATE_CONFIG = { + minInterval: 300, + maxInterval: 1000, +}; + +export function getInfraMetricColorStyles( + value: number, + theme: string, + metric: "cpu" | "memory" | "response" | "status", + statusValue?: string, +) { + const getLevel = (val: number, thresholds: [number, number, number]) => { + if (val >= thresholds[0]) return "critical"; + if (val >= thresholds[1]) return "warning"; + if (val >= thresholds[2]) return "moderate"; + return "good"; + }; + + let level: string; + if (metric === "cpu") level = getLevel(value, [90, 80, 60]); + else if (metric === "memory") level = getLevel(value, [95, 85, 70]); + else if (metric === "response") level = getLevel(value, [400, 200, 100]); + else level = statusValue || "good"; + + const isDark = theme === "dark" || theme === "modern-dark"; + const colorMap: Record = isDark + ? { + critical: { color: "#fca5a5", backgroundColor: "rgba(127, 29, 29, 0.4)" }, + warning: { color: "#fcd34d", backgroundColor: "rgba(146, 64, 14, 0.4)" }, + moderate: { color: "#60a5fa", backgroundColor: "rgba(30, 64, 175, 0.3)" }, + good: { color: "#4ade80", backgroundColor: "rgba(21, 128, 61, 0.3)" }, + } + : { + critical: { color: "#dc2626", backgroundColor: "#fef2f2" }, + warning: { color: "#d97706", backgroundColor: "#fffbeb" }, + moderate: { color: "#2563eb", backgroundColor: "#eff6ff" }, + good: { color: "#16a34a", backgroundColor: "#f0fdf4" }, + }; + + return colorMap[level] || colorMap.good; +} + +export function getInfraStatusColors(status: string, theme: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const map: Record = isDark + ? { + online: { color: "#6ee7b7", backgroundColor: "rgba(6, 95, 70, 0.4)", fontWeight: "600" }, + warning: { color: "#fcd34d", backgroundColor: "rgba(146, 64, 14, 0.4)", fontWeight: "600" }, + critical: { + color: "#fca5a5", + backgroundColor: "rgba(153, 27, 27, 0.4)", + fontWeight: "600", + }, + maintenance: { + color: "#93c5fd", + backgroundColor: "rgba(30, 64, 175, 0.4)", + fontWeight: "600", + }, + offline: { color: "#d1d5db", backgroundColor: "rgba(75, 85, 99, 0.4)", fontWeight: "600" }, + } + : { + online: { color: "#16a34a", backgroundColor: "#f0fdf4", fontWeight: "600" }, + warning: { color: "#d97706", backgroundColor: "#fffbeb", fontWeight: "600" }, + critical: { color: "#dc2626", backgroundColor: "#fef2f2", fontWeight: "600" }, + maintenance: { color: "#2563eb", backgroundColor: "#eff6ff", fontWeight: "600" }, + offline: { color: "#4b5563", backgroundColor: "#f9fafb", fontWeight: "600" }, + }; + return map[status] || map.offline; +} + +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 2442c879f..ab1be2460 100644 --- a/packages/examples/solid/src/demos/live-update/LiveUpdateDemo.tsx +++ b/packages/examples/solid/src/demos/live-update/LiveUpdateDemo.tsx @@ -1,7 +1,7 @@ import { onMount, onCleanup } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, TableAPI } from "@simple-table/solid"; -import { liveUpdateConfig, liveUpdateData } from "@simple-table/examples-shared"; +import { liveUpdateConfig, liveUpdateData } from "./live-update.demo-data"; import "@simple-table/solid/styles.css"; export default function LiveUpdateDemo(props: { height?: string | number; theme?: Theme }) { @@ -100,7 +100,7 @@ export default function LiveUpdateDemo(props: { height?: string | number; theme? return ( (tableRef = api)} - defaultHeaders={liveUpdateConfig.headers} + defaultHeaders={defaultHeadersFromCore(liveUpdateConfig.headers)} rows={liveUpdateConfig.rows} 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 new file mode 100644 index 000000000..5edfd2b29 --- /dev/null +++ b/packages/examples/solid/src/demos/live-update/live-update.demo-data.ts @@ -0,0 +1,61 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export const liveUpdateHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "product", label: "Product", width: 180, type: "string" }, + { + accessor: "price", label: "Price", width: "1fr", type: "number", + valueFormatter: ({ value }) => typeof value === "number" ? `$${value.toFixed(2)}` : "$0.00", + }, + { accessor: "stock", label: "In Stock", width: 100, type: "number" }, + { + accessor: "stockHistory", label: "Stock Trend", width: 140, type: "lineAreaChart", align: "center", + tooltip: "Stock levels over the last 20 updates", + chartOptions: { color: "#10b981", fillColor: "#34d399", fillOpacity: 0.2, strokeWidth: 2, height: 35 }, + }, + { accessor: "sales", label: "Sales", width: 100, type: "number" }, + { + accessor: "salesHistory", label: "Sales Trend", width: 140, type: "barChart", align: "center", + tooltip: "Sales activity over the last 12 updates", + chartOptions: { color: "#f59e0b", gap: 2, height: 35 }, + }, +]; + +export const generateStockHistory = (currentStock: number, length = 20) => { + const history: number[] = []; + for (let i = 0; i < length; i++) { + const variation = (Math.random() - 0.5) * 30; + history.push(Math.max(0, Math.round(currentStock + variation))); + } + return history; +}; + +export const generateSalesHistory = (_currentSales: number, length = 12) => { + const history: number[] = []; + for (let i = 0; i < length; i++) { + history.push(Math.floor(Math.random() * 4)); + } + return history; +}; + +export const liveUpdateData = [ + { 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) }, + { id: 4, product: "Smart Water Bottle", price: 34.99, stock: 48, sales: 134, stockHistory: generateStockHistory(48), salesHistory: generateSalesHistory(134) }, + { id: 5, product: "Ceramic Coffee Mug", price: 18.99, stock: 124, sales: 298, stockHistory: generateStockHistory(124), salesHistory: generateSalesHistory(298) }, + { id: 6, product: "Wireless Phone Charger", price: 29.99, stock: 67, sales: 156, stockHistory: generateStockHistory(67), salesHistory: generateSalesHistory(156) }, + { id: 7, product: "Essential Oil Diffuser", price: 52.99, stock: 89, sales: 203, stockHistory: generateStockHistory(89), salesHistory: generateSalesHistory(203) }, + { id: 8, product: "Stainless Steel Tumbler", price: 22.99, stock: 134, sales: 267, stockHistory: generateStockHistory(134), salesHistory: generateSalesHistory(267) }, + { id: 9, product: "LED Desk Lamp", price: 39.99, stock: 95, sales: 176, stockHistory: generateStockHistory(95), salesHistory: generateSalesHistory(176) }, + { id: 10, product: "Organic Cotton Towel", price: 26.99, stock: 87, sales: 145, stockHistory: generateStockHistory(87), salesHistory: generateSalesHistory(145) }, + { id: 11, product: "Portable Phone Stand", price: 15.99, stock: 203, sales: 387, stockHistory: generateStockHistory(203), salesHistory: generateSalesHistory(387) }, + { id: 12, product: "Aromatherapy Candle", price: 31.99, stock: 56, sales: 112, stockHistory: generateStockHistory(56), salesHistory: generateSalesHistory(112) }, +]; + +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 fdf0fc5b9..f3f4e2426 100644 --- a/packages/examples/solid/src/demos/loading-state/LoadingStateDemo.tsx +++ b/packages/examples/solid/src/demos/loading-state/LoadingStateDemo.tsx @@ -1,7 +1,7 @@ import { createSignal, onMount, onCleanup } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, Row } from "@simple-table/solid"; -import { loadingStateConfig } from "@simple-table/examples-shared"; +import { loadingStateConfig } from "./loading-state.demo-data"; import "@simple-table/solid/styles.css"; export default function LoadingStateDemo(props: { height?: string | number; theme?: Theme }) { @@ -34,7 +34,7 @@ export default function LoadingStateDemo(props: { height?: string | number; them
`$${(value as number).toLocaleString()}`, + }, + { accessor: "status", label: "Status", width: 120 }, +]; + +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 11fc54301..2d8a7b004 100644 --- a/packages/examples/solid/src/demos/manufacturing/ManufacturingDemo.tsx +++ b/packages/examples/solid/src/demos/manufacturing/ManufacturingDemo.tsx @@ -1,16 +1,17 @@ -import { SimpleTable } from "@simple-table/solid"; -import type { Theme, SolidHeaderObject } from "@simple-table/solid"; -import { manufacturingConfig, getManufacturingStatusColors } from "@simple-table/examples-shared"; -import type { ManufacturingRow } from "@simple-table/examples-shared"; +import { SimpleTable, asRows, mapToSolidHeaderObjects } from "@simple-table/solid"; +import type { Theme, SolidHeaderObject, 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(): SolidHeaderObject[] { const baseHeaders = [...manufacturingConfig.headers]; - return baseHeaders.map((h) => { + return mapToSolidHeaderObjects( + baseHeaders.map((h) => { if (h.accessor === "productLine") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); return hasChildren ? {d.productLine} : d.productLine; @@ -20,7 +21,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "station") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return {d.id}; @@ -36,7 +37,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "status") { return { ...h, - cellRenderer: ({ row, theme }) => { + cellRenderer: ({ row, theme }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return "—"; @@ -48,7 +49,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "outputRate" || h.accessor === "defectCount" || h.accessor === "energy") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); const value = d[h.accessor as keyof ManufacturingRow] as number; @@ -59,7 +60,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "cycletime") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return {d.cycletime?.toFixed(1)}; @@ -70,7 +71,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "efficiency") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); const color = d.efficiency >= 90 ? "#52c41a" : d.efficiency >= 75 ? "#1890ff" : "#ff4d4f"; @@ -88,7 +89,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "defectRate") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); const rate = typeof d.defectRate === "number" ? d.defectRate : parseFloat(String(d.defectRate)); @@ -100,7 +101,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "downtime") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); const hours = typeof d.downtime === "number" ? d.downtime : parseFloat(String(d.downtime)); @@ -112,7 +113,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "utilization") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return {d.utilization?.toFixed(0)}%; @@ -123,7 +124,7 @@ function getHeaders(): SolidHeaderObject[] { if (h.accessor === "maintenanceDate") { return { ...h, - cellRenderer: ({ row }) => { + cellRenderer: ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const hasChildren = d.stations && Array.isArray(d.stations); if (hasChildren) return "—"; @@ -149,11 +150,12 @@ function getHeaders(): SolidHeaderObject[] { }; } return h; - }); + }), + ); } export default function ManufacturingDemo(props: { height?: string | number; theme?: Theme }) { return ( - + ); } diff --git a/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts b/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts new file mode 100644 index 000000000..d92afc1c0 --- /dev/null +++ b/packages/examples/solid/src/demos/manufacturing/manufacturing.demo-data.ts @@ -0,0 +1,125 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + +export interface ManufacturingRow { + id: string; + productLine: string; + station: string; + machineType: string; + status: string; + outputRate: number; + cycletime: number; + efficiency: number; + defectRate: number; + defectCount: number; + downtime: number; + utilization: number; + energy: number; + maintenanceDate: string; + stations?: ManufacturingRow[]; +} + + +const PRODUCT_LINES = ["Assembly Line A", "Assembly Line B", "Welding Station", "Paint Shop", "Quality Control", "Packaging Unit", "CNC Machining", "Injection Molding"]; +const STATIONS = ["Station Alpha", "Station Beta", "Station Gamma", "Station Delta", "Station Epsilon"]; +const MACHINE_TYPES = ["CNC Mill", "Lathe", "Welder", "Press", "Robot Arm", "Conveyor", "Inspector", "Dryer"]; +const MANUFACTURING_STATUSES = ["Running", "Scheduled Maintenance", "Unplanned Downtime", "Idle", "Setup"]; + +export function generateManufacturingData(count: number = 8): ManufacturingRow[] { + return Array.from({ length: count }, (_, i) => { + const stationCount = 3 + Math.floor(Math.random() * 3); + const stations: ManufacturingRow[] = Array.from({ length: stationCount }, (_, j) => { + const efficiency = Math.floor(70 + Math.random() * 28); + const defectRate = Math.round((0.1 + Math.random() * 4) * 100) / 100; + const downtime = Math.round((0.1 + Math.random() * 3) * 100) / 100; + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + Math.floor(Math.random() * 30)); + return { + id: `${PRODUCT_LINES[i % PRODUCT_LINES.length]}-S${j + 1}`, + productLine: PRODUCT_LINES[i % PRODUCT_LINES.length], + station: STATIONS[j % STATIONS.length], + machineType: MACHINE_TYPES[(i + j) % MACHINE_TYPES.length], + status: MANUFACTURING_STATUSES[j % MANUFACTURING_STATUSES.length], + outputRate: Math.floor(500 + Math.random() * 2000), + cycletime: Math.round((10 + Math.random() * 50) * 10) / 10, + efficiency, + defectRate, + defectCount: Math.floor(defectRate * 10 + Math.random() * 20), + downtime, + utilization: Math.floor(60 + Math.random() * 38), + energy: Math.floor(100 + Math.random() * 900), + maintenanceDate: futureDate.toISOString().split("T")[0], + }; + }); + + const totalOutput = stations.reduce((s, st) => s + st.outputRate, 0); + const avgEfficiency = Math.round(stations.reduce((s, st) => s + st.efficiency, 0) / stations.length); + const avgCycletime = Math.round((stations.reduce((s, st) => s + st.cycletime, 0) / stations.length) * 10) / 10; + const avgDefectRate = Math.round((stations.reduce((s, st) => s + st.defectRate, 0) / stations.length) * 100) / 100; + const totalDefects = stations.reduce((s, st) => s + st.defectCount, 0); + const totalDowntime = Math.round(stations.reduce((s, st) => s + st.downtime, 0) * 100) / 100; + const avgUtilization = Math.round(stations.reduce((s, st) => s + st.utilization, 0) / stations.length); + const totalEnergy = stations.reduce((s, st) => s + st.energy, 0); + + return { + id: PRODUCT_LINES[i % PRODUCT_LINES.length], + productLine: PRODUCT_LINES[i % PRODUCT_LINES.length], + station: "", + machineType: "", + status: "", + outputRate: totalOutput, + cycletime: avgCycletime, + efficiency: avgEfficiency, + defectRate: avgDefectRate, + defectCount: totalDefects, + downtime: totalDowntime, + utilization: avgUtilization, + energy: totalEnergy, + maintenanceDate: "", + stations, + }; + }); +} + +export const manufacturingData = generateManufacturingData(8); + +export const manufacturingHeaders: HeaderObject[] = [ + { accessor: "productLine", label: "Production Line", width: 180, expandable: true, isSortable: true, isEditable: false, align: "left", type: "string" }, + { accessor: "station", label: "Workstation", width: 150, isSortable: true, isEditable: false, align: "left", type: "string" }, + { accessor: "machineType", label: "Machine Type", width: 150, isSortable: true, isEditable: false, align: "left", type: "string" }, + { + accessor: "status", label: "Status", width: 180, isSortable: true, isEditable: false, align: "center", type: "string", + valueGetter: ({ row }) => { + if (row.stations && Array.isArray(row.stations)) return 999; + const priorityMap: Record = { "Unplanned Downtime": 1, Idle: 2, Setup: 3, "Scheduled Maintenance": 4, Running: 5 }; + return priorityMap[String(row.status)] || 999; + }, + }, + { accessor: "outputRate", label: "Output (units/shift)", width: 200, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "cycletime", label: "Cycle Time (s)", width: 140, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "efficiency", label: "Efficiency", width: 150, isSortable: true, isEditable: false, align: "center", type: "number", aggregation: { type: "average" } }, + { accessor: "defectRate", label: "Defect Rate", width: 120, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "defectCount", label: "Defects", width: 120, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "downtime", label: "Downtime (h)", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "utilization", label: "Utilization", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "average" } }, + { accessor: "energy", label: "Energy (kWh)", width: 130, isSortable: true, isEditable: false, align: "right", type: "number", aggregation: { type: "sum" } }, + { accessor: "maintenanceDate", label: "Next Maintenance", width: 200, isSortable: true, isEditable: false, align: "center", type: "date" }, +]; + +export function getManufacturingStatusColors(status: string, theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + const isLight = theme === "light" || theme === "modern-light"; + const colorMaps: Record = { + Running: isDark ? { bg: "rgba(6, 95, 70, 0.4)", text: "#6ee7b7" } : isLight ? { bg: "#dcfce7", text: "#16a34a" } : { bg: "#f6ffed", text: "#2a6a0d" }, + "Scheduled Maintenance": isDark ? { bg: "rgba(30, 64, 175, 0.4)", text: "#93c5fd" } : isLight ? { bg: "#dbeafe", text: "#3b82f6" } : { bg: "#e6f7ff", text: "#0050b3" }, + "Unplanned Downtime": isDark ? { bg: "rgba(153, 27, 27, 0.4)", text: "#fca5a5" } : isLight ? { bg: "#fee2e2", text: "#dc2626" } : { bg: "#fff1f0", text: "#a8071a" }, + Idle: isDark ? { bg: "rgba(146, 64, 14, 0.4)", text: "#fcd34d" } : isLight ? { bg: "#fef3c7", text: "#d97706" } : { bg: "#fff7e6", text: "#ad4e00" }, + Setup: isDark ? { bg: "rgba(109, 40, 217, 0.4)", text: "#c4b5fd" } : isLight ? { bg: "#e9d5ff", text: "#9333ea" } : { bg: "#f9f0ff", text: "#391085" }, + }; + return colorMaps[status] || (isDark ? { bg: "rgba(75, 85, 99, 0.4)", text: "#d1d5db" } : isLight ? { bg: "#f3f4f6", text: "#6b7280" } : { bg: "#f0f0f0", text: "rgba(0, 0, 0, 0.85)" }); +} + +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 a5ad90f21..3831e4681 100644 --- a/packages/examples/solid/src/demos/music/MusicDemo.tsx +++ b/packages/examples/solid/src/demos/music/MusicDemo.tsx @@ -1,9 +1,9 @@ import { SimpleTable } from "@simple-table/solid"; import type { Theme, SolidHeaderObject, TableAPI } from "@simple-table/solid"; -import { musicData, getMusicThemeColors } from "@simple-table/examples-shared"; -import type { MusicArtist } from "@simple-table/examples-shared"; +import { musicData, getMusicThemeColors } from "./music.demo-data"; +import type { MusicArtist } from "./music.demo-data"; import "@simple-table/solid/styles.css"; -import "@simple-table/examples-shared/styles/music-theme.css"; +import "./music-theme.css"; const Tag = (props: { children: any; color?: string; theme?: string }) => { const c = () => getMusicThemeColors(props.theme); diff --git a/packages/examples/solid/src/demos/music/music-theme.css b/packages/examples/solid/src/demos/music/music-theme.css new file mode 100644 index 000000000..f2538a958 --- /dev/null +++ b/packages/examples/solid/src/demos/music/music-theme.css @@ -0,0 +1,13 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +.music-theme-container * { + font-family: "Inter"; +} + +.music-theme-container .st-header-label-text { + font-weight: 500; + font-size: 12px; +} +.music-theme-container .st-cell-content { + font-size: 12px; +} diff --git a/packages/examples/solid/src/demos/music/music.demo-data.ts b/packages/examples/solid/src/demos/music/music.demo-data.ts new file mode 100644 index 000000000..06914565b --- /dev/null +++ b/packages/examples/solid/src/demos/music/music.demo-data.ts @@ -0,0 +1,215 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + +export interface MusicArtist { + id: number; + rank: 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; +} + + +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(); +} + +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; }; + + 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, + }; + }); +} + +export const musicData = generateMusicData(50); + +export const musicHeaders: HeaderObject[] = [ + { accessor: "rank", label: "#", width: 60, isSortable: true, isEditable: false, align: "center", type: "number", pinned: "left" }, + { accessor: "artistName", label: "Artist", width: 330, isSortable: true, isEditable: false, align: "left", type: "string", pinned: "left" }, + { accessor: "artistType", label: "Identity", width: 280, isSortable: false, isEditable: false, align: "left", type: "string" }, + { + accessor: "followersGroup", label: "Followers", width: 700, collapsible: true, + children: [ + { accessor: "followers", label: "Total Followers", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number" }, + { accessor: "followers7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "followers28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "followers60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + ], + }, + { accessor: "popularity", label: "Popularity", width: 180, isSortable: true, isEditable: false, align: "center", type: "number" }, + { + accessor: "playlistReachGroup", label: "Playlist Reach", width: 700, collapsible: true, + children: [ + { accessor: "playlistReach", label: "Total Reach", width: 180, showWhen: "always", isSortable: true, isEditable: false, type: "number" }, + { accessor: "playlistReach7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistReach28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistReach60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: 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", isSortable: true, isEditable: false, type: "number" }, + { accessor: "playlistCount7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistCount28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "playlistCount60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: 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", isSortable: true, isEditable: false, type: "number" }, + { accessor: "monthlyListeners7DayGrowth", label: "7-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "monthlyListeners28DayGrowth", label: "28-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + { accessor: "monthlyListeners60DayGrowth", label: "60-Day Growth", width: 160, isSortable: true, isEditable: false, align: "right", type: "number", showWhen: "parentExpanded" }, + ], + }, + { accessor: "conversionRate", label: "Conversion Rate", width: 150, isSortable: true, isEditable: false, align: "right", type: "number" }, + { accessor: "reachFollowersRatio", label: "Reach/Followers Ratio", width: 220, isSortable: true, isEditable: false, align: "right", type: "number" }, +]; + +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" }, + 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" }, + "modern-dark": { gray: "#e5e7eb", grayMuted: "#9ca3af", success: "#22c55e", successBg: "#052e16", error: "#ef4444", errorBg: "#450a0a", primary: "#60a5fa", primaryBg: "#1e3a8a", warning: "#f59e0b", warningBg: "#451a03", tagBorder: "#4b5563", tagBg: "#111827", tagText: "#f9fafb", highScore: "#22c55e", mediumScore: "#60a5fa", lowScore: "#fbbf24", veryLowScore: "#f87171" }, + dark: { gray: "#e5e7eb", grayMuted: "#9ca3af", success: "#22c55e", successBg: "#052e16", error: "#ef4444", errorBg: "#450a0a", primary: "#60a5fa", primaryBg: "#1e3a8a", warning: "#f59e0b", warningBg: "#451a03", tagBorder: "#4b5563", tagBg: "#111827", tagText: "#f9fafb", highScore: "#22c55e", mediumScore: "#60a5fa", lowScore: "#fbbf24", veryLowScore: "#f87171" }, +}; + +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 7162d0cbb..35f0cd917 100644 --- a/packages/examples/solid/src/demos/nested-headers/NestedHeadersDemo.tsx +++ b/packages/examples/solid/src/demos/nested-headers/NestedHeadersDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { nestedHeadersConfig } from "@simple-table/examples-shared"; +import { nestedHeadersConfig } from "./nested-headers.demo-data"; import "@simple-table/solid/styles.css"; export default function NestedHeadersDemo(props: { height?: string | number; theme?: Theme }) { return ( (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) => ({ + divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`, + divisionName: randomElement(divisionTypes), + revenue: `$${randomInt(5, 25)}B`, + profitMargin: `${randomInt(15, 50)}%`, + headcount: randomInt(50, 500), + location: randomElement(cities), +}); + +const generateCompany = (companyIndex: number) => { + const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex)); + return { + id: companyIndex + 1, + companyName: `${randomElement(companyNames)} ${randomElement(suffixes)}`, + industry: randomElement(industries), + founded: randomInt(1985, 2020), + headquarters: randomElement(cities), + stockSymbol: Array.from({ length: 4 }, () => String.fromCharCode(65 + randomInt(0, 25))).join(""), + marketCap: `$${randomInt(10, 200)}B`, + ceo: `${randomElement(firstNames)} ${randomElement(lastNames)}`, + revenue: `$${randomInt(5, 60)}B`, + employees: randomInt(5000, 100000), + divisions, + }; +}; + +export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i)); + +export const nestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionId", label: "Division ID", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: "1fr" }, +]; + +export const nestedTablesHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { defaultHeaders: nestedTablesDivisionHeaders }, + }, + { accessor: "stockSymbol", label: "Symbol", width: 100 }, + { accessor: "marketCap", label: "Market Cap", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const nestedTablesConfig = { + headers: nestedTablesHeaders, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + 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 2b8efa7f5..9b0e9ba33 100644 --- a/packages/examples/solid/src/demos/pagination/PaginationDemo.tsx +++ b/packages/examples/solid/src/demos/pagination/PaginationDemo.tsx @@ -1,7 +1,7 @@ import { createSignal } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "@simple-table/examples-shared"; +import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "./pagination.demo-data"; import "@simple-table/solid/styles.css"; export default function PaginationDemo(props: { @@ -31,7 +31,7 @@ export default function PaginationDemo(props: { return ( { - if (h.accessor === "status") { - return { - ...h, - cellRenderer: (cr: CellRendererProps) => { - const s = String(cr.row.status); - const colors = PROGRAMMATIC_CONTROL_STATUS_COLORS[s] ?? { bg: "#f3f4f6", color: "#374151" }; - return ( - - {s} - - ); - }, - }; - } - return { ...h }; - }); + const headers: SolidHeaderObject[] = mapToSolidHeaderObjects( + programmaticControlConfig.headers.map((h) => { + if (h.accessor === "status") { + return { + ...h, + cellRenderer: (cr: CellRendererProps) => { + const s = String(cr.row.status); + const colors = PROGRAMMATIC_CONTROL_STATUS_COLORS[s] ?? { + bg: "#f3f4f6", + color: "#374151", + }; + return ( + + {s} + + ); + }, + }; + } + return h; + }), + ); const handleSortByName = () => { tableRef?.applySortState({ accessor: "name", direction: "asc" }); @@ -64,7 +72,10 @@ export default function ProgrammaticControlDemo(props: { const hdrs = tableRef.getHeaders(); const sortState = tableRef.getSortState(); const filterState = tableRef.getFilterState(); - const totalValue = allRows.reduce((sum, r) => sum + (r.price as number) * (r.stock as number), 0); + 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); + }, 0); const sortInfo = sortState ? `${sortState.key.label} (${sortState.direction})` : "None"; alert( `Table Info:\n• Rows: ${allRows.length}\n• Columns: ${hdrs.length}\n• Active filters: ${Object.keys(filterState).length}\n• Sort: ${sortInfo}\n• Total inventory value: $${totalValue.toFixed(2)}`, 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 new file mode 100644 index 000000000..8c27191a1 --- /dev/null +++ b/packages/examples/solid/src/demos/programmatic-control/programmatic-control.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export const STATUS_COLORS: Record = { + Available: { bg: "#dcfce7", color: "#166534" }, + "Low Stock": { bg: "#fef3c7", color: "#92400e" }, + "Out of Stock": { bg: "#fee2e2", color: "#991b1b" }, +}; + +export const programmaticControlData = [ + { 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" }, + { id: 4, name: "Standing Desk", category: "Furniture", price: 399.99, stock: 0, status: "Out of Stock" }, + { id: 5, name: "Office Chair", category: "Furniture", price: 249.99, stock: 56, status: "Available" }, + { id: 6, name: "Monitor Stand", category: "Furniture", price: 79.99, stock: 8, status: "Low Stock" }, + { id: 7, name: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, status: "Available" }, + { id: 8, name: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, status: "Available" }, + { id: 9, name: "Desk Organizer", category: "Stationery", price: 24.99, stock: 5, status: "Low Stock" }, + { id: 10, name: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, status: "Available" }, + { id: 11, name: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, status: "Available" }, + { id: 12, name: "Desk Lamp LED", category: "Appliances", price: 44.99, stock: 0, status: "Out of Stock" }, +]; + +export const programmaticControlHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, type: "number", isSortable: true, filterable: true }, + { accessor: "name", label: "Product Name", width: "1fr", minWidth: 150, type: "string", isSortable: true, filterable: true }, + { + accessor: "category", + label: "Category", + width: 140, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Electronics", "Furniture", "Stationery", "Appliances"].map((v) => ({ label: v, value: v })), + }, + { accessor: "price", label: "Price", width: 110, align: "right", type: "number", isSortable: true, filterable: true, valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}` }, + { accessor: "stock", label: "Stock", width: 100, align: "right", type: "number", isSortable: true, filterable: true }, + { + accessor: "status", + label: "Status", + width: 110, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Available", "Low Stock", "Out of Stock"].map((v) => ({ label: v, value: v })), + }, +]; + +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 f0d109259..034ef87af 100644 --- a/packages/examples/solid/src/demos/quick-filter/QuickFilterDemo.tsx +++ b/packages/examples/solid/src/demos/quick-filter/QuickFilterDemo.tsx @@ -1,7 +1,7 @@ import { createSignal } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme, QuickFilterMode } from "@simple-table/solid"; -import { quickFilterConfig } from "@simple-table/examples-shared"; +import { quickFilterConfig } from "./quick-filter.demo-data"; import "@simple-table/solid/styles.css"; export default function QuickFilterDemo(props: { height?: string | number; theme?: Theme }) { @@ -59,7 +59,7 @@ export default function QuickFilterDemo(props: { height?: string | number; theme
`$${(value || 0).toLocaleString()}`, align: "right" }, + { accessor: "status", label: "Status", width: 100, type: "string" }, + { accessor: "location", label: "Location", width: 140, type: "string" }, +]; + +export const quickFilterData = [ + { 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" }, + { id: 4, name: "Diana Prince", age: 31, department: "Marketing", salary: 82000, status: "Inactive", location: "Chicago" }, + { id: 5, name: "Ethan Hunt", age: 29, department: "Sales", salary: 78000, status: "Active", location: "Boston" }, + { id: 6, name: "Fiona Green", age: 38, department: "Engineering", salary: 105000, status: "Active", location: "Seattle" }, + { id: 7, name: "George Wilson", age: 26, department: "Marketing", salary: 68000, status: "Active", location: "Austin" }, + { id: 8, name: "Hannah Lee", age: 33, department: "Sales", salary: 88000, status: "Inactive", location: "Denver" }, + { id: 9, name: "Isaac Chen", age: 27, department: "Engineering", salary: 92000, status: "Active", location: "San Diego" }, + { id: 10, name: "Julia Brown", age: 30, department: "Marketing", salary: 72000, status: "Inactive", location: "Miami" }, + { id: 11, name: "Kevin Davis", age: 28, department: "Sales", salary: 85000, status: "Active", location: "Phoenix" }, + { id: 12, name: "Laura Garcia", age: 32, department: "Engineering", salary: 102000, status: "Active", location: "San Antonio" }, +]; + +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 d34cd8227..2a49e1202 100644 --- a/packages/examples/solid/src/demos/quick-start/QuickStartDemo.tsx +++ b/packages/examples/solid/src/demos/quick-start/QuickStartDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { quickStartConfig } from "@simple-table/examples-shared"; +import { quickStartConfig } from "./quick-start.demo-data"; import "@simple-table/solid/styles.css"; export default function QuickStartDemo(props: { @@ -9,7 +9,7 @@ export default function QuickStartDemo(props: { }) { return ( ({ @@ -29,7 +29,7 @@ export default function RowGroupingDemo(props: { height?: string | number; theme
(tableRef = api)} - defaultHeaders={rowGroupingConfig.headers} + defaultHeaders={defaultHeadersFromCore(rowGroupingConfig.headers)} rows={rowGroupingConfig.rows} rowGrouping={rowGroupingConfig.tableProps.rowGrouping} enableStickyParents={true} 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 new file mode 100644 index 000000000..814f726d5 --- /dev/null +++ b/packages/examples/solid/src/demos/row-grouping/row-grouping.demo-data.ts @@ -0,0 +1,205 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export const rowGroupingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "performance", label: "Performance", width: 120, type: "string" }, + { accessor: "location", label: "Location", width: 130, type: "string" }, + { accessor: "growthRate", label: "Growth", width: 90, type: "string" }, + { accessor: "status", label: "Status", width: 110, type: "string" }, + { accessor: "established", label: "Est. Date", width: 110, type: "date" }, +]; + +export const rowGroupingData = [ + { + id: "company-1", + organization: "TechSolutions Inc.", + employees: 137, + budget: "$15.0M", + performance: "Exceeding", + location: "San Francisco", + growthRate: "+9%", + status: "Expanding", + established: "2018-01-01", + divisions: [ + { + id: "div-100", + organization: "Engineering Division", + employees: 97, + budget: "$10.6M", + performance: "Exceeding", + location: "Multiple", + growthRate: "+11%", + status: "Expanding", + established: "2018-01-15", + departments: [ + { id: "dept-1001", organization: "Frontend", employees: 28, budget: "$2.8M", performance: "Exceeding", location: "San Francisco", growthRate: "+12%", status: "Hiring", established: "2019-05-16" }, + { id: "dept-1002", organization: "Backend", employees: 32, budget: "$3.4M", performance: "Meeting", location: "Seattle", growthRate: "+8%", status: "Stable", established: "2018-03-22" }, + { id: "dept-1003", organization: "DevOps", employees: 15, budget: "$1.9M", performance: "Exceeding", location: "Remote", growthRate: "+15%", status: "Hiring", established: "2020-11-05" }, + { id: "dept-1004", organization: "Mobile", employees: 22, budget: "$2.5M", performance: "Meeting", location: "Austin", growthRate: "+10%", status: "Restructuring", established: "2019-08-12" }, + ], + }, + { + id: "div-101", + organization: "Product Division", + employees: 40, + budget: "$4.4M", + performance: "Meeting", + location: "Multiple", + growthRate: "+5%", + status: "Stable", + established: "2019-01-10", + departments: [ + { id: "dept-1101", organization: "Design", employees: 17, budget: "$1.8M", performance: "Meeting", location: "Portland", growthRate: "+6%", status: "Stable", established: "2019-02-28" }, + { id: "dept-1102", organization: "Research", employees: 9, budget: "$1.4M", performance: "Below Target", location: "Boston", growthRate: "+3%", status: "Reviewing", established: "2020-07-15" }, + { id: "dept-1103", organization: "QA Testing", employees: 14, budget: "$1.2M", performance: "Meeting", location: "Chicago", growthRate: "+5%", status: "Stable", established: "2019-11-01" }, + ], + }, + ], + }, + { + id: "company-2", + organization: "HealthFirst Group", + employees: 138, + budget: "$22.4M", + performance: "Meeting", + location: "Boston", + growthRate: "+8%", + status: "Stable", + established: "2010-01-01", + divisions: [ + { + id: "div-200", + organization: "Hospital Operations", + employees: 106, + budget: "$13.1M", + performance: "Meeting", + location: "Multiple", + growthRate: "+6%", + status: "Expanding", + established: "2010-01-05", + departments: [ + { id: "dept-2001", organization: "Emergency", employees: 48, budget: "$5.2M", performance: "Meeting", location: "New York", growthRate: "+4%", status: "Critical", established: "2010-06-14" }, + { id: "dept-2002", organization: "Cardiology", employees: 32, budget: "$4.8M", performance: "Exceeding", location: "Chicago", growthRate: "+9%", status: "Expanding", established: "2012-03-25" }, + { id: "dept-2003", organization: "Pediatrics", employees: 26, budget: "$3.1M", performance: "Meeting", location: "Boston", growthRate: "+7%", status: "Stable", established: "2014-08-30" }, + ], + }, + { + id: "div-201", + organization: "Research & Development", + employees: 32, + budget: "$9.3M", + performance: "Exceeding", + location: "Multiple", + growthRate: "+15%", + status: "Hiring", + established: "2017-01-10", + departments: [ + { id: "dept-2101", organization: "Clinical Trials", employees: 18, budget: "$4.2M", performance: "Exceeding", location: "San Diego", growthRate: "+12%", status: "Expanding", established: "2017-04-18" }, + { id: "dept-2102", organization: "Genomics", employees: 14, budget: "$5.1M", performance: "Exceeding", location: "Cambridge", growthRate: "+18%", status: "Hiring", established: "2019-02-21" }, + ], + }, + ], + }, + { + id: "company-3", + organization: "Global Finance", + employees: 121, + budget: "$15.5M", + performance: "Meeting", + location: "New York", + growthRate: "+3%", + status: "Restructuring", + established: "2005-01-01", + divisions: [ + { + id: "div-300", + organization: "Banking Operations", + employees: 121, + budget: "$15.5M", + performance: "Meeting", + location: "Multiple", + growthRate: "+3%", + status: "Stable", + established: "2005-01-15", + departments: [ + { id: "dept-3001", organization: "Retail Banking", employees: 56, budget: "$4.8M", performance: "Meeting", location: "New York", growthRate: "+2%", status: "Stable", established: "2005-11-08" }, + { id: "dept-3002", organization: "Investment", employees: 38, budget: "$7.2M", performance: "Exceeding", location: "Chicago", growthRate: "+11%", status: "Hiring", established: "2008-05-12" }, + { id: "dept-3003", organization: "Loans", employees: 27, budget: "$3.5M", performance: "Below Target", location: "Dallas", growthRate: "-3%", status: "Restructuring", established: "2010-03-17" }, + ], + }, + ], + }, + { + id: "company-4", + organization: "Apex University", + employees: 115, + budget: "$13.4M", + performance: "Meeting", + location: "Cambridge", + growthRate: "+6%", + status: "Stable", + established: "1992-01-01", + divisions: [ + { + id: "div-400", + organization: "Academic Departments", + employees: 115, + budget: "$13.4M", + performance: "Meeting", + location: "Multiple", + growthRate: "+6%", + status: "Stable", + established: "1992-01-15", + departments: [ + { id: "dept-4001", organization: "Computer Science", employees: 35, budget: "$3.8M", performance: "Meeting", location: "Boston", growthRate: "+8%", status: "Expanding", established: "1998-08-24" }, + { id: "dept-4002", organization: "Business", employees: 42, budget: "$4.5M", performance: "Exceeding", location: "Chicago", growthRate: "+6%", status: "Stable", established: "1995-09-15" }, + { id: "dept-4003", organization: "Engineering", employees: 38, budget: "$5.1M", performance: "Meeting", location: "San Francisco", growthRate: "+4%", status: "Stable", established: "1992-02-11" }, + ], + }, + ], + }, + { + id: "company-5", + organization: "Industrial Systems", + employees: 152, + budget: "$12.9M", + performance: "Meeting", + location: "Detroit", + growthRate: "+3%", + status: "Stable", + established: "2001-01-01", + divisions: [ + { + id: "div-500", + organization: "Production", + employees: 152, + budget: "$12.9M", + performance: "Meeting", + location: "Multiple", + growthRate: "+3%", + status: "Stable", + established: "2001-01-10", + departments: [ + { id: "dept-5001", organization: "Assembly", employees: 78, budget: "$6.2M", performance: "Meeting", location: "Detroit", growthRate: "+2%", status: "Stable", established: "2001-05-18" }, + { id: "dept-5002", organization: "Quality Control", employees: 32, budget: "$2.8M", performance: "Exceeding", location: "Pittsburgh", growthRate: "+5%", status: "Hiring", established: "2003-11-24" }, + { id: "dept-5003", organization: "Logistics", employees: 42, budget: "$3.9M", performance: "Meeting", location: "Indianapolis", growthRate: "+3%", status: "Stable", established: "2005-02-08" }, + ], + }, + ], + }, +]; + +export const rowGroupingConfig = { + headers: rowGroupingHeaders, + rows: rowGroupingData, + tableProps: { + rowGrouping: ["divisions", "departments"] as string[], + 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 ff8c2a861..dc89f8982 100644 --- a/packages/examples/solid/src/demos/row-height/RowHeightDemo.tsx +++ b/packages/examples/solid/src/demos/row-height/RowHeightDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { rowHeightConfig } from "@simple-table/examples-shared"; +import { rowHeightConfig } from "./row-height.demo-data"; import "@simple-table/solid/styles.css"; export default function RowHeightDemo(props: { height?: string | number; theme?: Theme }) { return ( 0 ? books.map((b) => b.title).join(", ") : "None"; }); - const headers: SolidHeaderObject[] = rowSelectionConfig.headers.map((h) => { + const headers: SolidHeaderObject[] = mapToSolidHeaderObjects(rowSelectionConfig.headers.map((h) => { if (h.accessor === "status") { return { ...h, @@ -32,8 +32,8 @@ export default function RowSelectionDemo(props: { }, }; } - return { ...h }; - }); + return h; + })); const handleSelectionChange = (selection: RowSelectionChangeProps) => { const selected = rowSelectionData.filter((book) => 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 new file mode 100644 index 000000000..f5a205153 --- /dev/null +++ b/packages/examples/solid/src/demos/row-selection/row-selection.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + + +export type LibraryBook = { + id: number; + isbn: string; + title: string; + author: string; + genre: string; + yearPublished: number; + pages: number; + rating: number; + status: string; + librarySection: string; + borrowedBy?: string; +}; + +export const rowSelectionData: LibraryBook[] = [ + { id: 1001, isbn: "978-0553418026", title: "The Quantum Chronicles", author: "Dr. Elena Vasquez", genre: "Science Fiction", yearPublished: 2019, pages: 324, rating: 4.7, status: "Available", librarySection: "Fiction A-L" }, + { id: 1002, isbn: "978-0316769488", title: "Digital Renaissance", author: "Marcus Chen", genre: "Technology", yearPublished: 2021, pages: 287, rating: 4.2, status: "Checked Out", librarySection: "Technology", borrowedBy: "Sarah Williams" }, + { id: 1003, isbn: "978-1400079179", title: "Echoes of Ancient Wisdom", author: "Prof. Amara Okafor", genre: "Philosophy", yearPublished: 2018, pages: 456, rating: 4.9, status: "Available", librarySection: "Philosophy" }, + { id: 1004, isbn: "978-0062315007", title: "The Midnight Observatory", author: "Luna Rodriguez", genre: "Mystery", yearPublished: 2020, pages: 298, rating: 4.4, status: "Reserved", librarySection: "Fiction M-Z" }, + { id: 1005, isbn: "978-0544003415", title: "Sustainable Architecture Now", author: "Kai Nakamura", genre: "Architecture", yearPublished: 2022, pages: 368, rating: 4.6, status: "Available", librarySection: "Architecture" }, + { id: 1006, isbn: "978-0147516466", title: "Neural Networks Simplified", author: "Dr. Priya Sharma", genre: "Computer Science", yearPublished: 2021, pages: 412, rating: 4.8, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Alex Thompson" }, + { id: 1007, isbn: "978-0547928227", title: "Culinary Traditions of the World", author: "Isabella Fontana", genre: "Cooking", yearPublished: 2019, pages: 276, rating: 4.3, status: "Available", librarySection: "Lifestyle" }, + { id: 1008, isbn: "978-0525509288", title: "The Biomimicry Revolution", author: "Dr. James Whitfield", genre: "Biology", yearPublished: 2020, pages: 345, rating: 4.5, status: "Available", librarySection: "Science" }, + { id: 1009, isbn: "978-0345391803", title: "Symphonies in Code", author: "Aria Blackwood", genre: "Programming", yearPublished: 2022, pages: 423, rating: 4.7, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Emma Davis" }, + { id: 1010, isbn: "978-0812988407", title: "Urban Gardens & Green Spaces", author: "Miguel Santos", genre: "Gardening", yearPublished: 2021, pages: 189, rating: 4.1, status: "Available", librarySection: "Lifestyle" }, + { id: 1011, isbn: "978-0374533557", title: "The Psychology of Innovation", author: "Dr. Rachel Kim", genre: "Psychology", yearPublished: 2019, pages: 312, rating: 4.6, status: "Reserved", librarySection: "Psychology" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Book ID", width: 80, isSortable: true, type: "number" }, + { accessor: "isbn", label: "ISBN", width: 120, isSortable: true, type: "string" }, + { accessor: "title", label: "Title", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "author", label: "Author", width: 140, isSortable: true, type: "string" }, + { accessor: "genre", label: "Genre", width: 120, isSortable: true, type: "string" }, + { accessor: "yearPublished", label: "Year", width: 80, isSortable: true, type: "number" }, + { accessor: "pages", label: "Pages", width: 80, isSortable: true, type: "number" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number" }, + { accessor: "status", label: "Status", width: 100, isSortable: true, type: "string" }, + { accessor: "librarySection", label: "Section", width: 120, isSortable: true, type: "string" }, +]; + +export const rowSelectionConfig = { + headers: rowSelectionHeaders, + rows: rowSelectionData, + tableProps: { + enableRowSelection: true, + columnResizing: true, + 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 d68cf4357..8855f0832 100644 --- a/packages/examples/solid/src/demos/sales/SalesDemo.tsx +++ b/packages/examples/solid/src/demos/sales/SalesDemo.tsx @@ -1,12 +1,12 @@ import { createSignal, createEffect, onCleanup } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/solid"; import type { Theme, SolidHeaderObject, CellChangeProps } from "@simple-table/solid"; -import { salesConfig, getSalesThemeColors } from "@simple-table/examples-shared"; -import type { SalesRow } from "@simple-table/examples-shared"; +import { salesConfig, getSalesThemeColors } from "./sales.demo-data"; +import type { SalesRow } from "./sales.demo-data"; import "@simple-table/solid/styles.css"; function getHeaders(): SolidHeaderObject[] { - const headers: SolidHeaderObject[] = JSON.parse(JSON.stringify(salesConfig.headers)); + const headers = defaultHeadersFromCore(JSON.parse(JSON.stringify(salesConfig.headers))); const addRenderers = (hdrs: SolidHeaderObject[]) => { for (const h of hdrs) { diff --git a/packages/examples/solid/src/demos/sales/sales.demo-data.ts b/packages/examples/solid/src/demos/sales/sales.demo-data.ts new file mode 100644 index 000000000..3107b4512 --- /dev/null +++ b/packages/examples/solid/src/demos/sales/sales.demo-data.ts @@ -0,0 +1,114 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/solid"; + +export interface SalesRow { + id: number; + repName: string; + dealSize: number; + dealValue: number; + isWon: boolean; + closeDate: string; + commission: number; + profitMargin: number; + dealProfit: number; + category: string; +} + + +const SALES_REP_NAMES = ["Alex Morgan", "Sam Rivera", "Jordan Lee", "Taylor Kim", "Casey Chen", "Riley Park", "Morgan Wells", "Avery Quinn", "Devon Blake", "Harper Cole", "Quinn Foster", "Sage Turner", "Cameron Reed", "Jesse Nash", "Blake Palmer"]; +const CATEGORIES = ["Software", "Hardware", "Services", "Consulting", "Training", "Support"]; + +export function generateSalesData(count: number = 100): SalesRow[] { + return Array.from({ length: count }, (_, i) => { + const dealSize = Math.round((5000 + Math.random() * 200000) * 100) / 100; + const profitMargin = Math.round((0.15 + Math.random() * 0.7) * 1000) / 1000; + const dealValue = Math.round((dealSize / profitMargin) * 100) / 100; + const commission = Math.round(dealValue * 0.1 * 100) / 100; + const dealProfit = Math.round((dealSize - commission) * 100) / 100; + const closeYear = 2023 + Math.floor(Math.random() * 2); + const closeMonth = String(1 + Math.floor(Math.random() * 12)).padStart(2, "0"); + const closeDay = String(1 + Math.floor(Math.random() * 28)).padStart(2, "0"); + + return { + id: i + 1, + repName: SALES_REP_NAMES[i % SALES_REP_NAMES.length], + dealSize, + dealValue, + isWon: Math.random() > 0.35, + closeDate: `${closeYear}-${closeMonth}-${closeDay}`, + commission, + profitMargin, + dealProfit, + category: CATEGORIES[i % CATEGORIES.length], + }; + }); +} + +export const salesData = generateSalesData(100); + +export const salesHeaders: HeaderObject[] = [ + { accessor: "repName", label: "Sales Representative", width: "2fr", minWidth: 200, isSortable: true, isEditable: true, type: "string", tooltip: "Name of the sales representative" }, + { + accessor: "salesMetrics", label: "Sales Metrics", width: 600, isSortable: false, + children: [ + { + accessor: "dealSize", label: "Deal Size", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The size of the deal in dollars", + valueFormatter: ({ value }) => { if (typeof value !== "number") return "—"; return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }, + useFormattedValueForClipboard: true, useFormattedValueForCSV: true, + }, + { accessor: "dealValue", label: "Deal Value", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The value of the deal in dollars" }, + { accessor: "isWon", label: "Status", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "boolean", tooltip: "Whether the deal was won or lost" }, + { + accessor: "closeDate", label: "Close Date", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "date", tooltip: "The date the deal was closed", + valueFormatter: ({ value }) => { + if (typeof value !== "string") return "—"; + const [year, month, day] = value.split("-").map(Number); + const date = new Date(year, month - 1, day); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + ], + }, + { + accessor: "financialMetrics", label: "Financial Metrics", width: "1fr", minWidth: 140, isSortable: false, + children: [ + { accessor: "commission", label: "Commission", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The commission earned from the deal in dollars" }, + { + accessor: "profitMargin", label: "Profit Margin", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The profit margin of the deal", + valueFormatter: ({ value }) => { if (typeof value !== "number") return "—"; return `${(value * 100).toFixed(1)}%`; }, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => { if (typeof value !== "number") return "—"; return `${Math.round(value * 100)}%`; }, + }, + { accessor: "dealProfit", label: "Deal Profit", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "right", type: "number", tooltip: "The profit of the deal in dollars" }, + { + accessor: "category", label: "Category", width: "1fr", minWidth: 140, isSortable: true, isEditable: true, align: "center", type: "enum", tooltip: "The category of the deal", + enumOptions: CATEGORIES.map((c) => ({ label: c, value: c })), + valueGetter: ({ row }) => { + const priorityMap: Record = { Software: 1, Consulting: 2, Services: 3, Hardware: 4, Training: 5, Support: 6 }; + return priorityMap[String(row.category)] || 999; + }, + }, + ], + }, +]; + +export function getSalesThemeColors(theme?: string) { + const isDark = theme === "dark" || theme === "modern-dark"; + return { + gray: isDark ? "#f3f4f6" : "#374151", + grayMuted: isDark ? "#f3f4f6" : "#9ca3af", + successHigh: { color: isDark ? "#86efac" : "#15803d", fontWeight: "bold" as const }, + successMedium: isDark ? "#4ade80" : "#16a34a", + successLow: isDark ? "#22c55e" : "#22c55e", + info: isDark ? "#60a5fa" : "#3b82f6", + warning: isDark ? "#facc15" : "#ca8a04", + progressHigh: isDark ? "#34D399" : "#10B981", + progressMedium: isDark ? "#60A5FA" : "#3B82F6", + progressLow: isDark ? "#FBBF24" : "#D97706", + }; +} + +export const salesConfig = { + headers: salesHeaders, + rows: salesData, +} as const; 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 8fff1bda3..691fbf17b 100644 --- a/packages/examples/solid/src/demos/single-row-children/SingleRowChildrenDemo.tsx +++ b/packages/examples/solid/src/demos/single-row-children/SingleRowChildrenDemo.tsx @@ -1,12 +1,12 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { singleRowChildrenConfig } from "@simple-table/examples-shared"; +import { singleRowChildrenConfig } from "./single-row-children.demo-data"; import "@simple-table/solid/styles.css"; export default function SingleRowChildrenDemo(props: { height?: string | number; theme?: Theme }) { return ( (value as number).toFixed(2) }, + { accessor: "mathGrade", label: "Math", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "scienceGrade", label: "Science", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "englishGrade", label: "English", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "historyGrade", label: "History", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + ], + }, +]; + +export const singleRowChildrenConfig = { + headers: singleRowChildrenHeaders, + rows: singleRowChildrenData, + tableProps: { + columnResizing: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/solid/src/demos/spreadsheet/SpreadsheetDemo.tsx b/packages/examples/solid/src/demos/spreadsheet/SpreadsheetDemo.tsx index f2d61ae6e..3099bbace 100644 --- a/packages/examples/solid/src/demos/spreadsheet/SpreadsheetDemo.tsx +++ b/packages/examples/solid/src/demos/spreadsheet/SpreadsheetDemo.tsx @@ -1,18 +1,18 @@ import { createSignal, createMemo } from "solid-js"; -import { SimpleTable } from "@simple-table/solid"; -import type { Theme, SolidHeaderObject, CellChangeProps, HeaderObject } from "@simple-table/solid"; -import { spreadsheetConfig, recalculateAmortization } from "@simple-table/examples-shared"; -import type { SpreadsheetRow } from "@simple-table/examples-shared"; +import { SimpleTable, defaultHeadersFromCore } from "@simple-table/solid"; +import type { Theme, SolidHeaderObject, CellChangeProps } from "@simple-table/solid"; +import { spreadsheetConfig, recalculateAmortization } from "./spreadsheet.demo-data"; +import type { SpreadsheetRow } from "./spreadsheet.demo-data"; import "@simple-table/solid/styles.css"; -import "@simple-table/examples-shared/styles/spreadsheet-custom.css"; +import "./spreadsheet-custom.css"; export default function SpreadsheetDemo(props: { height?: string | number; theme?: Theme }) { const theme = () => props.theme ?? "light"; const [data, setData] = createSignal([...spreadsheetConfig.rows]); - const [additionalColumns, setAdditionalColumns] = createSignal([]); + const [additionalColumns, setAdditionalColumns] = createSignal([]); const headers = createMemo((): SolidHeaderObject[] => { - const baseHeaders: SolidHeaderObject[] = [...spreadsheetConfig.headers]; + const baseHeaders = defaultHeadersFromCore(spreadsheetConfig.headers); return [ ...baseHeaders, ...additionalColumns(), @@ -28,7 +28,7 @@ export default function SpreadsheetDemo(props: { height?: string | number; theme
`$${(value as number).toFixed(2)}`, + }, + { accessor: "stock", label: "Stock", width: 100, align: "right", isSortable: true, tooltip: "Available inventory count - number of units currently in warehouse stock" }, + { + accessor: "rating", + label: "Rating", + width: 100, + align: "center", + isSortable: true, + tooltip: "Customer satisfaction rating based on verified purchase reviews (scale: 1-5 stars)", + valueFormatter: ({ value }) => `${value}/5`, + }, + { accessor: "lastUpdated", label: "Last Updated", width: 150, isSortable: true, tooltip: "Most recent inventory update date in YYYY-MM-DD format" }, +]; + +export const tooltipConfig = { + headers: tooltipHeaders, + rows: tooltipData, + tableProps: { + columnResizing: true, + 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 abbdc5194..e72dae63b 100644 --- a/packages/examples/solid/src/demos/value-formatter/ValueFormatterDemo.tsx +++ b/packages/examples/solid/src/demos/value-formatter/ValueFormatterDemo.tsx @@ -1,6 +1,6 @@ -import { SimpleTable } from "@simple-table/solid"; +import {SimpleTable, defaultHeadersFromCore} from "@simple-table/solid"; import type { Theme } from "@simple-table/solid"; -import { valueFormatterConfig } from "@simple-table/examples-shared"; +import { valueFormatterConfig } from "./value-formatter.demo-data"; import "@simple-table/solid/styles.css"; export default function ValueFormatterDemo(props: { @@ -9,7 +9,7 @@ export default function ValueFormatterDemo(props: { }) { return ( = { + engineering: "ENG", + marketing: "MKT", + sales: "SLS", + product: "PRD", + design: "DSN", + operations: "OPS", +}; + +export const valueFormatterHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { + accessor: "firstName", + label: "Name", + width: 180, + type: "string", + valueFormatter: ({ value, row }) => { + return `${value as string} ${row.lastName as string}`; + }, + }, + { + accessor: "salary", + label: "Salary", + width: 140, + type: "number", + valueFormatter: ({ value }) => { + return `$${(value as number).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; + }, + useFormattedValueForClipboard: true, + useFormattedValueForCSV: true, + }, + { + accessor: "joinDate", + label: "Join Date", + width: 140, + type: "date", + valueFormatter: ({ value }) => { + const date = new Date(value as string); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + { + accessor: "performanceScore", + label: "Performance", + width: 130, + type: "number", + valueFormatter: ({ value }) => `${((value as number) * 100).toFixed(1)}%`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${Math.round((value as number) * 100)}%`, + }, + { + accessor: "balance", + label: "Balance", + width: 120, + type: "number", + valueFormatter: ({ value }) => { + const balance = value as number; + if (balance === 0) return "\u2014"; + if (balance < 0) return `($${Math.abs(balance).toFixed(2)})`; + return `$${balance.toFixed(2)}`; + }, + }, + { + accessor: "department", + label: "Department", + width: 150, + type: "string", + valueFormatter: ({ value }) => (value as string).toUpperCase(), + exportValueGetter: ({ value }) => { + const str = (value as string).toLowerCase(); + const code = DEPARTMENT_CODES[str] || "OTH"; + return `${(value as string).toUpperCase()} (${code})`; + }, + }, +]; + +export const valueFormatterConfig = { + headers: valueFormatterHeaders, + rows: valueFormatterData, + tableProps: { + selectableCells: true, + }, +} as const; diff --git a/packages/examples/solid/src/main.tsx b/packages/examples/solid/src/main.tsx index 9b606c92f..3ffd3ba07 100644 --- a/packages/examples/solid/src/main.tsx +++ b/packages/examples/solid/src/main.tsx @@ -1,8 +1,8 @@ import { render, Dynamic } from "solid-js/web"; import { lazy, Suspense, createSignal, Show, onMount, onCleanup } from "solid-js"; -import { DEMO_LIST } from "@simple-table/examples-shared"; +import { DEMO_LIST } from "./demo-list"; import type { Theme } from "@simple-table/solid"; -import "../../shared/src/styles/shell.css"; +import "./styles/shell.css"; const registry: Record> = { "quick-start": lazy(() => import("./demos/quick-start/QuickStartDemo")), diff --git a/packages/examples/solid/src/styles/shell.css b/packages/examples/solid/src/styles/shell.css new file mode 100644 index 000000000..e19fa2fe2 --- /dev/null +++ b/packages/examples/solid/src/styles/shell.css @@ -0,0 +1,63 @@ +.examples-shell { + display: flex; + height: 100vh; + overflow: hidden; +} + +.examples-sidebar { + width: 240px; + min-width: 240px; + background: #1e293b; + border-right: 1px solid #334155; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.examples-sidebar-header { + padding: 20px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748b; + border-bottom: 1px solid #334155; +} + +.examples-sidebar-nav { + list-style: none; + padding: 8px 0; + margin: 0; +} + +.examples-sidebar-link { + display: block; + width: 100%; + padding: 10px 20px; + background: none; + border: none; + color: #94a3b8; + text-decoration: none; + font-size: 14px; + font-family: inherit; + text-align: left; + cursor: pointer; + transition: background-color 0.15s, color 0.15s; +} + +.examples-sidebar-link:hover { + background-color: rgba(51, 65, 85, 0.5); + color: #e2e8f0; +} + +.examples-sidebar-link.active { + background-color: rgba(59, 130, 246, 0.15); + color: #60a5fa; + font-weight: 500; +} + +.examples-content { + flex: 1; + overflow-y: auto; + padding: 24px; +} diff --git a/packages/examples/solid/vite.config.ts b/packages/examples/solid/vite.config.ts index ff1c6cf91..90c898f43 100644 --- a/packages/examples/solid/vite.config.ts +++ b/packages/examples/solid/vite.config.ts @@ -13,8 +13,6 @@ export default defineConfig({ { find: "@simple-table/solid/styles.css", replacement: path.resolve(__dirname, "../../core/src/styles/base.css") }, { find: "@simple-table/solid", replacement: path.resolve(__dirname, "../../solid/src/index.ts") }, { find: "simple-table-core", replacement: path.resolve(__dirname, "../../core/src/index.ts") }, - { find: /^@simple-table\/examples-shared\/(.*)$/, replacement: path.resolve(__dirname, "../shared/src/$1") }, - { find: "@simple-table/examples-shared", replacement: path.resolve(__dirname, "../shared/src/index.ts") }, ], }, }); diff --git a/packages/examples/svelte/package.json b/packages/examples/svelte/package.json index 65ee006aa..7c551fb6f 100644 --- a/packages/examples/svelte/package.json +++ b/packages/examples/svelte/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@simple-table/svelte": "workspace:*", - "@simple-table/examples-shared": "workspace:*", "svelte": "^5.0.0" }, "devDependencies": { diff --git a/packages/examples/svelte/src/App.svelte b/packages/examples/svelte/src/App.svelte index a1f51d68b..425702d07 100644 --- a/packages/examples/svelte/src/App.svelte +++ b/packages/examples/svelte/src/App.svelte @@ -1,5 +1,5 @@ { + if (typeof value === "number") { + return value >= 1000000 + ? `${(value / 1000000).toFixed(1)}M` + : value >= 1000 + ? `${(value / 1000).toFixed(0)}K` + : value.toString(); + } + return ""; + }, + }, + { + accessor: "revenue", + label: "Monthly Revenue", + width: 140, + type: "string", + aggregation: { + type: "sum", + parseValue: (value) => { + if (typeof value !== "string") return 0; + const numericValue = parseFloat(value.replace(/[$K]/g, "")); + return isNaN(numericValue) ? 0 : numericValue; + }, + }, + valueFormatter: ({ value }) => { + if (typeof value === "number") return `$${value.toFixed(1)}K`; + if (typeof value === "string") return value; + return ""; + }, + }, + { + accessor: "rating", + label: "Rating", + width: 100, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${value.toFixed(1)} ⭐` : ""), + }, + { + accessor: "contentCount", + label: "Content", + width: 90, + type: "number", + aggregation: { type: "sum" }, + }, + { + accessor: "avgViewTime", + label: "Avg Watch Time", + width: 130, + type: "number", + aggregation: { type: "average" }, + valueFormatter: ({ value }) => (typeof value === "number" ? `${Math.round(value)}min` : ""), + }, + { accessor: "status", label: "Status", width: 120, type: "string" }, +]; + +export const aggregateFunctionsData = [ + { + id: 1, + name: "StreamFlix", + status: "Leading Platform", + categories: [ + { + id: 101, + name: "Gaming", + status: "Trending", + creators: [ + { id: 1001, name: "PixelMaster", followers: 2800000, revenue: "$45.2K", rating: 4.8, contentCount: 328, avgViewTime: 45, status: "Partner" }, + { id: 1002, name: "RetroGamer93", followers: 1200000, revenue: "$28.5K", rating: 4.6, contentCount: 156, avgViewTime: 52, status: "Partner" }, + { id: 1003, name: "SpeedrunQueen", followers: 890000, revenue: "$22.1K", rating: 4.9, contentCount: 89, avgViewTime: 38, status: "Partner" }, + ], + }, + { + id: 102, + name: "Music & Arts", + status: "Growing", + creators: [ + { id: 1101, name: "MelodyMaker", followers: 1650000, revenue: "$31.8K", rating: 4.7, contentCount: 203, avgViewTime: 28, status: "Partner" }, + { id: 1102, name: "DigitalArtist", followers: 720000, revenue: "$18.9K", rating: 4.5, contentCount: 127, avgViewTime: 35, status: "Affiliate" }, + { id: 1103, name: "JazzVibez", followers: 430000, revenue: "$12.4K", rating: 4.8, contentCount: 78, avgViewTime: 42, status: "Affiliate" }, + ], + }, + { + id: 103, + name: "Cooking & Lifestyle", + status: "Stable", + creators: [ + { id: 1201, name: "ChefExtraordinaire", followers: 3200000, revenue: "$58.7K", rating: 4.9, contentCount: 245, avgViewTime: 22, status: "Partner" }, + { id: 1202, name: "HomeDecorGuru", followers: 980000, revenue: "$19.3K", rating: 4.4, contentCount: 134, avgViewTime: 18, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 2, + name: "WatchNow", + status: "Competitor", + categories: [ + { + id: 201, + name: "Tech Reviews", + status: "Hot", + creators: [ + { id: 2001, name: "TechGuru2024", followers: 2100000, revenue: "$42.6K", rating: 4.6, contentCount: 189, avgViewTime: 35, status: "Partner" }, + { id: 2002, name: "GadgetWhisperer", followers: 1450000, revenue: "$29.1K", rating: 4.7, contentCount: 156, avgViewTime: 31, status: "Partner" }, + { id: 2003, name: "CodeReviewer", followers: 680000, revenue: "$16.8K", rating: 4.8, contentCount: 94, avgViewTime: 48, status: "Affiliate" }, + ], + }, + { + id: 202, + name: "Fitness & Health", + status: "Growing", + creators: [ + { id: 2101, name: "FitnessPhenom", followers: 1890000, revenue: "$35.4K", rating: 4.5, contentCount: 312, avgViewTime: 25, status: "Partner" }, + { id: 2102, name: "YogaMaster", followers: 1100000, revenue: "$21.7K", rating: 4.9, contentCount: 178, avgViewTime: 33, status: "Partner" }, + ], + }, + ], + }, + { + id: 3, + name: "CreativeSpace", + status: "Emerging", + categories: [ + { + id: 301, + name: "Photography", + status: "Niche", + creators: [ + { id: 3001, name: "LensArtist", followers: 750000, revenue: "$18.2K", rating: 4.7, contentCount: 145, avgViewTime: 27, status: "Partner" }, + { id: 3002, name: "NatureShooter", followers: 520000, revenue: "$13.5K", rating: 4.6, contentCount: 98, avgViewTime: 29, status: "Affiliate" }, + { id: 3003, name: "PortraitPro", followers: 390000, revenue: "$9.8K", rating: 4.8, contentCount: 67, avgViewTime: 24, status: "Affiliate" }, + ], + }, + { + id: 302, + name: "Animation & VFX", + status: "Specialized", + creators: [ + { id: 3101, name: "3DAnimator", followers: 640000, revenue: "$15.9K", rating: 4.9, contentCount: 58, avgViewTime: 41, status: "Partner" }, + { id: 3102, name: "VFXWizard", followers: 480000, revenue: "$12.3K", rating: 4.7, contentCount: 42, avgViewTime: 38, status: "Affiliate" }, + ], + }, + ], + }, + { + id: 4, + name: "EduStream", + status: "Educational Focus", + categories: [ + { + id: 401, + name: "Science & Math", + status: "Educational", + creators: [ + { id: 4001, name: "MathExplainer", followers: 1340000, revenue: "$26.8K", rating: 4.8, contentCount: 234, avgViewTime: 36, status: "Partner" }, + { id: 4002, name: "PhysicsPhun", followers: 890000, revenue: "$19.4K", rating: 4.6, contentCount: 167, avgViewTime: 42, status: "Partner" }, + { id: 4003, name: "ChemistryLab", followers: 560000, revenue: "$14.2K", rating: 4.7, contentCount: 89, avgViewTime: 33, status: "Affiliate" }, + ], + }, + { + id: 402, + name: "History & Culture", + status: "Informative", + creators: [ + { id: 4101, name: "HistoryBuff", followers: 920000, revenue: "$18.6K", rating: 4.5, contentCount: 145, avgViewTime: 39, status: "Partner" }, + { id: 4102, name: "CultureExplorer", followers: 670000, revenue: "$15.1K", rating: 4.8, contentCount: 112, avgViewTime: 45, status: "Affiliate" }, + ], + }, + ], + }, +]; + +export const aggregateFunctionsConfig = { + headers: aggregateFunctionsHeaders, + rows: aggregateFunctionsData, + tableProps: { + rowGrouping: ["categories", "creators"] as string[], + columnResizing: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/billing/BillingDemo.svelte b/packages/examples/svelte/src/demos/billing/BillingDemo.svelte index b58440e97..278bc8c06 100644 --- a/packages/examples/svelte/src/demos/billing/BillingDemo.svelte +++ b/packages/examples/svelte/src/demos/billing/BillingDemo.svelte @@ -1,8 +1,8 @@ - import { SimpleTable } from "@simple-table/svelte"; + import {SimpleTable, defaultHeadersFromCore} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte"; - import { cellHighlightingConfig } from "@simple-table/examples-shared"; + import { cellHighlightingConfig } from "./cell-highlighting.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); import { SimpleTable } from "@simple-table/svelte"; import type { Theme, HeaderObject, CellRenderer } from "@simple-table/svelte"; - import { cellRendererConfig } from "@simple-table/examples-shared"; - import type { CellRendererEmployee } from "@simple-table/examples-shared"; + import { cellRendererConfig } from "./cell-renderer.demo-data"; + import type { CellRendererEmployee } from "./cell-renderer.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); diff --git a/packages/examples/svelte/src/demos/cell-renderer/cell-renderer.demo-data.ts b/packages/examples/svelte/src/demos/cell-renderer/cell-renderer.demo-data.ts new file mode 100644 index 000000000..0f26e977c --- /dev/null +++ b/packages/examples/svelte/src/demos/cell-renderer/cell-renderer.demo-data.ts @@ -0,0 +1,51 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/svelte"; + + +export type CellRendererEmployee = { + id: number; + name: string; + website: string; + status: string; + progress: number; + rating: number; + verified: boolean; + tags: string[]; + teamMembers: { name: string; role: string }[]; +}; + +export const cellRendererData: CellRendererEmployee[] = [ + { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] }, + { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] }, + { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] }, + { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] }, + { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] }, + { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] }, + { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] }, + { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] }, + { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] }, + { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] }, + { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { accessor: "name", label: "Name", width: 180, type: "string" }, + { accessor: "teamMembers", label: "Team", width: 280, type: "string" }, + { accessor: "website", label: "Website", width: 180, type: "string" }, + { accessor: "status", label: "Status", width: 120, type: "string" }, + { accessor: "progress", label: "Progress", width: 150, type: "number" }, + { accessor: "rating", label: "Rating", width: 150, type: "number" }, + { accessor: "verified", label: "Verified", width: 100, type: "boolean" }, + { accessor: "tags", label: "Tags", width: 250, type: "string" }, +]; + +export const cellRendererConfig = { + headers: cellRendererHeaders, + rows: cellRendererData, + tableProps: { + selectableCells: true, + customTheme: { rowHeight: 48 }, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/charts/ChartsDemo.svelte b/packages/examples/svelte/src/demos/charts/ChartsDemo.svelte index 6904f22d0..11d2c9ede 100644 --- a/packages/examples/svelte/src/demos/charts/ChartsDemo.svelte +++ b/packages/examples/svelte/src/demos/charts/ChartsDemo.svelte @@ -1,7 +1,7 @@ ({ row }: { row: Record }) => + `$${((row[accessor] as number) || 0).toLocaleString()}`; + +const monthCol = (accessor: string, label: string): HeaderObject => ({ + accessor, + label, + width: 100, + showWhen: "parentExpanded" as const, + isSortable: true, + align: "right", + type: "number", + cellRenderer: fmt(accessor), +}); + +export const collapsibleColumnsHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, isSortable: true }, + { accessor: "name", label: "Sales Rep", minWidth: 150, width: "1fr", isSortable: true }, + { accessor: "region", label: "Region", width: 140, isSortable: true }, + { + accessor: "quarterlySales", + label: "Quarterly Sales", + width: 500, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "totalSales", label: "Total Sales", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("totalSales") }, + { accessor: "q1Sales", label: "Q1", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q1Sales") }, + { accessor: "q2Sales", label: "Q2", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q2Sales") }, + { accessor: "q3Sales", label: "Q3", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q3Sales") }, + { accessor: "q4Sales", label: "Q4", width: 120, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("q4Sales") }, + ], + }, + { + accessor: "monthlyPerformance", + label: "Monthly Performance", + width: 800, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "avgMonthly", label: "Avg Monthly", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("avgMonthly") }, + { accessor: "bestMonth", label: "Best Month", width: 130, showWhen: "parentCollapsed" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("bestMonth") }, + 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"), + monthCol("oct", "Oct"), monthCol("nov", "Nov"), monthCol("dec", "Dec"), + ], + }, + { + accessor: "productCategories", + label: "Product Categories", + width: 450, + collapsible: true, + collapseDefault: true, + children: [ + { accessor: "topCategory", label: "Top Category", width: 140, showWhen: "parentCollapsed" as const, isSortable: true, type: "string" }, + { accessor: "softwareSales", label: "Software", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("softwareSales") }, + { accessor: "hardwareSales", label: "Hardware", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("hardwareSales") }, + { accessor: "servicesSales", label: "Services", width: 130, showWhen: "parentExpanded" as const, isSortable: true, align: "right", type: "number", cellRenderer: fmt("servicesSales") }, + ], + }, +]; + +export const collapsibleColumnsConfig = { + headers: collapsibleColumnsHeaders, + rows: collapsibleColumnsData, + tableProps: { + columnResizing: true, + editColumns: true, + selectableCells: true, + columnReordering: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/column-alignment/ColumnAlignmentDemo.svelte b/packages/examples/svelte/src/demos/column-alignment/ColumnAlignmentDemo.svelte index a0b8d7b4e..1d7929548 100644 --- a/packages/examples/svelte/src/demos/column-alignment/ColumnAlignmentDemo.svelte +++ b/packages/examples/svelte/src/demos/column-alignment/ColumnAlignmentDemo.svelte @@ -1,14 +1,14 @@ import { SimpleTable } from "@simple-table/svelte"; import type { Theme, HeaderObject } from "@simple-table/svelte"; - import { columnEditingData, columnEditingHeaders } from "@simple-table/examples-shared"; + import { columnEditingData, columnEditingHeaders } from "./column-editing.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); diff --git a/packages/examples/svelte/src/demos/column-editing/column-editing.demo-data.ts b/packages/examples/svelte/src/demos/column-editing/column-editing.demo-data.ts new file mode 100644 index 000000000..643f3d1bd --- /dev/null +++ b/packages/examples/svelte/src/demos/column-editing/column-editing.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/svelte"; + + +export const columnEditingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", width: 150, type: "string" }, + { accessor: "department", label: "Department", width: 150, type: "string" }, +]; + +export const columnEditingData = [ + { 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" }, + { id: 4, name: "Luna Martinez", age: 23, role: "Junior Developer", department: "Engineering", email: "luna.martinez@company.com" }, + { id: 5, name: "Tyler Anderson", age: 31, role: "DevOps Engineer", department: "Operations", email: "tyler.anderson@company.com" }, + { id: 6, name: "Zara Kim", age: 28, role: "Product Designer", department: "Design", email: "zara.kim@company.com" }, + { id: 7, name: "Kai Thompson", age: 26, role: "Full Stack Developer", department: "Engineering", email: "kai.thompson@company.com" }, + { id: 8, name: "Ava Singh", age: 33, role: "Product Manager", department: "Product", email: "ava.singh@company.com" }, + { id: 9, name: "Jordan Walsh", age: 25, role: "Marketing Specialist", department: "Growth", email: "jordan.walsh@company.com" }, + { id: 10, name: "Phoenix Lee", age: 30, role: "Backend Developer", department: "Engineering", email: "phoenix.lee@company.com" }, + { id: 11, name: "River Jackson", age: 24, role: "Growth Designer", department: "Design", email: "river.jackson@company.com" }, + { id: 12, name: "Atlas Morgan", age: 32, role: "Tech Lead", department: "Engineering", email: "atlas.morgan@company.com" }, +]; + +export const columnEditingConfig = { + headers: columnEditingHeaders, + rows: columnEditingData, + tableProps: { enableHeaderEditing: true, selectableColumns: true }, +} as const; diff --git a/packages/examples/svelte/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.svelte b/packages/examples/svelte/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.svelte index 6d7d30ca5..76701b072 100644 --- a/packages/examples/svelte/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.svelte +++ b/packages/examples/svelte/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.svelte @@ -1,12 +1,12 @@ `$${(value as number).toLocaleString()}`, + }, + { accessor: "department", label: "Department", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const columnEditorCustomRendererConfig = { + headers: columnEditorCustomRendererHeaders, + rows: columnEditorCustomRendererData, + tableProps: { + editColumns: true, + }, +} as const; + +export const COLUMN_EDITOR_TEXT = "Manage Columns"; +export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…"; + +export function buildVanillaColumnEditorRowRenderer(props: ColumnEditorRowRendererProps): HTMLElement { + const row = document.createElement("div"); + Object.assign(row.style, { + display: "flex", + alignItems: "center", + gap: "8px", + padding: "6px 8px", + borderRadius: "6px", + background: "#f8fafc", + marginBottom: "4px", + }); + + if (props.components.checkbox) { + const span = document.createElement("span"); + if (typeof props.components.checkbox === "string") { + span.innerHTML = props.components.checkbox; + } else { + span.appendChild(props.components.checkbox as Node); + } + row.appendChild(span); + } + + const label = document.createElement("span"); + Object.assign(label.style, { flex: "1", fontSize: "13px", fontWeight: "500" }); + label.textContent = props.header.label; + row.appendChild(label); + + if (props.components.dragIcon) { + const span = document.createElement("span"); + Object.assign(span.style, { cursor: "grab", opacity: "0.5" }); + if (typeof props.components.dragIcon === "string") { + span.innerHTML = props.components.dragIcon; + } else { + span.appendChild(props.components.dragIcon as Node); + } + row.appendChild(span); + } + + return row; +} diff --git a/packages/examples/svelte/src/demos/column-filtering/ColumnFilteringDemo.svelte b/packages/examples/svelte/src/demos/column-filtering/ColumnFilteringDemo.svelte index adbf6e34b..146fdc3db 100644 --- a/packages/examples/svelte/src/demos/column-filtering/ColumnFilteringDemo.svelte +++ b/packages/examples/svelte/src/demos/column-filtering/ColumnFilteringDemo.svelte @@ -1,14 +1,14 @@ { + const salary = row.salary as number; + return `$${salary.toLocaleString()}`; + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 130, + type: "date", + isSortable: true, + filterable: true, + }, + { + accessor: "isActive", + label: "Active", + width: 100, + align: "center", + type: "boolean", + isSortable: true, + filterable: true, + }, +]; + +export const columnFilteringConfig = { + headers: columnFilteringHeaders, + rows: COLUMN_FILTERING_DATA, +} as const; diff --git a/packages/examples/svelte/src/demos/column-pinning/ColumnPinningDemo.svelte b/packages/examples/svelte/src/demos/column-pinning/ColumnPinningDemo.svelte index 605152759..0195e3f89 100644 --- a/packages/examples/svelte/src/demos/column-pinning/ColumnPinningDemo.svelte +++ b/packages/examples/svelte/src/demos/column-pinning/ColumnPinningDemo.svelte @@ -1,14 +1,14 @@ import { SimpleTable } from "@simple-table/svelte"; import type { Theme, HeaderObject } from "@simple-table/svelte"; - import { columnReorderingConfig } from "@simple-table/examples-shared"; + import { columnReorderingConfig } from "./column-reordering.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); diff --git a/packages/examples/svelte/src/demos/column-reordering/column-reordering.demo-data.ts b/packages/examples/svelte/src/demos/column-reordering/column-reordering.demo-data.ts new file mode 100644 index 000000000..bce2edd68 --- /dev/null +++ b/packages/examples/svelte/src/demos/column-reordering/column-reordering.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/svelte"; + + +export const columnReorderingHeaders: HeaderObject[] = [ + { 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" }, + { accessor: "role", label: "Role", minWidth: 100, width: "1fr", type: "string" }, + { accessor: "department", disableReorder: true, label: "Department", width: "1fr", type: "string" }, +]; + +export const columnReorderingData = [ + { 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" }, + { id: 4, name: "Cadet Orbit Williams", age: 26, role: "Flight Engineer", department: "Engineering" }, + { id: 5, name: "Dr. Galaxy Chen", age: 31, role: "Life Support Specialist", department: "Engineering" }, + { id: 6, name: "Lt. Meteor Lee", age: 29, role: "Navigation Officer", department: "Flight Operations" }, + { id: 7, name: "Dr. Comet Hassan", age: 33, role: "Mission Planner", department: "Planning" }, + { id: 8, name: "Major Pulsar White", age: 36, role: "Communications Director", department: "Communications" }, + { id: 9, name: "Specialist Quasar Black", age: 28, role: "Systems Analyst", department: "Technology" }, + { id: 10, name: "Engineer Supernova Blue", age: 35, role: "Propulsion Engineer", department: "Engineering" }, + { id: 11, name: "Dr. Aurora Kumar", age: 30, role: "Planetary Geologist", department: "Science" }, + { id: 12, name: "Admiral Cosmos Silver", age: 45, role: "Program Director", department: "Leadership" }, +]; + +export const columnReorderingConfig = { + headers: columnReorderingHeaders, + rows: columnReorderingData, + tableProps: { columnReordering: true }, +} as const; diff --git a/packages/examples/svelte/src/demos/column-resizing/ColumnResizingDemo.svelte b/packages/examples/svelte/src/demos/column-resizing/ColumnResizingDemo.svelte index 8450e6687..5fa4366e9 100644 --- a/packages/examples/svelte/src/demos/column-resizing/ColumnResizingDemo.svelte +++ b/packages/examples/svelte/src/demos/column-resizing/ColumnResizingDemo.svelte @@ -1,7 +1,7 @@ - import { SimpleTable } from "@simple-table/svelte"; + import {SimpleTable, defaultHeadersFromCore} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte"; - import { columnSortingConfig } from "@simple-table/examples-shared"; + import { columnSortingConfig } from "./column-sorting.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); { + return (value as string).charAt(0).toUpperCase() + (value as string).slice(1); + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 140, + isSortable: true, + type: "date", + valueFormatter: ({ value }) => { + if (typeof value === "string") { + return new Date(value).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + return String(value); + }, + }, +]; + +export const columnSortingConfig = { + headers: columnSortingHeaders, + rows: COLUMN_SORTING_DATA, + tableProps: { + initialSortColumn: "age", + initialSortDirection: "desc" as const, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/column-visibility/ColumnVisibilityDemo.svelte b/packages/examples/svelte/src/demos/column-visibility/ColumnVisibilityDemo.svelte index 81fbe4fb0..f45776c26 100644 --- a/packages/examples/svelte/src/demos/column-visibility/ColumnVisibilityDemo.svelte +++ b/packages/examples/svelte/src/demos/column-visibility/ColumnVisibilityDemo.svelte @@ -1,14 +1,14 @@ new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const columnVisibilityConfig = { + headers: columnVisibilityHeaders, + rows: columnVisibilityData, + tableProps: { + editColumns: true, + columnEditorConfig: { + text: "Manage Columns", + searchEnabled: true, + searchPlaceholder: "Search columns…", + }, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/column-width/ColumnWidthDemo.svelte b/packages/examples/svelte/src/demos/column-width/ColumnWidthDemo.svelte index 529532955..d64f262d5 100644 --- a/packages/examples/svelte/src/demos/column-width/ColumnWidthDemo.svelte +++ b/packages/examples/svelte/src/demos/column-width/ColumnWidthDemo.svelte @@ -1,7 +1,7 @@ (value as number).toLocaleString(), + }, + { + accessor: "date", + label: "Date", + width: 130, + type: "date", + isSortable: true, + valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }), + }, +]; + +export const customIconsConfig = { + headers: customIconsHeaders, + rows: customIconsData, +} as const; + +export function createSvgIcon(pathD: string, color = "#3b82f6", size = 14): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", String(size)); + svg.setAttribute("height", String(size)); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", color); + svg.setAttribute("stroke-width", "2.5"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", pathD); + svg.appendChild(path); + return svg; +} + +export const ICON_PATHS = { + sortUp: "M12 19V5M5 12l7-7 7 7", + sortDown: "M12 5v14M19 12l-7 7-7-7", + filter: "M3 4h18l-7 8.5V18l-4 2V12.5L3 4z", + expand: "M9 5l7 7-7 7", + next: "M9 5l7 7-7 7", + prev: "M15 19l-7-7 7-7", +} as const; + +export function buildVanillaCustomIcons() { + return { + sortUp: createSvgIcon(ICON_PATHS.sortUp, "#6366f1"), + sortDown: createSvgIcon(ICON_PATHS.sortDown, "#6366f1"), + filter: createSvgIcon(ICON_PATHS.filter, "#8b5cf6"), + expand: createSvgIcon(ICON_PATHS.expand, "#6366f1"), + next: createSvgIcon(ICON_PATHS.next, "#2563eb"), + prev: createSvgIcon(ICON_PATHS.prev, "#2563eb"), + }; +} diff --git a/packages/examples/svelte/src/demos/custom-theme/CustomThemeDemo.svelte b/packages/examples/svelte/src/demos/custom-theme/CustomThemeDemo.svelte index d1f85784c..a673586f7 100644 --- a/packages/examples/svelte/src/demos/custom-theme/CustomThemeDemo.svelte +++ b/packages/examples/svelte/src/demos/custom-theme/CustomThemeDemo.svelte @@ -1,15 +1,15 @@ formatPhone(value as string), + }, + { accessor: "email", label: "Email", width: 180, type: "string" }, + { accessor: "city", label: "City", width: 140, type: "string", isSortable: true }, + { accessor: "status", label: "Status", width: 100, type: "string" }, +]; + +export const customThemeConfig = { + headers: customThemeHeaders, + rows: customThemeData, + tableProps: { + theme: "custom" as const, + customTheme: { + rowHeight: 40, + headerHeight: 44, + }, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.svelte b/packages/examples/svelte/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.svelte index 88b39a0a7..6e324843a 100644 --- a/packages/examples/svelte/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.svelte +++ b/packages/examples/svelte/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.svelte @@ -1,12 +1,12 @@ - import { SimpleTable } from "@simple-table/svelte"; + import {SimpleTable, defaultHeadersFromCore} from "@simple-table/svelte"; import type { Theme, TableFilterState } from "@simple-table/svelte"; - import { externalFilterConfig, matchesFilter } from "@simple-table/examples-shared"; + import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); @@ -25,7 +25,7 @@ Number(filter.value); + case "lessThan": + return Number(value) < Number(filter.value); + case "greaterThanOrEqual": + return Number(value) >= Number(filter.value); + case "lessThanOrEqual": + return Number(value) <= Number(filter.value); + case "between": + return ( + filter.values != null && + Number(value) >= Number(filter.values[0]) && + Number(value) <= Number(filter.values[1]) + ); + case "in": + return filter.values != null && filter.values.includes(value); + case "notIn": + return filter.values != null && !filter.values.includes(value); + case "isEmpty": + return value == null || value === ""; + case "isNotEmpty": + return value != null && value !== ""; + default: + return true; + } +} + +const DEPARTMENT_OPTIONS = [ + { label: "AI Research", value: "AI Research" }, + { label: "UX Design", value: "UX Design" }, + { label: "DevOps", value: "DevOps" }, + { label: "Marketing", value: "Marketing" }, + { label: "Engineering", value: "Engineering" }, + { label: "Product", value: "Product" }, + { label: "Sales", value: "Sales" }, +]; + +const LOCATION_OPTIONS = [ + { label: "San Francisco", value: "San Francisco" }, + { label: "Tokyo", value: "Tokyo" }, + { label: "Lagos", value: "Lagos" }, + { label: "Mexico City", value: "Mexico City" }, + { label: "Kolkata", value: "Kolkata" }, + { label: "Stockholm", value: "Stockholm" }, + { label: "Dubai", value: "Dubai" }, + { label: "Milan", value: "Milan" }, + { label: "Seoul", value: "Seoul" }, + { label: "Austin", value: "Austin" }, + { label: "London", value: "London" }, + { label: "Moscow", value: "Moscow" }, +]; + +export const externalFilterData = [ + { 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" }, + { id: 4, name: "Santiago Rodriguez", age: 27, email: "s.rodriguez@techcorp.com", salary: 82000, department: "Marketing", active: true, location: "Mexico City" }, + { id: 5, name: "Priya Chakraborty", age: 33, email: "priya.c@techcorp.com", salary: 118000, department: "Engineering", active: true, location: "Kolkata" }, + { id: 6, name: "Magnus Eriksson", age: 38, email: "magnus.erik@techcorp.com", salary: 110000, department: "Product", active: false, location: "Stockholm" }, + { id: 7, name: "Zara Al-Rashid", age: 31, email: "zara.alrashid@techcorp.com", salary: 98000, department: "Sales", active: true, location: "Dubai" }, + { id: 8, name: "Luca Rossi", age: 26, email: "luca.rossi@techcorp.com", salary: 75000, department: "Marketing", active: true, location: "Milan" }, + { id: 9, name: "Dr. Sarah Kim", age: 45, email: "sarah.kim@techcorp.com", salary: 165000, department: "AI Research", active: true, location: "Seoul" }, + { id: 10, name: "Olumide Adebayo", age: 30, email: "olumide.a@techcorp.com", salary: 105000, department: "Engineering", active: false, location: "Austin" }, + { id: 11, name: "Isabella Chen", age: 24, email: "isabella.chen@techcorp.com", salary: 68000, department: "UX Design", active: true, location: "London" }, + { id: 12, name: "Dmitri Volkov", age: 39, email: "dmitri.volkov@techcorp.com", salary: 135000, department: "DevOps", active: true, location: "Moscow" }, +]; + +export const externalFilterHeaders: HeaderObject[] = [ + { accessor: "name", label: "Name", width: "1fr", minWidth: 120, filterable: true, type: "string" }, + { accessor: "age", label: "Age", width: 120, filterable: true, type: "number" }, + { + accessor: "department", + label: "Department", + width: 150, + filterable: true, + type: "enum", + enumOptions: DEPARTMENT_OPTIONS, + }, + { + accessor: "location", + label: "Location", + width: 150, + filterable: true, + type: "enum", + enumOptions: LOCATION_OPTIONS, + }, + { accessor: "active", label: "Active", width: 120, filterable: true, type: "boolean" }, + { + accessor: "salary", + label: "Salary", + width: 120, + filterable: true, + type: "number", + align: "right", + valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, + }, +]; + +export const externalFilterConfig = { + headers: externalFilterHeaders, + rows: externalFilterData, + tableProps: { externalFilterHandling: true, columnResizing: true }, +} as const; diff --git a/packages/examples/svelte/src/demos/external-sort/ExternalSortDemo.svelte b/packages/examples/svelte/src/demos/external-sort/ExternalSortDemo.svelte index d5d704454..6b12f13b9 100644 --- a/packages/examples/svelte/src/demos/external-sort/ExternalSortDemo.svelte +++ b/packages/examples/svelte/src/demos/external-sort/ExternalSortDemo.svelte @@ -1,7 +1,7 @@ `$${(value as number).toLocaleString()}`, + }, +]; + +export const externalSortConfig = { + headers: externalSortHeaders, + rows: externalSortData, + tableProps: { externalSortHandling: true, columnResizing: true }, +} as const; diff --git a/packages/examples/svelte/src/demos/footer-renderer/FooterRendererDemo.svelte b/packages/examples/svelte/src/demos/footer-renderer/FooterRendererDemo.svelte index 8f52933a5..e9d514750 100644 --- a/packages/examples/svelte/src/demos/footer-renderer/FooterRendererDemo.svelte +++ b/packages/examples/svelte/src/demos/footer-renderer/FooterRendererDemo.svelte @@ -1,7 +1,7 @@ import { SimpleTable } from "@simple-table/svelte"; import type { Theme, HeaderObject } from "@simple-table/svelte"; - import { headerRendererConfig } from "@simple-table/examples-shared"; + import { headerRendererConfig } from "./header-renderer.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); diff --git a/packages/examples/svelte/src/demos/header-renderer/header-renderer.demo-data.ts b/packages/examples/svelte/src/demos/header-renderer/header-renderer.demo-data.ts new file mode 100644 index 000000000..42ea5ce61 --- /dev/null +++ b/packages/examples/svelte/src/demos/header-renderer/header-renderer.demo-data.ts @@ -0,0 +1,32 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject, Row } from "@simple-table/svelte"; + + +export const headerRendererData: Row[] = [ + { 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" }, + { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering" }, + { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics" }, + { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering" }, + { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design" }, + { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering" }, +]; + +export const headerRendererHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number", isSortable: true }, + { accessor: "name", label: "Employee Name", width: 180, type: "string", isSortable: true }, + { accessor: "email", label: "Email Address", width: 200, type: "string" }, + { accessor: "role", label: "Job Role", width: 130, type: "string", isSortable: true }, + { accessor: "salary", label: "Annual Salary", width: 140, type: "number", isSortable: true }, + { accessor: "department", label: "Department", width: 150, type: "string", isSortable: true }, +]; + +export const headerRendererConfig = { + headers: headerRendererHeaders, + rows: headerRendererData, + tableProps: { + selectableCells: true, + columnResizing: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/hr/HRDemo.svelte b/packages/examples/svelte/src/demos/hr/HRDemo.svelte index 9ac21b06c..59770f0f3 100644 --- a/packages/examples/svelte/src/demos/hr/HRDemo.svelte +++ b/packages/examples/svelte/src/demos/hr/HRDemo.svelte @@ -1,8 +1,8 @@ - import { SimpleTable } from "@simple-table/svelte"; + import {SimpleTable, defaultHeadersFromCore} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte"; - import { nestedTablesConfig, generateNestedTablesData } from "@simple-table/examples-shared"; + import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "500px", theme }: { height?: string | number; theme?: Theme } = $props(); @@ -11,7 +11,7 @@ (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) => ({ + divisionId: `DIV-${String(companyIndex * 10 + divisionIndex).padStart(3, "0")}`, + divisionName: randomElement(divisionTypes), + revenue: `$${randomInt(5, 25)}B`, + profitMargin: `${randomInt(15, 50)}%`, + headcount: randomInt(50, 500), + location: randomElement(cities), +}); + +const generateCompany = (companyIndex: number) => { + const divisions = Array.from({ length: randomInt(3, 7) }, (_, i) => generateDivision(i, companyIndex)); + return { + id: companyIndex + 1, + companyName: `${randomElement(companyNames)} ${randomElement(suffixes)}`, + industry: randomElement(industries), + founded: randomInt(1985, 2020), + headquarters: randomElement(cities), + stockSymbol: Array.from({ length: 4 }, () => String.fromCharCode(65 + randomInt(0, 25))).join(""), + marketCap: `$${randomInt(10, 200)}B`, + ceo: `${randomElement(firstNames)} ${randomElement(lastNames)}`, + revenue: `$${randomInt(5, 60)}B`, + employees: randomInt(5000, 100000), + divisions, + }; +}; + +export const generateNestedTablesData = (count: number = 25) => Array.from({ length: count }, (_, i) => generateCompany(i)); + +export const nestedTablesDivisionHeaders: HeaderObject[] = [ + { accessor: "divisionId", label: "Division ID", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "profitMargin", label: "Profit Margin", width: 130 }, + { accessor: "headcount", label: "Headcount", width: 110, type: "number" }, + { accessor: "location", label: "Location", width: "1fr" }, +]; + +export const nestedTablesHeaders: HeaderObject[] = [ + { + accessor: "companyName", + label: "Company", + width: 200, + expandable: true, + nestedTable: { defaultHeaders: nestedTablesDivisionHeaders }, + }, + { accessor: "stockSymbol", label: "Symbol", width: 100 }, + { accessor: "marketCap", label: "Market Cap", width: 120 }, + { accessor: "revenue", label: "Revenue", width: 120 }, + { accessor: "employees", label: "Employees", width: 120, type: "number" }, +]; + +export const nestedTablesConfig = { + headers: nestedTablesHeaders, + tableProps: { + rowGrouping: ["divisions"] as string[], + getRowId: ({ row }: { row: Record }) => row.id as string, + expandAll: false, + columnResizing: true, + autoExpandColumns: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/pagination/PaginationDemo.svelte b/packages/examples/svelte/src/demos/pagination/PaginationDemo.svelte index 0323e5198..d5690530a 100644 --- a/packages/examples/svelte/src/demos/pagination/PaginationDemo.svelte +++ b/packages/examples/svelte/src/demos/pagination/PaginationDemo.svelte @@ -1,7 +1,7 @@ import { SimpleTable } from "@simple-table/svelte"; import type { Theme, HeaderObject } from "@simple-table/svelte"; - import { programmaticControlConfig, PROGRAMMATIC_CONTROL_STATUS_COLORS } from "@simple-table/examples-shared"; + import { programmaticControlConfig, PROGRAMMATIC_CONTROL_STATUS_COLORS } from "./programmatic-control.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); diff --git a/packages/examples/svelte/src/demos/programmatic-control/programmatic-control.demo-data.ts b/packages/examples/svelte/src/demos/programmatic-control/programmatic-control.demo-data.ts new file mode 100644 index 000000000..09f100c04 --- /dev/null +++ b/packages/examples/svelte/src/demos/programmatic-control/programmatic-control.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/svelte"; + + +export const STATUS_COLORS: Record = { + Available: { bg: "#dcfce7", color: "#166534" }, + "Low Stock": { bg: "#fef3c7", color: "#92400e" }, + "Out of Stock": { bg: "#fee2e2", color: "#991b1b" }, +}; + +export const programmaticControlData = [ + { 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" }, + { id: 4, name: "Standing Desk", category: "Furniture", price: 399.99, stock: 0, status: "Out of Stock" }, + { id: 5, name: "Office Chair", category: "Furniture", price: 249.99, stock: 56, status: "Available" }, + { id: 6, name: "Monitor Stand", category: "Furniture", price: 79.99, stock: 8, status: "Low Stock" }, + { id: 7, name: "Notebook Set", category: "Stationery", price: 12.99, stock: 445, status: "Available" }, + { id: 8, name: "Pen Collection", category: "Stationery", price: 19.99, stock: 312, status: "Available" }, + { id: 9, name: "Desk Organizer", category: "Stationery", price: 24.99, stock: 5, status: "Low Stock" }, + { id: 10, name: "Coffee Maker", category: "Appliances", price: 89.99, stock: 78, status: "Available" }, + { id: 11, name: "Electric Kettle", category: "Appliances", price: 34.99, stock: 134, status: "Available" }, + { id: 12, name: "Desk Lamp LED", category: "Appliances", price: 44.99, stock: 0, status: "Out of Stock" }, +]; + +export const programmaticControlHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 70, type: "number", isSortable: true, filterable: true }, + { accessor: "name", label: "Product Name", width: "1fr", minWidth: 150, type: "string", isSortable: true, filterable: true }, + { + accessor: "category", + label: "Category", + width: 140, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Electronics", "Furniture", "Stationery", "Appliances"].map((v) => ({ label: v, value: v })), + }, + { accessor: "price", label: "Price", width: 110, align: "right", type: "number", isSortable: true, filterable: true, valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}` }, + { accessor: "stock", label: "Stock", width: 100, align: "right", type: "number", isSortable: true, filterable: true }, + { + accessor: "status", + label: "Status", + width: 110, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Available", "Low Stock", "Out of Stock"].map((v) => ({ label: v, value: v })), + }, +]; + +export const programmaticControlConfig = { + headers: programmaticControlHeaders, + rows: programmaticControlData, +} as const; + +export { STATUS_COLORS as PROGRAMMATIC_CONTROL_STATUS_COLORS }; diff --git a/packages/examples/svelte/src/demos/quick-filter/QuickFilterDemo.svelte b/packages/examples/svelte/src/demos/quick-filter/QuickFilterDemo.svelte index 6a192200e..a9c4150b6 100644 --- a/packages/examples/svelte/src/demos/quick-filter/QuickFilterDemo.svelte +++ b/packages/examples/svelte/src/demos/quick-filter/QuickFilterDemo.svelte @@ -1,7 +1,7 @@ - import { SimpleTable } from "@simple-table/svelte"; + import {SimpleTable, defaultHeadersFromCore} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte"; - import { rowGroupingConfig } from "@simple-table/examples-shared"; + import { rowGroupingConfig } from "./row-grouping.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props(); @@ -30,7 +30,7 @@ }) => String(row.id), + columnResizing: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/row-height/RowHeightDemo.svelte b/packages/examples/svelte/src/demos/row-height/RowHeightDemo.svelte index db39cb8ca..5e7f69d93 100644 --- a/packages/examples/svelte/src/demos/row-height/RowHeightDemo.svelte +++ b/packages/examples/svelte/src/demos/row-height/RowHeightDemo.svelte @@ -1,14 +1,14 @@ import { SimpleTable } from "@simple-table/svelte"; import type { Theme, RowSelectionChangeProps, HeaderObject } from "@simple-table/svelte"; - import { rowSelectionConfig, rowSelectionData } from "@simple-table/examples-shared"; - import type { LibraryBook } from "@simple-table/examples-shared"; + import { rowSelectionConfig, rowSelectionData } from "./row-selection.demo-data"; + import type { LibraryBook } from "./row-selection.demo-data"; import "@simple-table/svelte/styles.css"; let { height = "348px", theme }: { height?: string | number; theme?: Theme } = $props(); diff --git a/packages/examples/svelte/src/demos/row-selection/row-selection.demo-data.ts b/packages/examples/svelte/src/demos/row-selection/row-selection.demo-data.ts new file mode 100644 index 000000000..9ed67b4ab --- /dev/null +++ b/packages/examples/svelte/src/demos/row-selection/row-selection.demo-data.ts @@ -0,0 +1,56 @@ +// Self-contained demo table setup for this example. +import type { HeaderObject } from "@simple-table/svelte"; + + +export type LibraryBook = { + id: number; + isbn: string; + title: string; + author: string; + genre: string; + yearPublished: number; + pages: number; + rating: number; + status: string; + librarySection: string; + borrowedBy?: string; +}; + +export const rowSelectionData: LibraryBook[] = [ + { id: 1001, isbn: "978-0553418026", title: "The Quantum Chronicles", author: "Dr. Elena Vasquez", genre: "Science Fiction", yearPublished: 2019, pages: 324, rating: 4.7, status: "Available", librarySection: "Fiction A-L" }, + { id: 1002, isbn: "978-0316769488", title: "Digital Renaissance", author: "Marcus Chen", genre: "Technology", yearPublished: 2021, pages: 287, rating: 4.2, status: "Checked Out", librarySection: "Technology", borrowedBy: "Sarah Williams" }, + { id: 1003, isbn: "978-1400079179", title: "Echoes of Ancient Wisdom", author: "Prof. Amara Okafor", genre: "Philosophy", yearPublished: 2018, pages: 456, rating: 4.9, status: "Available", librarySection: "Philosophy" }, + { id: 1004, isbn: "978-0062315007", title: "The Midnight Observatory", author: "Luna Rodriguez", genre: "Mystery", yearPublished: 2020, pages: 298, rating: 4.4, status: "Reserved", librarySection: "Fiction M-Z" }, + { id: 1005, isbn: "978-0544003415", title: "Sustainable Architecture Now", author: "Kai Nakamura", genre: "Architecture", yearPublished: 2022, pages: 368, rating: 4.6, status: "Available", librarySection: "Architecture" }, + { id: 1006, isbn: "978-0147516466", title: "Neural Networks Simplified", author: "Dr. Priya Sharma", genre: "Computer Science", yearPublished: 2021, pages: 412, rating: 4.8, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Alex Thompson" }, + { id: 1007, isbn: "978-0547928227", title: "Culinary Traditions of the World", author: "Isabella Fontana", genre: "Cooking", yearPublished: 2019, pages: 276, rating: 4.3, status: "Available", librarySection: "Lifestyle" }, + { id: 1008, isbn: "978-0525509288", title: "The Biomimicry Revolution", author: "Dr. James Whitfield", genre: "Biology", yearPublished: 2020, pages: 345, rating: 4.5, status: "Available", librarySection: "Science" }, + { id: 1009, isbn: "978-0345391803", title: "Symphonies in Code", author: "Aria Blackwood", genre: "Programming", yearPublished: 2022, pages: 423, rating: 4.7, status: "Checked Out", librarySection: "Computer Science", borrowedBy: "Emma Davis" }, + { id: 1010, isbn: "978-0812988407", title: "Urban Gardens & Green Spaces", author: "Miguel Santos", genre: "Gardening", yearPublished: 2021, pages: 189, rating: 4.1, status: "Available", librarySection: "Lifestyle" }, + { id: 1011, isbn: "978-0374533557", title: "The Psychology of Innovation", author: "Dr. Rachel Kim", genre: "Psychology", yearPublished: 2019, pages: 312, rating: 4.6, status: "Reserved", librarySection: "Psychology" }, + { 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: HeaderObject[] = [ + { accessor: "id", label: "Book ID", width: 80, isSortable: true, type: "number" }, + { accessor: "isbn", label: "ISBN", width: 120, isSortable: true, type: "string" }, + { accessor: "title", label: "Title", minWidth: 150, width: "1fr", isSortable: true, type: "string" }, + { accessor: "author", label: "Author", width: 140, isSortable: true, type: "string" }, + { accessor: "genre", label: "Genre", width: 120, isSortable: true, type: "string" }, + { accessor: "yearPublished", label: "Year", width: 80, isSortable: true, type: "number" }, + { accessor: "pages", label: "Pages", width: 80, isSortable: true, type: "number" }, + { accessor: "rating", label: "Rating", width: 80, isSortable: true, type: "number" }, + { accessor: "status", label: "Status", width: 100, isSortable: true, type: "string" }, + { accessor: "librarySection", label: "Section", width: 120, isSortable: true, type: "string" }, +]; + +export const rowSelectionConfig = { + headers: rowSelectionHeaders, + rows: rowSelectionData, + tableProps: { + enableRowSelection: true, + columnResizing: true, + columnReordering: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/sales/SalesDemo.svelte b/packages/examples/svelte/src/demos/sales/SalesDemo.svelte index 4d8ca299b..2d8b69df1 100644 --- a/packages/examples/svelte/src/demos/sales/SalesDemo.svelte +++ b/packages/examples/svelte/src/demos/sales/SalesDemo.svelte @@ -1,8 +1,8 @@ (value as number).toFixed(2) }, + { accessor: "mathGrade", label: "Math", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "scienceGrade", label: "Science", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "englishGrade", label: "English", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + { accessor: "historyGrade", label: "History", width: 90, isSortable: true, type: "number", align: "right", showWhen: "parentExpanded" }, + ], + }, +]; + +export const singleRowChildrenConfig = { + headers: singleRowChildrenHeaders, + rows: singleRowChildrenData, + tableProps: { + columnResizing: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/spreadsheet/SpreadsheetDemo.svelte b/packages/examples/svelte/src/demos/spreadsheet/SpreadsheetDemo.svelte index 9727be07e..83cb317b5 100644 --- a/packages/examples/svelte/src/demos/spreadsheet/SpreadsheetDemo.svelte +++ b/packages/examples/svelte/src/demos/spreadsheet/SpreadsheetDemo.svelte @@ -1,10 +1,10 @@ `$${(value as number).toFixed(2)}`, + }, + { accessor: "stock", label: "Stock", width: 100, align: "right", isSortable: true, tooltip: "Available inventory count - number of units currently in warehouse stock" }, + { + accessor: "rating", + label: "Rating", + width: 100, + align: "center", + isSortable: true, + tooltip: "Customer satisfaction rating based on verified purchase reviews (scale: 1-5 stars)", + valueFormatter: ({ value }) => `${value}/5`, + }, + { accessor: "lastUpdated", label: "Last Updated", width: 150, isSortable: true, tooltip: "Most recent inventory update date in YYYY-MM-DD format" }, +]; + +export const tooltipConfig = { + headers: tooltipHeaders, + rows: tooltipData, + tableProps: { + columnResizing: true, + columnReordering: true, + selectableCells: true, + }, +} as const; diff --git a/packages/examples/svelte/src/demos/value-formatter/ValueFormatterDemo.svelte b/packages/examples/svelte/src/demos/value-formatter/ValueFormatterDemo.svelte index 2b0eb939f..b7266e201 100644 --- a/packages/examples/svelte/src/demos/value-formatter/ValueFormatterDemo.svelte +++ b/packages/examples/svelte/src/demos/value-formatter/ValueFormatterDemo.svelte @@ -1,14 +1,14 @@ = { + engineering: "ENG", + marketing: "MKT", + sales: "SLS", + product: "PRD", + design: "DSN", + operations: "OPS", +}; + +export const valueFormatterHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 60, type: "number" }, + { + accessor: "firstName", + label: "Name", + width: 180, + type: "string", + valueFormatter: ({ value, row }) => { + return `${value as string} ${row.lastName as string}`; + }, + }, + { + accessor: "salary", + label: "Salary", + width: 140, + type: "number", + valueFormatter: ({ value }) => { + return `$${(value as number).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; + }, + useFormattedValueForClipboard: true, + useFormattedValueForCSV: true, + }, + { + accessor: "joinDate", + label: "Join Date", + width: 140, + type: "date", + valueFormatter: ({ value }) => { + const date = new Date(value as string); + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + }, + }, + { + accessor: "performanceScore", + label: "Performance", + width: 130, + type: "number", + valueFormatter: ({ value }) => `${((value as number) * 100).toFixed(1)}%`, + useFormattedValueForClipboard: true, + exportValueGetter: ({ value }) => `${Math.round((value as number) * 100)}%`, + }, + { + accessor: "balance", + label: "Balance", + width: 120, + type: "number", + valueFormatter: ({ value }) => { + const balance = value as number; + if (balance === 0) return "\u2014"; + if (balance < 0) return `($${Math.abs(balance).toFixed(2)})`; + return `$${balance.toFixed(2)}`; + }, + }, + { + accessor: "department", + label: "Department", + width: 150, + type: "string", + valueFormatter: ({ value }) => (value as string).toUpperCase(), + exportValueGetter: ({ value }) => { + const str = (value as string).toLowerCase(); + const code = DEPARTMENT_CODES[str] || "OTH"; + return `${(value as string).toUpperCase()} (${code})`; + }, + }, +]; + +export const valueFormatterConfig = { + headers: valueFormatterHeaders, + rows: valueFormatterData, + tableProps: { + selectableCells: true, + }, +} as const; diff --git a/packages/examples/svelte/src/main.ts b/packages/examples/svelte/src/main.ts index 373a56a57..6d5fea462 100644 --- a/packages/examples/svelte/src/main.ts +++ b/packages/examples/svelte/src/main.ts @@ -1,6 +1,6 @@ import { mount } from "svelte"; import App from "./App.svelte"; -import "../../shared/src/styles/shell.css"; +import "./styles/shell.css"; const app = mount(App, { target: document.getElementById("app")! }); diff --git a/packages/examples/svelte/src/styles/shell.css b/packages/examples/svelte/src/styles/shell.css new file mode 100644 index 000000000..e19fa2fe2 --- /dev/null +++ b/packages/examples/svelte/src/styles/shell.css @@ -0,0 +1,63 @@ +.examples-shell { + display: flex; + height: 100vh; + overflow: hidden; +} + +.examples-sidebar { + width: 240px; + min-width: 240px; + background: #1e293b; + border-right: 1px solid #334155; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.examples-sidebar-header { + padding: 20px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748b; + border-bottom: 1px solid #334155; +} + +.examples-sidebar-nav { + list-style: none; + padding: 8px 0; + margin: 0; +} + +.examples-sidebar-link { + display: block; + width: 100%; + padding: 10px 20px; + background: none; + border: none; + color: #94a3b8; + text-decoration: none; + font-size: 14px; + font-family: inherit; + text-align: left; + cursor: pointer; + transition: background-color 0.15s, color 0.15s; +} + +.examples-sidebar-link:hover { + background-color: rgba(51, 65, 85, 0.5); + color: #e2e8f0; +} + +.examples-sidebar-link.active { + background-color: rgba(59, 130, 246, 0.15); + color: #60a5fa; + font-weight: 500; +} + +.examples-content { + flex: 1; + overflow-y: auto; + padding: 24px; +} diff --git a/packages/examples/svelte/vite.config.ts b/packages/examples/svelte/vite.config.ts index 6c5f33b19..5c2057e3c 100644 --- a/packages/examples/svelte/vite.config.ts +++ b/packages/examples/svelte/vite.config.ts @@ -13,8 +13,6 @@ export default defineConfig({ { find: "@simple-table/svelte/styles.css", replacement: path.resolve(__dirname, "../../core/src/styles/base.css") }, { find: "@simple-table/svelte", replacement: path.resolve(__dirname, "../../svelte/src/index.ts") }, { find: "simple-table-core", replacement: path.resolve(__dirname, "../../core/src/index.ts") }, - { find: /^@simple-table\/examples-shared\/(.*)$/, replacement: path.resolve(__dirname, "../shared/src/$1") }, - { find: "@simple-table/examples-shared", replacement: path.resolve(__dirname, "../shared/src/index.ts") }, ], }, }); diff --git a/packages/examples/vanilla/package.json b/packages/examples/vanilla/package.json index e86199a8f..23309de94 100644 --- a/packages/examples/vanilla/package.json +++ b/packages/examples/vanilla/package.json @@ -9,8 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "simple-table-core": "workspace:*", - "@simple-table/examples-shared": "workspace:*" + "simple-table-core": "workspace:*" }, "devDependencies": { "typescript": "^5.0.0", diff --git a/packages/examples/vanilla/src/demo-list.ts b/packages/examples/vanilla/src/demo-list.ts new file mode 100644 index 000000000..7c1935b68 --- /dev/null +++ b/packages/examples/vanilla/src/demo-list.ts @@ -0,0 +1,57 @@ +export const DEMO_LIST = [ + { id: "quick-start", label: "Quick Start" }, + { id: "column-filtering", label: "Column Filtering" }, + { id: "column-sorting", label: "Column Sorting" }, + { id: "value-formatter", label: "Value Formatter" }, + { id: "pagination", label: "Pagination" }, + { id: "column-pinning", label: "Column Pinning" }, + { id: "column-alignment", label: "Column Alignment" }, + { id: "column-width", label: "Column Width" }, + { id: "column-resizing", label: "Column Resizing" }, + { id: "column-reordering", label: "Column Reordering" }, + { id: "column-selection", label: "Column Selection" }, + { id: "column-editing", label: "Column Editing" }, + { id: "cell-editing", label: "Cell Editing" }, + { id: "cell-highlighting", label: "Cell Highlighting" }, + { id: "themes", label: "Themes" }, + { id: "row-height", label: "Row Height" }, + { id: "table-height", label: "Table Height" }, + { id: "quick-filter", label: "Quick Filter" }, + { id: "nested-headers", label: "Nested Headers" }, + { id: "external-sort", label: "External Sort" }, + { id: "external-filter", label: "External Filter" }, + { id: "loading-state", label: "Loading State" }, + { id: "infinite-scroll", label: "Infinite Scroll" }, + { id: "row-selection", label: "Row Selection" }, + { id: "csv-export", label: "CSV Export" }, + { id: "programmatic-control", label: "Programmatic Control" }, + { id: "row-grouping", label: "Row Grouping" }, + { id: "aggregate-functions", label: "Aggregate Functions" }, + { id: "collapsible-columns", label: "Collapsible Columns" }, + { id: "cell-renderer", label: "Cell Renderer" }, + { id: "header-renderer", label: "Header Renderer" }, + { id: "footer-renderer", label: "Footer Renderer" }, + { id: "cell-clicking", label: "Cell Clicking" }, + { id: "tooltip", label: "Tooltip" }, + { id: "custom-theme", label: "Custom Theme" }, + { id: "custom-icons", label: "Custom Icons" }, + { id: "empty-state", label: "Empty State" }, + { id: "column-visibility", label: "Column Visibility" }, + { id: "column-editor-custom-renderer", label: "Column Editor Custom Renderer" }, + { id: "single-row-children", label: "Single Row Children" }, + { id: "nested-tables", label: "Nested Tables" }, + { id: "dynamic-nested-tables", label: "Dynamic Nested Tables" }, + { id: "dynamic-row-loading", label: "Dynamic Row Loading" }, + { id: "charts", label: "Charts" }, + { id: "live-update", label: "Live Update" }, + { id: "crm", label: "CRM" }, + { id: "infrastructure", label: "Infrastructure" }, + { id: "music", label: "Music" }, + { id: "billing", label: "Billing" }, + { id: "manufacturing", label: "Manufacturing" }, + { id: "hr", label: "HR" }, + { id: "sales", label: "Sales" }, + { id: "spreadsheet", label: "Spreadsheet" }, +] as const; + +export type DemoId = (typeof DEMO_LIST)[number]["id"]; diff --git a/packages/examples/vanilla/src/demos/aggregate-functions/AggregateFunctionsDemo.ts b/packages/examples/vanilla/src/demos/aggregate-functions/AggregateFunctionsDemo.ts index 42e328f85..7ccb8347b 100644 --- a/packages/examples/vanilla/src/demos/aggregate-functions/AggregateFunctionsDemo.ts +++ b/packages/examples/vanilla/src/demos/aggregate-functions/AggregateFunctionsDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { aggregateFunctionsConfig } from "@simple-table/examples-shared"; +import { aggregateFunctionsConfig } from "./aggregate-functions.demo-data"; import "simple-table-core/styles.css"; export function renderAggregateFunctionsDemo( diff --git a/packages/examples/shared/src/configs/aggregate-functions-config.ts b/packages/examples/vanilla/src/demos/aggregate-functions/aggregate-functions.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/aggregate-functions-config.ts rename to packages/examples/vanilla/src/demos/aggregate-functions/aggregate-functions.demo-data.ts index 83b807e83..7b77f43f9 100644 --- a/packages/examples/shared/src/configs/aggregate-functions-config.ts +++ b/packages/examples/vanilla/src/demos/aggregate-functions/aggregate-functions.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const aggregateFunctionsHeaders: HeaderObject[] = [ { accessor: "name", label: "Name", width: 200, expandable: true, type: "string" }, { @@ -26,7 +28,8 @@ export const aggregateFunctionsHeaders: HeaderObject[] = [ type: "string", aggregation: { type: "sum", - parseValue: (value: string) => { + parseValue: (value) => { + if (typeof value !== "string") return 0; const numericValue = parseFloat(value.replace(/[$K]/g, "")); return isNaN(numericValue) ? 0 : numericValue; }, diff --git a/packages/examples/vanilla/src/demos/billing/BillingDemo.ts b/packages/examples/vanilla/src/demos/billing/BillingDemo.ts index e84f1ae20..8c834f985 100644 --- a/packages/examples/vanilla/src/demos/billing/BillingDemo.ts +++ b/packages/examples/vanilla/src/demos/billing/BillingDemo.ts @@ -1,7 +1,7 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject, CellRenderer, Row } from "simple-table-core"; -import { billingConfig } from "@simple-table/examples-shared"; -import type { BillingRow } from "@simple-table/examples-shared"; +import { billingConfig } from "./billing.demo-data"; +import type { BillingRow } from "./billing.demo-data"; import "simple-table-core/styles.css"; export function renderBillingDemo( diff --git a/packages/examples/shared/src/configs/billing-config.ts b/packages/examples/vanilla/src/demos/billing/billing.demo-data.ts similarity index 94% rename from packages/examples/shared/src/configs/billing-config.ts rename to packages/examples/vanilla/src/demos/billing/billing.demo-data.ts index 388c4779a..a91694d57 100644 --- a/packages/examples/shared/src/configs/billing-config.ts +++ b/packages/examples/vanilla/src/demos/billing/billing.demo-data.ts @@ -1,5 +1,19 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; -import type { BillingRow } from "../types/billing"; + +export interface BillingRow { + id: string | number; + name: string; + type: string; + amount: number; + deferredRevenue: number; + recognizedRevenue: number; + invoices?: BillingRow[]; + charges?: BillingRow[]; + [key: `balance_${string}`]: number; + [key: `revenue_${string}`]: number; +} + 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"]; diff --git a/packages/examples/vanilla/src/demos/cell-clicking/CellClickingDemo.ts b/packages/examples/vanilla/src/demos/cell-clicking/CellClickingDemo.ts index 977f95352..f870fc11f 100644 --- a/packages/examples/vanilla/src/demos/cell-clicking/CellClickingDemo.ts +++ b/packages/examples/vanilla/src/demos/cell-clicking/CellClickingDemo.ts @@ -1,7 +1,7 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject, CellClickProps } from "simple-table-core"; -import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "@simple-table/examples-shared"; -import type { ProjectTask } from "@simple-table/examples-shared"; +import { cellClickingHeaders, cellClickingData, CELL_CLICKING_STATUSES } from "./cell-clicking.demo-data"; +import type { ProjectTask } from "./cell-clicking.demo-data"; import "simple-table-core/styles.css"; export function renderCellClickingDemo( diff --git a/packages/examples/shared/src/configs/cell-clicking-config.ts b/packages/examples/vanilla/src/demos/cell-clicking/cell-clicking.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/cell-clicking-config.ts rename to packages/examples/vanilla/src/demos/cell-clicking/cell-clicking.demo-data.ts index 45e360521..06cf2a7d0 100644 --- a/packages/examples/shared/src/configs/cell-clicking-config.ts +++ b/packages/examples/vanilla/src/demos/cell-clicking/cell-clicking.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export type ProjectTask = { id: number; task: string; @@ -41,3 +43,5 @@ export const cellClickingConfig = { headers: cellClickingHeaders, rows: cellClickingData, } as const; + +export { STATUSES as CELL_CLICKING_STATUSES }; diff --git a/packages/examples/vanilla/src/demos/cell-editing/CellEditingDemo.ts b/packages/examples/vanilla/src/demos/cell-editing/CellEditingDemo.ts index e09720e3b..505907712 100644 --- a/packages/examples/vanilla/src/demos/cell-editing/CellEditingDemo.ts +++ b/packages/examples/vanilla/src/demos/cell-editing/CellEditingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, CellChangeProps } from "simple-table-core"; -import { cellEditingConfig } from "@simple-table/examples-shared"; +import { cellEditingConfig } from "./cell-editing.demo-data"; import "simple-table-core/styles.css"; export function renderCellEditingDemo( diff --git a/packages/examples/shared/src/configs/cell-editing-config.ts b/packages/examples/vanilla/src/demos/cell-editing/cell-editing.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/cell-editing-config.ts rename to packages/examples/vanilla/src/demos/cell-editing/cell-editing.demo-data.ts index 61695aab6..4fb9cbaa8 100644 --- a/packages/examples/shared/src/configs/cell-editing-config.ts +++ b/packages/examples/vanilla/src/demos/cell-editing/cell-editing.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const cellEditingHeaders: HeaderObject[] = [ { accessor: "firstName", label: "First Name", width: "1fr", minWidth: 100, isEditable: true, type: "string" }, { accessor: "lastName", label: "Last Name", width: 120, isEditable: true, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/cell-highlighting/CellHighlightingDemo.ts b/packages/examples/vanilla/src/demos/cell-highlighting/CellHighlightingDemo.ts index 070e7e1c4..2e7fcfefa 100644 --- a/packages/examples/vanilla/src/demos/cell-highlighting/CellHighlightingDemo.ts +++ b/packages/examples/vanilla/src/demos/cell-highlighting/CellHighlightingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { cellHighlightingConfig } from "@simple-table/examples-shared"; +import { cellHighlightingConfig } from "./cell-highlighting.demo-data"; import "simple-table-core/styles.css"; export function renderCellHighlightingDemo( diff --git a/packages/examples/shared/src/configs/cell-highlighting-config.ts b/packages/examples/vanilla/src/demos/cell-highlighting/cell-highlighting.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/cell-highlighting-config.ts rename to packages/examples/vanilla/src/demos/cell-highlighting/cell-highlighting.demo-data.ts index 2f0ddda16..2c3dd76a3 100644 --- a/packages/examples/shared/src/configs/cell-highlighting-config.ts +++ b/packages/examples/vanilla/src/demos/cell-highlighting/cell-highlighting.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const cellHighlightingHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, { accessor: "name", label: "Name", minWidth: 80, width: "1fr", isSortable: true, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/cell-renderer/CellRendererDemo.ts b/packages/examples/vanilla/src/demos/cell-renderer/CellRendererDemo.ts index 059f6d277..f31a3e576 100644 --- a/packages/examples/vanilla/src/demos/cell-renderer/CellRendererDemo.ts +++ b/packages/examples/vanilla/src/demos/cell-renderer/CellRendererDemo.ts @@ -1,7 +1,7 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject, CellRenderer } from "simple-table-core"; -import { cellRendererConfig } from "@simple-table/examples-shared"; -import type { CellRendererEmployee } from "@simple-table/examples-shared"; +import { cellRendererConfig } from "./cell-renderer.demo-data"; +import type { CellRendererEmployee } from "./cell-renderer.demo-data"; import "simple-table-core/styles.css"; const html = (str: string): Node => { diff --git a/packages/examples/shared/src/configs/cell-renderer-config.ts b/packages/examples/vanilla/src/demos/cell-renderer/cell-renderer.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/cell-renderer-config.ts rename to packages/examples/vanilla/src/demos/cell-renderer/cell-renderer.demo-data.ts index 1061b2a9b..032e8268e 100644 --- a/packages/examples/shared/src/configs/cell-renderer-config.ts +++ b/packages/examples/vanilla/src/demos/cell-renderer/cell-renderer.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export type CellRendererEmployee = { id: number; name: string; diff --git a/packages/examples/vanilla/src/demos/charts/ChartsDemo.ts b/packages/examples/vanilla/src/demos/charts/ChartsDemo.ts index b0b8428ff..bff94260c 100644 --- a/packages/examples/vanilla/src/demos/charts/ChartsDemo.ts +++ b/packages/examples/vanilla/src/demos/charts/ChartsDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { chartsConfig } from "@simple-table/examples-shared"; +import { chartsConfig } from "./charts.demo-data"; import "simple-table-core/styles.css"; export function renderChartsDemo( diff --git a/packages/examples/shared/src/configs/charts-config.ts b/packages/examples/vanilla/src/demos/charts/charts.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/charts-config.ts rename to packages/examples/vanilla/src/demos/charts/charts.demo-data.ts index 96bd5fcd8..74c05f94e 100644 --- a/packages/examples/shared/src/configs/charts-config.ts +++ b/packages/examples/vanilla/src/demos/charts/charts.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + const generateTrendData = (baseValue: number, volatility: number, length: number = 12): number[] => { const data: number[] = []; let current = baseValue; diff --git a/packages/examples/vanilla/src/demos/collapsible-columns/CollapsibleColumnsDemo.ts b/packages/examples/vanilla/src/demos/collapsible-columns/CollapsibleColumnsDemo.ts index 1cee7292b..c01f05a2b 100644 --- a/packages/examples/vanilla/src/demos/collapsible-columns/CollapsibleColumnsDemo.ts +++ b/packages/examples/vanilla/src/demos/collapsible-columns/CollapsibleColumnsDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { collapsibleColumnsConfig } from "@simple-table/examples-shared"; +import { collapsibleColumnsConfig } from "./collapsible-columns.demo-data"; import "simple-table-core/styles.css"; export function renderCollapsibleColumnsDemo( diff --git a/packages/examples/shared/src/configs/collapsible-columns-config.ts b/packages/examples/vanilla/src/demos/collapsible-columns/collapsible-columns.demo-data.ts similarity index 99% rename from packages/examples/shared/src/configs/collapsible-columns-config.ts rename to packages/examples/vanilla/src/demos/collapsible-columns/collapsible-columns.demo-data.ts index 15581f900..6101ee81b 100644 --- a/packages/examples/shared/src/configs/collapsible-columns-config.ts +++ b/packages/examples/vanilla/src/demos/collapsible-columns/collapsible-columns.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const collapsibleColumnsData = [ { 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 }, diff --git a/packages/examples/vanilla/src/demos/column-alignment/ColumnAlignmentDemo.ts b/packages/examples/vanilla/src/demos/column-alignment/ColumnAlignmentDemo.ts index b1459d530..d89cf3d1e 100644 --- a/packages/examples/vanilla/src/demos/column-alignment/ColumnAlignmentDemo.ts +++ b/packages/examples/vanilla/src/demos/column-alignment/ColumnAlignmentDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnAlignmentConfig } from "@simple-table/examples-shared"; +import { columnAlignmentConfig } from "./column-alignment.demo-data"; import "simple-table-core/styles.css"; export function renderColumnAlignmentDemo( diff --git a/packages/examples/shared/src/configs/column-alignment-config.ts b/packages/examples/vanilla/src/demos/column-alignment/column-alignment.demo-data.ts similarity index 96% rename from packages/examples/shared/src/configs/column-alignment-config.ts rename to packages/examples/vanilla/src/demos/column-alignment/column-alignment.demo-data.ts index 57c1f60cd..1cd3fce1d 100644 --- a/packages/examples/shared/src/configs/column-alignment-config.ts +++ b/packages/examples/vanilla/src/demos/column-alignment/column-alignment.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const columnAlignmentHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, align: "left", type: "number" }, { accessor: "name", label: "Name", minWidth: 100, width: "1fr", align: "center", type: "string" }, diff --git a/packages/examples/vanilla/src/demos/column-editing/ColumnEditingDemo.ts b/packages/examples/vanilla/src/demos/column-editing/ColumnEditingDemo.ts index f8f18c3d7..d7b26ec87 100644 --- a/packages/examples/vanilla/src/demos/column-editing/ColumnEditingDemo.ts +++ b/packages/examples/vanilla/src/demos/column-editing/ColumnEditingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject } from "simple-table-core"; -import { columnEditingData, columnEditingHeaders } from "@simple-table/examples-shared"; +import { columnEditingData, columnEditingHeaders } from "./column-editing.demo-data"; import "simple-table-core/styles.css"; export function renderColumnEditingDemo( diff --git a/packages/examples/shared/src/configs/column-editing-config.ts b/packages/examples/vanilla/src/demos/column-editing/column-editing.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/column-editing-config.ts rename to packages/examples/vanilla/src/demos/column-editing/column-editing.demo-data.ts index 5fbe5f10d..8f6dd3a39 100644 --- a/packages/examples/shared/src/configs/column-editing-config.ts +++ b/packages/examples/vanilla/src/demos/column-editing/column-editing.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const columnEditingHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 120, width: "1fr", type: "string" }, diff --git a/packages/examples/vanilla/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.ts b/packages/examples/vanilla/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.ts index 06c727efe..c8e0b9dc6 100644 --- a/packages/examples/vanilla/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.ts +++ b/packages/examples/vanilla/src/demos/column-editor-custom-renderer/ColumnEditorCustomRendererDemo.ts @@ -5,7 +5,7 @@ import { COLUMN_EDITOR_TEXT, COLUMN_EDITOR_SEARCH_PLACEHOLDER, buildVanillaColumnEditorRowRenderer, -} from "@simple-table/examples-shared"; +} from "./column-editor-custom-renderer.demo-data"; import "simple-table-core/styles.css"; export function renderColumnEditorCustomRendererDemo( diff --git a/packages/examples/shared/src/configs/column-editor-custom-renderer-config.ts b/packages/examples/vanilla/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/column-editor-custom-renderer-config.ts rename to packages/examples/vanilla/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts index 473333fe1..18737bd58 100644 --- a/packages/examples/shared/src/configs/column-editor-custom-renderer-config.ts +++ b/packages/examples/vanilla/src/demos/column-editor-custom-renderer/column-editor-custom-renderer.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row, ColumnEditorRowRendererProps } from "simple-table-core"; + 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" }, diff --git a/packages/examples/vanilla/src/demos/column-filtering/ColumnFilteringDemo.ts b/packages/examples/vanilla/src/demos/column-filtering/ColumnFilteringDemo.ts index c064e8f25..fb759e157 100644 --- a/packages/examples/vanilla/src/demos/column-filtering/ColumnFilteringDemo.ts +++ b/packages/examples/vanilla/src/demos/column-filtering/ColumnFilteringDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnFilteringConfig } from "@simple-table/examples-shared"; +import { columnFilteringConfig } from "./column-filtering.demo-data"; import "simple-table-core/styles.css"; export function renderColumnFilteringDemo( diff --git a/packages/examples/shared/src/data/column-filtering-data.ts b/packages/examples/vanilla/src/demos/column-filtering/column-filtering.demo-data.ts similarity index 54% rename from packages/examples/shared/src/data/column-filtering-data.ts rename to packages/examples/vanilla/src/demos/column-filtering/column-filtering.demo-data.ts index 15b48a4d7..625a7bd07 100644 --- a/packages/examples/shared/src/data/column-filtering-data.ts +++ b/packages/examples/vanilla/src/demos/column-filtering/column-filtering.demo-data.ts @@ -1,4 +1,7 @@ +// Self-contained demo table setup for this example. import type { Row } from "simple-table-core"; +import type { HeaderObject } from "simple-table-core"; + export const COLUMN_FILTERING_DATA: Row[] = [ { @@ -128,3 +131,92 @@ export const COLUMN_FILTERING_DATA: Row[] = [ isActive: true, }, ]; + + +export const DEPARTMENT_OPTIONS = [ + { label: "Editorial", value: "Editorial" }, + { label: "Production", value: "Production" }, + { label: "Marketing", value: "Marketing" }, + { label: "Sales", value: "Sales" }, + { label: "Operations", value: "Operations" }, + { label: "Human Resources", value: "Human Resources" }, + { label: "Finance", value: "Finance" }, + { label: "Legal", value: "Legal" }, + { label: "IT Support", value: "IT Support" }, + { label: "Customer Service", value: "Customer Service" }, + { label: "Research & Development", value: "Research & Development" }, + { label: "Quality Assurance", value: "Quality Assurance" }, +]; + +export const columnFilteringHeaders: HeaderObject[] = [ + { + accessor: "id", + label: "ID", + width: 80, + type: "number", + isSortable: true, + filterable: true, + }, + { + accessor: "name", + label: "Employee Name", + width: "1fr", + minWidth: 150, + type: "string", + isSortable: true, + filterable: true, + }, + { + accessor: "department", + label: "Department", + width: "1fr", + minWidth: 120, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: DEPARTMENT_OPTIONS, + }, + { + accessor: "role", + label: "Role", + width: 140, + type: "string", + isSortable: true, + filterable: true, + }, + { + accessor: "salary", + label: "Salary", + width: 120, + align: "right", + type: "number", + isSortable: true, + filterable: true, + cellRenderer: ({ row }) => { + const salary = row.salary as number; + return `$${salary.toLocaleString()}`; + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 130, + type: "date", + isSortable: true, + filterable: true, + }, + { + accessor: "isActive", + label: "Active", + width: 100, + align: "center", + type: "boolean", + isSortable: true, + filterable: true, + }, +]; + +export const columnFilteringConfig = { + headers: columnFilteringHeaders, + rows: COLUMN_FILTERING_DATA, +} as const; diff --git a/packages/examples/vanilla/src/demos/column-pinning/ColumnPinningDemo.ts b/packages/examples/vanilla/src/demos/column-pinning/ColumnPinningDemo.ts index 5d680c5fd..5114cd865 100644 --- a/packages/examples/vanilla/src/demos/column-pinning/ColumnPinningDemo.ts +++ b/packages/examples/vanilla/src/demos/column-pinning/ColumnPinningDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnPinningConfig } from "@simple-table/examples-shared"; +import { columnPinningConfig } from "./column-pinning.demo-data"; import "simple-table-core/styles.css"; export function renderColumnPinningDemo( diff --git a/packages/examples/shared/src/configs/column-pinning-config.ts b/packages/examples/vanilla/src/demos/column-pinning/column-pinning.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/column-pinning-config.ts rename to packages/examples/vanilla/src/demos/column-pinning/column-pinning.demo-data.ts index d299c462e..e06d5c677 100644 --- a/packages/examples/shared/src/configs/column-pinning-config.ts +++ b/packages/examples/vanilla/src/demos/column-pinning/column-pinning.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const columnPinningHeaders: HeaderObject[] = [ { accessor: "name", label: "Name", width: 132, pinned: "left", type: "string" }, { accessor: "email", label: "Email", width: 220, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/column-reordering/ColumnReorderingDemo.ts b/packages/examples/vanilla/src/demos/column-reordering/ColumnReorderingDemo.ts index fa9395563..25ccc641f 100644 --- a/packages/examples/vanilla/src/demos/column-reordering/ColumnReorderingDemo.ts +++ b/packages/examples/vanilla/src/demos/column-reordering/ColumnReorderingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnReorderingConfig } from "@simple-table/examples-shared"; +import { columnReorderingConfig } from "./column-reordering.demo-data"; import "simple-table-core/styles.css"; export function renderColumnReorderingDemo( diff --git a/packages/examples/shared/src/configs/column-reordering-config.ts b/packages/examples/vanilla/src/demos/column-reordering/column-reordering.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/column-reordering-config.ts rename to packages/examples/vanilla/src/demos/column-reordering/column-reordering.demo-data.ts index e6b90e085..81d1f8d32 100644 --- a/packages/examples/shared/src/configs/column-reordering-config.ts +++ b/packages/examples/vanilla/src/demos/column-reordering/column-reordering.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const columnReorderingHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: "1fr", type: "string" }, diff --git a/packages/examples/vanilla/src/demos/column-resizing/ColumnResizingDemo.ts b/packages/examples/vanilla/src/demos/column-resizing/ColumnResizingDemo.ts index 4f687ee61..a910db011 100644 --- a/packages/examples/vanilla/src/demos/column-resizing/ColumnResizingDemo.ts +++ b/packages/examples/vanilla/src/demos/column-resizing/ColumnResizingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject } from "simple-table-core"; -import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "@simple-table/examples-shared"; +import { columnResizingHeaders, columnResizingData, COLUMN_RESIZING_STORAGE_KEY } from "./column-resizing.demo-data"; import "simple-table-core/styles.css"; export function renderColumnResizingDemo( diff --git a/packages/examples/shared/src/configs/column-resizing-config.ts b/packages/examples/vanilla/src/demos/column-resizing/column-resizing.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/column-resizing-config.ts rename to packages/examples/vanilla/src/demos/column-resizing/column-resizing.demo-data.ts index dc89af4c8..dbe5395d1 100644 --- a/packages/examples/shared/src/configs/column-resizing-config.ts +++ b/packages/examples/vanilla/src/demos/column-resizing/column-resizing.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const COLUMN_RESIZING_STORAGE_KEY = "columnResizingDemo_widths"; export const columnResizingHeaders: HeaderObject[] = [ diff --git a/packages/examples/vanilla/src/demos/column-selection/ColumnSelectionDemo.ts b/packages/examples/vanilla/src/demos/column-selection/ColumnSelectionDemo.ts index 5e867d2e8..e8a4e17f3 100644 --- a/packages/examples/vanilla/src/demos/column-selection/ColumnSelectionDemo.ts +++ b/packages/examples/vanilla/src/demos/column-selection/ColumnSelectionDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnSelectionConfig } from "@simple-table/examples-shared"; +import { columnSelectionConfig } from "./column-selection.demo-data"; import "simple-table-core/styles.css"; export function renderColumnSelectionDemo( diff --git a/packages/examples/shared/src/configs/column-selection-config.ts b/packages/examples/vanilla/src/demos/column-selection/column-selection.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/column-selection-config.ts rename to packages/examples/vanilla/src/demos/column-selection/column-selection.demo-data.ts index 9e9697aff..02af04d6f 100644 --- a/packages/examples/shared/src/configs/column-selection-config.ts +++ b/packages/examples/vanilla/src/demos/column-selection/column-selection.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const columnSelectionHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, { accessor: "name", label: "Name", minWidth: 120, width: "1fr", isSortable: true, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/column-sorting/ColumnSortingDemo.ts b/packages/examples/vanilla/src/demos/column-sorting/ColumnSortingDemo.ts index 8136958db..0a7613e61 100644 --- a/packages/examples/vanilla/src/demos/column-sorting/ColumnSortingDemo.ts +++ b/packages/examples/vanilla/src/demos/column-sorting/ColumnSortingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnSortingConfig } from "@simple-table/examples-shared"; +import { columnSortingConfig } from "./column-sorting.demo-data"; import "simple-table-core/styles.css"; export function renderColumnSortingDemo( diff --git a/packages/examples/shared/src/data/column-sorting-data.ts b/packages/examples/vanilla/src/demos/column-sorting/column-sorting.demo-data.ts similarity index 59% rename from packages/examples/shared/src/data/column-sorting-data.ts rename to packages/examples/vanilla/src/demos/column-sorting/column-sorting.demo-data.ts index a9d988466..82b458ae3 100644 --- a/packages/examples/shared/src/data/column-sorting-data.ts +++ b/packages/examples/vanilla/src/demos/column-sorting/column-sorting.demo-data.ts @@ -1,4 +1,7 @@ +// Self-contained demo table setup for this example. import type { Row } from "simple-table-core"; +import type { HeaderObject } from "simple-table-core"; + export const COLUMN_SORTING_DATA: Row[] = [ { @@ -98,3 +101,47 @@ export const COLUMN_SORTING_DATA: Row[] = [ startDate: "2011-04-12", }, ]; + + +export const columnSortingHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { accessor: "name", label: "Name", width: 180, isSortable: true, type: "string" }, + { accessor: "age", label: "Age", width: 80, isSortable: true, type: "number" }, + { accessor: "role", label: "Role", width: 200, isSortable: true, type: "string" }, + { + accessor: "department", + label: "Department", + width: 180, + isSortable: true, + type: "string", + valueFormatter: ({ value }) => { + return (value as string).charAt(0).toUpperCase() + (value as string).slice(1); + }, + }, + { + accessor: "startDate", + label: "Start Date", + width: 140, + isSortable: true, + type: "date", + valueFormatter: ({ value }) => { + if (typeof value === "string") { + return new Date(value).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + return String(value); + }, + }, +]; + +export const columnSortingConfig = { + headers: columnSortingHeaders, + rows: COLUMN_SORTING_DATA, + tableProps: { + initialSortColumn: "age", + initialSortDirection: "desc" as const, + }, +} as const; diff --git a/packages/examples/vanilla/src/demos/column-visibility/ColumnVisibilityDemo.ts b/packages/examples/vanilla/src/demos/column-visibility/ColumnVisibilityDemo.ts index 1bc6a64aa..cfe4c82ca 100644 --- a/packages/examples/vanilla/src/demos/column-visibility/ColumnVisibilityDemo.ts +++ b/packages/examples/vanilla/src/demos/column-visibility/ColumnVisibilityDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnVisibilityConfig } from "@simple-table/examples-shared"; +import { columnVisibilityConfig } from "./column-visibility.demo-data"; import "simple-table-core/styles.css"; export function renderColumnVisibilityDemo( diff --git a/packages/examples/shared/src/configs/column-visibility-config.ts b/packages/examples/vanilla/src/demos/column-visibility/column-visibility.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/column-visibility-config.ts rename to packages/examples/vanilla/src/demos/column-visibility/column-visibility.demo-data.ts index aa64e48a5..1fc024b1f 100644 --- a/packages/examples/shared/src/configs/column-visibility-config.ts +++ b/packages/examples/vanilla/src/demos/column-visibility/column-visibility.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + 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" }, diff --git a/packages/examples/vanilla/src/demos/column-width/ColumnWidthDemo.ts b/packages/examples/vanilla/src/demos/column-width/ColumnWidthDemo.ts index 5ded4def7..1f0d12454 100644 --- a/packages/examples/vanilla/src/demos/column-width/ColumnWidthDemo.ts +++ b/packages/examples/vanilla/src/demos/column-width/ColumnWidthDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { columnWidthConfig } from "@simple-table/examples-shared"; +import { columnWidthConfig } from "./column-width.demo-data"; import "simple-table-core/styles.css"; export function renderColumnWidthDemo( diff --git a/packages/examples/shared/src/configs/column-width-config.ts b/packages/examples/vanilla/src/demos/column-width/column-width.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/column-width-config.ts rename to packages/examples/vanilla/src/demos/column-width/column-width.demo-data.ts index e0f6ce32f..38e4d1ca4 100644 --- a/packages/examples/shared/src/configs/column-width-config.ts +++ b/packages/examples/vanilla/src/demos/column-width/column-width.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const columnWidthHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "name", label: "Name", width: "1fr", minWidth: 120, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/crm/CRMDemo.ts b/packages/examples/vanilla/src/demos/crm/CRMDemo.ts index add2a9536..22d62d53b 100644 --- a/packages/examples/vanilla/src/demos/crm/CRMDemo.ts +++ b/packages/examples/vanilla/src/demos/crm/CRMDemo.ts @@ -1,5 +1,5 @@ import { SimpleTableVanilla } from "simple-table-core"; -import type { Theme, HeaderObject, CellRenderer, FooterRendererProps } from "simple-table-core"; +import type { HeaderObject, CellRenderer, FooterRendererProps } from "simple-table-core"; import { crmData, CRM_THEME_COLORS_LIGHT, @@ -7,10 +7,10 @@ import { CRM_FOOTER_COLORS_LIGHT, CRM_FOOTER_COLORS_DARK, generateVisiblePages, -} from "@simple-table/examples-shared"; -import type { CRMLead } from "@simple-table/examples-shared"; +} from "./crm.demo-data"; +import type { CRMLead, CrmShellTheme } from "./crm.demo-data"; import "simple-table-core/styles.css"; -import "../../../../shared/src/styles/crm-custom-theme.css"; +import "./crm-custom-theme.css"; function el(tag: string, styles?: Partial, text?: string): HTMLElement { const e = document.createElement(tag); @@ -470,10 +470,10 @@ function createCRMFooter( export function renderCRMDemo( container: HTMLElement, - options?: { height?: string | number; theme?: Theme }, + options?: { height?: string | number; theme?: CrmShellTheme }, ): SimpleTableVanilla { const isDark = - options?.theme === "custom" || + options?.theme === "custom-dark" || options?.theme === "dark" || options?.theme === "modern-dark"; const footerColors = isDark ? CRM_FOOTER_COLORS_DARK : CRM_FOOTER_COLORS_LIGHT; diff --git a/packages/examples/vanilla/src/demos/crm/crm-custom-theme.css b/packages/examples/vanilla/src/demos/crm/crm-custom-theme.css new file mode 100644 index 000000000..8ed3366f4 --- /dev/null +++ b/packages/examples/vanilla/src/demos/crm/crm-custom-theme.css @@ -0,0 +1,161 @@ +@import url("https://fonts.googleapis.com/css2?family=BBH+Sans+Hegarty&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"); + +.custom-theme-container .st-wrapper { + border-radius: 0; +} +.custom-theme-container .st-header-label-text { + font-size: 12px; +} + +.custom-theme-container * { + font-family: Plus Jakarta Sans, sans-serif; + font-optical-sizing: auto; + font-style: normal; +} +.custom-theme-container .st-header-resize-handle { + height: 100%; + width: 4px; +} +.custom-theme-container.theme-custom-light { + --st-border-radius: 4px; + --st-cell-padding: 12px; + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + --st-scrollbar-bg-color: #f1f5f9; + --st-scrollbar-thumb-color: #cbd5e1; + --st-border-color: none; + --st-odd-row-background-color: white; + --st-even-row-background-color: white; + --st-odd-column-background-color: white; + --st-even-column-background-color: oklch(98.5% 0.002 247.839); + --st-hover-row-background-color: oklch(96.7% 0.003 264.542); + --st-selected-row-background-color: oklch(96.7% 0.003 264.542); + --st-header-background-color: rgb(249 250 251); + --st-header-label-color: oklch(55.1% 0.027 264.364); + --st-header-icon-color: oklch(55.1% 0.027 264.364); + --st-dragging-background-color: oklch(96.7% 0.003 264.542); + --st-selected-cell-background-color: oklch(96.7% 0.003 264.542); + --st-selected-first-cell-background-color: oklch(92.8% 0.006 264.531); + --st-footer-background-color: oklch(98.5% 0.002 247.839); + --st-cell-color: oklch(37.3% 0.034 259.733); + --st-cell-odd-row-color: oklch(37.3% 0.034 259.733); + --st-edit-cell-shadow: 0 4px 6px -1px rgba(106, 114, 130, 0.1), + 0 2px 4px -1px rgba(106, 114, 130, 0.06); + --st-selected-cell-color: oklch(27.8% 0.033 256.848); + --st-selected-first-cell-color: oklch(27.8% 0.033 256.848); + --st-resize-handle-color: oklch(96.7% 0.003 264.542); + --st-separator-border-color: oklch(92.8% 0.006 264.531); + --st-last-group-row-separator-border-color: oklch(87.2% 0.01 258.338); + --st-selected-border-color: #6a7282; + --st-editable-cell-focus-border-color: #6a7282; + --st-checkbox-checked-background-color: #6a7282; + --st-checkbox-checked-border-color: #6a7282; + --st-column-editor-background-color: white; + --st-column-editor-popout-background-color: white; + --st-button-hover-background-color: oklch(96.7% 0.003 264.542); + --st-button-active-background-color: #6a7282; + --st-cell-flash-color: oklch(92.8% 0.006 264.531); + --st-copy-flash-color: #6a7282; + --st-warning-flash-color: oklch(80.8% 0.114 19.571); + --st-header-selected-background-color: #6a7282; + --st-header-selected-label-color: #6a7282; + --st-header-selected-icon-color: #6a7282; + --st-header-highlight-indicator-color: oklch(87.2% 0.01 258.338); + --st-selection-highlight-indicator-color: oklch(87.2% 0.01 258.338); + --st-drag-separator-color: #6a7282; + --st-next-prev-btn-color: oklch(44.6% 0.03 256.802); + --st-next-prev-btn-disabled-color: oklch(70.7% 0.022 261.325); + --st-page-btn-color: oklch(44.6% 0.03 256.802); + --st-page-btn-hover-background-color: oklch(96.7% 0.003 264.542); + --st-column-editor-text-color: oklch(55.1% 0.027 264.364); + --st-checkbox-border-color: oklch(87.2% 0.01 258.338); + --st-dropdown-group-label-color: oklch(55.1% 0.027 264.364); + --st-datepicker-weekday-color: oklch(55.1% 0.027 264.364); + --st-datepicker-other-month-color: oklch(70.7% 0.022 261.325); + --st-filter-button-disabled-background-color: oklch(87.2% 0.01 258.338); + --st-filter-button-disabled-text-color: oklch(55.1% 0.027 264.364); +} + +.custom-theme-container.theme-custom-light .st-wrapper { + background-color: white; +} + +.custom-theme-container.theme-custom-light .st-header-resize-handle { + background-color: oklch(96.7% 0.003 264.542); +} + +.custom-theme-container.theme-custom-dark { + --st-border-radius: 4px; + --st-cell-padding: 12px; + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + --st-scrollbar-bg-color: #1e293b; + --st-scrollbar-thumb-color: #475569; + --st-border-color: none; + --st-odd-row-background-color: #0f172a; + --st-even-row-background-color: #0f172a; + --st-odd-column-background-color: #0f172a; + --st-even-column-background-color: #1e293b; + --st-hover-row-background-color: #1e293b; + --st-selected-row-background-color: #1e293b; + --st-header-background-color: #0a0f1a; + --st-header-label-color: #94a3b8; + --st-header-icon-color: #94a3b8; + --st-dragging-background-color: #1e293b; + --st-selected-cell-background-color: #1e293b; + --st-selected-first-cell-background-color: #334155; + --st-footer-background-color: #0f172a; + --st-cell-color: #cbd5e1; + --st-cell-odd-row-color: #cbd5e1; + --st-edit-cell-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + --st-selected-cell-color: #e2e8f0; + --st-selected-first-cell-color: #e2e8f0; + --st-resize-handle-color: #1e293b; + --st-separator-border-color: #334155; + --st-last-group-row-separator-border-color: #475569; + --st-selected-border-color: #94a3b8; + --st-editable-cell-focus-border-color: #94a3b8; + --st-checkbox-checked-background-color: #94a3b8; + --st-checkbox-checked-border-color: #94a3b8; + --st-column-editor-background-color: #1e293b; + --st-column-editor-popout-background-color: #1e293b; + --st-button-hover-background-color: #334155; + --st-button-active-background-color: #94a3b8; + --st-cell-flash-color: #334155; + --st-copy-flash-color: #94a3b8; + --st-warning-flash-color: #ef4444; + --st-header-selected-background-color: #94a3b8; + --st-header-selected-label-color: #94a3b8; + --st-header-selected-icon-color: #94a3b8; + --st-header-highlight-indicator-color: #475569; + --st-selection-highlight-indicator-color: #475569; + --st-drag-separator-color: #94a3b8; + --st-next-prev-btn-color: #cbd5e1; + --st-next-prev-btn-disabled-color: #64748b; + --st-page-btn-color: #cbd5e1; + --st-page-btn-hover-background-color: #334155; + --st-column-editor-text-color: #94a3b8; + --st-checkbox-border-color: #475569; + --st-dropdown-group-label-color: #94a3b8; + --st-datepicker-weekday-color: #94a3b8; + --st-datepicker-other-month-color: #64748b; + --st-filter-button-disabled-background-color: #334155; + --st-filter-button-disabled-text-color: #64748b; +} + +.custom-theme-container.theme-custom-dark .st-wrapper { + background-color: #0f172a; +} + +.custom-theme-container.theme-custom-dark .st-header-resize-handle { + background-color: #475569; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/examples/shared/src/configs/crm-config.ts b/packages/examples/vanilla/src/demos/crm/crm.demo-data.ts similarity index 93% rename from packages/examples/shared/src/configs/crm-config.ts rename to packages/examples/vanilla/src/demos/crm/crm.demo-data.ts index 8da29c2db..803726ec4 100644 --- a/packages/examples/shared/src/configs/crm-config.ts +++ b/packages/examples/vanilla/src/demos/crm/crm.demo-data.ts @@ -1,5 +1,21 @@ -import type { HeaderObject } from "simple-table-core"; -import type { CRMLead } from "../types/crm"; +// Self-contained demo table setup for this example. +import type { HeaderObject, Theme } from "simple-table-core"; + +export type CrmShellTheme = Theme | "custom-light" | "custom-dark"; + +export type CRMLead = { + id: number; + name: string; + title: string; + company: string; + linkedin: boolean; + signal: string; + aiScore: number; + emailStatus: string; + timeAgo: string; + list: string; +}; + const FIRST_NAMES = ["Emma", "Liam", "Sophia", "Noah", "Olivia", "James", "Ava", "William", "Isabella", "Oliver", "Mia", "Benjamin", "Charlotte", "Elijah", "Amelia", "Lucas", "Harper", "Mason", "Evelyn", "Logan"]; const LAST_NAMES = ["Chen", "Rodriguez", "Kim", "Thompson", "Martinez", "Anderson", "Taylor", "Brown", "Wilson", "Johnson", "Lee", "Garcia", "Davis", "Miller", "Moore", "Jackson", "White", "Harris", "Martin", "Clark"]; diff --git a/packages/examples/vanilla/src/demos/csv-export/CsvExportDemo.ts b/packages/examples/vanilla/src/demos/csv-export/CsvExportDemo.ts index 4f805b8b4..8cec3f574 100644 --- a/packages/examples/vanilla/src/demos/csv-export/CsvExportDemo.ts +++ b/packages/examples/vanilla/src/demos/csv-export/CsvExportDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject } from "simple-table-core"; -import { csvExportHeaders, csvExportData, csvExportConfig } from "@simple-table/examples-shared"; +import { csvExportHeaders, csvExportData, csvExportConfig } from "./csv-export.demo-data"; import "simple-table-core/styles.css"; export function renderCsvExportDemo( @@ -56,7 +56,10 @@ export function renderCsvExportDemo( const api = table.getAPI(); const rows = api.getAllRows(); const hdrs = api.getHeaders(); - const totalRevenue = rows.reduce((sum, r) => sum + (Number(r.revenue) || 0), 0); + const totalRevenue = rows.reduce( + (sum, r) => sum + (Number((r.row as { revenue?: unknown }).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 })}`, ); diff --git a/packages/examples/shared/src/configs/csv-export-config.ts b/packages/examples/vanilla/src/demos/csv-export/csv-export.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/csv-export-config.ts rename to packages/examples/vanilla/src/demos/csv-export/csv-export.demo-data.ts index b09416c90..709bc554e 100644 --- a/packages/examples/shared/src/configs/csv-export-config.ts +++ b/packages/examples/vanilla/src/demos/csv-export/csv-export.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + const CATEGORY_CODES: Record = { electronics: "ELEC", furniture: "FURN", diff --git a/packages/examples/vanilla/src/demos/custom-icons/CustomIconsDemo.ts b/packages/examples/vanilla/src/demos/custom-icons/CustomIconsDemo.ts index 21561a961..93b06edd9 100644 --- a/packages/examples/vanilla/src/demos/custom-icons/CustomIconsDemo.ts +++ b/packages/examples/vanilla/src/demos/custom-icons/CustomIconsDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { customIconsConfig, buildVanillaCustomIcons } from "@simple-table/examples-shared"; +import { customIconsConfig, buildVanillaCustomIcons } from "./custom-icons.demo-data"; import "simple-table-core/styles.css"; export function renderCustomIconsDemo( diff --git a/packages/examples/shared/src/configs/custom-icons-config.ts b/packages/examples/vanilla/src/demos/custom-icons/custom-icons.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/custom-icons-config.ts rename to packages/examples/vanilla/src/demos/custom-icons/custom-icons.demo-data.ts index 52dd27011..8660efe49 100644 --- a/packages/examples/shared/src/configs/custom-icons-config.ts +++ b/packages/examples/vanilla/src/demos/custom-icons/custom-icons.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const customIconsData: Row[] = [ { 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" }, @@ -39,7 +41,7 @@ export const customIconsConfig = { rows: customIconsData, } as const; -export function createSvgIcon(pathD: string, color = "#3b82f6", size = 14): HTMLElement { +export function createSvgIcon(pathD: string, color = "#3b82f6", size = 14): SVGSVGElement { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("width", String(size)); svg.setAttribute("height", String(size)); diff --git a/packages/examples/vanilla/src/demos/custom-theme/CustomThemeDemo.ts b/packages/examples/vanilla/src/demos/custom-theme/CustomThemeDemo.ts index e5a804794..0d504bdce 100644 --- a/packages/examples/vanilla/src/demos/custom-theme/CustomThemeDemo.ts +++ b/packages/examples/vanilla/src/demos/custom-theme/CustomThemeDemo.ts @@ -1,8 +1,8 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { customThemeConfig } from "@simple-table/examples-shared"; +import { customThemeConfig } from "./custom-theme.demo-data"; import "simple-table-core/styles.css"; -import "../../../../shared/src/styles/custom-theme.css"; +import "./custom-theme.css"; export function renderCustomThemeDemo( container: HTMLElement, diff --git a/packages/examples/vanilla/src/demos/custom-theme/custom-theme.css b/packages/examples/vanilla/src/demos/custom-theme/custom-theme.css new file mode 100644 index 000000000..417233142 --- /dev/null +++ b/packages/examples/vanilla/src/demos/custom-theme/custom-theme.css @@ -0,0 +1,90 @@ +.theme-custom { + --st-border-radius: 8px; + --st-cell-padding: 12px; + + --st-spacing-small: 6px; + --st-spacing-medium: 10px; + + --st-scrollbar-bg-color: var(--st-stone-100); + --st-scrollbar-thumb-color: var(--st-violet-500); + + --st-border-color: var(--st-stone-200); + --st-odd-row-background-color: var(--st-amber-50); + --st-even-row-background-color: var(--st-amber-100); + --st-odd-column-background-color: var(--st-amber-50); + --st-even-column-background-color: var(--st-white); + --st-hover-row-background-color: var(--st-amber-200); + --st-selected-row-background-color: var(--st-violet-50); + --st-header-background-color: var(--st-violet-600); + --st-header-label-color: var(--st-white); + --st-header-icon-color: var(--st-white); + --st-dragging-background-color: var(--st-violet-50); + --st-selected-cell-background-color: var(--st-violet-50); + --st-selected-first-cell-background-color: var(--st-violet-100); + --st-footer-background-color: var(--st-stone-100); + --st-cell-color: var(--st-stone-700); + --st-cell-odd-row-color: var(--st-stone-700); + --st-edit-cell-shadow: 0 4px 6px -1px rgba(124, 58, 237, 0.1), + 0 2px 4px -1px rgba(124, 58, 237, 0.06); + --st-selected-cell-color: var(--st-violet-800); + --st-selected-first-cell-color: var(--st-violet-800); + --st-resize-handle-color: var(--st-violet-400); + --st-resize-handle-selected-color: var(--st-white); + --st-separator-border-color: var(--st-stone-200); + --st-last-group-row-separator-border-color: var(--st-stone-300); + + --st-selected-border-color: var(--st-violet-500); + --st-editable-cell-focus-border-color: var(--st-violet-500); + + --st-checkbox-checked-background-color: var(--st-violet-500); + --st-checkbox-checked-border-color: var(--st-violet-600); + --st-column-editor-background-color: var(--st-white); + --st-column-editor-popout-background-color: var(--st-white); + --st-button-hover-background-color: var(--st-violet-50); + --st-button-active-background-color: var(--st-violet-600); + --st-cell-flash-color: var(--st-violet-200); + --st-copy-flash-color: var(--st-violet-500); + --st-warning-flash-color: var(--st-red-300); + + --st-header-selected-background-color: var(--st-violet-700); + --st-header-selected-label-color: var(--st-white); + --st-header-selected-icon-color: var(--st-white); + + --st-header-highlight-indicator-color: var(--st-stone-300); + --st-selection-highlight-indicator-color: var(--st-stone-300); + + --st-next-prev-btn-color: var(--st-stone-600); + --st-next-prev-btn-disabled-color: var(--st-stone-400); + + --st-page-btn-color: var(--st-stone-600); + --st-page-btn-hover-background-color: var(--st-amber-100); + + --st-column-editor-text-color: var(--st-stone-500); + + --st-checkbox-border-color: var(--st-stone-300); + + --st-dropdown-group-label-color: var(--st-stone-500); + + --st-datepicker-weekday-color: var(--st-stone-500); + --st-datepicker-other-month-color: var(--st-stone-400); + + --st-filter-button-disabled-background-color: var(--st-stone-300); + --st-filter-button-disabled-text-color: var(--st-stone-500); + + --st-sub-header-background-color: var(--st-violet-500); + --st-sub-cell-background-color: var(--st-amber-75); + --st-sub-cell-hover-background-color: var(--st-amber-100); + --st-dragging-sub-header-background-color: var(--st-violet-300); + --st-selected-sub-cell-background-color: var(--st-violet-100); + --st-selected-sub-cell-color: var(--st-violet-900); + + --st-chart-color: var(--st-violet-500); + --st-chart-fill-color: var(--st-violet-300); + + --st-drag-separator-color: var(--st-violet-500); +} + +.theme-custom .st-header-label { + font-weight: 600 !important; + letter-spacing: 0.025em !important; +} diff --git a/packages/examples/shared/src/configs/custom-theme-config.ts b/packages/examples/vanilla/src/demos/custom-theme/custom-theme.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/custom-theme-config.ts rename to packages/examples/vanilla/src/demos/custom-theme/custom-theme.demo-data.ts index c4d2ff970..3b904a4eb 100644 --- a/packages/examples/shared/src/configs/custom-theme-config.ts +++ b/packages/examples/vanilla/src/demos/custom-theme/custom-theme.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const customThemeData: Row[] = [ { 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" }, diff --git a/packages/examples/vanilla/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.ts b/packages/examples/vanilla/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.ts index 1852938d3..34dab8f69 100644 --- a/packages/examples/vanilla/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.ts +++ b/packages/examples/vanilla/src/demos/dynamic-nested-tables/DynamicNestedTablesDemo.ts @@ -4,8 +4,8 @@ import { dynamicNestedTablesConfig, dynamicNestedTablesData, fetchDivisionsForCompany, -} from "@simple-table/examples-shared"; -import type { DynamicCompany } from "@simple-table/examples-shared"; +} from "./dynamic-nested-tables.demo-data"; +import type { DynamicCompany } from "./dynamic-nested-tables.demo-data"; import "simple-table-core/styles.css"; export function renderDynamicNestedTablesDemo( diff --git a/packages/examples/shared/src/configs/dynamic-nested-tables-config.ts b/packages/examples/vanilla/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/dynamic-nested-tables-config.ts rename to packages/examples/vanilla/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts index a335e01e2..e674e2e5e 100644 --- a/packages/examples/shared/src/configs/dynamic-nested-tables-config.ts +++ b/packages/examples/vanilla/src/demos/dynamic-nested-tables/dynamic-nested-tables.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export interface DynamicCompany extends Row { id: string; companyName: string; diff --git a/packages/examples/vanilla/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.ts b/packages/examples/vanilla/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.ts index 151908a3f..2fbf4ef11 100644 --- a/packages/examples/vanilla/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.ts +++ b/packages/examples/vanilla/src/demos/dynamic-row-loading/DynamicRowLoadingDemo.ts @@ -5,8 +5,8 @@ import { generateInitialRegions, fetchStoresForRegion, fetchProductsForStore, -} from "@simple-table/examples-shared"; -import type { DynamicRegion, DynamicStore } from "@simple-table/examples-shared"; +} from "./dynamic-row-loading.demo-data"; +import type { DynamicRegion, DynamicStore } from "./dynamic-row-loading.demo-data"; import "simple-table-core/styles.css"; export function renderDynamicRowLoadingDemo( diff --git a/packages/examples/shared/src/configs/dynamic-row-loading-config.ts b/packages/examples/vanilla/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts similarity index 99% rename from packages/examples/shared/src/configs/dynamic-row-loading-config.ts rename to packages/examples/vanilla/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts index 6c506b11a..815198310 100644 --- a/packages/examples/shared/src/configs/dynamic-row-loading-config.ts +++ b/packages/examples/vanilla/src/demos/dynamic-row-loading/dynamic-row-loading.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export interface DynamicRegion extends Row { id: string; name: string; diff --git a/packages/examples/vanilla/src/demos/empty-state/EmptyStateDemo.ts b/packages/examples/vanilla/src/demos/empty-state/EmptyStateDemo.ts index 9317576e5..dab46707f 100644 --- a/packages/examples/vanilla/src/demos/empty-state/EmptyStateDemo.ts +++ b/packages/examples/vanilla/src/demos/empty-state/EmptyStateDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { emptyStateConfig, buildEmptyStateElement } from "@simple-table/examples-shared"; +import { emptyStateConfig, buildEmptyStateElement } from "./empty-state.demo-data"; import "simple-table-core/styles.css"; export function renderEmptyStateDemo( diff --git a/packages/examples/shared/src/configs/empty-state-config.ts b/packages/examples/vanilla/src/demos/empty-state/empty-state.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/empty-state-config.ts rename to packages/examples/vanilla/src/demos/empty-state/empty-state.demo-data.ts index 42b0fd38b..34d820879 100644 --- a/packages/examples/shared/src/configs/empty-state-config.ts +++ b/packages/examples/vanilla/src/demos/empty-state/empty-state.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const emptyStateData: Row[] = []; export const emptyStateHeaders: HeaderObject[] = [ diff --git a/packages/examples/vanilla/src/demos/external-filter/ExternalFilterDemo.ts b/packages/examples/vanilla/src/demos/external-filter/ExternalFilterDemo.ts index f921c1d90..c50ac0f74 100644 --- a/packages/examples/vanilla/src/demos/external-filter/ExternalFilterDemo.ts +++ b/packages/examples/vanilla/src/demos/external-filter/ExternalFilterDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, TableFilterState } from "simple-table-core"; -import { externalFilterConfig, matchesFilter } from "@simple-table/examples-shared"; +import { externalFilterConfig, matchesFilter } from "./external-filter.demo-data"; import "simple-table-core/styles.css"; export function renderExternalFilterDemo( diff --git a/packages/examples/shared/src/configs/external-filter-config.ts b/packages/examples/vanilla/src/demos/external-filter/external-filter.demo-data.ts similarity index 99% rename from packages/examples/shared/src/configs/external-filter-config.ts rename to packages/examples/vanilla/src/demos/external-filter/external-filter.demo-data.ts index d772ecb72..d25306034 100644 --- a/packages/examples/shared/src/configs/external-filter-config.ts +++ b/packages/examples/vanilla/src/demos/external-filter/external-filter.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, TableFilterState } from "simple-table-core"; + type CellValue = string | number | boolean | null | undefined; export function matchesFilter( diff --git a/packages/examples/vanilla/src/demos/external-sort/ExternalSortDemo.ts b/packages/examples/vanilla/src/demos/external-sort/ExternalSortDemo.ts index 7fd920662..57e968c04 100644 --- a/packages/examples/vanilla/src/demos/external-sort/ExternalSortDemo.ts +++ b/packages/examples/vanilla/src/demos/external-sort/ExternalSortDemo.ts @@ -1,6 +1,6 @@ -import { SimpleTableVanilla } from "simple-table-core"; -import type { Theme, SortColumn } from "simple-table-core"; -import { externalSortConfig } from "@simple-table/examples-shared"; +import { SimpleTableVanilla, asRows } from "simple-table-core"; +import type { Theme, SortColumn, Row } from "simple-table-core"; +import { externalSortConfig } from "./external-sort.demo-data"; import "simple-table-core/styles.css"; export function renderExternalSortDemo( @@ -9,22 +9,27 @@ export function renderExternalSortDemo( ): SimpleTableVanilla { let currentSort: SortColumn | null = null; - function getSortedRows() { - const rows = [...externalSortConfig.rows]; + function getSortedRows(): Row[] { + const rows = [...asRows(externalSortConfig.rows)]; if (!currentSort) return rows; const accessor = currentSort.key.accessor as string; + const type = currentSort.key.type; const dir = currentSort.direction; return rows.sort((a, b) => { const aVal = a[accessor]; const bVal = b[accessor]; - const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; + if (aVal === bVal) return 0; + const cmp = + type === "number" + ? (Number(aVal) || 0) - (Number(bVal) || 0) + : String(aVal).localeCompare(String(bVal)); return dir === "asc" ? cmp : -cmp; }); } const table = new SimpleTableVanilla(container, { defaultHeaders: externalSortConfig.headers, - rows: externalSortConfig.rows, + rows: asRows(externalSortConfig.rows), height: options?.height ?? "400px", theme: options?.theme, externalSortHandling: true, diff --git a/packages/examples/shared/src/configs/external-sort-config.ts b/packages/examples/vanilla/src/demos/external-sort/external-sort.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/external-sort-config.ts rename to packages/examples/vanilla/src/demos/external-sort/external-sort.demo-data.ts index a499f62cf..357316116 100644 --- a/packages/examples/shared/src/configs/external-sort-config.ts +++ b/packages/examples/vanilla/src/demos/external-sort/external-sort.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const externalSortData = [ { 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" }, diff --git a/packages/examples/vanilla/src/demos/footer-renderer/FooterRendererDemo.ts b/packages/examples/vanilla/src/demos/footer-renderer/FooterRendererDemo.ts index 7d9afbcf3..499cf3a73 100644 --- a/packages/examples/vanilla/src/demos/footer-renderer/FooterRendererDemo.ts +++ b/packages/examples/vanilla/src/demos/footer-renderer/FooterRendererDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, FooterRendererProps } from "simple-table-core"; -import { footerRendererConfig } from "@simple-table/examples-shared"; +import { footerRendererConfig } from "./footer-renderer.demo-data"; import "simple-table-core/styles.css"; function getFooterColors(theme?: Theme) { diff --git a/packages/examples/shared/src/configs/footer-renderer-config.ts b/packages/examples/vanilla/src/demos/footer-renderer/footer-renderer.demo-data.ts similarity index 99% rename from packages/examples/shared/src/configs/footer-renderer-config.ts rename to packages/examples/vanilla/src/demos/footer-renderer/footer-renderer.demo-data.ts index 7fae6341b..25f58623b 100644 --- a/packages/examples/shared/src/configs/footer-renderer-config.ts +++ b/packages/examples/vanilla/src/demos/footer-renderer/footer-renderer.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const footerRendererHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "product", label: "Product Name", width: 220, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/header-renderer/HeaderRendererDemo.ts b/packages/examples/vanilla/src/demos/header-renderer/HeaderRendererDemo.ts index 63afc4e78..4435fc90e 100644 --- a/packages/examples/vanilla/src/demos/header-renderer/HeaderRendererDemo.ts +++ b/packages/examples/vanilla/src/demos/header-renderer/HeaderRendererDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject, Row } from "simple-table-core"; -import { headerRendererConfig } from "@simple-table/examples-shared"; +import { headerRendererConfig } from "./header-renderer.demo-data"; import "simple-table-core/styles.css"; type SortDir = "asc" | "desc" | null; diff --git a/packages/examples/shared/src/configs/header-renderer-config.ts b/packages/examples/vanilla/src/demos/header-renderer/header-renderer.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/header-renderer-config.ts rename to packages/examples/vanilla/src/demos/header-renderer/header-renderer.demo-data.ts index 8c417cd21..f67ee9ca8 100644 --- a/packages/examples/shared/src/configs/header-renderer-config.ts +++ b/packages/examples/vanilla/src/demos/header-renderer/header-renderer.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const headerRendererData: Row[] = [ { 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" }, diff --git a/packages/examples/vanilla/src/demos/hr/HRDemo.ts b/packages/examples/vanilla/src/demos/hr/HRDemo.ts index e643b83f3..9609168cd 100644 --- a/packages/examples/vanilla/src/demos/hr/HRDemo.ts +++ b/packages/examples/vanilla/src/demos/hr/HRDemo.ts @@ -1,7 +1,7 @@ -import { SimpleTableVanilla } from "simple-table-core"; -import type { Theme, HeaderObject, CellRenderer, CellChangeProps } from "simple-table-core"; -import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "@simple-table/examples-shared"; -import type { HREmployee, HRTagColorKey } from "@simple-table/examples-shared"; +import { SimpleTableVanilla, asRows } from "simple-table-core"; +import type { Theme, HeaderObject, CellRenderer, CellChangeProps, CellRendererProps } from "simple-table-core"; +import { hrConfig, getHRThemeColors, HR_STATUS_COLOR_MAP } from "./hr.demo-data"; +import type { HREmployee, HRTagColorKey } from "./hr.demo-data"; import "simple-table-core/styles.css"; function el(tag: string, styles?: Partial, children?: (Node | string)[]): HTMLElement { @@ -17,7 +17,7 @@ function el(tag: string, styles?: Partial, children?: (Node function buildHRHeaders(): HeaderObject[] { const renderers: Record = { - fullName: ({ row, theme }) => { + fullName: ({ row, theme }: CellRendererProps) => { const c = getHRThemeColors(theme); const d = row as unknown as HREmployee; const initials = `${d.firstName?.charAt(0) || ""}${d.lastName?.charAt(0) || ""}`; @@ -36,7 +36,7 @@ function buildHRHeaders(): HeaderObject[] { return el("div", { display: "flex", alignItems: "center" }, [avatar, info]); }, - performanceScore: ({ row, theme }) => { + performanceScore: ({ row, theme }: CellRendererProps) => { const d = row as unknown as HREmployee; const score = d.performanceScore; const c = getHRThemeColors(theme); @@ -57,7 +57,7 @@ function buildHRHeaders(): HeaderObject[] { return el("div", { width: "100%", display: "flex", flexDirection: "column" }, [track, label]); }, - hireDate: ({ row, theme }) => { + hireDate: ({ row, theme }: CellRendererProps) => { const d = row as unknown as HREmployee; if (!d.hireDate) return ""; const [year, month, day] = d.hireDate.split("-").map(Number); @@ -68,19 +68,20 @@ function buildHRHeaders(): HeaderObject[] { ]); }, - yearsOfService: ({ row, theme }) => { - if (row.yearsOfService === null) return ""; + yearsOfService: ({ row, theme }: CellRendererProps) => { + const d = row as unknown as HREmployee; + if (d.yearsOfService === null) return ""; const c = getHRThemeColors(theme); - return el("span", { color: c.gray }, [`${row.yearsOfService} yrs`]); + return el("span", { color: c.gray }, [`${d.yearsOfService} yrs`]); }, - salary: ({ row, theme }) => { + salary: ({ row, theme }: CellRendererProps) => { const d = row as unknown as HREmployee; const c = getHRThemeColors(theme); return el("span", { color: c.gray }, [`$${d.salary.toLocaleString()}`]); }, - status: ({ row, theme }) => { + status: ({ row, theme }: CellRendererProps) => { const d = row as unknown as HREmployee; if (!d.status) return ""; const c = getHRThemeColors(theme); @@ -104,7 +105,7 @@ export function renderHRDemo( container: HTMLElement, options?: { height?: string | number; theme?: Theme }, ): SimpleTableVanilla { - let rows = [...hrConfig.rows]; + let rows = [...asRows(hrConfig.rows)]; const rowHeight = 48; const heightNum = typeof options?.height === "number" ? options.height : 400; const rowsPerPage = Math.floor(heightNum / rowHeight); diff --git a/packages/examples/shared/src/configs/hr-config.ts b/packages/examples/vanilla/src/demos/hr/hr.demo-data.ts similarity index 94% rename from packages/examples/shared/src/configs/hr-config.ts rename to packages/examples/vanilla/src/demos/hr/hr.demo-data.ts index a9b2d7cab..2a574de4b 100644 --- a/packages/examples/shared/src/configs/hr-config.ts +++ b/packages/examples/vanilla/src/demos/hr/hr.demo-data.ts @@ -1,5 +1,23 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; -import type { HREmployee } from "../types/hr"; + +export interface HREmployee { + id: number; + firstName: string; + lastName: string; + fullName: string; + position: string; + performanceScore: number; + department: string; + email: string; + location: string; + hireDate: string; + yearsOfService: number; + salary: number; + status: string; + isRemoteEligible: boolean; +} + const HR_FIRST_NAMES = ["James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda", "David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Sarah", "Charles", "Karen"]; const HR_LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"]; diff --git a/packages/examples/vanilla/src/demos/infinite-scroll/InfiniteScrollDemo.ts b/packages/examples/vanilla/src/demos/infinite-scroll/InfiniteScrollDemo.ts index 195721419..444b6a20d 100644 --- a/packages/examples/vanilla/src/demos/infinite-scroll/InfiniteScrollDemo.ts +++ b/packages/examples/vanilla/src/demos/infinite-scroll/InfiniteScrollDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, Row } from "simple-table-core"; -import { infiniteScrollConfig, generateInfiniteScrollData } from "@simple-table/examples-shared"; +import { infiniteScrollConfig, generateInfiniteScrollData } from "./infinite-scroll.demo-data"; import "simple-table-core/styles.css"; const MAX_ROWS = 200; diff --git a/packages/examples/shared/src/configs/infinite-scroll-config.ts b/packages/examples/vanilla/src/demos/infinite-scroll/infinite-scroll.demo-data.ts similarity index 96% rename from packages/examples/shared/src/configs/infinite-scroll-config.ts rename to packages/examples/vanilla/src/demos/infinite-scroll/infinite-scroll.demo-data.ts index a92ba5426..e5ab657df 100644 --- a/packages/examples/shared/src/configs/infinite-scroll-config.ts +++ b/packages/examples/vanilla/src/demos/infinite-scroll/infinite-scroll.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + 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"]; diff --git a/packages/examples/vanilla/src/demos/infrastructure/InfrastructureDemo.ts b/packages/examples/vanilla/src/demos/infrastructure/InfrastructureDemo.ts index 6addb44aa..9b0ba00ba 100644 --- a/packages/examples/vanilla/src/demos/infrastructure/InfrastructureDemo.ts +++ b/packages/examples/vanilla/src/demos/infrastructure/InfrastructureDemo.ts @@ -5,8 +5,8 @@ import { INFRA_UPDATE_CONFIG, getInfraMetricColorStyles, getInfraStatusColors, -} from "@simple-table/examples-shared"; -import type { InfrastructureServer } from "@simple-table/examples-shared"; +} from "./infrastructure.demo-data"; +import type { InfrastructureServer } from "./infrastructure.demo-data"; import "simple-table-core/styles.css"; function getHeaders(currentTheme?: Theme): HeaderObject[] { diff --git a/packages/examples/shared/src/configs/infrastructure-config.ts b/packages/examples/vanilla/src/demos/infrastructure/infrastructure.demo-data.ts similarity index 94% rename from packages/examples/shared/src/configs/infrastructure-config.ts rename to packages/examples/vanilla/src/demos/infrastructure/infrastructure.demo-data.ts index b6c12cf50..af0fad420 100644 --- a/packages/examples/shared/src/configs/infrastructure-config.ts +++ b/packages/examples/vanilla/src/demos/infrastructure/infrastructure.demo-data.ts @@ -1,5 +1,22 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; -import type { InfrastructureServer } from "../types/infrastructure"; + +export interface InfrastructureServer { + id: number; + serverId: string; + serverName: string; + cpuUsage: number; + cpuHistory: number[]; + memoryUsage: number; + diskUsage: number; + responseTime: number; + networkIn: number; + networkOut: number; + activeConnections: number; + requestsPerSec: number; + status: "online" | "warning" | "critical" | "maintenance" | "offline"; +} + const SERVER_PREFIXES = [ "web", diff --git a/packages/examples/vanilla/src/demos/live-update/LiveUpdateDemo.ts b/packages/examples/vanilla/src/demos/live-update/LiveUpdateDemo.ts index 1d522085c..bfa2e1ad4 100644 --- a/packages/examples/vanilla/src/demos/live-update/LiveUpdateDemo.ts +++ b/packages/examples/vanilla/src/demos/live-update/LiveUpdateDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { liveUpdateConfig, liveUpdateData } from "@simple-table/examples-shared"; +import { liveUpdateConfig, liveUpdateData } from "./live-update.demo-data"; import "simple-table-core/styles.css"; export function renderLiveUpdateDemo( diff --git a/packages/examples/shared/src/configs/live-update-config.ts b/packages/examples/vanilla/src/demos/live-update/live-update.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/live-update-config.ts rename to packages/examples/vanilla/src/demos/live-update/live-update.demo-data.ts index 0cd1dccdc..639eb7593 100644 --- a/packages/examples/shared/src/configs/live-update-config.ts +++ b/packages/examples/vanilla/src/demos/live-update/live-update.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const liveUpdateHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 60, type: "number" }, { accessor: "product", label: "Product", width: 180, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/loading-state/LoadingStateDemo.ts b/packages/examples/vanilla/src/demos/loading-state/LoadingStateDemo.ts index 889128459..f4d9d6693 100644 --- a/packages/examples/vanilla/src/demos/loading-state/LoadingStateDemo.ts +++ b/packages/examples/vanilla/src/demos/loading-state/LoadingStateDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, Row } from "simple-table-core"; -import { loadingStateConfig } from "@simple-table/examples-shared"; +import { loadingStateConfig } from "./loading-state.demo-data"; import "simple-table-core/styles.css"; export function renderLoadingStateDemo( diff --git a/packages/examples/shared/src/configs/loading-state-config.ts b/packages/examples/vanilla/src/demos/loading-state/loading-state.demo-data.ts similarity index 96% rename from packages/examples/shared/src/configs/loading-state-config.ts rename to packages/examples/vanilla/src/demos/loading-state/loading-state.demo-data.ts index 741dcce49..9920131da 100644 --- a/packages/examples/shared/src/configs/loading-state-config.ts +++ b/packages/examples/vanilla/src/demos/loading-state/loading-state.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const loadingStateData = [ { 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" }, diff --git a/packages/examples/vanilla/src/demos/manufacturing/ManufacturingDemo.ts b/packages/examples/vanilla/src/demos/manufacturing/ManufacturingDemo.ts index 080d8ac93..6a28e586b 100644 --- a/packages/examples/vanilla/src/demos/manufacturing/ManufacturingDemo.ts +++ b/packages/examples/vanilla/src/demos/manufacturing/ManufacturingDemo.ts @@ -1,7 +1,7 @@ -import { SimpleTableVanilla } from "simple-table-core"; -import type { Theme, HeaderObject, CellRenderer } from "simple-table-core"; -import { manufacturingConfig, getManufacturingStatusColors } from "@simple-table/examples-shared"; -import type { ManufacturingRow } from "@simple-table/examples-shared"; +import { SimpleTableVanilla, asRows } from "simple-table-core"; +import type { Theme, HeaderObject, CellRenderer, CellRendererProps } from "simple-table-core"; +import { manufacturingConfig, getManufacturingStatusColors } from "./manufacturing.demo-data"; +import type { ManufacturingRow } from "./manufacturing.demo-data"; import "simple-table-core/styles.css"; function hasStations(row: Record): boolean { @@ -9,7 +9,7 @@ function hasStations(row: Record): boolean { } function getHeaders(): HeaderObject[] { - const productLineRenderer: CellRenderer = ({ row }) => { + const productLineRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; if (hasStations(row)) { const span = document.createElement("span"); @@ -20,7 +20,7 @@ function getHeaders(): HeaderObject[] { return d.productLine; }; - const stationRenderer: CellRenderer = ({ row }) => { + const stationRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; if (hasStations(row)) { const span = document.createElement("span"); @@ -45,7 +45,7 @@ function getHeaders(): HeaderObject[] { return wrapper; }; - const statusRenderer: CellRenderer = ({ row, theme }) => { + const statusRenderer: CellRenderer = ({ row, theme }: CellRendererProps) => { if (hasStations(row)) return "—"; const d = row as unknown as ManufacturingRow; const status = d.status; @@ -60,7 +60,7 @@ function getHeaders(): HeaderObject[] { return span; }; - const boldParentNumberRenderer = (accessor: keyof ManufacturingRow): CellRenderer => ({ row }) => { + const boldParentNumberRenderer = (accessor: keyof ManufacturingRow): CellRenderer => ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const value = d[accessor] as number; const div = document.createElement("div"); @@ -69,7 +69,7 @@ function getHeaders(): HeaderObject[] { return div; }; - const cycletimeRenderer: CellRenderer = ({ row }) => { + const cycletimeRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; if (hasStations(row)) { const span = document.createElement("span"); @@ -80,7 +80,7 @@ function getHeaders(): HeaderObject[] { return String(d.cycletime); }; - const efficiencyRenderer: CellRenderer = ({ row }) => { + const efficiencyRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const eff = d.efficiency; const isParent = hasStations(row); @@ -111,7 +111,7 @@ function getHeaders(): HeaderObject[] { return wrapper; }; - const defectRateRenderer: CellRenderer = ({ row }) => { + const defectRateRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const isParent = hasStations(row); const rate = d.defectRate; @@ -122,7 +122,7 @@ function getHeaders(): HeaderObject[] { return span; }; - const downtimeRenderer: CellRenderer = ({ row }) => { + const downtimeRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; const isParent = hasStations(row); const hours = d.downtime; @@ -133,7 +133,7 @@ function getHeaders(): HeaderObject[] { return span; }; - const utilizationRenderer: CellRenderer = ({ row }) => { + const utilizationRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as ManufacturingRow; if (hasStations(row)) { const span = document.createElement("span"); @@ -144,7 +144,7 @@ function getHeaders(): HeaderObject[] { return `${d.utilization}%`; }; - const maintenanceDateRenderer: CellRenderer = ({ row }) => { + const maintenanceDateRenderer: CellRenderer = ({ row }: CellRendererProps) => { if (hasStations(row)) return "—"; const d = row as unknown as ManufacturingRow; const [year, month, day] = d.maintenanceDate.split("-").map(Number); @@ -202,7 +202,7 @@ export function renderManufacturingDemo( defaultHeaders: getHeaders(), height: options?.height ?? "400px", rowGrouping: ["stations"], - rows: manufacturingConfig.rows, + rows: asRows(manufacturingConfig.rows), selectableCells: true, theme: options?.theme, }); diff --git a/packages/examples/shared/src/configs/manufacturing-config.ts b/packages/examples/vanilla/src/demos/manufacturing/manufacturing.demo-data.ts similarity index 94% rename from packages/examples/shared/src/configs/manufacturing-config.ts rename to packages/examples/vanilla/src/demos/manufacturing/manufacturing.demo-data.ts index a5a1e00d6..0ad9caabe 100644 --- a/packages/examples/shared/src/configs/manufacturing-config.ts +++ b/packages/examples/vanilla/src/demos/manufacturing/manufacturing.demo-data.ts @@ -1,5 +1,24 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; -import type { ManufacturingRow } from "../types/manufacturing"; + +export interface ManufacturingRow { + id: string; + productLine: string; + station: string; + machineType: string; + status: string; + outputRate: number; + cycletime: number; + efficiency: number; + defectRate: number; + defectCount: number; + downtime: number; + utilization: number; + energy: number; + maintenanceDate: string; + stations?: ManufacturingRow[]; +} + const PRODUCT_LINES = ["Assembly Line A", "Assembly Line B", "Welding Station", "Paint Shop", "Quality Control", "Packaging Unit", "CNC Machining", "Injection Molding"]; const STATIONS = ["Station Alpha", "Station Beta", "Station Gamma", "Station Delta", "Station Epsilon"]; diff --git a/packages/examples/vanilla/src/demos/music/MusicDemo.ts b/packages/examples/vanilla/src/demos/music/MusicDemo.ts index d6d890dbb..1b65e06b9 100644 --- a/packages/examples/vanilla/src/demos/music/MusicDemo.ts +++ b/packages/examples/vanilla/src/demos/music/MusicDemo.ts @@ -1,9 +1,9 @@ -import { SimpleTableVanilla } from "simple-table-core"; -import type { Theme, HeaderObject, CellRenderer } from "simple-table-core"; -import { musicData, getMusicThemeColors } from "@simple-table/examples-shared"; -import type { MusicArtist } from "@simple-table/examples-shared"; +import { SimpleTableVanilla, asRows } from "simple-table-core"; +import type { Theme, HeaderObject, CellRenderer, CellRendererProps } from "simple-table-core"; +import { musicData, getMusicThemeColors } from "./music.demo-data"; +import type { MusicArtist } from "./music.demo-data"; import "simple-table-core/styles.css"; -import "@simple-table/examples-shared/styles/music-theme.css"; +import "./music-theme.css"; function el(tag: string, styles?: Partial, children?: (Node | string)[]): HTMLElement { const e = document.createElement(tag); @@ -63,7 +63,7 @@ function growthMetric( function buildMusicHeaders(theme?: string): HeaderObject[] { const c = getMusicThemeColors(theme); - const artistRenderer: CellRenderer = ({ row }) => { + const artistRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; let hash = 0; for (let i = 0; i < d.artistName.length; i++) hash = d.artistName.charCodeAt(i) + ((hash << 5) - hash); @@ -88,7 +88,7 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { return el("div", { display: "flex", alignItems: "center", gap: "12px" }, [avatar, info]); }; - const artistTypeRenderer: CellRenderer = ({ row }) => { + const artistTypeRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("div", { display: "flex", flexDirection: "column", gap: "4px" }, [ el("div", { fontSize: "13px", color: c.gray }, [`${d.artistType}, ${d.pronouns}`]), @@ -97,7 +97,7 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { ]); }; - const followersRenderer: CellRenderer = ({ row }) => { + const followersRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("div", { display: "flex", flexDirection: "column", gap: "4px", alignItems: "flex-start" }, [ el("div", { fontSize: "14px", color: c.gray }, [d.followersFormatted]), @@ -105,7 +105,7 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { ]); }; - const playlistReachRenderer: CellRenderer = ({ row }) => { + const playlistReachRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const isPos = d.playlistReachChange >= 0; const pct = Math.abs(d.playlistReachChangePercent).toFixed(2); @@ -115,7 +115,7 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { ]); }; - const playlistCountRenderer: CellRenderer = ({ row }) => { + const playlistCountRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("div", { display: "flex", flexDirection: "column", gap: "4px", alignItems: "flex-start" }, [ el("div", { fontSize: "14px", color: c.gray }, [d.playlistCount.toLocaleString()]), @@ -123,7 +123,7 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { ]); }; - const monthlyListenersRenderer: CellRenderer = ({ row }) => { + const monthlyListenersRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const isPos = d.monthlyListenersChange >= 0; const pct = Math.abs(d.monthlyListenersChangePercent).toFixed(2); @@ -133,7 +133,7 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { ]); }; - const popularityRenderer: CellRenderer = ({ row }) => { + const popularityRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const isPos = d.popularityChangePercent >= 0; const wrapper = el("div", { display: "flex", justifyContent: "center" }); @@ -141,17 +141,17 @@ function buildMusicHeaders(theme?: string): HeaderObject[] { return wrapper; }; - const conversionRateRenderer: CellRenderer = ({ row }) => { + const conversionRateRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("span", { color: c.gray }, [`${d.conversionRate.toFixed(2)}%`]); }; - const ratioRenderer: CellRenderer = ({ row }) => { + const ratioRenderer: CellRenderer = ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; return el("span", { color: c.gray }, [`${d.reachFollowersRatio.toFixed(1)}x`]); }; - const growthCell = (valueKey: keyof MusicArtist, pctKey: keyof MusicArtist, signed: boolean): CellRenderer => ({ row }) => { + const growthCell = (valueKey: keyof MusicArtist, pctKey: keyof MusicArtist, signed: boolean): CellRenderer => ({ row }: CellRendererProps) => { const d = row as unknown as MusicArtist; const val = d[valueKey] as number; const pct = d[pctKey] as number; @@ -215,7 +215,7 @@ export function renderMusicDemo( const table = new SimpleTableVanilla(wrapper, { defaultHeaders: buildMusicHeaders(options?.theme), - rows: [...musicData], + rows: asRows([...musicData]), height: options?.height ?? "400px", theme: options?.theme, selectableCells: true, diff --git a/packages/examples/vanilla/src/demos/music/music-theme.css b/packages/examples/vanilla/src/demos/music/music-theme.css new file mode 100644 index 000000000..f2538a958 --- /dev/null +++ b/packages/examples/vanilla/src/demos/music/music-theme.css @@ -0,0 +1,13 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +.music-theme-container * { + font-family: "Inter"; +} + +.music-theme-container .st-header-label-text { + font-weight: 500; + font-size: 12px; +} +.music-theme-container .st-cell-content { + font-size: 12px; +} diff --git a/packages/examples/shared/src/configs/music-config.ts b/packages/examples/vanilla/src/demos/music/music.demo-data.ts similarity index 86% rename from packages/examples/shared/src/configs/music-config.ts rename to packages/examples/vanilla/src/demos/music/music.demo-data.ts index bad70d76c..266f997d8 100644 --- a/packages/examples/shared/src/configs/music-config.ts +++ b/packages/examples/vanilla/src/demos/music/music.demo-data.ts @@ -1,5 +1,64 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; -import type { MusicArtist } from "../types/music"; + +export interface MusicArtist { + id: number; + rank: 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; +} + 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"]; diff --git a/packages/examples/vanilla/src/demos/nested-headers/NestedHeadersDemo.ts b/packages/examples/vanilla/src/demos/nested-headers/NestedHeadersDemo.ts index 5931e355a..db15c96e7 100644 --- a/packages/examples/vanilla/src/demos/nested-headers/NestedHeadersDemo.ts +++ b/packages/examples/vanilla/src/demos/nested-headers/NestedHeadersDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { nestedHeadersConfig } from "@simple-table/examples-shared"; +import { nestedHeadersConfig } from "./nested-headers.demo-data"; import "simple-table-core/styles.css"; export function renderNestedHeadersDemo( diff --git a/packages/examples/shared/src/configs/nested-headers-config.ts b/packages/examples/vanilla/src/demos/nested-headers/nested-headers.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/nested-headers-config.ts rename to packages/examples/vanilla/src/demos/nested-headers/nested-headers.demo-data.ts index 359138fd0..f32d51bff 100644 --- a/packages/examples/shared/src/configs/nested-headers-config.ts +++ b/packages/examples/vanilla/src/demos/nested-headers/nested-headers.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const nestedHeadersHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, { accessor: "name", label: "Name", width: "1fr", isSortable: true, type: "string" }, diff --git a/packages/examples/vanilla/src/demos/nested-tables/NestedTablesDemo.ts b/packages/examples/vanilla/src/demos/nested-tables/NestedTablesDemo.ts index 0dd58f915..57921888c 100644 --- a/packages/examples/vanilla/src/demos/nested-tables/NestedTablesDemo.ts +++ b/packages/examples/vanilla/src/demos/nested-tables/NestedTablesDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { nestedTablesConfig, generateNestedTablesData } from "@simple-table/examples-shared"; +import { nestedTablesConfig, generateNestedTablesData } from "./nested-tables.demo-data"; import "simple-table-core/styles.css"; export function renderNestedTablesDemo( diff --git a/packages/examples/shared/src/configs/nested-tables-config.ts b/packages/examples/vanilla/src/demos/nested-tables/nested-tables.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/nested-tables-config.ts rename to packages/examples/vanilla/src/demos/nested-tables/nested-tables.demo-data.ts index f6e70db3f..70fe494f4 100644 --- a/packages/examples/shared/src/configs/nested-tables-config.ts +++ b/packages/examples/vanilla/src/demos/nested-tables/nested-tables.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + 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"]; const firstNames = ["Jane", "John", "Emily", "Michael", "Sarah", "David", "Lisa", "Robert", "Maria", "James", "Jennifer", "William", "Patricia", "Richard", "Linda"]; diff --git a/packages/examples/vanilla/src/demos/pagination/PaginationDemo.ts b/packages/examples/vanilla/src/demos/pagination/PaginationDemo.ts index ec6883246..2e5bfb579 100644 --- a/packages/examples/vanilla/src/demos/pagination/PaginationDemo.ts +++ b/packages/examples/vanilla/src/demos/pagination/PaginationDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "@simple-table/examples-shared"; +import { paginationConfig, paginationData, PAGINATION_ROWS_PER_PAGE } from "./pagination.demo-data"; import "simple-table-core/styles.css"; export function renderPaginationDemo( diff --git a/packages/examples/shared/src/configs/pagination-config.ts b/packages/examples/vanilla/src/demos/pagination/pagination.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/pagination-config.ts rename to packages/examples/vanilla/src/demos/pagination/pagination.demo-data.ts index 01173bd57..bb08fafa5 100644 --- a/packages/examples/shared/src/configs/pagination-config.ts +++ b/packages/examples/vanilla/src/demos/pagination/pagination.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const PAGINATION_ROWS_PER_PAGE = 9; export const paginationHeaders: HeaderObject[] = [ diff --git a/packages/examples/vanilla/src/demos/programmatic-control/ProgrammaticControlDemo.ts b/packages/examples/vanilla/src/demos/programmatic-control/ProgrammaticControlDemo.ts index 6973632c6..e77a90f67 100644 --- a/packages/examples/vanilla/src/demos/programmatic-control/ProgrammaticControlDemo.ts +++ b/packages/examples/vanilla/src/demos/programmatic-control/ProgrammaticControlDemo.ts @@ -3,7 +3,7 @@ import type { Theme, HeaderObject } from "simple-table-core"; import { programmaticControlConfig, PROGRAMMATIC_CONTROL_STATUS_COLORS, -} from "@simple-table/examples-shared"; +} from "./programmatic-control.demo-data"; import "simple-table-core/styles.css"; export function renderProgrammaticControlDemo( diff --git a/packages/examples/shared/src/configs/programmatic-control-config.ts b/packages/examples/vanilla/src/demos/programmatic-control/programmatic-control.demo-data.ts similarity index 79% rename from packages/examples/shared/src/configs/programmatic-control-config.ts rename to packages/examples/vanilla/src/demos/programmatic-control/programmatic-control.demo-data.ts index 31c0ee9f2..e4cfaea0c 100644 --- a/packages/examples/shared/src/configs/programmatic-control-config.ts +++ b/packages/examples/vanilla/src/demos/programmatic-control/programmatic-control.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const STATUS_COLORS: Record = { Available: { bg: "#dcfce7", color: "#166534" }, "Low Stock": { bg: "#fef3c7", color: "#92400e" }, @@ -24,13 +26,31 @@ export const programmaticControlData = [ export const programmaticControlHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 70, type: "number", isSortable: true, filterable: true }, { accessor: "name", label: "Product Name", width: "1fr", minWidth: 150, type: "string", isSortable: true, filterable: true }, - { accessor: "category", label: "Category", width: 140, type: "enum", isSortable: true, filterable: true, enumOptions: ["Electronics", "Furniture", "Stationery", "Appliances"] }, + { + accessor: "category", + label: "Category", + width: 140, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Electronics", "Furniture", "Stationery", "Appliances"].map((v) => ({ label: v, value: v })), + }, { accessor: "price", label: "Price", width: 110, align: "right", type: "number", isSortable: true, filterable: true, valueFormatter: ({ value }) => `$${(value as number).toFixed(2)}` }, { accessor: "stock", label: "Stock", width: 100, align: "right", type: "number", isSortable: true, filterable: true }, - { accessor: "status", label: "Status", width: 110, type: "enum", isSortable: true, filterable: true, enumOptions: ["Available", "Low Stock", "Out of Stock"] }, + { + accessor: "status", + label: "Status", + width: 110, + type: "enum", + isSortable: true, + filterable: true, + enumOptions: ["Available", "Low Stock", "Out of Stock"].map((v) => ({ label: v, value: v })), + }, ]; export const programmaticControlConfig = { headers: programmaticControlHeaders, rows: programmaticControlData, } as const; + +export { STATUS_COLORS as PROGRAMMATIC_CONTROL_STATUS_COLORS }; diff --git a/packages/examples/vanilla/src/demos/quick-filter/QuickFilterDemo.ts b/packages/examples/vanilla/src/demos/quick-filter/QuickFilterDemo.ts index 599565d50..fe0d101f5 100644 --- a/packages/examples/vanilla/src/demos/quick-filter/QuickFilterDemo.ts +++ b/packages/examples/vanilla/src/demos/quick-filter/QuickFilterDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, QuickFilterMode } from "simple-table-core"; -import { quickFilterConfig } from "@simple-table/examples-shared"; +import { quickFilterConfig } from "./quick-filter.demo-data"; import "simple-table-core/styles.css"; export function renderQuickFilterDemo( diff --git a/packages/examples/shared/src/configs/quick-filter-config.ts b/packages/examples/vanilla/src/demos/quick-filter/quick-filter.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/quick-filter-config.ts rename to packages/examples/vanilla/src/demos/quick-filter/quick-filter.demo-data.ts index 7ade18d40..1dc33566a 100644 --- a/packages/examples/shared/src/configs/quick-filter-config.ts +++ b/packages/examples/vanilla/src/demos/quick-filter/quick-filter.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const quickFilterHeaders: HeaderObject[] = [ { accessor: "name", label: "Employee Name", width: 180, type: "string" }, { accessor: "age", label: "Age", width: 80, type: "number" }, diff --git a/packages/examples/vanilla/src/demos/quick-start/QuickStartDemo.ts b/packages/examples/vanilla/src/demos/quick-start/QuickStartDemo.ts index 47bc990c0..16a734830 100644 --- a/packages/examples/vanilla/src/demos/quick-start/QuickStartDemo.ts +++ b/packages/examples/vanilla/src/demos/quick-start/QuickStartDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { quickStartConfig } from "@simple-table/examples-shared"; +import { quickStartConfig } from "./quick-start.demo-data"; import "simple-table-core/styles.css"; export function renderQuickStartDemo( diff --git a/packages/examples/shared/src/data/quick-start-data.ts b/packages/examples/vanilla/src/demos/quick-start/quick-start.demo-data.ts similarity index 66% rename from packages/examples/shared/src/data/quick-start-data.ts rename to packages/examples/vanilla/src/demos/quick-start/quick-start.demo-data.ts index c1e17d39b..d6b1e4086 100644 --- a/packages/examples/shared/src/data/quick-start-data.ts +++ b/packages/examples/vanilla/src/demos/quick-start/quick-start.demo-data.ts @@ -1,4 +1,7 @@ +// Self-contained demo table setup for this example. import type { Row } from "simple-table-core"; +import type { HeaderObject } from "simple-table-core"; + export const QUICK_START_DATA: Row[] = [ { @@ -98,3 +101,30 @@ export const QUICK_START_DATA: Row[] = [ startDate: "2019-12-02", }, ]; + + +export const quickStartHeaders: HeaderObject[] = [ + { accessor: "id", label: "ID", width: 80, isSortable: true, type: "number" }, + { + accessor: "name", + label: "Name", + minWidth: 80, + width: "1fr", + isSortable: true, + type: "string", + }, + { accessor: "age", label: "Age", width: 100, isSortable: true, type: "number" }, + { accessor: "role", label: "Role", width: 150, isSortable: true, type: "string" }, + { accessor: "department", label: "Department", width: 150, isSortable: true, type: "string" }, + { accessor: "startDate", label: "Start Date", width: 150, isSortable: true, type: "date" }, +]; + +export const quickStartConfig = { + headers: quickStartHeaders, + rows: QUICK_START_DATA, + tableProps: { + editColumns: true, + selectableCells: true, + customTheme: { rowHeight: 32 }, + }, +} as const; diff --git a/packages/examples/vanilla/src/demos/row-grouping/RowGroupingDemo.ts b/packages/examples/vanilla/src/demos/row-grouping/RowGroupingDemo.ts index 8cb2b5665..44c1fd0d2 100644 --- a/packages/examples/vanilla/src/demos/row-grouping/RowGroupingDemo.ts +++ b/packages/examples/vanilla/src/demos/row-grouping/RowGroupingDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { rowGroupingConfig } from "@simple-table/examples-shared"; +import { rowGroupingConfig } from "./row-grouping.demo-data"; import "simple-table-core/styles.css"; export function renderRowGroupingDemo( diff --git a/packages/examples/shared/src/configs/row-grouping-config.ts b/packages/examples/vanilla/src/demos/row-grouping/row-grouping.demo-data.ts similarity index 99% rename from packages/examples/shared/src/configs/row-grouping-config.ts rename to packages/examples/vanilla/src/demos/row-grouping/row-grouping.demo-data.ts index 41bb5cc46..4ddf5b3c2 100644 --- a/packages/examples/shared/src/configs/row-grouping-config.ts +++ b/packages/examples/vanilla/src/demos/row-grouping/row-grouping.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const rowGroupingHeaders: HeaderObject[] = [ { accessor: "organization", label: "Organization", width: 200, expandable: true, type: "string" }, { accessor: "employees", label: "Employees", width: 100, type: "number" }, diff --git a/packages/examples/vanilla/src/demos/row-height/RowHeightDemo.ts b/packages/examples/vanilla/src/demos/row-height/RowHeightDemo.ts index 3ad4c0ea0..5cf913b02 100644 --- a/packages/examples/vanilla/src/demos/row-height/RowHeightDemo.ts +++ b/packages/examples/vanilla/src/demos/row-height/RowHeightDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { rowHeightConfig } from "@simple-table/examples-shared"; +import { rowHeightConfig } from "./row-height.demo-data"; import "simple-table-core/styles.css"; export function renderRowHeightDemo( diff --git a/packages/examples/shared/src/configs/row-height-config.ts b/packages/examples/vanilla/src/demos/row-height/row-height.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/row-height-config.ts rename to packages/examples/vanilla/src/demos/row-height/row-height.demo-data.ts index e4f1edfa9..1b7387ae5 100644 --- a/packages/examples/shared/src/configs/row-height-config.ts +++ b/packages/examples/vanilla/src/demos/row-height/row-height.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const rowHeightHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 150, width: "1fr", type: "string" }, diff --git a/packages/examples/vanilla/src/demos/row-selection/RowSelectionDemo.ts b/packages/examples/vanilla/src/demos/row-selection/RowSelectionDemo.ts index 584c96362..feaf27f3c 100644 --- a/packages/examples/vanilla/src/demos/row-selection/RowSelectionDemo.ts +++ b/packages/examples/vanilla/src/demos/row-selection/RowSelectionDemo.ts @@ -1,7 +1,7 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject } from "simple-table-core"; -import { rowSelectionConfig, rowSelectionData } from "@simple-table/examples-shared"; -import type { LibraryBook } from "@simple-table/examples-shared"; +import { rowSelectionConfig, rowSelectionData } from "./row-selection.demo-data"; +import type { LibraryBook } from "./row-selection.demo-data"; import "simple-table-core/styles.css"; export function renderRowSelectionDemo( diff --git a/packages/examples/shared/src/configs/row-selection-config.ts b/packages/examples/vanilla/src/demos/row-selection/row-selection.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/row-selection-config.ts rename to packages/examples/vanilla/src/demos/row-selection/row-selection.demo-data.ts index 4ee987997..e8527cbb8 100644 --- a/packages/examples/shared/src/configs/row-selection-config.ts +++ b/packages/examples/vanilla/src/demos/row-selection/row-selection.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export type LibraryBook = { id: number; isbn: string; diff --git a/packages/examples/vanilla/src/demos/sales/SalesDemo.ts b/packages/examples/vanilla/src/demos/sales/SalesDemo.ts index 0cef5bbb1..5ad8d4e11 100644 --- a/packages/examples/vanilla/src/demos/sales/SalesDemo.ts +++ b/packages/examples/vanilla/src/demos/sales/SalesDemo.ts @@ -1,7 +1,7 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme, HeaderObject, CellRenderer, CellChangeProps, Row } from "simple-table-core"; -import { salesConfig, getSalesThemeColors } from "@simple-table/examples-shared"; -import type { SalesRow } from "@simple-table/examples-shared"; +import { salesConfig, getSalesThemeColors } from "./sales.demo-data"; +import type { SalesRow } from "./sales.demo-data"; import "simple-table-core/styles.css"; function el(tag: string, styles?: Partial, children?: (Node | string)[]): HTMLElement { diff --git a/packages/examples/shared/src/configs/sales-config.ts b/packages/examples/vanilla/src/demos/sales/sales.demo-data.ts similarity index 95% rename from packages/examples/shared/src/configs/sales-config.ts rename to packages/examples/vanilla/src/demos/sales/sales.demo-data.ts index f9147e77d..20d378915 100644 --- a/packages/examples/shared/src/configs/sales-config.ts +++ b/packages/examples/vanilla/src/demos/sales/sales.demo-data.ts @@ -1,5 +1,19 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; -import type { SalesRow } from "../types/sales"; + +export interface SalesRow { + id: number; + repName: string; + dealSize: number; + dealValue: number; + isWon: boolean; + closeDate: string; + commission: number; + profitMargin: number; + dealProfit: number; + category: string; +} + const SALES_REP_NAMES = ["Alex Morgan", "Sam Rivera", "Jordan Lee", "Taylor Kim", "Casey Chen", "Riley Park", "Morgan Wells", "Avery Quinn", "Devon Blake", "Harper Cole", "Quinn Foster", "Sage Turner", "Cameron Reed", "Jesse Nash", "Blake Palmer"]; const CATEGORIES = ["Software", "Hardware", "Services", "Consulting", "Training", "Support"]; diff --git a/packages/examples/vanilla/src/demos/single-row-children/SingleRowChildrenDemo.ts b/packages/examples/vanilla/src/demos/single-row-children/SingleRowChildrenDemo.ts index 901e8ac87..3f3a31750 100644 --- a/packages/examples/vanilla/src/demos/single-row-children/SingleRowChildrenDemo.ts +++ b/packages/examples/vanilla/src/demos/single-row-children/SingleRowChildrenDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { singleRowChildrenConfig } from "@simple-table/examples-shared"; +import { singleRowChildrenConfig } from "./single-row-children.demo-data"; import "simple-table-core/styles.css"; export function renderSingleRowChildrenDemo( diff --git a/packages/examples/shared/src/configs/single-row-children-config.ts b/packages/examples/vanilla/src/demos/single-row-children/single-row-children.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/single-row-children-config.ts rename to packages/examples/vanilla/src/demos/single-row-children/single-row-children.demo-data.ts index f6a0ff0d2..ab8c987a0 100644 --- a/packages/examples/shared/src/configs/single-row-children-config.ts +++ b/packages/examples/vanilla/src/demos/single-row-children/single-row-children.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const singleRowChildrenData = [ { 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 }, diff --git a/packages/examples/vanilla/src/demos/spreadsheet/SpreadsheetDemo.ts b/packages/examples/vanilla/src/demos/spreadsheet/SpreadsheetDemo.ts index c43ae7a99..02c28a800 100644 --- a/packages/examples/vanilla/src/demos/spreadsheet/SpreadsheetDemo.ts +++ b/packages/examples/vanilla/src/demos/spreadsheet/SpreadsheetDemo.ts @@ -1,9 +1,9 @@ -import { SimpleTableVanilla } from "simple-table-core"; +import { SimpleTableVanilla, asRows } from "simple-table-core"; import type { Theme, HeaderObject, CellChangeProps } from "simple-table-core"; -import { spreadsheetConfig, recalculateAmortization } from "@simple-table/examples-shared"; -import type { SpreadsheetRow } from "@simple-table/examples-shared"; +import { spreadsheetConfig, recalculateAmortization } from "./spreadsheet.demo-data"; +import type { SpreadsheetRow } from "./spreadsheet.demo-data"; import "simple-table-core/styles.css"; -import "@simple-table/examples-shared/styles/spreadsheet-custom.css"; +import "./spreadsheet-custom.css"; export function renderSpreadsheetDemo( container: HTMLElement, @@ -13,7 +13,7 @@ export function renderSpreadsheetDemo( wrapper.className = "spreadsheet-container"; container.appendChild(wrapper); - let rows = [...spreadsheetConfig.rows]; + let sheetRows: SpreadsheetRow[] = [...spreadsheetConfig.rows]; let additionalColumns: HeaderObject[] = []; function buildHeaders(): HeaderObject[] { @@ -72,7 +72,7 @@ export function renderSpreadsheetDemo( const table = new SimpleTableVanilla(wrapper, { defaultHeaders: buildHeaders(), - rows, + rows: asRows(sheetRows), height: options?.height ?? "400px", theme: options?.theme, columnBorders: true, @@ -85,13 +85,13 @@ export function renderSpreadsheetDemo( useOddEvenRowBackground: true, customTheme: { rowHeight: 22 }, onCellEdit: ({ accessor, newValue, row }: CellChangeProps) => { - rows = rows.map((item) => { + sheetRows = sheetRows.map((item) => { if (item.id === row.id) { - return recalculateAmortization(item as SpreadsheetRow, accessor, newValue as string | number); + return recalculateAmortization(item, accessor, newValue as string | number); } return item; }); - table.update({ rows }); + table.update({ rows: asRows(sheetRows) }); }, }); return table; diff --git a/packages/examples/vanilla/src/demos/spreadsheet/spreadsheet-custom.css b/packages/examples/vanilla/src/demos/spreadsheet/spreadsheet-custom.css new file mode 100644 index 000000000..872519cbb --- /dev/null +++ b/packages/examples/vanilla/src/demos/spreadsheet/spreadsheet-custom.css @@ -0,0 +1,28 @@ +.spreadsheet-container .st-cell-content { + font-size: 13px !important; +} + +.spreadsheet-container .st-header-label { + font-size: 13px !important; + font-weight: 500 !important; +} + +.spreadsheet-container .simple-table-cell { + padding: 4px 8px !important; +} + +.spreadsheet-container .simple-table { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, + sans-serif; +} + +.spreadsheet-container .st-header-resize-handle { + height: 10px !important; +} + +.spreadsheet-container .st-checkbox-custom { + min-height: 14px !important; + min-width: 14px !important; + max-height: 14px !important; + max-width: 14px !important; +} diff --git a/packages/examples/shared/src/configs/spreadsheet-config.ts b/packages/examples/vanilla/src/demos/spreadsheet/spreadsheet.demo-data.ts similarity index 94% rename from packages/examples/shared/src/configs/spreadsheet-config.ts rename to packages/examples/vanilla/src/demos/spreadsheet/spreadsheet.demo-data.ts index bf88552f0..b792a0e3e 100644 --- a/packages/examples/shared/src/configs/spreadsheet-config.ts +++ b/packages/examples/vanilla/src/demos/spreadsheet/spreadsheet.demo-data.ts @@ -1,5 +1,16 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; -import type { SpreadsheetRow } from "../types/spreadsheet"; + +export interface SpreadsheetRow { + id: number; + principal: number; + interestRate: number; + monthlyPayment: number; + remainingBalance: number; + totalPaid: number; + interestPaid: number; +} + export function generateSpreadsheetData(count: number = 100): SpreadsheetRow[] { return Array.from({ length: count }, (_, i) => { diff --git a/packages/examples/vanilla/src/demos/table-height/TableHeightDemo.ts b/packages/examples/vanilla/src/demos/table-height/TableHeightDemo.ts index 9ecefc0aa..61498be3f 100644 --- a/packages/examples/vanilla/src/demos/table-height/TableHeightDemo.ts +++ b/packages/examples/vanilla/src/demos/table-height/TableHeightDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { tableHeightConfig } from "@simple-table/examples-shared"; +import { tableHeightConfig } from "./table-height.demo-data"; import "simple-table-core/styles.css"; export function renderTableHeightDemo( diff --git a/packages/examples/shared/src/configs/table-height-config.ts b/packages/examples/vanilla/src/demos/table-height/table-height.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/table-height-config.ts rename to packages/examples/vanilla/src/demos/table-height/table-height.demo-data.ts index 47f0db585..5c04be799 100644 --- a/packages/examples/shared/src/configs/table-height-config.ts +++ b/packages/examples/vanilla/src/demos/table-height/table-height.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject } from "simple-table-core"; + export const tableHeightHeaders: HeaderObject[] = [ { accessor: "id", label: "ID", width: 80, type: "number" }, { accessor: "name", label: "Name", minWidth: 150, width: "1fr", type: "string" }, diff --git a/packages/examples/vanilla/src/demos/themes/ThemesDemo.ts b/packages/examples/vanilla/src/demos/themes/ThemesDemo.ts index d7449b383..83495a766 100644 --- a/packages/examples/vanilla/src/demos/themes/ThemesDemo.ts +++ b/packages/examples/vanilla/src/demos/themes/ThemesDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { themesConfig, AVAILABLE_THEMES } from "@simple-table/examples-shared"; +import { themesConfig, AVAILABLE_THEMES } from "./themes.demo-data"; import "simple-table-core/styles.css"; export function renderThemesDemo( diff --git a/packages/examples/shared/src/configs/themes-config.ts b/packages/examples/vanilla/src/demos/themes/themes.demo-data.ts similarity index 97% rename from packages/examples/shared/src/configs/themes-config.ts rename to packages/examples/vanilla/src/demos/themes/themes.demo-data.ts index 06243096a..5da9f05f9 100644 --- a/packages/examples/shared/src/configs/themes-config.ts +++ b/packages/examples/vanilla/src/demos/themes/themes.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Theme } from "simple-table-core"; + export const AVAILABLE_THEMES: { value: Theme; label: string }[] = [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" }, diff --git a/packages/examples/vanilla/src/demos/tooltip/TooltipDemo.ts b/packages/examples/vanilla/src/demos/tooltip/TooltipDemo.ts index 8db3ce700..ae4b47950 100644 --- a/packages/examples/vanilla/src/demos/tooltip/TooltipDemo.ts +++ b/packages/examples/vanilla/src/demos/tooltip/TooltipDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { tooltipConfig } from "@simple-table/examples-shared"; +import { tooltipConfig } from "./tooltip.demo-data"; import "simple-table-core/styles.css"; export function renderTooltipDemo( diff --git a/packages/examples/shared/src/configs/tooltip-config.ts b/packages/examples/vanilla/src/demos/tooltip/tooltip.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/tooltip-config.ts rename to packages/examples/vanilla/src/demos/tooltip/tooltip.demo-data.ts index a0d944270..b0b6b1349 100644 --- a/packages/examples/shared/src/configs/tooltip-config.ts +++ b/packages/examples/vanilla/src/demos/tooltip/tooltip.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const tooltipData: Row[] = [ { 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" }, diff --git a/packages/examples/vanilla/src/demos/value-formatter/ValueFormatterDemo.ts b/packages/examples/vanilla/src/demos/value-formatter/ValueFormatterDemo.ts index 173780240..c3de56ad5 100644 --- a/packages/examples/vanilla/src/demos/value-formatter/ValueFormatterDemo.ts +++ b/packages/examples/vanilla/src/demos/value-formatter/ValueFormatterDemo.ts @@ -1,6 +1,6 @@ import { SimpleTableVanilla } from "simple-table-core"; import type { Theme } from "simple-table-core"; -import { valueFormatterConfig } from "@simple-table/examples-shared"; +import { valueFormatterConfig } from "./value-formatter.demo-data"; import "simple-table-core/styles.css"; export function renderValueFormatterDemo( diff --git a/packages/examples/shared/src/configs/value-formatter-config.ts b/packages/examples/vanilla/src/demos/value-formatter/value-formatter.demo-data.ts similarity index 98% rename from packages/examples/shared/src/configs/value-formatter-config.ts rename to packages/examples/vanilla/src/demos/value-formatter/value-formatter.demo-data.ts index a59ee7bd3..3a3fc94dd 100644 --- a/packages/examples/shared/src/configs/value-formatter-config.ts +++ b/packages/examples/vanilla/src/demos/value-formatter/value-formatter.demo-data.ts @@ -1,5 +1,7 @@ +// Self-contained demo table setup for this example. import type { HeaderObject, Row } from "simple-table-core"; + export const valueFormatterData: Row[] = [ { 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" }, diff --git a/packages/examples/vanilla/src/main.ts b/packages/examples/vanilla/src/main.ts index 114f9da4b..74d27e47a 100644 --- a/packages/examples/vanilla/src/main.ts +++ b/packages/examples/vanilla/src/main.ts @@ -1,6 +1,6 @@ -import { DEMO_LIST } from "@simple-table/examples-shared"; +import { DEMO_LIST } from "./demo-list"; import type { Theme } from "simple-table-core"; -import "../../shared/src/styles/shell.css"; +import "./styles/shell.css"; type DemoRenderer = ( container: HTMLElement, diff --git a/packages/examples/vanilla/src/styles/shell.css b/packages/examples/vanilla/src/styles/shell.css new file mode 100644 index 000000000..e19fa2fe2 --- /dev/null +++ b/packages/examples/vanilla/src/styles/shell.css @@ -0,0 +1,63 @@ +.examples-shell { + display: flex; + height: 100vh; + overflow: hidden; +} + +.examples-sidebar { + width: 240px; + min-width: 240px; + background: #1e293b; + border-right: 1px solid #334155; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.examples-sidebar-header { + padding: 20px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #64748b; + border-bottom: 1px solid #334155; +} + +.examples-sidebar-nav { + list-style: none; + padding: 8px 0; + margin: 0; +} + +.examples-sidebar-link { + display: block; + width: 100%; + padding: 10px 20px; + background: none; + border: none; + color: #94a3b8; + text-decoration: none; + font-size: 14px; + font-family: inherit; + text-align: left; + cursor: pointer; + transition: background-color 0.15s, color 0.15s; +} + +.examples-sidebar-link:hover { + background-color: rgba(51, 65, 85, 0.5); + color: #e2e8f0; +} + +.examples-sidebar-link.active { + background-color: rgba(59, 130, 246, 0.15); + color: #60a5fa; + font-weight: 500; +} + +.examples-content { + flex: 1; + overflow-y: auto; + padding: 24px; +} diff --git a/packages/examples/vanilla/vite.config.ts b/packages/examples/vanilla/vite.config.ts index ea438da0a..cdfa14f18 100644 --- a/packages/examples/vanilla/vite.config.ts +++ b/packages/examples/vanilla/vite.config.ts @@ -10,8 +10,6 @@ export default defineConfig({ alias: [ { find: "simple-table-core/styles.css", replacement: path.resolve(__dirname, "../../core/src/styles/base.css") }, { find: "simple-table-core", replacement: path.resolve(__dirname, "../../core/src/index.ts") }, - { find: /^@simple-table\/examples-shared\/(.*)$/, replacement: path.resolve(__dirname, "../shared/src/$1") }, - { find: "@simple-table/examples-shared", replacement: path.resolve(__dirname, "../shared/src/index.ts") }, ], }, }); diff --git a/packages/examples/vue/package.json b/packages/examples/vue/package.json index 888665d81..54a77c05d 100644 --- a/packages/examples/vue/package.json +++ b/packages/examples/vue/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@simple-table/vue": "workspace:*", - "@simple-table/examples-shared": "workspace:*", "vue": "^3.0.0" }, "devDependencies": { diff --git a/packages/examples/vue/src/App.vue b/packages/examples/vue/src/App.vue index 21bede85d..666d118fb 100644 --- a/packages/examples/vue/src/App.vue +++ b/packages/examples/vue/src/App.vue @@ -1,6 +1,6 @@ `; // --- Solid --- -const solidDemo = `import { SimpleTable } from "simple-table-solid"; -import type { Theme } from "simple-table-solid"; -import { ${camel}Config } from "@simple-table/examples-shared"; -import "simple-table-core/styles.css"; +const solidDemo = `import { SimpleTable, defaultHeadersFromCore } from "@simple-table/solid"; +import type { Theme } from "@simple-table/solid"; +import { ${camel}Config } from "./${demoName}.demo-data"; +import "@simple-table/solid/styles.css"; export default function ${pascal}Demo(props: { height?: string | number; @@ -157,7 +154,7 @@ export default function ${pascal}Demo(props: { }) { return ( import("./demos/${demoName}/${pascal}Demo"); Vue: registry["${demoName}"] = () => import("./demos/${demoName}/${pascal}Demo.vue"); Svelte: registry["${demoName}"] = () => import("./demos/${demoName}/${pascal}Demo.svelte");