@rokkit/stories 1.0.0-next.134 → 1.0.0-next.136

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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @typedef {Object} Example
3
+ * @property {Object[]} files
4
+ * @property {import('svelte').SvelteComponent} App
5
+ */
6
+ export class StoryBuilder {
7
+ constructor(sources: any, modules: any);
8
+ get loading(): boolean;
9
+ get error(): null;
10
+ get examples(): Record<string, Example>;
11
+ get fragments(): Object[];
12
+ /**
13
+ * @param {string} name
14
+ * @returns {Example}
15
+ */
16
+ getExample(name: string): Example;
17
+ getFragment(index: any): Object;
18
+ hasExample(name: any): boolean;
19
+ hasFragment(index: any): boolean;
20
+ #private;
21
+ }
22
+ export type Example = {
23
+ files: Object[];
24
+ App: import("svelte").SvelteComponent;
25
+ };
@@ -0,0 +1,11 @@
1
+ export { default as Code } from "./Code.svelte";
2
+ export { default as CodeViewer } from "./CodeViewer.svelte";
3
+ export { default as CopyToClipboard } from "./CopyToClipboard.svelte";
4
+ export { default as Demo } from "./Demo.svelte";
5
+ export { default as StoryViewer } from "./StoryViewer.svelte";
6
+ export { default as StoryComponent } from "./StoryComponent.svelte";
7
+ export { default as StoryError } from "./StoryError.svelte";
8
+ export { default as StoryLoading } from "./StoryLoading.svelte";
9
+ export { StoryBuilder } from "./builder.svelte.js";
10
+ export { highlightCode, preloadHighlighter } from "./lib/shiki.js";
11
+ export { fetchImports, getSlug, getSections, getAllSections, groupFiles, fetchStories, findSection, findGroupForSection } from "./lib/stories.js";
@@ -36,9 +36,10 @@ export function fetchStories(sources: Object, modules: Object): Promise<{
36
36
  }>;
37
37
  /**
38
38
  * Get all individual sections flattened from groups
39
+ * @param {Metadata[]} sections - The sections to flatten
39
40
  * @returns {Array} Array of all tutorial sections
40
41
  */
41
- export function getAllSections(): any[];
42
+ export function getAllSections(sections: Metadata[]): any[];
42
43
  /**
43
44
  * Find a section by its ID
44
45
  * @param {string} slug - The section ID to find
@@ -50,7 +51,7 @@ export function findSection(sections: any, slug: string): Object | null;
50
51
  * @param {string} sectionId - The section ID to find the group for
51
52
  * @returns {Object|null} The group object or null if not found
52
53
  */
53
- export function findGroupForSection(sections: any, sectionId: string): Object | null;
54
+ export function findGroupForSection(sections: any, slug: any): Object | null;
54
55
  export type SourceFile = {
55
56
  /**
56
57
  * - The file path.
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@rokkit/stories",
3
- "version": "1.0.0-next.134",
3
+ "version": "1.0.0-next.136",
4
4
  "description": "Utilities for building tutorials.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/jerrythomas/rokkit.git"
8
+ },
5
9
  "publishConfig": {
6
10
  "access": "public"
7
11
  },
@@ -19,11 +23,13 @@
19
23
  "./package.json": "./package.json",
20
24
  ".": {
21
25
  "types": "./dist/index.d.ts",
26
+ "svelte": "./src/index.js",
22
27
  "import": "./src/index.js"
23
28
  }
24
29
  },
25
30
  "files": [
26
31
  "src/**/*.js",
32
+ "src/**/*.svelte",
27
33
  "dist/**/*.d.ts",
28
34
  "README.md",
29
35
  "LICENSE"
@@ -39,6 +45,11 @@
39
45
  "frontmatter": "^0.0.3",
40
46
  "ramda": "^0.32.0",
41
47
  "shiki": "^3.23.0",
42
- "@rokkit/core": "latest"
48
+ "@rokkit/core": "workspace:latest",
49
+ "@rokkit/states": "workspace:latest",
50
+ "@rokkit/ui": "workspace:latest"
51
+ },
52
+ "peerDependencies": {
53
+ "svelte": "^5.0.0"
43
54
  }
44
55
  }
