storyloco 1.15.2 → 2.0.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
@@ -24,7 +24,7 @@ A collection of slick Storyblok field plugins built with Svelte 5, TypeScript, a
24
24
  ### Storyblok Client
25
25
 
26
26
  - **🎯 StoryblokClient** - Reactive wrapper for Storyblok with Svelte 5 integration
27
- - **🔧 Utilities** - Re-exported `editable`, `Block`, and `richtext` functions
27
+ - **🔧 Utilities** - `attrs` for wiring a block to the visual editor, plus re-exported `editable`, `Block`, and `richtext` functions
28
28
 
29
29
  ### Shared Components
30
30
 
@@ -68,18 +68,20 @@ import type { Video, Heading, Link, Input, Asset, SEO } from 'storyloco'
68
68
  import { StoryblokClient } from 'storyloco'
69
69
  import { PUBLIC_STORYBLOK_ACCESS_TOKEN } from '$env/static/public'
70
70
 
71
- // Create client (defaults to $lib/blocks/*.svelte) - init() called automatically
72
- export const client = new StoryblokClient(PUBLIC_STORYBLOK_ACCESS_TOKEN)
71
+ export const client = new StoryblokClient({
72
+ token: PUBLIC_STORYBLOK_ACCESS_TOKEN,
73
+ relations: ['shared.items'],
74
+ components: import.meta.glob(['$blocks/**/*.svelte', '$templates/*.svelte'])
75
+ })
76
+ ```
73
77
 
74
- // With custom component globs
75
- export const client = new StoryblokClient(
76
- PUBLIC_STORYBLOK_ACCESS_TOKEN,
77
- import.meta.glob(['$blocks/*.svelte', '$templates/*.svelte'])
78
- )
78
+ Everything is configured here rather than by subclassing, and `init()` runs on its own — the
79
+ API methods await it, so you never call it on the server. `components` takes the result of
80
+ `import.meta.glob`: Vite has to see the literal pattern to rewrite it, so the glob has to stay
81
+ at your call site rather than move into the client.
79
82
 
80
- // Single glob pattern
81
- export const client = new StoryblokClient(PUBLIC_STORYBLOK_ACCESS_TOKEN, import.meta.glob('$blocks/*.svelte'))
82
- ```
83
+ `client.handle_error` is on the instance, so `.catch(client.handle_error)` works without
84
+ wrapping anything.
83
85
 
84
86
  #### 2. Use in your Svelte components
85
87
 
