@rokkit/ui 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +7 -6
  2. package/src/MarkdownRenderer.svelte +13 -1
  3. package/src/components/AlertList.svelte +1 -13
  4. package/src/components/Avatar.svelte +1 -18
  5. package/src/components/Badge.svelte +1 -16
  6. package/src/components/BreadCrumbs.svelte +2 -21
  7. package/src/components/Card.svelte +1 -18
  8. package/src/components/Carousel.svelte +2 -27
  9. package/src/components/CodeBlock.svelte +130 -0
  10. package/src/components/CodeGroup.svelte +408 -0
  11. package/src/components/Connector.svelte +2 -8
  12. package/src/components/Divider.svelte +1 -8
  13. package/src/components/Frame.svelte +30 -0
  14. package/src/components/ItemContent.svelte +4 -2
  15. package/src/components/Message.svelte +1 -22
  16. package/src/components/NavContent.svelte +1 -16
  17. package/src/components/Pill.svelte +1 -24
  18. package/src/components/ProgressBar.svelte +1 -8
  19. package/src/components/Rating.svelte +2 -22
  20. package/src/components/ResponsiveGrid.svelte +1 -14
  21. package/src/components/Reveal.svelte +1 -23
  22. package/src/components/Select.svelte +90 -9
  23. package/src/components/Shine.svelte +1 -19
  24. package/src/components/Stack.svelte +1 -16
  25. package/src/components/StatusList.svelte +3 -2
  26. package/src/components/Stepper.svelte +2 -41
  27. package/src/components/Tilt.svelte +1 -13
  28. package/src/components/Toggle.svelte +83 -33
  29. package/src/components/index.ts +3 -0
  30. package/src/index.ts +4 -0
  31. package/src/types/alert-list.ts +28 -0
  32. package/src/types/avatar.ts +41 -0
  33. package/src/types/badge.ts +37 -0
  34. package/src/types/breadcrumbs.ts +43 -0
  35. package/src/types/card.ts +38 -0
  36. package/src/types/carousel.ts +49 -0
  37. package/src/types/code.ts +102 -1
  38. package/src/types/connector.ts +22 -0
  39. package/src/types/divider.ts +20 -0
  40. package/src/types/frame.ts +29 -0
  41. package/src/types/index.ts +66 -0
  42. package/src/types/item-content.ts +26 -0
  43. package/src/types/message.ts +54 -0
  44. package/src/types/nav-content.ts +31 -0
  45. package/src/types/pill.ts +43 -0
  46. package/src/types/progress-bar.ts +19 -0
  47. package/src/types/rating.ts +43 -0
  48. package/src/types/responsive-grid.ts +25 -0
  49. package/src/types/reveal.ts +49 -0
  50. package/src/types/shine.ts +37 -0
  51. package/src/types/stack.ts +40 -0
  52. package/src/types/status-list.ts +42 -0
  53. package/src/types/stepper.ts +74 -0
  54. package/src/types/tilt.ts +28 -0
  55. package/src/types/toggle.ts +12 -0
  56. package/src/types/tree.ts +46 -4
  57. package/src/utils/palette.ts +4 -2
  58. package/src/utils/shiki.ts +22 -1
  59. package/src/utils/upload.js +8 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Data driven UI components for Rokkit applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,8 @@
22
22
  "./utils/palette": {
23
23
  "types": "./src/utils/palette.ts",
24
24
  "default": "./src/utils/palette.ts"
25
- }
25
+ },
26
+ "./package.json": "./package.json"
26
27
  },
27
28
  "files": [
28
29
  "src",
@@ -43,10 +44,10 @@
43
44
  "dropdown"
44
45
  ],
45
46
  "dependencies": {
46
- "@rokkit/core": "1.0.4",
47
- "@rokkit/data": "1.0.4",
48
- "@rokkit/states": "1.0.4",
49
- "@rokkit/actions": "1.0.4",
47
+ "@rokkit/core": "1.1.0",
48
+ "@rokkit/data": "1.1.0",
49
+ "@rokkit/states": "1.1.0",
50
+ "@rokkit/actions": "1.1.0",
50
51
  "marked": "^15.0.0",
51
52
  "dompurify": "^3.0.0"
52
53
  },
@@ -22,12 +22,24 @@
22
22
 
23
23
  const tokens = $derived(marked.lexer(markdown))
24
24
 
