// --------------------------------------------------------------------------- // customer-grid / iconShapes.ts // Wave-8 items I18 + I20 — ONE geometry source for the grid's icon vocabulary, // rendered by TWO very different painters: // // - React / — DOM svg in panels, popovers, menus // - glide headerIcons sprites — canvas, drawn from an SVG *string* // // Glide's sprite API takes a function returning SVG SOURCE, so a header icon can // never be a React component. Keeping the paths as DATA (IconShape[]) and giving // each painter its own thin renderer is what stops the two from drifting — the // alternative (hand-copying every path into a template literal) guarantees the // header and the panel eventually disagree about what a "date" looks like. // // Geometry rules: 16x16 viewBox, stroke-based, 1.35 stroke, round caps/joins, // currentColor. Vector paths only — NEVER emoji (owner constant). // --------------------------------------------------------------------------- import type { AggName, DisplayMode, FieldType, FolderShape, FolderTone } from "./types"; import { LP_BLUE, LP_BLUE_DEEP, LP_GREEN, LP_GREEN_DEEP, LP_LINE, LP_MUTED, LP_RED, LP_RED_DEEP, LP_YELLOW, LP_YELLOW_DEEP, } from "./theme"; /** One drawing primitive. `fill: true` fills the path instead of stroking it * (the rating star is the only shape that reads better solid). */ export type IconShape = { d: string; fill?: boolean }; /** A circle as a path — two half-arcs. Sprites are SVG *source*, so every shape * has to survive being serialized into a string; paths do, elements * would need a second serializer branch for no benefit. */ const circle = (cx: number, cy: number, r: number): string => `M${cx - r} ${cy}a${r} ${r} 0 1 0 ${r * 2} 0a${r} ${r} 0 1 0 ${-r * 2} 0`; const CALENDAR: IconShape[] = [ { d: "M3.2 4.6h9.6v8.2H3.2z" }, { d: "M3.2 7.2h9.6" }, { d: "M5.8 3v3.2" }, { d: "M10.2 3v3.2" }, ]; /** * Field type → icon geometry. A TOTAL record on purpose: adding a FieldType * without an icon is a compile error, not a silently blank header. */ export const TYPE_SHAPES: Record = { text: [{ d: "M3 5h10M3 8h10M3 11h6" }], status: [{ d: "M4 13V3.5h7.6L10.1 6l1.5 2.5H4" }], currency: [ { d: "M8 2.8v10.4" }, { d: "M10.6 5.4A2.6 2.6 0 0 0 8.2 4.2H7.4a2 2 0 0 0 0 4h1.2a2 2 0 0 1 0 4H7.8a2.6 2.6 0 0 1-2.4-1.4" }, ], int: [{ d: "M6.2 3L4.8 13M11.2 3l-1.4 10M3.4 6.2h9.2M2.9 9.8h9.2" }], date: CALENDAR, pct: [ { d: circle(4.6, 4.6, 1.6) }, { d: circle(11.4, 11.4, 1.6) }, { d: "M12.2 3.9L3.8 12.3" }, ], select: [{ d: "M3.2 3.8h9.6v8.4H3.2z" }, { d: "M6.2 7.2l1.8 1.8 1.8-1.8" }], user: [ { d: circle(8, 6, 2.4) }, { d: "M3.6 13c0-2.4 2-3.8 4.4-3.8s4.4 1.4 4.4 3.8" }, ], multiselect: [ { d: "M3 4.6h2.2v2.2H3zM3 9.2h2.2v2.2H3z" }, { d: "M7.2 5.7h6M7.2 10.3h6" }, ], checkbox: [{ d: "M3.4 3.4h9.2v9.2H3.4z" }, { d: "M5.8 8.1l1.8 1.9 3.4-3.9" }], phone: [ { d: "M5.1 3.2L7 5.1 5.6 7a7.2 7.2 0 0 0 3.4 3.4l1.9-1.4 1.9 1.9-1.5 1.6c-3 .5-8.3-4.8-7.8-7.8z" }, ], email: [{ d: "M3 4.4h10v7.2H3z" }, { d: "M3 4.9l5 3.9 5-3.9" }], url: [ { d: "M7 5.4L8.4 4a2.6 2.6 0 0 1 3.7 3.7L10.7 9" }, { d: "M9 10.6L7.6 12a2.6 2.6 0 0 1-3.7-3.7L5.3 7" }, { d: "M6.2 9.8l3.6-3.6" }, ], rating: [ { d: "M8 2.9l1.63 3.3 3.64.53-2.63 2.57.62 3.63L8 11.24 4.74 12.93l.62-3.63L2.73 6.73l3.64-.53z", fill: true, }, ], created_time: [{ d: circle(8, 8, 5.2) }, { d: "M8 4.9v3.4l2.3 1.4" }], formula: [ { d: "M5.6 12.8V5.4a2 2 0 0 1 3.2-1.6" }, { d: "M4.2 7.6h4.6" }, { d: "M10.2 8.4l3 3.4M13.2 8.4l-3 3.4" }, ], // Wave-18 C5-AUTOFIELD (D's spec, applied by C as client-vocab registrar). A 290° cycle ring // with an arrowhead, wrapped around a solid run-triangle: a job that runs, repeatedly. // Deliberately NOT a bolt (`FOLDER_SHAPE_PATHS.bolt` already means "Priority") and not a clock // (`created_time` owns the closed rim + hands). automation: [ { d: "M10.8 4.1A4.8 4.8 0 1 1 5.3 4.1" }, { d: "M4.2 6L5.3 4.1 3.1 4.5" }, { d: "M6.9 6.1L9.8 8 6.9 9.9z", fill: true }, ], // Wave-22 C7 (added by C as client-vocab registrar, the W18 automation precedent). A rising // series on an axis: a measure OVER TIME, which is what a metric field is. Deliberately not // the formula fx (that computes over the ROW) and not a bare number (int owns ##). metric: [ { d: "M3.2 3.2v9.6h9.6" }, { d: "M5 10.4l2.4-2.6 1.9 1.5 3.1-3.9" }, ], // Wave-23 C7 — TWO BRACES facing each other with a dot between them: the universal mark for // "a structured document", and the one glyph in this table that draws its own SYNTAX rather // than a picture of what the value means. Deliberately not a document page (nothing here owns // that yet, but a page reads as a file/attachment, which a json cell is not) and not a tree // of nodes (too fine to survive 16px). The centre dot is what keeps the two braces from // reading as parentheses at small sizes. json: [ { d: "M6.4 3.2c-1.5 0-1.5 3.4-1.5 3.4S4.8 8 3.4 8s1.5 1.4 1.5 1.4 0 3.4 1.5 3.4" }, { d: "M9.6 3.2c1.5 0 1.5 3.4 1.5 3.4s.1 1.4 1.5 1.4-1.5 1.4-1.5 1.4 0 3.4-1.5 3.4" }, { d: circle(8, 8, 0.85), fill: true }, ], // ⭐ Wave-27 item 13 (R13) — THE ANGLE BRACKETS, the mark every editor on earth uses for // "this is source". Drawn as two chevrons with a slash leaning between them, which is what // separates it from `json` two entries up: json draws BRACES (a document's own syntax), code // draws BRACKETS (a snippet's). Deliberately not a terminal prompt (that reads as "run this", // and R13 is explicit there is no execution engine) and not a page of lines (`list` mode and // `text` already trade on that reading). code: [ { d: "M5.6 4.9 2.6 8l3 3.1" }, { d: "M10.4 4.9 13.4 8l-3 3.1" }, { d: "M9.1 3.6 6.9 12.4" }, ], // Wave-19 R7 — a framed picture: the mount, a sun, and the hill line every photo glyph // resolves to at 16px. Drawn on the same 16-unit grid as its neighbours. image: [ { d: "M2.6 3.4h10.8v9.2H2.6z" }, { d: circle(6, 6.3, 1.1) }, { d: "M2.6 10.6L6.1 7.6l2.5 2.1 2.2-1.8 2.6 2.2" }, ], // ⭐ 2026-08-07 — TWO INTERLOCKING CHAIN LINKS, the one glyph everybody already reads as // "this points at something else". Drawn as two rounded rectangles overlapping at the centre // rather than as an arrow into a box: an arrow would mean navigation, and a link column is a // relation that exists in both directions whether or not you follow it. link: [ { d: "M6.6 5.2H4.9a2.8 2.8 0 000 5.6h1.7" }, { d: "M9.4 5.2h1.7a2.8 2.8 0 010 5.6H9.4" }, { d: "M5.6 8h4.8" }, ], // ⭐ 2026-08-07 — THREE BARS FOLDING INTO ONE, read top-to-bottom: many linked values // collapsing to a single aggregate. Deliberately not a sigma (too fine at 16px, and it would // claim SUM when the function is chosen per column) and not a funnel (that is filtering, // which is what `limit` does — a different half of the same field). rollup: [ { d: "M3.2 4.4h9.6" }, { d: "M4.8 8h6.4" }, { d: "M6.6 11.6h2.8" }, ], }; /** * Display mode → icon geometry (I18). Also total: the wave-8 Dashboard mode * cannot land in DISPLAY_MODES without the compiler demanding its icon here. */ export const MODE_SHAPES: Record = { grid: [ { d: "M2.6 3.4h10.8v9.2H2.6z" }, { d: "M2.6 6.5h10.8M2.6 9.5h10.8M6.4 3.4v9.2M10 3.4v9.2" }, ], list: [{ d: "M3 4.6h1.4M6.4 4.6h6.6M3 8h1.4M6.4 8h6.6M3 11.4h1.4M6.4 11.4h6.6" }], // I19c — a framed set of bars: "several charts", not "one chart". chart: [ { d: "M2.6 3.4h10.8v9.2H2.6z" }, { d: "M5.4 10.6V7.2M8 10.6V5.4M10.6 10.6V8.6" }, ], // I10 (C2) — the LEGACY key. Kept so this record stays total over DisplayMode, which is // what makes "accept 'dashboard' on read forever" a compile-time guarantee rather than a // promise. Same drawing: a stored 'dashboard' IS a chart. dashboard: [ { d: "M2.6 3.4h10.8v9.2H2.6z" }, { d: "M5.4 10.6V7.2M8 10.6V5.4M10.6 10.6V8.6" }, ], calendar: CALENDAR, kanban: [{ d: "M2.8 3.4h3.1v9.2H2.8zM6.5 3.4h3.1v6.2H6.5zM10.2 3.4h3.1v7.6h-3.1z" }], map: [ { d: "M8 2.6a3.6 3.6 0 0 1 3.6 3.6c0 2.7-3.6 7.2-3.6 7.2S4.4 8.9 4.4 6.2A3.6 3.6 0 0 1 8 2.6z" }, { d: circle(8, 6.1, 1.3) }, ], // 2026-08-02 item 7 — the time-series view. Drawn as a framed grid with a trend running // through it, because that is literally what the panel is: metric ROWS x bucket COLUMNS with // a per-row line toggle. Deliberately not the plain `line` chart mark — a chart view and a // time-series table must not be the same picture in the same rail. timeseries: [ { d: "M2.6 3.4h10.8v9.2H2.6z" }, { d: "M2.6 6.6h10.8M6.2 3.4v9.2" }, { d: "M7.2 10.8l2-2.4 1.6 1.2 1.8-2.6" }, ], // Wave-18 C6-CATALOG — an OPEN BOOK: two facing leaves with a spine between them, and a // product block sitting on the left one. Every other mode in this table draws a way of // arranging RECORDS; this one has to read as a printed artifact, so it is the only mark here // with a spine and a gutter. Deliberately not a page-with-lines (`list` owns that reading) and // not a framed grid (`grid`/`chart`/`timeseries` share the frame). catalog: [ { d: "M2.4 4.2h4.6a1.6 1.6 0 0 1 1 .4l0 8a1.6 1.6 0 0 0-1-.4H2.4z" }, { d: "M13.6 4.2H9a1.6 1.6 0 0 0-1 .4l0 8a1.6 1.6 0 0 1 1-.4h4.6z" }, { d: "M3.9 6.4h2.3v2.8H3.9z" }, ], // Wave-23 C9 — a SHEET WITH A WRITING LINE: two filled answer bars and an empty rule beneath // them. Every other mode here draws a way of ARRANGING records that already exist; this one // has to read as a record being MADE, so it is the only mark whose bottom line is open. // Deliberately not a clipboard (nothing else here has a frame with a tab) and not a pencil // (an edit affordance means something else app-wide). // ⭐ Wave-27 C3 (owner item 8) — a CARD WITH TWO ARROWS LEAVING IT, left and right. Every // other mark here draws an arrangement of many records; this one has to read as ONE record // with two exits, because that is exactly what the deck is. Deliberately not a stack of cards // (that is a lane, and `kanban` owns it) and not a hand or a gesture glyph (nothing else in // this vocabulary draws a body part, and it would read as "drag" rather than "decide"). swipe: [ { d: "M5.4 3.4h5.2v9.2H5.4z" }, { d: "M3.6 8H1.4M2.8 6.8 1.4 8l1.4 1.2" }, { d: "M12.4 8h2.2M13.2 6.8 14.6 8l-1.4 1.2" }, ], form: [ { d: "M3.4 2.8h9.2v10.4H3.4z" }, { d: "M5.6 5.6h4.8" }, { d: "M5.6 8h4.8" }, { d: "M5.6 10.6h2.6" }, ], }; /** * Wave-9 I16 — one mark per CHART KIND. Total over `ChartKind`, so a kind added to C2's * vocabulary cannot ship without a drawing. * * ⚠ The owner asked for an icon on every chart-type option, and the wave-8 ruling stands: * an `