[Haik]: pt3 of going thru knip errors

This commit is contained in:
haikdc
2026-03-30 23:51:05 -07:00
parent 0dd56bb599
commit 30a0dc5a6f
23 changed files with 75 additions and 725 deletions
@@ -1,11 +1,5 @@
export { cn } from "@/lib/utils";
export { Button } from "@/components/ui/button";
export {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export {
Accordion,
AccordionContent,
@@ -35,7 +35,7 @@ import type {
} from "./types";
import type { FormatConfig } from "./formatters";
export const DEFAULT_LOCALE = "en-US" as const;
const DEFAULT_LOCALE = "en-US" as const;
function isNumericFormat(format?: FormatConfig): boolean {
const kind = format?.kind;
@@ -60,8 +60,8 @@ const DataTableContext = React.createContext<
DataTableContextValue<any> | undefined
>(undefined);
export function useDataTable<T extends object = RowData>() {
const context = React.use(DataTableContext) as
function useDataTable<T extends object = RowData>() {
const context = React.useContext(DataTableContext) as
| DataTableContextValue<T>
| undefined;
if (!context) {
@@ -2,7 +2,48 @@
import * as React from "react";
import { cn, Badge, Tooltip, TooltipContent, TooltipTrigger } from "./_adapter";
import { resolveSafeNavigationHref } from "../shared/media";
function sanitizeHref(href?: string): string | undefined {
if (!href) return undefined;
const candidate = href.trim();
if (!candidate) return undefined;
if (
candidate.startsWith("/") ||
candidate.startsWith("./") ||
candidate.startsWith("../") ||
candidate.startsWith("?") ||
candidate.startsWith("#")
) {
if (candidate.startsWith("//")) return undefined;
// eslint-disable-next-line no-control-regex -- intentionally matching control characters
if (/[\u0000-\u001F\u007F]/.test(candidate)) return undefined;
return candidate;
}
try {
const url = new URL(candidate);
if (url.protocol === "http:" || url.protocol === "https:") {
return url.toString();
}
} catch {
return undefined;
}
return undefined;
}
function resolveSafeNavigationHref(
...candidates: Array<string | null | undefined>
): string | undefined {
for (const candidate of candidates) {
const safeHref = sanitizeHref(candidate ?? undefined);
if (safeHref) {
return safeHref;
}
}
return undefined;
}
type Tone = "success" | "warning" | "danger" | "info" | "neutral";
@@ -44,7 +85,7 @@ interface DeltaValueProps {
locale?: string;
}
export function DeltaValue({ value, options, locale }: DeltaValueProps) {
function DeltaValue({ value, options, locale }: DeltaValueProps) {
const decimals = options?.decimals ?? 2;
const upIsPositive = options?.upIsPositive ?? true;
const showSign = options?.showSign ?? true;
@@ -90,7 +131,7 @@ interface StatusBadgeProps {
options?: Extract<FormatConfig, { kind: "status" }>;
}
export function StatusBadge({ value, options }: StatusBadgeProps) {
function StatusBadge({ value, options }: StatusBadgeProps) {
const config = options?.statusMap?.[value] ?? {
tone: "neutral" as Tone,
label: value,
@@ -130,7 +171,7 @@ interface CurrencyValueProps {
locale?: string;
}
export function CurrencyValue({ value, options, locale }: CurrencyValueProps) {
function CurrencyValue({ value, options, locale }: CurrencyValueProps) {
const currency = options?.currency ?? "USD";
const decimals = options?.decimals ?? 2;
@@ -150,7 +191,7 @@ interface PercentValueProps {
locale?: string;
}
export function PercentValue({ value, options, locale }: PercentValueProps) {
function PercentValue({ value, options, locale }: PercentValueProps) {
const decimals = options?.decimals ?? 2;
const showSign = options?.showSign ?? false;
const basis = options?.basis ?? "fraction";
@@ -173,7 +214,7 @@ interface DateValueProps {
locale?: string;
}
export function DateValue({ value, options, locale }: DateValueProps) {
function DateValue({ value, options, locale }: DateValueProps) {
const dateFormat = options?.dateFormat ?? "short";
const date = new Date(value);
@@ -248,7 +289,7 @@ interface BooleanValueProps {
options?: Extract<FormatConfig, { kind: "boolean" }>;
}
export function BooleanValue({ value, options }: BooleanValueProps) {
function BooleanValue({ value, options }: BooleanValueProps) {
const labels = options?.labels ?? { true: "Yes", false: "No" };
const label = value ? labels.true : labels.false;
const variant = value ? "secondary" : "outline";
@@ -265,7 +306,7 @@ interface LinkValueProps {
>;
}
export function LinkValue({ value, options, row }: LinkValueProps) {
function LinkValue({ value, options, row }: LinkValueProps) {
const rawHref =
options?.hrefKey && row ? String(row[options.hrefKey] ?? "") : value;
const href = resolveSafeNavigationHref(rawHref);
@@ -300,7 +341,7 @@ interface NumberValueProps {
locale?: string;
}
export function NumberValue({ value, options, locale }: NumberValueProps) {
function NumberValue({ value, options, locale }: NumberValueProps) {
const decimals = options?.decimals ?? 0;
const unit = options?.unit ?? "";
const compact = options?.compact ?? false;
@@ -327,7 +368,7 @@ interface BadgeValueProps {
options?: Extract<FormatConfig, { kind: "badge" }>;
}
export function BadgeValue({ value, options }: BadgeValueProps) {
function BadgeValue({ value, options }: BadgeValueProps) {
const tone = options?.colorMap?.[value] ?? "neutral";
const variant =
@@ -362,7 +403,7 @@ interface ArrayValueProps {
options?: Extract<FormatConfig, { kind: "array" }>;
}
export function ArrayValue({ value, options }: ArrayValueProps) {
function ArrayValue({ value, options }: ArrayValueProps) {
const maxVisible = options?.maxVisible ?? 3;
const items: (string | number | boolean | null)[] = Array.isArray(value)
? value
@@ -1,29 +0,0 @@
export { DataTable, useDataTable } from "./data-table";
export { renderFormattedValue } from "./formatters";
export {
NumberValue,
CurrencyValue,
PercentValue,
DeltaValue,
DateValue,
BooleanValue,
LinkValue,
BadgeValue,
StatusBadge,
ArrayValue,
} from "./formatters";
export type {
Column,
DataTableProps,
DataTableSerializableProps,
DataTableClientProps,
DataTableRowData,
RowPrimitive,
RowData,
ColumnKey,
} from "./types";
export type { FormatConfig } from "./formatters";
export { sortData, parseNumericLike } from "./utilities";
@@ -1,345 +0,0 @@
import { z } from "zod";
import {
ToolUIIdSchema,
ToolUIReceiptSchema,
ToolUIRoleSchema,
} from "../shared/schema";
import { defineToolUiContract } from "../shared/contract";
import type { Column, DataTableProps, RowData } from "./types";
const AlignEnum = z.enum(["left", "right", "center"]);
const PriorityEnum = z.enum(["primary", "secondary", "tertiary"]);
const formatSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("text") }),
z.object({
kind: z.literal("number"),
decimals: z.number().optional(),
unit: z.string().optional(),
compact: z.boolean().optional(),
showSign: z.boolean().optional(),
}),
z.object({
kind: z.literal("currency"),
currency: z.string(),
decimals: z.number().optional(),
}),
z.object({
kind: z.literal("percent"),
decimals: z.number().optional(),
showSign: z.boolean().optional(),
basis: z.enum(["fraction", "unit"]).optional(),
}),
z.object({
kind: z.literal("date"),
dateFormat: z.enum(["short", "long", "relative"]).optional(),
}),
z.object({
kind: z.literal("delta"),
decimals: z.number().optional(),
upIsPositive: z.boolean().optional(),
showSign: z.boolean().optional(),
}),
z.object({
kind: z.literal("status"),
statusMap: z.record(
z.string(),
z.object({
tone: z.enum(["success", "warning", "danger", "info", "neutral"]),
label: z.string().optional(),
}),
),
}),
z.object({
kind: z.literal("boolean"),
labels: z
.object({
true: z.string(),
false: z.string(),
})
.optional(),
}),
z.object({
kind: z.literal("link"),
hrefKey: z.string().optional(),
external: z.boolean().optional(),
}),
z.object({
kind: z.literal("badge"),
colorMap: z
.record(
z.string(),
z.enum(["success", "warning", "danger", "info", "neutral"]),
)
.optional(),
}),
z.object({
kind: z.literal("array"),
maxVisible: z.number().optional(),
}),
]);
export const serializableColumnSchema = z.object({
key: z.string(),
label: z.string(),
abbr: z.string().optional(),
sortable: z.boolean().optional(),
align: AlignEnum.optional(),
width: z.string().optional(),
truncate: z.boolean().optional(),
priority: PriorityEnum.optional(),
hideOnMobile: z.boolean().optional(),
format: formatSchema.optional(),
});
const JsonPrimitiveSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
]);
/**
* Schema for serializable row data.
*
* Supports:
* - Primitives: string, number, boolean, null
* - Arrays of primitives: string[], number[], boolean[], or mixed primitive arrays
*
* Does NOT support:
* - Functions
* - Class instances (Date, Map, Set, etc.)
* - Plain objects (use format configs instead)
*
* @example
* Valid row data:
* ```json
* {
* "name": "Widget",
* "price": 29.99,
* "active": true,
* "tags": ["electronics", "featured"],
* "metrics": [1.2, 3.4, 5.6],
* "flags": [true, false, true],
* "mixed": ["label", 42, true]
* }
* ```
*/
export const serializableDataSchema = z.record(
z.string(),
z.union([JsonPrimitiveSchema, z.array(JsonPrimitiveSchema)]),
);
/**
* Zod schema for validating DataTable payloads from LLM tool calls.
*
* This schema validates the serializable parts of a DataTable:
* - id: Unique identifier for this tool UI in the conversation
* - columns: Column definitions (keys, labels, formatting, etc.)
* - data: Data rows (primitives only - no functions or class instances)
* - optional presentation props: rowIdKey, sort/defaultSort, locale, etc.
*
* Non-serializable props like `onSortChange`, `className`, and sibling action surfaces
* must be provided separately in your React component.
*
* @example
* ```ts
* const result = SerializableDataTableSchema.safeParse(llmResponse)
* if (result.success) {
* // result.data contains validated id, columns, and data
* }
* ```
*/
export const SerializableDataTableSchema = z.object({
id: ToolUIIdSchema,
role: ToolUIRoleSchema.optional(),
receipt: ToolUIReceiptSchema.optional(),
columns: z.array(serializableColumnSchema),
data: z.array(serializableDataSchema),
rowIdKey: z.string().optional(),
defaultSort: z
.object({
by: z.string().optional(),
direction: z.enum(["asc", "desc"]).optional(),
})
.optional(),
sort: z
.object({
by: z.string().optional(),
direction: z.enum(["asc", "desc"]).optional(),
})
.optional(),
emptyMessage: z.string().optional(),
maxHeight: z.string().optional(),
locale: z.string().optional(),
});
const SerializableDataTableSchemaContract = defineToolUiContract(
"DataTable",
SerializableDataTableSchema,
);
/**
* Type representing the serializable parts of a DataTable payload.
*
* This type includes only JSON-serializable data that can come from LLM tool calls:
* - Column definitions (format configs, alignment, labels, etc.)
* - Row data (primitives: strings, numbers, booleans, null, string arrays)
*
* Excluded from this type:
* - Event handlers (`onSortChange`)
* - React-specific props (`className`)
*
* @example
* ```ts
* const payload: SerializableDataTable = {
* id: "data-table-expenses",
* columns: [
* { key: "name", label: "Name" },
* { key: "price", label: "Price", format: { kind: "currency", currency: "USD" } }
* ],
* data: [
* { name: "Widget", price: 29.99 }
* ]
* }
* ```
*/
export type SerializableDataTable = z.infer<typeof SerializableDataTableSchema>;
/**
* Validates and parses a DataTable payload from unknown data (e.g., LLM tool call result).
*
* This function:
* 1. Validates the input against the `SerializableDataTableSchema`
* 2. Throws a descriptive error if validation fails
* 3. Returns typed serializable props ready to pass to the `<DataTable>` component
*
* The returned props are **serializable only** - you must provide client-side props
* separately (onSortChange, className).
*
* @param input - Unknown data to validate (typically from an LLM tool call)
* @returns Validated and typed DataTable serializable props (id, columns, data)
* @throws Error with validation details if input is invalid
*
* @example
* ```tsx
* function MyToolUI({ result }: { result: unknown }) {
* const serializableProps = parseSerializableDataTable(result)
*
* return (
* <DataTable
* {...serializableProps}
* />
* )
* }
* ```
*/
export function parseSerializableDataTable(
input: unknown,
): Pick<
DataTableProps<RowData>,
| "id"
| "role"
| "receipt"
| "columns"
| "data"
| "rowIdKey"
| "defaultSort"
| "sort"
| "emptyMessage"
| "maxHeight"
| "locale"
> {
const {
id,
role,
receipt,
columns,
data,
rowIdKey,
defaultSort,
sort,
emptyMessage,
maxHeight,
locale,
} = SerializableDataTableSchemaContract.parse(input);
return {
id,
role,
receipt,
columns: columns as unknown as Column<RowData>[],
data: data as RowData[],
rowIdKey: rowIdKey as keyof RowData | undefined,
defaultSort: defaultSort
? {
by: defaultSort.by as keyof RowData | undefined,
direction: defaultSort.direction,
}
: undefined,
sort: sort
? {
by: sort.by as keyof RowData | undefined,
direction: sort.direction,
}
: undefined,
emptyMessage,
maxHeight,
locale,
};
}
export function safeParseSerializableDataTable(
input: unknown,
): Pick<
DataTableProps<RowData>,
| "id"
| "role"
| "receipt"
| "columns"
| "data"
| "rowIdKey"
| "defaultSort"
| "sort"
| "emptyMessage"
| "maxHeight"
| "locale"
> | null {
const res = SerializableDataTableSchemaContract.safeParse(input);
if (!res) return null;
const {
id,
role,
receipt,
columns,
data,
rowIdKey,
defaultSort,
sort,
emptyMessage,
maxHeight,
locale,
} = res;
return {
id,
role,
receipt,
columns: columns as unknown as Column<RowData>[],
data: data as RowData[],
rowIdKey: rowIdKey as keyof RowData | undefined,
defaultSort: defaultSort
? {
by: defaultSort.by as keyof RowData | undefined,
direction: defaultSort.direction,
}
: undefined,
sort: sort
? {
by: sort.by as keyof RowData | undefined,
direction: sort.direction,
}
: undefined,
emptyMessage,
maxHeight,
locale,
};
}
@@ -69,7 +69,7 @@ export function sortData<T, K extends Extract<keyof T, string>>(
* Accepts any JSON-serializable primitive or array of primitives.
* Arrays are converted to comma-separated strings.
*/
export function getRowIdentifier(
function getRowIdentifier(
row: Record<
string,
string | number | boolean | null | (string | number | boolean | null)[]
@@ -210,7 +210,7 @@ export function getDataTableMobileDescriptionId(surfaceId: string): string {
* parseNumericLike("50%") // 50
* parseNumericLike("(1234)") // -1234
*/
export function parseNumericLike(input: string): number | null {
function parseNumericLike(input: string): number | null {
// Normalize whitespace (spaces, NBSPs, thin spaces)
let s = input.replace(/[\u00A0\u202F\s]/g, "").trim();
if (!s) return null;
@@ -1,27 +0,0 @@
import { z } from "zod";
export const AspectRatioSchema = z
.enum(["auto", "1:1", "4:3", "16:9", "9:16"])
.default("auto");
export type AspectRatio = z.infer<typeof AspectRatioSchema>;
export const MediaFitSchema = z.enum(["cover", "contain"]).default("cover");
export type MediaFit = z.infer<typeof MediaFitSchema>;
export const RATIO_CLASS_MAP: Record<AspectRatio, string> = {
auto: "",
"1:1": "aspect-square",
"4:3": "aspect-[4/3]",
"16:9": "aspect-video",
"9:16": "aspect-[9/16]",
};
export function getRatioClass(ratio: AspectRatio): string {
return RATIO_CLASS_MAP[ratio];
}
export function getFitClass(fit: MediaFit): string {
return fit === "cover" ? "object-cover" : "object-contain";
}
@@ -1,30 +0,0 @@
export function formatDuration(durationMs: number): string {
const totalSeconds = Math.round(durationMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}`;
}
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
/**
* Format file size in bytes to human-readable string.
* @example formatFileSize(1024) => "1 KB"
* @example formatFileSize(1536000) => "1.5 MB"
*/
export function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
const units = ["KB", "MB", "GB"];
let size = bytes / 1024;
let unit = 0;
while (size >= 1024 && unit < units.length - 1) {
size /= 1024;
unit += 1;
}
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
}
@@ -1,19 +0,0 @@
export {
AspectRatioSchema,
MediaFitSchema,
RATIO_CLASS_MAP,
getRatioClass,
getFitClass,
type AspectRatio,
type MediaFit,
} from "./aspect-ratio";
export { OVERLAY_GRADIENT } from "./overlay-gradient";
export { formatDuration, formatFileSize } from "./format-utils";
export { sanitizeHref } from "./sanitize-href";
export {
resolveSafeNavigationHref,
openSafeNavigationHref,
} from "./safe-navigation";
@@ -1,19 +0,0 @@
export const OVERLAY_GRADIENT = `linear-gradient(
to bottom,
hsl(0, 0%, 0%) 0%,
hsla(0, 0%, 0%, 0.987) 8.3%,
hsla(0, 0%, 0%, 0.951) 16.6%,
hsla(0, 0%, 0%, 0.896) 24.6%,
hsla(0, 0%, 0%, 0.825) 32.5%,
hsla(0, 0%, 0%, 0.741) 40.1%,
hsla(0, 0%, 0%, 0.648) 47.6%,
hsla(0, 0%, 0%, 0.55) 54.8%,
hsla(0, 0%, 0%, 0.45) 61.7%,
hsla(0, 0%, 0%, 0.352) 68.3%,
hsla(0, 0%, 0%, 0.259) 74.5%,
hsla(0, 0%, 0%, 0.175) 80.4%,
hsla(0, 0%, 0%, 0.104) 86%,
hsla(0, 0%, 0%, 0.049) 91.1%,
hsla(0, 0%, 0%, 0.013) 95.8%,
hsla(0, 0%, 0%, 0) 100%
)` as const;
@@ -1,23 +0,0 @@
import { sanitizeHref } from "./sanitize-href";
export function resolveSafeNavigationHref(
...candidates: Array<string | null | undefined>
): string | undefined {
for (const candidate of candidates) {
const safeHref = sanitizeHref(candidate ?? undefined);
if (safeHref) {
return safeHref;
}
}
return undefined;
}
export function openSafeNavigationHref(href: string | undefined): boolean {
if (!href || typeof window === "undefined") {
return false;
}
window.open(href, "_blank", "noopener,noreferrer");
return true;
}
@@ -1,28 +0,0 @@
export function sanitizeHref(href?: string): string | undefined {
if (!href) return undefined;
const candidate = href.trim();
if (!candidate) return undefined;
if (
candidate.startsWith("/") ||
candidate.startsWith("./") ||
candidate.startsWith("../") ||
candidate.startsWith("?") ||
candidate.startsWith("#")
) {
if (candidate.startsWith("//")) return undefined;
// eslint-disable-next-line no-control-regex -- intentionally matching control characters
if (/[\u0000-\u001F\u007F]/.test(candidate)) return undefined;
return candidate;
}
try {
const url = new URL(candidate);
if (url.protocol === "http:" || url.protocol === "https:") {
return url.toString();
}
} catch {
return undefined;
}
return undefined;
}
-63
View File
@@ -3,7 +3,6 @@ import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
function Dialog({
...props
@@ -23,12 +22,6 @@ function DialogPortal({
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
@@ -79,43 +72,6 @@ function DialogContent({
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
@@ -129,28 +85,9 @@ function DialogTitle({
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}