@@ -108,7 +110,7 @@ export const client = new StoryblokClient(PUBLIC_STORYBLOK_ACCESS_TOKEN, import.
108
110
 
109
111
  ```svelte
110
112
  <script lang="ts">
111
- import { editable } from 'storyloco'
113
+ import { attrs } from 'storyloco'
112
114
 
113
115
  // This is auto-generated by the Storyblok Schema plugin
114
116
  import type { Blok, Hero } from '$lib/components.schema.js'
@@ -120,12 +122,78 @@ export const client = new StoryblokClient(PUBLIC_STORYBLOK_ACCESS_TOKEN, import.
120
122
  let { blok } = $props()
121
123
  </script>
122
124
 
123
- <section use:editable={blok}>
125
+ <section {...attrs(blok)}>
124
126
  <h1>{blok.headline}</h1>
125
127
  <p>{blok.subheadline}</p>
126
128
  </section>
127
129
  ```
128
130
 
131
+ `attrs` makes the block editable in the visual editor and gives it a stable `id` — the
132
+ block's `anchor` field when it has one, so sections can be linked to, otherwise its `_uid`.
133
+
134
+ Prefer it to `use:editable`. Being an attachment rather than an action it spreads, so the
135
+ element keeps its own attributes, and you can wrap it to add project-specific ones in one
136
+ place rather than repeating them in every block:
137
+
138
+ ```typescript
139
+ // Mapping section fields onto CSS custom properties is left to you — the field names and
140
+ // the design tokens are yours. Wrap `attrs` rather than reaching past it:
141
+ //
142
+ // export function section<T extends SectionFields>(blok: Blok<T>) {
143
+ // let style = ''
144
+ // if (blok.theme) style += `--theme: var(--c-${blok.theme});`
145
+ // if (blok.pad_top) style += `--pt: var(--s-${blok.pad_top});`
146
+ // return { ...attrs(blok), style }
147
+ // }
148
+ ```
149
+
150
+ `editable` is still exported for anything `attrs` doesn't cover.
151
+
152
+ ### Read Content on the Server
153
+
154
+ `storyloco/server` binds a client to the current request, so reads carry the version from
155
+ the `version` hook and the resolve options you'd otherwise repeat everywhere. Server-only,
156
+ since it reads the active request.
157
+
158
+ **`$lib/storyblok.server.ts`:**
159
+
160
+ ```typescript
161
+ import { setup } from 'storyloco/server'
162
+ import { client } from './storyblok.js'
163
+
164
+ export const storyblok = setup(client)
165
+ ```
166
+
167
+ Then anywhere on the server:
168
+
169
+ ```typescript
170
+ import { storyblok } from '$lib/storyblok.server.js'
171
+
172
+ // A single story, with the client's `relations` resolved
173
+ await storyblok.story('layout')
174
+
175
+ // A list. `total` is the unpaged count, for "showing n of m"
176
+ const { stories, total } = await storyblok.stories({ content_type: 'blog', per_page: 2 })
177
+
178
+ // The link tree, flattened — Storyblok returns it keyed by uuid
179
+ const links = await storyblok.links({ starts_with: 'blog/' })
180
+ ```
181
+
182
+ Your params are spread last, so any default can be overridden:
183
+
184
+ ```typescript
185
+ await storyblok.story('layout', { resolve_assets: 0 })
186
+ ```
187
+
188
+ It's callable too, for anything the methods don't cover:
189
+
190
+ ```typescript
191
+ const { client, version } = storyblok()
192
+ await client.get('cdn/datasource_entries', { version })
193
+ ```
194
+
195
+ Errors go through `handle_error`, so a missing story is a 404 and anything else a 500.
196
+
129
197
  ### Use Shared Components
130
198
 
131
199
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyloco",
3
- "version": "1.15.2",
3
+ "version": "2.0.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",
@@ -25,13 +25,15 @@
25
25
  "packages/plans/types.ts",
26
26
  "packages/link/types.ts",
27
27
  "packages/asset/types.ts",
28
+ "packages/asset/r2.ts",
28
29
  "packages/theme/types.ts",
29
30
  "packages/shared/dist",
30
31
  "packages/vite/dist",
31
32
  "packages/ticketsource/types.ts",
32
33
  "packages/input/types.ts",
33
34
  "packages/time/types.ts",
34
- "packages/seo/types.ts"
35
+ "packages/seo/types.ts",
36
+ "packages/hubspot_form/types.ts"
35
37
  ],
36
38
  "exports": {
37
39
  ".": {
@@ -77,27 +79,38 @@
77
79
  "types": "./packages/vite/dist/hooks.d.ts",
78
80
  "import": "./packages/vite/dist/hooks.js"
79
81
  },
82
+ "./server": {
83
+ "types": "./packages/vite/dist/server.d.ts",
84
+ "import": "./packages/vite/dist/server.js"
85
+ },
80
86
  "./time": {
81
87
  "types": "./packages/time/types.ts",
82
88
  "svelte": "./packages/time/src/app.svelte.ts"
83
89
  },
84
90
  "./seo": {
85
91
  "types": "./packages/seo/types.ts"
92
+ },
93
+ "./hubspot_form": {
94
+ "types": "./packages/hubspot_form/types.ts",
95
+ "svelte": "./packages/hubspot_form/src/app.svelte.ts"
86
96
  }
87
97
  },
88
98
  "devDependencies": {
89
- "@types/node": "24.10.0",
90
- "@typescript-eslint/eslint-plugin": "8.46.3",
91
- "@typescript-eslint/parser": "8.46.3",
92
- "eslint": "9.39.1",
93
- "kitto": "1.0.0-rc.17",
94
- "typescript": "5.9.3",
95
- "vite": "7.2.0"
99
+ "@types/node": "26.1.1",
100
+ "@typescript-eslint/eslint-plugin": "8.65.0",
101
+ "@typescript-eslint/parser": "8.65.0",
102
+ "eslint": "10.7.0",
103
+ "kitto": "1.6.0",
104
+ "oxfmt": "0.59.0",
105
+ "typescript": "6.0.3",
106
+ "vite": "8.1.5"
96
107
  },
