@realnation/builder-shared-sdk 2.4.0 → 2.4.2

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.
@@ -13,14 +13,7 @@ export * from './imageFit.js';
13
13
  export { wrapText, tokenize, layoutLines, type MeasureText, type TextAlign, type TextVAlign, type PlacedLine, } from './text.js';
14
14
  export { renderSouvenir, drawOrder, pageHasOpaqueBackground, souvenirSignature, loadSouvenirImage, } from './render.js';
15
15
  import { type SouvenirOptions, type SouvenirPage, type SouvenirResult, type SouvenirValues } from './types.js';
16
- /**
17
- * Element ids for output-bound elements are `outputs@@nodeId@@key[@@tab@@rand]`.
18
- * Modules know field keys; the page knows element ids. This bridges the two.
19
- */
20
- export declare function parseSouvenirOutputId(eid: string): {
21
- nodeId: string;
22
- key: string;
23
- } | null;
16
+ export { parseSouvenirOutputId } from './outputId.js';
24
17
  /**
25
18
  * Spreads `{ fieldKey: value }` over every element on the page bound to that
26
19
  * field, so a module never has to know how element ids are built.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/souvenir/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,SAAS,EACT,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAKpB;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKzF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACtD,MAAM,CAAC,EAAE,MAAM,GACd,cAAc,CAUhB;AAYD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,YAAY,EAClB,MAAM,GAAE,cAAmB,EAC3B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAqBzB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,GAAE,cAAmB,GAAG,MAAM,CAEnF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/souvenir/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,SAAS,EACT,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EACtD,MAAM,CAAC,EAAE,MAAM,GACd,cAAc,CAUhB;AAYD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,YAAY,EAClB,MAAM,GAAE,cAAmB,EAC3B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAqBzB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,GAAE,cAAmB,GAAG,MAAM,CAEnF"}
@@ -14,20 +14,8 @@ export { wrapText, tokenize, layoutLines, } from './text.js';
14
14
  export { renderSouvenir, drawOrder, pageHasOpaqueBackground, souvenirSignature, loadSouvenirImage, } from './render.js';
15
15
  import { pageHasOpaqueBackground, renderSouvenir, souvenirSignature } from './render.js';
16
16
  import { SouvenirError, } from './types.js';
17
- const OUTPUT_SEPARATOR = '@@';
18
- const OUTPUT_PREFIX = 'outputs';
19
- /**
20
- * Element ids for output-bound elements are `outputs@@nodeId@@key[@@tab@@rand]`.
21
- * Modules know field keys; the page knows element ids. This bridges the two.
22
- */
23
- export function parseSouvenirOutputId(eid) {
24
- if (!eid.startsWith(OUTPUT_PREFIX + OUTPUT_SEPARATOR))
25
- return null;
26
- const parts = eid.split(OUTPUT_SEPARATOR);
27
- if (parts.length < 3)
28
- return null;
29
- return { nodeId: parts[1], key: parts[2] };
30
- }
17
+ import { parseSouvenirOutputId } from './outputId.js';
18
+ export { parseSouvenirOutputId } from './outputId.js';
31
19
  /**
32
20
  * Spreads `{ fieldKey: value }` over every element on the page bound to that
33
21
  * field, so a module never has to know how element ids are built.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Element ids for output-bound elements are `outputs@@nodeId@@key[@@tab@@rand]`.
3
+ * Modules know field keys; the page knows element ids. This bridges the two.
4
+ */
5
+ export declare function parseSouvenirOutputId(eid: string): {
6
+ nodeId: string;
7
+ key: string;
8
+ } | null;
9
+ //# sourceMappingURL=outputId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outputId.d.ts","sourceRoot":"","sources":["../../src/souvenir/outputId.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKzF"}
@@ -0,0 +1,14 @@
1
+ const OUTPUT_SEPARATOR = '@@';
2
+ const OUTPUT_PREFIX = 'outputs';
3
+ /**
4
+ * Element ids for output-bound elements are `outputs@@nodeId@@key[@@tab@@rand]`.
5
+ * Modules know field keys; the page knows element ids. This bridges the two.
6
+ */
7
+ export function parseSouvenirOutputId(eid) {
8
+ if (!eid.startsWith(OUTPUT_PREFIX + OUTPUT_SEPARATOR))
9
+ return null;
10
+ const parts = eid.split(OUTPUT_SEPARATOR);
11
+ if (parts.length < 3)
12
+ return null;
13
+ return { nodeId: parts[1], key: parts[2] };
14
+ }
@@ -9,7 +9,22 @@
9
9
  * mirrors the runner's `elementStyle`, which is what the operator sees in the
