@capybara-hq/plugin-sdk 0.1.0-rc.7 → 0.1.0-rc.9

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,168 @@
1
+ /** Every slot id of the catalog, in catalog order (UI slots spec §3). */
2
+ export const UI_SLOT_IDS = [
3
+ "conversation.panel",
4
+ "conversation.properties.section",
5
+ "conversation.tool-result.card",
6
+ "org.page",
7
+ "file.preview",
8
+ "agent.detail.section",
9
+ "org.settings.section",
10
+ "platform.page",
11
+ ];
12
+ const SLOT_DATA = [
13
+ {
14
+ id: "conversation.panel",
15
+ version: 1,
16
+ owner: "components/conversation/RightArea.tsx",
17
+ scope: "conversation",
18
+ accepts: ["view", "embed"],
19
+ cardinality: "list",
20
+ minRole: "guest",
21
+ args: ["conversationId", "item"],
22
+ density: "regular",
23
+ icon: "optional",
24
+ description: {
25
+ en: "A panel beside the conversation, opened from the right area.",
26
+ zh: "对话旁的面板,从右侧区域打开。",
27
+ },
28
+ },
29
+ {
30
+ id: "conversation.properties.section",
31
+ version: 1,
32
+ owner: "components/conversation/PropertiesSections.tsx",
33
+ scope: "conversation",
34
+ accepts: ["view", "embed"],
35
+ cardinality: "list",
36
+ minRole: "guest",
37
+ args: ["conversationId"],
38
+ density: "dense",
39
+ icon: "optional",
40
+ anchors: ["context", "files", "background"],
41
+ description: {
42
+ en: "A section in the conversation properties list.",
43
+ zh: "对话属性列表中的一个分区。",
44
+ },
45
+ },
46
+ {
47
+ id: "conversation.tool-result.card",
48
+ version: 1,
49
+ owner: "components/conversation/RightArea.tsx",
50
+ scope: "conversation",
51
+ accepts: ["view", "embed"],
52
+ cardinality: "list",
53
+ minRole: "guest",
54
+ args: ["conversationId", "card"],
55
+ density: "regular",
56
+ icon: "optional",
57
+ description: {
58
+ en: "A card a tool result opens in the conversation.",
59
+ zh: "工具结果在对话中打开的卡片。",
60
+ },
61
+ },
62
+ {
63
+ id: "org.page",
64
+ version: 1,
65
+ owner: "app/(app)/p/[plugin]/[id]/view.tsx",
66
+ scope: "org",
67
+ accepts: ["view", "embed"],
68
+ cardinality: "list",
69
+ minRole: "member",
70
+ args: [],
71
+ density: "regular",
72
+ icon: "optional",
73
+ description: {
74
+ en: "A full page in the organization navigation.",
75
+ zh: "组织导航中的完整页面。",
76
+ },
77
+ },
78
+ {
79
+ id: "file.preview",
80
+ version: 1,
81
+ owner: "components/conversation/FilePanel.tsx",
82
+ scope: "file",
83
+ accepts: ["embed"],
84
+ cardinality: "list",
85
+ minRole: "guest",
86
+ args: ["conversationId", "fileId"],
87
+ density: "regular",
88
+ icon: "optional",
89
+ description: {
90
+ en: "A previewer for a file of the conversation.",
91
+ zh: "对话中文件的预览器。",
92
+ },
93
+ },
94
+ {
95
+ id: "agent.detail.section",
96
+ version: 1,
97
+ owner: "components/agent/AgentPage.tsx",
98
+ scope: "agent",
99
+ accepts: ["view", "embed"],
100
+ cardinality: "list",
101
+ minRole: "guest",
102
+ args: ["agentId"],
103
+ density: "regular",
104
+ icon: "optional",
105
+ description: {
106
+ en: "A section on the detail page of an agent.",
107
+ zh: "智能体详情页中的一个分区。",
108
+ },
109
+ },
110
+ {
111
+ id: "org.settings.section",
112
+ version: 1,
113
+ owner: "app/(app)/settings/organization/view.tsx",
114
+ scope: "org",
115
+ accepts: ["view", "embed"],
116
+ cardinality: "list",
117
+ minRole: "admin",
118
+ args: [],
119
+ density: "regular",
120
+ icon: "optional",
121
+ description: {
122
+ en: "A section on the organization settings page.",
123
+ zh: "组织设置页中的一个分区。",
124
+ },
125
+ },
126
+ {
127
+ id: "platform.page",
128
+ version: 1,
129
+ owner: "app/(app)/admin/p/[plugin]/[id]/view.tsx",
130
+ scope: "platform",
131
+ accepts: ["view", "embed"],
132
+ cardinality: "list",
133
+ minRole: "superAdmin",
134
+ args: [],
135
+ density: "regular",
136
+ icon: "optional",
137
+ description: {
138
+ en: "A full page in the platform administration.",
139
+ zh: "平台管理中的完整页面。",
140
+ },
141
+ },
142
+ ];
143
+ export const UI_SLOTS = SLOT_DATA;
144
+ export function uiSlot(id) {
145
+ return UI_SLOTS.find((s) => s.id === id);
146
+ }
147
+ const SLOT_REF_RE = /^([a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+)(?:@([1-9][0-9]{0,2}))?$/;
148
+ /** Reads `<id>` or `<id>@<version>` (version 1 when omitted); does not check the catalog. */
149
+ export function parseSlotRef(ref) {
150
+ const m = SLOT_REF_RE.exec(ref);
151
+ if (!m)
152
+ return null;
153
+ return { id: m[1], version: m[2] === undefined ? 1 : Number(m[2]) };
154
+ }
155
+ /** @deprecated step A of the UI slots spec §4 */
156
+ export const PLACE_SLOTS = {
157
+ panel: "conversation.panel",
158
+ conversation: "conversation.panel",
159
+ section: "conversation.properties.section",
160
+ inline: "conversation.tool-result.card",
161
+ page: "org.page",
162
+ "file-preview": "file.preview",
163
+ };
164
+ export const UI_ARRANGEABLE_SLOTS = UI_SLOT_IDS.filter((id) => id !== "file.preview");
165
+ export const UI_CONTRIBUTION_KEY_RE = /^[a-z][a-z0-9-]{1,40}\/[a-z][a-z0-9-]{0,39}$/;
166
+ export function uiContributionKey(plugin, id) {
167
+ return `${plugin}/${id}`;
168
+ }
package/dist/ui.d.ts CHANGED
@@ -1,18 +1,13 @@
1
1
  import type { Locale } from "./cards.js";
2
2
  import { type FormAnswer, type InputForm } from "./tool.js";
3
+ import type { UiSlotId, UiSlotScope } from "./ui-slots.js";
3
4
  /**
4
- * Where a ui contribution goes (UI refresh spec §5.3):
5
- * - `panel`: a right panel of the conversation page, opened from the extension's icon in the title
6
- * bar (one at a time);
7
- * - `section`: a section of the conversation's properties panel;
8
- * - `inline`: an inline card in the conversation, hanging off a tool result (`ToolResult.card`);
9
- * - `page`: an org page under "Extensions";
10
- * - `file-preview`: an embed previewer of `file-preview@1` (embed only, see `FilePreviewer.embed`);
11
- * - `conversation`: the old right-side tab; it means `panel` (see {@link normalizePlace}).
5
+ * The places of the first UI design, kept as a deprecated input (UI slots spec §4, step A): each maps
6
+ * to a slot through `PLACE_SLOTS` (`panel` and `conversation` → `conversation.panel`, `section` →
7
+ * `conversation.properties.section`, `inline` → `conversation.tool-result.card`, `page` →
8
+ * `org.page`, `file-preview` → `file.preview`).
12
9
  */
13
10
  export type UiPlace = "conversation" | "panel" | "section" | "inline" | "page" | "file-preview";
14
- /** A place with the old `conversation` mapped to `panel`. */
15
- export declare function normalizePlace(place: UiPlace): Exclude<UiPlace, "conversation">;
16
11
  /**
17
12
  * Icons a contribution may use (design system icon names; `puzzle` is the default and is drawn as
18
13
  * the design `extension` icon). The title-bar button of a panel shows it.
@@ -20,8 +15,9 @@ export declare function normalizePlace(place: UiPlace): Exclude<UiPlace, "conver
20
15
  export declare const UI_ICONS: readonly ["list-checks", "layout-grid", "file", "chart", "calendar", "users", "bot", "book", "message-square", "puzzle"];
21
16
  export type UiIcon = (typeof UI_ICONS)[number];
22
17
  /**
23
- * An "open" target: a `panel` contribution of the same extension, scrolled to the block or item
24
- * whose `id` equals `item` (the panel's render receives it as {@link UiRenderArgs.item}).
18
+ * An "open" target: a `conversation.panel` contribution of the same extension, scrolled to the
19
+ * block or item whose `id` equals `item` (the panel's render receives it as
20
+ * {@link UiRenderArgs.item}).
25
21
  */
26
22
  export interface UiOpen {
27
23
  contribution: string;
@@ -39,7 +35,19 @@ export interface UiContribution {
39
35
  zh: string;
40
36
  en: string;
41
37
  };
42
- place: UiPlace;
38
+ /**
39
+ * The slot it goes in: `"<id>"` (version 1) or `"<id>@<n>"`, an id of `UI_SLOTS`. Exactly one of
40
+ * `slot` and `place`. An unknown slot or version does not fail the extension: the contribution is
41
+ * dropped (see `resolveUiSlots`).
42
+ */
43
+ slot?: string;
44
+ /** @deprecated use slot (UI slots spec §4, step A) */
45
+ place?: UiPlace;
46
+ /**
47
+ * One of the slot's `anchors` to be placed after; omitted (or not an anchor of the slot) = the
48
+ * slot's first anchor.
49
+ */
50
+ after?: string;
43
51
  kind: UiKind;
44
52
  /** 仅 embed:插件路由下 `/ui/` 开头的路径,须在 `permissions.http` 中声明为 GET。 */
45
53
  path?: string;
@@ -150,20 +158,41 @@ export declare const UI_LIMITS: {
150
158
  */
151
159
  export declare function validateUiView(v: unknown): string[];
152
160
  /**
153
- * 调用者在组织内的角色(由高到低 owner / admin / member / guest)。`place: "page"` 的贡献只给 member 以上
154
- * (内核对 guest 返回 404,插件收不到 guest 的组织页面请求);对话位置的贡献给能访问该对话的任何角色。
161
+ * The caller's role in the org (owner / admin / member / guest, highest first). A slot's `minRole`
162
+ * decides who may use its contributions, then its scope's rule (the kernel answers 404 to anyone
163
+ * else, so the extension never sees such a request): `org.page` member and up,
164
+ * `org.settings.section` admin and up; conversation slots any role that can read the
165
+ * conversation; `agent.detail.section` any role that can read the agent (read-only viewers
166
+ * included: tighten on `role` if you must).
155
167
  */
156
168
  export type UiRole = "owner" | "admin" | "member" | "guest";
157
- /** 内核调用插件 `render` 时传入的参数(身份由内核鉴权后给出,插件拿不到用户的会话凭据)。 */
158
- export interface UiRenderArgs {
159
- orgId: string;
169
+ /** The render args every scope shares (identity comes from the kernel's auth, never the caller's credentials). */
170
+ export interface UiRenderArgsBase {
160
171
  accountId: string;
172
+ /** The catalog slot the contribution resolved to. */
173
+ slot: UiSlotId;
174
+ locale: Locale;
175
+ signal: AbortSignal;
176
+ /**
177
+ * The extension's effective deployment config: for an org, the platform's deployment config
178
+ * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
179
+ * platform's config (equal to `ctx.config`) while the org follows it; for the platform scope,
180
+ * the platform's config. Use it, not `ctx.config`, for work done for this org. Optional in the
181
+ * type so older extensions compile; the kernel always passes it.
182
+ */
183
+ config?: unknown;
184
+ }
185
+ /** 内核调用插件 `render` 时传入的参数,组织内的位置(会话、智能体、组织、文件)。 */
186
+ export interface UiOrgRenderArgs extends UiRenderArgsBase {
187
+ /** The scope of {@link slot}: which of the context ids below are set. */
188
+ scope: Exclude<UiSlotScope, "platform">;
189
+ orgId: string;
161
190
  /** 调用者此刻在 `orgId` 的角色(内核按成员关系填,插件可据此收紧按钮等,见 {@link UiRole})。 */
162
191
  role: UiRole;
163
192
  /** 仅对话视图。 */
164
193
  conversationId?: string;
165
- locale: Locale;
166
- signal: AbortSignal;
194
+ /** Only an agent view (`agent.detail.section`): an agent of `orgId` the caller can read. */
195
+ agentId?: string;
167
196
  /** A panel opened from an open target: the block or item `id` to scroll to. */
168
197
  item?: string;
169
198
  /**
@@ -175,21 +204,36 @@ export interface UiRenderArgs {
175
204
  callId: string;
176
205
  key?: string;
177
206
  };
178
- /**
179
- * The extension's effective deployment config for this org: the platform's deployment config
180
- * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
181
- * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`,
182
- * for work done for this org. Optional in the type so older extensions compile; the kernel
183
- * always passes it.
184
- */
185
- config?: unknown;
186
207
  }
187
- /** 视图里的按钮或表单提交:`actionId` 来自块;表单提交时带 `answer`。 */
188
- export interface UiActionArgs extends UiRenderArgs {
208
+ /**
209
+ * The render args of a `platform.page` contribution: rendered for a super admin in the admin
210
+ * console, with no org (and so no org role, conversation or agent).
211
+ */
212
+ export interface UiPlatformRenderArgs extends UiRenderArgsBase {
213
+ scope: "platform";
214
+ orgId?: undefined;
215
+ role: "superAdmin";
216
+ conversationId?: undefined;
217
+ agentId?: undefined;
218
+ item?: undefined;
219
+ card?: undefined;
220
+ }
221
+ /** 内核调用插件 `render` 时传入的参数:narrow on `scope` (`"platform"` has no `orgId`). */
222
+ export type UiRenderArgs = UiOrgRenderArgs | UiPlatformRenderArgs;
223
+ /** What a button or form submit adds to the render args. */
224
+ export interface UiActionInput {
189
225
  actionId: string;
190
226
  payload?: unknown;
191
227
  answer?: FormAnswer;
192
228
  }
229
+ /** An action of an org-scope view. */
230
+ export interface UiOrgActionArgs extends UiOrgRenderArgs, UiActionInput {
231
+ }
232
+ /** An action of a `platform.page` view. */
233
+ export interface UiPlatformActionArgs extends UiPlatformRenderArgs, UiActionInput {
234
+ }
235
+ /** 视图里的按钮或表单提交:`actionId` 来自块;表单提交时带 `answer`。 */
236
+ export type UiActionArgs = UiOrgActionArgs | UiPlatformActionArgs;
193
237
  /** 动作结果:一条提示,或一份新的视图。 */
194
238
  export type UiActionResult = {
195
239
  toast?: {
@@ -199,21 +243,55 @@ export type UiActionResult = {
199
243
  } | {
200
244
  view: UiView;
201
245
  };
202
- /** 嵌入令牌里的声明(经 `ctx.ui.verifyEmbedToken` 校验后可信)。`exp` 为 Unix 秒。 */
203
- export interface EmbedClaims {
246
+ /** The embed claims both scopes share. `exp` 为 Unix 秒。 */
247
+ export interface EmbedClaimsBase {
204
248
  plugin: string;
205
- orgId: string;
206
249
  accountId: string;
250
+ contribution: string;
251
+ exp: number;
252
+ }
253
+ /** An org-scope embed token's claims (every slot but `platform.page`). */
254
+ export interface OrgEmbedClaims extends EmbedClaimsBase {
255
+ scope: "org";
256
+ orgId: string;
207
257
  /** 调用者的角色:签发时写入;`ctx.ui.verifyEmbedToken` 返回校验时刻的当前角色。 */
208
258
  role: UiRole;
209
259
  conversationId?: string;
260
+ /** Only an agent embed's token (`agent.detail.section`): the agent it was issued for. */
261
+ agentId?: string;
210
262
  /** Only a `file-preview` embed's token: the one file (of `conversationId`) it previews. */
211
263
  fileId?: string;
212
- contribution: string;
213
- exp: number;
214
264
  }
265
+ /** A `platform.page` embed token's claims: a super admin's, with no org, conversation, agent or file. */
266
+ export interface PlatformEmbedClaims extends EmbedClaimsBase {
267
+ scope: "platform";
268
+ orgId?: undefined;
269
+ role: "superAdmin";
270
+ conversationId?: undefined;
271
+ agentId?: undefined;
272
+ fileId?: undefined;
273
+ }
274
+ /**
275
+ * 嵌入令牌里的声明(经 `ctx.ui.verifyEmbedToken` 校验后可信): narrow on `scope` (`"platform"` has no
276
+ * `orgId` and `role` `"superAdmin"`).
277
+ */
278
+ export type EmbedClaims = OrgEmbedClaims | PlatformEmbedClaims;
215
279
  /** 外层页面与嵌入 iframe 之间 `postMessage` 协议的版本标识。 */
216
280
  export declare const EMBED_PROTOCOL = "capybara-embed/1";
281
+ /**
282
+ * What an embed's `init` says about where it is shown, narrowed on `scope` like {@link EmbedClaims}:
283
+ * an org's page, panel, section or previewer (`orgId`, and the conversation when there is one), or
284
+ * a platform page of the admin console (no org).
285
+ */
286
+ export type EmbedInitContext = {
287
+ scope: "org";
288
+ orgId: string;
289
+ conversationId?: string;
290
+ } | {
291
+ scope: "platform";
292
+ orgId?: undefined;
293
+ conversationId?: undefined;
294
+ };
217
295
  /** 外层页面发给 iframe 的消息。 */
218
296
  export type EmbedParentMessage = {
219
297
  protocol: typeof EMBED_PROTOCOL;
@@ -222,10 +300,7 @@ export type EmbedParentMessage = {
222
300
  locale: Locale;
223
301
  theme: "dark" | "light";
224
302
  tokens: Record<string, string>;
225
- context: {
226
- orgId: string;
227
- conversationId?: string;
228
- };
303
+ context: EmbedInitContext;
229
304
  } | {
230
305
  protocol: typeof EMBED_PROTOCOL;
231
306
  type: "token";
package/dist/ui.js CHANGED
@@ -1,9 +1,5 @@
1
1
  import { validateInputForm } from "./tool.js";
2
2
  import { isHttpUrl } from "./url.js";
3
- /** A place with the old `conversation` mapped to `panel`. */
4
- export function normalizePlace(place) {
5
- return place === "conversation" ? "panel" : place;
6
- }
7
3
  /**
8
4
  * Icons a contribution may use (design system icon names; `puzzle` is the default and is drawn as
9
5
  * the design `extension` icon). The title-bar button of a panel shows it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/plugin-sdk",
3
- "version": "0.1.0-rc.7",
3
+ "version": "0.1.0-rc.9",
4
4
  "description": "Capybara plugin SDK: the types and pure functions plugins are written against",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -29,6 +29,7 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "kysely": "^0.29.6",
32
+ "saxes": "6.0.0",
32
33
  "semver": "^7.8.5",
33
34
  "yaml": "^2.9.1",
34
35
  "zod": "^4.6.7"
@@ -194,6 +194,25 @@
194
194
  "additionalProperties": false
195
195
  }
196
196
  },
197
+ "themes": {
198
+ "maxItems": 10,
199
+ "type": "array",
200
+ "items": {
201
+ "type": "object",
202
+ "properties": {
203
+ "path": {
204
+ "type": "string",
205
+ "minLength": 3,
206
+ "maxLength": 512,
207
+ "pattern": "^\\.\\/"
208
+ }
209
+ },
210
+ "required": [
211
+ "path"
212
+ ],
213
+ "additionalProperties": false
214
+ }
215
+ },
197
216
  "setupGuides": {
198
217
  "maxItems": 8,
199
218
  "type": "array",
@@ -0,0 +1,225 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://capybarahq.com/schemas/theme.v1.schema.json",
4
+ "title": "Capybara theme file themes/<id>.json",
5
+ "type": "object",
6
+ "properties": {
7
+ "$schema": {
8
+ "type": "string",
9
+ "maxLength": 512
10
+ },
11
+ "name": {
12
+ "type": "object",
13
+ "properties": {
14
+ "zh": {
15
+ "type": "string",
16
+ "minLength": 1,
17
+ "maxLength": 100,
18
+ "pattern": "\\S"
19
+ },
20
+ "en": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "maxLength": 100,
24
+ "pattern": "\\S"
25
+ }
26
+ },
27
+ "required": [
28
+ "zh",
29
+ "en"
30
+ ],
31
+ "additionalProperties": false
32
+ },
33
+ "description": {
34
+ "type": "object",
35
+ "properties": {
36
+ "en": {
37
+ "type": "string",
38
+ "minLength": 1,
39
+ "maxLength": 1024,
40
+ "pattern": "\\S"
41
+ },
42
+ "zh": {
43
+ "type": "string",
44
+ "minLength": 1,
45
+ "maxLength": 1024,
46
+ "pattern": "\\S"
47
+ }
48
+ },
49
+ "required": [
50
+ "en"
51
+ ],
52
+ "additionalProperties": false
53
+ },
54
+ "schemes": {
55
+ "type": "object",
56
+ "properties": {
57
+ "dark": {
58
+ "type": "object",
59
+ "properties": {
60
+ "color": {
61
+ "type": "object",
62
+ "propertyNames": {
63
+ "type": "string",
64
+ "maxLength": 64
65
+ },
66
+ "additionalProperties": {
67
+ "type": "string",
68
+ "maxLength": 256
69
+ }
70
+ },
71
+ "shadow": {
72
+ "type": "object",
73
+ "propertyNames": {
74
+ "type": "string",
75
+ "maxLength": 64
76
+ },
77
+ "additionalProperties": {
78
+ "type": "string",
79
+ "maxLength": 256
80
+ }
81
+ },
82
+ "radius": {
83
+ "type": "object",
84
+ "properties": {
85
+ "sm": {
86
+ "type": "integer",
87
+ "minimum": 0,
88
+ "maximum": 16
89
+ },
90
+ "md": {
91
+ "type": "integer",
92
+ "minimum": 0,
93
+ "maximum": 16
94
+ },
95
+ "bubble": {
96
+ "type": "integer",
97
+ "minimum": 0,
98
+ "maximum": 16
99
+ }
100
+ },
101
+ "additionalProperties": false
102
+ }
103
+ },
104
+ "additionalProperties": false
105
+ },
106
+ "light": {
107
+ "type": "object",
108
+ "properties": {
109
+ "color": {
110
+ "type": "object",
111
+ "propertyNames": {
112
+ "type": "string",
113
+ "maxLength": 64
114
+ },
115
+ "additionalProperties": {
116
+ "type": "string",
117
+ "maxLength": 256
118
+ }
119
+ },
120
+ "shadow": {
121
+ "type": "object",
122
+ "propertyNames": {
123
+ "type": "string",
124
+ "maxLength": 64
125
+ },
126
+ "additionalProperties": {
127
+ "type": "string",
128
+ "maxLength": 256
129
+ }
130
+ },
131
+ "radius": {
132
+ "type": "object",
133
+ "properties": {
134
+ "sm": {
135
+ "type": "integer",
136
+ "minimum": 0,
137
+ "maximum": 16
138
+ },
139
+ "md": {
140
+ "type": "integer",
141
+ "minimum": 0,
142
+ "maximum": 16
143
+ },
144
+ "bubble": {
145
+ "type": "integer",
146
+ "minimum": 0,
147
+ "maximum": 16
148
+ }
149
+ },
150
+ "additionalProperties": false
151
+ }
152
+ },
153
+ "additionalProperties": false
154
+ }
155
+ },
156
+ "additionalProperties": false
157
+ },
158
+ "fonts": {
159
+ "maxItems": 8,
160
+ "type": "array",
161
+ "items": {
162
+ "type": "object",
163
+ "properties": {
164
+ "family": {
165
+ "type": "string",
166
+ "pattern": "^[A-Za-z0-9 -]{1,64}$"
167
+ },
168
+ "file": {
169
+ "type": "string",
170
+ "maxLength": 512
171
+ },
172
+ "weight": {
173
+ "type": "integer",
174
+ "minimum": 100,
175
+ "maximum": 900
176
+ },
177
+ "style": {
178
+ "type": "string",
179
+ "enum": [
180
+ "normal",
181
+ "italic"
182
+ ]
183
+ },
184
+ "role": {
185
+ "type": "string",
186
+ "enum": [
187
+ "prose",
188
+ "mono"
189
+ ]
190
+ }
191
+ },
192
+ "required": [
193
+ "family",
194
+ "file",
195
+ "weight",
196
+ "role"
197
+ ],
198
+ "additionalProperties": false
199
+ }
200
+ },
201
+ "logo": {
202
+ "type": "object",
203
+ "properties": {
204
+ "mark": {
205
+ "type": "string",
206
+ "maxLength": 512
207
+ },
208
+ "wordmark": {
209
+ "type": "string",
210
+ "pattern": "\\S"
211
+ },
212
+ "favicon": {
213
+ "type": "string",
214
+ "maxLength": 512
215
+ }
216
+ },
217
+ "additionalProperties": false
218
+ }
219
+ },
220
+ "required": [
221
+ "name",
222
+ "schemes"
223
+ ],
224
+ "additionalProperties": false
225
+ }