@danxbot/plugin-sdk 0.2.41 → 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.
package/dist/cli.js CHANGED
@@ -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
@@ -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
@@ -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.41",
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": [
@@ -46,7 +50,7 @@
46
50
  "test": "vitest run"
47
51
  },
48
52
  "devDependencies": {
49
- "@danxbot/ui": "30.1.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",