@c2-markdown-ui/svelte 0.0.0-stage → 0.4.2

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 CHANGED
@@ -1,3 +1,92 @@
1
- # Temporary Holding Version
1
+ # @c2-markdown-ui/svelte
2
+ **Transform Markdown into interactive Svelte components—instantly.**
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
- "name": "@c2-markdown-ui/svelte",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "version": "0.4.2",
3
+ "description": "svelte renderer for markdown-ui",
4
+ "name": "@c2-markdown-ui/svelte",
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>
@@ -0,0 +1,2 @@
1
+ // Reexport your entry components here
2
+ export { default as MarkdownUI } from './MarkdownUI.svelte';
@@ -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>