@danxbot/plugin-sdk 0.2.39 → 0.2.42

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @danxbot/plugin-sdk
2
2
 
3
- Build a 50 Arms **card-type plugin**: a small React bundle that gives a card type its own card view (`CardView`) and/or card-list view (`CardListView`), loaded into 50 Arms' own page with `import()`.
3
+ Build a 50 Arms **plugin**: a small React bundle that gives a card type its own card view (`CardView`), adds a card-list view (`CardListView`), and/or adds **Widgets** to the Dashboard's Widget Library, loaded into 50 Arms' own page with `import()`.
4
4
 
5
5
  A plugin does not bundle React, `@danxbot/ui` or the card features. The app serves them under fixed import specifiers through an import map, so a plugin shares the app's single React and renders the app's own components:
6
6
 
@@ -45,6 +45,52 @@ A plugin styles itself with its own CSS (import a `.css` file; the build emits i
45
45
 
46
46
  The build refuses to bundle a copy of React, React DOM, `@danxbot/ui` or `react-router`: import only the specifiers above. For routing, use the kit's `Link` and `useNavigate`; `StandardCardPage` reads the plan the page was opened from out of the URL, so a view that wraps it passes only `{card, mutation}`.
47
47
 
48
+ ## Widgets
49
+
50
+ A plugin can add Widgets: tiles a person places on their Dashboard from the Widget Library. A Widget is a component the entry exports plus a declaration, the built-in Widgets' own shape, from `@danxbot/plugin-sdk/widgets`:
51
+
52
+ - `key`: the Widget's own key inside the plugin, lowercase words joined by `-` (a placement stores `plugin:<plugin key>:<key>`);
53
+ - `size`: its `default`, `min` and `max` size in cells of the 24-column grid;
54
+ - `config` and `settings`: the fields a placement may set (`config`, fixed by whoever edits the surface) and a viewer may override (`settings`). Each field is a `string`, `number` or `boolean` with a `default` and a `label` (`{ text: "..." }`, the words the Configure form shows); a `string` with `enum` also needs `optionLabels`, a label for every option;
55
+ - `readsSurfaceSettings`: which surface-wide settings it reads (`date_range`, `keyword`);
56
+ - `needsHostContext`: the host facts it cannot draw without (none on the Dashboard).
57
+
58
+ `src/widgets.ts`:
59
+
60
+ ```ts
61
+ import { declareWidget, type PluginWidget } from "@danxbot/plugin-sdk/widgets";
62
+
63
+ export const OPEN_BUGS = declareWidget({
64
+ key: "open-bugs",
65
+ size: { default: { w: 6, h: 4 }, min: { w: 3, h: 2 }, max: { w: 24, h: 12 } },
66
+ config: {},
67
+ settings: { board: { type: "string", label: { text: "Board" }, default: "", maxLength: 200 } },
68
+ readsSurfaceSettings: [],
69
+ needsHostContext: [],
70
+ });
71
+
72
+ export const WIDGETS: PluginWidget[] = [
73
+ { export: "OpenBugs", title: "Open bugs", description: "How many bugs are open on each board.", declaration: OPEN_BUGS },
74
+ ];
75
+ ```
76
+
77
+ The entry exports the component, which receives `WidgetProps` (its `config` and `settings` with the defaults filled in, the surface settings it reads, and `saveSettings` for the viewer's own choices) and reads its data with the card kit's hooks:
78
+
79
+ ```tsx
80
+ import type { WidgetProps } from "@danxbot/plugin-sdk/widgets";
81
+ import { OPEN_BUGS } from "./widgets";
82
+
83
+ export function OpenBugs({ settings }: WidgetProps<typeof OPEN_BUGS.config, typeof OPEN_BUGS.settings>) {
84
+ return <p>{settings.board === "" ? "Every board" : settings.board}</p>;
85
+ }
86
+ ```
87
+
88
+ `vite.config.ts` names them: `danxPlugin({ widgets: WIDGETS })`. The build records them in `danx-plugin.json` (`widgets`) and refuses a Widget whose declaration cannot work or whose component the entry does not export; the upload checks the declarations again. A plugin may declare only Widgets and export no view; it then publishes without a screenshot (the preview draws views only).
89
+
90
+ Who sees a plugin's Widget is who it runs for: a revision you publish runs for nobody until you try it, so your Widget is listed in your own Widget Library as "Not approved", and choosing it asks before it runs in your session; it is private to you. Once an approver approves the revision and a team admin makes it active, it is the team's.
91
+
92
+ **For an agent asked to make a widget** ("make me a widget that shows ..."): work in the person's own repo, in one of their plugin projects or a new one; add the declaration, the component and the `widgets` option as above; commit; publish with `plugin_publish` from the person's own session (the revision is then theirs); then tell them to open their Widget Library and pick it.
93
+
48
94
  ## The card kit is a contract
49
95
 
50
96
  A runtime export of `@danxbot/card-kit@1`, and its parameters, is never removed or changed in place; the keys of the exported data types follow the Versioning section below. A breaking change is `@danxbot/card-kit@2`, served beside `@1` until no approved plugin revision needs it. A plugin's manifest records the majors it was built against, and the app refuses to run a revision built for a major it no longer serves.
@@ -46,10 +46,15 @@ declare module "@danxbot/card-kit@1" {
46
46
  type PacingLimit = "five_hour" | "weekly" | "spend";
47
47
  type ProvenanceSourceKind = "card" | "comment" | "session_thread" | "problem" | "url";
48
48
  type ProvenancePartyKind = "person" | "agent";
49
+ type ResourceType = "plan" | "problem" | "issue" | "plan_record" | "plan_section" | "design" | "decision";
50
+ interface ResourceRef {
51
+ type: ResourceType;
52
+ id: string;
53
+ }
49
54
 
50
55
  type IssueStatus$1 = "Review" | "ToDo" | "In Progress" | "Backlog" | "Done" | "Cancelled";
51
56
  type EffortLevelName = "min" | "very_low" | "low" | "medium" | "high" | "very_high" | "max";
52
- type IssueProblemType = "question" | "action" | "design" | "decision_review";
57
+ type IssueProblemType = "design" | "question" | "action" | "decision_review";
53
58
  type DecisionOutcome = "answered" | "rejected";
54
59
  type IssueDependencyKind = "depends_on" | "conflict_on";
55
60
  type IssueDispatchKind = "work" | "triage" | "recovery" | "flesh-out" | "chat" | "main-sync-repair" | "worktree-maintenance" | "manual" | "landing" | `gate-${string}`;
@@ -115,10 +120,9 @@ declare module "@danxbot/card-kit@1" {
115
120
  triage_enabled: boolean;
116
121
  content_hash: string;
117
122
  }
118
- type CommentResourceType = "issue" | "plan" | "problem" | "design" | "decision";
119
123
  interface CommentResourceTag {
120
124
  tag: string;
121
- resource_type: CommentResourceType | null;
125
+ resource_type: ResourceType | null;
122
126
  resource_id: string | null;
123
127
  }
124
128
  interface AuthorV2 {
@@ -230,14 +234,18 @@ declare module "@danxbot/card-kit@1" {
230
234
  at: string | null;
231
235
  }
232
236
 
233
- type QualityGatePhase = "plan" | "code";
234
-
235
237
  interface ViaKey {
236
238
  id: number;
237
239
  name: string;
238
240
  }
239
241
  type MirrorProviderId = "trello" | "slack" | "asana";
240
242
  type SourceOrigin = "danxbot" | MirrorProviderId | "";
243
+ type WorkMarkResourceType = "plan" | "problem" | "issue" | "plan_record" | "plan_section" | "design" | "decision";
244
+ interface WorkMarkResource extends ResourceRef {
245
+ type: WorkMarkResourceType;
246
+ }
247
+
248
+ type QualityGatePhase = "plan" | "code";
241
249
  type ControlValue = boolean | string;
242
250
 
243
251
  type ControlSource = "card" | "plan" | "board" | "team" | "system";
@@ -269,6 +277,7 @@ declare module "@danxbot/card-kit@1" {
269
277
  } | null;
270
278
  landed_sha: string | null;
271
279
  failure_reason: string | null;
280
+ waiting_board_id: string | null;
272
281
  approve_solution_id: number;
273
282
  withdraw_solution_id: number;
274
283
  }
@@ -283,6 +292,10 @@ declare module "@danxbot/card-kit@1" {
283
292
  by_name: string | null;
284
293
  at: string | null;
285
294
  }
295
+ interface DecisionRecordPage {
296
+ offset: number;
297
+ target_found: boolean | null;
298
+ }
286
299
 
287
300
  type ListType = "archived" | "review" | "ready" | "in_progress" | "completed" | "cancelled";
288
301
 
@@ -395,6 +408,7 @@ declare module "@danxbot/card-kit@1" {
395
408
  interface V2CopyDecisionVersion {
396
409
  version_no: number;
397
410
  action: DecisionRecordAction;
411
+ title: string;
398
412
  text: string;
399
413
  kind: DecisionRecordKind;
400
414
  impact: number;
@@ -954,6 +968,7 @@ declare module "@danxbot/card-kit@1" {
954
968
  start_commit?: undefined | null | { at: string; sha: string | null; error: "no_repo" | "not_github_repo" | "no_token" | "github_refused" | "github_unreachable" | "unreadable_tip" | "unexpected" | null; error_detail: string | null; };
955
969
  problem_total?: undefined | number;
956
970
  decision_record_total?: undefined | number;
971
+ decision_record_page?: undefined | DecisionRecordPage;
957
972
  decision_record_approved_total?: undefined | number;
958
973
  lane_header?: undefined | false | true;
959
974
  design_count?: undefined | number;
@@ -1029,6 +1044,7 @@ declare module "@danxbot/card-kit@1" {
1029
1044
  record_created_at: string;
1030
1045
  updated_at: string;
1031
1046
  action: "created" | "changed" | "deleted" | "restored";
1047
+ title: string;
1032
1048
  text: string;
1033
1049
  kind: "design" | "business_rule" | "architecture" | "other";
1034
1050
  impact: number;
@@ -1044,6 +1060,7 @@ declare module "@danxbot/card-kit@1" {
1044
1060
  via_key: null | ViaKey;
1045
1061
  created_at: string;
1046
1062
  comment_count: number;
1063
+ unread_comment_count: number;
1047
1064
  versions?: undefined | (Array<{
1048
1065
  id: number;
1049
1066
  record_id: number;
@@ -1051,6 +1068,7 @@ declare module "@danxbot/card-kit@1" {
1051
1068
  version_no: number;
1052
1069
  approval: DecisionApprovalState;
1053
1070
  action: "created" | "changed" | "deleted" | "restored";
1071
+ title: string;
1054
1072
  text: string;
1055
1073
  kind: "design" | "business_rule" | "architecture" | "other";
1056
1074
  impact: number;
@@ -1084,6 +1102,7 @@ declare module "@danxbot/card-kit@1" {
1084
1102
  version_no: number;
1085
1103
  approval: DecisionApprovalState;
1086
1104
  action: "created" | "changed" | "deleted" | "restored";
1105
+ title: string;
1087
1106
  text: string;
1088
1107
  kind: "design" | "business_rule" | "architecture" | "other";
1089
1108
  impact: number;
@@ -1109,6 +1128,15 @@ declare module "@danxbot/card-kit@1" {
1109
1128
  by_name: string;
1110
1129
  at: string;
1111
1130
  }>);
1131
+ work_marks?: undefined | Array<{
1132
+ id: number;
1133
+ via_key_id: number;
1134
+ resource: WorkMarkResource;
1135
+ note: string;
1136
+ by: AuthorV2;
1137
+ started_at: string;
1138
+ expires_at: string;
1139
+ }>;
1112
1140
  designs?: undefined | (Array<{
1113
1141
  id: number;
1114
1142
  design_no: number;
@@ -1263,7 +1291,7 @@ declare module "@danxbot/card-kit@1" {
1263
1291
  phase: "plan" | "code";
1264
1292
  };
1265
1293
  type ProblemViewV2 = {
1266
- type: "question" | "action" | "design" | "decision_review";
1294
+ type: "design" | "question" | "action" | "decision_review";
1267
1295
  open: boolean;
1268
1296
  ordinal: number;
1269
1297
  id: number;
@@ -1272,6 +1300,7 @@ declare module "@danxbot/card-kit@1" {
1272
1300
  content_hash: string;
1273
1301
  summary: null | string;
1274
1302
  comment_count: number;
1303
+ unread_comment_count: number;
1275
1304
  decisions: Array<{
1276
1305
  id: number;
1277
1306
  problem_id: number;
@@ -1310,6 +1339,7 @@ declare module "@danxbot/card-kit@1" {
1310
1339
  version_no: number;
1311
1340
  approval: DecisionApprovalState;
1312
1341
  action: "created" | "changed" | "deleted" | "restored";
1342
+ title: string;
1313
1343
  text: string;
1314
1344
  kind: "design" | "business_rule" | "architecture" | "other";
1315
1345
  impact: number;
package/dist/cli.js CHANGED
@@ -5891,7 +5891,7 @@ var CARD_KIT_LIBRARY = "@danxbot/card-kit";
5891
5891
  var PLUGIN_RUNTIME_LIBRARIES = [
5892
5892
  { name: "react", major: 19 },
5893
5893
  { name: "react-dom", major: 19 },
5894
- { name: "@danxbot/ui", major: 28 },
5894
+ { name: "@danxbot/ui", major: 30 },
5895
5895
  { name: CARD_KIT_LIBRARY, major: CARD_KIT_MAJOR }
5896
5896
  ];
5897
5897
  var PLUGIN_RUNTIME_SPECIFIERS = [
@@ -5932,7 +5932,7 @@ ${output.trim()}`));
5932
5932
  }
5933
5933
  function isManifest(value) {
5934
5934
  const manifest = value;
5935
- return typeof manifest === "object" && manifest !== null && typeof manifest.entry === "string" && Array.isArray(manifest.exports) && Array.isArray(manifest.files) && manifest.files.every((file) => typeof file?.path === "string");
5935
+ return typeof manifest === "object" && manifest !== null && typeof manifest.entry === "string" && Array.isArray(manifest.exports) && Array.isArray(manifest.widgets) && Array.isArray(manifest.files) && manifest.files.every((file) => typeof file?.path === "string");
5936
5936
  }
5937
5937
  async function readBuiltPlugin(projectDir) {
5938
5938
  const distDir = join3(resolve2(projectDir), PLUGIN_DIST_DIR);
@@ -5942,7 +5942,7 @@ async function readBuiltPlugin(projectDir) {
5942
5942
  } catch (err) {
5943
5943
  throw new Error(`danx-plugin: the build left no readable ${PLUGIN_DIST_DIR}/${PLUGIN_MANIFEST_FILE} in ${projectDir} (is the danxPlugin() preset in vite.config?): ${err.message}`);
5944
5944
  }
5945
- if (!isManifest(manifest)) throw new Error(`danx-plugin: ${PLUGIN_DIST_DIR}/${PLUGIN_MANIFEST_FILE} in ${projectDir} has no entry, no exports list or no files list of paths`);
5945
+ if (!isManifest(manifest)) throw new Error(`danx-plugin: ${PLUGIN_DIST_DIR}/${PLUGIN_MANIFEST_FILE} in ${projectDir} has no entry, no exports list, no widgets list or no files list of paths`);
5946
5946
  const files = await Promise.all(manifest.files.map(async (file) => ({ path: file.path, bytes: await readFile2(join3(distDir, file.path)) })));
5947
5947
  return { manifest, files };
5948
5948
  }
package/dist/publish.d.ts CHANGED
@@ -8,6 +8,95 @@ declare function sourceFiles(projectDir: string): Promise<string[]>;
8
8
  /** The project's source as a gzip-compressed tar archive, ready for `source_archive_base64`. The same files give the same bytes (fixed mtime, `portable`). */
9
9
  declare function archiveSource(projectDir: string): Promise<Buffer>;
10
10
 
11
+ /** The names of the surface-wide settings, one per member of {@link SurfaceWideSettings} (a new member fails `tsc` here). */
12
+ declare const SURFACE_WIDE_SETTING_KEYS: readonly ["date_range", "keyword"];
13
+ type SurfaceWideSettingKey = (typeof SURFACE_WIDE_SETTING_KEYS)[number];
14
+
15
+ /**
16
+ * DX-6233 / DX-6269 (PLAN-96 G-6) - the schema a Widget's config and settings are declared in, and the check of a value against it. Import-free of Node: the
17
+ * app imports this module at runtime (`@backend/widget-surfaces/value-schema.js`) to draw a placement's form and to check what is stored, and the server
18
+ * imports the same module to refuse a write the schema rejects, so the two can never disagree about what a Widget accepts.
19
+ *
20
+ * Every field carries its default, so the schema is also where a Widget's defaults live. A value is checked as a (possibly partial) set of the schema's
21
+ * fields: a key the schema does not name, or a value of the wrong type, is an issue, never read as a default.
22
+ */
23
+ /**
24
+ * DX-6254 - the words that name a field, or one option of a field: a built-in Widget's message id, which the app looks up in the person's language, or the text a
25
+ * plugin's author wrote (a plugin brings no message catalog). The Configure form shows these, never a field's key or an option's stored id.
26
+ */
27
+ type FieldText = {
28
+ readonly messageId: string;
29
+ } | {
30
+ readonly text: string;
31
+ };
32
+ type ValueField = {
33
+ readonly type: "string";
34
+ readonly label: FieldText;
35
+ readonly default: string;
36
+ readonly maxLength?: number;
37
+ readonly enum?: readonly string[];
38
+ /** The words for each option of `enum`, by option id; present exactly when `enum` is, naming every option and no other. The stored value is the id. */
39
+ readonly optionLabels?: Readonly<Record<string, FieldText>>;
40
+ } | {
41
+ readonly type: "number";
42
+ readonly label: FieldText;
43
+ readonly default: number;
44
+ readonly min?: number;
45
+ readonly max?: number;
46
+ readonly integer?: boolean;
47
+ } | {
48
+ readonly type: "boolean";
49
+ readonly label: FieldText;
50
+ readonly default: boolean;
51
+ };
52
+ type ValueSchema = Readonly<Record<string, ValueField>>;
53
+
54
+ /**
55
+ * DX-6269 (PLAN-96 G-6) - the layout rules of a widget surface, as pure functions: snapping to whole cells, a Widget's size limits, collision, clamping and the
56
+ * first free space. Import-free of Node. The app runs them live while a placement is dragged or resized (so what the person sees while dragging is what is
57
+ * saved), and the server runs the same functions to check every layout write and to place a new or changed placement, so the two never disagree.
58
+ *
59
+ * Positions are 0-based cells; the grid is `GRID_COLUMNS` wide with unlimited rows. A layout never has two boxes overlapping: moving, resizing or changing a
60
+ * box pushes whatever it now overlaps down to just below it, and whatever that one now overlaps in turn. The box being edited never moves for another.
61
+ */
62
+
63
+ interface Size {
64
+ w: number;
65
+ h: number;
66
+ }
67
+
68
+ /**
69
+ * DX-6269 (PLAN-96 G-6; DX-6233 finding (d)) - a Widget's DECLARATION: the part of a Widget that is data - its key, the sizes it may take, its config and
70
+ * settings schemas, the surface-wide settings it reads and the host context it needs. The component that draws it, its title and its description are the
71
+ * app's (`frontend/src/lib/widgets/catalog.ts`). Import-free of Node, so the app, the server and DX-6254's plugin SDK all read the one declaration: the app
72
+ * to draw the Widget and its forms, the server to refuse a placement write the declaration rejects.
73
+ */
74
+
75
+ type CellSize = Size;
76
+ interface WidgetDeclaration<C extends ValueSchema = ValueSchema, S extends ValueSchema = ValueSchema> {
77
+ /** Stable identity, stored on every placement: lowercase words joined by `-` for a built-in, `plugin:<plugin key>:<widget key>` for a plugin's (`pluginWidgetKey`). */
78
+ readonly key: string;
79
+ /** The size a new placement gets, and the smallest and largest one may take, in cells. */
80
+ readonly size: {
81
+ readonly default: CellSize;
82
+ readonly min: CellSize;
83
+ readonly max: CellSize;
84
+ };
85
+ readonly config: C;
86
+ readonly settings: S;
87
+ readonly readsSurfaceSettings: readonly SurfaceWideSettingKey[];
88
+ /** The host context keys the Widget cannot render without. */
89
+ readonly needsHostContext: readonly string[];
90
+ }
91
+
92
+ /**
93
+ * DX-5170 / DX-4679 section 2 - the shape of `danx-plugin.json`, the manifest `@danxbot/plugin-sdk` writes beside a built plugin and the
94
+ * upload validator (DX-5171) checks before a revision is stored: the entry module and every listed file present with the stated size and
95
+ * sha256, nothing unlisted, and `runtime` naming the majors the plugin was built against (`runtime-contract.ts`).
96
+ *
97
+ * Pure types and constants; the writer is the SDK's, the checks are the server's.
98
+ */
99
+
11
100
  /** One file of the built plugin, other than the manifest itself (a file cannot hold its own hash). */
12
101
  interface PluginManifestFile {
13
102
  /** Relative to the output folder, `/` separated. */
@@ -33,10 +122,22 @@ interface PluginManifest {
33
122
  * Where two plugins declare one type, the one activated first wins (`plugins.activated_at`).
34
123
  */
35
124
  readonly cardTypes?: readonly string[];
36
- /** DX-5282: which view modules the entry exports (`PLUGIN_VIEW_EXPORTS`), recorded at build from the entry chunk's real exports; never empty. */
125
+ /** DX-5282: which view modules the entry exports (`PLUGIN_VIEW_EXPORTS`), recorded at build from the entry chunk's real exports; empty only for a plugin that declares Widgets. */
37
126
  readonly exports: readonly PluginViewExport[];
127
+ /** DX-6254: the Widgets the plugin adds to the Widget Library, each drawn by one of the entry's exports; empty when it adds none. Checked by `manifestWidgetIssues`. */
128
+ readonly widgets: readonly PluginManifestWidget[];
38
129
  readonly files: readonly PluginManifestFile[];
39
130
  }
131
+ /** DX-6254: one Widget a plugin declares. Its words are the author's own text: a plugin brings no message catalog. */
132
+ interface PluginManifestWidget {
133
+ /** The name of the entry's export that draws the Widget, recorded at build only when the entry chunk really exports it. */
134
+ readonly export: string;
135
+ readonly title: string;
136
+ /** One sentence for the Widget Library: what the Widget shows. */
137
+ readonly description: string;
138
+ /** The built-in Widgets' own declaration, with `key` the Widget's key inside the plugin; a placement stores `plugin:<plugin key>:<key>`. */
139
+ readonly declaration: WidgetDeclaration;
140
+ }
40
141
 
41
142
  /** The project's build output folder: the SDK's Vite preset builds into Vite's default `dist`. */
42
143
  declare const PLUGIN_DIST_DIR = "dist";
package/dist/publish.js CHANGED
@@ -5886,7 +5886,7 @@ var CARD_KIT_LIBRARY = "@danxbot/card-kit";
5886
5886
  var PLUGIN_RUNTIME_LIBRARIES = [
5887
5887
  { name: "react", major: 19 },
5888
5888
  { name: "react-dom", major: 19 },
5889
- { name: "@danxbot/ui", major: 28 },
5889
+ { name: "@danxbot/ui", major: 30 },
5890
5890
  { name: CARD_KIT_LIBRARY, major: CARD_KIT_MAJOR }
5891
5891
  ];
5892
5892
  var PLUGIN_RUNTIME_SPECIFIERS = [
@@ -5927,7 +5927,7 @@ ${output.trim()}`));
5927
5927
  }
5928
5928
  function isManifest(value) {
5929
5929
  const manifest = value;
5930
- return typeof manifest === "object" && manifest !== null && typeof manifest.entry === "string" && Array.isArray(manifest.exports) && Array.isArray(manifest.files) && manifest.files.every((file) => typeof file?.path === "string");
5930
+ return typeof manifest === "object" && manifest !== null && typeof manifest.entry === "string" && Array.isArray(manifest.exports) && Array.isArray(manifest.widgets) && Array.isArray(manifest.files) && manifest.files.every((file) => typeof file?.path === "string");
5931
5931
  }
5932
5932
  async function readBuiltPlugin(projectDir) {
5933
5933
  const distDir = join3(resolve2(projectDir), PLUGIN_DIST_DIR);
@@ -5937,7 +5937,7 @@ async function readBuiltPlugin(projectDir) {
5937
5937
  } catch (err) {
5938
5938
  throw new Error(`danx-plugin: the build left no readable ${PLUGIN_DIST_DIR}/${PLUGIN_MANIFEST_FILE} in ${projectDir} (is the danxPlugin() preset in vite.config?): ${err.message}`);
5939
5939
  }
5940
- if (!isManifest(manifest)) throw new Error(`danx-plugin: ${PLUGIN_DIST_DIR}/${PLUGIN_MANIFEST_FILE} in ${projectDir} has no entry, no exports list or no files list of paths`);
5940
+ if (!isManifest(manifest)) throw new Error(`danx-plugin: ${PLUGIN_DIST_DIR}/${PLUGIN_MANIFEST_FILE} in ${projectDir} has no entry, no exports list, no widgets list or no files list of paths`);
5941
5941
  const files = await Promise.all(manifest.files.map(async (file) => ({ path: file.path, bytes: await readFile2(join3(distDir, file.path)) })));
5942
5942
  return { manifest, files };
5943
5943
  }
package/dist/vite.d.ts CHANGED
@@ -1,5 +1,105 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
+ /** The names of the surface-wide settings, one per member of {@link SurfaceWideSettings} (a new member fails `tsc` here). */
4
+ declare const SURFACE_WIDE_SETTING_KEYS: readonly ["date_range", "keyword"];
5
+ type SurfaceWideSettingKey = (typeof SURFACE_WIDE_SETTING_KEYS)[number];
6
+
7
+ /**
8
+ * DX-6233 / DX-6269 (PLAN-96 G-6) - the schema a Widget's config and settings are declared in, and the check of a value against it. Import-free of Node: the
9
+ * app imports this module at runtime (`@backend/widget-surfaces/value-schema.js`) to draw a placement's form and to check what is stored, and the server
10
+ * imports the same module to refuse a write the schema rejects, so the two can never disagree about what a Widget accepts.
11
+ *
12
+ * Every field carries its default, so the schema is also where a Widget's defaults live. A value is checked as a (possibly partial) set of the schema's
13
+ * fields: a key the schema does not name, or a value of the wrong type, is an issue, never read as a default.
14
+ */
15
+ /**
16
+ * DX-6254 - the words that name a field, or one option of a field: a built-in Widget's message id, which the app looks up in the person's language, or the text a
17
+ * plugin's author wrote (a plugin brings no message catalog). The Configure form shows these, never a field's key or an option's stored id.
18
+ */
19
+ type FieldText = {
20
+ readonly messageId: string;
21
+ } | {
22
+ readonly text: string;
23
+ };
24
+ type ValueField = {
25
+ readonly type: "string";
26
+ readonly label: FieldText;
27
+ readonly default: string;
28
+ readonly maxLength?: number;
29
+ readonly enum?: readonly string[];
30
+ /** The words for each option of `enum`, by option id; present exactly when `enum` is, naming every option and no other. The stored value is the id. */
31
+ readonly optionLabels?: Readonly<Record<string, FieldText>>;
32
+ } | {
33
+ readonly type: "number";
34
+ readonly label: FieldText;
35
+ readonly default: number;
36
+ readonly min?: number;
37
+ readonly max?: number;
38
+ readonly integer?: boolean;
39
+ } | {
40
+ readonly type: "boolean";
41
+ readonly label: FieldText;
42
+ readonly default: boolean;
43
+ };
44
+ type ValueSchema = Readonly<Record<string, ValueField>>;
45
+
46
+ /**
47
+ * DX-6269 (PLAN-96 G-6) - the layout rules of a widget surface, as pure functions: snapping to whole cells, a Widget's size limits, collision, clamping and the
48
+ * first free space. Import-free of Node. The app runs them live while a placement is dragged or resized (so what the person sees while dragging is what is
49
+ * saved), and the server runs the same functions to check every layout write and to place a new or changed placement, so the two never disagree.
50
+ *
51
+ * Positions are 0-based cells; the grid is `GRID_COLUMNS` wide with unlimited rows. A layout never has two boxes overlapping: moving, resizing or changing a
52
+ * box pushes whatever it now overlaps down to just below it, and whatever that one now overlaps in turn. The box being edited never moves for another.
53
+ */
54
+
55
+ interface Size {
56
+ w: number;
57
+ h: number;
58
+ }
59
+
60
+ /**
61
+ * DX-6269 (PLAN-96 G-6; DX-6233 finding (d)) - a Widget's DECLARATION: the part of a Widget that is data - its key, the sizes it may take, its config and
62
+ * settings schemas, the surface-wide settings it reads and the host context it needs. The component that draws it, its title and its description are the
63
+ * app's (`frontend/src/lib/widgets/catalog.ts`). Import-free of Node, so the app, the server and DX-6254's plugin SDK all read the one declaration: the app
64
+ * to draw the Widget and its forms, the server to refuse a placement write the declaration rejects.
65
+ */
66
+
67
+ type CellSize = Size;
68
+ interface WidgetDeclaration<C extends ValueSchema = ValueSchema, S extends ValueSchema = ValueSchema> {
69
+ /** Stable identity, stored on every placement: lowercase words joined by `-` for a built-in, `plugin:<plugin key>:<widget key>` for a plugin's (`pluginWidgetKey`). */
70
+ readonly key: string;
71
+ /** The size a new placement gets, and the smallest and largest one may take, in cells. */
72
+ readonly size: {
73
+ readonly default: CellSize;
74
+ readonly min: CellSize;
75
+ readonly max: CellSize;
76
+ };
77
+ readonly config: C;
78
+ readonly settings: S;
79
+ readonly readsSurfaceSettings: readonly SurfaceWideSettingKey[];
80
+ /** The host context keys the Widget cannot render without. */
81
+ readonly needsHostContext: readonly string[];
82
+ }
83
+
84
+ /**
85
+ * DX-5170 / DX-4679 section 2 - the shape of `danx-plugin.json`, the manifest `@danxbot/plugin-sdk` writes beside a built plugin and the
86
+ * upload validator (DX-5171) checks before a revision is stored: the entry module and every listed file present with the stated size and
87
+ * sha256, nothing unlisted, and `runtime` naming the majors the plugin was built against (`runtime-contract.ts`).
88
+ *
89
+ * Pure types and constants; the writer is the SDK's, the checks are the server's.
90
+ */
91
+
92
+ /** DX-6254: one Widget a plugin declares. Its words are the author's own text: a plugin brings no message catalog. */
93
+ interface PluginManifestWidget {
94
+ /** The name of the entry's export that draws the Widget, recorded at build only when the entry chunk really exports it. */
95
+ readonly export: string;
96
+ readonly title: string;
97
+ /** One sentence for the Widget Library: what the Widget shows. */
98
+ readonly description: string;
99
+ /** The built-in Widgets' own declaration, with `key` the Widget's key inside the plugin; a placement stores `plugin:<plugin key>:<key>`. */
100
+ readonly declaration: WidgetDeclaration;
101
+ }
102
+
3
103
  /** The entry module every plugin build produces; the manifest names it and the app `import()`s it. */
4
104
  declare const PLUGIN_ENTRY_FILE = "index.js";
5
105
  interface DanxPluginOptions {
@@ -10,6 +110,12 @@ interface DanxPluginOptions {
10
110
  * and active, and the team can always set the type back to it. Leave it out for a plugin made for no particular type.
11
111
  */
12
112
  readonly cardTypes?: readonly string[];
113
+ /**
114
+ * DX-6254: the Widgets the plugin adds to the Widget Library. Each names the entry's export that draws it (a component taking `WidgetProps`), its title and
115
+ * description as the Library shows them, and its declaration (`declareWidget` from `@danxbot/plugin-sdk/widgets`, `key` being the Widget's own key, lowercase
116
+ * words). The build refuses a Widget whose declaration cannot work or whose export the entry does not have.
117
+ */
118
+ readonly widgets?: readonly PluginManifestWidget[];
13
119
  }
14
120
  /**
15
121
  * The Vite preset for a 50 Arms card-type plugin: an ES-module library build whose output the app loads with `import()`.
@@ -19,7 +125,7 @@ interface DanxPluginOptions {
19
125
  * - Anything else is bundled. Bundling a copy of a shared package (`react-dom/client` or `react-router` are not in the contract) would run a second copy
20
126
  * beside the app's, so that fails the build instead of shipping.
21
127
  * - Code-split chunks load by relative import from the entry's folder; CSS is emitted as one `index.css`.
22
- * - `danx-plugin.json` is written beside the output: every file's size and sha256, the entry, the view modules it exports, the stylesheets and the runtime majors.
128
+ * - `danx-plugin.json` is written beside the output: every file's size and sha256, the entry, the view modules it exports, its Widgets, the stylesheets and the runtime majors.
23
129
  */
24
130
  declare function danxPlugin(options?: DanxPluginOptions): Plugin;
25
131
 
package/dist/vite.js CHANGED
@@ -4,7 +4,7 @@ var CARD_KIT_LIBRARY = "@danxbot/card-kit";
4
4
  var PLUGIN_RUNTIME_LIBRARIES = [
5
5
  { name: "react", major: 19 },
6
6
  { name: "react-dom", major: 19 },
7
- { name: "@danxbot/ui", major: 28 },
7
+ { name: "@danxbot/ui", major: 30 },
8
8
  { name: CARD_KIT_LIBRARY, major: CARD_KIT_MAJOR }
9
9
  ];
10
10
  var PLUGIN_RUNTIME_SPECIFIERS = [
@@ -32,6 +32,7 @@ import { readdirSync, readFileSync, writeFileSync } from "node:fs";
32
32
  import { join, relative, sep } from "node:path";
33
33
 
34
34
  // ../../src/plugins/plugin-manifest.ts
35
+ var PLUGIN_KEY_PATTERN = "^[a-z0-9][a-z0-9_-]{0,63}$";
35
36
  var PLUGIN_MANIFEST_FILE = "danx-plugin.json";
36
37
  var CARD_VIEW_EXPORT = "CardView";
37
38
  var CARD_LIST_VIEW_EXPORT = "CardListView";
@@ -49,7 +50,7 @@ function describeFile(root, file) {
49
50
  sha256: createHash("sha256").update(bytes).digest("hex")
50
51
  };
51
52
  }
52
- function buildManifest(outDir, entry, exports, cardTypes = []) {
53
+ function buildManifest(outDir, entry, exports, cardTypes, widgets) {
53
54
  const files = filesUnder(outDir).map((file) => describeFile(outDir, file)).filter((file) => file.path !== PLUGIN_MANIFEST_FILE);
54
55
  if (!files.some((file) => file.path === entry)) throw new Error(`danx-plugin: the build produced no entry module ${entry} in ${outDir}`);
55
56
  return {
@@ -57,18 +58,177 @@ function buildManifest(outDir, entry, exports, cardTypes = []) {
57
58
  styles: files.filter((file) => file.path.endsWith(".css")).map((file) => file.path),
58
59
  runtime: pluginRuntimeContract().majors,
59
60
  exports: [...exports],
61
+ widgets: [...widgets],
60
62
  // DX-6178: the card types the plugin's card view is made for, written only when the author named some.
61
63
  ...cardTypes.length === 0 ? {} : { cardTypes: [...cardTypes] },
62
64
  files
63
65
  };
64
66
  }
65
- function writeManifest(outDir, entry, exports, cardTypes = []) {
66
- const manifest = buildManifest(outDir, entry, exports, cardTypes);
67
+ function writeManifest(outDir, entry, exports, cardTypes, widgets) {
68
+ const manifest = buildManifest(outDir, entry, exports, cardTypes, widgets);
67
69
  writeFileSync(join(outDir, PLUGIN_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
68
70
  `);
69
71
  return manifest;
70
72
  }
71
73
 
74
+ // ../../src/widget-surfaces/model.ts
75
+ var GRID_COLUMNS = 24;
76
+ var SURFACE_WIDE_SETTING_KEYS = ["date_range", "keyword"];
77
+
78
+ // ../../src/widget-surfaces/value-schema.ts
79
+ function fieldIssue(field, schema, value) {
80
+ switch (schema.type) {
81
+ case "string":
82
+ if (typeof value !== "string") return { field, rule: "notText" };
83
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) return { field, rule: "tooLong", limit: schema.maxLength };
84
+ if (schema.enum !== void 0 && !schema.enum.includes(value)) return { field, rule: "notOneOf", options: schema.enum };
85
+ return null;
86
+ case "number":
87
+ if (typeof value !== "number" || !Number.isFinite(value)) return { field, rule: "notNumber" };
88
+ if (schema.integer === true && !Number.isInteger(value)) return { field, rule: "notWhole" };
89
+ if (schema.min !== void 0 && value < schema.min) return { field, rule: "tooSmall", limit: schema.min };
90
+ if (schema.max !== void 0 && value > schema.max) return { field, rule: "tooLarge", limit: schema.max };
91
+ return null;
92
+ case "boolean":
93
+ return typeof value === "boolean" ? null : { field, rule: "notBoolean" };
94
+ }
95
+ }
96
+ function valueIssues(schema, values) {
97
+ if (typeof values !== "object" || values === null || Array.isArray(values)) return [{ field: "", rule: "notObject" }];
98
+ const issues = [];
99
+ for (const [name, value] of Object.entries(values)) {
100
+ const field = schema[name];
101
+ if (field === void 0) issues.push({ field: name, rule: "unknownField" });
102
+ else {
103
+ const issue = fieldIssue(name, field, value);
104
+ if (issue !== null) issues.push(issue);
105
+ }
106
+ }
107
+ return issues;
108
+ }
109
+ var blank = (text) => ("messageId" in text ? text.messageId : text.text).trim() === "";
110
+ function labelIssues(schema) {
111
+ const issues = [];
112
+ for (const [name, field] of Object.entries(schema)) {
113
+ if (blank(field.label)) issues.push(`field "${name}" has an empty label`);
114
+ if (field.type !== "string") continue;
115
+ const options = field.enum ?? [];
116
+ const labelled = Object.keys(field.optionLabels ?? {});
117
+ if (field.enum !== void 0 && field.optionLabels === void 0) issues.push(`field "${name}" offers options and labels none of them`);
118
+ if (field.enum === void 0 && field.optionLabels !== void 0) issues.push(`field "${name}" labels options it does not offer`);
119
+ for (const option of options) if (!labelled.includes(option)) issues.push(`field "${name}" has no label for option "${option}"`);
120
+ for (const option of labelled) if (!options.includes(option) && field.enum !== void 0) issues.push(`field "${name}" labels "${option}", which is not one of its options`);
121
+ for (const [option, text] of Object.entries(field.optionLabels ?? {})) if (blank(text)) issues.push(`field "${name}" has an empty label for option "${option}"`);
122
+ }
123
+ return issues;
124
+ }
125
+ function defaultsOf(schema) {
126
+ return Object.fromEntries(Object.entries(schema).map(([name, field]) => [name, field.default]));
127
+ }
128
+
129
+ // ../../src/widget-surfaces/declaration.ts
130
+ var WIDGET_KEY_SHAPE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
131
+ var PLUGIN_KEY_SHAPE = new RegExp(PLUGIN_KEY_PATTERN);
132
+ var WIDGET_KEY_MAX_LENGTH = 120;
133
+ var PLUGIN_WIDGET_PREFIX = "plugin:";
134
+ function pluginWidgetKey(pluginKey, widgetKey) {
135
+ return `${PLUGIN_WIDGET_PREFIX}${pluginKey}:${widgetKey}`;
136
+ }
137
+ function declarationBodyIssues(def) {
138
+ const issues = [];
139
+ const { min, max, default: initial } = def.size;
140
+ for (const [label, size] of [["min", min], ["default", initial], ["max", max]]) {
141
+ if (!Number.isInteger(size.w) || !Number.isInteger(size.h) || size.w < 1 || size.h < 1) issues.push(`${label} size ${size.w}x${size.h} is not whole cells`);
142
+ if (size.w > GRID_COLUMNS) issues.push(`${label} width ${size.w} is wider than the ${GRID_COLUMNS}-column grid`);
143
+ }
144
+ if (initial.w < min.w || initial.h < min.h || initial.w > max.w || initial.h > max.h) issues.push("the default size is outside the min and max sizes");
145
+ for (const scope of ["config", "settings"]) {
146
+ for (const issue of valueIssues(def[scope], defaultsOf(def[scope]))) issues.push(`${scope} default: ${JSON.stringify(issue)}`);
147
+ for (const issue of labelIssues(def[scope])) issues.push(`${scope}: ${issue}`);
148
+ }
149
+ for (const key of def.readsSurfaceSettings) {
150
+ if (!SURFACE_WIDE_SETTING_KEYS.includes(key)) issues.push(`"${key}" is not a surface-wide setting`);
151
+ }
152
+ if (new Set(def.readsSurfaceSettings).size !== def.readsSurfaceSettings.length) issues.push("readsSurfaceSettings names a setting twice");
153
+ return issues;
154
+ }
155
+
156
+ // ../../src/plugins/manifest-widgets.ts
157
+ var EXPORT_SHAPE = /^[A-Z][A-Za-z0-9]*$/;
158
+ var MAX_WIDGET_TITLE_LENGTH = 80;
159
+ var MAX_WIDGET_DESCRIPTION_LENGTH = 300;
160
+ var MAX_PLUGIN_WIDGETS = 32;
161
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
162
+ var keysAre = (value, required, optional = []) => required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
163
+ var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
164
+ var isFieldText = (value) => isObject(value) && (keysAre(value, ["messageId"]) && typeof value.messageId === "string" || keysAre(value, ["text"]) && typeof value.text === "string");
165
+ var isSize = (value) => isObject(value) && keysAre(value, ["w", "h"]) && typeof value.w === "number" && typeof value.h === "number";
166
+ var isOptionalNumber = (value) => value === void 0 || typeof value === "number";
167
+ function fieldShapeIsValid(field) {
168
+ if (!isObject(field) || !isFieldText(field.label)) return false;
169
+ switch (field.type) {
170
+ case "string":
171
+ return keysAre(field, ["type", "label", "default"], ["maxLength", "enum", "optionLabels"]) && typeof field.default === "string" && isOptionalNumber(field.maxLength) && (field.enum === void 0 || isStringArray(field.enum)) && (field.optionLabels === void 0 || isObject(field.optionLabels) && Object.values(field.optionLabels).every(isFieldText));
172
+ case "number":
173
+ return keysAre(field, ["type", "label", "default"], ["min", "max", "integer"]) && typeof field.default === "number" && isOptionalNumber(field.min) && isOptionalNumber(field.max) && (field.integer === void 0 || typeof field.integer === "boolean");
174
+ case "boolean":
175
+ return keysAre(field, ["type", "label", "default"]) && typeof field.default === "boolean";
176
+ default:
177
+ return false;
178
+ }
179
+ }
180
+ function declarationShapeIssues(value) {
181
+ if (!isObject(value) || !keysAre(value, ["key", "size", "config", "settings", "readsSurfaceSettings", "needsHostContext"])) {
182
+ return ["the declaration must hold exactly key, size, config, settings, readsSurfaceSettings and needsHostContext"];
183
+ }
184
+ const issues = [];
185
+ if (typeof value.key !== "string") issues.push("the key must be text");
186
+ if (!isObject(value.size) || !keysAre(value.size, ["default", "min", "max"]) || ![value.size.default, value.size.min, value.size.max].every(isSize)) issues.push("size must hold default, min and max, each {w, h} in cells");
187
+ for (const scope of ["config", "settings"]) {
188
+ const schema = value[scope];
189
+ if (!isObject(schema)) issues.push(`${scope} must be an object of fields`);
190
+ else for (const [name, field] of Object.entries(schema)) if (!fieldShapeIsValid(field)) issues.push(`${scope} field "${name}" is not a string, number or boolean field with a label and a default`);
191
+ }
192
+ if (!isStringArray(value.readsSurfaceSettings)) issues.push("readsSurfaceSettings must be a list of setting names");
193
+ if (!isStringArray(value.needsHostContext)) issues.push("needsHostContext must be a list of host context keys");
194
+ return issues;
195
+ }
196
+ function manifestWidgetIssues(widgets, pluginKey) {
197
+ if (!Array.isArray(widgets)) return ["widgets must be a list"];
198
+ if (widgets.length > MAX_PLUGIN_WIDGETS) return [`a plugin declares at most ${MAX_PLUGIN_WIDGETS} Widgets`];
199
+ const issues = [];
200
+ const exports = /* @__PURE__ */ new Set();
201
+ const keys = /* @__PURE__ */ new Set();
202
+ widgets.forEach((widget, index) => {
203
+ if (!isObject(widget) || !keysAre(widget, ["export", "title", "description", "declaration"])) {
204
+ issues.push(`widget ${index + 1}: must hold exactly export, title, description and declaration`);
205
+ return;
206
+ }
207
+ const name = typeof widget.export === "string" ? widget.export : `widget ${index + 1}`;
208
+ const say = (issue) => issues.push(`${name}: ${issue}`);
209
+ if (typeof widget.export !== "string" || !EXPORT_SHAPE.test(widget.export) || PLUGIN_VIEW_EXPORTS.includes(widget.export)) {
210
+ say(`the export must be a component name (a capital letter, then letters and digits), and not ${PLUGIN_VIEW_EXPORTS.join(" or ")}`);
211
+ } else if (exports.has(widget.export)) say("two Widgets name this export");
212
+ else exports.add(widget.export);
213
+ for (const [field, max] of [["title", MAX_WIDGET_TITLE_LENGTH], ["description", MAX_WIDGET_DESCRIPTION_LENGTH]]) {
214
+ const text = widget[field];
215
+ if (typeof text !== "string" || text.trim() === "" || text.length > max) say(`the ${field} must be text of 1 to ${max} characters`);
216
+ }
217
+ const shape = declarationShapeIssues(widget.declaration);
218
+ if (shape.length > 0) {
219
+ for (const issue of shape) say(issue);
220
+ return;
221
+ }
222
+ const declaration = widget.declaration;
223
+ if (!WIDGET_KEY_SHAPE.test(declaration.key)) say(`the key "${declaration.key}" is not lowercase words joined by "-"`);
224
+ else if (keys.has(declaration.key)) say(`two Widgets have the key "${declaration.key}"`);
225
+ else keys.add(declaration.key);
226
+ if (pluginKey !== null && pluginWidgetKey(pluginKey, declaration.key).length > WIDGET_KEY_MAX_LENGTH) say(`its stored key ${pluginWidgetKey(pluginKey, declaration.key)} is longer than ${WIDGET_KEY_MAX_LENGTH} characters`);
227
+ for (const issue of declarationBodyIssues(declaration)) say(issue);
228
+ });
229
+ return issues;
230
+ }
231
+
72
232
  // src/vite.ts
73
233
  var PLUGIN_ENTRY_FILE = "index.js";
74
234
  var STYLESHEET_FILE = "index.css";
@@ -78,6 +238,7 @@ function bundledNeverPackage(moduleId) {
78
238
  }
79
239
  function danxPlugin(options = {}) {
80
240
  const entry = options.entry ?? "src/index.tsx";
241
+ const widgets = options.widgets ?? [];
81
242
  let viewExports = [];
82
243
  return {
83
244
  name: "danx-plugin",
@@ -106,11 +267,15 @@ function danxPlugin(options = {}) {
106
267
  const entryChunk = Object.values(bundle).find((output) => output.type === "chunk" && output.isEntry);
107
268
  if (entryChunk?.type !== "chunk") throw new Error(`danx-plugin: the build produced no entry chunk for ${entry}`);
108
269
  viewExports = PLUGIN_VIEW_EXPORTS.filter((name) => entryChunk.exports.includes(name));
109
- if (viewExports.length === 0) {
270
+ if (viewExports.length === 0 && widgets.length === 0) {
110
271
  throw new Error(
111
- `danx-plugin: ${entry} must export ${PLUGIN_VIEW_EXPORTS.join(" and/or ")}, but it exports ${entryChunk.exports.length === 0 ? "nothing" : entryChunk.exports.join(", ")}.`
272
+ `danx-plugin: ${entry} must export ${PLUGIN_VIEW_EXPORTS.join(" and/or ")}, or the plugin must declare Widgets, but it exports ${entryChunk.exports.length === 0 ? "nothing" : entryChunk.exports.join(", ")}.`
112
273
  );
113
274
  }
275
+ const issues = manifestWidgetIssues(widgets, null);
276
+ if (issues.length > 0) throw new Error(`danx-plugin: the plugin's Widgets cannot be used: ${issues.join("; ")}`);
277
+ const missing = widgets.filter((widget) => !entryChunk.exports.includes(widget.export)).map((widget) => widget.export);
278
+ if (missing.length > 0) throw new Error(`danx-plugin: ${entry} does not export ${missing.join(", ")}, which the plugin's Widgets name as their components.`);
114
279
  for (const output of Object.values(bundle)) {
115
280
  if (output.type !== "chunk") continue;
116
281
  for (const id of output.moduleIds) {
@@ -124,7 +289,7 @@ function danxPlugin(options = {}) {
124
289
  },
125
290
  writeBundle(output) {
126
291
  if (output.dir === void 0) throw new Error("danx-plugin: the build has no output directory to write the manifest into");
127
- writeManifest(output.dir, PLUGIN_ENTRY_FILE, viewExports, options.cardTypes ?? []);
292
+ writeManifest(output.dir, PLUGIN_ENTRY_FILE, viewExports, options.cardTypes ?? [], widgets);
128
293
  }
129
294
  };
130
295
  }
@@ -0,0 +1,142 @@
1
+ declare const DATE_RANGES: readonly ["24h", "7d", "30d"];
2
+ type DateRange = (typeof DATE_RANGES)[number];
3
+ /** Every surface-wide setting a surface stores a default for. */
4
+ interface SurfaceWideSettings {
5
+ date_range: DateRange;
6
+ keyword: string;
7
+ }
8
+ /** The names of the surface-wide settings, one per member of {@link SurfaceWideSettings} (a new member fails `tsc` here). */
9
+ declare const SURFACE_WIDE_SETTING_KEYS: readonly ["date_range", "keyword"];
10
+ type SurfaceWideSettingKey = (typeof SURFACE_WIDE_SETTING_KEYS)[number];
11
+
12
+ /**
13
+ * DX-6233 / DX-6269 (PLAN-96 G-6) - the schema a Widget's config and settings are declared in, and the check of a value against it. Import-free of Node: the
14
+ * app imports this module at runtime (`@backend/widget-surfaces/value-schema.js`) to draw a placement's form and to check what is stored, and the server
15
+ * imports the same module to refuse a write the schema rejects, so the two can never disagree about what a Widget accepts.
16
+ *
17
+ * Every field carries its default, so the schema is also where a Widget's defaults live. A value is checked as a (possibly partial) set of the schema's
18
+ * fields: a key the schema does not name, or a value of the wrong type, is an issue, never read as a default.
19
+ */
20
+ /**
21
+ * DX-6254 - the words that name a field, or one option of a field: a built-in Widget's message id, which the app looks up in the person's language, or the text a
22
+ * plugin's author wrote (a plugin brings no message catalog). The Configure form shows these, never a field's key or an option's stored id.
23
+ */
24
+ type FieldText = {
25
+ readonly messageId: string;
26
+ } | {
27
+ readonly text: string;
28
+ };
29
+ type ValueField = {
30
+ readonly type: "string";
31
+ readonly label: FieldText;
32
+ readonly default: string;
33
+ readonly maxLength?: number;
34
+ readonly enum?: readonly string[];
35
+ /** The words for each option of `enum`, by option id; present exactly when `enum` is, naming every option and no other. The stored value is the id. */
36
+ readonly optionLabels?: Readonly<Record<string, FieldText>>;
37
+ } | {
38
+ readonly type: "number";
39
+ readonly label: FieldText;
40
+ readonly default: number;
41
+ readonly min?: number;
42
+ readonly max?: number;
43
+ readonly integer?: boolean;
44
+ } | {
45
+ readonly type: "boolean";
46
+ readonly label: FieldText;
47
+ readonly default: boolean;
48
+ };
49
+ type ValueSchema = Readonly<Record<string, ValueField>>;
50
+ type FieldValue<F extends ValueField> = F extends {
51
+ type: "string";
52
+ } ? string : F extends {
53
+ type: "number";
54
+ } ? number : boolean;
55
+ type ValuesOf<S extends ValueSchema> = {
56
+ [K in keyof S]: FieldValue<S[K]>;
57
+ };
58
+
59
+ /**
60
+ * DX-6269 (PLAN-96 G-6) - the layout rules of a widget surface, as pure functions: snapping to whole cells, a Widget's size limits, collision, clamping and the
61
+ * first free space. Import-free of Node. The app runs them live while a placement is dragged or resized (so what the person sees while dragging is what is
62
+ * saved), and the server runs the same functions to check every layout write and to place a new or changed placement, so the two never disagree.
63
+ *
64
+ * Positions are 0-based cells; the grid is `GRID_COLUMNS` wide with unlimited rows. A layout never has two boxes overlapping: moving, resizing or changing a
65
+ * box pushes whatever it now overlaps down to just below it, and whatever that one now overlaps in turn. The box being edited never moves for another.
66
+ */
67
+
68
+ interface Size {
69
+ w: number;
70
+ h: number;
71
+ }
72
+
73
+ /**
74
+ * DX-6269 (PLAN-96 G-6; DX-6233 finding (d)) - a Widget's DECLARATION: the part of a Widget that is data - its key, the sizes it may take, its config and
75
+ * settings schemas, the surface-wide settings it reads and the host context it needs. The component that draws it, its title and its description are the
76
+ * app's (`frontend/src/lib/widgets/catalog.ts`). Import-free of Node, so the app, the server and DX-6254's plugin SDK all read the one declaration: the app
77
+ * to draw the Widget and its forms, the server to refuse a placement write the declaration rejects.
78
+ */
79
+
80
+ type CellSize = Size;
81
+ interface WidgetDeclaration<C extends ValueSchema = ValueSchema, S extends ValueSchema = ValueSchema> {
82
+ /** Stable identity, stored on every placement: lowercase words joined by `-` for a built-in, `plugin:<plugin key>:<widget key>` for a plugin's (`pluginWidgetKey`). */
83
+ readonly key: string;
84
+ /** The size a new placement gets, and the smallest and largest one may take, in cells. */
85
+ readonly size: {
86
+ readonly default: CellSize;
87
+ readonly min: CellSize;
88
+ readonly max: CellSize;
89
+ };
90
+ readonly config: C;
91
+ readonly settings: S;
92
+ readonly readsSurfaceSettings: readonly SurfaceWideSettingKey[];
93
+ /** The host context keys the Widget cannot render without. */
94
+ readonly needsHostContext: readonly string[];
95
+ }
96
+ /** Identity function that lets TypeScript infer the schemas, so a component's `config` and `settings` props are typed from them. */
97
+ declare function declareWidget<C extends ValueSchema, S extends ValueSchema>(declaration: WidgetDeclaration<C, S>): WidgetDeclaration<C, S>;
98
+
99
+ /**
100
+ * DX-6254 (moved from the app's `catalog.ts`, DX-6233) - what a host hands a Widget's component: the contract between the app and every Widget, a built-in's or a
101
+ * plugin's. Types only and import-free of Node, so the app and `@danxbot/plugin-sdk/widgets` read this one module.
102
+ */
103
+
104
+ /** The extra facts about where a host is that a host passes to a Widget (for a plan page, `{ plan: "96" }`); the Dashboard passes none. */
105
+ type HostContext = Readonly<Record<string, string>>;
106
+ interface WidgetProps<C extends ValueSchema, S extends ValueSchema> {
107
+ /** The Widget's config defaults, then the placement's config. */
108
+ config: ValuesOf<C>;
109
+ /** The Widget's defaults, then the placement's defaults (everyone's), then the viewer's own override. */
110
+ settings: ValuesOf<S>;
111
+ /** Only the surface-wide settings the Widget declares it reads (`readsSurfaceSettings`); never the others. */
112
+ surface: Partial<SurfaceWideSettings>;
113
+ host: HostContext;
114
+ placement: {
115
+ readonly id: number;
116
+ readonly name: string | null;
117
+ };
118
+ /** Saves the viewer's own override of some of the Widget's settings for this placement. Rejects with the server's refusal. */
119
+ saveSettings: (patch: Partial<ValuesOf<S>>) => Promise<void>;
120
+ }
121
+
122
+ /**
123
+ * DX-5170 / DX-4679 section 2 - the shape of `danx-plugin.json`, the manifest `@danxbot/plugin-sdk` writes beside a built plugin and the
124
+ * upload validator (DX-5171) checks before a revision is stored: the entry module and every listed file present with the stated size and
125
+ * sha256, nothing unlisted, and `runtime` naming the majors the plugin was built against (`runtime-contract.ts`).
126
+ *
127
+ * Pure types and constants; the writer is the SDK's, the checks are the server's.
128
+ */
129
+
130
+ /** DX-6254: one Widget a plugin declares. Its words are the author's own text: a plugin brings no message catalog. */
131
+ interface PluginManifestWidget {
132
+ /** The name of the entry's export that draws the Widget, recorded at build only when the entry chunk really exports it. */
133
+ readonly export: string;
134
+ readonly title: string;
135
+ /** One sentence for the Widget Library: what the Widget shows. */
136
+ readonly description: string;
137
+ /** The built-in Widgets' own declaration, with `key` the Widget's key inside the plugin; a placement stores `plugin:<plugin key>:<key>`. */
138
+ readonly declaration: WidgetDeclaration;
139
+ }
140
+
141
+ export { declareWidget };
142
+ export type { CellSize, FieldText, HostContext, PluginManifestWidget as PluginWidget, SurfaceWideSettings, ValueField, ValueSchema, ValuesOf, WidgetDeclaration, WidgetProps };
@@ -0,0 +1,11 @@
1
+ // ../../src/plugins/plugin-manifest.ts
2
+ var PLUGIN_KEY_PATTERN = "^[a-z0-9][a-z0-9_-]{0,63}$";
3
+
4
+ // ../../src/widget-surfaces/declaration.ts
5
+ function declareWidget(declaration) {
6
+ return declaration;
7
+ }
8
+ var PLUGIN_KEY_SHAPE = new RegExp(PLUGIN_KEY_PATTERN);
9
+ export {
10
+ declareWidget
11
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danxbot/plugin-sdk",
3
- "version": "0.2.39",
4
- "description": "SDK for building 50 Arms card-type plugins (DX-5170): the card kit's type declarations, a Vite preset that builds a plugin against the app's shared runtime and writes its manifest, and the danx-plugin CLI.",
3
+ "version": "0.2.42",
4
+ "description": "SDK for building 50 Arms plugins (DX-5170): the card kit's type declarations, the Widget declaration kit, a Vite preset that builds a plugin against the app's shared runtime and writes its manifest, and the danx-plugin CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -18,6 +18,10 @@
18
18
  "./publish": {
19
19
  "types": "./dist/publish.d.ts",
20
20
  "default": "./dist/publish.js"
21
+ },
22
+ "./widgets": {
23
+ "types": "./dist/widgets.d.ts",
24
+ "default": "./dist/widgets.js"
21
25
  }
22
26
  },
23
27
  "files": [
@@ -33,7 +37,7 @@
33
37
  "node": ">=20"
34
38
  },
35
39
  "peerDependencies": {
36
- "@danxbot/ui": "^28.0.0",
40
+ "@danxbot/ui": "^30.0.0",
37
41
  "@types/react": "^19.0.0",
38
42
  "react": "^19.0.0",
39
43
  "react-router": "^7.0.0",
@@ -46,7 +50,7 @@
46
50
  "test": "vitest run"
47
51
  },
48
52
  "devDependencies": {
49
- "@danxbot/ui": "28.16.0",
53
+ "@danxbot/ui": "30.1.3",
50
54
  "@types/node": "^22.0.0",
51
55
  "@types/react": "^19.2.2",
52
56
  "esbuild": "^0.25.0",