{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-browser",
  "title": "DataBrowser",
  "description": "Render static row snapshots or host-backed data browsing with local search, filters, sort, row details, pagination, and row actions.",
  "dependencies": [
    "ai",
    "zod",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "card",
    "button",
    "badge",
    "input",
    "dialog",
    "drawer",
    "select",
    "shobky/fable-ui/core"
  ],
  "files": [
    {
      "path": "components/fable-ui/data-browser/index.ts",
      "content": "export * from \"./data-browser\"\nexport * from \"./data-browser.types\"\nexport * from \"./resolve-intent\"\nexport * from \"./show-table\"\nexport * from \"@/lib/fable-ui/tools/show-data-browser-tool\"\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/index.ts"
    },
    {
      "path": "lib/fable-ui/tools/show-data-browser-tool.ts",
      "content": "import { lazy } from \"react\"\nimport { tool } from \"ai\"\nimport { z } from \"zod\"\n\nimport {\n  defineFableComponent,\n  fableRegistry,\n  type DataSourceRegistry,\n} from \"@/lib/fable-ui/core\"\nimport {\n  dataColumnSchema,\n  dataFilterSchema,\n  dataSortSchema,\n  sortStateSchema,\n} from \"@/lib/fable-ui/core/schemas\"\nimport { resolveDataBrowserIntent } from \"@/components/fable-ui/data-browser/resolve-intent\"\n\nconst DataBrowser = lazy(() =>\n  import(\"@/components/fable-ui/data-browser/data-browser\").then((module) => ({\n    default: module.DataBrowser,\n  })),\n)\nconst ShowTable = lazy(() =>\n  import(\"@/components/fable-ui/data-browser/show-table\").then((module) => ({\n    default: module.ShowTable,\n  })),\n)\n\nconst rowSchema = z.record(z.string(), z.unknown())\n\nexport const showTableInputSchema = z.object({\n  title: z.string().min(1),\n  description: z.string().optional(),\n  columns: z.array(dataColumnSchema).min(1).max(12),\n  rows: z.array(rowSchema).max(200),\n  pageSize: z.number().int().positive().max(100).optional(),\n})\n\nexport type ShowTableInput = z.infer<typeof showTableInputSchema>\n\nexport const showDataBrowserInputSchema = z.object({\n  title: z.string().min(1),\n  entityLabel: z.string().min(1).optional(),\n  description: z.string().optional(),\n  resourceId: z\n    .string()\n    .min(1)\n    .optional()\n    .describe(\n      \"Use only a resource id from fableRegistry.getAgentResourceManifest(). Never invent resource ids.\"\n    ),\n  searchPlaceholder: z.string().optional(),\n  columns: z.array(dataColumnSchema).optional(),\n  rows: z.array(rowSchema).max(200).optional(),\n  filters: z.array(dataFilterSchema).optional(),\n  sortOptions: z.array(dataSortSchema).optional(),\n  initialFilters: z.record(z.string(), z.unknown()).optional(),\n  initialSort: sortStateSchema.optional(),\n  initialSearch: z.string().optional(),\n  visibleColumns: z.array(z.string().min(1)).optional(),\n  detail: z\n    .object({\n      title: z.string().optional(),\n      description: z.string().optional(),\n      fields: z.array(z.string()).optional(),\n    })\n    .optional(),\n  pageSize: z.number().int().positive().max(100).optional(),\n})\n\nexport type ShowDataBrowserInput = z.infer<typeof showDataBrowserInputSchema>\n\nexport function describeAvailableResources(\n  registry: DataSourceRegistry = fableRegistry\n) {\n  return JSON.stringify(registry.getAgentResourceManifest(), null, 2)\n}\n\nexport function createShowTableTool() {\n  return tool({\n    description:\n      \"Show a static table snapshot when display-ready rows are already available. Supports up to 200 rows with pagination. Do not use for raw SQL, raw Firestore paths, secrets, or authorization decisions.\",\n    inputSchema: showTableInputSchema,\n    execute: async (input) => input,\n  })\n}\n\nexport const showTableTool = createShowTableTool()\n\nexport function createShowDataBrowserTool() {\n  return tool({\n    description: [\n      \"Show a trusted data browser for search, filters, sort, pagination, or row detail.\",\n      \"Use resourceId only from fableRegistry.getAgentResourceManifest(); never invent resource IDs.\",\n      \"Never pass raw SQL, raw Firestore paths, raw collection names, secrets, or authorization decisions.\",\n      \"The host owns data access, permissions, validation, allowed filters, allowed sorts, and row actions.\",\n      \"If no resourceId is available, include safe static rows and columns only when the data is already present.\",\n    ].join(\" \"),\n    inputSchema: showDataBrowserInputSchema,\n    execute: async (input) => input,\n  })\n}\n\nexport const showDataBrowserTool = createShowDataBrowserTool()\n\nexport const showTable = defineFableComponent({\n  name: \"show_table\",\n  schema: showTableInputSchema,\n  tool: showTableTool,\n  renderer: {\n    Component: ShowTable,\n    loadingProps: { title: \"Table\", columns: [], rows: [], isLoading: true },\n    emptyProps: { title: \"Table\", columns: [], rows: [] },\n    errorProps: (description: string) => ({\n      title: \"Table unavailable\",\n      columns: [],\n      rows: [],\n      error: { title: \"Table unavailable\", description },\n    }),\n    toProps: (data: ShowTableInput) => ({ ...data }),\n  },\n})\n\nexport const showDataBrowser = defineFableComponent({\n  name: \"show_data_browser\",\n  schema: showDataBrowserInputSchema,\n  tool: showDataBrowserTool,\n  renderer: {\n    Component: DataBrowser,\n    loadingProps: {\n      title: \"Data browser\",\n      entityLabel: \"rows\",\n      columns: [],\n      rows: [],\n      isLoading: true,\n    },\n    emptyProps: {\n      title: \"Data browser\",\n      entityLabel: \"rows\",\n      columns: [],\n      rows: [],\n    },\n    errorProps: (description: string) => ({\n      title: \"Data browser unavailable\",\n      entityLabel: \"rows\",\n      columns: [],\n      rows: [],\n      error: { title: \"Data browser unavailable\", description },\n    }),\n    toProps: (data: ShowDataBrowserInput) => resolveDataBrowserIntent(data),\n  },\n})\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/tools/show-data-browser-tool.ts"
    },
    {
      "path": "lib/fable-ui/tools/show-table-tool.ts",
      "content": "export {\n  createShowTableTool,\n  showTable,\n  showTableTool,\n  showTableInputSchema,\n  type ShowTableInput,\n} from \"./show-data-browser-tool\"\n",
      "type": "registry:lib",
      "target": "@lib/fable-ui/tools/show-table-tool.ts"
    },
    {
      "path": "lib/fable-ui/manifests/show-table.md",
      "content": "---\ntool: show_table\ntype: registry:block\n---\n\n# show_table\n\nUse for static snapshots where display-ready rows are already available. Supports up to 200 rows with pagination; set an explicit page size for larger payloads.\n\nAvoid `show_table` when the user needs browsing, filtering, sorting, details, row actions, or live data access. Use `show_data_browser` for those cases.\n",
      "type": "registry:file",
      "target": "@lib/fable-ui/manifests/show-table.md"
    },
    {
      "path": "components/fable-ui/data-browser/data-browser.tsx",
      "content": "\"use client\"\n\nimport { cva } from \"class-variance-authority\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { cn } from \"@/lib/utils\"\nimport type { DataRow } from \"@/lib/fable-ui/core\"\nimport type { DataBrowserProps } from \"./data-browser.types\"\nimport { useDataBrowserActions } from \"./hooks/use-data-browser-actions\"\nimport { useDataBrowserQuery } from \"./hooks/use-data-browser-query\"\nimport { useRowDetailSurface } from \"./hooks/use-row-detail-surface\"\nimport {\n  getFallbackFilters,\n  getFallbackSortOptions,\n} from \"./lib/fallback-filters\"\nimport { InlineError } from \"./internal/inline-error\"\nimport { PaginationFooter } from \"./internal/pagination-footer\"\nimport { ResponsiveDetailSurface } from \"./internal/responsive-detail-surface\"\nimport { TableView } from \"./internal/table-view\"\nimport { DataBrowserToolbar } from \"./internal/toolbar\"\n\nconst dataBrowserVariants = cva(\"w-full overflow-hidden\", {\n  variants: {\n    variant: {\n      default: \"bg-card shadow-sm\",\n      elevated: \"bg-card shadow-lg shadow-foreground/5\",\n      subtle: \"bg-muted/40 shadow-none\",\n    },\n    size: {\n      sm: \"\",\n      md: \"\",\n      lg: \"\",\n    },\n  },\n  defaultVariants: {\n    variant: \"default\",\n    size: \"md\",\n  },\n})\n\nconst contentGap = {\n  sm: \"gap-3 p-4\",\n  md: \"gap-4 p-5\",\n  lg: \"gap-5 p-6\",\n} as const\n\nexport function DataBrowser<Row extends DataRow = DataRow>({\n  title,\n  entityLabel,\n  description,\n  resourceId,\n  searchPlaceholder,\n  columns,\n  rows = [],\n  filters,\n  sortOptions,\n  initialFilters,\n  initialSearch,\n  initialSort,\n  pageSize = 8,\n  detail: detailConfig,\n  rowActions,\n  onViewRow,\n  onRowAction,\n  onRowActionSuccess,\n  renderDetail,\n  isLoading,\n  isDisabled,\n  error,\n  variant,\n  size = \"md\",\n}: DataBrowserProps<Row>) {\n  const effectiveFilters = filters ?? getFallbackFilters(rows, columns)\n  const effectiveSortOptions = sortOptions ?? getFallbackSortOptions(columns)\n  const detail = useRowDetailSurface<Row>()\n  const query = useDataBrowserQuery<Row>({\n    resourceId,\n    rows,\n    columns,\n    filters: effectiveFilters,\n    sortOptions: effectiveSortOptions,\n    initialFilters,\n    initialSearch,\n    initialSort,\n    pageSize,\n  })\n  const actions = rowActions ?? query.resource?.actions ?? []\n  const browserActions = useDataBrowserActions<Row>({\n    resourceId,\n    onRowAction,\n    onRowActionSuccess,\n    refetch: query.refetch,\n  })\n  const busy = Boolean(isLoading || query.isLoading)\n  const visibleError = error\n    ? error\n    : query.error\n      ? { title: \"Unable to load data\", description: query.error.message }\n      : undefined\n  const totalRows = query.totalRows\n  const hasNextPage = Boolean(\n    query.nextCursor || query.page * query.pageSize < totalRows\n  )\n  const hasPreviousPage = Boolean(query.previousCursor || query.page > 1)\n  const resolvedEntityLabel =\n    entityLabel || query.resource?.entityLabel || \"rows\"\n  const shouldShowDetails = Boolean(\n    detailConfig ||\n      renderDetail ||\n      onViewRow ||\n      actions.length > 0\n  )\n\n  function viewRow(row: Row) {\n    if (onViewRow) {\n      onViewRow(row)\n      return\n    }\n\n    detail.open(row)\n  }\n\n  return (\n    <>\n      <Card\n        className={cn(\n          dataBrowserVariants({ variant, size }),\n          \"transition-[box-shadow,opacity] duration-200\",\n          isDisabled && \"opacity-60\"\n        )}\n        data-fable-ui=\"data-browser\"\n        aria-busy={busy || undefined}\n      >\n        <CardHeader>\n          <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n            <div className=\"flex flex-col gap-1\">\n              <CardTitle className=\"text-base\">\n                {title || query.resource?.label || \"Data browser\"}\n              </CardTitle>\n              {description ? (\n                <CardDescription>{description}</CardDescription>\n              ) : null}\n            </div>\n            <div className=\"flex items-center gap-2 self-start\">\n              <Badge variant=\"secondary\">\n                {totalRows}{\" \"}\n                {totalRows === 1\n                  ? resolvedEntityLabel.replace(/s$/, \"\")\n                  : resolvedEntityLabel}\n              </Badge>\n            </div>\n          </div>\n        </CardHeader>\n        <CardContent\n          className={cn(\n            \"flex min-h-0 flex-col\",\n            contentGap[size]\n          )}\n        >\n          <DataBrowserToolbar\n            search={query.state.search}\n            searchPlaceholder={\n              searchPlaceholder ?? `Search ${resolvedEntityLabel}`\n            }\n            filters={effectiveFilters}\n            filterValues={query.state.filters}\n            sortOptions={effectiveSortOptions}\n            sort={query.state.sort}\n            isDisabled={isDisabled}\n            onSearchChange={query.setSearch}\n            onFilterChange={query.setFilter}\n            onSortChange={query.setSort}\n            onClearFilters={query.clearFilters}\n          />\n          {visibleError ? (\n            <InlineError\n              title={visibleError.title}\n              description={visibleError.description}\n            />\n          ) : null}\n          {busy ? (\n            <p className=\"text-sm text-muted-foreground\">\n              Loading {resolvedEntityLabel}...\n            </p>\n          ) : null}\n          <TableView\n            columns={columns}\n            rows={query.rows}\n            entityLabel={resolvedEntityLabel}\n            isDisabled={isDisabled}\n            onViewRow={shouldShowDetails ? viewRow : undefined}\n          />\n          <PaginationFooter\n            page={query.page}\n            pageSize={query.pageSize}\n            totalRows={totalRows}\n            hasNextPage={hasNextPage}\n            hasPreviousPage={hasPreviousPage}\n            isDisabled={isDisabled}\n            onPageChange={query.setPage}\n          />\n        </CardContent>\n      </Card>\n      <ResponsiveDetailSurface\n        row={detail.row}\n        title={`${title || \"Detail\"} detail`}\n        description={description}\n        columns={columns}\n        detail={detailConfig}\n        actions={actions}\n        actionError={browserActions.actionError}\n        pendingActionId={browserActions.pendingActionId}\n        renderDetail={renderDetail}\n        onClose={detail.close}\n        onRunAction={browserActions.runAction}\n      />\n    </>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/data-browser.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/data-browser.types.ts",
      "content": "import type {\n  DataActionConfig,\n  DataColumn,\n  DataFilter,\n  DataQueryResult,\n  DataRow,\n  DataSort,\n  SortState,\n} from \"@/lib/fable-ui/core\"\nimport type { ReactNode } from \"react\"\n\nexport type DataBrowserVariant = \"default\" | \"elevated\" | \"subtle\"\nexport type DataBrowserSize = \"sm\" | \"md\" | \"lg\"\n\nexport type DataBrowserRowAction<Row extends DataRow = DataRow> = DataActionConfig & {\n  run?: (row: Row, values?: Record<string, unknown>) => Promise<unknown> | unknown\n}\n\nexport type DataBrowserDetail<Row extends DataRow = DataRow> = {\n  title?: string\n  description?: string\n  fields?: string[]\n  row?: Row\n}\n\nexport type DataBrowserProps<Row extends DataRow = DataRow> = {\n  title: string\n  entityLabel: string\n  description?: string\n  resourceId?: string\n  searchPlaceholder?: string\n  columns: DataColumn[]\n  rows?: Row[]\n  filters?: DataFilter[]\n  sortOptions?: DataSort[]\n  initialFilters?: Record<string, unknown>\n  initialSearch?: string\n  initialSort?: SortState\n  pageSize?: number\n  detail?: DataBrowserDetail<Row>\n  rowActions?: DataBrowserRowAction<Row>[]\n  onViewRow?: (row: Row) => void\n  onRowAction?: (\n    action: DataBrowserRowAction<Row>,\n    row: Row,\n    values?: Record<string, unknown>,\n  ) => Promise<unknown> | unknown\n  onRowActionSuccess?: (result: unknown, action: DataBrowserRowAction<Row>, row: Row) => void\n  renderDetail?: (row: Row) => ReactNode\n  isLoading?: boolean\n  isDisabled?: boolean\n  error?: {\n    title: string\n    description?: string\n  }\n  variant?: DataBrowserVariant\n  size?: DataBrowserSize\n}\n\nexport type DataBrowserQueryState = {\n  search: string\n  filters: Record<string, unknown>\n  sort?: SortState\n  page: number\n  pageSize: number\n  cursor?: string\n}\n\nexport type DataBrowserQueryResult<Row extends DataRow = DataRow> = DataQueryResult<Row> & {\n  isLoading: boolean\n  error?: Error\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/data-browser.types.ts"
    },
    {
      "path": "components/fable-ui/data-browser/show-table.tsx",
      "content": "import type { DataColumn, DataRow } from \"@/lib/fable-ui/core\"\nimport { DataBrowser } from \"./data-browser\"\n\nexport function ShowTable({\n  title,\n  description,\n  columns,\n  rows,\n  pageSize = 10,\n  isLoading,\n  isDisabled,\n  error,\n}: {\n  title: string\n  description?: string\n  columns: DataColumn[]\n  rows: DataRow[]\n  pageSize?: number\n  isLoading?: boolean\n  isDisabled?: boolean\n  error?: {\n    title: string\n    description?: string\n  }\n}) {\n  return (\n    <DataBrowser\n      title={title}\n      description={description}\n      entityLabel=\"rows\"\n      columns={columns}\n      rows={rows}\n      pageSize={pageSize}\n      isLoading={isLoading}\n      isDisabled={isDisabled}\n      error={error}\n      searchPlaceholder=\"Search rows\"\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/show-table.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/resolve-intent.ts",
      "content": "import {\n  fableRegistry,\n  type DataColumn,\n  type DataFilter,\n  type DataRow,\n  type DataSort,\n  type DataSourceRegistry,\n  type SortState,\n} from \"@/lib/fable-ui/core\"\nimport type { DataBrowserProps } from \"./data-browser.types\"\n\nexport type DataBrowserIntent<Row extends DataRow = DataRow> = {\n  title: string\n  entityLabel?: string\n  description?: string\n  resourceId?: string\n  searchPlaceholder?: string\n  columns?: DataColumn[]\n  rows?: Row[]\n  filters?: DataFilter[]\n  sortOptions?: DataSort[]\n  initialFilters?: Record<string, unknown>\n  initialSort?: SortState\n  initialSearch?: string\n  pageSize?: number\n  visibleColumns?: string[]\n  detail?: DataBrowserProps<Row>[\"detail\"]\n}\n\nexport function resolveDataBrowserIntent<Row extends DataRow = DataRow>(\n  intent: DataBrowserIntent<Row>,\n  registry: DataSourceRegistry = fableRegistry,\n): DataBrowserProps<Row> {\n  if (!intent.resourceId) {\n    return {\n      title: intent.title,\n      description: intent.description,\n      entityLabel: intent.entityLabel ?? \"rows\",\n      searchPlaceholder: intent.searchPlaceholder,\n      columns: intent.columns ?? [],\n      rows: intent.rows ?? [],\n      filters: intent.filters,\n      sortOptions: intent.sortOptions,\n      initialFilters: intent.initialFilters,\n      initialSort: intent.initialSort,\n      initialSearch: intent.initialSearch,\n      pageSize: intent.pageSize,\n      detail: intent.detail,\n    }\n  }\n\n  const resource = registry.getResource(intent.resourceId)\n\n  if (!resource) {\n    return {\n      title: intent.title || \"Data browser\",\n      entityLabel: intent.entityLabel ?? \"rows\",\n      columns: [],\n      rows: [],\n      error: {\n        title: \"Resource unavailable\",\n        description: `The resource \"${intent.resourceId}\" is not registered by the host app.`,\n      },\n    }\n  }\n\n  const visibleColumnSet = new Set(intent.visibleColumns ?? [])\n  const columns = visibleColumnSet.size\n    ? resource.columns.map((column) => ({\n        ...column,\n        hidden: !visibleColumnSet.has(column.key),\n      }))\n    : resource.columns\n\n  return {\n    title: intent.title || resource.label,\n    description: intent.description,\n    entityLabel: resource.entityLabel,\n    resourceId: resource.id,\n    searchPlaceholder: intent.searchPlaceholder,\n    columns,\n    filters: resource.filters,\n    sortOptions: resource.sort,\n    rowActions: resource.actions,\n    initialFilters: intent.initialFilters,\n    initialSort: intent.initialSort,\n    initialSearch: intent.initialSearch,\n    pageSize: intent.pageSize,\n    detail: intent.detail,\n  }\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/resolve-intent.ts"
    },
    {
      "path": "components/fable-ui/data-browser/hooks/use-data-browser-query.ts",
      "content": "import * as React from \"react\"\n\nimport {\n  queryLocalRows,\n  useOptionalFableDataContext,\n  fableRegistry,\n  type DataColumn,\n  type DataFilter,\n  type DataQueryResult,\n  type DataRow,\n  type DataSort,\n  type SortState,\n} from \"@/lib/fable-ui/core\"\nimport type { DataBrowserQueryState } from \"../data-browser.types\"\n\nconst emptyContext: Record<string, unknown> = {}\n\nfunction normalizePageSize(pageSize: number) {\n  return Math.max(1, Math.min(100, Math.floor(pageSize || 10)))\n}\n\nexport function useDataBrowserQuery<Row extends DataRow>({\n  resourceId,\n  rows = [],\n  columns,\n  initialFilters = {},\n  initialSearch = \"\",\n  initialSort,\n  pageSize = 8,\n}: {\n  resourceId?: string\n  rows?: Row[]\n  columns: DataColumn[]\n  filters?: DataFilter[]\n  sortOptions?: DataSort[]\n  initialFilters?: Record<string, unknown>\n  initialSearch?: string\n  initialSort?: SortState\n  pageSize?: number\n}) {\n  const dataContext = useOptionalFableDataContext()\n  const registry = dataContext?.registry ?? fableRegistry\n  const context = dataContext?.context ?? emptyContext\n  const normalizedPageSize = normalizePageSize(pageSize)\n  const [state, setState] = React.useState<DataBrowserQueryState>({\n    search: initialSearch,\n    filters: initialFilters,\n    sort: initialSort,\n    page: 1,\n    pageSize: normalizedPageSize,\n  })\n  const [remoteResult, setRemoteResult] =\n    React.useState<DataQueryResult<Row> | null>(null)\n  const [isLoading, setIsLoading] = React.useState(false)\n  const [error, setError] = React.useState<Error | undefined>()\n  const [revision, setRevision] = React.useState(0)\n\n  const resource = resourceId ? registry.getResource(resourceId) : undefined\n\n  const query = React.useMemo(\n    () => ({\n      search: state.search,\n      filters: state.filters,\n      sort: state.sort,\n      page: state.page,\n      pageSize: state.pageSize,\n      cursor: state.cursor,\n    }),\n    [state]\n  )\n\n  React.useEffect(() => {\n    if (!resourceId) {\n      return\n    }\n\n    let isActive = true\n\n    queueMicrotask(() => {\n      if (isActive) {\n        setIsLoading(true)\n        setError(undefined)\n      }\n    })\n\n    registry\n      .list<Row>(resourceId, query, context)\n      .then((result) => {\n        if (isActive) {\n          setRemoteResult(result)\n        }\n      })\n      .catch((nextError) => {\n        if (isActive) {\n          setError(\n            nextError instanceof Error\n              ? nextError\n              : new Error(\"Unable to load resource.\")\n          )\n        }\n      })\n      .finally(() => {\n        if (isActive) {\n          setIsLoading(false)\n        }\n      })\n\n    return () => {\n      isActive = false\n    }\n  }, [context, query, registry, resourceId, revision])\n\n  const localResult = React.useMemo(() => {\n    if (resourceId) {\n      return null\n    }\n\n    return queryLocalRows(rows, query, columns)\n  }, [columns, query, resourceId, rows])\n\n  const result = resourceId ? remoteResult : localResult\n\n  const updateState = React.useCallback(\n    (next: Partial<DataBrowserQueryState>) => {\n      setState((current) => ({\n        ...current,\n        ...next,\n        page: next.page ?? 1,\n        cursor: next.cursor,\n      }))\n    },\n    []\n  )\n\n  return {\n    resource,\n    state,\n    setSearch: (search: string) => updateState({ search }),\n    setFilter: (key: string, value: unknown) =>\n      updateState({ filters: { ...state.filters, [key]: value } }),\n    clearFilters: () => updateState({ filters: {} }),\n    setSort: (sort?: SortState) => updateState({ sort }),\n    setPage: (page: number) =>\n      setState((current) => ({\n        ...current,\n        page,\n        cursor:\n          page > current.page\n            ? result?.nextCursor\n            : page < current.page\n              ? result?.previousCursor\n              : current.cursor,\n      })),\n    result,\n    rows: result?.rows ?? [],\n    totalRows: result?.totalRows ?? 0,\n    page: result?.page ?? state.page,\n    pageSize: result?.pageSize ?? state.pageSize,\n    nextCursor: result?.nextCursor,\n    previousCursor: result?.previousCursor,\n    isLoading,\n    error,\n    refetch: () => setRevision((value) => value + 1),\n  }\n}\n",
      "type": "registry:hook",
      "target": "@components/fable-ui/data-browser/hooks/use-data-browser-query.ts"
    },
    {
      "path": "components/fable-ui/data-browser/hooks/use-data-browser-actions.ts",
      "content": "import * as React from \"react\"\n\nimport { getDataBrowserRowId } from \"../lib/get-row-id\"\nimport type { DataBrowserRowAction } from \"../data-browser.types\"\nimport { useOptionalFableDataContext, fableRegistry, type DataRow } from \"@/lib/fable-ui/core\"\n\nexport function useDataBrowserActions<Row extends DataRow>({\n  resourceId,\n  onRowAction,\n  onRowActionSuccess,\n  refetch,\n}: {\n  resourceId?: string\n  onRowAction?: (\n    action: DataBrowserRowAction<Row>,\n    row: Row,\n    values?: Record<string, unknown>,\n  ) => Promise<unknown> | unknown\n  onRowActionSuccess?: (result: unknown, action: DataBrowserRowAction<Row>, row: Row) => void\n  refetch?: () => void\n}) {\n  const dataContext = useOptionalFableDataContext()\n  const registry = dataContext?.registry ?? fableRegistry\n  const context = dataContext?.context ?? {}\n  const [pendingActionId, setPendingActionId] = React.useState<string | null>(null)\n  const [error, setError] = React.useState<string | null>(null)\n\n  const runAction = React.useCallback(\n    async (action: DataBrowserRowAction<Row>, row: Row, values?: Record<string, unknown>) => {\n      setError(null)\n      setPendingActionId(action.id)\n\n      try {\n        const result = action.run\n          ? await action.run(row, values)\n          : onRowAction\n            ? await onRowAction(action, row, values)\n            : resourceId\n              ? await registry.executeAction(\n                  {\n                    actionId: action.id,\n                    resourceId,\n                    rowId: getDataBrowserRowId(row),\n                    values,\n                  },\n                  context,\n                )\n              : undefined\n\n        onRowActionSuccess?.(result, action, row)\n        refetch?.()\n        return result\n      } catch (nextError) {\n        const message = nextError instanceof Error ? nextError.message : \"The action failed.\"\n        setError(message)\n        return undefined\n      } finally {\n        setPendingActionId(null)\n      }\n    },\n    [context, onRowAction, onRowActionSuccess, refetch, registry, resourceId],\n  )\n\n  return {\n    runAction,\n    pendingActionId,\n    actionError: error,\n  }\n}\n",
      "type": "registry:hook",
      "target": "@components/fable-ui/data-browser/hooks/use-data-browser-actions.ts"
    },
    {
      "path": "components/fable-ui/data-browser/hooks/use-row-detail-surface.ts",
      "content": "import * as React from \"react\"\n\nimport type { DataRow } from \"@/lib/fable-ui/core\"\n\nexport function useRowDetailSurface<Row extends DataRow>() {\n  const [row, setRow] = React.useState<Row | null>(null)\n\n  return {\n    row,\n    isOpen: Boolean(row),\n    open: setRow,\n    close: () => setRow(null),\n  }\n}\n",
      "type": "registry:hook",
      "target": "@components/fable-ui/data-browser/hooks/use-row-detail-surface.ts"
    },
    {
      "path": "components/fable-ui/data-browser/internal/inline-error.tsx",
      "content": "export function InlineError({\n  title,\n  description,\n}: {\n  title: string\n  description?: string\n}) {\n  return (\n    <div className=\"rounded-md border border-destructive/20 bg-destructive/5 p-3\">\n      <p className=\"text-sm font-medium text-destructive\">{title}</p>\n      {description ? <p className=\"text-sm text-muted-foreground\">{description}</p> : null}\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/internal/inline-error.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/internal/pagination-footer.tsx",
      "content": "import { Button } from \"@/components/ui/button\"\n\nexport function PaginationFooter({\n  page,\n  pageSize,\n  totalRows,\n  hasNextPage,\n  hasPreviousPage,\n  isDisabled,\n  onPageChange,\n}: {\n  page: number\n  pageSize: number\n  totalRows: number\n  hasNextPage: boolean\n  hasPreviousPage: boolean\n  isDisabled?: boolean\n  onPageChange: (page: number) => void\n}) {\n  const start = totalRows === 0 ? 0 : (page - 1) * pageSize + 1\n  const end = Math.min(page * pageSize, totalRows)\n  const totalPages = Math.max(1, Math.ceil(totalRows / pageSize))\n\n  return (\n    <nav\n      className=\"flex flex-col gap-3 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between\"\n      aria-label=\"Table pagination\"\n    >\n      <span className=\"tabular-nums\" aria-live=\"polite\">\n        {start}-{end} of {totalRows} - Page {Math.min(page, totalPages)} of{\" \"}\n        {totalPages}\n      </span>\n      <div className=\"flex gap-2\">\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          aria-label=\"Go to previous page\"\n          disabled={isDisabled || !hasPreviousPage}\n          onClick={() => onPageChange(Math.max(1, page - 1))}\n        >\n          Previous\n        </Button>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          aria-label=\"Go to next page\"\n          disabled={isDisabled || !hasNextPage}\n          onClick={() => onPageChange(page + 1)}\n        >\n          Next\n        </Button>\n      </div>\n    </nav>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/internal/pagination-footer.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/internal/responsive-detail-surface.tsx",
      "content": "import type { DataColumn, DataRow } from \"@/lib/fable-ui/core\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport {\n  Drawer,\n  DrawerContent,\n  DrawerDescription,\n  DrawerHeader,\n  DrawerTitle,\n} from \"@/components/ui/drawer\"\nimport * as React from \"react\"\nimport type { DataBrowserDetail, DataBrowserRowAction } from \"../data-browser.types\"\nimport { RowDetail } from \"./row-detail\"\n\nfunction useIsMobile() {\n  const [isMobile, setIsMobile] = React.useState(false)\n\n  React.useEffect(() => {\n    const query = window.matchMedia(\"(max-width: 767px)\")\n    const update = () => setIsMobile(query.matches)\n\n    update()\n    query.addEventListener(\"change\", update)\n\n    return () => query.removeEventListener(\"change\", update)\n  }, [])\n\n  return isMobile\n}\n\nexport function ResponsiveDetailSurface<Row extends DataRow>({\n  row,\n  title,\n  description,\n  columns,\n  detail,\n  actions,\n  actionError,\n  pendingActionId,\n  renderDetail,\n  onClose,\n  onRunAction,\n}: {\n  row: Row | null\n  title: string\n  description?: string\n  columns: DataColumn[]\n  detail?: DataBrowserDetail<Row>\n  actions: DataBrowserRowAction<Row>[]\n  actionError?: string | null\n  pendingActionId?: string | null\n  renderDetail?: (row: Row) => React.ReactNode\n  onClose: () => void\n  onRunAction: (action: DataBrowserRowAction<Row>, row: Row, values?: Record<string, unknown>) => void\n}) {\n  const isMobile = useIsMobile()\n  const isOpen = Boolean(row)\n  const resolvedTitle = detail?.title ?? title\n  const resolvedDescription = detail?.description ?? description\n\n  if (isMobile) {\n    return (\n      <Drawer open={isOpen} onOpenChange={(open) => (!open ? onClose() : undefined)}>\n        <DrawerContent>\n          <DrawerHeader>\n            <DrawerTitle>{resolvedTitle}</DrawerTitle>\n            {resolvedDescription ? <DrawerDescription>{resolvedDescription}</DrawerDescription> : null}\n          </DrawerHeader>\n          <div className=\"max-h-[70vh] overflow-y-auto px-4 pb-4\">\n            {row ? (\n              <RowDetail\n                row={row}\n                columns={columns}\n                detail={detail}\n                actions={actions}\n                actionError={actionError}\n                pendingActionId={pendingActionId}\n                renderDetail={renderDetail}\n                onRunAction={onRunAction}\n              />\n            ) : null}\n          </div>\n        </DrawerContent>\n      </Drawer>\n    )\n  }\n\n  return (\n    <Dialog open={isOpen} onOpenChange={(open) => (!open ? onClose() : undefined)}>\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle>{resolvedTitle}</DialogTitle>\n          {resolvedDescription ? <DialogDescription>{resolvedDescription}</DialogDescription> : null}\n        </DialogHeader>\n        {row ? (\n          <RowDetail\n            row={row}\n            columns={columns}\n            detail={detail}\n            actions={actions}\n            actionError={actionError}\n            pendingActionId={pendingActionId}\n            renderDetail={renderDetail}\n            onRunAction={onRunAction}\n          />\n        ) : null}\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/internal/responsive-detail-surface.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/internal/row-detail.tsx",
      "content": "import * as React from \"react\"\n\nimport { formatDataCell, type DataColumn, type DataRow } from \"@/lib/fable-ui/core\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport type { DataBrowserDetail, DataBrowserRowAction } from \"../data-browser.types\"\n\nexport function RowDetail<Row extends DataRow>({\n  row,\n  columns,\n  detail,\n  actions,\n  actionError,\n  pendingActionId,\n  renderDetail,\n  onRunAction,\n}: {\n  row: Row\n  columns: DataColumn[]\n  detail?: DataBrowserDetail<Row>\n  actions: DataBrowserRowAction<Row>[]\n  actionError?: string | null\n  pendingActionId?: string | null\n  renderDetail?: (row: Row) => React.ReactNode\n  onRunAction: (action: DataBrowserRowAction<Row>, row: Row, values?: Record<string, unknown>) => void\n}) {\n  const [actionValues, setActionValues] = React.useState<Record<string, Record<string, unknown>>>({})\n\n  if (renderDetail) {\n    return <>{renderDetail(row)}</>\n  }\n\n  const detailFields = new Set(detail?.fields ?? [])\n  const visibleColumns = columns.filter(\n    (column) =>\n      !column.hidden &&\n      (detailFields.size === 0 || detailFields.has(column.key))\n  )\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <dl className=\"grid gap-3 sm:grid-cols-2\">\n        {visibleColumns.map((column) => {\n          const formattedValue = formatDataCell(row, column)\n\n          return (\n            <div key={column.key} className=\"rounded-md border bg-muted/30 p-3\">\n              <dt className=\"text-xs font-medium text-muted-foreground\">{column.label}</dt>\n              <dd className=\"mt-1 break-words text-sm font-medium leading-6\">\n                {formattedValue === \"\" ? \"-\" : formattedValue}\n              </dd>\n            </div>\n          )\n        })}\n      </dl>\n      {actionError ? (\n        <div className=\"rounded-md border border-destructive/20 bg-destructive/5 p-3 text-sm text-destructive\">\n          {actionError}\n        </div>\n      ) : null}\n      {actions.length > 0 ? (\n        <div className=\"flex flex-col gap-3\">\n          {actions.map((action) => {\n            const values = actionValues[action.id] ?? {}\n\n            return (\n              <div key={action.id} className=\"rounded-md border p-3\">\n                <div className=\"flex flex-col gap-2\">\n                  <p className=\"text-sm font-medium\">{action.label}</p>\n                  {action.description ? (\n                    <p className=\"text-xs text-muted-foreground\">{action.description}</p>\n                  ) : null}\n                  {action.fields?.map((field) => (\n                    <label key={field.name} className=\"flex flex-col gap-1 text-xs font-medium\">\n                      {field.label}\n                      <Input\n                        value={String(values[field.name] ?? \"\")}\n                        onChange={(event) =>\n                          setActionValues((current) => ({\n                            ...current,\n                            [action.id]: {\n                              ...values,\n                              [field.name]: event.target.value,\n                            },\n                          }))\n                        }\n                      />\n                    </label>\n                  ))}\n                  <Button\n                    type=\"button\"\n                    variant={action.variant === \"destructive\" ? \"destructive\" : \"default\"}\n                    size=\"sm\"\n                    disabled={pendingActionId === action.id}\n                    onClick={() => onRunAction(action, row, values)}\n                  >\n                    {pendingActionId === action.id ? \"Running...\" : action.label}\n                  </Button>\n                </div>\n              </div>\n            )\n          })}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/internal/row-detail.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/internal/table-view.tsx",
      "content": "import {\n  formatDataCell,\n  type DataColumn,\n  type DataRow,\n} from \"@/lib/fable-ui/core\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { CellAvatar, isAvatarFieldKey } from \"../lib/cell-avatar\"\nimport { getDataBrowserRowId } from \"../lib/get-row-id\"\n\nfunction getColumnMinWidth(column: DataColumn, isFirstColumn: boolean) {\n  if (typeof column.width === \"number\") {\n    return column.width\n  }\n\n  if (typeof column.width === \"string\") {\n    const parsed = column.width.endsWith(\"px\")\n      ? Number.parseInt(column.width, 10)\n      : Number.NaN\n    return Number.isFinite(parsed) ? parsed : 160\n  }\n\n  if (isFirstColumn) {\n    return 220\n  }\n\n  if (column.type === \"currency\" || column.type === \"number\") {\n    return 132\n  }\n\n  if (column.type === \"date\") {\n    return 140\n  }\n\n  if (column.type === \"datetime\") {\n    return 180\n  }\n\n  if (column.type === \"boolean\" || column.type === \"badge\") {\n    return 120\n  }\n\n  return Math.max(144, Math.min(260, column.label.length * 12 + 72))\n}\n\nfunction getColumnWidth(column: DataColumn, minWidth: number) {\n  if (typeof column.width === \"number\") {\n    return `${column.width}px`\n  }\n\n  return column.width ?? `${minWidth}px`\n}\n\nfunction getAlignClass(align: DataColumn[\"align\"]) {\n  if (align === \"right\") {\n    return \"text-right\"\n  }\n\n  if (align === \"center\") {\n    return \"text-center\"\n  }\n\n  return \"text-left\"\n}\n\nexport function TableView<Row extends DataRow>({\n  columns,\n  rows,\n  entityLabel,\n  isDisabled,\n  onViewRow,\n}: {\n  columns: DataColumn[]\n  rows: Row[]\n  entityLabel: string\n  isDisabled?: boolean\n  onViewRow?: (row: Row) => void\n}) {\n  const visibleColumns = columns.filter((column) => !column.hidden)\n  const columnWidths = visibleColumns.map((column, index) =>\n    getColumnMinWidth(column, index === 0)\n  )\n  const actionColumnWidth = onViewRow ? 112 : 0\n  const tableMinWidth = Math.max(\n    visibleColumns.length\n      ? columnWidths.reduce((total, width) => total + width, actionColumnWidth)\n      : 320,\n    640\n  )\n  const colSpan = Math.max(1, visibleColumns.length + (onViewRow ? 1 : 0))\n\n  return (\n    <div\n      className=\"min-h-0 overflow-auto rounded-md border scrollbar-pretty max-h-[min(60vh,36rem)]\"\n    >\n      <table\n        className=\"w-full table-auto text-sm\"\n        style={{ minWidth: tableMinWidth }}\n      >\n        <thead className=\"bg-muted/60\">\n          <tr>\n            {visibleColumns.map((column, index) => (\n              <th\n                key={column.key}\n                className={cn(\n                  \"px-3 py-2 font-medium whitespace-nowrap\",\n                  getAlignClass(column.align)\n                )}\n                data-align={column.align ?? \"left\"}\n                style={{\n                  minWidth: columnWidths[index],\n                  width: getColumnWidth(column, columnWidths[index]),\n                }}\n              >\n                {column.label}\n              </th>\n            ))}\n            {onViewRow ? (\n              <th className=\"w-28 px-3 py-2 text-right font-medium\">View</th>\n            ) : null}\n          </tr>\n        </thead>\n        <tbody>\n          {rows.length === 0 ? (\n            <tr>\n              <td\n                className=\"px-3 py-8 text-center text-muted-foreground\"\n                colSpan={colSpan}\n              >\n                No {entityLabel} to display.\n              </td>\n            </tr>\n          ) : (\n            rows.map((row, index) => (\n              <tr key={getDataBrowserRowId(row, index)} className=\"border-t\">\n                {visibleColumns.map((column, columnIndex) => {\n                  const rowId = getDataBrowserRowId(row, index)\n                  const formattedValue = formatDataCell(row, column)\n                  const cellValue =\n                    columnIndex === 0 && isAvatarFieldKey(column.key)\n                      ? \"\"\n                      : formattedValue\n\n                  return (\n                    <td\n                      key={`${rowId}-${column.key}`}\n                      className={cn(\n                        \"px-3 py-2 align-middle\",\n                        getAlignClass(column.align)\n                      )}\n                      style={{\n                        minWidth: columnWidths[columnIndex],\n                        width: getColumnWidth(\n                          column,\n                          columnWidths[columnIndex]\n                        ),\n                      }}\n                    >\n                      {columnIndex === 0 ? (\n                        <div className=\"flex min-w-0 items-center gap-2\">\n                          <CellAvatar row={row} />\n                          <span className=\"min-w-0 truncate\">{cellValue}</span>\n                        </div>\n                      ) : (\n                        <span className=\"block min-w-0 truncate\">\n                          {formattedValue}\n                        </span>\n                      )}\n                    </td>\n                  )\n                })}\n                {onViewRow ? (\n                  <td className=\"px-3 py-2 text-right\">\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      disabled={isDisabled}\n                      onClick={() => onViewRow(row)}\n                    >\n                      View\n                    </Button>\n                  </td>\n                ) : null}\n              </tr>\n            ))\n          )}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/internal/table-view.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/internal/toolbar.tsx",
      "content": "import { SearchIcon } from \"lucide-react\"\n\nimport type { DataFilter, DataSort, SortState } from \"@/lib/fable-ui/core\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\n\nfunction getOptionValue(option: string | { value: string | number | boolean }) {\n  return typeof option === \"string\" ? option : String(option.value)\n}\n\nfunction getOptionLabel(option: string | { label: string }) {\n  return typeof option === \"string\" ? option : option.label\n}\n\nexport function DataBrowserToolbar({\n  search,\n  searchPlaceholder,\n  filters,\n  filterValues,\n  sortOptions,\n  sort,\n  isDisabled,\n  onSearchChange,\n  onFilterChange,\n  onSortChange,\n  onClearFilters,\n}: {\n  search: string\n  searchPlaceholder: string\n  filters: DataFilter[]\n  filterValues: Record<string, unknown>\n  sortOptions: DataSort[]\n  sort?: SortState\n  isDisabled?: boolean\n  onSearchChange: (value: string) => void\n  onFilterChange: (key: string, value: unknown) => void\n  onSortChange: (sort?: SortState) => void\n  onClearFilters: () => void\n}) {\n  const hasFilters = Object.values(filterValues).some(\n    (value) => value != null && value !== \"\"\n  )\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"flex flex-col gap-2 sm:flex-row\">\n        <label className=\"relative flex-1\">\n          <span className=\"sr-only\">{searchPlaceholder}</span>\n          <SearchIcon className=\"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground\" />\n          <Input\n            value={search}\n            placeholder={searchPlaceholder}\n            disabled={isDisabled}\n            aria-label={searchPlaceholder}\n            className=\"pl-9\"\n            onChange={(event) => onSearchChange(event.target.value)}\n          />\n        </label>\n        {sortOptions.length > 0 ? (\n          <Select\n            value={sort ? `${sort.key}:${sort.direction}` : \"\"}\n            disabled={isDisabled}\n            onValueChange={(value) => {\n              if (!value) {\n                onSortChange(undefined)\n                return\n              }\n\n              const [key, direction] = value.split(\":\")\n              onSortChange({\n                key,\n                direction: direction === \"desc\" ? \"desc\" : \"asc\",\n              })\n            }}\n          >\n            <SelectTrigger className=\"w-full sm:w-48\" aria-label=\"Sort rows\">\n              <SelectValue placeholder=\"Sort\" />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectGroup>\n                {sortOptions.flatMap((option) =>\n                  (option.directions ?? [\"asc\", \"desc\"]).map((direction) => (\n                    <SelectItem\n                      key={`${option.key}:${direction}`}\n                      value={`${option.key}:${direction}`}\n                    >\n                      {option.label} {direction === \"asc\" ? \"asc\" : \"desc\"}\n                    </SelectItem>\n                  ))\n                )}\n              </SelectGroup>\n            </SelectContent>\n          </Select>\n        ) : null}\n      </div>\n      {filters.length > 0 ? (\n        <div className=\"flex flex-wrap gap-2\">\n          {filters.map((filter) => {\n            if (filter.type === \"select\" && filter.options?.length) {\n              return (\n                <Select\n                  key={filter.key}\n                  value={String(filterValues[filter.key] ?? \"\")}\n                  disabled={isDisabled}\n                  onValueChange={(value) => onFilterChange(filter.key, value)}\n                >\n                  <SelectTrigger\n                    className=\"w-full sm:w-44\"\n                    aria-label={filter.label}\n                  >\n                    <SelectValue placeholder={filter.label} />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectGroup>\n                      {filter.options.map((option) => (\n                        <SelectItem\n                          key={getOptionValue(option)}\n                          value={getOptionValue(option)}\n                        >\n                          {getOptionLabel(option)}\n                        </SelectItem>\n                      ))}\n                    </SelectGroup>\n                  </SelectContent>\n                </Select>\n              )\n            }\n\n            return (\n              <Input\n                key={filter.key}\n                value={String(filterValues[filter.key] ?? \"\")}\n                placeholder={filter.label}\n                disabled={isDisabled}\n                aria-label={filter.label}\n                className=\"w-full sm:w-44\"\n                onChange={(event) =>\n                  onFilterChange(filter.key, event.target.value)\n                }\n              />\n            )\n          })}\n          {hasFilters ? (\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              disabled={isDisabled}\n              onClick={onClearFilters}\n            >\n              Clear\n            </Button>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "@components/fable-ui/data-browser/internal/toolbar.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/lib/cell-avatar.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\n\nimport {\n  getDataValue,\n  normalizeDataValue,\n  type DataRow,\n} from \"@/lib/fable-ui/core\"\nimport { cn } from \"@/lib/utils\"\n\nconst avatarFieldNames = new Set([\n  \"avatar\",\n  \"avatarurl\",\n  \"image\",\n  \"imageurl\",\n  \"picture\",\n  \"pictureurl\",\n  \"photo\",\n  \"photourl\",\n])\n\nconst labelFieldNames = [\n  \"name\",\n  \"fullname\",\n  \"displayname\",\n  \"title\",\n  \"label\",\n  \"customer\",\n  \"username\",\n  \"email\",\n]\n\nfunction normalizeKey(key: string) {\n  return key.replace(/[^a-z0-9]/gi, \"\").toLowerCase()\n}\n\nexport function isAvatarFieldKey(key: string) {\n  return avatarFieldNames.has(normalizeKey(key))\n}\n\nfunction getRowKeys(row: DataRow) {\n  const keys = new Set(Object.keys(row))\n  const cells = row.cells\n\n  if (cells && typeof cells === \"object\" && !Array.isArray(cells)) {\n    Object.keys(cells as Record<string, unknown>).forEach((key) =>\n      keys.add(key)\n    )\n  }\n\n  return Array.from(keys)\n}\n\nfunction getFirstMatchingValue(row: DataRow, keys: string[]) {\n  const rowKeys = getRowKeys(row)\n\n  for (const expectedKey of keys) {\n    const key = rowKeys.find(\n      (candidate) => normalizeKey(candidate) === expectedKey\n    )\n    const value = key ? getDataValue(row, key) : undefined\n\n    if (value != null && normalizeDataValue(value).trim() !== \"\") {\n      return value\n    }\n  }\n\n  return undefined\n}\n\nfunction getImageUrl(value: unknown) {\n  const text = normalizeDataValue(value).trim()\n\n  if (!text) {\n    return undefined\n  }\n\n  if (/^data:image\\/[a-z0-9.+-]+[,;]/i.test(text)) {\n    return text\n  }\n\n  try {\n    const url = new URL(text)\n    return url.protocol === \"http:\" || url.protocol === \"https:\"\n      ? text\n      : undefined\n  } catch {\n    return undefined\n  }\n}\n\nfunction getInitials(value: unknown) {\n  const text = normalizeDataValue(value).trim()\n\n  if (!text) {\n    return undefined\n  }\n\n  const initials = text\n    .split(/[\\s._-]+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((part) => part[0])\n    .join(\"\")\n    .toUpperCase()\n\n  return initials || text.slice(0, 2).toUpperCase()\n}\n\nexport function getAvatarPresentation(row: DataRow) {\n  const avatarValue = getFirstMatchingValue(row, Array.from(avatarFieldNames))\n  const labelValue = getFirstMatchingValue(row, labelFieldNames)\n  const src = getImageUrl(avatarValue)\n  const initials = getInitials(labelValue)\n  const label = normalizeDataValue(labelValue).trim() || \"Row\"\n\n  if (!src && !initials) {\n    return null\n  }\n\n  return { src, initials: initials ?? \"?\", label }\n}\n\nexport function CellAvatar({\n  row,\n  className,\n}: {\n  row: DataRow\n  className?: string\n}) {\n  const [imageFailed, setImageFailed] = useState(false)\n  const avatar = getAvatarPresentation(row)\n\n  if (!avatar) {\n    return null\n  }\n\n  return (\n    <span\n      className={cn(\n        \"flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-xs font-medium text-muted-foreground\",\n        className\n      )}\n      aria-hidden=\"true\"\n    >\n      {avatar.src && !imageFailed ? (\n        // eslint-disable-next-line @next/next/no-img-element\n        <img\n          src={avatar.src}\n          alt=\"\"\n          className=\"size-full object-cover\"\n          loading=\"lazy\"\n          onError={() => setImageFailed(true)}\n        />\n      ) : (\n        avatar.initials\n      )}\n    </span>\n  )\n}\n",
      "type": "registry:lib",
      "target": "@components/fable-ui/data-browser/lib/cell-avatar.tsx"
    },
    {
      "path": "components/fable-ui/data-browser/lib/fallback-filters.ts",
      "content": "import { getDataValue, type DataColumn, type DataFilter, type DataRow, type DataSort } from \"@/lib/fable-ui/core\"\n\nconst fallbackFilterKeys = new Set([\"status\", \"type\", \"category\", \"role\"])\n\nexport function getFallbackFilters(rows: DataRow[], columns: DataColumn[]): DataFilter[] {\n  return columns\n    .filter((column) => column.filterable || fallbackFilterKeys.has(column.key))\n    .map((column) => {\n      const values = Array.from(\n        new Set(\n          rows\n            .map((row) => getDataValue(row, column.key))\n            .filter((value) => value != null && value !== \"\")\n            .map(String),\n        ),\n      ).slice(0, 20)\n\n      return {\n        key: column.key,\n        label: column.label,\n        type: values.length > 0 ? \"select\" : \"text\",\n        options: values,\n      } satisfies DataFilter\n    })\n}\n\nexport function getFallbackSortOptions(columns: DataColumn[]): DataSort[] {\n  return columns\n    .filter((column) => column.sortable !== false)\n    .map((column) => ({\n      key: column.key,\n      label: column.label,\n      directions: [\"asc\", \"desc\"],\n    }))\n}\n",
      "type": "registry:lib",
      "target": "@components/fable-ui/data-browser/lib/fallback-filters.ts"
    },
    {
      "path": "components/fable-ui/data-browser/lib/get-row-id.ts",
      "content": "import { getRowId as getCoreRowId, type DataRow } from \"@/lib/fable-ui/core\"\n\nexport function getDataBrowserRowId(row: DataRow, index = 0) {\n  return getCoreRowId(row) ?? String(index)\n}\n",
      "type": "registry:lib",
      "target": "@components/fable-ui/data-browser/lib/get-row-id.ts"
    },
    {
      "path": "lib/fable-ui/manifests/show-data-browser.md",
      "content": "---\ntool: show_data_browser\ntype: registry:block\n---\n\n# show_data_browser\n\nUse for browsing, searching, filtering, sorting, pagination, row detail, or row actions over host-owned data.\n\nStatic rows are allowed only when display-ready data is already present. Keep static payloads to 200 rows or fewer and set an explicit page size for large payloads.\n\nThe model must not pass raw SQL, raw Firestore query code, secrets, or authorization decisions. The host owns data access and allowed operations.\n",
      "type": "registry:file",
      "target": "@lib/fable-ui/manifests/show-data-browser.md"
    }
  ],
  "type": "registry:block"
}