svelte-crumbs 1.0.2 → 1.2.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
@@ -14,7 +14,29 @@ Automatic, SSR-ready breadcrumbs for SvelteKit via route-level metadata exports.
14
14
  npm install svelte-crumbs
15
15
  ```
16
16
 
17
- ### 2. Export breadcrumbs from your routes
17
+ ### 2. Enable experimental async
18
+
19
+ This library relies on Svelte's experimental `async` compiler option for top-level `await` in components. This is **required**.
20
+
21
+ ```js
22
+ // svelte.config.js
23
+ const config = {
24
+ kit: {
25
+ experimental: {
26
+ remoteFunctions: true
27
+ }
28
+ },
29
+ compilerOptions: {
30
+ experimental: {
31
+ async: true
32
+ }
33
+ }
34
+ };
35
+ ```
36
+
37
+ > To also use [remote functions](https://svelte.dev/docs/kit/remote-functions) in your breadcrumb resolvers, add `kit.experimental.remoteFunctions: true` as well.
38
+
39
+ ### 3. Export breadcrumbs from your routes
18
40
 
19
41
  ```svelte
20
42
  <!-- src/routes/products/+page.svelte -->
@@ -27,7 +49,7 @@ npm install svelte-crumbs
27
49
  </script>
28
50
  ```
29
51
 
30
- ### 3. Render in your layout
52
+ ### 4. Render in your layout
31
53
 
32
54
  ```svelte
33
55
  <!-- src/routes/+layout.svelte -->
@@ -218,30 +240,10 @@ type Breadcrumb = BreadcrumbData & { url: string };
218
240
 
219
241
  ## Requirements
220
242
 
243
+ - **Svelte 5** with `compilerOptions.experimental.async: true` — uses `$derived(await ...)` for reactive, SSR-safe breadcrumbs
221
244
  - **SvelteKit 2** — relies on `$app/state` and `import.meta.glob`
222
- - **Svelte 5** — uses runes (`$derived`)
223
245
  - Route groups (`(group)`) are stripped from paths
224
246
 
225
- ### Optional: enable async and remote functions
226
-
227
- The library works without any experimental flags — you can use load functions or resolve breadcrumbs manually. However, to unlock top-level `await` in components and remote function support, enable these flags:
228
-
229
- ```js
230
- // svelte.config.js
231
- const config = {
232
- compilerOptions: {
233
- experimental: {
234
- async: true // top-level await in components
235
- }
236
- },
237
- kit: {
238
- experimental: {
239
- remoteFunctions: true // call server functions from breadcrumb resolvers
240
- }
241
- }
242
- };
243
- ```
244
-
245
247
  ## License
246
248
 
