UI Updates (#246)

* minor ui updates to shiki

* feat: add markdown renderer

* feat(ui): add scroll area component and expand markdown renderer

* feat(ui): expand markdown syntax highlighting with additional language support

* feat(ui): add markdown language support to syntax highlighting

* feat(ui): update markdown syntax highlighting theme to GitHub Dark Default

* feat(ui): add bash language support to markdown syntax highlighting

* feat: add globe component

* Change RetroGrid bg color

* feat: add headless command list to store detail component

* feat: update markdown renderer

Replace svelte-markdown with svelte-exmarkdown, with custom tauri link renderer and code highlight support

* format and fix eslint
This commit is contained in:
Huakun 2025-03-13 21:30:52 -04:00 committed by GitHub
parent cd7301255b
commit 310969e597
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2212 additions and 149 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@kksh/desktop",
"version": "0.1.33",
"version": "0.1.34",
"description": "",
"type": "module",
"scripts": {

927
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -86,42 +86,22 @@ class ExtensionTemplate extends TemplateUiCommand {
}
async load() {
return ui.render(
new Markdown(`
# Hello World
[GitHub HuakunShen](https://github.com/HuakunShen)
![](https://github.com/huakunshen.png)
<img src="https://github.com/huakunshen.png" />
`)
)
ui.setSearchBarPlaceholder("Search for items")
const sections = getSections(2)
const items = getItems(5)
return ui.render(
new List.List({
items: items.map(
(item) =>
new List.Item({
title: item.name,
value: item.id
// icon: new Icon({
// type: IconType.enum.Iconify,
// value: "mingcute:appstore-fill"
// })
})
),
sections: sections.map(
(section) =>
new List.Section({
title: section.name,
items: section.items.map(
(item) =>
new List.Item({
title: item.name,
value: item.id
})
)
})
)
})
)
ui.showLoadingBar(true)
setTimeout(() => {
ui.showLoadingBar(false)
clipboard.paste()
}, 2000)
const extPath = await path.extensionDir()
const cmd = shell.createCommand("deno", ["run", "/Users/hk/Dev/kunkun/deno.ts"])
cmd.stdout.on("data", (data) => {
@ -189,10 +169,14 @@ class ExtensionTemplate extends TemplateUiCommand {
]),
new Markdown(`
# Hello World
[GitHub HuakunShen](https://github.com/HuakunShen)
![](https://github.com/huakunshen.png)
<img src="https://github.com/huakunshen.png" />
<img src="https://github.com/huakunshen.png" />
<img src="https://github.com/huakunshen.png" />
`)
`)
],
width: 50
}),

View File

@ -32,6 +32,10 @@
"./animation": {
"types": "./src/components/animation/index.ts",
"svelte": "./src/components/animation/index.ts"
},
"./markdown": {
"types": "./src/components/markdown/index.ts",
"svelte": "./src/components/markdown/index.ts"
}
},
"scripts": {
@ -76,17 +80,24 @@
"@internationalized/date": "^3.7.0",
"@kksh/supabase": "workspace:*",
"@shikijs/langs": "^2.3.2",
"@shikijs/rehype": "^3.2.1",
"@shikijs/themes": "^2.3.2",
"@std/semver": "npm:@jsr/std__semver@^1.0.3",
"@tanstack/svelte-virtual": "^3.13.2",
"cobe": "^0.6.3",
"dompurify": "^3.2.3",
"fuse.js": "^7.1.0",
"gsap": "^3.12.7",
"katex": "^0.16.21",
"moment": "^2.30.1",
"pretty-bytes": "^6.1.1",
"rehype-class-names": "^2.0.0",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-math": "^6.0.0",
"shiki-magic-move": "^0.5.2",
"svelte-exmarkdown": "^4.0.3",
"svelte-inspect-value": "^0.3.0",
"svelte-markdown": "^0.4.1",
"svelte-motion": "^0.12.2",
"valibot": "1.0.0-beta.12"
}

View File

@ -0,0 +1,120 @@
<script lang="ts">
import createGlobe from "cobe"
import { onMount } from "svelte"
import { Spring, spring } from "svelte/motion"
import { cn } from "../../utils"
let x = spring(0, {
stiffness: 0.04,
damping: 0.4,
precision: 0.005
})
// let className = ""
// export { className as class }
let { locations = [], class: className }: { class?: string; locations?: [number, number][] } =
$props()
let pointerInteracting: number | null = null
let pointerInteractionMovement = 0
let canvas: HTMLCanvasElement
let phi = 0
let width = 0
// $: console.log(width, "X")
let onResize = () => {
width = canvas.offsetWidth
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let onRender = (state: Record<string, any>) => {
if (!pointerInteracting) {
phi += 0.005
}
state.phi = phi + $x
state.width = width * 2
state.height = width * 2
}
onMount(() => {
// Adds the resize event listener when the component is mounted
window.addEventListener("resize", onResize)
onResize()
// Initializes the globe with specific options
const globe = createGlobe(canvas, {
devicePixelRatio: 2,
width: width,
height: width,
phi: 0,
theta: 0.3,
dark: 1,
diffuse: 0.4, // 1.2
mapSamples: 16000,
mapBrightness: 1.2, // 6
baseColor: [0.3, 0.3, 0.3],
markerColor: [251 / 255, 100 / 255, 21 / 255],
glowColor: [1, 1, 1],
markers: locations.map((location) => {
return {
location: location,
size: 0.03
}
}),
// [
// { location: [14.5995, 120.9842], size: 0.03 },
// { location: [19.076, 72.8777], size: 0.03 },
// { location: [23.8103, 90.4125], size: 0.05 },
// { location: [30.0444, 31.2357], size: 0.07 },
// { location: [39.9042, 116.4074], size: 0.08 },
// { location: [-23.5505, -46.6333], size: 0.05 },
// { location: [19.4326, -99.1332], size: 0.04 },
// { location: [40.7128, -74.006], size: 0.1 },
// { location: [34.6937, 135.5022], size: 0.05 },
// { location: [41.0082, 28.9784], size: 0.06 }
// ],
// onRender: (state) => {
// if (!pointerInteracting) {
// // Called on every animation frame.
// // `state` will be an empty object, return updated params.
// phi += 0.009;
// }
// state.phi = phi + $x;
// // phi += 0.01;
// },
onRender: onRender
})
// Removes the resize event listener when the component is unmounted to prevent memory leaks
return () => {
window.removeEventListener("resize", onResize)
}
})
</script>
<main class={cn("absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]", className)}>
<canvas
class="h-full w-full [contain:layout_paint_size]"
bind:this={canvas}
onpointerdown={(e) => {
pointerInteracting = e.clientX - pointerInteractionMovement
canvas.style.cursor = "grabbing"
}}
onpointerup={() => {
pointerInteracting = null
canvas.style.cursor = "grab"
}}
onpointerout={() => {
pointerInteracting = null
canvas.style.cursor = "grab"
}}
onmousemove={(e) => {
if (pointerInteracting !== null) {
console.log("working")
const delta = e.clientX - pointerInteracting
pointerInteractionMovement = delta
x.set(delta / 200)
}
}}
></canvas>
</main>

View File

@ -2,10 +2,12 @@
import { onMount } from "svelte"
import { Motion, useMotionTemplate, useMotionValue } from "svelte-motion"
import { cn } from "../../utils"
import BorderBeam from "./BorderBeam.svelte"
export let gradientSize: number = 200
export let gradientColor: string = "#262626"
export let gradientOpacity: number = 0.8
export let borderBeam: boolean = false
let className: string = ""
export { className as class }
@ -42,6 +44,9 @@
className
)}
>
{#if borderBeam}
<BorderBeam size={150} duration={12} />
{/if}
<div class="relative z-10">
<!-- Default -->
<slot>

View File

@ -29,6 +29,8 @@
<!-- Background Gradient -->
<div
class="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black"
class={cn(
"dark:from-background absolute inset-0 bg-gradient-to-t from-white to-transparent to-90%"
)}
></div>
</div>

View File

@ -5,3 +5,4 @@ export { default as RetroGrid } from "./RetroGrid.svelte"
export { default as AuroraText } from "./AuroraText.svelte"
export { default as WordRotate } from "./WordRotate.svelte"
export { default as MagicCard } from "./MagicCard.svelte"
export { default as Globe } from "./Globe.svelte"

View File

@ -36,7 +36,7 @@
{#await highlighter2 then highlighter}
<ShikiMagicMove
class={cn("", className)}
class={cn("p-3", className)}
{lang}
theme={theme ?? "vitesse-dark"}
{highlighter}

View File

@ -5,36 +5,46 @@
import type { HTMLAttributes } from "svelte/elements"
import { open } from "tauri-plugin-shellx-api"
const {
let {
href,
class: className = "",
children
style,
children,
ref = $bindable(null)
}: {
href: string
href?: string
style?: HTMLAttributes<HTMLAnchorElement>["style"]
class?: HTMLAttributes<HTMLAnchorElement>["class"]
children: Snippet
ref: HTMLAnchorElement | HTMLButtonElement | null
} = $props()
// @ts-expect-error window.__TAURI_INTERNALS__ is not defined in the browser
const isInTauri = browser ? !!window.__TAURI_INTERNALS__ : false
function handleClick() {
open(href)
if (href) {
open(href)
}
}
</script>
{#if isInTauri}
<button
bind:this={ref}
class={cn(
"text-left font-medium text-blue-600 hover:cursor-pointer hover:underline dark:text-blue-500",
className
)}
{style}
onclick={handleClick}
>
{@render children?.()}
</button>
{:else}
<a
bind:this={ref}
{href}
{style}
target="_blank"
class={cn(
"text-left font-medium text-blue-600 hover:cursor-pointer hover:underline dark:text-blue-500",

View File

@ -260,7 +260,7 @@
<h2 class="text-lg font-bold">Commands</h2>
<ul>
{#if manifest}
{#each [...(manifest.customUiCmds ?? []), ...(manifest.templateUiCmds ?? [])] as cmd}
{#each [...(manifest.customUiCmds ?? []), ...(manifest.templateUiCmds ?? []), ...(manifest.headlessCmds ?? [])] as cmd}
<li>
<div class="flex items-center space-x-3">
{#if manifest}

View File

@ -1,10 +1,10 @@
<script lang="ts">
import { cn } from "@kksh/ui/utils"
import SvelteMarkdown from "svelte-markdown"
import Markdown from "../../markdown/Markdown.svelte"
const { markdown, class: className }: { markdown: string; class?: string } = $props()
</script>
<div class={cn("prose dark:prose-invert", className)}>
<SvelteMarkdown source={markdown} />
<Markdown md={markdown} />
</div>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { Button } from "@kksh/svelte5"
import { CopyIcon } from "lucide-svelte"
import type { Snippet } from "svelte"
import TauriLink from "../common/TauriLink.svelte"
let a: HTMLAnchorElement | HTMLButtonElement | null = $state(null)
let {
children,
class: className,
href,
style,
...rest
}: { children: Snippet; href?: string; class?: string; style?: string } = $props()
</script>
<TauriLink {href} class={className} {style} bind:ref={a}>{@render children()}</TauriLink>

View File

@ -0,0 +1,88 @@
<script lang="ts">
import rehypeShikiFromHighlighter from "@shikijs/rehype/core"
import rehypeClassNames from "rehype-class-names"
import rehypeKatex from "rehype-katex"
import rehypeRaw from "rehype-raw"
import remarkMath from "remark-math"
import { createHighlighterCoreSync } from "shiki/core"
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
import bash from "shiki/langs/bash.mjs"
import cpp from "shiki/langs/cpp.mjs"
import csharp from "shiki/langs/csharp.mjs"
import go from "shiki/langs/go.mjs"
import html from "shiki/langs/html.mjs"
import java from "shiki/langs/java.mjs"
import json from "shiki/langs/json.mjs"
import kotlin from "shiki/langs/kotlin.mjs"
import markdown from "shiki/langs/markdown.mjs"
import php from "shiki/langs/php.mjs"
import python from "shiki/langs/python.mjs"
import ruby from "shiki/langs/ruby.mjs"
import rust from "shiki/langs/rust.mjs"
import shell from "shiki/langs/shell.mjs"
import svelte from "shiki/langs/svelte.mjs"
import swift from "shiki/langs/swift.mjs"
import ts from "shiki/langs/typescript.mjs"
import yaml from "shiki/langs/yaml.mjs"
import githubDarkDefault from "shiki/themes/github-dark-default.mjs"
import Markdown from "svelte-exmarkdown"
import type { Plugin } from "svelte-exmarkdown"
import { gfmPlugin } from "svelte-exmarkdown/gfm"
import A from "./A.svelte"
import Pre from "./Pre.svelte"
const addClass: Plugin = {
rehypePlugin: [
rehypeClassNames,
{
pre: "p-4 rounded-md overflow-auto"
}
]
}
const shikiPlugin = {
rehypePlugin: [
rehypeShikiFromHighlighter,
createHighlighterCoreSync({
themes: [githubDarkDefault],
langs: [
ts,
svelte,
json,
html,
rust,
python,
java,
cpp,
csharp,
go,
ruby,
php,
markdown,
kotlin,
swift,
yaml,
shell,
bash
],
engine: createJavaScriptRegexEngine()
}),
{
theme: "github-dark-default"
}
]
} satisfies Plugin
const plugins: Plugin[] = [
shikiPlugin,
gfmPlugin(),
{ rehypePlugin: [rehypeRaw] },
{ remarkPlugin: [remarkMath], rehypePlugin: [rehypeKatex] },
addClass,
{ renderer: { pre: Pre, a: A } }
]
let { md }: { md: string } = $props()
</script>
<Markdown {md} {plugins} />

View File

@ -0,0 +1,25 @@
<script lang="ts">
import { Button } from "@kksh/svelte5"
import { CopyIcon } from "lucide-svelte"
import type { Snippet } from "svelte"
let pre: HTMLPreElement
let {
children,
class: className,
style
}: { children: Snippet; class?: string; style?: string } = $props()
</script>
<div class="relative">
<pre class={className} {style} bind:this={pre}>{@render children()}</pre>
<Button
size="icon"
variant="outline"
onclick={() => navigator.clipboard.writeText(pre.textContent ?? "")}
class="absolute right-2 top-2"
>
<CopyIcon />
</Button>
</div>

View File

@ -0,0 +1 @@
export { default as Markdown } from "./Markdown.svelte"

View File

@ -0,0 +1,10 @@
import Scrollbar from "./scroll-area-scrollbar.svelte"
import Root from "./scroll-area.svelte"
export {
Root,
Scrollbar,
//,
Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar
}

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive, type WithoutChild } from "bits-ui"
import { cn } from "../../../utils"
let {
ref = $bindable(null),
class: className,
orientation = "vertical",
children,
...restProps
}: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> = $props()
</script>
<ScrollAreaPrimitive.Scrollbar
bind:ref
{orientation}
class={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px",
orientation === "horizontal" && "h-2.5 w-full border-t border-t-transparent p-px",
className
)}
{...restProps}
>
{@render children?.()}
<ScrollAreaPrimitive.Thumb
class={cn("bg-border relative rounded-full", orientation === "vertical" && "flex-1")}
/>
</ScrollAreaPrimitive.Scrollbar>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive, type WithoutChild } from "bits-ui"
import { cn } from "../../../utils"
import { Scrollbar } from "./index.js"
let {
ref = $bindable(null),
viewportRef = $bindable(null),
class: className,
orientation = "vertical",
scrollbarXClasses = "",
scrollbarYClasses = "",
children,
...restProps
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
viewportRef?: HTMLDivElement | null
orientation?: "vertical" | "horizontal" | "both" | undefined
scrollbarXClasses?: string | undefined
scrollbarYClasses?: string | undefined
} = $props()
</script>
<ScrollAreaPrimitive.Root bind:ref {...restProps} class={cn("relative overflow-hidden", className)}>
<ScrollAreaPrimitive.Viewport bind:ref={viewportRef} class="h-full w-full rounded-[inherit]">
{@render children?.()}
</ScrollAreaPrimitive.Viewport>
{#if orientation === "vertical" || orientation === "both"}
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
{/if}
{#if orientation === "horizontal" || orientation === "both"}
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
{/if}
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>

View File

@ -15,4 +15,5 @@ export { default as RetroGrid } from "./components/animation/RetroGrid.svelte"
export { default as AuroraText } from "./components/animation/AuroraText.svelte"
export * as Constants from "./constants"
export * as Form from "./components/ui/form"
export * as ScrollArea from "./components/ui/scroll-area"
export { default as ModeToggle } from "./components/theme/mode-toggle.svelte"

1003
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff