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,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
|
-
|
|
53
|
-
|
|
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
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
91
|
+
Get started
|
|
75
92
|
</h2>
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
<
|
|
80
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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 }
|