@rokkit/ui 1.0.0-next.98 → 1.0.1

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.
Files changed (81) hide show
  1. package/README.md +159 -1
  2. package/package.json +52 -53
  3. package/src/MarkdownRenderer.svelte +106 -0
  4. package/src/components/AlertList.svelte +59 -0
  5. package/src/components/Avatar.svelte +68 -0
  6. package/src/components/Badge.svelte +57 -0
  7. package/src/components/BreadCrumbs.svelte +94 -0
  8. package/src/components/Button.svelte +91 -0
  9. package/src/components/ButtonGroup.svelte +9 -0
  10. package/src/components/Card.svelte +71 -0
  11. package/src/components/Carousel.svelte +169 -0
  12. package/src/components/Code.svelte +189 -0
  13. package/src/components/Connector.svelte +46 -0
  14. package/src/components/Divider.svelte +24 -0
  15. package/src/components/Dropdown.svelte +202 -0
  16. package/src/components/FloatingAction.svelte +367 -0
  17. package/src/components/FloatingNavigation.svelte +256 -0
  18. package/src/components/Grid.svelte +128 -0
  19. package/src/components/ItemContent.svelte +25 -0
  20. package/src/components/LazyTree.svelte +171 -0
  21. package/src/components/List.svelte +224 -0
  22. package/src/components/Menu.svelte +270 -0
  23. package/src/components/Message.svelte +75 -0
  24. package/src/components/MultiSelect.svelte +367 -0
  25. package/src/components/PaletteManager.svelte +360 -0
  26. package/src/components/Pill.svelte +83 -0
  27. package/src/components/ProgressBar.svelte +31 -0
  28. package/src/components/Range.svelte +346 -0
  29. package/src/components/Rating.svelte +115 -0
  30. package/src/components/Reveal.svelte +46 -0
  31. package/src/components/SearchFilter.svelte +88 -0
  32. package/src/components/Select.svelte +424 -0
  33. package/src/components/Shine.svelte +87 -0
  34. package/src/components/Stack.svelte +38 -0
  35. package/src/components/StatusList.svelte +18 -0
  36. package/src/components/Stepper.svelte +174 -0
  37. package/src/components/Swatch.svelte +102 -0
  38. package/src/components/Switch.svelte +71 -0
  39. package/src/components/Table.svelte +245 -0
  40. package/src/components/Tabs.svelte +202 -0
  41. package/src/components/Tilt.svelte +68 -0
  42. package/src/components/Timeline.svelte +61 -0
  43. package/src/components/Toggle.svelte +93 -0
  44. package/src/components/Toolbar.svelte +306 -0
  45. package/src/components/ToolbarGroup.svelte +17 -0
  46. package/src/components/Tooltip.svelte +121 -0
  47. package/src/components/Tree.svelte +143 -0
  48. package/src/components/UploadFileStatus.svelte +102 -0
  49. package/src/components/UploadProgress.svelte +127 -0
  50. package/src/components/UploadTarget.svelte +126 -0
  51. package/src/components/index.ts +48 -0
  52. package/src/index.ts +59 -0
  53. package/src/markdown-plugin.ts +12 -0
  54. package/src/types/button.ts +86 -0
  55. package/src/types/code.ts +46 -0
  56. package/src/types/floating-action.ts +123 -0
  57. package/src/types/floating-navigation.ts +80 -0
  58. package/src/types/index.ts +58 -0
  59. package/src/types/list.ts +200 -0
  60. package/src/types/menu.ts +95 -0
  61. package/src/types/palette.ts +160 -0
  62. package/src/types/range.ts +51 -0
  63. package/src/types/search-filter.ts +67 -0
  64. package/src/types/select.ts +176 -0
  65. package/src/types/switch.ts +68 -0
  66. package/src/types/table.ts +219 -0
  67. package/src/types/tabs.ts +103 -0
  68. package/src/types/timeline.ts +53 -0
  69. package/src/types/toggle.ts +68 -0
  70. package/src/types/toolbar.ts +167 -0
  71. package/src/types/tooltip.ts +21 -0
  72. package/src/types/tree.ts +250 -0
  73. package/src/types/upload-file-status.ts +45 -0
  74. package/src/types/upload-progress.ts +111 -0
  75. package/src/types/upload-target.ts +71 -0
  76. package/src/utils/palette.ts +618 -0
  77. package/src/utils/shiki.ts +114 -0
  78. package/src/utils/upload.js +145 -0
  79. package/LICENSE +0 -21
  80. package/src/index.js +0 -8
  81. package/src/wrappers.js +0 -2
