@dolphy-app/extension-sdk 0.4.0 → 0.5.0

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.
@@ -1,7 +1,8 @@
1
1
  # Recipe: a theme
2
2
 
3
- A theme is data: no TypeScript, no `main.mjs`. This recipe is the `theme`
4
- template (`npx @dolphy-app/create-extension <dir> --id acme.hello --template theme`).
3
+ A theme is data that the client part registers: no server part, no `main.mjs`.
4
+ This recipe is the `theme` template
5
+ (`npx @dolphy-app/create-extension <dir> --id acme.hello --template theme`).
5
6
  The files below are exactly what the generator writes for the id `acme.hello`.
6
7
  See [quick-start.md](quick-start.md) for the commands.
7
8
 
@@ -18,49 +19,74 @@ File `extension.json` (theme):
18
19
  "name": "Midnight",
19
20
  "description": "A dark color theme with an amber accent for the Dolphy app.",
20
21
  "author": "your-github-login",
21
- "tags": ["theme"],
22
- "contributes": {
23
- "themes": [
24
- {
25
- "id": "acme.hello",
26
- "label": "Midnight",
27
- "dark": true,
28
- "colors": {
29
- "background": "#101820",
30
- "surface": "#1B2733",
31
- "on-background": "#E6EDF3",
32
- "on-surface": "#E6EDF3",
33
- "primary": "#FFB000",
34
- "on-primary": "#101820"
35
- },
36
- "variables": { "border-opacity": 0.2 }
37
- }
38
- ]
39
- }
22
+ "tags": ["theme"]
40
23
  }
41
24
  ```
42
25
 
43
- - `contributes.themes[]` has an `id` (not `system`, `light` or `dark`), a
44
- `label` of 1–60 characters and `dark`, which says whether the theme is dark
45
- and so picks the base colors of the interface that `colors` then override.
26
+ The manifest holds the identity only; the colors are in the code.
27
+
28
+ ## The code
29
+
30
+ File `src/theme.ts` (theme):
31
+
32
+ ```ts
33
+ import type { ThemeRegistration } from '@dolphy-app/extension-sdk';
34
+
35
+ // the allowed color and variable keys are `THEME_COLOR_KEYS` and
36
+ // `THEME_VARIABLE_KEYS` of '@dolphy-app/extension-sdk'
37
+ export const midnight: ThemeRegistration = {
38
+ id: 'acme.hello',
39
+ label: 'Midnight',
40
+ dark: true,
41
+ colors: {
42
+ background: '#101820',
43
+ surface: '#1B2733',
44
+ 'on-background': '#E6EDF3',
45
+ 'on-surface': '#E6EDF3',
46
+ primary: '#FFB000',
47
+ 'on-primary': '#101820',
48
+ },
49
+ variables: { 'border-opacity': 0.2 },
50
+ };
51
+ ```
52
+
53
+ File `src/index.ts` (theme):
54
+
55
+ ```ts
56
+ import { defineClient } from '@dolphy-app/extension-sdk';
57
+ import { midnight } from './theme.ts';
58
+
59
+ // runs in the app window: a theme is data, there is no server part
60
+ export const client = defineClient((c) => {
61
+ c.addTheme(midnight);
62
+ });
63
+ ```
64
+
65
+ - `client.addTheme(registration)` adds a tile to Settings → Appearance next to
66
+ System, Light and Dark. `id` is the extension id or starts with it and a dot,
67
+ and is not `system`, `light` or `dark`; `label` is a `LocalizedText` of 1–60
68
+ characters; `dark` says whether the theme is dark, which picks the base colors
69
+ of the interface that `colors` then override.
46
70
  - `colors` are `#rrggbb` or `#rrggbbaa` values for a fixed list of roles
