storyloco 2.5.0 → 2.7.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/README.md CHANGED
@@ -84,8 +84,11 @@ import type { DateEntry, Dates } from 'storyloco/dates'
84
84
 
85
85
  const show = ({ timezone }: Dates, entry: DateEntry) =>
86
86
  entry.utc_start
87
- ? new Intl.DateTimeFormat('en-GB', { timeZone: timezone, dateStyle: 'medium', timeStyle: 'short' })
88
- .formatRange(new Date(entry.utc_start), new Date(entry.utc_end ?? entry.utc_start))
87
+ ? new Intl.DateTimeFormat('en-GB', {
88
+ timeZone: timezone,
89
+ dateStyle: 'medium',
90
+ timeStyle: 'short'
91
+ }).formatRange(new Date(entry.utc_start), new Date(entry.utc_end ?? entry.utc_start))
89
92
  : [entry.start_date, entry.end_date].filter(Boolean).join(' – ')
90
93
  ```
91
94
 
@@ -114,6 +117,19 @@ at your call site rather than move into the client.
114
117
  `client.handle_error` is on the instance, so `.catch(client.handle_error)` works without
115
118
  wrapping anything.
116
119
 
120
+ Errors carry only a `message` by default. If your `App.Error` needs more, pass `error` to shape
121
+ the body of every 404 and 500 storyloco raises — it's typed against your own `App.Error`:
122
+
123
+ ```typescript
124
+ export const client = new StoryblokClient({
125
+ token: PUBLIC_STORYBLOK_ACCESS_TOKEN,
126
+ error: status => ({
127
+ message: status === 404 ? 'Story not found' : 'Storyblok request failed',
128
+ code: status === 404 ? 'NOT_FOUND' : 'STORYBLOK'
129
+ })
130
+ })
131
+ ```
132
+
117
133
  #### 2. Use in your Svelte components
118
134
 
119
135
  **`+page.svelte`:**
@@ -252,7 +268,7 @@ const { client, version } = storyblok()
252
268
  await client.get('cdn/datasource_entries', { version })
253
269
  ```
254
270
 
255
- Errors go through `handle_error`, so a missing story is a 404 and anything else a 500.
271
+ Errors go through `client.handle_error`, so a missing story is a 404 and anything else a 500, shaped by the client's `error` option.
256
272
 
257
273
  ### Use Shared Components
