Skip to content

Commit a571197

Browse files
committed
feat: add code-block component
1 parent de7f3c6 commit a571197

File tree

13 files changed

+414
-24
lines changed

13 files changed

+414
-24
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ AI Elements Vue includes the following components:
8888
| `prompt-input` | ✅ 已完成 | Advanced input component with model selection |
8989
| `actions` | ✅ 已完成 | Interactive action buttons for AI responses |
9090
| `branch` | ✅ 已完成 | Branch visualization for conversation flows |
91-
| `code-block` | ❌ 未完成 | Syntax-highlighted code display with copy functionality |
91+
| `code-block` | ✅ 已完成 | Syntax-highlighted code display with copy functionality |
9292
| `image` | ❌ 未完成 | AI-generated image display component |
9393
| `inline-citation` | ❌ 未完成 | Inline source citations |
9494
| `loader` | ❌ 未完成 | Loading states for AI operations |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { CodeBlock } from '@repo/elements/code-block'
3+
4+
const code = `<template>
5+
<div>
6+
<h1>Hello, {{ name }}!</h1>
7+
<p>This is an example Vue component.</p>
8+
</div>
9+
</template>
10+
11+
<script setup lang="ts">
12+
interface Props {
13+
name: string
14+
}
15+
16+
defineProps<Props>()
17+
<\/script>`
18+
</script>
19+
20+
<template>
21+
<CodeBlock :code="code" lang="vue" />
22+
</template>

apps/test/app/pages/index.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@repo/shadcn-vue/compo
33
import ActionsHover from '~/examples/actions-hover.vue'
44
import Actions from '~/examples/actions.vue'
55
import Branch from '~/examples/branch.vue'
6+
import CodeBlock from '~/examples/code-block.vue'
67
import Conversation from '~/examples/conversation.vue'
78
import MessageMarkdown from '~/examples/message-markdown.vue'
89
import Message from '~/examples/message.vue'
@@ -18,6 +19,7 @@ const components = [
1819
{ name: 'Conversation', Component: Conversation },
1920
{ name: 'Response', Component: Response },
2021
{ name: 'MessageMarkdown', Component: MessageMarkdown },
22+
{ name: 'CodeBlock', Component: CodeBlock },
2123
]
2224
</script>
2325

apps/www/content/2.components/6.branch.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ icon: lucide:git-branch
66

77
The `Branch` component manages multiple versions of AI messages, allowing users to navigate between different response branches. It provides a clean, modern interface with customizable themes and keyboard-accessible navigation buttons.
88

9-
::::ComponentLoader{label="Branch" componentName="Branch"}
10-
::::
9+
:::ComponentLoader{label="Branch" componentName="Branch"}
10+
:::
1111

1212
## Install using CLI
1313

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
title: Code Block
3+
description:
4+
icon: lucide:code
5+
---
6+
7+
The `CodeBlock` component provides syntax highlighting and copy to clipboard functionality for code blocks. It uses Shiki for syntax highlighting and includes automatic light/dark theme switching.
8+
9+
## Install using CLI
10+
11+
:::tabs{variant="card"}
12+
::div{label="ai-elements-vue"}
13+
```sh
14+
npx ai-elements-vue@latest add code-block
15+
```
16+
::
17+
::div{label="shadcn-vue"}
18+
19+
```sh
20+
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/code-block.json
21+
```
22+
::
23+
:::
24+
25+
## Install Manually
26+
27+
Copy and paste the following files into the same folder.
28+
29+
:::code-group
30+
```vue [CodeBlock.vue]
31+
<script setup lang="ts">
32+
import type { BuiltinTheme, BundledLanguage } from 'shiki'
33+
import { transformerCopyButton } from '@selemondev/shiki-transformer-copy-button'
34+
import CodeBlock from 'shiki-block-vue'
35+
36+
interface Props {
37+
code: string
38+
lang: BundledLanguage
39+
}
40+
41+
const props = withDefaults(defineProps<Props>(), {})
42+
43+
const theme: { light: BuiltinTheme, dark: BuiltinTheme } = {
44+
light: 'vitesse-light',
45+
dark: 'vitesse-dark',
46+
}
47+
</script>
48+
49+
<template>
50+
<CodeBlock
51+
:lang="props.lang"
52+
:code="props.code"
53+
:theme="theme"
54+
class="overflow-hidden"
55+
:transformers="[
56+
transformerCopyButton({
57+
duration: 3000,
58+
display: 'ready',
59+
successIcon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E`,
60+
copyIcon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E`,
61+
}),
62+
]"
63+
/>
64+
</template>
65+
66+
<style scoped>
67+
/* Dark Mode */
68+
@media (prefers-color-scheme: dark) {
69+
:deep(.shiki),
70+
:deep(.shiki span) {
71+
color: var(--shiki-dark) !important;
72+
background-color: var(--shiki-dark-bg) !important;
73+
/* Optional, if you also want font styles */
74+
font-style: var(--shiki-dark-font-style) !important;
75+
font-weight: var(--shiki-dark-font-weight) !important;
76+
text-decoration: var(--shiki-dark-text-decoration) !important;
77+
}
78+
}
79+
80+
html.dark :deep(.shiki),
81+
html.dark :deep(.shiki span) {
82+
color: var(--shiki-dark) !important;
83+
background-color: var(--shiki-dark-bg) !important;
84+
font-style: var(--shiki-dark-font-style) !important;
85+
font-weight: var(--shiki-dark-font-weight) !important;
86+
text-decoration: var(--shiki-dark-text-decoration) !important;
87+
}
88+
89+
:deep(.shiki--code--block) {
90+
width: 100%;
91+
}
92+
93+
:deep(pre) {
94+
z-index: 1;
95+
padding: 24px;
96+
border-radius: 10px;
97+
overflow-x: auto;
98+
-ms-overflow-style: none;
99+
scrollbar-width: none;
100+
position: relative;
101+
background-color: #F9F9F9 !important;
102+
}
103+
104+
:deep(code) {
105+
display: block;
106+
line-height: 1.7;
107+
font-size: 15px;
108+
}
109+
</style>
110+
```
111+
112+
```ts [index.ts]
113+
export { default as CodeBlock } from './CodeBlock.vue'
114+
```
115+
:::
116+
117+
## Usage
118+
119+
```vue
120+
<script setup lang="ts">
121+
import { CodeBlock } from '@/components/ai-elements/code-block'
122+
123+
const code = `<template>
124+
<div>
125+
<h1>Hello, {{ name }}!</h1>
126+
<p>This is an example Vue component.</p>
127+
</div>
128+
</template>
129+
130+
<script setup lang="ts">
131+
interface Props {
132+
name: string
133+
}
134+
135+
defineProps<Props>()
136+
<\/script>`
137+
</script>
138+
139+
<template>
140+
<CodeBlock :code="code" lang="vue" />
141+
</template>
142+
```
143+
144+
## Features
145+
146+
- Syntax highlighting with Shiki
147+
- Copy to clipboard functionality
148+
- Automatic light/dark theme switching
149+
- Support for all Shiki bundled languages
150+
- Customizable themes (vitesse-light/vitesse-dark)
151+
- Responsive design with horizontal scrolling
152+
- Clean, modern styling
153+
- Accessible design
154+
155+
## Props
156+
157+
### `<CodeBlock />`
158+
159+
:::field-group
160+
::field{name="code" type="string" required}
161+
The code content to display.
162+
::
163+
164+
::field{name="lang" type="BundledLanguage" required}
165+
The programming language for syntax highlighting. Supports all Shiki bundled languages.
166+
::
167+
:::

