create-grist-widget 0.3.10 → 0.3.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-grist-widget",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,11 +30,12 @@ rendering all live in `src/App.tsx` — that's the one file to edit.
30
30
 
31
31
  1. **Widgets use the SDK.** No direct `window.grist.*` calls — use `useGrist()` and the
32
32
  other `grist-widget-sdk` hooks.
33
- 2. **`pnpm dev` shows the showcase hub today, not a seeded preview.** Opened outside Grist
34
- it renders `TemplateLanding` (onboarding + links to released versions); the widget
35
- itself only renders once actually embedded in a Grist document. An offline, seeded
36
- `pnpm dev` loop is planned but not shipped in this template yet — don't describe one as
37
- if it exists.
33
+ 2. **`pnpm dev` is a bare shell, on purpose — not a seeded preview.** Opened outside Grist
34
+ it renders `LocalDevNotice`; the widget itself only renders once actually embedded in a
35
+ Grist document. This is deliberate, not a gap: an emulator can't guarantee parity with
36
+ real Grist, so a fake local preview would teach the wrong lesson. Trust `pnpm test`
37
+ (headless, real assertions) for local feedback, and the `dev`-branch loop below for real
38
+ behavior — don't build or suggest a seeded `pnpm dev` preview.
38
39
  3. **Widget changes get a test.** `src/App.test.tsx` uses `renderWithGrist` + `presets` from
39
40
  `grist-widget-sdk/emulator/testing` — no browser or real Grist doc needed. Run `pnpm test`.
40
41
  4. **Always develop on `dev`, release by merging to `main`.** Commit and push to `dev` for
@@ -49,7 +50,7 @@ rendering all live in `src/App.tsx` — that's the one file to edit.
49
50
  | Intent | Command |
50
51
  | --- | --- |
51
52
  | Install | `pnpm install` |
52
- | Dev server (showcase hub outside Grist) | `pnpm dev` |
53
+ | Dev server (bare shell outside Grist, pointing at the `dev` loop) | `pnpm dev` |
53
54
  | Build | `pnpm build` |
54
55
  | Test | `pnpm test` |
55
56
  | Lint | `pnpm lint` |
