create-grist-widget 0.2.11 → 0.2.13

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/README.md CHANGED
@@ -46,6 +46,11 @@ simplest way to get from zero to a deployed widget:
46
46
  folder `/ (root)`. This can only be set once `gh-pages` exists, which is
47
47
  why it's a step *after* the first push, not before.
48
48
 
49
+ > ⚠️ **Not `main`.** Pointing Pages at `main` serves this repo's raw,
50
+ > unbuilt source instead of the built site — the symptom is a blank page
51
+ > with a `/src/main.tsx` 404 in the browser console, even though the
52
+ > workflow itself reports success.
53
+
49
54
  Once that's saved, your widget is live at two URLs:
50
55
 
51
56
  - `https://<owner>.github.io/<repo>/latest/` — mutable, always points at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-grist-widget",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,8 +13,11 @@ name: Deploy widget to GitHub Pages
13
13
  # One-time setup this workflow can't do for you: enable Pages (Settings ->
14
14
  # Pages -> Source: Deploy from a branch -> gh-pages -> / (root); this workflow
15
15
  # creates the gh-pages branch itself if missing, but Pages must be pointed at
16
- # it once) and make sure Actions has write access (Settings -> Actions ->
17
- # General -> Workflow permissions -> Read and write permissions).
16
+ # it once -- NOT main, which serves this repo's raw unbuilt source instead
17
+ # (symptom: a blank page with a 404 for /src/main.tsx in the browser
18
+ # console, even though this workflow reports success) and make sure Actions
19
+ # has write access (Settings -> Actions -> General -> Workflow permissions ->
20
+ # Read and write permissions).
18
21
 
19
22
  on:
20
23
  push:
@@ -56,10 +59,16 @@ jobs:
56
59
  uses: actions/setup-node@v4
57
60
  with:
58
61
  node-version: 22
59
- cache: pnpm
60
62
 
63
+ # Not --frozen-lockfile, and no `cache: pnpm` above: a freshly
64
+ # scaffolded repo never has a committed pnpm-lock.yaml (the CLI's
65
+ # auto-commit runs before anyone's first `pnpm install`), and
66
+ # --frozen-lockfile/cache: pnpm both hard-require one to exist —
67
+ # found live, the very first real deploy failed with "Dependencies
68
+ # lock file is not found". A plain install resolves fresh every run
69
+ # regardless of whether a lockfile was ever committed.
61
70
  - name: Install dependencies
62
- run: pnpm install --frozen-lockfile
71
+ run: pnpm install
63
72
 
64
73
  - name: Clone gh-pages into ./site
65
74
  run: |
@@ -92,6 +92,12 @@ automatically.
92
92
  → `/ (root)`. The workflow creates the `gh-pages` branch itself the first
93
93
  time it runs (if it doesn't exist yet), but Pages needs to be pointed at
94
94
  it once.
95
+ > ⚠️ **Not `main`.** If Pages is left on (or accidentally set to) `main`,
96
+ > it serves this repo's raw, unbuilt source — including a script tag
97
+ > pointing at `/src/main.tsx` — instead of the built site. The symptom is
98
+ > a blank/black page with a 404 for `/src/main.tsx` in the browser
99
+ > console, even though the workflow itself reports success (it pushed the
100
+ > right build to `gh-pages`; Pages is just reading from the wrong place).
95
101
  2. **Settings → Actions → General → Workflow permissions** → "Read and write
96
102
  permissions". New repos sometimes default the workflow's token to
97
103
  read-only, which would fail the push to `gh-pages` with a 403.
@@ -133,12 +133,22 @@ export function placeTarget({ siteDir, channel, version, distDir, sha, ref }) {
133
133
  }
134
134
  return { placed: "dev" }
135
135
  }
136
- // release
136
+ // release: immutable v<version>/, mutable latest/, and the same build
137
+ // placed directly at the site root too. main.tsx renders the showcase hub
138
+ // (TemplateLanding) at any URL with no recognized channel suffix — the
139
+ // bare root included — so without this, a real scaffolded repo's own
140
+ // https://owner.github.io/repo/ has nothing deployed there at all and
141
+ // 404s instead of ever reaching that hub. Reusing the release dist here is
142
+ // exactly like latest/ reusing it: the asset URLs it references (under
143
+ // v<version>/assets/) already exist from the versionDir placement above,
144
+ // so nothing 404s.
137
145
  const versionDir = join(siteDir, `v${version}`)