47
71
  (`background`, `surface`, `primary`, the `on-…` colors for text drawn on them,
48
- `error`, `success`…). A key outside the list is rejected by `pnpm validate`.
49
- Pair every background with a readable text color.
50
- - `variables` are optional tokens from a fixed list; here `border-opacity`, a
51
- number from 0 to 1.
52
- - `build` of a project with no code writes only `extension.json` to `dist-ext`.
72
+ `error`, `success`…). The list is `THEME_COLOR_KEYS` of the SDK; a key outside
73
+ it fails the registration. Pair every background with a readable text color.
74
+ - `variables` are optional tokens from `THEME_VARIABLE_KEYS`; here
75
+ `border-opacity`, a number from 0 to 1.
76
+ - The build of this project writes `extension.json` and `client.mjs` to
77
+ `dist-ext`: there is no `server` export, so no `main.mjs`.
53
78
 
54
79
  ## The test
55
80
 
56
81
  File `test/theme.test.ts` (theme):
57
82
 
58
83
  ```ts
84
+ import { createTestClient } from '@dolphy-app/extension-sdk/testing';
59
85
  import { describe, expect, it } from 'vitest';
60
- import manifest from '../extension.json';
86
+ import { client } from '../src/index.ts';
87
+ import { midnight } from '../src/theme.ts';
61
88
 
62
- const [theme] = manifest.contributes.themes;
63
- const colors: Record<string, string> = theme.colors;
89
+ const colors = midnight.colors;
64
90
 
65
91
  // WCAG relative luminance of a #rrggbb color
66
92
  const luminance = (hex: string): number => {
@@ -79,6 +105,12 @@ const contrast = (foreground: string, background: string): number => {
79
105
  };
80
106
 
81
107
  describe('acme.hello: theme', () => {
108
+ it('the client adds the theme', async () => {
109
+ const running = await createTestClient(client, { extensionId: 'acme.hello' });
110
+ expect(running.themes).toEqual([midnight]);
111
+ await running.dispose();
112
+ });
113
+
82
114
  it.each([
83
115
  ['on-surface', 'surface'],
84
116
  ['on-background', 'background'],
@@ -91,16 +123,16 @@ describe('acme.hello: theme', () => {
91
123
  it('a dark theme has a dark background and a light text', () => {
92
124
  const background = luminance(colors['background'] as string);
93
125
  const text = luminance(colors['on-background'] as string);
94
- expect(theme.dark ? background < text : background > text).toBe(true);
126
+ expect(midnight.dark ? background < text : background > text).toBe(true);
95
127
  });
96
128
  });
97
129
  ```
98
130
 
99
- A project with no code still has something worth testing. The test reads the
100
- manifest and checks the WCAG contrast of each text color against its background
101
- (at least 4.5:1) and that `dark` agrees with the colors. A failing contrast is a
102
- real bug the learner would see, and the test also gives `vitest run` a file to
103
- run. Keep the test when you change the colors.
131
+ `createTestClient(client, { extensionId })` runs `client` on a context that
132
+ records what it adds, so the test sees the theme in `running.themes`. The rest
133
+ checks the WCAG contrast of each text color against its background (at least
134
+ 4.5:1) and that `dark` agrees with the colors. A failing contrast is a real bug
135
+ the learner would see. Keep the test when you change the colors.
104
136
 
105
137
  ## Try and ship
106
138
 
@@ -111,6 +143,6 @@ pnpm dev
111
143
  ```
112
144
 
113
145
  With `DOLPHY_DEV_EXTENSIONS` pointing at `dist-ext` (see the quick start) the
114
- theme appears as a tile in Settings → Appearance next to System, Light and
115
- Dark; saving `extension.json` is picked up by the running app. Change the id, the label and the colors; change `tags` and
146
+ theme appears as a tile in Settings → Appearance; saving a file is picked up by
147
+ the running app. Change the id, the label and the colors; change `tags` and
116
148
  `description` too, the catalog shows them.
@@ -1,11 +1,10 @@
1
- # Recipe: visibility conditions and dependencies
1
+ # Recipe: visibility conditions, a widget and dependencies
2
2
 
3
- Show a command, a panel or a widget only where it makes sense (`when`), and
4
- require another extension (`dependencies`). Both are manifest data, so this
5
- recipe is a small project whose test reads the manifest. This recipe has no
6
- template of its own: start from `blank` and replace the three files below,
7
- which are checked as a whole project. See [quick-start.md](quick-start.md) for
8
- the commands.
3
+ Show a command or a panel only where it makes sense (`when`), draw a component
4
+ into a screen of the app (an injection), and require another extension
5
+ (`dependencies`). This recipe has no template of its own: start from `blank`
6
+ and replace the files below, which are checked as a whole project. See
7
+ [quick-start.md](quick-start.md) for the commands.
9
8
 
10
9
  ## The manifest
11
10
 
@@ -24,83 +23,146 @@ File `extension.json` (when-dependencies):
24
23
  "dependencies": [
25
24
  { "id": "acme.cards", "range": ">=1.0.0 <2.0.0" },
26
25
  { "id": "dolphy.choice" }
27
- ],
28
- "contributes": {
29
- "commands": [
30
- {
31
- "id": "acme.hello.report",
32
- "title": "Course report",
33
- "when": "route == 'courses' && course.active"
34
- }
35
- ],
36
- "panels": [
37
- {
38
- "id": "acme.hello.board",
39
- "title": "Course board",
40
- "when": "course.active && !session.active"
41
- }
42
- ]
43
- }
26
+ ]
44
27
  }
45
28
  ```
