@rokkit/ui 1.0.5 → 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.
- package/package.json +7 -6
- package/src/MarkdownRenderer.svelte +13 -1
- package/src/components/AlertList.svelte +1 -13
- package/src/components/Avatar.svelte +1 -18
- package/src/components/Badge.svelte +1 -16
- package/src/components/BreadCrumbs.svelte +2 -21
- package/src/components/Card.svelte +1 -18
- package/src/components/Carousel.svelte +2 -27
- package/src/components/CodeBlock.svelte +130 -0
- package/src/components/CodeGroup.svelte +408 -0
- package/src/components/Connector.svelte +2 -8
- package/src/components/Divider.svelte +1 -8
- package/src/components/Frame.svelte +30 -0
- package/src/components/ItemContent.svelte +4 -2
- package/src/components/Message.svelte +1 -22
- package/src/components/NavContent.svelte +1 -16
- package/src/components/Pill.svelte +1 -24
- package/src/components/ProgressBar.svelte +1 -8
- package/src/components/Rating.svelte +2 -22
- package/src/components/ResponsiveGrid.svelte +1 -14
- package/src/components/Reveal.svelte +1 -23
- package/src/components/Select.svelte +90 -9
- package/src/components/Shine.svelte +1 -19
- package/src/components/Stack.svelte +1 -16
- package/src/components/StatusList.svelte +3 -2
- package/src/components/Stepper.svelte +2 -41
- package/src/components/Tilt.svelte +1 -13
- package/src/components/Toggle.svelte +83 -33
- package/src/components/index.ts +3 -0
- package/src/index.ts +4 -0
- package/src/types/alert-list.ts +28 -0
- package/src/types/avatar.ts +41 -0
- package/src/types/badge.ts +37 -0
- package/src/types/breadcrumbs.ts +43 -0
- package/src/types/card.ts +38 -0
- package/src/types/carousel.ts +49 -0
- package/src/types/code.ts +102 -1
- package/src/types/connector.ts +22 -0
- package/src/types/divider.ts +20 -0
- package/src/types/frame.ts +29 -0
- package/src/types/index.ts +66 -0
- package/src/types/item-content.ts +26 -0
- package/src/types/message.ts +54 -0
- package/src/types/nav-content.ts +31 -0
- package/src/types/pill.ts +43 -0
- package/src/types/progress-bar.ts +19 -0
- package/src/types/rating.ts +43 -0
- package/src/types/responsive-grid.ts +25 -0
- package/src/types/reveal.ts +49 -0
- package/src/types/shine.ts +37 -0
- package/src/types/stack.ts +40 -0
- package/src/types/status-list.ts +42 -0
- package/src/types/stepper.ts +74 -0
- package/src/types/tilt.ts +28 -0
- package/src/types/toggle.ts +12 -0
- package/src/types/tree.ts +46 -4
- package/src/utils/palette.ts +4 -2
- package/src/utils/shiki.ts +22 -1
- 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
|
|
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
|
|
47
|
-
"@rokkit/data": "1.0
|
|
48
|
-
"@rokkit/states": "1.0
|
|
49
|
-
"@rokkit/actions": "1.0
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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>
|