create-grist-widget 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +79 -7
  2. package/bin/create-grist-widget.mjs +485 -90
  3. package/examples/attachment-gallery/WidgetApp.tsx +202 -0
  4. package/examples/bulk-mark-done/WidgetApp.tsx +43 -0
  5. package/examples/declared-columns/WidgetApp.tsx +42 -0
  6. package/examples/form-edit/WidgetApp.tsx +225 -0
  7. package/examples/hello-world/WidgetApp.tsx +15 -0
  8. package/examples/mark-done/WidgetApp.tsx +44 -0
  9. package/examples/reads-demo/WidgetApp.tsx +69 -0
  10. package/examples/rest-fetch/WidgetApp.tsx +49 -0
  11. package/examples/safe-parse/WidgetApp.tsx +61 -0
  12. package/examples/schema-migration/WidgetApp.tsx +67 -0
  13. package/examples/schema-preview/WidgetApp.tsx +26 -0
  14. package/examples/slice-hooks/WidgetApp.tsx +47 -0
  15. package/examples/spreadsheet-grid/GristDataSheetGrid.tsx +376 -0
  16. package/examples/spreadsheet-grid/GristRdgGrid.tsx +189 -0
  17. package/examples/spreadsheet-grid/GristSchemaToolbar.tsx +293 -0
  18. package/examples/spreadsheet-grid/WidgetApp.tsx +133 -0
  19. package/examples/spreadsheet-grid/grist-rdg-choice-column.tsx +616 -0
  20. package/examples/spreadsheet-grid/grist-rdg-columns.ts +397 -0
  21. package/examples/task-board/WidgetApp.tsx +229 -0
  22. package/examples/theme-demo/WidgetApp.tsx +26 -0
  23. package/examples/widget-options/WidgetApp.tsx +34 -0
  24. package/examples/writes-demo/WidgetApp.tsx +75 -0
  25. package/manifest.json +304 -0
  26. package/package.json +14 -2
  27. package/src/git-gate.mjs +30 -0
  28. package/template/scripts/deploy.mjs +35 -1
  29. package/ui-source/ShadcnDataSheetGrid.css +117 -0
  30. package/ui-source/ShadcnDataSheetGrid.tsx +46 -0
  31. package/ui-source/badge.tsx +49 -0
  32. package/vendor/compose-example.mjs +252 -0
