{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dbt-model-graph",
  "title": "Dbt Model Graph",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/dbt-model-graph/dbt-model-graph.component.ts",
      "content": "import { Component, DestroyRef, Input, computed, inject, signal, ChangeDetectionStrategy } from '@angular/core'\nimport {\n  ArrowUpRight,\n  Check,\n  CheckCircle2,\n  Clock,\n  Code2,\n  Copy,\n  Database,\n  FileCode,\n  GitBranch,\n  HardDrive,\n  Info,\n  Key,\n  Layers,\n  Link2,\n  Loader2,\n  LucideAngularModule,\n  Play,\n  Search,\n  ShieldCheck,\n  Table2,\n  Tag,\n  User,\n  Workflow,\n  Zap,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent } from '@/ui/card/card.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\nimport {\n  UiTabsComponent,\n  UiTabsContentComponent,\n  UiTabsListComponent,\n  UiTabsTriggerComponent,\n} from '@/ui/tabs/tabs.component'\n\nexport type DbtModelGraphTab = 'model-sql' | 'compiled-sql' | 'contract'\n\nexport interface ModelColumn {\n  name: string\n  dataType: string\n  nullable: boolean\n  isPrimaryKey?: boolean\n  isForeignKey?: boolean\n  foreignKeyRef?: string\n  isClusterKey?: boolean\n  tests: Array<{\n    name: 'unique' | 'not_null' | 'relationships' | 'accepted_values'\n    status: 'pass' | 'warn' | 'fail'\n    detail?: string\n  }>\n  testDuration: string\n  description: string\n}\n\nexport interface UpstreamNode {\n  id: string\n  name: string\n  type: 'view' | 'table' | 'source'\n  package: string\n  rowCount: string\n  freshness: string\n  freshnessStatus: 'pass' | 'warn'\n  description: string\n}\n\nexport interface DownstreamNode {\n  id: string\n  name: string\n  type: 'table' | 'incremental' | 'exposure'\n  package: string\n  consumers: string\n  sla: string\n  description: string\n}\n\nconst modelSqlSource = `{{\n  config(\n    materialized = 'incremental',\n    unique_key = 'order_id',\n    on_schema_change = 'sync_all_columns',\n    incremental_strategy = 'merge',\n    cluster_by = ['order_date', 'customer_id'],\n    tags = ['finance', 'core', 'daily_sla']\n  )\n}}\n\nWITH payments AS (\n  SELECT\n    payment_id,\n    order_id,\n    payment_method,\n    amount_usd,\n    gateway_fee_usd,\n    status AS payment_status,\n    created_at AS payment_created_at\n  FROM {{ ref('stg_stripe__payments') }}\n  {% if is_incremental() %}\n    WHERE updated_at >= (SELECT coalesce(max(payment_created_at), '1970-01-01') FROM {{ this }})\n  {% endif %}\n),\n\norders_source AS (\n  SELECT\n    order_id,\n    customer_id,\n    order_number,\n    order_status,\n    total_amount_usd,\n    tax_amount_usd,\n    discount_amount_usd,\n    currency_code,\n    order_date,\n    created_at,\n    updated_at\n  FROM {{ ref('stg_shopify__orders') }}\n  {% if is_incremental() %}\n    WHERE updated_at >= (SELECT coalesce(max(updated_at), '1970-01-01') FROM {{ this }})\n  {% endif %}\n),\n\ncustomers AS (\n  SELECT\n    customer_id,\n    customer_tier,\n    country_code,\n    lifetime_order_count\n  FROM {{ ref('dim_customers') }}\n),\n\naggregated_payments AS (\n  SELECT\n    order_id,\n    sum(case when payment_status = 'succeeded' then amount_usd else 0 end) as total_paid_usd,\n    sum(gateway_fee_usd) as total_fees_usd,\n    count(payment_id) as payment_attempts_count,\n    max(payment_created_at) as last_payment_at\n  FROM payments\n  GROUP BY 1\n),\n\nfinal_model AS (\n  SELECT\n    ord.order_id,\n    ord.order_number,\n    ord.customer_id,\n    cust.customer_tier,\n    cust.country_code,\n    ord.order_status,\n    ord.order_date,\n    ord.currency_code,\n    ord.total_amount_usd,\n    ord.tax_amount_usd,\n    ord.discount_amount_usd,\n    coalesce(pay.total_paid_usd, 0.00) as total_paid_usd,\n    coalesce(pay.total_fees_usd, 0.00) as payment_fees_usd,\n    round(ord.total_amount_usd - coalesce(pay.total_fees_usd, 0.00), 2) as net_revenue_usd,\n    case\n      when pay.total_paid_usd >= ord.total_amount_usd then 'fully_paid'\n      when pay.total_paid_usd > 0 then 'partially_paid'\n      else 'unpaid'\n    end as payment_settlement_status,\n    coalesce(pay.payment_attempts_count, 0) as payment_attempts,\n    ord.created_at,\n    ord.updated_at\n  FROM orders_source ord\n  LEFT JOIN aggregated_payments pay ON ord.order_id = pay.order_id\n  LEFT JOIN customers cust ON ord.customer_id = cust.customer_id\n)\n\nSELECT * FROM final_model`\n\nconst compiledSqlSource = `-- Compiled target warehouse query: Snowflake / AWS us-east-1\n-- Execution target: \"ANALYTICS_PROD\".\"MARTS\".\"fct_orders\"\n-- Incremental Merge Strategy with cluster keys [order_date, customer_id]\n\nMERGE INTO \"ANALYTICS_PROD\".\"MARTS\".\"fct_orders\" AS target\nUSING (\n  WITH payments AS (\n    SELECT\n      payment_id,\n      order_id,\n      payment_method,\n      amount_usd,\n      gateway_fee_usd,\n      status AS payment_status,\n      created_at AS payment_created_at\n    FROM \"ANALYTICS_PROD\".\"STAGING\".\"stg_stripe__payments\"\n    WHERE updated_at >= (SELECT coalesce(max(payment_created_at), '1970-01-01') FROM \"ANALYTICS_PROD\".\"MARTS\".\"fct_orders\")\n  ),\n\n  orders_source AS (\n    SELECT\n      order_id,\n      customer_id,\n      order_number,\n      order_status,\n      total_amount_usd,\n      tax_amount_usd,\n      discount_amount_usd,\n      currency_code,\n      order_date,\n      created_at,\n      updated_at\n    FROM \"ANALYTICS_PROD\".\"STAGING\".\"stg_shopify__orders\"\n    WHERE updated_at >= (SELECT coalesce(max(updated_at), '1970-01-01') FROM \"ANALYTICS_PROD\".\"MARTS\".\"fct_orders\")\n  ),\n\n  customers AS (\n    SELECT\n      customer_id,\n      customer_tier,\n      country_code,\n      lifetime_order_count\n    FROM \"ANALYTICS_PROD\".\"CORE\".\"dim_customers\"\n  ),\n\n  aggregated_payments AS (\n    SELECT\n      order_id,\n      sum(case when payment_status = 'succeeded' then amount_usd else 0 end) as total_paid_usd,\n      sum(gateway_fee_usd) as total_fees_usd,\n      count(payment_id) as payment_attempts_count,\n      max(payment_created_at) as last_payment_at\n    FROM payments\n    GROUP BY 1\n  ),\n\n  final_model AS (\n    SELECT\n      ord.order_id,\n      ord.order_number,\n      ord.customer_id,\n      cust.customer_tier,\n      cust.country_code,\n      ord.order_status,\n      ord.order_date,\n      ord.currency_code,\n      ord.total_amount_usd,\n      ord.tax_amount_usd,\n      ord.discount_amount_usd,\n      coalesce(pay.total_paid_usd, 0.00) as total_paid_usd,\n      coalesce(pay.total_fees_usd, 0.00) as payment_fees_usd,\n      round(ord.total_amount_usd - coalesce(pay.total_fees_usd, 0.00), 2) as net_revenue_usd,\n      case\n        when pay.total_paid_usd >= ord.total_amount_usd then 'fully_paid'\n        when pay.total_paid_usd > 0 then 'partially_paid'\n        else 'unpaid'\n      end as payment_settlement_status,\n      coalesce(pay.payment_attempts_count, 0) as payment_attempts,\n      ord.created_at,\n      ord.updated_at\n    FROM orders_source ord\n    LEFT JOIN aggregated_payments pay ON ord.order_id = pay.order_id\n    LEFT JOIN customers cust ON ord.customer_id = cust.customer_id\n  )\n\n  SELECT * FROM final_model\n) AS source\nON target.order_id = source.order_id\nWHEN MATCHED THEN UPDATE SET\n  order_number = source.order_number,\n  customer_id = source.customer_id,\n  customer_tier = source.customer_tier,\n  country_code = source.country_code,\n  order_status = source.order_status,\n  order_date = source.order_date,\n  total_amount_usd = source.total_amount_usd,\n  tax_amount_usd = source.tax_amount_usd,\n  discount_amount_usd = source.discount_amount_usd,\n  total_paid_usd = source.total_paid_usd,\n  payment_fees_usd = source.payment_fees_usd,\n  net_revenue_usd = source.net_revenue_usd,\n  payment_settlement_status = source.payment_settlement_status,\n  payment_attempts = source.payment_attempts,\n  updated_at = source.updated_at\nWHEN NOT MATCHED THEN INSERT (\n  order_id, order_number, customer_id, customer_tier, country_code,\n  order_status, order_date, currency_code, total_amount_usd, tax_amount_usd,\n  discount_amount_usd, total_paid_usd, payment_fees_usd, net_revenue_usd,\n  payment_settlement_status, payment_attempts, created_at, updated_at\n) VALUES (\n  source.order_id, source.order_number, source.customer_id, source.customer_tier, source.country_code,\n  source.order_status, source.order_date, source.currency_code, source.total_amount_usd, source.tax_amount_usd,\n  source.discount_amount_usd, source.total_paid_usd, source.payment_fees_usd, source.net_revenue_usd,\n  source.payment_settlement_status, source.payment_attempts, source.created_at, source.updated_at\n);`\n\nconst upstreamNodes: UpstreamNode[] = [\n  {\n    id: 'stg_stripe__payments',\n    name: 'stg_stripe__payments',\n    type: 'view',\n    package: 'analytics_dw.staging.stripe',\n    rowCount: '1,420,800 rows',\n    freshness: 'Pass · 12m ago',\n    freshnessStatus: 'pass',\n    description: 'Raw webhook payment charges, refunds, and gateway fee captures from Stripe API.',\n  },\n  {\n    id: 'stg_shopify__orders',\n    name: 'stg_shopify__orders',\n    type: 'view',\n    package: 'analytics_dw.staging.shopify',\n    rowCount: '984,200 rows',\n    freshness: 'Pass · 5m ago',\n    freshnessStatus: 'pass',\n    description: 'E-commerce transactional headers, tax summaries, discount allocations, and checkout states.',\n  },\n  {\n    id: 'dim_customers',\n    name: 'dim_customers',\n    type: 'table',\n    package: 'analytics_dw.core',\n    rowCount: '248,500 rows',\n    freshness: 'Pass · 1h ago',\n    freshnessStatus: 'pass',\n    description: 'Type-2 slowly changing dimension master record for authenticated customer accounts.',\n  },\n]\n\nconst downstreamNodes: DownstreamNode[] = [\n  {\n    id: 'mart_finance_mrr',\n    name: 'mart_finance_mrr',\n    type: 'table',\n    package: 'analytics_dw.marts.finance',\n    consumers: 'Finance Team & NetSuite Sync',\n    sla: 'Tier 1 · Daily 06:00 UTC',\n    description: 'Monthly recurring revenue waterfall and cohort retention analysis table.',\n  },\n  {\n    id: 'mart_executive_kpis',\n    name: 'mart_executive_kpis',\n    type: 'table',\n    package: 'analytics_dw.marts.executive',\n    consumers: 'Executive Dashboard & Board Reports',\n    sla: 'Tier 1 · Daily 07:00 UTC',\n    description: 'High-level business health rollup metrics: GMV, net revenue, CAC payback, and refund rates.',\n  },\n  {\n    id: 'tableau_revenue_dashboard',\n    name: 'tableau_revenue_dashboard',\n    type: 'exposure',\n    package: 'exposures.bi.tableau',\n    consumers: '120 Active BI Users',\n    sla: 'Hourly Sync',\n    description: 'Production executive revenue tracking workbook and daily pacing alerts.',\n  },\n]\n\nconst columns: ModelColumn[] = [\n  {\n    name: 'order_id',\n    dataType: 'STRING (VARCHAR)',\n    nullable: false,\n    isPrimaryKey: true,\n    tests: [\n      { name: 'unique', status: 'pass' },\n      { name: 'not_null', status: 'pass' },\n    ],\n    testDuration: '12.4ms',\n    description: 'Surrogate primary key generated for each transactional order record.',\n  },\n  {\n    name: 'order_number',\n    dataType: 'STRING (VARCHAR)',\n    nullable: false,\n    tests: [\n      { name: 'unique', status: 'pass' },\n      { name: 'not_null', status: 'pass' },\n    ],\n    testDuration: '14.1ms',\n    description: 'Human-readable sequential invoice reference number from checkout.',\n  },\n  {\n    name: 'customer_id',\n    dataType: 'STRING (VARCHAR)',\n    nullable: false,\n    isForeignKey: true,\n    foreignKeyRef: 'dim_customers.customer_id',\n    tests: [\n      { name: 'not_null', status: 'pass' },\n      { name: 'relationships', status: 'pass', detail: 'dim_customers' },\n    ],\n    testDuration: '28.6ms',\n    description: 'Foreign key reference linking order to the verified master customer account.',\n  },\n  {\n    name: 'customer_tier',\n    dataType: 'STRING (VARCHAR)',\n    nullable: true,\n    tests: [\n      {\n        name: 'accepted_values',\n        status: 'pass',\n        detail: 'enterprise, pro, starter, free',\n      },\n    ],\n    testDuration: '9.2ms',\n    description: 'Customer loyalty and billing subscription tier at the time of purchase.',\n  },\n  {\n    name: 'country_code',\n    dataType: 'STRING (CHAR(2))',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '8.4ms',\n    description: 'ISO 3166-1 alpha-2 two-letter billing destination country code.',\n  },\n  {\n    name: 'order_status',\n    dataType: 'STRING (VARCHAR)',\n    nullable: false,\n    tests: [\n      {\n        name: 'accepted_values',\n        status: 'pass',\n        detail: 'completed, processing, shipped, cancelled, refunded',\n      },\n      { name: 'not_null', status: 'pass' },\n    ],\n    testDuration: '15.3ms',\n    description: 'Current order fulfillment and delivery lifecycle state.',\n  },\n  {\n    name: 'order_date',\n    dataType: 'DATE',\n    nullable: false,\n    isClusterKey: true,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '7.9ms',\n    description: 'Calendar transaction date used as primary cluster and pruning key.',\n  },\n  {\n    name: 'currency_code',\n    dataType: 'STRING (CHAR(3))',\n    nullable: false,\n    tests: [\n      {\n        name: 'accepted_values',\n        status: 'pass',\n        detail: 'USD, EUR, GBP, CAD',\n      },\n    ],\n    testDuration: '6.8ms',\n    description: 'ISO 4217 three-letter currency code in which the charge was denominated.',\n  },\n  {\n    name: 'total_amount_usd',\n    dataType: 'DECIMAL(12,2)',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '10.1ms',\n    description: 'Gross order amount in base USD currency including tax and discounts.',\n  },\n  {\n    name: 'tax_amount_usd',\n    dataType: 'DECIMAL(10,2)',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '9.0ms',\n    description: 'Total sales tax and VAT amount captured for remittance.',\n  },\n  {\n    name: 'discount_amount_usd',\n    dataType: 'DECIMAL(10,2)',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '8.1ms',\n    description: 'Promotional discount and coupon deductions applied at checkout.',\n  },\n  {\n    name: 'total_paid_usd',\n    dataType: 'DECIMAL(12,2)',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '11.3ms',\n    description: 'Sum of all verified cleared customer payments from Stripe ledger.',\n  },\n  {\n    name: 'payment_fees_usd',\n    dataType: 'DECIMAL(10,2)',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '9.8ms',\n    description: 'Merchant interchange and gateway transaction processing fees.',\n  },\n  {\n    name: 'net_revenue_usd',\n    dataType: 'DECIMAL(12,2)',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '10.5ms',\n    description: 'Net recognized revenue after deducting gateway processing fees.',\n  },\n  {\n    name: 'payment_settlement_status',\n    dataType: 'STRING (VARCHAR)',\n    nullable: false,\n    tests: [\n      {\n        name: 'accepted_values',\n        status: 'pass',\n        detail: 'fully_paid, partially_paid, unpaid',\n      },\n      { name: 'not_null', status: 'pass' },\n    ],\n    testDuration: '8.7ms',\n    description: 'Payment clearing and reconciliation flag calculated against order total.',\n  },\n  {\n    name: 'payment_attempts',\n    dataType: 'INTEGER',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '6.2ms',\n    description: 'Number of authorization attempts recorded in the payment gateway.',\n  },\n  {\n    name: 'created_at',\n    dataType: 'TIMESTAMP_TZ',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '8.0ms',\n    description: 'Initial order creation timestamp in UTC timezone.',\n  },\n  {\n    name: 'updated_at',\n    dataType: 'TIMESTAMP_TZ',\n    nullable: false,\n    tests: [{ name: 'not_null', status: 'pass' }],\n    testDuration: '8.3ms',\n    description: 'Last modified timestamp used as incremental watermark boundary.',\n  },\n]\n\nfunction escapeHtml(str: string): string {\n  return str\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#039;')\n}\n\nexport function highlightJinjaSqlLine(line: string): string {\n  if (!line) return ''\n  const escaped = escapeHtml(line)\n\n  // Comments\n  if (escaped.trim().startsWith('--') || escaped.trim().startsWith('{#')) {\n    return `<span class=\"text-muted-foreground/60 italic\">${escaped}</span>`\n  }\n\n  // Highlight Jinja tags: {{ ... }} and {% ... %}\n  let result = escaped.replace(/(\\{\\{[\\s\\S]*?\\}\\}|\\{%[\\s\\S]*?%\\})/g, (jinjaMatch) => {\n    const inner = jinjaMatch\n      .replace(\n        /\\b(config|ref|source|is_incremental|this|var|materialized|unique_key|on_schema_change|incremental_strategy|cluster_by|tags)\\b/g,\n        '<span class=\"text-warning font-semibold\">$1</span>',\n      )\n      .replace(/(\\{\\{|\\}\\}|\\{%|%\\})/g, '<span class=\"text-chart-1 font-bold\">$1</span>')\n    return `<span class=\"bg-chart-1/10 px-1 py-0.5 rounded text-chart-1\">${inner}</span>`\n  })\n\n  // SQL Keywords\n  result = result.replace(\n    /\\b(WITH|SELECT|FROM|JOIN|LEFT JOIN|INNER JOIN|RIGHT JOIN|FULL JOIN|ON|WHERE|GROUP BY|ORDER BY|AND|OR|NOT|AS|CASE|WHEN|THEN|ELSE|END|MERGE INTO|USING|WHEN MATCHED|WHEN NOT MATCHED|INSERT|UPDATE|SET|VALUES|OVER|PARTITION BY)\\b/g,\n    '<span class=\"text-info font-semibold\">$1</span>',\n  )\n\n  // SQL Functions\n  result = result.replace(\n    /\\b(coalesce|sum|count|max|min|avg|round|cast|date_trunc|concat)\\b/gi,\n    '<span class=\"text-chart-2 font-medium\">$1</span>',\n  )\n\n  // Strings\n  result = result.replace(/(&#039;[^&#039;]*&#039;)/g, '<span class=\"text-success\">$1</span>')\n\n  return result\n}\n\nexport function highlightCompiledSqlLine(line: string): string {\n  if (!line) return ''\n  const escaped = escapeHtml(line)\n\n  // Comments\n  if (escaped.trim().startsWith('--')) {\n    return `<span class=\"text-muted-foreground/60 italic\">${escaped}</span>`\n  }\n\n  let result = escaped.replace(\n    /\\b(MERGE INTO|USING|WHEN MATCHED|WHEN NOT MATCHED|THEN UPDATE SET|THEN INSERT|VALUES|WITH|SELECT|FROM|LEFT JOIN|INNER JOIN|RIGHT JOIN|JOIN|ON|WHERE|GROUP BY|ORDER BY|AND|OR|NOT|AS|CASE|WHEN|THEN|ELSE|END)\\b/g,\n    '<span class=\"text-info font-semibold\">$1</span>',\n  )\n\n  result = result.replace(\n    /\\b(coalesce|sum|count|max|min|avg|round|cast)\\b/gi,\n    '<span class=\"text-chart-2 font-medium\">$1</span>',\n  )\n\n  // Identifiers in quotes\n  result = result.replace(/(&quot;[A-Z0-9_]+&quot;)/g, '<span class=\"text-warning font-medium\">$1</span>')\n\n  // Strings\n  result = result.replace(/(&#039;[^&#039;]*&#039;)/g, '<span class=\"text-success\">$1</span>')\n\n  return result\n}\n\nconst NODE_BASE =\n  'focus-visible:ring-ring flex w-full cursor-pointer flex-col rounded-md border p-1.5 text-left transition-[background-color,box-shadow] focus-visible:ring-2 focus-visible:outline-none'\n\n/**\n * dbt Cloud style model studio: DAG lineage, model docs, Jinja / compiled SQL viewers and\n * the column contract test suite. Port of the React `DbtModelGraph` block, 1:1.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dbt-model-graph, [ui-dbt-model-graph]',\n  standalone: true,\n  // React renders the root <div> itself: keep this wrapper out of the layout.\n  host: { class: 'contents' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiInputComponent,\n    UiTableComponent,\n    UiTableHeaderComponent,\n    UiTableBodyComponent,\n    UiTableRowComponent,\n    UiTableHeadComponent,\n    UiTableCellComponent,\n    UiTabsComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n    UiTabsContentComponent,\n  ],\n  template: `\n    <div data-slot=\"dbt-model-graph\" [class]=\"rootClass\">\n      <!-- TOP HEADER -->\n      <header class=\"border-border bg-card/70 border-b px-4 py-3 sm:px-6\">\n        <div class=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n          <!-- Left: Model Identity & Badges -->\n          <div class=\"flex flex-wrap items-center gap-2.5\">\n            <div class=\"bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg shadow-xs\">\n              <lucide-icon [img]=\"Workflow\" class=\"size-4\" />\n            </div>\n\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <div class=\"flex items-center gap-1.5 font-mono text-sm font-semibold tracking-tight\">\n                <span class=\"text-muted-foreground font-normal\">model.analytics.</span>\n                <span class=\"text-foreground\">fct_orders</span>\n              </div>\n\n              <!-- Copy ref pill -->\n              <button\n                type=\"button\"\n                class=\"border-border bg-muted/50 text-muted-foreground hover:text-foreground hover:bg-muted inline-flex min-h-6 items-center gap-1 rounded-md border px-2 py-0.5 font-mono text-xs transition-colors\"\n                title=\"Copy dbt ref() macro\"\n                (click)=\"copyModelRef()\"\n              >\n                @if (copiedRef()) {\n                  <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                } @else {\n                  <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                }\n                <span>{{ copiedRef() ? 'Copied Ref!' : dbtMacroRef }}</span>\n              </button>\n            </div>\n\n            <div class=\"flex flex-wrap items-center gap-1.5\">\n              <ui-badge wrap variant=\"outline\" class=\"gap-1 font-mono text-xs font-normal\">\n                <lucide-icon [img]=\"Layers\" class=\"text-info size-3\" />\n                Table · Incremental\n              </ui-badge>\n\n              <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">analytics_dw v1.8</ui-badge>\n\n              <ui-badge\n                wrap\n                variant=\"outline\"\n                class=\"border-success/30 bg-success/10 text-success gap-1.5 font-mono text-xs font-medium\"\n              >\n                <span class=\"bg-success size-1.5 animate-pulse rounded-full\"></span>\n                {{ buildStatusText() }}\n              </ui-badge>\n            </div>\n          </div>\n\n          <!-- Right: Primary & Secondary Actions -->\n          <div class=\"flex flex-wrap items-center gap-2\">\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-8 gap-1.5 text-xs font-medium\"\n              [disabled]=\"isTesting()\"\n              (click)=\"handleTestModel()\"\n            >\n              @if (isTesting()) {\n                <lucide-icon [img]=\"Loader2\" class=\"size-3.5 animate-spin\" />\n              } @else {\n                <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-3.5\" />\n              }\n              {{ isTesting() ? 'Running 7 Tests...' : 'Test Model (dbt test)' }}\n            </button>\n\n            <button\n              ui-button\n              variant=\"default\"\n              size=\"sm\"\n              class=\"bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 text-xs font-semibold shadow-xs\"\n              [disabled]=\"isBuilding()\"\n              (click)=\"handleBuildModel()\"\n            >\n              @if (isBuilding()) {\n                <lucide-icon [img]=\"Loader2\" class=\"size-3.5 animate-spin\" />\n              } @else {\n                <lucide-icon [img]=\"Play\" class=\"size-3.5 fill-current\" />\n              }\n              {{ isBuilding() ? 'Building (dbt run)...' : 'Build Model (dbt run)' }}\n            </button>\n          </div>\n        </div>\n\n        <!-- Secondary Metadata Strip -->\n        <div\n          class=\"border-border/60 text-muted-foreground mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 border-t pt-2.5 font-mono text-xs\"\n        >\n          <div class=\"flex items-center gap-1.5\">\n            <lucide-icon [img]=\"Database\" class=\"text-info size-3\" />\n            <span>Warehouse:</span>\n            <span class=\"text-foreground font-medium\">Snowflake PROD_WH (XS)</span>\n          </div>\n          <div class=\"flex items-center gap-1.5\">\n            <lucide-icon [img]=\"HardDrive\" class=\"text-warning size-3\" />\n            <span>Target Schema:</span>\n            <span class=\"text-foreground font-medium\">ANALYTICS_PROD.MARTS</span>\n          </div>\n          <div class=\"flex items-center gap-1.5\">\n            <lucide-icon [img]=\"Clock\" class=\"text-muted-foreground size-3\" />\n            <span>Last Built:</span>\n            <span class=\"text-foreground\">{{ lastBuildTime() }}</span>\n          </div>\n          <div class=\"flex items-center gap-1.5\">\n            <lucide-icon [img]=\"ShieldCheck\" class=\"text-success size-3\" />\n            <span>Contract Tests:</span>\n            <span class=\"text-success font-medium\">7/7 Passing</span>\n          </div>\n          <div class=\"flex items-center gap-1.5\">\n            <lucide-icon [img]=\"Table2\" class=\"text-chart-1 size-3\" />\n            <span>Row Count:</span>\n            <span class=\"text-foreground\">2,840,190 rows</span>\n          </div>\n        </div>\n      </header>\n\n      <!-- 2-COLUMN MODEL STUDIO LAYOUT -->\n      <div class=\"divide-border grid flex-1 grid-cols-1 divide-y lg:grid-cols-12 lg:divide-x lg:divide-y-0\">\n        <!-- LEFT PANEL: METADATA & DAG LINEAGE -->\n        <aside class=\"bg-muted/15 flex flex-col space-y-4 p-4 sm:p-5 lg:col-span-5\">\n          <!-- Interactive Lineage DAG Card -->\n          <ui-card class=\"border-border bg-card overflow-hidden border p-3.5 shadow-none\">\n            <div class=\"flex flex-wrap items-center justify-between pb-2.5\">\n              <div class=\"flex items-center gap-1.5 text-xs font-semibold\">\n                <lucide-icon [img]=\"GitBranch\" class=\"text-primary size-3.5\" />\n                <span>DAG Lineage Dependency Graph</span>\n              </div>\n              <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">\n                -1 Upstream · +1 Downstream\n              </ui-badge>\n            </div>\n\n            <!-- Visual DAG Node Canvas -->\n            <div class=\"border-border/70 bg-muted/30 relative rounded-lg border p-3\">\n              <div class=\"grid grid-cols-3 items-center gap-2 text-xs\">\n                <!-- Upstream Column -->\n                <div class=\"space-y-1.5\">\n                  <span class=\"text-muted-foreground block text-center font-mono text-xs font-medium\">\n                    Upstream (3)\n                  </span>\n                  @for (node of upstreamNodes; track node.id) {\n                    <button\n                      type=\"button\"\n                      [class]=\"nodeClass(node.id)\"\n                      [attr.aria-pressed]=\"selectedNodeId() === node.id\"\n                      (click)=\"selectedNodeId.set(node.id)\"\n                    >\n                      <div class=\"flex flex-wrap items-center justify-between\">\n                        <span class=\"truncate font-mono text-xs font-medium\">{{ node.name }}</span>\n                      </div>\n                      <div\n                        class=\"text-muted-foreground flex flex-wrap items-center justify-between pt-0.5 font-mono text-xs\"\n                      >\n                        <span class=\"uppercase\">{{ node.type }}</span>\n                        <span class=\"bg-success size-1.5 rounded-full\"></span>\n                      </div>\n                    </button>\n                  }\n                </div>\n\n                <!-- Center Current Node -->\n                <div class=\"flex flex-col items-center justify-center space-y-1 px-1\">\n                  <span class=\"text-primary font-mono text-xs font-semibold\">Current Model</span>\n                  <button\n                    type=\"button\"\n                    [class]=\"currentNodeClass()\"\n                    [attr.aria-pressed]=\"selectedNodeId() === 'fct_orders'\"\n                    (click)=\"selectedNodeId.set('fct_orders')\"\n                  >\n                    <lucide-icon [img]=\"Workflow\" class=\"text-primary mb-1 size-4\" />\n                    <span class=\"text-foreground font-mono text-xs font-bold\">fct_orders</span>\n                    <ui-badge wrap variant=\"outline\" class=\"border-primary/40 mt-1 font-mono text-xs\">\n                      incremental\n                    </ui-badge>\n                  </button>\n                </div>\n\n                <!-- Downstream Column -->\n                <div class=\"space-y-1.5\">\n                  <span class=\"text-muted-foreground block text-center font-mono text-xs font-medium\">\n                    Downstream (3)\n                  </span>\n                  @for (node of downstreamNodes; track node.id) {\n                    <button\n                      type=\"button\"\n                      [class]=\"nodeClass(node.id)\"\n                      [attr.aria-pressed]=\"selectedNodeId() === node.id\"\n                      (click)=\"selectedNodeId.set(node.id)\"\n                    >\n                      <div class=\"flex flex-wrap items-center justify-between\">\n                        <span class=\"truncate font-mono text-xs font-medium\">{{ node.name }}</span>\n                      </div>\n                      <div\n                        class=\"text-muted-foreground flex flex-wrap items-center justify-between pt-0.5 font-mono text-xs\"\n                      >\n                        <span class=\"uppercase\">{{ node.type }}</span>\n                        <span class=\"bg-success size-1.5 rounded-full\"></span>\n                      </div>\n                    </button>\n                  }\n                </div>\n              </div>\n            </div>\n          </ui-card>\n\n          <!-- Model Documentation & Metadata Card -->\n          <ui-card class=\"border-border bg-card space-y-3 border p-4 shadow-none\">\n            <div class=\"border-border flex flex-wrap items-center justify-between border-b pb-2\">\n              <div class=\"flex items-center gap-1.5 text-xs font-semibold\">\n                <lucide-icon [img]=\"Info\" class=\"text-primary size-3.5\" />\n                <span>Model Documentation &amp; Metadata</span>\n              </div>\n              <ui-badge wrap variant=\"outline\" class=\"font-mono text-xs\">Contract Enforced</ui-badge>\n            </div>\n\n            <p class=\"text-muted-foreground text-xs leading-relaxed\">\n              Core transactional orders fact table at individual order grain. Captures gross revenue metrics, sales tax\n              breakdowns, promotional discount allocations, payment gateway fees, and settlement lifecycle stages.\n            </p>\n\n            <div class=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n              <div class=\"border-border/60 bg-muted/20 space-y-0.5 rounded-md border p-2 text-xs\">\n                <span class=\"text-muted-foreground text-xs\">Model Owner</span>\n                <div class=\"flex items-center gap-1.5 font-medium\">\n                  <lucide-icon [img]=\"User\" class=\"text-primary size-3\" />\n                  <span>Analytics Engineering</span>\n                </div>\n              </div>\n\n              <div class=\"border-border/60 bg-muted/20 space-y-0.5 rounded-md border p-2 text-xs\">\n                <span class=\"text-muted-foreground text-xs\">Freshness SLA</span>\n                <div class=\"text-success flex items-center gap-1.5 font-medium\">\n                  <lucide-icon [img]=\"Zap\" class=\"text-warning size-3\" />\n                  <span>&lt; 2 hours (Compliant)</span>\n                </div>\n              </div>\n            </div>\n\n            <!-- Tags -->\n            <div class=\"space-y-1.5 pt-1\">\n              <span class=\"text-muted-foreground flex items-center gap-1 text-xs\">\n                <lucide-icon [img]=\"Tag\" class=\"size-3\" />\n                Model Tags\n              </span>\n              <div class=\"flex flex-wrap items-center gap-1.5\">\n                <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">#finance</ui-badge>\n                <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">#core</ui-badge>\n                <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">#daily_sla</ui-badge>\n                <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">#tier-1-kpi</ui-badge>\n              </div>\n            </div>\n\n            <!-- Cluster Keys & Strategy -->\n            <div class=\"border-border/60 bg-muted/30 space-y-1.5 rounded-lg border p-2.5 font-mono text-xs\">\n              <div class=\"flex justify-between\">\n                <span class=\"text-muted-foreground\">Incremental Strategy:</span>\n                <span class=\"text-foreground font-semibold\">Merge (unique_key: order_id)</span>\n              </div>\n              <div class=\"flex justify-between\">\n                <span class=\"text-muted-foreground\">Cluster Keys:</span>\n                <span class=\"text-foreground font-semibold\">order_date, customer_id</span>\n              </div>\n            </div>\n          </ui-card>\n\n          <!-- Upstream Sources & References List -->\n          <ui-card class=\"border-border bg-card space-y-2.5 border p-4 shadow-none\">\n            <div class=\"flex flex-wrap items-center justify-between\">\n              <div class=\"flex items-center gap-1.5 text-xs font-semibold\">\n                <lucide-icon [img]=\"Link2\" class=\"text-info size-3.5\" />\n                <span>Upstream Sources &amp; References ({{ upstreamNodes.length }})</span>\n              </div>\n              <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">\n                {{ upstreamNodes.length }} models\n              </ui-badge>\n            </div>\n\n            <div class=\"space-y-2\">\n              @for (node of upstreamNodes; track node.id) {\n                <div\n                  class=\"border-border/70 bg-muted/20 hover:bg-muted/40 rounded-lg border p-2.5 text-xs transition-colors\"\n                >\n                  <div class=\"flex flex-wrap items-center justify-between\">\n                    <span class=\"text-foreground font-mono font-semibold\">{{ node.name }}</span>\n                    <ui-badge wrap variant=\"outline\" class=\"font-mono text-xs uppercase\">{{ node.type }}</ui-badge>\n                  </div>\n                  <p class=\"text-muted-foreground mt-1 text-xs\">{{ node.description }}</p>\n                  <div class=\"text-muted-foreground mt-2 flex flex-wrap items-center justify-between font-mono text-xs\">\n                    <span>{{ node.rowCount }}</span>\n                    <span class=\"text-success font-medium\">{{ node.freshness }}</span>\n                  </div>\n                </div>\n              }\n            </div>\n          </ui-card>\n\n          <!-- Downstream Marts & BI Exposures List -->\n          <ui-card class=\"border-border bg-card space-y-2.5 border p-4 shadow-none\">\n            <div class=\"flex flex-wrap items-center justify-between\">\n              <div class=\"flex items-center gap-1.5 text-xs font-semibold\">\n                <lucide-icon [img]=\"ArrowUpRight\" class=\"text-warning size-3.5\" />\n                <span>Downstream Marts &amp; BI Exposures ({{ downstreamNodes.length }})</span>\n              </div>\n              <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">\n                {{ downstreamNodes.length }} targets\n              </ui-badge>\n            </div>\n\n            <div class=\"space-y-2\">\n              @for (node of downstreamNodes; track node.id) {\n                <div\n                  class=\"border-border/70 bg-muted/20 hover:bg-muted/40 rounded-lg border p-2.5 text-xs transition-colors\"\n                >\n                  <div class=\"flex flex-wrap items-center justify-between\">\n                    <span class=\"text-foreground font-mono font-semibold\">{{ node.name }}</span>\n                    <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs uppercase\">{{ node.type }}</ui-badge>\n                  </div>\n                  <p class=\"text-muted-foreground mt-1 text-xs\">{{ node.description }}</p>\n                  <div class=\"text-muted-foreground mt-2 flex flex-wrap items-center justify-between font-mono text-xs\">\n                    <span class=\"truncate pr-2\">Consumers: {{ node.consumers }}</span>\n                    <span class=\"text-foreground shrink-0 font-medium\">{{ node.sla }}</span>\n                  </div>\n                </div>\n              }\n            </div>\n          </ui-card>\n        </aside>\n\n        <!-- RIGHT PANEL: TABS STUDIO -->\n        <main class=\"bg-card flex flex-col overflow-hidden lg:col-span-7\">\n          <ui-tabs [value]=\"activeTab()\" (valueChange)=\"setActiveTab($event)\" class=\"flex flex-1 flex-col\">\n            <!-- Right Tab Header -->\n            <div class=\"border-border bg-muted/30 flex flex-wrap items-center justify-between gap-2 border-b px-4 py-2\">\n              <div ui-tabs-list class=\"grid h-8 grid-cols-3\">\n                <button ui-tabs-trigger value=\"model-sql\" class=\"gap-1.5 text-xs\">\n                  <lucide-icon [img]=\"Code2\" class=\"text-primary size-3.5\" />\n                  <span>Model SQL</span>\n                </button>\n                <button ui-tabs-trigger value=\"compiled-sql\" class=\"gap-1.5 text-xs\">\n                  <lucide-icon [img]=\"FileCode\" class=\"text-info size-3.5\" />\n                  <span>Compiled SQL</span>\n                </button>\n                <button ui-tabs-trigger value=\"contract\" class=\"gap-1.5 text-xs\">\n                  <lucide-icon [img]=\"ShieldCheck\" class=\"text-success size-3.5\" />\n                  <span>Model Contract &amp; Tests</span>\n                </button>\n              </div>\n\n              <!-- Actions per active tab -->\n              <div class=\"flex items-center gap-2\">\n                @if (activeTab() === 'model-sql') {\n                  <button ui-button variant=\"ghost\" size=\"sm\" class=\"h-7 gap-1 text-xs\" (click)=\"copySourceSql()\">\n                    @if (copiedSql()) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                    }\n                    <span>{{ copiedSql() ? 'Copied' : 'Copy SQL' }}</span>\n                  </button>\n                }\n\n                @if (activeTab() === 'compiled-sql') {\n                  <button ui-button variant=\"ghost\" size=\"sm\" class=\"h-7 gap-1 text-xs\" (click)=\"copyCompiledSql()\">\n                    @if (copiedCompiled()) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                    }\n                    <span>{{ copiedCompiled() ? 'Copied' : 'Copy Compiled' }}</span>\n                  </button>\n                }\n\n                @if (activeTab() === 'contract') {\n                  <ui-badge wrap variant=\"outline\" class=\"text-success font-mono text-xs\">\n                    {{ passingTestsCount }} Tests Passing\n                  </ui-badge>\n                }\n              </div>\n            </div>\n\n            <!-- TAB 1: MODEL SQL -->\n            <ui-tabs-content value=\"model-sql\" class=\"mt-0 flex flex-1 flex-col overflow-hidden\">\n              <div\n                class=\"border-border bg-muted/15 text-muted-foreground flex flex-wrap items-center justify-between border-b px-4 py-1.5 font-mono text-xs\"\n              >\n                <div class=\"flex items-center gap-2\">\n                  <span class=\"text-foreground font-medium\">fct_orders.sql</span>\n                  <span>·</span>\n                  <span>Jinja + Snowflake Dialect</span>\n                </div>\n                <div class=\"flex items-center gap-3\">\n                  <span>{{ modelSqlLines.length }} lines · {{ modelSqlSource.length }} chars</span>\n                </div>\n              </div>\n\n              <div class=\"relative flex flex-1 overflow-auto bg-neutral-950 text-neutral-100 dark:bg-neutral-950\">\n                <div\n                  class=\"w-11 shrink-0 overflow-hidden border-r border-neutral-800 bg-neutral-900/60 py-3 pr-2 text-right font-mono text-xs leading-relaxed text-neutral-500 select-none\"\n                >\n                  @for (line of modelSqlHtml; track $index) {\n                    <div>{{ $index + 1 }}</div>\n                  }\n                </div>\n                <div class=\"flex-1 overflow-auto p-3 font-mono text-xs leading-relaxed\">\n                  @for (html of modelSqlHtml; track $index) {\n                    <div class=\"font-mono whitespace-pre\" [innerHTML]=\"html\"></div>\n                  }\n                </div>\n              </div>\n            </ui-tabs-content>\n\n            <!-- TAB 2: COMPILED SQL -->\n            <ui-tabs-content value=\"compiled-sql\" class=\"mt-0 flex flex-1 flex-col overflow-hidden\">\n              <div\n                class=\"border-border bg-muted/15 text-muted-foreground flex flex-wrap items-center justify-between border-b px-4 py-1.5 font-mono text-xs\"\n              >\n                <div class=\"flex items-center gap-2\">\n                  <span class=\"text-foreground font-medium\">\n                    target/compiled/analytics_dw/models/marts/fct_orders.sql\n                  </span>\n                </div>\n                <div class=\"flex items-center gap-2\">\n                  <ui-badge wrap variant=\"outline\" class=\"font-mono text-xs\">\n                    Est. Cost: 0.14 Credits · 1.4 GB Scan\n                  </ui-badge>\n                </div>\n              </div>\n\n              <div class=\"relative flex flex-1 overflow-auto bg-neutral-950 text-neutral-100 dark:bg-neutral-950\">\n                <div\n                  class=\"w-11 shrink-0 overflow-hidden border-r border-neutral-800 bg-neutral-900/60 py-3 pr-2 text-right font-mono text-xs leading-relaxed text-neutral-500 select-none\"\n                >\n                  @for (line of compiledSqlHtml; track $index) {\n                    <div>{{ $index + 1 }}</div>\n                  }\n                </div>\n                <div class=\"flex-1 overflow-auto p-3 font-mono text-xs leading-relaxed\">\n                  @for (html of compiledSqlHtml; track $index) {\n                    <div class=\"font-mono whitespace-pre\" [innerHTML]=\"html\"></div>\n                  }\n                </div>\n              </div>\n            </ui-tabs-content>\n\n            <!-- TAB 3: MODEL CONTRACT & TESTS -->\n            <ui-tabs-content value=\"contract\" class=\"mt-0 flex flex-1 flex-col space-y-4 overflow-hidden p-4 sm:p-5\">\n              <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n                <div class=\"relative w-full sm:w-72\">\n                  <lucide-icon\n                    [img]=\"Search\"\n                    class=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\"\n                  />\n                  <ui-input\n                    [value]=\"columnSearch()\"\n                    (valueChange)=\"columnSearch.set($event)\"\n                    placeholder=\"Filter columns or tests...\"\n                    class=\"h-8 pl-8 font-mono text-xs\"\n                  />\n                </div>\n\n                <div class=\"flex flex-wrap items-center gap-2\">\n                  <ui-badge wrap variant=\"outline\" class=\"text-success gap-1 font-mono text-xs\">\n                    <lucide-icon [img]=\"ShieldCheck\" class=\"size-3\" />\n                    contract.enforced: true\n                  </ui-badge>\n                  <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs\">\n                    {{ filteredColumns().length }} of {{ columns.length }} columns\n                  </ui-badge>\n                </div>\n              </div>\n\n              <ui-card class=\"border-border flex-1 overflow-hidden border shadow-none\">\n                <div class=\"overflow-x-auto\">\n                  <div ui-table>\n                    <thead ui-table-header>\n                      <tr ui-table-row class=\"bg-muted/40 hover:bg-muted/40\">\n                        <th ui-table-head class=\"text-xs font-semibold\">Column Name</th>\n                        <th ui-table-head class=\"text-xs font-semibold\">Data Type</th>\n                        <th ui-table-head class=\"text-xs font-semibold\">Nullability</th>\n                        <th ui-table-head class=\"text-xs font-semibold\">Active Tests &amp; Constraints</th>\n                        <th ui-table-head class=\"text-xs font-semibold\">Latency</th>\n                        <th ui-table-head class=\"text-xs font-semibold\">Description</th>\n                      </tr>\n                    </thead>\n                    <tbody ui-table-body>\n                      @for (col of filteredColumns(); track col.name) {\n                        <tr ui-table-row class=\"text-xs\">\n                          <td ui-table-cell class=\"font-mono font-medium\">\n                            <div class=\"flex items-center gap-1.5\">\n                              @if (col.isPrimaryKey) {\n                                <lucide-icon [img]=\"Key\" class=\"text-warning size-3.5 shrink-0\" />\n                              } @else if (col.isForeignKey) {\n                                <lucide-icon [img]=\"Link2\" class=\"text-info size-3.5 shrink-0\" />\n                              }\n                              <span [class]=\"col.isPrimaryKey ? 'text-warning font-semibold' : ''\">{{ col.name }}</span>\n                            </div>\n                          </td>\n                          <td ui-table-cell>\n                            <ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs font-normal\">\n                              {{ col.dataType }}\n                            </ui-badge>\n                          </td>\n                          <td ui-table-cell>\n                            @if (!col.nullable) {\n                              <ui-badge wrap variant=\"outline\" class=\"text-muted-foreground font-mono text-xs\">\n                                NOT NULL\n                              </ui-badge>\n                            } @else {\n                              <span class=\"text-muted-foreground font-mono text-xs\">NULLABLE</span>\n                            }\n                          </td>\n                          <td ui-table-cell>\n                            <div class=\"flex flex-wrap items-center gap-1\">\n                              @if (col.isPrimaryKey) {\n                                <ui-badge wrap class=\"border-warning/30 bg-warning/15 text-warning font-mono text-xs\">\n                                  PK\n                                </ui-badge>\n                              }\n                              @if (col.isClusterKey) {\n                                <ui-badge\n                                  wrap\n                                  variant=\"secondary\"\n                                  class=\"border-chart-1/30 bg-chart-1/15 text-chart-1 font-mono text-xs\"\n                                >\n                                  CLUSTER\n                                </ui-badge>\n                              }\n                              @for (test of col.tests; track test.name) {\n                                <ui-badge wrap variant=\"secondary\" class=\"gap-1 font-mono text-xs\">\n                                  <span class=\"bg-success size-1.5 rounded-full\"></span>\n                                  {{ test.name }}{{ test.detail ? ' (' + test.detail + ')' : '' }}\n                                </ui-badge>\n                              }\n                            </div>\n                          </td>\n                          <td ui-table-cell class=\"text-muted-foreground font-mono text-xs\">{{ col.testDuration }}</td>\n                          <td ui-table-cell class=\"text-muted-foreground max-w-[220px] truncate text-xs\">\n                            {{ col.description }}\n                          </td>\n                        </tr>\n                      }\n                    </tbody>\n                  </div>\n                </div>\n              </ui-card>\n\n              <!-- Bottom Summary Banner -->\n              <div\n                class=\"border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 rounded-lg border p-3 text-xs\"\n              >\n                <div class=\"flex items-center gap-2\">\n                  <lucide-icon [img]=\"ShieldCheck\" class=\"text-success size-4\" />\n                  <span class=\"text-foreground font-medium\">Model Contract Coverage: 100% Enforced</span>\n                </div>\n                <div class=\"text-muted-foreground flex items-center gap-4 font-mono text-xs\">\n                  <span>18 Columns</span>\n                  <span>·</span>\n                  <span>21 Assertions</span>\n                  <span>·</span>\n                  <span class=\"text-success font-semibold\">0 Failures</span>\n                </div>\n              </div>\n            </ui-tabs-content>\n          </ui-tabs>\n        </main>\n      </div>\n    </div>\n  `,\n})\nexport class UiDbtModelGraphComponent {\n  @Input('class') className?: string\n\n  readonly activeTab = signal<DbtModelGraphTab>('model-sql')\n  readonly columnSearch = signal('')\n  readonly isBuilding = signal(false)\n  readonly isTesting = signal(false)\n  readonly copiedRef = signal(false)\n  readonly copiedSql = signal(false)\n  readonly copiedCompiled = signal(false)\n  readonly buildStatusText = signal('Success · Built in 14.2s')\n  readonly lastBuildTime = signal('12 mins ago')\n  readonly selectedNodeId = signal('fct_orders')\n\n  readonly dbtMacroRef = \"{{ ref('fct_orders') }}\"\n  readonly modelSqlSource = modelSqlSource\n  readonly modelSqlLines = modelSqlSource.split('\\n')\n  readonly modelSqlHtml = this.modelSqlLines.map(highlightJinjaSqlLine)\n  readonly compiledSqlHtml = compiledSqlSource.split('\\n').map(highlightCompiledSqlLine)\n  readonly upstreamNodes = upstreamNodes\n  readonly downstreamNodes = downstreamNodes\n  readonly columns = columns\n  readonly passingTestsCount = columns.reduce(\n    (acc, col) => acc + col.tests.filter((t) => t.status === 'pass').length,\n    0,\n  )\n\n  readonly filteredColumns = computed(() => {\n    const query = this.columnSearch().trim().toLowerCase()\n    if (!query) return columns\n    return columns.filter(\n      (col) =>\n        col.name.toLowerCase().includes(query) ||\n        col.dataType.toLowerCase().includes(query) ||\n        col.description.toLowerCase().includes(query) ||\n        col.tests.some(\n          (t) => t.name.toLowerCase().includes(query) || (t.detail && t.detail.toLowerCase().includes(query)),\n        ),\n    )\n  })\n\n  protected readonly ArrowUpRight = ArrowUpRight\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Clock = Clock\n  protected readonly Code2 = Code2\n  protected readonly Copy = Copy\n  protected readonly Database = Database\n  protected readonly FileCode = FileCode\n  protected readonly GitBranch = GitBranch\n  protected readonly HardDrive = HardDrive\n  protected readonly Info = Info\n  protected readonly Key = Key\n  protected readonly Layers = Layers\n  protected readonly Link2 = Link2\n  protected readonly Loader2 = Loader2\n  protected readonly Play = Play\n  protected readonly Search = Search\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Table2 = Table2\n  protected readonly Tag = Tag\n  protected readonly User = User\n  protected readonly Workflow = Workflow\n  protected readonly Zap = Zap\n\n  private readonly timers = new Set<ReturnType<typeof setTimeout>>()\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t)))\n  }\n\n  get rootClass(): string {\n    return cn(\n      'bg-background text-foreground border-border flex w-full flex-col overflow-hidden rounded-xl border shadow-xs',\n      this.className,\n    )\n  }\n\n  nodeClass(id: string): string {\n    return cn(\n      NODE_BASE,\n      this.selectedNodeId() === id\n        ? 'border-primary bg-primary/10 text-primary shadow-xs'\n        : 'border-border bg-background hover:bg-muted/80 text-foreground',\n    )\n  }\n\n  currentNodeClass(): string {\n    return cn(\n      'border-primary bg-primary/10 ring-primary/20 focus-visible:ring-ring flex w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 p-2.5 text-center shadow-xs ring-2 focus-visible:ring-2 focus-visible:outline-none',\n      this.selectedNodeId() === 'fct_orders' && 'ring-primary/40',\n    )\n  }\n\n  setActiveTab(value: string): void {\n    if (value === 'model-sql' || value === 'compiled-sql' || value === 'contract') this.activeTab.set(value)\n  }\n\n  handleBuildModel(): void {\n    if (this.isBuilding()) return\n    this.isBuilding.set(true)\n    this.later(1200, () => {\n      this.isBuilding.set(false)\n      this.buildStatusText.set('Success · Built in 14.2s (Just now)')\n      this.lastBuildTime.set('Just now')\n    })\n  }\n\n  handleTestModel(): void {\n    if (this.isTesting()) return\n    this.isTesting.set(true)\n    this.later(900, () => this.isTesting.set(false))\n  }\n\n  copyModelRef(): void {\n    this.copy(this.dbtMacroRef, this.copiedRef)\n  }\n\n  copySourceSql(): void {\n    this.copy(modelSqlSource, this.copiedSql)\n  }\n\n  copyCompiledSql(): void {\n    this.copy(compiledSqlSource, this.copiedCompiled)\n  }\n\n  private copy(text: string, flag: { set(v: boolean): void }): void {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) {\n      navigator.clipboard.writeText(text).catch(() => {})\n      flag.set(true)\n      this.later(2000, () => flag.set(false))\n    }\n  }\n\n  private later(ms: number, fn: () => void): void {\n    const t = setTimeout(() => {\n      this.timers.delete(t)\n      fn()\n    }, ms)\n    this.timers.add(t)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/dbt-model-graph/dbt-model-graph.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/dbt-model-graph/index.ts",
      "content": "export {\n  UiDbtModelGraphComponent,\n  highlightJinjaSqlLine,\n  highlightCompiledSqlLine,\n  type DbtModelGraphTab,\n  type ModelColumn,\n  type UpstreamNode,\n  type DownstreamNode,\n} from './dbt-model-graph.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/dbt-model-graph/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/table.json",
    "https://uipkge.dev/r/angular/tabs.json"
  ],
  "description": "dbt Cloud style data modeling DAG dependency graph, compiled SQL view, and model documentation with lineage tree, Jinja/SQL source editor, compiled warehouse query, and column contract test suite.",
  "categories": [
    "devops",
    "app"
  ]
}