247
249
  MIT
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import { fly } from 'svelte/transition';
3
+ import { flip } from 'svelte/animate';
4
+ import { onMount } from 'svelte';
5
+ import type { Breadcrumb } from '../types.js';
6
+
7
+ let { crumbs }: { crumbs: Breadcrumb[] } = $props();
8
+
9
+ let mounted = $state(false);
10
+ onMount(() => {
11
+ mounted = true;
12
+ });
13
+
14
+ // eslint-disable-next-line svelte/prefer-writable-derived -- intentionally stale: out-transitions need the previous count
15
+ let prevCount = $state(crumbs.length);
16
+ $effect(() => {
17
+ prevCount = crumbs.length;
18
+ });
19
+
20
+ function flyIn(node: Element, params: { y?: number; duration?: number; i?: number }) {
21
+ if (!mounted) return { duration: 0 };
22
+ const { i: index = 0, ...flyParams } = params;
23
+ return fly(node, { ...flyParams, delay: index * 60 });
24
+ }
25
+
26
+ function flyOut(node: Element, params: { y?: number; duration?: number; i?: number }) {
27
+ if (!mounted) return { duration: 0 };
28
+ const { i: index = 0, ...flyParams } = params;
29
+ const reverseIndex = prevCount - 1 - index;
30
+ return fly(node, { ...flyParams, delay: Math.max(0, reverseIndex) * 60 });
31
+ }
32
+ </script>
33
+
34
+ <nav
35
+ aria-label="Breadcrumbs"
36
+ class="flex items-center gap-2 rounded-lg bg-(--color-code-bg) px-4 py-3 text-sm"
37
+ >
38
+ {#each crumbs as crumb, i (crumb.url)}
39
+ <span
40
+ class="inline-flex items-center gap-2"
41
+ in:flyIn={{ y: -10, duration: 200, i }}
42
+ out:flyOut={{ y: 10, duration: 200, i }}
43
+ animate:flip={{ duration: 200 }}
44
+ >
45
+ {#if i > 0}
46
+ <span aria-hidden="true" class="text-(--color-text-muted)">▶︎</span>
47
+ {/if}
48
+ {#if crumb.icon}
49
+ {@const Icon = crumb.icon}
50
+ <Icon />
51
+ {/if}
52
+ {#if i < crumbs.length - 1 || crumbs.length === 1}
53
+ <a href={crumb.url} class="text-(--color-text-secondary) hover:text-(--color-text-primary) hover:underline"
54
+ >{crumb.label}</a
55
+ >
56
+ {:else}
57
+ <span class="text-(--color-text-primary)" aria-current="page">{crumb.label}</span>
58
+ {/if}
59
+ </span>
60
+ {/each}
61
+ </nav>
@@ -0,0 +1,7 @@
1
+ import type { Breadcrumb } from '../types.js';
2
+ type $$ComponentProps = {
3
+ crumbs: Breadcrumb[];
4
+ };
5
+ declare const Breadcrumbs: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
7
+ export default Breadcrumbs;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { codeToHtml } from 'shiki';
3
+ import { getTheme } from '../stores/theme.svelte.js';
4
+
5
+ let { code, lang = 'ts', raw = false }: { code: string; lang?: string; raw?: boolean } = $props();
6
+
7
+ const theme = $derived(getTheme());
8
+
9
+ function wrap(inner: string): string {
10
+ const indented = inner
11
+ .split('\n')
12
+ .map((line) => '\t' + line)
13
+ .join('\n');
14
+ return '<' + 'script module lang="ts">\n' + indented + '\n</' + 'script>';
15
+ }
16
+
17
+ const html = $derived(
18
+ await codeToHtml(raw ? code : wrap(code), {
19
+ lang,
20
+ theme: theme === 'dark' ? 'github-dark' : 'github-light'
21
+ })
22
+ );
23
+ </script>
24
+
25
+ <div class="mt-4 overflow-x-auto rounded-lg border border-(--color-border) text-sm [&_pre]:p-4">
26
+ {@html html}
27
+ </div>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ code: string;
3
+ lang?: string;
4
+ raw?: boolean;
5
+ };
6
+ declare const CodeBlock: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type CodeBlock = ReturnType<typeof CodeBlock>;
8
+ export default CodeBlock;
@@ -0,0 +1,3 @@
1
+ <footer class="mt-12 border-t border-(--color-border) py-6 text-center text-sm text-(--color-text-muted)">
2
+ MIT &copy; {new Date().getFullYear()} svelte-crumbs (use this however you like)
3
+ </footer>
@@ -0,0 +1,26 @@
1
+ export default Footer;
2
+ type Footer = SvelteComponent<{
3
+ [x: string]: never;
4
+ }, {
5
+ [evt: string]: CustomEvent<any>;
6
+ }, {}> & {
7
+ $$bindings?: string | undefined;
8
+ };
9
+ declare const Footer: $$__sveltets_2_IsomorphicComponent<{
10
+ [x: string]: never;
11
+ }, {
12
+ [evt: string]: CustomEvent<any>;
13
+ }, {}, {}, string>;
14
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
+ new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
16
+ $$bindings?: Bindings;
17
+ } & Exports;
18
+ (internal: unknown, props: {
19
+ $$events?: Events;
20
+ $$slots?: Slots;
21
+ }): Exports & {
22
+ $set?: any;
23
+ $on?: any;
24
+ };
25
+ z_$$bindings?: Bindings;
26
+ }
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { navigation } from '../config/navigation.js';
4
+
5
+ let { open = false, onClose }: { open?: boolean; onClose: () => void } = $props();
6
+
7
+ function isActive(href: string): boolean {
8
+ return page.url.pathname === href;
9
+ }
10
+ </script>
11
+
12
+ <!-- Mobile overlay -->
13
+ {#if open}
14
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
15
+ <div
16
+ class="fixed inset-0 z-40 bg-black/50 lg:hidden"
17
+ onclick={onClose}
18
+ onkeydown={(e) => e.key === 'Escape' && onClose()}
19
+ ></div>
20
+ {/if}
21
+
22
+ <aside
23
+ class="fixed top-14 bottom-0 z-50 w-64 overflow-y-auto border-r border-(--color-border) bg-(--color-bg-sidebar) p-4 transition-transform duration-200 lg:z-30 lg:translate-x-0"
24
+ class:max-lg:-translate-x-full={!open}
25
+ class:max-lg:translate-x-0={open}
26
+ >
27
+ <nav>
28
+ {#each navigation as section}
29
+ <div class="mb-6">
30
+ <h4 class="mb-2 text-xs font-semibold tracking-wider text-(--color-text-muted) uppercase">
31
+ {section.title}
32
+ </h4>
33
+ <ul class="space-y-1">
34
+ {#each section.items as item}
35
+ <li>
36
+ <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
37
+ <a
38
+ href={item.href}
39
+ onclick={onClose}
40
+ class="block rounded-md px-3 py-1.5 text-sm transition-colors {isActive(item.href)
41
+ ? 'bg-(--color-accent)/10 font-medium text-(--color-accent)'
42
+ : 'text-(--color-text-secondary) hover:bg-(--color-code-bg) hover:text-(--color-text-primary)'}"
43
+ >
44
+ {item.label}
45
+ </a>
46
+ </li>
47
+ {/each}
48
+ </ul>
49
+ </div>
50
+ {/each}
51
+ </nav>
52
+ </aside>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ open?: boolean;
3
+ onClose: () => void;
4
+ };
5
+ declare const Sidebar: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type Sidebar = ReturnType<typeof Sidebar>;
7
+ export default Sidebar;
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { toggleTheme, getTheme } from '../stores/theme.svelte.js';
3
+
4
+ const appVersion: string = __APP_VERSION__;
5
+
6
+ let { onToggleSidebar }: { onToggleSidebar: () => void } = $props();
7
+
8
+ const theme = $derived(getTheme());
9
+ </script>
10
+
11
+ <header
12
+ class="fixed top-0 right-0 left-0 z-40 flex h-14 items-center justify-between border-b border-(--color-border) bg-(--color-bg-nav) px-4"
13
+ >
14
+ <div class="flex items-center gap-3">
15
+ <button
16
+ class="lg:hidden rounded-md p-1.5 text-(--color-text-secondary) hover:text-(--color-text-primary) hover:bg-(--color-code-bg)"
17
+ onclick={onToggleSidebar}
18
+ aria-label="Toggle sidebar"
19
+ >
20
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21
+ <line x1="3" y1="6" x2="21" y2="6" />
22
+ <line x1="3" y1="12" x2="21" y2="12" />
23
+ <line x1="3" y1="18" x2="21" y2="18" />
24
+ </svg>
25
+ </button>
26
+ <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
27
+ <a href="/" class="text-lg font-semibold text-(--color-text-primary)">svelte-crumbs</a>
28
+ <span class="rounded-full bg-(--color-code-bg) px-2 py-0.5 text-xs text-(--color-text-muted)">v{appVersion}</span>
29
+ </div>
30
+
31
+ <div class="flex items-center gap-2">
32
+ <button
33
+ class="rounded-md p-1.5 text-(--color-text-secondary) hover:text-(--color-text-primary) hover:bg-(--color-code-bg)"
34
+ onclick={toggleTheme}
35
+ aria-label="Toggle theme"
36
+ >
37
+ {#if theme === 'dark'}
38
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
39
+ <circle cx="12" cy="12" r="5" />
40
+ <line x1="12" y1="1" x2="12" y2="3" />
41
+ <line x1="12" y1="21" x2="12" y2="23" />
42
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
43
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
44
+ <line x1="1" y1="12" x2="3" y2="12" />
45
+ <line x1="21" y1="12" x2="23" y2="12" />
46
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
47
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
48
+ </svg>
49
+ {:else}
50
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
51
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
52
+ </svg>
53
+ {/if}
54
+ </button>
55
+ <a
56
+ href="https://github.com/torrfura/svelte-crumbs"
57
+ target="_blank"
58
+ rel="noopener noreferrer"
59
+ class="rounded-md p-1.5 text-(--color-text-secondary) hover:text-(--color-text-primary) hover:bg-(--color-code-bg)"
60
+ aria-label="GitHub"
61
+ >
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
63
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
64
+ </svg>
65
+ </a>
66
+ </div>
67
+ </header>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ onToggleSidebar: () => void;
3
+ };
4
+ declare const TopNav: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type TopNav = ReturnType<typeof TopNav>;
6
+ export default TopNav;
@@ -0,0 +1,9 @@
1
+ export interface NavItem {
2
+ label: string;
3
+ href: string;
4
+ }
5
+ export interface NavSection {
6
+ title: string;
7
+ items: NavItem[];
8
+ }
9
+ export declare const navigation: NavSection[];
@@ -0,0 +1,29 @@
1
+ export const navigation = [
2
+ {
3
+ title: 'Getting Started',
4
+ items: [
5
+ { label: 'Introduction', href: '/' },
6
+ { label: 'Installation', href: '/docs/getting-started' },
7
+ { label: 'API Reference', href: '/docs/api-reference' }
8
+ ]
9
+ },
10
+ {
11
+ title: 'Patterns',
12
+ items: [
13
+ { label: 'Static Label', href: '/products' },
14
+ { label: 'Dynamic from Load Data', href: '/products/42' },
15
+ { label: 'Nested Static', href: '/products/42/edit' },
16
+ { label: 'Remote Function', href: '/docs/getting-started' },
17
+ { label: 'Optimistic Update', href: '/playground' },
18
+ { label: 'No Breadcrumb', href: '/about' }
19
+ ]
20
+ },
21
+ {
22
+ title: 'Examples',
23
+ items: [
24
+ { label: 'Products', href: '/products' },
25
+ { label: 'Documentation', href: '/docs' },
26
+ { label: 'Reactive Updates', href: '/playground' }
27
+ ]
28
+ }
29
+ ];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { createBreadcrumbs } from './breadcrumbs/create-breadcrumbs.svelte.js';
2
+ export { default as Breadcrumbs } from './components/breadcrumbs.svelte';
2
3
  export { buildBreadcrumbMap } from './routing/build-breadcrumb-map.js';
3
4
  export { filePathToRoute, matchDynamicRoute } from './routing/match-route.js';
4
5
  export { getResolversForRoute } from './routing/get-resolvers-for-route.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { createBreadcrumbs } from './breadcrumbs/create-breadcrumbs.svelte.js';
2
+ export { default as Breadcrumbs } from './components/breadcrumbs.svelte';
2
3
  export { buildBreadcrumbMap } from './routing/build-breadcrumb-map.js';
3
4
  export { filePathToRoute, matchDynamicRoute } from './routing/match-route.js';
4
5
  export { getResolversForRoute } from './routing/get-resolvers-for-route.js';
@@ -0,0 +1,6 @@
1
+ type Theme = 'light' | 'dark';
2
+ export declare function getTheme(): Theme;
3
+ export declare function setTheme(value: Theme): void;
4
+ export declare function toggleTheme(): void;
5
+ export declare function initTheme(): void;
6
+ export {};
@@ -0,0 +1,21 @@
1
+ let theme = $state('light');
2
+ export function getTheme() {
3
+ return theme;
4
+ }
5
+ export function setTheme(value) {
6
+ theme = value;
7
+ if (typeof document !== 'undefined') {
8
+ document.documentElement.classList.toggle('dark', value === 'dark');
9
+ localStorage.setItem('theme', value);
10
+ }
11
+ }
12
+ export function toggleTheme() {
13
+ setTheme(theme === 'light' ? 'dark' : 'light');
14
+ }
15
+ export function initTheme() {
16
+ if (typeof document === 'undefined')
17
+ return;
18
+ const stored = localStorage.getItem('theme');
19
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
20
+ setTheme(stored ?? (prefersDark ? 'dark' : 'light'));
21
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-crumbs",
3
- "version": "1.0.2",
4
- "description": "Automatic breadcrumbs for SvelteKit via route-level metadata exports",
3
+ "version": "1.2.0",
4
+ "description": "Automatic, SSR-ready breadcrumbs for SvelteKit zero config, async resolvers, remote function support",
5
5
  "license": "MIT",
6
6
  "homepage": "https://svelte-crumbs.vercel.app",
7
7
  "repository": {
@@ -46,7 +46,6 @@
46
46
  "svelte": "^5.0.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@changesets/cli": "^2.29.8",
50
49
  "@eslint/compat": "^2.0.2",
51
50
  "@eslint/js": "^9.39.3",
52
51
  "@sveltejs/adapter-auto": "^7.0.1",
@@ -76,8 +75,12 @@
76
75
  },
77
76
  "keywords": [
78
77
  "svelte",
78
+ "svelte5",
79
79
  "sveltekit",
80
80
  "breadcrumbs",
81
- "navigation"
81
+ "navigation",
82
+ "ssr",
83
+ "async",
84
+ "remote-functions"
82
85
  ]
83
86
  }