create-grist-widget 0.3.7 → 0.3.10

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.
Files changed (33) hide show
  1. package/README.md +79 -7
  2. package/bin/create-grist-widget.mjs +485 -90
  3. package/examples/attachment-gallery/WidgetApp.tsx +202 -0
  4. package/examples/bulk-mark-done/WidgetApp.tsx +43 -0
  5. package/examples/declared-columns/WidgetApp.tsx +42 -0
  6. package/examples/form-edit/WidgetApp.tsx +225 -0
  7. package/examples/hello-world/WidgetApp.tsx +15 -0
  8. package/examples/mark-done/WidgetApp.tsx +44 -0
  9. package/examples/reads-demo/WidgetApp.tsx +69 -0
  10. package/examples/rest-fetch/WidgetApp.tsx +49 -0
  11. package/examples/safe-parse/WidgetApp.tsx +61 -0
  12. package/examples/schema-migration/WidgetApp.tsx +67 -0
  13. package/examples/schema-preview/WidgetApp.tsx +26 -0
  14. package/examples/slice-hooks/WidgetApp.tsx +47 -0
  15. package/examples/spreadsheet-grid/GristDataSheetGrid.tsx +376 -0
  16. package/examples/spreadsheet-grid/GristRdgGrid.tsx +189 -0
  17. package/examples/spreadsheet-grid/GristSchemaToolbar.tsx +293 -0
  18. package/examples/spreadsheet-grid/WidgetApp.tsx +133 -0
  19. package/examples/spreadsheet-grid/grist-rdg-choice-column.tsx +616 -0
  20. package/examples/spreadsheet-grid/grist-rdg-columns.ts +397 -0
  21. package/examples/task-board/WidgetApp.tsx +229 -0
  22. package/examples/theme-demo/WidgetApp.tsx +26 -0
  23. package/examples/widget-options/WidgetApp.tsx +34 -0
  24. package/examples/writes-demo/WidgetApp.tsx +75 -0
  25. package/manifest.json +304 -0
  26. package/package.json +14 -2
  27. package/src/git-gate.mjs +30 -0
  28. package/template/scripts/deploy.mjs +35 -1
  29. package/template/vite.config.ts +10 -0
  30. package/ui-source/ShadcnDataSheetGrid.css +117 -0
  31. package/ui-source/ShadcnDataSheetGrid.tsx +46 -0
  32. package/ui-source/badge.tsx +49 -0
  33. package/vendor/compose-example.mjs +252 -0
