@vendure-io/ui 2.0.0-beta.7 → 2.0.0-beta.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendure-io/ui",
3
- "version": "2.0.0-beta.7",
3
+ "version": "2.0.0-beta.9",
4
4
  "description": "React component library for Vendure, built on shadcn/ui and Tailwind v4",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "homepage": "https://github.com/vendurehq/design/tree/main/packages/ui",
@@ -49,7 +49,7 @@
49
49
  "@shikijs/themes": "^3.23.0",
50
50
  "@shikijs/transformers": "^3.22.0",
51
51
  "@tanstack/react-table": "^8.21.3",
52
- "@vendure-io/design-tokens": "^2.0.0-beta.5",
52
+ "@vendure-io/design-tokens": "^2.0.0-beta.7",
53
53
  "class-variance-authority": "^0.7.1",
54
54
  "clsx": "^2.1.1",
55
55
  "cmdk": "^1.1.1",
@@ -53,9 +53,9 @@ interface AnonymizedTokenProps extends Omit<React.ComponentProps<'span'>, 'child
53
53
  copyable?: boolean;
54
54
  /** How long the check-mark feedback stays visible, in ms. */
55
55
  timeout?: CopyButtonProps['timeout'];
56
- /** Called after a successful copy. Wire your toast here. */
56
+ /** Called after a successful copy. Wire your toast here. Falls back to `CopyFeedbackProvider`. */
57
57
  onCopied?: CopyButtonProps['onCopied'];
58
- /** Called when the clipboard write fails. */
58
+ /** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
59
59
  onCopyError?: CopyButtonProps['onCopyError'];
60
60
  /** Accessible label before copying. @default "Copy token" */
61
61
  copyLabel?: CopyButtonProps['copyLabel'];
@@ -1,16 +1,15 @@
1
1
  'use client';
2
2
 
3
- import {
4
- transformerNotationDiff,
5
- transformerNotationErrorLevel,
6
- transformerNotationFocus,
7
- transformerNotationHighlight,
8
- transformerNotationWordHighlight,
9
- } from '@shikijs/transformers';
10
3
  import { Button } from '@vendure-io/ui/components/atoms/button';
11
4
  import { ScrollArea, ScrollBar } from '@vendure-io/ui/components/atoms/scroll-area';
12
5
  import { Tooltip, TooltipContent, TooltipTrigger } from '@vendure-io/ui/components/atoms/tooltip';
6
+ import { useCopyFeedback } from '@vendure-io/ui/components/molecules/copy-feedback-provider';
13
7
  import { useCopy } from '@vendure-io/ui/hooks/use-copy';
8
+ import {
9
+ highlightCode,
10
+ normalizeLanguage,
11
+ type SupportedLanguage,
12
+ } from '@vendure-io/ui/lib/highlight';
14
13
  import { cn } from '@vendure-io/ui/lib/utils';