46
29
 
47
- - `when` is a boolean expression over five keys of the app window: `route`
48
- (the name of the current screen), `course.active` (one course is in focus),
49
- `session.active` (a study session is open), `locale` (`ru` or `en`) and
50
- `theme.dark`. Operators: `==`, `!=`, `in ('a', 'b')`, `&&`, `||`, `!` and
51
- parentheses; strings are in single quotes. At most 200 characters. An unknown
52
- key or value, a wrong type or a syntax error is a manifest error with the
53
- position, so `pnpm validate` catches a typo before the app does.
54
- - While the condition is false a command is not in the palette and its keys do
55
- nothing, a panel's menu item is hidden, and a widget is not drawn. The value
56
- follows the route, the course, the session, the language and the theme
57
- without a reload. Your own code still reaches the command (`ctx.call`) and
58
- the panel (`openPanel`): `when` hides, it does not forbid.
59
- - `dependencies` lists up to 16 extensions that must be present, enabled,
60
- loaded and in the version `range` (comparators separated by a space, such as
61
- `>=1.0.0 <2.0.0`; no `^` or `~`). Otherwise the extension is shown as
62
- "dependencies not met" in Settings → Extensions, with the missing, disabled
63
- or mismatched one named, and contributes nothing. Disabling or enabling a
64
- dependency updates the dependents at once.
65
- - Nothing installs a dependency for the user, and extensions cannot call each
66
- other: a dependency only says "this must be there". A repeat and a dependency
67
- on yourself are manifest errors; extensions that depend on each other in a
68
- circle are not loaded.
30
+ `dependencies` is the only part of this recipe that lives in the manifest: up to
31
+ 16 extensions that must be present, enabled, loaded and in the version `range`
32
+ (comparators separated by a space, such as `>=1.0.0 <2.0.0`; no `^` or `~`).
33
+ Otherwise the extension is shown as "dependencies not met" in Settings →
34
+ Extensions, with the missing, disabled or mismatched one named, and contributes
35
+ nothing. Disabling or enabling a dependency updates the dependents at once.
36
+
37
+ Nothing installs a dependency for the user, and extensions cannot call each
38
+ other: a dependency only says "this must be there". A repeat and a dependency on
39
+ yourself are manifest errors; extensions that depend on each other in a circle
40
+ are not loaded.
69
41
 
70
42
  ## The code
71
43
 
72
44
  File `src/index.ts` (when-dependencies):
73
45
 
