@rokkit/ui 1.0.0-next.151 → 1.0.0-next.152

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/MarkdownRenderer.svelte +96 -33
  3. package/src/components/AlertList.svelte +1 -1
  4. package/src/components/Avatar.svelte +68 -0
  5. package/src/components/Badge.svelte +57 -0
  6. package/src/components/BreadCrumbs.svelte +1 -1
  7. package/src/components/Card.svelte +17 -2
  8. package/src/components/Carousel.svelte +1 -1
  9. package/src/components/Code.svelte +1 -1
  10. package/src/components/Divider.svelte +24 -0
  11. package/src/components/Dropdown.svelte +2 -2
  12. package/src/components/FloatingNavigation.svelte +2 -2
  13. package/src/components/Grid.svelte +1 -1
  14. package/src/components/LazyTree.svelte +1 -1
  15. package/src/components/List.svelte +6 -5
  16. package/src/components/Menu.svelte +1 -1
  17. package/src/components/Message.svelte +1 -3
  18. package/src/components/MultiSelect.svelte +2 -2
  19. package/src/components/Range.svelte +1 -1
  20. package/src/components/Rating.svelte +1 -1
  21. package/src/components/SearchFilter.svelte +2 -2
  22. package/src/components/Select.svelte +2 -2
  23. package/src/components/Stack.svelte +38 -0
  24. package/src/components/Stepper.svelte +1 -1
  25. package/src/components/Swatch.svelte +1 -1
  26. package/src/components/Table.svelte +23 -10
  27. package/src/components/Tabs.svelte +2 -2
  28. package/src/components/Toggle.svelte +1 -1
  29. package/src/components/Toolbar.svelte +1 -1
  30. package/src/components/Tree.svelte +1 -1
  31. package/src/components/UploadFileStatus.svelte +1 -1
  32. package/src/components/UploadProgress.svelte +1 -1
  33. package/src/components/UploadTarget.svelte +1 -1
  34. package/src/components/index.ts +4 -0
  35. package/src/index.ts +5 -1
  36. package/src/markdown-plugin.ts +4 -4
  37. package/src/types/table.ts +10 -1
  38. package/src/types/toggle.ts +1 -1
  39. package/src/types/upload-file-status.ts +1 -1
  40. package/src/types/upload-progress.ts +1 -1
  41. 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.151",
3
+ "version": "1.0.0-next.152",
4
4
  "description": "Data driven UI components for Rokkit applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,43 +1,106 @@
1
1
  <script lang="ts">
2
- import { marked } from 'marked'
3
- import type { Token, TokensList } from 'marked'
4
- import DOMPurify from 'isomorphic-dompurify'
5
- import type { MarkdownPlugin } from './markdown-plugin.js'
2
+ import { marked } from 'marked'
3
+ import type { Token, TokensList } from 'marked'
4
+ import DOMPurify from 'isomorphic-dompurify'
5
+ import type { Component, Snippet } from 'svelte'
6
+ import type { MarkdownPlugin } from './markdown-plugin.js'
6
7
 
7
- interface Props {
8
- markdown: string
9
- plugins?: MarkdownPlugin[]
10
- }
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
+ }
11
16
 
12
- let { markdown, plugins = [] }: Props = $props()
17
+ let { markdown, plugins = [], crossfilterWrapper }: Props = $props()
13
18
 
14
- const pluginMap = $derived(
15
- Object.fromEntries(plugins.map((p) => [p.language.toLowerCase(), p.component]))
16
- )
19
+ const pluginMap = $derived(
20
+ Object.fromEntries(plugins.map((p) => [p.language.toLowerCase(), p.component]))
21
+ )
17
22
 
18
- const tokens = $derived(marked.lexer(markdown))
23
+ const tokens = $derived(marked.lexer(markdown))
19
24
 
20
- function tokenToSafeHtml(token: Token): string {
21
- const tokenList = Object.assign([token], { links: (tokens as TokensList).links ?? {} }) as TokensList
22
- const raw = marked.parser(tokenList)
23
- return DOMPurify.sanitize(raw)
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
+ })
25
77
  </script>
26
78
 
27
79
  <div class="markdown-renderer" data-markdown>
28
- {#each tokens as token, i (i)}
29
- {#if token.type === 'code'}
30
- {@const lang = (token.lang ?? '').toLowerCase()}
31
- {@const Plugin = pluginMap[lang]}
32
- {#if Plugin}
33
- <Plugin code={token.text} />
34
- {:else}
35
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
36
- {@html tokenToSafeHtml(token)}
37
- {/if}
38
- {:else}
39
- <!-- eslint-disable-next-line svelte/no-at-html-tags -->
40
- {@html tokenToSafeHtml(token)}
41
- {/if}
42
- {/each}
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}
43
106
  </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}
@@ -25,7 +25,7 @@
25
25
  const {
26
26
  items = [],
27
27
  fields,
28
- label = messages.current.breadcrumbs.label,
28
+ label = messages.breadcrumbs.label,
29
29
  icons: userIcons = {} as BreadCrumbsIcons,
30
30
  onclick,
31
31
  crumb,
@@ -18,7 +18,15 @@
18
18
  children?: Snippet
19
19
  }
20
20
 
21
- const { href, onclick, variant = 'default', class: className = '', header, footer, children }: CardProps = $props()
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 type="button" data-card data-card-interactive data-variant={variant} class={className || undefined} {onclick}>
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.current.carousel, ...userLabels })
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.current.code, ...userLabels })
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 = 'Select...',
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.current.floatingNav.label,
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.current.floatingNav, ...userLabels })
27
+ const labels = $derived({ ...messages.floatingNav, ...userLabels })
28
28
 
