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 +5 -0
- package/package.json +1 -1
- package/template/.github/workflows/deploy.yml +13 -4
- package/template/README.md +6 -0
- package/template/scripts/deploy.mjs +12 -2
- package/template/src/components/grist-status-chip.tsx +27 -15
- package/template/src/components/template-landing.tsx +96 -44
- package/template/src/components/ui/tabs.tsx +88 -0
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
|
@@ -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
|
|
17
|
-
#
|
|
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
|
|
71
|
+
run: pnpm install
|
|
63
72
|
|
|
64
73
|
- name: Clone gh-pages into ./site
|
|
65
74
|
run: |
|
package/template/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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. */
|
|
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
|
|
92
|
-
* `<GristHandshakeProvider>` alongside
|
|
93
|
-
* (grist-widget-sdk/advanced) as safe
|
|
94
|
-
* page's `ensureGristReady()` singleton.
|
|
95
|
-
* it never drives the real widget's connection, just
|
|
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
|
|
101
|
-
<
|
|
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
|
|
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
|
-
|
|
87
|
+
Get started
|
|
75
88
|
</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>
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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 }
|