create-grist-widget 0.3.9 → 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 +1 -1
- package/template/AGENTS.md +10 -8
- package/template/README.md +1 -1
- package/template/src/components/local-dev-notice.tsx +55 -0
- package/template/src/components/{channel-notice.tsx → release-info.tsx} +13 -27
- package/template/src/lib/showcase-routing.ts +28 -16
- package/template/src/lib/showcase-versions.ts +3 -3
- package/template/src/main.tsx +21 -19
- package/template/vite.config.ts +10 -0
- package/template/src/components/scaffold-footer.tsx +0 -17
- package/template/src/components/template-landing.tsx +0 -309
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -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`
|
|
34
|
-
it renders `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 (
|
|
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
|
-
-
|
|
91
|
-
|
|
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
|
package/template/README.md
CHANGED
|
@@ -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
|
|
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 <example>
|
|
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
|
|
18
|
-
//
|
|
19
|
-
// --
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
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
|
|
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
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
24
|
-
* e.g. "/grist-widget-sdk/template/latest/" ->
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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)
|
|
51
|
+
if (match) {
|
|
52
|
+
channel = { kind: "version", version: match[1] }
|
|
53
|
+
matchedSuffix = true
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
const hubPath =
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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,
|
package/template/src/main.tsx
CHANGED
|
@@ -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 {
|
|
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
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
30
|
+
const { channel } = parseShowcasePath(window.location.pathname)
|
|
29
31
|
|
|
30
32
|
createRoot(document.getElementById("root")!).render(
|
|
31
33
|
<StrictMode>
|
|
32
34
|
<ThemeProvider>
|
|
33
|
-
{
|
|
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
|
-
<
|
|
54
|
+
<ReleaseInfo channel={channel} />
|
|
53
55
|
</div>
|
|
54
56
|
)}
|
|
55
57
|
</ThemeProvider>
|
package/template/vite.config.ts
CHANGED
|
@@ -26,5 +26,15 @@ export default defineConfig({
|
|
|
26
26
|
},
|
|
27
27
|
test: {
|
|
28
28
|
environment: "jsdom",
|
|
29
|
+
// Most examples/<id>/ don't ship their own WidgetApp.test.tsx yet (it's
|
|
30
|
+
// optional, per docs/planning/2026-08-execution-plan.md §3) --
|
|
31
|
+
// overlayExampleSrc correctly deletes the template's own App.test.tsx
|
|
32
|
+
// when swapping in one that has none (it would otherwise assert against
|
|
33
|
+
// content that's no longer there), so `create --example <id>` / `use
|
|
34
|
+
// <id>` routinely leave a project with zero test files. Without this,
|
|
35
|
+
// `pnpm test` exits 1 ("No test files found") on every one of those --
|
|
36
|
+
// a project that legitimately has no tests yet should report green, not
|
|
37
|
+
// red. Found live testing the real published create-grist-widget@0.3.9.
|
|
38
|
+
passWithNoTests: true,
|
|
29
39
|
},
|
|
30
40
|
})
|
|
@@ -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
|
-
}
|