xertica-ui 1.8.0 → 1.8.1

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.
@@ -1,263 +1,263 @@
1
- import React, { useState } from 'react';
2
- import { Check, Copy } from 'lucide-react';
3
- import { Button } from '../../ui/button';
4
- import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5
-
6
- // Elegant custom theme inspired by Xertica.ai design system
7
- const elegantTheme = {
8
- 'code[class*="language-"]': {
9
- color: 'var(--foreground)',
10
- fontFamily: 'var(--font-mono)',
11
- fontSize: '0.875rem',
12
- textAlign: 'left' as const,
13
- whiteSpace: 'pre' as const,
14
- wordSpacing: 'normal',
15
- wordBreak: 'normal',
16
- lineHeight: '1.6',
17
- tabSize: 2,
18
- hyphens: 'none' as const,
19
- },
20
- 'pre[class*="language-"]': {
21
- color: 'var(--foreground)',
22
- fontFamily: 'var(--font-mono)',
23
- fontSize: '0.875rem',
24
- textAlign: 'left' as const,
25
- whiteSpace: 'pre' as const,
26
- wordSpacing: 'normal',
27
- wordBreak: 'normal',
28
- lineHeight: '1.6',
29
- tabSize: 2,
30
- hyphens: 'none' as const,
31
- padding: '1rem',
32
- margin: 0,
33
- overflow: 'auto',
34
- background: 'transparent',
35
- },
36
- 'comment': {
37
- color: 'var(--muted-foreground)',
38
- fontStyle: 'italic',
39
- },
40
- 'prolog': {
41
- color: 'var(--muted-foreground)',
42
- },
43
- 'doctype': {
44
- color: 'var(--muted-foreground)',
45
- },
46
- 'cdata': {
47
- color: 'var(--muted-foreground)',
48
- },
49
- 'punctuation': {
50
- color: 'var(--muted-foreground)',
51
- },
52
- 'property': {
53
- color: 'var(--chart-4)',
54
- },
55
- 'tag': {
56
- color: 'var(--chart-1)',
57
- },
58
- 'boolean': {
59
- color: 'var(--chart-5)',
60
- },
61
- 'number': {
62
- color: 'var(--chart-5)',
63
- },
64
- 'constant': {
65
- color: 'var(--chart-5)',
66
- },
67
- 'symbol': {
68
- color: 'var(--chart-5)',
69
- },
70
- 'deleted': {
71
- color: 'var(--chart-5)',
72
- },
73
- 'selector': {
74
- color: 'var(--chart-2)',
75
- },
76
- 'attr-name': {
77
- color: 'var(--chart-4)',
78
- },
79
- 'string': {
80
- color: 'var(--chart-2)',
81
- },
82
- 'char': {
83
- color: 'var(--chart-2)',
84
- },
85
- 'builtin': {
86
- color: 'var(--chart-4)',
87
- },
88
- 'inserted': {
89
- color: 'var(--chart-2)',
90
- },
91
- 'operator': {
92
- color: 'var(--primary)',
93
- },
94
- 'entity': {
95
- color: 'var(--chart-4)',
96
- },
97
- 'url': {
98
- color: 'var(--chart-1)',
99
- },
100
- '.language-css .token.string': {
101
- color: 'var(--chart-2)',
102
- },
103
- '.style .token.string': {
104
- color: 'var(--chart-2)',
105
- },
106
- 'atrule': {
107
- color: 'var(--chart-3)',
108
- },
109
- 'attr-value': {
110
- color: 'var(--chart-2)',
111
- },
112
- 'keyword': {
113
- color: 'var(--chart-3)',
114
- },
115
- 'function': {
116
- color: 'var(--chart-1)',
117
- },
118
- 'class-name': {
119
- color: 'var(--chart-4)',
120
- },
121
- 'regex': {
122
- color: 'var(--chart-5)',
123
- },
124
- 'important': {
125
- color: 'var(--chart-5)',
126
- fontWeight: 'bold',
127
- },
128
- 'variable': {
129
- color: 'var(--chart-5)',
130
- },
131
- };
132
-
133
- /**
134
- * Props for the CodeBlock component.
135
- */
136
- interface CodeBlockProps {
137
- /** The code string to be displayed */
138
- code: string;
139
- /** Programming language for syntax highlighting */
140
- language?: 'typescript' | 'tsx' | 'css' | 'bash' | 'jsx';
141
- /** Optional filename to display in the header */
142
- filename?: string;
143
- /** Whether to show line numbers */
144
- showLineNumbers?: boolean;
145
- }
146
-
147
- /**
148
- * Enhanced Syntax Highlighter component with "Copy to Clipboard" functionality.
149
- *
150
- * @description
151
- * Uses `react-syntax-highlighter` with a custom "Elegant" theme inspired by Xertica's
152
- * design tokens. It includes a fallback mechanism for the Clipboard API and
153
- * optionally displays a header with the filename and language.
154
- *
155
- * @ai-rules
156
- * 1. Theme: Uses a custom `elegantTheme` that maps to system CSS variables. Avoid overriding these colors manually.
157
- * 2. Clipboard: The copy functionality is robust with multiple fallback methods.
158
- * 3. Content: Ensure `code` is passed as a raw string without pre-formatting indentation if possible.
159
- */
160
- export const CodeBlock = ({
161
- code,
162
- language = 'tsx',
163
- filename,
164
- showLineNumbers = false
165
- }: CodeBlockProps) => {
166
- const [copied, setCopied] = useState(false);
167
-
168
- const handleCopy = async () => {
169
- try {
170
- // Try modern Clipboard API first (with permission check)
171
- if (navigator.clipboard && navigator.clipboard.writeText) {
172
- try {
173
- await navigator.clipboard.writeText(code);
174
- } catch (clipboardError: any) {
175
- // If clipboard API fails due to permissions, fall back to execCommand
176
- if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
177
- throw new Error('Clipboard permission denied, falling back');
178
- }
179
- throw clipboardError;
180
- }
181
- } else {
182
- throw new Error('Clipboard API not available');
183
- }
184
-
185
- setCopied(true);
186
- setTimeout(() => setCopied(false), 2000);
187
- } catch (err) {
188
- // Fallback for browsers that don't support Clipboard API or when permissions are denied
189
- try {
190
- const textArea = document.createElement('textarea');
191
- textArea.value = code;
192
- textArea.style.position = 'fixed';
193
- textArea.style.left = '-999999px';
194
- textArea.style.top = '-999999px';
195
- document.body.appendChild(textArea);
196
- textArea.focus();
197
- textArea.select();
198
-
199
- const successful = document.execCommand('copy');
200
- document.body.removeChild(textArea);
201
-
202
- if (successful) {
203
- setCopied(true);
204
- setTimeout(() => setCopied(false), 2000);
205
- } else {
206
- console.error('Failed to copy text: execCommand returned false');
207
- }
208
- } catch (fallbackErr) {
209
- console.error('All copy methods failed:', fallbackErr);
210
- }
211
- }
212
- };
213
-
214
- // Ensure code is a string
215
- const codeString = typeof code === 'string' ? code : String(code || '');
216
-
217
- return (
218
- <div className="relative group rounded-[var(--radius)] border border-border overflow-hidden bg-muted/30">
219
- {filename && (
220
- <div className="flex items-center justify-between px-4 py-2 bg-muted border-b border-border">
221
- <span className="text-xs text-muted-foreground font-mono">{filename}</span>
222
- <span className="text-xs text-muted-foreground uppercase">{language}</span>
223
- </div>
224
- )}
225
-
226
- <div className="relative">
227
- <Button
228
- variant="ghost"
229
- size="icon"
230
- className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 bg-background/80 hover:bg-background"
231
- onClick={handleCopy}
232
- aria-label={copied ? "Copiado" : "Copiar código"}
233
- >
234
- {copied ? (
235
- <Check className="w-4 h-4 text-[var(--toast-success-icon)]" aria-hidden="true" />
236
- ) : (
237
- <Copy className="w-4 h-4" aria-hidden="true" />
238
- )}
239
- </Button>
240
-
241
- <div className="overflow-x-auto w-full max-w-full">
242
- <SyntaxHighlighter
243
- language={language}
244
- style={elegantTheme as any}
245
- showLineNumbers={showLineNumbers}
246
- wrapLines={true}
247
- customStyle={{
248
- margin: 0,
249
- padding: '1rem',
250
- background: 'transparent',
251
- fontSize: 'inherit',
252
- }}
253
- codeTagProps={{
254
- className: "text-[length:inherit]"
255
- }}
256
- >
257
- {codeString}
258
- </SyntaxHighlighter>
259
- </div>
260
- </div>
261
- </div>
262
- );
1
+ import React, { useState } from 'react';
2
+ import { Check, Copy } from 'lucide-react';
3
+ import { Button } from '../../ui/button';
4
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5
+
6
+ // Elegant custom theme inspired by Xertica.ai design system
7
+ const elegantTheme = {
8
+ 'code[class*="language-"]': {
9
+ color: 'var(--foreground)',
10
+ fontFamily: 'var(--font-mono)',
11
+ fontSize: '0.875rem',
12
+ textAlign: 'left' as const,
13
+ whiteSpace: 'pre' as const,
14
+ wordSpacing: 'normal',
15
+ wordBreak: 'normal',
16
+ lineHeight: '1.6',
17
+ tabSize: 2,
18
+ hyphens: 'none' as const,
19
+ },
20
+ 'pre[class*="language-"]': {
21
+ color: 'var(--foreground)',
22
+ fontFamily: 'var(--font-mono)',
23
+ fontSize: '0.875rem',
24
+ textAlign: 'left' as const,
25
+ whiteSpace: 'pre' as const,
26
+ wordSpacing: 'normal',
27
+ wordBreak: 'normal',
28
+ lineHeight: '1.6',
29
+ tabSize: 2,
30
+ hyphens: 'none' as const,
31
+ padding: '1rem',
32
+ margin: 0,
33
+ overflow: 'auto',
34
+ background: 'transparent',
35
+ },
36
+ 'comment': {
37
+ color: 'var(--muted-foreground)',
38
+ fontStyle: 'italic',
39
+ },
40
+ 'prolog': {
41
+ color: 'var(--muted-foreground)',
42
+ },
43
+ 'doctype': {
44
+ color: 'var(--muted-foreground)',
45
+ },
46
+ 'cdata': {
47
+ color: 'var(--muted-foreground)',
48
+ },
49
+ 'punctuation': {
50
+ color: 'var(--muted-foreground)',
51
+ },
52
+ 'property': {
53
+ color: 'var(--chart-4)',
54
+ },
55
+ 'tag': {
56
+ color: 'var(--chart-1)',
57
+ },
58
+ 'boolean': {
59
+ color: 'var(--chart-5)',
60
+ },
61
+ 'number': {
62
+ color: 'var(--chart-5)',
63
+ },
64
+ 'constant': {
65
+ color: 'var(--chart-5)',
66
+ },
67
+ 'symbol': {
68
+ color: 'var(--chart-5)',
69
+ },
70
+ 'deleted': {
71
+ color: 'var(--chart-5)',
72
+ },
73
+ 'selector': {
74
+ color: 'var(--chart-2)',
75
+ },
76
+ 'attr-name': {
77
+ color: 'var(--chart-4)',
78
+ },
79
+ 'string': {
80
+ color: 'var(--chart-2)',
81
+ },
82
+ 'char': {
83
+ color: 'var(--chart-2)',
84
+ },
85
+ 'builtin': {
86
+ color: 'var(--chart-4)',
87
+ },
88
+ 'inserted': {
89
+ color: 'var(--chart-2)',
90
+ },
91
+ 'operator': {
92
+ color: 'var(--primary)',
93
+ },
94
+ 'entity': {
95
+ color: 'var(--chart-4)',
96
+ },
97
+ 'url': {
98
+ color: 'var(--chart-1)',
99
+ },
100
+ '.language-css .token.string': {
101
+ color: 'var(--chart-2)',
102
+ },
103
+ '.style .token.string': {
104
+ color: 'var(--chart-2)',
105
+ },
106
+ 'atrule': {
107
+ color: 'var(--chart-3)',
108
+ },
109
+ 'attr-value': {
110
+ color: 'var(--chart-2)',
111
+ },
112
+ 'keyword': {
113
+ color: 'var(--chart-3)',
114
+ },
115
+ 'function': {
116
+ color: 'var(--chart-1)',
117
+ },
118
+ 'class-name': {
119
+ color: 'var(--chart-4)',
120
+ },
121
+ 'regex': {
122
+ color: 'var(--chart-5)',
123
+ },
124
+ 'important': {
125
+ color: 'var(--chart-5)',
126
+ fontWeight: 'bold',
127
+ },
128
+ 'variable': {
129
+ color: 'var(--chart-5)',
130
+ },
131
+ };
132
+
133
+ /**
134
+ * Props for the CodeBlock component.
135
+ */
136
+ interface CodeBlockProps {
137
+ /** The code string to be displayed */
138
+ code: string;
139
+ /** Programming language for syntax highlighting */
140
+ language?: 'typescript' | 'tsx' | 'css' | 'bash' | 'jsx';
141
+ /** Optional filename to display in the header */
142
+ filename?: string;
143
+ /** Whether to show line numbers */
144
+ showLineNumbers?: boolean;
145
+ }
146
+
147
+ /**
148
+ * Enhanced Syntax Highlighter component with "Copy to Clipboard" functionality.
149
+ *
150
+ * @description
151
+ * Uses `react-syntax-highlighter` with a custom "Elegant" theme inspired by Xertica's
152
+ * design tokens. It includes a fallback mechanism for the Clipboard API and
153
+ * optionally displays a header with the filename and language.
154
+ *
155
+ * @ai-rules
156
+ * 1. Theme: Uses a custom `elegantTheme` that maps to system CSS variables. Avoid overriding these colors manually.
157
+ * 2. Clipboard: The copy functionality is robust with multiple fallback methods.
158
+ * 3. Content: Ensure `code` is passed as a raw string without pre-formatting indentation if possible.
159
+ */
160
+ export const CodeBlock = ({
161
+ code,
162
+ language = 'tsx',
163
+ filename,
164
+ showLineNumbers = false
165
+ }: CodeBlockProps) => {
166
+ const [copied, setCopied] = useState(false);
167
+
168
+ const handleCopy = async () => {
169
+ try {
170
+ // Try modern Clipboard API first (with permission check)
171
+ if (navigator.clipboard && navigator.clipboard.writeText) {
172
+ try {
173
+ await navigator.clipboard.writeText(code);
174
+ } catch (clipboardError: any) {
175
+ // If clipboard API fails due to permissions, fall back to execCommand
176
+ if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
177
+ throw new Error('Clipboard permission denied, falling back');
178
+ }
179
+ throw clipboardError;
180
+ }
181
+ } else {
182
+ throw new Error('Clipboard API not available');
183
+ }
184
+
185
+ setCopied(true);
186
+ setTimeout(() => setCopied(false), 2000);
187
+ } catch (err) {
188
+ // Fallback for browsers that don't support Clipboard API or when permissions are denied
189
+ try {
190
+ const textArea = document.createElement('textarea');
191
+ textArea.value = code;
192
+ textArea.style.position = 'fixed';
193
+ textArea.style.left = '-999999px';
194
+ textArea.style.top = '-999999px';
195
+ document.body.appendChild(textArea);
196
+ textArea.focus();
197
+ textArea.select();
198
+
199
+ const successful = document.execCommand('copy');
200
+ document.body.removeChild(textArea);
201
+
202
+ if (successful) {
203
+ setCopied(true);
204
+ setTimeout(() => setCopied(false), 2000);
205
+ } else {
206
+ console.error('Failed to copy text: execCommand returned false');
207
+ }
208
+ } catch (fallbackErr) {
209
+ console.error('All copy methods failed:', fallbackErr);
210
+ }
211
+ }
212
+ };
213
+
214
+ // Ensure code is a string
215
+ const codeString = typeof code === 'string' ? code : String(code || '');
216
+
217
+ return (
218
+ <div className="relative group rounded-[var(--radius)] border border-border overflow-hidden bg-muted/30">
219
+ {filename && (
220
+ <div className="flex items-center justify-between px-4 py-2 bg-muted border-b border-border">
221
+ <span className="text-xs text-muted-foreground font-mono">{filename}</span>
222
+ <span className="text-xs text-muted-foreground uppercase">{language}</span>
223
+ </div>
224
+ )}
225
+
226
+ <div className="relative">
227
+ <Button
228
+ variant="ghost"
229
+ size="icon"
230
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 bg-background/80 hover:bg-background"
231
+ onClick={handleCopy}
232
+ aria-label={copied ? "Copiado" : "Copiar código"}
233
+ >
234
+ {copied ? (
235
+ <Check className="w-4 h-4 text-[var(--toast-success-icon)]" aria-hidden="true" />
236
+ ) : (
237
+ <Copy className="w-4 h-4" aria-hidden="true" />
238
+ )}
239
+ </Button>
240
+
241
+ <div className="overflow-x-auto w-full max-w-full">
242
+ <SyntaxHighlighter
243
+ language={language}
244
+ style={elegantTheme as any}
245
+ showLineNumbers={showLineNumbers}
246
+ wrapLines={true}
247
+ customStyle={{
248
+ margin: 0,
249
+ padding: '1rem',
250
+ background: 'transparent',
251
+ fontSize: 'inherit',
252
+ }}
253
+ codeTagProps={{
254
+ className: "text-[length:inherit]"
255
+ }}
256
+ >
257
+ {codeString}
258
+ </SyntaxHighlighter>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ );
263
263
  };