258
274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, and Tailwind CSS",
5
5
  "keywords": [
6
6
  "storyblok",
@@ -31,7 +31,6 @@
31
31
  "packages/vite/dist",
32
32
  "packages/vite/src/lib/currency.ts",
33
33
  "!packages/vite/dist/**/*.test.*",
34
- "packages/ticketsource/types.ts",
35
34
  "packages/input/types.ts",
36
35
  "packages/time/types.ts",
37
36
  "packages/seo/types.ts",
@@ -53,10 +52,6 @@
53
52
  "./plans": {
54
53
  "types": "./packages/plans/types.ts"
55
54
  },
56
- "./ticketsource": {
57
- "types": "./packages/ticketsource/types.ts",
58
- "svelte": "./packages/ticketsource/src/app.svelte.ts"
59
- },
60
55
  "./input": {
61
56
  "types": "./packages/input/types.ts",
62
57
  "svelte": "./packages/input/src/app.svelte.ts"
@@ -71,6 +66,7 @@
71
66
  "./asset": {
72
67
  "types": "./packages/asset/types.ts"
73
68
  },
69
+ "./shared/plugin.css": "./packages/shared/dist/plugin.css",
74
70
  "./shared/utils": {
75
71
  "types": "./packages/shared/dist/utils.d.ts",
76
72
  "svelte": "./packages/shared/dist/utils.js"
@@ -115,7 +111,6 @@
115
111
  "@types/node": "26.1.1",
116
112
  "@typescript-eslint/eslint-plugin": "8.65.0",
117
113
  "@typescript-eslint/parser": "8.65.0",
118
- "eslint": "10.7.0",
119
114
  "kitto": "1.6.0",
120
115
  "oxfmt": "0.59.0",
121
116
  "typescript": "6.0.3",
@@ -131,7 +126,7 @@
131
126
  "scripts": {
132
127
  "add-plugin": "npx @storyblok/field-plugin-cli@latest add --dir \"./packages\" --structure monorepo",
133
128
  "build": "cd packages/vite && npm run build && cd ../../packages/shared && npm run build",
134
- "lint": "oxfmt --check && eslint --ext .js .",
129
+ "lint": "oxfmt --check && bun run --filter './packages/*' lint",
135
130
  "format": "oxfmt"
136
131
  },
137
132
  "type": "module",
@@ -18,4 +18,5 @@
18
18
  className
19
19
  )}
20
20
  bind:value
21
- {...restProps}></textarea>
21
+ {...restProps}
22
+ ></textarea>
@@ -0,0 +1,474 @@
1
+ /* Shared look for UI Loco's Storyblok space plugins (Shop, Mux Library), close to Storyblok's own
2
+ editor UI. Wrap the plugin in `.plugin` and use its classes: card, button, segments, search,
3
+ status, table, empty, problem, skeleton. Import with `import 'storyloco/shared/plugin.css'`. */
4
+ .plugin {
5
+ --sb-ink: #1b243f;
6
+ --sb-muted: #5f6781;
7
+ --sb-line: #e2e5ec;
8
+ --sb-bg: #f6f7f9;
9
+ --sb-surface: #fff;
10
+ /* Hovered rows, open orders and inset controls inside a card. */
11
+ --sb-raised: #f6f7f9;
12
+ --sb-line-strong: #c9ceda;
13
+ --sb-accent: #00b3b0;
14
+ /* Teal text and links; the primary button matches storyloco's field plugins. */
15
+ --sb-accent-ink: #05807f;
16
+ --sb-accent-wash: #e5f7f6;
17
+ --sb-primary: #05807f;
18
+ --sb-primary-hover: #046867;
19
+ --sb-on-primary: #fff;
20
+ /* Tooltips and toasts. */
21
+ --sb-tip: #1b243f;
22
+ --sb-on-tip: #fff;
23
+ --sb-shimmer: #eceef3;
24
+ /* Chart bars: validated for contrast and chroma against the white card. */
25
+ --sb-bar: #00938f;
26
+ --sb-good: #1c7c47;
27
+ --sb-good-wash: #e5f4ea;
28
+ --sb-warn: #8a5a00;
29
+ --sb-warn-wash: #fdf1d8;
30
+ --sb-danger: #b42323;
31
+ --sb-danger-wash: #fce9e9;
32
+ --sb-mono: ui-monospace, 'SF Mono', Menlo, monospace;
33
+ --sb-shadow: 0 1px 2px rgb(27 36 63 / 0.04), 0 1px 3px rgb(27 36 63 / 0.06);
34
+ --sb-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
35
+
36
+ /* Dark tokens follow storyloco's field plugins (packages/shared global.css), which
37
+ also switch on the system colour scheme. */
38
+ @media (prefers-color-scheme: dark) {
39
+ --sb-ink: #f8fafc;
40
+ --sb-muted: #8f93a2;
41
+ --sb-line: rgb(255 255 255 / 0.1);
42
+ --sb-line-strong: rgb(255 255 255 / 0.22);
43
+ /* Storyblok's own dark sidebar and dashboard: cards share the page colour,
44
+ with raised panels a step lighter. */
45
+ --sb-bg: #242328;
46
+ --sb-surface: #242328;
47
+ --sb-raised: #333237;
48
+ --sb-accent-ink: #3bc4b9;
49
+ --sb-accent-wash: rgb(59 196 185 / 0.14);
50
+ --sb-primary: #05807f;
51
+ --sb-primary-hover: #06918f;
52
+ --sb-on-primary: #fff;
53
+ --sb-tip: #45454d;
54
+ --sb-shimmer: #3d3d44;
55
+ /* Validated against the dark card. */
56
+ --sb-bar: #2aa9a3;
57
+ --sb-good: #5fd08f;
58
+ --sb-good-wash: rgb(95 208 143 / 0.14);
59
+ --sb-warn: #f0b650;
60
+ --sb-warn-wash: rgb(240 182 80 / 0.14);
61
+ --sb-danger: #ff6467;
62
+ --sb-danger-wash: rgb(255 100 103 / 0.14);
63
+ --sb-shadow: none;
64
+
65
+ color-scheme: dark;
66
+ }
67
+
68
+ background: var(--sb-bg);
69
+ color: var(--sb-ink);
70
+ font:
71
+ 14px/1.45 system-ui,
72
+ -apple-system,
73
+ 'Segoe UI',
74
+ Roboto,
75
+ sans-serif;
76
+ -webkit-font-smoothing: antialiased;
77
+ min-block-size: 100dvh;
78
+ padding: 28px 32px 48px;
79
+
80
+ @media (width < 640px) {
81
+ padding: 20px 16px 40px;
82
+ }
83
+
84
+ & ::selection {
85
+ background: var(--sb-accent-wash);
86
+ }
87
+
88
+ /* The site's global styles would otherwise leak in. */
89
+ & :is(p, h1, h2, h3, dl, ul) {
90
+ margin: 0;
91
+ }
92
+
93
+ & svg {
94
+ display: inline-block;
95
+ flex: none;
96
+ max-inline-size: none;
97
+ }
98
+
99
+ & .card {
100
+ background: var(--sb-surface);
101
+ border: 1px solid var(--sb-line);
102
+ border-radius: 10px;
103
+ box-shadow: var(--sb-shadow);
104
+ padding: 20px;
105
+
106
+ /* Tables and toolbars run edge to edge; they pad themselves. */
107
+ &.flush {
108
+ overflow: hidden;
109
+ padding: 0;
110
+ }
111
+ }
112
+
113
+ /* In an edge-to-edge card the table lines up with the toolbar above it. */
114
+ & .flush > .table :is(th, td):first-child {
115
+ padding-inline-start: 20px;
116
+ }
117
+
118
+ & .flush > .table :is(th, td):last-child {
119
+ padding-inline-end: 16px;
120
+ }
121
+
122
+ /* A search field with its magnifier inside. */
123
+ & .search {
124
+ align-items: center;
125
+ display: flex;
126
+ position: relative;
127
+
128
+ & svg {
129
+ color: var(--sb-muted);
130
+ inset-inline-start: 11px;
131
+ pointer-events: none;
132
+ position: absolute;
133
+ z-index: 1;
134
+ }
135
+
136
+ & input {
137
+ flex: 1;
138
+ min-inline-size: 0;
139
+ padding-inline-start: 34px;
140
+ }
141
+ }
142
+
143
+ /* Segmented control, e.g. the order status filter. */
144
+ & .segments {
145
+ background: var(--sb-raised);
146
+ border: 1px solid var(--sb-line);
147
+ border-radius: 8px;
148
+ display: flex;
149
+ gap: 2px;
150
+ padding: 3px;
151
+
152
+ & button {
153
+ border-radius: 5px;
154
+ color: var(--sb-muted);
155
+ cursor: pointer;
156
+ font-size: 13px;
157
+ font-weight: 500;
158
+ padding: 5px 11px;
159
+ text-transform: capitalize;
160
+ transition:
161
+ background 0.15s,
162
+ color 0.15s;
163
+
164
+ &:hover {
165
+ color: var(--sb-ink);
166
+ }
167
+
168
+ &[aria-checked='true'] {
169
+ background: var(--sb-surface);
170
+ box-shadow:
171
+ 0 1px 2px rgb(0 0 0 / 0.08),
172
+ 0 0 0 1px var(--sb-line);
173
+ color: var(--sb-ink);
174
+ }
175
+
176
+ &:focus-visible {
177
+ outline: 2px solid var(--sb-accent);
178
+ outline-offset: 1px;
179
+ }
180
+ }
181
+ }
182
+
183
+ & .card-head {
184
+ align-items: baseline;
185
+ display: flex;
186
+ flex-wrap: wrap;
187
+ gap: 4px 12px;
188
+ justify-content: space-between;
189
+
190
+ & h2 {
191
+ font-size: 15px;
192
+ font-weight: 600;
193
+ }
194
+ }
195
+
196
+ & .muted {
197
+ color: var(--sb-muted);
198
+ }
199
+
200
+ & input,
201
+ & textarea {
202
+ background: var(--sb-surface);
203
+ border: 1px solid var(--sb-line);
204
+ border-radius: 8px;
205
+ color: var(--sb-ink);
206
+ font: inherit;
207
+ padding: 8px 12px;
208
+ transition:
209
+ border-color 0.15s,
210
+ box-shadow 0.15s;
211
+
212
+ &::placeholder {
213
+ color: var(--sb-muted);
214
+ opacity: 0.8;
215
+ }
216
+
217
+ &:hover {
218
+ border-color: var(--sb-line-strong);
219
+ }
220
+
221
+ &:focus {
222
+ border-color: var(--sb-accent);
223
+ box-shadow: 0 0 0 3px var(--sb-accent-wash);
224
+ outline: none;
225
+ }
226
+ }
227
+
228
+ & .button {
229
+ align-items: center;
230
+ background: var(--sb-surface);
231
+ border: 1px solid var(--sb-line);
232
+ border-radius: 8px;
233
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
234
+ color: var(--sb-ink);
235
+ cursor: pointer;
236
+ display: inline-flex;
237
+ font-size: 14px;
238
+ font-weight: 500;
239
+ gap: 6px;
240
+ padding: 8px 14px;
241
+ text-decoration: none;
242
+ transition:
243
+ background 0.15s,
244
+ border-color 0.15s;
245
+ white-space: nowrap;
246
+
247
+ &:hover {
248
+ background: var(--sb-raised);
249
+ border-color: var(--sb-line-strong);
250
+ }
251
+
252
+ &:active {
253
+ transform: translateY(1px);
254
+ }
255
+
256
+ &:focus-visible {
257
+ outline: 2px solid var(--sb-accent);
258
+ outline-offset: 2px;
259
+ }
260
+
261
+ &:disabled {
262
+ cursor: wait;
263
+ opacity: 0.6;
264
+ }
265
+
266
+ &.primary {
267
+ background: var(--sb-primary);
268
+ border-color: var(--sb-primary);
269
+ color: var(--sb-on-primary);
270
+
271
+ &:hover {
272
+ background: var(--sb-primary-hover);
273
+ }
274
+ }
275
+
276
+ &.danger {
277
+ background: var(--sb-danger);
278
+ border-color: var(--sb-danger);
279
+ color: var(--sb-surface);
280
+
281
+ &:hover {
282
+ background: var(--sb-danger);
283
+ filter: brightness(1.08);
284
+ }
285
+ }
286
+ }
287
+
288
+ & .link {
289
+ color: var(--sb-accent-ink);
290
+ cursor: pointer;
291
+ font-weight: 500;
292
+ text-align: start;
293
+
294
+ &:hover {
295
+ text-decoration: underline;
296
+ }
297
+ }
298
+
299
+ & .pill {
300
+ background: var(--sb-accent-wash);
301
+ border-radius: 999px;
302
+ color: var(--sb-accent-ink);
303
+ display: inline-block;
304
+ flex: none;
305
+ font-size: 11px;
306
+ font-weight: 600;
307
+ letter-spacing: 0.02em;
308
+ line-height: 18px;
309
+ margin-inline: 4px;
310
+ padding: 0 8px;
311
+ }
312
+
313
+ /* A dot and the word, so state never rides on colour alone. */
314
+ & .status {
315
+ align-items: center;
316
+ border-radius: 999px;
317
+ display: inline-flex;
318
+ font-size: 12px;
319
+ font-weight: 500;
320
+ gap: 6px;
321
+ padding: 2px 9px 2px 7px;
322
+ text-transform: capitalize;
323
+
324
+ &::before {
325
+ background: currentColor;
326
+ block-size: 6px;
327
+ border-radius: 50%;
328
+ content: '';
329
+ inline-size: 6px;
330
+ }
331
+
332
+ &[data-status='paid'] {
333
+ background: var(--sb-good-wash);
334
+ color: var(--sb-good);
335
+ }
336
+
337
+ &[data-status='pending'] {
338
+ background: var(--sb-warn-wash);
339
+ color: var(--sb-warn);
340
+ }
341
+
342
+ &[data-status='failed'] {
343
+ background: var(--sb-danger-wash);
344
+ color: var(--sb-danger);
345
+ }
346
+
347
+ &[data-status='refunded'] {
348
+ background: var(--sb-raised);
349
+ color: var(--sb-muted);
350
+ }
351
+
352
+ &[data-status='ready'] {
353
+ background: var(--sb-good-wash);
354
+ color: var(--sb-good);
355
+ }
356
+
357
+ &:is([data-status='preparing'], [data-status='unused']) {
358
+ background: var(--sb-warn-wash);
359
+ color: var(--sb-warn);
360
+ }
361
+
362
+ &[data-status='errored'] {
363
+ background: var(--sb-danger-wash);
364
+ color: var(--sb-danger);
365
+ }
366
+ }
367
+
368
+ & .table {
369
+ border-collapse: collapse;
370
+ inline-size: 100%;
371
+
372
+ & th,
373
+ & td {
374
+ border-block-end: 1px solid var(--sb-line);
375
+ padding: 10px 12px;
376
+ text-align: start;
377
+ vertical-align: middle;
378
+ }
379
+
380
+ & th:first-child,
381
+ & td:first-child {
382
+ padding-inline-start: 4px;
383
+ }
384
+
385
+ & th {
386
+ color: var(--sb-muted);
387
+ font-size: 12px;
388
+ font-weight: 500;
389
+ white-space: nowrap;
390
+ }
391
+
392
+ & tbody tr:last-child td {
393
+ border-block-end: 0;
394
+ }
395
+
396
+ & .number {
397
+ font-variant-numeric: tabular-nums;
398
+ text-align: end;
399
+ white-space: nowrap;
400
+ }
401
+ }
402
+
403
+ & .empty {
404
+ padding: 48px 16px;
405
+ text-align: center;
406
+
407
+ & > svg {
408
+ background: var(--sb-raised);
409
+ block-size: 56px;
410
+ border-radius: 50%;
411
+ box-sizing: border-box;
412
+ color: var(--sb-muted);
413
+ inline-size: 56px;
414
+ margin-block-end: 12px;
415
+ padding: 14px;
416
+ }
417
+
418
+ & p + p {
419
+ margin-block-start: 4px;
420
+ }
421
+ }
422
+
423
+ & .problem {
424
+ align-items: center;
425
+ background: var(--sb-danger-wash);
426
+ border-radius: 6px;
427
+ color: var(--sb-danger);
428
+ display: flex;
429
+ flex-wrap: wrap;
430
+ gap: 12px;
431
+ justify-content: space-between;
432
+ padding: 10px 12px;
433
+ }
434
+
435
+ & .skeleton {
436
+ display: grid;
437
+ gap: 10px;
438
+
439
+ & > div {
440
+ animation: shimmer 1.4s ease-in-out infinite;
441
+ background: linear-gradient(
442
+ 90deg,
443
+ var(--sb-raised) 25%,
444
+ var(--sb-shimmer) 50%,
445
+ var(--sb-raised) 75%
446
+ )
447
+ 0 0 / 200% 100%;
448
+ block-size: 36px;
449
+ border-radius: 6px;
450
+ }
451
+ }
452
+
453
+ & .u-hidden {
454
+ clip-path: inset(50%);
455
+ position: absolute;
456
+ white-space: nowrap;
457
+ inline-size: 1px;
458
+ block-size: 1px;
459
+ overflow: hidden;
460
+ }
461
+ }
462
+
463
+ @keyframes shimmer {
464
+ to {
465
+ background-position: -200% 0;
466
+ }
467
+ }
468
+
469
+ @media (prefers-reduced-motion: reduce) {
470
+ .plugin * {
471
+ animation: none !important;
472
+ transition: none !important;
473
+ }
474
+ }
@@ -1,6 +1,6 @@
1
1
  import { getRequestEvent } from "$app/server";
2
2
  import { error } from "@sveltejs/kit";
3
- import { handle_error, } from "./storyblok.svelte.js";
3
+ import {} from "./storyblok.svelte.js";
4
4
  function is_not_found(err) {
5
5
  return typeof err === "object" && err !== null && "status" in err && err.status === 404;
6
6
  }
@@ -52,7 +52,7 @@ export function setup(client) {
52
52
  .catch((err) => {
53
53
  if (is_not_found(err))
54
54
  return null;
55
- return handle_error(err);
55
+ return client.handle_error(err);
56
56
  });
57
57
  return (response?.data.story ?? null);
58
58
  }
@@ -60,7 +60,7 @@ export function setup(client) {
60
60
  async function story(slug, params = {}) {
61
61
  const found = await find(slug, params);
62
62
  if (!found)
63
- error(404, "Story not found");
63
+ error(404, client.error(404));
64
64
  return found;
65
65
  }
66
66
  async function stories(params = {}) {
@@ -71,7 +71,7 @@ export function setup(client) {
71
71
  resolve_assets: 1,
72
72
  ...params,
73
73
  })
74
- .catch(handle_error);
74
+ .catch(client.handle_error);
75
75
  return { stories: response.data.stories, total: response.total ?? 0 };
76
76
  }
77
77
  async function all(params = {}) {
@@ -83,7 +83,7 @@ export function setup(client) {
83
83
  per_page: 100,
84
84
  ...params,
85
85
  })
86
- .catch(handle_error);
86
+ .catch(client.handle_error);
87
87
  }
