@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.
- package/README.md +159 -1
- package/package.json +52 -53
- package/src/MarkdownRenderer.svelte +106 -0
- package/src/components/AlertList.svelte +59 -0
- package/src/components/Avatar.svelte +68 -0
- package/src/components/Badge.svelte +57 -0
- package/src/components/BreadCrumbs.svelte +94 -0
- package/src/components/Button.svelte +91 -0
- package/src/components/ButtonGroup.svelte +9 -0
- package/src/components/Card.svelte +71 -0
- package/src/components/Carousel.svelte +169 -0
- package/src/components/Code.svelte +189 -0
- package/src/components/Connector.svelte +46 -0
- package/src/components/Divider.svelte +24 -0
- package/src/components/Dropdown.svelte +202 -0
- package/src/components/FloatingAction.svelte +367 -0
- package/src/components/FloatingNavigation.svelte +256 -0
- package/src/components/Grid.svelte +128 -0
- package/src/components/ItemContent.svelte +25 -0
- package/src/components/LazyTree.svelte +171 -0
- package/src/components/List.svelte +224 -0
- package/src/components/Menu.svelte +270 -0
- package/src/components/Message.svelte +75 -0
- package/src/components/MultiSelect.svelte +367 -0
- package/src/components/PaletteManager.svelte +360 -0
- package/src/components/Pill.svelte +83 -0
- package/src/components/ProgressBar.svelte +31 -0
- package/src/components/Range.svelte +346 -0
- package/src/components/Rating.svelte +115 -0
- package/src/components/Reveal.svelte +46 -0
- package/src/components/SearchFilter.svelte +88 -0
- package/src/components/Select.svelte +424 -0
- package/src/components/Shine.svelte +87 -0
- package/src/components/Stack.svelte +38 -0
- package/src/components/StatusList.svelte +18 -0
- package/src/components/Stepper.svelte +174 -0
- package/src/components/Swatch.svelte +102 -0
- package/src/components/Switch.svelte +71 -0
- package/src/components/Table.svelte +245 -0
- package/src/components/Tabs.svelte +202 -0
- package/src/components/Tilt.svelte +68 -0
- package/src/components/Timeline.svelte +61 -0
- package/src/components/Toggle.svelte +93 -0
- package/src/components/Toolbar.svelte +306 -0
- package/src/components/ToolbarGroup.svelte +17 -0
- package/src/components/Tooltip.svelte +121 -0
- package/src/components/Tree.svelte +143 -0
- package/src/components/UploadFileStatus.svelte +102 -0
- package/src/components/UploadProgress.svelte +127 -0
- package/src/components/UploadTarget.svelte +126 -0
- package/src/components/index.ts +48 -0
- package/src/index.ts +59 -0
- package/src/markdown-plugin.ts +12 -0
- package/src/types/button.ts +86 -0
- package/src/types/code.ts +46 -0
- package/src/types/floating-action.ts +123 -0
- package/src/types/floating-navigation.ts +80 -0
- package/src/types/index.ts +58 -0
- package/src/types/list.ts +200 -0
- package/src/types/menu.ts +95 -0
- package/src/types/palette.ts +160 -0
- package/src/types/range.ts +51 -0
- package/src/types/search-filter.ts +67 -0
- package/src/types/select.ts +176 -0
- package/src/types/switch.ts +68 -0
- package/src/types/table.ts +219 -0
- package/src/types/tabs.ts +103 -0
- package/src/types/timeline.ts +53 -0
- package/src/types/toggle.ts +68 -0
- package/src/types/toolbar.ts +167 -0
- package/src/types/tooltip.ts +21 -0
- package/src/types/tree.ts +250 -0
- package/src/types/upload-file-status.ts +45 -0
- package/src/types/upload-progress.ts +111 -0
- package/src/types/upload-target.ts +71 -0
- package/src/utils/palette.ts +618 -0
- package/src/utils/shiki.ts +114 -0
- package/src/utils/upload.js +145 -0
- package/LICENSE +0 -21
- package/src/index.js +0 -8
- package/src/wrappers.js +0 -2
package/README.md
CHANGED
|
@@ -1,3 +1,161 @@
|
|
|
1
1
|
# @rokkit/ui
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
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": "./
|
|
39
|
-
"
|
|
40
|
-
"
|
|
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
|
-
"
|
|
45
|
-
"@rokkit/
|
|
46
|
-
"@rokkit/
|
|
47
|
-
"@rokkit/
|
|
48
|
-
"
|
|
49
|
-
"
|
|
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
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
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>
|