74
46
  ```ts
75
- import { defineExtension, defineExtensionPanel } from '@dolphy-app/extension-sdk';
76
- import type { ExtensionPanels } from '@dolphy-app/extension-sdk';
47
+ export { client } from './client.ts';
48
+ export { server } from './server.ts';
49
+ ```
50
+
51
+ File `src/server.ts` (when-dependencies):
52
+
53
+ ```ts
54
+ import { defineServer } from '@dolphy-app/extension-sdk';
55
+
56
+ export const server = defineServer((s) => {
57
+ s.registerCommand({
58
+ id: 'acme.hello.report',
59
+ title: { en: 'Course report', ru: 'Отчёт по курсу' },
60
+ when: "route == 'courses' && course.active",
61
+ run: () => ({ courses: 1 }),
62
+ });
63
+ });
64
+ ```
65
+
66
+ File `src/client.ts` (when-dependencies):
67
+
68
+ ```ts
69
+ import { anchorSelector, defineClient } from '@dolphy-app/extension-sdk';
70
+ import { Board } from './board.ts';
71
+ import { PlanNote } from './plan-note.ts';
72
+
73
+ export const client = defineClient((c) => {
74
+ c.addPanel({
75
+ id: 'acme.hello.board',
76
+ title: { en: 'Course board', ru: 'Доска курса' },
77
+ when: 'course.active && !session.active',
78
+ component: Board,
79
+ });
80
+
81
+ // drawn inside the "Daily plan" screen, at the anchor the app keeps stable
82
+ c.addInjection({
83
+ id: 'acme.hello.plan-note',
84
+ target: anchorSelector('dailyPlan'),
85
+ component: PlanNote,
86
+ });
87
+ });
88
+ ```
89
+
90
+ File `src/board.ts` (when-dependencies):
91
+
92
+ ```ts
93
+ import { defineComponent, h } from 'vue';
77
94
 
78
- export const host = defineExtension({
79
- commands: { 'acme.hello.report': () => ({ courses: 1 }) },
95
+ export const Board = defineComponent({
96
+ setup: () => () => h('p', 'Board'),
80
97
  });
98
+ ```
99
+
100
+ File `src/plan-note.ts` (when-dependencies):
81
101
 
82
- export const panels = {
83
- 'acme.hello.board': defineExtensionPanel({
84
- mount(container) {
85
- container.textContent = 'Board';
86
- },
87
- }),
88
- } satisfies ExtensionPanels;
102
+ ```ts
103
+ import { useInjection } from '@dolphy-app/extension-sdk/client';
104
+ import { defineComponent, h } from 'vue';
105
+
106
+ // `useInjection()` tells the component where the app drew it
107
+ export const PlanNote = defineComponent({
108
+ setup() {
109
+ const injection = useInjection();
110
+ return () => h('p', `Drawn at the daily plan: ${injection.position}`);
111
+ },
112
+ });
89
113
  ```
90
114
 
91
- The conditions are in the manifest, so the code is ordinary: it does not check
92
- where it is shown. The command and the panel are only reachable where the
93
- condition is true.
115
+ - `when` is a boolean expression over five keys of the app window: `route`
116
+ (the name of the current screen, one of `WHEN_ROUTES`), `course.active` (one
117
+ course is in focus), `session.active` (a study session is open), `locale`
118
+ (`ru` or `en`) and `theme.dark`. Operators: `==`, `!=`, `in ('a', 'b')`, `&&`,
119
+ `||`, `!` and parentheses; strings are in single quotes. At most 200
120
+ characters. It goes on a command (`server.registerCommand`, also on one of its
121
+ `keybindings`), a client command (`client.addCommand`) or a panel
122
+ (`client.addPanel`). An unknown key or value, a wrong type or a syntax error
123
+ fails the registration with the position in the message.
124
+ - While the condition is false a command is not in the palette and its keys do
125
+ nothing, and a panel's menu item is hidden. The value follows the route, the
126
+ course, the session, the language and the theme without a reload. Your own code
127
+ still reaches the command (`panel.call`) and the panel (`openPanel`): `when`
128
+ hides, it does not forbid.
129
+ - `client.addInjection({ id, target, position?, component })` draws the
130
+ component at every element that matches the CSS selector `target`: `before` or
131
+ `after` it, or inside it as its first (`prepend`) or last (`append`, the
132
+ default) child. The window watches its DOM: the component is mounted when a
133
+ target appears and removed when it goes away or when the extension is
134
+ unloaded. An injection has no `when`: it exists where the target does.
135
+ - `anchorSelector('dailyPlan')` is `[data-ext-anchor="dailyPlan"]`, the place
136
+ the app marks in the "Daily plan" screen and keeps stable. Any other selector
137
+ depends on the markup of the app, which can change between versions, so the
138
+ injection may silently stop finding its target after an update; prefer an
139
+ anchor.
140
+ - The injected component runs in the app's own tree: `inject`, Vuetify, the
141
+ theme and the language work. A failure shows an error card in its place and
142
+ does not touch the rest of the window.
94
143
 
