@notegen/plugin-api 0.1.0 → 0.1.1

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,91 @@ 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
+ - `markdown`: formatted text with raw HTML, links and image loading disabled.
164
+ - `badge`, `empty`, `loading`: standard theme-aware feedback.
165
+ - Form fields additionally support `search`, ISO `date`, and searchable
166
+ `note-picker`. Supply note choices as `{label,value}` through existing scoped
167
+ `notes.list` permissions. The picker does not enumerate files or grant access.
168
+
169
+ Nested forms retain their values when a surrounding section/tab rerenders. IDs
170
+ must be unique per block type across the whole document. Limits: 6 nesting levels,
171
+ 200 total blocks, 50 blocks per container, 100 list items, 20 actions, 12 tabs,
172
+ and the existing 128 KiB document limit. Command ownership is validated recursively.
173
+
174
+ List open/toggle/action commands receive `{generation,itemId}`; toggle adds
175
+ `checked`. Context actions add `actionId` and optionally nested `argument`.
176
+ Reorder commands receive `{generation,itemIds}` with the complete requested order.
177
+ The plugin must reject stale generations and invalid IDs, persist successful
178
+ changes, then publish updated content. The host never mutates plugin storage.
179
+
180
+ An action may provide `confirmation: {title,description?,confirmLabel,cancelLabel}`.
181
+ Its command runs only after confirmation. Legacy `navigation-list` remains a
182
+ compatibility adapter; new plugins should compose toolbar and item-list blocks.
183
+
184
+ Supported symbolic icons include bookmark, calendar-days, file-text, files, folder,
185
+ folder-open, layout-template, list-checks, list-todo, link, search, plus, minus,
186
+ trash-2, pencil, copy, check, x, star, pin, tag, settings, more-horizontal, arrow-up,
187
+ arrow-down, download, upload, external-link, list, table-2, columns-3, clock,
188
+ book-open, code, sparkles, shuffle, refresh-cw, chart-no-axes-combined, file-input,
189
+ and flask-conical. Unknown icons fall back to a puzzle icon. Raw SVG/HTML and
190
+ 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[];
@@ -165,6 +169,8 @@ export interface ReadNoteOptions {
165
169
  path: string;
166
170
  }
