{"version":3,"file":"index.mjs","names":[],"sources":["../../src/TooltipIfTruncated/TooltipIfTruncated.tsx","../../src/TruncatedText/TruncatedText.helpers.ts","../../src/TruncatedText/TruncatedText.primitives.tsx","../../src/TruncatedText/TruncatedText.tsx","../../src/useTruncate/useTruncate.ts"],"sourcesContent":["import { type Optional, RefToTgphRef } from \"@telegraph/helpers\";\nimport { Tooltip, type TooltipProps } from \"@telegraph/tooltip\";\nimport { Text } from \"@telegraph/typography\";\nimport { type ReactNode, isValidElement, useRef, useState } from \"react\";\n\nexport type TooltipIfTruncatedProps = Optional<TooltipProps, \"label\"> & {\n  /**\n   * How to decide whether the trigger is truncated. Defaults to\n   * `scrollWidth > clientWidth`, which is correct for a single clipped element.\n   * Pass a custom test for cases the default can't see — e.g. a middle-truncated\n   * group whose container never overflows but whose segments do.\n   */\n  isTruncated?: (trigger: HTMLElement) => boolean;\n};\n\nconst overflows = (el: HTMLElement) => el.scrollWidth > el.clientWidth;\n\n/**\n * Shows a tooltip only when the child is actually truncated.\n *\n * Truncation is measured lazily, at the moment the tooltip tries to open\n * (hover/focus) — a discrete user event — rather than in a render effect. This\n * removes the every-render `setState` + `ResizeObserver` machinery that could\n * cascade into \"Maximum update depth exceeded\" (React #185, KNO-14285) in a\n * re-render-heavy tree. Each open re-measures fresh, so a newly opened tooltip is\n * never stale. An already-open tooltip is not force-closed if a later resize makes\n * the trigger fit again — gating runs on open attempts, not continuously — so it\n * closes on the next pointer-leave; that is the deliberate trade-off for dropping\n * the continuous `ResizeObserver`.\n */\nconst TooltipIfTruncated = ({\n  label,\n  children,\n  isTruncated = overflows,\n  onOpenChange,\n  ...props\n}: TooltipIfTruncatedProps) => {\n  const triggerRef = useRef<HTMLElement>(null);\n  const [open, setOpen] = useState(false);\n\n  const textToDisplayInTooltip = (() => {\n    if (label !== undefined) return label;\n    if (typeof children === \"string\") return children;\n    if (isValidElement(children)) {\n      const childProps = children.props as Record<string, unknown>;\n      return childProps.children;\n    }\n  })();\n\n  return (\n    <Tooltip\n      label={\n        <Text as=\"span\" size=\"1\">\n          {textToDisplayInTooltip as ReactNode}\n        </Text>\n      }\n      triggerRef={triggerRef}\n      // Spread caller props first so the controlled `open` / `onOpenChange`\n      // gating below always wins — otherwise a pass-through `onOpenChange` (or\n      // `open`) would replace the truncation gate and leave `open` stuck at\n      // `false`, so the tooltip could never appear. A caller's `onOpenChange`\n      // is still honored — it's forwarded from inside our handler.\n      {...props}\n      open={open}\n      onOpenChange={(next) => {\n        // Measure once, here — never during render. If the content isn't\n        // clipped, refuse to open. `setOpen` only ever fires from this user\n        // event, so there is no render→effect→setState cycle to run away.\n        const trigger = triggerRef.current;\n        if (next && !(trigger && isTruncated(trigger))) return;\n        setOpen(next);\n        onOpenChange?.(next);\n      }}\n    >\n      <RefToTgphRef>{children}</RefToTgphRef>\n    </Tooltip>\n  );\n};\n\nexport { TooltipIfTruncated };\n","import { useEffect, useLayoutEffect } from \"react\";\n\nimport type { Split, SplitMode, TruncatePriority } from \"./TruncatedText.types\";\n\n// ── Split strategies ────────────────────────────────────────────────────────\n// Pure string logic (no DOM): given the full string, return a `[head, tail]`\n// pair. The head is clipped from its end and the tail from its start, so the\n// elision lands in the middle (\"apps/…/BaseStepCard.tsx\"). Ported from Pierre's\n// `truncate`.\n\nconst isWhitespace = (char: string | undefined) =>\n  char !== undefined && /\\s/.test(char);\n\n// Nudge a split index away from a whitespace seam so `white-space: nowrap`\n// doesn't collapse the boundary space (\"Hello world\" → \"Helloworld\").\nconst avoidWhitespaceBoundary = (contents: string, centerIndex: number) => {\n  const isOnBoundary = (index: number) =>\n    isWhitespace(contents[index - 1]) || isWhitespace(contents[index]);\n  if (!isOnBoundary(centerIndex)) return centerIndex;\n  for (let offset = 1; offset < contents.length; offset++) {\n    const before = centerIndex - offset;\n    if (before > 0 && !isOnBoundary(before)) return before;\n    const after = centerIndex + offset;\n    if (after < contents.length && !isOnBoundary(after)) return after;\n  }\n  return centerIndex;\n};\n\nconst centerSplitIndex = (s: string) =>\n  avoidWhitespaceBoundary(s, Math.ceil(s.length / 2));\n\nexport const splitCenter = (s: string): [string, string] => {\n  if (s.length < 2) return [s, \"\"];\n  const i = centerSplitIndex(s);\n  return [s.slice(0, i), s.slice(i)];\n};\n\nconst splitAtLast = (\n  s: string,\n  char: string,\n  maxTailLength: number,\n): [string, string] => {\n  if (s.length < 4) return [s, \"\"];\n  const idx = s.lastIndexOf(char) + 1;\n  const tailLength = s.length - idx;\n  const i = idx >= 1 && tailLength <= maxTailLength ? idx : centerSplitIndex(s);\n  return [s.slice(0, i), s.slice(i)];\n};\n\nexport const splitExtension = (s: string): [string, string] =>\n  splitAtLast(s, \".\", 10);\nexport const splitLeafPath = (s: string): [string, string] =>\n  splitAtLast(s, \"/\", 25);\n\nexport const splitByIndex = (s: string, index: number): [string, string] =>\n  Number.isFinite(index) ? [s.slice(0, index), s.slice(index)] : splitCenter(s);\n\n/** Keep the last `offset` characters in the tail (e.g. a file extension). */\nexport const splitLast = (s: string, offset: number): [string, string] =>\n  offset > 0 && offset < s.length\n    ? splitByIndex(s, s.length - offset)\n    : splitCenter(s);\n\n/** Keep the first `offset` characters in the head. */\nexport const splitFirst = (s: string, offset: number): [string, string] =>\n  offset > 0 && offset < s.length ? splitByIndex(s, offset) : splitCenter(s);\n\nconst SPLITTERS: Record<SplitMode, (s: string) => [string, string]> = {\n  center: splitCenter,\n  extension: splitExtension,\n  \"leaf-path\": splitLeafPath,\n};\n\nexport const resolveSplit = (\n  split: Split,\n  contents: string,\n): [string, string] => {\n  if (typeof split === \"function\") return split(contents);\n  if (typeof split === \"number\") return splitByIndex(contents, split);\n  if (Array.isArray(split)) {\n    const [type, offset] = split;\n    return type === \"last\"\n      ? splitLast(contents, offset)\n      : splitFirst(contents, offset);\n  }\n  return SPLITTERS[split](contents);\n};\n\n// ── Measure-and-slice (middle truncation) ───────────────────────────────────\n\n// `useLayoutEffect` on the client (measures before paint, so no flash), plain\n// `useEffect` on the server (avoids React's SSR warning).\nexport const useIsomorphicLayoutEffect =\n  typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\n/**\n * The longest `head…tail` slice of `text` that fits `available` px, measured with\n * `measure`. Only whole characters are dropped, so it never cuts a glyph, and the\n * result fits exactly, so there is no trailing-slack gap at the seam.\n *\n * `split=\"center\"` is balanced — it keeps a prefix of the head AND a suffix of the\n * tail so the marker sits in the visual middle and both ends stay visible at any\n * width. The anchored strategies (`leaf-path`, `extension`, offsets) instead keep\n * one whole side (chosen by `priority`) and truncate the other.\n */\nexport const sliceToFit = (\n  text: string,\n  split: Split,\n  priority: TruncatePriority,\n  marker: string,\n  available: number,\n  measure: (s: string) => number,\n): string => {\n  // Sub-pixel safety: `offsetWidth`/`clientWidth` are rounded, so a string that\n  // measures at exactly `available` can still render a fraction wider and get a\n  // \"…\" appended by the fallback `text-overflow`. Apply the 2px margin\n  // everywhere — including this early \"fits\" check — otherwise a string in that\n  // band is returned whole and left unflagged, yet the CSS ellipsis still clips\n  // it, so the trigger looks truncated but shows no tooltip.\n  const fits = (s: string) => measure(s) <= available - 2;\n  if (fits(text)) return text; // fits with the margin — no truncation\n  const [head, tail] = resolveSplit(split, text);\n  // Largest n in [0, max] for which `pick(n)` fits (fit is monotonic in n).\n  const largest = (max: number, pick: (n: number) => string) => {\n    let lo = 0;\n    let hi = max;\n    let best = 0;\n    while (lo <= hi) {\n      const mid = (lo + hi) >> 1;\n      if (fits(pick(mid))) {\n        best = mid;\n        lo = mid + 1;\n      } else {\n        hi = mid - 1;\n      }\n    }\n    return best;\n  };\n\n  // `center` is balanced: keep a head prefix AND a tail suffix so the marker\n  // lands in the visual middle and both ends stay visible. `priority` gives the\n  // odd character to one side. (The anchored strategies below keep one side\n  // whole — right for a filename or extension, wrong for a centered elision.)\n  if (split === \"center\") {\n    const build = (k: number) => {\n      let h = Math.min(\n        priority === \"start\" ? Math.ceil(k / 2) : Math.floor(k / 2),\n        head.length,\n      );\n      const t = Math.min(k - h, tail.length);\n      h = Math.min(k - t, head.length);\n      return head.slice(0, h) + marker + tail.slice(tail.length - t);\n    };\n    return build(largest(head.length + tail.length, build));\n  }\n\n  if (priority === \"start\") {\n    // Keep the head whole, trim the tail from its start; if the head alone won't\n    // fit, drop the tail and trim the head from its end.\n    if (fits(head + marker)) {\n      const n = largest(\n        tail.length,\n        (n) => head + marker + tail.slice(tail.length - n),\n      );\n      return head + marker + tail.slice(tail.length - n);\n    }\n    const n = largest(head.length, (n) => head.slice(0, n) + marker);\n    return head.slice(0, n) + marker;\n  }\n\n  // priority \"end\" (default): keep the tail whole, trim the head from its end; if\n  // the tail alone won't fit, drop the head and trim the tail from its start.\n  if (fits(marker + tail)) {\n    const n = largest(head.length, (n) => head.slice(0, n) + marker + tail);\n    return head.slice(0, n) + marker + tail;\n  }\n  const n = largest(tail.length, (n) => marker + tail.slice(tail.length - n));\n  return marker + tail.slice(tail.length - n);\n};\n\n// ── Truncation predicates (for `TooltipIfTruncated`'s `isTruncated`) ─────────\n\n// The measure-and-slice output always fits, so the trigger never overflows; gate\n// the tooltip on whether the string was actually sliced instead of on `scrollWidth`.\nexport const middleSliceIsTruncated = (el: HTMLElement) =>\n  el.matches(\"[data-tgph-middle-truncated]\") ||\n  el.querySelector(\"[data-tgph-middle-truncated]\") != null;\n\n// A `variant=\"fade\"` middle group never overflows its own box (its segments do),\n// so the tooltip gates on whether either engine segment is clipped, not the trigger.\nexport const middleIsTruncated = (el: HTMLElement) =>\n  Array.from(el.querySelectorAll(\"[data-tgph-truncate]\")).some(\n    (seg) => seg.scrollWidth > seg.clientWidth,\n  );\n","import { useRef, useState } from \"react\";\n\nimport { sliceToFit, useIsomorphicLayoutEffect } from \"./TruncatedText.helpers\";\nimport type { Split, TruncatePriority } from \"./TruncatedText.types\";\n\ntype MiddleTruncateProps = {\n  text: string;\n  split?: Split;\n  priority?: TruncatePriority;\n  /** The elision marker. Concatenated into the string, so it must be a string. */\n  marker?: string;\n};\n\n/**\n * Middle truncation by measurement: render exactly the `head…tail` characters\n * that fit — crisp ellipsis, no gap, and never a cut glyph, at every width.\n *\n * The measurement is deliberately minimal and only runs where it's needed: this\n * component is mounted solely for `TruncatedText mode=\"middle\"`, and it recomputes\n * only on real size changes (a `ResizeObserver`), guarding `setState` so redundant\n * ticks are no-ops. There is no measure-in-render or unconditional `setState`, so\n * it cannot reproduce the \"Maximum update depth exceeded\" loop (React #185).\n */\nconst MiddleTruncate = ({\n  text,\n  split = \"center\",\n  priority = \"end\",\n  marker = \"…\",\n}: MiddleTruncateProps) => {\n  const ref = useRef<HTMLSpanElement>(null);\n  const [display, setDisplay] = useState(text);\n\n  useIsomorphicLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n\n    // A hidden probe measures candidate strings at their real rendered width — no\n    // canvas/font guessing. It is 0×0 + `overflow:hidden` (so it never adds page\n    // scroll) and measured via `scrollWidth` (the natural, unclipped text width).\n    // It lives in `el`'s parent, not `el`: `el`'s text child is React-managed — a\n    // re-render wipes an appended probe (and would leave `measure` reading 0) —\n    // and `el`'s `textContent` is read as the rendered slice, which a probe child\n    // would pollute. The parent shares the inherited font, so measurements match.\n    const probe = document.createElement(\"span\");\n    probe.setAttribute(\"aria-hidden\", \"true\");\n    probe.style.cssText =\n      \"position:absolute;top:0;left:0;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;white-space:nowrap;\";\n    const host = el.parentElement ?? el;\n    host.appendChild(probe);\n    const measure = (s: string) => {\n      if (!probe.isConnected) host.appendChild(probe); // survive a host re-render\n      probe.textContent = s;\n      return probe.scrollWidth;\n    };\n\n    const compute = () => {\n      const available = el.clientWidth;\n      // Until the box has a width and the font has loaded (an unloaded webfont\n      // measures 0-width), the text can't be sliced. Fall back to the full text\n      // — correct content, with the CSS ellipsis as a crude cue — rather than\n      // leaving a stale slice of a previous `text`; a later resize or\n      // `fonts.ready` re-slices. (Never a bare `return`, so `display` always\n      // tracks the current `text`.)\n      const next =\n        !available || measure(text) === 0\n          ? text\n          : sliceToFit(text, split, priority, marker, available, measure);\n      setDisplay((prev) => (prev === next ? prev : next));\n    };\n\n    compute();\n    const observer = new ResizeObserver(compute);\n    observer.observe(el);\n    // A webfont's metrics change the fit, but loading one doesn't resize the box\n    // (so the observer won't fire) — re-measure once fonts settle.\n    let active = true;\n    void document.fonts?.ready?.then(() => active && compute());\n\n    return () => {\n      active = false;\n      observer.disconnect();\n      probe.remove();\n    };\n  }, [text, split, priority, marker]);\n\n  return (\n    <span\n      ref={ref}\n      // Flags \"this was sliced\" → gates the tooltip. Full text shows unflagged\n      // only when we couldn't measure (no width / unloaded font) — i.e. while the\n      // element isn't visibly rendered — so there is never a visibly clipped\n      // label without a tooltip; the next measure slices and flags it.\n      data-tgph-middle-truncated={display === text ? undefined : \"\"}\n      style={{\n        display: \"block\",\n        minWidth: 0,\n        overflow: \"hidden\",\n        whiteSpace: \"nowrap\",\n        // Fallback for the pre-measurement paint (SSR / first frame): end-clip\n        // crisply rather than hard-cut. Once sliced, the text fits and this is a\n        // no-op.\n        textOverflow: \"ellipsis\",\n      }}\n    >\n      {display}\n    </span>\n  );\n};\n\nexport { MiddleTruncate };\n","import type { TgphComponentProps, TgphElement } from \"@telegraph/helpers\";\nimport { Text } from \"@telegraph/typography\";\nimport { type ComponentPropsWithoutRef, type ReactNode, type Ref } from \"react\";\n\nimport { TooltipIfTruncated } from \"../TooltipIfTruncated\";\n\nimport {\n  middleIsTruncated,\n  middleSliceIsTruncated,\n  resolveSplit,\n} from \"./TruncatedText.helpers\";\nimport { MiddleTruncate } from \"./TruncatedText.primitives\";\nimport type {\n  Split,\n  TruncatePriority,\n  TruncateVariant,\n  TruncatedTextMode,\n} from \"./TruncatedText.types\";\n\nexport type {\n  CustomSplitFn,\n  Split,\n  SplitOffset,\n  TruncatedTextMode,\n  TruncatePriority,\n  TruncateVariant,\n} from \"./TruncatedText.types\";\n\n// ── Internal CSS engine ─────────────────────────────────────────────────────\n// The extra modes are purely presentational: no state, no effects, no\n// ResizeObserver, no `scrollWidth` measurement. Overflow detection lives\n// entirely in `TruncatedText.styles.css` via a container size-query (ported from\n// Pierre's `truncate`). Because there is no measure→setState machinery, these\n// helpers cannot produce the \"Maximum update depth exceeded\" loop that a\n// JS-measured path could (see KNO-14285). They are an implementation detail of\n// `TruncatedText` and are not exported.\n\n/** `truncate` clips the end; `fruncate` clips the start. */\ntype ClipMode = \"truncate\" | \"fruncate\";\n\ntype ClipProps = {\n  children?: ReactNode;\n  mode?: ClipMode;\n  /** The overflow indicator. Defaults to an ellipsis. */\n  marker?: ReactNode;\n  variant?: TruncateVariant;\n  /** Forwarded to the root element so it can act as a tooltip trigger. */\n  tgphRef?: Ref<HTMLDivElement>;\n} & Omit<ComponentPropsWithoutRef<\"div\">, \"children\">;\n\nconst ClipContent = ({\n  mode,\n  children,\n}: {\n  mode: ClipMode;\n  children?: ReactNode;\n}) => (\n  <div>\n    <div data-tgph-truncate-content=\"visible\">\n      {mode === \"fruncate\" ? <span>{children}</span> : children}\n    </div>\n    <div data-tgph-truncate-content=\"overflow\" aria-hidden>\n      {mode === \"fruncate\" ? <span>{children}</span> : children}\n    </div>\n  </div>\n);\n\nconst ClipMarker = ({ marker }: { marker: ReactNode }) => (\n  <div aria-hidden data-tgph-truncate-marker-cell>\n    <div data-tgph-truncate-marker>{marker}</div>\n  </div>\n);\n\n/** One-line clip with a fade/ellipsis marker at the clipped edge. */\nconst TruncateClip = ({\n  children,\n  mode = \"truncate\",\n  marker = \"…\",\n  variant = \"default\",\n  tgphRef,\n  ...props\n}: ClipProps) => {\n  const content = (\n    <ClipContent key=\"content\" mode={mode}>\n      {children}\n    </ClipContent>\n  );\n  const markerNode = <ClipMarker key=\"marker\" marker={marker} />;\n\n  return (\n    <div\n      ref={tgphRef}\n      data-tgph-truncate={mode}\n      data-tgph-truncate-variant={variant}\n      {...props}\n    >\n      <div data-tgph-truncate-grid>\n        {mode === \"truncate\"\n          ? [content, markerNode]\n          : [markerNode, content, <div key=\"fill\" data-tgph-truncate-fill />]}\n      </div>\n    </div>\n  );\n};\n\ntype MiddleClipProps = {\n  children: string;\n  split?: Split;\n  marker?: ReactNode;\n  variant?: TruncateVariant;\n  /** Which end stays intact first when space is tight. Defaults to `end`. */\n  priority?: TruncatePriority;\n} & Omit<ComponentPropsWithoutRef<\"div\">, \"children\">;\n\n// Middle truncation (string only, like Pierre): split the string, clip the head\n// from its end and the tail from its start, so the middle is elided.\nconst MiddleClip = ({\n  children,\n  split = \"center\",\n  marker = \"…\",\n  variant = \"default\",\n  priority = \"end\",\n  ...props\n}: MiddleClipProps) => {\n  const [head, tail] = resolveSplit(split, children);\n\n  // Exactly one segment truncates (the other is kept whole by the priority flex\n  // in the stylesheet), so only that segment's container query fires and a single\n  // marker reveals at the seam — no de-duplication needed.\n  return (\n    <div\n      data-tgph-truncate-group=\"middle\"\n      data-tgph-truncate-priority={priority}\n      {...props}\n    >\n      <div data-tgph-truncate-segment=\"head\">\n        <TruncateClip mode=\"truncate\" marker={marker} variant={variant}>\n          {head}\n        </TruncateClip>\n      </div>\n      <div data-tgph-truncate-segment=\"tail\">\n        <TruncateClip mode=\"fruncate\" marker={marker} variant={variant}>\n          {tail}\n        </TruncateClip>\n      </div>\n    </div>\n  );\n};\n\n// Wrap RTL-clipped (front-truncated) content so its own glyphs keep logical\n// order. The RTL base direction — which is what flips `text-overflow: ellipsis`\n// to the start — would otherwise reorder leading/trailing neutral characters\n// (a leading \"/\" or \".\" would jump to the opposite end).\nconst LTR_ISOLATE = { direction: \"ltr\", unicodeBidi: \"isolate\" } as const;\n\n// ── Public component ────────────────────────────────────────────────────────\n\nexport type TruncatedTextProps<T extends TgphElement = \"span\"> = {\n  tooltipProps?: Partial<TgphComponentProps<typeof TooltipIfTruncated>>;\n  /**\n   * `truncate` (default) clips the end, `fruncate` the start, and `middle`\n   * elides the middle — all with native `text-overflow: ellipsis`, so they clip\n   * on glyph boundaries (never mid-glyph) and need no imported CSS. `middle`\n   * requires string children. A `fade` `variant` or a custom `marker` upgrades\n   * to the CSS engine (modern browsers only; constrain the engine clip box via\n   * `style`, not `maxWidth`).\n   */\n  mode?: TruncatedTextMode;\n  /** Ellipsis (default) vs. a soft fade into the background. `fade` uses the engine. */\n  variant?: TruncateVariant;\n  /** A custom overflow marker (defaults to the native ellipsis). Uses the engine. */\n  marker?: ReactNode;\n  /** `middle` mode: where to split the string. */\n  split?: Split;\n  /** `middle` mode: which end stays whole; the other truncates (one ellipsis). */\n  priority?: TruncatePriority;\n} & TgphComponentProps<typeof Text<T>>;\n\nconst TruncatedText = <T extends TgphElement = \"span\">(\n  truncatedTextProps: TruncatedTextProps<T>,\n) => {\n  const {\n    mode = \"truncate\",\n    variant,\n    marker: markerProp,\n    split,\n    priority,\n    tooltipProps,\n    style,\n    children,\n    as,\n    ...props\n  } = truncatedTextProps as TruncatedTextProps<\"span\">;\n  // `Text` types `as` as required and resolves its own `T` from it, so it has\n  // to be one concrete element. The value is forwarded unchanged, including\n  // `undefined`, so `Text`'s \"as prop is required\" throw still fires.\n  const textAs = as as \"span\";\n  // Middle truncation wraps its segments in a container, so it falls back to a\n  // `div` when no `as` is given.\n  const middleTextAs = (as ?? \"div\") as \"span\";\n  // The tooltip must show the full, unclipped string. Letting TooltipIfTruncated\n  // auto-extract it only unwraps one child level, so an engine/`middle` child\n  // would surface the elided, marker-laden markup instead. Pass the string\n  // explicitly; callers can still override it via `tooltipProps.label`.\n  const label = typeof children === \"string\" ? children : undefined;\n\n  // An empty marker means \"no custom marker\" — fall back to the native ellipsis\n  // rather than opting into the engine with a blank delimiter.\n  const marker = markerProp === \"\" ? undefined : markerProp;\n\n  // The overlay-marker engine (a container size-query + a masked marker) is used\n  // only for effects native truncation can't produce: the `fade` variant and\n  // custom `marker`s. Plain ellipsis truncation — end, front, and middle — uses\n  // native `text-overflow: ellipsis`, which clips on glyph boundaries (never\n  // mid-glyph), needs no JS/container query, and requires no imported CSS.\n  const useEngine = variant === \"fade\" || marker !== undefined;\n\n  // Middle truncation. `variant=\"fade\"` uses the CSS engine (a dissolve). The\n  // default measures and slices the string to exactly the `head…tail` characters\n  // that fit — a crisp \"…\", no seam gap, and never a cut glyph, at every width.\n  // Pure CSS can't do all three at once (box-filling vs glyph-boundary clipping),\n  // so this is the one mode that measures; the JS is scoped to `middle`, runs only\n  // on real resizes, and is guarded (see TruncatedText.primitives).\n  if (mode === \"middle\" && typeof children === \"string\") {\n    // Measure-and-slice can only splice a *string* marker into the text; fall to\n    // the CSS engine for the `fade` variant or a ReactNode marker (which it can\n    // render as a node, while measure-and-slice would drop it).\n    const stringMarker = typeof marker === \"string\" ? marker : undefined;\n    if (\n      variant === \"fade\" ||\n      (marker !== undefined && stringMarker === undefined)\n    ) {\n      return (\n        <TooltipIfTruncated\n          label={label}\n          {...tooltipProps}\n          isTruncated={middleIsTruncated}\n        >\n          <Text\n            as={middleTextAs}\n            {...props}\n            style={{ display: \"flex\", minWidth: 0, ...style }}\n          >\n            <MiddleClip\n              variant={variant ?? \"default\"}\n              marker={marker}\n              split={split}\n              priority={priority}\n            >\n              {children}\n            </MiddleClip>\n          </Text>\n        </TooltipIfTruncated>\n      );\n    }\n    return (\n      <TooltipIfTruncated\n        label={label}\n        {...tooltipProps}\n        isTruncated={middleSliceIsTruncated}\n      >\n        <Text\n          as={middleTextAs}\n          {...props}\n          // `display: block` so an inline `as` (e.g. \"span\") still honors\n          // `maxWidth` — the slice measures this element's width.\n          style={{ display: \"block\", minWidth: 0, ...style }}\n        >\n          <MiddleTruncate\n            text={children}\n            split={split}\n            priority={priority}\n            marker={stringMarker}\n          />\n        </Text>\n      </TooltipIfTruncated>\n    );\n  }\n\n  // Engine end/front truncation. The inner Text carries typography; the engine\n  // root (which overflows when clipped) is the tooltip trigger.\n  if (useEngine && (mode === \"truncate\" || mode === \"fruncate\")) {\n    return (\n      <TooltipIfTruncated label={label} {...tooltipProps}>\n        <TruncateClip\n          mode={mode}\n          variant={variant}\n          marker={marker}\n          style={style}\n        >\n          <Text as={textAs} {...props}>\n            {children}\n          </Text>\n        </TruncateClip>\n      </TooltipIfTruncated>\n    );\n  }\n\n  // Native front truncation: `direction: rtl` flips `text-overflow: ellipsis` to\n  // clip (and place the ellipsis) at the start. The RTL base would reorder\n  // leading/trailing neutrals (a leading \"/\" would jump to the end), so the LTR\n  // content is wrapped in an isolate to keep its logical glyph order intact.\n  if (mode === \"fruncate\") {\n    return (\n      <TooltipIfTruncated label={label} {...tooltipProps}>\n        <Text\n          as={textAs}\n          style={{\n            display: \"block\",\n            overflow: \"hidden\",\n            whiteSpace: \"nowrap\",\n            textOverflow: \"ellipsis\",\n            direction: \"rtl\",\n            ...style,\n          }}\n          {...props}\n        >\n          <span style={LTR_ISOLATE}>{children}</span>\n        </Text>\n      </TooltipIfTruncated>\n    );\n  }\n\n  // Default: native end truncation. Also the fallback when `middle` is given\n  // non-string children.\n  return (\n    <TooltipIfTruncated label={label} {...tooltipProps}>\n      <Text\n        as={textAs}\n        style={{\n          display: \"block\",\n          overflow: \"hidden\",\n          whiteSpace: \"nowrap\",\n          textOverflow: \"ellipsis\",\n          ...style,\n        }}\n        {...props}\n      >\n        {children}\n      </Text>\n    </TooltipIfTruncated>\n  );\n};\n\nexport { TruncatedText };\n","import {\n  type DependencyList,\n  type RefObject,\n  useEffect,\n  useState,\n} from \"react\";\n\ntype UseTruncateParams = {\n  tgphRef: RefObject<HTMLElement | null>;\n};\n\n/**\n * Reactive \"is this element currently truncated?\" (`scrollWidth > clientWidth`),\n * kept up to date with a `ResizeObserver`.\n *\n * This is the JS counterpart to `TruncatedText`'s CSS-only engine modes, for\n * two different needs:\n * - Use `TruncatedText` (`mode=\"fruncate\"` / `\"middle\"`, `variant=\"fade\"`) when\n *   you only need to *render* truncated text — detection stays in CSS, no JS runs.\n * - Use `useTruncate` when you need the truncation state *as a value in render*\n *   — to gate a tooltip, toggle \"show more\", swap an icon, etc.\n *\n * A pure CSS container query can't be read back into React state without\n * observation, so a `ResizeObserver` is the right bridge. It measures the\n * element you point it at, so it works on any clipped element — native\n * `text-overflow: ellipsis` or a `TruncatedText` engine root. (`mode=\"middle\"`\n * is the exception: its container never overflows, so measure a segment, or\n * gate a tooltip with `TooltipIfTruncated`'s `isTruncated` prop instead.)\n *\n * The state write is value-guarded and the observer is created once per mounted\n * element, so a redundant measurement is a genuine no-op even when the\n * surrounding tree re-renders constantly (e.g. a ReactFlow graph). That is what\n * keeps it from cascading into \"Maximum update depth exceeded\" (React #185,\n * KNO-14285) — the crash came from the previous unconditional `setState`, not\n * from measuring.\n */\nconst useTruncate = (\n  { tgphRef }: UseTruncateParams,\n  deps: DependencyList = [],\n) => {\n  const [truncated, setTruncated] = useState(false);\n\n  useEffect(() => {\n    const element = tgphRef.current;\n\n    // Only write when the value actually changes. React usually absorbs a\n    // same-value `setState` via its bailout, but in a continuously\n    // re-rendering tree the fiber's lanes stay dirty and that bailout can be\n    // defeated; returning the previous value keeps a redundant update a true\n    // no-op regardless of how (un)stable the caller's `deps` are.\n    const setTruncatedIfChanged = (next: boolean) =>\n      setTruncated((prev) => (prev === next ? prev : next));\n\n    if (!element) {\n      setTruncatedIfChanged(false);\n      return;\n    }\n\n    const checkTruncation = () =>\n      setTruncatedIfChanged(element.scrollWidth > element.clientWidth);\n\n    // Initial check\n    checkTruncation();\n\n    // Re-check whenever the element is resized.\n    const resizeObserver = new ResizeObserver(checkTruncation);\n    resizeObserver.observe(element);\n\n    return () => resizeObserver.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [tgphRef, ...deps]);\n\n  return { truncated };\n};\n\nexport { useTruncate };\n"],"mappings":";;;;;;AAeA,IAAM,KAAa,MAAoB,EAAG,cAAc,EAAG,aAerD,KAAsB,EAC1B,UACA,aACA,iBAAc,GACd,iBACA,GAAG,QAC0B;CAC7B,IAAM,IAAa,EAAoB,IAAI,GACrC,CAAC,GAAM,KAAW,EAAS,EAAK,GAEhC,WAAgC;EACpC,IAAI,MAAU,KAAA,GAAW,OAAO;EAChC,IAAI,OAAO,KAAa,UAAU,OAAO;EACzC,IAAI,EAAe,CAAQ,GAEzB,OADmB,EAAS,MACV;CAEtB,EAAA,CAAG;CAEH,OACE,kBAAC,GAAD;EACE,OACE,kBAAC,GAAD;GAAM,IAAG;GAAO,MAAK;GAClB,UAAA;EACG,CAAA;EAEI;EAMZ,GAAI;EACE;EACN,eAAe,MAAS;GAItB,IAAM,IAAU,EAAW;GACvB,KAAQ,EAAE,KAAW,EAAY,CAAO,OAC5C,EAAQ,CAAI,GACZ,IAAe,CAAI;EACrB;EAEA,UAAA,kBAAC,GAAD,EAAe,YAAuB,CAAA;CAC/B,CAAA;AAEb,GCnEM,KAAgB,MACpB,MAAS,KAAA,KAAa,KAAK,KAAK,CAAI,GAIhC,KAA2B,GAAkB,MAAwB;CACzE,IAAM,KAAgB,MACpB,EAAa,EAAS,IAAQ,EAAE,KAAK,EAAa,EAAS,EAAM;CACnE,IAAI,CAAC,EAAa,CAAW,GAAG,OAAO;CACvC,KAAK,IAAI,IAAS,GAAG,IAAS,EAAS,QAAQ,KAAU;EACvD,IAAM,IAAS,IAAc;EAC7B,IAAI,IAAS,KAAK,CAAC,EAAa,CAAM,GAAG,OAAO;EAChD,IAAM,IAAQ,IAAc;EAC5B,IAAI,IAAQ,EAAS,UAAU,CAAC,EAAa,CAAK,GAAG,OAAO;CAC9D;CACA,OAAO;AACT,GAEM,KAAoB,MACxB,EAAwB,GAAG,KAAK,KAAK,EAAE,SAAS,CAAC,CAAC,GAEvC,KAAe,MAAgC;CAC1D,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE;CAC/B,IAAM,IAAI,EAAiB,CAAC;CAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AACnC,GAEM,KACJ,GACA,GACA,MACqB;CACrB,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE;CAC/B,IAAM,IAAM,EAAE,YAAY,CAAI,IAAI,GAC5B,IAAa,EAAE,SAAS,GACxB,IAAI,KAAO,KAAK,KAAc,IAAgB,IAAM,EAAiB,CAAC;CAC5E,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AACnC,GAEa,KAAkB,MAC7B,EAAY,GAAG,KAAK,EAAE,GACX,KAAiB,MAC5B,EAAY,GAAG,KAAK,EAAE,GAEX,KAAgB,GAAW,MACtC,OAAO,SAAS,CAAK,IAAI,CAAC,EAAE,MAAM,GAAG,CAAK,GAAG,EAAE,MAAM,CAAK,CAAC,IAAI,EAAY,CAAC,GAGjE,KAAa,GAAW,MACnC,IAAS,KAAK,IAAS,EAAE,SACrB,EAAa,GAAG,EAAE,SAAS,CAAM,IACjC,EAAY,CAAC,GAGN,KAAc,GAAW,MACpC,IAAS,KAAK,IAAS,EAAE,SAAS,EAAa,GAAG,CAAM,IAAI,EAAY,CAAC,GAErE,IAAgE;CACpE,QAAQ;CACR,WAAW;CACX,aAAa;AACf,GAEa,KACX,GACA,MACqB;CACrB,IAAI,OAAO,KAAU,YAAY,OAAO,EAAM,CAAQ;CACtD,IAAI,OAAO,KAAU,UAAU,OAAO,EAAa,GAAU,CAAK;CAClE,IAAI,MAAM,QAAQ,CAAK,GAAG;EACxB,IAAM,CAAC,GAAM,KAAU;EACvB,OAAO,MAAS,SACZ,EAAU,GAAU,CAAM,IAC1B,EAAW,GAAU,CAAM;CACjC;CACA,OAAO,EAAU,EAAM,CAAC,CAAQ;AAClC,GAMa,IACX,OAAO,SAAW,MAAc,IAAkB,GAYvC,KACX,GACA,GACA,GACA,GACA,GACA,MACW;CAOX,IAAM,KAAQ,MAAc,EAAQ,CAAC,KAAK,IAAY;CACtD,IAAI,EAAK,CAAI,GAAG,OAAO;CACvB,IAAM,CAAC,GAAM,KAAQ,EAAa,GAAO,CAAI,GAEvC,KAAW,GAAa,MAAgC;EAC5D,IAAI,IAAK,GACL,IAAK,GACL,IAAO;EACX,OAAO,KAAM,IAAI;GACf,IAAM,IAAO,IAAK,KAAO;GACzB,AAAI,EAAK,EAAK,CAAG,CAAC,KAChB,IAAO,GACP,IAAK,IAAM,KAEX,IAAK,IAAM;EAEf;EACA,OAAO;CACT;CAMA,IAAI,MAAU,UAAU;EACtB,IAAM,KAAS,MAAc;GAC3B,IAAI,IAAI,KAAK,IACX,MAAa,UAAU,KAAK,KAAK,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,GAC1D,EAAK,MACP,GACM,IAAI,KAAK,IAAI,IAAI,GAAG,EAAK,MAAM;GAErC,OADA,IAAI,KAAK,IAAI,IAAI,GAAG,EAAK,MAAM,GACxB,EAAK,MAAM,GAAG,CAAC,IAAI,IAAS,EAAK,MAAM,EAAK,SAAS,CAAC;EAC/D;EACA,OAAO,EAAM,EAAQ,EAAK,SAAS,EAAK,QAAQ,CAAK,CAAC;CACxD;CAEA,IAAI,MAAa,SAAS;EAGxB,IAAI,EAAK,IAAO,CAAM,GAAG;GACvB,IAAM,IAAI,EACR,EAAK,SACJ,MAAM,IAAO,IAAS,EAAK,MAAM,EAAK,SAAS,CAAC,CACnD;GACA,OAAO,IAAO,IAAS,EAAK,MAAM,EAAK,SAAS,CAAC;EACnD;EACA,IAAM,IAAI,EAAQ,EAAK,SAAS,MAAM,EAAK,MAAM,GAAG,CAAC,IAAI,CAAM;EAC/D,OAAO,EAAK,MAAM,GAAG,CAAC,IAAI;CAC5B;CAIA,IAAI,EAAK,IAAS,CAAI,GAAG;EACvB,IAAM,IAAI,EAAQ,EAAK,SAAS,MAAM,EAAK,MAAM,GAAG,CAAC,IAAI,IAAS,CAAI;EACtE,OAAO,EAAK,MAAM,GAAG,CAAC,IAAI,IAAS;CACrC;CACA,IAAM,IAAI,EAAQ,EAAK,SAAS,MAAM,IAAS,EAAK,MAAM,EAAK,SAAS,CAAC,CAAC;CAC1E,OAAO,IAAS,EAAK,MAAM,EAAK,SAAS,CAAC;AAC5C,GAMa,KAA0B,MACrC,EAAG,QAAQ,8BAA8B,KACzC,EAAG,cAAc,8BAA8B,KAAK,MAIzC,KAAqB,MAChC,MAAM,KAAK,EAAG,iBAAiB,sBAAsB,CAAC,CAAC,CAAC,MACrD,MAAQ,EAAI,cAAc,EAAI,WACjC,GC1KI,KAAkB,EACtB,SACA,WAAQ,UACR,cAAW,OACX,YAAS,UACgB;CACzB,IAAM,IAAM,EAAwB,IAAI,GAClC,CAAC,GAAS,KAAc,EAAS,CAAI;CAuD3C,OArDA,QAAgC;EAC9B,IAAM,IAAK,EAAI;EACf,IAAI,CAAC,GAAI;EAST,IAAM,IAAQ,SAAS,cAAc,MAAM;EAE3C,AADA,EAAM,aAAa,eAAe,MAAM,GACxC,EAAM,MAAM,UACV;EACF,IAAM,IAAO,EAAG,iBAAiB;EACjC,EAAK,YAAY,CAAK;EACtB,IAAM,KAAW,OACV,EAAM,eAAa,EAAK,YAAY,CAAK,GAC9C,EAAM,cAAc,GACb,EAAM,cAGT,UAAgB;GACpB,IAAM,IAAY,EAAG,aAOf,IACJ,CAAC,KAAa,EAAQ,CAAI,MAAM,IAC5B,IACA,EAAW,GAAM,GAAO,GAAU,GAAQ,GAAW,CAAO;GAClE,GAAY,MAAU,MAAS,IAAO,IAAO,CAAK;EACpD;EAEA,EAAQ;EACR,IAAM,IAAW,IAAI,eAAe,CAAO;EAC3C,EAAS,QAAQ,CAAE;EAGnB,IAAI,IAAS;EAGb,OAFA,SAAc,OAAO,OAAO,WAAW,KAAU,EAAQ,CAAC,SAE7C;GAGX,AAFA,IAAS,IACT,EAAS,WAAW,GACpB,EAAM,OAAO;EACf;CACF,GAAG;EAAC;EAAM;EAAO;EAAU;CAAM,CAAC,GAGhC,kBAAC,QAAD;EACO;EAKL,8BAA4B,MAAY,IAAO,KAAA,IAAY;EAC3D,OAAO;GACL,SAAS;GACT,UAAU;GACV,UAAU;GACV,YAAY;GAIZ,cAAc;EAChB;EAEC,UAAA;CACG,CAAA;AAEV,GCzDM,KAAe,EACnB,SACA,kBAKA,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;CAAK,8BAA2B;CAC7B,UAAA,MAAS,aAAa,kBAAC,QAAD,EAAO,YAAe,CAAA,IAAI;AAC9C,CAAA,GACL,kBAAC,OAAD;CAAK,8BAA2B;CAAW,eAAA;CACxC,UAAA,MAAS,aAAa,kBAAC,QAAD,EAAO,YAAe,CAAA,IAAI;AAC9C,CAAA,CACF,EAAA,CAAA,GAGD,KAAc,EAAE,gBACpB,kBAAC,OAAD;CAAK,eAAA;CAAY,kCAAA;CACf,UAAA,kBAAC,OAAD;EAAK,6BAAA;EAA2B,UAAA;CAAY,CAAA;AACzC,CAAA,GAID,KAAgB,EACpB,aACA,UAAO,YACP,YAAS,KACT,aAAU,WACV,YACA,GAAG,QACY;CACf,IAAM,IACJ,kBAAC,GAAD;EAAiC;EAC9B;CACU,GAFI,SAEJ,GAET,IAAa,kBAAC,GAAD,EAAiC,UAAS,GAA1B,QAA0B;CAE7D,OACE,kBAAC,OAAD;EACE,KAAK;EACL,sBAAoB;EACpB,8BAA4B;EAC5B,GAAI;EAEJ,UAAA,kBAAC,OAAD;GAAK,2BAAA;GACF,UAAA,MAAS,aACN,CAAC,GAAS,CAAU,IACpB;IAAC;IAAY;IAAS,kBAAC,OAAD,EAAgB,2BAAA,GAAyB,GAAhC,MAAgC;GAAC;EACjE,CAAA;CACF,CAAA;AAET,GAaM,KAAc,EAClB,aACA,WAAQ,UACR,YAAS,KACT,aAAU,WACV,cAAW,OACX,GAAG,QACkB;CACrB,IAAM,CAAC,GAAM,KAAQ,EAAa,GAAO,CAAQ;CAKjD,OACE,kBAAC,OAAD;EACE,4BAAyB;EACzB,+BAA6B;EAC7B,GAAI;EAHN,UAAA,CAKE,kBAAC,OAAD;GAAK,8BAA2B;GAC9B,UAAA,kBAAC,GAAD;IAAc,MAAK;IAAmB;IAAiB;IACpD,UAAA;GACW,CAAA;EACX,CAAA,GACL,kBAAC,OAAD;GAAK,8BAA2B;GAC9B,UAAA,kBAAC,GAAD;IAAc,MAAK;IAAmB;IAAiB;IACpD,UAAA;GACW,CAAA;EACX,CAAA,CACF;;AAET,GAMM,IAAc;CAAE,WAAW;CAAO,aAAa;AAAU,GAyBzD,KACJ,MACG;CACH,IAAM,EACJ,UAAO,YACP,YACA,QAAQ,GACR,UACA,aACA,iBACA,UACA,aACA,OACA,GAAG,MACD,GAIE,IAAS,GAGT,IAAgB,KAAM,OAKtB,IAAQ,OAAO,KAAa,WAAW,IAAW,KAAA,GAIlD,IAAS,MAAe,KAAK,KAAA,IAAY,GAOzC,IAAY,MAAY,UAAU,MAAW,KAAA;CAQnD,IAAI,MAAS,YAAY,OAAO,KAAa,UAAU;EAIrD,IAAM,IAAe,OAAO,KAAW,WAAW,IAAS,KAAA;EA4B3D,OA1BE,MAAY,UACX,MAAW,KAAA,KAAa,MAAiB,KAAA,IAGxC,kBAAC,GAAD;GACS;GACP,GAAI;GACJ,aAAa;GAEb,UAAA,kBAAC,GAAD;IACE,IAAI;IACJ,GAAI;IACJ,OAAO;KAAE,SAAS;KAAQ,UAAU;KAAG,GAAG;IAAM;IAEhD,UAAA,kBAAC,GAAD;KACE,SAAS,KAAW;KACZ;KACD;KACG;KAET;IACS,CAAA;GACR,CAAA;EACY,CAAA,IAItB,kBAAC,GAAD;GACS;GACP,GAAI;GACJ,aAAa;GAEb,UAAA,kBAAC,GAAD;IACE,IAAI;IACJ,GAAI;IAGJ,OAAO;KAAE,SAAS;KAAS,UAAU;KAAG,GAAG;IAAM;IAEjD,UAAA,kBAAC,GAAD;KACE,MAAM;KACC;KACG;KACV,QAAQ;IACT,CAAA;GACG,CAAA;EACY,CAAA;CAExB;CAgDA,OA5CI,MAAc,MAAS,cAAc,MAAS,cAE9C,kBAAC,GAAD;EAA2B;EAAO,GAAI;EACpC,UAAA,kBAAC,GAAD;GACQ;GACG;GACD;GACD;GAEP,UAAA,kBAAC,GAAD;IAAM,IAAI;IAAQ,GAAI;IACnB;GACG,CAAA;EACM,CAAA;CACI,CAAA,IAQpB,MAAS,aAET,kBAAC,GAAD;EAA2B;EAAO,GAAI;EACpC,UAAA,kBAAC,GAAD;GACE,IAAI;GACJ,OAAO;IACL,SAAS;IACT,UAAU;IACV,YAAY;IACZ,cAAc;IACd,WAAW;IACX,GAAG;GACL;GACA,GAAI;GAEJ,UAAA,kBAAC,QAAD;IAAM,OAAO;IAAc;GAAe,CAAA;EACtC,CAAA;CACY,CAAA,IAOtB,kBAAC,GAAD;EAA2B;EAAO,GAAI;EACpC,UAAA,kBAAC,GAAD;GACE,IAAI;GACJ,OAAO;IACL,SAAS;IACT,UAAU;IACV,YAAY;IACZ,cAAc;IACd,GAAG;GACL;GACA,GAAI;GAEH;EACG,CAAA;CACY,CAAA;AAExB,GClTM,KACJ,EAAE,cACF,IAAuB,CAAC,MACrB;CACH,IAAM,CAAC,GAAW,KAAgB,EAAS,EAAK;CAgChD,OA9BA,QAAgB;EACd,IAAM,IAAU,EAAQ,SAOlB,KAAyB,MAC7B,GAAc,MAAU,MAAS,IAAO,IAAO,CAAK;EAEtD,IAAI,CAAC,GAAS;GACZ,EAAsB,EAAK;GAC3B;EACF;EAEA,IAAM,UACJ,EAAsB,EAAQ,cAAc,EAAQ,WAAW;EAGjE,EAAgB;EAGhB,IAAM,IAAiB,IAAI,eAAe,CAAe;EAGzD,OAFA,EAAe,QAAQ,CAAO,SAEjB,EAAe,WAAW;CAEzC,GAAG,CAAC,GAAS,GAAG,CAAI,CAAC,GAEd,EAAE,aAAU;AACrB"}