@notegen/plugin-api 0.1.0 → 0.1.2

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
@@ -100,3 +100,93 @@ to the old data branch. Reinstalling an existing fingerprint reuses its branch.
100
100
  This does not roll back note writes, attachments, settings or remote effects.
101
101
  Use explicit data schema versions and idempotent migration steps. The test host
102
102
  does not simulate package installation or versioned storage.
103
+
104
+ ## Active document paths
105
+
106
+ `editor.getActiveEditor()` and active-editor events may include `path`, the workspace-relative Markdown path. It is protected by `editor.read` and never contains an absolute filesystem path. Older hosts and non-workspace documents may omit it; plugins must handle absence without treating the opaque document ID as a path.
107
+
108
+ ## Opening existing notes only
109
+
110
+ Updated hosts accept `notes.openOrCreate({ ..., open: true, create: false })`. This requires `notes.open` only and fails for missing files without creating them. Omitted `create` preserves the original behavior and requires `notes.create`. Open-only mode requires an updated host; older hosts do not implement this option.
111
+
112
+ ## Navigation lists (updated local hosts)
113
+
114
+ The `navigation-list` UI block provides a compact host-rendered sortable list. Each item has a unique stable ID and a label. Item open/remove commands receive `{ generation, itemId }`; the add command receives `{ generation }`. Reorder receives `{ generation, itemIds }` with the complete new order. Plugins must check generation, reject duplicate/missing/foreign IDs, persist the new order, and publish a new document. Hosts validate all referenced commands and limit lists to 100 items. Older hosts reject this block type.
115
+
116
+ ## API 0.1.1: editor menus and composable UI
117
+
118
+ New contributions require `apiVersion: "^0.1.1"`. The corresponding NoteGen host
119
+ changes must be present; publishing this SDK does not update an installed app.
120
+ The older `^0.1.0` plugin contract remains accepted by the new host.
121
+
122
+ Menu locations now include `editor/slash`, `editor/context`, `editor/selection`
123
+ (the text-selection floating toolbar), `editor/toolbar` (visual editor footer),
124
+ `tab/context`, `file/context`, and `mobile/writing/overflow`.
125
+ `editor/context` uses Alt/Option + right-click to preserve the native clipboard menu.
126
+ The tab menu also retains file-menu contributions, deduplicated by command ID.
127
+
128
+ Each menu accepts `icon`, `group`, `order`, `when`, and `enableWhen`. Groups sort
129
+ lexically, then entries sort by ascending order. A menu icon overrides the command
130
+ icon. Commands accept up to 20 `keywords` for slash-menu and palette search.
131
+ Selection/toolbar menus display three direct buttons and put additional commands
132
+ in an accessible overflow menu. Existing built-in editing commands remain intact.
133
+
134
+ ```json
135
+ {
136
+ "location": "editor/selection",
137
+ "command": "com.example.notes.extract",
138
+ "icon": "files",
139
+ "group": "notes",
140
+ "order": 10,
141
+ "when": "editor == markdown && selection",
142
+ "enableWhen": "!readOnly && !codeBlock"
143
+ }
144
+ ```
145
+
146
+ Conditions support boolean `selection`, `readOnly`, `codeBlock`; string `editor`,
147
+ `resourceKind` (`file`, `folder`, `root`), and `resourceExt` (e.g. `md`). Use `!`,
148
+ `==`, `!=`, `&&`, `||`; `&&` binds more tightly than `||`. Parentheses, arbitrary
149
+ properties and executable JavaScript are rejected. Unknown/missing context fails
150
+ closed, including negated conditions. Conditions control UI only, never permission
151
+ grants. Read selection/text through `editor.read`; keep the captured editor ID and
152
+ revision when applying an edit through `editor.write`. Menu arguments do not leak
153
+ selected text or circumvent permissions.
154
+
155
+ Composable blocks:
156
+
157
+ - `layout`: row/column, small/medium/large gap, nested `blocks`.
158
+ - `section`: title, nested blocks, optional collapse and initial open state.
159
+ - `tabs`: stable ID, accessible label, tabs with ID/label/blocks.
160
+ - `toolbar`: labeled actions with optional icon, iconOnly, variant and confirmation.
161
+ - `item-list`: stable IDs, generation, click action, optional checkbox action,
162
+ drag and keyboard reordering, context actions and a touch-accessible overflow.
163
+ Items may supply `metadata` (up to 1024 characters) for a separate information
164
+ line below the description, such as a workspace-relative file path.
165
+ - `markdown`: formatted text with raw HTML, links and image loading disabled.
166
+ - `badge`, `empty`, `loading`: standard theme-aware feedback.
167
+ - Form fields additionally support `search`, ISO `date`, and searchable
168
+ `note-picker`. Supply note choices as `{label,value}` through existing scoped
169
+ `notes.list` permissions. The picker does not enumerate files or grant access.
170
+
171
+ Nested forms retain their values when a surrounding section/tab rerenders. IDs
172
+ must be unique per block type across the whole document. Limits: 6 nesting levels,
173
+ 200 total blocks, 50 blocks per container, 100 list items, 20 actions, 12 tabs,
174
+ and the existing 128 KiB document limit. Command ownership is validated recursively.
175
+
176
+ List open/toggle/action commands receive `{generation,itemId}`; toggle adds
177
+ `checked`. Context actions add `actionId` and optionally nested `argument`.
178
+ Reorder commands receive `{generation,itemIds}` with the complete requested order.
179
+ The plugin must reject stale generations and invalid IDs, persist successful
180
+ changes, then publish updated content. The host never mutates plugin storage.
181
+
182
+ An action may provide `confirmation: {title,description?,confirmLabel,cancelLabel}`.
183
+ Its command runs only after confirmation. Legacy `navigation-list` remains a
184
+ compatibility adapter; new plugins should compose toolbar and item-list blocks.
185
+
186
+ Supported symbolic icons include bookmark, calendar-days, file-text, files, folder,
187
+ folder-open, layout-template, list-checks, list-todo, link, search, plus, minus,
188
+ trash-2, pencil, copy, check, x, star, pin, tag, settings, more-horizontal, arrow-up,
189
+ arrow-down, download, upload, external-link, list, table-2, columns-3, clock,
190
+ book-open, code, sparkles, shuffle, refresh-cw, chart-no-axes-combined, file-input,
191
+ and flask-conical. Unknown icons fall back to a puzzle icon. Raw SVG/HTML and
192
+ external icon URLs are not accepted as executable markup.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** The public API version implemented by this release of NoteGen. */
2
- export declare const PLUGIN_API_VERSION: "0.1.0";
2
+ export declare const PLUGIN_API_VERSION: "0.1.1";
3
3
  export type PluginPlatform = 'desktop' | 'ios' | 'android';