@@ -87,6 +88,7 @@ Full detail, including the two live URLs (`/latest/` and `/v<version>/`): `READM
87
88
  ## Anti-patterns
88
89
 
89
90
  - Calling `window.grist.*` inside widget code instead of the SDK's hooks
90
- - Describing a seeded offline `pnpm dev` preview — that doesn't exist in this template yet
91
- (only a seeded offline `pnpm test`, via `renderWithGrist`, does)
91
+ - Building or describing a seeded/live `pnpm dev` preview — deliberately not how this
92
+ template works (see above); `pnpm test` is the trusted local loop, real Grist via `/dev/`
93
+ is the trusted real one
92
94
  - Merging `dev` into `main` without bumping `package.json`'s `version` first
@@ -4,7 +4,7 @@ Base template to build a Grist widget with `grist-widget-sdk`.
4
4
 
5
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
6
 
7
- Opened outside a Grist iframe, `main.tsx` picks between two components purely by URL shape (`src/lib/showcase-routing.ts`, no router needed): a bare path with no recognized channel suffix renders `TemplateLanding` (its own hero, onboarding, and released-version index); `/latest/`, `/dev/`, or `/v<version>/` renders `ChannelNotice` instead — a distinct hero explaining which build this is, chips to jump to any other version/channel (`src/lib/showcase-versions.ts`), and a copy-this-URL helper for pasting into Grist's custom widget field. Both render `ScaffoldFooter` (`src/components/scaffold-footer.tsx`) at the bottom — a small, purely informational note ("Scaffolded from `create-grist-widget@X.Y.Z`") built from `package.json`'s `createGristWidgetVersion` field, which the CLI stamps on every real scaffold (`vite.config.ts` injects it as `__CREATE_GRIST_WIDGET_VERSION__`). Renders nothing if that field is absent, e.g. this pristine template source itself.
7
+ Opened outside a Grist iframe, `main.tsx` picks between two components — never a live preview of the widget itself, since nothing here proves anything about real Grist behavior: local `pnpm dev` renders `LocalDevNotice`, a bare shell pointing at the real verification loop below (push to `dev`, open it in a real Grist document); any built/deployed URL (root, `/latest/`, `/dev/`, `/v<version>/` — all carrying the same build, see Deployment below) renders `ReleaseInfo` instead — which build this is, chips to jump to any other version/channel (hover one to see when it shipped; `src/lib/showcase-versions.ts`), and a copy-this-URL helper for pasting into Grist's custom widget field.
8
8
 
9
9
  When actually embedded, `GristStatusChip` (`src/components/grist-status-chip.tsx`) shows a small pill with live handshake status — connecting, retrying with a countdown, connected, or unavailable — using `useAmbientGristHandshake()` from `grist-widget-sdk/advanced`. It must be mounted *inside* `GristWidgetProvider` (see `src/main.tsx`): it only observes that provider's own handshake manager and never mounts a second one, so it's purely observational and can never duplicate or race the real handshake (see the [handshake API reference](https://gristwidgets.com/docs/sdk/api/handshake)).
10
10
 
@@ -0,0 +1,55 @@
1
+ import {
2
+ Card,
3
+ CardDescription,
4
+ CardHeader,
5
+ CardTitle,
6
+ } from "@/components/ui/card"
7
+
8
+ // Shown at the root of a local `pnpm dev` server, never inside Grist.
9
+ // Deliberately a bare shell, not a seeded live preview: an emulator can't
10
+ // guarantee parity with real Grist, so the loop worth trusting here is
11
+ // `pnpm test` (renderWithGrist, headless) plus the real `dev`-branch loop
12
+ // this points at below -- see AGENTS.md's note on why this isn't a fake
13
+ // preview instead.
14
+ export function LocalDevNotice() {
15
+ return (
16
+ <div className="mx-auto flex min-h-full max-w-md flex-col justify-center px-6 py-16">
17
+ <p className="mb-2 text-center text-sm font-medium text-muted-foreground">
18
+ grist-widget-template · local dev
19
+ </p>
20
+ <h1 className="text-center font-heading text-2xl font-semibold tracking-tight">
21
+ Not embedded in Grist
22
+ </h1>
23
+ <p className="mx-auto mt-3 mb-8 max-w-sm text-center text-sm text-muted-foreground">
24
+ This is a local dev server — there's no Grist here to render against.
25
+ </p>
26
+
27
+ <Card className="mb-4">
28
+ <CardHeader>
29
+ <CardTitle>Verify against real Grist</CardTitle>
30
+ <CardDescription>
31
+ Commit and push to <code className="text-foreground">dev</code>,
32
+ then open the printed{" "}
33
+ <code className="text-foreground">/dev/</code> URL in a real
34
+ Grist document — every later push self-reloads inside it within
35
+ seconds. See the README's Deployment section for the full loop.
36
+ </CardDescription>
37
+ </CardHeader>
38
+ </Card>
39
+
40
+ <Card>
41
+ <CardHeader>
42
+ <CardTitle>Not building this widget?</CardTitle>
43
+ <CardDescription>
44
+ <code className="text-foreground">npx grist-widget list</code> to
45
+ see other starting points,{" "}
46
+ <code className="text-foreground">
47
+ npx grist-widget use &lt;example&gt;
48
+ </code>{" "}
49
+ to swap this one out.
50
+ </CardDescription>
51
+ </CardHeader>
52
+ </Card>
53
+ </div>
54
+ )
55
+ }
@@ -1,6 +1,5 @@
1
1
  import { useState } from "react"
2
2
 
3
- import { ScaffoldFooter } from "@/components/scaffold-footer"
4
3
  import { Button } from "@/components/ui/button"
5
4
  import {
6
5
  Card,
@@ -11,16 +10,15 @@ import {
11
10
  } from "@/components/ui/card"
12
11
  import { Input } from "@/components/ui/input"
13
12
  import { channelLabel, type Channel } from "@/lib/showcase-routing"
14
- import { devUrl, useVersions, versionUrl } from "@/lib/showcase-versions"
13
+ import { devUrl, formatDate, useVersions, versionUrl } from "@/lib/showcase-versions"
15
14
  import { cn } from "@/lib/utils"
16
15
 
17
- // Shown instead of the widget tree on a specific channel build
18
- // (/latest/, /dev/, /v<version>/) when it's opened directly in a browser tab
19
- // -- not the rich showcase hub (that's `TemplateLanding`, shown when the URL
20
- // has no recognized channel suffix). Deliberately minimal, with its own hero
21
- // distinct from the hub's: this page's job is just to explain what you're
22
- // looking at, help you jump to a different build, and help you paste this
23
- // exact URL into Grist.
16
+ // Shown on every non-embedded, built/deployed URL of this widget -- root,
17
+ // /latest/, /dev/, /v<version>/, all carrying the same build per
18
+ // scripts/deploy.mjs's plan() -- when opened outside Grist. Local `pnpm dev`
19
+ // renders `LocalDevNotice` instead (see src/main.tsx): this component's
20
+ // "paste this URL into Grist" instruction only makes sense for a real,
21
+ // publicly reachable URL, never a bare localhost one.
24
22
  function VersionChips({ current }: { current: Channel }) {
25
23
  const { versions } = useVersions()
26
24
 
@@ -38,6 +36,7 @@ function VersionChips({ current }: { current: Channel }) {
38
36
  {versions !== null && versions.length > 0 && (
39
37
  <a
40
38
  href={versionUrl(versions[0].version)}
39
+ title={formatDate(versions[0].publishedAt)}
41
40
  className={chipClass(
42
41
  current.kind === "version" && current.version === versions[0].version,
43
42
  )}
@@ -52,6 +51,7 @@ function VersionChips({ current }: { current: Channel }) {
52
51
  <a
53
52
  key={v.version}
54
53
  href={versionUrl(v.version)}
54
+ title={formatDate(v.publishedAt)}
55
55
  className={chipClass(
56
56
  current.kind === "version" && current.version === v.version,
57
57
  )}
@@ -63,13 +63,7 @@ function VersionChips({ current }: { current: Channel }) {
63
63
  )
64
64
  }
65
65
 
66
- export function ChannelNotice({
67
- channel,
68
- hubPath,
69
- }: {
70
- channel: Channel
71
- hubPath: string
72
- }) {
66
+ export function ReleaseInfo({ channel }: { channel: Channel }) {
73
67
  const [copied, setCopied] = useState(false)
74
68
  const url = window.location.href
75
69
 
@@ -90,12 +84,13 @@ export function ChannelNotice({
90
84
  </h1>
91
85
  <p className="mx-auto mt-3 mb-8 max-w-sm text-center text-sm text-muted-foreground">
92
86
  You're viewing the <span className="font-mono">{channelLabel(channel)}</span>{" "}
93
- build directly, outside of Grist.
87
+ build directly, outside of Grist. Hover a chip below to see when it
88
+ shipped.
94
89
  </p>
95
90
 
96
91
  <VersionChips current={channel} />
97
92
 
98
- <Card className="mb-4">
93
+ <Card>
99
94
  <CardHeader>
100
95
  <CardTitle>Use this as a Grist custom widget</CardTitle>
101
96
  <CardDescription>
@@ -111,15 +106,6 @@ export function ChannelNotice({
111
106
  </div>
112
107
  </CardContent>
113
108
  </Card>
114
-
115
- <a
116
- href={hubPath}
117
- className="text-center text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
118
- >
119
- ← Back to the template home
120
- </a>
121
-
122
- <ScaffoldFooter />
123
109
  </div>
124
110
  )
125
111
  }
@@ -1,8 +1,7 @@
1
1
  // Parses the deployed URL shape every channel of this template (or a real
2
- // scaffolded widget) uses -- /latest/, /dev/, /v<version>/ -- so main.tsx can
3
- // tell a "hub" page (no recognized channel suffix, e.g. this repo's own
4
- // /template/) from a "channel" page (a specific release/dev build) using
5
- // nothing but window.location.pathname. No router needed for two states.
2
+ // scaffolded widget) uses -- /latest/, /dev/, /v<version>/ -- so main.tsx and
3
+ // ReleaseInfo can label the current build using nothing but
4
+ // window.location.pathname. No router needed.
6
5
  export type Channel =
7
6
  | { kind: "latest" }
8
7
  | { kind: "dev" }
@@ -20,28 +19,41 @@ export function channelLabel(channel: Channel): string {
20
19
  }
21
20
 
22
21
  /**
23
- * `hubPath` is the current path with the trailing channel segment stripped,
24
- * e.g. "/grist-widget-sdk/template/latest/" -> "/grist-widget-sdk/template/".
25
- * `channel` is null when the path itself already looks like a hub (no
26
- * recognized suffix) -- e.g. this repo's own bare /template/, or a local
27
- * `pnpm dev` server at "/".
22
+ * `hubPath` is the current path with a recognized trailing channel segment
23
+ * stripped, e.g. "/grist-widget-sdk/template/latest/" ->
24
+ * "/grist-widget-sdk/template/" -- used to build root-relative URLs
25
+ * (versions.json, /dev/, /v<version>/) regardless of how deep this deploy's
26
+ * own base path is (see showcase-versions.ts).
27
+ *
28
+ * `channel` always resolves to something: `scripts/deploy.mjs` places the
29
+ * same build at the bare deployed root as at `/latest/`, so a path with no
30
+ * recognized suffix (the root itself, or a local `pnpm dev` server) defaults
31
+ * to `{ kind: "latest" }` -- that's genuinely what's there.
28
32
  */
29
33
  export function parseShowcasePath(pathname: string): {
30
- channel: Channel | null
34
+ channel: Channel
31
35
  hubPath: string
32
36
  } {
33
37
  const trimmed = pathname.replace(/\/+$/, "")
34
38
  const segments = trimmed.split("/")
35
39
  const last = segments[segments.length - 1] ?? ""
36
40
 
37
- let channel: Channel | null = null
38
- if (last === "latest") channel = { kind: "latest" }
39
- else if (last === "dev") channel = { kind: "dev" }
40
- else {
41
+ let channel: Channel = { kind: "latest" }
42
+ let matchedSuffix = false
43
+ if (last === "latest") {
44
+ channel = { kind: "latest" }
45
+ matchedSuffix = true
46
+ } else if (last === "dev") {
47
+ channel = { kind: "dev" }
48
+ matchedSuffix = true
49
+ } else {
41
50
  const match = /^v(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(last)
42
- if (match) channel = { kind: "version", version: match[1] }
51
+ if (match) {
52
+ channel = { kind: "version", version: match[1] }
53
+ matchedSuffix = true
54
+ }
43
55
  }
44
56
 
45
- const hubPath = channel ? segments.slice(0, -1).join("/") + "/" : pathname
57
+ const hubPath = matchedSuffix ? segments.slice(0, -1).join("/") + "/" : pathname
46
58
  return { channel, hubPath }
47
59
  }
@@ -2,9 +2,9 @@ import { useEffect, useState } from "react"
2
2
 
3
3
  import { parseShowcasePath } from "@/lib/showcase-routing"
4
4
 
5
- // Shared by the showcase hub (TemplateLanding) and the per-channel notice
6
- // (ChannelNotice) -- both need the same released-version list. URLs are
7
- // root-relative to wherever THIS deploy actually lives (derived from
5
+ // Used by ReleaseInfo, which needs the released-version list on every
6
+ // non-embedded, deployed URL. URLs are root-relative to wherever THIS
7
+ // deploy actually lives (derived from
8
8
  // window.location.pathname via parseShowcasePath's hubPath), not hardcoded
9
9
  // to the grist-widget-sdk monorepo's own showcase: a real scaffolded
10
10
  // widget has its own versions.json and its own v<version>/ + dev/ dirs,
@@ -2,10 +2,10 @@ import { StrictMode } from "react"
2
2
  import { createRoot } from "react-dom/client"
3
3
  import { GristBoundary, GristWidgetProvider } from "grist-widget-sdk"
4
4
 
5
- import { ChannelNotice } from "@/components/channel-notice"
6
5
  import { GristSdkAlerts } from "@/components/grist-sdk-alerts"
7
6
  import { GristStatusChip } from "@/components/grist-status-chip"
8
- import { TemplateLanding } from "@/components/template-landing"
7
+ import { LocalDevNotice } from "@/components/local-dev-notice"
8
+ import { ReleaseInfo } from "@/components/release-info"
9
9
  import { ThemeProvider } from "@/components/theme-provider.tsx"
10
10
  import { parseShowcasePath } from "@/lib/showcase-routing"
11
11
  import "./index.css"
@@ -13,28 +13,26 @@ import App, { GRIST_OPTIONS } from "./App.tsx"
13
13
 
14
14
  // A Grist custom widget only ever runs embedded in Grist's own iframe.
15
15
  // Opened directly in a browser tab (window.self === window.top), there's no
16
- // Grist to connect to. Two different "not embedded" experiences, told apart
17
- // purely by URL shape (see src/lib/showcase-routing.ts):
18
- // - no recognized channel suffix (this repo's own bare /template/, or a
19
- // local `pnpm dev` server) -> the rich showcase hub (TemplateLanding):
20
- // onboarding + a link to every released version and the dev channel.
21
- // - a recognized channel suffix (/latest/, /dev/, /v<version>/) -> a
22
- // minimal notice: which build this is, a link back to the hub, and a
16
+ // Grist to connect to -- two different non-embedded experiences, told apart
17
+ // by import.meta.env.DEV (a real embed always wins over either, checked
18
+ // first):
19
+ // - `pnpm dev` (DEV) -> LocalDevNotice: a bare shell pointing at the real
20
+ // verification loop (push to `dev`, open it in a real Grist doc) --
21
+ // deliberately not a live/seeded preview. An emulator can't guarantee
22
+ // parity with real Grist, so pretending otherwise here would teach the
23
+ // wrong lesson; `pnpm test` (renderWithGrist, headless) is the loop
24
+ // worth trusting locally. See AGENTS.md.
25
+ // - any built/deployed URL (root, /latest/, /dev/, /v<version>/, all
26
+ // carrying the same build per scripts/deploy.mjs) -> ReleaseInfo: which
27
+ // build this is, chips to jump to any other version/channel, and a
23
28
  // copy-this-URL helper for pasting into Grist's custom widget field.
24
- // The hub always wins over embedding -- /template/ is never meant to
25
- // function as an actual widget, even if someone pastes it into Grist by
26
- // mistake.
27
29
  const isEmbedded = window.self !== window.top
28
- const { channel, hubPath } = parseShowcasePath(window.location.pathname)
30
+ const { channel } = parseShowcasePath(window.location.pathname)
29
31
 
30
32
  createRoot(document.getElementById("root")!).render(
31
33
  <StrictMode>
32
34
  <ThemeProvider>
33
- {!channel ? (
34
- <div className="min-h-full w-full bg-background text-foreground">
35
- <TemplateLanding />
36
- </div>
37
- ) : isEmbedded ? (
35
+ {isEmbedded ? (
38
36
  <GristWidgetProvider options={GRIST_OPTIONS}>
39
37
  <GristStatusChip />
40
38
  <GristBoundary
@@ -47,9 +45,13 @@ createRoot(document.getElementById("root")!).render(
47
45
  </div>
48
46
  </GristBoundary>
49
47
  </GristWidgetProvider>
48
+ ) : import.meta.env.DEV ? (
49
+ <div className="min-h-full w-full bg-background text-foreground">
50
+ <LocalDevNotice />
51
+ </div>
50
52
  ) : (
51
53
  <div className="min-h-full w-full bg-background text-foreground">
52
- <ChannelNotice channel={channel} hubPath={hubPath} />
54
+ <ReleaseInfo channel={channel} />
53
55
  </div>
54
56
  )}
55
57
  </ThemeProvider>
@@ -1,17 +0,0 @@
1
- // Shown on the showcase hub and channel pages, never inside Grist (there's
2
- // no room, and it's not relevant to using the widget). Purely informational:
3
- // which create-grist-widget release this repo's package.json was scaffolded
4
- // from -- distinct from the repo's own `version`, which the deploy pipeline
5
- // always starts fresh at 0.0.1 regardless of this value (see
6
- // scripts/deploy.mjs's `plan()`). Renders nothing for the pristine template
7
- // source itself, or any copy predating this field.
8
- export function ScaffoldFooter() {
9
- if (!__CREATE_GRIST_WIDGET_VERSION__) return null
10
-
11
- return (
12
- <p className="mt-10 text-center text-xs text-muted-foreground">
13
- Scaffolded from{" "}
14
- <code className="text-foreground">create-grist-widget@{__CREATE_GRIST_WIDGET_VERSION__}</code>
15
- </p>
16
- )
17
- }
@@ -1,309 +0,0 @@
1
- import { useState } from "react"
2
-
3
- import { ScaffoldFooter } from "@/components/scaffold-footer"
4
- import { Button } from "@/components/ui/button"
5
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
6
- import { devUrl, formatDate, useVersions, versionUrl } from "@/lib/showcase-versions"
7
-
8
- // The showcase "hub" page: rendered whenever this deploy's URL has no
9
- // recognized channel suffix (see src/lib/showcase-routing.ts) -- this repo's
10
- // own bare /template/, or a local `pnpm dev` server. Individual channel
11
- // builds (/latest/, /dev/, /v<version>/) render `ChannelNotice` instead when
12
- // not embedded in Grist, with a link back here. Its hero introduces the
13
- // template project itself -- deliberately different from ChannelNotice's
14
- // hero, which is about the specific build you're looking at. Same component
15
- // for this repo's own showcase as for a real scaffolded widget that happens
16
- // to have something deployed at its own bare root; the only per-deploy
17
- // difference is which /v<version>/ dirs actually exist upstream.
18
-
19
- // A ready-to-paste instruction for an AI coding agent, not a human. Written
20
- // to work first-shot: it explains *why* to scaffold into a subdirectory
21
- // (create-grist-widget's CLI refuses a non-empty target -- and a repo
22
- // Claude Code is already connected to always has a .git folder) rather than
23
- // just saying "scaffold and push", which left room for the agent to try
24
- // (and fail) `npm create grist-widget .` in place.
25
- const CLAUDE_CODE_PROMPT = `Scaffold a new Grist custom widget here using \`npm create grist-widget\` (https://www.npmjs.com/package/create-grist-widget). This repo is an empty git clone already connected to GitHub, so scaffold into a new subdirectory first -- the CLI refuses a non-empty target directory, and this one already has a .git folder. Then push the scaffold's \`main\` and \`dev\` branches to this repository's own \`origin\` remote, so the bundled GitHub Pages deploy workflow runs. Report back the /latest/ and /v<version>/ GitHub Pages URLs once it's live.`
26
-
27
- function CopyableBlock({ text }: { text: string }) {
28
- const [copied, setCopied] = useState(false)
29
-
30
- function copy() {
31
- navigator.clipboard.writeText(text).then(() => {
32
- setCopied(true)
33
- setTimeout(() => setCopied(false), 1500)
34
- })
35
- }
36
-
37
- return (
38
- <div className="relative">
39
- <pre className="overflow-x-auto rounded-lg bg-muted p-4 pr-16 text-xs whitespace-pre-wrap">
40
- <code>{text}</code>
41
- </pre>
42
- <Button
43
- type="button"
44
- variant="secondary"
45
- size="sm"
46
- className="absolute top-2 right-2"
47
- onClick={copy}
48
- >
49
- {copied ? "Copied" : "Copy"}
50
- </Button>
51
- </div>
52
- )
53
- }
54
-
55
- function CreateRepoButton() {
56
- return (
57
- <div className="mt-2 ml-5">
58
- <Button asChild>
59
- <a href="https://github.com/new" target="_blank" rel="noreferrer">
60
- Create a new repository
61
- </a>
62
- </Button>
63
- </div>
64
- )
65
- }
66
-
67
- // The fastest path: GitHub's own "generate from template" flow, with
68
- // history included so gh-pages/Pages content comes along -- no
69
- // create-grist-widget CLI, no local install. See the template README's
70
- // "Used GitHub's 'Use this template' button instead of `npm create
71
- // grist-widget`?" note for what this trades away (package.json rename,
72
- // version reset) in exchange for zero local setup.
73
- function CopyStarterRepoButton() {
74
- return (
75
- <div className="mt-2 ml-5">
76
- <Button asChild>
77
- <a
78
- href="https://github.com/arthurblanchon/grist-widget-template/generate"
79
- target="_blank"
80
- rel="noreferrer"
81
- >
82
- Copy starter repo
83
- </a>
84
- </Button>
85
- </div>
86
- )
87
- }
88
-
89
- export function TemplateLanding() {
90
- const { versions, error } = useVersions()
91
-
92
- return (
93
- <div className="mx-auto max-w-2xl px-6 py-16">
94
- <header className="mb-10 text-center">
95
- <p className="mb-2 text-sm font-medium text-muted-foreground">
96
- grist-widget-template
97
- </p>
98
- <h1 className="font-heading text-3xl font-semibold tracking-tight">
99
- Build a Grist custom widget in minutes
100
- </h1>
101
- <p className="mx-auto mt-3 max-w-md text-sm text-muted-foreground">
102
- A React + TypeScript + Tailwind starting point for{" "}
103
- <code className="text-foreground">grist-widget-sdk</code>. Scaffold
104
- your own copy, or browse what's already been released below.
105
- </p>
106
- </header>
107
-
108
- <div className="mb-10 rounded-lg border p-4">
109
- <h2 className="mb-3 font-heading text-base font-semibold">
110
- Quickstart
111
- </h2>
112
- <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
113
- <li>
114
- Copy this starter repo. On the next page, check{" "}
115
- <strong className="text-foreground">Include all branches</strong>{" "}
116
- so GitHub Pages comes already configured — no manual Settings
117
- step.
118
- <CopyStarterRepoButton />
119
- </li>
120
- <li>
121
- Your new widget starts deploying on its first push. Ask your AI
122
- coding agent for two URLs: the{" "}
123
- <strong className="text-foreground">dev</strong> build (auto-reloads
124
- as you iterate) and the{" "}
125
- <strong className="text-foreground">latest</strong> build (ready to
126
- paste into a production Grist document).
127
- </li>
128
- </ol>
129
- <div className="mt-4 rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-xs text-muted-foreground">
130
- <strong className="text-foreground">
131
- If your AI can't see the new widget repo,
132
- </strong>{" "}
133
- check its GitHub App has been granted access to it:{" "}
134
- <a
135
- href="https://github.com/apps/claude"
136
- target="_blank"
137
- rel="noreferrer"
138
- className="underline underline-offset-4 hover:text-foreground"
139
- >
140
- Claude
141
- </a>
142
- ,{" "}
143
- <a
144
- href="https://github.com/apps/chatgpt-codex-connector"
145
- target="_blank"
146
- rel="noreferrer"
147
- className="underline underline-offset-4 hover:text-foreground"
148
- >
149
- Codex
150
- </a>
151
- , or{" "}
152
- <a
153
- href="https://github.com/apps/cursor"
154
- target="_blank"
155
- rel="noreferrer"
156
- className="underline underline-offset-4 hover:text-foreground"
157
- >
158
- Cursor
159
- </a>
160
- .
161
- </div>
162
- </div>
163
-
164
- <div className="mb-10 rounded-lg border p-4">
165
- <h2 className="mb-3 font-heading text-base font-semibold">
166
- Prefer the CLI?
167
- </h2>
168
- <Tabs defaultValue="claude-code">
169
- <TabsList className="mb-4">
170
- <TabsTrigger value="claude-code">Using Claude Code?</TabsTrigger>
171
- <TabsTrigger value="manual">Manual setup</TabsTrigger>
172
- </TabsList>
173
-
174
- <TabsContent value="claude-code">
175
- <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
176
- <li>
177
- Create a new, empty GitHub repo for the widget.
178
- <CreateRepoButton />
179
- </li>
180
- <li>
181
- Install the{" "}
182
- <a
183
- href="https://github.com/apps/claude"
184
- target="_blank"
185
- rel="noreferrer"
186
- className="underline underline-offset-4 hover:text-foreground"
187
- >
188
- Claude GitHub App
189
- </a>{" "}
190
- on that repo.
191
- </li>
192
- <li>
193
- Start a Claude Code session connected to it and paste this
194
- prompt:
195
- <div className="mt-2 ml-5">
196
- <CopyableBlock text={CLAUDE_CODE_PROMPT} />
197
- </div>
198
- </li>
199
- </ol>
200
- </TabsContent>
201
-
202
- <TabsContent value="manual">
203
- <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
204
- <li>
205
- Scaffold it and try it locally:
206
- <div className="mt-2 ml-5">
207
- <pre className="overflow-x-auto rounded-lg bg-muted p-4 text-xs">
208
- <code>{`npm create grist-widget my-widget
209
- cd my-widget
210
- pnpm install
211
- pnpm dev`}</code>
212
- </pre>
213
- </div>
214
- </li>
215
- <li>
216
- Create a new, empty GitHub repo for the widget.
217
- <CreateRepoButton />
218
- </li>
219
- <li>
220
- Push it:
221
- <div className="mt-2 ml-5">
222
- <pre className="overflow-x-auto rounded-lg bg-muted p-4 text-xs">
223
- <code>{`git remote add origin https://github.com/<owner>/<repo>.git
224
- git push -u origin main
225
- git push origin dev`}</code>
226
- </pre>
227
- </div>
228
- </li>
229
- <li>
230
- Once that first push's workflow run completes (it creates a{" "}
231
- <code className="text-foreground">gh-pages</code> branch),
232
- enable Pages: Settings → Pages → Source → "Deploy from a
233
- branch" → branch <code className="text-foreground">gh-pages</code>{" "}
234
- → <code className="text-foreground">/ (root)</code>.
235
- <p className="mt-2 text-xs text-muted-foreground">
236
- Not <code className="text-foreground">main</code> — that
237
- serves this repo's raw, unbuilt source instead of the built
238
- site (a blank page with a{" "}
239
- <code className="text-foreground">/src/main.tsx</code> 404
240
- in the console).
241
- </p>
242
- <p className="mt-2 text-xs text-muted-foreground">
243
- If that first run fails with a permissions error pushing to{" "}
244
- <code className="text-foreground">gh-pages</code>, Settings
245
- → Actions → General → Workflow permissions → "Read and
246
- write permissions" fixes it — most repos don't need this.
247
- </p>
248
- </li>
249
- </ol>
250
- </TabsContent>
251
- </Tabs>
252
- </div>
253
-
254
- <section>
255
- <div className="mb-3 flex items-center justify-between">
256
- <h2 className="font-heading text-base font-semibold">
257
- Released template versions
258
- </h2>
259
- <a
260
- href={devUrl()}
261
- className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
262
- >
263
- Live dev preview →
264
- </a>
265
- </div>
266
- {error ? (
267
- <p className="text-sm text-muted-foreground">
268
- Couldn't load the version list.
269
- </p>
270
- ) : versions === null ? (
271
- <p className="text-sm text-muted-foreground">Loading…</p>
272
- ) : versions.length === 0 ? (
273
- <p className="text-sm text-muted-foreground">
274
- No versions published yet.
275
- </p>
276
- ) : (
277
- <ul className="divide-y divide-border rounded-lg border">
278
- {versions.map((v, i) => (
279
- <li
280
- key={v.version}
281
- className="flex items-center justify-between px-4 py-3"
282
- >
283
- <div className="flex items-center gap-2">
284
- <span className="font-mono text-sm">v{v.version}</span>
285
- {i === 0 && (
286
- <span className="rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
287
- latest
288
- </span>
289
- )}
290
- <span className="text-xs text-muted-foreground">
291
- {formatDate(v.publishedAt)}
292
- </span>
293
- </div>
294
- <a
295
- href={versionUrl(v.version)}
296
- className="text-sm underline underline-offset-4 hover:text-foreground"
297
- >
298
- Preview
299
- </a>
300
- </li>
301
- ))}
302
- </ul>
303
- )}
304
- </section>
305
-
306
- <ScaffoldFooter />
307
- </div>
308
- )
309
- }