95
144
  ## The test
96
145
 
97
- File `test/when.test.ts` (when-dependencies):
146
+ File `test/index.test.ts` (when-dependencies):
98
147
 
99
148
  ```ts
100
- import { evaluateWhen, parseWhen } from '@dolphy-app/extension-sdk';
149
+ // @vitest-environment happy-dom
150
+ import {
151
+ INJECTION_HANDLE_KEY,
152
+ anchorSelector,
153
+ evaluateWhen,
154
+ parseWhen,
155
+ } from '@dolphy-app/extension-sdk';
101
156
  import type { WhenContext } from '@dolphy-app/extension-sdk';
157
+ import {
158
+ createTestClient,
159
+ createTestServer,
160
+ } from '@dolphy-app/extension-sdk/testing';
102
161
  import { describe, expect, it } from 'vitest';
162
+ import { createApp, h } from 'vue';
103
163
  import manifest from '../extension.json';
164
+ import { client, server } from '../src/index.ts';
165
+ import { PlanNote } from '../src/plan-note.ts';
104
166
 
105
167
  const context = (overrides: Partial<WhenContext> = {}): WhenContext => ({
106
168
  route: 'courses',
@@ -111,27 +173,35 @@ const context = (overrides: Partial<WhenContext> = {}): WhenContext => ({
111
173
  ...overrides,
112
174
  });
113
175
 
114
- const [command] = manifest.contributes.commands;
115
- const [panel] = manifest.contributes.panels;
176
+ const parsed = (when: string | null | undefined) => {
177
+ if (when === null || when === undefined) throw new Error('no condition');
178
+ return parseWhen(when);
179
+ };
116
180
 
117
181
  describe('acme.hello: when', () => {
118
- it('shows the report on the Courses screen with a course in focus', () => {
119
- const when = parseWhen(command.when);
120
- expect(evaluateWhen(when, context())).toBe(true);
121
- expect(evaluateWhen(when, context({ route: 'daily-plan' }))).toBe(
122
- false,
182
+ it('shows the report on the Courses screen with a course in focus', async () => {
183
+ const running = await createTestServer(server, {
184
+ extensionId: 'acme.hello',
185
+ });
186
+ const report = running.registration.commands.find(
187
+ ({ id }) => id === 'acme.hello.report',
123
188
  );
124
- expect(
125
- evaluateWhen(when, context({ 'course.active': false })),
126
- ).toBe(false);
189
+ const when = parsed(report?.when);
190
+ expect(evaluateWhen(when, context())).toBe(true);
191
+ expect(evaluateWhen(when, context({ route: 'daily-plan' }))).toBe(false);
192
+ expect(evaluateWhen(when, context({ 'course.active': false }))).toBe(false);
193
+ await running.dispose();
127
194
  });
128
195
 
129
- it('hides the board during a study session', () => {
130
- const when = parseWhen(panel.when);
196
+ it('hides the board during a study session', async () => {
197
+ const running = await createTestClient(client, {
198
+ extensionId: 'acme.hello',
199
+ });
200
+ const board = running.panels.find(({ id }) => id === 'acme.hello.board');
201
+ const when = parsed(board?.when);
131
202
  expect(evaluateWhen(when, context())).toBe(true);
132
- expect(
133
- evaluateWhen(when, context({ 'session.active': true })),
134
- ).toBe(false);
203
+ expect(evaluateWhen(when, context({ 'session.active': true }))).toBe(false);
204
+ await running.dispose();
135
205
  });
136
206
 
137
207
  it('reports a typo in a key with its position', () => {
@@ -139,6 +209,35 @@ describe('acme.hello: when', () => {
139
209
  });
140
210
  });
141
211
 
212
+ describe('acme.hello: injection', () => {
213
+ it('is drawn at the anchor of the daily plan', async () => {
214
+ const running = await createTestClient(client, {
215
+ extensionId: 'acme.hello',
216
+ });
217
+ expect(running.injections).toEqual([
218
+ {
219
+ id: 'acme.hello.plan-note',
220
+ target: anchorSelector('dailyPlan'),
221
+ position: 'append',
222
+ component: PlanNote,
223
+ },
224
+ ]);
225
+ await running.dispose();
226
+ });
227
+
228
+ it('the component reads where the app drew it', () => {
229
+ const host = document.createElement('div');
230
+ const app = createApp({ render: () => h(PlanNote) });
231
+ app.provide(INJECTION_HANDLE_KEY, {
232
+ target: document.createElement('div'),
233
+ position: 'append',
234
+ });
235
+ app.mount(host);
236
+ expect(host.textContent).toBe('Drawn at the daily plan: append');
237
+ app.unmount();
238
+ });
239
+ });
240
+
142
241
  describe('acme.hello: dependencies', () => {
143
242
  it('names each required extension once, and not itself', () => {
144
243
  const ids = manifest.dependencies.map(({ id }) => id);
@@ -148,11 +247,18 @@ describe('acme.hello: dependencies', () => {
148
247
  });
149
248
  ```
