| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { ReactNode } from "react"; |
| import type { DisplayMode, FieldType, FolderIcon, FolderTone } from "./types"; |
| import { DEFAULT_FOLDER_SHAPE, DEFAULT_FOLDER_TONE } from "./types"; |
| import type { IconShape } from "./iconShapes"; |
| import type { ChartKindKey } from "./iconShapes"; |
| import { |
| CHART_KIND_SHAPES, |
| CHART_KIND_TONE, |
| FOLDER_SHAPE_PATHS, |
| folderTonePaint, |
| MODE_SHAPES, |
| TYPE_SHAPES, |
| } from "./iconShapes"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function Shapes({ shapes }: { shapes?: IconShape[] }) { |
| return ( |
| <> |
| {(shapes ?? []).map((s, i) => |
| s.fill ? ( |
| <path key={i} d={s.d} fill="currentColor" /> |
| ) : ( |
| <path |
| key={i} |
| d={s.d} |
| fill="none" |
| stroke="currentColor" |
| strokeWidth={1.35} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| /> |
| ) |
| )} |
| </> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function FieldTypeIcon({ |
| type, |
| size = 14, |
| title, |
| }: { |
| type: FieldType | "measure"; |
| size?: number; |
| title?: string; |
| }) { |
| return ( |
| <svg |
| className="cg-type-icon" |
| width={size} |
| height={size} |
| viewBox="0 0 16 16" |
| aria-hidden={title ? undefined : true} |
| role={title ? "img" : undefined} |
| aria-label={title} |
| > |
| {title && <title>{title}</title>} |
| <Shapes shapes={TYPE_SHAPES[type === "measure" ? "currency" : type]} /> |
| </svg> |
| ); |
| } |
|
|
| |
| export function ModeIcon({ mode, size = 14 }: { mode: DisplayMode; size?: number }) { |
| return ( |
| <svg className="cg-mode-icon" width={size} height={size} viewBox="0 0 16 16" aria-hidden> |
| <Shapes shapes={MODE_SHAPES[mode]} /> |
| </svg> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function ToneModeIcon({ |
| mode, |
| tone, |
| size = 15, |
| }: { |
| mode: DisplayMode; |
| tone: FolderTone; |
| size?: number; |
| }) { |
| const paint = folderTonePaint(tone); |
| return ( |
| <svg |
| className="cg-mode-icon cg-tone-icon" |
| width={size} |
| height={size} |
| viewBox="0 0 16 16" |
| aria-hidden |
| > |
| {/* R5: `?? []` for the same reason as `Shapes` β `mode` is a STORED display mode off |
| the wire, so a view saved under a kind this build no longer knows makes the lookup |
| undefined and `.map` a TypeError. */} |
| {(MODE_SHAPES[mode] ?? []).map((s, i) => |
| s.fill ? ( |
| <path key={i} d={s.d} fill={paint.stroke} /> |
| ) : ( |
| <path |
| key={i} |
| d={s.d} |
| // OUTLINE ONLY β the Airtable register (owner, 2026-07-29: "it doesn't have a |
| // full color, only the linings"). This used to tint the first path with |
| // `paint.fill`, which made every mark read as a solid pastel BLOCK with the |
| // drawing hidden inside it: at 16px the grid's own lines were invisible against |
| // their own fill. The tone now lives entirely in the STROKE, which is the `-d` |
| // weight and is the only part that was ever carrying the shape. |
| fill="none" |
| stroke={paint.stroke} |
| strokeWidth={1.35} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| /> |
| ) |
| )} |
| </svg> |
| ); |
| } |
|
|
| |
| |
| |
| |
| export function ChartKindIcon({ |
| kind, |
| size = 15, |
| }: { |
| kind: ChartKindKey; |
| size?: number; |
| }) { |
| const paint = folderTonePaint(CHART_KIND_TONE[kind]); |
| return ( |
| <svg |
| className="cg-mode-icon cg-tone-icon" |
| width={size} |
| height={size} |
| viewBox="0 0 16 16" |
| aria-hidden |
| > |
| {/* R5: same guard, same reason β `kind` comes from a saved chart definition. */} |
| {(CHART_KIND_SHAPES[kind] ?? []).map((s, i) => ( |
| <path |
| key={i} |
| d={s.d} |
| // Same rule as ToneModeIcon: the FIRST path is the shape's body, so it takes the |
| // pastel fill and the rest stay strokes over it. Bar/line are pure strokes, so |
| // their first path has no interior to fill and this is a no-op for them. |
| fill={i === 0 && |
| (kind === "area" || kind === "donut" || kind === "kpi" || kind === "table") |
| ? paint.fill |
| : "none"} |
| stroke={paint.stroke} |
| strokeWidth={1.35} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| /> |
| ))} |
| </svg> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const mi = { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none" } as const; |
| const miStroke = { |
| stroke: "currentColor", |
| strokeWidth: 1.3, |
| strokeLinecap: "round" as const, |
| strokeLinejoin: "round" as const, |
| }; |
|
|
| const MENU_ICONS = { |
| duplicate: ( |
| <> |
| <rect x="5.5" y="5.5" width="8" height="8" rx="1.5" {...miStroke} /> |
| <path d="M10.5 2.5h-7a1 1 0 0 0-1 1v7" {...miStroke} /> |
| </> |
| ), |
| insertLeft: ( |
| <> |
| <line x1="3" y1="2.5" x2="3" y2="13.5" {...miStroke} /> |
| <path d="M13.5 8H6.5M9.2 5.3 6.5 8l2.7 2.7" {...miStroke} /> |
| </> |
| ), |
| insertRight: ( |
| <> |
| <line x1="13" y1="2.5" x2="13" y2="13.5" {...miStroke} /> |
| <path d="M2.5 8h7M6.8 5.3 9.5 8l-2.7 2.7" {...miStroke} /> |
| </> |
| ), |
| addEnd: <path d="M8 3v10M3 8h10" {...miStroke} />, |
| rename: <path d="m9.8 3.2 3 3L6.2 12.8l-3.6.6.6-3.6 6.6-6.6Z" {...miStroke} />, |
| description: <path d="M3 4.5h10M3 8h10M3 11.5h6" {...miStroke} />, |
| permissions: ( |
| <> |
| <rect x="3.5" y="7" width="9" height="6" rx="1" {...miStroke} /> |
| <path d="M5.5 7V5.2a2.5 2.5 0 0 1 5 0V7" {...miStroke} /> |
| </> |
| ), |
| |
| |
| unlock: ( |
| <> |
| <rect x="3.5" y="7" width="9" height="6" rx="1" {...miStroke} /> |
| <path d="M5.5 7V5.2a2.5 2.5 0 0 1 5 0" {...miStroke} /> |
| </> |
| ), |
| format: ( |
| <> |
| <path d="M3 5h5.5M12.5 5H13M3 11h1M7 11h6" {...miStroke} /> |
| <circle cx="10.5" cy="5" r="1.6" {...miStroke} /> |
| <circle cx="4.9" cy="11" r="1.6" {...miStroke} /> |
| </> |
| ), |
| swap: ( |
| <path |
| d="M3.5 6h9M10.2 3.7 12.5 6l-2.3 2.3M12.5 10.5h-9M5.8 8.2 3.5 10.5l2.3 2.3" |
| {...miStroke} |
| /> |
| ), |
| sortAsc: ( |
| <> |
| <path d="M4.5 3v10M4.5 13l-2-2M4.5 13l2-2" {...miStroke} /> |
| <path d="M8.5 4.5h2M8.5 8h3.5M8.5 11.5h5" {...miStroke} /> |
| </> |
| ), |
| sortDesc: ( |
| <> |
| <path d="M4.5 3v10M4.5 13l-2-2M4.5 13l2-2" {...miStroke} /> |
| <path d="M8.5 4.5h5M8.5 8h3.5M8.5 11.5h2" {...miStroke} /> |
| </> |
| ), |
| off: ( |
| <> |
| <circle cx="8" cy="8" r="5.5" {...miStroke} /> |
| <line x1="4.4" y1="11.6" x2="11.6" y2="4.4" {...miStroke} /> |
| </> |
| ), |
| filter: <path d="M2.5 3.5h11L9.5 8.5v3.8L6.5 14V8.5L2.5 3.5Z" {...miStroke} />, |
| group: ( |
| <> |
| <line x1="2.5" y1="4" x2="13.5" y2="4" {...miStroke} /> |
| <line x1="5.5" y1="8" x2="13.5" y2="8" {...miStroke} /> |
| <line x1="5.5" y1="12" x2="13.5" y2="12" {...miStroke} /> |
| </> |
| ), |
| pin: ( |
| <> |
| <path d="M6 2.5h4l-.5 4L12 9v1H4V9l2.5-2.5L6 2.5Z" {...miStroke} /> |
| <line x1="8" y1="10" x2="8" y2="13.5" {...miStroke} /> |
| </> |
| ), |
| unpin: ( |
| <> |
| <path d="M6 2.5h4l-.5 4L12 9v1H4V9l2.5-2.5L6 2.5Z" {...miStroke} /> |
| <line x1="8" y1="10" x2="8" y2="13.5" {...miStroke} /> |
| <line x1="3" y1="13" x2="13" y2="3" {...miStroke} /> |
| </> |
| ), |
| hide: ( |
| <> |
| <path d="M2.5 8s2-3.5 5.5-3.5S13.5 8 13.5 8s-2 3.5-5.5 3.5S2.5 8 2.5 8Z" {...miStroke} /> |
| <circle cx="8" cy="8" r="1.5" {...miStroke} /> |
| <line x1="3.5" y1="12.5" x2="12.5" y2="3.5" {...miStroke} /> |
| </> |
| ), |
| trash: <path d="M3.5 4.5h9M6.5 4.5V3h3v1.5M5 4.5l.6 8.5h4.8l.6-8.5" {...miStroke} />, |
| |
| download: ( |
| <> |
| <path d="M8 2.5v7M5.3 6.8 8 9.5l2.7-2.7" {...miStroke} /> |
| <path d="M3 11v1.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V11" {...miStroke} /> |
| </> |
| ), |
| cohortAdd: ( |
| <> |
| <path d="M2.5 4h7M2.5 7.5h5M2.5 11h4" {...miStroke} /> |
| <path d="M11.5 8.5v5M9 11h5" {...miStroke} /> |
| </> |
| ), |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| upload: ( |
| <> |
| <path d="M8 9.5v-7M5.3 5.2 8 2.5l2.7 2.7" {...miStroke} /> |
| <path d="M3 11v1.5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V11" {...miStroke} /> |
| </> |
| ), |
| } as const; |
|
|
| export type MenuIconName = keyof typeof MENU_ICONS; |
|
|
| |
| export function MenuIcon({ name }: { name: MenuIconName }) { |
| return ( |
| <svg {...mi} aria-hidden> |
| {MENU_ICONS[name]} |
| </svg> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function MenuLabel({ |
| icon, |
| text, |
| }: { |
| icon: MenuIconName | ReactNode; |
| text: string; |
| }) { |
| return ( |
| <> |
| <span className="cg-mi-ic" aria-hidden> |
| {typeof icon === "string" ? <MenuIcon name={icon as MenuIconName} /> : icon} |
| </span> |
| <span className="cg-mi-text">{text}</span> |
| </> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function EyeOffIcon({ size = 14 }: { size?: number }) { |
| return ( |
| <svg |
| className="cg-eyeoff-icon" |
| width={size} |
| height={size} |
| viewBox="0 0 16 16" |
| fill="none" |
| aria-hidden |
| > |
| <path |
| d="M2.5 8s2-3.5 5.5-3.5S13.5 8 13.5 8s-2 3.5-5.5 3.5S2.5 8 2.5 8Z" |
| stroke="currentColor" |
| strokeWidth={1.4} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| /> |
| <path |
| d="M9.6 8a1.6 1.6 0 1 1-3.2 0 1.6 1.6 0 0 1 3.2 0Z" |
| stroke="currentColor" |
| strokeWidth={1.4} |
| strokeLinejoin="round" |
| /> |
| <path |
| d="M3.3 12.7 12.7 3.3" |
| stroke="currentColor" |
| strokeWidth={1.4} |
| strokeLinecap="round" |
| /> |
| </svg> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| export function LockMark({ size = 11 }: { size?: number }) { |
| return ( |
| <svg |
| className="cg-lock-mark" |
| width={size} |
| height={size} |
| viewBox="0 0 16 16" |
| aria-hidden |
| > |
| <path |
| d="M4.2 7.2h7.6v5.6H4.2z" |
| fill="none" |
| stroke="currentColor" |
| strokeWidth={1.35} |
| strokeLinejoin="round" |
| /> |
| <path |
| d="M5.9 7.2V5.5a2.1 2.1 0 0 1 4.2 0v1.7" |
| fill="none" |
| stroke="currentColor" |
| strokeWidth={1.35} |
| strokeLinecap="round" |
| /> |
| </svg> |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| export function FolderMark({ |
| icon, |
| size = 14, |
| }: { |
| icon?: FolderIcon; |
| size?: number; |
| }) { |
| const shape = icon?.shape ?? DEFAULT_FOLDER_SHAPE; |
| const paint = folderTonePaint(icon?.tone ?? DEFAULT_FOLDER_TONE); |
| return ( |
| <svg |
| className="cg-folder-mark" |
| width={size} |
| height={size} |
| viewBox="0 0 16 16" |
| aria-hidden |
| > |
| {/* R5: same guard β a folder's shape is stored in nav prefs. */} |
| {(FOLDER_SHAPE_PATHS[shape] ?? []).map((s, i) => ( |
| <path |
| key={i} |
| d={s.d} |
| // Outline only, same ruling as ToneModeIcon β a filled folder sitting directly |
| // above outlined view rows would make the rail read as two icon vocabularies. |
| fill="none" |
| stroke={paint.stroke} |
| strokeWidth={1.35} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| /> |
| ))} |
| </svg> |
| ); |
| } |
|
|