@@ -0,0 +1,189 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
+ import type { Column } from "react-datasheet-grid"
3
+
4
+ import {
5
+ ShadcnDataSheetGrid,
6
+ type DataSheetGridProps,
7
+ } from "@/components/ui/ShadcnDataSheetGrid"
8
+
9
+ type RowWithOptionalId = { id?: number } & Record<string, unknown>
10
+
11
+ export type GristGridOperation = {
12
+ type: "UPDATE" | "DELETE" | "CREATE"
13
+ fromRowIndex: number
14
+ toRowIndex: number
15
+ }
16
+
17
+ export type GristRdgGridColumn<RowT extends RowWithOptionalId> = {
18
+ id: string
19
+ column: Partial<Column<RowT, unknown, unknown>>
20
+ minWidth?: number
21
+ }
22
+
23
+ export type GristRdgGridProps<RowT extends RowWithOptionalId> = {
24
+ rows: RowT[]
25
+ columns: GristRdgGridColumn<RowT>[]
26
+ onChange: NonNullable<DataSheetGridProps<RowT>["onChange"]>
27
+ onActiveRowIdChange?: (rowId: number) => void
28
+ selectedRowId?: number | null
29
+ invalidCellKeys?: ReadonlySet<string>
30
+ height?: number
31
+ rowHeight?: number
32
+ autoAddRow?: boolean
33
+ lockRows?: boolean
34
+ className?: string
35
+ footer?: React.ReactNode
36
+ }
37
+
38
+ export function GristRdgGrid<RowT extends RowWithOptionalId>({
39
+ rows,
40
+ columns,
41
+ onChange,
42
+ onActiveRowIdChange,
43
+ selectedRowId,
44
+ invalidCellKeys,
45
+ height,
46
+ rowHeight = 32,
47
+ autoAddRow = false,
48
+ lockRows = false,
49
+ className,
50
+ footer,
51
+ }: GristRdgGridProps<RowT>) {
52
+ const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
53
+ const resizeStateRef = useRef<{
54
+ colId: string
55
+ startX: number
56
+ startWidth: number
57
+ } | null>(null)
58
+
59
+ const startColumnResize = useCallback(
60
+ (e: React.PointerEvent, colId: string, minWidth: number) => {
61
+ e.preventDefault()
62
+ e.stopPropagation()
63
+ const current = columnWidths[colId]
64
+ const startWidth = typeof current === "number" ? current : minWidth
65
+ resizeStateRef.current = {
66
+ colId,
67
+ startX: e.clientX,
68
+ startWidth,
69
+ }
70
+ },
71
+ [columnWidths]
72
+ )
73
+
74
+ useEffect(() => {
75
+ const onMove = (e: PointerEvent) => {
76
+ const state = resizeStateRef.current
77
+ if (!state) return
78
+ const next = Math.max(70, state.startWidth + (e.clientX - state.startX))
79
+ setColumnWidths((prev) => ({ ...prev, [state.colId]: next }))
80
+ }
81
+ const onUp = () => {
82
+ resizeStateRef.current = null
83
+ }
84
+ window.addEventListener("pointermove", onMove)
85
+ window.addEventListener("pointerup", onUp)
86
+ return () => {
87
+ window.removeEventListener("pointermove", onMove)
88
+ window.removeEventListener("pointerup", onUp)
89
+ }
90
+ }, [])
91
+
92
+ const resolvedColumns = useMemo<Partial<Column<RowT, unknown, unknown>>[]>(
93
+ () =>
94
+ columns.map(({ id: colId, column, minWidth: explicitMinWidth }) => {
95
+ const minWidth = explicitMinWidth ?? (colId === "id" ? 70 : 120)
96
+ const hasCustomWidth = typeof columnWidths[colId] === "number"
97
+ const basis = hasCustomWidth
98
+ ? (columnWidths[colId] as number)
99
+ : minWidth
100
+ const existingCellClassName = column.cellClassName
101
+ return {
102
+ ...column,
103
+ cellClassName: (opt: {
104
+ rowData: RowT
105
+ rowIndex: number
106
+ columnId?: string
107
+ }) => {
108
+ const baseClass =
109
+ typeof existingCellClassName === "function"
110
+ ? (
111
+ existingCellClassName as (
112
+ arg: typeof opt
113
+ ) => string | undefined
114
+ )(opt)
115
+ : (existingCellClassName ?? "")
116
+ const isInvalid =
117
+ typeof opt.rowData?.id === "number" &&
118
+ Boolean(invalidCellKeys?.has(`${opt.rowData.id}:${colId}`))
119
+ if (!isInvalid) return baseClass
120
+ return baseClass
121
+ ? `${baseClass} grist-invalid-number-cell`
122
+ : "grist-invalid-number-cell"
123
+ },
124
+ minWidth,
125
+ basis,
126
+ grow: hasCustomWidth ? 0 : 1,
127
+ shrink: hasCustomWidth ? 0 : 1,
128
+ title: (
129
+ <div className="relative flex h-full w-full items-center pr-3">
130
+ <span className="block w-full truncate pr-2">
131
+ {String(column.title ?? colId)}
132
+ </span>
133
+ <button
134
+ type="button"
135
+ aria-label={`Resize ${colId}`}
136
+ className="absolute inset-y-0 right-0 w-2 cursor-col-resize bg-border/40 hover:bg-border"
137
+ onPointerDown={(e) => startColumnResize(e, colId, minWidth)}
138
+ />
139
+ </div>
140
+ ),
141
+ } as Partial<Column<RowT, unknown, unknown>>
142
+ }),
143
+ [columns, columnWidths, invalidCellKeys, startColumnResize]
144
+ )
145
+
146
+ const handleActiveCellChange = useCallback(
147
+ (opts: unknown) => {
148
+ if (!onActiveRowIdChange) return
149
+ const cell =
150
+ typeof opts === "object" && opts !== null && "cell" in opts
151
+ ? (opts as { cell?: { rowId?: unknown; row?: unknown } | null }).cell
152
+ : (opts as { rowId?: unknown; row?: unknown } | null)
153
+ if (!cell) return
154
+ let rowId: number | null = null
155
+ if (typeof cell.rowId === "number") {
156
+ rowId = cell.rowId
157
+ } else if (typeof cell.row === "number") {
158
+ const row = rows[cell.row]
159
+ rowId = typeof row?.id === "number" ? row.id : null
160
+ }
161
+ if (rowId === null || rowId === selectedRowId) return
162
+ onActiveRowIdChange(rowId)
163
+ },
164
+ [onActiveRowIdChange, rows, selectedRowId]
165
+ )
166
+
167
+ return (
168
+ <div className="flex h-full min-h-0 flex-col gap-2">
169
+ <style>{`.grist-invalid-number-cell{background:rgba(239,68,68,0.12)!important;}`}</style>
170
+ <div className="min-h-0 flex-1 overflow-auto">
171
+ <ShadcnDataSheetGrid<RowT>
172
+ value={rows}
173
+ columns={resolvedColumns}
174
+ onChange={onChange}
175
+ onActiveCellChange={handleActiveCellChange as never}
176
+ rowKey="id"
177
+ height={height}
178
+ rowHeight={rowHeight}
179
+ autoAddRow={autoAddRow}
180
+ lockRows={lockRows}
181
+ className={className}
182
+ />
183
+ </div>
184
+ {footer ? (
185
+ <p className="shrink-0 text-xs text-muted-foreground">{footer}</p>
186
+ ) : null}
187
+ </div>
188
+ )
189
+ }
@@ -0,0 +1,293 @@
1
+ import { useState } from "react"
2
+ import { Plus, RefreshCw, X } from "lucide-react"
3
+
4
+ import {
5
+ gristModifyColumnAction,
6
+ gristRemoveColumnAction,
7
+ gristRenameColumnAction,
8
+ useGristContext,
9
+ useGristSchema,
10
+ type GristUserActionTuple,
11
+ type UseGristSchemaResult,
12
+ } from "grist-widget-sdk"
13
+
14
+ import { Button } from "@/components/ui/button"
15
+ import { Input } from "@/components/ui/input"
16
+
17
+ import {
18
+ GRIST_TOOLBAR_COLUMN_TYPES,
19
+ type GristToolbarColumnType,
20
+ } from "./grist-rdg-columns"
21
+
22
+ export type GristSchemaToolbarProps = {
23
+ /**
24
+ * Optional pre-fetched schema. Shared with `<GristDataSheetGrid>` so we
25
+ * don't issue duplicate `_grist_Tables_column` reads.
26
+ */
27
+ schema?: UseGristSchemaResult
28
+ /** Per-action toggles. All enabled by default. */
29
+ enable?: {
30
+ add?: boolean
31
+ rename?: boolean
32
+ retype?: boolean
33
+ remove?: boolean
34
+ }
35
+ className?: string
36
+ }
37
+
38
+ /**
39
+ * Renders inline controls for column add / rename / type-change / remove
40
+ * against the currently-selected Grist table.
41
+ *
42
+ * Writes use `useGrist().applyActions(...)` with the SDK's exported action
43
+ * builders. Schema is reloaded after each mutation so the chip list reflects
44
+ * the new state immediately.
45
+ */
46
+ export function GristSchemaToolbar({
47
+ schema: schemaProp,
48
+ enable,
49
+ className,
50
+ }: GristSchemaToolbarProps) {
51
+ const w = useGristContext()
52
+ const internalSchema = useGristSchema({
53
+ tableId: schemaProp ? undefined : (w.currentTableId ?? undefined),
54
+ requiredAccess: "full",
55
+ })
56
+ const schema = schemaProp ?? internalSchema
57
+
58
+ const [adding, setAdding] = useState(false)
59
+ const [newColId, setNewColId] = useState("")
60
+ const [newType, setNewType] = useState<GristToolbarColumnType>("Text")
61
+
62
+ const tableId = w.currentTableId
63
+ const can = {
64
+ add: enable?.add ?? true,
65
+ rename: enable?.rename ?? true,
66
+ retype: enable?.retype ?? true,
67
+ remove: enable?.remove ?? true,
68
+ }
69
+
70
+ if (!tableId) return null
71
+ if (!schema.table) {
72
+ return (
73
+ <p className={className ?? "text-xs text-muted-foreground"}>
74
+ Loading schema…
75
+ </p>
76
+ )
77
+ }
78
+
79
+ const cols = Object.entries(schema.table.columns ?? {})
80
+
81
+ const handleAdd = async () => {
82
+ const id = newColId.trim()
83
+ if (!id) return
84
+ try {
85
+ // `AddVisibleColumn` (instead of plain `AddColumn`) also adds the new
86
+ // column to the *active* section's view fields, so Grist users see it
87
+ // straight away in the table view they're working with. There's no
88
+ // dedicated builder for this in the SDK; we hand-write the tuple.
89
+ const action: GristUserActionTuple = [
90
+ "AddVisibleColumn",
91
+ tableId,
92
+ id,
93
+ { type: newType, label: id },
94
+ ]
95
+ await w.applyActions([action])
96
+ await schema.reload()
97
+ setAdding(false)
98
+ setNewColId("")
99
+ setNewType("Text")
100
+ } catch {
101
+ // Surfaced via <GristSdkAlerts>.
102
+ }
103
+ }
104
+
105
+ const handleRename = async (oldId: string, rawNewId: string) => {
106
+ const newId = rawNewId.trim()
107
+ if (!newId || newId === oldId) return
108
+ try {
109
+ await w.applyActions([gristRenameColumnAction(tableId, oldId, newId)])
110
+ await schema.reload()
111
+ } catch {
112
+ /* surfaced via <GristSdkAlerts> */
113
+ }
114
+ }
115
+
116
+ const handleType = async (colId: string, type: string) => {
117
+ try {
118
+ await w.applyActions([gristModifyColumnAction(tableId, colId, { type })])
119
+ await schema.reload()
120
+ } catch {
121
+ /* surfaced via <GristSdkAlerts> */
122
+ }
123
+ }
124
+
125
+ const handleDelete = async (colId: string) => {
126
+ if (!window.confirm(`Remove column "${colId}"?`)) return
127
+ try {
128
+ await w.applyActions([gristRemoveColumnAction(tableId, colId)])
129
+ await schema.reload()
130
+ } catch {
131
+ /* surfaced via <GristSdkAlerts> */
132
+ }
133
+ }
134
+
135
+ const isPending = w.actionStatus === "running"
136
+
137
+ return (
138
+ <div className={className ?? "flex flex-wrap items-center gap-1.5"}>
139
+ <span className="mr-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">
140
+ Columns
141
+ </span>
142
+ <Button
143
+ type="button"
144
+ variant="ghost"
145
+ size="icon-xs"
146
+ disabled={isPending || schema.loading}
147
+ onClick={() => void schema.reload()}
148
+ aria-label="Reload schema"
149
+ title="Reload schema"
150
+ >
151
+ <RefreshCw
152
+ className={schema.loading ? "size-3 animate-spin" : "size-3"}
153
+ />
154
+ </Button>
155
+
156
+ {cols.map(([colId, col]) => {
157
+ const currentType =
158
+ (typeof col.type === "string" ? col.type : "Text") || "Text"
159
+ return (
160
+ <div
161
+ key={colId}
162
+ className="inline-flex items-center gap-1 rounded-md border bg-background px-1.5 py-0.5 text-xs"
163
+ >
164
+ <Input
165
+ key={colId}
166
+ defaultValue={colId}
167
+ disabled={!can.rename || isPending}
168
+ className="h-6 w-24 px-1.5 text-xs"
169
+ onKeyDown={(e) => {
170
+ if (e.key === "Enter") {
171
+ e.currentTarget.blur()
172
+ } else if (e.key === "Escape") {
173
+ e.currentTarget.value = colId
174
+ e.currentTarget.blur()
175
+ }
176
+ }}
177
+ onBlur={(e) => {
178
+ if (!can.rename) return
179
+ void handleRename(colId, e.currentTarget.value)
180
+ }}
181
+ aria-label={`Rename ${colId}`}
182
+ />
183
+ <select
184
+ value={
185
+ GRIST_TOOLBAR_COLUMN_TYPES.includes(
186
+ currentType as GristToolbarColumnType
187
+ )
188
+ ? (currentType as GristToolbarColumnType)
189
+ : "Any"
190
+ }
191
+ disabled={!can.retype || isPending}
192
+ onChange={(e) => void handleType(colId, e.target.value)}
193
+ className="h-6 rounded border border-input bg-background px-1 text-xs"
194
+ aria-label={`Type of ${colId}`}
195
+ >
196
+ {GRIST_TOOLBAR_COLUMN_TYPES.map((t) => (
197
+ <option key={t} value={t}>
198
+ {t}
199
+ </option>
200
+ ))}
201
+ {/* Show the actual type if it's not in the toolbar's curated list (e.g. Ref:Foo). */}
202
+ {!GRIST_TOOLBAR_COLUMN_TYPES.includes(
203
+ currentType as GristToolbarColumnType
204
+ ) ? (
205
+ <option value={currentType}>{currentType}</option>
206
+ ) : null}
207
+ </select>
208
+ <Button
209
+ type="button"
210
+ variant="ghost"
211
+ size="icon-xs"
212
+ disabled={!can.remove || isPending}
213
+ onClick={() => void handleDelete(colId)}
214
+ aria-label={`Remove ${colId}`}
215
+ >
216
+ <X className="size-3" />
217
+ </Button>
218
+ </div>
219
+ )
220
+ })}
221
+
222
+ {can.add ? (
223
+ adding ? (
224
+ <div className="inline-flex items-center gap-1 rounded-md border bg-background px-1.5 py-0.5 text-xs">
225
+ <Input
226
+ autoFocus
227
+ placeholder="colId"
228
+ value={newColId}
229
+ disabled={isPending}
230
+ className="h-6 w-24 px-1.5 text-xs"
231
+ onChange={(e) => setNewColId(e.target.value)}
232
+ onKeyDown={(e) => {
233
+ if (e.key === "Enter") {
234
+ e.preventDefault()
235
+ void handleAdd()
236
+ } else if (e.key === "Escape") {
237
+ setAdding(false)
238
+ setNewColId("")
239
+ }
240
+ }}
241
+ />
242
+ <select
243
+ value={newType}
244
+ disabled={isPending}
245
+ onChange={(e) =>
246
+ setNewType(e.target.value as GristToolbarColumnType)
247
+ }
248
+ className="h-6 rounded border border-input bg-background px-1 text-xs"
249
+ aria-label="New column type"
250
+ >
251
+ {GRIST_TOOLBAR_COLUMN_TYPES.map((t) => (
252
+ <option key={t} value={t}>
253
+ {t}
254
+ </option>
255
+ ))}
256
+ </select>
257
+ <Button
258
+ type="button"
259
+ size="xs"
260
+ disabled={isPending || !newColId.trim()}
261
+ onClick={() => void handleAdd()}
262
+ >
263
+ Add
264
+ </Button>
265
+ <Button
266
+ type="button"
267
+ size="xs"
268
+ variant="ghost"
269
+ disabled={isPending}
270
+ onClick={() => {
271
+ setAdding(false)
272
+ setNewColId("")
273
+ }}
274
+ >
275
+ Cancel
276
+ </Button>
277
+ </div>
278
+ ) : (
279
+ <Button
280
+ type="button"
281
+ size="xs"
282
+ variant="outline"
283
+ onClick={() => setAdding(true)}
284
+ disabled={isPending}
285
+ >
286
+ <Plus className="size-3" />
287
+ Column
288
+ </Button>
289
+ )
290
+ ) : null}
291
+ </div>
292
+ )
293
+ }
@@ -0,0 +1,133 @@
1
+ import { useEffect, useRef, useState } from "react"
2
+
3
+ import {
4
+ useGristContext,
5
+ useGristSchema,
6
+ useWidgetMetadata,
7
+ type UseGristOptions,
8
+ } from "grist-widget-sdk"
9
+
10
+ import { Button } from "@/components/ui/button"
11
+
12
+ import { GristDataSheetGrid } from "./GristDataSheetGrid"
13
+ import { GristSchemaToolbar } from "./GristSchemaToolbar"
14
+
15
+ function downloadReplicaJson(label: string, doc: unknown) {
16
+ const safe = label.replace(/[^a-z0-9-]+/gi, "-").toLowerCase()
17
+ const blob = new Blob([JSON.stringify(doc, null, 2)], {
18
+ type: "application/json",
19
+ })
20
+ const url = URL.createObjectURL(blob)
21
+ const a = document.createElement("a")
22
+ a.href = url
23
+ a.download = `replica-${safe}.json`
24
+ a.click()
25
+ URL.revokeObjectURL(url)
26
+ }
27
+
28
+ export const GRIST_OPTIONS: UseGristOptions = {
29
+ /** Required so `table.update` / `applyUserActions` succeed. */
30
+ requiredAccess: "full",
31
+ /** Needed so `setCursorPos` / linked selection can drive host row focus. */
32
+ allowSelectBy: true,
33
+ }
34
+
35
+ export const WIDGET_METADATA = {
36
+ title: "Spreadsheet grid",
37
+ description:
38
+ "Selected Grist section rendered as a fully-synced, editable spreadsheet via react-datasheet-grid, plus a schema toolbar and replica-document export.",
39
+ } as const
40
+
41
+ export function WidgetApp() {
42
+ useWidgetMetadata(WIDGET_METADATA)
43
+
44
+ // Hoist the schema fetch so the grid + toolbar share it (single
45
+ // `_grist_Tables_column` round-trip per reload).
46
+ const w = useGristContext()
47
+ const schema = useGristSchema({
48
+ tableId: w.currentTableId ?? undefined,
49
+ requiredAccess: "full",
50
+ excludeManualSort: true,
51
+ })
52
+
53
+ const [exporting, setExporting] = useState<string | null>(null)
54
+ const [showSchemaToolbar, setShowSchemaToolbar] = useState(false)
55
+ const gridHostRef = useRef<HTMLDivElement | null>(null)
56
+ const [gridHeight, setGridHeight] = useState<number | undefined>(undefined)
57
+
58
+ useEffect(() => {
59
+ const host = gridHostRef.current
60
+ if (!host) return
61
+ const update = () => {
62
+ const next = Math.floor(host.getBoundingClientRect().height)
63
+ if (next > 0) setGridHeight(next)
64
+ }
65
+ update()
66
+ const observer = new ResizeObserver(update)
67
+ observer.observe(host)
68
+ return () => observer.disconnect()
69
+ }, [])
70
+
71
+ async function exportReplica(
72
+ key: string,
73
+ includeRows: false | "samples" | "full"
74
+ ) {
75
+ if (!w.isReady) return
76
+ setExporting(key)
77
+ try {
78
+ const doc = await w.buildReplicaDocumentFromDocApi({
79
+ includeRows,
80
+ source: "spreadsheet-grid-export",
81
+ })
82
+ downloadReplicaJson(key, doc)
83
+ } finally {
84
+ setExporting(null)
85
+ }
86
+ }
87
+
88
+ return (
89
+ <div className="flex h-svh min-h-0 flex-col gap-3 p-4">
90
+ <div className="flex flex-wrap items-center gap-2">
91
+ <Button
92
+ type="button"
93
+ variant="outline"
94
+ size="sm"
95
+ disabled={!w.isReady || exporting !== null}
96
+ onClick={() => void exportReplica("schema-only", false)}
97
+ >
98
+ {exporting === "schema-only" ? "…" : "Export schema"}
99
+ </Button>
100
+ <Button
101
+ type="button"
102
+ variant="outline"
103
+ size="sm"
104
+ disabled={!w.isReady || exporting !== null}
105
+ onClick={() => void exportReplica("schema-samples", "samples")}
106
+ >
107
+ {exporting === "schema-samples" ? "…" : "Export schema + samples"}
108
+ </Button>
109
+ <Button
110
+ type="button"
111
+ variant="outline"
112
+ size="sm"
113
+ disabled={!w.isReady || exporting !== null}
114
+ onClick={() => void exportReplica("schema-data", "full")}
115
+ >
116
+ {exporting === "schema-data" ? "…" : "Export schema + data"}
117
+ </Button>
118
+ <Button
119
+ type="button"
120
+ variant="outline"
121
+ size="sm"
122
+ onClick={() => setShowSchemaToolbar((v) => !v)}
123
+ >
124
+ {showSchemaToolbar ? "Hide schema toolbar" : "Show schema toolbar"}
125
+ </Button>
126
+ </div>
127
+ {showSchemaToolbar ? <GristSchemaToolbar schema={schema} /> : null}
128
+ <div ref={gridHostRef} className="min-h-0 flex-1 overflow-hidden">
129
+ <GristDataSheetGrid schema={schema} autoAddRow height={gridHeight} />
130
+ </div>
131
+ </div>
132
+ )
133
+ }