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.
- package/README.md +79 -7
- package/bin/create-grist-widget.mjs +485 -90
- package/examples/attachment-gallery/WidgetApp.tsx +202 -0
- package/examples/bulk-mark-done/WidgetApp.tsx +43 -0
- package/examples/declared-columns/WidgetApp.tsx +42 -0
- package/examples/form-edit/WidgetApp.tsx +225 -0
- package/examples/hello-world/WidgetApp.tsx +15 -0
- package/examples/mark-done/WidgetApp.tsx +44 -0
- package/examples/reads-demo/WidgetApp.tsx +69 -0
- package/examples/rest-fetch/WidgetApp.tsx +49 -0
- package/examples/safe-parse/WidgetApp.tsx +61 -0
- package/examples/schema-migration/WidgetApp.tsx +67 -0
- package/examples/schema-preview/WidgetApp.tsx +26 -0
- package/examples/slice-hooks/WidgetApp.tsx +47 -0
- package/examples/spreadsheet-grid/GristDataSheetGrid.tsx +376 -0
- package/examples/spreadsheet-grid/GristRdgGrid.tsx +189 -0
- package/examples/spreadsheet-grid/GristSchemaToolbar.tsx +293 -0
- package/examples/spreadsheet-grid/WidgetApp.tsx +133 -0
- package/examples/spreadsheet-grid/grist-rdg-choice-column.tsx +616 -0
- package/examples/spreadsheet-grid/grist-rdg-columns.ts +397 -0
- package/examples/task-board/WidgetApp.tsx +229 -0
- package/examples/theme-demo/WidgetApp.tsx +26 -0
- package/examples/widget-options/WidgetApp.tsx +34 -0
- package/examples/writes-demo/WidgetApp.tsx +75 -0
- package/manifest.json +304 -0
- package/package.json +14 -2
- package/src/git-gate.mjs +30 -0
- package/template/scripts/deploy.mjs +35 -1
- package/template/vite.config.ts +10 -0
- package/ui-source/ShadcnDataSheetGrid.css +117 -0
- package/ui-source/ShadcnDataSheetGrid.tsx +46 -0
- package/ui-source/badge.tsx +49 -0
- 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()
|