@vendure-io/ui 2.0.0-beta.16 → 2.0.0-beta.17

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/LICENSE.md ADDED
@@ -0,0 +1,34 @@
1
+ # License
2
+
3
+ Copyright (C) Vendure GmbH
4
+
5
+ This software is available under two different licenses:
6
+
7
+ - GNU General Public License version 3 (GPLv3) as Vendure Community Edition
8
+ - Vendure Commercial License (VCL)
9
+
10
+ The default Vendure license, without a valid Vendure Commercial License agreement, is the Open-Source GPLv3 license.
11
+
12
+ ## GNU General Public License version 3 (GPLv3)
13
+
14
+ If you decide to choose the GPLv3 license, you must comply with the following terms:
15
+
16
+ This program is free software: you can redistribute it and/or modify
17
+ it under the terms of the GNU General Public License as published by
18
+ the Free Software Foundation, either version 3 of the License, or
19
+ (at your option) any later version.
20
+
21
+ This program is distributed in the hope that it will be useful,
22
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
23
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
+ GNU General Public License for more details.
25
+
26
+ You should have received a copy of the GNU General Public License
27
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
28
+
29
+ ## Vendure Commercial License (VCL)
30
+
31
+ Alternatively, commercial and supported versions of the program - also known as
32
+ Commercial Distributions - must be used in accordance with the terms and conditions
33
+ contained in a separate written agreement between you and Vendure GmbH.
34
+ For more information about the Vendure Commercial License (VCL) please contact contact@vendure.io.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendure-io/ui",
3
- "version": "2.0.0-beta.16",
3
+ "version": "2.0.0-beta.17",
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",
@@ -21,11 +21,12 @@
21
21
  "src",
22
22
  "!src/**/*.test.ts",
23
23
  "!src/**/*.test.tsx",
24
- "README.md"
24
+ "README.md",
25
+ "LICENSE.md"
25
26
  ],
26
27
  "scripts": {
27
28
  "lint": "biome check src/",
28
- "check-types": "tsc --noEmit",
29
+ "check-types": "tsc --noEmit && tsc --noEmit -p tsconfig.stories.json",
29
30
  "test": "bun test",
30
31
  "pack:check": "bun pm pack --dry-run"
31
32
  },
@@ -49,7 +50,7 @@
49
50
  "@shikijs/themes": "^3.23.0",
50
51
  "@shikijs/transformers": "^3.22.0",
51
52
  "@tanstack/react-table": "^8.21.3",
52
- "@vendure-io/design-tokens": "^2.0.0-beta.7",
53
+ "@vendure-io/design-tokens": "^2.0.0-beta.8",
53
54
  "class-variance-authority": "^0.7.1",
54
55
  "clsx": "^2.1.1",
55
56
  "cmdk": "^1.1.1",
@@ -490,7 +490,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
490
490
  }
491
491
 
