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
65 collapsed lines
1import { companiesWithPricePerf } from "@1771technologies/grid-sample-data/companies-with-price-performance";2import { useMemo } from "react";3import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts";4import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";5import styled from "@emotion/styled";6import { CacheProvider } from "@emotion/react";7import createCache from "@emotion/cache";8
9type PerformanceData = (typeof companiesWithPricePerf)[number];10export interface GridSpec {11 readonly data: PerformanceData;12}13
14const columns: Grid.Column<GridSpec>[] = [15 { id: "Company", widthFlex: 2 },16 { id: "Country", widthFlex: 2 },17 { id: "Founded", type: "number" },18 { id: "Employee Cnt", name: "Employees", type: "number", cellRenderer: NumberCell },19 { id: "Price", type: "number", cellRenderer: PriceCell },20];21
22const rowDetailExpansions = new Set(["leaf-0"]);23const rowDetailRenderer: Grid.Props<GridSpec>["rowDetailRenderer"] = (p) => {24 if (!p.api.rowIsLeaf(p.row)) return;25 return (26 <div27 style={{28 width: "100%",29 height: "100%",30 boxSizing: "border-box",31 padding: "20px 20px 20px 0px",32 }}33 >34 <PriceChart row={p.row} />35 </div>36 );37};38const base: Grid.ColumnBase<GridSpec> = { width: 100, widthFlex: 1 };39
40const Cell = styled(Grid.Cell)`41 display: flex;42 align-items: center;43 padding-inline: 8px;44 background-color: light-dark(white, hsla(190, 32%, 6%, 1));45 color: light-dark(hsla(175, 6%, 38%, 1), hsla(175, 10%, 86%, 1));46 font-size: 14px;47 border-bottom: 1px solid light-dark(hsla(175, 20%, 95%, 1), hsla(177, 19%, 17%, 1));48`;49
50const HeaderCell = styled(Grid.HeaderCell)`51 display: flex;52 align-items: center;53 padding-inline: 8px;54 background-color: light-dark(hsla(175, 12%, 92%, 1), hsla(177, 19%, 17%, 1));55 color: light-dark(hsla(177, 19%, 17%, 1), hsla(175, 12%, 92%, 1));56 text-transform: capitalize;57 font-size: 14px;58`;59
60const Row = styled(Grid.Row)`61 &[data-ln-alternate="true"] [data-ln-cell="true"] {62 background-color: light-dark(hsl(0, 27%, 98%), hsl(184, 33%, 8%));63 }64`;65
66
67const RowsContainer = styled(Grid.RowsContainer)`68 & [data-ln-row-detail="true"] {69 padding: 24px;70 }71
72 & [data-ln-row-detail="true"] > div {73 border: 1px solid var(--lng1771-gray-30);74 border-radius: 8px;75 background-color: light-dark(white, transparent);76 }77`;78
79export default function ThemingDemo() {80 const ds = useClientDataSource({81 data: companiesWithPricePerf,82 });83
84 const cache = useMemo(() => {85 return createCache({ key: "x" });86 }, []);87
88 return (89 <CacheProvider value={cache}>90 <div style={{ height: 500 }}>91 <Grid92 columns={columns}93 columnBase={base}94 rowSource={ds}95 rowDetailRenderer={rowDetailRenderer}96 rowDetailExpansions={rowDetailExpansions}97 rowDetailHeight={300}98 >99 <Grid.Viewport>100 <Grid.Header>101 {(cells) => {102 return (103 <Grid.HeaderRow>104 {cells.map((cell) => {105 if (cell.kind === "group") return null;106
107 return <HeaderCell key={cell.id} cell={cell} />;108 })}109 </Grid.HeaderRow>110 );111 }}112 </Grid.Header>113 <RowsContainer>114 <Grid.RowsCenter>115 {(row) => {116 if (row.kind === "full-width") return null;117
118 return (119 <Row row={row}>120 {row.cells.map((cell) => {121 return <Cell cell={cell} key={cell.id} />;122 })}123 </Row>124 );125 }}126 </Grid.RowsCenter>127 </RowsContainer>128 </Grid.Viewport>129 </Grid>130 </div>131 </CacheProvider>132 );133}76 collapsed lines
134
135function PriceChart({ row }: { row: Grid.T.RowLeaf<GridSpec["data"]> }) {136 const data = useMemo(() => {137 if (!row.data) return [];138 const weeks: Record<string, { week: number; [key: string]: number }> = Object.fromEntries(139 Array.from({ length: 52 }, (_, i) => [i + 1, { week: i + 1 }]),140 );141
142 const data = row.data["1 Year Perf"];143
144 data.forEach((dp, i) => {145 weeks[i + 1][row.id] = dp;146 });147 return Object.values(weeks).sort((l, r) => l.week - r.week);148 }, [row.data, row.id]);149
150 return (151 <ResponsiveContainer height="100%" width="100%">152 <AreaChart data={data}>153 <defs>154 <linearGradient key={row.id} id={row.id} x1="0" y1="0" x2="0" y2="1">155 <stop offset="5%" stopColor={color.stop5} stopOpacity={0.8} />156 <stop offset="95%" stopColor={color.stop95} stopOpacity={0} />157 </linearGradient>158 </defs>159 <XAxis160 dataKey="week"161 ticks={[5, 10, 15, 20, 25, 30, 35, 40, 45, 50]}162 fontSize="14px"163 tickLine={false}164 />165 <YAxis fontFamily="Inter" fontSize="14px" tickLine={false} axisLine={false} />166 <CartesianGrid vertical={false} stroke="var(--ln-border)" />167
168 <Area169 key={row.id}170 type="monotone"171 dataKey={row.id}172 stroke={color.solid}173 strokeWidth={3}174 fillOpacity={1}175 fill={`url(#${row.id})`}176 />177 </AreaChart>178 </ResponsiveContainer>179 );180}181const color = {182 name: "Ruby Red",183 solid: "#CC5500",184 stop5: "#CC5500",185 stop95: "transparent",186};187
188const formatter = new Intl.NumberFormat("en-US", {189 maximumFractionDigits: 2,190 minimumFractionDigits: 0,191});192
193export function PriceCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {194 const field = api.columnField(column, row);195
196 if (typeof field === "number") {197 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;198
199 return "$" + formatter.format(field);200 }201
202 return `${field ?? "-"}`;203}204
205export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {206 const field = api.columnField(column, row);207
208 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;209}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]:not([data-ln-cell-selection-is-unit="true"]) {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 [data-ln-cell-selection-rect][data-ln-cell-selection-is-unit="true"] {85 outline: 1px solid rgba(0, 0, 255, 1);86 outline-offset: -1px;87 }88`;89
90export default function ThemingDemo() {91 const [selections, setSelections] = useState([{ rowStart: 4, rowEnd: 7, columnStart: 2, columnEnd: 4 }]);92 const ds = useClientDataSource({ data: bankDataSmall });93
94 const cache = useMemo(() => {95 return createCache({ key: "x" });96 }, []);97
98 return (99 <CacheProvider value={cache}>100 <div>101 <div style={{ height: 500 }}>102 <Grid103 rowSource={ds}104 columns={columns}105 columnBase={base}106 cellSelectionMode="range"107 cellSelections={selections}108 onCellSelectionChange={setSelections}109 >110 <Grid.Viewport>111 <Grid.Header>112 {(cells) => {113 return (114 <Grid.HeaderRow>115 {cells.map((cell) => {116 if (cell.kind === "group") return null;117
118 return <HeaderCell key={cell.id} cell={cell} />;119 })}120 </Grid.HeaderRow>121 );122 }}123 </Grid.Header>124 <RowsContainer>125 <Grid.RowsCenter>126 {(row) => {127 if (row.kind === "full-width") return null;128
129 return (130 <Row row={row}>131 {row.cells.map((cell) => {132 return <Cell cell={cell} key={cell.id} />;133 })}134 </Row>135 );136 }}137 </Grid.RowsCenter>138 </RowsContainer>139 </Grid.Viewport>140 </Grid>141 </div>142 </div>143 </CacheProvider>144 );145}146
27 collapsed lines
147
148const formatter = new Intl.NumberFormat("en-US", {149 maximumFractionDigits: 2,150 minimumFractionDigits: 0,151});152export function BalanceCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {153 const field = api.columnField(column, row);154
155 if (typeof field === "number") {156 if (field < 0) return `-$${formatter.format(Math.abs(field))}`;157
158 return "$" + formatter.format(field);159 }160
161 return `${field ?? "-"}`;162}163export function DurationCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {164 const field = api.columnField(column, row);165
166 return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;167}168
169export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {170 const field = api.columnField(column, row);171
172 return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;173}Here is the styled wrapper that targets the cell-selection rectangle:
1const RowsContainer = styled(Grid.RowsContainer)`2 & [data-ln-cell-selection-rect]:not([data-ln-cell-selection-is-unit="true"]) {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][data-ln-cell-selection-is-unit="true"] {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.