15
14
  import {
16
15
  CheckIcon,
@@ -32,8 +31,6 @@ import {
32
31
  useRef,
33
32
  useState,
34
33
  } from 'react';
35
- import { createHighlighterCore } from 'shiki/core';
36
- import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
37
34
 
38
35
  // =============================================================================
39
36
  // Types
@@ -56,9 +53,14 @@ type CodeBlockProps = PropsWithChildren<ComponentProps<'div'>> & {
56
53
  packageManagerSwitcher?: boolean;
57
54
  /** Extra toolbar actions rendered before the built-in copy button. Compose with CodeBlockAction. */
58
55
  actions?: ReactNode;
59
- /** Called after a successful copy. Wire your toast here. The DS never toasts. */
56
+ /**
57
+ * Called after a successful copy. Wire your toast here. The DS never toasts.
58
+ * Function props can't cross an RSC boundary — when rendering from a server
59
+ * component, omit this and mount `CopyFeedbackProvider` instead; it is the
60
+ * fallback when no prop is passed.
61
+ */
60
62
  onCopied?: () => void;
61
- /** Called when the clipboard write fails. */
63
+ /** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
62
64
  onCopyError?: (error: Error) => void;
63
65
  };
64
66
 
@@ -81,53 +83,6 @@ const STORAGE_EVENT = 'vendure-ui-package-manager-change';
81
83
  const COLLAPSE_THRESHOLD = 40; // Lines needed to trigger collapse
82
84
  const COLLAPSED_HEIGHT = 300; // px height when collapsed (roughly 15-20 lines)
83
85
 
84
- const languageLoaders = {
85
- bash: () => import('@shikijs/langs/bash').then((module) => module.default),
86
- css: () => import('@shikijs/langs/css').then((module) => module.default),
87
- dotenv: () => import('@shikijs/langs/dotenv').then((module) => module.default),
88
- graphql: () => import('@shikijs/langs/graphql').then((module) => module.default),
89
- html: () => import('@shikijs/langs/html').then((module) => module.default),
90
- ini: () => import('@shikijs/langs/ini').then((module) => module.default),
91
- javascript: () => import('@shikijs/langs/javascript').then((module) => module.default),
92
- json: () => import('@shikijs/langs/json').then((module) => module.default),
93
- jsonc: () => import('@shikijs/langs/jsonc').then((module) => module.default),
94
- jsx: () => import('@shikijs/langs/jsx').then((module) => module.default),
95
- markdown: () => import('@shikijs/langs/markdown').then((module) => module.default),
96
- mdx: () => import('@shikijs/langs/mdx').then((module) => module.default),
97
- python: () => import('@shikijs/langs/python').then((module) => module.default),
98
- shellscript: () => import('@shikijs/langs/shellscript').then((module) => module.default),
99
- sql: () => import('@shikijs/langs/sql').then((module) => module.default),
100
- tsx: () => import('@shikijs/langs/tsx').then((module) => module.default),
101
- typescript: () => import('@shikijs/langs/typescript').then((module) => module.default),
102
- yaml: () => import('@shikijs/langs/yaml').then((module) => module.default),
103
- } as const;
104
-
105
- type SupportedLanguage = keyof typeof languageLoaders;
106
-
107
- const themeLoaders = [
108
- () => import('@shikijs/themes/github-light').then((module) => module.default),
109
- () => import('@shikijs/themes/github-dark-default').then((module) => module.default),
110
- ] as const;
111
-
112
- let highlighterPromise: ReturnType<typeof createHighlighterCore> | null = null;
113
-
114
- function getHighlighter(): ReturnType<typeof createHighlighterCore> {
115
- highlighterPromise ??= Promise.all([
116
- Promise.all(themeLoaders.map((loadTheme) => loadTheme())),
117
- Promise.all(Object.values(languageLoaders).map((loadLanguage) => loadLanguage())),
118
- ]).then(([themes, languages]) =>
119
- createHighlighterCore({
120
- themes,
121
- langs: languages.flat(),
122
- engine: createJavaScriptRegexEngine(),
123
- }),
124
- );
125
-
126
- return highlighterPromise;
127
- }
128
-
129
- const highlightedCodeCache = new Map<string, Promise<string>>();
130
-
131
86
  /**
132
87
  * Get the stored package manager preference from localStorage
133
88
  */
@@ -445,83 +400,6 @@ function processCode(
445
400
  };
446
401
  }
447
402
 
448
- // =============================================================================
449
- // Shiki Highlighting
450
- // =============================================================================
451
-
452
- /**
453
- * Highlight code using Shiki with all transformers.
454
- * Uses Shiki's native notation for highlighting:
455
- * - // [!code highlight] - highlight a line (use language-appropriate comment)
456
- * - // [!code ++] / // [!code --] - diff highlighting
457
- * - // [!code focus] - focus mode (blur other lines)
458
- * - // [!code word:myVar] - highlight specific word
459
- * - // [!code error] / // [!code warning] - error levels
460
- */
461
- async function highlightCode(code: string, language: SupportedLanguage): Promise<string> {
462
- const cacheKey = `${language}:${code}`;
463
- const cached = highlightedCodeCache.get(cacheKey);
464
- if (cached) return cached;
465
-
466
- const highlighted = getHighlighter().then((highlighter) =>
467
- highlighter.codeToHtml(code, {
468
- lang: language,
469
- themes: {
470
- light: 'github-light',
471
- dark: 'github-dark-default',
472
- },
473
- transformers: [
474
- transformerNotationDiff({ matchAlgorithm: 'v3' }),
475
- transformerNotationHighlight({ matchAlgorithm: 'v3' }),
476
- transformerNotationWordHighlight({ matchAlgorithm: 'v3' }),
477
- transformerNotationFocus({ matchAlgorithm: 'v3' }),
478
- transformerNotationErrorLevel({ matchAlgorithm: 'v3' }),
479
- ],
480
- }),
481
- );
482
- highlightedCodeCache.set(cacheKey, highlighted);
483
- return highlighted;
484
- }
485
-
486
- /**
487
- * Language aliases that map to Shiki's bundled language names
488
- */
489
- const LANGUAGE_ALIASES: Record<string, SupportedLanguage> = {
490
- env: 'dotenv',
491
- js: 'javascript',
492
- ts: 'typescript',
493
- sh: 'bash',
494
- shell: 'bash',
495
- yml: 'yaml',
496
- py: 'python',
497
- md: 'markdown',
498
- plaintext: 'ini',
499
- text: 'ini',
500
- chroma: 'ini',
501
- };
502
-
503
- /**
504
- * Normalize language identifier for Shiki/BundledLanguage.
505
- * Falls back to 'ini' for unsupported languages (minimal highlighting).
506
- */
507
- function normalizeLanguage(lang?: string): SupportedLanguage {
508
- const normalized = lang?.toLowerCase() || 'ini';
509
-
510
- // Check for aliases first
511
- const alias = LANGUAGE_ALIASES[normalized];
512
- if (alias) {
513
- return alias;
514
- }
515
-
516
- // Check if the language is one of the explicitly bundled grammars.
517
- if (normalized in languageLoaders) {
518
- return normalized as SupportedLanguage;
519
- }
520
-
521
- // Fallback to 'ini' for unsupported languages (has minimal highlighting)
522
- return 'ini';
523
- }
524
-
525
403
  /**
526
404
  * Escape HTML special characters for safe display (fallback)
527
405
  */
@@ -975,7 +853,9 @@ function SyntaxHighlightedContent({ code, language }: SyntaxHighlightedContentPr
975
853
  * long snippets, and an optional npm→pnpm/yarn/bun package-manager switcher.
976
854
  * Highlighting is lazy (a shared highlighter singleton + per-snippet cache), and
977
855
  * line/diff/focus/word decorations use Shiki's `[!code ...]` notations. The DS
978
- * never toasts. Wire `onCopied`/`onCopyError` to your own feedback.
856
+ * never toasts. Wire `onCopied`/`onCopyError` to your own feedback — or, when
857
+ * rendering from server components (where function props can't be passed),
858
+ * mount `CopyFeedbackProvider` once and leave the props off.
979
859
  */
980
860
  export function CodeBlock({
981
861
  className,
@@ -990,6 +870,7 @@ export function CodeBlock({
990
870
  ...props
991
871
  }: CodeBlockProps) {
992
872
  const { copied, copy } = useCopy();
873
+ const copyFeedback = useCopyFeedback();
993
874
  const [activePackageManager, setActivePackageManager] = usePackageManager();
994
875
  const [isExpanded, setIsExpanded] = useState(false);
995
876
  const containerRef = useRef<HTMLDivElement>(null);
@@ -1035,8 +916,8 @@ export function CodeBlock({
1035
916
 
1036
917
  const handleCopy = async () => {
1037
918
  const ok = await copy(displayCode);
1038
- if (ok) onCopied?.();
1039
- else onCopyError?.(new Error('Failed to copy to the clipboard'));
919
+ if (ok) (onCopied ?? copyFeedback.onCopied)?.();
920
+ else (onCopyError ?? copyFeedback.onCopyError)?.(new Error('Failed to copy to the clipboard'));
1040
921
  };
1041
922
 
1042
923
  const toolbar = <CodeBlockToolbar onCopy={handleCopy} isCopied={copied} actions={actions} />;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { createContext, type ReactNode, useContext, useMemo } from 'react';
4
+
5
+ // Function props can't cross a server→client boundary, so a copy surface
6
+ // rendered from RSC (e.g. MDX docs) could never receive an `onCopied` callback
7
+ // for toast wiring. This context is the RSC-safe alternative: mount
8
+ // CopyFeedbackProvider once in a client component (wire your toast there — the
9
+ // DS never toasts), and copy surfaces resolve their feedback in a fixed order —
10
+ // explicit prop → this context → nothing beyond the built-in copied icon.
11
+ interface CopyFeedbackContextValue {
12
+ /** Called after a successful copy. Wire your toast here — the DS never toasts. */
13
+ onCopied?: () => void;
14
+ /** Called when the clipboard write fails. */
15
+ onCopyError?: (error: Error) => void;
16
+ }
17
+
18
+ const CopyFeedbackContext = createContext<CopyFeedbackContextValue>({});
19
+
20
+ function CopyFeedbackProvider({
21
+ children,
22
+ onCopied,
23
+ onCopyError,
24
+ }: CopyFeedbackContextValue & { children: ReactNode }) {
25
+ const value = useMemo(() => ({ onCopied, onCopyError }), [onCopied, onCopyError]);
26
+ return <CopyFeedbackContext.Provider value={value}>{children}</CopyFeedbackContext.Provider>;
27
+ }
28
+
29
+ function useCopyFeedback(): CopyFeedbackContextValue {
30
+ return useContext(CopyFeedbackContext);
31
+ }
32
+
33
+ export { CopyFeedbackProvider, useCopyFeedback, type CopyFeedbackContextValue };
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Button } from '@vendure-io/ui/components/atoms/button';
4
+ import { useCopyFeedback } from '@vendure-io/ui/components/molecules/copy-feedback-provider';
4
5
  import { useCopy } from '@vendure-io/ui/hooks/use-copy';
5
6
  import { cn } from '@vendure-io/ui/lib/utils';
6
7
  import { CheckIcon, CopyIcon } from 'lucide-react';
@@ -11,9 +12,16 @@ interface CopyButtonProps extends Omit<React.ComponentProps<typeof Button>, 'val
11
12
  value: string;
12
13
  /** How long the check-mark feedback stays visible, in ms. @default 2000 */
13
14
  timeout?: number;
14
- /** Called after a successful copy. Wire your toast here — the DS never toasts. */
15
+ /**
16
+ * Called after a successful copy. Wire your toast here — the DS never toasts.
17
+ * Falls back to `CopyFeedbackProvider` when omitted (the RSC-safe path, since
18
+ * function props can't be passed from server components).
19
+ */
15
20
  onCopied?: () => void;
16
- /** Called when the clipboard write fails (e.g. permissions, insecure context). */
21
+ /**
22
+ * Called when the clipboard write fails (e.g. permissions, insecure context).
23
+ * Falls back to `CopyFeedbackProvider` when omitted.
24
+ */
17
25
  onCopyError?: (error: Error) => void;
18
26
  /** Accessible label before copying. @default "Copy" */
19
27
  copyLabel?: string;
@@ -40,6 +48,7 @@ function CopyButton({
40
48
  ...props
41
49
  }: CopyButtonProps) {
42
50
  const { copied, copy } = useCopy({ timeout });
51
+ const copyFeedback = useCopyFeedback();
43
52
 
44
53
  return (
45
54
  <Button
@@ -53,8 +62,9 @@ function CopyButton({
53
62
  onClick?.(event);
54
63
  if (event.defaultPrevented) return;
55
64
  const ok = await copy(value);
56
- if (ok) onCopied?.();
57
- else onCopyError?.(new Error('Failed to copy to the clipboard'));
65
+ if (ok) (onCopied ?? copyFeedback.onCopied)?.();
66
+ else
67
+ (onCopyError ?? copyFeedback.onCopyError)?.(new Error('Failed to copy to the clipboard'));
58
68
  }}
59
69
  {...props}
60
70
  >
@@ -71,9 +81,9 @@ interface CopyableTextProps {
71
81
  className?: string;
72
82
  /** How long the check-mark feedback stays visible, in ms. @default 2000 */
73
83
  timeout?: number;
74
- /** Called after a successful copy. Wire your toast here — the DS never toasts. */
84
+ /** Called after a successful copy. Wire your toast here — the DS never toasts. Falls back to `CopyFeedbackProvider`. */
75
85
  onCopied?: () => void;
76
- /** Called when the clipboard write fails. */
86
+ /** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
77
87
  onCopyError?: (error: Error) => void;
78
88
  }
79
89
 
@@ -40,7 +40,7 @@ interface IdChipProps {
40
40
  /** Render the copy affordance. @default true */
41
41
  copyable?: boolean;
42
42
  className?: string;
43
- /** Called after a successful copy. Wire your toast here — the DS never toasts. */
43
+ /** Called after a successful copy. Wire your toast here — the DS never toasts. Falls back to `CopyFeedbackProvider`. */
44
44
  onCopied?: () => void;
45
45
  }
46
46
 
@@ -0,0 +1,141 @@
1
+ import {
2
+ transformerNotationDiff,
3
+ transformerNotationErrorLevel,
4
+ transformerNotationFocus,
5
+ transformerNotationHighlight,
6
+ transformerNotationWordHighlight,
7
+ } from '@shikijs/transformers';
8
+ import { createHighlighterCore } from 'shiki/core';
9
+ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
10
+
11
+ /**
12
+ * The design system's Shiki setup: a lazy shared highlighter with the DS themes
13
+ * (github-light / github-dark-default), a fixed grammar set, and the `[!code ...]`
14
+ * notation transformers. Exported so consumers rendering highlighted HTML outside
15
+ * of `CodeBlock` (e.g. custom docs pipelines) reuse the exact same setup instead
16
+ * of duplicating it — and get the lazy per-language chunks instead of the full
17
+ * Shiki bundle.
18
+ */
19
+
20
+ const languageLoaders = {
21
+ bash: () => import('@shikijs/langs/bash').then((module) => module.default),
22
+ css: () => import('@shikijs/langs/css').then((module) => module.default),
23
+ dotenv: () => import('@shikijs/langs/dotenv').then((module) => module.default),
24
+ graphql: () => import('@shikijs/langs/graphql').then((module) => module.default),
25
+ html: () => import('@shikijs/langs/html').then((module) => module.default),
26
+ ini: () => import('@shikijs/langs/ini').then((module) => module.default),
27
+ javascript: () => import('@shikijs/langs/javascript').then((module) => module.default),
28
+ json: () => import('@shikijs/langs/json').then((module) => module.default),
29
+ jsonc: () => import('@shikijs/langs/jsonc').then((module) => module.default),
30
+ jsx: () => import('@shikijs/langs/jsx').then((module) => module.default),
31
+ markdown: () => import('@shikijs/langs/markdown').then((module) => module.default),
32
+ mdx: () => import('@shikijs/langs/mdx').then((module) => module.default),
33
+ python: () => import('@shikijs/langs/python').then((module) => module.default),
34
+ shellscript: () => import('@shikijs/langs/shellscript').then((module) => module.default),
35
+ sql: () => import('@shikijs/langs/sql').then((module) => module.default),
36
+ tsx: () => import('@shikijs/langs/tsx').then((module) => module.default),
37
+ typescript: () => import('@shikijs/langs/typescript').then((module) => module.default),
38
+ yaml: () => import('@shikijs/langs/yaml').then((module) => module.default),
39
+ } as const;
40
+
41
+ type SupportedLanguage = keyof typeof languageLoaders;
42
+
43
+ const themeLoaders = [
44
+ () => import('@shikijs/themes/github-light').then((module) => module.default),
45
+ () => import('@shikijs/themes/github-dark-default').then((module) => module.default),
46
+ ] as const;
47
+
48
+ let highlighterPromise: ReturnType<typeof createHighlighterCore> | null = null;
49
+
50
+ function getHighlighter(): ReturnType<typeof createHighlighterCore> {
51
+ highlighterPromise ??= Promise.all([
52
+ Promise.all(themeLoaders.map((loadTheme) => loadTheme())),
53
+ Promise.all(Object.values(languageLoaders).map((loadLanguage) => loadLanguage())),
54
+ ]).then(([themes, languages]) =>
55
+ createHighlighterCore({
56
+ themes,
57
+ langs: languages.flat(),
58
+ engine: createJavaScriptRegexEngine(),
59
+ }),
60
+ );
61
+
62
+ return highlighterPromise;
63
+ }
64
+
65
+ const highlightedCodeCache = new Map<string, Promise<string>>();
66
+
67
+ /**
68
+ * Highlight code using Shiki with all transformers.
69
+ * Uses Shiki's native notation for highlighting:
70
+ * - // [!code highlight] - highlight a line (use language-appropriate comment)
71
+ * - // [!code ++] / // [!code --] - diff highlighting
72
+ * - // [!code focus] - focus mode (blur other lines)
73
+ * - // [!code word:myVar] - highlight specific word
74
+ * - // [!code error] / // [!code warning] - error levels
75
+ */
76
+ async function highlightCode(code: string, language: SupportedLanguage): Promise<string> {
77
+ const cacheKey = `${language}:${code}`;
78
+ const cached = highlightedCodeCache.get(cacheKey);
79
+ if (cached) return cached;
80
+
81
+ const highlighted = getHighlighter().then((highlighter) =>
82
+ highlighter.codeToHtml(code, {
83
+ lang: language,
84
+ themes: {
85
+ light: 'github-light',
86
+ dark: 'github-dark-default',
87
+ },
88
+ transformers: [
89
+ transformerNotationDiff({ matchAlgorithm: 'v3' }),
90
+ transformerNotationHighlight({ matchAlgorithm: 'v3' }),
91
+ transformerNotationWordHighlight({ matchAlgorithm: 'v3' }),
92
+ transformerNotationFocus({ matchAlgorithm: 'v3' }),
93
+ transformerNotationErrorLevel({ matchAlgorithm: 'v3' }),
94
+ ],
95
+ }),
96
+ );
97
+ highlightedCodeCache.set(cacheKey, highlighted);
98
+ return highlighted;
99
+ }
100
+
101
+ /**
102
+ * Language aliases that map to Shiki's bundled language names
103
+ */
104
+ const LANGUAGE_ALIASES: Record<string, SupportedLanguage> = {
105
+ env: 'dotenv',
106
+ js: 'javascript',
107
+ ts: 'typescript',
108
+ sh: 'bash',
109
+ shell: 'bash',
110
+ yml: 'yaml',
111
+ py: 'python',
112
+ md: 'markdown',
113
+ plaintext: 'ini',
114
+ text: 'ini',
115
+ chroma: 'ini',
116
+ };
117
+
118
+ /**
119
+ * Normalize language identifier for Shiki/BundledLanguage.
120
+ * Falls back to 'ini' for unsupported languages (minimal highlighting).
121
+ */
122
+ function normalizeLanguage(lang?: string): SupportedLanguage {
123
+ const normalized = lang?.toLowerCase() || 'ini';
124
+
125
+ // Check for aliases first
126
+ const alias = LANGUAGE_ALIASES[normalized];
127
+ if (alias) {
128
+ return alias;
129
+ }
130
+
131
+ // Check if the language is one of the explicitly bundled grammars.
132
+ if (normalized in languageLoaders) {
133
+ return normalized as SupportedLanguage;
134
+ }
135
+
136
+ // Fallback to 'ini' for unsupported languages (has minimal highlighting)
137
+ return 'ini';
138
+ }
139
+
140
+ export { getHighlighter, highlightCode, normalizeLanguage };
141
+ export type { SupportedLanguage };