10
10
  * editor and the preview.
11
11
  */
12
- import { type SouvenirOptions, type SouvenirPage, type SouvenirValues } from './types.js';
12
+ import { type SouvenirOptions, type SouvenirPage, type SouvenirProperty, type SouvenirValues } from './types.js';
13
+ /**
14
+ * An element bound to a Data field whose field has no value is left out, the
15
+ * way the runner hides it on a live page. Falling back to what the Builder
16
+ * stored for it would draw the field's name (such as "Message") into the image.
17
+ */
18
+ export declare function isUnfilledOutput(eid: string, values: SouvenirValues): boolean;
19
+ /**
20
+ * The border the runner shows for an element, or null when it shows none:
21
+ * border style None, no width, or a transparent colour. Shapes and every other
22
+ * element share these rules, as they share the runner's `elementStyle`.
23
+ */
24
+ export declare function borderOf(property: SouvenirProperty): {
25
+ width: number;
26
+ color: string;
27
+ } | null;
13
28
  /** Loads one image with EXIF orientation applied, or throws a SouvenirError. */
14
29
  export declare function loadSouvenirImage(url: string, crossOrigin: string | null | undefined): Promise<CanvasImageSource & {
15
30
  width: number;
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/souvenir/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,YAAY,EAEjB,KAAK,cAAc,EAEpB,MAAM,YAAY,CAAC;AAkRpB,gFAAgF;AAChF,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,iBAAiB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBhE;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAOtD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAYnE;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAMpF;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,YAAY,EAClB,MAAM,GAAE,cAAmB,EAC3B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAoE5B"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/souvenir/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EAEpB,MAAM,YAAY,CAAC;AA6CpB;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAI7E;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO5F;AAyPD,gFAAgF;AAChF,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,iBAAiB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBhE;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAStD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAYnE;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAMpF;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,YAAY,EAClB,MAAM,GAAE,cAAmB,EAC3B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAsE5B"}
@@ -10,8 +10,9 @@
10
10
  * editor and the preview.
11
11
  */
12
12
  import { computeImageFitDrawRect, hashSignature, normalizeImageFit } from './imageFit.js';
13
+ import { parseSouvenirOutputId } from './outputId.js';
13
14
  import { layoutLines, wrapText } from './text.js';
14
- import { isSouvenirSizeAllowed, SouvenirError, SOUVENIR_ELEMENT_TYPES, } from './types.js';
15
+ import { isSouvenirInteractive, isSouvenirSizeAllowed, SouvenirError, SOUVENIR_ELEMENT_TYPES, } from './types.js';
15
16
  const DEFAULT_LINE_HEIGHT = 1.2;
16
17
  const TRANSPARENT = 'TRANSPARENT';
