create-grist-widget 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arthur Blanchon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # create-grist-widget
2
+
3
+ Scaffold a new [Grist](https://www.getgrist.com/) custom widget, preconfigured with
4
+ [`grist-widget-sdk`](https://www.npmjs.com/package/grist-widget-sdk):
5
+
6
+ ```bash
7
+ npm create grist-widget my-widget
8
+ cd my-widget
9
+ pnpm install
10
+ pnpm dev
11
+ ```
12
+
13
+ The template ships embedded in this package — nothing is fetched from GitHub at
14
+ scaffold time. See [Getting started](https://grist-widgets.com/guide/getting-started)
15
+ for the full walkthrough (dev loop, deploying, embedding in Grist).
16
+
17
+ MIT licensed.
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readdirSync, cpSync, readFileSync, writeFileSync } from "node:fs"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "template")
7
+
8
+ const rawName = process.argv[2]
9
+ if (!rawName) {
10
+ console.error("Usage: npm create grist-widget <widget-name>")
11
+ process.exit(1)
12
+ }
13
+
14
+ // Mirrors npm's own unscoped package name rule, kept intentionally loose.
15
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(rawName)) {
16
+ console.error(`"${rawName}" is not a valid name — use lowercase letters, digits, and hyphens.`)
17
+ process.exit(1)
18
+ }
19
+
20
+ const target = path.resolve(process.cwd(), rawName)
21
+ if (existsSync(target) && readdirSync(target).length > 0) {
22
+ console.error(`"${rawName}" already exists and is not empty.`)
23
+ process.exit(1)
24
+ }
25
+ mkdirSync(target, { recursive: true })
26
+ cpSync(TEMPLATE_DIR, target, { recursive: true })
27
+
28
+ const title = rawName
29
+ .split("-")
30
+ .map((w) => w[0].toUpperCase() + w.slice(1))
31
+ .join(" ")
32
+
33
+ // Small, fixed set of files carry the template's own identity — swap it for the user's.
34
+ const edits = [
35
+ ["package.json", [["grist-widget-template-vite", rawName]]],
36
+ ["README.md", [["grist-widget-template-vite", rawName]]],
37
+ ["index.html", [["<title>Grist Widget</title>", `<title>${title}</title>`]]],
38
+ ["src/App.tsx", [['title: "Grist Widget Template",', `title: "${title}",`]]],
39
+ ]
40
+ for (const [file, subs] of edits) {
41
+ const p = path.join(target, file)
42
+ let content = readFileSync(p, "utf8")
43
+ for (const [search, replace] of subs) content = content.split(search).join(replace)
44
+ writeFileSync(p, content)
45
+ }
46
+
47
+ console.log(`Created ${rawName}/\n`)
48
+ console.log(` cd ${rawName}`)
49
+ console.log(" pnpm install # or npm/yarn")
50
+ console.log(" pnpm dev")
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "create-grist-widget",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "bin": {
11
+ "create-grist-widget": "./bin/create-grist-widget.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "template",
16
+ "LICENSE"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ArthurBlanchon/grist-widget-sdk.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/ArthurBlanchon/grist-widget-sdk/issues"
27
+ },
28
+ "homepage": "https://grist-widgets.com",
29
+ "keywords": [
30
+ "grist",
31
+ "widget",
32
+ "scaffold",
33
+ "cli",
34
+ "create-grist-widget"
35
+ ],
36
+ "scripts": {
37
+ "build": "node scripts/build-template.mjs"
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ coverage/
3
+ .pnpm-store/
4
+ pnpm-lock.yaml
5
+ package-lock.json
6
+ pnpm-lock.yaml
7
+ yarn.lock
@@ -0,0 +1,11 @@
1
+ {
2
+ "endOfLine": "lf",
3
+ "semi": false,
4
+ "singleQuote": false,
5
+ "tabWidth": 2,
6
+ "trailingComma": "es5",
7
+ "printWidth": 80,
8
+ "plugins": ["prettier-plugin-tailwindcss"],
9
+ "tailwindStylesheet": "src/index.css",
10
+ "tailwindFunctions": ["cn", "cva"]
11
+ }
@@ -0,0 +1,43 @@
1
+ # grist-widget-template-vite
2
+
3
+ Base template to build a Grist widget with `grist-widget-sdk`.
4
+
5
+ `src/main.tsx` wraps the app with `GristWidgetProvider`, `GristBoundary`, and `GristSdkAlerts`. The latter maps `getGristSdkAlertDescriptors()` from the SDK to shadcn `Alert` (`src/components/grist-sdk-alerts.tsx` + `src/components/ui/alert.tsx`); keep them in sync with the playground when you change alert styling.
6
+
7
+ `src/App.tsx` uses `useGrist<TaskRow, TaskMapped>()` for the selected row (`w.record`, `w.mode`) and remounts the UI with `key={rowKey}` when the row changes — same pattern as `widgets/create-email-draft`. See `src/grist-types.example.ts` for typing patterns.
8
+
9
+ - **ESLint** blocks direct `grist` global usage in `src/` — use the SDK only.
10
+ - Uncomment `GRIST_OPTIONS.columns` in `App.tsx` to enable column mapping; `main.tsx` sets `GristBoundary gate="canRender"` when columns are declared. Mapping alerts use `GristSdkAlerts`.
11
+
12
+ To add widget tests later, see [Testing](https://github.com/ArthurBlanchon/grist-widget-sdk/blob/main/apps/docs/guide/testing.md) (`renderWithGrist` from `grist-widget-sdk/emulator/testing`).
13
+
14
+ **Monorepo dev:** this template resolves `grist-widget-sdk` from `packages/core/dist` (like the other widgets), not from SDK source. After changing the SDK, run `pnpm prebuild` or `pnpm --filter grist-widget-sdk build` before `pnpm dev`.
15
+
16
+ ## How this template is distributed
17
+
18
+ This is the **source** template. It is consumed two ways:
19
+
20
+ - **In the SDK monorepo (development):** it's a workspace member and depends on
21
+ the SDK via `"grist-widget-sdk": "workspace:^"`, resolved from
22
+ `packages/core/dist`. Run `pnpm --filter grist-widget-sdk build` after
23
+ changing the SDK, then `pnpm dev` here.
24
+ - **Externally (your own repo):** the `create-grist-widget` CLI
25
+ (`npm create grist-widget my-widget`) copies this template and rewrites the
26
+ dependency to the published npm range (`^0.x`), so nothing points back at
27
+ this repo. See [Getting started](https://grist-widgets.com/guide/getting-started).
28
+
29
+ `pnpm-workspace.yaml` here only pre-approves esbuild's build script so a
30
+ standalone `pnpm install` (pnpm 11) exits cleanly; inside the monorepo it's
31
+ ignored (the root workspace governs).
32
+
33
+ ## Deployment
34
+
35
+ A bundled GitHub Actions workflow that publishes a scaffolded widget to
36
+ **your** GitHub Pages — immutable versioned URLs (`/v<version>/` +
37
+ `/latest/`) plus a push-to-`dev` preview channel, the same model the SDK's
38
+ own widgets use — is still on the roadmap (ROADMAP Phase 2).
39
+
40
+ > Until then, build with `pnpm build` and host the `dist/` folder on any
41
+ > static host (GitHub Pages, Cloudflare Pages, …). `create-grist-widget`
42
+ > already scaffolds this template via `npm create grist-widget`.
43
+
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-nova",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/index.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
@@ -0,0 +1,34 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ rules: {
23
+ 'react-refresh/only-export-components': 'off',
24
+ 'no-restricted-globals': [
25
+ 'error',
26
+ {
27
+ name: 'grist',
28
+ message:
29
+ 'Use grist-widget-sdk (GristWidgetProvider, useGrist) instead of the grist global in widget code.',
30
+ },
31
+ ],
32
+ },
33
+ },
34
+ ])
@@ -0,0 +1,39 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Grist Widget</title>
8
+ <style>
9
+ html,
10
+ body {
11
+ margin: 0;
12
+ height: 100%;
13
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
14
+ }
15
+ @media (prefers-color-scheme: dark) {
16
+ html,
17
+ body {
18
+ background-color: #0a0a0a;
19
+ color: #fafafa;
20
+ }
21
+ }
22
+ @media (prefers-color-scheme: light) {
23
+ html,
24
+ body {
25
+ background-color: #fafafa;
26
+ color: #1a1a1a;
27
+ }
28
+ }
29
+ #root {
30
+ min-height: 100%;
31
+ }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <div id="root"></div>
36
+ <script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
37
+ <script type="module" src="/src/main.tsx"></script>
38
+ </body>
39
+ </html>
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "grist-widget-template-vite",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "format": "prettier --write \"**/*.{ts,tsx}\"",
11
+ "typecheck": "tsc -b",
12
+ "preview": "vite preview"
13
+ },
14
+ "dependencies": {
15
+ "@fontsource-variable/geist": "^5.2.8",
16
+ "grist-widget-sdk": "^0.2.1",
17
+ "@tailwindcss/vite": "^4.2.1",
18
+ "class-variance-authority": "^0.7.1",
19
+ "clsx": "^2.1.1",
20
+ "lucide-react": "^1.8.0",
21
+ "radix-ui": "^1.4.3",
22
+ "react": "^19.2.4",
23
+ "react-dom": "^19.2.4",
24
+ "shadcn": "^4.4.0",
25
+ "tailwind-merge": "^3.5.0",
26
+ "tailwindcss": "^4.2.1",
27
+ "tslib": "^2.8.1",
28
+ "tw-animate-css": "^1.4.0"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^9.39.4",
32
+ "@types/node": "^24.12.0",
33
+ "@types/react": "^19.2.14",
34
+ "@types/react-dom": "^19.2.3",
35
+ "@vitejs/plugin-react": "^5.2.0",
36
+ "eslint": "^9.39.4",
37
+ "eslint-plugin-react-hooks": "^7.0.1",
38
+ "eslint-plugin-react-refresh": "^0.5.2",
39
+ "globals": "^16.5.0",
40
+ "prettier": "^3.8.1",
41
+ "prettier-plugin-tailwindcss": "^0.7.2",
42
+ "typescript": "~5.9.3",
43
+ "typescript-eslint": "^8.57.1",
44
+ "vite": "^7.3.1"
45
+ }
46
+ }
@@ -0,0 +1,8 @@
1
+ # pnpm 11 reads build-script approvals from here (not from package.json).
2
+ # esbuild (pulled in by Vite) ships a build script; without this, a standalone
3
+ # `pnpm install` exits non-zero asking you to approve it. esbuild works fine
4
+ # regardless (its binary comes from optional platform packages), so we approve
5
+ # it up front. Inside the SDK monorepo this file is ignored — the root
6
+ # pnpm-workspace.yaml governs there.
7
+ allowBuilds:
8
+ esbuild: true
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,101 @@
1
+ import { Button } from "@/components/ui/button"
2
+ import {
3
+ useGrist,
4
+ useWidgetMetadata,
5
+ type UseGristOptions,
6
+ type UseGristResult,
7
+ } from "grist-widget-sdk"
8
+
9
+ import type { TaskMapped, TaskRow } from "./grist-types.example"
10
+
11
+ export const GRIST_OPTIONS: UseGristOptions = {
12
+ requiredAccess: "read table",
13
+ // Uncomment to require column mapping in the widget config panel:
14
+ // columns: [{ name: "title", type: "Text" }, { name: "done", type: "Bool" }],
15
+ }
16
+
17
+ export const WIDGET_METADATA = {
18
+ title: "Grist Widget Template",
19
+ description:
20
+ "Starter template for building Grist widgets with React and Vite.",
21
+ } as const
22
+
23
+ function GristSelectionDebug({ w }: { w: UseGristResult }) {
24
+ return (
25
+ <details className="rounded-md border border-dashed border-muted-foreground/40 bg-muted/30 p-3 text-xs">
26
+ <summary className="cursor-pointer font-medium text-muted-foreground">
27
+ Grist selection debug
28
+ </summary>
29
+ <dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 font-mono">
30
+ <dt className="text-muted-foreground">status</dt>
31
+ <dd>{w.status}</dd>
32
+ <dt className="text-muted-foreground">mode</dt>
33
+ <dd>{w.mode}</dd>
34
+ <dt className="text-muted-foreground">isReady</dt>
35
+ <dd>{String(w.isReady)}</dd>
36
+ <dt className="text-muted-foreground">record.id</dt>
37
+ <dd>{w.record?.id != null ? String(w.record.id) : "—"}</dd>
38
+ </dl>
39
+ <pre className="mt-2 max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-background/80 p-2">
40
+ {w.record != null
41
+ ? JSON.stringify(w.record, null, 2)
42
+ : "null (no row selected)"}
43
+ </pre>
44
+ </details>
45
+ )
46
+ }
47
+
48
+ function TemplateBody({ w }: { w: UseGristResult }) {
49
+ if (w.mode === "empty") {
50
+ return (
51
+ <div className="flex flex-col gap-4 p-6 text-sm">
52
+ <p>Select a row in Grist to start.</p>
53
+ <GristSelectionDebug w={w} />
54
+ </div>
55
+ )
56
+ }
57
+
58
+ if (w.mode === "new-row") {
59
+ return (
60
+ <div className="flex flex-col gap-4 p-6 text-sm">
61
+ <p>Create the new row in Grist, then continue here.</p>
62
+ <GristSelectionDebug w={w} />
63
+ </div>
64
+ )
65
+ }
66
+
67
+ return (
68
+ <div className="flex min-h-svh p-6">
69
+ <div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
70
+ <div>
71
+ <h1 className="font-medium">Widget connected to Grist</h1>
72
+ <p>Record id: {String(w.record?.id ?? "")}</p>
73
+ <p>You can now render values from the selected record.</p>
74
+ <Button className="mt-2">Button</Button>
75
+ </div>
76
+ <GristSelectionDebug w={w} />
77
+ <div className="font-mono text-xs text-muted-foreground">
78
+ (Press <kbd>d</kbd> to toggle dark mode)
79
+ </div>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Remount the body when the selected row changes (same pattern as
87
+ * `widgets/create-email-draft`). Keeps local `useState` in sync with Grist.
88
+ */
89
+ export function App() {
90
+ useWidgetMetadata(WIDGET_METADATA)
91
+
92
+ const w = useGrist<TaskRow, TaskMapped>()
93
+ const rowKey =
94
+ w.record && typeof w.record.id === "number"
95
+ ? String(w.record.id)
96
+ : w.mode
97
+
98
+ return <TemplateBody key={rowKey} w={w} />
99
+ }
100
+
101
+ export default App
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
@@ -0,0 +1,80 @@
1
+ import { useMemo, type ReactNode } from "react"
2
+ import { AlertTriangle, Link2 } from "lucide-react"
3
+
4
+ import {
5
+ useGristSdkAlertDescriptors,
6
+ useGristOptional,
7
+ type GetGristSdkAlertDescriptorsOptions,
8
+ type GristSdkAlertSeverity,
9
+ type UseGristResult,
10
+ } from "grist-widget-sdk"
11
+
12
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
13
+ import { cn } from "@/lib/utils"
14
+
15
+ export type GristSdkAlertsProps = {
16
+ /** Defaults to the nearest `GristWidgetProvider`. */
17
+ widget?: UseGristResult
18
+ children: ReactNode
19
+ className?: string
20
+ }
21
+
22
+ const GRIST_ALERT_OPTIONS: GetGristSdkAlertDescriptorsOptions = {
23
+ sectionNotLinkedHint:
24
+ "In Grist, select this widget section on the page, then use Link to connect a table, card, or chart view as the selector.",
25
+ }
26
+
27
+ function alertSeverityClass(severity: GristSdkAlertSeverity): string | undefined {
28
+ switch (severity) {
29
+ case "warning":
30
+ return "border-amber-500/40 bg-amber-500/10 text-amber-950 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100"
31
+ case "info":
32
+ return "border-muted-foreground/30 bg-muted/50"
33
+ default:
34
+ return undefined
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Maps {@link getGristSdkAlertDescriptors} from `grist-widget-sdk` to
40
+ * shadcn {@link Alert} (styling follows this app’s `components.json`).
41
+ */
42
+ export function GristSdkAlerts({ widget, children, className }: GristSdkAlertsProps) {
43
+ const fromContext = useGristOptional()
44
+ const w = widget ?? fromContext
45
+
46
+ if (!w) {
47
+ throw new Error(
48
+ "GristSdkAlerts requires a `widget` prop or <GristWidgetProvider>"
49
+ )
50
+ }
51
+
52
+ const alertOptions = useMemo(() => GRIST_ALERT_OPTIONS, [])
53
+ const alerts = useGristSdkAlertDescriptors(w, alertOptions)
54
+
55
+ if (alerts.length === 0) {
56
+ return <>{children}</>
57
+ }
58
+
59
+ return (
60
+ <div className={cn("flex flex-col gap-3", className)}>
61
+ {alerts.map((a) => (
62
+ <Alert
63
+ key={a.id}
64
+ role={a.ariaRole}
65
+ variant={a.severity === "error" ? "destructive" : "default"}
66
+ className={alertSeverityClass(a.severity)}
67
+ >
68
+ {a.kind === "section-not-linked" || a.kind === "source-not-wired" ? (
69
+ <Link2 className="size-4" aria-hidden />
70
+ ) : (
71
+ <AlertTriangle className="size-4" aria-hidden />
72
+ )}
73
+ <AlertTitle>{a.title}</AlertTitle>
74
+ <AlertDescription>{a.message}</AlertDescription>
75
+ </Alert>
76
+ ))}
77
+ {children}
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1,229 @@
1
+ import * as React from "react"
2
+
3
+ type Theme = "dark" | "light" | "system"
4
+ type ResolvedTheme = "dark" | "light"
5
+
6
+ type ThemeProviderProps = {
7
+ children: React.ReactNode
8
+ defaultTheme?: Theme
9
+ storageKey?: string
10
+ disableTransitionOnChange?: boolean
11
+ }
12
+
13
+ type ThemeProviderState = {
14
+ theme: Theme
15
+ setTheme: (theme: Theme) => void
16
+ }
17
+
18
+ const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
19
+ const THEME_VALUES: Theme[] = ["dark", "light", "system"]
20
+
21
+ const ThemeProviderContext = React.createContext<
22
+ ThemeProviderState | undefined
23
+ >(undefined)
24
+
25
+ function isTheme(value: string | null): value is Theme {
26
+ if (value === null) {
27
+ return false
28
+ }
29
+
30
+ return THEME_VALUES.includes(value as Theme)
31
+ }
32
+
33
+ function getSystemTheme(): ResolvedTheme {
34
+ if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
35
+ return "dark"
36
+ }
37
+
38
+ return "light"
39
+ }
40
+
41
+ function disableTransitionsTemporarily() {
42
+ const style = document.createElement("style")
43
+ style.appendChild(
44
+ document.createTextNode(
45
+ "*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
46
+ )
47
+ )
48
+ document.head.appendChild(style)
49
+
50
+ return () => {
51
+ window.getComputedStyle(document.body)
52
+ requestAnimationFrame(() => {
53
+ requestAnimationFrame(() => {
54
+ style.remove()
55
+ })
56
+ })
57
+ }
58
+ }
59
+
60
+ function isEditableTarget(target: EventTarget | null) {
61
+ if (!(target instanceof HTMLElement)) {
62
+ return false
63
+ }
64
+
65
+ if (target.isContentEditable) {
66
+ return true
67
+ }
68
+
69
+ const editableParent = target.closest(
70
+ "input, textarea, select, [contenteditable='true']"
71
+ )
72
+ if (editableParent) {
73
+ return true
74
+ }
75
+
76
+ return false
77
+ }
78
+
79
+ export function ThemeProvider({
80
+ children,
81
+ defaultTheme = "system",
82
+ storageKey = "theme",
83
+ disableTransitionOnChange = true,
84
+ ...props
85
+ }: ThemeProviderProps) {
86
+ const [theme, setThemeState] = React.useState<Theme>(() => {
87
+ const storedTheme = localStorage.getItem(storageKey)
88
+ if (isTheme(storedTheme)) {
89
+ return storedTheme
90
+ }
91
+
92
+ return defaultTheme
93
+ })
94
+
95
+ const setTheme = React.useCallback(
96
+ (nextTheme: Theme) => {
97
+ localStorage.setItem(storageKey, nextTheme)
98
+ setThemeState(nextTheme)
99
+ },
100
+ [storageKey]
101
+ )
102
+
103
+ const applyTheme = React.useCallback(
104
+ (nextTheme: Theme) => {
105
+ const root = document.documentElement
106
+ const resolvedTheme =
107
+ nextTheme === "system" ? getSystemTheme() : nextTheme
108
+ const restoreTransitions = disableTransitionOnChange
109
+ ? disableTransitionsTemporarily()
110
+ : null
111
+
112
+ root.classList.remove("light", "dark")
113
+ root.classList.add(resolvedTheme)
114
+
115
+ if (restoreTransitions) {
116
+ restoreTransitions()
117
+ }
118
+ },
119
+ [disableTransitionOnChange]
120
+ )
121
+
122
+ React.useEffect(() => {
123
+ applyTheme(theme)
124
+
125
+ if (theme !== "system") {
126
+ return undefined
127
+ }
128
+
129
+ const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
130
+ const handleChange = () => {
131
+ applyTheme("system")
132
+ }
133
+
134
+ mediaQuery.addEventListener("change", handleChange)
135
+
136
+ return () => {
137
+ mediaQuery.removeEventListener("change", handleChange)
138
+ }
139
+ }, [theme, applyTheme])
140
+
141
+ React.useEffect(() => {
142
+ const handleKeyDown = (event: KeyboardEvent) => {
143
+ if (event.repeat) {
144
+ return
145
+ }
146
+
147
+ if (event.metaKey || event.ctrlKey || event.altKey) {
148
+ return
149
+ }
150
+
151
+ if (isEditableTarget(event.target)) {
152
+ return
153
+ }
154
+
155
+ if (event.key.toLowerCase() !== "d") {
156
+ return
157
+ }
158
+
159
+ setThemeState((currentTheme) => {
160
+ const nextTheme =
161
+ currentTheme === "dark"
162
+ ? "light"
163
+ : currentTheme === "light"
164
+ ? "dark"
165
+ : getSystemTheme() === "dark"
166
+ ? "light"
167
+ : "dark"
168
+
169
+ localStorage.setItem(storageKey, nextTheme)
170
+ return nextTheme
171
+ })
172
+ }
173
+
174
+ window.addEventListener("keydown", handleKeyDown)
175
+
176
+ return () => {
177
+ window.removeEventListener("keydown", handleKeyDown)
178
+ }
179
+ }, [storageKey])
180
+
181
+ React.useEffect(() => {
182
+ const handleStorageChange = (event: StorageEvent) => {
183
+ if (event.storageArea !== localStorage) {
184
+ return
185
+ }
186
+
187
+ if (event.key !== storageKey) {
188
+ return
189
+ }
190
+
191
+ if (isTheme(event.newValue)) {
192
+ setThemeState(event.newValue)
193
+ return
194
+ }
195
+
196
+ setThemeState(defaultTheme)
197
+ }
198
+
199
+ window.addEventListener("storage", handleStorageChange)
200
+
201
+ return () => {
202
+ window.removeEventListener("storage", handleStorageChange)
203
+ }
204
+ }, [defaultTheme, storageKey])
205
+
206
+ const value = React.useMemo(
207
+ () => ({
208
+ theme,
209
+ setTheme,
210
+ }),
211
+ [theme, setTheme]
212
+ )
213
+
214
+ return (
215
+ <ThemeProviderContext.Provider {...props} value={value}>
216
+ {children}
217
+ </ThemeProviderContext.Provider>
218
+ )
219
+ }
220
+
221
+ export const useTheme = () => {
222
+ const context = React.useContext(ThemeProviderContext)
223
+
224
+ if (context === undefined) {
225
+ throw new Error("useTheme must be used within a ThemeProvider")
226
+ }
227
+
228
+ return context
229
+ }
@@ -0,0 +1,80 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ role = "alert",
26
+ ...props
27
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
28
+ return (
29
+ <div
30
+ data-slot="alert"
31
+ role={role}
32
+ className={cn(alertVariants({ variant }), className)}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+
38
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
39
+ return (
40
+ <div
41
+ data-slot="alert-title"
42
+ className={cn(
43
+ "col-start-2 row-start-1 line-clamp-1 min-h-4 font-medium tracking-tight",
44
+ className
45
+ )}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function AlertDescription({
52
+ className,
53
+ ...props
54
+ }: React.ComponentProps<"div">) {
55
+ return (
56
+ <div
57
+ data-slot="alert-description"
58
+ className={cn(
59
+ "col-start-2 row-start-2 text-muted-foreground [&_p]:leading-relaxed",
60
+ className
61
+ )}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div
70
+ data-slot="alert-action"
71
+ className={cn(
72
+ "col-start-2 row-start-1 justify-self-end pt-0.5 pr-2 sm:col-start-3 sm:row-span-2 sm:row-start-1 sm:pt-2 sm:pr-2",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ )
78
+ }
79
+
80
+ export { Alert, AlertTitle, AlertDescription, AlertAction }
@@ -0,0 +1,67 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ outline:
14
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
15
+ secondary:
16
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
17
+ ghost:
18
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
19
+ destructive:
20
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default:
25
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
28
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
29
+ icon: "size-8",
30
+ "icon-xs":
31
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
32
+ "icon-sm":
33
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
34
+ "icon-lg": "size-9",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ size: "default",
40
+ },
41
+ }
42
+ )
43
+
44
+ function Button({
45
+ className,
46
+ variant = "default",
47
+ size = "default",
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<"button"> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean
53
+ }) {
54
+ const Comp = asChild ? Slot.Root : "button"
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export { Button, buttonVariants }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Example row types for `useGrist<TRow, TMapped>()`.
3
+ * Rename to `grist-types.ts` and align field names with your `GRIST_OPTIONS.columns`.
4
+ */
5
+
6
+ /** Raw section row (real column ids from Grist). */
7
+ export type TaskRow = {
8
+ id: number
9
+ Title?: string
10
+ Done?: boolean
11
+ }
12
+
13
+ /** Logical names after column mapping (matches `columns[].name`). */
14
+ export type TaskMapped = {
15
+ title: string
16
+ done: boolean
17
+ }
@@ -0,0 +1,141 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @import "@fontsource-variable/geist";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ @theme inline {
9
+ --font-heading: var(--font-sans);
10
+ --font-sans: 'Geist Variable', sans-serif;
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --color-foreground: var(--foreground);
41
+ --color-background: var(--background);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
49
+ }
50
+
51
+ :root {
52
+ color-scheme: light;
53
+ --background: oklch(1 0 0);
54
+ --foreground: oklch(0.145 0 0);
55
+ --card: oklch(1 0 0);
56
+ --card-foreground: oklch(0.145 0 0);
57
+ --popover: oklch(1 0 0);
58
+ --popover-foreground: oklch(0.145 0 0);
59
+ --primary: oklch(0.205 0 0);
60
+ --primary-foreground: oklch(0.985 0 0);
61
+ --secondary: oklch(0.97 0 0);
62
+ --secondary-foreground: oklch(0.205 0 0);
63
+ --muted: oklch(0.97 0 0);
64
+ --muted-foreground: oklch(0.556 0 0);
65
+ --accent: oklch(0.97 0 0);
66
+ --accent-foreground: oklch(0.205 0 0);
67
+ --destructive: oklch(0.577 0.245 27.325);
68
+ --border: oklch(0.922 0 0);
69
+ --input: oklch(0.922 0 0);
70
+ --ring: oklch(0.708 0 0);
71
+ --chart-1: oklch(0.87 0 0);
72
+ --chart-2: oklch(0.556 0 0);
73
+ --chart-3: oklch(0.439 0 0);
74
+ --chart-4: oklch(0.371 0 0);
75
+ --chart-5: oklch(0.269 0 0);
76
+ --radius: 0.625rem;
77
+ --sidebar: oklch(0.985 0 0);
78
+ --sidebar-foreground: oklch(0.145 0 0);
79
+ --sidebar-primary: oklch(0.205 0 0);
80
+ --sidebar-primary-foreground: oklch(0.985 0 0);
81
+ --sidebar-accent: oklch(0.97 0 0);
82
+ --sidebar-accent-foreground: oklch(0.205 0 0);
83
+ --sidebar-border: oklch(0.922 0 0);
84
+ --sidebar-ring: oklch(0.708 0 0);
85
+ }
86
+
87
+ .dark {
88
+ color-scheme: dark;
89
+ --background: oklch(0.145 0 0);
90
+ --foreground: oklch(0.985 0 0);
91
+ --card: oklch(0.205 0 0);
92
+ --card-foreground: oklch(0.985 0 0);
93
+ --popover: oklch(0.205 0 0);
94
+ --popover-foreground: oklch(0.985 0 0);
95
+ --primary: oklch(0.922 0 0);
96
+ --primary-foreground: oklch(0.205 0 0);
97
+ --secondary: oklch(0.269 0 0);
98
+ --secondary-foreground: oklch(0.985 0 0);
99
+ --muted: oklch(0.269 0 0);
100
+ --muted-foreground: oklch(0.708 0 0);
101
+ --accent: oklch(0.269 0 0);
102
+ --accent-foreground: oklch(0.985 0 0);
103
+ --destructive: oklch(0.704 0.191 22.216);
104
+ --border: oklch(1 0 0 / 10%);
105
+ --input: oklch(1 0 0 / 15%);
106
+ --ring: oklch(0.556 0 0);
107
+ --chart-1: oklch(0.87 0 0);
108
+ --chart-2: oklch(0.556 0 0);
109
+ --chart-3: oklch(0.439 0 0);
110
+ --chart-4: oklch(0.371 0 0);
111
+ --chart-5: oklch(0.269 0 0);
112
+ --sidebar: oklch(0.205 0 0);
113
+ --sidebar-foreground: oklch(0.985 0 0);
114
+ --sidebar-primary: oklch(0.488 0.243 264.376);
115
+ --sidebar-primary-foreground: oklch(0.985 0 0);
116
+ --sidebar-accent: oklch(0.269 0 0);
117
+ --sidebar-accent-foreground: oklch(0.985 0 0);
118
+ --sidebar-border: oklch(1 0 0 / 10%);
119
+ --sidebar-ring: oklch(0.556 0 0);
120
+ }
121
+
122
+ @layer base {
123
+ * {
124
+ @apply border-border outline-ring/50;
125
+ }
126
+ html,
127
+ body,
128
+ #root {
129
+ min-height: 100%;
130
+ height: 100%;
131
+ }
132
+ html {
133
+ @apply bg-background font-sans;
134
+ }
135
+ body {
136
+ @apply bg-background text-foreground;
137
+ }
138
+ #root {
139
+ @apply bg-background text-foreground;
140
+ }
141
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,26 @@
1
+ import { StrictMode } from "react"
2
+ import { createRoot } from "react-dom/client"
3
+ import { GristBoundary, GristWidgetProvider } from "grist-widget-sdk"
4
+
5
+ import { GristSdkAlerts } from "@/components/grist-sdk-alerts"
6
+ import { ThemeProvider } from "@/components/theme-provider.tsx"
7
+ import "./index.css"
8
+ import App, { GRIST_OPTIONS } from "./App.tsx"
9
+
10
+ createRoot(document.getElementById("root")!).render(
11
+ <StrictMode>
12
+ <ThemeProvider>
13
+ <GristWidgetProvider options={GRIST_OPTIONS}>
14
+ <GristBoundary
15
+ gate={GRIST_OPTIONS.columns?.length ? "canRender" : "ready"}
16
+ >
17
+ <div className="min-h-full w-full bg-background text-foreground">
18
+ <GristSdkAlerts>
19
+ <App />
20
+ </GristSdkAlerts>
21
+ </div>
22
+ </GristBoundary>
23
+ </GristWidgetProvider>
24
+ </ThemeProvider>
25
+ </StrictMode>
26
+ )
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true,
26
+ "baseUrl": ".",
27
+ "paths": {
28
+ "@/*": ["./src/*"]
29
+ }
30
+ },
31
+ "include": ["src"]
32
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ],
7
+ "compilerOptions": {
8
+ "baseUrl": ".",
9
+ "paths": {
10
+ "@/*": ["./src/*"]
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,14 @@
1
+ import path from "path"
2
+ import tailwindcss from "@tailwindcss/vite"
3
+ import react from "@vitejs/plugin-react"
4
+ import { defineConfig } from "vite"
5
+
6
+ // https://vite.dev/config/
7
+ export default defineConfig({
8
+ plugins: [react(), tailwindcss()],
9
+ resolve: {
10
+ alias: {
11
+ "@": path.resolve(__dirname, "./src"),
12
+ },
13
+ },
14
+ })