@pipe0/react 1.0.39 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/components/ui/button.d.mts +1 -1
- package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +6 -2
- package/dist/records/index.d.mts +3 -0
- package/dist/records/record-value.d.mts +42 -0
- package/dist/records/record-value.d.mts.map +1 -0
- package/dist/records/record-value.mjs +229 -0
- package/dist/records/record-value.mjs.map +1 -0
- package/dist/records/record-widgets.d.mts +69 -0
- package/dist/records/record-widgets.d.mts.map +1 -0
- package/dist/records/record-widgets.mjs +273 -0
- package/dist/records/record-widgets.mjs.map +1 -0
- package/dist/records/records-table.d.mts +51 -0
- package/dist/records/records-table.d.mts.map +1 -0
- package/dist/records/records-table.mjs +168 -0
- package/dist/records/records-table.mjs.map +1 -0
- package/dist/styles/pipe0-form.css +1 -1
- package/dist/utils/catalog-helpers.d.mts +2 -11
- package/dist/utils/catalog-helpers.d.mts.map +1 -1
- package/dist/utils/catalog-helpers.mjs +2 -53
- package/dist/utils/catalog-helpers.mjs.map +1 -1
- package/dist/utils/field-icons.d.mts +15 -0
- package/dist/utils/field-icons.d.mts.map +1 -0
- package/dist/utils/field-icons.mjs +58 -0
- package/dist/utils/field-icons.mjs.map +1 -0
- package/dist/widgets/avatar-group.d.mts +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-value.mjs","names":[],"sources":["../../src/records/record-value.tsx"],"sourcesContent":["import type { DisplayValueMark, RecordFieldFormat } from \"@pipe0/base\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/utils.js\";\nimport {\n FormatGlyph,\n type RecordValueField,\n type RecordWidgetKey,\n RecordWidgets,\n StateTile,\n} from \"./record-widgets.js\";\n\nconst CELL_VALUE_LENGTH_LIMIT = 60;\n\nconst JSON_PREVIEW_MAX_ENTRIES = 4;\nconst JSON_PREVIEW_MAX_STRING = 24;\n\nfunction jsonScalarPreview(value: unknown): string {\n if (typeof value === \"string\") {\n const truncated =\n value.length > JSON_PREVIEW_MAX_STRING\n ? `${value.slice(0, JSON_PREVIEW_MAX_STRING)}…`\n : value;\n return `\"${truncated}\"`;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\" || value === null) {\n return String(value);\n }\n if (Array.isArray(value)) return \"[ … ]\";\n if (typeof value === \"object\") return \"{ … }\";\n return String(value);\n}\n\n/**\n * Monospace preview chip for JSON values (the JSON signature chip). Keys in\n * foreground, values in muted gray, and only the outer brackets take the\n * chip's hue — the bracket is the glyph, so no syntax colouring.\n */\nfunction JsonChip({ value }: { value: unknown }) {\n let body: ReactNode;\n if (Array.isArray(value)) {\n const shown = value.slice(0, JSON_PREVIEW_MAX_ENTRIES);\n const items = shown.map(jsonScalarPreview).join(\", \");\n body = (\n <>\n <span data-slot=\"sig-glyph\">[ </span>\n {`${items}${value.length > shown.length ? \", …\" : \"\"}`}\n <span data-slot=\"sig-glyph\"> ]</span>\n </>\n );\n } else if (value && typeof value === \"object\") {\n const entries = Object.entries(value).slice(0, JSON_PREVIEW_MAX_ENTRIES);\n const truncated = Object.keys(value).length > entries.length;\n body = (\n <>\n <span data-slot=\"sig-glyph\">{\"{ \"}</span>\n {entries.map(([key, entryValue], i) => (\n <span key={key}>\n <span className=\"pz:text-foreground\">\"{key}\"</span>\n {`: ${jsonScalarPreview(entryValue)}`}\n {(i < entries.length - 1 || truncated) && \", \"}\n </span>\n ))}\n {truncated && \"…\"}\n <span data-slot=\"sig-glyph\">{\" }\"}</span>\n </>\n );\n } else {\n body = JSON.stringify(value)?.substring(0, CELL_VALUE_LENGTH_LIMIT) ?? \"\";\n }\n\n return (\n <span className=\"p0-json-chip pz:inline-flex pz:max-w-full pz:min-w-0 pz:items-center pz:rounded-md pz:px-2 pz:py-0.5\">\n <span className=\"pz:truncate pz:font-mono pz:text-xs pz:leading-normal pz:whitespace-nowrap pz:text-muted-foreground\">\n {body}\n </span>\n </span>\n );\n}\n\n/**\n * Rich-text-style marks on a display label → classes. Marks compose (a set),\n * so the classes simply join. \"bold\" maps to semibold — the calm table\n * aesthetic treats full bold as shouting.\n */\nconst MARK_CLASSES: Record<DisplayValueMark, string> = {\n bold: \"pz:font-semibold\",\n italic: \"pz:italic\",\n underline: \"pz:underline pz:underline-offset-2\",\n strikethrough: \"pz:line-through\",\n};\n\nfunction marksClassName(field: RecordValueField): string {\n return (field.widgets?.display_value?.marks ?? []).map((mark) => MARK_CLASSES[mark]).join(\" \");\n}\n\n// Formats rendered as an outbound link. Bare domains get their scheme added\n// at render time (see `RecordValueText`).\nconst LINK_FORMATS = new Set<RecordFieldFormat>([\"profile_url\", \"url\", \"website_url\", \"domain\"]);\n\nexport function isLinkFormat(format: RecordFieldFormat): boolean {\n return format !== null && LINK_FORMATS.has(format);\n}\n\n// Identifiers read as one token — a person with their avatar, a company with\n// its logo, a domain, an email, a handle — so they sit on a quiet gray chip\n// that binds glyph and text together. Provenance tiles stay outside the chip.\nconst IDENTITY_FORMATS = new Set<RecordFieldFormat>([\n \"domain\",\n \"email\",\n \"profile_url\",\n \"url\",\n \"website_url\",\n \"phone\",\n]);\n\nexport function isIdentityValue(field: RecordValueField): boolean {\n if (field.status !== \"completed\" || field.value == null) return false;\n if (field.format !== null && IDENTITY_FORMATS.has(field.format)) return true;\n return Boolean(field.widgets?.avatar || field.widgets?.entity_logo);\n}\n\n/**\n * Signature values (LinkedIn profile, contact data) get their own hue, see the\n * signature chips in pipe0.css; every other identity sits on the gray chip.\n */\nfunction chipMaterial(field: RecordValueField): string {\n if (field.widgets?.icon?.key === \"linkedin\") return \"p0-linkedin-chip\";\n // No leading glyph on contact chips, so the text gets the full inner padding.\n if (field.format === \"email\" || field.format === \"phone\") return \"p0-contact-chip pz:pl-2\";\n return \"p0-cell-chip\";\n}\n\n/**\n * Booleans read as a state, not a word: a quiet check tile for true, an empty\n * tile plus muted label for false. Both stay in the gray palette so a column\n * of booleans adds no local contrast; the check mark alone carries the signal.\n */\nfunction BooleanValue({ value }: { value: boolean }) {\n return value ? (\n <span className=\"pz:inline-flex pz:items-center pz:gap-1.5\">\n <StateTile state=\"checked\" />\n <span>True</span>\n </span>\n ) : (\n <span className=\"pz:inline-flex pz:items-center pz:gap-1.5 pz:text-muted-foreground\">\n <StateTile state=\"empty\" />\n <span>False</span>\n </span>\n );\n}\n\n/** The value's text: status wording, a link, a JSON chip, a boolean tile, or the plain value. */\nexport function RecordValueText({\n field,\n inChip = false,\n}: {\n field: RecordValueField;\n inChip?: boolean;\n}) {\n if (field.status === \"processing\")\n return <span className=\"pz:text-muted-foreground pz:animate-status-pulse\">Running…</span>;\n if (field.status === \"queued\")\n return <span className=\"pz:text-muted-foreground pz:animate-status-pulse\">Queued…</span>;\n if (field.status === \"skipped\") return <span>{field.reason?.summary || \"Skipped\"}</span>;\n if (field.status === \"failed\")\n return <span className=\"pz:text-failed-foreground\">{field.reason?.summary || \"Failed\"}</span>;\n if (field.status === \"no_result\") return field.reason?.summary || \"No result\";\n if (field.status === \"pending\") return \"\";\n\n if (field.type === \"boolean\") return <BooleanValue value={Boolean(field.value)} />;\n\n const displayLabel = field.widgets?.display_value?.label;\n\n if (field.type === \"json\") {\n return displayLabel ? (\n <span\n className={cn(\"pz:text-muted-foreground pz:text-sm pz:underline\", marksClassName(field))}\n >\n {displayLabel}\n </span>\n ) : (\n <JsonChip value={field.value} />\n );\n }\n\n if (isLinkFormat(field.format) && field.type === \"string\") {\n const url = String(field.value);\n // Bare domains (e.g. company_domain) are valid values but relative hrefs\n const href = /^https?:\\/\\//i.test(url) ? url : `https://${url}`;\n const display = (displayLabel ?? url).substring(0, CELL_VALUE_LENGTH_LIMIT);\n return (\n <a\n href={href}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n onClick={(e) => e.stopPropagation()}\n className={cn(\n // Explicit colour and decoration: the library's reset covers only\n // its form root, so outside a reset host a bare <a> is blue.\n \"pz:cursor-pointer pz:text-inherit pz:underline-offset-2 pz:decoration-muted-foreground/60 pz:hover:decoration-foreground\",\n inChip ? \"pz:no-underline pz:hover:underline\" : \"pz:underline\",\n )}\n >\n {display}\n </a>\n );\n }\n\n if (displayLabel) {\n const marks = marksClassName(field);\n return marks ? <span className={marks}>{displayLabel}</span> : displayLabel;\n }\n\n if (field.type === \"number\") return String(field.value as number);\n\n if (field.type === \"string\") {\n return <span>{String(field.value as string).substring(0, CELL_VALUE_LENGTH_LIMIT)}</span>;\n }\n\n if (field.type === \"unknown\") return JSON.stringify(field.value);\n\n const _: never = field.type;\n return JSON.stringify(field.value);\n}\n\nconst DEFAULT_WIDGETS: readonly RecordWidgetKey[] = [\n \"avatar\",\n \"entity_logo\",\n \"external_link\",\n \"location_indicator\",\n \"icon\",\n \"emoji\",\n \"action_summary\",\n];\n\nexport interface RecordValueProps {\n field: RecordValueField;\n /** Which glyph widgets to draw, in order. Defaults to every glyph widget. */\n widgets?: readonly RecordWidgetKey[];\n /**\n * Rendered after the identity chip (or the glyphs) and before plain text —\n * the slot for provenance tiles such as a waterfall or a status tile.\n */\n afterGlyphs?: ReactNode;\n /** Class for the plain-text box (not used when the value sits on a chip). */\n textClassName?: string;\n}\n\n/**\n * A read-only record value: glyph widgets, then the value — identity values\n * (domains, emails, profiles, people, companies) on a chip. Renders as\n * `display: contents` so the parts lay out in the host's flex row.\n */\nexport function RecordValue({\n field,\n widgets = DEFAULT_WIDGETS,\n afterGlyphs,\n textClassName,\n}: RecordValueProps) {\n const glyphs = (\n <>\n <FormatGlyph field={field} />\n <RecordWidgets field={field} widgets={widgets} />\n </>\n );\n\n if (isIdentityValue(field)) {\n return (\n <span data-p0=\"record-value\" className=\"pz:contents\">\n <span\n className={cn(\n \"pz:inline-flex pz:min-w-0 pz:max-w-75 pz:shrink pz:items-center pz:gap-1.5 pz:py-0.5 pz:pr-2 pz:pl-1\",\n // Domains are outlined pills (an address you can open), the other\n // identities sit on the filled gray chip.\n field.format === \"domain\"\n ? \"pz:rounded-full pz:border pz:border-border pz:bg-background pz:pl-1.5 pz:shadow-[0_1px_1px_rgb(0_0_0/0.03)]\"\n : cn(\"pz:rounded-md\", chipMaterial(field)),\n )}\n >\n {glyphs}\n <span className=\"pz:min-w-0 pz:truncate\">\n <RecordValueText field={field} inChip />\n </span>\n </span>\n {afterGlyphs}\n </span>\n );\n }\n\n return (\n <span data-p0=\"record-value\" className=\"pz:contents\">\n {glyphs}\n {afterGlyphs}\n <span\n className={cn(\n \"pz:block pz:basis-0 pz:grow pz:overflow-hidden pz:max-w-75 pz:text-ellipsis\",\n textClassName,\n )}\n >\n <RecordValueText field={field} />\n </span>\n </span>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as _$react from "react";
|
|
2
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
import { RecordField } from "@pipe0/base";
|
|
4
|
+
|
|
5
|
+
//#region src/records/record-widgets.d.ts
|
|
6
|
+
/** The slice of a record field the read-only renderers look at. */
|
|
7
|
+
type RecordValueField = Pick<RecordField, "value" | "status" | "type" | "format" | "reason" | "widgets">;
|
|
8
|
+
/**
|
|
9
|
+
* Rewrites every image URL the records components render (avatars, logos,
|
|
10
|
+
* favicons). Identity by default; a host with a strict image policy (the MCP
|
|
11
|
+
* Apps view) routes them through one allowed domain.
|
|
12
|
+
*/
|
|
13
|
+
declare const RecordImageUrlContext: _$react.Context<(src: string) => string>;
|
|
14
|
+
/** 24px round avatar: the image when it loads, coloured initials otherwise. */
|
|
15
|
+
declare function RecordAvatar({
|
|
16
|
+
name,
|
|
17
|
+
imageSrc
|
|
18
|
+
}: {
|
|
19
|
+
name: string;
|
|
20
|
+
imageSrc?: string | null;
|
|
21
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* LinkedIn's mark, drawn as the real "in" bug: one filled rounded square with
|
|
24
|
+
* the letters cut out (an SVG mask), so the letters show whatever surface the
|
|
25
|
+
* badge sits on.
|
|
26
|
+
*/
|
|
27
|
+
declare function LinkedinMark({
|
|
28
|
+
className
|
|
29
|
+
}: {
|
|
30
|
+
className?: string;
|
|
31
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
32
|
+
/** LinkedIn's mark in LinkedIn blue, in a 20px slot. */
|
|
33
|
+
declare function LinkedinBadge({
|
|
34
|
+
className,
|
|
35
|
+
iconClassName
|
|
36
|
+
}: {
|
|
37
|
+
className?: string;
|
|
38
|
+
iconClassName?: string;
|
|
39
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* The one glyph for a yes/no state in a cell: a quiet check tile, an empty
|
|
42
|
+
* tile, or the failed tile. Booleans and action outcomes share it so a
|
|
43
|
+
* column of "Enrolled" reads like a column of "True" — one vocabulary, gray.
|
|
44
|
+
*/
|
|
45
|
+
declare function StateTile({
|
|
46
|
+
state
|
|
47
|
+
}: {
|
|
48
|
+
state: "checked" | "empty" | "failed";
|
|
49
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
50
|
+
/** A favicon for link formats that carry no logo widget of their own. */
|
|
51
|
+
declare function FormatGlyph({
|
|
52
|
+
field
|
|
53
|
+
}: {
|
|
54
|
+
field: RecordValueField;
|
|
55
|
+
}): _$react_jsx_runtime0.JSX.Element | null;
|
|
56
|
+
type RecordWidgetKey = "avatar" | "entity_logo" | "external_link" | "location_indicator" | "icon" | "emoji" | "action_summary";
|
|
57
|
+
/** The glyph widgets a field carries, in the order given. */
|
|
58
|
+
declare function RecordWidgets({
|
|
59
|
+
field,
|
|
60
|
+
widgets,
|
|
61
|
+
className
|
|
62
|
+
}: {
|
|
63
|
+
field: RecordValueField;
|
|
64
|
+
widgets: readonly RecordWidgetKey[];
|
|
65
|
+
className?: string;
|
|
66
|
+
}): _$react_jsx_runtime0.JSX.Element | null;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { FormatGlyph, LinkedinBadge, LinkedinMark, RecordAvatar, RecordImageUrlContext, RecordValueField, RecordWidgetKey, RecordWidgets, StateTile };
|
|
69
|
+
//# sourceMappingURL=record-widgets.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-widgets.d.mts","names":[],"sources":["../../src/records/record-widgets.tsx"],"mappings":";;;;;;KAMY,gBAAA,GAAmB,IAAA,CAC7B,WAAA;;;AADF;;;cAUa,qBAAA,EAA4E,OAAA,CAAvD,OAAA,EAAA,GAAA;;iBAgDlB,YAAA,CAAA;EAAe,IAAA;EAAM;AAAA;EAAc,IAAA;EAAc,QAAA;AAAA,IAA0B,oBAAA,CAAA,GAAA,CAAA,OAAA;;AAA3F;;;;iBAmCgB,YAAA,CAAA;EAAe;AAAA;EAAe,SAAA;AAAA,IAAoB,oBAAA,CAAA,GAAA,CAAA,OAAA;;iBA+BlD,aAAA,CAAA;EACd,SAAA;EACA;AAAA;EAEA,SAAA;EACA,aAAA;AAAA,IACD,oBAAA,CAAA,GAAA,CAAA,OAAA;;;;;;iBAmEe,SAAA,CAAA;EAAY;AAAA;EAAW,KAAA;AAAA,IAAuC,oBAAA,CAAA,GAAA,CAAA,OAAA;;iBAuE9D,WAAA,CAAA;EAAc;AAAA;EAAW,KAAA,EAAO,gBAAA;AAAA,IAAkB,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,KActD,eAAA;;iBAUI,aAAA,CAAA;EACd,KAAA;EACA,OAAA;EACA;AAAA;EAEA,KAAA,EAAO,gBAAA;EACP,OAAA,WAAkB,eAAA;EAClB,SAAA;AAAA,IACD,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.mjs";
|
|
2
|
+
import { Building2, Check, ExternalLink, X } from "../components/internal/icons.mjs";
|
|
3
|
+
import { createContext, useContext, useId, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/records/record-widgets.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Rewrites every image URL the records components render (avatars, logos,
|
|
9
|
+
* favicons). Identity by default; a host with a strict image policy (the MCP
|
|
10
|
+
* Apps view) routes them through one allowed domain.
|
|
11
|
+
*/
|
|
12
|
+
const RecordImageUrlContext = createContext((src) => src);
|
|
13
|
+
function faviconUrl(domain) {
|
|
14
|
+
return `https://www.google.com/s2/favicons?sz=32&domain=${domain.replace(/^(https?:\/\/)?(www\.)?/, "")}`;
|
|
15
|
+
}
|
|
16
|
+
function hideBrokenImage(e) {
|
|
17
|
+
const target = e.currentTarget;
|
|
18
|
+
target.style.display = "none";
|
|
19
|
+
target.onerror = null;
|
|
20
|
+
}
|
|
21
|
+
const AVATAR_COLORS = [
|
|
22
|
+
"pz:bg-red-100 pz:text-red-800",
|
|
23
|
+
"pz:bg-blue-100 pz:text-blue-800",
|
|
24
|
+
"pz:bg-green-100 pz:text-green-800",
|
|
25
|
+
"pz:bg-yellow-100 pz:text-yellow-800",
|
|
26
|
+
"pz:bg-purple-100 pz:text-purple-800",
|
|
27
|
+
"pz:bg-pink-100 pz:text-pink-800",
|
|
28
|
+
"pz:bg-indigo-100 pz:text-indigo-800",
|
|
29
|
+
"pz:bg-cyan-100 pz:text-cyan-800",
|
|
30
|
+
"pz:bg-orange-100 pz:text-orange-800",
|
|
31
|
+
"pz:bg-teal-100 pz:text-teal-800",
|
|
32
|
+
"pz:bg-amber-100 pz:text-amber-800",
|
|
33
|
+
"pz:bg-lime-100 pz:text-lime-800"
|
|
34
|
+
];
|
|
35
|
+
function avatarColor(name) {
|
|
36
|
+
let hash = 0;
|
|
37
|
+
for (let i = 0; i < name.length; i++) {
|
|
38
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
39
|
+
hash = hash & hash;
|
|
40
|
+
}
|
|
41
|
+
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
|
|
42
|
+
}
|
|
43
|
+
function avatarLetters(name) {
|
|
44
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
45
|
+
if (parts.length === 0) return "AN";
|
|
46
|
+
return ((Array.from(parts[0])[0] ?? "") + (parts.length > 1 ? Array.from(parts[parts.length - 1])[0] ?? "" : "")).toUpperCase() || "AN";
|
|
47
|
+
}
|
|
48
|
+
/** 24px round avatar: the image when it loads, coloured initials otherwise. */
|
|
49
|
+
function RecordAvatar({ name, imageSrc }) {
|
|
50
|
+
const imageUrl = useContext(RecordImageUrlContext);
|
|
51
|
+
const [broken, setBroken] = useState(false);
|
|
52
|
+
const showImage = Boolean(imageSrc) && !broken;
|
|
53
|
+
return /* @__PURE__ */ jsx("span", {
|
|
54
|
+
className: cn("pz:relative pz:flex pz:size-6 pz:shrink-0 pz:overflow-hidden pz:rounded-full pz:select-none", "pz:shadow-[inset_0_0_0_0.5px_rgba(17,17,17,0.1)]", !showImage && avatarColor(name)),
|
|
55
|
+
children: showImage ? /* @__PURE__ */ jsx("img", {
|
|
56
|
+
src: imageSrc ? imageUrl(imageSrc) : void 0,
|
|
57
|
+
loading: "lazy",
|
|
58
|
+
decoding: "async",
|
|
59
|
+
alt: name,
|
|
60
|
+
className: "pz:aspect-square pz:size-full pz:object-cover",
|
|
61
|
+
onError: () => setBroken(true)
|
|
62
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
63
|
+
className: "pz:flex pz:size-full pz:items-center pz:justify-center pz:text-[10px] pz:font-medium pz:tracking-tight",
|
|
64
|
+
children: avatarLetters(name)
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* LinkedIn's mark, drawn as the real "in" bug: one filled rounded square with
|
|
70
|
+
* the letters cut out (an SVG mask), so the letters show whatever surface the
|
|
71
|
+
* badge sits on.
|
|
72
|
+
*/
|
|
73
|
+
function LinkedinMark({ className }) {
|
|
74
|
+
const maskId = useId();
|
|
75
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
76
|
+
viewBox: "0 0 24 24",
|
|
77
|
+
"aria-hidden": "true",
|
|
78
|
+
className,
|
|
79
|
+
children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("mask", {
|
|
80
|
+
id: maskId,
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx("rect", {
|
|
83
|
+
x: "1.5",
|
|
84
|
+
y: "1.5",
|
|
85
|
+
width: "21",
|
|
86
|
+
height: "21",
|
|
87
|
+
rx: "4.6",
|
|
88
|
+
fill: "white"
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ jsx("circle", {
|
|
91
|
+
cx: "7.15",
|
|
92
|
+
cy: "7.15",
|
|
93
|
+
r: "1.65",
|
|
94
|
+
fill: "black"
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsx("rect", {
|
|
97
|
+
x: "5.65",
|
|
98
|
+
y: "9.75",
|
|
99
|
+
width: "3",
|
|
100
|
+
height: "8.75",
|
|
101
|
+
rx: "0.35",
|
|
102
|
+
fill: "black"
|
|
103
|
+
}),
|
|
104
|
+
/* @__PURE__ */ jsx("path", {
|
|
105
|
+
fill: "black",
|
|
106
|
+
d: "M10.55 9.75h2.85v1.3c.5-.85 1.5-1.55 2.95-1.55 2.3 0 3.25 1.5 3.25 3.85v5.15h-2.95v-4.6c0-1.15-.4-1.9-1.45-1.9-1.15 0-1.8.85-1.8 2v4.5h-2.85z"
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
}) }), /* @__PURE__ */ jsx("rect", {
|
|
110
|
+
x: "1.5",
|
|
111
|
+
y: "1.5",
|
|
112
|
+
width: "21",
|
|
113
|
+
height: "21",
|
|
114
|
+
rx: "4.6",
|
|
115
|
+
fill: "currentColor",
|
|
116
|
+
mask: `url(#${maskId})`
|
|
117
|
+
})]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** LinkedIn's mark in LinkedIn blue, in a 20px slot. */
|
|
121
|
+
function LinkedinBadge({ className, iconClassName }) {
|
|
122
|
+
return /* @__PURE__ */ jsx("span", {
|
|
123
|
+
className: cn("pz:flex pz:size-5 pz:shrink-0 pz:items-center pz:justify-center pz:text-[#0A66C2] pz:dark:text-[#3d8ee0]", className),
|
|
124
|
+
children: /* @__PURE__ */ jsx(LinkedinMark, { className: cn("pz:size-4", iconClassName) })
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const SENIORITY_BARS = {
|
|
128
|
+
seniority_junior: 0,
|
|
129
|
+
seniority_frontline: 1,
|
|
130
|
+
seniority_ic: 1,
|
|
131
|
+
seniority_senior_ic: 2,
|
|
132
|
+
seniority_manager: 2,
|
|
133
|
+
seniority_leadership: 3,
|
|
134
|
+
seniority_c_suite: 4
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Seniority is a rank, so it renders as one: four bars filled up to the
|
|
138
|
+
* level (the priority glyph convention).
|
|
139
|
+
*/
|
|
140
|
+
function SeniorityBars({ level }) {
|
|
141
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
142
|
+
role: "img",
|
|
143
|
+
"aria-label": `Seniority ${level} of 4`,
|
|
144
|
+
viewBox: "0 0 14 14",
|
|
145
|
+
className: "pz:size-3 pz:shrink-0 pz:text-muted-foreground",
|
|
146
|
+
fill: "currentColor",
|
|
147
|
+
children: [
|
|
148
|
+
5,
|
|
149
|
+
8,
|
|
150
|
+
11,
|
|
151
|
+
14
|
|
152
|
+
].map((h, i) => /* @__PURE__ */ jsx("rect", {
|
|
153
|
+
x: i * 3.6,
|
|
154
|
+
y: 14 - h,
|
|
155
|
+
width: 2.6,
|
|
156
|
+
height: h,
|
|
157
|
+
rx: .8,
|
|
158
|
+
opacity: i < level ? 1 : .25
|
|
159
|
+
}, h))
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function IconWidget({ widget }) {
|
|
163
|
+
if (widget.key === "linkedin") return /* @__PURE__ */ jsx(LinkedinBadge, {});
|
|
164
|
+
if (widget.key === "job") return /* @__PURE__ */ jsx(Building2, {
|
|
165
|
+
strokeWidth: .8,
|
|
166
|
+
className: "pz:size-4"
|
|
167
|
+
});
|
|
168
|
+
const level = SENIORITY_BARS[widget.key];
|
|
169
|
+
if (level === void 0) return null;
|
|
170
|
+
return /* @__PURE__ */ jsx(SeniorityBars, { level });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The one glyph for a yes/no state in a cell: a quiet check tile, an empty
|
|
174
|
+
* tile, or the failed tile. Booleans and action outcomes share it so a
|
|
175
|
+
* column of "Enrolled" reads like a column of "True" — one vocabulary, gray.
|
|
176
|
+
*/
|
|
177
|
+
function StateTile({ state }) {
|
|
178
|
+
if (state === "failed") return /* @__PURE__ */ jsx("span", {
|
|
179
|
+
"aria-hidden": true,
|
|
180
|
+
className: "pz:flex pz:size-4 pz:shrink-0 pz:items-center pz:justify-center pz:rounded-[4px] pz:bg-failed pz:text-failed-foreground",
|
|
181
|
+
children: /* @__PURE__ */ jsx(X, {
|
|
182
|
+
className: "pz:size-3",
|
|
183
|
+
strokeWidth: 2.5
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
if (state === "checked") return /* @__PURE__ */ jsx("span", {
|
|
187
|
+
"aria-hidden": true,
|
|
188
|
+
className: "pz:flex pz:size-4 pz:shrink-0 pz:items-center pz:justify-center pz:rounded-[4px] pz:border pz:border-input pz:bg-muted pz:text-muted-foreground",
|
|
189
|
+
children: /* @__PURE__ */ jsx(Check, {
|
|
190
|
+
className: "pz:size-3",
|
|
191
|
+
strokeWidth: 2.5
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
return /* @__PURE__ */ jsx("span", {
|
|
195
|
+
"aria-hidden": true,
|
|
196
|
+
className: "pz:size-4 pz:shrink-0 pz:rounded-[4px] pz:border pz:border-input"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function ActionSummaryWidget({ widget }) {
|
|
200
|
+
if (widget.result_status === "completed") return /* @__PURE__ */ jsx(StateTile, { state: "checked" });
|
|
201
|
+
if (widget.result_status === "failed") return /* @__PURE__ */ jsx(StateTile, { state: "failed" });
|
|
202
|
+
widget.result_status;
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/** Favicons and logos render raw at 16px — no tile chrome, no cast shadow. */
|
|
206
|
+
function LogoImage({ src, alt }) {
|
|
207
|
+
return /* @__PURE__ */ jsx("img", {
|
|
208
|
+
src: useContext(RecordImageUrlContext)(src),
|
|
209
|
+
alt,
|
|
210
|
+
loading: "lazy",
|
|
211
|
+
decoding: "async",
|
|
212
|
+
className: "pz:inline-block pz:size-4 pz:shrink-0 pz:rounded-[3.5px] pz:object-contain",
|
|
213
|
+
onError: hideBrokenImage
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Icon-only anchor in the left widget rail. The value text stays plain (cell
|
|
218
|
+
* clicks keep their normal behavior); only the icon navigates.
|
|
219
|
+
*/
|
|
220
|
+
function ExternalLinkWidget({ url }) {
|
|
221
|
+
return /* @__PURE__ */ jsx("a", {
|
|
222
|
+
href: url,
|
|
223
|
+
target: "_blank",
|
|
224
|
+
rel: "noreferrer noopener",
|
|
225
|
+
onClick: (e) => e.stopPropagation(),
|
|
226
|
+
title: url,
|
|
227
|
+
className: "pz:shrink-0 pz:text-muted-foreground/60 pz:hover:text-foreground",
|
|
228
|
+
children: /* @__PURE__ */ jsx(ExternalLink, { className: "pz:size-3.5" })
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** A favicon for link formats that carry no logo widget of their own. */
|
|
232
|
+
function FormatGlyph({ field }) {
|
|
233
|
+
if (field.status !== "completed" || typeof field.value !== "string") return null;
|
|
234
|
+
if (field.format === "website_url" || field.format === "url") return /* @__PURE__ */ jsx(LogoImage, {
|
|
235
|
+
src: faviconUrl(field.value),
|
|
236
|
+
alt: "Favicon"
|
|
237
|
+
});
|
|
238
|
+
if (field.format === "domain" && !field.widgets?.entity_logo) return /* @__PURE__ */ jsx(LogoImage, {
|
|
239
|
+
src: faviconUrl(field.value),
|
|
240
|
+
alt: "Favicon"
|
|
241
|
+
});
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/** The glyph widgets a field carries, in the order given. */
|
|
245
|
+
function RecordWidgets({ field, widgets, className }) {
|
|
246
|
+
const available = field.widgets;
|
|
247
|
+
if (!available) return null;
|
|
248
|
+
const rendered = widgets.flatMap((key) => {
|
|
249
|
+
if (key === "avatar" && available.avatar) return [/* @__PURE__ */ jsx(RecordAvatar, {
|
|
250
|
+
name: available.avatar.name,
|
|
251
|
+
imageSrc: available.avatar.image_url
|
|
252
|
+
}, key)];
|
|
253
|
+
if (key === "entity_logo" && available.entity_logo?.image_url) return [/* @__PURE__ */ jsx(LogoImage, {
|
|
254
|
+
src: available.entity_logo.image_url,
|
|
255
|
+
alt: available.entity_logo.entity
|
|
256
|
+
}, key)];
|
|
257
|
+
if (key === "external_link" && available.external_link?.url && field.status === "completed") return [/* @__PURE__ */ jsx(ExternalLinkWidget, { url: available.external_link.url }, key)];
|
|
258
|
+
if (key === "location_indicator" && available.location_indicator) return [/* @__PURE__ */ jsx("span", { children: available.location_indicator.emoji }, key)];
|
|
259
|
+
if (key === "emoji" && available.emoji) return [/* @__PURE__ */ jsx("span", { children: available.emoji.value }, key)];
|
|
260
|
+
if (key === "icon" && available.icon) return [/* @__PURE__ */ jsx(IconWidget, { widget: available.icon }, key)];
|
|
261
|
+
if (key === "action_summary" && available.action_summary) return [/* @__PURE__ */ jsx(ActionSummaryWidget, { widget: available.action_summary }, key)];
|
|
262
|
+
return [];
|
|
263
|
+
});
|
|
264
|
+
if (rendered.length === 0) return null;
|
|
265
|
+
return /* @__PURE__ */ jsx("span", {
|
|
266
|
+
className: cn("pz:flex pz:shrink-0 pz:items-center pz:gap-1", className),
|
|
267
|
+
children: rendered
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
//#endregion
|
|
272
|
+
export { FormatGlyph, LinkedinBadge, LinkedinMark, RecordAvatar, RecordImageUrlContext, RecordWidgets, StateTile };
|
|
273
|
+
//# sourceMappingURL=record-widgets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-widgets.mjs","names":[],"sources":["../../src/records/record-widgets.tsx"],"sourcesContent":["import type { RecordField, Widgets } from \"@pipe0/base\";\nimport { createContext, type SyntheticEvent, useContext, useId, useState } from \"react\";\nimport { Building2, Check, ExternalLink, X } from \"../components/internal/icons.js\";\nimport { cn } from \"../lib/utils.js\";\n\n/** The slice of a record field the read-only renderers look at. */\nexport type RecordValueField = Pick<\n RecordField,\n \"value\" | \"status\" | \"type\" | \"format\" | \"reason\" | \"widgets\"\n>;\n\n/**\n * Rewrites every image URL the records components render (avatars, logos,\n * favicons). Identity by default; a host with a strict image policy (the MCP\n * Apps view) routes them through one allowed domain.\n */\nexport const RecordImageUrlContext = createContext<(src: string) => string>((src) => src);\n\n// Same URL as @pipe0/base's getFaviconUrlGoogle, kept local so this module\n// stays free of @pipe0/base value imports (see utils/field-icons.tsx).\nfunction faviconUrl(domain: string): string {\n return `https://www.google.com/s2/favicons?sz=32&domain=${domain.replace(/^(https?:\\/\\/)?(www\\.)?/, \"\")}`;\n}\n\nfunction hideBrokenImage(e: SyntheticEvent<HTMLImageElement>) {\n const target = e.currentTarget;\n target.style.display = \"none\";\n target.onerror = null;\n}\n\nconst AVATAR_COLORS = [\n \"pz:bg-red-100 pz:text-red-800\",\n \"pz:bg-blue-100 pz:text-blue-800\",\n \"pz:bg-green-100 pz:text-green-800\",\n \"pz:bg-yellow-100 pz:text-yellow-800\",\n \"pz:bg-purple-100 pz:text-purple-800\",\n \"pz:bg-pink-100 pz:text-pink-800\",\n \"pz:bg-indigo-100 pz:text-indigo-800\",\n \"pz:bg-cyan-100 pz:text-cyan-800\",\n \"pz:bg-orange-100 pz:text-orange-800\",\n \"pz:bg-teal-100 pz:text-teal-800\",\n \"pz:bg-amber-100 pz:text-amber-800\",\n \"pz:bg-lime-100 pz:text-lime-800\",\n];\n\n// Same hash as the dash avatars, so a person keeps their colour everywhere.\nfunction avatarColor(name: string): string {\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n hash = hash & hash;\n }\n return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];\n}\n\nfunction avatarLetters(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return \"AN\";\n const first = Array.from(parts[0])[0] ?? \"\";\n const last = parts.length > 1 ? (Array.from(parts[parts.length - 1])[0] ?? \"\") : \"\";\n return (first + last).toUpperCase() || \"AN\";\n}\n\n/** 24px round avatar: the image when it loads, coloured initials otherwise. */\nexport function RecordAvatar({ name, imageSrc }: { name: string; imageSrc?: string | null }) {\n const imageUrl = useContext(RecordImageUrlContext);\n const [broken, setBroken] = useState(false);\n const showImage = Boolean(imageSrc) && !broken;\n return (\n <span\n className={cn(\n \"pz:relative pz:flex pz:size-6 pz:shrink-0 pz:overflow-hidden pz:rounded-full pz:select-none\",\n \"pz:shadow-[inset_0_0_0_0.5px_rgba(17,17,17,0.1)]\",\n !showImage && avatarColor(name),\n )}\n >\n {showImage ? (\n <img\n src={imageSrc ? imageUrl(imageSrc) : undefined}\n loading=\"lazy\"\n decoding=\"async\"\n alt={name}\n className=\"pz:aspect-square pz:size-full pz:object-cover\"\n onError={() => setBroken(true)}\n />\n ) : (\n <span className=\"pz:flex pz:size-full pz:items-center pz:justify-center pz:text-[10px] pz:font-medium pz:tracking-tight\">\n {avatarLetters(name)}\n </span>\n )}\n </span>\n );\n}\n\n/**\n * LinkedIn's mark, drawn as the real \"in\" bug: one filled rounded square with\n * the letters cut out (an SVG mask), so the letters show whatever surface the\n * badge sits on.\n */\nexport function LinkedinMark({ className }: { className?: string }) {\n const maskId = useId();\n return (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" className={className}>\n <defs>\n <mask id={maskId}>\n <rect x=\"1.5\" y=\"1.5\" width=\"21\" height=\"21\" rx=\"4.6\" fill=\"white\" />\n {/* i */}\n <circle cx=\"7.15\" cy=\"7.15\" r=\"1.65\" fill=\"black\" />\n <rect x=\"5.65\" y=\"9.75\" width=\"3\" height=\"8.75\" rx=\"0.35\" fill=\"black\" />\n {/* n */}\n <path\n fill=\"black\"\n d=\"M10.55 9.75h2.85v1.3c.5-.85 1.5-1.55 2.95-1.55 2.3 0 3.25 1.5 3.25 3.85v5.15h-2.95v-4.6c0-1.15-.4-1.9-1.45-1.9-1.15 0-1.8.85-1.8 2v4.5h-2.85z\"\n />\n </mask>\n </defs>\n <rect\n x=\"1.5\"\n y=\"1.5\"\n width=\"21\"\n height=\"21\"\n rx=\"4.6\"\n fill=\"currentColor\"\n mask={`url(#${maskId})`}\n />\n </svg>\n );\n}\n\n/** LinkedIn's mark in LinkedIn blue, in a 20px slot. */\nexport function LinkedinBadge({\n className,\n iconClassName,\n}: {\n className?: string;\n iconClassName?: string;\n}) {\n return (\n <span\n className={cn(\n \"pz:flex pz:size-5 pz:shrink-0 pz:items-center pz:justify-center pz:text-[#0A66C2] pz:dark:text-[#3d8ee0]\",\n className,\n )}\n >\n <LinkedinMark className={cn(\"pz:size-4\", iconClassName)} />\n </span>\n );\n}\n\n// Filled bars per seniority bucket. Unknown renders nothing rather than a\n// placeholder glyph — absence is quieter than a question mark.\nconst SENIORITY_BARS: Partial<Record<NonNullable<Widgets[\"icon\"]>[\"key\"], number>> = {\n seniority_junior: 0,\n seniority_frontline: 1,\n seniority_ic: 1,\n seniority_senior_ic: 2,\n seniority_manager: 2,\n seniority_leadership: 3,\n seniority_c_suite: 4,\n};\n\n/**\n * Seniority is a rank, so it renders as one: four bars filled up to the\n * level (the priority glyph convention).\n */\nfunction SeniorityBars({ level }: { level: number }) {\n const heights = [5, 8, 11, 14];\n return (\n <svg\n role=\"img\"\n aria-label={`Seniority ${level} of 4`}\n viewBox=\"0 0 14 14\"\n className=\"pz:size-3 pz:shrink-0 pz:text-muted-foreground\"\n fill=\"currentColor\"\n >\n {heights.map((h, i) => (\n <rect\n key={h}\n x={i * 3.6}\n y={14 - h}\n width={2.6}\n height={h}\n rx={0.8}\n opacity={i < level ? 1 : 0.25}\n />\n ))}\n </svg>\n );\n}\n\nfunction IconWidget({ widget }: { widget: NonNullable<Widgets[\"icon\"]> }) {\n if (widget.key === \"linkedin\") return <LinkedinBadge />;\n if (widget.key === \"job\") return <Building2 strokeWidth={0.8} className=\"pz:size-4\" />;\n const level = SENIORITY_BARS[widget.key];\n if (level === undefined) return null;\n return <SeniorityBars level={level} />;\n}\n\n/**\n * The one glyph for a yes/no state in a cell: a quiet check tile, an empty\n * tile, or the failed tile. Booleans and action outcomes share it so a\n * column of \"Enrolled\" reads like a column of \"True\" — one vocabulary, gray.\n */\nexport function StateTile({ state }: { state: \"checked\" | \"empty\" | \"failed\" }) {\n if (state === \"failed\") {\n return (\n <span\n aria-hidden\n className=\"pz:flex pz:size-4 pz:shrink-0 pz:items-center pz:justify-center pz:rounded-[4px] pz:bg-failed pz:text-failed-foreground\"\n >\n <X className=\"pz:size-3\" strokeWidth={2.5} />\n </span>\n );\n }\n if (state === \"checked\") {\n return (\n <span\n aria-hidden\n className=\"pz:flex pz:size-4 pz:shrink-0 pz:items-center pz:justify-center pz:rounded-[4px] pz:border pz:border-input pz:bg-muted pz:text-muted-foreground\"\n >\n <Check className=\"pz:size-3\" strokeWidth={2.5} />\n </span>\n );\n }\n return (\n <span\n aria-hidden\n className=\"pz:size-4 pz:shrink-0 pz:rounded-[4px] pz:border pz:border-input\"\n />\n );\n}\n\nfunction ActionSummaryWidget({ widget }: { widget: NonNullable<Widgets[\"action_summary\"]> }) {\n if (widget.result_status === \"completed\") return <StateTile state=\"checked\" />;\n if (widget.result_status === \"failed\") return <StateTile state=\"failed\" />;\n const _: never = widget.result_status;\n return null;\n}\n\n/** Favicons and logos render raw at 16px — no tile chrome, no cast shadow. */\nfunction LogoImage({ src, alt }: { src: string; alt: string }) {\n const imageUrl = useContext(RecordImageUrlContext);\n return (\n <img\n src={imageUrl(src)}\n alt={alt}\n loading=\"lazy\"\n decoding=\"async\"\n className=\"pz:inline-block pz:size-4 pz:shrink-0 pz:rounded-[3.5px] pz:object-contain\"\n onError={hideBrokenImage}\n />\n );\n}\n\n/**\n * Icon-only anchor in the left widget rail. The value text stays plain (cell\n * clicks keep their normal behavior); only the icon navigates.\n */\nfunction ExternalLinkWidget({ url }: { url: string }) {\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n onClick={(e) => e.stopPropagation()}\n title={url}\n className=\"pz:shrink-0 pz:text-muted-foreground/60 pz:hover:text-foreground\"\n >\n <ExternalLink className=\"pz:size-3.5\" />\n </a>\n );\n}\n\n/** A favicon for link formats that carry no logo widget of their own. */\nexport function FormatGlyph({ field }: { field: RecordValueField }) {\n if (field.status !== \"completed\" || typeof field.value !== \"string\") return null;\n\n if (field.format === \"website_url\" || field.format === \"url\") {\n return <LogoImage src={faviconUrl(field.value)} alt=\"Favicon\" />;\n }\n // Catalog domain fields (company_domain) already carry an entity_logo\n // widget; only fill in for user-made domain columns so the tile is not doubled.\n if (field.format === \"domain\" && !field.widgets?.entity_logo) {\n return <LogoImage src={faviconUrl(field.value)} alt=\"Favicon\" />;\n }\n return null;\n}\n\nexport type RecordWidgetKey =\n | \"avatar\"\n | \"entity_logo\"\n | \"external_link\"\n | \"location_indicator\"\n | \"icon\"\n | \"emoji\"\n | \"action_summary\";\n\n/** The glyph widgets a field carries, in the order given. */\nexport function RecordWidgets({\n field,\n widgets,\n className,\n}: {\n field: RecordValueField;\n widgets: readonly RecordWidgetKey[];\n className?: string;\n}) {\n const available = field.widgets;\n if (!available) return null;\n\n const rendered = widgets.flatMap((key) => {\n if (key === \"avatar\" && available.avatar) {\n return [\n <RecordAvatar\n key={key}\n name={available.avatar.name}\n imageSrc={available.avatar.image_url}\n />,\n ];\n }\n if (key === \"entity_logo\" && available.entity_logo?.image_url) {\n return [\n <LogoImage\n key={key}\n src={available.entity_logo.image_url}\n alt={available.entity_logo.entity}\n />,\n ];\n }\n if (key === \"external_link\" && available.external_link?.url && field.status === \"completed\") {\n return [<ExternalLinkWidget key={key} url={available.external_link.url} />];\n }\n if (key === \"location_indicator\" && available.location_indicator) {\n return [<span key={key}>{available.location_indicator.emoji}</span>];\n }\n if (key === \"emoji\" && available.emoji) {\n return [<span key={key}>{available.emoji.value}</span>];\n }\n if (key === \"icon\" && available.icon) {\n return [<IconWidget key={key} widget={available.icon} />];\n }\n if (key === \"action_summary\" && available.action_summary) {\n return [<ActionSummaryWidget key={key} widget={available.action_summary} />];\n }\n return [];\n });\n\n if (rendered.length === 0) return null;\n\n // Row layout matters once a field carries more than one widget (e.g.\n // x_profile_match emits entity_logo + avatar).\n return (\n <span className={cn(\"pz:flex pz:shrink-0 pz:items-center pz:gap-1\", className)}>\n {rendered}\n </span>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import { RecordField } from "@pipe0/base";
|
|
3
|
+
|
|
4
|
+
//#region src/records/records-table.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A cell as both run responses carry it: a pipes record's field
|
|
7
|
+
* (`RecordField`) or a search result's field, which leaves `type` and
|
|
8
|
+
* `format` to its column's definition.
|
|
9
|
+
*/
|
|
10
|
+
type RecordsTableCell = Pick<RecordField, "value" | "status" | "widgets"> & Partial<Pick<RecordField, "type" | "format" | "reason">>;
|
|
11
|
+
/**
|
|
12
|
+
* One row of native cells, field → cell: a pipes record's `fields`, or one
|
|
13
|
+
* search result, exactly as the API returns it.
|
|
14
|
+
*/
|
|
15
|
+
type RecordsTableRow = Record<string, RecordsTableCell | null | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* The column metadata the table reads. Pipes field definitions
|
|
18
|
+
* (`PipesResponse.field_definitions`) and search field annotations
|
|
19
|
+
* (`SearchResponse.field_definitions`) both fit as they are.
|
|
20
|
+
*/
|
|
21
|
+
interface RecordsTableColumn {
|
|
22
|
+
label?: string | null;
|
|
23
|
+
type?: RecordField["type"];
|
|
24
|
+
format?: RecordField["format"];
|
|
25
|
+
order?: number | null;
|
|
26
|
+
/** Input columns get no outcome count; pipe and search outputs do. */
|
|
27
|
+
added_by?: {
|
|
28
|
+
ref: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface RecordsTableProps {
|
|
32
|
+
rows: RecordsTableRow[];
|
|
33
|
+
fields?: Record<string, RecordsTableColumn>;
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Rewrites image URLs (avatars, logos, favicons), e.g. through an image proxy. */
|
|
36
|
+
imageUrl?: (src: string) => string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read-only table of a run's native rows: one column per field, each cell
|
|
40
|
+
* rendered the way a pipe0 Sheet renders it, with its status. Empty cells
|
|
41
|
+
* show a muted dash; failed cells keep their reason.
|
|
42
|
+
*/
|
|
43
|
+
declare function RecordsTable({
|
|
44
|
+
rows,
|
|
45
|
+
fields,
|
|
46
|
+
imageUrl,
|
|
47
|
+
className
|
|
48
|
+
}: RecordsTableProps): _$react_jsx_runtime0.JSX.Element;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { RecordsTable, RecordsTableCell, RecordsTableColumn, RecordsTableProps, RecordsTableRow };
|
|
51
|
+
//# sourceMappingURL=records-table.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"records-table.d.mts","names":[],"sources":["../../src/records/records-table.tsx"],"mappings":";;;;;;;AAcA;;KAAY,gBAAA,GAAmB,IAAA,CAAK,WAAA,oCAClC,OAAA,CAAQ,IAAA,CAAK,WAAA;;;;;KAMH,eAAA,GAAkB,MAAA,SAAe,gBAAA;;;;;;UAO5B,kBAAA;EACf,KAAA;EACA,IAAA,GAAO,WAAA;EACP,MAAA,GAAS,WAAA;EACT,KAAA;EAXyB;EAazB,QAAA;IAAa,GAAA;EAAA;AAAA;AAAA,UAGE,iBAAA;EACf,IAAA,EAAM,eAAA;EACN,MAAA,GAAS,MAAA,SAAe,kBAAA;EACxB,SAAA;EAVA;EAYA,QAAA,IAAY,GAAA;AAAA;;;;;;iBAuHE,YAAA,CAAA;EAAe,IAAA;EAAM,MAAA;EAAa,QAAA;EAAU;AAAA,GAAa,iBAAA,GAAiB,oBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.mjs";
|
|
2
|
+
import { Check, Copy } from "../components/internal/icons.mjs";
|
|
3
|
+
import { getFieldFormatIcon } from "../utils/field-icons.mjs";
|
|
4
|
+
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table.mjs";
|
|
5
|
+
import { RecordImageUrlContext, StateTile } from "./record-widgets.mjs";
|
|
6
|
+
import { RecordValue } from "./record-value.mjs";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
|
|
10
|
+
//#region src/records/records-table.tsx
|
|
11
|
+
/** Declared columns by their display order, then any field only the rows carry. `id` is never shown. */
|
|
12
|
+
function columnNames(fields, rows) {
|
|
13
|
+
const declared = Object.entries(fields).map(([name, column], index) => ({
|
|
14
|
+
name,
|
|
15
|
+
rank: column.order ?? Number.MAX_SAFE_INTEGER,
|
|
16
|
+
index
|
|
17
|
+
})).sort((a, b) => a.rank - b.rank || a.index - b.index).map(({ name }) => name);
|
|
18
|
+
const names = new Set(declared);
|
|
19
|
+
for (const row of rows) for (const name of Object.keys(row)) names.add(name);
|
|
20
|
+
names.delete("id");
|
|
21
|
+
return [...names];
|
|
22
|
+
}
|
|
23
|
+
function tallyColumn(rows, name) {
|
|
24
|
+
const tally = {
|
|
25
|
+
completed: 0,
|
|
26
|
+
no_result: 0,
|
|
27
|
+
failed: 0,
|
|
28
|
+
total: 0
|
|
29
|
+
};
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
const status = row[name]?.status;
|
|
32
|
+
if (!status) continue;
|
|
33
|
+
tally.total += 1;
|
|
34
|
+
if (status === "completed") tally.completed += 1;
|
|
35
|
+
else if (status === "no_result") tally.no_result += 1;
|
|
36
|
+
else if (status === "failed") tally.failed += 1;
|
|
37
|
+
}
|
|
38
|
+
return tally;
|
|
39
|
+
}
|
|
40
|
+
function isEmpty(cell) {
|
|
41
|
+
return !cell || cell.status === "completed" && (cell.value == null || cell.value === "");
|
|
42
|
+
}
|
|
43
|
+
/** What a cell's copy button puts on the clipboard; null = no button (empty or trivial values). */
|
|
44
|
+
function copyableText(value) {
|
|
45
|
+
if (value === null || value === "" || typeof value === "boolean") return null;
|
|
46
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
async function writeClipboard(text) {
|
|
50
|
+
try {
|
|
51
|
+
await navigator.clipboard.writeText(text);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
const area = document.createElement("textarea");
|
|
55
|
+
area.value = text;
|
|
56
|
+
area.setAttribute("readonly", "");
|
|
57
|
+
area.style.position = "fixed";
|
|
58
|
+
area.style.opacity = "0";
|
|
59
|
+
document.body.appendChild(area);
|
|
60
|
+
area.select();
|
|
61
|
+
const copied = document.execCommand("copy");
|
|
62
|
+
area.remove();
|
|
63
|
+
return copied;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function CopyCellButton({ text }) {
|
|
67
|
+
const [copied, setCopied] = useState(false);
|
|
68
|
+
return /* @__PURE__ */ jsx("button", {
|
|
69
|
+
type: "button",
|
|
70
|
+
"aria-label": "Copy value",
|
|
71
|
+
title: copied ? "Copied" : "Copy",
|
|
72
|
+
onClick: async (e) => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
if (!await writeClipboard(text)) return;
|
|
75
|
+
setCopied(true);
|
|
76
|
+
setTimeout(() => setCopied(false), 1500);
|
|
77
|
+
},
|
|
78
|
+
className: cn("pz:absolute pz:top-1/2 pz:right-1.5 pz:-translate-y-1/2 pz:inline-flex pz:size-6 pz:items-center pz:justify-center pz:rounded-md", "pz:border pz:border-border pz:bg-background pz:text-muted-foreground pz:shadow-xs pz:hover:text-foreground", "pz:opacity-0 pz:transition-opacity pz:group-hover/cell:opacity-100 pz:focus-visible:opacity-100", copied && "pz:opacity-100"),
|
|
79
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "pz:size-3.5" }) : /* @__PURE__ */ jsx(Copy, { className: "pz:size-3.5" })
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function TallyBadge({ tally }) {
|
|
83
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
84
|
+
title: [
|
|
85
|
+
`${tally.completed} completed`,
|
|
86
|
+
tally.no_result ? `${tally.no_result} no result` : null,
|
|
87
|
+
tally.failed ? `${tally.failed} failed` : null
|
|
88
|
+
].filter(Boolean).join(" · "),
|
|
89
|
+
className: cn("pz:ml-1.5 pz:rounded pz:px-1 pz:font-normal pz:text-[11px] pz:tabular-nums", tally.failed > 0 ? "pz:bg-failed pz:text-failed-foreground" : "pz:text-muted-foreground"),
|
|
90
|
+
children: [
|
|
91
|
+
tally.completed,
|
|
92
|
+
"/",
|
|
93
|
+
tally.total
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read-only table of a run's native rows: one column per field, each cell
|
|
99
|
+
* rendered the way a pipe0 Sheet renders it, with its status. Empty cells
|
|
100
|
+
* show a muted dash; failed cells keep their reason.
|
|
101
|
+
*/
|
|
102
|
+
function RecordsTable({ rows, fields = {}, imageUrl, className }) {
|
|
103
|
+
const names = columnNames(fields, rows);
|
|
104
|
+
const table = /* @__PURE__ */ jsx("div", {
|
|
105
|
+
"data-p0": "records-table",
|
|
106
|
+
className: cn("pz:overflow-auto pz:font-sans pz:text-foreground pz:text-sm pz:antialiased", className),
|
|
107
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
108
|
+
"data-slot": "table",
|
|
109
|
+
className: "pz:w-full pz:border-separate pz:border-spacing-0 pz:caption-bottom",
|
|
110
|
+
children: [/* @__PURE__ */ jsx(TableHeader, {
|
|
111
|
+
className: "pz:sticky pz:top-0 pz:z-10 pz:bg-muted",
|
|
112
|
+
children: /* @__PURE__ */ jsx(TableRow, {
|
|
113
|
+
className: "pz:hover:bg-transparent",
|
|
114
|
+
children: names.map((name) => {
|
|
115
|
+
const column = fields[name];
|
|
116
|
+
const sample = rows.find((row) => row[name])?.[name];
|
|
117
|
+
const Icon = getFieldFormatIcon(sample?.format ?? column?.format ?? null, sample?.type ?? column?.type ?? "unknown");
|
|
118
|
+
const tally = column?.added_by?.ref === "input" ? null : tallyColumn(rows, name);
|
|
119
|
+
return /* @__PURE__ */ jsx(TableHead, {
|
|
120
|
+
className: "pz:h-8 pz:border-b pz:border-border pz:px-2.5 pz:text-xs pz:text-muted-foreground",
|
|
121
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
122
|
+
className: "pz:inline-flex pz:items-center pz:gap-1.5",
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsx(Icon, { className: "pz:size-3.5 pz:shrink-0" }),
|
|
125
|
+
/* @__PURE__ */ jsx("span", { children: column?.label || name }),
|
|
126
|
+
tally && tally.total > 0 && /* @__PURE__ */ jsx(TallyBadge, { tally })
|
|
127
|
+
]
|
|
128
|
+
})
|
|
129
|
+
}, name);
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
}), /* @__PURE__ */ jsx(TableBody, { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx(TableRow, {
|
|
133
|
+
className: "pz:hover:bg-muted/50",
|
|
134
|
+
children: names.map((name) => {
|
|
135
|
+
const cell = row[name];
|
|
136
|
+
const copyText = cell && cell.status === "completed" ? copyableText(cell.value) : null;
|
|
137
|
+
return /* @__PURE__ */ jsxs(TableCell, {
|
|
138
|
+
className: "pz:group/cell pz:relative pz:h-10 pz:border-b pz:border-border pz:px-2.5 pz:py-0",
|
|
139
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
140
|
+
className: cn("pz:flex pz:h-full pz:items-center pz:gap-2 pz:whitespace-nowrap", (cell?.status === "no_result" || cell?.status === "skipped") && "pz:text-muted-foreground pz:opacity-60"),
|
|
141
|
+
title: cell?.status === "failed" ? cell.reason?.message ?? void 0 : void 0,
|
|
142
|
+
children: !cell || isEmpty(cell) ? /* @__PURE__ */ jsx("span", {
|
|
143
|
+
className: "pz:text-muted-foreground/60",
|
|
144
|
+
children: "—"
|
|
145
|
+
}) : /* @__PURE__ */ jsx(RecordValue, {
|
|
146
|
+
field: {
|
|
147
|
+
...cell,
|
|
148
|
+
type: cell.type ?? fields[name]?.type ?? "unknown",
|
|
149
|
+
format: cell.format ?? fields[name]?.format ?? null,
|
|
150
|
+
reason: cell.reason ?? null
|
|
151
|
+
},
|
|
152
|
+
afterGlyphs: cell.status === "failed" && /* @__PURE__ */ jsx(StateTile, { state: "failed" })
|
|
153
|
+
})
|
|
154
|
+
}), copyText !== null && /* @__PURE__ */ jsx(CopyCellButton, { text: copyText })]
|
|
155
|
+
}, name);
|
|
156
|
+
})
|
|
157
|
+
}, String(row.id?.value ?? rowIndex))) })]
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
return imageUrl ? /* @__PURE__ */ jsx(RecordImageUrlContext.Provider, {
|
|
161
|
+
value: imageUrl,
|
|
162
|
+
children: table
|
|
163
|
+
}) : table;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { RecordsTable };
|
|
168
|
+
//# sourceMappingURL=records-table.mjs.map
|