150
249
 
151
- `parseWhen` and `evaluateWhen` are the functions the app and `dolphy-ext
152
- validate` use. `evaluateWhen` is pure: pass an object with the five keys.
250
+ `createTestServer` and `createTestClient` hand the test what the code
251
+ registered: `running.registration.commands` for the server (with `when` as
252
+ registered) and `running.panels` and `running.injections` for the client. The
253
+ test feeds that text to `parseWhen` and `evaluateWhen`, the functions the app
254
+ uses; `evaluateWhen` is pure, so pass an object with the five keys. The
255
+ dependencies are manifest data, so the test reads `extension.json`.
153
256
 
154
257
  ## Try and ship
155
258
 
156
- Run `pnpm validate`: it fails on a bad `when` or a bad `dependencies` entry.
157
- In the app the extension's row in Settings → Extensions shows "dependencies not
158
- met" until `acme.cards` is installed and enabled.
259
+ Run `pnpm build` and `pnpm validate`, which fails on a bad `dependencies`
260
+ entry. The test parses every `when`, so `pnpm test` catches a typo before the
261
+ app does (the app fails the registration with the position). In the app the
262
+ extension's row in Settings → Extensions shows "dependencies not met" until
263
+ `acme.cards` is installed and enabled. Open the Daily plan screen to see the
264
+ injected note.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dolphy-app/extension-sdk",
3
- "version": "0.4.0",
4
- "description": "SDK for extension authors: defineExtension, defineAnswerView, test helpers",
3
+ "version": "0.5.0",
4
+ "description": "SDK for extension authors: defineServer, defineClient, defineRpc, usePanel, useInjection, useApp, useEngine, useRpc, defineMountable, callRpc, reactComponent, test harness",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "exports": {
@@ -9,13 +9,21 @@
9
9
  "types": "./dist/index.d.ts",
10
10
  "default": "./dist/index.js"
11
11
  },
12
- "./runtime": {
13
- "types": "./dist/runtime.d.ts",
14
- "default": "./dist/runtime.js"
12
+ "./client": {
13
+ "types": "./dist/client.d.ts",
14
+ "default": "./dist/client.js"
15
+ },
16
+ "./rpc": {
17
+ "types": "./dist/rpc.d.ts",
18
+ "default": "./dist/rpc.js"
15
19
  },
16
20
  "./testing": {
17
21
  "types": "./dist/testing.d.ts",
18
22
  "default": "./dist/testing.js"
23
+ },
24
+ "./react": {
25
+ "types": "./dist/react.d.ts",
26
+ "default": "./dist/react.js"
19
27
  }
20
28
  },
21
29
  "types": "./dist/index.d.ts",
@@ -24,8 +32,22 @@
24
32
  "docs"
25
33
  ],
26
34
  "dependencies": {
27
- "@dolphy-app/extension-api": "0.4.0",
28
- "ajv": "^8"
35
+ "@dolphy-app/extension-api": "0.5.0",
36
+ "ajv": "^8",
37
+ "zod": "4.6.5"
38
+ },
39
+ "peerDependencies": {
40
+ "vue": "^3.5",
41
+ "react": "^19",
42
+ "react-dom": "^19"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "react": {
46
+ "optional": true
47
+ },
48
+ "react-dom": {
49
+ "optional": true
50
+ }
29
51
  },
