@rokkit/ui 1.0.0-next.151 → 1.0.0-next.153
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/package.json +2 -2
- package/src/MarkdownRenderer.svelte +102 -33
- package/src/components/AlertList.svelte +1 -1
- package/src/components/Avatar.svelte +68 -0
- package/src/components/Badge.svelte +57 -0
- package/src/components/BreadCrumbs.svelte +1 -1
- package/src/components/Card.svelte +17 -2
- package/src/components/Carousel.svelte +1 -1
- package/src/components/Code.svelte +1 -1
- package/src/components/Divider.svelte +24 -0
- package/src/components/Dropdown.svelte +2 -2
- package/src/components/FloatingNavigation.svelte +2 -2
- package/src/components/Grid.svelte +1 -1
- package/src/components/LazyTree.svelte +1 -1
- package/src/components/List.svelte +6 -5
- package/src/components/Menu.svelte +1 -1
- package/src/components/Message.svelte +1 -3
- package/src/components/MultiSelect.svelte +2 -2
- package/src/components/Range.svelte +1 -1
- package/src/components/Rating.svelte +1 -1
- package/src/components/SearchFilter.svelte +2 -2
- package/src/components/Select.svelte +2 -2
- package/src/components/Stack.svelte +38 -0
- package/src/components/Stepper.svelte +1 -1
- package/src/components/Swatch.svelte +1 -1
- package/src/components/Table.svelte +23 -10
- package/src/components/Tabs.svelte +2 -2
- package/src/components/Toggle.svelte +1 -1
- package/src/components/Toolbar.svelte +1 -1
- package/src/components/Tree.svelte +1 -1
- package/src/components/UploadFileStatus.svelte +1 -1
- package/src/components/UploadProgress.svelte +1 -1
- package/src/components/UploadTarget.svelte +1 -1
- package/src/components/index.ts +4 -0
- package/src/index.ts +5 -1
- package/src/markdown-plugin.ts +4 -4
- package/src/types/table.ts +10 -1
- package/src/types/toggle.ts +1 -1
- package/src/types/upload-file-status.ts +1 -1
- package/src/types/upload-progress.ts +1 -1
- package/src/types/upload-target.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/ui",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.153",
|
|
4
4
|
"description": "Data driven UI components for Rokkit applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@rokkit/states": "latest",
|
|
49
49
|
"@rokkit/actions": "latest",
|
|
50
50
|
"marked": "^15.0.0",
|
|
51
|
-
"
|
|
51
|
+
"dompurify": "^3.0.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"shiki": "^3.23.0",
|
|
@@ -1,43 +1,112 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
interface Props {
|
|
8
|
+
markdown: string
|
|
9
|
+
plugins?: MarkdownPlugin[]
|
|
10
|
+
/** Optional wrapper component (e.g. CrossFilter from @rokkit/chart) for grouping
|
|
11
|
+
* co-labelled plot blocks. When provided, plot code blocks whose JSON contains a
|
|
12
|
+
* `crossfilter` field with the same value are wrapped in a shared instance. */
|
|
13
|
+
crossfilterWrapper?: Component<{ children?: Snippet }>
|
|
14
|
+
}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
let { markdown, plugins = [], crossfilterWrapper }: Props = $props()
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
let purify: { sanitize: (s: string) => string } | null = $state(null)
|
|
19
|
+
$effect(() => {
|
|
20
|
+
import('dompurify').then((m) => {
|
|
21
|
+
purify = m.default
|
|
22
|
+
})
|
|
23
|
+
})
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
const pluginMap = $derived(
|
|
26
|
+
Object.fromEntries(plugins.map((p) => [p.language.toLowerCase(), p.component]))
|
|
27
|
+
)
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
const tokens = $derived(marked.lexer(markdown))
|
|
30
|
+
|
|
31
|
+
function tokenToSafeHtml(token: Token): string {
|
|
32
|
+
const tokenList = Object.assign([token], {
|
|
33
|
+
links: (tokens as TokensList).links ?? {}
|
|
34
|
+
}) as TokensList
|
|
35
|
+
const raw = marked.parser(tokenList)
|
|
36
|
+
return purify ? purify.sanitize(raw) : raw
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Extract crossfilter group ID from a code token's text, or null if absent/invalid. */
|
|
40
|
+
function getCfGroup(token: Token): string | null {
|
|
41
|
+
if (token.type !== 'code') return null
|
|
42
|
+
const lang = (token.lang ?? '').toLowerCase()
|
|
43
|
+
if (!pluginMap[lang]) return null
|
|
44
|
+
try {
|
|
45
|
+
const spec = JSON.parse(token.text)
|
|
46
|
+
return typeof spec?.crossfilter === 'string' ? spec.crossfilter : null
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* When crossfilterWrapper is provided, pre-pass tokens into segments:
|
|
54
|
+
* - { type: 'group', id, tokens[] } — plot tokens sharing a crossfilter ID
|
|
55
|
+
* - { type: 'token', token } — all other tokens
|
|
56
|
+
*/
|
|
57
|
+
type Segment =
|
|
58
|
+
| { type: 'group'; id: string; items: Token[] }
|
|
59
|
+
| { type: 'token'; token: Token }
|
|
60
|
+
|
|
61
|
+
const segments = $derived.by((): Segment[] => {
|
|
62
|
+
if (!crossfilterWrapper) return tokens.map((t) => ({ type: 'token', token: t }))
|
|
63
|
+
|
|
64
|
+
const result: Segment[] = []
|
|
65
|
+
const groupMap = new Map<string, { type: 'group'; id: string; items: Token[] }>()
|
|
66
|
+
|
|
67
|
+
for (const token of tokens) {
|
|
68
|
+
const cfId = getCfGroup(token)
|
|
69
|
+
if (cfId) {
|
|
70
|
+
let group = groupMap.get(cfId)
|
|
71
|
+
if (!group) {
|
|
72
|
+
group = { type: 'group', id: cfId, items: [] }
|
|
73
|
+
groupMap.set(cfId, group)
|
|
74
|
+
result.push(group)
|
|
75
|
+
}
|
|
76
|
+
group.items.push(token)
|
|
77
|
+
} else {
|
|
78
|
+
result.push({ type: 'token', token })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result
|
|
82
|
+
})
|
|
25
83
|
</script>
|
|
26
84
|
|
|
27
85
|
<div class="markdown-renderer" data-markdown>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
86
|
+
{#each segments as seg (seg.type === 'group' ? `cf-${ seg.id}` : segments.indexOf(seg))}
|
|
87
|
+
{#if seg.type === 'group'}
|
|
88
|
+
{@const Wrapper = crossfilterWrapper}
|
|
89
|
+
<Wrapper>
|
|
90
|
+
{#each seg.items as token, i (i)}
|
|
91
|
+
{@const Plugin = pluginMap[(token.lang ?? '').toLowerCase()]}
|
|
92
|
+
{#if Plugin}<Plugin code={token.text} />{/if}
|
|
93
|
+
{/each}
|
|
94
|
+
</Wrapper>
|
|
95
|
+
{:else}
|
|
96
|
+
{@const token = seg.token}
|
|
97
|
+
{#if token.type === 'code'}
|
|
98
|
+
{@const lang = (token.lang ?? '').toLowerCase()}
|
|
99
|
+
{@const Plugin = pluginMap[lang]}
|
|
100
|
+
{#if Plugin}
|
|
101
|
+
<Plugin code={token.text} />
|
|
102
|
+
{:else}
|
|
103
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
104
|
+
{@html tokenToSafeHtml(token)}
|
|
105
|
+
{/if}
|
|
106
|
+
{:else}
|
|
107
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
108
|
+
{@html tokenToSafeHtml(token)}
|
|
109
|
+
{/if}
|
|
110
|
+
{/if}
|
|
111
|
+
{/each}
|
|
43
112
|
</div>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
// Portal to document.body so position:fixed is relative to the viewport,
|
|
25
25
|
// not clipped by any overflow:auto ancestor (e.g. the docs main column).
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
function mountPortal(node: HTMLElement) {
|
|
28
28
|
document.body.appendChild(node)
|
|
29
29
|
return { destroy: () => node.remove() }
|
|
@@ -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}
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
children?: Snippet
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
href,
|
|
23
|
+
onclick,
|
|
24
|
+
variant = 'default',
|
|
25
|
+
class: className = '',
|
|
26
|
+
header,
|
|
27
|
+
footer,
|
|
28
|
+
children
|
|
29
|
+
}: CardProps = $props()
|
|
22
30
|
</script>
|
|
23
31
|
|
|
24
32
|
{#snippet cardContent()}
|
|
@@ -46,7 +54,14 @@
|
|
|
46
54
|
{@render cardContent()}
|
|
47
55
|
</a>
|
|
48
56
|
{:else if onclick}
|
|
49
|
-
<button
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
data-card
|
|
60
|
+
data-card-interactive
|
|
61
|
+
data-variant={variant}
|
|
62
|
+
class={className || undefined}
|
|
63
|
+
{onclick}
|
|
64
|
+
>
|
|
50
65
|
{@render cardContent()}
|
|
51
66
|
</button>
|
|
52
67
|
{:else}
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
children
|
|
44
44
|
}: CarouselProps & { labels?: Record<string, string> } = $props()
|
|
45
45
|
|
|
46
|
-
const labels = $derived({ ...messages.
|
|
46
|
+
const labels = $derived({ ...messages.carousel, ...userLabels })
|
|
47
47
|
|
|
48
48
|
let hovered = $state(false)
|
|
49
49
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
class: className = ''
|
|
16
16
|
}: CodeProps & { labels?: Record<string, string> } = $props()
|
|
17
17
|
|
|
18
|
-
const labels = $derived({ ...messages.
|
|
18
|
+
const labels = $derived({ ...messages.code, ...userLabels })
|
|
19
19
|
|
|
20
20
|
// Merge icons with defaults
|
|
21
21
|
const icons = $derived<CodeStateIcons>({ ...defaultCodeStateIcons, ...userIcons })
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface DividerProps {
|
|
3
|
+
/** Orientation of the divider */
|
|
4
|
+
orientation?: 'horizontal' | 'vertical'
|
|
5
|
+
/** Optional label text displayed in the center */
|
|
6
|
+
label?: string
|
|
7
|
+
/** Additional CSS class */
|
|
8
|
+
class?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { orientation = 'horizontal', label, class: className = '' }: DividerProps = $props()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
data-divider
|
|
16
|
+
data-orientation={orientation}
|
|
17
|
+
class={className || undefined}
|
|
18
|
+
role="separator"
|
|
19
|
+
aria-orientation={orientation}
|
|
20
|
+
>
|
|
21
|
+
{#if label}
|
|
22
|
+
<span data-divider-label>{label}</span>
|
|
23
|
+
{/if}
|
|
24
|
+
</div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* data-direction — panel direction (down | up)
|
|
22
22
|
*/
|
|
23
23
|
// @ts-nocheck
|
|
24
|
-
import { ProxyTree, Wrapper } from '@rokkit/states'
|
|
24
|
+
import { ProxyTree, Wrapper, messages } from '@rokkit/states'
|
|
25
25
|
import { Navigator, Trigger } from '@rokkit/actions'
|
|
26
26
|
import { DEFAULT_STATE_ICONS } from '@rokkit/core'
|
|
27
27
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
items = [],
|
|
35
35
|
fields = {},
|
|
36
36
|
value = $bindable(),
|
|
37
|
-
placeholder =
|
|
37
|
+
placeholder = messages.select,
|
|
38
38
|
icon,
|
|
39
39
|
size = 'md',
|
|
40
40
|
disabled = false,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
observe = true,
|
|
17
17
|
observerOptions = { rootMargin: '-20% 0px -70% 0px', threshold: 0 },
|
|
18
18
|
size = 'md',
|
|
19
|
-
label = messages.
|
|
19
|
+
label = messages.floatingNav.label,
|
|
20
20
|
labels: userLabels = {},
|
|
21
21
|
onselect,
|
|
22
22
|
onpinchange,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
class: className = ''
|
|
25
25
|
}: FloatingNavigationProps & { labels?: Record<string, string> } = $props()
|
|
26
26
|
|
|
27
|
-
const labels = $derived({ ...messages.
|
|
27
|
+
const labels = $derived({ ...messages.floatingNav, ...userLabels })
|
|
28
28
|
|
|
29
29
|
const icons = $derived({
|
|
30
30
|
pin: DEFAULT_STATE_ICONS.action.pin,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
[key: string]: unknown
|
|
46
46
|
} = $props()
|
|
47
47
|
|
|
48
|
-
const labels = $derived({ ...messages.
|
|
48
|
+
const labels = $derived({ ...messages.tree, ...userLabels })
|
|
49
49
|
|
|
50
50
|
const icons = $derived({ ...DEFAULT_STATE_ICONS.folder, ...userIcons })
|
|
51
51
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
size = 'md',
|
|
46
46
|
disabled = false,
|
|
47
47
|
collapsible = false,
|
|
48
|
-
label = messages.
|
|
48
|
+
label = messages.list.label,
|
|
49
49
|
icons: userIcons = {} as ListIcons,
|
|
50
50
|
onselect,
|
|
51
51
|
class: className = '',
|
|
@@ -102,14 +102,15 @@
|
|
|
102
102
|
function expandAncestorGroups(activeKey: string | null) {
|
|
103
103
|
for (const [key, proxy] of wrapper.lookup) {
|
|
104
104
|
if (!proxy.hasChildren) continue
|
|
105
|
-
proxy.expanded =
|
|
106
|
-
activeKey !== null &&
|
|
107
|
-
(activeKey === key || activeKey.startsWith(`${key }-`))
|
|
105
|
+
proxy.expanded = activeKey !== null && (activeKey === key || activeKey.startsWith(`${key}-`))
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
function syncExpandedGroups() {
|
|
112
|
-
if (!collapsible) {
|
|
110
|
+
if (!collapsible) {
|
|
111
|
+
expandAllGroups()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
113
114
|
expandAncestorGroups(findActiveKey())
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
// @ts-nocheck
|
|
28
28
|
import type { ProxyItem } from '@rokkit/states'
|
|
29
|
-
import { Wrapper, ProxyTree } from '@rokkit/states'
|
|
29
|
+
import { Wrapper, ProxyTree, messages } from '@rokkit/states'
|
|
30
30
|
import { SvelteSet } from 'svelte/reactivity'
|
|
31
31
|
import { Navigator, Trigger } from '@rokkit/actions'
|
|
32
32
|
import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
fields = {},
|
|
45
45
|
value = $bindable<unknown[]>([]),
|
|
46
46
|
selected = $bindable<unknown[]>([]),
|
|
47
|
-
placeholder =
|
|
47
|
+
placeholder = messages.select,
|
|
48
48
|
size = 'md',
|
|
49
49
|
disabled = false,
|
|
50
50
|
maxDisplay = 3,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
class: className = ''
|
|
21
21
|
}: RangeProps & { labels?: Record<string, string> } = $props()
|
|
22
22
|
|
|
23
|
-
const labels = $derived({ ...messages.
|
|
23
|
+
const labels = $derived({ ...messages.range, ...userLabels })
|
|
24
24
|
|
|
25
25
|
// ─── Pixel state ────────────────────────────────────────────────
|
|
26
26
|
let trackWidth = $state(0)
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
}: SearchFilterProps & { labels?: Record<string, string> } = $props()
|
|
17
17
|
|
|
18
18
|
const labels = $derived({
|
|
19
|
-
clear: messages.
|
|
20
|
-
remove: messages.
|
|
19
|
+
clear: messages.search_.clear,
|
|
20
|
+
remove: messages.filter.remove,
|
|
21
21
|
...userLabels
|
|
22
22
|
})
|
|
23
23
|
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
// @ts-nocheck
|
|
41
41
|
import type { ProxyItem } from '@rokkit/states'
|
|
42
|
-
import { Wrapper, ProxyTree } from '@rokkit/states'
|
|
42
|
+
import { Wrapper, ProxyTree, messages } from '@rokkit/states'
|
|
43
43
|
import { SvelteSet } from 'svelte/reactivity'
|
|
44
44
|
import { Navigator, Trigger } from '@rokkit/actions'
|
|
45
45
|
import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
fields = {},
|
|
57
57
|
value = $bindable(),
|
|
58
58
|
selected = $bindable<unknown>(null),
|
|
59
|
-
placeholder =
|
|
59
|
+
placeholder = messages.select,
|
|
60
60
|
size = 'md',
|
|
61
61
|
disabled = false,
|
|
62
62
|
filterable = false,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface StackProps {
|
|
5
|
+
/** Layout direction */
|
|
6
|
+
direction?: 'vertical' | 'horizontal'
|
|
7
|
+
/** Gap size between children */
|
|
8
|
+
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
9
|
+
/** Cross-axis alignment */
|
|
10
|
+
align?: 'start' | 'center' | 'end' | 'stretch'
|
|
11
|
+
/** Main-axis justification */
|
|
12
|
+
justify?: 'start' | 'center' | 'end' | 'between' | 'around'
|
|
13
|
+
/** Stack children */
|
|
14
|
+
children: Snippet
|
|
15
|
+
/** Additional CSS class */
|
|
16
|
+
class?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
direction = 'vertical',
|
|
21
|
+
gap = 'md',
|
|
22
|
+
align,
|
|
23
|
+
justify,
|
|
24
|
+
children,
|
|
25
|
+
class: className = ''
|
|
26
|
+
}: StackProps = $props()
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
data-stack
|
|
31
|
+
data-direction={direction}
|
|
32
|
+
data-gap={gap}
|
|
33
|
+
data-align={align || undefined}
|
|
34
|
+
data-justify={justify || undefined}
|
|
35
|
+
class={className || undefined}
|
|
36
|
+
>
|
|
37
|
+
{@render children()}
|
|
38
|
+
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { TableProps, TableColumn, TableSortIcons } from '../types/table.js'
|
|
3
3
|
import { defaultTableSortIcons } from '../types/table.js'
|
|
4
|
-
import { TableController } from '@rokkit/states'
|
|
4
|
+
import { TableController, messages } from '@rokkit/states'
|
|
5
5
|
import { navigator } from '@rokkit/actions'
|
|
6
6
|
import { untrack } from 'svelte'
|
|
7
7
|
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
data = [],
|
|
10
10
|
columns: userColumns,
|
|
11
11
|
value,
|
|
12
|
+
values = $bindable<unknown[]>([]),
|
|
13
|
+
selectable = 'single' as 'single' | 'multi' | false,
|
|
12
14
|
caption,
|
|
13
15
|
size = 'md',
|
|
14
16
|
striped = false,
|
|
17
|
+
responsive = false,
|
|
15
18
|
disabled = false,
|
|
16
19
|
fields: userFields,
|
|
17
20
|
onselect,
|
|
@@ -33,9 +36,17 @@
|
|
|
33
36
|
new TableController(data, {
|
|
34
37
|
columns: userColumns,
|
|
35
38
|
fields: userFields,
|
|
36
|
-
value
|
|
39
|
+
value,
|
|
40
|
+
multiselect: selectable === 'multi'
|
|
37
41
|
})
|
|
38
42
|
)
|
|
43
|
+
|
|
44
|
+
// Sync values binding from controller selection in multi-select mode
|
|
45
|
+
$effect(() => {
|
|
46
|
+
if (selectable === 'multi') {
|
|
47
|
+
values = controller.selected.slice()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
39
50
|
let tableRef = $state<HTMLElement | null>(null)
|
|
40
51
|
|
|
41
52
|
// Sync data changes to controller
|
|
@@ -60,7 +71,7 @@
|
|
|
60
71
|
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
61
72
|
if (target && target !== document.activeElement) {
|
|
62
73
|
target.focus()
|
|
63
|
-
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
74
|
+
target.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -91,15 +102,15 @@
|
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
function handleSelectAction() {
|
|
105
|
+
if (selectable === false || disabled) return
|
|
106
|
+
|
|
94
107
|
const key = controller.focusedKey
|
|
95
108
|
if (!key) return
|
|
96
109
|
|
|
97
110
|
const proxy = controller.lookup.get(key)
|
|
98
111
|
if (!proxy) return
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
onselect?.(proxy.value, proxy.value as Record<string, unknown>)
|
|
102
|
-
}
|
|
113
|
+
onselect?.(proxy.value, proxy.value as Record<string, unknown>)
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
// ─── Sort ───────────────────────────────────────────────────────
|
|
@@ -136,12 +147,14 @@
|
|
|
136
147
|
bind:this={tableRef}
|
|
137
148
|
data-table
|
|
138
149
|
data-size={size}
|
|
150
|
+
data-selectable={selectable || undefined}
|
|
139
151
|
data-disabled={disabled || undefined}
|
|
152
|
+
data-table-responsive={responsive || undefined}
|
|
140
153
|
class={className || undefined}
|
|
141
154
|
onfocusin={handleFocusIn}
|
|
142
155
|
use:navigator={{ wrapper: controller, orientation: 'vertical' }}
|
|
143
156
|
>
|
|
144
|
-
<table role="grid" aria-label={caption} data-striped={striped || undefined}>
|
|
157
|
+
<table role="grid" aria-label={caption} data-table-striped={striped || undefined}>
|
|
145
158
|
{#if caption}
|
|
146
159
|
<caption data-table-caption>{caption}</caption>
|
|
147
160
|
{/if}
|
|
@@ -188,7 +201,7 @@
|
|
|
188
201
|
</tr>
|
|
189
202
|
{:else}
|
|
190
203
|
<tr data-table-empty-row>
|
|
191
|
-
<td data-table-empty colspan={controller.columns.length}>
|
|
204
|
+
<td data-table-empty colspan={controller.columns.length}>{messages.table.empty}</td>
|
|
192
205
|
</tr>
|
|
193
206
|
{/if}
|
|
194
207
|
{:else}
|
|
@@ -210,12 +223,12 @@
|
|
|
210
223
|
>
|
|
211
224
|
{#each controller.columns as column (column.name)}
|
|
212
225
|
{#if cellSnippet}
|
|
213
|
-
<td data-table-cell data-column={column.name} style:text-align={column.align}>
|
|
226
|
+
<td data-table-cell data-column={column.name} data-label={column.label ?? column.name} style:text-align={column.align}>
|
|
214
227
|
{@render cellSnippet(getCellValue(row, column), column, row)}
|
|
215
228
|
</td>
|
|
216
229
|
{:else}
|
|
217
230
|
{@const cellIcon = getCellIcon(row, column)}
|
|
218
|
-
<td data-table-cell data-column={column.name} style:text-align={column.align}>
|
|
231
|
+
<td data-table-cell data-column={column.name} data-label={column.label ?? column.name} style:text-align={column.align}>
|
|
219
232
|
{#if cellIcon}
|
|
220
233
|
<span data-cell-icon class={cellIcon} aria-hidden="true"></span>
|
|
221
234
|
{/if}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
align = 'start',
|
|
34
34
|
name = 'tabs',
|
|
35
35
|
editable = false,
|
|
36
|
-
placeholder =
|
|
36
|
+
placeholder = messages.tabs.placeholder,
|
|
37
37
|
disabled = false,
|
|
38
38
|
labels: userLabels = {},
|
|
39
39
|
class: className = '',
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
...snippets
|
|
45
45
|
}: TabsProps & { labels?: Record<string, string>; [key: string]: unknown } = $props()
|
|
46
46
|
|
|
47
|
-
const labels = $derived({ ...messages.
|
|
47
|
+
const labels = $derived({ ...messages.tabs, ...userLabels })
|
|
48
48
|
|
|
49
49
|
// ─── Wrapper ──────────────────────────────────────────────────────────────
|
|
50
50
|
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
[key: string]: unknown
|
|
44
44
|
} = $props()
|
|
45
45
|
|
|
46
|
-
const labels = $derived({ ...messages.
|
|
46
|
+
const labels = $derived({ ...messages.tree, ...userLabels })
|
|
47
47
|
|
|
48
48
|
const icons = $derived({ ...DEFAULT_STATE_ICONS.folder, ...userIcons })
|
|
49
49
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
icons: userIcons = {} as Record<string, string>
|
|
32
32
|
}: UploadFileStatusProps = $props()
|
|
33
33
|
|
|
34
|
-
const labels = $derived({ ...messages.
|
|
34
|
+
const labels = $derived({ ...messages.uploadProgress, ...userLabels })
|
|
35
35
|
const icons = $derived({
|
|
36
36
|
cancel: DEFAULT_STATE_ICONS.action.cancel,
|
|
37
37
|
retry: DEFAULT_STATE_ICONS.action.retry,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
// ─── Labels ──────────────────────────────────────────────────────────────
|
|
40
40
|
|
|
41
|
-
const labels = $derived({ ...messages.
|
|
41
|
+
const labels = $derived({ ...messages.uploadProgress, ...userLabels })
|
|
42
42
|
|
|
43
43
|
// ─── Field resolution ────────────────────────────────────────────────────
|
|
44
44
|
// Pass upload-specific field mappings through to List/Grid so ProxyItem
|
package/src/components/index.ts
CHANGED
|
@@ -41,3 +41,7 @@ export { default as FloatingNavigation } from './FloatingNavigation.svelte'
|
|
|
41
41
|
export { default as StatusList } from './StatusList.svelte'
|
|
42
42
|
export { default as Message } from './Message.svelte'
|
|
43
43
|
export { default as AlertList } from './AlertList.svelte'
|
|
44
|
+
export { default as Divider } from './Divider.svelte'
|
|
45
|
+
export { default as Stack } from './Stack.svelte'
|
|
46
|
+
export { default as Badge } from './Badge.svelte'
|
|
47
|
+
export { default as Avatar } from './Avatar.svelte'
|
package/src/index.ts
CHANGED
package/src/markdown-plugin.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { Component } from 'svelte'
|
|
|
5
5
|
* The component receives { code: string } as props.
|
|
6
6
|
*/
|
|
7
7
|
export interface MarkdownPlugin {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/** Fenced code block language to match (e.g. 'plot', 'table', 'sparkline') */
|
|
9
|
+
language: string
|
|
10
|
+
/** Svelte component to render the block. Receives { code: string } */
|
|
11
|
+
component: Component<{ code: string }>
|
|
12
12
|
}
|
package/src/types/table.ts
CHANGED
|
@@ -166,9 +166,15 @@ export interface TableProps {
|
|
|
166
166
|
/** Column definitions (auto-derived from data if not provided) */
|
|
167
167
|
columns?: TableColumn[]
|
|
168
168
|
|
|
169
|
-
/** Currently selected row value */
|
|
169
|
+
/** Currently selected row value (single-select) */
|
|
170
170
|
value?: unknown
|
|
171
171
|
|
|
172
|
+
/** Currently selected row values (multi-select, bindable) */
|
|
173
|
+
values?: unknown[]
|
|
174
|
+
|
|
175
|
+
/** Selection mode: 'single' (default), 'multi', or false (no selection) */
|
|
176
|
+
selectable?: 'single' | 'multi' | false
|
|
177
|
+
|
|
172
178
|
/** Table caption for accessibility */
|
|
173
179
|
caption?: string
|
|
174
180
|
|
|
@@ -178,6 +184,9 @@ export interface TableProps {
|
|
|
178
184
|
/** Enable alternating row colors */
|
|
179
185
|
striped?: boolean
|
|
180
186
|
|
|
187
|
+
/** Enable responsive card layout on mobile (< 640px) */
|
|
188
|
+
responsive?: boolean
|
|
189
|
+
|
|
181
190
|
/** Whether the entire table is disabled */
|
|
182
191
|
disabled?: boolean
|
|
183
192
|
|
package/src/types/toggle.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface ToggleProps {
|
|
|
57
57
|
/** Whether the entire toggle is disabled */
|
|
58
58
|
disabled?: boolean
|
|
59
59
|
|
|
60
|
-
/** Accessible label for the radiogroup. Default: messages.
|
|
60
|
+
/** Accessible label for the radiogroup. Default: messages.toggle.label */
|
|
61
61
|
label?: string
|
|
62
62
|
|
|
63
63
|
/** Additional CSS classes */
|
|
@@ -37,7 +37,7 @@ export interface UploadFileStatusProps {
|
|
|
37
37
|
/** Called when remove is clicked */
|
|
38
38
|
onremove?: (proxy: ProxyItem) => void
|
|
39
39
|
|
|
40
|
-
/** Label overrides merged with messages.
|
|
40
|
+
/** Label overrides merged with messages.uploadProgress */
|
|
41
41
|
labels?: Record<string, string>
|
|
42
42
|
|
|
43
43
|
/** Icon class overrides for action buttons (cancel, retry, remove) */
|
|
@@ -100,7 +100,7 @@ export interface UploadProgressProps {
|
|
|
100
100
|
/** Called when clear all is clicked */
|
|
101
101
|
onclear?: () => void
|
|
102
102
|
|
|
103
|
-
/** Label overrides merged with messages.
|
|
103
|
+
/** Label overrides merged with messages.uploadProgress */
|
|
104
104
|
labels?: Record<string, string>
|
|
105
105
|
|
|
106
106
|
/** Additional CSS classes */
|
|
@@ -51,7 +51,7 @@ export interface UploadTargetProps {
|
|
|
51
51
|
/** Disable the drop zone */
|
|
52
52
|
disabled?: boolean
|
|
53
53
|
|
|
54
|
-
/** Label overrides merged with messages.
|
|
54
|
+
/** Label overrides merged with messages.uploadTarget */
|
|
55
55
|
labels?: Record<string, string>
|
|
56
56
|
|
|
57
57
|
/** Called with validated files after drop or browse */
|