492
492
  const sidebarMenuButtonVariants = cva(
493
- "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg:first-child]:size-4.5 [&>svg:first-child]:[stroke-width:2.125] [&_svg]:shrink-0",
493
+ "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&>svg:first-child]:size-4.5 [&>svg:first-child]:[stroke-width:2.125] [&_svg]:shrink-0",
494
494
  {
495
495
  variants: {
496
496
  variant: {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Brand glyphs for common file extensions, shown next to the filename in the
3
+ * header. Path data is inlined from Simple Icons (CC0) so the package takes no
4
+ * icon dependency; glyphs render monochrome via currentColor and inherit the
5
+ * muted header foreground. Unknown extensions fall back to the generic
6
+ * lucide FileIcon.
7
+ */
8
+ interface FileTypeIconDef {
9
+ /** Brand name of the glyph */
10
+ title: string;
11
+ /** SVG path data on a 24x24 viewBox */
12
+ path: string;
13
+ }
14
+
15
+ const typescriptIcon: FileTypeIconDef = {
16
+ title: 'TypeScript',
17
+ path: 'M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z',
18
+ };
19
+
20
+ const reactIcon: FileTypeIconDef = {
21
+ title: 'React',
22
+ path: 'M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z',
23
+ };
24
+
25
+ const javascriptIcon: FileTypeIconDef = {
26
+ title: 'JavaScript',
27
+ path: 'M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z',
28
+ };
29
+
30
+ const jsonIcon: FileTypeIconDef = {
31
+ title: 'JSON',
32
+ path: 'M12.043 23.968c.479-.004.953-.029 1.426-.094a11.805 11.805 0 003.146-.863 12.404 12.404 0 003.793-2.542 11.977 11.977 0 002.44-3.427 11.794 11.794 0 001.02-3.476c.149-1.16.135-2.346-.045-3.499a11.96 11.96 0 00-.793-2.788 11.197 11.197 0 00-.854-1.617c-1.168-1.837-2.861-3.314-4.81-4.3a12.835 12.835 0 00-2.172-.87h-.005c.119.063.24.132.345.201.12.074.239.146.351.225a8.93 8.93 0 011.559 1.33c1.063 1.145 1.797 2.548 2.218 4.041.284.982.434 1.998.495 3.017.044.743.044 1.491-.047 2.229-.149 1.27-.554 2.51-1.228 3.596a7.475 7.475 0 01-1.903 2.084c-1.244.928-2.877 1.482-4.436 1.114a3.916 3.916 0 01-.748-.258 4.692 4.692 0 01-.779-.45 6.08 6.08 0 01-1.244-1.105 6.507 6.507 0 01-1.049-1.747 7.366 7.366 0 01-.494-2.54c-.03-1.273.225-2.553.854-3.67a6.43 6.43 0 011.663-1.918c.225-.178.464-.333.704-.479l.016-.007a5.121 5.121 0 00-1.441-.12 4.963 4.963 0 00-1.228.24c-.359.12-.704.27-1.019.45a6.146 6.146 0 00-.733.494c-.211.18-.42.36-.615.555-1.123 1.153-1.768 2.682-2.022 4.256-.15.973-.15 1.96-.091 2.95.105 1.395.391 2.787.945 4.062a8.518 8.518 0 001.348 2.173 8.14 8.14 0 003.132 2.23 7.934 7.934 0 002.113.54c.074.015.149.015.209.015zm-2.934-.398a4.102 4.102 0 01-.45-.228 8.5 8.5 0 01-2.038-1.534c-1.094-1.137-1.827-2.566-2.247-4.08a15.184 15.184 0 01-.495-3.172 12.14 12.14 0 01.046-2.082c.135-1.257.495-2.501 1.124-3.58a6.889 6.889 0 011.783-2.053 6.23 6.23 0 011.633-.9 5.363 5.363 0 013.522-.045c.029 0 .029 0 .045.03.015.015.045.015.06.03.045.016.104.045.165.074.239.12.479.271.704.42a6.294 6.294 0 012.097 2.502c.42.914.615 1.934.631 2.938.014 1.079-.18 2.157-.645 3.146a6.42 6.42 0 01-2.638 2.832c.09.03.18.045.271.075.225.044.449.074.688.074 1.468.045 2.892-.66 3.94-1.647.195-.18.375-.375.54-.585.225-.27.435-.54.614-.823.239-.375.435-.75.614-1.154a8.112 8.112 0 00.509-1.664c.196-1.004.211-2.022.149-3.026-.135-2.022-.673-4.045-1.842-5.724a9.054 9.054 0 00-.555-.719 9.868 9.868 0 00-1.063-1.034 8.477 8.477 0 00-1.363-.915 9.927 9.927 0 00-1.692-.598l-.3-.06c-.209-.03-.42-.044-.634-.06a8.453 8.453 0 00-1.015.016c-.704.045-1.412.16-2.112.337C5.799 1.227 2.863 3.566 1.3 6.67A11.834 11.834 0 00.238 9.801a11.81 11.81 0 00-.104 3.775c.12 1.02.374 2.023.778 2.977.227.57.511 1.124.825 1.648 1.094 1.783 2.683 3.236 4.51 4.24.688.39 1.408.69 2.157.944.226.074.45.15.689.21z',
33
+ };
34
+
35
+ const htmlIcon: FileTypeIconDef = {
36
+ title: 'HTML5',
37
+ path: 'M1.5 0h21l-1.91 21.563L11.977 24l-8.564-2.438L1.5 0zm7.031 9.75l-.232-2.718 10.059.003.23-2.622L5.412 4.41l.698 8.01h9.126l-.326 3.426-2.91.804-2.955-.81-.188-2.11H6.248l.33 4.171L12 19.351l5.379-1.443.744-8.157H8.531z',
38
+ };
39
+
40
+ const cssIcon: FileTypeIconDef = {
41
+ title: 'CSS',
42
+ path: 'M0 0v20.16A3.84 3.84 0 0 0 3.84 24h16.32A3.84 3.84 0 0 0 24 20.16V3.84A3.84 3.84 0 0 0 20.16 0Zm14.256 13.08c1.56 0 2.28 1.08 2.304 2.64h-1.608c.024-.288-.048-.6-.144-.84-.096-.192-.288-.264-.552-.264-.456 0-.696.264-.696.84-.024.576.288.888.768 1.08.72.288 1.608.744 1.92 1.296q.432.648.432 1.656c0 1.608-.912 2.592-2.496 2.592-1.656 0-2.4-1.032-2.424-2.688h1.68c0 .792.264 1.176.792 1.176.264 0 .456-.072.552-.24.192-.312.24-1.176-.048-1.512-.312-.408-.912-.6-1.32-.816q-.828-.396-1.224-.936c-.24-.36-.36-.888-.36-1.536 0-1.44.936-2.472 2.424-2.448m5.4 0c1.584 0 2.304 1.08 2.328 2.64h-1.608c0-.288-.048-.6-.168-.84-.096-.192-.264-.264-.528-.264-.48 0-.72.264-.72.84s.288.888.792 1.08c.696.288 1.608.744 1.92 1.296.264.432.408.984.408 1.656.024 1.608-.888 2.592-2.472 2.592-1.68 0-2.424-1.056-2.448-2.688h1.68c0 .744.264 1.176.792 1.176.264 0 .456-.072.552-.24.216-.312.264-1.176-.048-1.512-.288-.408-.888-.6-1.32-.816-.552-.264-.96-.576-1.2-.936s-.36-.888-.36-1.536c-.024-1.44.912-2.472 2.4-2.448m-11.031.018c.711-.006 1.419.198 1.839.63.432.432.672 1.128.648 1.992H9.336c.024-.456-.096-.792-.432-.96-.312-.144-.768-.048-.888.24-.12.264-.192.576-.168.864v3.504c0 .744.264 1.128.768 1.128a.65.65 0 0 0 .552-.264c.168-.24.192-.552.168-.84h1.776c.096 1.632-.984 2.712-2.568 2.688-1.536 0-2.496-.864-2.472-2.472v-4.032c0-.816.24-1.44.696-1.848.432-.408 1.146-.624 1.857-.63',
43
+ };
44
+
45
+ const markdownIcon: FileTypeIconDef = {
46
+ title: 'Markdown',
47
+ path: 'M22.27 19.385H1.73A1.73 1.73 0 010 17.655V6.345a1.73 1.73 0 011.73-1.73h20.54A1.73 1.73 0 0124 6.345v11.308a1.73 1.73 0 01-1.73 1.731zM5.769 15.923v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.232 12h-2.309V8.077h-2.307V12h-2.308l3.461 4.039z',
48
+ };
49
+
50
+ const mdxIcon: FileTypeIconDef = {
51
+ title: 'MDX',
52
+ path: 'M.79 7.12h22.42c.436 0 .79.355.79.792v8.176c0 .436-.354.79-.79.79H.79a.79.79 0 0 1-.79-.79V7.912a.79.79 0 0 1 .79-.791V7.12Zm2.507 7.605v-3.122l1.89 1.89L7.12 11.56v3.122h1.055v-5.67l-2.99 2.99L2.24 9.056v5.67h1.055v-.001Zm8.44-1.845-1.474-1.473-.746.746 2.747 2.747 2.745-2.747-.746-.746-1.473 1.473v-4h-1.054v4Zm10.041.987-2.175-2.175 2.22-2.22-.746-.746-2.22 2.22-2.22-2.22-.747.746 2.22 2.22-2.176 2.177.746.746 2.177-2.177 2.176 2.175.745-.746Z',
53
+ };
54
+
55
+ const yamlIcon: FileTypeIconDef = {
56
+ title: 'YAML',
57
+ path: 'm0 .97 4.111 6.453v4.09h2.638v-4.09L11.053.969H8.214L5.58 5.125 2.965.969Zm12.093.024-4.47 10.544h2.114l.97-2.345h4.775l.804 2.345h2.26L14.255.994Zm1.133 2.225 1.463 3.87h-3.096zm3.06 9.475v10.29H24v-2.199h-5.454v-8.091zm-12.175.002v10.335h2.217v-7.129l2.32 4.792h1.746l2.4-4.96v7.295h2.127V12.696h-2.904L9.44 17.37l-2.455-4.674Z',
58
+ };
59
+
60
+ const graphqlIcon: FileTypeIconDef = {
61
+ title: 'GraphQL',
62
+ path: 'M12.002 0a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm8.54 4.931a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm0 9.862a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm-8.54 4.931a2.138 2.138 0 1 0 0 4.276 2.138 2.138 0 1 0 0-4.276zm-8.542-4.93a2.138 2.138 0 1 0 0 4.276 2.138 2.138 0 1 0 0-4.277zm0-9.863a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm8.542-3.378L2.953 6.777v10.448l9.049 5.224 9.047-5.224V6.777zm0 1.601 7.66 13.27H4.34zm-1.387.371L3.97 15.037V7.363zm2.774 0 6.646 3.838v7.674zM5.355 17.44h13.293l-6.646 3.836z',
63
+ };
64
+
65
+ const bashIcon: FileTypeIconDef = {
66
+ title: 'Bash',
67
+ path: 'M21.038,4.9l-7.577-4.498C13.009,0.134,12.505,0,12,0c-0.505,0-1.009,0.134-1.462,0.403L2.961,4.9 C2.057,5.437,1.5,6.429,1.5,7.503v8.995c0,1.073,0.557,2.066,1.462,2.603l7.577,4.497C10.991,23.866,11.495,24,12,24 c0.505,0,1.009-0.134,1.461-0.402l7.577-4.497c0.904-0.537,1.462-1.529,1.462-2.603V7.503C22.5,6.429,21.943,5.437,21.038,4.9z M15.17,18.946l0.013,0.646c0.001,0.078-0.05,0.167-0.111,0.198l-0.383,0.22c-0.061,0.031-0.111-0.007-0.112-0.085L14.57,19.29 c-0.328,0.136-0.66,0.169-0.872,0.084c-0.04-0.016-0.057-0.075-0.041-0.142l0.139-0.584c0.011-0.046,0.036-0.092,0.069-0.121 c0.012-0.011,0.024-0.02,0.036-0.026c0.022-0.011,0.043-0.014,0.062-0.006c0.229,0.077,0.521,0.041,0.802-0.101 c0.357-0.181,0.596-0.545,0.592-0.907c-0.003-0.328-0.181-0.465-0.613-0.468c-0.55,0.001-1.064-0.107-1.072-0.917 c-0.007-0.667,0.34-1.361,0.889-1.8l-0.007-0.652c-0.001-0.08,0.048-0.168,0.111-0.2l0.37-0.236 c0.061-0.031,0.111,0.007,0.112,0.087l0.006,0.653c0.273-0.109,0.511-0.138,0.726-0.088c0.047,0.012,0.067,0.076,0.048,0.151 l-0.144,0.578c-0.011,0.044-0.036,0.088-0.065,0.116c-0.012,0.012-0.025,0.021-0.038,0.028c-0.019,0.01-0.038,0.013-0.057,0.009 c-0.098-0.022-0.332-0.073-0.699,0.113c-0.385,0.195-0.52,0.53-0.517,0.778c0.003,0.297,0.155,0.387,0.681,0.396 c0.7,0.012,1.003,0.318,1.01,1.023C16.105,17.747,15.736,18.491,15.17,18.946z M19.143,17.859c0,0.06-0.008,0.116-0.058,0.145 l-1.916,1.164c-0.05,0.029-0.09,0.004-0.09-0.056v-0.494c0-0.06,0.037-0.093,0.087-0.122l1.887-1.129 c0.05-0.029,0.09-0.004,0.09,0.056V17.859z M20.459,6.797l-7.168,4.427c-0.894,0.523-1.553,1.109-1.553,2.187v8.833 c0,0.645,0.26,1.063,0.66,1.184c-0.131,0.023-0.264,0.039-0.398,0.039c-0.42,0-0.833-0.114-1.197-0.33L3.226,18.64 c-0.741-0.44-1.201-1.261-1.201-2.142V7.503c0-0.881,0.46-1.702,1.201-2.142l7.577-4.498c0.363-0.216,0.777-0.33,1.197-0.33 c0.419,0,0.833,0.114,1.197,0.33l7.577,4.498c0.624,0.371,1.046,1.013,1.164,1.732C21.686,6.557,21.12,6.411,20.459,6.797z',
68
+ };
69
+
70
+ const dockerIcon: FileTypeIconDef = {
71
+ title: 'Docker',
72
+ path: 'M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z',
73
+ };
74
+
75
+ const FILE_TYPE_ICONS: Record<string, FileTypeIconDef> = {
76
+ ts: typescriptIcon,
77
+ mts: typescriptIcon,
78
+ cts: typescriptIcon,
79
+ tsx: reactIcon,
80
+ jsx: reactIcon,
81
+ js: javascriptIcon,
82
+ mjs: javascriptIcon,
83
+ cjs: javascriptIcon,
84
+ json: jsonIcon,
85
+ jsonc: jsonIcon,
86
+ html: htmlIcon,
87
+ css: cssIcon,
88
+ md: markdownIcon,
89
+ mdx: mdxIcon,
90
+ yml: yamlIcon,
91
+ yaml: yamlIcon,
92
+ graphql: graphqlIcon,
93
+ gql: graphqlIcon,
94
+ sh: bashIcon,
95
+ bash: bashIcon,
96
+ zsh: bashIcon,
97
+ dockerfile: dockerIcon,
98
+ };
99
+
100
+ /**
101
+ * Resolve the brand icon for a filename by its extension. Matching uses the
102
+ * basename, so directory names never interfere. `Dockerfile` matches by full
103
+ * basename since it has no extension. Returns undefined for unknown extensions
104
+ * and dotfiles; the header then falls back to the generic file icon.
105
+ */
106
+ function matchFileTypeIcon(filename: string): FileTypeIconDef | undefined {
107
+ const basename = filename.split('/').pop()?.toLowerCase() ?? '';
108
+ if (basename === 'dockerfile') return FILE_TYPE_ICONS.dockerfile;
109
+ const dotIndex = basename.lastIndexOf('.');
110
+ if (dotIndex <= 0) return undefined;
111
+ return FILE_TYPE_ICONS[basename.slice(dotIndex + 1)];
112
+ }
113
+
114
+ function FileTypeIcon({ icon }: { icon: FileTypeIconDef }) {
115
+ return (
116
+ <svg viewBox="0 0 24 24" className="size-3.5 shrink-0 fill-current" aria-hidden="true">
117
+ <path d={icon.path} />
118
+ </svg>
119
+ );
120
+ }
121
+
122
+ export { FileTypeIcon, matchFileTypeIcon };
123
+ export type { FileTypeIconDef };
@@ -0,0 +1,177 @@
1
+ type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
2
+
3
+ const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun'];
4
+
5
+ /**
6
+ * Detects if a single line is an npm/npx command that can be transformed
7
+ */
8
+ function isNpmLine(line: string): boolean {
9
+ const trimmed = line.trim();
10
+ return /^(npm\s+(install|i|add|run|exec|create|init|ci|remove|uninstall|rm)|npx\s+)/.test(
11
+ trimmed,
12
+ );
13
+ }
14
+
15
+ /**
16
+ * Detects if the code contains any package manager commands that can be transformed
17
+ */
18
+ function isPackageManagerCommand(code: string): boolean {
19
+ const lines = code.split('\n');
20
+ return lines.some((line) => isNpmLine(line));
21
+ }
22
+
23
+ /**
24
+ * Transforms a single npm/npx command line to the equivalent for other package
25
+ * managers, preserving the line's leading whitespace. Returns the line unchanged
26
+ * when no rewrite applies.
27
+ */
28
+ function transformSingleLine(line: string, targetPm: PackageManager): string {
29
+ if (targetPm === 'npm') return line;
30
+
31
+ const transformed = transformTrimmedCommand(line.trim(), targetPm);
32
+ if (transformed === null) return line;
33
+
34
+ const indent = line.slice(0, line.length - line.trimStart().length);
35
+ return indent + transformed;
36
+ }
37
+
38
+ function transformTrimmedCommand(
39
+ trimmed: string,
40
+ targetPm: Exclude<PackageManager, 'npm'>,
41
+ ): string | null {
42
+ // Handle npx commands
43
+ if (trimmed.startsWith('npx ')) {
44
+ const rest = trimmed.slice(4);
45
+ switch (targetPm) {
46
+ case 'pnpm':
47
+ return `pnpm dlx ${rest}`;
48
+ case 'yarn':
49
+ return `yarn dlx ${rest}`;
50
+ case 'bun':
51
+ return `bunx ${rest}`;
52
+ }
53
+ }
54
+
55
+ // Handle npm create / npm init
56
+ if (/^npm\s+(create|init)\s+/.test(trimmed)) {
57
+ const match = trimmed.match(/^npm\s+(create|init)\s+(.+)$/);
58
+ if (match) {
59
+ const args = match[2];
60
+ switch (targetPm) {
61
+ case 'pnpm':
62
+ return `pnpm create ${args}`;
63
+ case 'yarn':
64
+ return `yarn create ${args}`;
65
+ case 'bun':
66
+ return `bun create ${args}`;
67
+ }
68
+ }
69
+ }
70
+
71
+ // Handle npm install / npm i / npm add (with packages)
72
+ if (/^npm\s+(install|i|add)\s+/.test(trimmed)) {
73
+ const match = trimmed.match(/^npm\s+(install|i|add)\s+(.+)$/);
74
+ if (match) {
75
+ const packages = match[2] ?? '';
76
+ // Match dev flags as whole tokens so package names containing "-D"
77
+ // don't false-positive.
78
+ const tokens = packages.split(/\s+/);
79
+ const devFlag = tokens.includes('-D') || tokens.includes('--save-dev');
80
+ const cleanPackages = tokens
81
+ .filter((token) => token !== '-D' && token !== '--save-dev')
82
+ .join(' ');
83
+
84
+ switch (targetPm) {
85
+ case 'pnpm':
86
+ return devFlag ? `pnpm add -D ${cleanPackages}` : `pnpm add ${cleanPackages}`;
87
+ case 'yarn':
88
+ return devFlag ? `yarn add -D ${cleanPackages}` : `yarn add ${cleanPackages}`;
89
+ case 'bun':
90
+ return devFlag ? `bun add -d ${cleanPackages}` : `bun add ${cleanPackages}`;
91
+ }
92
+ }
93
+ }
94
+
95
+ // Handle npm install (without packages - install from package.json)
96
+ if (/^npm\s+(install|i|ci)$/.test(trimmed)) {
97
+ switch (targetPm) {
98
+ case 'pnpm':
99
+ return 'pnpm install';
100
+ case 'yarn':
101
+ return 'yarn';
102
+ case 'bun':
103
+ return 'bun install';
104
+ }
105
+ }
106
+
107
+ // Handle npm run <script>
108
+ if (/^npm\s+run\s+/.test(trimmed)) {
109
+ const match = trimmed.match(/^npm\s+run\s+(.+)$/);
110
+ if (match) {
111
+ const script = match[1];
112
+ switch (targetPm) {
113
+ case 'pnpm':
114
+ return `pnpm run ${script}`;
115
+ case 'yarn':
116
+ return `yarn ${script}`;
117
+ case 'bun':
118
+ return `bun run ${script}`;
119
+ }
120
+ }
121
+ }
122
+
123
+ // Handle npm remove / npm uninstall / npm rm
124
+ if (/^npm\s+(remove|uninstall|rm)\s+/.test(trimmed)) {
125
+ const match = trimmed.match(/^npm\s+(remove|uninstall|rm)\s+(.+)$/);
126
+ if (match) {
127
+ const packages = match[2];
128
+ switch (targetPm) {
129
+ case 'pnpm':
130
+ return `pnpm remove ${packages}`;
131
+ case 'yarn':
132
+ return `yarn remove ${packages}`;
133
+ case 'bun':
134
+ return `bun remove ${packages}`;
135
+ }
136
+ }
137
+ }
138
+
139
+ // Handle npm exec
140
+ if (/^npm\s+exec\s+/.test(trimmed)) {
141
+ const match = trimmed.match(/^npm\s+exec\s+(.+)$/);
142
+ if (match) {
143
+ const rest = match[1];
144
+ switch (targetPm) {
145
+ case 'pnpm':
146
+ return `pnpm exec ${rest}`;
147
+ case 'yarn':
148
+ return `yarn exec ${rest}`;
149
+ case 'bun':
150
+ return `bun x ${rest}`;
151
+ }
152
+ }
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ /**
159
+ * Transforms all npm/npx commands in a code block to the target package manager.
160
+ * Leaves non-npm lines unchanged.
161
+ */
162
+ function transformCommand(code: string, targetPm: PackageManager): string {
163
+ if (targetPm === 'npm') return code;
164
+
165
+ const lines = code.split('\n');
166
+ const transformedLines = lines.map((line) => {
167
+ if (isNpmLine(line)) {
168
+ return transformSingleLine(line, targetPm);
169
+ }
170
+ return line;
171
+ });
172
+
173
+ return transformedLines.join('\n');
174
+ }
175
+
176
+ export { PACKAGE_MANAGERS, isPackageManagerCommand, transformCommand };
177
+ export type { PackageManager };
@@ -3,12 +3,23 @@
3
3
  import { Button } from '@vendure-io/ui/components/atoms/button';
4
4
  import { ScrollArea, ScrollBar } from '@vendure-io/ui/components/atoms/scroll-area';
5
5
  import { Tooltip, TooltipContent, TooltipTrigger } from '@vendure-io/ui/components/atoms/tooltip';
6
+ import {
7
+ FileTypeIcon,
8
+ matchFileTypeIcon,
9
+ } from '@vendure-io/ui/components/molecules/code-block/file-type-icons';
10
+ import {
11
+ isPackageManagerCommand,
12
+ PACKAGE_MANAGERS,
13
+ type PackageManager,
14
+ transformCommand,
15
+ } from '@vendure-io/ui/components/molecules/code-block/transform-command';
6
16
  import { useCopyFeedback } from '@vendure-io/ui/components/molecules/copy-feedback-provider';
7
17
  import { useCopy } from '@vendure-io/ui/hooks/use-copy';
8
18
  import {
9
19
  highlightCode,
10
20
  normalizeLanguage,
11
21
  type SupportedLanguage,
22
+ supportsNotationComments,
12
23
  } from '@vendure-io/ui/lib/highlight';
13
24
  import { cn } from '@vendure-io/ui/lib/utils';
14
25
  import {
@@ -21,24 +32,14 @@ import {
21
32
  TerminalIcon,
22
33
  } from 'lucide-react';
23
34
  import { motion } from 'motion/react';
24
- import {
25
- type ComponentProps,
26
- type PropsWithChildren,
27
- type ReactNode,
28
- useEffect,
29
- useId,
30
- useMemo,
31
- useRef,
32
- useState,
33
- } from 'react';
34
-
35
- // =============================================================================
36
- // Types
37
- // =============================================================================
38
-
39
- type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
40
-
41
- type CodeBlockProps = PropsWithChildren<ComponentProps<'div'>> & {
35
+ import { type ComponentProps, type ReactNode, useEffect, useId, useMemo, useState } from 'react';
36
+
37
+ type CodeBlockProps = Omit<ComponentProps<'div'>, 'children'> & {
38
+ /**
39
+ * The code to render, as a plain string. Anything else (elements, numbers)
40
+ * would stringify uselessly, so the type rejects it.
41
+ */
42
+ children?: string;
42
43
  /** Programming language for syntax highlighting */
43
44
  language?: string;
44
45
  /**
@@ -71,15 +72,9 @@ interface CodeBlockActionProps {
71
72
  disabled?: boolean;
72
73
  }
73
74
 
74
- // =============================================================================
75
- // Constants
76
- // =============================================================================
77
-
78
- const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun'];
79
75
  const STORAGE_KEY = 'vendure-ui-package-manager';
80
76
  const STORAGE_EVENT = 'vendure-ui-package-manager-change';
81
77
 
82
- // Expand/collapse constants
83
78
  const COLLAPSE_THRESHOLD = 40; // Lines needed to trigger collapse
84
79
  const COLLAPSED_HEIGHT = 300; // px height when collapsed (roughly 15-20 lines)
85
80
 
@@ -119,7 +114,6 @@ function setStoredPackageManager(pm: PackageManager): void {
119
114
  function usePackageManager(): [PackageManager, (pm: PackageManager) => void] {
120
115
  const [packageManager, setPackageManager] = useState<PackageManager>('npm');
121
116
 
122
- // Initialize from localStorage on mount
123
117
  useEffect(() => {
124
118
  setPackageManager(getStoredPackageManager());
125
119
  }, []);
@@ -144,201 +138,6 @@ function usePackageManager(): [PackageManager, (pm: PackageManager) => void] {
144
138
  return [packageManager, updatePackageManager];
145
139
  }
146
140
 
147
- // =============================================================================
148
- // Utility Functions
149
- // =============================================================================
150
-
151
- /**
152
- * Detects if a single line is an npm/npx command that can be transformed
153
- */
154
- function isNpmLine(line: string): boolean {
155
- const trimmed = line.trim();
156
- return /^(npm\s+(install|i|add|run|exec|create|init|ci|remove|uninstall|rm)|npx\s+)/.test(
157
- trimmed,
158
- );
159
- }
160
-
161
- /**
162
- * Detects if the code contains any package manager commands that can be transformed
163
- */
164
- function isPackageManagerCommand(code: string): boolean {
165
- const lines = code.split('\n');
166
- return lines.some((line) => isNpmLine(line));
167
- }
168
-
169
- /**
170
- * Transforms a single npm/npx command line to the equivalent for other package managers
171
- */
172
- function transformSingleLine(line: string, targetPm: PackageManager): string {
173
- const trimmed = line.trim();
174
-
175
- if (targetPm === 'npm') return line;
176
-
177
- // Handle npx commands
178
- if (trimmed.startsWith('npx ')) {
179
- const rest = trimmed.slice(4);
180
- switch (targetPm) {
181
- case 'pnpm':
182
- return `pnpm dlx ${rest}`;
183
- case 'yarn':
184
- return `yarn dlx ${rest}`;
185
- case 'bun':
186
- return `bunx ${rest}`;
187
- }
188
- }
189
-
190
- // Handle npm create / npm init
191
- if (/^npm\s+(create|init)\s+/.test(trimmed)) {
192
- const match = trimmed.match(/^npm\s+(create|init)\s+(.+)$/);
193
- if (match) {
194
- const args = match[2];
195
- switch (targetPm) {
196
- case 'pnpm':
197
- return `pnpm create ${args}`;
198
- case 'yarn':
199
- return `yarn create ${args}`;
200
- case 'bun':
201
- return `bun create ${args}`;
202
- }
203
- }
204
- }
205
-
206
- // Handle npm install / npm i / npm add (with packages)
207
- if (/^npm\s+(install|i|add)\s+/.test(trimmed)) {
208
- const match = trimmed.match(/^npm\s+(install|i|add)\s+(.+)$/);
209
- if (match) {
210
- const packages = match[2] ?? '';
211
- // Handle flags
212
- const devFlag = packages.includes('-D') || packages.includes('--save-dev');
213
- const cleanPackages = packages
214
- .replace(/\s*-D\s*/, ' ')
215
- .replace(/\s*--save-dev\s*/, ' ')
216
- .trim();
217
-
218
- switch (targetPm) {
219
- case 'pnpm':
220
- return devFlag ? `pnpm add -D ${cleanPackages}` : `pnpm add ${cleanPackages}`;
221
- case 'yarn':
222
- return devFlag ? `yarn add -D ${cleanPackages}` : `yarn add ${cleanPackages}`;
223
- case 'bun':
224
- return devFlag ? `bun add -d ${cleanPackages}` : `bun add ${cleanPackages}`;
225
- }
226
- }
227
- }
228
-
229
- // Handle npm install (without packages - install from package.json)
230
- if (/^npm\s+(install|i|ci)$/.test(trimmed)) {
231
- switch (targetPm) {
232
- case 'pnpm':
233
- return 'pnpm install';
234
- case 'yarn':
235
- return 'yarn';
236
- case 'bun':
237
- return 'bun install';
238
- }
239
- }
240
-
241
- // Handle npm run <script>
242
- if (/^npm\s+run\s+/.test(trimmed)) {
243
- const match = trimmed.match(/^npm\s+run\s+(.+)$/);
244
- if (match) {
245
- const script = match[1];
246
- switch (targetPm) {
247
- case 'pnpm':
248
- return `pnpm run ${script}`;
249
- case 'yarn':
250
- return `yarn ${script}`;
251
- case 'bun':
252
- return `bun run ${script}`;
253
- }
254
- }
255
- }
256
-
257
- // Handle npm remove / npm uninstall / npm rm
258
- if (/^npm\s+(remove|uninstall|rm)\s+/.test(trimmed)) {
259
- const match = trimmed.match(/^npm\s+(remove|uninstall|rm)\s+(.+)$/);
260
- if (match) {
261
- const packages = match[2];
262
- switch (targetPm) {
263
- case 'pnpm':
264
- return `pnpm remove ${packages}`;
265
- case 'yarn':
266
- return `yarn remove ${packages}`;
267
- case 'bun':
268
- return `bun remove ${packages}`;
269
- }
270
- }
271
- }
272
-
273
- // Handle npm exec
274
- if (/^npm\s+exec\s+/.test(trimmed)) {
275
- const match = trimmed.match(/^npm\s+exec\s+(.+)$/);
276
- if (match) {
277
- const rest = match[1];
278
- switch (targetPm) {
279
- case 'pnpm':
280
- return `pnpm exec ${rest}`;
281
- case 'yarn':
282
- return `yarn exec ${rest}`;
283
- case 'bun':
284
- return `bun x ${rest}`;
285
- }
286
- }
287
- }
288
-
289
- return line;
290
- }
291
-
292
- /**
293
- * Transforms all npm/npx commands in a code block to the target package manager.
294
- * Leaves non-npm lines unchanged.
295
- */
296
- function transformCommand(code: string, targetPm: PackageManager): string {
297
- if (targetPm === 'npm') return code;
298
-
299
- const lines = code.split('\n');
300
- const transformedLines = lines.map((line) => {
301
- if (isNpmLine(line)) {
302
- return transformSingleLine(line, targetPm);
303
- }
304
- return line;
305
- });
306
-
307
- return transformedLines.join('\n');
308
- }
309
-
310
- /**
311
- * Languages where Shiki transformers work (have // or # comment syntax)
312
- */
313
- const SHIKI_TRANSFORMER_LANGUAGES = new Set([
314
- 'javascript',
315
- 'js',
316
- 'typescript',
317
- 'ts',
318
- 'tsx',
319
- 'jsx',
320
- 'java',
321
- 'c',
322
- 'cpp',
323
- 'csharp',
324
- 'cs',
325
- 'go',
326
- 'rust',
327
- 'swift',
328
- 'kotlin',
329
- 'scala',
330
- 'php',
331
- 'ruby',
332
- 'python',
333
- 'py',
334
- 'bash',
335
- 'shell',
336
- 'sh',
337
- 'zsh',
338
- 'yaml',
339
- 'yml',
340
- ]);
341
-
342
141
  /**
343
142
  * Process code to extract filename directive and strip Shiki notations for unsupported languages.
344
143
  * Supports: // filename: path/to/file.ts (must be first line)
@@ -349,7 +148,8 @@ const SHIKI_TRANSFORMER_LANGUAGES = new Set([
349
148
  * - // [!code ++] and // [!code --] for diff
350
149
  * - // [!code focus] for focus mode
351
150
  *
352
- * For languages like 'text' that don't support comments, these notations are stripped.
151
+ * For languages where the notation would not survive highlighting (see
152
+ * `supportsNotationComments`), these notations are stripped.
353
153
  */
354
154
  function processCode(
355
155
  code: string,
@@ -362,8 +162,7 @@ function processCode(
362
162
  const cleanLines: string[] = [];
363
163
  let extractedFilename: string | undefined;
364
164
 
365
- // Check if we need to strip Shiki notations (for languages without comment support)
366
- const shouldStripNotations = !SHIKI_TRANSFORMER_LANGUAGES.has(language?.toLowerCase() || '');
165
+ const shouldStripNotations = !supportsNotationComments(language);
367
166
 
368
167
  for (let i = 0; i < lines.length; i++) {
369
168
  let line = lines[i] ?? '';
@@ -378,11 +177,8 @@ function processCode(
378
177
  }
379
178
  }
380
179
 
381
- // Strip Shiki notations for unsupported languages
382
180
  if (shouldStripNotations) {
383
- // Remove // [!code ...] notations
384
181
  line = line.replace(/\s*\/\/\s*\[!code\s+[^\]]+\]\s*$/, '');
385
- // Remove # [!code ...] notations
386
182
  line = line.replace(/\s*#\s*\[!code\s+[^\]]+\]\s*$/, '');
387
183
  // Skip lines that are only the notation (e.g., standalone "// [!code highlight]")
388
184
  if (trimmedLine.match(/^(\/\/|#)\s*\[!code\s+[^\]]+\]\s*$/)) {
@@ -390,7 +186,6 @@ function processCode(
390
186
  }
391
187
  }
392
188
 
393
- // Add line to clean output
394
189
  cleanLines.push(line);
395
190
  }
396
191
 
@@ -412,135 +207,6 @@ function escapeHtml(text: string): string {
412
207
  .replace(/'/g, '&#039;');
413
208
  }
414
209
 
415
- // =============================================================================
416
- // File Type Icons
417
- // =============================================================================
418
-
419
- /**
420
- * Brand glyphs for common file extensions, shown next to the filename in the
421
- * header. Path data is inlined from Simple Icons (CC0) so the package takes no
422
- * icon dependency; glyphs render monochrome via currentColor and inherit the
423
- * muted header foreground. Unknown extensions fall back to the generic
424
- * lucide FileIcon.
425
- */
426
- interface FileTypeIconDef {
427
- /** Brand name of the glyph */
428
- title: string;
429
- /** SVG path data on a 24x24 viewBox */
430
- path: string;
431
- }
432
-
433
- const typescriptIcon: FileTypeIconDef = {
434
- title: 'TypeScript',
435
- path: 'M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z',
436
- };
437
-
438
- const reactIcon: FileTypeIconDef = {
439
- title: 'React',
440
- path: 'M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z',
441
- };
442
-
443
- const javascriptIcon: FileTypeIconDef = {
444
- title: 'JavaScript',
445
- path: 'M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z',
446
- };
447
-
448
- const jsonIcon: FileTypeIconDef = {
449
- title: 'JSON',
450
- path: 'M12.043 23.968c.479-.004.953-.029 1.426-.094a11.805 11.805 0 003.146-.863 12.404 12.404 0 003.793-2.542 11.977 11.977 0 002.44-3.427 11.794 11.794 0 001.02-3.476c.149-1.16.135-2.346-.045-3.499a11.96 11.96 0 00-.793-2.788 11.197 11.197 0 00-.854-1.617c-1.168-1.837-2.861-3.314-4.81-4.3a12.835 12.835 0 00-2.172-.87h-.005c.119.063.24.132.345.201.12.074.239.146.351.225a8.93 8.93 0 011.559 1.33c1.063 1.145 1.797 2.548 2.218 4.041.284.982.434 1.998.495 3.017.044.743.044 1.491-.047 2.229-.149 1.27-.554 2.51-1.228 3.596a7.475 7.475 0 01-1.903 2.084c-1.244.928-2.877 1.482-4.436 1.114a3.916 3.916 0 01-.748-.258 4.692 4.692 0 01-.779-.45 6.08 6.08 0 01-1.244-1.105 6.507 6.507 0 01-1.049-1.747 7.366 7.366 0 01-.494-2.54c-.03-1.273.225-2.553.854-3.67a6.43 6.43 0 011.663-1.918c.225-.178.464-.333.704-.479l.016-.007a5.121 5.121 0 00-1.441-.12 4.963 4.963 0 00-1.228.24c-.359.12-.704.27-1.019.45a6.146 6.146 0 00-.733.494c-.211.18-.42.36-.615.555-1.123 1.153-1.768 2.682-2.022 4.256-.15.973-.15 1.96-.091 2.95.105 1.395.391 2.787.945 4.062a8.518 8.518 0 001.348 2.173 8.14 8.14 0 003.132 2.23 7.934 7.934 0 002.113.54c.074.015.149.015.209.015zm-2.934-.398a4.102 4.102 0 01-.45-.228 8.5 8.5 0 01-2.038-1.534c-1.094-1.137-1.827-2.566-2.247-4.08a15.184 15.184 0 01-.495-3.172 12.14 12.14 0 01.046-2.082c.135-1.257.495-2.501 1.124-3.58a6.889 6.889 0 011.783-2.053 6.23 6.23 0 011.633-.9 5.363 5.363 0 013.522-.045c.029 0 .029 0 .045.03.015.015.045.015.06.03.045.016.104.045.165.074.239.12.479.271.704.42a6.294 6.294 0 012.097 2.502c.42.914.615 1.934.631 2.938.014 1.079-.18 2.157-.645 3.146a6.42 6.42 0 01-2.638 2.832c.09.03.18.045.271.075.225.044.449.074.688.074 1.468.045 2.892-.66 3.94-1.647.195-.18.375-.375.54-.585.225-.27.435-.54.614-.823.239-.375.435-.75.614-1.154a8.112 8.112 0 00.509-1.664c.196-1.004.211-2.022.149-3.026-.135-2.022-.673-4.045-1.842-5.724a9.054 9.054 0 00-.555-.719 9.868 9.868 0 00-1.063-1.034 8.477 8.477 0 00-1.363-.915 9.927 9.927 0 00-1.692-.598l-.3-.06c-.209-.03-.42-.044-.634-.06a8.453 8.453 0 00-1.015.016c-.704.045-1.412.16-2.112.337C5.799 1.227 2.863 3.566 1.3 6.67A11.834 11.834 0 00.238 9.801a11.81 11.81 0 00-.104 3.775c.12 1.02.374 2.023.778 2.977.227.57.511 1.124.825 1.648 1.094 1.783 2.683 3.236 4.51 4.24.688.39 1.408.69 2.157.944.226.074.45.15.689.21z',
451
- };
452
-
453
- const htmlIcon: FileTypeIconDef = {
454
- title: 'HTML5',
455
- path: 'M1.5 0h21l-1.91 21.563L11.977 24l-8.564-2.438L1.5 0zm7.031 9.75l-.232-2.718 10.059.003.23-2.622L5.412 4.41l.698 8.01h9.126l-.326 3.426-2.91.804-2.955-.81-.188-2.11H6.248l.33 4.171L12 19.351l5.379-1.443.744-8.157H8.531z',
456
- };
457
-
458
- const cssIcon: FileTypeIconDef = {
459
- title: 'CSS',
460
- path: 'M0 0v20.16A3.84 3.84 0 0 0 3.84 24h16.32A3.84 3.84 0 0 0 24 20.16V3.84A3.84 3.84 0 0 0 20.16 0Zm14.256 13.08c1.56 0 2.28 1.08 2.304 2.64h-1.608c.024-.288-.048-.6-.144-.84-.096-.192-.288-.264-.552-.264-.456 0-.696.264-.696.84-.024.576.288.888.768 1.08.72.288 1.608.744 1.92 1.296q.432.648.432 1.656c0 1.608-.912 2.592-2.496 2.592-1.656 0-2.4-1.032-2.424-2.688h1.68c0 .792.264 1.176.792 1.176.264 0 .456-.072.552-.24.192-.312.24-1.176-.048-1.512-.312-.408-.912-.6-1.32-.816q-.828-.396-1.224-.936c-.24-.36-.36-.888-.36-1.536 0-1.44.936-2.472 2.424-2.448m5.4 0c1.584 0 2.304 1.08 2.328 2.64h-1.608c0-.288-.048-.6-.168-.84-.096-.192-.264-.264-.528-.264-.48 0-.72.264-.72.84s.288.888.792 1.08c.696.288 1.608.744 1.92 1.296.264.432.408.984.408 1.656.024 1.608-.888 2.592-2.472 2.592-1.68 0-2.424-1.056-2.448-2.688h1.68c0 .744.264 1.176.792 1.176.264 0 .456-.072.552-.24.216-.312.264-1.176-.048-1.512-.288-.408-.888-.6-1.32-.816-.552-.264-.96-.576-1.2-.936s-.36-.888-.36-1.536c-.024-1.44.912-2.472 2.4-2.448m-11.031.018c.711-.006 1.419.198 1.839.63.432.432.672 1.128.648 1.992H9.336c.024-.456-.096-.792-.432-.96-.312-.144-.768-.048-.888.24-.12.264-.192.576-.168.864v3.504c0 .744.264 1.128.768 1.128a.65.65 0 0 0 .552-.264c.168-.24.192-.552.168-.84h1.776c.096 1.632-.984 2.712-2.568 2.688-1.536 0-2.496-.864-2.472-2.472v-4.032c0-.816.24-1.44.696-1.848.432-.408 1.146-.624 1.857-.63',
461
- };
462
-
463
- const markdownIcon: FileTypeIconDef = {
464
- title: 'Markdown',
465
- path: 'M22.27 19.385H1.73A1.73 1.73 0 010 17.655V6.345a1.73 1.73 0 011.73-1.73h20.54A1.73 1.73 0 0124 6.345v11.308a1.73 1.73 0 01-1.73 1.731zM5.769 15.923v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.232 12h-2.309V8.077h-2.307V12h-2.308l3.461 4.039z',
466
- };
467
-
468
- const mdxIcon: FileTypeIconDef = {
469
- title: 'MDX',
470
- path: 'M.79 7.12h22.42c.436 0 .79.355.79.792v8.176c0 .436-.354.79-.79.79H.79a.79.79 0 0 1-.79-.79V7.912a.79.79 0 0 1 .79-.791V7.12Zm2.507 7.605v-3.122l1.89 1.89L7.12 11.56v3.122h1.055v-5.67l-2.99 2.99L2.24 9.056v5.67h1.055v-.001Zm8.44-1.845-1.474-1.473-.746.746 2.747 2.747 2.745-2.747-.746-.746-1.473 1.473v-4h-1.054v4Zm10.041.987-2.175-2.175 2.22-2.22-.746-.746-2.22 2.22-2.22-2.22-.747.746 2.22 2.22-2.176 2.177.746.746 2.177-2.177 2.176 2.175.745-.746Z',
471
- };
472
-
473
- const yamlIcon: FileTypeIconDef = {
474
- title: 'YAML',
475
- path: 'm0 .97 4.111 6.453v4.09h2.638v-4.09L11.053.969H8.214L5.58 5.125 2.965.969Zm12.093.024-4.47 10.544h2.114l.97-2.345h4.775l.804 2.345h2.26L14.255.994Zm1.133 2.225 1.463 3.87h-3.096zm3.06 9.475v10.29H24v-2.199h-5.454v-8.091zm-12.175.002v10.335h2.217v-7.129l2.32 4.792h1.746l2.4-4.96v7.295h2.127V12.696h-2.904L9.44 17.37l-2.455-4.674Z',
476
- };
477
-
478
- const graphqlIcon: FileTypeIconDef = {
479
- title: 'GraphQL',
480
- path: 'M12.002 0a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm8.54 4.931a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm0 9.862a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm-8.54 4.931a2.138 2.138 0 1 0 0 4.276 2.138 2.138 0 1 0 0-4.276zm-8.542-4.93a2.138 2.138 0 1 0 0 4.276 2.138 2.138 0 1 0 0-4.277zm0-9.863a2.138 2.138 0 1 0 0 4.277 2.138 2.138 0 1 0 0-4.277zm8.542-3.378L2.953 6.777v10.448l9.049 5.224 9.047-5.224V6.777zm0 1.601 7.66 13.27H4.34zm-1.387.371L3.97 15.037V7.363zm2.774 0 6.646 3.838v7.674zM5.355 17.44h13.293l-6.646 3.836z',
481
- };
482
-
483
- const bashIcon: FileTypeIconDef = {
484
- title: 'Bash',
485
- path: 'M21.038,4.9l-7.577-4.498C13.009,0.134,12.505,0,12,0c-0.505,0-1.009,0.134-1.462,0.403L2.961,4.9 C2.057,5.437,1.5,6.429,1.5,7.503v8.995c0,1.073,0.557,2.066,1.462,2.603l7.577,4.497C10.991,23.866,11.495,24,12,24 c0.505,0,1.009-0.134,1.461-0.402l7.577-4.497c0.904-0.537,1.462-1.529,1.462-2.603V7.503C22.5,6.429,21.943,5.437,21.038,4.9z M15.17,18.946l0.013,0.646c0.001,0.078-0.05,0.167-0.111,0.198l-0.383,0.22c-0.061,0.031-0.111-0.007-0.112-0.085L14.57,19.29 c-0.328,0.136-0.66,0.169-0.872,0.084c-0.04-0.016-0.057-0.075-0.041-0.142l0.139-0.584c0.011-0.046,0.036-0.092,0.069-0.121 c0.012-0.011,0.024-0.02,0.036-0.026c0.022-0.011,0.043-0.014,0.062-0.006c0.229,0.077,0.521,0.041,0.802-0.101 c0.357-0.181,0.596-0.545,0.592-0.907c-0.003-0.328-0.181-0.465-0.613-0.468c-0.55,0.001-1.064-0.107-1.072-0.917 c-0.007-0.667,0.34-1.361,0.889-1.8l-0.007-0.652c-0.001-0.08,0.048-0.168,0.111-0.2l0.37-0.236 c0.061-0.031,0.111,0.007,0.112,0.087l0.006,0.653c0.273-0.109,0.511-0.138,0.726-0.088c0.047,0.012,0.067,0.076,0.048,0.151 l-0.144,0.578c-0.011,0.044-0.036,0.088-0.065,0.116c-0.012,0.012-0.025,0.021-0.038,0.028c-0.019,0.01-0.038,0.013-0.057,0.009 c-0.098-0.022-0.332-0.073-0.699,0.113c-0.385,0.195-0.52,0.53-0.517,0.778c0.003,0.297,0.155,0.387,0.681,0.396 c0.7,0.012,1.003,0.318,1.01,1.023C16.105,17.747,15.736,18.491,15.17,18.946z M19.143,17.859c0,0.06-0.008,0.116-0.058,0.145 l-1.916,1.164c-0.05,0.029-0.09,0.004-0.09-0.056v-0.494c0-0.06,0.037-0.093,0.087-0.122l1.887-1.129 c0.05-0.029,0.09-0.004,0.09,0.056V17.859z M20.459,6.797l-7.168,4.427c-0.894,0.523-1.553,1.109-1.553,2.187v8.833 c0,0.645,0.26,1.063,0.66,1.184c-0.131,0.023-0.264,0.039-0.398,0.039c-0.42,0-0.833-0.114-1.197-0.33L3.226,18.64 c-0.741-0.44-1.201-1.261-1.201-2.142V7.503c0-0.881,0.46-1.702,1.201-2.142l7.577-4.498c0.363-0.216,0.777-0.33,1.197-0.33 c0.419,0,0.833,0.114,1.197,0.33l7.577,4.498c0.624,0.371,1.046,1.013,1.164,1.732C21.686,6.557,21.12,6.411,20.459,6.797z',
486
- };
487
-
488
- const dockerIcon: FileTypeIconDef = {
489
- title: 'Docker',
490
- path: 'M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z',
491
- };
492
-
493
- const FILE_TYPE_ICONS: Record<string, FileTypeIconDef> = {
494
- ts: typescriptIcon,
495
- mts: typescriptIcon,
496
- cts: typescriptIcon,
497
- tsx: reactIcon,
498
- jsx: reactIcon,
499
- js: javascriptIcon,
500
- mjs: javascriptIcon,
501
- cjs: javascriptIcon,
502
- json: jsonIcon,
503
- jsonc: jsonIcon,
504
- html: htmlIcon,
505
- css: cssIcon,
506
- md: markdownIcon,
507
- mdx: mdxIcon,
508
- yml: yamlIcon,
509
- yaml: yamlIcon,
510
- graphql: graphqlIcon,
511
- gql: graphqlIcon,
512
- sh: bashIcon,
513
- bash: bashIcon,
514
- zsh: bashIcon,
515
- dockerfile: dockerIcon,
516
- };
517
-
518
- /**
519
- * Resolve the brand icon for a filename by its extension. Matching uses the
520
- * basename, so directory names never interfere. `Dockerfile` matches by full
521
- * basename since it has no extension. Returns undefined for unknown extensions
522
- * and dotfiles; the header then falls back to the generic file icon.
523
- */
524
- function matchFileTypeIcon(filename: string): FileTypeIconDef | undefined {
525
- const basename = filename.split('/').pop()?.toLowerCase() ?? '';
526
- if (basename === 'dockerfile') return FILE_TYPE_ICONS.dockerfile;
527
- const dotIndex = basename.lastIndexOf('.');
528
- if (dotIndex <= 0) return undefined;
529
- return FILE_TYPE_ICONS[basename.slice(dotIndex + 1)];
530
- }
531
-
532
- function FileTypeIcon({ icon }: { icon: FileTypeIconDef }) {
533
- return (
534
- <svg viewBox="0 0 24 24" className="size-3.5 shrink-0 fill-current" aria-hidden="true">
535
- <path d={icon.path} />
536
- </svg>
537
- );
538
- }
539
-
540
- // =============================================================================
541
- // Style Constants
542
- // =============================================================================
543
-
544
210
  const darkModeClassNames = cn(
545
211
  'dark:[&_.shiki]:!text-[var(--shiki-dark)]',
546
212
  'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]',
@@ -581,10 +247,6 @@ const lineFocusedClassNames = cn(
581
247
 
582
248
  const wordHighlightClassNames = cn('[&_.highlighted-word]:bg-code-highlight');
583
249
 
584
- // =============================================================================
585
- // Sub-Components
586
- // =============================================================================
587
-
588
250
  /**
589
251
  * Tooltip icon-button used for code-block toolbar actions (and the built-in copy
590
252
  * button). Compose these into the `actions` slot of {@link CodeBlock}.
@@ -700,7 +362,6 @@ function CodeBlockHeader({
700
362
  toolbar,
701
363
  id,
702
364
  }: CodeBlockHeaderProps) {
703
- // Determine what label to show
704
365
  const getLabel = () => {
705
366
  if (filename) {
706
367
  const fileTypeIcon = matchFileTypeIcon(filename);
@@ -727,7 +388,6 @@ function CodeBlockHeader({
727
388
  );
728
389
  }
729
390
 
730
- // Default: show language or "Code"
731
391
  return (
732
392
  <div className="text-muted-foreground flex items-center gap-1.5 text-sm">
733
393
  <CodeIcon className="size-3.5" />
@@ -804,10 +464,6 @@ const codeBlockContentClassName = cn(
804
464
  '[&_.line]:relative',
805
465
  );
806
466
 
807
- // =============================================================================
808
- // Syntax Highlighted Content Component
809
- // =============================================================================
810
-
811
467
  interface SyntaxHighlightedContentProps {
812
468
  code: string;
813
469
  language: SupportedLanguage;
@@ -844,10 +500,6 @@ function SyntaxHighlightedContent({ code, language }: SyntaxHighlightedContentPr
844
500
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
845
501
  }
846
502
 
847
- // =============================================================================
848
- // Main Component
849
- // =============================================================================
850
-
851
503
  /**
852
504
  * Shiki-highlighted code block with a header, copy button, collapse/expand for
853
505
  * long snippets, and an optional npm→pnpm/yarn/bun package-manager switcher.
@@ -873,35 +525,23 @@ export function CodeBlock({
873
525
  const copyFeedback = useCopyFeedback();
874
526
  const [activePackageManager, setActivePackageManager] = usePackageManager();
875
527
  const [isExpanded, setIsExpanded] = useState(false);
876
- const containerRef = useRef<HTMLDivElement>(null);
877
528
  const instanceId = useId();
878
529
 
879
- // Process the raw code
880
- const rawCode = useMemo(() => {
881
- return typeof children === 'string'
882
- ? children
883
- : Array.isArray(children)
884
- ? children.join('')
885
- : String(children || '');
886
- }, [children]);
530
+ const rawCode = children ?? '';
887
531
 
888
- // Process code directives (filename extraction, strip notations for unsupported languages)
889
532
  const { cleanCode, extractedFilename } = useMemo(
890
533
  () => processCode(rawCode, language),
891
534
  [rawCode, language],
892
535
  );
893
536
 
894
- // Use provided filename or extracted one
895
537
  const displayFilename = filename || extractedFilename;
896
538
 
897
- // Detect if this is a package manager command (only when the switcher is enabled)
898
539
  const isShellLanguage =
899
540
  language?.toLowerCase() === 'bash' ||
900
541
  language?.toLowerCase() === 'shell' ||
901
542
  language?.toLowerCase() === 'sh';
902
543
  const isCommand = packageManagerSwitcher && isShellLanguage && isPackageManagerCommand(cleanCode);
903
544
 
904
- // Get the display code (transformed for selected package manager if applicable)
905
545
  const displayCode = useMemo(() => {
906
546
  if (isCommand) {
907
547
  return transformCommand(cleanCode, activePackageManager);
@@ -909,7 +549,6 @@ export function CodeBlock({
909
549
  return cleanCode;
910
550
  }, [cleanCode, isCommand, activePackageManager]);
911
551
 
912
- // Calculate if the code block is collapsible
913
552
  const lineCount = useMemo(() => displayCode.split('\n').length, [displayCode]);
914
553
  const isCollapsible = lineCount > COLLAPSE_THRESHOLD;
915
554
  const hiddenLineCount = lineCount - Math.floor(COLLAPSED_HEIGHT / 24); // Approximate visible lines based on line height
@@ -932,7 +571,6 @@ export function CodeBlock({
932
571
  'bg-card not-prose border-border relative mb-4 w-full contain-inline-size overflow-hidden rounded-md border text-sm lg:text-base',
933
572
  className,
934
573
  )}
935
- ref={containerRef}
936
574
  >
937
575
  {!hideHeader && (
938
576
  <CodeBlockHeader
@@ -91,9 +91,13 @@ function FileDropzone({
91
91
  const [validating, setValidating] = React.useState(false);
92
92
  const [rejections, setRejections] = React.useState<FileRejection[]>([]);
93
93
  const dragDepth = React.useRef(0);
94
+ // Synchronous mirror of `validating`: a drop while an async validateFile is
95
+ // in flight would otherwise start a second addFiles against a stale `value`.
96
+ const busy = React.useRef(false);
94
97
 
95
98
  async function addFiles(incoming: readonly File[]) {
96
- if (disabled || incoming.length === 0) return;
99
+ if (disabled || busy.current || incoming.length === 0) return;
100
+ busy.current = true;
97
101
  setValidating(true);
98
102
  const accepted: File[] = [];
99
103
  const rejected: FileRejection[] = [];
@@ -118,11 +122,15 @@ function FileDropzone({
118
122
 
119
123
  if (incoming.length > remaining) {
120
124
  for (const file of incoming.slice(remaining)) {
121
- rejected.push({ file, messages: [`You can select up to ${maxFiles} files.`] });
125
+ rejected.push({
126
+ file,
127
+ messages: [`You can select up to ${maxFiles} ${maxFiles === 1 ? 'file' : 'files'}.`],
128
+ });
122
129
  }
123
130
  }
124
131
 
125
132
  setRejections(rejected);
133
+ busy.current = false;
126
134
  setValidating(false);
127
135
  if (rejected.length > 0) onRejected?.(rejected);
128
136
  if (accepted.length > 0) {
@@ -211,7 +219,9 @@ function FileDropzone({
211
219
  {value.length > 0 ? (
212
220
  <ul data-slot="file-dropzone-files" className="flex flex-col gap-2">
213
221
  {value.map((file, index) => (
214
- <li key={`${file.name}-${file.size}-${file.lastModified}`}>
222
+ // Metadata alone can collide (two distinct files with identical
223
+ // name, size and mtime), so the index disambiguates the key.
224
+ <li key={`${file.name}-${file.size}-${file.lastModified}-${index}`}>
215
225
  {renderFile ? (
216
226
  renderFile(file, () => removeFile(index))
217
227
  ) : (
@@ -240,8 +250,8 @@ function FileDropzone({
240
250
 
241
251
  {rejections.length > 0 ? (
242
252
  <div id={errorId} role="alert" className="text-destructive text-sm">
243
- {rejections.map(({ file, messages }) => (
244
- <p key={`${file.name}-${file.size}`}>
253
+ {rejections.map(({ file, messages }, index) => (
254
+ <p key={`${file.name}-${file.size}-${index}`}>
245
255
  <span className="font-medium">{file.name}:</span> {messages.join(' ')}
246
256
  </p>
247
257
  ))}
@@ -137,5 +137,38 @@ function normalizeLanguage(lang?: string): SupportedLanguage {
137
137
  return 'ini';
138
138
  }
139
139
 
140
- export { getHighlighter, highlightCode, normalizeLanguage };
140
+ /**
141
+ * Bundled grammars whose `//` / `#` comment syntax lets the `[!code ...]`
142
+ * notation transformers match. Only languages with a loader above can qualify:
143
+ * anything else falls back to the `ini` grammar, which renders the notation
144
+ * comment as literal text instead of consuming it.
145
+ */
146
+ const NOTATION_TRANSFORMER_LANGUAGES = new Set<SupportedLanguage>([
147
+ 'javascript',
148
+ 'typescript',
149
+ 'jsx',
150
+ 'tsx',
151
+ 'python',
152
+ 'bash',
153
+ 'shellscript',
154
+ 'yaml',
155
+ ]);
156
+
157
+ /**
158
+ * Whether `[!code ...]` notations in `lang` survive highlighting. Alias-aware:
159
+ * the check runs on the normalized language, so `ts`, `py`, `sh`, … qualify
160
+ * through their canonical grammar. Callers should strip notations when this
161
+ * returns false, or they render as literal text.
162
+ */
163
+ function supportsNotationComments(lang?: string): boolean {
164
+ return NOTATION_TRANSFORMER_LANGUAGES.has(normalizeLanguage(lang));
165
+ }
166
+
167
+ export {
168
+ NOTATION_TRANSFORMER_LANGUAGES,
169
+ getHighlighter,
170
+ highlightCode,
171
+ normalizeLanguage,
172
+ supportsNotationComments,
173
+ };
141
174
  export type { SupportedLanguage };
package/src/lib/utils.ts CHANGED
@@ -4,22 +4,3 @@ import { twMerge } from 'tailwind-merge';
4
4
  export function cn(...inputs: ClassValue[]): string {
5
5
  return twMerge(clsx(inputs));
6
6
  }
7
-
8
- export function slugify(str: string): string {
9
- return str
10
- .toLowerCase()
11
- .trim()
12
- .replace(/[^\w\s-]/g, '')
13
- .replace(/[\s_-]+/g, '-')
14
- .replace(/^-+|-+$/g, '');
15
- }
16
-
17
- export function hashString(str: string): number {
18
- let hash = 0;
19
- for (let i = 0; i < str.length; i++) {
20
- const char = str.charCodeAt(i);
21
- hash = (hash << 5) - hash + char;
22
- hash |= 0;
23
- }
24
- return Math.abs(hash);
25
- }