{
  "name": "health-chart",
  "title": "Health Chart",
  "description": "Kura Health Chart component source.",
  "type": "ui",
  "add": "when-added",
  "files": [
    {
      "type": "ui",
      "role": "file",
      "content": "<script module lang=\"ts\">\n  export type HealthChartStatus = 'red' | 'orange' | 'green' | 'gray';\n  export type HealthChartRange = '60m' | '24h' | '30d' | '60d';\n\n  export type DataPoint = {\n    status: HealthChartStatus;\n    description: string;\n    timestamp: Date;\n  };\n</script>\n\n<script lang=\"ts\">\n  import * as Tooltip from '$lib/components/ui/tooltip/index.js';\n  import { cn } from '$lib/utils.js';\n  import { DateTime, Duration, Interval } from 'luxon';\n  import type { SvelteHTMLElements } from 'svelte/elements';\n\n  type RangeConfig = {\n    count: number;\n    unit: Duration;\n    latestStart: DateTime;\n  };\n\n  type Bucket = {\n    start: DateTime;\n    end: DateTime;\n    point?: DataPoint;\n  };\n\n  type Props = Omit<SvelteHTMLElements['div'], 'children'> & {\n    data?: Array<DataPoint>;\n    range?: HealthChartRange;\n    emptyStatus?: HealthChartStatus;\n    emptyText?: string;\n  };\n\n  let {\n    data = [],\n    range = '60d',\n    emptyStatus = 'gray',\n    emptyText = 'No data',\n    class: className,\n    ...restProps\n  }: Props = $props();\n\n  const statusClasses: Record<HealthChartStatus, string> = {\n    green: 'bg-success/80 hover:bg-success data-[state=open]:bg-success',\n    orange: 'bg-warning/85 hover:bg-warning data-[state=open]:bg-warning',\n    red: 'bg-destructive/85 hover:bg-destructive data-[state=open]:bg-destructive',\n    gray: 'bg-muted-foreground/25 hover:bg-muted-foreground/35 data-[state=open]:bg-muted-foreground/35'\n  };\n\n  function getRangeConfig(range: HealthChartRange): RangeConfig {\n    const now = DateTime.now();\n\n    switch (range) {\n      case '60m':\n        return {\n          count: 60,\n          unit: Duration.fromObject({ minute: 1 }),\n          latestStart: now.startOf('minute')\n        };\n      case '24h':\n        return {\n          count: 24,\n          unit: Duration.fromObject({ hour: 1 }),\n          latestStart: now.startOf('hour')\n        };\n      case '30d':\n        return {\n          count: 30,\n          unit: Duration.fromObject({ day: 1 }),\n          latestStart: now.startOf('day')\n        };\n      case '60d':\n        return {\n          count: 60,\n          unit: Duration.fromObject({ day: 1 }),\n          latestStart: now.startOf('day')\n        };\n    }\n  }\n\n  function scaleDuration(duration: Duration, amount: number): Duration {\n    return duration.mapUnits((value) => value * amount);\n  }\n\n  function isInBucket(timestamp: Date, bucket: Bucket) {\n    return Interval.fromDateTimes(bucket.start, bucket.end).contains(\n      DateTime.fromJSDate(timestamp)\n    );\n  }\n\n  function formatBucketLabel(bucket: Bucket) {\n    const dateTimeFormat = \"d LLLL yyyy 'at' HH:mm\";\n    const dateFormat = 'd LLLL yyyy';\n    const timeFormat = 'HH:mm';\n\n    if (bucket.point) {\n      return DateTime.fromJSDate(bucket.point.timestamp).toFormat(dateTimeFormat);\n    }\n\n    if (range === '60m' || range === '24h') {\n      if (bucket.start.hasSame(bucket.end, 'day')) {\n        return `${bucket.start.toFormat(dateTimeFormat)} - ${bucket.end.toFormat(timeFormat)}`;\n      }\n\n      return `${bucket.start.toFormat(dateTimeFormat)} - ${bucket.end.toFormat(dateTimeFormat)}`;\n    }\n\n    return bucket.start.toFormat(dateFormat);\n  }\n\n  const buckets = $derived.by(() => {\n    const { count, unit, latestStart } = getRangeConfig(range);\n    const oldestStart = latestStart.minus(scaleDuration(unit, count - 1));\n    const newestFirst = data\n      .toSorted((a, b) => a.timestamp.getTime() - b.timestamp.getTime())\n      .toReversed();\n\n    return Array.from({ length: count }, (_, i): Bucket => {\n      const start = oldestStart.plus(scaleDuration(unit, i));\n      const bucket = {\n        start,\n        end: start.plus(unit)\n      };\n\n      const point = newestFirst.find((point) => isInBucket(point.timestamp, bucket));\n\n      return {\n        ...bucket,\n        point\n      };\n    });\n  });\n</script>\n\n<Tooltip.Provider delayDuration={80} skipDelayDuration={300} disableCloseOnTriggerClick>\n  <div\n    data-slot=\"health-chart\"\n    data-range={range}\n    class={cn('flex h-10 w-full items-end gap-px', className)}\n    {...restProps}\n  >\n    {#each buckets as bucket (bucket.start.toMillis())}\n      {const status = bucket.point?.status ?? emptyStatus}\n      {const dateLabel = formatBucketLabel(bucket)}\n      {const description = bucket.point?.description ?? emptyText}\n      {const datetime =\n        (bucket.point ? DateTime.fromJSDate(bucket.point.timestamp) : bucket.start).toISO() ??\n        undefined}\n\n      <Tooltip.Root>\n        <Tooltip.Trigger\n          type=\"button\"\n          aria-label={`${dateLabel}: ${description}`}\n          data-health-chart-bar\n          data-status={status}\n          data-empty={bucket.point ? undefined : true}\n          tabindex={bucket.point ? 0 : -1}\n          class={cn(\n            'h-full min-w-px flex-1 cursor-default rounded-t-bar rounded-b-bar border border-transparent p-0 opacity-90 transition-[background-color,opacity,transform] duration-200 hover:opacity-100 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 data-[state=open]:opacity-100',\n            statusClasses[status]\n          )}\n        />\n        <Tooltip.Content\n          sideOffset={6}\n          class=\"hairline-frame max-w-64 flex-col items-start gap-1.5 px-2.5 py-2 text-left leading-relaxed\"\n        >\n          <time {datetime} class=\"font-mono text-[11px]/none text-muted-foreground\">\n            {dateLabel}\n          </time>\n          <p class=\"max-w-60 text-xs/relaxed font-medium text-popover-foreground\">\n            {description}\n          </p>\n        </Tooltip.Content>\n      </Tooltip.Root>\n    {/each}\n  </div>\n</Tooltip.Provider>\n",
      "path": "health-chart/health-chart.svelte",
      "_imports_": [
        {
          "import": "$lib/components/ui/tooltip/index.js",
          "item": "tooltip",
          "file": {
            "type": "ui",
            "path": "tooltip/index.ts"
          },
          "meta": {}
        },
        {
          "import": "$lib/utils.js",
          "item": "utils",
          "file": {
            "type": "lib",
            "path": "utils.ts"
          },
          "meta": {}
        }
      ],
      "registryDependencies": [],
      "dependencies": [],
      "devDependencies": []
    },
    {
      "type": "ui",
      "role": "file",
      "content": "import HealthChart, {\n  type DataPoint,\n  type HealthChartRange,\n  type HealthChartStatus\n} from './health-chart.svelte';\n\nexport { HealthChart, type DataPoint, type HealthChartRange, type HealthChartStatus };\n",
      "path": "health-chart/index.ts",
      "_imports_": [
        {
          "import": "./health-chart.svelte",
          "item": "health-chart",
          "file": {
            "type": "ui",
            "path": "health-chart/health-chart.svelte"
          },
          "meta": {}
        }
      ],
      "registryDependencies": [],
      "dependencies": [],
      "devDependencies": []
    }
  ],
  "registryDependencies": [
    "styles",
    "tooltip",
    "utils"
  ],
  "dependencies": [
    {
      "ecosystem": "js",
      "name": "luxon",
      "version": "^3.7.2"
    }
  ],
  "devDependencies": [
    {
      "ecosystem": "js",
      "name": "@types/luxon",
      "version": "^3.7.2"
    }
  ]
}