17
18
  const num = (value, fallback = 0) => {
@@ -40,6 +41,33 @@ function geometryOf(property) {
40
41
  function isHidden(property) {
41
42
  return property['display'] === 'off';
42
43
  }
44
+ /**
45
+ * An element bound to a Data field whose field has no value is left out, the
46
+ * way the runner hides it on a live page. Falling back to what the Builder
47
+ * stored for it would draw the field's name (such as "Message") into the image.
48
+ */
49
+ export function isUnfilledOutput(eid, values) {
50
+ if (!parseSouvenirOutputId(eid))
51
+ return false;
52
+ const value = values[eid];
53
+ return value === undefined || value === null || String(value) === '';
54
+ }
55
+ /**
56
+ * The border the runner shows for an element, or null when it shows none:
57
+ * border style None, no width, or a transparent colour. Shapes and every other
58
+ * element share these rules, as they share the runner's `elementStyle`.
59
+ */
60
+ export function borderOf(property) {
61
+ if (property['border-style'] === 'none')
62
+ return null;
63
+ const width = num(property['border-width']);
64
+ if (width <= 0)
65
+ return null;
66
+ const raw = typeof property['border-color'] === 'string' ? property['border-color'].trim() : '';
67
+ if (raw === TRANSPARENT)
68
+ return null;
69
+ return { width, color: raw || '#94a3b8' };
70
+ }
43
71
  function roundedRectPath(ctx, x, y, width, height, radius) {
44
72
  const r = Math.max(0, Math.min(radius, Math.min(width, height) / 2));
45
73
  ctx.beginPath();
@@ -95,21 +123,16 @@ function fillBackground(ctx, property, width, height, radius, ellipse) {
95
123
  * size the operator gave it, at the cost of half a border width along the edge.
96
124
  */
97
125
  function strokeBorder(ctx, property, width, height, radius, ellipse) {
98
- if (property['border-style'] === 'none')
126
+ const border = borderOf(property);
127
+ if (!border)
99
128
  return;
100
- const borderWidth = num(property['border-width']);
101
- if (borderWidth <= 0)
102
- return;
103
- const color = colorOf(property['border-color']) || '#94a3b8';
104
- if (!color)
105
- return;
106
- const inset = borderWidth / 2;
129
+ const inset = border.width / 2;
107
130
  if (ellipse)
108
- ellipsePath(ctx, inset, inset, width - borderWidth, height - borderWidth);
131
+ ellipsePath(ctx, inset, inset, width - border.width, height - border.width);
109
132
  else
110
- roundedRectPath(ctx, inset, inset, width - borderWidth, height - borderWidth, Math.max(0, radius - inset));
111
- ctx.lineWidth = borderWidth;
112
- ctx.strokeStyle = color;
133
+ roundedRectPath(ctx, inset, inset, width - border.width, height - border.width, Math.max(0, radius - inset));
134
+ ctx.lineWidth = border.width;
135
+ ctx.strokeStyle = border.color;
113
136
  ctx.stroke();
114
137
  }
115
138
  function fontOf(property, fontSize) {
@@ -121,39 +144,55 @@ function fontOf(property, fontSize) {
121
144
  const quoted = /[",]/.test(family) || !/\s/.test(family) ? family : `"${family}"`;
122
145
  return `${style}${weight}${fontSize}px ${quoted}`;
123
146
  }
124
- function drawText(ctx, property, geo, content) {
147
+ /**
148
+ * Lays the text out inside the box already in element space. `padX` is the side
149
+ * padding the runner leaves on shape text.
150
+ */
151
+ function drawTextInBox(c, property, radius, width, height, content, padX = 0) {
125
152
  const fontSize = num(property['font-size'], 14);
126
153
  if (fontSize <= 0 || !content)
127
154
  return;
128
155
  const lineHeight = num(property['line-height'], DEFAULT_LINE_HEIGHT) || DEFAULT_LINE_HEIGHT;
129
156
  const align = (property['alignment'] || 'center');
130
157
  const vAlign = (property['v-alignment'] || 'middle');
158
+ const boxWidth = Math.max(0, width - padX * 2);
159
+ if (boxWidth <= 0)
160
+ return;
161
+ c.save();
162
+ roundedRectPath(c, 0, 0, width, height, radius);
163
+ c.clip();
164
+ c.font = fontOf(property, fontSize);
165
+ c.textBaseline = 'alphabetic';
166
+ c.textAlign = 'left';
167
+ c.fillStyle = colorOf(property['color']) || '#000000';
168
+ const measure = (text) => c.measureText(text).width;
169
+ const lines = wrapText(content, boxWidth, measure);
170
+ const placed = layoutLines(lines, { x: padX, y: 0, width: boxWidth, height }, fontSize, lineHeight, align, vAlign, measure);
171
+ const formatting = Array.isArray(property['formatting']) ? property['formatting'] : [];
172
+ const underline = formatting.includes('Underline');
173
+ const strike = formatting.includes('Strikethrough');
174
+ for (const line of placed) {
175
+ if (!line.text)
176
+ continue;
177
+ c.fillText(line.text, line.x, line.y);
178
+ if (underline)
179
+ c.fillRect(line.x, line.y + fontSize * 0.12, line.width, Math.max(1, fontSize / 16));
180
+ if (strike)
181
+ c.fillRect(line.x, line.y - fontSize * 0.28, line.width, Math.max(1, fontSize / 16));
182
+ }
183
+ c.restore();
184
+ }
185
+ function drawText(ctx, property, geo, content) {
186
+ if (!content)
187
+ return;
131
188
  inElementSpace(ctx, geo, (c, width, height) => {
132
189
  fillBackground(c, property, width, height, geo.radius, false);
133
- roundedRectPath(c, 0, 0, width, height, geo.radius);
134
- c.clip();
135
- c.font = fontOf(property, fontSize);
136
- c.textBaseline = 'alphabetic';
137
- c.textAlign = 'left';
138
- c.fillStyle = colorOf(property['color']) || '#000000';
139
- const measure = (text) => c.measureText(text).width;
140
- const lines = wrapText(content, width, measure);
141
- const placed = layoutLines(lines, { x: 0, y: 0, width, height }, fontSize, lineHeight, align, vAlign, measure);
142
- const formatting = Array.isArray(property['formatting']) ? property['formatting'] : [];
143
- const underline = formatting.includes('Underline');
144
- const strike = formatting.includes('Strikethrough');
145
- for (const line of placed) {
146
- if (!line.text)
147
- continue;
148
- c.fillText(line.text, line.x, line.y);
149
- if (underline)
150
- c.fillRect(line.x, line.y + fontSize * 0.12, line.width, Math.max(1, fontSize / 16));
151
- if (strike)
152
- c.fillRect(line.x, line.y - fontSize * 0.28, line.width, Math.max(1, fontSize / 16));
153
- }
190
+ drawTextInBox(c, property, geo.radius, width, height, content);
154
191
  });
155
192
  inElementSpace(ctx, geo, (c, width, height) => strokeBorder(c, property, width, height, geo.radius, false));
156
193
  }
194
+ /** The runner pads shape text by 4px on each side; match it so the wrap is the same. */
195
+ const SHAPE_TEXT_PADDING = 4;
157
196
  function drawImage(ctx, property, geo, image) {
158
197
  inElementSpace(ctx, geo, (c, width, height) => {
159
198
  fillBackground(c, property, width, height, geo.radius, false);
@@ -207,18 +246,14 @@ function drawShape(ctx, type, property, geo) {
207
246
  c.fillStyle = fill;
208
247
  c.fill();
209
248
  }
210
- const strokeWidth = num(property['stroke-width'], 0) || num(property['border-width'], 0);
211
- const strokeColor = colorOf(property['stroke-color']) || colorOf(property['border-color']);
212
- if (strokeWidth > 0 && strokeColor) {
213
- const inset = strokeWidth / 2;
214
- if (ellipse)
215
- ellipsePath(c, inset, inset, width - strokeWidth, height - strokeWidth);
216
- else
217
- roundedRectPath(c, inset, inset, width - strokeWidth, height - strokeWidth, Math.max(0, geo.radius - inset));
218
- c.lineWidth = strokeWidth;
219
- c.strokeStyle = strokeColor;
220
- c.stroke();
221
- }
249
+ // Rectangles and ellipses take their outline from the border settings, like
250
+ // on a live page; a new shape stores a 1px grey border with style None.
251
+ strokeBorder(c, property, width, height, geo.radius, ellipse);
252
+ // Text typed into a shape (a Button preset is a shape with text) is part of
253
+ // the picture, the same as it is on a live page.
254
+ const content = typeof property['content'] === 'string' ? property['content'] : '';
255
+ if (content)
256
+ drawTextInBox(c, property, geo.radius, width, height, content, SHAPE_TEXT_PADDING);
222
257
  });
223
258
  }
224
259
  /** Loads one image with EXIF orientation applied, or throws a SouvenirError. */
@@ -251,6 +286,8 @@ export function drawOrder(page) {
251
286
  return Object.keys(page.elements ?? {})
252
287
  .filter((eid) => SOUVENIR_ELEMENT_TYPES.includes(page.elements[eid]?.type))
253
288
  .filter((eid) => !isHidden(page.properties?.[eid] ?? {}))
289
+ // Controls stay on screen over the picture, so they are not in it
290
+ .filter((eid) => !isSouvenirInteractive(page.elements[eid]?.type, page.properties?.[eid] ?? {}))
254
291
  .map((eid, index) => ({ eid, index, z: num((page.properties?.[eid] ?? {})['zIndex']) }))
255
292
  .sort((a, b) => (a.z === b.z ? a.index - b.index : a.z - b.z))
256
293
  .map((entry) => entry.eid);
@@ -317,6 +354,8 @@ export async function renderSouvenir(page, values = {}, options = {}) {
317
354
  const type = page.elements[eid]?.type;
318
355
  if (type !== 'image' && type !== 'image-frame' && type !== 'bg')
319
356
  return;
357
+ if (isUnfilledOutput(eid, values))
358
+ return;
320
359
  const property = page.properties?.[eid] ?? {};
321
360
  const url = (values[eid] ?? property['source'] ?? '').toString().trim();
322
361
  if (!url)
@@ -335,6 +374,8 @@ export async function renderSouvenir(page, values = {}, options = {}) {
335
374
  const geo = geometryOf(property);
336
375
  if (geo.width <= 0 || geo.height <= 0)
337
376
  continue;
377
+ if (isUnfilledOutput(eid, values))
378
+ continue;
338
379
  if (type === 'text') {
339
380
  const content = (values[eid] ?? property['content'] ?? '').toString();
340
381
  drawText(ctx, property, geo, content);
@@ -14,6 +14,19 @@
14
14
  /** Element types the composer draws. Anything else on the page is skipped. */
15
15
  export declare const SOUVENIR_ELEMENT_TYPES: readonly ["bg", "image", "image-frame", "text", "shape-rect", "shape-ellipse", "shape-line"];
16
16
  export type SouvenirElementType = (typeof SOUVENIR_ELEMENT_TYPES)[number];
17
+ /**
18
+ * Element types that do something when the participant taps them, or that count
19
+ * down. On a page the module composes and displays itself, the runner keeps
20
+ * these on screen over the picture, so the composer must leave them out or the
21
+ * same thing would appear twice (`qik-sense-runner/src/services/pageRender.ts`
22
+ * keeps the same list).
23
+ */
24
+ export declare const SOUVENIR_INTERACTIVE_TYPES: readonly ["timeout", "button", "qr", "link"];
25
+ /**
26
+ * True when this element is a control rather than part of the picture: an
27
+ * interactive type, or any element the operator gave a target to.
28
+ */
29
+ export declare function isSouvenirInteractive(type: string | undefined | null, property?: SouvenirProperty): boolean;
17
30
  /** Anything the Builder stores for one element. Values arrive untyped from the API. */
18
31
  export type SouvenirProperty = Record<string, unknown>;
19
32
  /** One element as the Builder's `detail.elements` records it. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/souvenir/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,8FAQzB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEvD,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC1C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC9C;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAEvE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wGAAwG;IACxG,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,iBAAiB,CAAC;IAC1B,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,0EAA0E;AAC1E,qBAAa,aAAc,SAAQ,KAAK;aAEpB,IAAI,EAChB,QAAQ,GACR,UAAU,GACV,WAAW,GACX,oBAAoB,GACpB,cAAc,GACd,eAAe;gBANH,IAAI,EAChB,QAAQ,GACR,UAAU,GACV,WAAW,GACX,oBAAoB,GACpB,cAAc,GACd,eAAe,EACnB,OAAO,EAAE,MAAM;CAKlB;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,WAAa,CAAC;AAE5C,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAK5E;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE,CAW5E"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/souvenir/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,8FAQzB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,8CAA+C,CAAC;AAKvF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,QAAQ,GAAE,gBAAqB,GAAG,OAAO,CAG/G;AAED,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEvD,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC1C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC9C;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAEvE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wGAAwG;IACxG,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,iBAAiB,CAAC;IAC1B,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,0EAA0E;AAC1E,qBAAa,aAAc,SAAQ,KAAK;aAEpB,IAAI,EAChB,QAAQ,GACR,UAAU,GACV,WAAW,GACX,oBAAoB,GACpB,cAAc,GACd,eAAe;gBANH,IAAI,EAChB,QAAQ,GACR,UAAU,GACV,WAAW,GACX,oBAAoB,GACpB,cAAc,GACd,eAAe,EACnB,OAAO,EAAE,MAAM;CAKlB;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,WAAa,CAAC;AAE5C,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAK5E;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,EAAE,CAW5E"}
@@ -21,6 +21,25 @@ export const SOUVENIR_ELEMENT_TYPES = [
21
21
  'shape-ellipse',
22
22
  'shape-line',
23
23
  ];
24
+ /**
25
+ * Element types that do something when the participant taps them, or that count
26
+ * down. On a page the module composes and displays itself, the runner keeps
27
+ * these on screen over the picture, so the composer must leave them out or the
28
+ * same thing would appear twice (`qik-sense-runner/src/services/pageRender.ts`
29
+ * keeps the same list).
30
+ */
31
+ export const SOUVENIR_INTERACTIVE_TYPES = ['timeout', 'button', 'qr', 'link'];
32
+ /** Property keys that turn any element into something tappable. */
33
+ const INTERACTIVE_PROPERTIES = ['connection-key', 'url', 'key-code'];
34
+ /**
35
+ * True when this element is a control rather than part of the picture: an
36
+ * interactive type, or any element the operator gave a target to.
37
+ */
38
+ export function isSouvenirInteractive(type, property = {}) {
39
+ if (SOUVENIR_INTERACTIVE_TYPES.includes(String(type ?? '')))
40
+ return true;
41
+ return INTERACTIVE_PROPERTIES.some((key) => String(property?.[key] ?? '').trim() !== '');
42
+ }
24
43
  /** Thrown for every refusal the caller is expected to handle and show. */
25
44
  export class SouvenirError extends Error {
26
45
  code;
package/package.json CHANGED
@@ -1,64 +1,64 @@
1
- {
2
- "name": "@realnation/builder-shared-sdk",
3
- "version": "2.4.0",
4
- "type": "module",
5
- "private": false,
6
- "exports": {
7
- "./package.json": "./package.json",
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js"
11
- },
12
- "./runtime": {
13
- "types": "./dist/runtime/index.d.ts",
14
- "import": "./dist/runtime/index.js"
15
- },
16
- "./runtime/vue": {
17
- "types": "./dist/runtime/vue/index.d.ts",
18
- "import": "./dist/runtime/vue/index.js"
19
- },
20
- "./interaction": {
21
- "types": "./dist/interaction/index.d.ts",
22
- "import": "./dist/interaction/index.js"
23
- },
24
- "./interaction/vue": {
25
- "types": "./dist/interaction/vue.d.ts",
26
- "import": "./dist/interaction/vue.js"
27
- },
28
- "./souvenir": {
29
- "types": "./dist/souvenir/index.d.ts",
30
- "import": "./dist/souvenir/index.js"
31
- }
32
- },
33
- "files": [
34
- "dist"
35
- ],
36
- "scripts": {
37
- "build": "tsc -p tsconfig.json",
38
- "clean": "rimraf dist",
39
- "test": "vitest run",
40
- "test:watch": "vitest",
41
- "prepublishOnly": "npm run clean && npm run test && npm run build",
42
- "prepare": "npm run clean && npm run build"
43
- },
44
- "dependencies": {
45
- "ali-oss": "^6.23.0",
46
- "axios": "^1.7.7"
47
- },
48
- "peerDependencies": {
49
- "vue": "^3.4.0"
50
- },
51
- "peerDependenciesMeta": {
52
- "vue": {
53
- "optional": true
54
- }
55
- },
56
- "devDependencies": {
57
- "@types/ali-oss": "^6.16.11",
58
- "@types/node": "^26.1.2",
59
- "rimraf": "^5.0.5",
60
- "typescript": "^5.4.5",
61
- "vitest": "^2.1.0",
62
- "vue": "^3.4.0"
63
- }
64
- }
1
+ {
2
+ "name": "@realnation/builder-shared-sdk",
3
+ "version": "2.4.2",
4
+ "type": "module",
5
+ "private": false,
6
+ "exports": {
7
+ "./package.json": "./package.json",
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./runtime": {
13
+ "types": "./dist/runtime/index.d.ts",
14
+ "import": "./dist/runtime/index.js"
15
+ },
16
+ "./runtime/vue": {
17
+ "types": "./dist/runtime/vue/index.d.ts",
18
+ "import": "./dist/runtime/vue/index.js"
19
+ },
20
+ "./interaction": {
21
+ "types": "./dist/interaction/index.d.ts",
22
+ "import": "./dist/interaction/index.js"
23
+ },
24
+ "./interaction/vue": {
25
+ "types": "./dist/interaction/vue.d.ts",
26
+ "import": "./dist/interaction/vue.js"
27
+ },
28
+ "./souvenir": {
29
+ "types": "./dist/souvenir/index.d.ts",
30
+ "import": "./dist/souvenir/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "clean": "rimraf dist",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "prepublishOnly": "npm run clean && npm run test && npm run build",
42
+ "prepare": "npm run clean && npm run build"
43
+ },
44
+ "dependencies": {
45
+ "ali-oss": "^6.23.0",
46
+ "axios": "^1.7.7"
47
+ },
48
+ "peerDependencies": {
49
+ "vue": "^3.4.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "vue": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@types/ali-oss": "^6.16.11",
58
+ "@types/node": "^26.1.2",
59
+ "rimraf": "^5.0.5",
60
+ "typescript": "^5.4.5",
61
+ "vitest": "^2.1.0",
62
+ "vue": "^3.4.0"
63
+ }
64
+ }