@reopt-ai/opt-ui 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/COMPONENT_CATALOG.md +177 -66
  2. package/README.md +21 -5
  3. package/dist/core/index.cjs +106 -104
  4. package/dist/core/index.d.cts +3 -3
  5. package/dist/core/index.d.ts +3 -3
  6. package/dist/core/index.js +3 -3
  7. package/dist/docs/02-components/01-core.md +92 -41
  8. package/dist/docs/02-components/02-visuals.md +1 -1
  9. package/dist/docs/02-components/03-shells.md +96 -26
  10. package/dist/docs/02-components/04-surfaces.md +1 -1
  11. package/dist/docs/02-components/index.md +5 -5
  12. package/dist/{field-sidebar-B5vk3luQ.d.cts → field-sidebar-B5SQC35J.d.cts} +226 -3
  13. package/dist/field-sidebar-B5SQC35J.d.cts.map +1 -0
  14. package/dist/{field-sidebar-B7QkWvMG.js → field-sidebar-BXl5RJ3q.js} +599 -43
  15. package/dist/field-sidebar-BXl5RJ3q.js.map +1 -0
  16. package/dist/{field-sidebar-Dk9gJGA_.d.ts → field-sidebar-CBeWE8xX.d.ts} +226 -3
  17. package/dist/field-sidebar-CBeWE8xX.d.ts.map +1 -0
  18. package/dist/{field-sidebar-mFHemxBz.cjs → field-sidebar-DAuN4jPV.cjs} +883 -309
  19. package/dist/id-registry.cjs +46 -6
  20. package/dist/id-registry.d.cts.map +1 -1
  21. package/dist/id-registry.d.ts.map +1 -1
  22. package/dist/id-registry.js +46 -6
  23. package/dist/id-registry.js.map +1 -1
  24. package/dist/id-registry.json +97 -12
  25. package/dist/index.cjs +593 -587
  26. package/dist/index.d.cts +10 -6
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.ts +10 -6
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +20 -19
  31. package/dist/index.js.map +1 -1
  32. package/dist/{key-pad-menu-D8OS_VCT.d.cts → key-pad-menu-BxTy_u0Q.d.cts} +152 -51
  33. package/dist/key-pad-menu-BxTy_u0Q.d.cts.map +1 -0
  34. package/dist/{key-pad-menu-DoGskWbp.cjs → key-pad-menu-Cg6rZBie.cjs} +670 -923
  35. package/dist/{key-pad-menu-EEb6Ho5h.d.ts → key-pad-menu-u5GTJl9B.d.ts} +152 -51
  36. package/dist/key-pad-menu-u5GTJl9B.d.ts.map +1 -0
  37. package/dist/{key-pad-menu-BFd79JoS.js → key-pad-menu-wXkWn1I1.js} +500 -699
  38. package/dist/key-pad-menu-wXkWn1I1.js.map +1 -0
  39. package/dist/meta.cjs +486 -9
  40. package/dist/meta.d.cts.map +1 -1
  41. package/dist/meta.d.ts.map +1 -1
  42. package/dist/meta.js +486 -9
  43. package/dist/meta.js.map +1 -1
  44. package/dist/shells/index.cjs +4 -1
  45. package/dist/shells/index.d.cts +2 -2
  46. package/dist/shells/index.d.ts +2 -2
  47. package/dist/shells/index.js +2 -2
  48. package/dist/{highlight-CGcrYGMC.cjs → text-truncate-BZdL1k5Z.cjs} +608 -83
  49. package/dist/{highlight-C2sfnJN8.js → text-truncate-C17HHCR-.js} +534 -87
  50. package/dist/text-truncate-C17HHCR-.js.map +1 -0
  51. package/dist/{field-token-CSOrU5gl.d.ts → text-truncate-DW78lg4S.d.cts} +63 -4
  52. package/dist/text-truncate-DW78lg4S.d.cts.map +1 -0
  53. package/dist/{field-token-CSOrU5gl.d.cts → text-truncate-DW78lg4S.d.ts} +63 -4
  54. package/dist/text-truncate-DW78lg4S.d.ts.map +1 -0
  55. package/package.json +2 -2
  56. package/dist/field-sidebar-B5vk3luQ.d.cts.map +0 -1
  57. package/dist/field-sidebar-B7QkWvMG.js.map +0 -1
  58. package/dist/field-sidebar-Dk9gJGA_.d.ts.map +0 -1
  59. package/dist/field-token-CSOrU5gl.d.cts.map +0 -1
  60. package/dist/field-token-CSOrU5gl.d.ts.map +0 -1
  61. package/dist/highlight-C2sfnJN8.js.map +0 -1
  62. package/dist/key-pad-menu-BFd79JoS.js.map +0 -1
  63. package/dist/key-pad-menu-D8OS_VCT.d.cts.map +0 -1
  64. package/dist/key-pad-menu-EEb6Ho5h.d.ts.map +0 -1
@@ -3,8 +3,8 @@ import { clsx } from "clsx";
3
3
  import { twMerge } from "tailwind-merge";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import * as React from "react";
6
- import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
7
- import { AlertDialogPanel, ComboboxGroup, ComboboxGroupLabel, ComboboxInput, ComboboxItem, ComboboxPopover, ComboboxRoot, Composite, CompositeItem, CompositeProvider, CompositeRow, DialogDescription, DialogDisclosure, DialogDismiss, DialogHeading, DialogPanel, DialogRoot, DisclosureContent, DisclosureRoot, DisclosureTrigger, FormCheckbox, FormControl, FormDescription, FormError, FormField, FormGroup, FormGroupLabel, FormInput, FormLabel, FormProvider, FormPush, FormRadio, FormRadioGroup, FormRemove, FormReset, FormRoot, FormSubmit, FormSwitch, MenuButtonArrow, MenuItem, MenuItemCheckbox, MenuItemRadio, MenuPopover, MenuRoot, MenuSeparator, MenuTrigger, MenubarContainer, MenubarRoot, PopoverClose, PopoverContent, PopoverRoot, PopoverTrigger, SelectItem, SelectLabel, SelectPopover, SelectRoot, SelectTrigger, Tab, TabList, TabPanel, TabsRoot, ToolbarButton, ToolbarContainer, ToolbarRoot, ToolbarSeparator, Tooltip, TooltipAnchor, TooltipProvider, useFieldValue, useFormContext, useFormStore } from "@reopt-ai/opt-ui-primitives";
6
+ import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
7
+ import { AlertDialogPanel, ComboboxGroup, ComboboxGroupLabel, ComboboxInput, ComboboxItem, ComboboxPopover, ComboboxRoot, Composite, CompositeItem, CompositeProvider, CompositeRow, DialogDescription, DialogDisclosure, DialogDismiss, DialogHeading, DialogPanel, DialogRoot, DisclosureContent, DisclosureRoot, DisclosureTrigger, FormCheckbox, FormControl, FormDescription, FormError, FormField, FormGroup, FormGroupLabel, FormInput, FormLabel, FormProvider, FormPush, FormRadio, FormRadioGroup, FormRemove, FormReset, FormRoot, FormSubmit, FormSwitch, MenuButtonArrow, MenuItem, MenuItemCheckbox, MenuItemRadio, MenuPopover, MenuRoot, MenuSeparator, MenuTrigger, MenubarContainer, MenubarRoot, Meter, PopoverClose, PopoverContent, PopoverRoot, PopoverTrigger, SelectItem, SelectLabel, SelectPopover, SelectRoot, SelectTrigger, Tab, TabList, TabPanel, TabsRoot, ToolbarButton, ToolbarContainer, ToolbarRoot, ToolbarSeparator, Tooltip, TooltipAnchor, TooltipProvider, useFieldValue, useFormContext, useFormStore } from "@reopt-ai/opt-ui-primitives";
8
8
  import { AtSign, Bell, Binary, Braces, Brackets, Calendar, CalendarClock, CalendarDays, ChartColumn, Check, ChevronLeft, ChevronRight, CircleAlert, CircleCheck, CircleDollarSign, CircleHelp, CreditCard, Download, Eye, FileText, Fingerprint, Flag, Folder, Globe, Hash, Heart, Info, Layers, Link2, List, LogIn, Mail, MapPin, MessageCircle, MousePointerClick, Network, Percent, Search, Settings, Share2, ShoppingCart, Sigma, Star, Timer, ToggleLeft, TrendingUp, TriangleAlert, Type, Upload, User, UserPlus, XCircle, Zap } from "lucide-react";