apps/www/nuxt.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export default defineNuxtConfig({
1313

1414
components: [
1515
{ path: '~/components' },
16-
{ path: '../../packages/examples/src', pathPrefix: false },
1716
],
1817

1918
i18n: {

packages/elements/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
},
99
"dependencies": {
1010
"@repo/shadcn-vue": "workspace:*",
11+
"@selemondev/shiki-transformer-copy-button": "^0.0.2",
1112
"ai": "^5.0.51",
1213
"lucide-vue-next": "^0.544.0",
14+
"shiki-block-vue": "^1.0.5",
1315
"streamdown-vue": "^1.0.21",
1416
"vue": "^3.5.21",
1517
"vue-stick-to-bottom": "^0.1.0"
1618
},
1719
"devDependencies": {
20+
"shiki": "^3.13.0",
1821
"typescript": "^5.9.2"
1922
}
2023
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<script setup lang="ts">
2+
import type { BuiltinTheme, BundledLanguage } from 'shiki'
3+
import { transformerCopyButton } from '@selemondev/shiki-transformer-copy-button'
4+
import CodeBlock from 'shiki-block-vue'
5+
6+
interface Props {
7+
code: string
8+
lang: BundledLanguage
9+
}
10+
11+
const props = withDefaults(defineProps<Props>(), {})
12+
13+
const theme: { light: BuiltinTheme, dark: BuiltinTheme } = {
14+
light: 'vitesse-light',
15+
dark: 'vitesse-dark',
16+
}
17+
</script>
18+
19+
<template>
20+
<CodeBlock
21+
:lang="props.lang"
22+
:code="props.code"
23+
:theme="theme"
24+
class="overflow-hidden"
25+
:transformers="[
26+
transformerCopyButton({
27+
duration: 3000,
28+
display: 'ready',
29+
successIcon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E`,
30+
copyIcon: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E`,
31+
}),
32+
]"
33+
/>
34+
</template>
35+
36+
<style>
37+
/* Dark Mode */
38+
@media (prefers-color-scheme: dark) {
39+
.shiki,
40+
.shiki span {
41+
color: var(--shiki-dark) !important;
42+
background-color: var(--shiki-dark-bg) !important;
43+
/* Optional, if you also want font styles */
44+
font-style: var(--shiki-dark-font-style) !important;
45+
font-weight: var(--shiki-dark-font-weight) !important;
46+
text-decoration: var(--shiki-dark-text-decoration) !important;
47+
}
48+
}
49+
50+
html.dark .shiki,
51+
html.dark .shiki span {
52+
color: var(--shiki-dark) !important;
53+
background-color: var(--shiki-dark-bg) !important;
54+
font-style: var(--shiki-dark-font-style) !important;
55+
font-weight: var(--shiki-dark-font-weight) !important;
56+
text-decoration: var(--shiki-dark-text-decoration) !important;
57+
}
58+
59+
.shiki--code--block {
60+
width: 100%;
61+
}
62+
63+
pre {
64+
z-index: 1;
65+
padding: 24px;
66+
border-radius: 10px;
67+
overflow-x: auto;
68+
-ms-overflow-style: none;
69+
scrollbar-width: none;
70+
position: relative;
71+
background-color: #F9F9F9 !important;
72+
}
73+
74+
code {
75+
display: block;
76+
line-height: 1.7;
77+
font-size: 15px;
78+
}
79+
</style>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as CodeBlock } from './CodeBlock.vue'

packages/elements/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './actions'
22
export * from './branch'
3+
export * from './code-block'
34
export * from './conversation'
45
export * from './message'
56
export * from './prompt-input'

0 commit comments

Comments
 (0)