25
+ // DOMPurify ships as a fully-initialised object in browser environments and
26
+ // as a factory function on the server (it needs to be bound to a DOM window
27
+ // — usually jsdom — before .sanitize is available). To stay SSR-safe without
28
+ // pulling jsdom into every consumer's build, fall back to a pass-through
29
+ // when .sanitize is unavailable. Consumers rendering untrusted markdown in
30
+ // SSR contexts should pre-sanitize before passing it to this component;
31
+ // post-hydration the client runs the real sanitiser regardless.
32
+ const sanitize: (raw: string) => string =
33
+ typeof DOMPurify?.sanitize === 'function'
34
+ ? (raw) => DOMPurify.sanitize(raw) as string
35
+ : (raw) => raw
36
+
25
37
  function tokenToSafeHtml(token: Token): string {
26
38
  const tokenList = Object.assign([token], {
27
39
  links: (tokens as TokensList).links ?? {}
28
40
  }) as TokensList
29
41
  const raw = marked.parser(tokenList)
30
- return DOMPurify.sanitize(raw)
42
+ return sanitize(raw)
31
43
  }
32
44
 
33
45
  /** Extract crossfilter group ID from a code token's text, or null if absent/invalid. */
@@ -2,21 +2,9 @@
2
2
  import type { Snippet } from 'svelte'
3
3
  import { SvelteSet } from 'svelte/reactivity'
4
4
  import { alerts } from '@rokkit/states'
5
+ import type { AlertListProps } from '../types/alert-list.js'
5
6
  import Message from './Message.svelte'
6
7
 
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
8
  const { position = 'top-right', class: className = '' }: AlertListProps = $props()
21
9
 
22
10
  const dismissing = new SvelteSet<string>()
@@ -1,22 +1,5 @@
1
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
- }
2
+ import type { AvatarProps } from '../types/avatar.js'
20
3
 
21
4
  const {
22
5
  src,
@@ -1,20 +1,5 @@
1
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
- }
2
+ import type { BadgeProps } from '../types/badge.js'
18
3
 
19
4
  const {
20
5
  count,
@@ -1,26 +1,7 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte'
3
2
  import { ProxyItem, messages } from '@rokkit/states'
4
3
  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
- }
4
+ import type { BreadCrumbsIcons, BreadCrumbsProps } from '../types/breadcrumbs.js'
24
5
 
25
6
  const {
26
7
  items = [],
@@ -30,7 +11,7 @@
30
11
  onclick,
31
12
  crumb,
32
13
  class: className = ''
33
- }: BreadCrumbsProps & { label?: string } = $props()
14
+ }: BreadCrumbsProps = $props()
34
15
 
35
16
  const icons = $derived({ separator: DEFAULT_STATE_ICONS.navigate.right, ...userIcons })
36
17
 
@@ -1,22 +1,5 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte'
3
-
4
- interface CardProps {
5
- /** Optional href to render as a link */
6
- href?: string
7
- /** Click handler (only applies when no href) */
8
- onclick?: () => void
9
- /** Visual variant */
10
- variant?: 'default' | 'primary' | 'secondary' | 'tertiary'
11
- /** Additional CSS class */
12
- class?: string
13
- /** Card header snippet */
14
- header?: Snippet
15
- /** Card footer snippet */
16
- footer?: Snippet
17
- /** Card body content */
18
- children?: Snippet
19
- }
2
+ import type { CardProps } from '../types/card.js'
20
3
 
21
4
  const {
22
5
  href,
@@ -1,32 +1,7 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte'
3
2
  import { swipeable, keyboard } from '@rokkit/actions'
4
3
  import { messages } from '@rokkit/states'
5
-
6
- interface CarouselProps {
7
- /** Number of slides (required when using children snippet) */
8
- count?: number
9
- /** Current slide index (bindable) */
10
- current?: number
11
- /** Auto-advance slides */
12
- autoplay?: boolean
13
- /** Autoplay interval in milliseconds (default: 5000) */
14
- interval?: number
15
- /** Wrap around at ends (default: true) */
16
- loop?: boolean
17
- /** Show navigation dots (default: true) */
18
- showDots?: boolean
19
- /** Show prev/next arrow buttons (default: true) */
20
- showArrows?: boolean
21
- /** Transition effect (default: 'slide') */
22
- transition?: 'slide' | 'fade' | 'none'
23
- /** Additional CSS class */
24
- class?: string
25
- /** Slide content — receives (index, current) */
26
- slide?: Snippet<[number, number]>
27
- /** Children snippet (alternative to slide) */
28
- children?: Snippet
29
- }
4
+ import type { CarouselProps } from '../types/carousel.js'
30
5
 
31
6
  let {
32
7
  count = 0,
@@ -41,7 +16,7 @@
41
16
  class: className = '',
42
17
  slide,
43
18
  children
44
- }: CarouselProps & { labels?: Record<string, string> } = $props()
19
+ }: CarouselProps = $props()
45
20
 
46
21
  const labels = $derived({ ...messages.carousel, ...userLabels })
