-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat(shared-ui): add ShikiCode component for syntax highlighting #482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9bf7a96
9717222
3c3364b
cd4ce98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { ShikiCode } from "./shiki-code"; | ||
| export type { ShikiCodeProps } from "./shiki-code"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import type { BundledLanguage, BundledTheme } from "shiki"; | ||
|
||
| import { cache, memo, use } from "react"; | ||
| import { createJavaScriptRegexEngine } from "shiki"; | ||
| import { createHighlighterCore } from "shiki/core"; | ||
|
|
||
| export interface ShikiCodeProps { | ||
| /** | ||
| * The code to highlight | ||
| */ | ||
| code: string; | ||
| /** | ||
| * The language to use for syntax highlighting | ||
| * @default "typescript" | ||
| */ | ||
| language?: BundledLanguage; | ||
| /** | ||
| * The theme to use for syntax highlighting | ||
| * @default "github-dark" | ||
| */ | ||
| theme?: BundledTheme; | ||
| /** | ||
| * Additional CSS class names | ||
| */ | ||
| className?: string; | ||
| } | ||
|
|
||
| const getHighlighter = cache(async () => { | ||
| return await createHighlighterCore({ | ||
| themes: [import("shiki/themes/github-dark.mjs")], | ||
luxass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| langs: [ | ||
| import("shiki/langs/javascript.mjs"), | ||
| import("shiki/langs/typescript.mjs"), | ||
| import("shiki/langs/json.mjs"), | ||
luxass marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ], | ||
| engine: createJavaScriptRegexEngine(), | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * A syntax highlighting component powered by Shiki. | ||
| * Renders code to HTML with syntax highlighting. | ||
| * | ||
| * @example | ||
| * ```tsx | ||
| * <ShikiCode code="const x = 1;" language="typescript" /> | ||
| * ``` | ||
| */ | ||
| export const ShikiCode = memo<ShikiCodeProps>(({ | ||
| code, | ||
| language = "typescript", | ||
| theme = "github-dark", | ||
| className, | ||
| }) => { | ||
| const highlighter = use(getHighlighter()); | ||
|
|
||
| const html = highlighter.codeToHtml(code, { | ||
| lang: language, | ||
| theme, | ||
| }); | ||
|
|
||
| // eslint-disable-next-line react-dom/no-dangerously-set-innerhtml | ||
| return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />; | ||
|
Comment on lines
+48
to
+62
|
||
| }); | ||
luxass marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+1
to
+63
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import { cn } from "#lib/utils"; | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| } from "#ui/dialog"; | ||
| import { | ||
| InputGroup, | ||
| InputGroupAddon, | ||
| } from "#ui/input-group"; | ||
| import { Command as CommandPrimitive } from "cmdk"; | ||
| import { CheckIcon, SearchIcon } from "lucide-react"; | ||
| import * as React from "react"; | ||
|
|
||
| function Command({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive>) { | ||
| return ( | ||
| <CommandPrimitive | ||
| data-slot="command" | ||
| className={cn( | ||
| "bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function CommandDialog({ | ||
| title = "Command Palette", | ||
| description = "Search for a command to run...", | ||
| children, | ||
| className, | ||
| showCloseButton = false, | ||
| ...props | ||
| }: Omit<React.ComponentProps<typeof Dialog>, "children"> & { | ||
| title?: string; | ||
| description?: string; | ||
| className?: string; | ||
| showCloseButton?: boolean; | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <Dialog {...props}> | ||
| <DialogHeader className="sr-only"> | ||
| <DialogTitle>{title}</DialogTitle> | ||
| <DialogDescription>{description}</DialogDescription> | ||
| </DialogHeader> | ||
| <DialogContent | ||
| className={cn( | ||
| "rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0", | ||
| className, | ||
| )} | ||
| showCloseButton={showCloseButton} | ||
| > | ||
| {children} | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| } | ||
|
|
||
| function CommandInput({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive.Input>) { | ||
| return ( | ||
| <div data-slot="command-input-wrapper" className="p-1 pb-0"> | ||
| <InputGroup className="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!"> | ||
| <CommandPrimitive.Input | ||
| data-slot="command-input" | ||
| className={cn( | ||
| "w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| <InputGroupAddon> | ||
| <SearchIcon className="size-4 shrink-0 opacity-50" /> | ||
| </InputGroupAddon> | ||
| </InputGroup> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function CommandList({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive.List>) { | ||
| return ( | ||
| <CommandPrimitive.List | ||
| data-slot="command-list" | ||
| className={cn( | ||
| "no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function CommandEmpty({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive.Empty>) { | ||
| return ( | ||
| <CommandPrimitive.Empty | ||
| data-slot="command-empty" | ||
| className={cn("py-6 text-center text-sm", className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function CommandGroup({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive.Group>) { | ||
| return ( | ||
| <CommandPrimitive.Group | ||
| data-slot="command-group" | ||
| className={cn("text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium", className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function CommandSeparator({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive.Separator>) { | ||
| return ( | ||
| <CommandPrimitive.Separator | ||
| data-slot="command-separator" | ||
| className={cn("bg-border -mx-1 h-px", className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function CommandItem({ | ||
| className, | ||
| children, | ||
| ...props | ||
| }: React.ComponentProps<typeof CommandPrimitive.Item>) { | ||
| return ( | ||
| <CommandPrimitive.Item | ||
| data-slot="command-item" | ||
| className={cn( | ||
| "data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", | ||
| className, | ||
| )} | ||
| {...props} | ||
| > | ||
| {children} | ||
| <CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" /> | ||
| </CommandPrimitive.Item> | ||
| ); | ||
| } | ||
|
|
||
| function CommandShortcut({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<"span">) { | ||
| return ( | ||
| <span | ||
| data-slot="command-shortcut" | ||
| className={cn("text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest", className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export { | ||
| Command, | ||
| CommandDialog, | ||
| CommandEmpty, | ||
| CommandGroup, | ||
| CommandInput, | ||
| CommandItem, | ||
| CommandList, | ||
| CommandSeparator, | ||
| CommandShortcut, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ContextMenu component is implemented but missing from the package.json exports. This component cannot be imported by consumers of the package. Add an export entry for "./ui/context-menu" pointing to "./dist/ui/context-menu.mjs".