29
29
  const icons = $derived({
30
30
  pin: DEFAULT_STATE_ICONS.action.pin,
@@ -41,7 +41,7 @@
41
41
  disabled = false,
42
42
  minSize = '120px',
43
43
  gap = '1rem',
44
- label = messages.current.grid.label,
44
+ label = messages.grid.label,
45
45
  onselect,
46
46
  class: className = '',
47
47
  ...snippets
@@ -45,7 +45,7 @@
45
45
  [key: string]: unknown
46
46
  } = $props()
47
47
 
48
- const labels = $derived({ ...messages.current.tree, ...userLabels })
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.current.list.label,
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) { expandAllGroups(); return }
110
+ if (!collapsible) {
111
+ expandAllGroups()
112
+ return
113
+ }
113
114
  expandAncestorGroups(findActiveKey())
114
115
  }
115
116
 
@@ -61,7 +61,7 @@
61
61
  size = 'md',
62
62
  disabled = false,
63
63
  collapsible = false,
64
- label = messages.current.menu.label,
64
+ label = messages.menu.label,
65
65
  icon,
66
66
  showArrow = true,
67
67
  align = 'start',
@@ -70,8 +70,6 @@
70
70
  {/if}
71
71
 
72
72
  {#if dismissible}
73
- <button type="button" data-message-dismiss aria-label="Dismiss" onclick={ondismiss}
74
- >×</button
75
- >
73
+ <button type="button" data-message-dismiss aria-label="Dismiss" onclick={ondismiss}>×</button>
76
74
  {/if}
77
75
  </div>
@@ -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 = 'Select...',
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.current.range, ...userLabels })
23
+ const labels = $derived({ ...messages.range, ...userLabels })
24
24
 
25
25
  // ─── Pixel state ────────────────────────────────────────────────
26
26
  let trackWidth = $state(0)
@@ -27,7 +27,7 @@
27
27
  value = $bindable(0),
28
28
  max = 5,
29
29
  disabled = false,
30
- label = messages.current.rating.label,
30
+ label = messages.rating.label,
31
31
  icons: userIcons = {} as RatingIcons,
32
32
  onchange,
33
33
  class: className = ''
@@ -16,8 +16,8 @@
16
16
  }: SearchFilterProps & { labels?: Record<string, string> } = $props()
17
17
 
18
18
  const labels = $derived({
19
- clear: messages.current.search_.clear,
20
- remove: messages.current.filter.remove,
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 = 'Select...',
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>
@@ -52,7 +52,7 @@
52
52
  currentStage = $bindable(0),
53
53
  linear = false,
54
54
  orientation = 'horizontal',
55
- label = messages.current.stepper.label,
55
+ label = messages.stepper.label,
56
56
  icons: userIcons,
57
57
  onclick,
58
58
  content,
@@ -11,7 +11,7 @@
11
11
  shape = 'square',
12
12
  size = 'md',
13
13
  disabled = false,
14
- label = messages.current.swatch?.label ?? 'Select',
14
+ label = messages.swatch?.label ?? 'Select',
15
15
  class: className = '',
16
16
  onchange,
17
17
  item: itemSnippet,
@@ -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
- if (!disabled) {
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}> No data </td>
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 = 'Select a tab to view its content.',
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.current.tabs, ...userLabels })
47
+ const labels = $derived({ ...messages.tabs, ...userLabels })
48
48
 
49
49
  // ─── Wrapper ──────────────────────────────────────────────────────────────
50
50
 
@@ -13,7 +13,7 @@
13
13
  showLabels = true,
14
14
  size = 'md',
15
15
  disabled = false,
16
- label = messages.current.toggle.label,
16
+ label = messages.toggle.label,
17
17
  class: className = '',
18
18
  ...snippets
19
19
  }: ToggleProps & { [key: string]: unknown } = $props()
@@ -22,7 +22,7 @@
22
22
  compact = false,
23
23
  showDividers = true,
24
24
  disabled = false,
25
- label = messages.current.toolbar.label,
25
+ label = messages.toolbar.label,
26
26
  onclick,
27
27
  class: className = '',
28
28
  item: itemSnippet,
@@ -43,7 +43,7 @@
43
43
  [key: string]: unknown
44
44
  } = $props()
45
45
 
46
- const labels = $derived({ ...messages.current.tree, ...userLabels })
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.current.uploadProgress, ...userLabels })
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.current.uploadProgress, ...userLabels })
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
@@ -31,7 +31,7 @@
31
31
  const content = $derived(snippets.content as ((dragging: boolean) => unknown) | undefined)
32
32
 
33
33
  const resolvedLabels = $derived({
34
- ...messages.current.uploadTarget,
34
+ ...messages.uploadTarget,
35
35
  ...labels
36
36
  })
37
37
 
@@ -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
@@ -40,7 +40,11 @@ export {
40
40
  UploadProgress,
41
41
  StatusList,
42
42
  Message,
43
- AlertList
43
+ AlertList,
44
+ Divider,
45
+ Stack,
46
+ Badge,
47
+ Avatar
44
48
  } from './components/index.js'
45
49
 
46
50
  export { default as MarkdownRenderer } from './MarkdownRenderer.svelte'
@@ -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
- /** 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 }>
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
  }
@@ -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
 
@@ -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.current.toggle.label */
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.current.uploadProgress */
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.current.uploadProgress */
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.current.uploadTarget */
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 */