@camox/api-contract 0.43.0 → 0.44.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.
package/dist/index.js CHANGED
@@ -0,0 +1,102 @@
1
+ // src/icons.ts
2
+ import { getIconData, iconToSVG } from "@iconify/utils";
3
+ var ICON_CATALOG_VERSION = "2.2.532";
4
+ var collections = /* @__PURE__ */ new Map();
5
+ async function loadIconCollection(prefix) {
6
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(prefix)) throw new Error("Invalid icon collection");
7
+ const existing = collections.get(prefix);
8
+ if (existing) return existing;
9
+ const pending = (async () => {
10
+ const response = await fetch(
11
+ `https://unpkg.com/@iconify/json@${ICON_CATALOG_VERSION}/json/${prefix}.json`,
12
+ { signal: AbortSignal.timeout(3e4) }
13
+ );
14
+ if (!response.ok) throw new Error(`Unable to load icon collection ${prefix}`);
15
+ const data = await response.json();
16
+ if (data.prefix !== prefix || !data.icons || !data.info?.license)
17
+ throw new Error("Invalid icon catalog or missing license");
18
+ return data;
19
+ })();
20
+ collections.set(prefix, pending);
21
+ if (collections.size > 8) collections.delete(collections.keys().next().value);
22
+ try {
23
+ return await pending;
24
+ } catch (error) {
25
+ collections.delete(prefix);
26
+ throw error;
27
+ }
28
+ }
29
+ function iconCollectionIds(data) {
30
+ return [...Object.keys(data.icons), ...Object.keys(data.aliases ?? {})].filter((name) => getIconData(data, name) !== null).sort().map((name) => `${data.prefix}:${name}`);
31
+ }
32
+ function resolveIconSvg(data, id) {
33
+ const [prefix, name, extra] = id.split(":");
34
+ if (prefix !== data.prefix || !name || extra !== void 0) throw new Error("Invalid icon ID");
35
+ const icon = getIconData(data, name);
36
+ if (!icon) throw new Error(`Unknown icon: ${id}`);
37
+ const svg = iconToSVG(icon);
38
+ return {
39
+ body: svg.body,
40
+ attributes: svg.attributes,
41
+ attribution: data.info ? {
42
+ collection: data.info.name,
43
+ author: data.info.author.name,
44
+ license: data.info.license.title,
45
+ url: data.info.license.url
46
+ } : void 0
47
+ };
48
+ }
49
+ function scopeIconSvgIds(body, prefix) {
50
+ const ids = [...body.matchAll(/\sid="([^\s"]+)"/g)].map((match) => match[1]);
51
+ if (!ids.length) return body;
52
+ const pattern = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
53
+ return body.replace(
54
+ new RegExp(`([#;"])(${pattern})([")]|\\.[a-z])`, "g"),
55
+ (_match, before, id, after) => `${before}${prefix}${id}${after}`
56
+ );
57
+ }
58
+ function validateIconValue(value, schema) {
59
+ if (schema.fieldType !== "Icon") return;
60
+ if (typeof value !== "string" || !Array.isArray(schema.enum) || !schema.enum.includes(value)) {
61
+ throw new Error(`Invalid icon ID: ${String(value)}`);
62
+ }
63
+ }
64
+
65
+ // src/comments.ts
66
+ import { z } from "zod";
67
+ var COMMENT_MESSAGE_MAX_LENGTH = 1e4;
68
+ var objectId = z.number().int().positive();
69
+ var fieldName = z.string().min(1).max(200);
70
+ var commentTargetSchema = z.discriminatedUnion("kind", [
71
+ z.strictObject({ kind: z.literal("page") }),
72
+ z.strictObject({ kind: z.literal("block"), blockId: objectId }),
73
+ z.strictObject({ kind: z.literal("item"), blockId: objectId, itemId: objectId }),
74
+ z.strictObject({ kind: z.literal("block-field"), blockId: objectId, fieldName }),
75
+ z.strictObject({
76
+ kind: z.literal("item-field"),
77
+ blockId: objectId,
78
+ itemId: objectId,
79
+ fieldName
80
+ })
81
+ ]);
82
+ var commentSchema = z.object({
83
+ id: z.uuid(),
84
+ pageId: objectId,
85
+ environmentId: objectId,
86
+ message: z.string(),
87
+ resolved: z.boolean(),
88
+ target: commentTargetSchema.nullable(),
89
+ author: z.object({ name: z.string(), image: z.string().nullable() }),
90
+ createdAt: z.number()
91
+ });
92
+ export {
93
+ COMMENT_MESSAGE_MAX_LENGTH,
94
+ ICON_CATALOG_VERSION,
95
+ commentSchema,
96
+ commentTargetSchema,
97
+ iconCollectionIds,
98
+ loadIconCollection,
99
+ resolveIconSvg,
100
+ scopeIconSvgIds,
101
+ validateIconValue
102
+ };
@@ -18,6 +18,10 @@ export type QueryKey = [first: "camox", ...rest: Array<string | number>];
18
18
  */
