storyloco 2.3.0 → 2.5.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
@@ -16,6 +16,7 @@ A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, a
16
16
  - **📝 Input** - Comprehensive form input field with support for all HTML input types, checkboxes, radio buttons, selects, and textareas
17
17
  - **⏰ Time** - Simple time input field with step control
18
18
  - **💷 Currency** - Amount input with a currency selector; configure allowed codes via the `currencies` and `default_currency` options
19
+ - **📅 Dates** - List of dates, each with an optional time, end date and end time, plus a timezone picker; configure via the `min`, `max` and `auto_sort` options
19
20
 
20
21
  ### Vite Plugins
21
22
 
@@ -59,6 +60,35 @@ import type { SEO } from 'storyloco/seo'
59
60
  import type { Video, Heading, Link, Input, Asset, SEO } from 'storyloco'
60
61
  ```
61
62
 
63
+ ### Write Currency Values
64
+
65
+ Map spreadsheet cells or plain numbers to the currency field's stored shape, e.g. for a Management API import:
66
+
67
+ ```ts
68
+ import { to_currency } from 'storyloco/currency/utils'
69
+
70
+ to_currency('£1,234.50') // currency defaults to GBP
71
+ // { plugin: 'uiloco-currency', currency: 'GBP', amount: 1234.5, formatted: '£1,234.50' }
72
+ to_currency('', 'GBP') // null
73
+ to_currency('POA', 'GBP') // throws, as do negatives and too many decimal places
74
+ ```
75
+
76
+ `format_currency(amount, code)` gives the same `formatted` string the plugin writes.
77
+
78
+ ### Display Dates
79
+
80
+ Timed entries carry UTC instants, so the site formats them natively in the field's zone. All-day entries are dates, not moments: use `start_date`/`end_date` as they are.
81
+
82
+ ```ts
83
+ import type { DateEntry, Dates } from 'storyloco/dates'
84
+
85
+ const show = ({ timezone }: Dates, entry: DateEntry) =>
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))
89
+ : [entry.start_date, entry.end_date].filter(Boolean).join(' – ')
90
+ ```
91
+
62
92
  ### Use Storyblok Client
63
93
 
64
94
  #### 1. Create your Storyblok client
@@ -186,8 +216,8 @@ const links = await storyblok.links({ starts_with: 'blog/' })
186
216
  Reads with a `content_type` are typed from your generated schema: the plugin registers
187
217
  the space's content types with storyloco, so `stories({ content_type: 'product' })` yields
188
218
  product stories and `content_type: 'product,category'` a union of both — no type argument
189
- needed. Until a schema has been generated, or for a name it doesn't know, the content falls
190
- back to the SDK's permissive type. An explicit type argument always wins.
219
+ needed, and unknown names are rejected. Until a schema has been generated, any name is
220
+ accepted and the content is the SDK's permissive type. An explicit type argument always wins.
191
221
 
192
222
  Your params are spread last, so any default can be overridden:
193
223
 
@@ -289,6 +319,7 @@ export interface StoryblokCustomPlugins {
289
319
  }
290
320
  'loco-time': string
291
321
  'loco-currency': import('storyloco/currency').Currency
322
+ 'loco-dates': import('storyloco/dates').Dates
292
323
  'loco-asset': import('storyloco/asset').Asset
293
324
  }
294
325
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "2.3.0",
3
+ "version": "2.5.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",
@@ -29,12 +29,15 @@
29
29
  "packages/theme/types.ts",
30
30
  "packages/shared/dist",
31
31
  "packages/vite/dist",
32
+ "packages/vite/src/lib/currency.ts",
33
+ "!packages/vite/dist/**/*.test.*",
32
34
  "packages/ticketsource/types.ts",
33
35
  "packages/input/types.ts",
34
36
  "packages/time/types.ts",
35
37
  "packages/seo/types.ts",
36
38
  "packages/hubspot_form/types.ts",
37
- "packages/currency/types.ts"
39
+ "packages/currency/types.ts",
40
+ "packages/dates/types.ts"
38
41
  ],
39
42
  "exports": {
40
43
  ".": {
@@ -98,6 +101,14 @@
98
101
  "./currency": {
99
102
  "types": "./packages/currency/types.ts",
100
103
  "svelte": "./packages/currency/src/app.svelte.ts"
104
+ },
105
+ "./currency/utils": {
106
+ "types": "./packages/vite/dist/currency.d.ts",
107
+ "import": "./packages/vite/dist/currency.js"
108
+ },
109
+ "./dates": {
110
+ "types": "./packages/dates/types.ts",
111
+ "svelte": "./packages/dates/src/app.svelte.ts"
101
112
  }
102
113
  },
103
114
  "devDependencies": {
@@ -108,7 +119,7 @@
108
119
  "kitto": "1.6.0",
109
120
  "oxfmt": "0.59.0",
110
121
  "typescript": "6.0.3",
111
- "vite": "8.1.5"
122
+ "vite": "7.2.2"
112
123
  },
113
124
  "peerDependencies": {
114
125
  "@storyblok/svelte": "7.2.2",
@@ -1,11 +1 @@
1
- /**
2
- * Narrow `Code` to the codes configured in the field's `currencies` option,
3
- * e.g. `Currency<'GBP' | 'USD'>`.
4
- */
5
- export interface Currency<Code extends string = Uppercase<string>> {
6
- /** ISO 4217 code, e.g. `GBP` */
7
- currency: Code
8
- amount: number | null
9
- /** `amount` formatted for display, e.g. `£130,000.00`; `null` when there's no amount */
10
- formatted: string | null
11
- }
1
+ export type { Currency, CurrencyField } from '../vite/src/lib/currency.js'
@@ -0,0 +1,20 @@
1
+ export interface DateEntry {
2
+ /** `YYYY-MM-DD`, wall-clock in the field's `timezone` */
3
+ start_date: string
4
+ /** `HH:mm`; `null` makes this an all-day entry */
5
+ start_time: string | null
6
+ /** `YYYY-MM-DD`; `null` means the same day as `start_date` */
7
+ end_date: string | null
8
+ /** `HH:mm`; only set when `start_time` is */
9
+ end_time: string | null
10
+ /** the start as an ISO instant, e.g. `2026-09-15T10:00:00.000Z`; `null` for all-day entries, which are dates, not moments */
11
+ utc_start: string | null
12
+ /** the end as an ISO instant; `null` without an `end_time` */
13
+ utc_end: string | null
14
+ }
15
+
16
+ export interface Dates {
17
+ /** IANA zone the editor picked, e.g. `Europe/London` */
18
+ timezone: string
19
+ dates: DateEntry[]
20
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Narrow `Code` to the codes configured in the field's `currencies` option,
3
+ * e.g. `Currency<'GBP' | 'USD'>`.
4
+ */
5
+ export interface Currency<Code extends string = Uppercase<string>> {
6
+ /** ISO 4217 code, e.g. `GBP` */
7
+ currency: Code;
8
+ amount: number | null;
9
+ /** `amount` formatted for display, e.g. `£130,000.00`; `null` when there's no amount */
10
+ formatted: string | null;
11
+ }
12
+ /** The field's value as Storyblok stores it, `plugin` key included — what a Management API write needs. */
13
+ export type CurrencyField<Code extends string = Uppercase<string>> = Currency<Code> & {
14
+ plugin: "uiloco-currency";
15
+ };
16
+ /** `amount` as the plugin displays it, e.g. `£1,234.50`. The plugin formats with this, so the two can't drift. */
17
+ export declare function format_currency(amount: number, currency?: string): string;
18
+ /**
19
+ * A spreadsheet cell or plain number as the `uiloco-currency` field's value:
20
+ * `to_currency('£1,234.50', 'GBP')` → `{ plugin, currency: 'GBP', amount: 1234.5, formatted: '£1,234.50' }`.
21
+ *
22
+ * Takes what a CSV export of money looks like — a leading symbol or code, thousands
23
+ * commas — and throws on anything else rather than guessing: negatives, `POA`, a
24
+ * decimal comma, or more places than the currency has (`1.005` in GBP). An import
25
+ * should stop on a bad row, not write a wrong price. Blank returns `null`.
26
+ */
27
+ export declare function to_currency<Code extends Uppercase<string> = "GBP">(value: string | number | null | undefined, currency?: Code): CurrencyField<Code> | null;
@@ -0,0 +1,45 @@
1
+ /** `amount` as the plugin displays it, e.g. `£1,234.50`. The plugin formats with this, so the two can't drift. */
2
+ export function format_currency(amount, currency = "GBP") {
3
+ return new Intl.NumberFormat("en", {
4
+ style: "currency",
5
+ currency,
6
+ currencyDisplay: "narrowSymbol",
7
+ }).format(amount);
8
+ }
9
+ function minor_digits(currency) {
10
+ return (new Intl.NumberFormat("en", { style: "currency", currency }).resolvedOptions()
11
+ .maximumFractionDigits ?? 2);
12
+ }
13
+ /**
14
+ * A spreadsheet cell or plain number as the `uiloco-currency` field's value:
15
+ * `to_currency('£1,234.50', 'GBP')` → `{ plugin, currency: 'GBP', amount: 1234.5, formatted: '£1,234.50' }`.
16
+ *
17
+ * Takes what a CSV export of money looks like — a leading symbol or code, thousands
18
+ * commas — and throws on anything else rather than guessing: negatives, `POA`, a
19
+ * decimal comma, or more places than the currency has (`1.005` in GBP). An import
20
+ * should stop on a bad row, not write a wrong price. Blank returns `null`.
21
+ */
22
+ export function to_currency(value, currency = "GBP") {
23
+ if (value === null || value === undefined)
24
+ return null;
25
+ const input = String(value).trim();
26
+ if (input === "")
27
+ return null;
28
+ const raw = input
29
+ .replace(/^[^\d\s.,-]{1,3}\s?/, "")
30
+ .replace(/\s?[A-Z]{3}$/, "")
31
+ .replace(/,(?=\d{3}(?:\D|$))/g, "");
32
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(raw);
33
+ if (!match)
34
+ throw new RangeError(`Not a ${currency} amount: ${JSON.stringify(value)}`);
35
+ if ((match[2]?.length ?? 0) > minor_digits(currency)) {
36
+ throw new RangeError(`${JSON.stringify(value)} has more decimal places than ${currency} allows`);
37
+ }
38
+ const amount = Number(raw);
39
+ return {
40
+ plugin: "uiloco-currency",
41
+ currency,
42
+ amount,
43
+ formatted: format_currency(amount, currency),
44
+ };
45
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ISbStoriesParams, ISbStoryParams } from "@storyblok/svelte";
2
2
  import type { ISbLink, ISbLinksParams } from "storyblok-js-client";
3
- import { type ContentFor, type Story, type StoryblokClient, type Version } from "./storyblok.svelte.js";
3
+ import { type ContentFor, type ContentTypeParam, type Story, type StoryblokClient, type Version } from "./storyblok.svelte.js";
4
4
  /** The content version for the current request, as set by the `version` hook. */
5
5
  export declare function version(): Version;
6
6
  /**
@@ -39,7 +39,7 @@ export declare function setup(client: StoryblokClient): (() => {
39
39
  [index: string]: any;
40
40
  }>(slug: string, params?: ISbStoryParams) => Promise<Story<T> | null>;
41
41
  stories: {
42
- <K extends string>(params: ISbStoriesParams & {
42
+ <K extends ContentTypeParam>(params: ISbStoriesParams & {
43
43
  content_type: K;
44
44
  }): Promise<{
45
45
  stories: Story<ContentFor<K>>[];
@@ -47,18 +47,22 @@ export declare function setup(client: StoryblokClient): (() => {
47
47
  }>;
48
48
  <T = import("storyblok-js-client").ISbComponentType<string> & {
49
49
  [index: string]: any;
50
- }>(params?: ISbStoriesParams): Promise<{
50
+ }>(params?: Omit<ISbStoriesParams, "content_type"> & {
51
+ content_type?: ContentTypeParam;
52
+ }): Promise<{
51
53
  stories: Story<T>[];
52
54
  total: number;
53
55
  }>;
54
56
  };
55
57
  all: {
56
- <K extends string>(params: ISbStoriesParams & {
58
+ <K extends ContentTypeParam>(params: ISbStoriesParams & {
57
59
  content_type: K;
58
60
  }): Promise<Array<Story<ContentFor<K>>>>;
59
61
  <T = import("storyblok-js-client").ISbComponentType<string> & {
60
62
  [index: string]: any;
61
- }>(params?: ISbStoriesParams): Promise<Array<Story<T>>>;
63
+ }>(params?: Omit<ISbStoriesParams, "content_type"> & {
64
+ content_type?: ContentTypeParam;
65
+ }): Promise<Array<Story<T>>>;
62
66
  };
63
67
  /**
64
68
  * The link tree as a flat array — Storyblok returns it keyed by uuid, which is
@@ -152,11 +152,18 @@ export interface Registry {
152
152
  export type Content = ISbStoryData["content"];
153
153
  /** Each name in a comma-separated `content_type`. */
154
154
  type Names<S extends string> = S extends `${infer A},${infer B}` ? A | Names<B> : S;
155
+ /** Registered content type names; `never` until a schema has been generated. */
156
+ export type Known = keyof Registry & string;
157
+ /**
158
+ * What a `content_type` param accepts: any string until a schema registers names, then a
159
+ * registered name (autocompleted) or a comma-separated list starting with one.
160
+ */
161
+ export type ContentTypeParam = [Known] extends [never] ? string : Known | `${Known},${string}`;
155
162
  /**
156
163
  * The content behind a `content_type` param. Registered names map to their generated
157
164
  * types (a list to their union); anything else is the permissive `Content`.
158
165
  */
159
- export type ContentFor<K extends string> = [Names<K>] extends [keyof Registry] ? Registry[Names<K>] : Content;
166
+ export type ContentFor<K extends string> = [Names<K>] extends [Known] ? Registry[Names<K>] : Content;
160
167
  /**
161
168
  * A story per member of `T`, so a union of content types is a union of stories —
162
169
  * `Story<Product | Category>` is `ISbStoryData<Product> | ISbStoryData<Category>`, which
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Narrow `Code` to the codes configured in the field's `currencies` option,
3
+ * e.g. `Currency<'GBP' | 'USD'>`.
4
+ */
5
+ export interface Currency<Code extends string = Uppercase<string>> {
6
+ /** ISO 4217 code, e.g. `GBP` */
7
+ currency: Code
8
+ amount: number | null
9
+ /** `amount` formatted for display, e.g. `£130,000.00`; `null` when there's no amount */
10
+ formatted: string | null
11
+ }
12
+
13
+ /** The field's value as Storyblok stores it, `plugin` key included — what a Management API write needs. */
14
+ export type CurrencyField<Code extends string = Uppercase<string>> = Currency<Code> & {
15
+ plugin: "uiloco-currency"
16
+ }
17
+
18
+ /** `amount` as the plugin displays it, e.g. `£1,234.50`. The plugin formats with this, so the two can't drift. */
19
+ export function format_currency(amount: number, currency: string = "GBP"): string {
20
+ return new Intl.NumberFormat("en", {
21
+ style: "currency",
22
+ currency,
23
+ currencyDisplay: "narrowSymbol",
24
+ }).format(amount)
25
+ }
26
+
27
+ function minor_digits(currency: string): number {
28
+ return (
29
+ new Intl.NumberFormat("en", { style: "currency", currency }).resolvedOptions()
30
+ .maximumFractionDigits ?? 2
31
+ )
32
+ }
33
+
34
+ /**
35
+ * A spreadsheet cell or plain number as the `uiloco-currency` field's value:
36
+ * `to_currency('£1,234.50', 'GBP')` → `{ plugin, currency: 'GBP', amount: 1234.5, formatted: '£1,234.50' }`.
37
+ *
38
+ * Takes what a CSV export of money looks like — a leading symbol or code, thousands
39
+ * commas — and throws on anything else rather than guessing: negatives, `POA`, a
40
+ * decimal comma, or more places than the currency has (`1.005` in GBP). An import
41
+ * should stop on a bad row, not write a wrong price. Blank returns `null`.
42
+ */
43
+ export function to_currency<Code extends Uppercase<string> = "GBP">(
44
+ value: string | number | null | undefined,
45
+ currency: Code = "GBP" as Code
46
+ ): CurrencyField<Code> | null {
47
+ if (value === null || value === undefined) return null
48
+ const input = String(value).trim()
49
+ if (input === "") return null
50
+
51
+ const raw = input
52
+ .replace(/^[^\d\s.,-]{1,3}\s?/, "")
53
+ .replace(/\s?[A-Z]{3}$/, "")
54
+ .replace(/,(?=\d{3}(?:\D|$))/g, "")
55
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(raw)
56
+ if (!match) throw new RangeError(`Not a ${currency} amount: ${JSON.stringify(value)}`)
57
+ if ((match[2]?.length ?? 0) > minor_digits(currency)) {
58
+ throw new RangeError(`${JSON.stringify(value)} has more decimal places than ${currency} allows`)
59
+ }
60
+
61
+ const amount = Number(raw)
62
+ return {
63
+ plugin: "uiloco-currency",
64
+ currency,
65
+ amount,
66
+ formatted: format_currency(amount, currency),
67
+ }
68
+ }