{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "editor-diff-viewer",
  "type": "registry:component",
  "title": "Diff viewer",
  "description": "Two sources compared, tinted by change",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "@tanstack/highlight@1.1.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/editor/src/code-editor/highlight.ts",
      "target": "@ui/gpuixui/editor/code-editor/highlight.ts",
      "type": "registry:lib",
      "content": "import { tokenize } from \"@tanstack/highlight\";\nimport type { HighlightTokenClass } from \"@tanstack/highlight\";\nimport * as Arr from \"effect/Array\";\nimport type { SyntaxRole } from \"../../tokens/index.js\";\n\nexport interface HighlightRun {\n  readonly text: string;\n\n  readonly role?: SyntaxRole;\n}\n\nexport const tokenRole: Readonly<Record<HighlightTokenClass, SyntaxRole>> = {\n  attr: \"attribute\",\n  \"code-inline\": \"constant\",\n  command: \"functionBuiltin\",\n  comment: \"comment\",\n  deleted: \"invalid\",\n  function: \"function\",\n  heading: \"typeName\",\n  inserted: \"label\",\n  keyword: \"keyword\",\n  link: \"attribute\",\n  literal: \"string\",\n  meta: \"macroName\",\n  number: \"number\",\n  operator: \"operator\",\n  property: \"property\",\n  selector: \"typeName\",\n  string: \"string\",\n  tag: \"tag\",\n  type: \"typeName\",\n  variable: \"variable\",\n};\n\nexport const languageOf = (language: unknown): string =>\n  typeof language === \"string\" ? language : \"\";\n\nexport const codeRuns = (\n  source: string,\n  language: unknown,\n): ReadonlyArray<HighlightRun> =>\n  Arr.map(\n    tokenize(source, { lang: languageOf(language) }).tokens,\n    ({ className, value }) =>\n      className === undefined\n        ? { text: value }\n        : { text: value, role: tokenRole[className] },\n  );\n\nconst LINE_ENDINGS = /\\r\\n?|\\n/;\n\nexport const codeLines = (\n  source: string,\n  language: unknown,\n): ReadonlyArray<ReadonlyArray<HighlightRun>> => {\n  const [, pieces] = Arr.mapAccum(\n    Arr.flatMap(codeRuns(source, language), (run) =>\n      Arr.map(run.text.split(LINE_ENDINGS), (text, index) => ({\n        text,\n        role: run.role,\n        opensRow: index > 0,\n      })),\n    ),\n    0,\n    (row, piece) => {\n      const at = piece.opensRow ? row + 1 : row;\n      return [at, { text: piece.text, role: piece.role, row: at }];\n    },\n  );\n  return Arr.match(pieces, {\n    onEmpty: () => [[]],\n    onNonEmpty: (nonEmpty) =>\n      Arr.map(\n        Arr.groupWith(nonEmpty, (head, piece) => head.row === piece.row),\n        (row) =>\n          Arr.map(\n            Arr.filter(row, (piece) => piece.text.length > 0),\n            (piece) => ({ text: piece.text, role: piece.role }),\n          ),\n      ),\n  });\n};\n"
    },
    {
      "path": "packages/editor/src/diff/diff-viewer.tsx",
      "target": "@ui/gpuixui/editor/diff/diff-viewer.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { StyleDesc } from \"@gpuix/react\";\nimport {\n  mergeStyles,\n  resolveStyle,\n  type StateStyle,\n} from \"@gpuix/react/floating\";\nimport { cn } from \"../../tw/index.js\";\nimport { Fragment, type ReactNode } from \"react\";\nimport {\n  alpha,\n  statusTone,\n  textStyle,\n  type TextStyleProperties,\n  type Theme,\n  syntaxColors,\n} from \"../../tokens/index.js\";\nimport { useTheme } from \"../../tokens/react.js\";\nimport * as Arr from \"effect/Array\";\nimport { Match } from \"effect\";\nimport * as Option from \"effect/Option\";\nimport {\n  DEFAULT_CONTEXT,\n  blocks,\n  diffStrings,\n  modeOf,\n  numberDiff,\n  pairs,\n  rows,\n  styledRows,\n  wordMarks,\n  wrapStarts,\n  type DiffMode,\n  type DiffRow,\n  type RowKind,\n  type SplitPair,\n} from \"./logic.js\";\nimport { codeLines } from \"../code-editor/highlight.js\";\nimport { recipeStyle } from \"../internal/recipe.js\";\nimport { draftOf, labelOf } from \"../internal/text.js\";\n\nexport type { DiffMode } from \"./logic.js\";\n\nconst DIFF_NAME = \"Diff\";\n\nconst DIFF_ROOT =\n  \"flex flex-col overflow-hidden rounded-lg border border-input bg-background\";\n\nconst MONO_DIGIT = 0.6;\n\nexport interface DiffViewerState {\n  readonly changed: boolean;\n}\n\nexport type DiffInk = \"tone\" | \"code\";\n\nexport interface DiffAnnotation {\n  readonly line: number;\n\n  readonly height: number;\n\n  readonly content: ReactNode;\n}\n\nexport interface DiffViewerProps {\n  readonly before: string;\n\n  readonly after: string;\n\n  readonly mode?: DiffMode;\n\n  readonly contextLines?: number;\n\n  readonly lineNumbers?: boolean;\n\n  readonly wordDiff?: boolean;\n\n  readonly ink?: DiffInk;\n\n  readonly annotations?: ReadonlyArray<DiffAnnotation>;\n\n  readonly wrapColumns?: number;\n\n  readonly language?: string;\n\n  readonly firstLine?: { readonly before?: number; readonly after?: number };\n\n  readonly label?: string;\n\n  readonly className?: string;\n\n  readonly style?: StateStyle<DiffViewerState>;\n  readonly testId?: string;\n}\n\nconst rowFill = (theme: Theme, kind: RowKind): string | undefined =>\n  Match.value(kind).pipe(\n    Match.when(\"added\", () => statusTone(theme, \"success\").background),\n    Match.when(\"removed\", () => statusTone(theme, \"destructive\").background),\n    Match.when(\"hunk\", () => theme.colors.muted),\n    Match.when(\"context\", () => undefined),\n    Match.exhaustive,\n  );\n\nconst codeTone = (theme: Theme, kind: RowKind): string =>\n  kind === \"added\" ? theme.colors.success : theme.colors.destructive;\n\nconst rowBar = (theme: Theme, kind: RowKind): StyleDesc =>\n  kind === \"added\" || kind === \"removed\"\n    ? {\n        borderLeftWidth: theme.borderWidth.strong,\n        borderColor: codeTone(theme, kind),\n      }\n    : {};\n\nconst rowInk = (theme: Theme, kind: RowKind): string =>\n  Match.value(kind).pipe(\n    Match.when(\"added\", () => statusTone(theme, \"success\").foreground),\n    Match.when(\"removed\", () => statusTone(theme, \"destructive\").foreground),\n    Match.when(\"hunk\", () => theme.colors.mutedForeground),\n    Match.when(\"context\", () => theme.colors.foreground),\n    Match.exhaustive,\n  );\n\nconst rowNumber = (number: Option.Option<number>): string =>\n  Option.match(number, {\n    onNone: () => \"\",\n    onSome: (value) => String(value),\n  });\n\nconst rowSign = (kind: RowKind): string =>\n  Match.value(kind).pipe(\n    Match.when(\"added\", () => \"+\"),\n    Match.when(\"removed\", () => \"-\"),\n    Match.when(\"hunk\", () => \" \"),\n    Match.when(\"context\", () => \" \"),\n    Match.exhaustive,\n  );\n\nconst gutterWidth = (\n  theme: Theme,\n  metrics: TextStyleProperties,\n  lines: number,\n): number =>\n  Math.ceil(String(Math.max(1, lines)).length * metrics.fontSize * MONO_DIGIT) +\n  theme.spacing.sm;\n\nconst lineRow = (\n  theme: Theme,\n  metrics: TextStyleProperties,\n  kind: RowKind,\n): StyleDesc => ({\n  display: \"flex\",\n  flexDirection: \"row\",\n  alignItems: \"stretch\",\n  height: metrics.lineHeight,\n  flexShrink: 0,\n  backgroundColor: rowFill(theme, kind),\n});\n\nconst gutterCell = (\n  theme: Theme,\n  metrics: TextStyleProperties,\n  kind: RowKind,\n  width: number,\n): StyleDesc => ({\n  ...metrics,\n  width,\n  flexShrink: 0,\n  color:\n    kind === \"context\" ? theme.colors.mutedForeground : rowInk(theme, kind),\n  textAlign: \"right\",\n  paddingRight: theme.spacing.xs,\n});\n\nconst codeCell = (\n  theme: Theme,\n  metrics: TextStyleProperties,\n  kind: RowKind,\n  ink: DiffInk = \"tone\",\n): StyleDesc => ({\n  ...metrics,\n  color:\n    ink === \"code\" && kind !== \"hunk\"\n      ? theme.colors.foreground\n      : rowInk(theme, kind),\n  whiteSpace: \"nowrap\",\n  paddingRight: theme.spacing.sm,\n});\n\nconst HunkHeading = ({\n  heading,\n  theme,\n  metrics,\n  testId,\n}: {\n  readonly heading: DiffRow;\n  readonly theme: Theme;\n  readonly metrics: TextStyleProperties;\n  readonly testId: string;\n}) => (\n  <div\n    testId={testId}\n    style={{\n      ...lineRow(theme, metrics, \"hunk\"),\n      paddingLeft: theme.spacing.sm,\n      paddingRight: theme.spacing.sm,\n    }}\n  >\n    <text\n      role=\"presentation\"\n      style={{ ...metrics, color: theme.colors.mutedForeground }}\n    >\n      {heading.text}\n    </text>\n  </div>\n);\n\nconst UnifiedRows = ({\n  script,\n  theme,\n  metrics,\n  numberWidth,\n  lineNumbers,\n  testId,\n}: {\n  readonly script: ReadonlyArray<DiffRow>;\n  readonly theme: Theme;\n  readonly metrics: TextStyleProperties;\n  readonly numberWidth: number;\n  readonly lineNumbers: boolean;\n  readonly testId: string;\n}) => (\n  <div\n    style={{\n      display: \"flex\",\n      flexDirection: \"column\",\n      overflowX: \"scroll\",\n      paddingTop: theme.spacing.xs,\n      paddingBottom: theme.spacing.xs,\n    }}\n  >\n    {script.map((row, index) =>\n      row.kind === \"hunk\" ? (\n        <HunkHeading\n          key={index}\n          heading={row}\n          theme={theme}\n          metrics={metrics}\n          testId={`${testId}-hunk-${index}`}\n        />\n      ) : (\n        <div\n          key={index}\n          testId={`${testId}-row-${index}`}\n          style={lineRow(theme, metrics, row.kind)}\n        >\n          <text\n            role=\"presentation\"\n            style={{\n              ...metrics,\n              width: theme.spacing.md,\n              flexShrink: 0,\n              textAlign: \"center\",\n              color: theme.colors.mutedForeground,\n            }}\n          >\n            {rowSign(row.kind)}\n          </text>\n          {lineNumbers ? (\n            <>\n              <text\n                role=\"presentation\"\n                style={gutterCell(theme, metrics, row.kind, numberWidth)}\n              >\n                {rowNumber(row.before)}\n              </text>\n              <text\n                role=\"presentation\"\n                style={gutterCell(theme, metrics, row.kind, numberWidth)}\n              >\n                {rowNumber(row.after)}\n              </text>\n            </>\n          ) : null}\n          <text role=\"presentation\" style={codeCell(theme, metrics, row.kind)}>\n            {row.text}\n          </text>\n        </div>\n      ),\n    )}\n  </div>\n);\n\nconst markColor = (theme: Theme, kind: RowKind, ink: DiffInk): string =>\n  ink === \"code\"\n    ? alpha(codeTone(theme, kind), 0.34)\n    : alpha(rowInk(theme, kind), 0.28);\n\nconst SplitColumn = ({\n  side,\n  script,\n  offset,\n  theme,\n  metrics,\n  numberWidth,\n  lineNumbers,\n  wordDiff,\n  ink,\n  annotations,\n  wrapColumns,\n  language,\n  shift,\n  testId,\n}: {\n  readonly wordDiff: boolean;\n  readonly ink: DiffInk;\n  readonly annotations: ReadonlyArray<DiffAnnotation>;\n  readonly wrapColumns: number | undefined;\n  readonly language: string | undefined;\n  readonly shift: number;\n  readonly side: \"before\" | \"after\";\n  readonly script: ReadonlyArray<SplitPair>;\n  readonly offset: number;\n  readonly theme: Theme;\n  readonly metrics: TextStyleProperties;\n  readonly numberWidth: number;\n  readonly lineNumbers: boolean;\n  readonly testId: string;\n}) => (\n  <div\n    testId={`${testId}-${side}`}\n    style={{\n      display: \"flex\",\n      flexDirection: \"column\",\n      flexGrow: 1,\n      flexBasis: 0,\n      minWidth: 0,\n      overflowX: \"scroll\",\n    }}\n  >\n    {script.map((pair, position) => {\n      const row = side === \"before\" ? pair.left : pair.right;\n      const index = offset + position;\n      const marks = Option.match(Option.all([pair.left, pair.right]), {\n        onNone: () => undefined,\n        onSome: ([left, right]) =>\n          wordDiff && left.kind === \"removed\" && right.kind === \"added\"\n            ? wordMarks(left.text, right.text)[side]\n            : undefined,\n      });\n      const kindHere = Option.match(row, {\n        onNone: (): RowKind => \"context\",\n        onSome: (value) => value.kind,\n      });\n      const lengths = [pair.left, pair.right].map((cell) =>\n        Option.match(cell, {\n          onNone: () => 0,\n          onSome: (value) => value.text.length,\n        }),\n      );\n      const rowLines =\n        wrapColumns === undefined\n          ? 1\n          : language === undefined\n            ? Math.max(\n                1,\n                ...lengths.map((length) => Math.ceil(length / wrapColumns)),\n              )\n            : Math.max(\n                ...[pair.left, pair.right].map((cell) =>\n                  Option.match(cell, {\n                    onNone: () => 1,\n                    onSome: (value) =>\n                      wrapStarts(value.text, wrapColumns).length,\n                  }),\n                ),\n              );\n      const afterLine = Option.flatMap(pair.right, (value) => value.after);\n      const note = Option.match(afterLine, {\n        onNone: () => undefined,\n        onSome: (line) => annotations.find((item) => item.line === line),\n      });\n      return (\n        <Fragment key={index}>\n          <div\n            testId={`${testId}-row-${index}-${side}`}\n            style={{\n              ...lineRow(theme, metrics, kindHere),\n              height: metrics.lineHeight * rowLines,\n              ...(ink === \"code\" ? rowBar(theme, kindHere) : {}),\n              ...(ink === \"code\" && kindHere !== \"context\" && Option.isSome(row)\n                ? { backgroundColor: alpha(codeTone(theme, kindHere), 0.16) }\n                : {}),\n            }}\n          >\n            {Option.match(row, {\n              onNone: () => null,\n              onSome: (value) => (\n                <>\n                  {lineNumbers ? (\n                    <text\n                      role=\"presentation\"\n                      style={gutterCell(\n                        theme,\n                        metrics,\n                        value.kind,\n                        numberWidth,\n                      )}\n                    >\n                      {rowNumber(\n                        Option.map(\n                          side === \"before\" ? value.before : value.after,\n                          (line) => line + shift,\n                        ),\n                      )}\n                    </text>\n                  ) : null}\n                  {language === undefined ? (\n                    <text\n                      role=\"presentation\"\n                      highlight={\n                        marks === undefined || marks.length === 0\n                          ? undefined\n                          : {\n                              ranges: marks.map(\n                                ([from, to]) => [from, to] as [number, number],\n                              ),\n                              color: markColor(theme, value.kind, ink),\n                            }\n                      }\n                      style={{\n                        ...codeCell(theme, metrics, value.kind, ink),\n                        ...(wrapColumns === undefined\n                          ? {}\n                          : {\n                              whiteSpace: \"normal\",\n                              flexGrow: 1,\n                              flexBasis: 0,\n                              minWidth: 0,\n                            }),\n                      }}\n                    >\n                      {value.text}\n                    </text>\n                  ) : (\n                    <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n                      {styledRows(\n                        codeLines(value.text, language)[0] ?? [],\n                        marks ?? [],\n                        wrapColumns ?? value.text.length + 1,\n                      ).map((runs, line) => (\n                        <div\n                          key={line}\n                          style={{ display: \"flex\", flexDirection: \"row\" }}\n                        >\n                          {runs.map((run, at) => (\n                            <text\n                              key={at}\n                              role=\"presentation\"\n                              highlight={\n                                run.marks.length === 0\n                                  ? undefined\n                                  : {\n                                      ranges: run.marks.map(\n                                        ([from, to]) =>\n                                          [from, to] as [number, number],\n                                      ),\n                                      color: markColor(theme, value.kind, ink),\n                                    }\n                              }\n                              style={{\n                                ...codeCell(theme, metrics, value.kind, ink),\n                                paddingRight: 0,\n                                ...(run.role === undefined\n                                  ? {}\n                                  : { color: syntaxColors(theme)[run.role] }),\n                              }}\n                            >\n                              {run.text}\n                            </text>\n                          ))}\n                        </div>\n                      ))}\n                    </div>\n                  )}\n                </>\n              ),\n            })}\n          </div>\n          {note === undefined ? null : (\n            <div\n              testId={`${testId}-note-${note.line}-${side}`}\n              style={{ height: note.height, flexShrink: 0 }}\n            >\n              {side === \"after\" ? note.content : null}\n            </div>\n          )}\n        </Fragment>\n      );\n    })}\n  </div>\n);\n\nconst SplitBlock = ({\n  script,\n  offset,\n  theme,\n  metrics,\n  numberWidth,\n  lineNumbers,\n  wordDiff,\n  ink,\n  annotations,\n  wrapColumns,\n  language,\n  firstLine,\n  testId,\n}: {\n  readonly wrapColumns: number | undefined;\n  readonly language: string | undefined;\n  readonly firstLine: DiffViewerProps[\"firstLine\"];\n  readonly wordDiff: boolean;\n  readonly ink: DiffInk;\n  readonly annotations: ReadonlyArray<DiffAnnotation>;\n  readonly script: ReadonlyArray<SplitPair>;\n  readonly offset: number;\n  readonly theme: Theme;\n  readonly metrics: TextStyleProperties;\n  readonly numberWidth: number;\n  readonly lineNumbers: boolean;\n  readonly testId: string;\n}) => (\n  <div style={{ display: \"flex\", flexDirection: \"row\", alignItems: \"stretch\" }}>\n    <SplitColumn\n      side=\"before\"\n      script={script}\n      offset={offset}\n      theme={theme}\n      metrics={metrics}\n      numberWidth={numberWidth}\n      lineNumbers={lineNumbers}\n      wordDiff={wordDiff}\n      ink={ink}\n      annotations={annotations}\n      wrapColumns={wrapColumns}\n      language={language}\n      shift={(firstLine?.before ?? 1) - 1}\n      testId={testId}\n    />\n    <div\n      testId={`${testId}-divider`}\n      style={{\n        width: theme.borderWidth.thin,\n        flexShrink: 0,\n        backgroundColor: theme.colors.border,\n      }}\n    />\n    <SplitColumn\n      side=\"after\"\n      script={script}\n      offset={offset}\n      theme={theme}\n      metrics={metrics}\n      numberWidth={numberWidth}\n      lineNumbers={lineNumbers}\n      wordDiff={wordDiff}\n      ink={ink}\n      annotations={annotations}\n      wrapColumns={wrapColumns}\n      language={language}\n      shift={(firstLine?.after ?? 1) - 1}\n      testId={testId}\n    />\n  </div>\n);\n\nconst SplitRows = ({\n  script,\n  theme,\n  metrics,\n  numberWidth,\n  lineNumbers,\n  wordDiff,\n  ink,\n  annotations,\n  wrapColumns,\n  language,\n  firstLine,\n  testId,\n}: {\n  readonly wrapColumns: number | undefined;\n  readonly language: string | undefined;\n  readonly firstLine: DiffViewerProps[\"firstLine\"];\n  readonly wordDiff: boolean;\n  readonly ink: DiffInk;\n  readonly annotations: ReadonlyArray<DiffAnnotation>;\n  readonly script: ReadonlyArray<SplitPair>;\n  readonly theme: Theme;\n  readonly metrics: TextStyleProperties;\n  readonly numberWidth: number;\n  readonly lineNumbers: boolean;\n  readonly testId: string;\n}) => {\n  const [, sections] = Arr.mapAccum(\n    blocks(script),\n    0,\n    (offset, block) =>\n      [offset + Arr.length(block.pairs), { block, offset }] as const,\n  );\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        paddingTop: theme.spacing.xs,\n        paddingBottom: theme.spacing.xs,\n      }}\n    >\n      {sections.map(({ block, offset }, index) =>\n        Option.match(block.heading, {\n          onSome: (heading) => (\n            <HunkHeading\n              key={index}\n              heading={heading}\n              theme={theme}\n              metrics={metrics}\n              testId={`${testId}-hunk-${offset}`}\n            />\n          ),\n          onNone: () => (\n            <SplitBlock\n              key={index}\n              script={block.pairs}\n              offset={offset}\n              theme={theme}\n              metrics={metrics}\n              numberWidth={numberWidth}\n              lineNumbers={lineNumbers}\n              wordDiff={wordDiff}\n              ink={ink}\n              annotations={annotations}\n              wrapColumns={wrapColumns}\n              language={language}\n              firstLine={firstLine}\n              testId={testId}\n            />\n          ),\n        }),\n      )}\n    </div>\n  );\n};\n\nconst contextOf = (value: number | undefined): number =>\n  value === undefined || !Number.isFinite(value) || value < 0\n    ? DEFAULT_CONTEXT\n    : Math.floor(value);\n\nexport const DiffViewer = ({\n  before,\n  after,\n  mode = \"unified\",\n  contextLines,\n  lineNumbers = true,\n  wordDiff = true,\n  ink = \"tone\",\n  annotations = [],\n  wrapColumns,\n  language,\n  firstLine,\n  label,\n  className,\n  style,\n  testId = \"diff-viewer\",\n}: DiffViewerProps) => {\n  const theme = useTheme();\n\n  const name = labelOf(label, DIFF_NAME);\n  const metrics = textStyle(theme, \"codeBlock\");\n\n  const view = modeOf(mode);\n\n  const numbered = numberDiff(diffStrings(draftOf(before), draftOf(after)));\n  const changed = Arr.some(numbered, (row) => row.kind !== \"context\");\n  const state: DiffViewerState = { changed };\n  const numberWidth = gutterWidth(theme, metrics, Arr.length(numbered));\n  const context = contextOf(contextLines);\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={name}\n      aria-description={changed ? \"Changes were found\" : \"No changes\"}\n      testId={testId}\n      style={mergeStyles(\n        recipeStyle(cn(DIFF_ROOT, className), theme),\n        resolveStyle(style, state),\n      )}\n    >\n      {view === \"unified\" ? (\n        <UnifiedRows\n          script={rows(numbered, context)}\n          theme={theme}\n          metrics={metrics}\n          numberWidth={numberWidth}\n          lineNumbers={lineNumbers}\n          testId={testId}\n        />\n      ) : (\n        <SplitRows\n          script={pairs(numbered, context)}\n          theme={theme}\n          metrics={metrics}\n          numberWidth={numberWidth}\n          lineNumbers={lineNumbers}\n          wordDiff={wordDiff}\n          ink={ink}\n          annotations={annotations}\n          wrapColumns={wrapColumns}\n          language={language}\n          firstLine={firstLine}\n          testId={testId}\n        />\n      )}\n    </div>\n  );\n};\n\nexport const diffViewerMeta = {\n  name: \"Diff viewer\",\n  tier: 4,\n  status: \"done\",\n  gpuixSupport: \"partial\",\n  limitations: [\n    \"the line diff is an LCS walk, `O(n·m)` in time and space, so very large files are slow\",\n    \"word marks and annotations paint in split mode; unified mode shows whole lines\",\n    \"the diff rows are presentational runs, so a reader hears the change count, not the diffed text\",\n    \"each split column scrolls its own horizontal overflow; the runtime has no sticky gutters, so a gutter travels with its column\",\n  ],\n  docs: \"docs/diff-viewer.mdx\",\n  example: \"examples/diff-viewer.example.tsx\",\n  testFile: \"src/diff/logic.test.ts\",\n} as const;\n"
    },
    {
      "path": "packages/editor/src/diff/logic.ts",
      "target": "@ui/gpuixui/editor/diff/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\nimport * as Str from \"effect/String\";\n\nexport const DiffMode = Schema.Literals([\"unified\", \"split\"]);\n\nexport type DiffMode = typeof DiffMode.Type;\n\nexport const isDiffMode = Schema.is(DiffMode);\n\nexport const modeOf = (\n  value: unknown,\n  fallback: DiffMode = \"unified\",\n): DiffMode => (isDiffMode(value) ? value : fallback);\n\nexport type LineKind = \"context\" | \"added\" | \"removed\";\n\nexport interface LineEdit {\n  readonly kind: LineKind;\n  readonly text: string;\n}\n\nconst LINE_ENDINGS = /\\r\\n?|\\n/;\n\nexport const splitLines = (source: string): ReadonlyArray<string> =>\n  Arr.match(Str.split(LINE_ENDINGS)(source), {\n    onEmpty: () => [],\n    onNonEmpty: (rows) =>\n      Str.isEmpty(Arr.getUnsafe(rows, Arr.length(rows) - 1))\n        ? Arr.dropRight(rows, 1)\n        : rows,\n  });\n\nexport const diffLines = (\n  before: ReadonlyArray<string>,\n  after: ReadonlyArray<string>,\n): ReadonlyArray<LineEdit> => {\n  const n = before.length;\n  const m = after.length;\n  const stride = m + 1;\n\n  const lengths = Arr.makeBy((n + 1) * stride, () => 0);\n  const cell = (i: number, j: number): number =>\n    Arr.getUnsafe(lengths, i * stride + j);\n  for (let i = n - 1; i >= 0; i--) {\n    for (let j = m - 1; j >= 0; j--) {\n      lengths[i * stride + j] =\n        Arr.getUnsafe(before, i) === Arr.getUnsafe(after, j)\n          ? cell(i + 1, j + 1) + 1\n          : Math.max(cell(i + 1, j), cell(i, j + 1));\n    }\n  }\n\n  const edits: Array<LineEdit> = [];\n  let i = 0;\n  let j = 0;\n  while (i < n && j < m) {\n    if (Arr.getUnsafe(before, i) === Arr.getUnsafe(after, j)) {\n      edits.push({ kind: \"context\", text: Arr.getUnsafe(before, i) });\n      i += 1;\n      j += 1;\n    } else if (cell(i + 1, j) >= cell(i, j + 1)) {\n      edits.push({ kind: \"removed\", text: Arr.getUnsafe(before, i) });\n      i += 1;\n    } else {\n      edits.push({ kind: \"added\", text: Arr.getUnsafe(after, j) });\n      j += 1;\n    }\n  }\n  while (i < n) {\n    edits.push({ kind: \"removed\", text: Arr.getUnsafe(before, i) });\n    i += 1;\n  }\n  while (j < m) {\n    edits.push({ kind: \"added\", text: Arr.getUnsafe(after, j) });\n    j += 1;\n  }\n  return edits;\n};\n\nexport const diffStrings = (\n  before: string,\n  after: string,\n): ReadonlyArray<LineEdit> => diffLines(splitLines(before), splitLines(after));\n\nexport const reconstructBefore = (\n  edits: ReadonlyArray<LineEdit>,\n): ReadonlyArray<string> =>\n  Arr.map(\n    Arr.filter(edits, (edit) => edit.kind !== \"added\"),\n    (edit) => edit.text,\n  );\n\nexport const reconstructAfter = (\n  edits: ReadonlyArray<LineEdit>,\n): ReadonlyArray<string> =>\n  Arr.map(\n    Arr.filter(edits, (edit) => edit.kind !== \"removed\"),\n    (edit) => edit.text,\n  );\n\nexport type RowKind = LineKind | \"hunk\";\n\nexport interface DiffRow {\n  readonly kind: RowKind;\n  readonly text: string;\n\n  readonly before: Option.Option<number>;\n\n  readonly after: Option.Option<number>;\n}\n\nexport const numberDiff = (\n  edits: ReadonlyArray<LineEdit>,\n): ReadonlyArray<DiffRow> => {\n  const rows: Array<DiffRow> = [];\n  let before = 0;\n  let after = 0;\n  for (const edit of edits) {\n    if (edit.kind === \"removed\") {\n      before += 1;\n      rows.push({\n        kind: edit.kind,\n        text: edit.text,\n        before: Option.some(before),\n        after: Option.none(),\n      });\n    } else if (edit.kind === \"added\") {\n      after += 1;\n      rows.push({\n        kind: edit.kind,\n        text: edit.text,\n        before: Option.none(),\n        after: Option.some(after),\n      });\n    } else {\n      before += 1;\n      after += 1;\n      rows.push({\n        kind: edit.kind,\n        text: edit.text,\n        before: Option.some(before),\n        after: Option.some(after),\n      });\n    }\n  }\n  return rows;\n};\n\nexport interface SplitPair {\n  readonly left: Option.Option<DiffRow>;\n  readonly right: Option.Option<DiffRow>;\n}\n\nconst pairRun = (run: ReadonlyArray<DiffRow>): ReadonlyArray<SplitPair> => {\n  const removals = Arr.filter(run, (row) => row.kind === \"removed\");\n  const additions = Arr.filter(run, (row) => row.kind === \"added\");\n  const height = Math.max(removals.length, additions.length);\n  if (height === 0) return [];\n  return Arr.map(Arr.range(0, height - 1), (index) => ({\n    left: Arr.get(removals, index),\n    right: Arr.get(additions, index),\n  }));\n};\n\nconst pairRows = (rows: ReadonlyArray<DiffRow>): ReadonlyArray<SplitPair> =>\n  Arr.match(rows, {\n    onEmpty: () => [],\n    onNonEmpty: (nonEmpty) =>\n      Arr.flatMap(\n        Arr.groupWith(\n          nonEmpty,\n          (left, right) =>\n            (left.kind === \"context\") === (right.kind === \"context\"),\n        ),\n        (group) =>\n          Arr.getUnsafe(group, 0).kind === \"context\"\n            ? Arr.map(group, (row) => ({\n                left: Option.some(row),\n                right: Option.some(row),\n              }))\n            : pairRun(group),\n      ),\n  });\n\nexport const DEFAULT_CONTEXT = 3;\n\nexport interface DiffHunk {\n  readonly heading: Option.Option<DiffRow>;\n  readonly rows: ReadonlyArray<DiffRow>;\n}\n\nconst numbersOf = (\n  rows: ReadonlyArray<DiffRow>,\n  side: (row: DiffRow) => Option.Option<number>,\n): ReadonlyArray<number> =>\n  Arr.reduce(rows, Arr.empty<number>(), (numbers, row) =>\n    Option.match(side(row), {\n      onNone: () => numbers,\n      onSome: (value) => Arr.append(numbers, value),\n    }),\n  );\n\nconst spanOf = (\n  all: ReadonlyArray<DiffRow>,\n  visible: ReadonlyArray<DiffRow>,\n  start: number,\n  side: (row: DiffRow) => Option.Option<number>,\n): { readonly start: number; readonly count: number } =>\n  Arr.match(numbersOf(visible, side), {\n    onEmpty: () => ({\n      start: Arr.length(\n        Arr.filter(Arr.take(all, start), (row) => Option.isSome(side(row))),\n      ),\n      count: 0,\n    }),\n    onNonEmpty: (values) => ({\n      start: Arr.getUnsafe(values, 0),\n      count: Arr.length(values),\n    }),\n  });\n\nconst headingOf = (\n  all: ReadonlyArray<DiffRow>,\n  visible: ReadonlyArray<DiffRow>,\n  start: number,\n): Option.Option<DiffRow> => {\n  if (start === 0 && Arr.length(visible) === Arr.length(all)) {\n    return Option.none();\n  }\n  const before = spanOf(all, visible, start, (row) => row.before);\n  const after = spanOf(all, visible, start, (row) => row.after);\n  return Option.some({\n    kind: \"hunk\",\n    text: `@@ -${before.start},${before.count} +${after.start},${after.count} @@`,\n    before: Option.none(),\n    after: Option.none(),\n  });\n};\n\nexport const hunks = (\n  rows: ReadonlyArray<DiffRow>,\n  context: number = DEFAULT_CONTEXT,\n): ReadonlyArray<DiffHunk> => {\n  const reach = context >= 0 ? context : 0;\n  const changes = Arr.filterMap(rows, (row, index) =>\n    row.kind === \"context\" ? Result.failVoid : Result.succeed(index),\n  );\n\n  const spans = Arr.reduce(\n    changes,\n    Arr.empty<{ readonly start: number; readonly end: number }>(),\n    (spans, index) =>\n      Option.match(Arr.last(spans), {\n        onNone: () => [{ start: index, end: index + 1 }],\n        onSome: (span) =>\n          index - span.end <= 2 * reach\n            ? Arr.append(Arr.dropRight(spans, 1), {\n                start: span.start,\n                end: index + 1,\n              })\n            : Arr.append(spans, { start: index, end: index + 1 }),\n      }),\n  );\n  return Arr.match(spans, {\n    onEmpty: () => [{ heading: Option.none(), rows }],\n    onNonEmpty: (runs) =>\n      Arr.map(runs, (span) => {\n        const start = Math.max(0, span.start - reach);\n        const end = Math.min(Arr.length(rows), span.end + reach);\n        const visible = Arr.drop(Arr.take(rows, end), start);\n        return { heading: headingOf(rows, visible, start), rows: visible };\n      }),\n  });\n};\n\nconst headingRows = (hunk: DiffHunk): ReadonlyArray<DiffRow> =>\n  Option.match(hunk.heading, {\n    onNone: () => [],\n    onSome: (heading) => [heading],\n  });\n\nconst flattenPair = (pair: SplitPair): ReadonlyArray<DiffRow> =>\n  Option.match(pair.left, {\n    onNone: () =>\n      Option.match(pair.right, {\n        onNone: () => [],\n        onSome: (row) => [row],\n      }),\n    onSome: (left) =>\n      left.kind === \"context\"\n        ? [left]\n        : Option.match(pair.right, {\n            onNone: () => [left],\n            onSome: (right) => [left, right],\n          }),\n  });\n\nexport const rows = (\n  numbered: ReadonlyArray<DiffRow>,\n  context: number = DEFAULT_CONTEXT,\n): ReadonlyArray<DiffRow> =>\n  Arr.flatMap(hunks(numbered, context), (hunk) =>\n    Arr.appendAll(\n      headingRows(hunk),\n      Arr.flatMap(pairRows(hunk.rows), flattenPair),\n    ),\n  );\n\nexport const pairs = (\n  numbered: ReadonlyArray<DiffRow>,\n  context: number = DEFAULT_CONTEXT,\n): ReadonlyArray<SplitPair> =>\n  Arr.flatMap(hunks(numbered, context), (hunk) =>\n    Arr.appendAll(\n      Arr.map(headingRows(hunk), (heading) => ({\n        left: Option.some(heading),\n        right: Option.none(),\n      })),\n      pairRows(hunk.rows),\n    ),\n  );\n\nexport interface SplitBlock {\n  readonly heading: Option.Option<DiffRow>;\n  readonly pairs: ReadonlyArray<SplitPair>;\n}\n\nconst isHeading = (pair: SplitPair): boolean =>\n  Option.exists(pair.left, (row) => row.kind === \"hunk\");\n\nexport const blocks = (\n  script: ReadonlyArray<SplitPair>,\n): ReadonlyArray<SplitBlock> =>\n  Arr.match(script, {\n    onEmpty: () => [],\n    onNonEmpty: (nonEmpty) =>\n      Arr.map(\n        Arr.groupWith(\n          nonEmpty,\n          (left, right) => isHeading(left) === isHeading(right),\n        ),\n        (group) =>\n          isHeading(Arr.getUnsafe(group, 0))\n            ? {\n                heading: Arr.getUnsafe(group, 0).left,\n                pairs: Arr.empty<SplitPair>(),\n              }\n            : { heading: Option.none(), pairs: group },\n      ),\n  });\n\nexport type MarkRange = readonly [number, number];\n\nexport interface WordMarks {\n  readonly before: ReadonlyArray<MarkRange>;\n  readonly after: ReadonlyArray<MarkRange>;\n}\n\nconst TOKEN = /\\s+|[\\p{L}\\p{N}_]+|[^\\s\\p{L}\\p{N}_]/gu;\n\nconst tokensOf = (line: string): ReadonlyArray<string> =>\n  line.match(TOKEN) ?? [];\n\nconst mergedRanges = (\n  tokens: ReadonlyArray<string>,\n  kept: ReadonlySet<number>,\n): ReadonlyArray<MarkRange> => {\n  const ranges: Array<[number, number]> = [];\n  let at = 0;\n  tokens.forEach((token, index) => {\n    const end = at + token.length;\n    if (!kept.has(index)) {\n      const last = ranges[ranges.length - 1];\n      if (last !== undefined && last[1] === at) last[1] = end;\n      else ranges.push([at, end]);\n    }\n    at = end;\n  });\n  return ranges;\n};\n\nexport const wordMarks = (before: string, after: string): WordMarks => {\n  const a = tokensOf(before);\n  const b = tokensOf(after);\n  const table = Array.from({ length: a.length + 1 }, () =>\n    Array.from({ length: b.length + 1 }, () => 0),\n  );\n  for (let i = a.length - 1; i >= 0; i--) {\n    for (let j = b.length - 1; j >= 0; j--) {\n      table[i]![j] =\n        a[i] === b[j]\n          ? table[i + 1]![j + 1]! + 1\n          : Math.max(table[i + 1]![j]!, table[i]![j + 1]!);\n    }\n  }\n  const keptA = new Set<number>();\n  const keptB = new Set<number>();\n  let i = 0;\n  let j = 0;\n  while (i < a.length && j < b.length) {\n    if (a[i] === b[j]) {\n      keptA.add(i++);\n      keptB.add(j++);\n    } else if (table[i + 1]![j]! >= table[i]![j + 1]!) i++;\n    else j++;\n  }\n  return { before: mergedRanges(a, keptA), after: mergedRanges(b, keptB) };\n};\n\nexport interface StyledRun<Role> {\n  readonly text: string;\n  readonly role?: Role;\n  readonly marks: ReadonlyArray<readonly [number, number]>;\n}\n\nconst BREAK_AFTER = \" .(,\";\n\nexport const wrapStarts = (\n  text: string,\n  columns: number,\n): ReadonlyArray<number> => {\n  const starts = [0];\n  let start = 0;\n  while (text.length - start > columns) {\n    const window = text.slice(start, start + columns);\n    const breaks = [...BREAK_AFTER].map((mark) => window.lastIndexOf(mark));\n    const at = Math.max(...breaks);\n    start += at > 0 ? at + 1 : columns;\n    starts.push(start);\n  }\n  return starts;\n};\n\nexport const styledRows = <Role>(\n  runs: ReadonlyArray<{ readonly text: string; readonly role?: Role }>,\n  marks: ReadonlyArray<readonly [number, number]>,\n  columns: number,\n): ReadonlyArray<ReadonlyArray<StyledRun<Role>>> => {\n  const text = runs.map((run) => run.text).join(\"\");\n  const starts = wrapStarts(text, columns);\n  const joined = marks.reduce<ReadonlyArray<readonly [number, number]>>(\n    (out, mark) => {\n      const last = out.at(-1);\n      return last !== undefined && text.slice(last[1], mark[0]).trim() === \"\"\n        ? [...out.slice(0, -1), [last[0], mark[1]] as const]\n        : [...out, mark];\n    },\n    [],\n  );\n  return starts.map((from, index) => {\n    const to = starts[index + 1] ?? text.length;\n    let at = 0;\n    return runs.flatMap((run) => {\n      const runFrom = at;\n      at += run.text.length;\n      const a = Math.max(from, runFrom);\n      const b = Math.min(to, at);\n      if (a >= b) return [];\n      const inside = joined.flatMap(([m0, m1]) => {\n        const c = Math.max(m0, a);\n        const d = Math.min(m1, b);\n        return c < d ? [[c - a, d - a] as const] : [];\n      });\n      return [\n        {\n          text: text.slice(a, b),\n          ...(run.role === undefined ? {} : { role: run.role }),\n          marks: inside,\n        },\n      ];\n    });\n  });\n};\n"
    },
    {
      "path": "packages/editor/src/internal/recipe.ts",
      "target": "@ui/gpuixui/editor/internal/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { classesToStyle } from \"../../tw/index.js\";\nimport type { Theme } from \"../../tokens/index.js\";\n\nexport const recipeStyle = (recipe: string, theme: Theme): StyleDesc =>\n  classesToStyle(recipe, theme).style;\n"
    },
    {
      "path": "packages/editor/src/internal/text.ts",
      "target": "@ui/gpuixui/editor/internal/text.ts",
      "type": "registry:lib",
      "content": "import { accessibleName } from \"../../primitives/core/index.js\";\n\nexport const draftOf = (value: unknown): string =>\n  typeof value === \"string\" ? value : \"\";\n\nexport const labelOf = (label: unknown, fallback: string): string =>\n  accessibleName(draftOf(label)) ?? fallback;\n"
    }
  ],
  "docs": "[Diff viewer](https://gpuixui.sudokw.com/editor/diff-viewer) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/editor/diff-viewer",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}