Grid Theming With Emotion
LyteNyte Grid can be styled using Emotion CSS (or any other CSS-in-JS library). This guide explains an approach compatible with styled-component style APIs.
Info
Modern React recommends avoiding runtime CSS-in-JS libraries. We include this guide because many codebases still rely on them. LyteNyte Grid does not assume a specific CSS framework, so a styled-component approach works as well as a build-time CSS approach.
Emotion Setup
To start using Emotion, install the @emotion/styled package:
npm install @emotion/styledpnpm add @emotion/styledyarn add @emotion/styledbun add @emotion/styledThis package provides Emotion’s styled-component API and lets you create pre-styled components:
1import styled from "@emotion/styled";2
3const Button = styled.button`4 color: turquoise;5`;We apply this pattern to style LyteNyte Grid components.
Creating Styled LyteNyte Grid Components
LyteNyte Grid is headless and exposes root primitives you can style individually.
Emotion accepts arbitrary components, and the grid primitives align with Emotion’s
API, so creating styled components is straightforward. For example, you can create a styled Grid.Cell:
1const Cell = styled(Grid.Cell)`2 display: flex;3 align-items: center;4 padding-inline: 8px;5 background-color: light-dark(white, hsla(190, 32%, 6%, 1));6 color: light-dark(hsla(175, 6%, 38%, 1), hsla(175, 10%, 86%, 1));7 font-size: 14px;8 border-bottom: 1px solid light-dark(hsla(175, 20%, 95%, 1), hsla(177, 19%, 17%, 1));9`;Use the Cell component in place of Grid.Cell when building the grid view.
The following example demonstrates this and also defines a styled header.
Styling With Emotion
29 collapsed lines
1import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";2import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";3import { CacheProvider } from "@emotion/react";4import createCache from "@emotion/cache";5import styled from "@emotion/styled";6import { useMemo } from "react";7
8export type BankData = (typeof bankDataSmall)[number];9interface GridSpec {10 readonly data: BankData;11}12
13const columns: Grid.Column<GridSpec>[] = [14 { name: "Job", id: "job", width: 120 },15 { name: "Age", id: "age", type: "number", width: 80, cellRenderer: NumberCell },16 { name: "Balance", id: "balance", type: "number", cellRenderer: BalanceCell },17 { name: "Education", id: "education" },18 { name: "Marital", id: "marital" },19 { name: "Default", id: "default" },20 { name: "Housing", id: "housing" },21 { name: "Loan", id: "loan" },22 { name: "Contact", id: "contact" },23 { name: "Day", id: "day", type: "number", cellRenderer: NumberCell },24 { name: "Month", id: "month" },25 { name: "Duration", id: "duration", type: "number", cellRenderer: DurationCell },26];27
28const base: Grid.ColumnBase<GridSpec> = { width: 100 };29
30
31const Cell = styled(Grid.Cell)`32 display: flex;33 align-items: center;34 padding-inline: 8px;35 background-color: light-dark(white, hsla(190, 32%, 6%, 1));36 color: light-dark(hsla(175, 6%, 38%, 1), hsla(175, 10%, 86%, 1));37 font-size: 14px;38 border-bottom: 1px solid light-dark(hsla(175, 20%, 95%, 1), hsla(177, 19%, 17%, 1));39
40 &[data-ln-type="number"] {41 justify-content: flex-end;42 }43`;44
45const HeaderCell = styled(Grid.HeaderCell)`46 display: flex;47 align-items: center;48 padding-inline: 8px;49 background-color: light-dark(hsla(175, 12%, 92%, 1), hsla(177, 19%, 17%, 1));50 color: light-dark(hsla(177, 19%, 17%, 1), hsla(175, 12%, 92%, 1));51 text-transform: capitalize;52 font-size: 14px;53
54 &[data-ln-type="number"] {55 justify-content: flex-end;56 }57`;58
59const Row = styled(Grid.Row)`60 &[data-ln-alternate="true"] [data-ln-cell="true"] {61 background-color: light-dark(hsl(0, 27%, 98%), hsl(184, 33%, 8%));62 }63`;64
65export default function ThemingDemo() {66 const ds = useClientDataSource({ data: bankDataSmall });67
68 const cache = useMemo(() => {69 return createCache({ key: "x" });70 }, []);71
72 return (73 <CacheProvider value={cache}>74 <div className="classes">75 <div style={{ height: 500 }}>76 <Grid rowSource={ds} columns={columns} columnBase={base}>77 <Grid.Viewport>78 <Grid.Header>79 {(cells) => {80 return (81 <Grid.HeaderRow>82 {cells.map((cell) => {83 if (cell.kind === "group") return null;84
85 return <HeaderCell key={cell.id} cell={cell} />;86 })}87 </Grid.HeaderRow>88 );89 }}90 </Grid.Header>91 <Grid.RowsContainer>92 <Grid.RowsCenter>93 {(row) => {94 if (row.kind === "full-width") return null;95
96 return (97 <Row row={row}>98 {row.cells.map((cell) => {99 return <Cell cell={cell} key={cell.id} />;100 })}101 </Row>102 );103 }}104 </Grid.RowsCenter>105 </Grid.RowsContainer>106 </Grid.Viewport>107 </Grid>108 </div>109 </div>110 </CacheProvider>111 );112}113
27 collapsed lines
114
115const formatter = new Intl.NumberFormat("en-US", {116 maximumFractionDigits: 2,117 minimumFractionDigits: 0,118});119export function BalanceCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {120 const field = api.columnField(column, row);121
122 if (typeof field === "number") {123 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;124
125 return "$" + formatter.format(field);126 }127
128 return `${field ?? "-"}`;129}130export function DurationCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {131 const field = api.columnField(column, row);132
133 return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;134}135
136export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {137 const field = api.columnField(column, row);138
139 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;140}Styling Elements Not Directly Exposed
The Grid Theming guide explains that some elements are not directly exposed through LyteNyte Grid’s public component interface. You must style these elements using data attributes or any other selector strategy. When using Emotion, you can target these elements with nested CSS selectors inside a styled wrapper.
Styling Row Detail
To style the row-detail element, wrap the Grid.Viewport or Grid.RowsContainer components
in a styled wrapper and target the element with the data-ln-row-detail attribute.
The example below shows this approach.
Emotion Styling Row Detail
66 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { companiesWithPricePerf } from "@1771technologies/grid-sample-data/companies-with-price-performance";3import { useMemo } from "react";4import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts";5import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";6import styled from "@emotion/styled";7import { CacheProvider } from "@emotion/react";8import createCache from "@emotion/cache";9
10type PerformanceData = (typeof companiesWithPricePerf)[number];11export interface GridSpec {12 readonly data: PerformanceData;13}14
15const columns: Grid.Column<GridSpec>[] = [16 { id: "Company", widthFlex: 2 },17 { id: "Country", widthFlex: 2 },18 { id: "Founded", type: "number" },19 { id: "Employee Cnt", name: "Employees", type: "number", cellRenderer: NumberCell },20 { id: "Price", type: "number", cellRenderer: PriceCell },21];22
23const rowDetailExpansions = new Set(["leaf-0"]);24const rowDetailRenderer: Grid.Props<GridSpec>["rowDetailRenderer"] = (p) => {25 if (!p.api.rowIsLeaf(p.row)) return;26 return (27 <div28 style={{29 width: "100%",30 height: "100%",31 boxSizing: "border-box",32 padding: "20px 20px 20px 0px",33 }}34 >35 <PriceChart row={p.row} />36 </div>37 );38};39const base: Grid.ColumnBase<GridSpec> = { width: 100, widthFlex: 1 };40
41const Cell = styled(Grid.Cell)`42 display: flex;43 align-items: center;44 padding-inline: 8px;45 background-color: light-dark(white, hsla(190, 32%, 6%, 1));46 color: light-dark(hsla(175, 6%, 38%, 1), hsla(175, 10%, 86%, 1));47 font-size: 14px;48 border-bottom: 1px solid light-dark(hsla(175, 20%, 95%, 1), hsla(177, 19%, 17%, 1));49`;50
51const HeaderCell = styled(Grid.HeaderCell)`52 display: flex;53 align-items: center;54 padding-inline: 8px;55 background-color: light-dark(hsla(175, 12%, 92%, 1), hsla(177, 19%, 17%, 1));56 color: light-dark(hsla(177, 19%, 17%, 1), hsla(175, 12%, 92%, 1));57 text-transform: capitalize;58 font-size: 14px;59`;60
61const Row = styled(Grid.Row)`62 &[data-ln-alternate="true"] [data-ln-cell="true"] {63 background-color: light-dark(hsl(0, 27%, 98%), hsl(184, 33%, 8%));64 }65`;66
67
68const RowsContainer = styled(Grid.RowsContainer)`69 & [data-ln-row-detail="true"] {70 padding: 24px;71 }72
73 & [data-ln-row-detail="true"] > div {74 border: 1px solid var(--lng1771-gray-30);75 border-radius: 8px;76 background-color: light-dark(white, transparent);77 }78`;79
80export default function ThemingDemo() {81 const ds = useClientDataSource({82 data: companiesWithPricePerf,83 });84
85 const cache = useMemo(() => {86 return createCache({ key: "x" });87 }, []);88
89 return (90 <CacheProvider value={cache}>91 <div style={{ height: 500 }}>92 <Grid93 columns={columns}94 columnBase={base}95 rowSource={ds}96 rowDetailRenderer={rowDetailRenderer}97 rowDetailExpansions={rowDetailExpansions}98 rowDetailHeight={300}99 >100 <Grid.Viewport>101 <Grid.Header>102 {(cells) => {103 return (104 <Grid.HeaderRow>105 {cells.map((cell) => {106 if (cell.kind === "group") return null;107
108 return <HeaderCell key={cell.id} cell={cell} />;109 })}110 </Grid.HeaderRow>111 );112 }}113 </Grid.Header>114 <RowsContainer>115 <Grid.RowsCenter>116 {(row) => {117 if (row.kind === "full-width") return null;118
119 return (120 <Row row={row}>121 {row.cells.map((cell) => {122 return <Cell cell={cell} key={cell.id} />;123 })}124 </Row>125 );126 }}127 </Grid.RowsCenter>128 </RowsContainer>129 </Grid.Viewport>130 </Grid>131 </div>132 </CacheProvider>133 );134}76 collapsed lines
135
136function PriceChart({ row }: { row: Grid.T.RowLeaf<GridSpec["data"]> }) {137 const data = useMemo(() => {138 if (!row.data) return [];139 const weeks: Record<string, { week: number; [key: string]: number }> = Object.fromEntries(140 Array.from({ length: 52 }, (_, i) => [i + 1, { week: i + 1 }]),141 );142
143 const data = row.data["1 Year Perf"];144
145 data.forEach((dp, i) => {146 weeks[i + 1][row.id] = dp;147 });148 return Object.values(weeks).sort((l, r) => l.week - r.week);149 }, [row.data, row.id]);150
151 return (152 <ResponsiveContainer height="100%" width="100%">153 <AreaChart data={data}>154 <defs>155 <linearGradient key={row.id} id={row.id} x1="0" y1="0" x2="0" y2="1">156 <stop offset="5%" stopColor={color.stop5} stopOpacity={0.8} />157 <stop offset="95%" stopColor={color.stop95} stopOpacity={0} />158 </linearGradient>159 </defs>160 <XAxis161 dataKey="week"162 ticks={[5, 10, 15, 20, 25, 30, 35, 40, 45, 50]}163 fontSize="14px"164 tickLine={false}165 />166 <YAxis fontFamily="Inter" fontSize="14px" tickLine={false} axisLine={false} />167 <CartesianGrid vertical={false} stroke="var(--ln-border)" />168
169 <Area170 key={row.id}171 type="monotone"172 dataKey={row.id}173 stroke={color.solid}174 strokeWidth={3}175 fillOpacity={1}176 fill={`url(#${row.id})`}177 />178 </AreaChart>179 </ResponsiveContainer>180 );181}182const color = {183 name: "Ruby Red",184 solid: "#CC5500",185 stop5: "#CC5500",186 stop95: "transparent",187};188
189const formatter = new Intl.NumberFormat("en-US", {190 maximumFractionDigits: 2,191 minimumFractionDigits: 0,192});193
194export function PriceCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {195 const field = api.columnField(column, row);196
197 if (typeof field === "number") {198 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;199
200 return "$" + formatter.format(field);201 }202
203 return `${field ?? "-"}`;204}205
206export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {207 const field = api.columnField(column, row);208
209 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;210}Here is the styled wrapper used to target the row-detail container:
1const RowsContainer = styled(Grid.RowsContainer)`2 & [data-ln-row-detail="true"] {3 padding: 24px;4 }5
6 & [data-ln-row-detail="true"] > div {7 border: 1px solid gray;8 border-radius: 8px;9 background-color: light-dark(white, transparent);10 }11`;Styling Cell Selection Ranges
Cell-selection rectangles can also be styled using a nested data-attribute selector within a styled wrapper. The example below demonstrates this:
Cell Selection Styling With Emotion
64 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import styled from "@emotion/styled";5import { CacheProvider } from "@emotion/react";6import { useMemo, useState } from "react";7import createCache from "@emotion/cache";8
9export type BankData = (typeof bankDataSmall)[number];10interface GridSpec {11 readonly data: BankData;12}13
14const columns: Grid.Column<GridSpec>[] = [15 { name: "Job", id: "job", width: 120 },16 { name: "Age", id: "age", type: "number", width: 80, cellRenderer: NumberCell },17 { name: "Balance", id: "balance", type: "number", cellRenderer: BalanceCell },18 { name: "Education", id: "education" },19 { name: "Marital", id: "marital" },20 { name: "Default", id: "default" },21 { name: "Housing", id: "housing" },22 { name: "Loan", id: "loan" },23 { name: "Contact", id: "contact" },24 { name: "Day", id: "day", type: "number", cellRenderer: NumberCell },25 { name: "Month", id: "month" },26 { name: "Duration", id: "duration", type: "number", cellRenderer: DurationCell },27];28
29const base: Grid.ColumnBase<GridSpec> = { width: 100 };30
31const Cell = styled(Grid.Cell)`32 display: flex;33 align-items: center;34 padding-inline: 8px;35 background-color: light-dark(white, hsla(190, 32%, 6%, 1));36 color: light-dark(hsla(175, 6%, 38%, 1), hsla(175, 10%, 86%, 1));37 font-size: 14px;38 border-bottom: 1px solid light-dark(hsla(175, 20%, 95%, 1), hsla(177, 19%, 17%, 1));39
40 &[data-ln-type="number"] {41 justify-content: flex-end;42 }43`;44
45const HeaderCell = styled(Grid.HeaderCell)`46 display: flex;47 align-items: center;48 padding-inline: 8px;49 background-color: light-dark(hsla(175, 12%, 92%, 1), hsla(177, 19%, 17%, 1));50 color: light-dark(hsla(177, 19%, 17%, 1), hsla(175, 12%, 92%, 1));51 text-transform: capitalize;52 font-size: 14px;53
54 &[data-ln-type="number"] {55 justify-content: flex-end;56 }57`;58
59const Row = styled(Grid.Row)`60 &[data-ln-alternate="true"] [data-ln-cell="true"] {61 background-color: light-dark(hsl(0, 27%, 98%), hsl(184, 33%, 8%));62 }63`;64
65
66const RowsContainer = styled(Grid.RowsContainer)`67 [data-ln-cell-selection-rect] {68 background-color: rgba(0, 0, 255, 0.08);69 box-sizing: border-box;70
71 &[data-ln-cell-selection-border-top="true"] {72 border-top: 1px solid rgba(0, 0, 255, 1);73 }74 &[data-ln-cell-selection-border-bottom="true"] {75 border-bottom: 1px solid rgba(0, 0, 255, 1);76 }77 &[data-ln-cell-selection-border-start="true"] {78 border-inline-start: 1px solid rgba(0, 0, 255, 1);79 }80 &[data-ln-cell-selection-border-end="true"] {81 border-inline-end: 1px solid rgba(0, 0, 255, 1);82 }83 }84`;85
86export default function ThemingDemo() {87 const [selections, setSelections] = useState([{ rowStart: 4, rowEnd: 7, columnStart: 2, columnEnd: 4 }]);88 const ds = useClientDataSource({ data: bankDataSmall });89
90 const cache = useMemo(() => {91 return createCache({ key: "x" });92 }, []);93
94 return (95 <CacheProvider value={cache}>96 <div>97 <div style={{ height: 500 }}>98 <Grid99 rowSource={ds}100 columns={columns}101 columnBase={base}102 cellSelectionMode="range"103 cellSelections={selections}104 onCellSelectionChange={setSelections}105 >106 <Grid.Viewport>107 <Grid.Header>108 {(cells) => {109 return (110 <Grid.HeaderRow>111 {cells.map((cell) => {112 if (cell.kind === "group") return null;113
114 return <HeaderCell key={cell.id} cell={cell} />;115 })}116 </Grid.HeaderRow>117 );118 }}119 </Grid.Header>120 <RowsContainer>121 <Grid.RowsCenter>122 {(row) => {123 if (row.kind === "full-width") return null;124
125 return (126 <Row row={row}>127 {row.cells.map((cell) => {128 return <Cell cell={cell} key={cell.id} />;129 })}130 </Row>131 );132 }}133 </Grid.RowsCenter>134 </RowsContainer>135 </Grid.Viewport>136 </Grid>137 </div>138 </div>139 </CacheProvider>140 );141}142
27 collapsed lines
143
144const formatter = new Intl.NumberFormat("en-US", {145 maximumFractionDigits: 2,146 minimumFractionDigits: 0,147});148export function BalanceCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {149 const field = api.columnField(column, row);150
151 if (typeof field === "number") {152 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;153
154 return "$" + formatter.format(field);155 }156
157 return `${field ?? "-"}`;158}159export function DurationCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {160 const field = api.columnField(column, row);161
162 return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;163}164
165export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {166 const field = api.columnField(column, row);167
168 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;169}Here is the styled wrapper that targets the cell-selection rectangle:
1const RowsContainer = styled(Grid.RowsContainer)`2 & [data-ln-cell-selection-rect] {3 background-color: var(--lng1771-primary-30);4 box-sizing: border-box;5
6 &[data-ln-cell-selection-border-top="true"] {7 border-top: 1px solid var(--lng1771-primary-50);8 }9 &[data-ln-cell-selection-border-bottom="true"] {10 border-bottom: 1px solid var(--lng1771-primary-50);11 }12 &[data-ln-cell-selection-border-start="true"] {13 border-inline-start: 1px solid var(--lng1771-primary-50);14 }15 &[data-ln-cell-selection-border-end="true"] {16 border-inline-end: 1px solid var(--lng1771-primary-50);17 }18 }19 & [data-ln-cell-selection-rect] {20 outline: 1px solid var(--lng1771-primary-50);21 outline-offset: -1px;22 }23`;Emotion And Pre-built Themes
LyteNyte Grid provides pre-styled themes implemented in plain CSS. These themes work with any CSS framework, including Emotion. You can include a pre-built theme in your application and layer Emotion-based styles on top. The example below demonstrates this:
Pre-built Themes With Emotion
34 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/grid-full.css";3import type { OrderData } from "@1771technologies/grid-sample-data/orders";4import { data } from "@1771technologies/grid-sample-data/orders";5import {6 AvatarCell,7 EmailCell,8 IdCell,9 PaymentMethodCell,10 PriceCell,11 ProductCell,12 PurchaseDateCell,13} from "./components.jsx";14import { useClientDataSource, Grid } from "@1771technologies/lytenyte-pro";15import { ThemePicker } from "./theme.jsx";16import { useMemo, useState } from "react";17import styled from "@emotion/styled";18import { CacheProvider } from "@emotion/react";19import createCache from "@emotion/cache";20import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";21
22export interface GridSpec {23 readonly data: OrderData;24}25
26const columns: Grid.Column<GridSpec>[] = [27 { id: "id", width: 60, widthMin: 60, cellRenderer: IdCell, name: "ID" },28 { id: "product", cellRenderer: ProductCell, width: 200, name: "Product" },29 { id: "price", type: "number", cellRenderer: PriceCell, width: 100, name: "Price" },30 { id: "customer", cellRenderer: AvatarCell, width: 180, name: "Customer" },31 { id: "purchaseDate", cellRenderer: PurchaseDateCell, name: "Purchase Date", width: 130 },32 { id: "paymentMethod", cellRenderer: PaymentMethodCell, name: "Payment Method", width: 150 },33 { id: "email", cellRenderer: EmailCell, width: 220, name: "Email" },34];35
36const Cell = styled(Grid.Cell)`37 display: flex;38 align-items: center;39 padding-inline: 8px;40 color: gray;41 font-size: 14px;42 border-bottom: 1px solid light-dark(rgb(197, 196, 196), rgb(56, 32, 32));43`;44
45const HeaderCell = styled(Grid.HeaderCell)`46 display: flex;47 align-items: center;48 padding-inline: 8px;49 text-transform: capitalize;50 font-size: 14px;51`;52
53export default function ThemingDemo() {54 const [selections, setSelections] = useState<Grid.T.DataRect[]>([55 { rowStart: 1, rowEnd: 3, columnStart: 1, columnEnd: 3 },56 ]);57 const ds = useClientDataSource({ data: data });58 const [theme, setTheme] = useState("ln-dark");59 const cache = useMemo(() => {60 return createCache({ key: "x" });61 }, []);62
63 return (64 <CacheProvider value={cache}>65 <div>66 <div className="bg-ln-gray-00 border-b-ln-border h-full w-full border-b py-2">67 <ThemePicker theme={theme} setTheme={setTheme} />68 </div>69 <div70 className={"ln-grid " + theme}71 style={{72 height: 500,73 colorScheme: theme.includes("light") || theme === "ln-cotton-candy" ? "light" : "dark",74 }}75 >76 <Grid77 rowHeight={50}78 columns={columns}79 rowSource={ds}80 slotShadows={ViewportShadows}81 cellSelections={selections}82 onCellSelectionChange={setSelections}83 cellSelectionMode="range"84 >85 <Grid.Viewport>86 <Grid.Header>87 {(cells) => {88 return (89 <Grid.HeaderRow>90 {cells.map((cell) => {91 if (cell.kind === "group") return null;92
93 return <HeaderCell key={cell.id} cell={cell} />;94 })}95 </Grid.HeaderRow>96 );97 }}98 </Grid.Header>99 <Grid.RowsContainer>100 <Grid.RowsCenter>101 {(row) => {102 if (row.kind === "full-width") return null;103
104 return (105 <Grid.Row row={row}>106 {row.cells.map((cell) => {107 return <Cell cell={cell} key={cell.id} />;108 })}109 </Grid.Row>110 );111 }}112 </Grid.RowsCenter>113 </Grid.RowsContainer>114 </Grid.Viewport>115 </Grid>116 </div>117 </div>118 </CacheProvider>119 );120}1import { format } from "date-fns";2import { type JSX, type ReactNode } from "react";3import type { Grid } from "@1771technologies/lytenyte-pro";4import type { GridSpec } from "./demo.jsx";5
6export function ProductCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {7 if (!api.rowIsLeaf(row) || !row.data) return;8
9 const url = row.data?.productThumbnail;10 const title = row.data.product;11 const desc = row.data.productDescription;12
13 return (14 <div className="flex h-full w-full items-center gap-2">15 <img className="border-ln-border-strong h-7 w-7 rounded-lg border" src={url} alt={title + desc} />16 <div className="text-ln-text-dark flex flex-col gap-0.5">17 <div className="font-semibold">{title}</div>18 <div className="text-ln-text-light text-xs">{desc}</div>19 </div>20 </div>21 );22}23
24export function AvatarCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {25 if (!api.rowIsLeaf(row) || !row.data) return;26
27 const url = row.data?.customerAvatar;28
29 const name = row.data.customer;30
31 return (32 <div className="flex h-full w-full items-center gap-2">33 <img className="border-ln-border-strong h-7 w-7 rounded-full border" src={url} alt={name} />34 <div className="text-ln-text-dark flex flex-col gap-0.5">35 <div>{name}</div>36 </div>37 </div>38 );39}40
41const formatter = new Intl.NumberFormat("en-Us", {42 minimumFractionDigits: 2,43 maximumFractionDigits: 2,44});45export function PriceCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {46 if (!api.rowIsLeaf(row) || !row.data) return;47
48 const price = formatter.format(row.data.price);49 const [dollars, cents] = price.split(".");50
51 return (52 <div className="flex h-full w-full items-center justify-end">53 <div className="flex items-baseline tabular-nums">54 <span className="text-ln-text font-semibold">${dollars}</span>.55 <span className="relative text-xs">{cents}</span>56 </div>57 </div>58 );59}60
61export function PurchaseDateCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {62 if (!api.rowIsLeaf(row) || !row.data) return;63
64 const formattedDate = format(row.data.purchaseDate, "dd MMM, yyyy");65
66 return <div className="flex h-full w-full items-center">{formattedDate}</div>;67}68export function IdCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {69 if (!api.rowIsLeaf(row) || !row.data) return;70
71 return <div className="text-xs tabular-nums">{row.data.id}</div>;72}73
74export function PaymentMethodCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {75 if (!api.rowIsLeaf(row) || !row.data) return;76
77 const cardNumber = row.data.cardNumber;78 const provider = row.data.paymentMethod;79
80 let Logo: ReactNode = null;81 if (provider === "Visa") Logo = <VisaLogo className="w-6" />;82 if (provider === "Mastercard") Logo = <MastercardLogo className="w-6" />;83
84 return (85 <div className="flex h-full w-full items-center gap-2">86 <div className="flex w-7 items-center justify-center">{Logo}</div>87 <div className="flex items-center gap-px">88 <div className="bg-ln-gray-40 size-2 rounded-full"></div>89 <div className="bg-ln-gray-40 size-2 rounded-full"></div>90 <div className="bg-ln-gray-40 size-2 rounded-full"></div>91 <div className="bg-ln-gray-40 size-2 rounded-full"></div>92 </div>93 <div className="tabular-nums">{cardNumber}</div>94 </div>95 );96}97
98export function EmailCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return;100
101 return <div className="text-ln-primary-50 flex h-full w-full items-center">{row.data.email}</div>;102}103
104const VisaLogo = (props: JSX.IntrinsicElements["svg"]) => (105 <svg xmlns="http://www.w3.org/2000/svg" width={2500} height={812} viewBox="0.5 0.5 999 323.684" {...props}>106 <path107 fill="#1434cb"108 d="M651.185.5c-70.933 0-134.322 36.766-134.322 104.694 0 77.9 112.423 83.28 112.423 122.415 0 16.478-18.884 31.229-51.137 31.229-45.773 0-79.984-20.611-79.984-20.611l-14.638 68.547s39.41 17.41 91.734 17.41c77.552 0 138.576-38.572 138.576-107.66 0-82.316-112.89-87.537-112.89-123.86 0-12.91 15.501-27.053 47.662-27.053 36.286 0 65.892 14.99 65.892 14.99l14.326-66.204S696.614.5 651.185.5zM2.218 5.497.5 15.49s29.842 5.461 56.719 16.356c34.606 12.492 37.072 19.765 42.9 42.353l63.51 244.832h85.138L379.927 5.497h-84.942L210.707 218.67l-34.39-180.696c-3.154-20.68-19.13-32.477-38.685-32.477H2.218zm411.865 0L347.449 319.03h80.999l66.4-313.534h-80.765zm451.759 0c-19.532 0-29.88 10.457-37.474 28.73L709.699 319.03h84.942l16.434-47.468h103.483l9.994 47.468H999.5L934.115 5.497h-68.273zm11.047 84.707 25.178 117.653h-67.454z"109 />110 </svg>111);112
113const MastercardLogo = (props: JSX.IntrinsicElements["svg"]) => (114 <svg115 xmlns="http://www.w3.org/2000/svg"116 width={2500}117 height={1524}118 viewBox="55.2 38.3 464.5 287.8"119 {...props}120 >121 <path122 fill="#f79f1a"123 d="M519.7 182.2c0 79.5-64.3 143.9-143.6 143.9s-143.6-64.4-143.6-143.9S296.7 38.3 376 38.3s143.7 64.4 143.7 143.9z"124 />125 <path126 fill="#ea001b"127 d="M342.4 182.2c0 79.5-64.3 143.9-143.6 143.9S55.2 261.7 55.2 182.2 119.5 38.3 198.8 38.3s143.6 64.4 143.6 143.9z"128 />129 <path130 fill="#ff5f01"131 d="M287.4 68.9c-33.5 26.3-55 67.3-55 113.3s21.5 87 55 113.3c33.5-26.3 55-67.3 55-113.3s-21.5-86.9-55-113.3z"132 />133 </svg>134);1import { ToggleGroup as TG } from "radix-ui";2import type { ClassValue } from "clsx";3import clsx from "clsx";4import { twMerge } from "tailwind-merge";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9
10export function ToggleGroup(props: Parameters<typeof TG.Root>[0]) {11 return (12 <TG.Root13 {...props}14 className={tw("bg-ln-gray-20 flex items-center gap-2 rounded-xl px-2 py-1", props.className)}15 ></TG.Root>16 );17}18
19export function ToggleItem(props: Parameters<typeof TG.Item>[0]) {20 return (21 <TG.Item22 {...props}23 className={tw(24 "text-ln-text flex cursor-pointer items-center justify-center px-2 py-1 text-xs font-bold outline-none focus:outline-none",25 "data-[state=on]:text-ln-text-dark data-[state=on]:bg-linear-to-b from-ln-gray-02 to-ln-gray-05 data-[state=on]:rounded-md",26 props.className,27 )}28 ></TG.Item>29 );30}31
32export function ThemePicker({ theme, setTheme }: { theme: string; setTheme: (s: string) => void }) {33 return (34 <div className={tw("flex h-full items-center gap-1 text-nowrap px-2 py-1")}>35 <div className={tw("text-light hidden text-xs font-medium md:block")}>Theme:</div>36 <ToggleGroup37 type="single"38 value={theme}39 className={tw("flex flex-wrap")}40 onValueChange={(c) => {41 if (!c) return;42 setTheme(c);43 }}44 >45 <ToggleItem value="ln-light">Light</ToggleItem>46 <ToggleItem value="ln-dark">Dark</ToggleItem>47 <ToggleItem value="ln-teal">LyteNyte Teal</ToggleItem>48 <ToggleItem value="ln-term">Term 256</ToggleItem>49 <ToggleItem value="ln-shadcn dark">Shadcn Dark</ToggleItem>50 <ToggleItem value="ln-shadcn light">Shadcn Light</ToggleItem>51 <ToggleItem value="ln-cotton-candy">Cotton Candy</ToggleItem>52 </ToggleGroup>53 </div>54 );55}Next Steps
- Grid Theming: Review the general theming guide to learn more about styling LyteNyte Grid.
- Columns Overview: Learn how column configuration influences the grid view.
- Cell Renderers: Render custom cell content with React components.