19
19
  export type ReadSource = "draft" | "live";
20
20
  export declare const queryKeys: {
21
+ comments: {
22
+ all: ["camox", string];
23
+ list: (pageId: number) => ["camox", string, string, number];
24
+ };
21
25
  pages: {
22
26
  list: ["camox", string, string];
23
27
  getByPath: (path: string, source?: ReadSource) => ["camox", string, string, string] | ["camox", string, string, string, ReadSource];
@@ -1,4 +1,8 @@
1
1
  export const queryKeys = {
2
+ comments: {
3
+ all: ["camox", "comments"],
4
+ list: (pageId) => ["camox", "comments", "list", pageId],
5
+ },
2
6
  pages: {
3
7
  list: ["camox", "pages", "list"],
4
8
  getByPath: (path, source) => source
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
@@ -17,6 +17,11 @@
17
17
  "default": "./dist/query-keys.js"
18
18
  }
19
19
  },
20
+ "dependencies": {
21
+ "@iconify/types": "^2.0.0",
22
+ "@iconify/utils": "^3.1.7",
23
+ "zod": "^4.4.3"
24
+ },
20
25
  "devDependencies": {
21
26
  "dts-bundle-generator": "^9.5.1",
22
27
  "esbuild": "^0.25.12"
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+
3
+ export const COMMENT_MESSAGE_MAX_LENGTH = 10_000;
4
+
5
+ const objectId = z.number().int().positive();
6
+ const fieldName = z.string().min(1).max(200);
7
+
8
+ export const commentTargetSchema = z.discriminatedUnion("kind", [
9
+ z.strictObject({ kind: z.literal("page") }),
10
+ z.strictObject({ kind: z.literal("block"), blockId: objectId }),
11
+ z.strictObject({ kind: z.literal("item"), blockId: objectId, itemId: objectId }),
12
+ z.strictObject({ kind: z.literal("block-field"), blockId: objectId, fieldName }),
13
+ z.strictObject({
14
+ kind: z.literal("item-field"),
15
+ blockId: objectId,
16
+ itemId: objectId,
17
+ fieldName,
18
+ }),
19
+ ]);
20
+
21
+ export const commentSchema = z.object({
22
+ id: z.uuid(),
23
+ pageId: objectId,
24
+ environmentId: objectId,
25
+ message: z.string(),
26
+ resolved: z.boolean(),
27
+ target: commentTargetSchema.nullable(),
28
+ author: z.object({ name: z.string(), image: z.string().nullable() }),
29
+ createdAt: z.number(),
30
+ });
31
+
32
+ export type CommentTarget = z.infer<typeof commentTargetSchema>;
33
+ export type Comment = z.infer<typeof commentSchema>;
package/src/icons.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { IconifyJSON } from "@iconify/types";
2
+ import { getIconData, iconToSVG } from "@iconify/utils";
3
+
4
+ // One immutable snapshot for declarations, schemas, picker and SVG resolution.
5
+ export const ICON_CATALOG_VERSION = "2.2.532";
6
+ export type IconSvgData = {
7
+ body: string;
8
+ attributes: Record<string, string>;
9
+ attribution?: { collection: string; author: string; license: string; url?: string };
10
+ };
11
+ const collections = new Map<string, Promise<IconifyJSON>>();
12
+
13
+ export async function loadIconCollection(prefix: string): Promise<IconifyJSON> {
14
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(prefix)) throw new Error("Invalid icon collection");
15
+ const existing = collections.get(prefix);
16
+ if (existing) return existing;
17
+ const pending = (async () => {
18
+ const response = await fetch(
19
+ `https://unpkg.com/@iconify/json@${ICON_CATALOG_VERSION}/json/${prefix}.json`,
20
+ { signal: AbortSignal.timeout(30_000) },
21
+ );
22
+ if (!response.ok) throw new Error(`Unable to load icon collection ${prefix}`);
23
+ const data = (await response.json()) as IconifyJSON;
24
+ if (data.prefix !== prefix || !data.icons || !data.info?.license)
25
+ throw new Error("Invalid icon catalog or missing license");
26
+ return data;
27
+ })();
28
+ collections.set(prefix, pending);
29
+ // Bound worker memory; failed fetches must be retryable.
30
+ if (collections.size > 8) collections.delete(collections.keys().next().value!);
31
+ try {
32
+ return await pending;
33
+ } catch (error) {
34
+ collections.delete(prefix);
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ export function iconCollectionIds(data: IconifyJSON): string[] {
40
+ return [...Object.keys(data.icons), ...Object.keys(data.aliases ?? {})]
41
+ .filter((name) => getIconData(data, name) !== null)
42
+ .sort()
43
+ .map((name) => `${data.prefix}:${name}`);
44
+ }
45
+
46
+ export function resolveIconSvg(data: IconifyJSON, id: string): IconSvgData {
47
+ const [prefix, name, extra] = id.split(":");
48
+ if (prefix !== data.prefix || !name || extra !== undefined) throw new Error("Invalid icon ID");
49
+ const icon = getIconData(data, name);
50
+ if (!icon) throw new Error(`Unknown icon: ${id}`);
51
+ const svg = iconToSVG(icon);
52
+ return {
53
+ body: svg.body,
54
+ attributes: svg.attributes,
55
+ attribution: data.info
56
+ ? {
57
+ collection: data.info.name,
58
+ author: data.info.author.name,
59
+ license: data.info.license.title,
60
+ url: data.info.license.url,
61
+ }
62
+ : undefined,
63
+ };
64
+ }
65
+
66
+ export function scopeIconSvgIds(body: string, prefix: string): string {
67
+ const ids = [...body.matchAll(/\sid="([^\s"]+)"/g)].map((match) => match[1]!);
68
+ if (!ids.length) return body;
69
+ const pattern = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
70
+ return body.replace(
71
+ new RegExp(`([#;"])(${pattern})([")]|\\.[a-z])`, "g"),
72
+ (_match, before: string, id: string, after: string) => `${before}${prefix}${id}${after}`,
73
+ );
74
+ }
75
+
76
+ export function validateIconValue(
77
+ value: unknown,
78
+ schema: { fieldType?: string; enum?: unknown },
79
+ ): void {
80
+ if (schema.fieldType !== "Icon") return;
81
+ if (typeof value !== "string" || !Array.isArray(schema.enum) || !schema.enum.includes(value)) {
82
+ throw new Error(`Invalid icon ID: ${String(value)}`);
83
+ }
84
+ }
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
+ export * from "./icons.ts";
2
+ export * from "./comments.ts";
1
3
  export type { Router } from "../../../apps/api/src/router";
package/src/query-keys.ts CHANGED
@@ -21,6 +21,10 @@ type QueryKeyGroup = Record<string, QueryKey | ((...args: any[]) => QueryKey)>;
21
21
  export type ReadSource = "draft" | "live";
22
22
 
23
23
  export const queryKeys = {
24
+ comments: {
25
+ all: ["camox", "comments"],
26
+ list: (pageId: number) => ["camox", "comments", "list", pageId],
27
+ },
24
28
  pages: {
25
29
  list: ["camox", "pages", "list"],
26
30
  getByPath: (path: string, source?: ReadSource) =>