(function P
TAB,
vertical ? TAB_VERTICAL : TAB_HORIZONTAL,
edge,
- active ? TAB_ACTIVE : cn(TAB_IDLE, `${edge}-(--ui-stroke-tertiary)`),
+ active ? TAB_ACTIVE : cn(TAB_IDLE, edge && `${edge}-(--ui-stroke-tertiary)`),
className
)}
data-active={active}
diff --git a/apps/desktop/src/lib/external-link.test.tsx b/apps/desktop/src/lib/external-link.test.tsx
index 772105648017..582b061e8ea6 100644
--- a/apps/desktop/src/lib/external-link.test.tsx
+++ b/apps/desktop/src/lib/external-link.test.tsx
@@ -112,11 +112,24 @@ describe('external link helpers', () => {
expect(openExternal).toHaveBeenCalledWith('https://example.com/path/to/resource')
})
- it('shows a trailing external-link icon', () => {
+ it('hides the trailing external-link icon by default', () => {
installDesktopBridge()
render(
Example link)
+ const link = screen.getByRole('link', { name: 'Example link' })
+ expect(link.querySelector('svg')).toBeNull()
+ })
+
+ it('shows a trailing external-link icon when opted in', () => {
+ installDesktopBridge()
+
+ render(
+
+ Example link
+ ,
+ )
+
const link = screen.getByRole('link', { name: 'Example link' })
expect(link.querySelector('svg')).toBeTruthy()
})
diff --git a/apps/desktop/src/lib/external-link.tsx b/apps/desktop/src/lib/external-link.tsx
index e9770d58f57b..17f82c6a2cb9 100644
--- a/apps/desktop/src/lib/external-link.tsx
+++ b/apps/desktop/src/lib/external-link.tsx
@@ -215,14 +215,14 @@ export function ExternalLink({
className,
href,
onClick,
- showExternalIcon = true,
+ showExternalIcon = false,
...rest
}: ExternalLinkProps) {
const target = normalizeExternalUrl(href)
return (
{
event.stopPropagation()
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index 919b5ff76ef4..0f7fdbdd3b47 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -317,6 +317,10 @@
:root.dark bumps the alpha since a dark card swallows shadow. Removed on
focus (the focus border carries the state). */
--dt-input-inset: inset 0 1px 1px color-mix(in srgb, #000 10%, transparent);
+ /* Hovered inactive tab: deepen it AWAY from the lighter active surface, so
+ hover can never be mistaken for selected. Dark mode needs more — black
+ over a dark gutter shows less. */
+ --ui-tab-hover-darken: 2.5%;
--dt-ring: var(--ui-stroke-primary);
--dt-midground: var(--theme-midground);
--dt-composer-ring: var(--ui-base);
@@ -446,6 +450,7 @@
--backdrop-invert-mul: 0;
/* Dark mode: a dark card needs a stronger black inset to show the recess. */
--dt-input-inset: inset 0 1px 1px color-mix(in srgb, #000 38%, transparent);
+ --ui-tab-hover-darken: 6%;
/* Dark needs a lighter resting border than light mode. */
--dt-input-border: 4%;
@@ -514,6 +519,32 @@
background: repeating-conic-gradient(currentColor 0% 25%, transparent 0% 50%) 0 0 / 0.125rem 0.125rem;
}
+/* Inline content links: a small tinted chip instead of an underline. Tint is
+ currentColor-relative (the old `decoration-current/20` idiom), so text and
+ fill share a hue on every theme from one `color` declaration. */
+.link-chip {
+ --link-chip-tint: 8%;
+
+ /* `ch`/`em` so the chip tracks the text at any size. Block padding is
+ asymmetric because an inline box's content area is ascent+descent — equal
+ padding paints the glyphs high. Keep it small: `padding-block` doesn't grow
+ the line box, so an over-padded chip creeps into the line above. */
+ padding: 0.05ch 0.5ch 0.2ch;
+ border-radius: 0.25rem;
+ color: var(--dt-primary);
+ background: color-mix(in srgb, currentColor var(--link-chip-tint), transparent);
+ /* Explicit: the base layer underlines every `a` (see the :where(a, …) reset). */
+ text-decoration: none;
+ /* A wrapped link gets a chip per line fragment, not one ragged box. */
+ -webkit-box-decoration-break: clone;
+ box-decoration-break: clone;
+ transition: background-color 0.12s ease;
+}
+
+.link-chip:hover {
+ --link-chip-tint: 15%;
+}
+
/* Hover-reveal suppression — the shared, declarative escape hatch.
A collapsed pane slides in when the pointer dwells on its thin edge trigger.
Controls that sit over that edge gutter would drag the panel in by accident.
@@ -610,14 +641,19 @@
--arc-c2: var(--dt-background);
--arc-angle: 160deg;
--arc-width: 0.078125rem;
- --arc-inset: -0.125rem;
+ /* How far the ring stands off its host, per side. */
+ --arc-standoff: 0.125rem;
+ /* An outset ring is only concentric when its radius grows by the standoff
+ (r = r_host + gap); `border-radius: inherit` leaves the corner pinched.
+ Defaults to the shared `rounded-md`; a host on another radius sets it. */
+ --arc-radius: var(--radius-md);
--arc-duration: 2.23s;
pointer-events: none;
position: absolute;
overflow: hidden;
- border-radius: inherit;
- inset: var(--arc-inset);
+ border-radius: calc(var(--arc-radius) + var(--arc-standoff));
+ inset: calc(var(--arc-standoff) * -1);
padding: var(--arc-width);
mask:
linear-gradient(#000 0 0) content-box,
@@ -719,6 +755,23 @@
--arc-duration: 3.27s;
}
+/* Sidebar session row — a thin, clipped, single-line host, so it differs twice
+ from the default ring:
+
+ 1. Flush, not outset. The sidebar scroller is `overflow-x-hidden` and rows sit
+ `gap-px` apart, so an outset ring loses its left/right runs to the clip.
+
+ 2. A visible tail. `--arc-c2` defaults to `--dt-background` — invisible
+ against the sidebar. With c0 a transparent sentinel and c2 invisible, only
+ the bright c1 stop survives each pass, and the gradient has three of them:
+ three moving dots instead of an arc. Tying c2 to c1 gives each spike a
+ falloff. (`arc-nous` already overrides c2, so the hero is unaffected.) */
+.arc-border.arc-row,
+:root.dark .arc-border.arc-row {
+ --arc-standoff: 0rem;
+ --arc-c2: color-mix(in srgb, var(--arc-c1) 45%, transparent);
+}
+
@media (prefers-reduced-motion: reduce) {
.arc-border::before {
animation: none;