9
9
  //#region src/lib/cn.ts
10
10
  /** Handles cn. */
@@ -120,6 +120,7 @@ const OPT_HOVER_ITEM = "[@media(any-hover:hover)]:hover:bg-accent-subtle active:
120
120
  const OPT_BORDER = "border border-border";
121
121
  const OPT_DIVIDER = "divide-border-subtle";
122
122
  const OPT_SURFACE = "rounded-lg bg-surface shadow-sm";
123
+ const OPT_SURFACE_RAISED = "rounded-xl bg-surface-raised shadow-md";
123
124
  const OPT_SURFACE_POPOVER = "z-50 rounded-lg border border-border bg-surface-overlay p-2 shadow-lg";
124
125
  const OPT_TEXT_PRIMARY = "text-text-primary";
125
126
  const OPT_TEXT_SECONDARY = "text-text-secondary";
@@ -218,44 +219,230 @@ function useControlledState(defaultValue, controlledValue, onChange) {
218
219
  }, [isControlled, onChange])];
219
220
  }
220
221
  //#endregion
222
+ //#region src/core/skeleton.tsx
223
+ const _excluded$17 = [
224
+ "variant",
225
+ "width",
226
+ "height",
227
+ "lines",
228
+ "animated",
229
+ "className",
230
+ "style"
231
+ ];
232
+ const _excluded2$9 = ["lines", "className"];
233
+ const _excluded3$8 = ["size", "className"];
234
+ const _excluded4$6 = ["className"];
235
+ const variantStyles$4 = {
236
+ text: "rounded h-4",
237
+ circular: "rounded-full",
238
+ rectangular: "rounded-lg"
239
+ };
240
+ /** Renders the skeleton component. */
241
+ function Skeleton(_ref) {
242
+ let { variant = "text", width, height, lines = 1, animated = true, className, style } = _ref, props = _objectWithoutProperties(_ref, _excluded$17);
243
+ const baseClass = cn("bg-bg-subtle", animated && "animate-pulse", variantStyles$4[variant], className);
244
+ const computedStyle = _objectSpread2({
245
+ width: typeof width === "number" ? `${width}px` : width,
246
+ height: typeof height === "number" ? `${height}px` : height
247
+ }, style);
248
+ if (variant === "text" && lines > 1) return /* @__PURE__ */ jsxs("div", {
249
+ className: "gap-element flex flex-col",
250
+ role: "status",
251
+ "aria-label": "Loading",
252
+ "data-opt-id": "EJT87",
253
+ "data-opt-slug": "skeleton.Skeleton",
254
+ children: [Array.from({ length: lines }).map((_, i) => /* @__PURE__ */ jsx("div", _objectSpread2({
255
+ className: cn(baseClass, i === lines - 1 && "w-4/5"),
256
+ style: i === lines - 1 ? void 0 : computedStyle
257
+ }, props), i)), /* @__PURE__ */ jsx("span", {
258
+ className: "sr-only",
259
+ children: "Loading..."
260
+ })]
261
+ });
262
+ return /* @__PURE__ */ jsx("div", _objectSpread2(_objectSpread2({
263
+ className: baseClass,
264
+ style: computedStyle,
265
+ role: "status",
266
+ "aria-label": "Loading"
267
+ }, props), {}, {
268
+ "data-opt-id": "EJT87",
269
+ "data-opt-slug": "skeleton.Skeleton",
270
+ children: /* @__PURE__ */ jsx("span", {
271
+ className: "sr-only",
272
+ children: "Loading..."
273
+ })
274
+ }));
275
+ }
276
+ /** Renders the skeleton text component. */
277
+ function SkeletonText(_ref2) {
278
+ let { lines = 3, className } = _ref2, props = _objectWithoutProperties(_ref2, _excluded2$9);
279
+ return /* @__PURE__ */ jsx(Skeleton, _objectSpread2(_objectSpread2({
280
+ variant: "text",
281
+ lines,
282
+ className
283
+ }, props), {}, {
284
+ "data-opt-id": "MX3D5",
285
+ "data-opt-slug": "skeleton.SkeletonText"
286
+ }));
287
+ }
288
+ /** Renders the skeleton avatar component. */
289
+ function SkeletonAvatar(_ref3) {
290
+ let { size = 40, className } = _ref3, props = _objectWithoutProperties(_ref3, _excluded3$8);
291
+ return /* @__PURE__ */ jsx(Skeleton, _objectSpread2(_objectSpread2({
292
+ variant: "circular",
293
+ width: size,
294
+ height: size,
295
+ className
296
+ }, props), {}, {
297
+ "data-opt-id": "MP0HZ",
298
+ "data-opt-slug": "skeleton.SkeletonAvatar"
299
+ }));
300
+ }
301
+ /** Renders the skeleton card component. */
302
+ function SkeletonCard(_ref4) {
303
+ let { className } = _ref4, props = _objectWithoutProperties(_ref4, _excluded4$6);
304
+ return /* @__PURE__ */ jsxs("div", _objectSpread2(_objectSpread2({ className: cn("gap-group flex flex-col", className) }, props), {}, {
305
+ "data-opt-id": "34V5D",
306
+ "data-opt-slug": "skeleton.SkeletonCard",
307
+ children: [
308
+ /* @__PURE__ */ jsx(Skeleton, {
309
+ variant: "rectangular",
310
+ height: 120
311
+ }),
312
+ /* @__PURE__ */ jsx(Skeleton, {
313
+ variant: "text",
314
+ width: "60%"
315
+ }),
316
+ /* @__PURE__ */ jsx(Skeleton, {
317
+ variant: "text",
318
+ lines: 2
319
+ })
320
+ ]
321
+ }));
322
+ }
323
+ /** Renders the skeleton table component. */
324
+ function SkeletonTable({ rows = 5, columns = 4, className }) {
325
+ return /* @__PURE__ */ jsxs("div", {
326
+ className: cn("gap-element flex flex-col", className),
327
+ role: "status",
328
+ "aria-label": "Loading table",
329
+ "data-opt-id": "3KEDN",
330
+ "data-opt-slug": "skeleton.SkeletonTable",
331
+ children: [
332
+ /* @__PURE__ */ jsx("div", {
333
+ className: "border-border-subtle gap-group flex border-b pb-2",
334
+ children: Array.from({ length: columns }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, {
335
+ variant: "text",
336
+ className: "flex-1",
337
+ height: 16
338
+ }, i))
339
+ }),
340
+ Array.from({ length: rows }).map((_, rowIndex) => /* @__PURE__ */ jsx("div", {
341
+ className: "gap-group flex py-2",
342
+ children: Array.from({ length: columns }).map((_, colIndex) => /* @__PURE__ */ jsx(Skeleton, {
343
+ variant: "text",
344
+ className: "flex-1",
345
+ height: 14
346
+ }, colIndex))
347
+ }, rowIndex)),
348
+ /* @__PURE__ */ jsx("span", {
349
+ className: "sr-only",
350
+ children: "Loading..."
351
+ })
352
+ ]
353
+ });
354
+ }
355
+ //#endregion
221
356
  //#region src/core/stat-card.tsx
