Changes
18 changed files (+735/-70)
-
-
@@ -55,7 +55,6 @@ "name": "@yamori/react_ui","dependencies": { "@bufbuild/protobuf": "^2.2.2", "@date-fns/tz": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-toast": "^1.2.4",
-
@@ -485,7 +484,7 @@ "@yamori/proto": ["@yamori/proto@workspace:packages/proto", { "devDependencies": { "@bufbuild/protobuf": "^2.2.2", "@bufbuild/protoc-gen-es": "^2.2.2" }, "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }],"@yamori/pwa": ["@yamori/pwa@workspace:packages/pwa", { "dependencies": { "@yamori/idb_backend": "packages/idb_backend", "@yamori/react_ui": "packages/react_ui", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@types/bun": "^1.1.14", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "rollup-plugin-license": "^3.5.3", "typescript": "^5.7.2", "vite": "^6.0.2", "zod": "^3.24.1" } }], "@yamori/react_ui": ["@yamori/react_ui@workspace:packages/react_ui", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@date-fns/tz": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/themes": "^3.1.6", "@tanstack/react-query": "^5.62.8", "@yamori/idb_backend": "packages/idb_backend", "@yamori/proto": "packages/proto", "date-fns": "^4.1.0", "react-hook-form": "^7.54.2", "urlpattern-polyfill": "^10.0.0" }, "devDependencies": { "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/react": "^8.4.7", "@storybook/react-vite": "^8.4.7", "@storybook/test": "^8.4.7", "@types/react": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "react": "19.x.x", "react-dom": "^19.0.0", "typescript": "^5.7.2", "vite": "^6.0.2" }, "peerDependencies": { "react": "19.x.x", "react-dom": "19.x.x" } }], "@yamori/react_ui": ["@yamori/react_ui@workspace:packages/react_ui", { "dependencies": { "@bufbuild/protobuf": "^2.2.2", "@date-fns/tz": "^1.2.0", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/themes": "^3.1.6", "@tanstack/react-query": "^5.62.8", "@yamori/idb_backend": "packages/idb_backend", "@yamori/proto": "packages/proto", "date-fns": "^4.1.0", "react-hook-form": "^7.54.2", "urlpattern-polyfill": "^10.0.0" }, "devDependencies": { "@storybook/addon-essentials": "^8.4.7", "@storybook/addon-interactions": "^8.4.7", "@storybook/react": "^8.4.7", "@storybook/react-vite": "^8.4.7", "@storybook/test": "^8.4.7", "@types/react": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "react": "19.x.x", "react-dom": "^19.0.0", "typescript": "^5.7.2", "vite": "^6.0.2" }, "peerDependencies": { "react": "19.x.x", "react-dom": "19.x.x" } }], "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
-
-
-
@@ -65,7 +65,6 @@ },"dependencies": { "@bufbuild/protobuf": "^2.2.2", "@date-fns/tz": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-toast": "^1.2.4",
-
-
-
@@ -35,6 +35,7 @@ id: {value: "wr-foo", }, displayName: "日本 太郎", providePaidLeaveKey: { key: new Uint8Array([]) }, workRecords: [ { date: toProtoDate(subDays(Date.now(), 1)),
-
@@ -50,6 +51,7 @@ id: {value: "wr-bar", }, displayName: "行政 花子", writeWorkRecordKey: { key: new Uint8Array([]) }, workRecords: [ { date: toProtoDate(subDays(Date.now(), 2)),
-
@@ -99,6 +101,25 @@ date: toProtoDate(subDays(Date.now(), 5)),record: { case: "dayOff", value: {}, }, }, ], }, { id: { value: "wr-qux", }, displayName: "浦島 次郎", providePaidLeaveKey: { key: new Uint8Array([]) }, writeWorkRecordKey: { key: new Uint8Array([]) }, workRecords: [ { date: toProtoDate(subDays(Date.now(), 2)), record: { case: "workingDay", value: { hasWorkerWorked: true, }, }, }, ],
-
-
-
@@ -0,0 +1,142 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { DataList, Flex, Grid, Text } from "@radix-ui/themes"; import type { Meta, StoryObj } from "@storybook/react"; import { type FC, useState } from "react"; import * as Spreadsheet from "./Spreadsheet.ts"; const ROWS = 9; const COLUMNS = 9; const CellPosition: FC<{ row: number; column: number }> = ({ row, column }) => { const col = String.fromCharCode("A".charCodeAt(0) + column); return col + (row + 1); }; export default { component: Spreadsheet.Root, render() { const [selection, setSelection] = useState<Spreadsheet.Selection>(() => { return { cursor: null, expandingTo: null, committed: [], cells: [], }; }); return ( <Flex direction="column" gap="3"> <Spreadsheet.Root asChild selection={selection} rows={ROWS} columns={COLUMNS} onSelectionChange={setSelection} > <Grid columns="repeat(10, 2rem)"> <Spreadsheet.Row asChild row={-1}> <div style={{ display: "contents" }}> <Spreadsheet.ColumnHeader /> {Array.from({ length: COLUMNS }, (_, i) => { return ( <Spreadsheet.ColumnHeader key={i} asChild> <Text align="center" color="gray" weight="bold"> {String.fromCharCode("A".charCodeAt(0) + i)} </Text> </Spreadsheet.ColumnHeader> ); })} </div> </Spreadsheet.Row> {Array.from({ length: ROWS }, (_, row) => { return ( <Spreadsheet.Row key={row} row={row} asChild> <div style={{ display: "contents" }}> <Spreadsheet.RowHeader asChild> <Text align="center" color="gray" weight="bold"> {row + 1} </Text> </Spreadsheet.RowHeader> {Array.from({ length: COLUMNS }, (_, column) => { return ( <Spreadsheet.Cell key={column} column={column} asChild> <Text align="center" color={ selection.cursor && selection.cursor.row === row && selection.cursor.column === column ? "red" : selection.cells.some( (value) => value.row === row && value.column === column, ) ? "pink" : undefined } > <CellPosition row={row} column={column} /> </Text> </Spreadsheet.Cell> ); })} </div> </Spreadsheet.Row> ); })} </Grid> </Spreadsheet.Root> <DataList.Root> <DataList.Item> <DataList.Label>Cursor</DataList.Label> <DataList.Value> {selection.cursor ? <CellPosition {...selection.cursor} /> : "---"} </DataList.Value> </DataList.Item> <DataList.Item> <DataList.Label>Expanding to</DataList.Label> <DataList.Value> {selection.expandingTo ? ( <CellPosition {...selection.expandingTo} /> ) : ( "---" )} </DataList.Value> </DataList.Item> <DataList.Item> <DataList.Label>Committed</DataList.Label> <DataList.Value> <Flex wrap="wrap" gap="2"> {selection.committed.map((cell) => ( <Text key={cell.row + "-" + cell.column}> <CellPosition {...cell} /> </Text> ))} </Flex> </DataList.Value> </DataList.Item> <DataList.Item> <DataList.Label>Final selected cells</DataList.Label> <DataList.Value> <Flex wrap="wrap" gap="2"> {selection.cells.map((cell) => ( <Text key={cell.row + "-" + cell.column}> <CellPosition {...cell} /> </Text> ))} </Flex> </DataList.Value> </DataList.Item> </DataList.Root> </Flex> ); }, } satisfies Meta<(typeof Spreadsheet)["Root"]>; type Story = StoryObj<(typeof Spreadsheet)["Root"]>; export const Demo: Story = {};
-
-
-
@@ -0,0 +1,10 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export * from "./Spreadsheet/Root.tsx"; export * from "./Spreadsheet/Cell.tsx"; export * from "./Spreadsheet/Row.tsx"; export * from "./Spreadsheet/RowHeader.tsx"; export * from "./Spreadsheet/ColumnHeader.tsx"; export { empty } from "./Spreadsheet/helpers.ts"; export type { GridCell, Selection } from "./Spreadsheet/types.ts";
-
-
-
@@ -0,0 +1,186 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Slot } from "@radix-ui/themes"; import { type FC, type ReactNode, use, useCallback } from "react"; import { GridSizeContext } from "./GridSizeContext.ts"; import { RowContext } from "./RowContext.ts"; import { UpdateSelectionContext } from "./UpdateSelectionContext.ts"; import { SelectionContext } from "./SelectionContext.ts"; import { getCellsForRange } from "./helpers.ts"; import type { GridCell, GridSize } from "./types.ts"; function moveCellByKeyboard( { row, column }: GridCell, size: GridSize, event: KeyboardEvent, ): GridCell { const prevColumn = column === 0 ? size.columns - 1 : column - 1; const nextColumn = column === size.columns - 1 ? 0 : column + 1; const isJumpKeyPressed = event.ctrlKey || event.metaKey; switch (event.key) { case "Tab": return { row, column: event.shiftKey ? prevColumn : nextColumn, }; case "ArrowLeft": return { row, column: isJumpKeyPressed ? 0 : prevColumn, }; case "ArrowRight": return { row, column: isJumpKeyPressed ? size.columns - 1 : nextColumn, }; case "ArrowDown": return { row: isJumpKeyPressed ? size.rows - 1 : row === size.rows - 1 ? 0 : row + 1, column, }; case "ArrowUp": return { row: isJumpKeyPressed ? 0 : row === 0 ? size.rows - 1 : row - 1, column, }; default: return { row, column }; } } export interface CellProps { className?: string; children: ReactNode; asChild?: boolean; column: number; } export const Cell: FC<CellProps> = ({ asChild, column, ...props }) => { const updater = use(UpdateSelectionContext); const row = use(RowContext); const size = use(GridSizeContext); const selection = use(SelectionContext); const Component = asChild ? Slot : "div"; const moveTo = useCallback( (row: number, column: number) => { updater(() => { return { cursor: { row, column }, expandingTo: null, committed: [], }; }); }, [updater], ); return ( <Component role="gridcell" tabIndex={0} aria-selected={selection.cells.some( (cell) => cell.row === row && cell.column === column, )} data-selection-start={ selection.cursor && selection.cursor.row === row && selection.cursor.column === column ? "" : undefined } data-gridrow={row} data-gridcol={column} style={{ userSelect: "none" }} onClick={(event) => { event.preventDefault(); if (event.shiftKey) { updater((prev) => { return { ...prev, expandingTo: { row, column }, }; }); return; } if (event.ctrlKey || event.metaKey) { updater((prev) => { if (!prev.cursor) { return { ...prev, cursor: { row, column }, expandingTo: null, }; } return { cursor: { row, column }, expandingTo: null, committed: [ ...prev.committed, ...getCellsForRange(prev.cursor, prev.expandingTo || prev.cursor), ], }; }); return; } moveTo(row, column); }} onKeyDown={(event) => { switch (event.key) { case "Tab": case "ArrowDown": case "ArrowUp": case "ArrowRight": case "ArrowLeft": { event.preventDefault(); let next: GridCell; if (event.key !== "Tab" && event.shiftKey) { next = moveCellByKeyboard( selection.expandingTo || { row, column }, size, event.nativeEvent, ); updater((prev) => { return { ...prev, expandingTo: next, }; }); } else { next = moveCellByKeyboard( selection.cursor || { row, column }, size, event.nativeEvent, ); moveTo(next.row, next.column); } const nextEl = document.querySelector( `[data-gridrow="${next.row}"][data-gridcol="${next.column}"]`, ); if (nextEl instanceof HTMLElement) { nextEl.focus(); } return; } } }} {...props} /> ); };
-
-
-
@@ -0,0 +1,19 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Slot } from "@radix-ui/themes"; import { type FC, type ReactNode } from "react"; export interface ColumnHeaderProps { className?: string; children?: ReactNode; asChild?: boolean; } export const ColumnHeader: FC<ColumnHeaderProps> = ({ asChild, ...props }) => { const Component = asChild ? Slot : "div"; return <Component role="columnheader" style={{ userSelect: "none" }} {...props} />; };
-
-
-
@@ -0,0 +1,11 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createContext } from "react"; import type { GridSize } from "./types.ts"; export const GridSizeContext = createContext<GridSize>({ rows: 0, columns: 0, });
-
-
-
@@ -0,0 +1,64 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Slot } from "@radix-ui/themes"; import { type FC, type ReactNode, useCallback, useMemo } from "react"; import { UpdateSelection, UpdateSelectionContext } from "./UpdateSelectionContext.ts"; import { GridSizeContext } from "./GridSizeContext.ts"; import { SelectionContext } from "./SelectionContext.ts"; import { getSelectedCells } from "./helpers.ts"; import type { GridSize, Selection } from "./types.ts"; export interface RootProps extends GridSize { children: ReactNode; asChild?: boolean; selection: Selection; onSelectionChange?(selection: Selection): void; } export const Root: FC<RootProps> = ({ asChild, children, rows, columns, selection, onSelectionChange, }) => { const Component = asChild ? Slot : "div"; const updater = useCallback<UpdateSelection>( (callback) => { if (!onSelectionChange) { return; } const next = callback(selection); onSelectionChange?.({ ...next, cells: getSelectedCells(next), }); }, [selection, onSelectionChange], ); const size = useMemo<GridSize>(() => { return { rows, columns }; }, [rows, columns]); return ( <SelectionContext.Provider value={selection}> <GridSizeContext.Provider value={size}> <UpdateSelectionContext.Provider value={updater}> <Component role="grid" aria-multiselectable> {children} </Component> </UpdateSelectionContext.Provider> </GridSizeContext.Provider> </SelectionContext.Provider> ); };
-
-
-
@@ -0,0 +1,27 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Slot } from "@radix-ui/themes"; import { type FC, type ReactNode } from "react"; import { RowContext } from "./RowContext.ts"; export interface RowProps { className?: string; children: ReactNode; asChild?: boolean; row: number; } export const Row: FC<RowProps> = ({ asChild, row, ...props }) => { const Component = asChild ? Slot : "div"; return ( <RowContext.Provider value={row}> <Component role="row" {...props} /> </RowContext.Provider> ); };
-
-
-
@@ -0,0 +1,6 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createContext } from "react"; export const RowContext = createContext<number>(NaN);
-
-
-
@@ -0,0 +1,19 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { Slot } from "@radix-ui/themes"; import { type FC, type ReactNode } from "react"; export interface RowHeaderProps { className?: string; children: ReactNode; asChild?: boolean; } export const RowHeader: FC<RowHeaderProps> = ({ asChild, ...props }) => { const Component = asChild ? Slot : "div"; return <Component role="rowheader" style={{ userSelect: "none" }} {...props} />; };
-
-
-
@@ -0,0 +1,13 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createContext } from "react"; import type { Selection } from "./types"; export const SelectionContext = createContext<Selection>({ cursor: null, expandingTo: null, committed: [], cells: [], });
-
-
-
@@ -0,0 +1,12 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { createContext } from "react"; import type { Selection } from "./types.ts"; export type UpdateSelection = ( updater: (prev: Selection) => Omit<Selection, "cells">, ) => void; export const UpdateSelectionContext = createContext<UpdateSelection>(() => void 0);
-
-
-
@@ -0,0 +1,49 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import type { GridCell, Selection } from "./types.ts"; export function getCellsForRange(from: GridCell, to: GridCell): readonly GridCell[] { const rowStart = Math.min(from.row, to.row); const rowEnd = Math.max(from.row, to.row); const colStart = Math.min(from.column, to.column); const colEnd = Math.max(from.column, to.column); const rows = rowEnd - rowStart + 1; const cols = colEnd - colStart + 1; return Array.from({ length: rows }, (_r, rowOffset) => { return Array.from({ length: cols }, (_c, colOffset) => { return { row: rowStart + rowOffset, column: colStart + colOffset, }; }); }).flat(); } export function getSelectedCells({ cursor, expandingTo, committed, }: Omit<Selection, "cells">): readonly GridCell[] { if (!cursor) { return committed; } const range = expandingTo ? getCellsForRange(cursor, expandingTo) : [cursor]; return [...range, ...committed].filter( (cell, i, cells) => cells.findIndex((c) => c.row === cell.row && c.column === cell.column) === i, ); } export function empty(): Selection { return { cursor: null, expandingTo: null, committed: [], cells: [], }; }
-
-
-
@@ -0,0 +1,19 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only export interface GridCell { row: number; column: number; } export interface GridSize { rows: number; columns: number; } export interface Selection { cursor: GridCell | null; expandingTo: GridCell | null; committed: readonly GridCell[]; cells: readonly GridCell[]; }
-
-
-
@@ -3,15 +3,59 @@ * SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>* SPDX-License-Identifier: AGPL-3.0-only */ .row { display: contents; } .rowHeaderCell { border-bottom: 1px solid var(--gray-a3); } .columnHeaderCell { border-right: 1px solid var(--gray-a3); } .writableCell { border: none; background-color: transparent; outline: none; } .recordCell { border-bottom: 1px solid var(--gray-a2); border-right: 1px solid var(--gray-a2); } .writableCell { position: relative; } .writableCell::after { content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; border-radius: var(--radius-1); outline-offset: -2px; outline-width: 2px; outline-style: solid; outline-color: transparent; pointer-events: none; } @media (any-hover: hover) { .writableCell:hover { background-color: var(--gray-a2); } } .writableCell[aria-selected="true"] { background-color: var(--accent-a2); } .writableCell[data-selection-start]::after { outline-color: var(--accent-7); }
-
-
-
@@ -31,7 +31,7 @@ startOfMonth,endOfMonth, eachDayOfInterval, } from "date-fns"; import { type FC, Fragment, use, useEffect, useMemo, useRef } from "react"; import { type FC, use, useEffect, useMemo, useRef, useState } from "react"; import { WorkRecordBadges } from "../../../components/WorkRecordBadges.tsx"; import { NavigationContext, URLContext } from "../../../contexts/Router.tsx";
-
@@ -40,6 +40,7 @@ import { IllegalMessageError } from "../../../errors/IllegalMessageError.ts";import { fromProtoDate, toProtoDate, isSameDate } from "../../../helpers.ts"; import { Layout } from "../Layout.tsx"; import * as Spreadsheet from "./Spreadsheet.ts"; import css from "./page.module.css"; export const Title: FC = () => "カレンダー";
-
@@ -166,6 +167,8 @@ },); }, [start, end]); const [selection, setSelection] = useState(() => Spreadsheet.empty()); const scrollAreaRef = useRef<HTMLDivElement>(null); useEffect(() => {
-
@@ -178,78 +181,100 @@ }, [month]);return ( <ScrollArea ref={scrollAreaRef} scrollbars="horizontal" size="2"> <Grid columns={`max-content repeat(${dates.length}, 5rem)`} pb="4"> <Box className={`${css.rowHeaderCell} ${css.columnHeaderCell}`} position="sticky" left="0" top="0" style={{ backgroundColor: "var(--color-background)", zIndex: 50 }} /> {dates.map((d) => ( <Flex key={+d} className={css.rowHeaderCell} position="sticky" top="0" align="baseline" justify="center" gap="1" py="3" px="4" style={{ zIndex: 10 }} > <Text size="3" weight="bold"> {d.getDate()} </Text> <Text size="1" weight="bold" color={d.getDay() === 0 ? "red" : d.getDay() === 6 ? "blue" : "gray"} > {weekdayFormatter.format(d)} </Text> </Flex> ))} {query.data?.map((worker) => { return ( <Fragment key={worker.id?.value}> <Flex className={css.columnHeaderCell} <Spreadsheet.Root asChild selection={selection} onSelectionChange={setSelection} rows={query.data?.length ?? 0} columns={dates.length} > <Grid columns={`max-content repeat(${dates.length}, 5rem)`} pb="4"> <Spreadsheet.Row className={css.row} row={-1}> <Spreadsheet.ColumnHeader asChild> <Box className={`${css.rowHeaderCell} ${css.columnHeaderCell}`} position="sticky" left="0" style={{ backgroundColor: "var(--color-background)", zIndex: 30 }} align="center" gap="2" px="2" py="4" top="0" style={{ backgroundColor: "var(--color-background)", zIndex: 50 }} /> </Spreadsheet.ColumnHeader> {dates.map((d) => ( <Spreadsheet.ColumnHeader key={+d} asChild> <Flex className={css.rowHeaderCell} position="sticky" top="0" align="baseline" justify="center" gap="1" py="3" px="4" style={{ zIndex: 10 }} > <Text size="3" weight="bold"> {d.getDate()} </Text> <Text size="1" weight="bold" color={d.getDay() === 0 ? "red" : d.getDay() === 6 ? "blue" : "gray"} > {weekdayFormatter.format(d)} </Text> </Flex> </Spreadsheet.ColumnHeader> ))} </Spreadsheet.Row> {query.data?.map((worker, workerIndex) => { return ( <Spreadsheet.Row key={worker.id?.value} className={css.row} row={workerIndex} > <Avatar size="2" fallback={worker.displayName[0] || <PersonIcon />} /> <Text>{worker.displayName}</Text> </Flex> {dates.map((date) => { const d = toProtoDate(date); const record = worker.workRecords.find( (record) => record.date && isSameDate(record.date, d), ); return ( <Spreadsheet.RowHeader asChild> <Flex key={+date} className={css.recordCell} direction="column" gap="1" p="1" minWidth="0" className={css.columnHeaderCell} position="sticky" left="0" style={{ backgroundColor: "var(--color-background)", zIndex: 30 }} align="center" gap="2" px="2" py="4" > {record && <WorkRecordBadges workRecord={record} />} <Avatar size="2" fallback={worker.displayName[0] || <PersonIcon />} /> <Text>{worker.displayName}</Text> </Flex> ); })} </Fragment> ); })} </Grid> </Spreadsheet.RowHeader> {dates.map((date, dateIndex) => { const d = toProtoDate(date); const record = worker.workRecords.find( (record) => record.date && isSameDate(record.date, d), ); const contents = record && <WorkRecordBadges workRecord={record} />; return ( <Spreadsheet.Cell key={+date} asChild column={dateIndex}> <Flex className={`${css.recordCell} ${css.writableCell}`} direction="column" gap="1" p="1" minWidth="0" > {contents} </Flex> </Spreadsheet.Cell> ); })} </Spreadsheet.Row> ); })} </Grid> </Spreadsheet.Root> </ScrollArea> ); };
-