create-grist-widget 0.3.7 → 0.3.9
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/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,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useGrist,
|
|
3
|
+
useWidgetMetadata,
|
|
4
|
+
type UseGristOptions,
|
|
5
|
+
} from "grist-widget-sdk"
|
|
6
|
+
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
|
|
9
|
+
export const GRIST_OPTIONS: UseGristOptions = {
|
|
10
|
+
requiredAccess: "full",
|
|
11
|
+
columns: [{ name: "Title", type: "Text" }],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const WIDGET_METADATA = {
|
|
15
|
+
title: "Table writes",
|
|
16
|
+
description: "create, update, upsert, destroy via w.table.",
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export function WidgetApp() {
|
|
20
|
+
useWidgetMetadata(WIDGET_METADATA)
|
|
21
|
+
const w = useGrist()
|
|
22
|
+
|
|
23
|
+
if (!w.columnMappingStatus.ok) {
|
|
24
|
+
return <p className="p-4 text-sm">Map the Title column.</p>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex flex-wrap gap-2 p-4">
|
|
29
|
+
<Button
|
|
30
|
+
type="button"
|
|
31
|
+
onClick={() =>
|
|
32
|
+
void w.table.create({ fields: w.mapBack({ Title: "New" }) })
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
create
|
|
36
|
+
</Button>
|
|
37
|
+
<Button
|
|
38
|
+
type="button"
|
|
39
|
+
disabled={!w.record}
|
|
40
|
+
onClick={() => {
|
|
41
|
+
if (!w.record) return
|
|
42
|
+
void w.table.update({
|
|
43
|
+
id: w.record.id as number,
|
|
44
|
+
fields: w.mapBack({ Title: "Updated" }),
|
|
45
|
+
})
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
update
|
|
49
|
+
</Button>
|
|
50
|
+
<Button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={() =>
|
|
53
|
+
void w.table.upsert({
|
|
54
|
+
require: w.mapBack({ Title: "Existing" }),
|
|
55
|
+
fields: w.mapBack({ Title: "Existing" }),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
upsert
|
|
60
|
+
</Button>
|
|
61
|
+
<Button
|
|
62
|
+
type="button"
|
|
63
|
+
disabled={!w.records?.length}
|
|
64
|
+
onClick={() => {
|
|
65
|
+
const ids = (w.records ?? [])
|
|
66
|
+
.slice(0, 3)
|
|
67
|
+
.map((r) => r.id as number)
|
|
68
|
+
if (ids.length) void w.table.destroy(ids)
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
destroy (first 3)
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "attachment-gallery",
|
|
4
|
+
"title": "Attachment gallery",
|
|
5
|
+
"description": "Thumbnail grid of every attachment in the selected row. Showcases extractGristAttachmentIdsFromCell + useGrist().fetchAttachmentBlob.",
|
|
6
|
+
"requiredAccess": "full",
|
|
7
|
+
"columns": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Attachments",
|
|
10
|
+
"type": "Attachments"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"allowSelectBy": false,
|
|
14
|
+
"hasCustomOptions": false,
|
|
15
|
+
"hasExampleDocument": false,
|
|
16
|
+
"usesUi": [
|
|
17
|
+
"button"
|
|
18
|
+
],
|
|
19
|
+
"sourcePath": "examples/attachment-gallery/WidgetApp.tsx"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "bulk-mark-done",
|
|
23
|
+
"title": "Bulk mark done",
|
|
24
|
+
"description": "Bulk table.update with an array of row patches.",
|
|
25
|
+
"requiredAccess": "full",
|
|
26
|
+
"columns": [
|
|
27
|
+
{
|
|
28
|
+
"name": "Done",
|
|
29
|
+
"type": "Bool"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"allowSelectBy": false,
|
|
33
|
+
"hasCustomOptions": false,
|
|
34
|
+
"hasExampleDocument": false,
|
|
35
|
+
"usesUi": [
|
|
36
|
+
"button"
|
|
37
|
+
],
|
|
38
|
+
"sourcePath": "examples/bulk-mark-done/WidgetApp.tsx"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "declared-columns",
|
|
42
|
+
"title": "Declared columns",
|
|
43
|
+
"description": "Shows GRIST_OPTIONS column declarations and mappedRecord after the user maps columns.",
|
|
44
|
+
"requiredAccess": "full",
|
|
45
|
+
"columns": [
|
|
46
|
+
{
|
|
47
|
+
"name": "Title",
|
|
48
|
+
"type": "Text"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Done",
|
|
52
|
+
"type": "Bool"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "Tags",
|
|
56
|
+
"type": "ChoiceList",
|
|
57
|
+
"allowMultiple": true,
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"allowSelectBy": false,
|
|
62
|
+
"hasCustomOptions": false,
|
|
63
|
+
"hasExampleDocument": false,
|
|
64
|
+
"usesUi": [],
|
|
65
|
+
"sourcePath": "examples/declared-columns/WidgetApp.tsx"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "form-edit",
|
|
69
|
+
"title": "Row editor",
|
|
70
|
+
"description": "Edit the selected row in a polished form. Showcases column mappings, write status, and error UI.",
|
|
71
|
+
"requiredAccess": "full",
|
|
72
|
+
"columns": [
|
|
73
|
+
{
|
|
74
|
+
"name": "Title",
|
|
75
|
+
"type": "Text"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "Description",
|
|
79
|
+
"type": "Text",
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "Done",
|
|
84
|
+
"type": "Bool"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "Priority",
|
|
88
|
+
"type": "Text",
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"allowSelectBy": false,
|
|
93
|
+
"hasCustomOptions": false,
|
|
94
|
+
"hasExampleDocument": false,
|
|
95
|
+
"usesUi": [
|
|
96
|
+
"alert",
|
|
97
|
+
"button",
|
|
98
|
+
"input"
|
|
99
|
+
],
|
|
100
|
+
"sourcePath": "examples/form-edit/WidgetApp.tsx"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "hello-world",
|
|
104
|
+
"title": "hello-world",
|
|
105
|
+
"description": null,
|
|
106
|
+
"requiredAccess": "read table",
|
|
107
|
+
"columns": [],
|
|
108
|
+
"allowSelectBy": false,
|
|
109
|
+
"hasCustomOptions": false,
|
|
110
|
+
"hasExampleDocument": false,
|
|
111
|
+
"usesUi": [],
|
|
112
|
+
"sourcePath": "examples/hello-world/WidgetApp.tsx"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "mark-done",
|
|
116
|
+
"title": "Mark done",
|
|
117
|
+
"description": "Single-row write via table.update.",
|
|
118
|
+
"requiredAccess": "full",
|
|
119
|
+
"columns": [
|
|
120
|
+
{
|
|
121
|
+
"name": "Done",
|
|
122
|
+
"type": "Bool"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"allowSelectBy": false,
|
|
126
|
+
"hasCustomOptions": false,
|
|
127
|
+
"hasExampleDocument": false,
|
|
128
|
+
"usesUi": [
|
|
129
|
+
"button"
|
|
130
|
+
],
|
|
131
|
+
"sourcePath": "examples/mark-done/WidgetApp.tsx"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "reads-demo",
|
|
135
|
+
"title": "Reads demo",
|
|
136
|
+
"description": "listTables, fetchTableRows, fetchRow, fetchSelectedRecord.",
|
|
137
|
+
"requiredAccess": "read table",
|
|
138
|
+
"columns": [],
|
|
139
|
+
"allowSelectBy": false,
|
|
140
|
+
"hasCustomOptions": false,
|
|
141
|
+
"hasExampleDocument": false,
|
|
142
|
+
"usesUi": [],
|
|
143
|
+
"sourcePath": "examples/reads-demo/WidgetApp.tsx"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "rest-fetch",
|
|
147
|
+
"title": "REST fetch",
|
|
148
|
+
"description": "fetchWithAuth against the Grist REST API.",
|
|
149
|
+
"requiredAccess": "full",
|
|
150
|
+
"columns": [],
|
|
151
|
+
"allowSelectBy": false,
|
|
152
|
+
"hasCustomOptions": false,
|
|
153
|
+
"hasExampleDocument": false,
|
|
154
|
+
"usesUi": [
|
|
155
|
+
"button"
|
|
156
|
+
],
|
|
157
|
+
"sourcePath": "examples/rest-fetch/WidgetApp.tsx"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "safe-parse",
|
|
161
|
+
"title": "Safe parse",
|
|
162
|
+
"description": "safeParseGristTableData with per-cell issues.",
|
|
163
|
+
"requiredAccess": "read table",
|
|
164
|
+
"columns": [],
|
|
165
|
+
"allowSelectBy": false,
|
|
166
|
+
"hasCustomOptions": false,
|
|
167
|
+
"hasExampleDocument": false,
|
|
168
|
+
"usesUi": [],
|
|
169
|
+
"sourcePath": "examples/safe-parse/WidgetApp.tsx"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "schema-migration",
|
|
173
|
+
"title": "Schema migration",
|
|
174
|
+
"description": "applyActions with gristAddVisibleColumnAction, gristRenameColumnAction, and gristRemoveColumnAction.",
|
|
175
|
+
"requiredAccess": "full",
|
|
176
|
+
"columns": [],
|
|
177
|
+
"allowSelectBy": false,
|
|
178
|
+
"hasCustomOptions": false,
|
|
179
|
+
"hasExampleDocument": false,
|
|
180
|
+
"usesUi": [
|
|
181
|
+
"button"
|
|
182
|
+
],
|
|
183
|
+
"sourcePath": "examples/schema-migration/WidgetApp.tsx"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"id": "schema-preview",
|
|
187
|
+
"title": "Schema preview",
|
|
188
|
+
"description": "useGristSchema with replicaRowMode schema+samples for LLM context.",
|
|
189
|
+
"requiredAccess": "read table",
|
|
190
|
+
"columns": [],
|
|
191
|
+
"allowSelectBy": false,
|
|
192
|
+
"hasCustomOptions": false,
|
|
193
|
+
"hasExampleDocument": false,
|
|
194
|
+
"usesUi": [],
|
|
195
|
+
"sourcePath": "examples/schema-preview/WidgetApp.tsx"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "slice-hooks",
|
|
199
|
+
"title": "Slice hooks",
|
|
200
|
+
"description": "useGristStatus, useGristSelection, useGristWrites, useGristTheme in separate child components.",
|
|
201
|
+
"requiredAccess": "full",
|
|
202
|
+
"columns": [
|
|
203
|
+
{
|
|
204
|
+
"name": "Title",
|
|
205
|
+
"type": "Text"
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"allowSelectBy": false,
|
|
209
|
+
"hasCustomOptions": false,
|
|
210
|
+
"hasExampleDocument": false,
|
|
211
|
+
"usesUi": [],
|
|
212
|
+
"sourcePath": "examples/slice-hooks/WidgetApp.tsx"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"id": "spreadsheet-grid",
|
|
216
|
+
"title": "Spreadsheet grid",
|
|
217
|
+
"description": "Selected Grist section rendered as a fully-synced, editable spreadsheet via react-datasheet-grid, plus a schema toolbar and replica-document export.",
|
|
218
|
+
"requiredAccess": "full",
|
|
219
|
+
"columns": [],
|
|
220
|
+
"allowSelectBy": true,
|
|
221
|
+
"hasCustomOptions": false,
|
|
222
|
+
"hasExampleDocument": false,
|
|
223
|
+
"usesUi": [
|
|
224
|
+
"button",
|
|
225
|
+
"input",
|
|
226
|
+
"ShadcnDataSheetGrid"
|
|
227
|
+
],
|
|
228
|
+
"sourcePath": "examples/spreadsheet-grid/WidgetApp.tsx"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"id": "task-board",
|
|
232
|
+
"title": "Task board",
|
|
233
|
+
"description": "Mini Kanban board grouped by Status. Drag a card to update Status; click to focus the row in linked sections.",
|
|
234
|
+
"requiredAccess": "full",
|
|
235
|
+
"columns": [
|
|
236
|
+
{
|
|
237
|
+
"name": "Title",
|
|
238
|
+
"type": "Text"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "Status",
|
|
242
|
+
"type": "Text"
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"name": "Priority",
|
|
246
|
+
"type": "Text",
|
|
247
|
+
"optional": true
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
"allowSelectBy": true,
|
|
251
|
+
"hasCustomOptions": false,
|
|
252
|
+
"hasExampleDocument": false,
|
|
253
|
+
"usesUi": [
|
|
254
|
+
"badge",
|
|
255
|
+
"card"
|
|
256
|
+
],
|
|
257
|
+
"sourcePath": "examples/task-board/WidgetApp.tsx"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "theme-demo",
|
|
261
|
+
"title": "Theme",
|
|
262
|
+
"description": "Read w.theme from the host document.",
|
|
263
|
+
"requiredAccess": "read table",
|
|
264
|
+
"columns": [],
|
|
265
|
+
"allowSelectBy": false,
|
|
266
|
+
"hasCustomOptions": false,
|
|
267
|
+
"hasExampleDocument": false,
|
|
268
|
+
"usesUi": [],
|
|
269
|
+
"sourcePath": "examples/theme-demo/WidgetApp.tsx"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"id": "widget-options",
|
|
273
|
+
"title": "Widget options",
|
|
274
|
+
"description": "Persist UI state with setWidgetOption / widgetOptions.",
|
|
275
|
+
"requiredAccess": "read table",
|
|
276
|
+
"columns": [],
|
|
277
|
+
"allowSelectBy": false,
|
|
278
|
+
"hasCustomOptions": false,
|
|
279
|
+
"hasExampleDocument": false,
|
|
280
|
+
"usesUi": [
|
|
281
|
+
"button"
|
|
282
|
+
],
|
|
283
|
+
"sourcePath": "examples/widget-options/WidgetApp.tsx"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"id": "writes-demo",
|
|
287
|
+
"title": "Table writes",
|
|
288
|
+
"description": "create, update, upsert, destroy via w.table.",
|
|
289
|
+
"requiredAccess": "full",
|
|
290
|
+
"columns": [
|
|
291
|
+
{
|
|
292
|
+
"name": "Title",
|
|
293
|
+
"type": "Text"
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
"allowSelectBy": false,
|
|
297
|
+
"hasCustomOptions": false,
|
|
298
|
+
"hasExampleDocument": false,
|
|
299
|
+
"usesUi": [
|
|
300
|
+
"button"
|
|
301
|
+
],
|
|
302
|
+
"sourcePath": "examples/writes-demo/WidgetApp.tsx"
|
|
303
|
+
}
|
|
304
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-grist-widget",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Scaffold a new Grist custom widget from the grist-widget-sdk Vite template",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
|
+
"src",
|
|
15
16
|
"template",
|
|
17
|
+
"vendor",
|
|
18
|
+
"examples",
|
|
19
|
+
"ui-source",
|
|
20
|
+
"manifest.json",
|
|
16
21
|
"LICENSE"
|
|
17
22
|
],
|
|
18
23
|
"publishConfig": {
|
|
@@ -33,7 +38,14 @@
|
|
|
33
38
|
"cli",
|
|
34
39
|
"create-grist-widget"
|
|
35
40
|
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@clack/prompts": "^1.7.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vitest": "^4.1.4"
|
|
46
|
+
},
|
|
36
47
|
"scripts": {
|
|
37
|
-
"build": "node scripts/build-template.mjs"
|
|
48
|
+
"build": "node scripts/build-template.mjs",
|
|
49
|
+
"test": "vitest run"
|
|
38
50
|
}
|
|
39
51
|
}
|
package/src/git-gate.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The safety gate `use <example-id>` rests on: overlayExampleSrc overwrites
|
|
2
|
+
// src/App.tsx (and any other example-specific files) in place, so the only
|
|
3
|
+
// thing that makes that recoverable is a clean git tree to diff/revert
|
|
4
|
+
// against. `create`/`init` always leave a repo behind today (see
|
|
5
|
+
// bin/create-grist-widget.mjs's gitInitCommit), but `use` checks for one
|
|
6
|
+
// explicitly rather than assuming it — the plan's own Done-when line calls
|
|
7
|
+
// this out: "it rests on the CLI's git init, which a future --no-git would
|
|
8
|
+
// silently remove." Checking `git rev-parse` (not just "does .git exist")
|
|
9
|
+
// is what keeps this correct independent of that future flag.
|
|
10
|
+
//
|
|
11
|
+
// Exported separately from bin/create-grist-widget.mjs so it has its own
|
|
12
|
+
// unit test (tests/git-gate.test.mjs) — see docs/planning/2026-08-execution-plan.md
|
|
13
|
+
// §105 ("the git gate has its own test").
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from "node:child_process"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} cwd
|
|
19
|
+
* @returns {{ ok: true } | { ok: false, reason: "no-repo" | "dirty" }}
|
|
20
|
+
*/
|
|
21
|
+
export function checkGitGate(cwd) {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, stdio: "pipe" })
|
|
24
|
+
} catch {
|
|
25
|
+
return { ok: false, reason: "no-repo" }
|
|
26
|
+
}
|
|
27
|
+
const status = execFileSync("git", ["status", "--porcelain"], { cwd, encoding: "utf8" })
|
|
28
|
+
if (status.trim()) return { ok: false, reason: "dirty" }
|
|
29
|
+
return { ok: true }
|
|
30
|
+
}
|
|
@@ -132,11 +132,45 @@ export function plan(siteDir, repo, event, ref, { force = false } = {}) {
|
|
|
132
132
|
if (!version) throw new Error("package.json has no version")
|
|
133
133
|
const versionDir = join(siteDir, `v${version}`)
|
|
134
134
|
const exists = isGenuineRelease(versionDir, repo)
|
|
135
|
+
const skip = exists && !force
|
|
136
|
+
// T-07: "merging without bumping the version publishes nothing" was a
|
|
137
|
+
// silent no-op, documented in two places (this file's own header comment,
|
|
138
|
+
// and AGENTS.md's Anti-patterns) instead of made unrepresentable. Turn it
|
|
139
|
+
// loud for the one case that actually means "I meant to publish and
|
|
140
|
+
// didn't": a genuine `push` to main (not `workflow_dispatch`, which has
|
|
141
|
+
// its own explicit --force for intentional rebuilds) landed on a repo
|
|
142
|
+
// that's already released before, and the version it resolved to was
|
|
143
|
+
// already published. If HEAD's parent had the *same* version, nothing
|
|
144
|
+
// about this push looks like an intentional re-run -- fail the workflow
|
|
145
|
+
// step instead of quietly reporting success. Falls back to today's silent
|
|
146
|
+
// skip if HEAD~1 doesn't resolve (e.g. a squashed/rebased history, or
|
|
147
|
+
// this being the repo's very first commit) -- this is a backstop for the
|
|
148
|
+
// common case, not a guarantee for every git topology.
|
|
149
|
+
if (event === "push" && !firstRelease && skip) {
|
|
150
|
+
try {
|
|
151
|
+
const previousPkgJson = execFileSync("git", ["show", "HEAD~1:package.json"], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
})
|
|
154
|
+
const previousVersion = JSON.parse(previousPkgJson).version
|
|
155
|
+
if (previousVersion === version) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`deploy: this push to main would publish nothing -- v${version} was already released and ` +
|
|
158
|
+
`package.json's version is unchanged from the previous commit. Bump "version" in package.json ` +
|
|
159
|
+
`before merging if you meant to publish new content (see AGENTS.md's Anti-patterns).`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// Re-throw our own message; swallow only git/JSON failures (HEAD~1
|
|
164
|
+
// missing, package.json didn't exist at that commit, etc.) and fall
|
|
165
|
+
// through to the existing silent-skip behavior for those cases.
|
|
166
|
+
if (err instanceof Error && err.message.startsWith("deploy: this push")) throw err
|
|
167
|
+
}
|
|
168
|
+
}
|
|
135
169
|
return {
|
|
136
170
|
context: "release",
|
|
137
171
|
version,
|
|
138
172
|
base: basePathFor(repo, "release", version),
|
|
139
|
-
skip
|
|
173
|
+
skip,
|
|
140
174
|
reason: exists ? (force ? "force-rebuild" : "already-published") : "new-version",
|
|
141
175
|
// Exposed so the workflow can gate a one-time repo cleanup (prune stray
|
|
142
176
|
// branches, reset dev to main) on exactly this signal -- see
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme bridge: react-datasheet-grid uses :root defaults and several hard-coded
|
|
3
|
+
* colors. This wrapper scopes shadcn design tokens (--background, --border,
|
|
4
|
+
* oklch, …) so the grid matches the rest of the UI in light/dark mode.
|
|
5
|
+
*/
|
|
6
|
+
.shadcn-dsg {
|
|
7
|
+
--dsg-border-color: var(--border);
|
|
8
|
+
--dsg-selection-border-color: var(--ring);
|
|
9
|
+
--dsg-selection-border-radius: 0;
|
|
10
|
+
--dsg-selection-border-width: 2px;
|
|
11
|
+
--dsg-selection-background-color: color-mix(in oklch, var(--primary) 14%, transparent);
|
|
12
|
+
--dsg-selection-disabled-border-color: color-mix(
|
|
13
|
+
in oklch,
|
|
14
|
+
var(--muted-foreground) 50%,
|
|
15
|
+
transparent
|
|
16
|
+
);
|
|
17
|
+
--dsg-selection-disabled-background-color: color-mix(
|
|
18
|
+
in oklch,
|
|
19
|
+
var(--muted) 55%,
|
|
20
|
+
transparent
|
|
21
|
+
);
|
|
22
|
+
--dsg-corner-indicator-width: 10px;
|
|
23
|
+
--dsg-header-text-color: var(--muted-foreground);
|
|
24
|
+
--dsg-header-active-text-color: var(--foreground);
|
|
25
|
+
--dsg-cell-background-color: var(--background);
|
|
26
|
+
--dsg-cell-disabled-background-color: var(--muted);
|
|
27
|
+
--dsg-transition-duration: 0.1s;
|
|
28
|
+
--dsg-expand-rows-indicator-width: 10px;
|
|
29
|
+
--dsg-scroll-shadow-width: 7px;
|
|
30
|
+
--dsg-scroll-shadow-color: color-mix(in oklch, var(--foreground) 14%, transparent);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.shadcn-dsg .dsg-container {
|
|
34
|
+
background: var(--background);
|
|
35
|
+
color: var(--foreground);
|
|
36
|
+
font-family: inherit;
|
|
37
|
+
font-size: 0.875rem;
|
|
38
|
+
line-height: 1.25rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.shadcn-dsg .dsg-row-header .dsg-cell {
|
|
42
|
+
background: var(--muted) !important;
|
|
43
|
+
color: var(--muted-foreground);
|
|
44
|
+
font-size: 0.75rem;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
letter-spacing: 0.05em;
|
|
47
|
+
text-transform: uppercase;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.shadcn-dsg .dsg-cell-header.dsg-cell-gutter {
|
|
51
|
+
box-shadow:
|
|
52
|
+
1px 0 var(--dsg-border-color),
|
|
53
|
+
0 1px var(--dsg-border-color);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.shadcn-dsg .dsg-active-cell-focus {
|
|
57
|
+
box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 55%, transparent);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.shadcn-dsg .dsg-add-row {
|
|
61
|
+
background: var(--muted);
|
|
62
|
+
color: var(--foreground);
|
|
63
|
+
border-color: var(--border);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.shadcn-dsg .dsg-add-row-btn,
|
|
67
|
+
.shadcn-dsg .dsg-add-row-input {
|
|
68
|
+
background: var(--background);
|
|
69
|
+
color: var(--foreground);
|
|
70
|
+
border-color: var(--border);
|
|
71
|
+
border-radius: var(--radius);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.shadcn-dsg .dsg-add-row-btn:hover,
|
|
75
|
+
.shadcn-dsg .dsg-add-row-input:hover {
|
|
76
|
+
border-color: var(--input);
|
|
77
|
+
background: color-mix(in oklch, var(--accent) 35%, var(--background));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.shadcn-dsg .dsg-add-row-btn:focus,
|
|
81
|
+
.shadcn-dsg .dsg-add-row-input:focus {
|
|
82
|
+
border-color: var(--ring);
|
|
83
|
+
outline: none;
|
|
84
|
+
box-shadow: 0 0 0 2px color-mix(in oklch, var(--ring) 35%, transparent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.shadcn-dsg .dsg-add-row-btn::before,
|
|
88
|
+
.shadcn-dsg .dsg-add-row-btn::after {
|
|
89
|
+
background: var(--muted-foreground);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.shadcn-dsg .dsg-context-menu {
|
|
93
|
+
background: var(--popover);
|
|
94
|
+
color: var(--popover-foreground);
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
border-radius: var(--radius);
|
|
97
|
+
box-shadow:
|
|
98
|
+
0 4px 6px -1px color-mix(in oklch, var(--foreground) 8%, transparent),
|
|
99
|
+
0 2px 4px -2px color-mix(in oklch, var(--foreground) 8%, transparent);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.shadcn-dsg .dsg-context-menu-item:hover {
|
|
103
|
+
background: var(--accent);
|
|
104
|
+
color: var(--accent-foreground);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.shadcn-dsg .dsg-expand-rows-rect {
|
|
108
|
+
background: color-mix(in oklch, var(--primary) 10%, transparent);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.shadcn-dsg .dsg-input {
|
|
112
|
+
color: var(--foreground);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.shadcn-dsg .dsg-input::placeholder {
|
|
116
|
+
color: var(--muted-foreground);
|
|
117
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { forwardRef } from "react"
|
|
2
|
+
import {
|
|
3
|
+
DynamicDataSheetGrid,
|
|
4
|
+
type DataSheetGridProps,
|
|
5
|
+
type DataSheetGridRef,
|
|
6
|
+
} from "react-datasheet-grid"
|
|
7
|
+
import "react-datasheet-grid/dist/style.css"
|
|
8
|
+
|
|
9
|
+
import { cn } from "@/lib/utils"
|
|
10
|
+
|
|
11
|
+
import "./ShadcnDataSheetGrid.css"
|
|
12
|
+
|
|
13
|
+
export type ShadcnDataSheetGridProps<T> = DataSheetGridProps<T> & {
|
|
14
|
+
/** Classes on the outer wrapper (border, radius, layout). */
|
|
15
|
+
wrapperClassName?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Themed datasheet grid aligned with shadcn tokens (light/dark).
|
|
20
|
+
* Uses `DynamicDataSheetGrid` so `columns` updates apply after mount.
|
|
21
|
+
*/
|
|
22
|
+
type ShadcnDataSheetGridComponent = <T>(
|
|
23
|
+
props: ShadcnDataSheetGridProps<T> & { ref?: React.Ref<DataSheetGridRef> }
|
|
24
|
+
) => React.ReactElement | null
|
|
25
|
+
|
|
26
|
+
const ShadcnDataSheetGridInner = <T,>(
|
|
27
|
+
{ wrapperClassName, ...props }: ShadcnDataSheetGridProps<T>,
|
|
28
|
+
ref: React.ForwardedRef<DataSheetGridRef>
|
|
29
|
+
) => {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
"shadcn-dsg min-h-0 min-w-0 overflow-hidden rounded-md border border-border bg-background text-foreground shadow-sm",
|
|
34
|
+
wrapperClassName
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<DynamicDataSheetGrid ref={ref} {...props} />
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const ShadcnDataSheetGrid = forwardRef(
|
|
43
|
+
ShadcnDataSheetGridInner
|
|
44
|
+
) as ShadcnDataSheetGridComponent
|
|
45
|
+
|
|
46
|
+
export type { DataSheetGridProps, DataSheetGridRef } from "react-datasheet-grid"
|