@@ -0,0 +1,28 @@
1
+ <script>
2
+ import { highlightCode } from './lib/shiki.js'
3
+ import { vibe } from '@rokkit/states'
4
+ import CopyToClipboard from './CopyToClipboard.svelte'
5
+
6
+ let { content, language } = $props()
7
+ let theme = $derived(vibe.mode === 'dark' ? 'github-dark' : 'github-light')
8
+ let highlightedCode = $derived(
9
+ content ? highlightCode(content, { lang: language, theme }) : Promise.resolve('')
10
+ )
11
+ </script>
12
+
13
+ <div data-code-root>
14
+ <div data-code-overlay>
15
+ <CopyToClipboard {content} class="" />
16
+ </div>
17
+ {#if language}
18
+ <span data-code-lang>{language}</span>
19
+ {/if}
20
+ {#await highlightedCode}
21
+ <div class="text-surface-floating p-2">Highlighting code...</div>
22
+ {:then code}
23
+ <!-- eslint-disable svelte/no-at-html-tags -->
24
+ {@html code}
25
+ {:catch error}
26
+ <div class="p-2 text-red-500">Error highlighting code: {error.message}</div>
27
+ {/await}
28
+ </div>
@@ -0,0 +1,16 @@
1
+ <script>
2
+ import { Tabs } from '@rokkit/ui'
3
+ import Code from './Code.svelte'
4
+ let { files = [] } = $props()
5
+ let current = $state()
6
+ $effect(() => {
7
+ if (files.length > 0 && current === undefined) current = files[0]
8
+ })
9
+ let fields = { label: 'name', icon: 'language' }
10
+ </script>
11
+
12
+ <Tabs options={files} {fields} bind:value={current} class="no-padding">
13
+ {#snippet tabPanel(item)}
14
+ <Code content={item.content} language={item.language} />
15
+ {/snippet}
16
+ </Tabs>
@@ -0,0 +1,27 @@
1
+ <script>
2
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
3
+ import { Button } from '@rokkit/ui'
4
+ let { content, class: className = 'absolute right-2 top-2 z-10', title = 'Copy code' } = $props()
5
+ let copySuccess = $state(false)
6
+
7
+ async function copyToClipboard() {
8
+ try {
9
+ await navigator.clipboard.writeText(content || '')
10
+ copySuccess = true
11
+ setTimeout(() => {
12
+ copySuccess = false
13
+ }, 2000)
14
+ } catch (err) {
15
+ console.error('Failed to copy code:', err)
16
+ }
17
+ }
18
+ </script>
19
+
20
+ <Button
21
+ onclick={copyToClipboard}
22
+ class={className}
23
+ {title}
24
+ style="ghost"
25
+ size="sm"
26
+ icon={copySuccess ? DEFAULT_STATE_ICONS.action.copysuccess : DEFAULT_STATE_ICONS.action.copy}
27
+ />
@@ -0,0 +1,115 @@
1
+ <script>
2
+ import Code from './Code.svelte'
3
+
4
+ let { App, code, language = 'svelte' } = $props()
5
+ let view = $state('preview')
6
+ </script>
7
+
8
+ <div data-demo-root>
9
+ <div data-demo-toggle>
10
+ <button
11
+ data-demo-btn
12
+ aria-pressed={view === 'preview'}
13
+ onclick={() => (view = 'preview')}
14
+ title="Preview"
15
+ >
16
+ <span class="i-solar:eye-bold-duotone"></span>
17
+ </button>
18
+ <button
19
+ data-demo-btn
20
+ aria-pressed={view === 'code'}
21
+ onclick={() => (view = 'code')}
22
+ title="Code"
23
+ >
24
+ <span class="i-solar:code-square-bold-duotone"></span>
25
+ </button>
26
+ </div>
27
+
28
+ {#if view === 'preview'}
29
+ <div data-demo-preview>
30
+ {#if App}<App />{/if}
31
+ </div>
32
+ {:else}
33
+ <div data-demo-code>
34
+ <Code content={code} {language} />
35
+ </div>
36
+ {/if}
37
+ </div>
38
+
39
+ <style>
40
+ [data-demo-root] {
41
+ position: relative;
42
+ min-height: 14rem;
43
+ border-radius: 0.5rem;
44
+ border: 1px solid var(--color-surface);
45
+ overflow: hidden;
46
+ }
47
+
48
+ [data-demo-toggle] {
49
+ position: absolute;
50
+ top: 0.625rem;
51
+ right: 0.625rem;
52
+ z-index: 20;
53
+ display: flex;
54
+ gap: 2px;
55
+ background: color-mix(in srgb, var(--color-surface) 80%, transparent);
56
+ backdrop-filter: blur(6px);
57
+ border: 1px solid var(--color-surface);
58
+ border-radius: 0.375rem;
59
+ padding: 2px;
60
+ }
61
+
62
+ [data-demo-btn] {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ width: 1.75rem;
67
+ height: 1.75rem;
68
+ border-radius: 0.25rem;
69
+ border: none;
70
+ background: transparent;
71
+ color: var(--color-surface);
72
+ cursor: pointer;
73
+ font-size: 0.875rem;
74
+ transition:
75
+ background 120ms ease,
76
+ color 120ms ease;
77
+ }
78
+
79
+ [data-demo-btn][aria-pressed='true'] {
80
+ background: var(--color-surface);
81
+ color: var(--color-on-surface);
82
+ }
83
+
84
+ [data-demo-btn]:hover:not([aria-pressed='true']) {
85
+ background: var(--color-surface-z2);
86
+ color: var(--color-surface-z7);
87
+ }
88
+
89
+ [data-demo-preview] {
90
+ @apply bg-graph-paper;
91
+ --unit: 20px;
92
+ --size: var(--unit);
93
+ --minor-grid: 0.5px;
94
+ --major-grid: 0px;
95
+ --graph-paper-color: rgba(128, 128, 128, 0.22);
96
+
97
+ background-color: var(--color-surface-z0);
98
+ box-shadow: inset 0 2px 10px rgb(0 0 0 / 0.07);
99
+
100
+ min-height: 14rem;
101
+ padding: 2rem 1.5rem;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ }
106
+
107
+ [data-demo-code] {
108
+ min-height: 14rem;
109
+ }
110
+
111
+ [data-demo-code] :global([data-code-root]) {
112
+ border-radius: 0;
113
+ border: none;
114
+ }
115
+ </style>
@@ -0,0 +1,17 @@
1
+ <script>
2
+ let { component, class: className = '' } = $props()
3
+ </script>
4
+
5
+ <div class="story-component p-6 {className}">
6
+ {#if component}
7
+ {@const Component = component}
8
+ <Component />
9
+ {:else}
10
+ <div
11
+ class="text-surface-floating border-surface-300 rounded border border-dashed p-4 text-center"
12
+ >
13
+ <p>Component loading in progress...</p>
14
+ <p class="mt-1 text-sm">If this persists, check console for errors</p>
15
+ </div>
16
+ {/if}
17
+ </div>
@@ -0,0 +1,26 @@
1
+ <script>
2
+ let { error, class: className = '' } = $props()
3
+ </script>
4
+
5
+ <div
6
+ class="story-error rounded-lg border border-red-200 bg-red-50 p-6 dark:border-red-800 dark:bg-red-900/20 {className}"
7
+ >
8
+ <div class="flex items-start space-x-3">
9
+ <div class="flex-shrink-0">
10
+ <svg class="h-5 w-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
11
+ <path
12
+ stroke-linecap="round"
13
+ stroke-linejoin="round"
14
+ stroke-width="2"
15
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.314 18.5c-.77.833.192 2.5 1.732 2.5z"
16
+ ></path>
17
+ </svg>
18
+ </div>
19
+ <div class="flex-1">
20
+ <h3 class="text-sm font-medium text-red-900 dark:text-red-100">Story Loading Failed</h3>
21
+ <p class="mt-1 text-sm text-red-800 dark:text-red-200">
22
+ {error}
23
+ </p>
24
+ </div>
25
+ </div>
26
+ </div>
@@ -0,0 +1,24 @@
1
+ <script>
2
+ let { message = 'Loading story...', class: className = '' } = $props()
3
+ </script>
4
+
5
+ <div class="story-loading border-surface-z2 bg-surface-z2 rounded-lg border p-6 {className}">
6
+ <div class="flex items-center justify-center space-x-3">
7
+ <div class="animate-spin">
8
+ <svg
9
+ class="text-surface-floating h-5 w-5"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ viewBox="0 0 24 24"
13
+ >
14
+ <path
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ stroke-width="2"
18
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
19
+ ></path>
20
+ </svg>
21
+ </div>
22
+ <span class="text-surface-floating text-sm">{message}</span>
23
+ </div>
24
+ </div>
@@ -0,0 +1,51 @@
1
+ <script>
2
+ import CodeViewer from './CodeViewer.svelte'
3
+ import { Button } from '@rokkit/ui'
4
+
5
+ let { App, files } = $props()
6
+ let showCode = $state(false)
7
+ </script>
8
+
9
+ <div data-story-root>
10
+ <!-- Demo preview area with grid background -->
11
+ <div
12
+ data-story-preview
13
+ class="preview-area border-surface-z2 text-surface-z8 rounded-t-md border p-6"
14
+ >
15
+ {#if App}
16
+ <App />
17
+ {:else}
18
+ <div>No preview available</div>
19
+ {/if}
20
+ </div>
21
+
22
+ <!-- Toolbar: toggle code -->
23
+ <div
24
+ class="border-surface-z2 bg-surface-z1 flex items-center justify-between rounded-b-md border border-t-0 px-3 py-1"
25
+ >
26
+ <span class="text-surface-z4 text-xs">Example</span>
27
+ <Button
28
+ label={showCode ? 'Hide code' : 'Show code'}
29
+ icon={showCode ? 'i-solar:minimize-square-bold-duotone' : 'i-solar:code-square-bold-duotone'}
30
+ onclick={() => (showCode = !showCode)}
31
+ style="ghost"
32
+ size="sm"
33
+ />
34
+ </div>
35
+
36
+ {#if showCode}
37
+ <div class="mt-2">
38
+ <CodeViewer {files} />
39
+ </div>
40
+ {/if}
41
+ </div>
42
+
43
+ <style>
44
+ .preview-area {
45
+ background-color: var(--color-surface-z2, oklch(from var(--color-surface) calc(l + 0.02) c h));
46
+ background-image:
47
+ linear-gradient(rgb(var(--color-surface-200, 128 128 128) / 0.25) 1px, transparent 1px),
48
+ linear-gradient(90deg, rgb(var(--color-surface-200, 128 128 128) / 0.25) 1px, transparent 1px);
49
+ background-size: 20px 20px;
50
+ }
51
+ </style>
@@ -0,0 +1,68 @@
1
+ import { fetchStories } from './lib/stories.js'
2
+
3
+ /**
4
+ * @typedef {Object} Example
5
+ * @property {Object[]} files
6
+ * @property {import('svelte').SvelteComponent} App
7
+ */
8
+
9
+ export class StoryBuilder {
10
+ #modules
11
+ #sources
12
+ /** @type {Record<string, Example>} */
13
+ #processed = $state({})
14
+ #loading = $state(true)
15
+ #error = $state(null)
16
+
17
+ constructor(sources, modules) {
18
+ this.#modules = modules
19
+ this.#sources = sources
20
+ this.#init()
21
+ }
22
+
23
+ async #init() {
24
+ try {
25
+ this.#processed = await fetchStories(this.#sources, this.#modules)
26
+ this.#loading = false
27
+ } catch (err) {
28
+ this.#error = err
29
+ this.#loading = false
30
+ }
31
+ }
32
+
33
+ get loading() {
34
+ return this.#loading
35
+ }
36
+
37
+ get error() {
38
+ return this.#error
39
+ }
40
+
41
+ get examples() {
42
+ return this.#processed || {}
43
+ }
44
+
45
+ get fragments() {
46
+ return this.#processed?.fragments?.files || []
47
+ }
48
+
49
+ /**
50
+ * @param {string} name
51
+ * @returns {Example}
52
+ */
53
+ getExample(name) {
54
+ return this.#processed?.[name]
55
+ }
56
+
57
+ getFragment(index) {
58
+ return this.#processed?.fragments?.files?.[index]
59
+ }
60
+
61
+ hasExample(name) {
62
+ return Boolean(this.#processed?.[name])
63
+ }
64
+
65
+ hasFragment(index) {
66
+ return Boolean(this.#processed?.fragments?.files?.[index])
67
+ }
68
+ }
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ export { default as Code } from './Code.svelte'
2
+ export { default as CodeViewer } from './CodeViewer.svelte'
3
+ export { default as CopyToClipboard } from './CopyToClipboard.svelte'
4
+ export { default as Demo } from './Demo.svelte'
5
+ export { default as StoryViewer } from './StoryViewer.svelte'
6
+ export { default as StoryComponent } from './StoryComponent.svelte'
7
+ export { default as StoryError } from './StoryError.svelte'
8
+ export { default as StoryLoading } from './StoryLoading.svelte'
9
+ export { StoryBuilder } from './builder.svelte.js'
10
+ export { highlightCode, preloadHighlighter } from './lib/shiki.js'
11
+ export {
12
+ fetchImports,
13
+ getSlug,
14
+ getSections,
15
+ getAllSections,
16
+ groupFiles,
17
+ fetchStories,
18
+ findSection,
19
+ findGroupForSection
20
+ } from './lib/stories.js'
@@ -164,10 +164,11 @@ export async function fetchStories(sources, modules) {
164
164
  }
165
165
  /**
166
166
  * Get all individual sections flattened from groups
167
+ * @param {Metadata[]} sections - The sections to flatten
167
168
  * @returns {Array} Array of all tutorial sections
168
169
  */
169
- export function getAllSections() {
170
- return sections.flatMap((group) => group.children)
170
+ export function getAllSections(sections) {
171
+ return sections.flatMap((group) => group.children ?? [])
171
172
  }
172
173
 
173
174
  /**
@@ -176,8 +177,9 @@ export function getAllSections() {
176
177
  * @returns {Object|null} The section object or null if not found
177
178
  */
178
179
  export function findSection(sections, slug) {
179
- for (const group of sections) {
180
- const section = group.children.find((child) => child.slug === slug)
180
+ for (const item of sections) {
181
+ if (item.slug === slug) return item
182
+ const section = item.children?.find((child) => child.slug === slug)
181
183
  if (section) return section
182
184
  }
183
185
  return {}
@@ -188,6 +190,6 @@ export function findSection(sections, slug) {
188
190
  * @param {string} sectionId - The section ID to find the group for
189
191
  * @returns {Object|null} The group object or null if not found
190
192
  */
191
- export function findGroupForSection(sections, sectionId) {
192
- return sections.find((group) => group.children.some((child) => child.id === sectionId)) || null
193
+ export function findGroupForSection(sections, slug) {
194
+ return sections.find((group) => group.children?.some((child) => child.slug === slug)) || null
193
195
  }
@@ -356,47 +356,47 @@ describe('stories.js', () => {
356
356
  const mockSections = [
357
357
  {
358
358
  title: 'Welcome',
359
- id: 'welcome',
359
+ slug: '/welcome',
360
360
  children: [
361
- { title: 'Introduction', id: 'intro' },
362
- { title: 'Getting Started', id: 'get-started' }
361
+ { title: 'Introduction', slug: '/welcome/intro' },
362
+ { title: 'Getting Started', slug: '/welcome/get-started' }
363
363
  ]
364
364
  },
365
365
  {
366
366
  title: 'Elements',
367
- id: 'elements',
367
+ slug: '/elements',
368
368
  children: [
369
- { title: 'List', id: 'list' },
370
- { title: 'Button', id: 'button' }
369
+ { title: 'List', slug: '/elements/list' },
370
+ { title: 'Button', slug: '/elements/button' }
371
371
  ]
372
372
  }
373
373
  ]
374
374
 
375
375
  it('should find the group containing a section', () => {
376
- const result = findGroupForSection(mockSections, 'list')
376
+ const result = findGroupForSection(mockSections, '/elements/list')
377
377
  expect(result).toEqual({
378
378
  title: 'Elements',
379
- id: 'elements',
379
+ slug: '/elements',
380
380
  children: [
381
- { title: 'List', id: 'list' },
382
- { title: 'Button', id: 'button' }
381
+ { title: 'List', slug: '/elements/list' },
382
+ { title: 'Button', slug: '/elements/button' }
383
383
  ]
384
384
  })
385
385
  })
386
386
 
387
387
  it('should return null when section is not found', () => {
388
- const result = findGroupForSection(mockSections, 'nonexistent')
388
+ const result = findGroupForSection(mockSections, '/nonexistent')
389
389
  expect(result).toBeNull()
390
390
  })
391
391
 
392
392
  it('should find group for section in first group', () => {
393
- const result = findGroupForSection(mockSections, 'intro')
393
+ const result = findGroupForSection(mockSections, '/welcome/intro')
394
394
  expect(result).toEqual({
395
395
  title: 'Welcome',
396
- id: 'welcome',
396
+ slug: '/welcome',
397
397
  children: [
398
- { title: 'Introduction', id: 'intro' },
399
- { title: 'Getting Started', id: 'get-started' }
398
+ { title: 'Introduction', slug: '/welcome/intro' },
399
+ { title: 'Getting Started', slug: '/welcome/get-started' }
400
400
  ]
401
401
  })
402
402
  })
File without changes
File without changes