167
171
  export interface OpenOrCreateNoteOptions {
172
+ /** Defaults to true. Set false with open: true to open an existing file using only notes.open. Missing files fail. */
173
+ create?: boolean;
168
174
  workspaceId: string;
169
175
  path: string;
170
176
  initialContent: string;
@@ -239,6 +245,8 @@ export interface NoteChangeEvent {
239
245
  previousPath?: string;
240
246
  }
241
247
  export interface ActiveEditorContext {
248
+ /** Workspace-relative Markdown path, when available. Never an absolute path. Requires editor.read. */
249
+ path?: string;
242
250
  windowId: string;
243
251
  editorId: string;
244
252
  documentId: string;
@@ -324,7 +332,7 @@ export type PluginFormField = {
324
332
  disabled?: boolean;
325
333
  visibleWhen?: PluginFormCondition;
326
334
  } & ({
327
- type: 'text' | 'textarea';
335
+ type: 'text' | 'textarea' | 'search' | 'date';
328
336
  value?: string;
329
337
  placeholder?: string;
330
338
  maxLength?: number;
@@ -334,7 +342,7 @@ export type PluginFormField = {
334
342
  min?: number;
335
343
  max?: number;
336
344
  } | {
337
- type: 'select';
345
+ type: 'select' | 'note-picker';
338
346
  value?: string;
339
347
  options: readonly {
340
348
  label: string;
@@ -385,7 +393,27 @@ export type PluginTableCell = string | {
385
393
  argument?: PluginCommandArgument;
386
394
  disabled?: boolean;
387
395
  };
388
- export type PluginUiBlock = PluginFormBlock | {
396
+ /** Host-rendered navigation list. Commands receive { generation, itemId? };
397
+ * reorder receives { generation, itemIds } containing the complete new order. */
398
+ export interface PluginNavigationListBlock {
399
+ type: 'navigation-list';
400
+ id: string;
401
+ generation: string;
402
+ label: string;
403
+ emptyText: string;
404
+ addLabel: string;
405
+ removeLabel: string;
406
+ reorderLabel: string;
407
+ items: readonly {
408
+ id: string;
409
+ label: string;
410
+ }[];
411
+ openCommand: string;
412
+ addCommand: string;
413
+ removeCommand: string;
414
+ reorderCommand: string;
415
+ }
416
+ export type PluginUiBlock = PluginExtendedUiBlock | PluginNavigationListBlock | PluginFormBlock | {
389
417
  type: 'separator';
390
418
  } | {
391
419
  type: 'callout';
@@ -615,3 +643,100 @@ export declare class PluginError extends Error {
615
643
  export declare function isPluginError(value: unknown): value is PluginError;
616
644
  /** Preserve literal manifest values while checking the v1 shape at compile time. */
617
645
  export declare function definePluginManifest<const Manifest extends PluginManifestV1>(manifest: Manifest): Manifest;
646
+ /** Menu conditions are data, never JavaScript. && binds more tightly than ||.
647
+ * Supported atoms: boolean keys / !key, or key == value / key != value.
648
+ * No parentheses, property access, or executable expressions are accepted. */
649
+ export interface PluginMenuContext {
650
+ editor?: 'markdown';
651
+ selection?: boolean;
652
+ readOnly?: boolean;
653
+ codeBlock?: boolean;
654
+ resourceKind?: 'file' | 'folder' | 'root';
655
+ resourceExt?: string;
656
+ }
657
+ export declare function isValidPluginMenuCondition(condition: string): boolean;
658
+ export declare function matchesPluginMenuCondition(condition: string | undefined, context: PluginMenuContext): boolean;
659
+ export interface PluginActionConfirmation {
660
+ title: string;
661
+ description?: string;
662
+ confirmLabel: string;
663
+ cancelLabel: string;
664
+ }
665
+ export interface PluginUiAction {
666
+ id: string;
667
+ label: string;
668
+ command: string;
669
+ argument?: PluginCommandArgument;
670
+ icon?: string;
671
+ iconOnly?: boolean;
672
+ confirmation?: PluginActionConfirmation;
673
+ disabled?: boolean;
674
+ variant?: 'default' | 'secondary' | 'destructive' | 'ghost' | 'outline';
675
+ }
676
+ export interface PluginItemListBlock {
677
+ type: 'item-list';
678
+ id: string;
679
+ generation: string;
680
+ label: string;
681
+ emptyText: string;
682
+ items: readonly {
683
+ id: string;
684
+ label: string;
685
+ description?: string;
686
+ icon?: string;
687
+ checked?: boolean;
688
+ disabled?: boolean;
689
+ }[];
690
+ openCommand?: string;
691
+ toggleCommand?: string;
692
+ reorderCommand?: string;
693
+ reorderLabel?: string;
694
+ actions?: readonly PluginUiAction[];
695
+ }
696
+ export type PluginExtendedUiBlock = PluginItemListBlock | {
697
+ type: 'layout';
698
+ id: string;
699
+ direction?: 'row' | 'column';
700
+ gap?: 'small' | 'medium' | 'large';
701
+ blocks: readonly PluginUiBlock[];
702
+ } | {
703
+ type: 'section';
704
+ id: string;
705
+ title: string;
706
+ collapsible?: boolean;
707
+ defaultOpen?: boolean;
708
+ blocks: readonly PluginUiBlock[];
709
+ } | {
710
+ type: 'tabs';
711
+ id: string;
712
+ label: string;
713
+ tabs: readonly {
714
+ id: string;
715
+ label: string;
716
+ blocks: readonly PluginUiBlock[];
717
+ }[];
718
+ } | {
719
+ type: 'toolbar';
720
+ id: string;
721
+ label: string;
722
+ actions: readonly PluginUiAction[];
723
+ } | {
724
+ type: 'markdown';
725
+ text: string;
726
+ } | {
727
+ type: 'badge';
728
+ text: string;
729
+ tone?: 'default' | 'secondary' | 'outline' | 'destructive';
730
+ } | {
731
+ type: 'empty';
732
+ title: string;
733
+ description?: string;
734
+ icon?: string;
735
+ } | {
736
+ type: 'loading';
737
+ label: string;
738
+ };
739
+ /** Walk a UI document with shared depth/count limits, including nested forms. */
740
+ export declare function flattenPluginUiBlocks(blocks: readonly PluginUiBlock[]): PluginUiBlock[];
741
+ /** Shared validation used by NoteGen and the SDK's in-memory host. */
742
+ 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', 'icon', 'checked', 'disabled']); return { id: text(item.id, 1024), label: text(item.label), description: optionalText(item.description, 2000), 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.1",
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": {