@@ -0,0 +1,49 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ function Badge({
31
+ className,
32
+ variant = "default",
33
+ asChild = false,
34
+ ...props
35
+ }: React.ComponentProps<"span"> &
36
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
37
+ const Comp = asChild ? Slot.Root : "span"
38
+
39
+ return (
40
+ <Comp
41
+ data-slot="badge"
42
+ data-variant={variant}
43
+ className={cn(badgeVariants({ variant }), className)}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ export { Badge, badgeVariants }
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ // Composes one example (examples/<id>/) into a full, buildable project by
3
+ // overlaying it onto the template — the same "copy a source dir, do fixed
4
+ // edits" pattern packages/create-grist-widget/scripts/build-template.mjs
5
+ // already uses to embed the template itself.
6
+ //
7
+ // The logic itself lives in createComposer({ templateDir, examplesDir,
8
+ // uiDir, manifestPath }), parameterized rather than hardcoded to this
9
+ // monorepo's own layout, because it now runs with TWO different path
10
+ // bindings:
11
+ //
12
+ // - monorepo-rooted (this file's own bottom section, exported under the
13
+ // names below) — scripts/build-site.mjs -> temp dir -> site/examples/<id>/,
14
+ // and scripts/smoke/create-widget.sh -> temp dir -> built against a
15
+ // packed SDK tarball.
16
+ // - package-rooted — packages/create-grist-widget/scripts/build-template.mjs
17
+ // copies this file verbatim into packages/create-grist-widget/vendor/
18
+ // (safe, since it now has no hardcoded paths left), and the CLI's own
19
+ // bin/create-grist-widget.mjs imports createComposer from there to bind
20
+ // it against the *published* package's own embedded template/examples/
21
+ // manifest — see batch H, docs/planning/2026-08-execution-plan.md §98-105.
22
+ //
23
+ // See docs/planning/2026-08-execution-plan.md §3 ("Composition is the same
24
+ // function as `use`") and batch G.
25
+ //
26
+ // Dependency-free (node builtins only), matching every other scripts/*.mjs
27
+ // in this repo.
28
+
29
+ import {
30
+ cpSync,
31
+ existsSync,
32
+ readFileSync,
33
+ readdirSync,
34
+ rmSync,
35
+ writeFileSync,
36
+ } from "node:fs"
37
+ import { basename, dirname, join, resolve } from "node:path"
38
+ import { fileURLToPath } from "node:url"
39
+
40
+ // The one example needing an npm dependency beyond what copying a missing
41
+ // `@/components/ui/*` file covers — spreadsheet-grid's grid rendering pulls
42
+ // in this package directly, not just through a ui primitive. Pinned to
43
+ // apps/playground's own version, the only other consumer. Example-id-keyed
44
+ // data, not a path — identical regardless of which composer binding uses it.
45
+ const EXTRA_DEPENDENCIES = {
46
+ "spreadsheet-grid": { "react-datasheet-grid": "^4.11.6" },
47
+ }
48
+
49
+ const TEMPLATE_SKIP = new Set(["node_modules", "dist", ".turbo"])
50
+
51
+ /** Union of every example's extra npm deps — see EXTRA_DEPENDENCIES above. */
52
+ export function allExtraDependencies() {
53
+ return Object.assign({}, ...Object.values(EXTRA_DEPENDENCIES))
54
+ }
55
+
56
+ /**
57
+ * Build a { composeExample, overlayExampleSrc, allExtraDependencies,
58
+ * mergeExtraDependencies } bound to one set of source directories. Two
59
+ * bindings exist today — see the file header comment.
60
+ */
61
+ export function createComposer({ templateDir, examplesDir, uiDir, manifestPath }) {
62
+ function loadManifestEntry(exampleId) {
63
+ if (!existsSync(manifestPath)) {
64
+ throw new Error(
65
+ `${basename(manifestPath)} not found — run \`pnpm build:widgets-manifest\` first (needed for usesUi).`
66
+ )
67
+ }
68
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"))
69
+ const entry = manifest.find((e) => e.id === exampleId)
70
+ if (!entry) {
71
+ throw new Error(`No ${basename(manifestPath)} entry for "${exampleId}" — is examples/${exampleId}/ real?`)
72
+ }
73
+ return entry
74
+ }
75
+
76
+ /**
77
+ * Overlay `examples/<exampleId>/` onto an *already-composed* project at
78
+ * `destDir` — everything except the template copy and `package.json` (so a
79
+ * caller can `pnpm install` once against a project whose `package.json`
80
+ * already lists every example's dependencies via {@link allExtraDependencies},
81
+ * then call this repeatedly and rebuild, never reinstalling). Used directly
82
+ * by {@link composeExample}; exported for callers doing that install-once
83
+ * optimization themselves (scripts/build-site.mjs,
84
+ * scripts/smoke/create-widget.sh), and by the CLI's `use <id>` (which calls
85
+ * this directly against an already-scaffolded project, never composeExample —
86
+ * see bin/create-grist-widget.mjs).
87
+ */
88
+ function overlayExampleSrc({ exampleId, destDir }) {
89
+ const exampleDir = join(examplesDir, exampleId)
90
+ if (!existsSync(join(exampleDir, "WidgetApp.tsx"))) {
91
+ throw new Error(`examples/${exampleId}/WidgetApp.tsx not found`)
92
+ }
93
+ const manifestEntry = loadManifestEntry(exampleId)
94
+ const srcDir = join(destDir, "src")
95
+
96
+ // Clear whatever the previous overlay (if any) left behind, except the
97
+ // template's own fixed structure (components/ui, lib, index.css, etc.) —
98
+ // only remove files this function itself may have added, so a stale
99
+ // sibling from a *different* previous example doesn't linger.
100
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
101
+ if (entry.name === "App.tsx" || entry.name === "App.test.tsx") continue
102
+ if (["components", "lib", "assets", "index.css", "vite-env.d.ts", "main.tsx"].includes(entry.name)) {
103
+ continue
104
+ }
105
+ rmSync(join(srcDir, entry.name), { recursive: true, force: true })
106
+ }
107
+
108
+ // The host contract's only mismatch: examples export `WidgetApp` (no
109
+ // default export), the template's main.tsx does a default-only import.
110
+ // A copy-time transform, not a rewrite of the source file on disk (which
111
+ // stays untouched under examples/) — same idiom as deploy.mjs's
112
+ // identityEdits.
113
+ const widgetAppSrc = readFileSync(join(exampleDir, "WidgetApp.tsx"), "utf8")
114
+ writeFileSync(join(srcDir, "App.tsx"), `${widgetAppSrc}\nexport default WidgetApp\n`)
115
+
116
+ const testPath = join(exampleDir, "WidgetApp.test.tsx")
117
+ const destTestPath = join(srcDir, "App.test.tsx")
118
+ if (existsSync(testPath)) {
119
+ writeFileSync(destTestPath, readFileSync(testPath, "utf8"))
120
+ } else if (existsSync(destTestPath)) {
121
+ // The template's own test (or a previous example's) asserts against
122
+ // content that's no longer there once App.tsx is someone else's widget.
123
+ rmSync(destTestPath)
124
+ }
125
+
126
+ // Every other file living alongside WidgetApp.tsx (e.g. spreadsheet-grid's 5
127
+ // sibling components) travels with it, verbatim.
128
+ for (const entry of readdirSync(exampleDir, { withFileTypes: true })) {
129
+ if (entry.name === "WidgetApp.tsx" || entry.name === "WidgetApp.test.tsx") continue
130
+ cpSync(join(exampleDir, entry.name), join(srcDir, entry.name), { recursive: true })
131
+ }
132
+
133
+ // `@/components/ui/*` the template doesn't already vendor (per the
134
+ // manifest's usesUi, computed from the real imports — see
135
+ // scripts/build-widgets-manifest.mjs). components.json is identical
136
+ // across playground/template/examples-in-CLI (verified, §4 of the plan),
137
+ // so a straight copy is style-safe. Never removed by the cleanup above
138
+ // (ui files from an unrelated earlier example are inert, harmless
139
+ // leftovers — re-added here idempotently either way).
140
+ const destUiDir = join(srcDir, "components/ui")
141
+ for (const uiName of manifestEntry.usesUi) {
142
+ const uiSrcPath = join(uiDir, `${uiName}.tsx`)
143
+ if (!existsSync(uiSrcPath) || existsSync(join(destUiDir, `${uiName}.tsx`))) continue
144
+ cpSync(uiSrcPath, join(destUiDir, `${uiName}.tsx`))
145
+ const cssPath = join(uiDir, `${uiName}.css`)
146
+ if (existsSync(cssPath)) cpSync(cssPath, join(destUiDir, `${uiName}.css`))
147
+ }
148
+
149
+ return { destDir, exampleId }
150
+ }
151
+
152
+ /**
153
+ * Additively merge `exampleId`'s extra npm dependencies (see
154
+ * EXTRA_DEPENDENCIES) into an already-composed project's package.json —
155
+ * never touches `name` or the `grist-widget-sdk` dependency spec. Split
156
+ * out from {@link composeExample} (which does this as part of one
157
+ * from-scratch package.json rewrite) for `use <id>`, which overlays onto
158
+ * an *existing* project and must only ever add, never rewrite identity.
159
+ */
160
+ function mergeExtraDependencies({ exampleId, destDir }) {
161
+ const extra = EXTRA_DEPENDENCIES[exampleId]
162
+ if (!extra || Object.keys(extra).length === 0) return { merged: false }
163
+ const pkgPath = join(destDir, "package.json")
164
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
165
+ pkg.dependencies = pkg.dependencies || {}
166
+ Object.assign(pkg.dependencies, extra)
167
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
168
+ return { merged: true, added: extra }
169
+ }
170
+
171
+ /**
172
+ * Compose `examples/<exampleId>/` onto a fresh copy of the template at
173
+ * `destDir`, one-shot (copies the template, overlays, and rewrites
174
+ * `package.json` in one call). `sdkDependency` replaces the template's own
175
+ * `workspace:^` (meaningless outside the pnpm workspace) — a `file:`
176
+ * reference to either `packages/core` (workspace source, already built), a
177
+ * packed tarball, or (for the CLI's `create`) a real published semver range.
178
+ *
179
+ * For composing *many* examples against one shared `pnpm install`, use
180
+ * {@link overlayExampleSrc} directly instead (see its own doc comment).
181
+ */
182
+ function composeExample({ exampleId, destDir, sdkDependency }) {
183
+ rmSync(destDir, { recursive: true, force: true })
184
+ cpSync(templateDir, destDir, {
185
+ recursive: true,
186
+ filter: (src) => !TEMPLATE_SKIP.has(basename(src)),
187
+ })
188
+
189
+ overlayExampleSrc({ exampleId, destDir })
190
+
191
+ const pkgPath = join(destDir, "package.json")
192
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
193
+ pkg.name = exampleId
194
+ pkg.dependencies["grist-widget-sdk"] = sdkDependency
195
+ Object.assign(pkg.dependencies, EXTRA_DEPENDENCIES[exampleId] ?? {})
196
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
197
+
198
+ return { destDir, exampleId }
199
+ }
200
+
201
+ return { composeExample, overlayExampleSrc, allExtraDependencies, mergeExtraDependencies }
202
+ }
203
+
204
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
205
+
206
+ const monorepo = createComposer({
207
+ templateDir: join(repoRoot, "templates/grist-widget-template-vite"),
208
+ examplesDir: join(repoRoot, "examples"),
209
+ uiDir: join(repoRoot, "apps/playground/src/components/ui"),
210
+ manifestPath: join(repoRoot, "widgets.json"),
211
+ })
212
+
213
+ // Re-exported under the names every existing call site already imports —
214
+ // scripts/build-site.mjs and scripts/smoke/create-widget.sh need zero
215
+ // changes for this refactor.
216
+ export const { composeExample, overlayExampleSrc, mergeExtraDependencies } = monorepo
217
+
218
+ function parseArgs(argv) {
219
+ const out = { _: [] }
220
+ for (let i = 0; i < argv.length; i++) {
221
+ const a = argv[i]
222
+ if (a.startsWith("--")) {
223
+ const key = a.slice(2)
224
+ const next = argv[i + 1]
225
+ if (next === undefined || next.startsWith("--")) out[key] = true
226
+ else {
227
+ out[key] = next
228
+ i++
229
+ }
230
+ } else out._.push(a)
231
+ }
232
+ return out
233
+ }
234
+
235
+ function main() {
236
+ const [exampleId, ...rest] = process.argv.slice(2)
237
+ const args = parseArgs(rest)
238
+ if (!exampleId || !args.dest || !args["sdk-dependency"]) {
239
+ console.error(
240
+ "Usage: node scripts/compose-example.mjs <id> --dest <dir> --sdk-dependency <spec>"
241
+ )
242
+ process.exit(2)
243
+ }
244
+ const res = composeExample({
245
+ exampleId,
246
+ destDir: resolve(args.dest),
247
+ sdkDependency: args["sdk-dependency"],
248
+ })
249
+ console.log(`composed: ${JSON.stringify(res)}`)
250
+ }
251
+
252
+ if (import.meta.url === `file://${process.argv[1]}`) main()