{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"records-table","title":"Records Table","description":"CRM-style grid with tags, sorting and relationship status.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json","https://ward.so/r/glide-menu.json"],"files":[{"path":"registry/ward/RecordsTable.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/records-table.css\";\nimport \"./styles/foundation.css\";\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport GlideMenu from \"./GlideMenu\";\n\n/* ─────────────────────────────────────────────────────────\n * RECORDS TABLE — an AI spreadsheet grid. Columns are\n * *properties*: click a header to open its configuration\n * popover (type, tool, grounding, inputs, prompt, run), add\n * a new AI property from the + header, and watch cells\n * resolve row-by-row while it calculates.\n * ───────────────────────────────────────────────────────── */\n\ntype Strength = \"strong\" | \"weak\" | \"veryweak\" | \"none\";\ntype SortKey = \"name\" | \"last\" | \"strength\";\ntype ColumnKey = \"company\" | \"categories\" | \"last\" | \"strength\" | \"links\" | \"ai\";\n\nconst DEFAULT_COLUMN_WIDTHS: Record<ColumnKey, number> = {\n  company: 270,\n  categories: 275,\n  last: 190,\n  strength: 210,\n  links: 175,\n  ai: 240,\n};\n\nconst STRENGTH: Record<Strength, { label: string; color: string; rank: number }> = {\n  strong: { label: \"Very strong\", color: \"var(--green)\", rank: 3 },\n  weak: { label: \"Weak\", color: \"var(--orange)\", rank: 2 },\n  veryweak: { label: \"Very weak\", color: \"var(--red)\", rank: 1 },\n  none: { label: \"No communication\", color: \"var(--ink-3)\", rank: 0 },\n};\n\n// A single mid-lightness base hue per tag. Background, text, and border are\n// derived from this via color-mix() against the theme tokens in .records-tag,\n// so the chips adapt to light and dark automatically (same pattern as FilterTable).\ntype TagColor = { base: string };\n\nconst TAG_PALETTE: Record<string, TagColor> = {\n  amber: { base: \"oklch(0.76 0.13 70)\" },\n  lime: { base: \"oklch(0.77 0.16 122)\" },\n  yellow: { base: \"oklch(0.80 0.15 101)\" },\n  purple: { base: \"oklch(0.62 0.18 293)\" },\n  orange: { base: \"oklch(0.71 0.16 48)\" },\n  cyan: { base: \"oklch(0.72 0.10 221)\" },\n  red: { base: \"oklch(0.64 0.19 27)\" },\n  magenta: { base: \"oklch(0.66 0.21 323)\" },\n  green: { base: \"oklch(0.70 0.13 162)\" },\n  pink: { base: \"oklch(0.67 0.19 3)\" },\n};\n\nconst TAG_COLORS: Record<string, TagColor> = {\n  B2B: TAG_PALETTE.amber,\n  B2C: TAG_PALETTE.lime,\n  Cafe: TAG_PALETTE.red,\n  Catering: TAG_PALETTE.magenta,\n  \"Dairy-free\": TAG_PALETTE.cyan,\n  Brand: TAG_PALETTE.purple,\n  Imports: TAG_PALETTE.orange,\n  Local: TAG_PALETTE.green,\n  Seasonal: TAG_PALETTE.yellow,\n  Sorbet: TAG_PALETTE.pink,\n  Vegan: TAG_PALETTE.lime,\n  Wholesale: TAG_PALETTE.amber,\n};\n\nexport type RecordRow = {\n  id: string;\n  name: string;\n  tags: string[];\n  last: string;\n  strength: Strength;\n  website?: string;\n};\n\nconst INITIAL_ROWS: RecordRow[] = [\n  { id: \"aurora\", name: \"Aurora Works — Reykjavík\", tags: [\"Brand\", \"Seasonal\"], last: \"9 days ago\", strength: \"strong\", website: \"aurora-works.example.com\" },\n  { id: \"kumo\", name: \"Kumo Studio — Tokyo\", tags: [\"B2C\", \"Cafe\", \"Vegan\"], last: \"3 weeks ago\", strength: \"strong\", website: \"kumo-studio.example.com\" },\n  { id: \"sol-nieve\", name: \"Sol y Nieve — Buenos Aires\", tags: [\"Brand\", \"Local\"], last: \"2 months ago\", strength: \"weak\", website: \"sol-y-nieve.example.com\" },\n  { id: \"maple-orbit\", name: \"Maple Orbit — Montréal\", tags: [\"B2B\", \"Wholesale\", \"Seasonal\"], last: \"15 days ago\", strength: \"weak\", website: \"maple-orbit.example.com\" },\n  { id: \"blue-fig\", name: \"Blue Fig Brand — Florence\", tags: [\"Brand\", \"Cafe\"], last: \"over 1 year ago\", strength: \"veryweak\", website: \"blue-fig.example.com\" },\n  { id: \"sahara-swirl\", name: \"Sahara Swirl — Marrakech\", tags: [\"Sorbet\", \"Local\"], last: \"5 months ago\", strength: \"veryweak\" },\n  { id: \"cloudberry\", name: \"Cloudberry Asset — Helsinki\", tags: [\"Dairy-free\", \"Seasonal\"], last: \"No contact\", strength: \"none\", website: \"cloudberry-asset.example.com\" },\n  { id: \"palm-sugar\", name: \"Palm Sugar Studio — Bangkok\", tags: [\"B2C\", \"Vegan\"], last: \"3 months ago\", strength: \"veryweak\", website: \"palm-sugar.example.com\" },\n  { id: \"cape-vanilla\", name: \"Cape Vanilla Co. — Cape Town\", tags: [\"Wholesale\", \"Imports\"], last: \"over 1 year ago\", strength: \"veryweak\", website: \"cape-vanilla.example.com\" },\n  { id: \"andes-snow\", name: \"Andes Snow Studio — Quito\", tags: [\"Brand\", \"Catering\"], last: \"almost 2 years ago\", strength: \"veryweak\" },\n  { id: \"tasman-sea\", name: \"Tasman Sea Brand — Hobart\", tags: [\"Brand\", \"Local\"], last: \"2 months ago\", strength: \"weak\", website: \"tasman-sea.example.com\" },\n  { id: \"silk-road\", name: \"Silk Road Sorbet — Tbilisi\", tags: [\"Sorbet\", \"Imports\"], last: \"about 1 month ago\", strength: \"weak\", website: \"silk-road.example.com\" },\n  { id: \"rosewater\", name: \"Rosewater Kulfi — Jaipur\", tags: [\"B2C\", \"Seasonal\"], last: \"2 months ago\", strength: \"veryweak\" },\n  { id: \"lumen\", name: \"Lumen Soft Serve — Copenhagen\", tags: [\"Dairy-free\", \"Cafe\"], last: \"8 months ago\", strength: \"weak\", website: \"lumen-soft-serve.example.com\" },\n  { id: \"cacao-norte\", name: \"Cacao Norte — Oaxaca\", tags: [\"B2B\", \"Local\", \"Wholesale\"], last: \"about 2 years ago\", strength: \"none\", website: \"cacao-norte.example.com\" },\n  { id: \"pine-pistachio\", name: \"Pine & Pistachio — Istanbul\", tags: [\"Brand\", \"Catering\"], last: \"about 1 month ago\", strength: \"veryweak\" },\n  { id: \"ember-asset\", name: \"Ember Asset Company — Seoul\", tags: [\"B2C\", \"Vegan\"], last: \"15 days ago\", strength: \"weak\", website: \"ember-asset.example.com\" },\n  { id: \"coral-coast\", name: \"Coral Coast Sorbet — Honolulu\", tags: [\"Sorbet\", \"Local\"], last: \"9 days ago\", strength: \"strong\", website: \"coral-coast.example.com\" },\n  { id: \"sunbird\", name: \"Sunbird Gelateria — Lisbon\", tags: [\"Brand\", \"Cafe\"], last: \"over 2 years ago\", strength: \"none\", website: \"sunbird.example.com\" },\n  { id: \"mooncake\", name: \"Mooncake Design — Singapore\", tags: [\"B2B\", \"Wholesale\"], last: \"about 1 month ago\", strength: \"veryweak\", website: \"mooncake-ice-cream.example.com\" },\n  { id: \"juniper\", name: \"Juniper & Cream — Vancouver\", tags: [\"Dairy-free\", \"Catering\"], last: \"No contact\", strength: \"none\" },\n  { id: \"mango-moon\", name: \"Mango Moon Brand — Nairobi\", tags: [\"Sorbet\", \"Vegan\"], last: \"almost 2 years ago\", strength: \"veryweak\", website: \"mango-moon.example.com\" },\n  { id: \"fjord-fizz\", name: \"Fjord Fizz Ice — Oslo\", tags: [\"Dairy-free\", \"Seasonal\"], last: \"No contact\", strength: \"none\" },\n  { id: \"pampa\", name: \"Pampa Studio — Córdoba\", tags: [\"B2C\", \"Local\"], last: \"12 months ago\", strength: \"veryweak\", website: \"pampa-studio.example.com\" },\n  { id: \"lotus-leaf\", name: \"Lotus Leaf Works — Hanoi\", tags: [\"Vegan\", \"Cafe\"], last: \"15 days ago\", strength: \"weak\" },\n  { id: \"saffron-sky\", name: \"Saffron Sky Kulfi — Dubai\", tags: [\"Imports\", \"Catering\"], last: \"almost 2 years ago\", strength: \"veryweak\", website: \"saffron-sky.example.com\" },\n  { id: \"alpine-plan\", name: \"Alpine Plan — Zürich\", tags: [\"B2B\", \"Brand\", \"Wholesale\"], last: \"4 days ago\", strength: \"strong\", website: \"alpine-plan.example.com\" },\n  { id: \"monsoon-mango\", name: \"Monsoon Mango — Mumbai\", tags: [\"Sorbet\", \"Vegan\", \"Catering\"], last: \"18 days ago\", strength: \"weak\", website: \"monsoon-mango.example.com\" },\n  { id: \"cedar-spoon\", name: \"Cedar Spoon — Beirut\", tags: [\"Cafe\", \"Local\", \"Seasonal\"], last: \"6 days ago\", strength: \"strong\", website: \"cedar-spoon.example.com\" },\n  { id: \"baltic-berry\", name: \"Baltic Berry — Tallinn\", tags: [\"Dairy-free\", \"Seasonal\", \"B2C\"], last: \"5 weeks ago\", strength: \"weak\", website: \"baltic-berry.example.com\" },\n  { id: \"delta-dairy\", name: \"Delta Dairy Works — New Orleans\", tags: [\"B2B\", \"Wholesale\", \"Local\"], last: \"2 days ago\", strength: \"strong\", website: \"delta-dairy.example.com\" },\n  { id: \"yuzu-yard\", name: \"Yuzu Yard — Kyoto\", tags: [\"Sorbet\", \"Cafe\", \"Seasonal\"], last: \"11 days ago\", strength: \"strong\", website: \"yuzu-yard.example.com\" },\n  { id: \"copper-asset\", name: \"Copper Asset — Melbourne\", tags: [\"Brand\", \"Cafe\", \"B2C\"], last: \"about 1 month ago\", strength: \"weak\", website: \"copper-asset.example.com\" },\n  { id: \"mint-medina\", name: \"Mint Medina — Tunis\", tags: [\"Dairy-free\", \"Vegan\", \"Local\"], last: \"No contact\", strength: \"none\" },\n  { id: \"glacier-grove\", name: \"Glacier Grove — Anchorage\", tags: [\"Seasonal\", \"Local\", \"Catering\"], last: \"7 weeks ago\", strength: \"weak\", website: \"glacier-grove.example.com\" },\n  { id: \"orchard-cloud\", name: \"Orchard Cloud — Lyon\", tags: [\"Brand\", \"Seasonal\", \"Cafe\"], last: \"5 days ago\", strength: \"strong\", website: \"orchard-cloud.example.com\" },\n  { id: \"tamarind-tide\", name: \"Tamarind Tide — Chennai\", tags: [\"Vegan\", \"Sorbet\", \"B2C\"], last: \"9 months ago\", strength: \"veryweak\", website: \"tamarind-tide.example.com\" },\n  { id: \"amber-work\", name: \"Amber Work — Prague\", tags: [\"Brand\", \"B2B\"], last: \"over 1 year ago\", strength: \"none\" },\n  { id: \"boreal-batch\", name: \"Boreal Batch — Yellowknife\", tags: [\"Dairy-free\", \"Local\", \"Seasonal\"], last: \"8 days ago\", strength: \"strong\", website: \"boreal-batch.example.com\" },\n  { id: \"coconut-commons\", name: \"Coconut Commons — Manila\", tags: [\"Vegan\", \"B2C\", \"Cafe\"], last: \"24 days ago\", strength: \"weak\", website: \"coconut-commons.example.com\" },\n  { id: \"dolomite-dairy\", name: \"Dolomite Dairy — Bolzano\", tags: [\"Brand\", \"Wholesale\"], last: \"3 days ago\", strength: \"strong\", website: \"dolomite-dairy.example.com\" },\n  { id: \"equator-cream\", name: \"Equator Cream — Kampala\", tags: [\"B2B\", \"Catering\", \"Local\"], last: \"10 months ago\", strength: \"veryweak\", website: \"equator-cream.example.com\" },\n  { id: \"hibiscus-house\", name: \"Hibiscus House — Accra\", tags: [\"Sorbet\", \"Cafe\"], last: \"6 weeks ago\", strength: \"weak\", website: \"hibiscus-house.example.com\" },\n  { id: \"lagoon-ladle\", name: \"Lagoon Ladle — Venice\", tags: [\"Brand\", \"Seasonal\", \"Catering\"], last: \"7 days ago\", strength: \"strong\", website: \"lagoon-ladle.example.com\" },\n  { id: \"midnight-milk\", name: \"Midnight Milk — Tromsø\", tags: [\"Dairy-free\", \"Vegan\", \"Wholesale\"], last: \"No contact\", strength: \"none\" },\n  { id: \"nomad-nougat\", name: \"Nomad Nougat — Ulaanbaatar\", tags: [\"Imports\", \"B2B\"], last: \"almost 2 years ago\", strength: \"none\", website: \"nomad-nougat.example.com\" },\n  { id: \"olive-snow\", name: \"Olive Snow — Athens\", tags: [\"Brand\", \"Cafe\", \"Local\"], last: \"4 days ago\", strength: \"strong\", website: \"olive-snow.example.com\" },\n  { id: \"pacific-pear\", name: \"Pacific Pear — Valparaíso\", tags: [\"Sorbet\", \"Seasonal\"], last: \"2 months ago\", strength: \"weak\", website: \"pacific-pear.example.com\" },\n  { id: \"quartz-asset\", name: \"Quartz Asset — Denver\", tags: [\"B2C\", \"Wholesale\"], last: \"10 days ago\", strength: \"strong\", website: \"quartz-asset.example.com\" },\n  { id: \"red-lantern\", name: \"Red Lantern Studio — Taipei\", tags: [\"Cafe\", \"Vegan\"], last: \"about 1 month ago\", strength: \"weak\", website: \"red-lantern.example.com\" },\n  { id: \"salt-silk\", name: \"Salt & Silk — Muscat\", tags: [\"Imports\", \"Catering\", \"Brand\"], last: \"8 months ago\", strength: \"veryweak\", website: \"salt-and-silk.example.com\" },\n  { id: \"tropic-plan\", name: \"Tropic Plan — San Juan\", tags: [\"Sorbet\", \"Local\", \"B2C\"], last: \"6 days ago\", strength: \"strong\", website: \"tropic-plan.example.com\" },\n  { id: \"umber-cream\", name: \"Umber Cream — Warsaw\", tags: [\"B2B\", \"Wholesale\", \"Cafe\"], last: \"5 weeks ago\", strength: \"weak\", website: \"umber-cream.example.com\" },\n  { id: \"vanilla-vale\", name: \"Vanilla Vale — Antananarivo\", tags: [\"Imports\", \"Local\"], last: \"No contact\", strength: \"none\" },\n  { id: \"willow-whip\", name: \"Willow Whip — Portland\", tags: [\"Dairy-free\", \"Vegan\", \"Cafe\"], last: \"3 days ago\", strength: \"strong\", website: \"willow-whip.example.com\" },\n  { id: \"zenith-brand\", name: \"Zenith Brand — Auckland\", tags: [\"Brand\", \"Seasonal\"], last: \"3 weeks ago\", strength: \"weak\", website: \"zenith-brand.example.com\" },\n  { id: \"apricot-atlas\", name: \"Apricot Atlas — Algiers\", tags: [\"Sorbet\", \"Imports\"], last: \"11 months ago\", strength: \"veryweak\", website: \"apricot-atlas.example.com\" },\n  { id: \"black-sesame\", name: \"Black Sesame Social — Bandung\", tags: [\"Vegan\", \"Cafe\", \"B2C\"], last: \"9 days ago\", strength: \"strong\", website: \"black-sesame.example.com\" },\n  { id: \"crimson-clover\", name: \"Crimson Clover — Brussels\", tags: [\"Brand\", \"Wholesale\", \"Catering\"], last: \"2 months ago\", strength: \"weak\", website: \"crimson-clover.example.com\" },\n  { id: \"dragonfruit-dock\", name: \"Dragonfruit Dock — Shenzhen\", tags: [\"Sorbet\", \"B2B\", \"Wholesale\"], last: \"No contact\", strength: \"none\" },\n];\n\n/* the AI column resolves to fictional competitor pairs */\nconst AI_LABEL = \"Competitors\";\nconst COMPETITOR_POOL = [\n  \"Frost & Ladle\",\n  \"Polar Pint Co.\",\n  \"Meltwater Studio\",\n  \"Cirrus Works\",\n  \"Golden Plan\",\n  \"Velvet Freeze\",\n  \"North Asset Collective\",\n  \"Sundae Syndicate\",\n];\nconst competitorsFor = (index: number) => `${COMPETITOR_POOL[index % 8]}, ${COMPETITOR_POOL[(index + 3) % 8]}`;\n\nfunction Icon({ children, size = 14, strokeWidth = 1.8 }: { children: React.ReactNode; size?: number; strokeWidth?: number }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n      {children}\n    </svg>\n  );\n}\n\n/* glyph library for property types & tools */\nconst TYPE_GLYPHS: Record<string, React.ReactNode> = {\n  Text: <path d=\"M4 6h16M4 12h10M4 18h7\" />,\n  File: <g><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /></g>,\n  Collection: <g><ellipse cx=\"12\" cy=\"5\" rx=\"8\" ry=\"3\" /><path d=\"M4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3\" /></g>,\n  \"Single select\": <g><circle cx=\"12\" cy=\"12\" r=\"9\" /><path d=\"m8.5 12 2.4 2.4 4.6-4.9\" /></g>,\n  \"Multi select\": <g><path d=\"M11 6h9M11 12h9M11 18h9\" /><path d=\"M4 6l1.5 1.5L8 5M4 12l1.5 1.5L8 11M4 18l1.5 1.5L8 17\" /></g>,\n  URL: <g><path d=\"M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1\" /><path d=\"M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1\" /></g>,\n  Reference: <path d=\"M7 17 17 7M9 7h8v8\" />,\n  JSON: <g><path d=\"M8 4c-2 0-2 2-2 3s.5 3-2 3c2.5 0 2 2 2 3s0 3 2 3\" /><path d=\"M16 4c2 0 2 2 2 3s-.5 3 2 3c-2.5 0-2 2-2 3s0 3-2 3\" /></g>,\n  \"File splitter\": <g><rect x=\"8\" y=\"8\" width=\"12\" height=\"12\" rx=\"2\" /><path d=\"M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2\" /></g>,\n  Date: <g><rect x=\"3\" y=\"5\" width=\"18\" height=\"16\" rx=\"2.5\" /><path d=\"M8 3v4M16 3v4M3 10h18\" /></g>,\n};\n\nconst TOOL_GLYPHS: Record<string, React.ReactNode> = {\n  model: <path d=\"M12 3l1.7 5.1a2 2 0 0 0 1.2 1.2L20 11l-5.1 1.7a2 2 0 0 0-1.2 1.2L12 19l-1.7-5.1a2 2 0 0 0-1.2-1.2L4 11l5.1-1.7a2 2 0 0 0 1.2-1.2z\" />,\n  web: <g><circle cx=\"12\" cy=\"12\" r=\"9\" /><path d=\"M3 12h18M12 3a13.5 13.5 0 0 1 3.5 9 13.5 13.5 0 0 1-3.5 9 13.5 13.5 0 0 1-3.5-9A13.5 13.5 0 0 1 12 3z\" /></g>,\n  user: <g><circle cx=\"12\" cy=\"8\" r=\"4\" /><path d=\"M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1\" /></g>,\n};\n\n/* per-property configuration shown in the popover */\ntype Prompt = { before: string; chip?: string; after?: string };\ntype ToolKind = \"model\" | \"web\" | \"user\";\ntype ColumnMeta = { type: string; tool: string; toolKind: ToolKind; inputs?: string; prompt?: Prompt };\n\nconst COLUMN_META: Record<string, ColumnMeta> = {\n  Company: { type: \"Text\", tool: \"User input\", toolKind: \"user\" },\n  Categories: { type: \"Multi select\", tool: \"Sprinkles 5\", toolKind: \"model\", inputs: \"Company\", prompt: { before: \"Tag each \", chip: \"Company\", after: \" with its market categories.\" } },\n  \"Last interaction\": { type: \"Date\", tool: \"User input\", toolKind: \"user\" },\n  \"Connection strength\": { type: \"Single select\", tool: \"Sprinkles 5\", toolKind: \"model\", inputs: \"Last interaction\", prompt: { before: \"Score the relationship from \", chip: \"Last interaction\", after: \".\" } },\n  Links: { type: \"URL\", tool: \"Web search\", toolKind: \"web\", inputs: \"Company\", prompt: { before: \"Find the website for \", chip: \"Company\", after: \".\" } },\n  [AI_LABEL]: { type: \"Text\", tool: \"Web search\", toolKind: \"web\", inputs: \"Company\", prompt: { before: \"Find competitors for \", chip: \"Company\" } },\n};\n\nconst NEW_PROPERTY_TYPES = [\"Text\", \"File\", \"Collection\", \"Single select\", \"Multi select\", \"URL\", \"Reference\", \"JSON\", \"File splitter\"];\nconst MODEL_OPTIONS = [\"Sprinkles 5\", \"Sprinkles 4.2\", \"Sprinkles Mini\"];\nconst INPUT_OPTIONS = [\"Company\", \"Categories\", \"Last interaction\", \"Connection strength\", \"Links\"];\n\nfunction Checkbox({ checked, mixed = false, onChange, label }: { checked: boolean; mixed?: boolean; onChange: () => void; label: string }) {\n  return (\n    <label className=\"records-checkbox\" title={label} onClick={(event) => event.stopPropagation()}>\n      <input type=\"checkbox\" checked={checked} onChange={onChange} aria-label={label} />\n      <span className={`records-checkbox-box ${checked || mixed ? \"is-active\" : \"\"}`}>\n        {mixed ? <span className=\"records-checkbox-dash\" /> : checked ? <Icon size={12}><path d=\"m5 12 4 4L19 6\" /></Icon> : null}\n      </span>\n    </label>\n  );\n}\n\nfunction Tag({ name }: { name: string }) {\n  const color = TAG_COLORS[name] ?? { base: \"var(--ink-3)\" };\n  return (\n    <span\n      className=\"records-tag\"\n      style={{ \"--tag-base\": color.base } as React.CSSProperties}\n    >\n      {name}\n    </span>\n  );\n}\n\nfunction TagList({ tags }: { tags: string[] }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const measureRef = useRef<HTMLDivElement>(null);\n  const [visibleCount, setVisibleCount] = useState(tags.length);\n\n  useLayoutEffect(() => {\n    const container = containerRef.current;\n    const measure = measureRef.current;\n    if (!container || !measure) return;\n\n    const update = () => {\n      const available = container.clientWidth;\n      const tagWidths = Array.from(measure.querySelectorAll<HTMLElement>(\"[data-tag-measure]\"), (tag) => tag.offsetWidth);\n      const moreWidth = measure.querySelector<HTMLElement>(\"[data-more-measure]\")?.offsetWidth ?? 0;\n      let used = 0;\n      let count = 0;\n\n      for (let index = 0; index < tagWidths.length; index += 1) {\n        const nextUsed = used + (count > 0 ? 4 : 0) + tagWidths[index];\n        const hiddenAfter = tags.length - (index + 1);\n        const totalWithOverflow = nextUsed + (hiddenAfter > 0 ? 4 + moreWidth : 0);\n        if (totalWithOverflow > available) break;\n        used = nextUsed;\n        count += 1;\n      }\n\n      setVisibleCount(count);\n    };\n\n    update();\n    const observer = new ResizeObserver(update);\n    observer.observe(container);\n    return () => observer.disconnect();\n  }, [tags]);\n\n  const hiddenCount = tags.length - visibleCount;\n\n  return (\n    <div ref={containerRef} className=\"records-tags\" title={tags.join(\", \")} aria-label={`Categories: ${tags.join(\", \")}`}>\n      <div ref={measureRef} className=\"records-tags-measure\" aria-hidden>\n        {tags.map((tag) => <span key={tag} data-tag-measure><Tag name={tag} /></span>)}\n        <span data-more-measure className=\"records-more-tag\">+{tags.length}</span>\n      </div>\n      {tags.slice(0, visibleCount).map((tag) => <Tag key={tag} name={tag} />)}\n      {hiddenCount > 0 && <span className=\"records-more-tag\">+{hiddenCount}</span>}\n    </div>\n  );\n}\n\nfunction CalcCell() {\n  return (\n    <span className=\"records-calc\">\n      <span className=\"records-muted\">Calculating…</span>\n      <span className=\"records-pulse\" />\n    </span>\n  );\n}\n\nfunction MiniSwitch({ on, onToggle, label }: { on: boolean; onToggle: () => void; label: string }) {\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={on}\n      aria-label={label}\n      onClick={onToggle}\n      className=\"relative h-4.5 w-7.5 shrink-0 rounded-full transition-colors duration-150\"\n      style={{ background: on ? \"var(--highlight)\" : \"var(--line-strong)\" }}\n    >\n      <span\n        className=\"absolute top-0.5 left-0.5 size-3.5 rounded-full bg-white shadow-btn transition-transform duration-150\"\n        style={{ transform: on ? \"translateX(12px)\" : \"translateX(0)\", transitionTimingFunction: \"cubic-bezier(0.23,1,0.32,1)\" }}\n      />\n    </button>\n  );\n}\n\nfunction HeaderCell({ label, icon, sortKey, sort, onSort, onResizeStart, resizing = false, className = \"\", selected = false, onPick }: { label: string; icon: React.ReactNode; sortKey?: SortKey; sort: { key: SortKey; dir: 1 | -1 }; onSort: (key: SortKey) => void; onResizeStart: (event: React.PointerEvent<HTMLSpanElement>) => void; resizing?: boolean; className?: string; selected?: boolean; onPick?: (event: React.MouseEvent) => void }) {\n  return (\n    <th className={`records-header-cell ${selected ? \"is-colsel\" : \"\"} ${className}`}>\n      {/* header click opens the property config; the arrow sorts */}\n      <button type=\"button\" className=\"records-header-button\" onClick={onPick}>\n        <span className=\"records-header-icon\">{icon}</span>\n        <span className=\"truncate\">{label}</span>\n        {sortKey && (\n          <span\n            role=\"button\"\n            tabIndex={0}\n            aria-label={`Sort by ${label}`}\n            onClick={(event) => {\n              event.stopPropagation();\n              onSort(sortKey);\n            }}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\" || event.key === \" \") {\n                event.preventDefault();\n                event.stopPropagation();\n                onSort(sortKey);\n              }\n            }}\n            className={`records-sort ${sort.key === sortKey ? \"is-visible\" : \"\"}`}\n            style={{ transform: sort.key === sortKey && sort.dir === -1 ? \"rotate(180deg)\" : undefined }}\n          >\n            <Icon size={12}><path d=\"M12 5v14M5 12l7 7 7-7\" /></Icon>\n          </span>\n        )}\n      </button>\n      <span\n        role=\"separator\"\n        aria-orientation=\"vertical\"\n        aria-label={`Resize ${label} column`}\n        className={`records-resize-handle ${resizing ? \"is-resizing\" : \"\"}`}\n        onPointerDown={onResizeStart}\n      />\n    </th>\n  );\n}\n\n/* config row inside the property popover */\nfunction ConfigRow({ label, children }: { label: string; children: React.ReactNode }) {\n  return (\n    <div className=\"relative flex h-8 items-center justify-between\">\n      <span className=\"text-[13px] text-ink-3\">{label}</span>\n      {children}\n    </div>\n  );\n}\n\nfunction ConfigPicker({\n  label,\n  options,\n  selected,\n  onSelect,\n}: {\n  label: string;\n  options: { label: string; icon: React.ReactNode }[];\n  selected: string;\n  onSelect: (value: string) => void;\n}) {\n  return (\n    <div\n      role=\"menu\"\n      aria-label={label}\n      className=\"absolute left-full top-0 z-30 ml-5 w-[210px] rounded-[12px] bg-card p-1.5 shadow-overlay\"\n      style={{ animation: \"pop-in 140ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"top left\" }}\n    >\n      <div className=\"px-2 pb-1 pt-0.5 text-[11.5px] font-medium text-ink-3\">{label}</div>\n      <GlideMenu className=\"flex flex-col gap-px\">\n        {options.map((option) => (\n          <button\n            key={option.label}\n            data-menu-row\n            type=\"button\"\n            role=\"menuitemradio\"\n            aria-checked={selected === option.label}\n            onClick={() => onSelect(option.label)}\n            className=\"relative z-10 flex h-8 w-full items-center gap-1.5 rounded-[8px] px-1.5 text-left text-[13px] font-medium text-ink\"\n          >\n            <span className=\"flex size-4 shrink-0 items-center justify-center text-ink-2\">{option.icon}</span>\n            <span className=\"min-w-0 flex-1 truncate\">{option.label}</span>\n            <span className={selected === option.label ? \"text-ink\" : \"invisible\"}>\n              <Icon size={14} strokeWidth={2.2}><path d=\"m5 12 4 4L19 6\" /></Icon>\n            </span>\n          </button>\n        ))}\n      </GlideMenu>\n    </div>\n  );\n}\n\nfunction InputPicker({\n  options,\n  selected,\n  onToggle,\n}: {\n  options: string[];\n  selected: string[];\n  onToggle: (value: string) => void;\n}) {\n  return (\n    <div\n      role=\"menu\"\n      aria-label=\"Calculation inputs\"\n      className=\"absolute left-full top-0 z-30 ml-5 w-[220px] rounded-[12px] bg-card p-1.5 shadow-overlay\"\n      style={{ animation: \"pop-in 140ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"top left\" }}\n    >\n      <div className=\"px-2 pb-1 pt-0.5 text-[11.5px] font-medium text-ink-3\">Use values from</div>\n      <GlideMenu className=\"flex flex-col gap-px\">\n        {options.map((option) => {\n          const checked = selected.includes(option);\n          return (\n            <button\n              key={option}\n              data-menu-row\n              type=\"button\"\n              role=\"menuitemcheckbox\"\n              aria-checked={checked}\n              onClick={() => onToggle(option)}\n              className=\"relative z-10 flex h-8 w-full items-center gap-1.5 rounded-[8px] px-1.5 text-left text-[13px] font-medium text-ink\"\n            >\n              <span className={`flex size-4 shrink-0 items-center justify-center rounded-[5px] border ${checked ? \"border-highlight bg-highlight text-primary-foreground\" : \"border-line-strong text-transparent\"}`}>\n                <Icon size={11} strokeWidth={2.4}><path d=\"m5 12 4 4L19 6\" /></Icon>\n              </span>\n              <span className=\"min-w-0 flex-1 truncate\">{option}</span>\n            </button>\n          );\n        })}\n      </GlideMenu>\n    </div>\n  );\n}\n\nexport default function RecordsTable({ rows = INITIAL_ROWS, fill = false }: { rows?: RecordRow[]; fill?: boolean; variant?: string }) {\n  const [selected, setSelected] = useState<Set<string>>(new Set());\n  const [sort, setSort] = useState<{ key: SortKey; dir: 1 | -1 }>({ key: \"name\", dir: 1 });\n  const [columnWidths, setColumnWidths] = useState(DEFAULT_COLUMN_WIDTHS);\n  const [actionColumnWidth, setActionColumnWidth] = useState(100);\n  const [columnWidthsLocked, setColumnWidthsLocked] = useState(false);\n  const [resizingColumn, setResizingColumn] = useState<ColumnKey | null>(null);\n  const initialColumnWidthsRef = useRef<Record<ColumnKey, number> | null>(null);\n  const tableRef = useRef<HTMLTableElement>(null);\n\n  /* property popover, anchored to the clicked header */\n  const [prop, setProp] = useState<{ col: string; x: number; y: number } | null>(null);\n  const [grounding, setGrounding] = useState(false);\n  const [groundingHelpOpen, setGroundingHelpOpen] = useState(false);\n  const [configMenu, setConfigMenu] = useState<\"type\" | \"tool\" | \"inputs\" | null>(null);\n  const [columnOverrides, setColumnOverrides] = useState<Record<string, Partial<ColumnMeta>>>({});\n  const [inputSelections, setInputSelections] = useState<Record<string, string[]>>({});\n  const [pinnedColumns, setPinnedColumns] = useState<Set<string>>(new Set());\n  const [moreSettingsOpen, setMoreSettingsOpen] = useState(false);\n  const [advancedSettings, setAdvancedSettings] = useState({ required: false, allowEmpty: true, confidence: false });\n  /* + new-property menu */\n  const [addOpen, setAddOpen] = useState<{ x: number; y: number } | null>(null);\n  const [tableMenuOpen, setTableMenuOpen] = useState<{ x: number; y: number } | null>(null);\n  /* the added AI column and its lifecycle */\n  const [aiAdded, setAiAdded] = useState(false);\n  const [aiDone, setAiDone] = useState(false);\n  const [pendingOpenAi, setPendingOpenAi] = useState(false);\n  const aiThRef = useRef<HTMLTableCellElement>(null);\n  /* programmatic scrolls (revealing the new column) shouldn't close popovers */\n  const ignoreScrollRef = useRef(false);\n  /* a running calculation resolves rows one by one */\n  const [calc, setCalc] = useState<{ col: string; resolved: number } | null>(null);\n\n  /* Let the table fill its available space once, then capture those rendered\n   * widths before paint. From that point on every column is explicit, so a\n   * resize changes only the dragged column and the table's total width. */\n  useLayoutEffect(() => {\n    if (columnWidthsLocked || !tableRef.current) return;\n    const headers = Array.from(tableRef.current.querySelectorAll<HTMLTableCellElement>(\"thead th\"));\n    if (headers.length < 6) return;\n\n    const measured: Record<ColumnKey, number> = {\n      company: headers[0].getBoundingClientRect().width,\n      categories: headers[1].getBoundingClientRect().width,\n      last: headers[2].getBoundingClientRect().width,\n      strength: headers[3].getBoundingClientRect().width,\n      links: headers[4].getBoundingClientRect().width,\n      ai: DEFAULT_COLUMN_WIDTHS.ai,\n    };\n    initialColumnWidthsRef.current = measured;\n    setColumnWidths(measured);\n    setActionColumnWidth(headers[headers.length - 1].getBoundingClientRect().width);\n    setColumnWidthsLocked(true);\n  }, [columnWidthsLocked]);\n\n  const visibleRows = useMemo(() => {\n    return [...rows].sort((a, b) => {\n      const value = sort.key === \"name\"\n        ? a.name.localeCompare(b.name)\n        : sort.key === \"last\"\n          ? a.last.localeCompare(b.last)\n          : STRENGTH[a.strength].rank - STRENGTH[b.strength].rank;\n      return value * sort.dir;\n    });\n  }, [rows, sort]);\n\n  /* stagger: one row resolves every beat */\n  useEffect(() => {\n    if (!calc) return;\n    if (calc.resolved > visibleRows.length) {\n      const timer = setTimeout(() => {\n        if (calc.col === AI_LABEL) setAiDone(true);\n        setCalc(null);\n      }, 0);\n      return () => clearTimeout(timer);\n    }\n    const t = setTimeout(() => setCalc((current) => (current ? { ...current, resolved: current.resolved + 1 } : current)), 110);\n    return () => clearTimeout(t);\n  }, [calc, visibleRows.length]);\n\n  /* after adding the AI column, scroll it into view and open its config\n   * anchored to the new header */\n  useEffect(() => {\n    if (!pendingOpenAi || !aiThRef.current) return;\n    const scroller = aiThRef.current.closest(\".records-scroll\");\n    if (scroller) {\n      ignoreScrollRef.current = true;\n      scroller.scrollLeft = scroller.scrollWidth;\n    }\n    const rect = aiThRef.current.getBoundingClientRect();\n    setProp({ col: AI_LABEL, x: Math.min(rect.left, window.innerWidth - 336), y: rect.bottom + 6 });\n    setPendingOpenAi(false);\n  }, [pendingOpenAi, aiAdded]);\n\n  /* click anywhere else closes popovers */\n  useEffect(() => {\n    if (!prop && !addOpen && !tableMenuOpen) return;\n    const close = (event: PointerEvent) => {\n      if (!(event.target as Element).closest(\"[data-recpop]\")) {\n        setProp(null);\n        setConfigMenu(null);\n        setGroundingHelpOpen(false);\n        setMoreSettingsOpen(false);\n        setAddOpen(null);\n        setTableMenuOpen(null);\n      }\n    };\n    document.addEventListener(\"pointerdown\", close);\n    return () => document.removeEventListener(\"pointerdown\", close);\n  }, [prop, addOpen, tableMenuOpen]);\n\n  const openProp = (col: string) => (event: React.MouseEvent) => {\n    const th = (event.currentTarget as Element).closest(\"th\");\n    if (!th) return;\n    setAddOpen(null);\n    setTableMenuOpen(null);\n    setConfigMenu(null);\n    setGroundingHelpOpen(false);\n    setMoreSettingsOpen(false);\n    setProp((current) => {\n      if (current?.col === col) return null;\n      const rect = th.getBoundingClientRect();\n      return { col, x: Math.min(rect.left, window.innerWidth - 336), y: rect.bottom + 6 };\n    });\n  };\n\n  const isCalc = (col: string, index: number) => !!calc && calc.col === col && index >= calc.resolved;\n\n  const allSelected = visibleRows.length > 0 && visibleRows.every((row) => selected.has(row.id));\n  const partiallySelected = !allSelected && visibleRows.some((row) => selected.has(row.id));\n\n  const toggleSort = (key: SortKey) => setSort((current) => current.key === key ? { key, dir: (current.dir * -1) as 1 | -1 } : { key, dir: 1 });\n  const startColumnResize = (key: ColumnKey, minWidth = 120) => (event: React.PointerEvent<HTMLSpanElement>) => {\n    event.preventDefault();\n    event.stopPropagation();\n    setProp(null);\n    setConfigMenu(null);\n    setGroundingHelpOpen(false);\n    setMoreSettingsOpen(false);\n    setAddOpen(null);\n    setTableMenuOpen(null);\n\n    const startX = event.clientX;\n    const startWidth = columnWidths[key];\n    const previousCursor = document.body.style.cursor;\n    const previousSelection = document.body.style.userSelect;\n    document.body.style.cursor = \"col-resize\";\n    document.body.style.userSelect = \"none\";\n    setResizingColumn(key);\n\n    const move = (moveEvent: PointerEvent) => {\n      const width = Math.max(minWidth, startWidth + moveEvent.clientX - startX);\n      setColumnWidths((current) => ({ ...current, [key]: width }));\n    };\n    const finish = () => {\n      window.removeEventListener(\"pointermove\", move);\n      window.removeEventListener(\"pointerup\", finish);\n      window.removeEventListener(\"pointercancel\", finish);\n      document.body.style.cursor = previousCursor;\n      document.body.style.userSelect = previousSelection;\n      setResizingColumn(null);\n    };\n\n    window.addEventListener(\"pointermove\", move);\n    window.addEventListener(\"pointerup\", finish);\n    window.addEventListener(\"pointercancel\", finish);\n  };\n  const toggleRow = (id: string) => setSelected((current) => {\n    const next = new Set(current);\n    if (next.has(id)) next.delete(id);\n    else next.add(id);\n    return next;\n  });\n  const toggleAll = () => setSelected((current) => {\n    const next = new Set(current);\n    if (allSelected) visibleRows.forEach((row) => next.delete(row.id));\n    else visibleRows.forEach((row) => next.add(row.id));\n    return next;\n  });\n\n  const meta = prop ? { ...COLUMN_META[prop.col], ...columnOverrides[prop.col] } : null;\n  const selectedInputs = prop && meta\n    ? inputSelections[prop.col] ?? (meta.inputs ? [meta.inputs] : [])\n    : [];\n  const tableWidth = columnWidths.company + columnWidths.categories + columnWidths.last + columnWidths.strength + columnWidths.links + (aiAdded ? columnWidths.ai : 0) + actionColumnWidth;\n\n  return (\n    <div className={`records-shell${fill ? \" is-fill\" : \"\"}`}>\n      <div\n        className=\"records-scroll\"\n        tabIndex={0}\n        aria-label=\"Companies table. Scroll horizontally and vertically to view all columns and records.\"\n        onScroll={() => {\n          if (ignoreScrollRef.current) {\n            ignoreScrollRef.current = false;\n            return;\n          }\n          setProp(null);\n          setConfigMenu(null);\n          setGroundingHelpOpen(false);\n          setMoreSettingsOpen(false);\n          setAddOpen(null);\n          setTableMenuOpen(null);\n        }}\n      >\n        <table ref={tableRef} className=\"records-table\" style={{ width: columnWidthsLocked ? tableWidth : \"100%\", minWidth: tableWidth }}>\n          <colgroup>\n            <col className=\"records-company-col\" style={{ width: columnWidths.company }} />\n            <col className=\"records-category-col\" style={{ width: columnWidths.categories }} />\n            <col className=\"records-last-col\" style={{ width: columnWidths.last }} />\n            <col className=\"records-strength-col\" style={{ width: columnWidths.strength }} />\n            <col className=\"records-link-col\" style={{ width: columnWidths.links }} />\n            {aiAdded && <col style={{ width: columnWidths.ai }} />}\n            <col style={{ width: 100 }} />\n          </colgroup>\n          <thead>\n            <tr>\n              <th className={`records-header-cell records-sticky-cell ${prop?.col === \"Company\" ? \"is-colsel\" : \"\"}`}>\n                <div className=\"records-company-header\" style={{ cursor: \"pointer\" }} onClick={(event) => openProp(\"Company\")(event)}>\n                  <Checkbox checked={allSelected} mixed={partiallySelected} onChange={toggleAll} label=\"Select all companies\" />\n                  <span>Company</span>\n                </div>\n                <span role=\"separator\" aria-orientation=\"vertical\" aria-label=\"Resize Company column\" className={`records-resize-handle ${resizingColumn === \"company\" ? \"is-resizing\" : \"\"}`} onPointerDown={startColumnResize(\"company\", 180)} />\n              </th>\n              <HeaderCell label=\"Categories\" selected={prop?.col === \"Categories\"} onPick={openProp(\"Categories\")} sort={sort} onSort={toggleSort} onResizeStart={startColumnResize(\"categories\")} resizing={resizingColumn === \"categories\"} icon={<Icon size={15}>{TYPE_GLYPHS[\"Multi select\"]}</Icon>} />\n              <HeaderCell label=\"Last interaction\" selected={prop?.col === \"Last interaction\"} onPick={openProp(\"Last interaction\")} sortKey=\"last\" sort={sort} onSort={toggleSort} onResizeStart={startColumnResize(\"last\")} resizing={resizingColumn === \"last\"} icon={<Icon size={15}>{TYPE_GLYPHS.Date}</Icon>} />\n              <HeaderCell label=\"Connection strength\" selected={prop?.col === \"Connection strength\"} onPick={openProp(\"Connection strength\")} sortKey=\"strength\" sort={sort} onSort={toggleSort} onResizeStart={startColumnResize(\"strength\")} resizing={resizingColumn === \"strength\"} icon={<Icon size={15}>{TYPE_GLYPHS[\"Single select\"]}</Icon>} />\n              <HeaderCell label=\"Links\" selected={prop?.col === \"Links\"} onPick={openProp(\"Links\")} sort={sort} onSort={toggleSort} onResizeStart={startColumnResize(\"links\")} resizing={resizingColumn === \"links\"} icon={<Icon size={15}>{TYPE_GLYPHS.URL}</Icon>} />\n              {aiAdded && (\n                <th ref={aiThRef} className={`records-header-cell ${prop?.col === AI_LABEL ? \"is-colsel\" : \"\"}`}>\n                  <button type=\"button\" className=\"records-header-button\" onClick={openProp(AI_LABEL)}>\n                    <span className=\"records-header-icon\"><Icon size={15}>{TYPE_GLYPHS.Text}</Icon></span>\n                    <span className=\"truncate\">{AI_LABEL}</span>\n                  </button>\n                  <span role=\"separator\" aria-orientation=\"vertical\" aria-label={`Resize ${AI_LABEL} column`} className={`records-resize-handle ${resizingColumn === \"ai\" ? \"is-resizing\" : \"\"}`} onPointerDown={startColumnResize(\"ai\")} />\n                </th>\n              )}\n              <th className=\"records-header-cell\">\n                <div className=\"flex h-[35px] items-center gap-1 px-2\">\n                  <button\n                    type=\"button\"\n                    aria-label=\"New property\"\n                    data-recpop\n                    onClick={(event) => {\n                      setProp(null);\n                      setTableMenuOpen(null);\n                      const rect = (event.currentTarget as Element).getBoundingClientRect();\n                      setAddOpen((current) => (current ? null : { x: Math.min(rect.left, window.innerWidth - 276), y: rect.bottom + 6 }));\n                    }}\n                    className=\"flex size-7 items-center justify-center rounded-[7px] text-ink-2 transition-colors duration-100 hover:bg-hover hover:text-ink\"\n                  >\n                    <Icon size={15} strokeWidth={2}><path d=\"M12 5v14M5 12h14\" /></Icon>\n                  </button>\n                  <button\n                    type=\"button\"\n                    aria-label=\"Table options\"\n                    aria-expanded={!!tableMenuOpen}\n                    data-recpop\n                    onClick={(event) => {\n                      setProp(null);\n                      setAddOpen(null);\n                      const rect = event.currentTarget.getBoundingClientRect();\n                      setTableMenuOpen((current) => current ? null : {\n                        x: Math.max(8, Math.min(rect.right - 220, window.innerWidth - 228)),\n                        y: rect.bottom + 6,\n                      });\n                    }}\n                    className=\"flex size-7 items-center justify-center rounded-[7px] text-ink-3 transition-colors duration-100 hover:bg-hover hover:text-ink\"\n                  >\n                    <svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden><circle cx=\"5\" cy=\"12\" r=\"1.6\" /><circle cx=\"12\" cy=\"12\" r=\"1.6\" /><circle cx=\"19\" cy=\"12\" r=\"1.6\" /></svg>\n                  </button>\n                </div>\n              </th>\n            </tr>\n          </thead>\n          {/* data cells stay silent — the papery link/flick sound is too much when scanning rows */}\n          <tbody data-sound-silent>\n            {visibleRows.map((row, index) => {\n              const selectedRow = selected.has(row.id);\n              const strength = STRENGTH[row.strength];\n              return <tr key={row.id} className={`records-row ${selectedRow ? \"is-selected\" : \"\"}`}>\n                <td className={`records-cell records-sticky-cell records-company-cell ${prop?.col === \"Company\" ? \"is-colsel\" : \"\"}`}><span className=\"records-rownum\">{index + 1}</span><Checkbox checked={selectedRow} onChange={() => toggleRow(row.id)} label={`Select ${row.name}`} /><span className=\"records-company-mark\">{row.name.slice(0, 1).toUpperCase()}</span><a href={row.website ? `https://${row.website}` : \"#\"} onClick={(event) => !row.website && event.preventDefault()} title={row.name} className={`records-company-name ${row.website ? \"has-link\" : \"\"}`}>{row.name}</a></td>\n                <td className={`records-cell ${prop?.col === \"Categories\" ? \"is-colsel\" : \"\"}`}>{isCalc(\"Categories\", index) ? <CalcCell /> : <TagList tags={row.tags} />}</td>\n                <td className={`records-cell ${row.last === \"No contact\" ? \"records-muted\" : \"\"} ${prop?.col === \"Last interaction\" ? \"is-colsel\" : \"\"}`}>{isCalc(\"Last interaction\", index) ? <CalcCell /> : row.last}</td>\n                <td className={`records-cell ${prop?.col === \"Connection strength\" ? \"is-colsel\" : \"\"}`}>{isCalc(\"Connection strength\", index) ? <CalcCell /> : <span className=\"records-strength\"><span className=\"records-strength-dot\" style={{ background: strength.color }} />{strength.label}</span>}</td>\n                <td className={`records-cell ${prop?.col === \"Links\" ? \"is-colsel\" : \"\"}`}>{isCalc(\"Links\", index) ? <CalcCell /> : row.website ? <a className=\"records-link\" href={`https://${row.website}`} title={row.website} target=\"_blank\" rel=\"noreferrer\"><span className=\"records-link-label\">{row.website}</span><Icon size={12}><path d=\"M14 5h5v5M19 5l-8 8\" /></Icon></a> : <span className=\"records-muted\">—</span>}</td>\n                {aiAdded && (\n                  <td className={`records-cell ${prop?.col === AI_LABEL ? \"is-colsel\" : \"\"}`}>\n                    {calc?.col === AI_LABEL ? (index < calc.resolved ? competitorsFor(index) : <CalcCell />) : aiDone ? competitorsFor(index) : <span className=\"records-muted\">—</span>}\n                  </td>\n                )}\n                <td className=\"records-cell\" />\n              </tr>;\n            })}\n          </tbody>\n          <tfoot>\n            <tr className=\"records-calculation-row\">\n              <td className=\"records-cell records-sticky-cell\">\n                <span className=\"records-footer-value records-calculation-label\"><span className=\"records-calculation-number\">{rows.length}</span> count</span>\n              </td>\n              <td className=\"records-cell\">\n                <button type=\"button\" className=\"records-add-calculation\"><Icon size={15}><path d=\"M12 5v14M5 12h14\" /></Icon>Add calculation</button>\n              </td>\n              <td className=\"records-cell records-muted\"><span className=\"records-footer-value\">—</span></td>\n              <td className=\"records-cell\">\n                <span className=\"records-footer-value records-average\"><span className=\"records-strength-dot\" style={{ background: \"var(--orange)\" }} />{Math.round(rows.reduce((sum, row) => sum + STRENGTH[row.strength].rank, 0) / rows.length / 3 * 100)}% average</span>\n              </td>\n              <td className=\"records-cell\"><span className=\"records-footer-value records-muted\">{rows.filter((row) => row.website).length} links</span></td>\n              {aiAdded && <td className=\"records-cell records-muted\"><span className=\"records-footer-value\">{aiDone ? `${rows.length} filled` : \"—\"}</span></td>}\n              <td className=\"records-cell\" />\n            </tr>\n          </tfoot>\n        </table>\n      </div>\n\n      {/* ── property configuration popover ─────────────────── */}\n      {prop && meta && (\n        <div\n          data-recpop\n          className=\"fixed z-50 w-[320px] rounded-[14px] bg-card px-3 pt-3 pb-1.5 shadow-overlay\"\n          style={{ top: prop.y, left: prop.x, animation: \"pop-in 160ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"top left\" }}\n        >\n          <div className=\"pb-2 text-[13.5px] font-medium text-ink\">{prop.col}</div>\n\n          <ConfigRow label=\"Type\">\n            <button\n              type=\"button\"\n              aria-haspopup=\"menu\"\n              aria-expanded={configMenu === \"type\"}\n              onClick={() => setConfigMenu((current) => current === \"type\" ? null : \"type\")}\n              className=\"flex items-center gap-1.5 rounded-[6px] px-1.5 py-1 text-[13px] font-medium text-ink transition-colors duration-100 hover:bg-hover\"\n            >\n              <span className=\"text-ink-2\"><Icon size={14}>{TYPE_GLYPHS[meta.type] ?? TYPE_GLYPHS.Text}</Icon></span>\n              {meta.type}\n              <span className=\"text-ink-3\"><Icon size={12} strokeWidth={2.2}><path d=\"M9 6l6 6-6 6\" /></Icon></span>\n            </button>\n            {configMenu === \"type\" && (\n              <ConfigPicker\n                label=\"Property type\"\n                selected={meta.type}\n                options={NEW_PROPERTY_TYPES.map((type) => ({ label: type, icon: <Icon size={15}>{TYPE_GLYPHS[type]}</Icon> }))}\n                onSelect={(type) => {\n                  setColumnOverrides((current) => ({ ...current, [prop.col]: { ...current[prop.col], type } }));\n                  setConfigMenu(null);\n                }}\n              />\n            )}\n          </ConfigRow>\n          <ConfigRow label=\"Tool\">\n            <button\n              type=\"button\"\n              aria-haspopup=\"menu\"\n              aria-expanded={configMenu === \"tool\"}\n              onClick={() => setConfigMenu((current) => current === \"tool\" ? null : \"tool\")}\n              className=\"flex items-center gap-1.5 rounded-[6px] px-1.5 py-1 text-[13px] font-medium text-ink transition-colors duration-100 hover:bg-hover\"\n            >\n              <span className={meta.toolKind === \"model\" ? \"text-highlight\" : \"text-ink-2\"}>\n                {meta.toolKind === \"model\"\n                  ? <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>{TOOL_GLYPHS.model}</svg>\n                  : <Icon size={14}>{TOOL_GLYPHS[meta.toolKind]}</Icon>}\n              </span>\n              {meta.tool}\n              <span className=\"text-ink-3\"><Icon size={12} strokeWidth={2.2}><path d=\"M9 6l6 6-6 6\" /></Icon></span>\n            </button>\n            {configMenu === \"tool\" && (\n              <ConfigPicker\n                label=\"Model\"\n                selected={meta.tool}\n                options={MODEL_OPTIONS.map((model) => ({\n                  label: model,\n                  icon: <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>{TOOL_GLYPHS.model}</svg>,\n                }))}\n                onSelect={(tool) => {\n                  setColumnOverrides((current) => ({ ...current, [prop.col]: { ...current[prop.col], tool, toolKind: \"model\" } }));\n                  setConfigMenu(null);\n                }}\n              />\n            )}\n          </ConfigRow>\n          <ConfigRow label=\"Grounding\">\n            <span className=\"flex items-center gap-2\">\n              <MiniSwitch label=\"Grounding\" on={grounding} onToggle={() => setGrounding((current) => !current)} />\n              <button\n                type=\"button\"\n                aria-label=\"About grounding\"\n                aria-expanded={groundingHelpOpen}\n                onClick={() => setGroundingHelpOpen((open) => !open)}\n                className=\"flex size-6 items-center justify-center rounded-[6px] text-ink-3 transition-colors duration-100 hover:bg-hover hover:text-ink\"\n              >\n                <Icon size={13}><g><circle cx=\"12\" cy=\"12\" r=\"9\" /><path d=\"M12 8h.01M11 12h1v4h1\" /></g></Icon>\n              </button>\n            </span>\n            {groundingHelpOpen && (\n              <div className=\"absolute right-0 top-[30px] z-30 w-[230px] rounded-[10px] px-3 py-2.5 text-[12px] leading-relaxed shadow-overlay\" style={{ color: \"var(--tooltip-fg)\", background: \"var(--tooltip-bg)\" }} role=\"status\">\n                Grounding lets the model verify generated values against connected sources.\n              </div>\n            )}\n          </ConfigRow>\n          <ConfigRow label=\"Inputs\">\n            <button\n              type=\"button\"\n              aria-haspopup=\"menu\"\n              aria-expanded={configMenu === \"inputs\"}\n              onClick={() => setConfigMenu((current) => current === \"inputs\" ? null : \"inputs\")}\n              className=\"flex max-w-[220px] items-center gap-1.5 rounded-[6px] px-1.5 py-1 text-[13px] text-ink-2 transition-colors duration-100 hover:bg-hover hover:text-ink\"\n            >\n              {selectedInputs.length ? (\n                <span className=\"flex min-w-0 items-center gap-1\">\n                  {selectedInputs.slice(0, 2).map((input) => (\n                    <span key={input} className=\"max-w-[92px] truncate rounded-[5px] bg-highlight-tint px-1.5 py-0.5 text-[12px] font-medium text-highlight-ink\">{input}</span>\n                  ))}\n                  {selectedInputs.length > 2 && <span className=\"text-[11px] font-medium text-ink-3\">+{selectedInputs.length - 2}</span>}\n                </span>\n              ) : (\n                <span>Select inputs</span>\n              )}\n              <span className=\"shrink-0 text-ink-3\"><Icon size={12} strokeWidth={2.2}><path d=\"M9 6l6 6-6 6\" /></Icon></span>\n            </button>\n            {configMenu === \"inputs\" && (\n              <InputPicker\n                selected={selectedInputs}\n                options={INPUT_OPTIONS.filter((input) => input !== prop.col)}\n                onToggle={(input) => {\n                  setInputSelections((current) => {\n                    const existing = current[prop.col] ?? (meta.inputs ? [meta.inputs] : []);\n                    const next = existing.includes(input) ? existing.filter((item) => item !== input) : [...existing, input];\n                    return { ...current, [prop.col]: next };\n                  });\n                }}\n              />\n            )}\n          </ConfigRow>\n\n          {/* prompt — @-mention chips inline */}\n          <div\n            contentEditable\n            suppressContentEditableWarning\n            role=\"textbox\"\n            aria-label={`${prop.col} calculation prompt`}\n            aria-multiline=\"true\"\n            spellCheck\n            className=\"mt-2 min-h-[88px] cursor-text rounded-[10px] bg-inset p-3 text-[13px] leading-relaxed shadow-hairline outline-none transition-[box-shadow] duration-150 focus:shadow-[0_0_0_2px_var(--highlight)]\"\n          >\n            {meta.prompt ? (\n              <span className=\"text-ink\">\n                {meta.prompt.before}\n                {meta.prompt.chip && <span contentEditable={false} className=\"rounded-[5px] bg-highlight-tint px-1.5 py-0.5 text-[12px] font-medium text-highlight-ink\">{meta.prompt.chip}</span>}\n                {meta.prompt.after}\n              </span>\n            ) : (\n              <span className=\"text-ink-3\">Set a prompt (press @ to mention an input)</span>\n            )}\n          </div>\n\n          <button\n            type=\"button\"\n            disabled={!!calc}\n            onClick={() => {\n              setCalc({ col: prop.col, resolved: 0 });\n              setProp(null);\n            }}\n            className=\"mt-2.5 flex h-9 w-full items-center justify-center gap-2 rounded-[9px] text-[12.5px] font-medium text-ink shadow-btn transition-[background-color,transform] duration-150 hover:bg-hover active:scale-[0.98] disabled:opacity-60\"\n          >\n            <Icon size={14} strokeWidth={1.9}><path d=\"M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6\" /></Icon>\n            Go calculate\n          </button>\n\n          <GlideMenu className=\"mt-3 flex flex-col gap-0.5 border-t border-line pt-2\" highlightClassName=\"-inset-x-1.5 rounded-[8px] bg-hover\">\n            <button\n              data-menu-row\n              type=\"button\"\n              aria-pressed={pinnedColumns.has(prop.col)}\n              onClick={() => setPinnedColumns((current) => {\n                const next = new Set(current);\n                if (next.has(prop.col)) next.delete(prop.col);\n                else next.add(prop.col);\n                return next;\n              })}\n              className=\"relative z-10 -mx-1.5 flex h-8 items-center gap-2.5 rounded-[8px] px-1.5 text-left text-[13px] leading-none text-ink transition-transform duration-150 active:scale-[0.96]\"\n            >\n              <span className={pinnedColumns.has(prop.col) ? \"text-highlight\" : \"text-ink-2\"}><Icon size={15}><path d=\"M12 17v5M8 3h8l-1 7 3 3H6l3-3-1-7z\" /></Icon></span>\n              {pinnedColumns.has(prop.col) ? \"Unpin\" : \"Pin\"}\n            </button>\n            <button\n              data-menu-row\n              type=\"button\"\n              aria-expanded={moreSettingsOpen}\n              onClick={() => setMoreSettingsOpen((open) => !open)}\n              className=\"relative z-10 -mx-1.5 flex h-8 items-center gap-2.5 rounded-[8px] px-1.5 text-left text-[13px] leading-none text-ink transition-transform duration-150 active:scale-[0.96]\"\n            >\n              <span className={moreSettingsOpen ? \"text-ink\" : \"text-ink-2\"}><Icon size={15}><g><circle cx=\"12\" cy=\"12\" r=\"3\" /><path d=\"M12 2v3M12 19v3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M2 12h3M19 12h3M4.9 19.1 7 17M17 7l2.1-2.1\" /></g></Icon></span>\n              <span className=\"flex-1\">More settings</span>\n              <span className={`text-ink-3 transition-transform duration-150 ${moreSettingsOpen ? \"rotate-90\" : \"\"}`}><Icon size={12} strokeWidth={2.2}><path d=\"M9 6l6 6-6 6\" /></Icon></span>\n            </button>\n            {prop.col === AI_LABEL && (\n              <button\n                data-menu-row\n                type=\"button\"\n                onClick={() => {\n                  setAiAdded(false);\n                  setAiDone(false);\n                  setProp(null);\n                }}\n                className=\"relative z-10 -mx-1.5 flex h-8 items-center gap-2.5 rounded-[8px] px-1.5 text-left text-[13px] leading-none text-ink transition-transform duration-150 active:scale-[0.96]\"\n              >\n                <span className=\"text-ink-2\"><Icon size={15}><g><path d=\"M10.6 5.1A9.8 9.8 0 0 1 12 5c7 0 10 7 10 7a16.3 16.3 0 0 1-2.1 3M6.6 6.6A16 16 0 0 0 2 12s3 7 10 7a9.7 9.7 0 0 0 5.4-1.6M3 3l18 18\" /><path d=\"M9.9 9.9a3 3 0 0 0 4.2 4.2\" /></g></Icon></span>\n                Hide from view\n              </button>\n            )}\n          </GlideMenu>\n\n          {moreSettingsOpen && (\n            <div className=\"mt-2 border-t border-line pt-2\" style={{ animation: \"fade-up 160ms cubic-bezier(0.23,1,0.32,1) both\" }}>\n              <div className=\"pb-1 text-[11.5px] font-medium text-ink-3\">Behavior</div>\n              <ConfigRow label=\"Required value\">\n                <MiniSwitch label=\"Required value\" on={advancedSettings.required} onToggle={() => setAdvancedSettings((current) => ({ ...current, required: !current.required }))} />\n              </ConfigRow>\n              <ConfigRow label=\"Allow empty results\">\n                <MiniSwitch label=\"Allow empty results\" on={advancedSettings.allowEmpty} onToggle={() => setAdvancedSettings((current) => ({ ...current, allowEmpty: !current.allowEmpty }))} />\n              </ConfigRow>\n              <ConfigRow label=\"Show confidence\">\n                <MiniSwitch label=\"Show confidence\" on={advancedSettings.confidence} onToggle={() => setAdvancedSettings((current) => ({ ...current, confidence: !current.confidence }))} />\n              </ConfigRow>\n            </div>\n          )}\n        </div>\n      )}\n\n      {/* ── new property type menu ─────────────────────────── */}\n      {addOpen && (\n        <div\n          data-recpop\n          className=\"fixed z-50 w-[260px] rounded-[14px] bg-card p-1.5 shadow-overlay\"\n          style={{ top: addOpen.y, left: addOpen.x, animation: \"pop-in 160ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"top left\" }}\n        >\n          <div className=\"px-2 pb-1 pt-1 text-[12px] font-medium text-ink-3\">New property</div>\n          <GlideMenu className=\"flex flex-col gap-px\">\n            {NEW_PROPERTY_TYPES.map((type) => (\n              <button\n                key={type}\n                data-menu-row\n                type=\"button\"\n                onClick={() => {\n                  setAddOpen(null);\n                  setAiDone(false);\n                  setAiAdded(true);\n                  setPendingOpenAi(true);\n                }}\n                className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[8px] px-2 text-left text-[13px] text-ink\"\n              >\n                <span className=\"text-ink-2\"><Icon size={15}>{TYPE_GLYPHS[type]}</Icon></span>\n                {type}\n              </button>\n            ))}\n          </GlideMenu>\n        </div>\n      )}\n\n      {/* ── table options menu ─────────────────────────────── */}\n      {tableMenuOpen && (\n        <div\n          data-recpop\n          className=\"fixed z-50 w-[220px] rounded-[14px] bg-card p-1.5 shadow-overlay\"\n          style={{ top: tableMenuOpen.y, left: tableMenuOpen.x, animation: \"pop-in 160ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"top right\" }}\n        >\n          <div className=\"px-2 pb-1 pt-1 text-[12px] font-medium text-ink-3\">Table options</div>\n          <GlideMenu className=\"flex flex-col gap-px\">\n          <button\n            data-menu-row\n            type=\"button\"\n            onClick={() => {\n              const position = tableMenuOpen;\n              setTableMenuOpen(null);\n              setAddOpen({ x: Math.min(position.x, window.innerWidth - 276), y: position.y });\n            }}\n            className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[8px] px-2 text-left text-[13px] text-ink\"\n          >\n            <span className=\"text-ink-2\"><Icon size={15} strokeWidth={2}><path d=\"M12 5v14M5 12h14\" /></Icon></span>\n            Add property\n          </button>\n          <button\n            data-menu-row\n            type=\"button\"\n            onClick={() => {\n              setColumnWidths({ company: 220, categories: 220, last: 155, strength: 180, links: 160, ai: 200 });\n              setTableMenuOpen(null);\n            }}\n            className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[8px] px-2 text-left text-[13px] text-ink\"\n          >\n            <span className=\"text-ink-2\"><Icon size={15}><path d=\"M4 8h16M7 4 3 8l4 4M17 4l4 4-4 4M4 16h16\" /></Icon></span>\n            Compact columns\n          </button>\n          <button\n            data-menu-row\n            type=\"button\"\n            onClick={() => {\n              setColumnWidths({ ...(initialColumnWidthsRef.current ?? DEFAULT_COLUMN_WIDTHS) });\n              setTableMenuOpen(null);\n            }}\n            className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[8px] px-2 text-left text-[13px] text-ink\"\n          >\n            <span className=\"text-ink-2\"><Icon size={15}><path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v6h6\" /></Icon></span>\n            Reset column widths\n          </button>\n          <div className=\"my-1 h-px bg-line\" />\n          <button\n            data-menu-row\n            type=\"button\"\n            onClick={() => {\n              setSelected(new Set());\n              setTableMenuOpen(null);\n            }}\n            className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[8px] px-2 text-left text-[13px] text-ink\"\n          >\n            <span className=\"text-ink-2\"><Icon size={15}><path d=\"M5 5l14 14M19 5 5 19\" /></Icon></span>\n            Clear selection\n          </button>\n          </GlideMenu>\n        </div>\n      )}\n    </div>\n  );\n}\n","type":"registry:component","target":"components/ward/RecordsTable.tsx"},{"path":"registry/ward/styles/records-table.css","content":"/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n/* ══════════════════════════════════════════════════════════\n * COMPONENT-SPECIFIC — RecordsTable\n * The full CRM grid, spreadsheet gutter, and AI column (~800 lines).\n * Copy this block only alongside RecordsTable; it is not part of the\n * generic foundation above.\n * ══════════════════════════════════════════════════════════ */\n.records-shell {\n  width: 100%;\n  min-width: 0;\n  overflow: hidden;\n  border: 1px solid var(--line);\n  border-radius: 10px;\n  background: var(--card);\n  box-shadow: 0 1px 2px oklch(0 0 0 / 0.06);\n}\n\n/* fill mode: the shell stretches to fill its container and drops its own\n * border/radius/shadow so it reads as part of the surrounding card rather\n * than a container-in-a-container. The grid body flexes to fill height. */\n.records-shell.is-fill {\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  min-height: 0;\n  height: 100%;\n  border: 0;\n  border-radius: 0;\n  box-shadow: none;\n}\n\n.records-shell.is-fill .records-scroll {\n  max-height: none;\n  flex: 1;\n}\n\n.records-toolbar {\n  display: flex;\n  min-height: 52px;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 8px 10px;\n  border-bottom: 1px solid var(--line-strong);\n  background: var(--card);\n}\n\n.records-toolbar-left,\n.records-toolbar-right,\n.records-company-header,\n.records-quiet-button,\n.records-secondary-button,\n.records-primary-button,\n.records-select-button,\n.records-strength,\n.records-link,\n.records-add-calculation {\n  display: inline-flex;\n  align-items: center;\n}\n\n.records-toolbar-left,\n.records-toolbar-right {\n  gap: 4px;\n}\n\n.records-toolbar-right {\n  flex-shrink: 0;\n}\n\n.records-select-button,\n.records-quiet-button,\n.records-secondary-button,\n.records-primary-button {\n  height: 32px;\n  gap: 7px;\n  border-radius: var(--radius-control);\n  padding: 0 10px;\n  font-size: 12px;\n  font-weight: 500;\n  white-space: nowrap;\n  transition: background-color 140ms var(--ease-out-strong), color 140ms var(--ease-out-strong), transform 140ms var(--ease-out-strong), box-shadow 140ms var(--ease-out-strong);\n}\n\n.records-select-button {\n  gap: 8px;\n  color: var(--ink);\n  border: 1px solid var(--line);\n  background: var(--card);\n}\n\n.records-select-button:hover,\n.records-quiet-button:hover,\n.records-secondary-button:hover {\n  background: var(--hover);\n  color: var(--ink);\n}\n\n.records-quiet-button {\n  color: var(--ink-2);\n}\n\n.records-quiet-button.is-active {\n  background: var(--highlight-tint);\n  color: var(--highlight-ink);\n}\n\n.records-secondary-button {\n  border: 1px solid var(--tooltip-border);\n  color: var(--ink-2);\n  box-shadow: none;\n}\n\n.records-primary-button {\n  margin-left: 3px;\n  background: var(--highlight);\n  color: oklch(1 0 0);\n  box-shadow: none;\n}\n\n.records-primary-button:hover {\n  filter: saturate(0.9) brightness(1.04);\n}\n\n.records-quiet-button:active,\n.records-secondary-button:active,\n.records-primary-button:active,\n.records-select-button:active,\n.records-add-calculation:active {\n  transform: scale(0.96);\n}\n\n.records-database-icon {\n  display: inline-flex;\n  color: var(--green);\n}\n\n.records-filter-wrap {\n  position: relative;\n}\n\n.records-filter-dot {\n  width: 5px;\n  height: 5px;\n  margin-left: -2px;\n  border-radius: 99px;\n  background: var(--highlight);\n}\n\n.records-filter-menu {\n  position: absolute;\n  z-index: 30;\n  top: calc(100% + 7px);\n  left: 0;\n  width: 190px;\n  padding: 6px;\n  border: 1px solid var(--line-strong);\n  border-radius: var(--radius-card);\n  background: var(--card);\n  box-shadow: var(--shadow-overlay);\n  animation: pop-in 160ms var(--ease-out-strong) both;\n  transform-origin: top left;\n}\n\n.records-sort-menu {\n  left: -48px;\n}\n\n.records-filter-label {\n  padding: 5px 8px 6px;\n  color: var(--ink-3);\n  font-size: 10.5px;\n  font-weight: 500;\n  letter-spacing: 0.04em;\n  text-transform: uppercase;\n}\n\n.records-filter-menu button {\n  display: flex;\n  width: 100%;\n  align-items: center;\n  gap: 7px;\n  border-radius: var(--radius-chip);\n  padding: 7px 8px;\n  color: var(--ink-2);\n  font-size: 12px;\n  text-align: left;\n  transition: background-color 120ms ease-out, color 120ms ease-out;\n}\n\n.records-filter-menu button:hover,\n.records-filter-menu button.is-selected {\n  background: var(--hover);\n  color: var(--ink);\n}\n\n.records-menu-check {\n  width: 13px;\n  color: var(--highlight-ink);\n  font-size: 12px;\n  font-weight: 500;\n  text-align: center;\n}\n\n.records-scroll {\n  max-height: 438px;\n  overflow: auto;\n  overscroll-behavior: none;\n  scrollbar-color: var(--line-strong) transparent;\n  scrollbar-gutter: stable;\n}\n\n.records-scroll:focus-visible {\n  outline: 2px solid var(--highlight);\n  outline-offset: -2px;\n}\n\n.records-table {\n  width: 100%;\n  min-width: 990px;\n  border-collapse: separate;\n  border-spacing: 0;\n  color: var(--ink);\n  font-size: 13px;\n  font-weight: 500;\n  table-layout: fixed;\n}\n\n.records-company-col { width: 270px; }\n.records-category-col { width: 275px; }\n.records-last-col { width: 190px; }\n.records-strength-col { width: 210px; }\n.records-link-col { width: 175px; }\n\n.records-table th,\n.records-table td {\n  border-right: 1px solid color-mix(in srgb, var(--line) 78%, transparent);\n  border-bottom: 1px solid color-mix(in srgb, var(--line) 78%, transparent);\n  text-align: left;\n  vertical-align: middle;\n}\n\n.records-table tr > :last-child {\n  border-right: 0;\n}\n\n.records-table thead th {\n  position: sticky;\n  z-index: 5;\n  top: 0;\n  height: 35px;\n  background: var(--card);\n  color: var(--ink-2);\n  font-size: 12.5px;\n  font-weight: 500;\n}\n\n.records-header-cell {\n  padding: 0;\n}\n\n.records-resize-handle {\n  position: absolute;\n  z-index: 12;\n  top: 0;\n  right: -6px;\n  width: 12px;\n  height: 100%;\n  cursor: col-resize;\n  touch-action: none;\n}\n\n.records-resize-handle::after {\n  position: absolute;\n  top: 7px;\n  bottom: 7px;\n  left: 5px;\n  width: 2px;\n  border-radius: 99px;\n  background: var(--highlight);\n  content: \"\";\n  opacity: 0;\n  transform: scaleY(0.65);\n  transition: opacity 120ms ease-out, transform 150ms var(--ease-out-strong);\n}\n\n.records-resize-handle:hover::after,\n.records-resize-handle.is-resizing::after {\n  opacity: 1;\n  transform: scaleY(1);\n}\n\n.records-header-cell.records-sticky-cell {\n  z-index: 7;\n}\n\n.records-header-button,\n.records-company-header {\n  width: 100%;\n  height: 35px;\n  gap: 8px;\n  padding: 0 12px;\n}\n\n.records-company-header {\n  padding-left: 6px;\n}\n\n.records-header-button {\n  display: flex;\n  align-items: center;\n  color: var(--ink-2);\n  text-align: left;\n  transition: background-color 120ms ease-out, color 120ms ease-out;\n}\n\n.records-header-button:hover {\n  background: var(--hover);\n  color: var(--ink);\n}\n\n.records-header-icon {\n  display: inline-flex;\n  flex-shrink: 0;\n  color: var(--ink-3);\n}\n\n.records-sort {\n  display: inline-flex;\n  flex-shrink: 0;\n  margin-left: auto;\n  opacity: 0;\n  transition: opacity 120ms ease-out, transform 160ms var(--ease-out-strong);\n}\n\n.records-header-button:hover .records-sort,\n.records-sort.is-visible {\n  opacity: 1;\n}\n\n.records-company-header {\n  display: flex;\n}\n\n.records-company-header > span:not(.records-checkbox-box) {\n  white-space: nowrap;\n}\n\n.records-add-field {\n  display: inline-flex;\n  margin-left: auto;\n  color: var(--ink-3);\n  opacity: 0;\n  transition: opacity 120ms ease-out, color 120ms ease-out;\n}\n\n.records-company-header:hover .records-add-field,\n.records-add-field:focus-visible {\n  opacity: 1;\n}\n\n.records-add-field:hover {\n  color: var(--ink);\n}\n\n.records-checkbox {\n  position: relative;\n  display: inline-flex;\n  width: 24px;\n  height: 24px;\n  flex: 0 0 24px;\n  align-items: center;\n  justify-content: center;\n  border-radius: var(--radius-chip);\n}\n\n.records-checkbox input {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  opacity: 0;\n}\n\n.records-checkbox-box {\n  display: inline-flex;\n  width: 18px;\n  height: 18px;\n  align-items: center;\n  justify-content: center;\n  border: 1px solid oklch(0.845 0.011 247.953);\n  border-radius: 6px;\n  color: oklch(0.446 0.018 251.343);\n  background: oklch(1 0 0);\n  transition: border-color 140ms ease-out, background-color 140ms ease-out, box-shadow 140ms ease-out, transform 140ms var(--ease-out-strong);\n}\n\n.records-checkbox:hover .records-checkbox-box {\n  border-color: oklch(0.773 0.016 251.194);\n  background: oklch(0.966 0.003 228.784);\n}\n\n.records-checkbox:active .records-checkbox-box {\n  transform: scale(0.96);\n}\n\n.records-checkbox input:focus-visible + .records-checkbox-box {\n  outline: 2px solid var(--highlight);\n  outline-offset: 2px;\n}\n\n.records-checkbox-box.is-active {\n  border-color: var(--highlight);\n  color: oklch(1 0 0);\n  background: var(--highlight);\n  box-shadow: none;\n}\n\n.records-checkbox-dash {\n  width: 8px;\n  height: 1.5px;\n  border-radius: 99px;\n  background: oklch(1 0 0);\n}\n\n:is(.dark, [data-theme=\"dark\"]) .records-checkbox-box {\n  border-color: oklch(0.45 0.017 254.711);\n  color: oklch(0.917 0.007 247.901);\n  background: oklch(0.346 0.015 252.294);\n}\n\n:is(.dark, [data-theme=\"dark\"]) .records-checkbox:hover .records-checkbox-box {\n  border-color: oklch(0.521 0.019 254);\n  background: oklch(0.391 0.016 251.761);\n}\n\n:is(.dark, [data-theme=\"dark\"]) .records-checkbox-box.is-active {\n  border-color: var(--highlight);\n  color: oklch(1 0 0);\n  background: var(--highlight);\n}\n\n:is(.dark, [data-theme=\"dark\"]) .records-checkbox-dash {\n  background: oklch(1 0 0);\n}\n\n.records-cell {\n  height: 35px;\n  padding: 0 8px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.records-sticky-cell {\n  position: sticky;\n  left: 0;\n  z-index: 2;\n  background: var(--card);\n  box-shadow: 5px 0 8px -10px oklch(0 0 0 / 0.4);\n}\n\n.records-company-cell {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  padding-left: 6px;\n  overflow: visible;\n}\n\n.records-row > .records-cell {\n  transition: background-color 120ms ease-out, color 120ms ease-out;\n}\n\n.records-row:hover > .records-cell {\n  background: var(--hover);\n}\n\n.records-row.is-selected > .records-cell {\n  background: color-mix(in srgb, var(--highlight) 7%, var(--card));\n}\n\n/* ── spreadsheet gutter ──────────────────────────────────\n * Row numbers at rest; the checkbox takes their place on\n * hover or once the row is selected. */\n.records-rownum {\n  display: inline-flex;\n  width: 24px;\n  height: 24px;\n  flex: 0 0 24px;\n  align-items: center;\n  justify-content: center;\n  color: var(--ink-3);\n  font-size: 11.5px;\n  font-variant-numeric: tabular-nums;\n}\n\n.records-row .records-checkbox {\n  display: none;\n}\n\n.records-row:hover .records-rownum,\n.records-row.is-selected .records-rownum {\n  display: none;\n}\n\n.records-row:hover .records-checkbox,\n.records-row.is-selected .records-checkbox {\n  display: inline-flex;\n}\n\n/* ── selected column — the property being configured ───── */\n.records-table th.is-colsel {\n  background: color-mix(in srgb, var(--highlight) 8%, var(--card));\n  box-shadow: inset 0 2px 0 var(--highlight);\n}\n\nth.is-colsel .records-header-button,\nth.is-colsel .records-company-header,\nth.is-colsel .records-header-icon {\n  color: var(--highlight-ink);\n}\n\n.records-table td.is-colsel {\n  background: color-mix(in srgb, var(--highlight) 4%, var(--card));\n}\n\n.records-row:hover > td.is-colsel {\n  background: color-mix(in srgb, var(--highlight) 8%, var(--card));\n}\n\n.records-row.is-selected > td.is-colsel {\n  background: color-mix(in srgb, var(--highlight) 10%, var(--card));\n}\n\n/* ── AI column calculating state ─────────────────────────── */\n.records-calc {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 8px;\n}\n\n.records-pulse {\n  width: 8px;\n  height: 8px;\n  flex: 0 0 8px;\n  border-radius: 50%;\n  background: var(--highlight);\n  animation: records-pulse 1.1s ease-in-out infinite;\n}\n\n@keyframes records-pulse {\n  0%,\n  100% {\n    opacity: 0.35;\n    transform: scale(0.8);\n  }\n  50% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n/* agent-screen — a static decorative cursor sitting on the capture (for show) */\n.agent-cursor {\n  position: absolute;\n  pointer-events: none;\n  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.35));\n}\n\n.records-company-mark {\n  display: inline-flex;\n  width: 20px;\n  height: 20px;\n  flex: 0 0 20px;\n  align-items: center;\n  justify-content: center;\n  border: 0;\n  border-radius: 6px;\n  color: var(--ink-2);\n  background: var(--field);\n  font-size: 10px;\n  font-weight: 500;\n}\n\n.records-company-name {\n  min-width: 0;\n  overflow: hidden;\n  color: var(--ink);\n  font-size: 13px;\n  font-weight: 500;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.records-company-name.has-link:hover,\n.records-company-name.has-link:focus-visible {\n  color: var(--highlight-ink);\n  text-decoration: underline;\n  text-underline-offset: 3px;\n}\n\n.records-tags {\n  position: relative;\n  display: flex;\n  width: 100%;\n  min-width: 0;\n  align-items: center;\n  gap: 4px;\n  overflow: hidden;\n}\n\n.records-tags-measure {\n  position: absolute;\n  top: 0;\n  left: 0;\n  display: flex;\n  width: max-content;\n  gap: 4px;\n  pointer-events: none;\n  visibility: hidden;\n}\n\n.records-tags-measure > span {\n  display: inline-flex;\n}\n\n.records-tag {\n  display: inline-flex;\n  height: 23px;\n  max-width: 115px;\n  align-items: center;\n  flex-shrink: 0;\n  overflow: hidden;\n  cursor: pointer;\n  border: 1px solid color-mix(in srgb, var(--tag-base) 32%, var(--card));\n  border-radius: 8px;\n  padding: 0 7px;\n  color: color-mix(in srgb, var(--tag-base) 92%, var(--ink));\n  background: color-mix(in srgb, var(--tag-base) 18%, var(--card));\n  font-size: 13px;\n  font-weight: 500;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.records-more-tag {\n  display: inline-flex;\n  height: 23px;\n  align-items: center;\n  flex-shrink: 0;\n  border: 1px solid var(--line-strong);\n  border-radius: 6px;\n  padding: 0 7px;\n  color: var(--ink-3);\n  background: var(--inset);\n  font-size: 11px;\n  font-weight: 500;\n}\n\n.records-more-tag {\n  border-color: var(--line-strong);\n  color: var(--ink-3);\n  background: var(--inset);\n}\n\n.records-muted {\n  color: var(--ink-3);\n}\n\n.records-strength {\n  gap: 8px;\n  color: var(--ink-2);\n}\n\n.records-strength-dot {\n  display: inline-block;\n  width: 8px;\n  height: 8px;\n  flex: 0 0 8px;\n  border-radius: 50%;\n}\n\n.records-link {\n  max-width: 100%;\n  gap: 5px;\n  overflow: hidden;\n  color: var(--highlight-ink);\n  text-overflow: ellipsis;\n  text-decoration: underline;\n  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);\n  text-underline-offset: 3px;\n  transition: color 120ms ease-out, text-decoration-color 120ms ease-out;\n}\n\n.records-link-label {\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.records-link svg {\n  flex-shrink: 0;\n}\n\n.records-link:hover,\n.records-link:focus-visible {\n  color: var(--ink);\n  text-decoration-color: currentColor;\n}\n\n.records-table tfoot td {\n  position: sticky;\n  z-index: 4;\n  bottom: 0;\n  height: 35px;\n  background: var(--inset);\n  color: var(--ink-2);\n  font-size: 14px;\n  font-weight: 500;\n  line-height: 1;\n}\n\n.records-table tfoot .records-sticky-cell {\n  z-index: 6;\n  background: var(--inset);\n}\n\n.records-calculation-label {\n  color: var(--ink-2);\n  font-weight: 500;\n}\n\n.records-footer-value {\n  display: inline-flex;\n  height: 35px;\n  align-items: center;\n  vertical-align: middle;\n}\n\n.records-calculation-number {\n  margin-right: 3px;\n  color: var(--ink);\n  font-variant-numeric: tabular-nums;\n}\n\n.records-add-calculation {\n  height: 35px;\n  gap: 6px;\n  color: var(--ink-3);\n  font-size: 14px;\n  font-weight: 500;\n  line-height: 1;\n  transition: color 120ms ease-out, transform 140ms var(--ease-out-strong);\n}\n\n.records-add-calculation:hover {\n  color: var(--ink);\n}\n\n.records-average {\n  display: inline-flex;\n  align-items: center;\n  gap: 7px;\n  color: var(--ink-2);\n}\n\n.records-calculation-row-secondary td {\n  position: sticky;\n  bottom: 35px;\n  background: color-mix(in srgb, var(--inset) 70%, var(--card));\n}\n\n.records-calculation-row-secondary .records-sticky-cell {\n  position: sticky;\n  background: color-mix(in srgb, var(--inset) 70%, var(--card));\n}\n\n.records-footer {\n  display: flex;\n  min-height: 35px;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  padding: 0 12px;\n  color: var(--ink-3);\n  font-size: 11.5px;\n}\n\n.records-footer strong {\n  color: var(--ink-2);\n  font-weight: 500;\n}\n\n.records-footer-hint {\n  display: inline-flex;\n  align-items: center;\n  gap: 5px;\n  opacity: 0;\n  transition: opacity 160ms ease-out;\n}\n\n.records-shell:hover .records-footer-hint {\n  opacity: 1;\n}\n\n.records-scroll-cue {\n  display: inline-flex;\n  color: var(--ink-3);\n}\n\n@media (max-width: 640px) {\n  .records-toolbar {\n    align-items: flex-start;\n    flex-direction: column;\n  }\n\n  .records-toolbar-left,\n  .records-toolbar-right {\n    width: 100%;\n  }\n\n  .records-toolbar-right {\n    justify-content: flex-end;\n  }\n\n}\n","type":"registry:file","target":"components/ward/styles/records-table.css"}],"meta":{"variants":["Default"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/RecordsTable.tsx","access":"free"},"categories":["data"],"type":"registry:component"}