Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1654,8 +1654,9 @@
menu, the liveness list, the thread sort and settings menus, the thread
details card) carry data-fork-glass-rim. The selected segment of every
segmented toggle group (tabs, and the pickers drawn as tabs) and of the
fork's SegmentedTabs is one more chip of it; outline pickers and a lone
segmented Toggle keep their look. The rules live in
fork's SegmentedTabs is one more chip of it, as is the design panel's
segment strip (its hairline dropped for the glass edge); outline pickers
and a lone segmented Toggle keep their look. The rules live in
theme.custom.glass-surfaces.css, loaded after the palettes sheet.
The send button (Figma 467:22121) has a translucent white face, white
glyph and bright inset rim. Its highlight hues follow the existing
Expand Down Expand Up @@ -1699,6 +1700,8 @@
- apps/web/src/components/ui/toggle-group.tsx
# Stamps data-fork-segmented-tabs and data-fork-segmented-tab.
- apps/web/src/custom/SegmentedTabs.tsx
# Stamps data-fork-design-segment on the design panel's segment strip.
- apps/web/src/custom/designMode/panel/DesignPanelLayoutControls.tsx
verify:
- apps/web/src/__fork_guards__/forkCoolDarkerTheme.test.ts
- apps/web/src/__fork_guards__/forkGlassSurfaces.test.ts
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/__fork_guards__/forkGlassSurfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,15 @@ describe("fork guard: glass surfaces", () => {
expect(
ruleBodyFor(rules, [`${MARKER}.dark${PALETTE}`, "[data-fork-segmented-tabs]"]),
).toContain("overflow-clip-margin");
expect(readSibling("../custom/designMode/panel/DesignPanelLayoutControls.tsx")).toContain(
"data-fork-design-segment\n",
);
expect(
ruleBodyFor(rules, [
`${MARKER}.dark${PALETTE}`,
'[data-fork-design-segment][aria-pressed="true"]',
]),
).toContain("var(--fork-glass-light)");
for (const source of [
"../components/chat/ChatComposer.tsx",
"../custom/voice/ForkDictationControl.tsx",
Expand Down
25 changes: 15 additions & 10 deletions apps/web/src/custom/designMode/ForkLayersTree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// oxlint-disable t3code/no-native-title-tooltip -- Vendored design-mode island (see .fork/customizations.yaml#fork-design-mode); native titles kept to stay close to the Forge source.
import {
ChevronDownIcon,
ChevronRight,
Expand All @@ -9,6 +8,7 @@ import {
} from "lucide-react";
import { memo, useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";

import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";

import { designModeBridge } from "./designModeBridge";
Expand Down Expand Up @@ -339,28 +339,34 @@ function LayersRail({ runtimeTabId }: { runtimeTabId: string }) {
aria-label="Filter layers"
className="h-6 w-full min-w-0 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground"
/>
{/* The ghost icon-xs Button, like the show-layers toggle this pairs with: the 24px
box and hover fill every icon-only control in the fork's chrome carries. */}
{query ? (
<button
<Button
variant="ghost"
size="icon-xs"
type="button"
title="Clear filter"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three buttons still show native title tooltips. Button merges its props onto the <button> it renders, so title="Clear filter" / "Collapse all" / "Hide layers" reach the DOM unchanged. Removing the file's oxlint-disable only works because t3code/no-native-title-tooltip checks intrinsic elements (/^[a-z]/) and can't see through a component. So the PR description's line "They no longer use native title tooltips" isn't accurate.

It also breaks the pairing the PR is going for. The show-layers toggle these swap with (ForkPreviewLayersToggle.tsx) uses Tooltip > TooltipTrigger render={<Button variant="ghost" size="icon-xs" …/>} > TooltipPopup. So the rail's buttons now look like it, but their tooltip is the OS one. Either wrap each in the same Tooltip pattern and drop title (aria-label already names them), or put the disable comment back and fix the description.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f7fcbf. You're right: Button passed title through, so the OS tooltip was still there and the lint rule just couldn't see it. The three controls now go through a small RailIconButton helper: Tooltip > TooltipTrigger render={<Button variant="ghost" size="icon-xs" aria-label={label} …/>} > TooltipPopup, the same build as ForkPreviewLayersToggle. There's no title anywhere, and the description is corrected.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't actually remove the native tooltip. Button merges title onto the rendered <button>, so hovering still shows the OS tooltip. no-native-title-tooltip only checks intrinsic elements, so moving to <Button title> hides the problem from the lint rule without fixing it, and deleting the file's exemption looks like a fix when it isn't. The PR description ("They no longer use native title tooltips") is wrong as a result.

It's also inconsistent with the control this is meant to match. ForkPreviewLayersToggle wraps the same ghost icon-xs Button in Tooltip / TooltipTrigger render={...} / TooltipPopup, so the show and hide halves of the same toggle now behave differently on hover.

The three buttons also each repeat variant="ghost" size="icon-xs" type="button" title=X aria-label=X. type="button" is already Button's default, and the label is written twice per control. A small local helper removes all of that, and the explanatory JSX comment can go too:

function RailIconButton({ label, ...props }: { label: string } & ComponentProps<typeof Button>) {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="ghost" size="icon-xs" aria-label={label} {...props} />} />
      <TooltipPopup>{label}</TooltipPopup>
    </Tooltip>
  );
}

Then the header is just <RailIconButton label="Clear filter" onClick={...}><XIcon /></RailIconButton> and so on. That passes the lint rule for real, and these controls match the show-layers toggle.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f7fcbf, with the helper you sketched: RailIconButton sets aria-label from label, wraps the button in the same Tooltip as the show-layers toggle, and drops the redundant type/title props and the JSX comment. The design-mode guard now pins label=\"Hide layers\" plus aria-label={label} instead of the old literal.

aria-label="Clear filter"
onClick={() => setQuery("")}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground [&_svg]:size-3"
>
<XIcon />
</button>
</Button>
) : (
<button
<Button
variant="ghost"
size="icon-xs"
type="button"
title="Collapse all"
aria-label="Collapse all"
onClick={() => setExpanded(Object.fromEntries(rows.map((row) => [row.node.id, false])))}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground [&_svg]:size-3.5"
>
<ChevronsDownUpIcon />
</button>
</Button>
)}
<button
<Button
variant="ghost"
size="icon-xs"
type="button"
title="Hide layers"
aria-label="Hide layers"
Expand All @@ -372,10 +378,9 @@ function LayersRail({ runtimeTabId }: { runtimeTabId: string }) {
// This button goes with the rail — hand focus to the control that brings it back.
focusLayersControl(LAYERS_SHOW_BUTTON);
}}
className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:text-foreground [&_svg]:size-3.5"
>
<PanelLeftCloseIcon />
</button>
</Button>
</header>
<div
ref={listRef}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/custom/designMode/panel/DesignPanelFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ function SpacingTokenAffordance({
className={cn(
"me-1 shrink-0 rounded-[3px] px-1 font-mono text-[9px] leading-4 transition-colors",
matched !== null
? "bg-primary/15 text-primary"
? "bg-[var(--fork-design-accent-bg)] text-[var(--fork-design-accent)]"
: "text-muted-foreground/50 hover:text-foreground",
)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function SegmentField({
type="button"
title={option.title}
aria-pressed={selected ? "true" : "false"}
data-fork-design-segment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this PR, this line is no longer the full story. The selected segment declares border border-border here, and theme.custom.glass-surfaces.css removes it with border-width: 0 under Glass. Anyone reading SegmentField (and its JSDoc, "with a hairline border") can't tell that the border goes away on one theme, or that the content box grows 2px when it does.

If SegmentField moves onto SegmentedTab (see the CSS comment), this conflict goes away. If it doesn't, keep the border decision in one place, for example a design token for the selected-segment border that the Glass palette sets to transparent, so the theme sheet isn't undoing a utility class from another file.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f7fcbf. The selected segment's hairline is now shadow-[inset_0_0_0_1px_var(--border)] instead of border border-border. Under Glass, the shared selected-tab rule replaces that shadow with the chip edge, so no theme sheet undoes a class from this file and the segment's box is the same in every theme. The JSDoc says so.

onClick={() => onSelect(option.value)}
className={cn(
"flex h-full flex-1 items-center justify-center rounded px-1 text-xs transition-colors [&_svg]:size-4",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/custom/designMode/panel/ScreenSizeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function ScreenSizeMenu({
</MenuRadioItem>
))}
</MenuRadioGroup>
<MenuSeparator />
<MenuSeparator className="-mx-1" />
<MenuCheckboxItem
variant="switch"
closeOnClick={false}
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/theme.custom.glass-surfaces.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@
overflow-clip-margin: 1px;
}

/* The design panel's segment strips are tabs on a field track too. Their
hairline goes: the glass edge replaces it, and a light drawn inside a
1px border would sit a pixel in from the edge. */
&.dark[data-fork-theme="cool-darker"] [data-fork-design-segment][aria-pressed="true"] {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a third version of "selected tab is a glass chip". The box-shadow here is the same halo and glint pair as [data-fork-segmented-tab][data-pressed] just above it, and the comment calls these "tabs on a field track too". Each segmented control we write by hand now costs a marker, a rule, a manifest line and two guard assertions, and that keeps growing.

A simpler fix: build SegmentField on SegmentedTabs/SegmentedTab (the base-ui ToggleGroup/Toggle). Its mixed state becomes value={[]}, and selection comes from data-pressed instead of aria-pressed. Then the existing rule covers it, and this block, data-fork-design-segment, the manifest line and the new guard assertions can all go. SegmentedTab currently drops className, so it would need to accept the design panel's track and fill classes. That's a small change in a fork-owned file.

If that's too much for this PR, at least add this selector to the existing [data-fork-segmented-tab][data-pressed] rule so the halo and glint are declared once. As written, the next tweak to the tab glass has to be made twice.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8f7fcbf, with no new rule. I didn't rebuild SegmentField on ToggleGroup/Toggle in this PR: that also changes keyboard behaviour (roving focus, arrow keys), which is outside a styling pass. Instead it joins the model at the marker level. Each segment stamps data-fork-segmented-tab, and the selected one adds data-pressed, so the existing [data-fork-segmented-tab][data-pressed] rule is the only place the tab glass is declared. The added block, the data-fork-design-segment marker and its guard assertions are gone. The guard now pins the stamp in DesignPanelLayoutControls.

border-width: 0;
box-shadow: var(--fork-glass-halo-soft), var(--fork-glass-light);
}

/* The composer as the same glass: the vessel is the pane, with the halo
round it and the glint, sheens and the popups' top and bottom glows on a
::after laid over everything inside. Over, because the prompt well covers
Expand Down
Loading