storyloco 2.1.0 → 2.3.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 +31 -0
- package/package.json +7 -2
- package/packages/currency/types.ts +11 -0
- package/packages/vite/dist/schema/plugin.js +5 -0
- package/packages/vite/dist/server.d.ts +33 -16
- package/packages/vite/dist/server.js +63 -35
- package/packages/vite/dist/storyblok.svelte.d.ts +40 -0
- package/packages/vite/dist/storyblok.svelte.js +13 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, a
|
|
|
15
15
|
- **🏗️ Plans** - Floor plan manager with drag-and-drop sorting for types, floors, dimensions, and rooms
|
|
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
|
+
- **💷 Currency** - Amount input with a currency selector; configure allowed codes via the `currencies` and `default_currency` options
|
|
18
19
|
|
|
19
20
|
### Vite Plugins
|
|
20
21
|
|
|
@@ -175,16 +176,45 @@ await storyblok.story('layout')
|
|
|
175
176
|
// A list. `total` is the unpaged count, for "showing n of m"
|
|
176
177
|
const { stories, total } = await storyblok.stories({ content_type: 'blog', per_page: 2 })
|
|
177
178
|
|
|
179
|
+
// Everything that matches, paged through the API's 100-per-page cap
|
|
180
|
+
const products = await storyblok.all({ content_type: 'product' })
|
|
181
|
+
|
|
178
182
|
// The link tree, flattened — Storyblok returns it keyed by uuid
|
|
179
183
|
const links = await storyblok.links({ starts_with: 'blog/' })
|
|
180
184
|
```
|
|
181
185
|
|
|
186
|
+
Reads with a `content_type` are typed from your generated schema: the plugin registers
|
|
187
|
+
the space's content types with storyloco, so `stories({ content_type: 'product' })` yields
|
|
188
|
+
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.
|
|
191
|
+
|
|
182
192
|
Your params are spread last, so any default can be overridden:
|
|
183
193
|
|
|
184
194
|
```typescript
|
|
185
195
|
await storyblok.story('layout', { resolve_assets: 0 })
|
|
186
196
|
```
|
|
187
197
|
|
|
198
|
+
`story` treats a missing story as a 404. Where a slug is allowed not to exist, `find` returns
|
|
199
|
+
`null` instead — the miss is then neither raised nor logged:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const product = await storyblok.find<Product>(`products/${slug}`)
|
|
203
|
+
if (!product) error(404, 'Product not found')
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The content type can be a union. Each member becomes its own story type, so `is_component`
|
|
207
|
+
can narrow the result — checking `story.content.component` yourself narrows only `content`,
|
|
208
|
+
not the story:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { is_component } from 'storyloco'
|
|
212
|
+
|
|
213
|
+
const story = await storyblok.find<Product | Category>(`products/${slug}`)
|
|
214
|
+
if (is_component(story, 'product')) story.content.sku // ISbStoryData<Product>
|
|
215
|
+
if (is_component(story, 'category')) story.content.description // ISbStoryData<Category>
|
|
216
|
+
```
|
|
217
|
+
|
|
188
218
|
It's callable too, for anything the methods don't cover:
|
|
189
219
|
|
|
190
220
|
```typescript
|
|
@@ -258,6 +288,7 @@ export interface StoryblokCustomPlugins {
|
|
|
258
288
|
twitter_description: string
|
|
259
289
|
}
|
|
260
290
|
'loco-time': string
|
|
291
|
+
'loco-currency': import('storyloco/currency').Currency
|
|
261
292
|
'loco-asset': import('storyloco/asset').Asset
|
|
262
293
|
}
|
|
263
294
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyloco",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"packages/input/types.ts",
|
|
34
34
|
"packages/time/types.ts",
|
|
35
35
|
"packages/seo/types.ts",
|
|
36
|
-
"packages/hubspot_form/types.ts"
|
|
36
|
+
"packages/hubspot_form/types.ts",
|
|
37
|
+
"packages/currency/types.ts"
|
|
37
38
|
],
|
|
38
39
|
"exports": {
|
|
39
40
|
".": {
|
|
@@ -93,6 +94,10 @@
|
|
|
93
94
|
"./hubspot_form": {
|
|
94
95
|
"types": "./packages/hubspot_form/types.ts",
|
|
95
96
|
"svelte": "./packages/hubspot_form/src/app.svelte.ts"
|
|
97
|
+
},
|
|
98
|
+
"./currency": {
|
|
99
|
+
"types": "./packages/currency/types.ts",
|
|
100
|
+
"svelte": "./packages/currency/src/app.svelte.ts"
|
|
96
101
|
}
|
|
97
102
|
},
|
|
98
103
|
"devDependencies": {
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}
|
|
@@ -218,6 +218,11 @@ import type { ISbStoryData } from '@storyblok/js';`, "");
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
export type ContentType = ContentTypes[keyof ContentTypes]
|
|
221
|
+
|
|
222
|
+
/** Lets storyloco type reads by \`content_type\` without a type argument. */
|
|
223
|
+
declare module "storyloco" {
|
|
224
|
+
interface Registry extends ContentTypes {}
|
|
225
|
+
}
|
|
221
226
|
`;
|
|
222
227
|
let content = component_types.replace(`// This file was generated by the storyblok CLI.
|
|
223
228
|
// DO NOT MODIFY THIS FILE BY HAND.`, header);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ISbStoriesParams,
|
|
1
|
+
import type { ISbStoriesParams, ISbStoryParams } from "@storyblok/svelte";
|
|
2
2
|
import type { ISbLink, ISbLinksParams } from "storyblok-js-client";
|
|
3
|
-
import { type StoryblokClient, type Version } from "./storyblok.svelte.js";
|
|
3
|
+
import { type ContentFor, 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
|
/**
|
|
@@ -19,7 +19,10 @@ export declare function version(): Version;
|
|
|
19
19
|
*
|
|
20
20
|
* // anywhere on the server
|
|
21
21
|
* await storyblok.story("layout")
|
|
22
|
-
* await storyblok.
|
|
22
|
+
* await storyblok.find<Product>(`products/${slug}`) // null when missing
|
|
23
|
+
* await storyblok.find<Product | Category>(slug) // a union of stories; narrow with is_component
|
|
24
|
+
* await storyblok.stories({ content_type: "blog", per_page: 2 }) // typed from the generated schema
|
|
25
|
+
* await storyblok.all<Product>({ content_type: "product" }) // every page
|
|
23
26
|
*
|
|
24
27
|
* const { client, version } = storyblok()
|
|
25
28
|
* await client.get("cdn/links", { version })
|
|
@@ -29,23 +32,37 @@ export declare function setup(client: StoryblokClient): (() => {
|
|
|
29
32
|
client: StoryblokClient;
|
|
30
33
|
version: Version;
|
|
31
34
|
}) & {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* unparameterised call behaves as before.
|
|
37
|
-
*/
|
|
38
|
-
story<T = import("storyblok-js-client").ISbComponentType<string> & {
|
|
35
|
+
story: <T = import("storyblok-js-client").ISbComponentType<string> & {
|
|
36
|
+
[index: string]: any;
|
|
37
|
+
}>(slug: string, params?: ISbStoryParams) => Promise<Story<T>>;
|
|
38
|
+
find: <T = import("storyblok-js-client").ISbComponentType<string> & {
|
|
39
39
|
[index: string]: any;
|
|
40
|
-
}>(slug: string, params?: ISbStoryParams)
|
|
40
|
+
}>(slug: string, params?: ISbStoryParams) => Promise<Story<T> | null>;
|
|
41
|
+
stories: {
|
|
42
|
+
<K extends string>(params: ISbStoriesParams & {
|
|
43
|
+
content_type: K;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
stories: Story<ContentFor<K>>[];
|
|
46
|
+
total: number;
|
|
47
|
+
}>;
|
|
48
|
+
<T = import("storyblok-js-client").ISbComponentType<string> & {
|
|
49
|
+
[index: string]: any;
|
|
50
|
+
}>(params?: ISbStoriesParams): Promise<{
|
|
51
|
+
stories: Story<T>[];
|
|
52
|
+
total: number;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
all: {
|
|
56
|
+
<K extends string>(params: ISbStoriesParams & {
|
|
57
|
+
content_type: K;
|
|
58
|
+
}): Promise<Array<Story<ContentFor<K>>>>;
|
|
59
|
+
<T = import("storyblok-js-client").ISbComponentType<string> & {
|
|
60
|
+
[index: string]: any;
|
|
61
|
+
}>(params?: ISbStoriesParams): Promise<Array<Story<T>>>;
|
|
62
|
+
};
|
|
41
63
|
/**
|
|
42
64
|
* The link tree as a flat array — Storyblok returns it keyed by uuid, which is
|
|
43
65
|
* almost never what you want. Filtering and ordering are left to the caller.
|
|
44
66
|
*/
|
|
45
67
|
links(params?: ISbLinksParams): Promise<ISbLink[]>;
|
|
46
|
-
/** A list of stories. `total` is the unpaged count, for "showing n of m". */
|
|
47
|
-
stories<T>(params?: ISbStoriesParams): Promise<{
|
|
48
|
-
stories: Array<ISbStoryData<T>>;
|
|
49
|
-
total: number;
|
|
50
|
-
}>;
|
|
51
68
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { getRequestEvent } from "$app/server";
|
|
2
|
-
import {
|
|
2
|
+
import { error } from "@sveltejs/kit";
|
|
3
|
+
import { handle_error, } from "./storyblok.svelte.js";
|
|
4
|
+
function is_not_found(err) {
|
|
5
|
+
return typeof err === "object" && err !== null && "status" in err && err.status === 404;
|
|
6
|
+
}
|
|
3
7
|
/** The content version for the current request, as set by the `version` hook. */
|
|
4
8
|
export function version() {
|
|
5
9
|
return getRequestEvent().locals.version;
|
|
@@ -20,7 +24,10 @@ export function version() {
|
|
|
20
24
|
*
|
|
21
25
|
* // anywhere on the server
|
|
22
26
|
* await storyblok.story("layout")
|
|
23
|
-
* await storyblok.
|
|
27
|
+
* await storyblok.find<Product>(`products/${slug}`) // null when missing
|
|
28
|
+
* await storyblok.find<Product | Category>(slug) // a union of stories; narrow with is_component
|
|
29
|
+
* await storyblok.stories({ content_type: "blog", per_page: 2 }) // typed from the generated schema
|
|
30
|
+
* await storyblok.all<Product>({ content_type: "product" }) // every page
|
|
24
31
|
*
|
|
25
32
|
* const { client, version } = storyblok()
|
|
26
33
|
* await client.get("cdn/links", { version })
|
|
@@ -28,25 +35,61 @@ export function version() {
|
|
|
28
35
|
*/
|
|
29
36
|
export function setup(client) {
|
|
30
37
|
const context = () => ({ client, version: version() });
|
|
38
|
+
/**
|
|
39
|
+
* A single story by full slug, or `null` when there isn't one. Resolves the client's
|
|
40
|
+
* `relations`, unlike `stories`. For slugs that are allowed not to exist — a miss is
|
|
41
|
+
* routine, so it's neither raised nor logged. `T` is the story's content type.
|
|
42
|
+
*/
|
|
43
|
+
async function find(slug, params = {}) {
|
|
44
|
+
const response = await client
|
|
45
|
+
.get(`cdn/stories/${slug}`, {
|
|
46
|
+
version: version(),
|
|
47
|
+
resolve_links: "url",
|
|
48
|
+
resolve_relations: client.relations,
|
|
49
|
+
resolve_assets: 1,
|
|
50
|
+
...params,
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
if (is_not_found(err))
|
|
54
|
+
return null;
|
|
55
|
+
return handle_error(err);
|
|
56
|
+
});
|
|
57
|
+
return (response?.data.story ?? null);
|
|
58
|
+
}
|
|
59
|
+
/** A single story by full slug; a missing one is a 404. `find` for stories that may not exist. */
|
|
60
|
+
async function story(slug, params = {}) {
|
|
61
|
+
const found = await find(slug, params);
|
|
62
|
+
if (!found)
|
|
63
|
+
error(404, "Story not found");
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
async function stories(params = {}) {
|
|
67
|
+
const response = await client
|
|
68
|
+
.get("cdn/stories", {
|
|
69
|
+
version: version(),
|
|
70
|
+
resolve_links: "url",
|
|
71
|
+
resolve_assets: 1,
|
|
72
|
+
...params,
|
|
73
|
+
})
|
|
74
|
+
.catch(handle_error);
|
|
75
|
+
return { stories: response.data.stories, total: response.total ?? 0 };
|
|
76
|
+
}
|
|
77
|
+
async function all(params = {}) {
|
|
78
|
+
return client
|
|
79
|
+
.getAll("cdn/stories", {
|
|
80
|
+
version: version(),
|
|
81
|
+
resolve_links: "url",
|
|
82
|
+
resolve_assets: 1,
|
|
83
|
+
per_page: 100,
|
|
84
|
+
...params,
|
|
85
|
+
})
|
|
86
|
+
.catch(handle_error);
|
|
87
|
+
}
|
|
31
88
|
return Object.assign(context, {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* unparameterised call behaves as before.
|
|
37
|
-
*/
|
|
38
|
-
async story(slug, params = {}) {
|
|
39
|
-
const response = await client
|
|
40
|
-
.get(`cdn/stories/${slug}`, {
|
|
41
|
-
version: version(),
|
|
42
|
-
resolve_links: "url",
|
|
43
|
-
resolve_relations: client.relations,
|
|
44
|
-
resolve_assets: 1,
|
|
45
|
-
...params,
|
|
46
|
-
})
|
|
47
|
-
.catch(handle_error);
|
|
48
|
-
return response.data.story;
|
|
49
|
-
},
|
|
89
|
+
story,
|
|
90
|
+
find,
|
|
91
|
+
stories,
|
|
92
|
+
all,
|
|
50
93
|
/**
|
|
51
94
|
* The link tree as a flat array — Storyblok returns it keyed by uuid, which is
|
|
52
95
|
* almost never what you want. Filtering and ordering are left to the caller.
|
|
@@ -57,20 +100,5 @@ export function setup(client) {
|
|
|
57
100
|
.catch(handle_error);
|
|
58
101
|
return Object.values(response.data.links ?? {});
|
|
59
102
|
},
|
|
60
|
-
/** A list of stories. `total` is the unpaged count, for "showing n of m". */
|
|
61
|
-
async stories(params = {}) {
|
|
62
|
-
const response = await client
|
|
63
|
-
.get("cdn/stories", {
|
|
64
|
-
version: version(),
|
|
65
|
-
resolve_links: "url",
|
|
66
|
-
resolve_assets: 1,
|
|
67
|
-
...params,
|
|
68
|
-
})
|
|
69
|
-
.catch(handle_error);
|
|
70
|
-
return {
|
|
71
|
-
stories: response.data.stories,
|
|
72
|
-
total: response.total ?? 0,
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
103
|
});
|
|
76
104
|
}
|
|
@@ -140,6 +140,46 @@ export interface Richtext<Blocks = unknown> {
|
|
|
140
140
|
content?: Array<Richtext<Blocks>>;
|
|
141
141
|
marks?: Array<Richtext<Blocks>>;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Content types by component name, for narrowing reads by `content_type`. Empty here;
|
|
145
|
+
* the schema plugin's generated file merges the space's `ContentTypes` into it, so
|
|
146
|
+
* `storyblok.stories({ content_type: "product" })` is typed as products without a type
|
|
147
|
+
* argument. Until a schema is generated every name falls back to the SDK's content type.
|
|
148
|
+
*/
|
|
149
|
+
export interface Registry {
|
|
150
|
+
}
|
|
151
|
+
/** The SDK's permissive content type: any component, any fields. */
|
|
152
|
+
export type Content = ISbStoryData["content"];
|
|
153
|
+
/** Each name in a comma-separated `content_type`. */
|
|
154
|
+
type Names<S extends string> = S extends `${infer A},${infer B}` ? A | Names<B> : S;
|
|
155
|
+
/**
|
|
156
|
+
* The content behind a `content_type` param. Registered names map to their generated
|
|
157
|
+
* types (a list to their union); anything else is the permissive `Content`.
|
|
158
|
+
*/
|
|
159
|
+
export type ContentFor<K extends string> = [Names<K>] extends [keyof Registry] ? Registry[Names<K>] : Content;
|
|
160
|
+
/**
|
|
161
|
+
* A story per member of `T`, so a union of content types is a union of stories —
|
|
162
|
+
* `Story<Product | Category>` is `ISbStoryData<Product> | ISbStoryData<Category>`, which
|
|
163
|
+
* `is_component` can narrow. `ISbStoryData<Product | Category>` can't be.
|
|
164
|
+
*/
|
|
165
|
+
export type Story<T> = T extends unknown ? ISbStoryData<T> : never;
|
|
166
|
+
/**
|
|
167
|
+
* Narrow a story (or a union of them) by its content's `component`. TypeScript won't
|
|
168
|
+
* narrow the story through the nested discriminant on its own.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* const story = await storyblok.find<Product | Category>(`products/${slug}`)
|
|
173
|
+
* if (is_component(story, "product")) story.content.sku // ISbStoryData<Product>
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export declare function is_component<S extends ISbStoryData<{
|
|
177
|
+
component?: string;
|
|
178
|
+
}>, K extends string>(story: S | null | undefined, component: K): story is Extract<S, {
|
|
179
|
+
content: {
|
|
180
|
+
component?: K;
|
|
181
|
+
};
|
|
182
|
+
}>;
|
|
143
183
|
/** Adds the `story.url` that `resolve_links: "url"` injects but the CLI doesn't declare. */
|
|
144
184
|
export type Resolved<T extends {
|
|
145
185
|
story?: unknown;
|
|
@@ -137,6 +137,19 @@ export class StoryblokClient {
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Narrow a story (or a union of them) by its content's `component`. TypeScript won't
|
|
142
|
+
* narrow the story through the nested discriminant on its own.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const story = await storyblok.find<Product | Category>(`products/${slug}`)
|
|
147
|
+
* if (is_component(story, "product")) story.content.sku // ISbStoryData<Product>
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function is_component(story, component) {
|
|
151
|
+
return story?.content.component === component;
|
|
152
|
+
}
|
|
140
153
|
/**
|
|
141
154
|
* Resolve a multilink to an href. Story links become root-relative, everything else passes
|
|
142
155
|
* through. Prefers `story.url` over `cached_url`, which goes stale when a target moves —
|