diff --git a/.fork/customizations.yaml b/.fork/customizations.yaml index 7dcd1b2f5f0..51f519cef61 100644 --- a/.fork/customizations.yaml +++ b/.fork/customizations.yaml @@ -516,7 +516,7 @@ - id: sidebar-v2-project-grouping intent: > Sidebar V2's active cards can be grouped under project headers, from a - switch at the top of the fork's project scope row. Upstream's flat list + switch at the top of the fork's projects filter menu. Upstream's flat list stays the default, and the preference is device-local (localStorage) rather than a field in packages/contracts. @@ -791,6 +791,11 @@ surface grain: upstream's noise would compound unspecified drift onto every chrome surface this palette freezes. + Dark --primary and --ring are white (#ffffff), not upstream's blue oklch — + focus underlines (border-ring), switches, and primary fills stay + monochrome. --primary-foreground is the stage (#191919) so white primary + buttons keep contrast. Cool Dark inherits these unless it overrides them. + Two blocks are required and the second is not redundant: upstream re-declares --background, --card, --border and the whole --sidebar-* family on the panel element itself, and a custom property set only at :root never @@ -852,9 +857,10 @@ Sidebar V2 body type is 0.8125rem (13px at the default root) for project headers and slim shelves. The panel remaps Tailwind's --text-xs and --text-sm (and their --line-height companions) to 0.8125rem / 1rem so those - share one size without rewriting each className. Search and All projects - opt out at 0.875rem (14px) via CHROME_CONTROL; card titles and the - repo/branch line opt out with literal rem sizes (0.875rem / 0.75rem — see + share one size without rewriting each className. Search, New thread, Add + project, and the Projects label opt out at 0.875rem (14px) via CHROME_TYPE; + card titles and the repo/branch line opt out with literal rem sizes + (0.875rem / 0.75rem — see sidebar-v2-card-rows) so the remap cannot flatten them. Rem, not px: a px pin would freeze the sidebar while h-4 and the rest of the app still honour the browser font-size setting. Line-height stays 1rem so the card's @@ -925,21 +931,24 @@ one leaves the other subtracting from a stale number, and the cards drift off the chrome icons' axis with nothing failing. - Chrome rows use ps-2 pe-3 with an inner px-1 control so their leading icon - also sits at 12px — matching card status and the group folder glyph. Search - and All projects controls are 28px tall (h-7), not Figma's 24, at 0.875rem - (14px) type — literal so the panel's 13px text-xs remap cannot shrink them. + Action rows (Search, New thread, Add project) use ps-2 pe-3 with an inner + px-1 control so their leading icon also sits at 12px — matching card status + and the group folder glyph. The Projects header uses symmetric px-3 (no + leading icon). Those controls are 32px tall (h-8), not Figma's 24, at + 0.875rem (14px) type — literal so the panel's 13px text-xs remap cannot + shrink them. These rows' own trailing inset is then 4px, and that 4px belongs to sidebar-v2-row-action-hit-area, which owns the trailing glyph column's single axis. It is not a scrollbar term and must not become one again. The brand moves to the header's trailing edge, where it stops competing - with the toggle for the corner the eye starts at. The search and project - rows are 36px and 12px rather than 32px and 14px so they read as chrome - rather than as two more list items; the project row takes a leading - caret-up-down with no trailing chevron and no favicon, and its trailing - action is folder-open. + with the toggle for the corner the eye starts at. Search, New thread, and + Add project are labeled action rows; Projects is a static muted label + whose trailing ListFilter (FunnelSimple) opens the scope / group-by menu + that used to live on the clickable projects control. When a project scope + is active the funnel lifts to foreground (and names the scope in its + aria-label / tooltip) so a filtered list is never a silent mode. The Dev channel's header art is the designer's own dither PNG instead of upstream's blue blueprint, painted as a covering background rather than a @@ -1012,12 +1021,12 @@ - id: fork-workspace-header intent: > - The workspace topbar is 56px. One token drives both it and the sidebar - header; the design draws them at 56 and 52, but they sit side by side - across a 1px divider, so a 4px split would misalign the seam. Read as - unintentional and kept equal. The .wco rule re-derives the token from - env(titlebar-area-height), so on Windows with Controls Overlay the OS still - dictates the height and only the fallback moves. + The workspace topbar is 52px. One token (--workspace-topbar-height) drives + both it and the sidebar header so they stay seam-aligned across the + divider. Settings and DiffPanelShell already hardcode the same 52px. The + .wco rule re-derives the token from env(titlebar-area-height), so on + Windows with Controls Overlay the OS still dictates the height and only + the fallback moves. Project and title swap weight: the project is the fixed, repeating half, so it takes semibold and becomes the landmark you navigate to, while the title diff --git a/apps/web/src/__fork_guards__/forkSidebarChrome.test.ts b/apps/web/src/__fork_guards__/forkSidebarChrome.test.ts index 1547ccbec24..cc735700239 100644 --- a/apps/web/src/__fork_guards__/forkSidebarChrome.test.ts +++ b/apps/web/src/__fork_guards__/forkSidebarChrome.test.ts @@ -149,13 +149,23 @@ describe("fork guard: fork-sidebar-chrome", () => { }); it("keeps the search and project rows fork-owned", () => { - // ~150 lines of pure presentation. Fenced in place it left SidebarV2 - // carrying the whole rewrite; here the fence is two call sites. + // Pure presentation. Fenced in place it left SidebarV2 carrying the whole + // rewrite; here the fence is two call sites (actions + projects filter). const sidebarV2 = readSibling("../components/SidebarV2.tsx"); - expect(sidebarV2).toContain(" { @@ -245,12 +255,30 @@ describe("fork guard: fork-sidebar-chrome", () => { expect(rows.split("CHROME_ROW_ICON_TINT").length - 1).toBeGreaterThanOrEqual(3); }); - it("keeps Search and All projects at 14px, outside the panel's 13px remap", () => { + it("keeps chrome type at 14px for actions and the Projects label", () => { + const rows = readSibling("../custom/SidebarV2ChromeRows.tsx"); + const type = /const CHROME_TYPE\s*=\s*"([^"]+)"/u.exec(rows)?.[1]; + expect(type).toBeDefined(); + expect(type).toContain("text-[0.875rem]"); + expect(type).not.toMatch(/\btext-xs\b/u); + expect(type).not.toMatch(/\btext-sm\b/u); + // Both the interactive control and the static Projects label read it. + expect(rows).toContain("CHROME_TYPE"); + expect(rows.split("CHROME_TYPE").length - 1).toBeGreaterThanOrEqual(3); + }); + + it("keeps Projects as a static label with an active-aware filter funnel", () => { const rows = readSibling("../custom/SidebarV2ChromeRows.tsx"); - const className = /const CHROME_CONTROL[\s\S]*?"([^"]+)"/u.exec(rows)?.[1]; - expect(className).toBeDefined(); - expect(className).toContain("text-[0.875rem]"); - expect(className).not.toMatch(/\btext-xs\b/u); - expect(className).not.toMatch(/\btext-sm\b/u); + expect(rows).toMatch(/>\s*Projects\s* { expect(blockFor(theme, STAGE)).toContain("--background: #191919"); }); + it("keeps dark primary and ring monochrome white", () => { + // Upstream's dark --primary / --ring are the same blue oklch. Focus + // underlines (border-ring on Search refs, etc.), switches, and primary + // fills must stay white in the fork — losing these drops blue accents + // back into an otherwise monochrome shell. + const stage = blockFor(theme, STAGE); + expect(stage).toContain("--primary: #ffffff"); + expect(stage).toContain("--primary-foreground: #191919"); + expect(stage).toContain("--ring: #ffffff"); + }); + it("keeps the panel above the stage, as the intent names", () => { // Intent: fork-surface-palette. Assert the ordering, not only the // constants — a value tweak is fine, silently inverting the hierarchy is diff --git a/apps/web/src/__fork_guards__/forkWorkspaceHeader.test.ts b/apps/web/src/__fork_guards__/forkWorkspaceHeader.test.ts index b657bd2a691..7cf39fb224b 100644 --- a/apps/web/src/__fork_guards__/forkWorkspaceHeader.test.ts +++ b/apps/web/src/__fork_guards__/forkWorkspaceHeader.test.ts @@ -72,12 +72,13 @@ describe("fork guard: fork-workspace-header", () => { expect(theme).toMatch(/--fork-pill-fg:\s*var\(--foreground\)/u); }); - it("raises the topbar to 56px in both the default and WCO derivations", () => { - // Two declarations, and missing the second leaves Windows Controls Overlay - // builds 4px short of every other platform. - expect(upstreamCss).toMatch(/--workspace-topbar-height:\s*56px/u); + it("keeps the topbar at 52px in both the default and WCO derivations", () => { + // One token for sidebar header and workspace header — a drift between the + // two misaligns the seam. Missing the WCO fallback leaves Windows Controls + // Overlay builds on a different height than every other platform. + expect(upstreamCss).toMatch(/--workspace-topbar-height:\s*52px/u); expect(upstreamCss).toMatch( - /--workspace-topbar-height:\s*env\(titlebar-area-height,\s*56px\)/u, + /--workspace-topbar-height:\s*env\(titlebar-area-height,\s*52px\)/u, ); }); diff --git a/apps/web/src/__fork_guards__/sidebarV2ProjectGrouping.test.ts b/apps/web/src/__fork_guards__/sidebarV2ProjectGrouping.test.ts index 364a57c6820..549f3a8524e 100644 --- a/apps/web/src/__fork_guards__/sidebarV2ProjectGrouping.test.ts +++ b/apps/web/src/__fork_guards__/sidebarV2ProjectGrouping.test.ts @@ -31,7 +31,7 @@ const sidebar = readSibling("../components/SidebarV2.tsx"); const chromeRows = readSibling("../custom/SidebarV2ChromeRows.tsx"); describe("fork guard: sidebar-v2-project-grouping", () => { - it("offers the switch from the project scope menu", () => { + it("offers the switch from the projects filter menu", () => { expect(chromeRows).toContain("Group by project"); expect(chromeRows).toContain("onGroupByProjectChange"); // Toggling a view preference must not dismiss the menu it lives in. diff --git a/apps/web/src/__fork_guards__/sidebarV2RowActionHitArea.test.ts b/apps/web/src/__fork_guards__/sidebarV2RowActionHitArea.test.ts index 4aa8e23cea5..5389b9e37b4 100644 --- a/apps/web/src/__fork_guards__/sidebarV2RowActionHitArea.test.ts +++ b/apps/web/src/__fork_guards__/sidebarV2RowActionHitArea.test.ts @@ -172,11 +172,14 @@ describe("fork guard: sidebar-v2-row-action-hit-area", () => { // 16px icons, matching the marks they line up with. Asserted positively: // a negative on `size-5` is anchored to class order and walks straight // through `className="shrink-0 size-5"`. - for (const name of ["PlusCircleIcon", "FolderOpenIcon"]) { - const tag = new RegExp(`<${name} className="([^"]*)"`, "u").exec(chromeRows)?.[1]; - expect(tag, `${name} is not rendered in the chrome rows`).toBeDefined(); - expect(tag).toMatch(/\bsize-4\b/u); - } + // New thread / Add project render through ChromeLabeledAction's Icon slot; + // the filter still mounts ListFilterIcon directly. Pin size-4 either way. + expect(chromeRows).toContain("icon={PlusCircleIcon}"); + expect(chromeRows).toContain("icon={FolderPlusIcon}"); + const labeledIcon = / {/* fork:begin fork-sidebar-chrome — see .fork/customizations.yaml#fork-sidebar-chrome - Both control rows are fork-owned; only these call sites live here. + Control rows are fork-owned; only these call sites live here. See custom/SidebarV2ChromeRows.tsx. */} - { void handleProjectActions(event, project); }} - onAddProject={openAddProjectCommandPalette} groupByProject={groupByProject} onGroupByProjectChange={setGroupByProject} // A switch that visibly does nothing teaches nothing. Where headers diff --git a/apps/web/src/custom/SidebarV2ChromeRows.tsx b/apps/web/src/custom/SidebarV2ChromeRows.tsx index d08b9eec017..ea080a2deea 100644 --- a/apps/web/src/custom/SidebarV2ChromeRows.tsx +++ b/apps/web/src/custom/SidebarV2ChromeRows.tsx @@ -1,31 +1,32 @@ /** - * The Sidebar V2 control rows — search, and the project scope filter — see - * `.fork/customizations.yaml#fork-sidebar-chrome`. + * The Sidebar V2 control rows — search, new thread, add project, and the + * projects filter — see `.fork/customizations.yaml#fork-sidebar-chrome`. * - * Metrics from Figma t3-fork node 113:3718: outer `ps-2 pe-3` (8/12), inner - * control `px-1` + `gap-1`. Controls are 28px tall (h-7); the leading icon - * still sits at 12px — the same axis as each card's status (list pad 8 + + * Metrics from Figma t3-fork node 149:6235: action rows use outer `ps-2 pe-3` + * (8/12); the Projects header uses symmetric `px-3` (no leading icon). Inner + * controls keep `px-1` + `gap-1`. Controls are 32px tall (h-8); the leading + * icon still sits at 12px — the same axis as each card's status (list pad 8 + * card `px-1`). * - * Fork-owned rather than fenced in place: this is ~150 lines of pure - * presentation, and leaving it inline meant `SidebarV2.tsx` carried the whole - * rewrite while the manifest could only watch the file it sat in. Here the - * fence upstream carries collapses to a call site. + * Fork-owned rather than fenced in place: this is pure presentation, and + * leaving it inline meant `SidebarV2.tsx` carried the whole rewrite while the + * manifest could only watch the file it sat in. Here the fence upstream + * carries collapses to call sites. * * The prop surface is wide because these rows are genuinely interactive — a - * command palette trigger, a radio group, two buttons and a per-project - * overflow action. It is all data and callbacks, though: no upstream state is - * reached into, so an upstream refactor of how that state is produced cannot - * break this file. + * command palette trigger, a radio group, two labeled actions and a + * per-project overflow action. It is all data and callbacks, though: no + * upstream state is reached into, so an upstream refactor of how that state + * is produced cannot break this file. */ import type { EnvironmentId } from "@t3tools/contracts"; -import type { MouseEvent as ReactMouseEvent } from "react"; +import type { ComponentType, MouseEvent as ReactMouseEvent, ReactNode, SVGProps } from "react"; import { - ChevronsUpDownIcon, EllipsisIcon, FolderIcon, - FolderOpenIcon, + FolderPlusIcon, + ListFilterIcon, PlusCircleIcon, SearchIcon, } from "lucide-react"; @@ -72,7 +73,7 @@ export interface SidebarV2ChromeProjectGroup { * once pushed its cards short of these rows by its reserved gutter; the list * now gives that width back out of its own end padding, so a gutter term * reappearing here would double-count it. */ -const CONTROL_ROW = cn("flex h-7 items-center gap-1", SIDEBAR_V2_TRAILING_OFFSET.chromeRow); +const CONTROL_ROW = cn("flex h-8 items-center gap-1", SIDEBAR_V2_TRAILING_OFFSET.chromeRow); /** Displaces sidebarMenuButtonVariants' base icon pair (muted-foreground at opacity-60, upstream v0.0.30): parent-level [&>svg] selectors outweigh the icon's own class, so without this the fork's /80 tint on the glyph is dead @@ -81,6 +82,11 @@ const CONTROL_ROW = cn("flex h-7 items-center gap-1", SIDEBAR_V2_TRAILING_OFFSET button that renders an icon as a direct child; the guard asserts the merged outcome, so a base-selector change that stops displacing shows up red. */ export const CHROME_ROW_ICON_TINT = "[&>svg]:text-sidebar-muted-foreground/80 [&>svg]:opacity-100"; +/** Shared 14px type for Search / New thread / Add project / Projects — literal + so the panel's 13px text-xs remap cannot shrink them. Action controls and + the static Projects label both read this; retuning it once keeps them + aligned. */ +const CHROME_TYPE = "text-[0.875rem] leading-4 font-normal text-sidebar-muted-foreground"; /** size-6 per the Figma chrome (24px boxes throughout the card-v2 design). That is the WCAG 2.5.8 floor for a fine pointer on an always-on control — deliberate and design-wide, not this button's private call; see the box @@ -96,80 +102,133 @@ const TRAILING_BUTTON = cn( const TOUCH_TARGET = "pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"; -const CHROME_CONTROL = - // Literal 0.875rem (14px): the panel remaps text-xs/text-sm to 13px for - // headers and slim shelves; Search / All projects stay a step larger. - "h-7 gap-1 rounded-md border-0 bg-transparent px-1 text-[0.875rem] leading-4 font-normal text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"; +const CHROME_CONTROL = cn( + "h-8 gap-1 rounded-md border-0 bg-transparent px-1 hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar", + CHROME_TYPE, +); + +const ACTION_GROUP = "ps-2 pe-3"; + +type ChromeIcon = ComponentType>; + +/** New thread and Add project share one shape; Search keeps its own + (CommandDialogTrigger + Kbd). */ +function ChromeLabeledAction(props: { + readonly icon: ChromeIcon; + readonly label: string; + readonly ariaLabel: string; + readonly title?: string | undefined; + readonly testId: string; + readonly disabled?: boolean | undefined; + readonly onClick: () => void; + readonly trailing?: ReactNode; +}) { + const Icon = props.icon; + return ( +
+ + + {props.label} + {props.trailing} + +
+ ); +} + +function ChromeSearchRow(props: { readonly commandPaletteShortcutLabel: string | null }) { + return ( +
+
+ + } + > + +
Search
+ {props.commandPaletteShortcutLabel ? ( + + {props.commandPaletteShortcutLabel} + + ) : null} +
+
+
+ ); +} -export function SidebarV2SearchRow(props: { +/** Search + New thread + Add project share one group so Figma's stacked + * action block (149:6235) does not pick up inter-group padding. */ +export function SidebarV2ChromeActionRows(props: { readonly commandPaletteShortcutLabel: string | null; readonly newThreadShortcutLabel: string | null; readonly newThreadDisabled: boolean; readonly onNewThread: () => void; + readonly onAddProject: () => void; }) { + const newThreadDisabledReason = props.newThreadDisabled + ? "Add a project to start a thread" + : undefined; return ( - // Figma 113:3718 — Search: pl-8 pr-12 pt-16 pb-4. - -
-
- - } - > - -
Search
- {props.commandPaletteShortcutLabel ? ( - - {props.commandPaletteShortcutLabel} - - ) : null} -
-
-
- - - } - > - - - - {props.newThreadShortcutLabel - ? `New thread (${props.newThreadShortcutLabel})` - : "New thread"} - - -
-
+ + + + {props.newThreadShortcutLabel} + + ) : null + } + /> + ); } export function SidebarV2ProjectScopeRow(props: { readonly projectGroups: ReadonlyArray; - readonly scopedProjectGroup: TProject | null; readonly projectScopeKey: string | null; + /** Display name for the active scope — null when showing all projects. */ + readonly scopedProjectDisplayName: string | null; readonly onProjectScopeChange: (scopeKey: string | null) => void; readonly menuOpen: boolean; readonly onMenuOpenChange: (open: boolean) => void; readonly onProjectActions: (event: ReactMouseEvent, project: TProject) => void; - readonly onAddProject: () => void; readonly groupByProject: boolean; readonly onGroupByProjectChange: (groupByProject: boolean) => void; /** Non-null disables the switch and says why — see the call site. */ @@ -177,27 +236,60 @@ export function SidebarV2ProjectScopeRow + // Figma 149:6235 — Projects: static label + filter trigger, px-12 py-8. + // pt-2 is the 8px gap between the action block and this header. +
+ + Projects + - - {/* Leading caret, no trailing chevron and no favicon: the design - puts the affordance where the eye enters the row, and the label - already names the project the favicon used to repeat. */} - - - {props.scopedProjectGroup?.displayName ?? "All projects"} - - - + + svg]:text-sidebar-foreground", + )} + aria-label={filterAriaLabel} + data-testid="sidebar-v2-project-filter" + data-active={isScoped ? "true" : undefined} + /> + } + /> + } + > + + + {filterTooltip} + + {/* Above the scope list, not below it: with enough projects the list scrolls, and a preference that decides how the whole sidebar reads should not be the thing you have to scroll to. @@ -270,26 +362,6 @@ export function SidebarV2ProjectScopeRow - - - } - > - {/* FolderOpen rather than FolderPlus, per the design. The action is - unchanged — it opens the palette to pick a folder to add — and - "open a folder" is the more literal reading of the click. */} - - - New project -
); diff --git a/apps/web/src/custom/icons/lucide-phosphor.tsx b/apps/web/src/custom/icons/lucide-phosphor.tsx index 8c162ba7630..03be2e5c889 100644 --- a/apps/web/src/custom/icons/lucide-phosphor.tsx +++ b/apps/web/src/custom/icons/lucide-phosphor.tsx @@ -93,6 +93,7 @@ import { FolderDashed as PhFolderDashed, FolderOpen as PhFolderOpen, FolderPlus as PhFolderPlus, + FunnelSimple as PhFunnelSimple, Gauge as PhGauge, Gear as PhGear, GearSix as PhGearSix, @@ -355,6 +356,8 @@ export const MoreVertical = icon("more-vertical", PhDotsThreeVertical, "bold"); export const PlayIcon = icon("play", PhPlay, "duotone"); export const ListChecksIcon = icon("list-checks", PhListChecks, "duotone"); export const ListTodoIcon = icon("list-todo", PhListChecks, "duotone"); +// Figma FunnelSimple — lucide's ListFilter (three descending bars). +export const ListFilterIcon = icon("list-filter", PhFunnelSimple, "bold"); export const Camera = icon("camera", PhCamera, "duotone"); export const PipetteIcon = icon("pipette", PhEyedropper, "duotone"); export const PaletteIcon = icon("palette", PhPalette, "duotone"); diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 6d5ea1d5e0c..60f240aec99 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -84,15 +84,14 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --glass-saturation: 1.14; --desktop-window-right-resize-inset: 0px; /* fork:begin fork-workspace-header — see .fork/customizations.yaml#fork-workspace-header - 56px, up from 52px. One token drives both the workspace header and the - sidebar header; the design draws them at 56 and 52 respectively, but they - sit side by side across a 1px divider, so a 4px split would misalign the - seam every user sees. Read as unintentional and kept equal. + 52px. One token drives both the workspace header and the sidebar header + so they stay seam-aligned across the divider. Settings and DiffPanelShell + already hardcode the same 52px. `.wco` below re-derives this from env(titlebar-area-height), so on Windows with Controls Overlay the OS still dictates the height and only the fallback moves. */ - --workspace-topbar-height: 56px; + --workspace-topbar-height: 52px; /* fork:end fork-workspace-header */ --workspace-controls-top: 0px; --workspace-controls-left: calc(env(safe-area-inset-left) + 0.75rem); @@ -118,7 +117,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil .wco { /* fork:begin fork-workspace-header — see .fork/customizations.yaml#fork-workspace-header */ - --workspace-topbar-height: env(titlebar-area-height, 56px); + --workspace-topbar-height: env(titlebar-area-height, 52px); /* fork:end fork-workspace-header */ --workspace-controls-top: env(titlebar-area-y, 0px); --workspace-controls-left: calc(env(titlebar-area-x, 0px) + 0.75rem); diff --git a/apps/web/src/theme.custom.css b/apps/web/src/theme.custom.css index bd8874e3ef1..244839f59c7 100644 --- a/apps/web/src/theme.custom.css +++ b/apps/web/src/theme.custom.css @@ -104,6 +104,13 @@ :root[data-fork="noahhendrickson-t3code"].dark { --background: #191919; --border: #2d2e2e; + /* Upstream's dark --primary / --ring are the same blue oklch. Fork chrome + is monochrome: focus underlines (border-ring), switches, and primary + fills read as white. --primary-foreground is the stage so white buttons + keep contrast. */ + --primary: #ffffff; + --primary-foreground: #191919; + --ring: #ffffff; /* Upstream lays a 0.035-opacity white noise over every chrome surface (`@utility surface-grain`, applied to the sidebar shell and the workspace inset). It was calibrated against a #000 shell. On these surfaces any