diff options
| author | Jay V <[email protected]> | 2025-06-18 20:18:07 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-06-18 20:18:10 -0400 |
| commit | a834bedc176140990d8d457f633e6d9c47aaf886 (patch) | |
| tree | 929380761df2e2f0101424b05705e6ff0ca8b6b8 /packages/web/src/components | |
| parent | 6a3392385e7a42f1338049fb66865c477fe60aff (diff) | |
| download | opencode-a834bedc176140990d8d457f633e6d9c47aaf886.tar.gz opencode-a834bedc176140990d8d457f633e6d9c47aaf886.zip | |
ignore: share copy link
Diffstat (limited to 'packages/web/src/components')
| -rw-r--r-- | packages/web/src/components/Header.astro | 2 | ||||
| -rw-r--r-- | packages/web/src/components/Share.tsx | 103 | ||||
| -rw-r--r-- | packages/web/src/components/share.module.css | 80 |
3 files changed, 146 insertions, 39 deletions
diff --git a/packages/web/src/components/Header.astro b/packages/web/src/components/Header.astro index 1b531db61..b3e393826 100644 --- a/packages/web/src/components/Header.astro +++ b/packages/web/src/components/Header.astro @@ -10,7 +10,7 @@ const path = Astro.url.pathname; const links = config.social || []; --- -{ path.startsWith("/share") +{ path.startsWith("/s") ? <div class="header sl-flex"> <div class="title-wrapper sl-flex"> <SiteTitle {...Astro.props} /> diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index 378ff62b9..e91048047 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -23,11 +23,13 @@ import { import { IconFolder, IconCpuChip, + IconHashtag, IconSparkles, IconGlobeAlt, IconDocument, IconQueueList, IconUserCircle, + IconCheckCircle, IconChevronDown, IconCommandLine, IconChevronRight, @@ -504,13 +506,52 @@ function ToolFooter(props: { time: number }) { ) } +interface AnchorProps extends JSX.HTMLAttributes<HTMLDivElement> { + id: string +} +function AnchorIcon(props: AnchorProps) { + const [local, rest] = splitProps(props, ["id", "children"]) + const [copied, setCopied] = createSignal(false) + + return ( + <div + {...rest} + data-element-anchor + title="Link to this message" + data-status={copied() ? "copied" : ""} + > + <a + href={`#${local.id}`} + onClick={e => { + e.preventDefault() + + const anchor = e.currentTarget + const hash = anchor.getAttribute("href") || "" + const { origin, pathname, search } = window.location + + navigator.clipboard + .writeText(`${origin}${pathname}${search}${hash}`) + .catch((err) => console.error("Copy failed", err)) + + setCopied(true) + setTimeout(() => setCopied(false), 3000) + }} + > + {local.children} + <IconHashtag width={18} height={18} /> + <IconCheckCircle width={18} height={18} /> + </a> + <span data-element-tooltip>Copied!</span> + </div> + ) +} + export default function Share(props: { id: string api: string info: Session.Info messages: Record<string, Message.Info> }) { - console.log(props.info) let hasScrolled = false const id = props.id @@ -844,9 +885,9 @@ export default function Share(props: { data-part-type="user-text" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Message"> + <AnchorIcon id={anchor()}> <IconUserCircle width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -874,9 +915,9 @@ export default function Share(props: { data-part-type="ai-text" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="AI response"> + <AnchorIcon id={anchor()}> <IconSparkles width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -922,12 +963,12 @@ export default function Share(props: { data-part-type="ai-model" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Model"> + <AnchorIcon id={anchor()}> <ProviderIcon size={18} provider={assistant().providerID} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -981,9 +1022,9 @@ export default function Share(props: { data-part-type="system-text" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="System message"> + <AnchorIcon id={anchor()}> <IconCpuChip width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1024,12 +1065,12 @@ export default function Share(props: { data-part-type="tool-grep" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Grep files"> + <AnchorIcon id={anchor()}> <IconDocumentMagnifyingGlass width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1118,12 +1159,12 @@ export default function Share(props: { data-part-type="tool-glob" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Glob files"> + <AnchorIcon id={anchor()}> <IconMagnifyingGlass width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1200,12 +1241,12 @@ export default function Share(props: { data-part-type="tool-list" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="List files"> + <AnchorIcon id={anchor()}> <IconRectangleStack width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1267,9 +1308,9 @@ export default function Share(props: { data-part-type="tool-read" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Read file"> + <AnchorIcon id={anchor()}> <IconDocument width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1364,9 +1405,9 @@ export default function Share(props: { data-part-type="tool-write" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Write file"> + <AnchorIcon id={anchor()}> <IconDocumentPlus width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1447,9 +1488,9 @@ export default function Share(props: { data-part-type="tool-edit" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Edit file"> + <AnchorIcon id={anchor()}> <IconPencilSquare width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1506,9 +1547,9 @@ export default function Share(props: { data-part-type="tool-bash" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Bash command"> + <AnchorIcon id={anchor()}> <IconCommandLine width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1554,9 +1595,9 @@ export default function Share(props: { data-part-type="tool-todo" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Plan"> + <AnchorIcon id={anchor()}> <IconQueueList width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1614,9 +1655,9 @@ export default function Share(props: { data-part-type="tool-fetch" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Web fetch"> + <AnchorIcon id={anchor()}> <IconGlobeAlt width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1675,12 +1716,12 @@ export default function Share(props: { data-part-type="tool-fallback" > <div data-section="decoration"> - <a href={`#${anchor()}`} title="Tool call"> + <AnchorIcon id={anchor()}> <IconWrenchScrewdriver width={18} height={18} /> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> @@ -1749,7 +1790,7 @@ export default function Share(props: { data-part-type="fallback" > <div data-section="decoration"> - <a href={`#${anchor()}`}> + <AnchorIcon id={anchor()}> <Switch fallback={ <IconWrenchScrewdriver @@ -1773,7 +1814,7 @@ export default function Share(props: { <IconUserCircle width={18} height={18} /> </Match> </Switch> - </a> + </AnchorIcon> <div></div> </div> <div data-section="content"> diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css index f14d8ba58..c068259ed 100644 --- a/packages/web/src/components/share.module.css +++ b/packages/web/src/components/share.module.css @@ -149,14 +149,80 @@ align-items: center; justify-content: flex-start; - a:first-child { - display: block; - flex: 0 0 auto; - width: 18px; - opacity: 0.65; - svg { - color: var(--sl-color-text-secondary); + [data-element-anchor] { + position: relative; + + a:first-child { display: block; + flex: 0 0 auto; + width: 18px; + opacity: 0.65; + + svg { + color: var(--sl-color-text-secondary); + display: block; + + &:nth-child(3) { + color: var(--sl-color-green-high); + } + } + + svg:nth-child(2), + svg:nth-child(3) { + display: none; + } + &:hover { + svg:nth-child(1) { + display: none; + } + svg:nth-child(2) { + display: block; + } + } + } + + [data-element-tooltip] { + position: absolute; + top: 50%; + left: calc(100% + 12px); + transform: translate(0, -50%); + line-height: 1.1; + padding: 0.375em 0.5em calc(0.375em + 2px); + background: var(--sl-color-white); + color: var(--sl-color-text-invert); + font-size: 0.6875rem; + border-radius: 7px; + white-space: nowrap; + + opacity: 0; + visibility: hidden; + + &::after { + content: ""; + position: absolute; + top: 50%; + left: -15px; + transform: translateY(-50%); + border: 8px solid transparent; + border-right-color: var(--sl-color-white); + } + } + + &[data-status="copied"] { + [data-element-tooltip] { + opacity: 1; + visibility: visible; + } + a, + a:hover { + svg:nth-child(1), + svg:nth-child(2) { + display: none; + } + svg:nth-child(3) { + display: block; + } + } } } |