@@ -1,57 +1,57 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { CodeBlock } from './CodeBlock';
3
- import React from 'react';
4
-
5
- const meta: Meta<typeof CodeBlock> = {
6
- title: 'Assistant/CodeBlock',
7
- component: CodeBlock,
8
- argTypes: {
9
- language: {
10
- control: 'select',
11
- options: ['typescript', 'tsx', 'css', 'bash', 'jsx'],
12
- },
13
- },
14
- };
15
-
16
- export default meta;
17
- type Story = StoryObj<typeof CodeBlock>;
18
-
19
- const exampleCode = `function HelloWorld() {
20
- const [count, setCount] = useState(0);
21
-
22
- return (
23
- <div className="flex flex-col gap-4">
24
- <h1 className="text-2xl font-bold">Counter</h1>
25
- <p>Current count: {count}</p>
26
- <Button onClick={() => setCount(count + 1)}>
27
- Increment
28
- </Button>
29
- </div>
30
- );
31
- }`;
32
-
33
- export const Default: Story = {
34
- args: {
35
- code: exampleCode,
36
- language: 'tsx',
37
- filename: 'Counter.tsx',
38
- showLineNumbers: false,
39
- },
40
- render: (args) => (
41
- <div className="w-full max-w-3xl">
42
- <CodeBlock {...args} />
43
- </div>
44
- ),
45
- };
46
-
47
- export const Simple: Story = {
48
- args: {
49
- code: 'npm install xertica-ui',
50
- language: 'bash',
51
- },
52
- render: (args) => (
53
- <div className="w-full max-w-xl">
54
- <CodeBlock {...args} />
55
- </div>
56
- ),
57
- };
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { CodeBlock } from './CodeBlock';
3
+ import React from 'react';
4
+
5
+ const meta: Meta<typeof CodeBlock> = {
6
+ title: 'Assistant/CodeBlock',
7
+ component: CodeBlock,
8
+ argTypes: {
9
+ language: {
10
+ control: 'select',
11
+ options: ['typescript', 'tsx', 'css', 'bash', 'jsx'],
12
+ },
13
+ },
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof CodeBlock>;
18
+
19
+ const exampleCode = `function HelloWorld() {
20
+ const [count, setCount] = useState(0);
21
+
22
+ return (
23
+ <div className="flex flex-col gap-4">
24
+ <h1 className="text-2xl font-bold">Counter</h1>
25
+ <p>Current count: {count}</p>
26
+ <Button onClick={() => setCount(count + 1)}>
27
+ Increment
28
+ </Button>
29
+ </div>
30
+ );
31
+ }`;
32
+
33
+ export const Default: Story = {
34
+ args: {
35
+ code: exampleCode,
36
+ language: 'tsx',
37
+ filename: 'Counter.tsx',
38
+ showLineNumbers: false,
39
+ },
40
+ render: (args) => (
41
+ <div className="w-full max-w-3xl">
42
+ <CodeBlock {...args} />
43
+ </div>
44
+ ),
45
+ };
46
+
47
+ export const Simple: Story = {
48
+ args: {
49
+ code: 'npm install xertica-ui',
50
+ language: 'bash',
51
+ },
52
+ render: (args) => (
53
+ <div className="w-full max-w-xl">
54
+ <CodeBlock {...args} />
55
+ </div>
56
+ ),
57
+ };