138
146
  replaceDir(versionDir, distDir)
139
147
  const latestDir = join(siteDir, "latest")
140
148
  replaceDir(latestDir, distDir)
141
- return { placed: `v${version}`, latest: "latest" }
149
+ mkdirSync(siteDir, { recursive: true })
150
+ cpSync(distDir, siteDir, { recursive: true })
151
+ return { placed: `v${version}`, latest: "latest", root: true }
142
152
  }
143
153
 
144
154
  /**
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react"
2
2
  import { GristHandshakeProvider, useGristHandshakeContext } from "grist-widget-sdk/advanced"
3
3
 
4
- import { deriveDisplayStatus } from "@/lib/grist-handshake-status"
4
+ import { deriveDisplayStatus, type HandshakeDisplayStatus } from "@/lib/grist-handshake-status"
5
5
  import { cn } from "@/lib/utils"
6
6
 
7
7
  // Fixed, approximate countdown shown while retrying -- not a mirror of the
@@ -49,12 +49,8 @@ function StatusDot({ pulsing, className }: { pulsing: boolean; className: string
49
49
  )
50
50
  }
51
51
 
52
- function ChipContents() {
53
- const { snapshot } = useGristHandshakeContext()
54
- const display = deriveDisplayStatus(snapshot.lifecycle)
55
- const retryAttempts = display.kind === "retrying" ? display.attempts : 0
56
- const secondsLeft = useRetryCountdown(retryAttempts, display.kind === "retrying")
57
-
52
+ /** Pure render of a display status. */
53
+ function ChipVisual({ display, secondsLeft }: { display: HandshakeDisplayStatus; secondsLeft: number }) {
58
54
  switch (display.kind) {
59
55
  case "connecting":
60
56
  return (
@@ -87,18 +83,34 @@ function ChipContents() {
87
83
  }
88
84
  }
89
85
 
86
+ function RealChipContents() {
87
+ const { snapshot } = useGristHandshakeContext()
88
+ const display = deriveDisplayStatus(snapshot.lifecycle)
89
+ const retryAttempts = display.kind === "retrying" ? display.attempts : 0
90
+ const secondsLeft = useRetryCountdown(retryAttempts, display.kind === "retrying")
91
+ return <ChipVisual display={display} secondsLeft={secondsLeft} />
92
+ }
93
+
90
94
  /**
91
- * Small pill showing live Grist handshake status. Mounts its own
92
- * `<GristHandshakeProvider>` alongside `<GristWidgetProvider>` -- documented
93
- * (grist-widget-sdk/advanced) as safe to coexist, since both share the
94
- * page's `ensureGristReady()` singleton. This chip is purely observational:
95
- * it never drives the real widget's connection, just reflects it.
95
+ * Small pill showing live Grist handshake status while actually embedded in
96
+ * Grist. Mounts its own `<GristHandshakeProvider>` alongside
97
+ * `<GristWidgetProvider>` -- documented (grist-widget-sdk/advanced) as safe
98
+ * to coexist, since both share the page's `ensureGristReady()` singleton.
99
+ * Purely observational: it never drives the real widget's connection, just
100
+ * reflects it.
96
101
  */
97
- export function GristStatusChip() {
102
+ export function GristStatusChip({ className }: { className?: string }) {
98
103
  return (
99
104
  <GristHandshakeProvider>
100
- <div className="pointer-events-none fixed top-2 right-2 z-50 flex items-center gap-1.5 rounded-full border bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur">
101
- <ChipContents />
105
+ <div className="pointer-events-none fixed top-2 right-2 z-50">
106
+ <div
107
+ className={cn(
108
+ "flex items-center gap-1.5 rounded-full border bg-background/90 px-2.5 py-1 text-xs text-muted-foreground shadow-sm backdrop-blur",
109
+ className,
110
+ )}
111
+ >
112
+ <RealChipContents />
113
+ </div>
102
114
  </div>
103
115
  </GristHandshakeProvider>
104
116
  )
@@ -1,6 +1,7 @@
1
1
  import { useState } from "react"
2
2
 
3
3
  import { Button } from "@/components/ui/button"
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
4
5
  import { DEV_URL, formatDate, useVersions, versionUrl } from "@/lib/showcase-versions"
5
6
 
6
7
  // The showcase "hub" page: rendered whenever this deploy's URL has no
@@ -50,6 +51,18 @@ function CopyableBlock({ text }: { text: string }) {
50
51
  )
51
52
  }
52
53
 
54
+ function CreateRepoButton() {
55
+ return (
56
+ <div className="mt-2 ml-5">
57
+ <Button asChild>
58
+ <a href="https://github.com/new" target="_blank" rel="noreferrer">
59
+ Create a new repository
60
+ </a>
61
+ </Button>
62
+ </div>
63
+ )
64
+ }
65
+
53
66
  export function TemplateLanding() {
54
67
  const { versions, error } = useVersions()
55
68
 
@@ -60,7 +73,7 @@ export function TemplateLanding() {
60
73
  grist-widget-template
61
74
  </p>
62
75
  <h1 className="font-heading text-3xl font-semibold tracking-tight">
63
- Build your own Grist custom widget
76
+ Build a Grist custom widget in minutes
64
77
  </h1>
65
78
  <p className="mx-auto mt-3 max-w-md text-sm text-muted-foreground">
66
79
  A React + TypeScript + Tailwind starting point for{" "}
@@ -71,54 +84,93 @@ export function TemplateLanding() {
71
84
 
72
85
  <div className="mb-10 rounded-lg border p-4">
73
86
  <h2 className="mb-3 font-heading text-base font-semibold">
74
- Using Claude Code?
87
+ Get started
75
88
  </h2>
76
- <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
77
- <li>
78
- Create a new, empty GitHub repo for the widget.
79
- <div className="mt-2 ml-5">
80
- <Button asChild>
81
- <a href="https://github.com/new" target="_blank" rel="noreferrer">
82
- Create a new repository
83
- </a>
84
- </Button>
85
- </div>
86
- </li>
87
- <li>
88
- Install the{" "}
89
- <a
90
- href="https://github.com/apps/claude"
91
- target="_blank"
92
- rel="noreferrer"
93
- className="underline underline-offset-4 hover:text-foreground"
94
- >
95
- Claude GitHub App
96
- </a>{" "}
97
- on that repo.
98
- </li>
99
- <li>
100
- Start a Claude Code session connected to it and paste this
101
- prompt:
102
- <div className="mt-2 ml-5">
103
- <CopyableBlock text={CLAUDE_CODE_PROMPT} />
104
- </div>
105
- </li>
106
- </ol>
107
- </div>
89
+ <Tabs defaultValue="claude-code">
90
+ <TabsList className="mb-4">
91
+ <TabsTrigger value="claude-code">Using Claude Code?</TabsTrigger>
92
+ <TabsTrigger value="manual">Manual setup</TabsTrigger>
93
+ </TabsList>
108
94
 
109
- <details className="mb-10 rounded-lg border p-4 text-sm text-muted-foreground">
110
- <summary className="cursor-pointer font-medium text-foreground">
111
- Prefer to run it yourself?
112
- </summary>
113
- <pre className="mt-3 overflow-x-auto rounded-lg bg-muted p-4 text-xs">
114
- <code>{`npm create grist-widget my-widget
115
- cd my-widget
95
+ <TabsContent value="claude-code">
96
+ <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
97
+ <li>
98
+ Create a new, empty GitHub repo for the widget.
99
+ <CreateRepoButton />
100
+ </li>
101
+ <li>
102
+ Install the{" "}
103
+ <a
104
+ href="https://github.com/apps/claude"
105
+ target="_blank"
106
+ rel="noreferrer"
107
+ className="underline underline-offset-4 hover:text-foreground"
108
+ >
109
+ Claude GitHub App
110
+ </a>{" "}
111
+ on that repo.
112
+ </li>
113
+ <li>
114
+ Start a Claude Code session connected to it and paste this
115
+ prompt:
116
+ <div className="mt-2 ml-5">
117
+ <CopyableBlock text={CLAUDE_CODE_PROMPT} />
118
+ </div>
119
+ </li>
120
+ </ol>
121
+ </TabsContent>
116
122
 
117
- git remote add origin https://github.com/<owner>/<repo>.git
123
+ <TabsContent value="manual">
124
+ <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
125
+ <li>
126
+ Scaffold it and try it locally:
127
+ <div className="mt-2 ml-5">
128
+ <pre className="overflow-x-auto rounded-lg bg-muted p-4 text-xs">
129
+ <code>{`npm create grist-widget my-widget
130
+ cd my-widget
131
+ pnpm install
132
+ pnpm dev`}</code>
133
+ </pre>
134
+ </div>
135
+ </li>
136
+ <li>
137
+ Create a new, empty GitHub repo for the widget.
138
+ <CreateRepoButton />
139
+ </li>
140
+ <li>
141
+ Push it:
142
+ <div className="mt-2 ml-5">
143
+ <pre className="overflow-x-auto rounded-lg bg-muted p-4 text-xs">
144
+ <code>{`git remote add origin https://github.com/<owner>/<repo>.git
118
145
  git push -u origin main
119
146
  git push origin dev`}</code>
120
- </pre>
121
- </details>
147
+ </pre>
148
+ </div>
149
+ </li>
150
+ <li>
151
+ Once that first push's workflow run completes (it creates a{" "}
152
+ <code className="text-foreground">gh-pages</code> branch),
153
+ enable Pages: Settings → Pages → Source → "Deploy from a
154
+ branch" → branch <code className="text-foreground">gh-pages</code>{" "}
155
+ → <code className="text-foreground">/ (root)</code>.
156
+ <p className="mt-2 text-xs text-muted-foreground">
157
+ Not <code className="text-foreground">main</code> — that
158
+ serves this repo's raw, unbuilt source instead of the built
159
+ site (a blank page with a{" "}
160
+ <code className="text-foreground">/src/main.tsx</code> 404
161
+ in the console).
162
+ </p>
163
+ <p className="mt-2 text-xs text-muted-foreground">
164
+ If that first run fails with a permissions error pushing to{" "}
165
+ <code className="text-foreground">gh-pages</code>, Settings
166
+ → Actions → General → Workflow permissions → "Read and
167
+ write permissions" fixes it — most repos don't need this.
168
+ </p>
169
+ </li>
170
+ </ol>
171
+ </TabsContent>
172
+ </Tabs>
173
+ </div>
122
174
 
123
175
  <section>
124
176
  <div className="mb-3 flex items-center justify-between">
@@ -0,0 +1,88 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Tabs as TabsPrimitive } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Tabs({
8
+ className,
9
+ orientation = "horizontal",
10
+ ...props
11
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
12
+ return (
13
+ <TabsPrimitive.Root
14
+ data-slot="tabs"
15
+ data-orientation={orientation}
16
+ className={cn(
17
+ "group/tabs flex gap-2 data-horizontal:flex-col",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ const tabsListVariants = cva(
26
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
27
+ {
28
+ variants: {
29
+ variant: {
30
+ default: "bg-muted",
31
+ line: "gap-1 bg-transparent",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "default",
36
+ },
37
+ }
38
+ )
39
+
40
+ function TabsList({
41
+ className,
42
+ variant = "default",
43
+ ...props
44
+ }: React.ComponentProps<typeof TabsPrimitive.List> &
45
+ VariantProps<typeof tabsListVariants>) {
46
+ return (
47
+ <TabsPrimitive.List
48
+ data-slot="tabs-list"
49
+ data-variant={variant}
50
+ className={cn(tabsListVariants({ variant }), className)}
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ function TabsTrigger({
57
+ className,
58
+ ...props
59
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
60
+ return (
61
+ <TabsPrimitive.Trigger
62
+ data-slot="tabs-trigger"
63
+ className={cn(
64
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
65
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
66
+ "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
67
+ "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
68
+ className
69
+ )}
70
+ {...props}
71
+ />
72
+ )
73
+ }
74
+
75
+ function TabsContent({
76
+ className,
77
+ ...props
78
+ }: React.ComponentProps<typeof TabsPrimitive.Content>) {
79
+ return (
80
+ <TabsPrimitive.Content
81
+ data-slot="tabs-content"
82
+ className={cn("flex-1 text-sm outline-none", className)}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }