@human-synthesis/norns-ui 0.1.0 → 0.2.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 +4 -2
- package/src/auto-import.js +8 -1
- package/src/components/ChatThread.n +97 -0
- package/src/components/DataTable.n +10 -2
- package/src/components/Feed.n +71 -0
- package/src/components/Kanban.n +17 -7
- package/src/components/LiveLog.n +64 -0
- package/src/components/PresenceAvatars.n +45 -0
- package/src/components/StreamText.n +64 -0
- package/src/components/Table.n +4 -0
- package/src/components/Timeline.n +5 -3
- package/src/components/Tree.n +4 -2
- package/src/contracts.js +135 -0
- package/src/lib/behaviors/index.js +1 -0
- package/src/lib/behaviors/optimistic.svelte.js +24 -0
- package/src/palette-manifest.json +4188 -0
- package/src/types/Chart.d.ts +21 -0
- package/src/types/ChatThread.d.ts +27 -0
- package/src/types/DataTable.d.ts +5 -1
- package/src/types/Feed.d.ts +19 -0
- package/src/types/Kanban.d.ts +26 -0
- package/src/types/Listbox.d.ts +19 -0
- package/src/types/LiveLog.d.ts +16 -0
- package/src/types/PresenceAvatars.d.ts +17 -0
- package/src/types/Spinner.d.ts +12 -0
- package/src/types/StreamText.d.ts +18 -0
- package/src/types/Table.d.ts +5 -1
- package/src/types/Timeline.d.ts +2 -0
- package/src/types/Tree.d.ts +2 -0
- package/src/types/Window.d.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@human-synthesis/norns-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "UI library for the Norns ecosystem — Pug + Civet components on Tailwind v4.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Daniel Teodoroiu (https://humansynthesis.ai)",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "bun test",
|
|
18
|
+
"build:manifest": "bun scripts/build-manifest.mjs",
|
|
18
19
|
"check:shims": "bun scripts/check-shims.mjs",
|
|
19
20
|
"check:shims:strict": "bun scripts/check-shims.mjs --strict"
|
|
20
21
|
},
|
|
@@ -30,12 +31,13 @@
|
|
|
30
31
|
"./styles/atoms": "./src/styles/atoms.css",
|
|
31
32
|
"./components/*": "./src/components/*",
|
|
32
33
|
"./contracts": "./src/contracts.js",
|
|
34
|
+
"./manifest": "./src/palette-manifest.json",
|
|
33
35
|
"./motion": "./src/motion/index.js",
|
|
34
36
|
"./motion/*": "./src/motion/*",
|
|
35
37
|
"./package.json": "./package.json"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
|
-
"@human-synthesis/norns": "^0.
|
|
40
|
+
"@human-synthesis/norns": "^0.2.0",
|
|
39
41
|
"@human-synthesis/norns-core": "^0.0.10",
|
|
40
42
|
"svelte": "^5.0.0",
|
|
41
43
|
"tailwindcss": "^4.0.0"
|
package/src/auto-import.js
CHANGED
|
@@ -123,7 +123,14 @@ export function presetUI() {
|
|
|
123
123
|
CopyButton: c('CopyButton'),
|
|
124
124
|
ThemeToggler: c('ThemeToggler'),
|
|
125
125
|
ShinyButton: c('ShinyButton'),
|
|
126
|
-
RippleButton: c('RippleButton')
|
|
126
|
+
RippleButton: c('RippleButton'),
|
|
127
|
+
|
|
128
|
+
// U-09 — realtime tier
|
|
129
|
+
StreamText: c('StreamText'),
|
|
130
|
+
ChatThread: c('ChatThread'),
|
|
131
|
+
Feed: c('Feed'),
|
|
132
|
+
PresenceAvatars: c('PresenceAvatars'),
|
|
133
|
+
LiveLog: c('LiveLog')
|
|
127
134
|
// Motion-driven components live in `@human-synthesis/norns-ui/motion`
|
|
128
135
|
// and are NOT registered here (opt-in import only).
|
|
129
136
|
},
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
.chat-thread(class!="{classes}")
|
|
2
|
+
ul.chat-thread-list
|
|
3
|
+
+if('messages.length === 0')
|
|
4
|
+
li.chat-thread-empty {emptyMessage}
|
|
5
|
+
+each('messages as msg')
|
|
6
|
+
li.chat-thread-msg(class!="{msg.self ? 'chat-thread-msg-self' : ''}")
|
|
7
|
+
+if('message')
|
|
8
|
+
| {@render message(msg)}
|
|
9
|
+
+if('!message')
|
|
10
|
+
+if('msg.from')
|
|
11
|
+
span.chat-thread-from {msg.from}
|
|
12
|
+
span.chat-thread-text {msg.text}
|
|
13
|
+
form.chat-thread-composer(onsubmit!="{submit}")
|
|
14
|
+
input.input.chat-thread-input(bind:value!="{draft}" placeholder!="{placeholder}")
|
|
15
|
+
button.btn.btn-primary(type="submit" disabled!="{draft.trim() === ''}") {sendLabel}
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
19
|
+
import { roomChannel as openRoom } from '@human-synthesis/norns/live-client'
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
roomChannel = undefined
|
|
23
|
+
send = 'chat'
|
|
24
|
+
receive = undefined
|
|
25
|
+
placeholder = 'Message'
|
|
26
|
+
sendLabel = 'Send'
|
|
27
|
+
emptyMessage = 'No messages yet'
|
|
28
|
+
message = undefined
|
|
29
|
+
class: extra = ''
|
|
30
|
+
} .= $props()
|
|
31
|
+
|
|
32
|
+
messages .= $state([])
|
|
33
|
+
draft .= $state('')
|
|
34
|
+
channel .= $state(undefined)
|
|
35
|
+
|
|
36
|
+
classes := $derived cn('chat-thread', extra)
|
|
37
|
+
|
|
38
|
+
$effect =>
|
|
39
|
+
return unless roomChannel
|
|
40
|
+
ch := openRoom(roomChannel)
|
|
41
|
+
channel = ch
|
|
42
|
+
off := ch.on(receive ?? send, (frame) => messages = [...messages, frame])
|
|
43
|
+
=>
|
|
44
|
+
off()
|
|
45
|
+
ch.close()
|
|
46
|
+
channel = undefined
|
|
47
|
+
|
|
48
|
+
submit := (e) =>
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
body := draft.trim()
|
|
51
|
+
return unless body and channel
|
|
52
|
+
channel.send(send, { text: body })
|
|
53
|
+
messages = [...messages, { text: body, self: true }]
|
|
54
|
+
draft = ''
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.chat-thread {
|
|
59
|
+
display: grid;
|
|
60
|
+
gap: var(--spacing-2, 0.5rem);
|
|
61
|
+
}
|
|
62
|
+
.chat-thread-list {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: var(--spacing-1, 0.25rem);
|
|
65
|
+
max-height: 24rem;
|
|
66
|
+
overflow-y: auto;
|
|
67
|
+
list-style: none;
|
|
68
|
+
margin: 0;
|
|
69
|
+
padding: 0;
|
|
70
|
+
}
|
|
71
|
+
.chat-thread-empty {
|
|
72
|
+
opacity: 0.6;
|
|
73
|
+
font-size: 0.875rem;
|
|
74
|
+
}
|
|
75
|
+
.chat-thread-msg {
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: var(--spacing-2, 0.5rem);
|
|
78
|
+
align-items: baseline;
|
|
79
|
+
padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);
|
|
80
|
+
border-radius: var(--radius-md, 0.375rem);
|
|
81
|
+
background: var(--color-surface-2, color-mix(in oklab, currentColor 4%, transparent));
|
|
82
|
+
}
|
|
83
|
+
.chat-thread-msg-self {
|
|
84
|
+
background: color-mix(in oklab, var(--color-primary-500, currentColor) 12%, transparent);
|
|
85
|
+
}
|
|
86
|
+
.chat-thread-from {
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
font-size: 0.8125rem;
|
|
89
|
+
}
|
|
90
|
+
.chat-thread-composer {
|
|
91
|
+
display: flex;
|
|
92
|
+
gap: var(--spacing-2, 0.5rem);
|
|
93
|
+
}
|
|
94
|
+
.chat-thread-input {
|
|
95
|
+
flex: 1;
|
|
96
|
+
}
|
|
97
|
+
</style>
|
|
@@ -17,12 +17,18 @@
|
|
|
17
17
|
+if('rows.length === 0')
|
|
18
18
|
tr.data-table-empty
|
|
19
19
|
td(colspan!="{columns.length}")
|
|
20
|
-
|
|
20
|
+
+if('empty')
|
|
21
|
+
| {@render empty()}
|
|
22
|
+
+if('!empty')
|
|
23
|
+
| {emptyMessage}
|
|
21
24
|
+each('rows as row, i')
|
|
22
25
|
tr(onclick!="{onrowclick ? () => onrowclick(row, i) : undefined}" class!="{onrowclick ? 'data-table-row-clickable' : ''}")
|
|
23
26
|
+each('columns as col')
|
|
24
27
|
td(class!="{col.cellClass ?? ''}")
|
|
25
|
-
|
|
28
|
+
+if('cell')
|
|
29
|
+
| {@render cell(row, col.key, row[col.key])}
|
|
30
|
+
+if('!cell')
|
|
31
|
+
| {row[col.key]}
|
|
26
32
|
|
|
27
33
|
<script>
|
|
28
34
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
@@ -38,6 +44,8 @@
|
|
|
38
44
|
sortKey = $bindable(undefined)
|
|
39
45
|
sortDir = $bindable('asc')
|
|
40
46
|
onrowclick
|
|
47
|
+
cell = undefined
|
|
48
|
+
empty = undefined
|
|
41
49
|
class: extra = ''
|
|
42
50
|
} .= $props()
|
|
43
51
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
ul.feed(class!="{classes}" aria-live="polite")
|
|
2
|
+
+if('entries.length === 0')
|
|
3
|
+
li.feed-empty {emptyMessage}
|
|
4
|
+
+each('entries as it')
|
|
5
|
+
li.feed-item
|
|
6
|
+
+if('item')
|
|
7
|
+
| {@render item(it)}
|
|
8
|
+
+if('!item')
|
|
9
|
+
| {typeof it === 'string' ? it : (it.text ?? JSON.stringify(it))}
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
+
import { streamSource as openStream, roomChannel as openRoom } from '@human-synthesis/norns/live-client'
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
streamSource = undefined
|
|
17
|
+
roomChannel = undefined
|
|
18
|
+
receive = '*'
|
|
19
|
+
max = 100
|
|
20
|
+
emptyMessage = 'Nothing yet'
|
|
21
|
+
item = undefined
|
|
22
|
+
class: extra = ''
|
|
23
|
+
} .= $props()
|
|
24
|
+
|
|
25
|
+
entries .= $state([])
|
|
26
|
+
|
|
27
|
+
classes := $derived cn('feed', extra)
|
|
28
|
+
|
|
29
|
+
append := (frame) =>
|
|
30
|
+
entries = [...entries, frame].slice(-Math.max(1, Number(max) || 100))
|
|
31
|
+
|
|
32
|
+
$effect =>
|
|
33
|
+
return unless roomChannel
|
|
34
|
+
ch := openRoom(roomChannel)
|
|
35
|
+
off := ch.on(receive, append)
|
|
36
|
+
=>
|
|
37
|
+
off()
|
|
38
|
+
ch.close()
|
|
39
|
+
|
|
40
|
+
$effect =>
|
|
41
|
+
return unless streamSource
|
|
42
|
+
let stopped = false
|
|
43
|
+
(async =>
|
|
44
|
+
try
|
|
45
|
+
for await (const frame of openStream(streamSource))
|
|
46
|
+
return if stopped
|
|
47
|
+
append(frame)
|
|
48
|
+
catch
|
|
49
|
+
return
|
|
50
|
+
)()
|
|
51
|
+
=> stopped = true
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<style>
|
|
55
|
+
.feed {
|
|
56
|
+
display: grid;
|
|
57
|
+
gap: var(--spacing-1, 0.25rem);
|
|
58
|
+
list-style: none;
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: 0;
|
|
61
|
+
}
|
|
62
|
+
.feed-empty {
|
|
63
|
+
opacity: 0.6;
|
|
64
|
+
font-size: 0.875rem;
|
|
65
|
+
}
|
|
66
|
+
.feed-item {
|
|
67
|
+
padding: var(--spacing-1, 0.25rem) var(--spacing-2, 0.5rem);
|
|
68
|
+
border-radius: var(--radius-md, 0.375rem);
|
|
69
|
+
background: var(--color-surface-2, color-mix(in oklab, currentColor 4%, transparent));
|
|
70
|
+
}
|
|
71
|
+
</style>
|
package/src/components/Kanban.n
CHANGED
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
span.kanban-column-count {col.cards.length}
|
|
12
12
|
.kanban-cards
|
|
13
13
|
+if('col.cards.length === 0')
|
|
14
|
-
.kanban-empty
|
|
14
|
+
.kanban-empty
|
|
15
|
+
+if('empty')
|
|
16
|
+
| {@render empty()}
|
|
17
|
+
+if('!empty')
|
|
18
|
+
| {emptyMessage}
|
|
15
19
|
+each('col.cards as card, i (cardId(card) ?? i)')
|
|
16
20
|
article.kanban-card(
|
|
17
21
|
draggable!="{movable}"
|
|
@@ -19,12 +23,16 @@
|
|
|
19
23
|
onclick!="{oncardclick ? () => oncardclick(card) : undefined}"
|
|
20
24
|
class!="{oncardclick ? 'kanban-card-clickable' : ''}"
|
|
21
25
|
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
+if('renderCard')
|
|
27
|
+
| {@render renderCard(card)}
|
|
28
|
+
+if('!renderCard')
|
|
29
|
+
.kanban-card-title {titleOf(card)}
|
|
30
|
+
+if('subtitle && card[subtitle] != null')
|
|
31
|
+
.kanban-card-subtitle {card[subtitle]}
|
|
25
32
|
|
|
26
33
|
<script>
|
|
27
34
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
35
|
+
import { optimisticOverlay } from '@human-synthesis/norns-ui/behaviors/optimistic.svelte.js'
|
|
28
36
|
|
|
29
37
|
{
|
|
30
38
|
data = {}
|
|
@@ -34,11 +42,13 @@
|
|
|
34
42
|
onMove = undefined
|
|
35
43
|
oncardclick = undefined
|
|
36
44
|
emptyMessage = 'No cards'
|
|
45
|
+
card: renderCard = undefined
|
|
46
|
+
empty = undefined
|
|
37
47
|
class: extra = ''
|
|
38
48
|
} .= $props()
|
|
39
49
|
|
|
40
50
|
// local overlay so a drop lands instantly; server truth wins on reload
|
|
41
|
-
moves
|
|
51
|
+
moves := optimisticOverlay()
|
|
42
52
|
|
|
43
53
|
movable := $derived onMove !== undefined
|
|
44
54
|
rootClasses := $derived cn('kanban', extra)
|
|
@@ -70,7 +80,7 @@
|
|
|
70
80
|
const byKey = new Map(out.map((c) => [c.key, c]))
|
|
71
81
|
for (const [key, cards] of Object.entries(grouped))
|
|
72
82
|
for (const card of Array.isArray(cards) ? cards : [])
|
|
73
|
-
const to = moves
|
|
83
|
+
const to = moves.get(cardId(card), key)
|
|
74
84
|
byKey.get(to)?.cards.push(card)
|
|
75
85
|
out
|
|
76
86
|
}
|
|
@@ -109,7 +119,7 @@
|
|
|
109
119
|
{ card, from } := dragged
|
|
110
120
|
dragged = undefined
|
|
111
121
|
return if from === to
|
|
112
|
-
moves
|
|
122
|
+
moves.set(cardId(card), to)
|
|
113
123
|
submitMove(card, to)
|
|
114
124
|
</script>
|
|
115
125
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
pre.live-log(class!="{classes}" bind:this!="{el}" aria-live="polite")
|
|
2
|
+
| {lines.join('\n')}
|
|
3
|
+
|
|
4
|
+
<script>
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
import { streamSource as openStream, roomChannel as openRoom } from '@human-synthesis/norns/live-client'
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
streamSource = undefined
|
|
10
|
+
roomChannel = undefined
|
|
11
|
+
receive = '*'
|
|
12
|
+
maxLines = 200
|
|
13
|
+
class: extra = ''
|
|
14
|
+
} .= $props()
|
|
15
|
+
|
|
16
|
+
lines .= $state([])
|
|
17
|
+
el .= $state(undefined)
|
|
18
|
+
|
|
19
|
+
classes := $derived cn('live-log', extra)
|
|
20
|
+
|
|
21
|
+
append := (frame) =>
|
|
22
|
+
line := typeof frame === 'string' ? frame : JSON.stringify(frame)
|
|
23
|
+
lines = [...lines, line].slice(-Math.max(1, Number(maxLines) || 200))
|
|
24
|
+
|
|
25
|
+
$effect =>
|
|
26
|
+
return unless roomChannel
|
|
27
|
+
ch := openRoom(roomChannel)
|
|
28
|
+
off := ch.on(receive, append)
|
|
29
|
+
=>
|
|
30
|
+
off()
|
|
31
|
+
ch.close()
|
|
32
|
+
|
|
33
|
+
$effect =>
|
|
34
|
+
return unless streamSource
|
|
35
|
+
let stopped = false
|
|
36
|
+
(async =>
|
|
37
|
+
try
|
|
38
|
+
for await (const frame of openStream(streamSource))
|
|
39
|
+
return if stopped
|
|
40
|
+
append(frame)
|
|
41
|
+
catch
|
|
42
|
+
return
|
|
43
|
+
)()
|
|
44
|
+
=> stopped = true
|
|
45
|
+
|
|
46
|
+
$effect =>
|
|
47
|
+
lines
|
|
48
|
+
el?.scrollTo?.(0, el.scrollHeight)
|
|
49
|
+
return
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.live-log {
|
|
54
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
55
|
+
font-size: 0.8125rem;
|
|
56
|
+
line-height: 1.5;
|
|
57
|
+
max-height: 20rem;
|
|
58
|
+
overflow-y: auto;
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem);
|
|
61
|
+
border-radius: var(--radius-md, 0.375rem);
|
|
62
|
+
background: var(--color-surface-2, color-mix(in oklab, currentColor 5%, transparent));
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.presence-avatars(class!="{classes}" role="status" aria-label!="{`${clients} ${label}`}")
|
|
2
|
+
AvatarGroup(items!="{people}" max!="{max}" size!="{size}" label!="{label}")
|
|
3
|
+
+if('showCount')
|
|
4
|
+
span.presence-avatars-count {clients} {label}
|
|
5
|
+
|
|
6
|
+
<script>
|
|
7
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
8
|
+
import { roomChannel as openRoom } from '@human-synthesis/norns/live-client'
|
|
9
|
+
import AvatarGroup from './AvatarGroup.n'
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
roomChannel = undefined
|
|
13
|
+
max = 5
|
|
14
|
+
size = 'sm'
|
|
15
|
+
label = 'online'
|
|
16
|
+
showCount = true
|
|
17
|
+
class: extra = ''
|
|
18
|
+
} .= $props()
|
|
19
|
+
|
|
20
|
+
clients .= $state(0)
|
|
21
|
+
|
|
22
|
+
classes := $derived cn('presence-avatars', extra)
|
|
23
|
+
people := $derived Array.from({ length: Math.min(clients, Number(max) || 5) }, (_, i) => ({ name: `Guest ${i + 1}` }))
|
|
24
|
+
|
|
25
|
+
$effect =>
|
|
26
|
+
return unless roomChannel
|
|
27
|
+
ch := openRoom(roomChannel)
|
|
28
|
+
off := ch.on('presence', (frame) => clients = Number(frame?.clients) || 0)
|
|
29
|
+
=>
|
|
30
|
+
off()
|
|
31
|
+
ch.close()
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.presence-avatars {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--spacing-2, 0.5rem);
|
|
39
|
+
}
|
|
40
|
+
.presence-avatars-count {
|
|
41
|
+
font-size: 0.8125rem;
|
|
42
|
+
opacity: 0.7;
|
|
43
|
+
font-variant-numeric: tabular-nums;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.stream-text(class!="{classes}" aria-live="polite" aria-busy!="{streaming}")
|
|
2
|
+
| {text}
|
|
3
|
+
+if('streaming')
|
|
4
|
+
span.stream-text-cursor(aria-hidden="true")
|
|
5
|
+
|
|
6
|
+
<script>
|
|
7
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
8
|
+
import { streamSource as openStream } from '@human-synthesis/norns/live-client'
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
streamSource = undefined
|
|
12
|
+
input = undefined
|
|
13
|
+
autostart = true
|
|
14
|
+
onframe = undefined
|
|
15
|
+
ondone = undefined
|
|
16
|
+
class: extra = ''
|
|
17
|
+
} .= $props()
|
|
18
|
+
|
|
19
|
+
text .= $state('')
|
|
20
|
+
streaming .= $state(false)
|
|
21
|
+
|
|
22
|
+
classes := $derived cn('stream-text', extra)
|
|
23
|
+
|
|
24
|
+
start := async (override) =>
|
|
25
|
+
return unless streamSource
|
|
26
|
+
return if streaming
|
|
27
|
+
text = ''
|
|
28
|
+
streaming = true
|
|
29
|
+
body := override ?? input
|
|
30
|
+
try
|
|
31
|
+
opts := body === undefined ? {} : { input: body }
|
|
32
|
+
for await (const frame of openStream(streamSource, opts))
|
|
33
|
+
delta := typeof frame === 'string' ? frame : (frame?.delta ?? '')
|
|
34
|
+
text += delta
|
|
35
|
+
onframe?.(frame)
|
|
36
|
+
finally
|
|
37
|
+
streaming = false
|
|
38
|
+
ondone?.(text)
|
|
39
|
+
|
|
40
|
+
$effect =>
|
|
41
|
+
start() if autostart
|
|
42
|
+
return
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.stream-text {
|
|
47
|
+
white-space: pre-wrap;
|
|
48
|
+
overflow-wrap: anywhere;
|
|
49
|
+
}
|
|
50
|
+
.stream-text-cursor {
|
|
51
|
+
display: inline-block;
|
|
52
|
+
width: 0.55ch;
|
|
53
|
+
height: 1em;
|
|
54
|
+
margin-left: 0.15ch;
|
|
55
|
+
vertical-align: text-bottom;
|
|
56
|
+
background: currentColor;
|
|
57
|
+
animation: stream-text-blink 1s steps(2, start) infinite;
|
|
58
|
+
}
|
|
59
|
+
@keyframes stream-text-blink {
|
|
60
|
+
to {
|
|
61
|
+
visibility: hidden;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</style>
|
package/src/components/Table.n
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
stickyHeader!="{stickyHeader}"
|
|
8
8
|
emptyMessage!="{emptyMessage}"
|
|
9
9
|
onrowclick!="{onrowclick}"
|
|
10
|
+
cell!="{cell}"
|
|
11
|
+
empty!="{empty}"
|
|
10
12
|
bind:sortKey
|
|
11
13
|
bind:sortDir
|
|
12
14
|
)
|
|
@@ -29,6 +31,8 @@
|
|
|
29
31
|
stickyHeader = false
|
|
30
32
|
emptyMessage = 'No data'
|
|
31
33
|
onrowclick
|
|
34
|
+
cell = undefined
|
|
35
|
+
empty = undefined
|
|
32
36
|
class: extra = ''
|
|
33
37
|
} .= $props()
|
|
34
38
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
ol(class!="{classes}")
|
|
2
|
-
+each('
|
|
2
|
+
+each('list as item, i')
|
|
3
3
|
li.timeline-item
|
|
4
4
|
.timeline-dot(class!="{item.variant ? `timeline-dot-${item.variant}` : ''}")
|
|
5
5
|
+if('item.icon')
|
|
6
6
|
Icon(name!="{item.icon}" size="size-3")
|
|
7
|
-
+if('i <
|
|
7
|
+
+if('i < list.length - 1')
|
|
8
8
|
.timeline-line
|
|
9
9
|
.timeline-body
|
|
10
10
|
+if('item.time')
|
|
@@ -19,9 +19,11 @@ ol(class!="{classes}")
|
|
|
19
19
|
import Icon from './Icon.n'
|
|
20
20
|
|
|
21
21
|
{
|
|
22
|
-
items =
|
|
22
|
+
items = undefined
|
|
23
|
+
data = undefined
|
|
23
24
|
class: extra = ''
|
|
24
25
|
} := $props()
|
|
25
26
|
|
|
27
|
+
list := items ?? data ?? []
|
|
26
28
|
classes := cn 'timeline', extra
|
|
27
29
|
</script>
|
package/src/components/Tree.n
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
ul.tree(class!="{classes}" role="tree")
|
|
2
|
-
+each('
|
|
2
|
+
+each('list as node')
|
|
3
3
|
| {@render renderNode(node, 0)}
|
|
4
4
|
|
|
5
5
|
+snippet('renderNode', node, depth)
|
|
@@ -33,12 +33,14 @@ ul.tree(class!="{classes}" role="tree")
|
|
|
33
33
|
import Icon from './Icon.n'
|
|
34
34
|
|
|
35
35
|
{
|
|
36
|
-
items =
|
|
36
|
+
items = undefined
|
|
37
|
+
data = undefined
|
|
37
38
|
expanded = $bindable([])
|
|
38
39
|
selected = $bindable(undefined)
|
|
39
40
|
class: extra = ''
|
|
40
41
|
} .= $props()
|
|
41
42
|
|
|
43
|
+
list := items ?? data ?? []
|
|
42
44
|
classes := cn 'tree', extra
|
|
43
45
|
|
|
44
46
|
hasChildren := (node) => Array.isArray(node.children) && node.children.length > 0
|