357
+ const TREND_GLYPH = {
358
+ up: "↑",
359
+ down: "↓",
360
+ neutral: "→"
361
+ };
362
+ /**
363
+ * Colour follows meaning, not direction. A rising bounce rate is a fall in
364
+ * quality, so a card declaring `polarity: "negative"` inverts the palette
365
+ * rather than congratulating the reader on a worse number.
366
+ */
367
+ function deltaToneClass(trend, polarity) {
368
+ if (trend === "neutral") return OPT_TEXT_TERTIARY;
369
+ return (polarity === "positive" ? trend === "up" : trend === "down") ? "text-success-fg" : "text-danger-fg";
370
+ }
371
+ function sparkStroke(trend, polarity) {
372
+ if (!trend || trend === "neutral") return "var(--opt-chart-1)";
373
+ return (polarity === "positive" ? trend === "up" : trend === "down") ? "var(--opt-chart-diverging-positive)" : "var(--opt-chart-diverging-negative)";
374
+ }
222
375
  /** Renders the stat card component. */
223
- function StatCard({ title, value, change, trend, sparklineData, className }) {
224
- const trendColor = trend === "up" ? "text-success-fg" : trend === "down" ? "text-danger-fg" : "text-text-tertiary";
225
- const trendIcon = trend === "up" ? "↑" : trend === "down" ? "↓" : "→";
226
- const sparkColor = trend === "up" ? "var(--opt-chart-diverging-positive)" : trend === "down" ? "var(--opt-chart-diverging-negative)" : "var(--opt-chart-1)";
227
- return /* @__PURE__ */ jsx("div", {
228
- className: cn(OPT_SURFACE, OPT_BORDER, "flex flex-col gap-1 p-5", className),
229
- "data-opt-id": "GAX47",
230
- "data-opt-slug": "stat-card.StatCard",
231
- children: /* @__PURE__ */ jsxs("div", {
232
- className: "flex items-start justify-between gap-4",
233
- children: [/* @__PURE__ */ jsxs("div", {
234
- className: "flex min-w-0 flex-col gap-1",
235
- children: [
236
- /* @__PURE__ */ jsx("span", {
237
- className: cn(OPT_TEXT_SECONDARY, "truncate text-sm"),
238
- children: title
239
- }),
240
- /* @__PURE__ */ jsx("span", {
241
- className: cn(OPT_TEXT_PRIMARY, "text-2xl font-bold"),
242
- children: value
243
- }),
244
- /* @__PURE__ */ jsxs("span", {
245
- className: cn("text-sm font-medium", trendColor),
246
- children: [
247
- trendIcon,
248
- " ",
249
- change
250
- ]
251
- })
252
- ]
253
- }), sparklineData && sparklineData.length > 1 && /* @__PURE__ */ jsx(MiniSparkline, {
254
- data: sparklineData,
255
- color: sparkColor
256
- })]
257
- })
376
+ function StatCard({ title, value, change, trend, polarity = "positive", description, hint, sparklineData, size = "md", loading = false, href, onSelect, selected = false, className }) {
377
+ const titleId = React.useId();
378
+ const hintId = React.useId();
379
+ const hasDelta = Boolean(change) && trend !== void 0;
380
+ const interactive = Boolean(href || onSelect);
381
+ const body = loading ? /* @__PURE__ */ jsxs("div", {
382
+ className: "flex flex-col gap-2",
383
+ children: [
384
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" }),
385
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-32" }),
386
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-20" })
387
+ ]
388
+ }) : /* @__PURE__ */ jsxs("div", {
389
+ className: "flex items-start justify-between gap-4",
390
+ children: [/* @__PURE__ */ jsxs("div", {
391
+ className: "flex min-w-0 flex-col gap-1",
392
+ children: [
393
+ /* @__PURE__ */ jsx("span", {
394
+ id: titleId,
395
+ className: cn(OPT_TEXT_SECONDARY, "truncate text-sm"),
396
+ children: title
397
+ }),
398
+ /* @__PURE__ */ jsx("span", {
399
+ className: cn(OPT_TEXT_PRIMARY, "font-bold tabular-nums", size === "sm" ? "text-xl" : "text-2xl"),
400
+ children: value
401
+ }),
402
+ hasDelta && /* @__PURE__ */ jsxs("span", {
403
+ className: cn("text-sm font-medium tabular-nums", deltaToneClass(trend, polarity)),
404
+ children: [
405
+ TREND_GLYPH[trend],
406
+ " ",
407
+ change
408
+ ]
409
+ }),
410
+ description && /* @__PURE__ */ jsx("span", {
411
+ className: cn("text-text-tertiary", "text-xs"),
412
+ children: description
413
+ }),
414
+ hint && /* @__PURE__ */ jsx("span", {
415
+ id: hintId,
416
+ className: "sr-only",
417
+ children: hint
418
+ })
419
+ ]
420
+ }), sparklineData && sparklineData.length > 1 && /* @__PURE__ */ jsx(MiniSparkline, {
421
+ data: sparklineData,
422
+ color: sparkStroke(trend, polarity)
423
+ })]
258
424
  });
425
+ const shared = {
426
+ className: cn(OPT_SURFACE, OPT_BORDER, "flex flex-col gap-1", size === "sm" ? "p-4" : "p-5", interactive && [
427
+ "hover:border-border-hover",
428
+ "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset",
429
+ "cursor-pointer text-left transition-colors"
430
+ ], selected && "border-accent", className),
431
+ title: hint,
432
+ "aria-describedby": hint ? hintId : void 0,
433
+ "data-opt-id": "GAX47",
434
+ "data-opt-slug": "stat-card.StatCard"
435
+ };
436
+ if (href) return /* @__PURE__ */ jsx("a", _objectSpread2(_objectSpread2({
437
+ href,
438
+ "aria-current": selected ? "true" : void 0
439
+ }, shared), {}, { children: body }));
440
+ if (onSelect) return /* @__PURE__ */ jsx("button", _objectSpread2(_objectSpread2({
441
+ type: "button",
442
+ onClick: onSelect,
443
+ "aria-pressed": selected
444
+ }, shared), {}, { children: body }));
445
+ return /* @__PURE__ */ jsx("div", _objectSpread2(_objectSpread2({}, shared), {}, { children: body }));
259
446
  }
260
447
  function MiniSparkline({ data, color }) {
261
448
  const width = 80;
@@ -307,7 +494,7 @@ const koBreadcrumbLabels = {
307
494
  navAriaLabel: "{label}(으)로 이동" };
308
495
  //#endregion
309
496
  //#region src/core/button.tsx
310
- const _excluded$15 = [
497
+ const _excluded$16 = [
311
498
  "variant",
312
499
  "size",
313
500
  "loading",
@@ -318,24 +505,24 @@ const _excluded$15 = [
318
505
  "children",
319
506
  "aria-busy"
320
507
  ];
321
- const variantStyles$2 = {
508
+ const variantStyles$3 = {
322
509
  primary: "bg-accent text-accent-fg hover:bg-accent-hover active:bg-accent-active focus-visible:ring-offset-2",
323
510
  secondary: "border border-border bg-surface text-text-primary hover:bg-bg-subtle active:bg-bg-muted focus-visible:ring-offset-1",
324
511
  ghost: "text-text-secondary hover:bg-bg-subtle active:bg-bg-muted focus-visible:ring-offset-1",
325
512
  danger: "bg-danger text-danger-fg hover:bg-danger active:bg-danger focus-visible:ring-offset-2"
326
513
  };
327
- const sizeStyles$4 = {
514
+ const sizeStyles$5 = {
328
515
  sm: "h-[var(--opt-control-sm)] px-[var(--opt-control-padding-sm)] text-xs gap-[var(--opt-space-1-5)]",
329
516
  md: "h-[var(--opt-control-md)] px-[var(--opt-control-padding-md)] text-sm gap-[var(--opt-space-2)]",
330
517
  lg: "h-[var(--opt-control-lg)] px-[var(--opt-control-padding-lg)] text-base gap-[var(--opt-space-2)]"
331
518
  };
332
519
  /** Renders the button component. */
333
520
  function Button(_ref) {
334
- let { variant = "primary", size = "md", loading = false, leftIcon, rightIcon, className, disabled, children, "aria-busy": ariaBusy } = _ref, props = _objectWithoutProperties(_ref, _excluded$15);
521
+ let { variant = "primary", size = "md", loading = false, leftIcon, rightIcon, className, disabled, children, "aria-busy": ariaBusy } = _ref, props = _objectWithoutProperties(_ref, _excluded$16);
335
522
  const isDisabled = disabled || loading;
336
523
  return /* @__PURE__ */ jsxs("button", _objectSpread2(_objectSpread2({
337
524
  type: "button",
338
- className: cn("inline-flex cursor-pointer items-center justify-center rounded-lg font-medium transition-colors", OPT_FOCUS_VISIBLE, variantStyles$2[variant], sizeStyles$4[size], isDisabled && "pointer-events-none cursor-not-allowed opacity-50", className),
525
+ className: cn("inline-flex cursor-pointer items-center justify-center rounded-lg font-medium transition-colors", OPT_FOCUS_VISIBLE, variantStyles$3[variant], sizeStyles$5[size], isDisabled && "pointer-events-none cursor-not-allowed opacity-50", className),
339
526
  disabled: isDisabled
340
527
  }, props), {}, {
341
528
  "aria-busy": loading ? true : ariaBusy,
@@ -377,44 +564,68 @@ const iconSizeClasses = {
377
564
  md: "text-4xl",
378
565
  lg: "text-5xl"
379
566
  };
567
+ const titleSizeClasses = {
568
+ sm: "text-base font-semibold",
569
+ md: "text-lg font-semibold",
570
+ lg: "text-xl font-semibold"
571
+ };
572
+ const iconToneClasses = {
573
+ default: OPT_TEXT_TERTIARY,
574
+ subdued: OPT_TEXT_TERTIARY,
575
+ error: "text-danger-fg"
576
+ };
577
+ function isActionList(actions) {
578
+ return Array.isArray(actions) && actions.every((entry) => typeof entry === "object" && entry !== null && "onClick" in entry);
579
+ }
380
580
  /** Renders the empty state component. */
381
- function EmptyState({ icon, title, description, actions, className, iconSize = "md", headingLevel = "h3" }) {
581
+ function EmptyState({ icon, title, description, body, footer, actions, className, iconSize = "md", headingLevel = "h3", titleSize = "md", layout = "vertical", hasBorder = false, tone = "default" }) {
382
582
  const HeadingTag = headingLevel;
583
+ const horizontal = layout === "horizontal";
584
+ const hasActions = isActionList(actions) ? actions.length > 0 : Boolean(actions);
383
585
  return /* @__PURE__ */ jsxs("div", {
384
- role: "status",
586
+ role: tone === "error" ? "alert" : "status",
385
587
  "aria-label": title,
386
- className: cn("flex flex-col items-center justify-center px-4 py-12 text-center", className),
387
- "data-opt-id": "5ZK92",
588
+ className: cn("flex px-4 py-12", horizontal ? "flex-row items-center justify-center gap-8 text-left" : "flex-col items-center justify-center text-center", hasBorder && ["border border-border", "rounded-xl"], tone === "subdued" && "bg-bg-subtle", className),
589
+ "data-opt-id": "MZV19",
388
590
  "data-opt-slug": "empty-state.EmptyState",
389
- children: [
390
- icon && /* @__PURE__ */ jsx("div", {
391
- className: cn(`mb-4 text-text-tertiary`, iconSizeClasses[iconSize]),
392
- "aria-hidden": "true",
393
- children: icon
394
- }),
395
- /* @__PURE__ */ jsx(HeadingTag, {
396
- className: `text-lg font-semibold ${OPT_TEXT_PRIMARY}`,
397
- children: title
398
- }),
399
- description && /* @__PURE__ */ jsx("p", {
400
- className: `mt-2 max-w-sm text-sm text-text-secondary`,
401
- children: description
402
- }),
403
- actions && actions.length > 0 && /* @__PURE__ */ jsx("div", {
404
- className: "mt-6 flex items-center gap-3",
405
- children: actions.map((action) => /* @__PURE__ */ jsx(Button, {
406
- onClick: action.onClick,
407
- disabled: action.disabled,
408
- variant: action.variant === "secondary" ? "secondary" : "primary",
409
- children: action.label
410
- }, action.id))
411
- })
412
- ]
591
+ children: [icon && /* @__PURE__ */ jsx("div", {
592
+ className: cn(iconToneClasses[tone], iconSizeClasses[iconSize], horizontal ? "shrink-0" : "mb-4"),
593
+ "aria-hidden": "true",
594
+ children: icon
595
+ }), /* @__PURE__ */ jsxs("div", {
596
+ className: cn("flex flex-col", horizontal ? "items-start" : "items-center"),
597
+ children: [
598
+ /* @__PURE__ */ jsx(HeadingTag, {
599
+ className: cn(titleSizeClasses[titleSize], OPT_TEXT_PRIMARY),
600
+ children: title
601
+ }),
602
+ body ? /* @__PURE__ */ jsx("div", {
603
+ className: cn("mt-2 max-w-md text-sm", OPT_TEXT_SECONDARY),
604
+ children: body
605
+ }) : description && /* @__PURE__ */ jsx("p", {
606
+ className: cn("mt-2 max-w-sm text-sm", "text-text-secondary"),
607
+ children: description
608
+ }),
609
+ hasActions && /* @__PURE__ */ jsx("div", {
610
+ className: cn("mt-6 flex flex-wrap items-center gap-3", !horizontal && "justify-center"),
611
+ children: isActionList(actions) ? actions.map((action) => /* @__PURE__ */ jsx(Button, {
612
+ onClick: action.onClick,
613
+ disabled: action.disabled,
614
+ variant: action.variant === "secondary" ? "secondary" : "primary",
615
+ children: action.label
616
+ }, action.id)) : actions
617
+ }),
618
+ footer && /* @__PURE__ */ jsx("div", {
619
+ className: cn("border-border-subtle mt-6 w-full border-t pt-4 text-sm", "text-text-tertiary"),
620
+ children: footer
621
+ })
622
+ ]
623
+ })]
413
624
  });
414
625
  }
415
626
  //#endregion
416
627
  //#region src/core/badge.tsx
417
- const _excluded$14 = [
628
+ const _excluded$15 = [
418
629
  "variant",
419
630
  "size",
420
631
  "dot",
@@ -423,7 +634,7 @@ const _excluded$14 = [
423
634
  "className",
424
635
  "children"
425
636
  ];
426
- const variantStyles$1 = {
637
+ const variantStyles$2 = {
427
638
  default: "bg-bg-subtle text-text-secondary",
428
639
  success: "bg-success-subtle text-success-fg",
429
640
  warning: "bg-warning-subtle text-warning-fg",
@@ -444,19 +655,19 @@ const progressStroke = {
444
655
  error: "stroke-danger",
445
656
  info: "stroke-info"
446
657
  };
447
- const sizeStyles$3 = {
658
+ const sizeStyles$4 = {
448
659
  sm: "px-1.5 py-0.5 text-[10px]",
449
660
  md: "px-2 py-0.5 text-[11px]"
450
661
  };
451
662
  /** Renders the badge component. */
452
663
  function Badge(_ref) {
453
- let { variant = "default", size = "md", dot = false, progress, progressSize = 16, className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$14);
664
+ let { variant = "default", size = "md", dot = false, progress, progressSize = 16, className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$15);
454
665
  const hasProgress = progress != null;
455
666
  const clampedProgress = hasProgress ? Math.min(100, Math.max(0, progress)) : 0;
456
667
  const radius = (progressSize - 3) / 2;
457
668
  const circumference = 2 * Math.PI * radius;
458
669
  const offset = circumference - clampedProgress / 100 * circumference;
459
- return /* @__PURE__ */ jsxs("span", _objectSpread2(_objectSpread2({ className: cn(OPT_BADGE, variantStyles$1[variant], sizeStyles$3[size], className) }, props), {}, {
670
+ return /* @__PURE__ */ jsxs("span", _objectSpread2(_objectSpread2({ className: cn(OPT_BADGE, variantStyles$2[variant], sizeStyles$4[size], className) }, props), {}, {
460
671
  "data-opt-id": "9GBGZ",
461
672
  "data-opt-slug": "badge.Badge",
462
673
  children: [
@@ -496,24 +707,24 @@ function Badge(_ref) {
496
707
  }
497
708
  //#endregion
498
709
  //#region src/core/spinner.tsx
499
- const _excluded$13 = [
710
+ const _excluded$14 = [
500
711
  "size",
501
712
  "className",
502
713
  "role",
503
714
  "aria-hidden",
504
715
  "aria-label"
505
716
  ];
506
- const sizeStyles$2 = {
717
+ const sizeStyles$3 = {
507
718
  sm: "h-4 w-4",
508
719
  md: "h-6 w-6",
509
720
  lg: "h-8 w-8"
510
721
  };
511
722
  /** Renders the spinner component. */
512
723
  function Spinner(_ref) {
513
- let { size = "md", className, role, "aria-hidden": ariaHidden, "aria-label": ariaLabel } = _ref, props = _objectWithoutProperties(_ref, _excluded$13);
724
+ let { size = "md", className, role, "aria-hidden": ariaHidden, "aria-label": ariaLabel } = _ref, props = _objectWithoutProperties(_ref, _excluded$14);
514
725
  const isDecorative = ariaHidden === true || ariaHidden === "true";
515
726
  return /* @__PURE__ */ jsxs("svg", _objectSpread2(_objectSpread2({
516
- className: cn("animate-spin text-current", sizeStyles$2[size], className),
727
+ className: cn("animate-spin text-current", sizeStyles$3[size], className),
517
728
  viewBox: "0 0 24 24",
518
729
  fill: "none",
519
730
  role: isDecorative ? void 0 : role !== null && role !== void 0 ? role : "status",
@@ -538,7 +749,7 @@ function Spinner(_ref) {
538
749
  }
539
750
  //#endregion
540
751
  //#region src/core/alert.tsx
541
- const _excluded$12 = [
752
+ const _excluded$13 = [
542
753
  "variant",
543
754
  "title",
544
755
  "icon",
@@ -547,7 +758,7 @@ const _excluded$12 = [
547
758
  "className",
548
759
  "children"
549
760
  ];
550
- const variantStyles = {
761
+ const variantStyles$1 = {
551
762
  info: "border-info bg-info-subtle text-info-fg",
552
763
  success: "border-success bg-success-subtle text-success-fg",
553
764
  warning: "border-warning bg-warning-subtle text-warning-fg",
@@ -607,11 +818,11 @@ const defaultIcons = {
607
818
  };
608
819
  /** Renders the alert component. */
609
820
  function Alert(_ref) {
610
- let { variant = "info", title, icon, dismissible = false, onDismiss, className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$12);
821
+ let { variant = "info", title, icon, dismissible = false, onDismiss, className, children } = _ref, props = _objectWithoutProperties(_ref, _excluded$13);
611
822
  const displayIcon = icon !== null && icon !== void 0 ? icon : defaultIcons[variant];
612
823
  return /* @__PURE__ */ jsx("div", _objectSpread2(_objectSpread2({
613
824
  role: "alert",
614
- className: cn("relative rounded-lg border p-4", variantStyles[variant], className)
825
+ className: cn("relative rounded-lg border p-4", variantStyles$1[variant], className)
615
826
  }, props), {}, {
616
827
  "data-opt-id": "4SPK6",
617
828
  "data-opt-slug": "alert.Alert",
@@ -663,7 +874,7 @@ function useInputId(id, label, prefix) {
663
874
  }
664
875
  //#endregion
665
876
  //#region src/core/input.tsx
666
- const _excluded$11 = [
877
+ const _excluded$12 = [
667
878
  "label",
668
879
  "error",
669
880
  "hint",
@@ -678,7 +889,7 @@ const _excluded$11 = [
678
889
  const inputSizeStyles = OPT_INPUT_SIZE;
679
890
  const Input = forwardRef((_ref, ref) => {
680
891
  var _useInputId;
681
- let { label, error, hint, leftIcon, rightIcon, fullWidth = false, size = "md", required, className, id } = _ref, props = _objectWithoutProperties(_ref, _excluded$11);
892
+ let { label, error, hint, leftIcon, rightIcon, fullWidth = false, size = "md", required, className, id } = _ref, props = _objectWithoutProperties(_ref, _excluded$12);
682
893
  const fallbackId = useId().replace(/:/g, "");
683
894
  const inputId = (_useInputId = useInputId(id, label, "input")) !== null && _useInputId !== void 0 ? _useInputId : `input-${fallbackId}`;
684
895
  const hasError = Boolean(error);
@@ -828,6 +1039,65 @@ function StarRating({ value = 0, onChange, max = 5, readOnly = false, size = "md
828
1039
  });
829
1040
  }
830
1041
  //#endregion
1042
+ //#region src/core/meter.tsx
1043
+ const _excluded$11 = [
1044
+ "value",
1045
+ "min",
1046
+ "max",
1047
+ "size",
1048
+ "variant",
1049
+ "showValue",
1050
+ "label",
1051
+ "getValueLabel",
1052
+ "className"
1053
+ ];
1054
+ const sizeStyles$2 = {
1055
+ sm: "h-1",
1056
+ md: "h-2",
1057
+ lg: "h-3"
1058
+ };
1059
+ const variantStyles = {
1060
+ default: "bg-accent",
1061
+ success: "bg-success",
1062
+ warning: "bg-warning",
1063
+ error: "bg-danger"
1064
+ };
1065
+ /**
1066
+ * A scalar measurement within a known range (disk usage, score, capacity) —
1067
+ * `role="meter"`, distinct from Progress (task completion). Built on the
1068
+ * opt-ui-primitives Meter primitive, which owns the role/aria/valuetext.
1069
+ */
1070
+ function Meter$1(_ref) {
1071
+ let { value, min = 0, max = 100, size = "md", variant = "default", showValue = false, label, getValueLabel, className } = _ref, props = _objectWithoutProperties(_ref, _excluded$11);
1072
+ const clamped = Math.min(Math.max(value, min), max);
1073
+ const percentage = max === min ? 0 : (clamped - min) / (max - min) * 100;
1074
+ return /* @__PURE__ */ jsxs("div", _objectSpread2(_objectSpread2({ className: cn("w-full", className) }, props), {}, {
1075
+ "data-opt-id": "G01TE",
1076
+ "data-opt-slug": "meter.Meter",
1077
+ children: [(showValue || label) && /* @__PURE__ */ jsxs("div", {
1078
+ className: "mb-1.5 flex items-center justify-between text-sm",
1079
+ children: [label && /* @__PURE__ */ jsx("span", {
1080
+ className: "text-text-primary font-medium",
1081
+ children: label
1082
+ }), showValue && /* @__PURE__ */ jsxs("span", {
1083
+ className: "text-text-tertiary",
1084
+ children: [Math.round(percentage), "%"]
1085
+ })]
1086
+ }), /* @__PURE__ */ jsx(Meter, {
1087
+ value: clamped,
1088
+ min,
1089
+ max,
1090
+ getValueLabel,
1091
+ "aria-label": label !== null && label !== void 0 ? label : `${Math.round(percentage)}%`,
1092
+ className: cn("bg-bg-subtle w-full overflow-hidden rounded-full", sizeStyles$2[size]),
1093
+ children: /* @__PURE__ */ jsx("div", {
1094
+ className: cn("h-full rounded-full transition-all duration-300 ease-out", variantStyles[variant]),
1095
+ style: { width: `${percentage}%` }
1096
+ })
1097
+ })]
1098
+ }));
1099
+ }
1100
+ //#endregion
831
1101
  //#region src/core/composite-zone.tsx
832
1102
  /** Renders the composite zone component. */
833
1103
  function CompositeZone({ children, label, focusLoop = true, focusWrap = true, className }) {
@@ -3510,6 +3780,55 @@ function FormDateRangePicker(_ref35) {
3510
3780
  }));
3511
3781
  }
3512
3782
  //#endregion
3783
+ //#region src/lib/formatters.ts
3784
+ const OPT_UI_DEFAULT_LOCALE = "ko-KR";
3785
+ const KST_OFFSET_MS = 324e5;
3786
+ function getKstDateParts(input) {
3787
+ const date = input instanceof Date ? input : new Date(input);
3788
+ if (Number.isNaN(date.getTime())) return null;
3789
+ const kstDate = new Date(date.getTime() + KST_OFFSET_MS);
3790
+ return {
3791
+ year: kstDate.getUTCFullYear(),
3792
+ month: kstDate.getUTCMonth() + 1,
3793
+ day: kstDate.getUTCDate(),
3794
+ hour: kstDate.getUTCHours(),
3795
+ minute: kstDate.getUTCMinutes(),
3796
+ second: kstDate.getUTCSeconds()
3797
+ };
3798
+ }
3799
+ function pad2(value) {
3800
+ return String(value).padStart(2, "0");
3801
+ }
3802
+ function meridiem(hour) {
3803
+ return hour < 12 ? "오전" : "오후";
3804
+ }
3805
+ function hour12(hour) {
3806
+ const normalized = hour % 12;
3807
+ return normalized === 0 ? 12 : normalized;
3808
+ }
3809
+ /** Formats a date as Korean short date text in the opt-ui default timezone. */
3810
+ function formatOptShortDate(input) {
3811
+ const parts = getKstDateParts(input);
3812
+ if (!parts) return "-";
3813
+ return `${parts.month}월 ${parts.day}일`;
3814
+ }
3815
+ /** Formats a date as Korean clock time in the opt-ui default timezone. */
3816
+ function formatOptTime(input) {
3817
+ const parts = getKstDateParts(input);
3818
+ if (!parts) return "-";
3819
+ return `${meridiem(parts.hour)} ${pad2(hour12(parts.hour))}:${pad2(parts.minute)}`;
3820
+ }
3821
+ /** Formats a date as Korean date-time text in the opt-ui default timezone. */
3822
+ function formatOptDateTime(input) {
3823
+ const parts = getKstDateParts(input);
3824
+ if (!parts) return "-";
3825
+ return `${parts.year}. ${parts.month}. ${parts.day}. ${meridiem(parts.hour)} ${hour12(parts.hour)}:${pad2(parts.minute)}:${pad2(parts.second)}`;
3826
+ }
3827
+ /** Formats numbers without depending on the runtime default locale. */
3828
+ function formatOptNumber(value) {
3829
+ return new Intl.NumberFormat(OPT_UI_DEFAULT_LOCALE).format(value);
3830
+ }
3831
+ //#endregion
3513
3832
  //#region src/core/checkbox.tsx
3514
3833
  const _excluded$1 = [
3515
3834
  "label",
@@ -4097,6 +4416,134 @@ function Highlight({ text, search, caseSensitive = false, firstOnly = false, cla
4097
4416
  });
4098
4417
  }
4099
4418
  //#endregion
4100
- export { ToolbarContainer$1 as $, Spinner as $t, FormSelectPopover as A, OPT_INPUT as An, DialogPanel$1 as At, DEFAULT_PRESETS as B, OPT_TEXT_PRIMARY as Bn, TabsRoot$1 as Bt, FormRadio$1 as C, OPT_DOT_GRAY as Cn, DialogBody as Ct, FormRoot$1 as D, OPT_FOCUS as Dn, DialogFooter as Dt, FormReset$1 as E, OPT_DOT_YELLOW as En, DialogDismiss$1 as Et, FormTagInput as F, OPT_TABLE_ROW_ACTIVE as Fn, SelectRoot$1 as Ft, ColorPicker as G, _objectWithoutProperties as Gn, CompositeItem as Gt, TagInput as H, OPT_TEXT_TERTIARY as Hn, DisclosureRoot$1 as Ht, FormTextarea as I, OPT_TD as In, SelectTrigger$1 as It, PopoverRoot$1 as J, CompositeZone as Jt, PopoverClose$1 as K, Kbd as Kn, CompositeProvider as Kt, useFieldValue as L, OPT_TD_MUTED as Ln, Tab$1 as Lt, FormStarRating as M, OPT_ITEM_BASE as Mn, SelectItem$1 as Mt, FormSubmit$1 as N, OPT_SURFACE as Nn, SelectLabel$1 as Nt, FormSelect as O, OPT_FOCUS_VISIBLE as On, DialogHeader as Ot, FormSwitch$1 as P, OPT_SURFACE_POPOVER as Pn, SelectPopover$1 as Pt, ToolbarButton$1 as Q, Alert as Qt, useFormContext as R, OPT_TEXTAREA_SIZE as Rn, TabList$1 as Rt, FormPush$1 as S, OPT_DIVIDER as Sn, AlertDialogPanel$1 as St, FormRemove$1 as T, OPT_DOT_RED as Tn, DialogDisclosure$1 as Tt, IconPicker as U, OPT_TH as Un, DisclosureTrigger$1 as Ut, DateRangePicker as V, OPT_TEXT_SECONDARY as Vn, DisclosureContent$1 as Vt, getIconRegistryEntry as W, _objectSpread2 as Wn, Composite as Wt, Slider as X, Input as Xt, PopoverTrigger$1 as Y, StarRating as Yt, NumberInput as Z, useInputId as Zt, FormIconPicker as _, OPT_BADGE_RED as _n, ComboboxGroupLabel$1 as _t, Pagination as a, koStarRatingLabels as an, MenuButtonArrow$1 as at, FormNumberInput as b, OPT_DISCLOSURE_CONTENT as bn, ComboboxPopover$1 as bt, FormCheckbox$1 as c, FieldMessages as cn, MenuItemRadio$1 as ct, FormDateRangePicker as d, OPT_ACTIVE_ITEM as dn, MenuSeparator$1 as dt, Badge as en, ToolbarRoot$1 as et, FormDescription$1 as f, OPT_ANIMATE_DRAWER as fn, MenuTrigger$1 as ft, FormGroupLabel$1 as g, OPT_BADGE_NEUTRAL as gn, ComboboxGroup$1 as gt, FormGroup$1 as h, OPT_BADGE_GREEN as hn, ComboboxEmpty as ht, resolveFieldType as i, koComboboxLabels as in, TooltipProvider$1 as it, FormSlider as j, OPT_INPUT_ERROR as jn, DialogRoot$1 as jt, FormSelectItem as k, OPT_HOVER_ITEM as kn, DialogHeading$1 as kt, FormColorPicker as l, RequiredMark as ln, MenuPopover$1 as lt, FormField$1 as m, OPT_BADGE_BLUE as mn, MenubarRoot$1 as mt, FIELD_TYPE_TOKENS as n, Button as nn, Tooltip$1 as nt, Checkbox as o, StatCard as on, MenuItem$1 as ot, FormError$1 as p, OPT_ANIMATE_ENTER_EXIT as pn, MenubarContainer$1 as pt, PopoverContent$1 as q, cn as qn, CompositeRow as qt, FieldToken as r, koBreadcrumbLabels as rn, TooltipAnchor$1 as rt, CheckboxGroup as s, useControlledState as sn, MenuItemCheckbox$1 as st, Highlight as t, EmptyState as tn, ToolbarSeparator$1 as tt, FormControl$1 as u, getDescribedBy as un, MenuRoot$1 as ut, FormInput$1 as v, OPT_BADGE_YELLOW as vn, ComboboxInput$1 as vt, FormRadioGroup$1 as w, OPT_DOT_GREEN as wn, DialogDescription$1 as wt, FormProvider$1 as x, OPT_DISCLOSURE_TRIGGER as xn, ComboboxRoot$1 as xt, FormLabel$1 as y, OPT_BORDER as yn, ComboboxItem$1 as yt, useFormStore as z, OPT_TEXT_LABEL as zn, TabPanel$1 as zt };
4419
+ //#region src/core/text-truncate.tsx
4420
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
4421
+ /** Measures text in the element's own resolved font, not an assumed one. */
4422
+ function createMeasurer(element) {
4423
+ const context = document.createElement("canvas").getContext("2d");
4424
+ if (!context) return null;
4425
+ const computed = window.getComputedStyle(element);
4426
+ context.font = [
4427
+ computed.fontStyle,
4428
+ computed.fontVariant,
4429
+ computed.fontWeight,
4430
+ `${computed.fontSize}/${computed.lineHeight}`,
4431
+ computed.fontFamily
4432
+ ].join(" ");
4433
+ const spacing = Number.parseFloat(computed.letterSpacing) || 0;
4434
+ return (value) => context.measureText(value).width + spacing * value.length;
4435
+ }
4436
+ /** Largest `n` in `[0, max]` for which `fits(n)` holds. */
4437
+ function largestFitting(max, fits) {
4438
+ let low = 0;
4439
+ let high = max;
4440
+ while (low < high) {
4441
+ const mid = Math.ceil((low + high) / 2);
4442
+ if (fits(mid)) low = mid;
4443
+ else high = mid - 1;
4444
+ }
4445
+ return low;
4446
+ }
4447
+ function truncateToWidth(text, side, ellipsis, available, measure) {
4448
+ if (measure(text) <= available) return text;
4449
+ const chars = Array.from(text);
4450
+ if (side === "end") {
4451
+ const keep = largestFitting(chars.length, (n) => measure(chars.slice(0, n).join("") + ellipsis) <= available);
4452
+ return chars.slice(0, keep).join("") + ellipsis;
4453
+ }
4454
+ if (side === "start") {
4455
+ const keep = largestFitting(chars.length, (n) => measure(ellipsis + chars.slice(chars.length - n).join("")) <= available);
4456
+ return ellipsis + chars.slice(chars.length - keep).join("");
4457
+ }
4458
+ if (side === "startEnd") {
4459
+ const keep = largestFitting(Math.floor(chars.length / 2), (n) => {
4460
+ const head = chars.slice(0, n).join("");
4461
+ const tail = chars.slice(chars.length - n).join("");
4462
+ return measure(head + ellipsis + tail) <= available;
4463
+ });
4464
+ if (keep === 0) return ellipsis;
4465
+ return chars.slice(0, keep).join("") + ellipsis + chars.slice(chars.length - keep).join("");
4466
+ }
4467
+ const span = largestFitting(chars.length, (n) => {
4468
+ const start = Math.floor((chars.length - n) / 2);
4469
+ return measure(ellipsis + chars.slice(start, start + n).join("") + ellipsis) <= available;
4470
+ });
4471
+ if (span === 0) return ellipsis;
4472
+ const start = Math.floor((chars.length - span) / 2);
4473
+ return ellipsis + chars.slice(start, start + span).join("") + ellipsis;
4474
+ }
4475
+ /**
4476
+ * TextTruncate — drops characters from the part of a string that identifies it
4477
+ * least.
4478
+ *
4479
+ * CSS can only cut the end. That is the wrong end for the strings analytics
4480
+ * shows most: two URLs that differ in their query string, two ids that share a
4481
+ * prefix, a stack frame whose filename is at the tail. This measures the real
4482
+ * text in the real font against the real available width and cuts where the
4483
+ * caller says.
4484
+ *
4485
+ * The full string stays in the accessibility tree and in `title`, so
4486
+ * truncation never costs the reader information — only space.
4487
+ *
4488
+ * @example
4489
+ * ```tsx
4490
+ * <TextTruncate text={event.url} truncation="startEnd" />
4491
+ * ```
4492
+ */
4493
+ function TextTruncate({ text, truncation = "startEnd", ellipsis = "…", width, children, className }) {
4494
+ const ref = useRef(null);
4495
+ const [available, setAvailable] = useState(width !== null && width !== void 0 ? width : null);
4496
+ useIsomorphicLayoutEffect(() => {
4497
+ if (width !== void 0) {
4498
+ setAvailable(width);
4499
+ return;
4500
+ }
4501
+ const element = ref.current;
4502
+ if (!element || typeof ResizeObserver === "undefined") return;
4503
+ const observer = new ResizeObserver((entries) => {
4504
+ const entry = entries[0];
4505
+ if (entry) setAvailable(entry.contentRect.width);
4506
+ });
4507
+ observer.observe(element);
4508
+ setAvailable(element.getBoundingClientRect().width);
4509
+ return () => observer.disconnect();
4510
+ }, [width]);
4511
+ const [rendered, setRendered] = useState(text);
4512
+ useIsomorphicLayoutEffect(() => {
4513
+ const element = ref.current;
4514
+ if (!element || available === null || available <= 0) {
4515
+ setRendered(text);
4516
+ return;
4517
+ }
4518
+ const measure = createMeasurer(element);
4519
+ if (!measure) {
4520
+ setRendered(text);
4521
+ return;
4522
+ }
4523
+ setRendered(truncateToWidth(text, truncation, ellipsis, available, measure));
4524
+ }, [
4525
+ text,
4526
+ truncation,
4527
+ ellipsis,
4528
+ available
4529
+ ]);
4530
+ const truncated = rendered !== text;
4531
+ return /* @__PURE__ */ jsxs("span", {
4532
+ ref,
4533
+ className: cn("block w-full overflow-hidden whitespace-nowrap", className),
4534
+ title: truncated ? text : void 0,
4535
+ "data-opt-id": "Y6BGX",
4536
+ "data-opt-slug": "text-truncate.TextTruncate",
4537
+ children: [/* @__PURE__ */ jsx("span", {
4538
+ "aria-hidden": truncated ? "true" : void 0,
4539
+ children: children ? children(rendered) : rendered
4540
+ }), truncated && /* @__PURE__ */ jsx("span", {
4541
+ className: "sr-only",
4542
+ children: text
4543
+ })]
4544
+ });
4545
+ }
4546
+ //#endregion
4547
+ export { PopoverContent$1 as $, OPT_TEXT_PRIMARY as $n, CompositeRow as $t, FormRadioGroup$1 as A, OPT_BADGE_YELLOW as An, DialogDescription$1 as At, FormSwitch$1 as B, OPT_FOCUS_VISIBLE as Bn, SelectPopover$1 as Bt, FormIconPicker as C, OPT_ACTIVE_ITEM as Cn, ComboboxGroupLabel$1 as Ct, FormProvider$1 as D, OPT_BADGE_GREEN as Dn, ComboboxRoot$1 as Dt, FormNumberInput as E, OPT_BADGE_BLUE as En, ComboboxPopover$1 as Et, FormSelectItem as F, OPT_DOT_GRAY as Fn, DialogHeading$1 as Ft, useFormStore as G, OPT_SURFACE as Gn, TabPanel$1 as Gt, FormTextarea as H, OPT_INPUT as Hn, SelectTrigger$1 as Ht, FormSelectPopover as I, OPT_DOT_GREEN as In, DialogPanel$1 as It, TagInput as J, OPT_TABLE_ROW_ACTIVE as Jn, DisclosureRoot$1 as Jt, DEFAULT_PRESETS as K, OPT_SURFACE_POPOVER as Kn, TabsRoot$1 as Kt, FormSlider as L, OPT_DOT_RED as Ln, DialogRoot$1 as Lt, FormReset$1 as M, OPT_DISCLOSURE_CONTENT as Mn, DialogDismiss$1 as Mt, FormRoot$1 as N, OPT_DISCLOSURE_TRIGGER as Nn, DialogFooter as Nt, FormPush$1 as O, OPT_BADGE_NEUTRAL as On, AlertDialogPanel$1 as Ot, FormSelect as P, OPT_DIVIDER as Pn, DialogHeader as Pt, PopoverClose$1 as Q, OPT_TEXT_LABEL as Qn, CompositeProvider as Qt, FormStarRating as R, OPT_DOT_YELLOW as Rn, SelectItem$1 as Rt, FormGroupLabel$1 as S, getDescribedBy as Sn, ComboboxGroup$1 as St, FormLabel$1 as T, OPT_ANIMATE_ENTER_EXIT as Tn, ComboboxItem$1 as Tt, useFieldValue as U, OPT_INPUT_ERROR as Un, Tab$1 as Ut, FormTagInput as V, OPT_HOVER_ITEM as Vn, SelectRoot$1 as Vt, useFormContext as W, OPT_ITEM_BASE as Wn, TabList$1 as Wt, getIconRegistryEntry as X, OPT_TD_MUTED as Xn, Composite as Xt, IconPicker as Y, OPT_TD as Yn, DisclosureTrigger$1 as Yt, ColorPicker as Z, OPT_TEXTAREA_SIZE as Zn, CompositeItem as Zt, FormDateRangePicker as _, SkeletonTable as _n, MenuSeparator$1 as _t, resolveFieldType as a, Alert as an, Kbd as ar, ToolbarContainer$1 as at, FormField$1 as b, FieldMessages as bn, MenubarRoot$1 as bt, CheckboxGroup as c, EmptyState as cn, Tooltip$1 as ct, formatOptNumber as d, koComboboxLabels as dn, MenuButtonArrow$1 as dt, CompositeZone as en, OPT_TEXT_SECONDARY as er, PopoverRoot$1 as et, formatOptShortDate as f, koStarRatingLabels as fn, MenuItem$1 as ft, FormControl$1 as g, SkeletonCard as gn, MenuRoot$1 as gt, FormColorPicker as h, SkeletonAvatar as hn, MenuPopover$1 as ht, FieldToken as i, useInputId as in, _objectWithoutProperties as ir, ToolbarButton$1 as it, FormRemove$1 as j, OPT_BORDER as jn, DialogDisclosure$1 as jt, FormRadio$1 as k, OPT_BADGE_RED as kn, DialogBody as kt, OPT_UI_DEFAULT_LOCALE as l, Button as ln, TooltipAnchor$1 as lt, FormCheckbox$1 as m, Skeleton as mn, MenuItemRadio$1 as mt, Highlight as n, StarRating as nn, OPT_TH as nr, Slider as nt, Pagination as o, Spinner as on, cn as or, ToolbarRoot$1 as ot, formatOptTime as p, StatCard as pn, MenuItemCheckbox$1 as pt, DateRangePicker as q, OPT_SURFACE_RAISED as qn, DisclosureContent$1 as qt, FIELD_TYPE_TOKENS as r, Input as rn, _objectSpread2 as rr, NumberInput as rt, Checkbox as s, Badge as sn, ToolbarSeparator$1 as st, TextTruncate as t, Meter$1 as tn, OPT_TEXT_TERTIARY as tr, PopoverTrigger$1 as tt, formatOptDateTime as u, koBreadcrumbLabels as un, TooltipProvider$1 as ut, FormDescription$1 as v, SkeletonText as vn, MenuTrigger$1 as vt, FormInput$1 as w, OPT_ANIMATE_DRAWER as wn, ComboboxInput$1 as wt, FormGroup$1 as x, RequiredMark as xn, ComboboxEmpty as xt, FormError$1 as y, useControlledState as yn, MenubarContainer$1 as yt, FormSubmit$1 as z, OPT_FOCUS as zn, SelectLabel$1 as zt };
4101
4548
 
4102
- //# sourceMappingURL=highlight-C2sfnJN8.js.map
4549
+ //# sourceMappingURL=text-truncate-C17HHCR-.js.map