@c2-markdown-ui/svelte 0.0.0-stage → 0.4.3
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/README.md +91 -2
- package/package.json +36 -4
- package/src/lib/MarkdownUI.svelte +60 -0
- package/src/lib/Navbar.svelte +144 -0
- package/src/lib/WidgetEvents.svelte +139 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/widgets/Button.svelte +30 -0
- package/src/lib/widgets/ButtonGroup.svelte +37 -0
- package/src/lib/widgets/Cascader.svelte +486 -0
- package/src/lib/widgets/Chart.svelte +278 -0
- package/src/lib/widgets/DatePicker.svelte +609 -0
- package/src/lib/widgets/Form.svelte +571 -0
- package/src/lib/widgets/Incomplete.svelte +72 -0
- package/src/lib/widgets/MultipleChoiceQuestion.svelte +65 -0
- package/src/lib/widgets/NumberInput.svelte +149 -0
- package/src/lib/widgets/Quiz.svelte +301 -0
- package/src/lib/widgets/Radio.svelte +180 -0
- package/src/lib/widgets/Row.svelte +140 -0
- package/src/lib/widgets/Select.svelte +276 -0
- package/src/lib/widgets/SelectMulti.svelte +130 -0
- package/src/lib/widgets/ShortAnswerQuestion.svelte +57 -0
- package/src/lib/widgets/Slider.svelte +46 -0
- package/src/lib/widgets/Switch.svelte +60 -0
- package/src/lib/widgets/Text.svelte +58 -0
- package/src/lib/widgets/TextInput.svelte +58 -0
- package/src/lib/widgets/TreeSelect.svelte +941 -0
- package/src/lib/widgets/Widget.svelte +90 -0
- package/src/lib/widgets/chartRemoteData.ts +84 -0
- package/src/lib/widgets/index.ts +23 -0
- package/src/lib/widgets/remoteChoices.ts +90 -0
- package/src/lib/widgets.css +1754 -0
package/README.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @c2-markdown-ui/svelte
|
|
2
|
+
**Transform Markdown into interactive Svelte components—instantly.**
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
Turn static docs into live experiences users can click, select, and submit.
|
|
5
|
+
|
|
6
|
+
✨ [Try the live demo](https://markdown-ui.com/) ✨
|
|
7
|
+
|
|
8
|
+
## Get started in 30 seconds
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @c2-markdown-ui/svelte @c2-markdown-ui/marked-ext marked
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```svelte
|
|
15
|
+
<script>
|
|
16
|
+
import { MarkdownUI } from '@c2-markdown-ui/svelte';
|
|
17
|
+
import '@c2-markdown-ui/svelte/widgets.css';
|
|
18
|
+
import { Marked } from 'marked';
|
|
19
|
+
import { markedUiExtension } from '@c2-markdown-ui/marked-ext';
|
|
20
|
+
|
|
21
|
+
const marked = new Marked();
|
|
22
|
+
marked.use(markedUiExtension);
|
|
23
|
+
|
|
24
|
+
const markdown = `
|
|
25
|
+
\`\`\`markdown-ui-widget
|
|
26
|
+
select env [dev staging prod]
|
|
27
|
+
\`\`\`
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
function handleWidgetEvent(event) {
|
|
31
|
+
console.log('User selected:', event.detail);
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<MarkdownUI
|
|
36
|
+
html={marked.parse(markdown)}
|
|
37
|
+
onwidgetevent={handleWidgetEvent}
|
|
38
|
+
/>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What you can build
|
|
42
|
+
|
|
43
|
+
**Quick inputs**
|
|
44
|
+
```svelte
|
|
45
|
+
text-input username "Username" "Enter name"
|
|
46
|
+
button-group env [dev staging prod] dev
|
|
47
|
+
select region [us-east us-west] us-east
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Complex forms**
|
|
51
|
+
```svelte
|
|
52
|
+
form deploy "Launch"
|
|
53
|
+
text-input name "App Name"
|
|
54
|
+
slider replicas 1 10 1 3
|
|
55
|
+
select env [dev prod] dev
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Interactive charts**
|
|
59
|
+
```svelte
|
|
60
|
+
chart-line
|
|
61
|
+
title: Monthly Revenue
|
|
62
|
+
height: 300
|
|
63
|
+
Month,Sales,Target
|
|
64
|
+
Jan,45000,50000
|
|
65
|
+
Feb,52000,50000
|
|
66
|
+
Mar,48000,55000
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Events made simple**
|
|
70
|
+
```svelte
|
|
71
|
+
function handleWidgetEvent(event) {
|
|
72
|
+
const { id, value } = event.detail;
|
|
73
|
+
// That's it - perfect event data
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## TypeScript ready
|
|
78
|
+
|
|
79
|
+
Full type safety out of the box. All components and events are properly typed for the best developer experience.
|
|
80
|
+
|
|
81
|
+
## Works everywhere
|
|
82
|
+
|
|
83
|
+
**SSR-safe by design.** No special setup needed for SvelteKit or any Svelte framework.
|
|
84
|
+
|
|
85
|
+
```svelte
|
|
86
|
+
// Just import and use - SSR handled automatically
|
|
87
|
+
import { MarkdownUI } from '@c2-markdown-ui/svelte';
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Compatibility:** Svelte 5+, SvelteKit, Vite, and all modern Svelte tooling.
|
|
91
|
+
|
|
92
|
+
MIT © 2025
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
+
"version": "0.4.3",
|
|
3
|
+
"description": "svelte renderer for markdown-ui",
|
|
2
4
|
"name": "@c2-markdown-ui/svelte",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/lib/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/lib/index.ts",
|
|
9
|
+
"./widgets.css": "./src/lib/widgets.css"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/lib",
|
|
13
|
+
"README.md",
|
|
14
|
+
"!src/lib/assets"
|
|
15
|
+
],
|
|
16
|
+
"svelte": "./src/lib/index.ts",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "vite dev",
|
|
19
|
+
"build": "vite build",
|
|
20
|
+
"preview": "vite preview",
|
|
21
|
+
"prepare": "svelte-kit sync || echo ''",
|
|
22
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
23
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@sveltejs/adapter-auto": "^6.1.0",
|
|
27
|
+
"@sveltejs/kit": "^2.36.2",
|
|
28
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.3",
|
|
29
|
+
"svelte": "^5.38.3",
|
|
30
|
+
"svelte-check": "^4.3.1"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"morphdom": "^2.7.8"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import morphdom from 'morphdom';
|
|
4
|
+
|
|
5
|
+
// Import Widget to register the custom element
|
|
6
|
+
import './widgets/Widget.svelte';
|
|
7
|
+
|
|
8
|
+
let { onwidgetevent, html } = $props();
|
|
9
|
+
let container: HTMLElement | undefined = $state();
|
|
10
|
+
|
|
11
|
+
onMount(() => {
|
|
12
|
+
container?.addEventListener('widget-event', handleChange as EventListener);
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
container?.removeEventListener('widget-event', handleChange as EventListener);
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function handleChange(e: CustomEvent<{id: string, value: unknown}>) {
|
|
20
|
+
// 未传入 onwidgetevent 时静默忽略(表单等交互组件仍会触发 widget-event)
|
|
21
|
+
if (typeof onwidgetevent !== 'function') return;
|
|
22
|
+
onwidgetevent(e);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 增量更新:用 morphdom 对比新旧 DOM,替换原先的 {@html html}——它在 html 变化时
|
|
26
|
+
// 销毁重建整个容器,导致所有 widget 的内部状态(用户已输入的值)被清空。
|
|
27
|
+
// 匹配规则:morphdom 默认按元素 id 匹配,markdown-ui-widget 均有唯一 id,
|
|
28
|
+
// 因此编辑属性/插入新组件时未变化的 widget 元素实例原样保留,输入状态不丢失。
|
|
29
|
+
let isFirstRender = true;
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (!container) return;
|
|
32
|
+
if (isFirstRender) {
|
|
33
|
+
container.innerHTML = html;
|
|
34
|
+
isFirstRender = false;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// 注意:morphdom 会把 DocumentFragment 替换成 firstElementChild——空片段时得到
|
|
38
|
+
// null 直接崩溃(Cannot read properties of null (reading 'nodeType')),且会丢失
|
|
39
|
+
// 其余顶层节点。因此这里必须用普通元素包裹,配合 childrenOnly 对比子节点。
|
|
40
|
+
const wrapper = document.createElement('div');
|
|
41
|
+
wrapper.innerHTML = html;
|
|
42
|
+
morphdom(container, wrapper, {
|
|
43
|
+
childrenOnly: true,
|
|
44
|
+
onBeforeElUpdated(fromEl, toEl) {
|
|
45
|
+
// 正在聚焦的元素跳过更新(防止外部重渲染打断输入)
|
|
46
|
+
if (fromEl === document.activeElement) return false;
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
onBeforeElChildrenUpdated(fromEl) {
|
|
50
|
+
// 自定义元素的子内容由其内部 Svelte 实例渲染,新模板中的 widget 是空壳,
|
|
51
|
+
// 必须跳过子节点 diff——否则已渲染内容会被清空。
|
|
52
|
+
// 属性(content/id)仍正常更新,属性变化会驱动内部组件按新 props 重渲染
|
|
53
|
+
if (fromEl.tagName === 'MARKDOWN-UI-WIDGET') return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div bind:this={container} class="widget-container"></div>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { page } from '$app/stores';
|
|
3
|
+
|
|
4
|
+
const navItems = [
|
|
5
|
+
{ href: '/', label: 'Home' },
|
|
6
|
+
{ href: 'https://markdown-ui-chat.blueprintlab.io', label: 'Chat Demo' },
|
|
7
|
+
{ href: '/streaming', label: 'Streaming Demo' },
|
|
8
|
+
{ href: '/editor', label: 'Editor Demo' },
|
|
9
|
+
{ href: '/spec', label: 'Specification' },
|
|
10
|
+
{ href: 'https://github.com/BlueprintDesignLab/markdown-ui', label: 'github' }
|
|
11
|
+
];
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<nav class="navbar">
|
|
15
|
+
<div class="nav-container">
|
|
16
|
+
<h1 class="nav-title">C2 Markdown UI</h1>
|
|
17
|
+
<ul class="nav-links">
|
|
18
|
+
{#each navItems as item}
|
|
19
|
+
<li>
|
|
20
|
+
<a
|
|
21
|
+
href={item.href}
|
|
22
|
+
class:active={$page.url.pathname === item.href}
|
|
23
|
+
target={item.href.startsWith("https") ? '_blank' : undefined}
|
|
24
|
+
rel={item.href.startsWith("https") ? 'noopener noreferrer' : undefined}
|
|
25
|
+
>
|
|
26
|
+
{item.label}
|
|
27
|
+
</a>
|
|
28
|
+
</li>
|
|
29
|
+
{/each}
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.navbar {
|
|
36
|
+
background: #ffffff;
|
|
37
|
+
border-bottom: 1px solid #f0f0f0;
|
|
38
|
+
position: sticky;
|
|
39
|
+
top: 0;
|
|
40
|
+
z-index: 100;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.nav-container {
|
|
44
|
+
max-width: 1200px;
|
|
45
|
+
margin: 0 auto;
|
|
46
|
+
padding: 0 2rem;
|
|
47
|
+
display: flex;
|
|
48
|
+
justify-content: space-between;
|
|
49
|
+
align-items: center;
|
|
50
|
+
height: 4rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.nav-title {
|
|
54
|
+
font-size: 1.5rem;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
color: rgba(0, 0, 0, 0.88);
|
|
57
|
+
margin: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.nav-links {
|
|
61
|
+
display: flex;
|
|
62
|
+
list-style: none;
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
gap: 2rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.nav-links a {
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
color: rgba(0, 0, 0, 0.65);
|
|
71
|
+
font-weight: 500;
|
|
72
|
+
padding: 0.5rem 1rem;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
transition: all 0.2s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.nav-links a:hover {
|
|
78
|
+
color: rgba(0, 0, 0, 0.88);
|
|
79
|
+
background: #fafafa;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.nav-links a.active {
|
|
83
|
+
color: #1677ff;
|
|
84
|
+
background: #e6f4ff;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@media (max-width: 768px) {
|
|
88
|
+
.nav-container {
|
|
89
|
+
padding: 0 1rem;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
height: auto;
|
|
92
|
+
gap: 1rem;
|
|
93
|
+
padding-top: 1rem;
|
|
94
|
+
padding-bottom: 1rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.nav-title {
|
|
98
|
+
font-size: 1.25rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.nav-links {
|
|
102
|
+
gap: 0.75rem;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.nav-links a {
|
|
108
|
+
padding: 0.5rem 0.75rem;
|
|
109
|
+
font-size: 0.875rem;
|
|
110
|
+
min-width: 80px;
|
|
111
|
+
text-align: center;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@media (max-width: 480px) {
|
|
116
|
+
.nav-container {
|
|
117
|
+
padding: 0 0.75rem;
|
|
118
|
+
gap: 0.75rem;
|
|
119
|
+
padding-top: 0.75rem;
|
|
120
|
+
padding-bottom: 0.75rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.nav-title {
|
|
124
|
+
font-size: 1.1rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.nav-links {
|
|
128
|
+
gap: 0.5rem;
|
|
129
|
+
width: 100%;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.nav-links li {
|
|
133
|
+
flex: 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.nav-links a {
|
|
137
|
+
padding: 0.625rem 0.5rem;
|
|
138
|
+
font-size: 0.8rem;
|
|
139
|
+
width: 100%;
|
|
140
|
+
display: block;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let { events = [], onClear = null, showClearButton = false } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<div class="events-panel">
|
|
6
|
+
<div class="panel-header">
|
|
7
|
+
<h3>Widget Events</h3>
|
|
8
|
+
<div class="header-controls">
|
|
9
|
+
<div class="events-count">
|
|
10
|
+
{events.length} events
|
|
11
|
+
</div>
|
|
12
|
+
{#if showClearButton && onClear}
|
|
13
|
+
<button class="clear-btn" onclick={onClear}>
|
|
14
|
+
Clear Events
|
|
15
|
+
</button>
|
|
16
|
+
{/if}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="events-list">
|
|
20
|
+
{#if events.length === 0}
|
|
21
|
+
<p class="no-events">No events yet. Interact with widgets in the preview!</p>
|
|
22
|
+
{/if}
|
|
23
|
+
|
|
24
|
+
{#each events as event}
|
|
25
|
+
<div class="event-item">
|
|
26
|
+
<div class="event-header">
|
|
27
|
+
<span class="event-id">ID: {event.id}</span>
|
|
28
|
+
<span class="event-time">{event.timestamp}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="event-value">
|
|
31
|
+
{JSON.stringify(event.value, null, 2)}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
{/each}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.events-panel {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
border-right: 1px solid #f0f0f0;
|
|
43
|
+
min-height: 0;
|
|
44
|
+
border-right: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.panel-header {
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
align-items: center;
|
|
51
|
+
padding: 1rem;
|
|
52
|
+
background: #ffffff;
|
|
53
|
+
border-bottom: 1px solid #f0f0f0;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.header-controls {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 1rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.panel-header h3 {
|
|
64
|
+
margin: 0;
|
|
65
|
+
font-size: 1rem;
|
|
66
|
+
color: rgba(0, 0, 0, 0.88);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.events-count {
|
|
70
|
+
font-size: 0.75rem;
|
|
71
|
+
color: rgba(0, 0, 0, 0.65);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.events-list {
|
|
75
|
+
flex: 1;
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
padding: 1rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.no-events {
|
|
81
|
+
color: rgba(0, 0, 0, 0.65);
|
|
82
|
+
font-style: italic;
|
|
83
|
+
text-align: center;
|
|
84
|
+
margin: 2rem 0;
|
|
85
|
+
font-size: 0.875rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.event-item {
|
|
89
|
+
margin-bottom: 0.75rem;
|
|
90
|
+
padding: 0.75rem;
|
|
91
|
+
background: #fafafa;
|
|
92
|
+
border: 1px solid #d9d9d9;
|
|
93
|
+
border-radius: 6px;
|
|
94
|
+
font-size: 0.75rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.event-header {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 0.25rem;
|
|
101
|
+
margin-bottom: 0.5rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.event-id {
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
color: #1677ff;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.event-time {
|
|
110
|
+
font-size: 0.7rem;
|
|
111
|
+
color: rgba(0, 0, 0, 0.65);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.event-value {
|
|
115
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
116
|
+
background: #f5f5f5;
|
|
117
|
+
padding: 0.5rem;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
color: rgba(0, 0, 0, 0.65);
|
|
120
|
+
word-break: break-all;
|
|
121
|
+
font-size: 0.7rem;
|
|
122
|
+
line-height: 1.4;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.clear-btn {
|
|
126
|
+
padding: 0.25rem 0.75rem;
|
|
127
|
+
background: #ff4d4f;
|
|
128
|
+
color: white;
|
|
129
|
+
border: none;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
font-size: 0.75rem;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
transition: background-color 0.2s;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.clear-btn:hover {
|
|
137
|
+
background: #cf1322;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
label?: string;
|
|
4
|
+
/** 背景色(十六进制或 CSS 颜色名) */
|
|
5
|
+
color?: string;
|
|
6
|
+
/** 字体颜色(十六进制或 CSS 颜色名) */
|
|
7
|
+
textColor?: string;
|
|
8
|
+
/** 尺寸:small / medium / large,默认 medium */
|
|
9
|
+
size?: 'small' | 'medium' | 'large';
|
|
10
|
+
/** 是否禁用 */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onchange: (value: string) => void;
|
|
13
|
+
}
|
|
14
|
+
let { label = '按钮', color, textColor, size = 'medium', disabled = false, onchange }: Props = $props();
|
|
15
|
+
|
|
16
|
+
// 组合内联样式:背景色 + 字体颜色
|
|
17
|
+
const style = $derived(
|
|
18
|
+
[color ? `background-color: ${color};` : '', textColor ? `color: ${textColor};` : ''].join('') || undefined
|
|
19
|
+
);
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="widget-button size-{size}"
|
|
25
|
+
{style}
|
|
26
|
+
{disabled}
|
|
27
|
+
onclick={() => onchange(label)}
|
|
28
|
+
>
|
|
29
|
+
{label}
|
|
30
|
+
</button>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { parseChoices, type RemoteChoice } from './remoteChoices';
|
|
3
|
+
interface Props {
|
|
4
|
+
choices: string[];
|
|
5
|
+
label?: string;
|
|
6
|
+
default?: string;
|
|
7
|
+
/** 是否禁用 */
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onchange: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
let { choices, label = '', default: initial, disabled = false, onchange }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const items = $derived(parseChoices(choices));
|
|
14
|
+
let selected = $state(initial ?? items[0]?.value ?? '');
|
|
15
|
+
|
|
16
|
+
function pick(item: RemoteChoice) {
|
|
17
|
+
if (disabled) return;
|
|
18
|
+
selected = item.value;
|
|
19
|
+
onchange(item.value);
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="widget-button-group">
|
|
24
|
+
{#if label}<label>{label}</label>{/if}
|
|
25
|
+
<div role="group" aria-label={label}>
|
|
26
|
+
{#each items as item (item.value)}
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
aria-pressed={selected === item.value}
|
|
30
|
+
{disabled}
|
|
31
|
+
onclick={() => pick(item)}
|
|
32
|
+
>
|
|
33
|
+
{item.label}
|
|
34
|
+
</button>
|
|
35
|
+
{/each}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|