97
108
  "peerDependencies": {
98
- "@storyblok/svelte": "5.2.14",
99
- "@storyblok/field-plugin": "^1.6.0",
100
- "svelte": "^5.0.0"
109
+ "@storyblok/svelte": "7.2.2",
110
+ "@storyblok/field-plugin": "^1.7.1",
111
+ "svelte": "^5.0.0",
112
+ "@sveltejs/kit": "^2.0.0",
113
+ "storyblok-js-client": "^7.0.0"
101
114
  },
102
115
  "scripts": {
103
116
  "add-plugin": "npx @storyblok/field-plugin-cli@latest add --dir \"./packages\" --structure monorepo",
@@ -110,8 +123,7 @@
110
123
  "packages/*"
111
124
  ],
112
125
  "dependencies": {
113
- "ora": "9.0.0",
114
- "oxfmt": "0.55.0",
115
- "path-to-regexp": "8.3.0"
126
+ "ora": "9.4.1",
127
+ "path-to-regexp": "8.4.2"
116
128
  }
117
129
  }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Vendored subset of the `moxyloco/r2` types.
3
+ *
4
+ * These are hand-copied from the moxy R2 OpenAPI client so storyloco consumers
5
+ * don't need `moxyloco` (and its `openapi-client-axios` dependency) installed
6
+ * just to read the asset field-plugin types. Only the schemas and operations the
7
+ * plugin actually uses are modelled here.
8
+ */
9
+
10
+ /* Asset resource in JSON:API format */
11
+ export interface R2Asset {
12
+ /* Resource type */
13
+ type: 'asset'
14
+
15
+ /* Unique asset identifier (uuid) */
16
+ id: string
17
+
18
+ attributes: R2AssetAttributes
19
+
20
+ links?: {
21
+ /* URL to this asset */
22
+ self?: string
23
+
24
+ /* Public URL for accessing the asset */
25
+ public?: string
26
+ }
27
+ }
28
+
29
+ /* Asset attributes */
30
+ export interface R2AssetAttributes {
31
+ /* Original filename */
32
+ filename: string
33
+
34
+ /* Display name for the asset (can be different from filename) */
35
+ name?: string
36
+
37
+ /* File size in bytes */
38
+ size_bytes: number
39
+
40
+ /* MIME type of the file */
41
+ content_type?: string
42
+
43
+ /* Detected file format (e.g. "jpeg", "png") */
44
+ format?: string
45
+
46
+ /* Image width in pixels (for images) */
47
+ width?: number
48
+
49
+ /* Image height in pixels (for images) */
50
+ height?: number
51
+
52
+ /* When the asset was uploaded (date-time) */
53
+ created_at: string
54
+
55
+ /* Title for the asset */
56
+ title?: string
57
+
58
+ /* Alt text for accessibility */
59
+ alt?: string
60
+
61
+ /* Source attribution */
62
+ source?: string
63
+
64
+ /* Copyright information */
65
+ copyright?: string
66
+
67
+ /* Focus point for image cropping */
68
+ focus?: string
69
+
70
+ /* Whether the asset is soft-deleted */
71
+ deleted?: boolean
72
+
73
+ /* When the asset was deleted, if applicable (date-time) */
74
+ deleted_at?: string | null
75
+ }
76
+
77
+ /* Folder resource in JSON:API format */
78
+ export interface R2Folder {
79
+ /* Resource type */
80
+ type: 'folder'
81
+
82
+ /* Unique folder identifier (uuid) */
83
+ id: string
84
+
85
+ attributes: R2FolderAttributes
86
+ }
87
+
88
+ /* Folder attributes */
89
+ export interface R2FolderAttributes {
90
+ /* Folder name */
91
+ name: string
92
+
93
+ /* UUID of parent folder (null for root folders) */
94
+ parent_id?: string | null
95
+
96
+ /* Number of assets in this folder */
97
+ asset_count: number
98
+ }
99
+
100
+ /* Structured folder tree format with unlimited nesting */
101
+ export interface R2FolderTree {
102
+ /* Folder name */
103
+ name: string
104
+
105
+ /* Folder UUID */
106
+ id: string
107
+
108
+ /* UUID of parent folder (null for root folders) */
109
+ parent_id?: string | null
110
+
111
+ /* Number of assets in this folder */
112
+ asset_count: number
113
+
114
+ /* Child folders (recursive) */
115
+ children: Array<R2FolderTree>
116
+ }
117
+
118
+ /* Request/response shapes for the R2 operations the plugin calls */
119
+ export namespace Paths {
120
+ export namespace ListAssets {
121
+ export namespace Responses {
122
+ export interface $200 {
123
+ data?: Array<R2Asset>
124
+ included?: Array<R2Folder>
125
+ meta?: {
126
+ page?: number
127
+ limit?: number
128
+ total?: number
129
+ total_pages?: number
130
+ }
131
+ links?: {
132
+ self?: string
133
+ next?: string
134
+ prev?: string
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ export namespace UpdateAssetsBulk {
141
+ export interface RequestBody {
142
+ /* Array of asset UUIDs to update */
143
+ assets: Array<string>
144
+ metadata: {
145
+ name?: string
146
+ alt?: string
147
+ title?: string
148
+ source?: string
149
+ copyright?: string
150
+ focus?: string
151
+ /* UUID of the folder to move the assets to (null for no folder) */
152
+ folder_id?: string | null
153
+ }
154
+ }
155
+
156
+ export namespace Responses {
157
+ export type $204 = Record<string, never>
158
+ }
159
+ }
160
+
161
+ export namespace UpdateAssetMetadata {
162
+ export interface RequestBody {
163
+ name?: string
164
+ alt?: string
165
+ title?: string
166
+ source?: string
167
+ copyright?: string
168
+ focus?: string
169
+ /* UUID of the folder to move the asset to (null for no folder) */
170
+ folder_id?: string | null
171
+ }
172
+
173
+ export namespace Responses {
174
+ export interface $200 {
175
+ data?: R2Asset
176
+ }
177
+ }
178
+ }
179
+
180
+ export namespace CreateFolder {
181
+ export interface RequestBody {
182
+ /* Folder name */
183
+ name: string
184
+
185
+ /* UUID of parent folder (null for root folders) */
186
+ parent_id?: string | null
187
+ }
188
+
189
+ export namespace Responses {
190
+ export interface $201 {
191
+ data?: R2Folder
192
+ meta?: {
193
+ /* Indicates the folder was created */
194
+ created?: boolean
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ export namespace ListFolders {
201
+ export namespace Responses {
202
+ export interface $200 {
203
+ data?: {
204
+ /* JSON:API format folder list */
205
+ list?: Array<R2Folder>
206
+
207
+ /* Structured tree format for easier frontend consumption */
208
+ structured?: Array<R2FolderTree>
209
+ }
210
+ meta?: {
211
+ /* Total number of folders */
212
+ total?: number
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
@@ -1,4 +1,4 @@
1
- import type { R2Asset, R2AssetAttributes } from 'moxyloco/r2'
1
+ import type { R2Asset, R2AssetAttributes } from './r2.js'
2
2
 
3
3
  interface StoryblokAsset {
4
4
  /* The numeric ID */
@@ -0,0 +1,4 @@
1
+ export interface HubspotForm {
2
+ id: string
3
+ name: string
4
+ }
@@ -1,7 +1,70 @@
1
- import type Mux from '@mux/mux-node'
1
+ /**
2
+ * Vendored subset of the Mux `Asset` type (previously from `@mux/mux-node`).
3
+ * Kept self-contained so consumers don't need the Mux SDK installed just to read
4
+ * these types. Only the fields the plugin actually touches are modelled — the real
5
+ * SDK `Asset` remains structurally assignable to this.
6
+ */
7
+ export interface MuxPlaybackID {
8
+ /* Unique identifier for the PlaybackID */
9
+ id: string
10
+
11
+ /* How the playback ID is secured */
12
+ policy: 'public' | 'signed' | 'drm'
13
+
14
+ /* DRM configuration used by this playback ID (only when `policy` is `drm`) */
15
+ drm_configuration_id?: string
16
+ }
17
+
18
+ export interface MuxAssetErrors {
19
+ /* Error messages with more details */
20
+ messages?: Array<string>
21
+
22
+ /* The type of error that occurred for this asset */
23
+ type?: string
24
+ }
25
+
26
+ export interface MuxAssetMeta {
27
+ /* Identifier for the creator of the asset */
28
+ creator_id?: string
29
+
30
+ /* Identifier linking the asset to your own data */
31
+ external_id?: string
32
+
33
+ /* The asset title */
34
+ title?: string
35
+ }
36
+
37
+ export interface MuxAsset {
38
+ /* Unique identifier for the Asset */
39
+ id: string
40
+
41
+ /* Time the Asset was created (Unix timestamp, seconds since epoch) */
42
+ created_at: string
43
+
44
+ /* The status of the asset */
45
+ status: 'preparing' | 'ready' | 'errored'
46
+
47
+ /* Object describing any errors that happened when processing this asset */
48
+ errors?: MuxAssetErrors
49
+
50
+ /* The aspect ratio of the asset in the form `width:height` (e.g. `16:9`) */
51
+ aspect_ratio?: string
52
+
53
+ /* The duration of the asset in seconds */
54
+ duration?: number
55
+
56
+ /* Playback ID objects used to build HLS playback URLs */
57
+ playback_ids?: Array<MuxPlaybackID>
58
+
59
+ /* Customer provided metadata about this asset */
60
+ meta?: MuxAssetMeta
61
+
62
+ /* Arbitrary passthrough value included in the asset details and webhooks */
63
+ passthrough?: string
64
+ }
2
65
 
3
66
  export interface Video {
4
- mux_video?: Mux.Video.Assets.Asset
67
+ mux_video?: MuxAsset
5
68
  playback_id?: string
6
69
  m3u8_url?: string
7
70
  title?: string
@@ -18,5 +18,4 @@
18
18
  className
19
19
  )}
20
20
  bind:value
21
- {...restProps}
22
- ></textarea>
21
+ {...restProps}></textarea>
@@ -54,7 +54,7 @@
54
54
 
55
55
  [data-theme='dark'] {
56
56
  --teal-muted: oklch(74.591% 0.11569 187.182);
57
- --background: oklch(32.303% 0.00881 285.843);
57
+ --background: #333338;
58
58
  --foreground: oklch(0.984 0.003 247.858);
59
59
  --card: oklch(26.239% 0.00928 285.704);
60
60
  --card-foreground: oklch(0.984 0.003 247.858);
@@ -1 +1,9 @@
1
+ import type { Handle } from "@sveltejs/kit";
1
2
  export { handle_redirects as redirects } from "./redirects/handle.js";
3
+ /**
4
+ * Put the Storyblok content version on `event.locals.version` — draft while developing and
5
+ * inside the visual editor (`?_storyblok`), published everywhere else.
6
+ *
7
+ * Declare it on `App.Locals` as `version: import("storyloco").Version`.
8
+ */
9
+ export declare const version: Handle;
@@ -1 +1,12 @@
1
+ import { dev } from "$app/environment";
1
2
  export { handle_redirects as redirects } from "./redirects/handle.js";
3
+ /**
4
+ * Put the Storyblok content version on `event.locals.version` — draft while developing and
5
+ * inside the visual editor (`?_storyblok`), published everywhere else.
6
+ *
7
+ * Declare it on `App.Locals` as `version: import("storyloco").Version`.
8
+ */
9
+ export const version = async ({ event, resolve }) => {
10
+ event.locals.version = dev || event.url.searchParams.has("_storyblok") ? "draft" : "published";
11
+ return await resolve(event);
12
+ };
@@ -4,9 +4,33 @@ import path from "node:path";
4
4
  import { $ } from "../shared/shell.js";
5
5
  import exec from "../shared/x.js";
6
6
  import { Logger } from "../shared/logger.js";
7
- import { format } from "oxfmt";
8
7
  const name = "vite-storyblok-schema";
9
8
  const logger = new Logger(name);
9
+ /**
10
+ * Formats generated code as a nicety, not a necessity. Prefers oxfmt if it's
11
+ * installed, falls back to prettier if that happens to be around, and otherwise
12
+ * leaves the code untouched. Never throws — formatting is entirely optional and
13
+ * these tools are intentionally not hard dependencies.
14
+ */
15
+ async function format_code(filename, content) {
16
+ try {
17
+ const oxfmt = await import("oxfmt");
18
+ const { code } = await oxfmt.format(filename, content);
19
+ return code;
20
+ }
21
+ catch {
22
+ /* oxfmt not available — try the next option */
23
+ }
24
+ try {
25
+ const specifier = "prettier";
26
+ const prettier = await import(specifier);
27
+ return await prettier.format(content, { filepath: filename });
28
+ }
29
+ catch {
30
+ /* prettier not available either — leave the code as-is */
31
+ }
32
+ return content;
33
+ }
10
34
  /**
11
35
  * @remarks
12
36
  * Vite plugin that automatically regenerates Storyblok component types at regular intervals
@@ -176,7 +200,7 @@ import type { ISbStoryData } from '@storyblok/js';`, "");
176
200
  content = content.replace(/any\[]/g, "unknown[]");
177
201
  // Make _uid and component optional (helps us reuse Storyblok types for non-storyblok components)
178
202
  content = content.replace(/^([a-zA-Z0-9_]+):/gm, "$1?:");
179
- const { code: formatted } = await format(path.basename(schema_ts_file), content);
203
+ const formatted = await format_code(path.basename(schema_ts_file), content);
180
204
  // Write the final file
181
205
  logger.start("Finalising schema file...");
182
206
  await writeFile(schema_ts_file, formatted);
@@ -0,0 +1,51 @@
1
+ import type { ISbStoriesParams, ISbStoryData, ISbStoryParams } from "@storyblok/svelte";
2
+ import type { ISbLink, ISbLinksParams } from "storyblok-js-client";
3
+ import { type StoryblokClient, type Version } from "./storyblok.svelte.js";
4
+ /** The content version for the current request, as set by the `version` hook. */
5
+ export declare function version(): Version;
6
+ /**
7
+ * Bind a client to the current request.
8
+ *
9
+ * Returns a callable: call it for the client and version to reach anything the methods
10
+ * don't cover, or use `.story` / `.stories` for the common reads. Those carry the
11
+ * request's version and the resolve options a Storyblok read almost always wants; the
12
+ * caller's params are spread last, so every default can be overridden. `resolve_links`
13
+ * is on because `href` depends on it.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // $lib/storyblok.server.ts
18
+ * export const storyblok = setup(client)
19
+ *
20
+ * // anywhere on the server
21
+ * await storyblok.story("layout")
22
+ * await storyblok.stories<Blog>({ content_type: "blog", per_page: 2 })
23
+ *
24
+ * const { client, version } = storyblok()
25
+ * await client.get("cdn/links", { version })
26
+ * ```
27
+ */
28
+ export declare function setup(client: StoryblokClient): (() => {
29
+ client: StoryblokClient;
30
+ version: Version;
31
+ }) & {
32
+ /**
33
+ * A single story by full slug. Resolves the client's `relations`, unlike `stories`.
34
+ *
35
+ * `T` is the story's content type; it defaults to the SDK's permissive one, so an
36
+ * unparameterised call behaves as before.
37
+ */
38
+ story<T = import("storyblok-js-client").ISbComponentType<string> & {
39
+ [index: string]: any;
40
+ }>(slug: string, params?: ISbStoryParams): Promise<ISbStoryData<T>>;
41
+ /**
42
+ * The link tree as a flat array — Storyblok returns it keyed by uuid, which is
43
+ * almost never what you want. Filtering and ordering are left to the caller.
44
+ */
45
+ 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
+ };