create-grist-widget 0.2.11 → 0.2.12

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.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 -- shared by the real (context-driven) and demo (cycling) chips. */
53
+ function ChipVisual({ display, secondsLeft }: { display: HandshakeDisplayStatus; secondsLeft: number }) {
58
54
  switch (display.kind) {
59
55
  case "connecting":
60
56
  return (
@@ -87,19 +83,89 @@ 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
+
94
+ // A fixed, looping sequence for the showcase hub -- there's no real Grist to
95
+ // connect to on that page (it's shown precisely because a real widget isn't
96
+ // embedded), so this is a cosmetic demo of the same visual states a real
97
+ // widget's chip goes through, not a real handshake attempt.
98
+ const DEMO_SEQUENCE: readonly { display: HandshakeDisplayStatus; ms: number }[] = [
99
+ { display: { kind: "connecting" }, ms: 1500 },
100
+ { display: { kind: "retrying", attempts: 1 }, ms: 3000 },
101
+ { display: { kind: "connected" }, ms: 3500 },
102
+ ]
103
+
104
+ function DemoChipContents() {
105
+ const [step, setStep] = useState(0)
106
+ const [countdownState, setCountdownState] = useState({ step, secondsLeft: 3 })
107
+ const current = DEMO_SEQUENCE[step]
108
+
109
+ // Reset the mini countdown the moment the demo cycle enters "retrying" --
110
+ // an "adjust state during render" pattern, not a setState-in-effect.
111
+ if (current.display.kind === "retrying" && countdownState.step !== step) {
112
+ setCountdownState({ step, secondsLeft: 3 })
113
+ }
114
+
115
+ useEffect(() => {
116
+ const advance = setTimeout(() => {
117
+ setStep((s) => (s + 1) % DEMO_SEQUENCE.length)
118
+ }, current.ms)
119
+ return () => clearTimeout(advance)
120
+ }, [step, current.ms])
121
+
122
+ useEffect(() => {
123
+ if (current.display.kind !== "retrying") return
124
+ const id = setInterval(() => {
125
+ setCountdownState((s) => ({ ...s, secondsLeft: s.secondsLeft > 0 ? s.secondsLeft - 1 : 0 }))
126
+ }, 1000)
127
+ return () => clearInterval(id)
128
+ }, [step, current.display.kind])
129
+
130
+ return <ChipVisual display={current.display} secondsLeft={countdownState.secondsLeft} />
131
+ }
132
+
90
133
  /**
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.
134
+ * Small pill showing Grist handshake status. Two modes:
135
+ *
136
+ * - Default: live status while actually embedded in Grist. Mounts its own
137
+ * `<GristHandshakeProvider>` alongside `<GristWidgetProvider>` --
138
+ * documented (grist-widget-sdk/advanced) as safe to coexist, since both
139
+ * share the page's `ensureGristReady()` singleton. Purely observational:
140
+ * it never drives the real widget's connection, just reflects it.
141
+ * - `demo`: a cosmetic, looping cycle through the same visual states, used
142
+ * on the showcase hub page where no real Grist connection is ever
143
+ * attempted -- a reference to the tech behind the template, not a status
144
+ * readout.
96
145
  */
97
- export function GristStatusChip() {
146
+ export function GristStatusChip({
147
+ demo = false,
148
+ className,
149
+ }: {
150
+ demo?: boolean
151
+ className?: string
152
+ }) {
153
+ const pill = (
154
+ <div
155
+ className={cn(
156
+ "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",
157
+ className,
158
+ )}
159
+ >
160
+ {demo ? <DemoChipContents /> : <RealChipContents />}
161
+ </div>
162
+ )
163
+
164
+ if (demo) return pill
165
+
98
166
  return (
99
167
  <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 />
102
- </div>
168
+ <div className="pointer-events-none fixed top-2 right-2 z-50">{pill}</div>
103
169
  </GristHandshakeProvider>
104
170
  )
105
171
  }
@@ -1,6 +1,8 @@
1
1
  import { useState } from "react"
2
2
 
3
+ import { GristStatusChip } from "@/components/grist-status-chip"
3
4
  import { Button } from "@/components/ui/button"
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
4
6
  import { DEV_URL, formatDate, useVersions, versionUrl } from "@/lib/showcase-versions"
5
7
 
6
8
  // The showcase "hub" page: rendered whenever this deploy's URL has no
@@ -50,17 +52,32 @@ function CopyableBlock({ text }: { text: string }) {
50
52
  )
51
53
  }
52
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
+
53
67
  export function TemplateLanding() {
54
68
  const { versions, error } = useVersions()
55
69
 
56
70
  return (
57
71
  <div className="mx-auto max-w-2xl px-6 py-16">
58
72
  <header className="mb-10 text-center">
73
+ <div className="mb-4 flex justify-center">
74
+ <GristStatusChip demo />
75
+ </div>
59
76
  <p className="mb-2 text-sm font-medium text-muted-foreground">
60
77
  grist-widget-template
61
78
  </p>
62
79
  <h1 className="font-heading text-3xl font-semibold tracking-tight">
63
- Build your own Grist custom widget
80
+ Build a Grist custom widget in minutes
64
81
  </h1>
65
82
  <p className="mx-auto mt-3 max-w-md text-sm text-muted-foreground">
66
83
  A React + TypeScript + Tailwind starting point for{" "}
@@ -71,54 +88,86 @@ export function TemplateLanding() {
71
88
 
72
89
  <div className="mb-10 rounded-lg border p-4">
73
90
  <h2 className="mb-3 font-heading text-base font-semibold">
74
- Using Claude Code?
91
+ Get started
75
92
  </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>
93
+ <Tabs defaultValue="claude-code">
94
+ <TabsList className="mb-4">
95
+ <TabsTrigger value="claude-code">Using Claude Code?</TabsTrigger>
96
+ <TabsTrigger value="manual">Manual setup</TabsTrigger>
97
+ </TabsList>
108
98
 
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
99
+ <TabsContent value="claude-code">
100
+ <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
101
+ <li>
102
+ Create a new, empty GitHub repo for the widget.
103
+ <CreateRepoButton />
104
+ </li>
105
+ <li>
106
+ Install the{" "}
107
+ <a
108
+ href="https://github.com/apps/claude"
109
+ target="_blank"
110
+ rel="noreferrer"
111
+ className="underline underline-offset-4 hover:text-foreground"
112
+ >
113
+ Claude GitHub App
114
+ </a>{" "}
115
+ on that repo.
116
+ </li>
117
+ <li>
118
+ Start a Claude Code session connected to it and paste this
119
+ prompt:
120
+ <div className="mt-2 ml-5">
121
+ <CopyableBlock text={CLAUDE_CODE_PROMPT} />
122
+ </div>
123
+ </li>
124
+ </ol>
125
+ </TabsContent>
116
126
 
117
- git remote add origin https://github.com/<owner>/<repo>.git
127
+ <TabsContent value="manual">
128
+ <ol className="list-inside list-decimal space-y-3 text-sm text-muted-foreground">
129
+ <li>
130
+ Scaffold it and try it locally:
131
+ <div className="mt-2 ml-5">
132
+ <pre className="overflow-x-auto rounded-lg bg-muted p-4 text-xs">
133
+ <code>{`npm create grist-widget my-widget
134
+ cd my-widget
135
+ pnpm install
136
+ pnpm dev`}</code>
137
+ </pre>
138
+ </div>
139
+ </li>
140
+ <li>
141
+ Create a new, empty GitHub repo for the widget.
142
+ <CreateRepoButton />
143
+ </li>
144
+ <li>
145
+ Push it:
146
+ <div className="mt-2 ml-5">
147
+ <pre className="overflow-x-auto rounded-lg bg-muted p-4 text-xs">
148
+ <code>{`git remote add origin https://github.com/<owner>/<repo>.git
118
149
  git push -u origin main
119
150
  git push origin dev`}</code>
120
- </pre>
121
- </details>
151
+ </pre>
152
+ </div>
153
+ </li>
154
+ <li>
155
+ Once that first push's workflow run completes (it creates a{" "}
156
+ <code className="text-foreground">gh-pages</code> branch),
157
+ enable Pages: Settings → Pages → Source → "Deploy from a
158
+ branch" → branch <code className="text-foreground">gh-pages</code>{" "}
159
+ → <code className="text-foreground">/ (root)</code>.
160
+ <p className="mt-2 text-xs text-muted-foreground">
161
+ If that first run fails with a permissions error pushing to{" "}
162
+ <code className="text-foreground">gh-pages</code>, Settings
163
+ → Actions → General → Workflow permissions → "Read and
164
+ write permissions" fixes it — most repos don't need this.
165
+ </p>
166
+ </li>
167
+ </ol>
168
+ </TabsContent>
169
+ </Tabs>
170
+ </div>
122
171
 
123
172
  <section>
124
173
  <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 }