88
88
  return Object.assign(context, {
89
89
  story,
@@ -97,7 +97,7 @@ export function setup(client) {
97
97
  async links(params = {}) {
98
98
  const response = await client
99
99
  .get("cdn/links", { version: version(), per_page: 1000, ...params })
100
- .catch(handle_error);
100
+ .catch(client.handle_error);
101
101
  return Object.values(response.data.links ?? {});
102
102
  },
103
103
  });
@@ -43,7 +43,15 @@ export interface ClientOptions {
43
43
  * news posts reuse the blog template. Applied by `template`.
44
44
  */
45
45
  aliases?: Record<string, string>;
46
+ /**
47
+ * The body of the errors storyloco raises, for projects whose `App.Error` needs more
48
+ * than a message — e.g. `(status) => ({ message: "Not found", code: "NOT_FOUND" })`.
49
+ * `cause` is the Storyblok error, when there was one.
50
+ */
51
+ error?: ErrorBody;
46
52
  }
53
+ /** Shapes the body of a 404 (missing story) or 500 (anything else Storyblok threw) */
54
+ export type ErrorBody = (status: 404 | 500, cause?: unknown) => App.Error;
47
55
  /** Type alias for the storyblok api instance */
48
56
  type API = ReturnType<typeof useStoryblokApi>;
49
57
  /**
@@ -83,8 +91,10 @@ export declare class StoryblokClient {
83
91
  put: API["put"];
84
92
  /** Make a DELETE request to the Storyblok API */
85
93
  delete: API["delete"];
86
- /** The standalone `handle_error`, on the instance so `.catch(client.handle_error)` works */
87
- handle_error: typeof handle_error;
94
+ /** Shapes the body of every error storyloco raises; see `ClientOptions.error` */
95
+ error: ErrorBody;
96
+ /** `handle_error` with this client's `error` shape, bound so `.catch(client.handle_error)` works */
97
+ handle_error: (err: unknown) => never;
88
98
  /**
89
99
  * Configured here rather than by subclassing, so setup is one expression.
90
100
  *
@@ -97,7 +107,7 @@ export declare class StoryblokClient {
97
107
  * })
98
108
  * ```
99
109
  */
100
- constructor({ token, components, relations, aliases }: ClientOptions);
110
+ constructor({ token, components, relations, aliases, error, }: ClientOptions);
101
111
  /**
102
112
  * Load the components and set up the SDK.
103
113
  *
@@ -215,9 +225,10 @@ export declare function href(link?: Link): string | undefined;
215
225
  * Turn a Storyblok error into a SvelteKit one, for `.catch(handle_error)`.
216
226
  *
217
227
  * A missing story is a 404; anything else is a 500, since the CMS being unreachable isn't
218
- * something to show a visitor a Storyblok status code for.
228
+ * something to show a visitor a Storyblok status code for. `body` shapes the error; prefer
229
+ * `client.handle_error`, which passes the client's own.
219
230
  */
220
- export declare function handle_error(err: unknown): never;
231
+ export declare function handle_error(err: unknown, body?: ErrorBody): never;
221
232
  /** Declaring these keeps `SbBlokData`'s index signature from widening every access. */
222
233
  interface BlokFields {
223
234
  anchor?: string;
@@ -2,6 +2,9 @@ import { storyblokInit, apiPlugin, renderRichText as richtext, useStoryblokBridg
2
2
  import { SvelteMap } from "svelte/reactivity";
3
3
  import { createAttachmentKey as attach } from "svelte/attachments";
4
4
  import { error } from "@sveltejs/kit";
5
+ const default_error = (status) => ({
6
+ message: status === 404 ? "Story not found" : "Internal server error",
7
+ });
5
8
  /**
6
9
  * Main Storyblok client class that handles component registration, API calls, and story bridging
7
10
  * Provides a unified interface for working with Storyblok in Svelte applications
@@ -56,8 +59,10 @@ export class StoryblokClient {
56
59
  put = async (...args) => (await this.#api()).put(...args);
57
60
  /** Make a DELETE request to the Storyblok API */
58
61
  delete = async (...args) => (await this.#api()).delete(...args);
59
- /** The standalone `handle_error`, on the instance so `.catch(client.handle_error)` works */
60
- handle_error = handle_error;
62
+ /** Shapes the body of every error storyloco raises; see `ClientOptions.error` */
63
+ error = default_error;
64
+ /** `handle_error` with this client's `error` shape, bound so `.catch(client.handle_error)` works */
65
+ handle_error = (err) => handle_error(err, this.error);
61
66
  /**
62
67
  * Configured here rather than by subclassing, so setup is one expression.
63
68
  *
@@ -70,11 +75,12 @@ export class StoryblokClient {
70
75
  * })
71
76
  * ```
72
77
  */
73
- constructor({ token, components = {}, relations = [], aliases = {} }) {
78
+ constructor({ token, components = {}, relations = [], aliases = {}, error = default_error, }) {
74
79
  this.#access_token = token;
75
80
  this.#component_imports = components;
76
81
  this.relations = relations;
77
82
  this.aliases = aliases;
83
+ this.error = error;
78
84
  // Eager, but handled so a failure surfaces to whoever awaits `init` rather than
79
85
  // as an unhandled rejection
80
86
  this.init().catch(() => { });
@@ -166,14 +172,15 @@ export function href(link) {
166
172
  * Turn a Storyblok error into a SvelteKit one, for `.catch(handle_error)`.
167
173
  *
168
174
  * A missing story is a 404; anything else is a 500, since the CMS being unreachable isn't
169
- * something to show a visitor a Storyblok status code for.
175
+ * something to show a visitor a Storyblok status code for. `body` shapes the error; prefer
176
+ * `client.handle_error`, which passes the client's own.
170
177
  */
171
- export function handle_error(err) {
178
+ export function handle_error(err, body = default_error) {
172
179
  console.error(err);
173
180
  const status = typeof err === "object" && err !== null && "status" in err ? err.status : undefined;
174
181
  if (status === 404)
175
- error(404, "Story not found");
176
- error(500, "Internal server error");
182
+ error(404, body(404, err));
183
+ error(500, body(500, err));
177
184
  }
178
185
  /**
179
186
  * Spread onto a block's root element to wire it up for the visual editor.
@@ -1,8 +0,0 @@
1
- export interface Ticketsource {
2
- event: string
3
- dates: Array<{
4
- id: string
5
- start: string
6
- price: string
7
- }>
8
- }