4
4
  export type PluginActivationEvent = `onCommand:${string}` | 'onEditor:markdown' | 'onWorkspace:open' | 'onNotes:change';
5
5
  export type PluginPermissionScope = 'active-editor' | 'workspace-file' | 'workspace-files' | 'workspace-folder' | 'network-origins';
@@ -28,6 +28,7 @@ export interface PluginCommandContribution {
28
28
  title: string;
29
29
  description?: string;
30
30
  icon?: string;
31
+ keywords?: readonly string[];
31
32
  suggestedShortcut?: string;
32
33
  }
33
34
  export interface PluginSettingOption {
@@ -91,12 +92,15 @@ export interface PluginViewContribution {
91
92
  location: 'left-sidebar' | 'right-sidebar' | 'editor-tab';
92
93
  icon?: string;
93
94
  }
94
- export type PluginMenuLocation = 'editor/slash' | 'editor/context' | 'file/context' | 'mobile/writing/overflow';
95
+ export type PluginMenuLocation = 'editor/slash' | 'editor/context' | 'editor/selection' | 'editor/toolbar' | 'tab/context' | 'file/context' | 'mobile/writing/overflow';
95
96
  export interface PluginMenuContribution {
96
97
  location: PluginMenuLocation;
97
98
  command: string;
98
99
  when?: string;
99
100
  group?: string;
101
+ order?: number;
102
+ icon?: string;
103
+ enableWhen?: string;
100
104
  }
101
105
  export interface PluginContributions {
102
106
  commands?: readonly PluginCommandContribution[];
@@ -156,6 +160,8 @@ export interface ResolvedDay {
156
160
  localDateTime: string;
157
161
  }
158
162
  export interface NoteSnapshot {
163
+ /** Saved file modification time in Unix milliseconds, when supported by the host/filesystem. */
164
+ modifiedAt?: number;
159
165
  id: string;
160
166
  path: string;
161
167
  revision: number;
@@ -165,6 +171,8 @@ export interface ReadNoteOptions {
165
171
  path: string;
166
172
  }
167
173
  export interface OpenOrCreateNoteOptions {
174
+ /** Defaults to true. Set false with open: true to open an existing file using only notes.open. Missing files fail. */
175
+ create?: boolean;
168
176
  workspaceId: string;
169
177
  path: string;
170
178
  initialContent: string;
@@ -239,6 +247,8 @@ export interface NoteChangeEvent {
239
247
  previousPath?: string;
240
248
  }
241
249
  export interface ActiveEditorContext {
250
+ /** Workspace-relative Markdown path, when available. Never an absolute path. Requires editor.read. */
251
+ path?: string;
242
252
  windowId: string;
243
253
  editorId: string;
244
254
  documentId: string;
@@ -324,7 +334,7 @@ export type PluginFormField = {
324
334
  disabled?: boolean;
325
335
  visibleWhen?: PluginFormCondition;
326
336
  } & ({
327
- type: 'text' | 'textarea';
337
+ type: 'text' | 'textarea' | 'search' | 'date';
328
338
  value?: string;
329
339
  placeholder?: string;
330
340
  maxLength?: number;
@@ -334,7 +344,7 @@ export type PluginFormField = {
334
344
  min?: number;
335
345
  max?: number;
336
346
  } | {
337
- type: 'select';
347
+ type: 'select' | 'note-picker';
338
348
  value?: string;
339
349
  options: readonly {
340
350
  label: string;
@@ -385,7 +395,27 @@ export type PluginTableCell = string | {
385
395
  argument?: PluginCommandArgument;
386
396
  disabled?: boolean;
387
397
  };
388
- export type PluginUiBlock = PluginFormBlock | {
398
+ /** Host-rendered navigation list. Commands receive { generation, itemId? };
399
+ * reorder receives { generation, itemIds } containing the complete new order. */
400
+ export interface PluginNavigationListBlock {
401
+ type: 'navigation-list';
402
+ id: string;
403
+ generation: string;
404
+ label: string;
405
+ emptyText: string;
406
+ addLabel: string;
407
+ removeLabel: string;
408
+ reorderLabel: string;
409
+ items: readonly {
410
+ id: string;
411
+ label: string;
412
+ }[];
413
+ openCommand: string;
414
+ addCommand: string;
415
+ removeCommand: string;
416
+ reorderCommand: string;
417
+ }
418
+ export type PluginUiBlock = PluginExtendedUiBlock | PluginNavigationListBlock | PluginFormBlock | {
389
419
  type: 'separator';
390
420
  } | {
391
421
  type: 'callout';
@@ -615,3 +645,101 @@ export declare class PluginError extends Error {
615
645
  export declare function isPluginError(value: unknown): value is PluginError;
616
646
  /** Preserve literal manifest values while checking the v1 shape at compile time. */
617
647
  export declare function definePluginManifest<const Manifest extends PluginManifestV1>(manifest: Manifest): Manifest;
648
+ /** Menu conditions are data, never JavaScript. && binds more tightly than ||.
649
+ * Supported atoms: boolean keys / !key, or key == value / key != value.
650
+ * No parentheses, property access, or executable expressions are accepted. */
651
+ export interface PluginMenuContext {
652
+ editor?: 'markdown';
653
+ selection?: boolean;
654
+ readOnly?: boolean;
655
+ codeBlock?: boolean;
656
+ resourceKind?: 'file' | 'folder' | 'root';
657
+ resourceExt?: string;
658
+ }
659
+ export declare function isValidPluginMenuCondition(condition: string): boolean;
660
+ export declare function matchesPluginMenuCondition(condition: string | undefined, context: PluginMenuContext): boolean;
661
+ export interface PluginActionConfirmation {
662
+ title: string;
663
+ description?: string;
664
+ confirmLabel: string;
665
+ cancelLabel: string;
666
+ }
667
+ export interface PluginUiAction {
668
+ id: string;
669
+ label: string;
670
+ command: string;
671
+ argument?: PluginCommandArgument;
672
+ icon?: string;
673
+ iconOnly?: boolean;
674
+ confirmation?: PluginActionConfirmation;
675
+ disabled?: boolean;
676
+ variant?: 'default' | 'secondary' | 'destructive' | 'ghost' | 'outline';
677
+ }
678
+ export interface PluginItemListBlock {
679
+ type: 'item-list';
680
+ id: string;
681
+ generation: string;
682
+ label: string;
683
+ emptyText: string;
684
+ items: readonly {
685
+ id: string;
686
+ label: string;
687
+ description?: string;
688
+ metadata?: string;
689
+ icon?: string;
690
+ checked?: boolean;
691
+ disabled?: boolean;
692
+ }[];
693
+ openCommand?: string;
694
+ toggleCommand?: string;
695
+ reorderCommand?: string;
696
+ reorderLabel?: string;
697
+ actions?: readonly PluginUiAction[];
698
+ }
699
+ export type PluginExtendedUiBlock = PluginItemListBlock | {
700
+ type: 'layout';
701
+ id: string;
702
+ direction?: 'row' | 'column';
703
+ gap?: 'small' | 'medium' | 'large';
704
+ blocks: readonly PluginUiBlock[];
705
+ } | {
706
+ type: 'section';
707
+ id: string;
708
+ title: string;
709
+ collapsible?: boolean;
710
+ defaultOpen?: boolean;
711
+ blocks: readonly PluginUiBlock[];
712
+ } | {
713
+ type: 'tabs';
714
+ id: string;
715
+ label: string;
716
+ tabs: readonly {
717
+ id: string;
718
+ label: string;
719
+ blocks: readonly PluginUiBlock[];
720
+ }[];
721
+ } | {
722
+ type: 'toolbar';
723
+ id: string;
724
+ label: string;
725
+ actions: readonly PluginUiAction[];
726
+ } | {
727
+ type: 'markdown';
728
+ text: string;
729
+ } | {
730
+ type: 'badge';
731
+ text: string;
732
+ tone?: 'default' | 'secondary' | 'outline' | 'destructive';
733
+ } | {
734
+ type: 'empty';
735
+ title: string;
736
+ description?: string;
737
+ icon?: string;
738
+ } | {
739
+ type: 'loading';
740
+ label: string;
741
+ };
742
+ /** Walk a UI document with shared depth/count limits, including nested forms. */
743
+ export declare function flattenPluginUiBlocks(blocks: readonly PluginUiBlock[]): PluginUiBlock[];
744
+ /** Shared validation used by NoteGen and the SDK's in-memory host. */
745
+ export declare function parsePluginUiExtension(value: unknown, parseChildren: (value: unknown) => readonly PluginUiBlock[]): PluginExtendedUiBlock | undefined;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** The public API version implemented by this release of NoteGen. */
2
- export const PLUGIN_API_VERSION = '0.1.0';
2
+ export const PLUGIN_API_VERSION = '0.1.1';
3
3
  export const PLUGIN_ERROR_CODES = Object.freeze([
4
4
  'PermissionDenied',
5
5
  'AlreadyRegistered',
@@ -45,3 +45,150 @@ export function isPluginError(value) {
45
45
  export function definePluginManifest(manifest) {
46
46
  return manifest;
47
47
  }
48
+ const menuBooleanKeys = new Set(['selection', 'readOnly', 'codeBlock']);
49
+ const menuStringKeys = new Set(['editor', 'resourceKind', 'resourceExt']);
50
+ function parseMenuAtom(atom) {
51
+ const boolean = /^(!)?(selection|readOnly|codeBlock)$/.exec(atom.trim());
52
+ if (boolean)
53
+ return { key: boolean[2], operator: '==', value: !boolean[1] };
54
+ const comparison = /^(\w+)\s*(==|!=)\s*([\w.-]+)$/.exec(atom.trim());
55
+ if (!comparison)
56
+ return null;
57
+ const [, key, operator, value] = comparison;
58
+ if (menuBooleanKeys.has(key) && (value === 'true' || value === 'false'))
59
+ return { key: key, operator, value: value === 'true' };
60
+ if (menuStringKeys.has(key))
61
+ return { key: key, operator, value };
62
+ return null;
63
+ }
64
+ export function isValidPluginMenuCondition(condition) {
65
+ return condition.length > 0 && condition.length <= 240
66
+ && condition.split('||').every(group => group.split('&&').every(atom => parseMenuAtom(atom) !== null));
67
+ }
68
+ export function matchesPluginMenuCondition(condition, context) {
69
+ if (condition === undefined)
70
+ return true;
71
+ if (!isValidPluginMenuCondition(condition))
72
+ return false;
73
+ return condition.split('||').some(group => group.split('&&').every(atom => {
74
+ const parsed = parseMenuAtom(atom);
75
+ const actual = context[parsed.key];
76
+ if (actual === undefined)
77
+ return false;
78
+ return parsed.operator === '==' ? actual === parsed.value : actual !== parsed.value;
79
+ }));
80
+ }
81
+ /** Walk a UI document with shared depth/count limits, including nested forms. */
82
+ export function flattenPluginUiBlocks(blocks) {
83
+ const result = [];
84
+ function visit(items, depth) {
85
+ if (depth > 6)
86
+ throw new PluginError('QuotaExceeded', 'Plugin UI nesting exceeds 6 levels');
87
+ for (const block of items) {
88
+ result.push(block);
89
+ if (result.length > 200)
90
+ throw new PluginError('QuotaExceeded', 'Plugin UI exceeds 200 blocks');
91
+ if (block.type === 'layout' || block.type === 'section')
92
+ visit(block.blocks, depth + 1);
93
+ if (block.type === 'tabs')
94
+ for (const tab of block.tabs)
95
+ visit(tab.blocks, depth + 1);
96
+ }
97
+ }
98
+ visit(blocks, 0);
99
+ const ids = new Set();
100
+ for (const block of result) {
101
+ if ('id' in block && block.id !== undefined) {
102
+ const key = `${block.type}:${block.id}`;
103
+ if (ids.has(key))
104
+ throw new PluginError('InvalidPath', 'Duplicate UI block ID');
105
+ ids.add(key);
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+ /** Shared validation used by NoteGen and the SDK's in-memory host. */
111
+ export function parsePluginUiExtension(value, parseChildren) {
112
+ const parsed = parsePluginUiExtensionValue(value, parseChildren);
113
+ // Validation constructs optional fields explicitly. Omit absent fields from
114
+ // the normalized result so snapshots remain valid JSON on both host paths.
115
+ // Raw input and command arguments are validated before this normalization.
116
+ return parsed === undefined ? undefined : JSON.parse(JSON.stringify(parsed));
117
+ }
118
+ function parsePluginUiExtensionValue(value, parseChildren) {
119
+ const fail = () => { throw new PluginError('InvalidPath', 'Malformed extended plugin UI block'); };
120
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : fail();
121
+ const text = (value, max = 500, min = 1) => typeof value === 'string' && value.length >= min && value.length <= max ? value : fail();
122
+ const boolean = (value) => value === undefined || typeof value === 'boolean' ? value : fail();
123
+ const choice = (value, options) => value === undefined ? undefined : options.includes(value) ? value : fail();
124
+ const keys = (value, allowed) => { if (Object.keys(value).some(key => !allowed.includes(key)))
125
+ fail(); };
126
+ const array = (value, max) => Array.isArray(value) && value.length <= max ? value : fail();
127
+ const unique = (items) => { if (new Set(items.map(item => item.id)).size !== items.length)
128
+ fail(); };
129
+ const optionalText = (value, max = 500) => value === undefined ? undefined : text(value, max);
130
+ const action = (value) => {
131
+ const v = record(value);
132
+ keys(v, ['id', 'label', 'command', 'argument', 'icon', 'iconOnly', 'confirmation', 'disabled', 'variant']);
133
+ // Arguments cross the existing JSON-only RPC boundary; reject non-JSON values here too.
134
+ const json = (value, depth = 0) => depth <= 12 && (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) || Array.isArray(value) && value.every(item => json(item, depth + 1)) || typeof value === 'object' && value !== null && Object.values(value).every(item => json(item, depth + 1)));
135
+ if (v.argument !== undefined && !json(v.argument))
136
+ fail();
137
+ let confirmation;
138
+ if (v.confirmation !== undefined) {
139
+ const c = record(v.confirmation);
140
+ keys(c, ['title', 'description', 'confirmLabel', 'cancelLabel']);
141
+ confirmation = { title: text(c.title, 240), description: optionalText(c.description, 2000), confirmLabel: text(c.confirmLabel, 160), cancelLabel: text(c.cancelLabel, 160) };
142
+ }
143
+ if (v.iconOnly === true && !v.icon)
144
+ fail();
145
+ return { iconOnly: boolean(v.iconOnly), confirmation, id: text(v.id, 160), label: text(v.label, 160), command: text(v.command, 220), argument: v.argument, icon: optionalText(v.icon, 80), disabled: boolean(v.disabled), variant: choice(v.variant, ['default', 'secondary', 'destructive', 'ghost', 'outline']) };
146
+ };
147
+ const v = record(value);
148
+ switch (v.type) {
149
+ case 'layout':
150
+ keys(v, ['type', 'id', 'direction', 'gap', 'blocks']);
151
+ return { type: 'layout', id: text(v.id, 160), direction: choice(v.direction, ['row', 'column']), gap: choice(v.gap, ['small', 'medium', 'large']), blocks: parseChildren(v.blocks) };
152
+ case 'section':
153
+ keys(v, ['type', 'id', 'title', 'collapsible', 'defaultOpen', 'blocks']);
154
+ return { type: 'section', id: text(v.id, 160), title: text(v.title, 240), collapsible: boolean(v.collapsible), defaultOpen: boolean(v.defaultOpen), blocks: parseChildren(v.blocks) };
155
+ case 'tabs': {
156
+ keys(v, ['type', 'id', 'label', 'tabs']);
157
+ const tabs = array(v.tabs, 12).map(value => { const tab = record(value); keys(tab, ['id', 'label', 'blocks']); return { id: text(tab.id, 160), label: text(tab.label, 160), blocks: parseChildren(tab.blocks) }; });
158
+ if (!tabs.length)
159
+ fail();
160
+ unique(tabs);
161
+ return { type: 'tabs', id: text(v.id, 160), label: text(v.label, 160), tabs };
162
+ }
163
+ case 'toolbar': {
164
+ keys(v, ['type', 'id', 'label', 'actions']);
165
+ const actions = array(v.actions, 20).map(action);
166
+ unique(actions);
167
+ return { type: 'toolbar', id: text(v.id, 160), label: text(v.label, 160), actions };
168
+ }
169
+ case 'item-list': {
170
+ keys(v, ['type', 'id', 'generation', 'label', 'emptyText', 'items', 'openCommand', 'toggleCommand', 'reorderCommand', 'reorderLabel', 'actions']);
171
+ const items = array(v.items, 100).map(value => { const item = record(value); keys(item, ['id', 'label', 'description', 'metadata', 'icon', 'checked', 'disabled']); return { id: text(item.id, 1024), label: text(item.label), description: optionalText(item.description, 2000), metadata: optionalText(item.metadata, 1024), icon: optionalText(item.icon, 80), checked: boolean(item.checked), disabled: boolean(item.disabled) }; });
172
+ unique(items);
173
+ const actions = v.actions === undefined ? undefined : array(v.actions, 20).map(action);
174
+ if (actions)
175
+ unique(actions);
176
+ if (v.reorderCommand !== undefined && v.reorderLabel === undefined)
177
+ fail();
178
+ return { type: 'item-list', id: text(v.id, 160), generation: text(v.generation, 160), label: text(v.label, 160), emptyText: text(v.emptyText, 500, 0), items, actions, openCommand: optionalText(v.openCommand, 220), toggleCommand: optionalText(v.toggleCommand, 220), reorderCommand: optionalText(v.reorderCommand, 220), reorderLabel: optionalText(v.reorderLabel, 160) };
179
+ }
180
+ case 'markdown':
181
+ keys(v, ['type', 'text']);
182
+ return { type: 'markdown', text: text(v.text, 20000, 0) };
183
+ case 'badge':
184
+ keys(v, ['type', 'text', 'tone']);
185
+ return { type: 'badge', text: text(v.text, 160), tone: choice(v.tone, ['default', 'secondary', 'outline', 'destructive']) };
186
+ case 'empty':
187
+ keys(v, ['type', 'title', 'description', 'icon']);
188
+ return { type: 'empty', title: text(v.title, 240), description: optionalText(v.description, 2000), icon: optionalText(v.icon, 80) };
189
+ case 'loading':
190
+ keys(v, ['type', 'label']);
191
+ return { type: 'loading', label: text(v.label, 160) };
192
+ default: return undefined;
193
+ }
194
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notegen/plugin-api",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Public TypeScript contract for NoteGen plugins.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "url": "https://github.com/codexu/note-gen-plugin-sdk/issues"
38
38
  },
39
39
  "notegen": {
40
- "pluginApiVersion": "0.1.0"
40
+ "pluginApiVersion": "0.1.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "typescript": "^5.8.3"
@@ -186,6 +186,7 @@
186
186
  "title": { "$ref": "#/$defs/localizedText" },
187
187
  "description": { "$ref": "#/$defs/localizedText" },
188
188
  "icon": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "maxLength": 80 },
189
+ "keywords": { "type": "array", "maxItems": 20, "items": { "type": "string", "minLength": 1, "maxLength": 80 } },
189
190
  "suggestedShortcut": { "type": "string", "minLength": 1, "maxLength": 80 }
190
191
  }
191
192
  },
@@ -277,10 +278,13 @@
277
278
  "additionalProperties": false,
278
279
  "required": ["location", "command"],
279
280
  "properties": {
280
- "location": { "enum": ["editor/slash", "editor/context", "file/context", "mobile/writing/overflow"] },
281
+ "location": { "enum": ["editor/slash", "editor/context", "editor/selection", "editor/toolbar", "tab/context", "file/context", "mobile/writing/overflow"] },
281
282
  "command": { "$ref": "#/$defs/namespacedId" },
282
283
  "when": { "type": "string", "minLength": 1, "maxLength": 240 },
283
- "group": { "type": "string", "minLength": 1, "maxLength": 80 }
284
+ "group": { "type": "string", "minLength": 1, "maxLength": 80 },
285
+ "enableWhen": { "type": "string", "minLength": 1, "maxLength": 240 },
286
+ "order": { "type": "integer", "minimum": -10000, "maximum": 10000 },
287
+ "icon": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "maxLength": 80 }
284
288
  }
285
289
  },
286
290
  "view": {