@openchamber/sdk 2.1.0 → 2.2.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.
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export declare const guestStatusControlsSchema: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
3
+ kind: z.ZodLiteral<"button">;
4
+ id: z.ZodString;
5
+ label: z.ZodString;
6
+ disabled: z.ZodOptional<z.ZodBoolean>;
7
+ }, z.core.$strict>, z.ZodObject<{
8
+ kind: z.ZodLiteral<"select">;
9
+ id: z.ZodString;
10
+ label: z.ZodString;
11
+ value: z.ZodString;
12
+ options: z.ZodArray<z.ZodObject<{
13
+ value: z.ZodString;
14
+ label: z.ZodString;
15
+ }, z.core.$strict>>;
16
+ disabled: z.ZodOptional<z.ZodBoolean>;
17
+ }, z.core.$strict>], "kind">>;
18
+ export declare const guestStatusControlEventSchema: z.ZodObject<{
19
+ id: z.ZodString;
20
+ value: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strict>;
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import { GUEST_STATUS_CONTROL_ID, GUEST_STATUS_CONTROL_LABEL_MAX, GUEST_STATUS_CONTROL_OPTIONS_MAX, GUEST_STATUS_CONTROL_VALUE_MAX, GUEST_STATUS_CONTROLS_MAX, } from "./status-controls.js";
3
+ const idSchema = z.string().regex(GUEST_STATUS_CONTROL_ID);
4
+ const labelSchema = z.string().min(1).max(GUEST_STATUS_CONTROL_LABEL_MAX);
5
+ const optionSchema = z.object({
6
+ value: z.string().min(1).max(GUEST_STATUS_CONTROL_VALUE_MAX),
7
+ label: labelSchema,
8
+ }).strict();
9
+ const buttonSchema = z.object({
10
+ kind: z.literal('button'),
11
+ id: idSchema,
12
+ label: labelSchema,
13
+ disabled: z.boolean().optional(),
14
+ }).strict();
15
+ const selectSchema = z.object({
16
+ kind: z.literal('select'),
17
+ id: idSchema,
18
+ label: labelSchema,
19
+ value: z.string().min(1).max(GUEST_STATUS_CONTROL_VALUE_MAX),
20
+ options: z.array(optionSchema).min(1).max(GUEST_STATUS_CONTROL_OPTIONS_MAX),
21
+ disabled: z.boolean().optional(),
22
+ }).strict().refine((control) => new Set(control.options.map((option) => option.value)).size === control.options.length, { message: 'status control option values must be unique', path: ['options'] }).refine((control) => control.options.some((option) => option.value === control.value), { message: 'status control value must be one of its options', path: ['value'] });
23
+ export const guestStatusControlsSchema = z.array(z.discriminatedUnion('kind', [buttonSchema, selectSchema]))
24
+ .max(GUEST_STATUS_CONTROLS_MAX)
25
+ .refine((controls) => new Set(controls.map((control) => control.id)).size === controls.length, { message: 'status control ids must be unique' });
26
+ export const guestStatusControlEventSchema = z.object({
27
+ id: idSchema,
28
+ value: z.string().min(1).max(GUEST_STATUS_CONTROL_VALUE_MAX).optional(),
29
+ }).strict();
@@ -0,0 +1,31 @@
1
+ export declare const GUEST_STATUS_CONTROLS_MAX = 4;
2
+ export declare const GUEST_STATUS_CONTROL_ID: RegExp;
3
+ export declare const GUEST_STATUS_CONTROL_LABEL_MAX = 60;
4
+ export declare const GUEST_STATUS_CONTROL_OPTIONS_MAX = 16;
5
+ export declare const GUEST_STATUS_CONTROL_VALUE_MAX = 80;
6
+ export type GuestStatusControlOption = {
7
+ value: string;
8
+ label: string;
9
+ };
10
+ export type GuestStatusControl = {
11
+ kind: 'button';
12
+ id: string;
13
+ label: string;
14
+ disabled?: boolean;
15
+ } | {
16
+ kind: 'select';
17
+ id: string;
18
+ label: string;
19
+ value: string;
20
+ options: GuestStatusControlOption[];
21
+ disabled?: boolean;
22
+ };
23
+ export type GuestStatusControlEvent = {
24
+ id: string;
25
+ value?: string;
26
+ };
27
+ /**
28
+ * Guest-side fast-fail guard for `setStatusControls`. The host zod schema is
29
+ * still authoritative because JavaScript callers can bypass this SDK method.
30
+ */
31
+ export declare const isGuestStatusControls: (controls: GuestStatusControl[]) => boolean;
@@ -0,0 +1,48 @@
1
+ export const GUEST_STATUS_CONTROLS_MAX = 4;
2
+ export const GUEST_STATUS_CONTROL_ID = /^[a-z][a-z0-9-]{0,39}$/;
3
+ export const GUEST_STATUS_CONTROL_LABEL_MAX = 60;
4
+ export const GUEST_STATUS_CONTROL_OPTIONS_MAX = 16;
5
+ export const GUEST_STATUS_CONTROL_VALUE_MAX = 80;
6
+ const isControlObject = (value) => (Object(value) === value && !Array.isArray(value));
7
+ const hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
8
+ const isLabel = (value) => (String(value) === value && value.length >= 1 && value.length <= GUEST_STATUS_CONTROL_LABEL_MAX);
9
+ const isValue = (value) => (String(value) === value && value.length >= 1 && value.length <= GUEST_STATUS_CONTROL_VALUE_MAX);
10
+ /**
11
+ * Guest-side fast-fail guard for `setStatusControls`. The host zod schema is
12
+ * still authoritative because JavaScript callers can bypass this SDK method.
13
+ */
14
+ export const isGuestStatusControls = (controls) => {
15
+ // The public type is precise, but this boundary also receives untyped JavaScript callers.
16
+ if (!Array.isArray(controls) || controls.length > GUEST_STATUS_CONTROLS_MAX)
17
+ return false;
18
+ const ids = new Set();
19
+ for (const control of controls) {
20
+ if (!isControlObject(control) || !GUEST_STATUS_CONTROL_ID.test(control.id) || !isLabel(control.label))
21
+ return false;
22
+ if (control.disabled !== undefined && control.disabled !== true && control.disabled !== false)
23
+ return false;
24
+ if (ids.has(control.id))
25
+ return false;
26
+ ids.add(control.id);
27
+ if (control.kind === 'button') {
28
+ if (!hasOnlyKeys(control, ['kind', 'id', 'label', 'disabled']))
29
+ return false;
30
+ continue;
31
+ }
32
+ if (control.kind !== 'select' || !hasOnlyKeys(control, ['kind', 'id', 'label', 'value', 'options', 'disabled']))
33
+ return false;
34
+ if (!isValue(control.value) || !Array.isArray(control.options)
35
+ || control.options.length < 1 || control.options.length > GUEST_STATUS_CONTROL_OPTIONS_MAX)
36
+ return false;
37
+ const values = new Set();
38
+ for (const option of control.options) {
39
+ if (!isControlObject(option) || !hasOnlyKeys(option, ['value', 'label'])
40
+ || !isValue(option.value) || !isLabel(option.label) || values.has(option.value))
41
+ return false;
42
+ values.add(option.value);
43
+ }
44
+ if (!values.has(control.value))
45
+ return false;
46
+ }
47
+ return true;
48
+ };
@@ -33,3 +33,5 @@ export { mountMenu } from './menu.ts';
33
33
  export type { MenuHandle, MenuItem, MenuProps } from './menu.ts';
34
34
  export { mountText, splitTextMedia } from './text.ts';
35
35
  export type { TextHandle, TextPart, TextProps } from './text.ts';
36
+ export { mountPopoverAnchor } from './popover-anchor.ts';
37
+ export type { PopoverAnchorHandle, PopoverAnchorOptions } from './popover-anchor.ts';
package/dist/ui/index.js CHANGED
@@ -15,3 +15,4 @@ export { mountSeparator } from "./separator.js";
15
15
  export { mountProgress } from "./progress.js";
16
16
  export { mountMenu } from "./menu.js";
17
17
  export { mountText, splitTextMedia } from "./text.js";
18
+ export { mountPopoverAnchor } from "./popover-anchor.js";
@@ -0,0 +1,16 @@
1
+ import type { HostClient } from '../host.ts';
2
+ import type { GuestPopoverSide } from '../popover.ts';
3
+ import type { JsonValue } from '../contract.ts';
4
+ export type PopoverAnchorOptions = {
5
+ host: Pick<HostClient, 'openPopover' | 'closePopover' | 'setPopoverAnchorActive' | 'onPopoverClosed'>;
6
+ getData: () => JsonValue;
7
+ width: number;
8
+ height: number;
9
+ side?: GuestPopoverSide;
10
+ label?: string;
11
+ };
12
+ export type PopoverAnchorHandle = {
13
+ dispose: () => void;
14
+ };
15
+ /** Adds a bounded host popover preview to an extension-owned element. */
16
+ export declare const mountPopoverAnchor: (element: HTMLElement, options: PopoverAnchorOptions) => PopoverAnchorHandle;
@@ -0,0 +1,198 @@
1
+ let nextPopoverAnchorId = 0;
2
+ const isKeyboardEvent = (event) => 'key' in event;
3
+ /** Adds a bounded host popover preview to an extension-owned element. */
4
+ export const mountPopoverAnchor = (element, options) => {
5
+ const document = element.ownerDocument;
6
+ const window = document.defaultView;
7
+ let timer = null;
8
+ let activeId = null;
9
+ let disposed = false;
10
+ let ignoreNextFocus = false;
11
+ let pointerActive = false;
12
+ let focusActive = false;
13
+ let observer = null;
14
+ const previousExpanded = element.getAttribute('aria-expanded');
15
+ const previousPopup = element.getAttribute('aria-haspopup');
16
+ const previousLabel = element.getAttribute('aria-label');
17
+ const ownsExpanded = previousExpanded === null;
18
+ const clearTimer = () => {
19
+ if (timer !== null)
20
+ clearTimeout(timer);
21
+ timer = null;
22
+ };
23
+ const removeActiveListeners = () => {
24
+ document.removeEventListener('scroll', closeForAnchor, true);
25
+ document.removeEventListener('pointerdown', closeForOutside, true);
26
+ window?.removeEventListener('resize', closeForAnchor);
27
+ observer?.disconnect();
28
+ observer = null;
29
+ };
30
+ const close = (reason = 'closed', restoreFocus = false) => {
31
+ const id = activeId;
32
+ clearTimer();
33
+ activeId = null;
34
+ removeActiveListeners();
35
+ element.removeAttribute('data-oc-popover-open');
36
+ if (ownsExpanded)
37
+ element.setAttribute('aria-expanded', 'false');
38
+ if (id)
39
+ void options.host.closePopover(id, reason).catch(() => undefined);
40
+ if (restoreFocus) {
41
+ ignoreNextFocus = true;
42
+ element.focus();
43
+ queueMicrotask(() => { ignoreNextFocus = false; });
44
+ }
45
+ };
46
+ const closeForAnchor = () => close();
47
+ const closeForOutside = (event) => {
48
+ const target = event.target;
49
+ // SAFETY: DOM pointer events target a Node; this avoids cross-realm constructors.
50
+ if (target !== element && !element.contains(target))
51
+ close();
52
+ };
53
+ const watchActive = () => {
54
+ document.addEventListener('scroll', closeForAnchor, true);
55
+ document.addEventListener('pointerdown', closeForOutside, true);
56
+ window?.addEventListener('resize', closeForAnchor);
57
+ observer = new MutationObserver(() => { if (!document.contains(element))
58
+ close(); });
59
+ observer.observe(document, { childList: true, subtree: true });
60
+ };
61
+ const reportAnchorActivity = () => {
62
+ if (activeId)
63
+ void options.host.setPopoverAnchorActive(activeId, pointerActive || focusActive).catch(() => undefined);
64
+ };
65
+ const open = (focus) => {
66
+ clearTimer();
67
+ if (disposed || activeId || !element.isConnected)
68
+ return;
69
+ const rect = element.getBoundingClientRect();
70
+ if (rect.width <= 0 || rect.height <= 0)
71
+ return;
72
+ const id = `oc-popover-${++nextPopoverAnchorId}`;
73
+ let data;
74
+ try {
75
+ data = options.getData();
76
+ }
77
+ catch {
78
+ return;
79
+ }
80
+ activeId = id;
81
+ element.setAttribute('data-oc-popover-open', 'true');
82
+ if (ownsExpanded)
83
+ element.setAttribute('aria-expanded', 'true');
84
+ watchActive();
85
+ void options.host.openPopover({
86
+ id,
87
+ anchor: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
88
+ width: options.width,
89
+ height: options.height,
90
+ side: options.side,
91
+ focus,
92
+ data,
93
+ }).catch(() => {
94
+ if (activeId === id)
95
+ close();
96
+ });
97
+ };
98
+ const scheduleOpen = () => {
99
+ if (!activeId && !disposed && timer === null)
100
+ timer = setTimeout(() => open(false), 250);
101
+ };
102
+ const onPointerEnter = (event) => {
103
+ if ('pointerType' in event && event.pointerType && event.pointerType !== 'mouse')
104
+ return;
105
+ pointerActive = true;
106
+ if (activeId)
107
+ reportAnchorActivity();
108
+ else if (!ignoreNextFocus)
109
+ scheduleOpen();
110
+ };
111
+ const onPointerLeave = (event) => {
112
+ if ('pointerType' in event && event.pointerType && event.pointerType !== 'mouse')
113
+ return;
114
+ pointerActive = false;
115
+ clearTimer();
116
+ reportAnchorActivity();
117
+ };
118
+ const onFocus = () => {
119
+ focusActive = true;
120
+ if (activeId)
121
+ reportAnchorActivity();
122
+ else if (!ignoreNextFocus && element.matches(':focus-visible'))
123
+ open(false);
124
+ };
125
+ const onFocusOut = () => {
126
+ focusActive = false;
127
+ reportAnchorActivity();
128
+ };
129
+ const onKeyDown = (event) => {
130
+ if (!isKeyboardEvent(event))
131
+ return;
132
+ if (event.key === 'ArrowRight') {
133
+ event.preventDefault();
134
+ if (activeId)
135
+ close();
136
+ open(true);
137
+ }
138
+ else if (event.key === 'Escape' && activeId) {
139
+ event.preventDefault();
140
+ close('escape', true);
141
+ }
142
+ };
143
+ const unsubscribe = options.host.onPopoverClosed((event) => {
144
+ if (event.id !== activeId)
145
+ return;
146
+ activeId = null;
147
+ clearTimer();
148
+ removeActiveListeners();
149
+ element.removeAttribute('data-oc-popover-open');
150
+ if (ownsExpanded)
151
+ element.setAttribute('aria-expanded', 'false');
152
+ if (event.reason === 'escape') {
153
+ ignoreNextFocus = true;
154
+ element.focus();
155
+ queueMicrotask(() => { ignoreNextFocus = false; });
156
+ }
157
+ });
158
+ element.setAttribute('aria-haspopup', 'dialog');
159
+ if (ownsExpanded)
160
+ element.setAttribute('aria-expanded', 'false');
161
+ if (options.label)
162
+ element.setAttribute('aria-label', options.label);
163
+ element.addEventListener('pointerenter', onPointerEnter);
164
+ // Crossing opaque iframe documents can omit an enter transition. A real
165
+ // pointer move over this element still establishes a new hover intent.
166
+ element.addEventListener('pointermove', onPointerEnter);
167
+ element.addEventListener('pointerleave', onPointerLeave);
168
+ element.addEventListener('focus', onFocus);
169
+ element.addEventListener('focusout', onFocusOut);
170
+ element.addEventListener('keydown', onKeyDown);
171
+ return { dispose: () => {
172
+ if (disposed)
173
+ return;
174
+ disposed = true;
175
+ clearTimer();
176
+ close();
177
+ unsubscribe();
178
+ element.removeAttribute('data-oc-popover-open');
179
+ if (previousExpanded === null)
180
+ element.removeAttribute('aria-expanded');
181
+ if (previousPopup === null)
182
+ element.removeAttribute('aria-haspopup');
183
+ else
184
+ element.setAttribute('aria-haspopup', previousPopup);
185
+ if (options.label) {
186
+ if (previousLabel === null)
187
+ element.removeAttribute('aria-label');
188
+ else
189
+ element.setAttribute('aria-label', previousLabel);
190
+ }
191
+ element.removeEventListener('pointerenter', onPointerEnter);
192
+ element.removeEventListener('pointermove', onPointerEnter);
193
+ element.removeEventListener('pointerleave', onPointerLeave);
194
+ element.removeEventListener('focus', onFocus);
195
+ element.removeEventListener('focusout', onFocusOut);
196
+ element.removeEventListener('keydown', onKeyDown);
197
+ } };
198
+ };
package/dist/ui/theme.js CHANGED
@@ -54,6 +54,16 @@ const TOKEN_VARS = [
54
54
  ['--font-mono', 'mono'],
55
55
  ['--radius', 'radius'],
56
56
  ];
57
+ const SYNTAX_TOKEN_VARS = [
58
+ ['--oc-syntax-keyword', '--syntax-keyword', 'syntaxKeyword'],
59
+ ['--oc-syntax-string', '--syntax-string', 'syntaxString'],
60
+ ['--oc-syntax-number', '--syntax-number', 'syntaxNumber'],
61
+ ['--oc-syntax-function', '--syntax-function', 'syntaxFunction'],
62
+ ['--oc-syntax-type', '--syntax-type', 'syntaxType'],
63
+ ['--oc-syntax-comment', '--syntax-comment', 'syntaxComment'],
64
+ ['--oc-syntax-variable', '--syntax-variable', 'syntaxVariable'],
65
+ ['--oc-syntax-operator', '--syntax-operator', 'syntaxOperator'],
66
+ ];
57
67
  /**
58
68
  * Paint the host theme onto the iframe root. Guest chrome reads these
59
69
  * variables, and the root itself gets the host font and text colour so plain
@@ -65,6 +75,11 @@ export const applyHostTheme = (theme, root) => {
65
75
  for (const [name, key] of TOKEN_VARS) {
66
76
  root.style.setProperty(name, theme.tokens[key]);
67
77
  }
78
+ for (const [alias, hostName, key] of SYNTAX_TOKEN_VARS) {
79
+ const value = theme.tokens[key] ?? '';
80
+ root.style.setProperty(alias, value);
81
+ root.style.setProperty(hostName, value);
82
+ }
68
83
  root.style.setProperty('font-family', theme.tokens.font);
69
84
  root.style.setProperty('font-size', '0.875rem');
70
85
  root.style.setProperty('line-height', '1.45');
@@ -104,15 +104,31 @@ export declare const guestSessionWorktreeSchema: z.ZodUnion<readonly [z.ZodBoole
104
104
  export declare const guestStorageRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
105
105
  op: z.ZodLiteral<"get">;
106
106
  key: z.ZodString;
107
+ scope: z.ZodOptional<z.ZodEnum<{
108
+ instance: "instance";
109
+ device: "device";
110
+ }>>;
107
111
  }, z.core.$strict>, z.ZodObject<{
108
112
  op: z.ZodLiteral<"delete">;
109
113
  key: z.ZodString;
114
+ scope: z.ZodOptional<z.ZodEnum<{
115
+ instance: "instance";
116
+ device: "device";
117
+ }>>;
110
118
  }, z.core.$strict>, z.ZodObject<{
111
119
  op: z.ZodLiteral<"set">;
112
120
  key: z.ZodString;
113
121
  value: z.ZodJSONSchema;
122
+ scope: z.ZodOptional<z.ZodEnum<{
123
+ instance: "instance";
124
+ device: "device";
125
+ }>>;
114
126
  }, z.core.$strict>, z.ZodObject<{
115
127
  op: z.ZodLiteral<"keys">;
128
+ scope: z.ZodOptional<z.ZodEnum<{
129
+ instance: "instance";
130
+ device: "device";
131
+ }>>;
116
132
  }, z.core.$strict>], "op">;
117
133
  export declare const guestStorageResultSchema: z.ZodUnion<readonly [z.ZodObject<{
118
134
  storage: z.ZodLiteral<true>;
@@ -31,10 +31,10 @@ export const guestSessionWorktreeSchema = z.union([
31
31
  ]);
32
32
  const storageKey = z.string().min(1).max(GUEST_STORAGE_KEY_MAX);
33
33
  export const guestStorageRequestSchema = z.discriminatedUnion('op', [
34
- z.object({ op: z.literal('get'), key: storageKey }).strict(),
35
- z.object({ op: z.literal('delete'), key: storageKey }).strict(),
36
- z.object({ op: z.literal('set'), key: storageKey, value: z.json().refine((value) => new TextEncoder().encode(JSON.stringify(value)).length <= GUEST_STORAGE_VALUE_BYTES) }).strict(),
37
- z.object({ op: z.literal('keys') }).strict(),
34
+ z.object({ op: z.literal('get'), key: storageKey, scope: z.enum(['instance', 'device']).optional() }).strict(),
35
+ z.object({ op: z.literal('delete'), key: storageKey, scope: z.enum(['instance', 'device']).optional() }).strict(),
36
+ z.object({ op: z.literal('set'), key: storageKey, value: z.json().refine((value) => new TextEncoder().encode(JSON.stringify(value)).length <= GUEST_STORAGE_VALUE_BYTES), scope: z.enum(['instance', 'device']).optional() }).strict(),
37
+ z.object({ op: z.literal('keys'), scope: z.enum(['instance', 'device']).optional() }).strict(),
38
38
  ]);
39
39
  export const guestStorageResultSchema = z.union([
40
40
  z.object({ storage: z.literal(true), op: z.literal('get'), found: z.literal(false) }),
@@ -74,17 +74,23 @@ export type GuestWorkspaceUpdate = {
74
74
  export declare const GUEST_STORAGE_KEY_MAX = 128;
75
75
  export declare const GUEST_STORAGE_VALUE_BYTES = 65536;
76
76
  export declare const GUEST_STORAGE_TOTAL_BYTES = 2097152;
77
+ /** Maximum bytes retained across all device-scoped guest storage namespaces. */
78
+ export declare const GUEST_DEVICE_STORAGE_TOTAL_BYTES = 16777216;
77
79
  export declare const GUEST_STORAGE_KEYS_MAX = 2000;
78
- export type GuestStorageRequest = {
80
+ export type GuestStorageScope = 'instance' | 'device';
81
+ export type GuestStorageOptions = {
82
+ scope?: GuestStorageScope;
83
+ };
84
+ export type GuestStorageRequest = ({
79
85
  op: 'get' | 'delete';
80
86
  key: string;
81
- } | {
87
+ } & GuestStorageOptions) | ({
82
88
  op: 'set';
83
89
  key: string;
84
90
  value: JsonValue;
85
- } | {
91
+ } & GuestStorageOptions) | ({
86
92
  op: 'keys';
87
- };
93
+ } & GuestStorageOptions);
88
94
  export type GuestStorageResult = {
89
95
  storage: true;
90
96
  } & ({
package/dist/workspace.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export const GUEST_STORAGE_KEY_MAX = 128;
2
2
  export const GUEST_STORAGE_VALUE_BYTES = 65_536;
3
3
  export const GUEST_STORAGE_TOTAL_BYTES = 2_097_152;
4
+ /** Maximum bytes retained across all device-scoped guest storage namespaces. */
5
+ export const GUEST_DEVICE_STORAGE_TOTAL_BYTES = 16_777_216;
4
6
  export const GUEST_STORAGE_KEYS_MAX = 2_000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/sdk",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Contract for OpenChamber guests: manifest parse and iframe host bridge.",
5
5
  "private": false,
6
6
  "license": "MIT",