30
52
  "engines": {
31
53
  "node": ">=22.12"
@@ -1,114 +0,0 @@
1
- import { ANSWER_EVENT, ELEMENT_NAME_PATTERN } from "@dolphy-app/extension-api";
2
-
3
- //#region packages/extension-sdk/src/answer-element.ts
4
- const logFailure = (tag, message, error) => {
5
- console.error({
6
- error,
7
- tag
8
- }, message);
9
- };
10
- const createAnswerElementClass = (tag, answerView) => class AnswerElement extends HTMLElement {
11
- #root = this.attachShadow({ mode: "open" });
12
- #props = {
13
- view: void 0,
14
- value: void 0,
15
- disabled: false,
16
- verdict: null
17
- };
18
- #instance = null;
19
- #isFlushScheduled = false;
20
- get view() {
21
- return this.#props.view;
22
- }
23
- set view(view) {
24
- this.#change({ view });
25
- }
26
- get value() {
27
- return this.#props.value;
28
- }
29
- set value(value) {
30
- this.#change({ value });
31
- }
32
- get disabled() {
33
- return this.#props.disabled;
34
- }
35
- set disabled(disabled) {
36
- this.#change({ disabled });
37
- }
38
- get verdict() {
39
- return this.#props.verdict;
40
- }
41
- set verdict(verdict) {
42
- this.#change({ verdict });
43
- }
44
- connectedCallback() {
45
- if (this.#instance !== null) return;
46
- const readLabel = () => this.getAttribute("aria-label");
47
- const api = {
48
- root: this.#root,
49
- get label() {
50
- return readLabel();
51
- },
52
- setAnswer: (value, complete) => {
53
- const detail = {
54
- value,
55
- complete
56
- };
57
- this.#emit(ANSWER_EVENT.change, detail);
58
- },
59
- submit: () => this.#emit(ANSWER_EVENT.submit, void 0)
60
- };
61
- try {
62
- this.#instance = answerView.mount(api, Object.freeze({ ...this.#props }));
63
- } catch (error) {
64
- logFailure(tag, "answer element failed to mount", error);
65
- }
66
- }
67
- disconnectedCallback() {
68
- const instance = this.#instance;
69
- this.#instance = null;
70
- if (instance === null) return;
71
- try {
72
- instance.destroy?.();
73
- } catch (error) {
74
- logFailure(tag, "answer element failed to destroy", error);
75
- }
76
- this.#root.replaceChildren();
77
- }
78
- #change(patch) {
79
- this.#props = {
80
- ...this.#props,
81
- ...patch
82
- };
83
- if (this.#instance === null || this.#isFlushScheduled) return;
84
- this.#isFlushScheduled = true;
85
- queueMicrotask(() => this.#flush());
86
- }
87
- #flush() {
88
- this.#isFlushScheduled = false;
89
- const instance = this.#instance;
90
- if (instance === null) return;
91
- try {
92
- instance.update(Object.freeze({ ...this.#props }));
93
- } catch (error) {
94
- logFailure(tag, "answer element failed to update", error);
95
- }
96
- }
97
- #emit(name, detail) {
98
- this.dispatchEvent(new CustomEvent(name, {
99
- detail,
100
- bubbles: true,
101
- composed: true
102
- }));
103
- }
104
- };
105
- /** Defines the kind's custom element; calling again with the same tag changes nothing. */
106
- const registerAnswerView = (tag, view) => {
107
- if (!ELEMENT_NAME_PATTERN.test(tag)) throw new TypeError(`invalid custom element name '${tag}'`);
108
- if (typeof view?.mount !== "function") throw new TypeError(`answer view for '${tag}' has no mount()`);
109
- if (customElements.get(tag) !== void 0) return;
110
- customElements.define(tag, createAnswerElementClass(tag, view));
111
- };
112
-
113
- //#endregion
114
- export { registerAnswerView as n, createAnswerElementClass as t };