47
22
 
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CodeBlock — code with chrome.
4
+ *
5
+ * Wraps a Shiki-highlighted code body in a Frame whose header carries
6
+ * the filename + language chip + optional action buttons (copy /
7
+ * download). Adding the chrome on top of the same Frame primitive
8
+ * the rest of the app uses keeps the visual rhythm consistent.
9
+ *
10
+ * Action buttons are opt-in: dev surfaces enable them per instance,
11
+ * end-user surfaces default to plain.
12
+ *
13
+ * For a plain unstyled code surface (no chrome), use the lower-level
14
+ * `<Code/>` component instead.
15
+ */
16
+ import { onMount } from 'svelte'
17
+ import { highlightCode } from '../utils/shiki.js'
18
+ import type { CodeBlockProps } from '../types/code.js'
19
+ import Frame from './Frame.svelte'
20
+
21
+ const {
22
+ code,
23
+ language = 'text',
24
+ filename = '',
25
+ height,
26
+ theme = 'auto',
27
+ allowCopy = false,
28
+ allowDownload = false,
29
+ actions
30
+ }: CodeBlockProps = $props()
31
+
32
+ // Track body data-mode so the Shiki theme follows light/dark changes.
33
+ let bodyMode = $state<'light' | 'dark'>('dark')
34
+ onMount(() => {
35
+ const sync = () => {
36
+ const m = document.body?.dataset.mode
37
+ bodyMode = m === 'light' ? 'light' : 'dark'
38
+ }
39
+ sync()
40
+ const observer = new MutationObserver(sync)
41
+ observer.observe(document.body, { attributes: true, attributeFilter: ['data-mode'] })
42
+ return () => observer.disconnect()
43
+ })
44
+ const effectiveTheme = $derived(theme === 'auto' ? bodyMode : theme)
45
+
46
+ // Store Shiki output in $state. Using a $derived Promise here would have
47
+ // {#await} reset to pending on every reactivity tick (each $derived call
48
+ // produces a new Promise reference). Fall back to a plain <pre> while
49
+ // Shiki initialises and on error.
50
+ let highlighted = $state<string | null>(null)
51
+ $effect(() => {
52
+ const c = code
53
+ const l = language
54
+ const t = effectiveTheme
55
+ let cancelled = false
56
+ highlightCode(c, { lang: l, theme: t })
57
+ .then((html) => {
58
+ if (!cancelled) highlighted = html
59
+ })
60
+ .catch(() => {
61
+ if (!cancelled) highlighted = null
62
+ })
63
+ return () => { cancelled = true }
64
+ })
65
+
66
+ const hasHeader = $derived(Boolean(filename || language || allowCopy || allowDownload || actions))
67
+
68
+ let copied = $state(false)
69
+ async function copyCode() {
70
+ try {
71
+ await navigator.clipboard.writeText(code)
72
+ copied = true
73
+ setTimeout(() => (copied = false), 1500)
74
+ } catch {
75
+ // Silent fail — clipboard API may require https or user gesture
76
+ }
77
+ }
78
+
79
+ function downloadCode() {
80
+ const blob = new Blob([code], { type: 'text/plain' })
81
+ const url = URL.createObjectURL(blob)
82
+ const a = document.createElement('a')
83
+ a.href = url
84
+ a.download = filename || `code.${language || 'txt'}`
85
+ a.click()
86
+ URL.revokeObjectURL(url)
87
+ }
88
+ </script>
89
+
90
+ <div data-code-block style:max-height={height || undefined}>
91
+ <Frame flush>
92
+ {#snippet header()}
93
+ {#if hasHeader}
94
+ <div data-code-block-header>
95
+ <div data-code-block-title>
96
+ <span data-code-block-icon class="view-code" aria-hidden="true"></span>
97
+ {#if filename}<span data-code-block-filename>{filename}</span>{/if}
98
+ {#if language}<span data-code-block-lang>{language}</span>{/if}
99
+ </div>
100
+ {#if allowCopy || allowDownload || actions}
101
+ <div data-code-block-actions>
102
+ {#if allowCopy}
103
+ <button type="button" onclick={copyCode} title="Copy code">
104
+ <span class={copied ? 'action-check' : 'action-copy'} aria-hidden="true"></span>
105
+ <span>{copied ? 'Copied' : 'Copy'}</span>
106
+ </button>
107
+ {/if}
108
+ {#if allowDownload}
109
+ <button type="button" onclick={downloadCode} title="Download as file">
110
+ <span class="action-download" aria-hidden="true"></span>
111
+ <span>.{language || 'txt'}</span>
112
+ </button>
113
+ {/if}
114
+ {#if actions}
115
+ {@render actions()}
116
+ {/if}
117
+ </div>
118
+ {/if}
119
+ </div>
120
+ {/if}
121
+ {/snippet}
122
+
123
+ {#if highlighted}
124
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
125
+ <div data-code-block-body>{@html highlighted}</div>
126
+ {:else}
127
+ <pre data-code-block-body><code>{code}</code></pre>
128
+ {/if}
129
+ </Frame>
130
+ </div>