mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
fix(ui): tint the active sidebar icon lightly instead of filling it solid (MUL-8351) (#9660)
Reverts the solid fill from 95294b1 back to the 20% body tint. Co-authored-by: Cursor <[email protected]> Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
ce565c1aa4
commit
d5d1382b8f
@@ -36,10 +36,10 @@ Every icon in the product is a Multica drawing exported from `@multica/ui/icons`
|
||||
|
||||
## Body and details
|
||||
|
||||
- Mark the main closed silhouette with `class="b"` (body). The active sidebar item fills it solid.
|
||||
- Mark the main closed silhouette with `class="b"` (body). The active sidebar item fills it at 20% opacity.
|
||||
- Any icon that can appear in navigation must have a body. A test enforces this for route icons.
|
||||
- Only mark closed shapes. An open arc with a fill closes into a stray chord.
|
||||
- Mark every stroke or dot that sits inside the body with `class="k"` (detail). The solid state knocks details out in the background colour; an unmarked detail disappears into the fill.
|
||||
- Mark strokes that sit inside the body with `class="k"` (detail).
|
||||
- **Detail budget.** At most two inner details. Every detail must carry meaning: a check, a symbol, a divider that defines the object. Drop decorative highlights, extra lines and texture. Icons render at 14px in dense rows, where a third detail turns into noise.
|
||||
|
||||
## Loop
|
||||
@@ -48,7 +48,7 @@ Every icon in the product is a Multica drawing exported from `@multica/ui/icons`
|
||||
2. Run `pnpm --filter @multica/ui icons --preview NewName Neighbour1 Neighbour2`. Open the printed HTML file in a browser and check four things:
|
||||
- At 48px: does the geometry sit on the grid, with even optical weight next to its neighbours?
|
||||
- At 18px and 14px: is it still legible, or do inner details merge?
|
||||
- In the solid (active) tile: does the silhouette fill, with every inner detail still visible?
|
||||
- In the tinted (active) tile: does only the silhouette fill?
|
||||
- Against similar glyphs: can it be told apart at a glance?
|
||||
3. Run `pnpm --filter @multica/ui icons` to regenerate `glyphs.ts` and `index.ts`.
|
||||
4. Import the new name from `@multica/ui/icons`. Then run `pnpm --filter @multica/ui test` and the typecheck for the package that uses it.
|
||||
|
||||
@@ -761,7 +761,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-body ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground data-active:hover:bg-sidebar-accent data-active:[&_svg_.icon-body]:fill-current data-active:[&_svg_.icon-cut]:stroke-sidebar-accent data-active:[&_svg_.icon-cut-fill]:fill-sidebar-accent [&_svg]:size-4.5 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-body ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground data-active:hover:bg-sidebar-accent data-active:[&_svg_.icon-body]:fill-current data-active:[&_svg_.icon-body]:[fill-opacity:0.2] [&_svg]:size-4.5 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -33,9 +33,9 @@ export function renderGlyphs() {
|
||||
'import type { IconNode } from "./create-icon";',
|
||||
"",
|
||||
"// Generated by drawings/generate.mjs from drawings/drawings.mjs; do not edit.",
|
||||
"// `icon-body` marks the main silhouette, which the active sidebar item fills",
|
||||
"// solid; `icon-cut` / `icon-cut-fill` mark the stroked / filled details inside",
|
||||
"// it, which that state knocks out in the background colour.",
|
||||
"// `icon-body` marks the main silhouette, which the active sidebar item tints",
|
||||
"// with a light fill; `icon-cut` / `icon-cut-fill` mark the stroked / filled",
|
||||
"// details inside it.",
|
||||
"",
|
||||
...Object.keys(DRAWINGS)
|
||||
.sort()
|
||||
@@ -79,17 +79,14 @@ export const OUTPUTS = {
|
||||
"index.ts": renderIndex,
|
||||
};
|
||||
|
||||
/** Contact sheet of `names` (default: all) at 48, 18 and 14px, plus the solid active-sidebar state. */
|
||||
/** Contact sheet of `names` (default: all) at 48, 18 and 14px, plus the active-sidebar tint. */
|
||||
export function renderPreview(names = Object.keys(DRAWINGS)) {
|
||||
const svg = (body, px) =>
|
||||
`<svg width="${px}" height="${px}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
|
||||
const tiles = names.map((n) => {
|
||||
if (!DRAWINGS[n]) throw new Error(`no drawing named ${n}`);
|
||||
const solid = DRAWINGS[n]
|
||||
.replace(/class="b"/g, 'class="b" fill="currentColor"')
|
||||
.replace(/class="k"([^>]*?)fill="currentColor"/g, 'class="k"$1fill="#ececef"')
|
||||
.replace(/class="k"(?![^>]*fill="#)/g, 'class="k" stroke="#ececef"');
|
||||
return `<figure>${svg(DRAWINGS[n], 48)}<div>${svg(DRAWINGS[n], 18)}${svg(DRAWINGS[n], 14)}<b>${svg(solid, 18)}</b></div><figcaption>${n}</figcaption></figure>`;
|
||||
const tinted = DRAWINGS[n].replace(/class="b"/g, 'class="b" fill="currentColor" fill-opacity=".2"');
|
||||
return `<figure>${svg(DRAWINGS[n], 48)}<div>${svg(DRAWINGS[n], 18)}${svg(DRAWINGS[n], 14)}<b>${svg(tinted, 18)}</b></div><figcaption>${n}</figcaption></figure>`;
|
||||
});
|
||||
return `<!doctype html><meta charset="utf-8"><style>
|
||||
body{margin:0;padding:20px;font:11px system-ui;color:#27272a;display:grid;grid-template-columns:repeat(auto-fill,110px);gap:8px}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { IconNode } from "./create-icon";
|
||||
|
||||
// Generated by drawings/generate.mjs from drawings/drawings.mjs; do not edit.
|
||||
// `icon-body` marks the main silhouette, which the active sidebar item fills
|
||||
// solid; `icon-cut` / `icon-cut-fill` mark the stroked / filled details inside
|
||||
// it, which that state knocks out in the background colour.
|
||||
// `icon-body` marks the main silhouette, which the active sidebar item tints
|
||||
// with a light fill; `icon-cut` / `icon-cut-fill` mark the stroked / filled
|
||||
// details inside it.
|
||||
|
||||
export const Activity: IconNode = [["path",{"d":"M2.75 12h3.5l2.75-7 6 14 2.75-7h3.5"}]];
|
||||
export const AlarmClock: IconNode = [["circle",{"cx":"12","cy":"13","r":"7.75","className":"icon-body"}],["path",{"d":"M12 9.5V13l2.25 1.5","className":"icon-cut"}],["path",{"d":"M3 5.75 5.75 3M21 5.75 18.25 3"}]];
|
||||
|
||||
@@ -22,9 +22,9 @@ describe("routeIconForPath", () => {
|
||||
expect(routeIconForPath(p.autopilots())).not.toBe(routeIconForPath(p.issues()));
|
||||
});
|
||||
|
||||
// The active sidebar item fills the glyph's `icon-body` solid; without one
|
||||
// the active state loses its fill.
|
||||
it.each(Object.entries(ROUTE_ICON_COMPONENTS))("%s marks a body for the solid active state", (_, Icon) => {
|
||||
// The active sidebar item tints the glyph's `icon-body`; without one the
|
||||
// active state loses its fill.
|
||||
it.each(Object.entries(ROUTE_ICON_COMPONENTS))("%s marks a body for the active tint", (_, Icon) => {
|
||||
const { container } = render(<Icon />);
|
||||
expect(container.querySelector("svg .icon-body")).not.toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user