package/README.md CHANGED
@@ -1,3 +1,161 @@
1
1
  # @rokkit/ui
2
2
 
3
- Makes all ui components available from atoms/molecules/organisms/templates/pages in one place.
3
+ Data-driven UI components for Svelte 5 applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rokkit/ui
9
+ # or
10
+ bun add @rokkit/ui
11
+ ```
12
+
13
+ Requires `svelte ^5.0.0` as a peer dependency.
14
+
15
+ ## Overview
16
+
17
+ `@rokkit/ui` provides 38 components covering forms, dropdowns, lists, navigation, data display, layout, and file upload. Components follow a data-first model: they adapt to your data structures via field mapping rather than requiring data to be reshaped. All components are unstyled by default — they expose `data-*` attribute hooks for theming via `@rokkit/themes`. Keyboard navigation and ARIA accessibility are built in.
18
+
19
+ ## Usage
20
+
21
+ ### List
22
+
23
+ ```svelte
24
+ <script>
25
+ import { List } from '@rokkit/ui'
26
+
27
+ const items = ['Apple', 'Banana', 'Cherry']
28
+ let selected = $state(null)
29
+ </script>
30
+
31
+ <List {items} bind:value={selected} onselect={(val) => console.log(val)} />
32
+ ```
33
+
34
+ ### Select with field mapping
35
+
36
+ When your data keys differ from the expected defaults, use the `fields` prop:
37
+
38
+ ```svelte
39
+ <script>
40
+ import { Select } from '@rokkit/ui'
41
+
42
+ const countries = [
43
+ { name: 'United States', code: 'us' },
44
+ { name: 'Germany', code: 'de' }
45
+ ]
46
+ let chosen = $state(null)
47
+ </script>
48
+
49
+ <Select options={countries} fields={{ label: 'name', value: 'code' }} bind:value={chosen} />
50
+ ```
51
+
52
+ ### Tabs with bind:value
53
+
54
+ ```svelte
55
+ <script>
56
+ import { Tabs } from '@rokkit/ui'
57
+
58
+ const tabs = [
59
+ { label: 'Overview', value: 'overview' },
60
+ { label: 'Settings', value: 'settings' }
61
+ ]
62
+ let activeTab = $state('overview')
63
+ </script>
64
+
65
+ <Tabs items={tabs} bind:value={activeTab}>
66
+ {#snippet tabPanel(tab)}
67
+ <div>Content for {tab.label}</div>
68
+ {/snippet}
69
+ </Tabs>
70
+ ```
71
+
72
+ ### Button variants
73
+
74
+ ```svelte
75
+ <script>
76
+ import { Button } from '@rokkit/ui'
77
+ </script>
78
+
79
+ <Button variant="primary" onclick={() => save()}>Save</Button>
80
+ <Button variant="default" onclick={() => cancel()}>Cancel</Button>
81
+ <Button href="/docs">Documentation</Button>
82
+ ```
83
+
84
+ ### Menu
85
+
86
+ ```svelte
87
+ <script>
88
+ import { Menu } from '@rokkit/ui'
89
+
90
+ const items = [
91
+ { text: 'Copy', value: 'copy' },
92
+ { text: 'Paste', value: 'paste' },
93
+ { text: 'Delete', value: 'delete', disabled: true }
94
+ ]
95
+ </script>
96
+
97
+ <Menu options={items} label="Actions" onselect={(value) => handleAction(value)} />
98
+ ```
99
+
100
+ ## Components
101
+
102
+ | Category | Components |
103
+ | ----------------- | -------------------------------------------------------------- |
104
+ | Form / Input | Button, ButtonGroup, Toggle, Switch, Range, SearchFilter, Tabs |
105
+ | Dropdown | Menu, Select, MultiSelect, Toolbar, ToolbarGroup |
106
+ | List / Navigation | List, Tree, LazyTree, BreadCrumbs |
107
+ | Layout | Card, Grid, Carousel, ProgressBar, Timeline |
108
+ | Data Display | Table, Rating, Pill, Connector, Stepper |
109
+ | Visual | Reveal, Tilt, Shine, Code, ItemContent |
110
+ | Advanced | PaletteManager, FloatingAction, FloatingNavigation |
111
+ | Upload | UploadTarget, UploadFileStatus, UploadProgress |
112
+
113
+ ## API
114
+
115
+ ### Standard props
116
+
117
+ Most components share a consistent interface:
118
+
119
+ | Prop | Description |
120
+ | ----------------------- | ----------------------------------------------------------------------- |
121
+ | `items` / `options` | Array of data items |
122
+ | `value` | Bindable selected value |
123
+ | `fields` | Field mapping — maps component-expected keys to your data's actual keys |
124
+ | `onchange` / `onselect` | Selection callback |
125
+
126
+ ### Field mapping
127
+
128
+ The `fields` prop lets any data structure work with any component without reshaping:
129
+
130
+ ```js
131
+ // Your data has 'name' and 'id' — map them to what the component expects
132
+ const fields = { label: 'name', value: 'id' }
133
+ ```
134
+
135
+ ### Snippet customization
136
+
137
+ Components accept Svelte 5 snippets for rendering overrides. This lets you customize presentation without forking the component:
138
+
139
+ ```svelte
140
+ <List {items}>
141
+ {#snippet itemContent(item)}
142
+ <span class="tag">{item.label}</span>
143
+ {/snippet}
144
+ </List>
145
+ ```
146
+
147
+ ## Exports
148
+
149
+ ```js
150
+ import { List, Select, Menu, Button /* ... */ } from '@rokkit/ui'
151
+ import type { ListProps, SelectFields } from '@rokkit/ui/types'
152
+ import { generatePalette } from '@rokkit/ui/utils/palette'
153
+ ```
154
+
155
+ ## Theming
156
+
157
+ Components use `data-*` attribute selectors for styling (e.g., `[data-list-item]`, `[data-button]`). Apply styles via `@rokkit/themes` or write your own CSS targeting these hooks.
158
+
159
+ ---
160
+
161
+ Part of [Rokkit](https://github.com/jerrythomas/rokkit) — a Svelte 5 component library and design system.
package/package.json CHANGED
@@ -1,63 +1,62 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.0.0-next.98",
4
- "description": "All UI components",
5
- "author": "Jerry Thomas <me@jerrythomas.name>",
6
- "license": "MIT",
7
- "main": "index.js",
8
- "module": "src/index.js",
9
- "types": "dist/index.d.ts",
10
- "type": "module",
11
- "publishConfig": {
12
- "access": "public"
13
- },
14
- "devDependencies": {
15
- "@sveltejs/vite-plugin-svelte": "^3.1.2",
16
- "@testing-library/svelte": "^5.2.1",
17
- "@vitest/coverage-v8": "^1.6.0",
18
- "@vitest/ui": "~1.6.0",
19
- "jsdom": "^24.1.3",
20
- "svelte": "^4.2.19",
21
- "typescript": "^5.6.2",
22
- "vite": "^5.4.6",
23
- "vitest": "~1.6.0",
24
- "shared-config": "1.0.0-next.98",
25
- "validators": "1.0.0-next.98"
3
+ "version": "1.0.1",
4
+ "description": "Data driven UI components for Rokkit applications",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/jerrythomas/rokkit.git"
26
8
  },
27
- "files": [
28
- "src/**/*.js",
29
- "src/**/*.svelte",
30
- "!src/mocks",
31
- "!src/**/*.spec.js"
32
- ],
9
+ "type": "module",
10
+ "svelte": "./src/index.ts",
11
+ "types": "./src/index.ts",
33
12
  "exports": {
34
- "./src": "./src",
35
- "./wrappers": "./src/wrappers/index.js",
36
- "./package.json": "./package.json",
37
13
  ".": {
38
- "types": "./dist/index.d.ts",
39
- "import": "./src/index.js",
40
- "svelte": "./src/index.js"
14
+ "types": "./src/index.ts",
15
+ "svelte": "./src/index.ts",
16
+ "default": "./src/index.ts"
17
+ },
18
+ "./types": {
19
+ "types": "./src/types/index.ts",
20
+ "default": "./src/types/index.ts"
21
+ },
22
+ "./utils/palette": {
23
+ "types": "./src/utils/palette.ts",
24
+ "default": "./src/utils/palette.ts"
41
25
  }
42
26
  },
27
+ "files": [
28
+ "src",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "scripts": {
33
+ "prepublishOnly": "cp ../../LICENSE .",
34
+ "postpublish": "rm -f LICENSE",
35
+ "check": "svelte-check --tsconfig ./tsconfig.json",
36
+ "build": "echo 'No build step needed for source-only package'"
37
+ },
38
+ "keywords": [
39
+ "ui",
40
+ "components",
41
+ "svelte",
42
+ "menu",
43
+ "dropdown"
44
+ ],
43
45
  "dependencies": {
44
- "ramda": "^0.30.1",
45
- "@rokkit/actions": "1.0.0-next.98",
46
- "@rokkit/atoms": "1.0.0-next.98",
47
- "@rokkit/core": "1.0.0-next.98",
48
- "@rokkit/layout": "1.0.0-next.98",
49
- "@rokkit/molecules": "1.0.0-next.98",
50
- "@rokkit/organisms": "1.0.0-next.98"
46
+ "@rokkit/core": "1.0.1",
47
+ "@rokkit/data": "1.0.1",
48
+ "@rokkit/states": "1.0.1",
49
+ "@rokkit/actions": "1.0.1",
50
+ "marked": "^15.0.0",
51
+ "dompurify": "^3.0.0"
51
52
  },
52
- "scripts": {
53
- "format": "prettier --write .",
54
- "lint": "eslint --fix .",
55
- "test:ct": "playwright test -c playwright-ct.config.ts",
56
- "test:ci": "vitest run",
57
- "test:ui": "vitest --ui",
58
- "test": "vitest",
59
- "coverage": "vitest run --coverage",
60
- "latest": "pnpm upgrade --latest && pnpm test:ci",
61
- "release": "pnpm publish --access public"
53
+ "peerDependencies": {
54
+ "shiki": "^3.23.0",
55
+ "svelte": "^5.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "svelte": "^5.53.5",
59
+ "svelte-check": "^4.4.3",
60
+ "typescript": "^5.9.3"
62
61
  }
63
- }
62
+ }
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ import { marked } from 'marked'
3
+ import type { Token, TokensList } from 'marked'
4
+ import type { Component, Snippet } from 'svelte'
5
+ import type { MarkdownPlugin } from './markdown-plugin.js'
6
+ import DOMPurify from 'dompurify'
7
+
8
+ interface Props {
9
+ markdown: string
10
+ plugins?: MarkdownPlugin[]
11
+ /** Optional wrapper component (e.g. CrossFilter from @rokkit/chart) for grouping
12
+ * co-labelled plot blocks. When provided, plot code blocks whose JSON contains a
13
+ * `crossfilter` field with the same value are wrapped in a shared instance. */
14
+ crossfilterWrapper?: Component<{ children?: Snippet }>
15
+ }
16
+
17
+ let { markdown, plugins = [], crossfilterWrapper }: Props = $props()
18
+
19
+ const pluginMap = $derived(
20
+ Object.fromEntries(plugins.map((p) => [p.language.toLowerCase(), p.component]))
21
+ )
22
+
23
+ const tokens = $derived(marked.lexer(markdown))
24
+
25
+ function tokenToSafeHtml(token: Token): string {
26
+ const tokenList = Object.assign([token], {
27
+ links: (tokens as TokensList).links ?? {}
28
+ }) as TokensList
29
+ const raw = marked.parser(tokenList)
30
+ return DOMPurify.sanitize(raw)
31
+ }
32
+
33
+ /** Extract crossfilter group ID from a code token's text, or null if absent/invalid. */
34
+ function getCfGroup(token: Token): string | null {
35
+ if (token.type !== 'code') return null
36
+ const lang = (token.lang ?? '').toLowerCase()
37
+ if (!pluginMap[lang]) return null
38
+ try {
39
+ const spec = JSON.parse(token.text)
40
+ return typeof spec?.crossfilter === 'string' ? spec.crossfilter : null
41
+ } catch {
42
+ return null
43
+ }
44
+ }
45
+
46
+ /**
47
+ * When crossfilterWrapper is provided, pre-pass tokens into segments:
48
+ * - { type: 'group', id, tokens[] } — plot tokens sharing a crossfilter ID
49
+ * - { type: 'token', token } — all other tokens
50
+ */
51
+ type Segment =
52
+ | { type: 'group'; id: string; items: Token[] }
53
+ | { type: 'token'; token: Token }
54
+
55
+ const segments = $derived.by((): Segment[] => {
56
+ if (!crossfilterWrapper) return tokens.map((t) => ({ type: 'token', token: t }))
57
+
58
+ const result: Segment[] = []
59
+ const groupMap = new Map<string, { type: 'group'; id: string; items: Token[] }>()
60
+
61
+ for (const token of tokens) {
62
+ const cfId = getCfGroup(token)
63
+ if (cfId) {
64
+ let group = groupMap.get(cfId)
65
+ if (!group) {
66
+ group = { type: 'group', id: cfId, items: [] }
67
+ groupMap.set(cfId, group)
68
+ result.push(group)
69
+ }
70
+ group.items.push(token)
71
+ } else {
72
+ result.push({ type: 'token', token })
73
+ }
74
+ }
75
+ return result
76
+ })
77
+ </script>
78
+
79
+ <div class="markdown-renderer" data-markdown>
80
+ {#each segments as seg (seg.type === 'group' ? `cf-${ seg.id}` : segments.indexOf(seg))}
81
+ {#if seg.type === 'group'}
82
+ {@const Wrapper = crossfilterWrapper}
83
+ <Wrapper>
84
+ {#each seg.items as token, i (i)}
85
+ {@const Plugin = pluginMap[(token.lang ?? '').toLowerCase()]}
86
+ {#if Plugin}<Plugin code={token.text} />{/if}
87
+ {/each}
88
+ </Wrapper>
89
+ {:else}
90
+ {@const token = seg.token}
91
+ {#if token.type === 'code'}
92
+ {@const lang = (token.lang ?? '').toLowerCase()}
93
+ {@const Plugin = pluginMap[lang]}
94
+ {#if Plugin}
95
+ <Plugin code={token.text} />
96
+ {:else}
97
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
98
+ {@html tokenToSafeHtml(token)}
99
+ {/if}
100
+ {:else}
101
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
102
+ {@html tokenToSafeHtml(token)}
103
+ {/if}
104
+ {/if}
105
+ {/each}
106
+ </div>
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { SvelteSet } from 'svelte/reactivity'
4
+ import { alerts } from '@rokkit/states'
5
+ import Message from './Message.svelte'
6
+
7
+ interface AlertListProps {
8
+ /** Screen position for the toast stack */
9
+ position?:
10
+ | 'top-right'
11
+ | 'top-center'
12
+ | 'top-left'
13
+ | 'bottom-right'
14
+ | 'bottom-center'
15
+ | 'bottom-left'
16
+ /** Additional CSS class */
17
+ class?: string
18
+ }
19
+
20
+ const { position = 'top-right', class: className = '' }: AlertListProps = $props()
21
+
22
+ const dismissing = new SvelteSet<string>()
23
+
24
+ // Portal to document.body so position:fixed is relative to the viewport,
25
+ // not clipped by any overflow:auto ancestor (e.g. the docs main column).
26
+
27
+ function mountPortal(node: HTMLElement) {
28
+ document.body.appendChild(node)
29
+ return { destroy: () => node.remove() }
30
+ }
31
+
32
+ function startDismiss(id: string) {
33
+ dismissing.add(id)
34
+ }
35
+
36
+ function onTransitionEnd(id: string, e: TransitionEvent) {
37
+ if (e.propertyName === 'max-height' && dismissing.has(id)) {
38
+ dismissing.delete(id)
39
+ alerts.dismiss(id)
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <div use:mountPortal data-alert-list data-position={position} class={className || undefined}>
45
+ {#each alerts.current as alert (alert.id)}
46
+ <div
47
+ data-dismissing={dismissing.has(alert.id) || undefined}
48
+ ontransitionend={(e) => onTransitionEnd(alert.id, e)}
49
+ >
50
+ <Message
51
+ type={alert.type as 'error' | 'info' | 'success' | 'warning'}
52
+ text={alert.text}
53
+ dismissible={alert.dismissible}
54
+ actions={alert.actions as Snippet}
55
+ ondismiss={() => startDismiss(alert.id)}
56
+ />
57
+ </div>
58
+ {/each}
59
+ </div>
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ interface AvatarProps {
3
+ /** Image source URL */
4
+ src?: string
5
+ /** Alt text for the image */
6
+ alt?: string
7
+ /** Explicit initials to display as fallback */
8
+ initials?: string
9
+ /** Full name — auto-derives initials if initials prop not provided */
10
+ name?: string
11
+ /** Size variant */
12
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
13
+ /** Online presence status */
14
+ status?: 'online' | 'offline' | 'away' | 'busy'
15
+ /** Shape variant */
16
+ shape?: 'circle' | 'square'
17
+ /** Additional CSS class */
18
+ class?: string
19
+ }
20
+
21
+ const {
22
+ src,
23
+ alt,
24
+ initials,
25
+ name,
26
+ size = 'md',
27
+ status,
28
+ shape = 'circle',
29
+ class: className = ''
30
+ }: AvatarProps = $props()
31
+
32
+ /** Derive initials from name if explicit initials not provided */
33
+ const resolvedInitials = $derived.by(() => {
34
+ if (initials) return initials
35
+ if (!name) return '?'
36
+ const parts = name.trim().split(/\s+/)
37
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase()
38
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
39
+ })
40
+
41
+ const altText = $derived(alt ?? name ?? 'Avatar')
42
+
43
+ let imgError = $state(false)
44
+
45
+ function handleImgError() {
46
+ imgError = true
47
+ }
48
+ </script>
49
+
50
+ <div
51
+ data-avatar
52
+ data-size={size}
53
+ data-shape={shape}
54
+ data-status={status || undefined}
55
+ class={className || undefined}
56
+ role="img"
57
+ aria-label={altText}
58
+ >
59
+ {#if src && !imgError}
60
+ <img src={src} alt={altText} data-avatar-img onerror={handleImgError} />
61
+ {:else}
62
+ <span data-avatar-initials aria-hidden="true">{resolvedInitials}</span>
63
+ {/if}
64
+
65
+ {#if status}
66
+ <span data-avatar-status data-status={status} aria-label={status}></span>
67
+ {/if}
68
+ </div>
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+
4
+ interface BadgeProps {
5
+ /** Numeric count to display */
6
+ count?: number
7
+ /** Maximum count before showing "max+" (default: 99) */
8
+ max?: number
9
+ /** Visual variant */
10
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'error'
11
+ /** Show as a small dot without content */
12
+ dot?: boolean
13
+ /** Content to wrap — when provided, badge positions absolutely over the child */
14
+ children?: Snippet
15
+ /** Additional CSS class */
16
+ class?: string
17
+ }
18
+
19
+ const {
20
+ count,
21
+ max = 99,
22
+ variant = 'default',
23
+ dot = false,
24
+ children,
25
+ class: className = ''
26
+ }: BadgeProps = $props()
27
+
28
+ const displayText = $derived.by(() => {
29
+ if (dot) return undefined
30
+ if (count === undefined) return undefined
31
+ return count > max ? `${max}+` : String(count)
32
+ })
33
+ </script>
34
+
35
+ {#if children}
36
+ <div data-badge-wrapper class={className || undefined}>
37
+ {@render children()}
38
+ <span
39
+ data-badge
40
+ data-variant={variant}
41
+ data-dot={dot || undefined}
42
+ aria-label={displayText ? `${displayText} notifications` : undefined}
43
+ >
44
+ {#if displayText}{displayText}{/if}
45
+ </span>
46
+ </div>
47
+ {:else}
48
+ <span
49
+ data-badge
50
+ data-variant={variant}
51
+ data-dot={dot || undefined}
52
+ class={className || undefined}
53
+ aria-label={displayText ? `${displayText} notifications` : undefined}
54
+ >
55
+ {#if displayText}{displayText}{/if}
56
+ </span>
57
+ {/if}
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import { ProxyItem, messages } from '@rokkit/states'
4
+ import { DEFAULT_STATE_ICONS } from '@rokkit/core'
5
+
6
+ interface BreadCrumbsIcons {
7
+ separator?: string
8
+ }
9
+
10
+ interface BreadCrumbsProps {
11
+ /** Array of breadcrumb items */
12
+ items?: unknown[]
13
+ /** Custom field mappings */
14
+ fields?: Record<string, string>
15
+ /** Custom icons */
16
+ icons?: BreadCrumbsIcons
17
+ /** Callback when a breadcrumb is clicked */
18
+ onclick?: (value: unknown, item: unknown) => void
19
+ /** Custom snippet for rendering each crumb */
20
+ crumb?: Snippet<[ProxyItem, boolean]>
21
+ /** Additional CSS class */
22
+ class?: string
23
+ }
24
+
25
+ const {
26
+ items = [],
27
+ fields,
28
+ label = messages.breadcrumbs.label,
29
+ icons: userIcons = {} as BreadCrumbsIcons,
30
+ onclick,
31
+ crumb,
32
+ class: className = ''
33
+ }: BreadCrumbsProps & { label?: string } = $props()
34
+
35
+ const icons = $derived({ separator: DEFAULT_STATE_ICONS.navigate.right, ...userIcons })
36
+
37
+ function createProxy(item: unknown): ProxyItem {
38
+ return new ProxyItem(item, fields)
39
+ }
40
+
41
+ function handleClick(proxy: ProxyItem) {
42
+ onclick?.(proxy.value, proxy.original)
43
+ }
44
+ </script>
45
+
46
+ {#snippet defaultCrumb(proxy: ProxyItem, _isLast: boolean)}
47
+ {#if proxy.get('icon')}
48
+ <span data-breadcrumb-icon class={proxy.get('icon')} aria-hidden="true"></span>
49
+ {/if}
50
+ <span data-breadcrumb-label>{proxy.label}</span>
51
+ {/snippet}
52
+
53
+ <nav data-breadcrumbs class={className || undefined} aria-label={label}>
54
+ <ol data-breadcrumb-list>
55
+ {#each items as item, index (index)}
56
+ {@const proxy = createProxy(item)}
57
+ {@const isLast = index === items.length - 1}
58
+
59
+ {#if index > 0}
60
+ <li data-breadcrumb-separator aria-hidden="true">
61
+ <span class={icons.separator}></span>
62
+ </li>
63
+ {/if}
64
+
65
+ <li data-breadcrumb-item data-current={isLast || undefined}>
66
+ {#if isLast}
67
+ <span data-breadcrumb-current aria-current="page">
68
+ {#if crumb}
69
+ {@render crumb(proxy, isLast)}
70
+ {:else}
71
+ {@render defaultCrumb(proxy, isLast)}
72
+ {/if}
73
+ </span>
74
+ {:else if proxy.get('href')}
75
+ <a href={proxy.get('href')} data-breadcrumb-link onclick={() => handleClick(proxy)}>
76
+ {#if crumb}
77
+ {@render crumb(proxy, isLast)}
78
+ {:else}
79
+ {@render defaultCrumb(proxy, isLast)}
80
+ {/if}
81
+ </a>
82
+ {:else}
83
+ <button type="button" data-breadcrumb-link onclick={() => handleClick(proxy)}>
84
+ {#if crumb}
85
+ {@render crumb(proxy, isLast)}
86
+ {:else}
87
+ {@render defaultCrumb(proxy, isLast)}
88
+ {/if}
89
+ </button>
90
+ {/if}
91
+ </li>
92
+ {/each}
93
+ </ol>
94
+ </nav>