datool 0.0.3 → 0.0.5

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>datool</title>
7
- <script type="module" crossorigin src="/assets/index-uoZ4c_I8.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BeRNeRUq.css">
7
+ <script type="module" crossorigin src="/assets/index-OdNyDkx7.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DUkIilaZ.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datool",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "Local-only config-driven log viewer with SSE streaming and a generic table UI.",
6
6
  "bin": {
@@ -41,6 +41,7 @@ import {
41
41
  } from "@/lib/data-table-search"
42
42
  import {
43
43
  readDatoolColumnVisibility,
44
+ readDatoolGrouping,
44
45
  readDatoolSearch,
45
46
  readSelectedStreamId,
46
47
  writeDatoolUrlState,
@@ -59,10 +60,89 @@ type ViewerExportColumn = {
59
60
  label: string
60
61
  }
61
62
 
63
+ const GROUPED_ROW_GAP = 32
64
+
62
65
  function toActionRows(rows: ViewerRow[]): Record<string, unknown>[] {
63
66
  return rows.map(({ __datoolRowId: _datoolRowId, ...row }) => row)
64
67
  }
65
68
 
69
+ function stringifyGroupingValue(value: unknown) {
70
+ if (value === undefined) {
71
+ return "undefined:"
72
+ }
73
+
74
+ if (value === null) {
75
+ return "null:"
76
+ }
77
+
78
+ if (value instanceof Date) {
79
+ return `date:${value.toISOString()}`
80
+ }
81
+
82
+ if (typeof value === "object") {
83
+ try {
84
+ return `object:${JSON.stringify(value)}`
85
+ } catch {
86
+ return `object:${String(value)}`
87
+ }
88
+ }
89
+
90
+ return `${typeof value}:${String(value)}`
91
+ }
92
+
93
+ function groupViewerRows(
94
+ rows: ViewerRow[],
95
+ columns: ViewerExportColumn[]
96
+ ): {
97
+ groupStartRowIds: Set<string>
98
+ rows: ViewerRow[]
99
+ } {
100
+ if (columns.length === 0 || rows.length === 0) {
101
+ return {
102
+ groupStartRowIds: new Set<string>(),
103
+ rows,
104
+ }
105
+ }
106
+
107
+ const groupOrder: string[] = []
108
+ const rowsByGroup = new Map<string, ViewerRow[]>()
109
+
110
+ for (const row of rows) {
111
+ const groupKey = columns
112
+ .map((column) =>
113
+ stringifyGroupingValue(getValueAtPath(row, column.accessorKey))
114
+ )
115
+ .join("\u001f")
116
+ const existingRows = rowsByGroup.get(groupKey)
117
+
118
+ if (existingRows) {
119
+ existingRows.push(row)
120
+ continue
121
+ }
122
+
123
+ groupOrder.push(groupKey)
124
+ rowsByGroup.set(groupKey, [row])
125
+ }
126
+
127
+ const groupedRows: ViewerRow[] = []
128
+ const groupStartRowIds = new Set<string>()
129
+
130
+ groupOrder.forEach((groupKey, index) => {
131
+ const groupRows = rowsByGroup.get(groupKey) ?? []
132
+
133
+ if (index > 0 && groupRows[0]) {
134
+ groupStartRowIds.add(groupRows[0].__datoolRowId)
135
+ }
136
+
137
+ groupedRows.push(...groupRows)
138
+ })
139
+
140
+ return {
141
+ groupStartRowIds,
142
+ rows: groupedRows,
143
+ }
144
+ }
145
+
66
146
  function applyActionRowChanges(
67
147
  currentRows: ViewerRow[],
68
148
  targetRowIds: string[],
@@ -225,6 +305,7 @@ function buildTableColumns(
225
305
  ? undefined
226
306
  : (column.accessorKey as Extract<keyof ViewerRow, string>),
227
307
  align: column.align,
308
+ enumColors: column.enumColors,
228
309
  header: column.header,
229
310
  id: resolveDatoolColumnId(column, index),
230
311
  kind: column.kind,
@@ -346,7 +427,10 @@ function DatoolTable({
346
427
  isLoadingConfig,
347
428
  rows,
348
429
  settingsColumns,
430
+ groupedColumnIds,
431
+ groupedRowStartIds,
349
432
  selectedStreamId,
433
+ setGroupedColumnIds,
350
434
  setColumnVisibility,
351
435
  searchInputRef,
352
436
  handleExport,
@@ -368,7 +452,10 @@ function DatoolTable({
368
452
  label: string
369
453
  visible: boolean
370
454
  }>
455
+ groupedColumnIds: string[]
456
+ groupedRowStartIds: Set<string>
371
457
  selectedStreamId: string | null
458
+ setGroupedColumnIds: React.Dispatch<React.SetStateAction<string[]>>
372
459
  setColumnVisibility: React.Dispatch<React.SetStateAction<VisibilityState>>
373
460
  searchInputRef: React.RefObject<DataTableSearchInputHandle | null>
374
461
  handleExport: (format: "csv" | "md") => void
@@ -377,6 +464,19 @@ function DatoolTable({
377
464
  setShouldConnect: React.Dispatch<React.SetStateAction<boolean>>
378
465
  }) {
379
466
  const { search, setSearch } = useDataTableContext<ViewerRow>()
467
+ const resolveRowStyle = React.useCallback(
468
+ (row: ViewerRow) => {
469
+ if (!groupedRowStartIds.has(row.__datoolRowId)) {
470
+ return undefined
471
+ }
472
+
473
+ return {
474
+ borderTop: `${GROUPED_ROW_GAP}px solid var(--color-table-gap)`,
475
+ boxShadow: `inset 0 1px 0 0 var(--color-border)`,
476
+ } satisfies React.CSSProperties
477
+ },
478
+ [groupedRowStartIds]
479
+ )
380
480
  const rowActions = React.useMemo<DataTableRowAction<ViewerRow>[]>(
381
481
  () => {
382
482
  const configActions =
@@ -525,9 +625,24 @@ function DatoolTable({
525
625
  />
526
626
  <ViewerSettings
527
627
  columns={settingsColumns}
628
+ groupedColumnIds={groupedColumnIds}
528
629
  isDisabled={isLoadingConfig || !activeStream}
529
630
  onExportCsv={() => handleExport("csv")}
530
631
  onExportMarkdown={() => handleExport("md")}
632
+ onClearGrouping={() => setGroupedColumnIds([])}
633
+ onToggleGrouping={(columnId, grouped) =>
634
+ setGroupedColumnIds((current) => {
635
+ if (grouped) {
636
+ return current.includes(columnId)
637
+ ? current
638
+ : [...current, columnId]
639
+ }
640
+
641
+ return current.filter(
642
+ (currentColumnId) => currentColumnId !== columnId
643
+ )
644
+ })
645
+ }
531
646
  onToggleColumn={(columnId, visible) =>
532
647
  setColumnVisibility((current) => ({
533
648
  ...current,
@@ -544,7 +659,7 @@ function DatoolTable({
544
659
 
545
660
  <div className="min-h-0 flex-1">
546
661
  {activeStream ? (
547
- <DataTable rowActions={rowActions} />
662
+ <DataTable rowActions={rowActions} rowStyle={resolveRowStyle} />
548
663
  ) : (
549
664
  <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
550
665
  No stream selected.
@@ -581,6 +696,7 @@ export default function App() {
581
696
  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(
582
697
  {}
583
698
  )
699
+ const [groupedColumnIds, setGroupedColumnIds] = React.useState<string[]>([])
584
700
  const eventSourceRef = React.useRef<EventSource | null>(null)
585
701
  const hasInitializedStreamRef = React.useRef(false)
586
702
  const [hydratedTableId, setHydratedTableId] = React.useState<string | null>(
@@ -676,6 +792,23 @@ export default function App() {
676
792
  exportColumns.filter((column) => columnVisibility[column.id] !== false),
677
793
  [columnVisibility, exportColumns]
678
794
  )
795
+ const exportColumnsById = React.useMemo(
796
+ () => new Map(exportColumns.map((column) => [column.id, column])),
797
+ [exportColumns]
798
+ )
799
+ const groupedExportColumns = React.useMemo(
800
+ () =>
801
+ groupedColumnIds.flatMap((columnId) => {
802
+ const column = exportColumnsById.get(columnId)
803
+
804
+ return column ? [column] : []
805
+ }),
806
+ [exportColumnsById, groupedColumnIds]
807
+ )
808
+ const groupedRowsState = React.useMemo(
809
+ () => groupViewerRows(rows, groupedExportColumns),
810
+ [groupedExportColumns, rows]
811
+ )
679
812
  const isConnecting = Boolean(selectedStreamId) && shouldConnect && !isConnected
680
813
  const columnIds = React.useMemo(
681
814
  () => exportColumns.map((column) => column.id),
@@ -693,12 +826,14 @@ export default function App() {
693
826
 
694
827
  React.useEffect(() => {
695
828
  if (!activeStream) {
829
+ setGroupedColumnIds([])
696
830
  setHydratedTableId(null)
697
831
  return
698
832
  }
699
833
 
700
834
  setSearch(readDatoolSearch(tableId))
701
835
  setColumnVisibility(readDatoolColumnVisibility(tableId, columnIds))
836
+ setGroupedColumnIds(readDatoolGrouping(tableId, columnIds))
702
837
  setHydratedTableId(tableId)
703
838
  }, [activeStream, columnIds, tableId])
704
839
 
@@ -711,6 +846,7 @@ export default function App() {
711
846
  writeDatoolUrlState({
712
847
  columnIds,
713
848
  columnVisibility,
849
+ groupBy: groupedColumnIds,
714
850
  search,
715
851
  selectedStreamId,
716
852
  tableId,
@@ -722,6 +858,7 @@ export default function App() {
722
858
  activeStream,
723
859
  columnIds,
724
860
  columnVisibility,
861
+ groupedColumnIds,
725
862
  hydratedTableId,
726
863
  search,
727
864
  selectedStreamId,
@@ -739,6 +876,7 @@ export default function App() {
739
876
  setColumnVisibility(
740
877
  readDatoolColumnVisibility(nextTableId, columnIds)
741
878
  )
879
+ setGroupedColumnIds(readDatoolGrouping(nextTableId, columnIds))
742
880
  setHydratedTableId(nextTableId)
743
881
  }
744
882
 
@@ -852,8 +990,8 @@ export default function App() {
852
990
  const fileBaseName = `${sanitizeFilePart(activeStream.label)}-${timeStamp}`
853
991
  const content =
854
992
  format === "csv"
855
- ? buildCsvContent(rows, visibleExportColumns)
856
- : buildMarkdownContent(rows, visibleExportColumns)
993
+ ? buildCsvContent(groupedRowsState.rows, visibleExportColumns)
994
+ : buildMarkdownContent(groupedRowsState.rows, visibleExportColumns)
857
995
 
858
996
  downloadTextFile(
859
997
  content,
@@ -861,15 +999,15 @@ export default function App() {
861
999
  format === "csv" ? "text/csv" : "text/markdown"
862
1000
  )
863
1001
  },
864
- [activeStream, rows, visibleExportColumns]
1002
+ [activeStream, groupedRowsState.rows, visibleExportColumns]
865
1003
  )
866
1004
 
867
1005
  return (
868
1006
  <DataTableProvider
869
- autoScrollToBottom
1007
+ autoScrollToBottom={groupedColumnIds.length === 0}
870
1008
  columnVisibility={columnVisibility}
871
1009
  columns={columns}
872
- data={rows}
1010
+ data={groupedRowsState.rows}
873
1011
  getRowId={(row) => row.__datoolRowId}
874
1012
  height="100%"
875
1013
  id={tableId}
@@ -888,8 +1026,11 @@ export default function App() {
888
1026
  isConnected={isConnected}
889
1027
  isConnecting={isConnecting}
890
1028
  isLoadingConfig={isLoadingConfig}
1029
+ groupedColumnIds={groupedColumnIds}
1030
+ groupedRowStartIds={groupedRowsState.groupStartRowIds}
891
1031
  rows={rows}
892
1032
  settingsColumns={settingsColumns}
1033
+ setGroupedColumnIds={setGroupedColumnIds}
893
1034
  setColumnVisibility={setColumnVisibility}
894
1035
  searchInputRef={searchInputRef}
895
1036
  selectedStreamId={selectedStreamId}
@@ -5,7 +5,9 @@ import * as React from "react"
5
5
 
6
6
  import type { DataTableColumnKind } from "./data-table-col-icon"
7
7
  import type { DataTableColumnMeta } from "./data-table-header-col"
8
+ import { EnumBadge } from "./enum-badge"
8
9
  import { cn } from "@/lib/utils"
10
+ import type { DatoolEnumColorMap } from "../../shared/types"
9
11
 
10
12
  function getAlignmentClassName(align: DataTableColumnMeta["align"] = "left") {
11
13
  switch (align) {
@@ -36,11 +38,28 @@ function formatNumber(value: number) {
36
38
  }).format(value)
37
39
  }
38
40
 
39
- function fallbackCellValue(value: unknown, kind?: DataTableColumnKind) {
41
+ function fallbackCellValue(
42
+ value: unknown,
43
+ kind?: DataTableColumnKind,
44
+ options?: {
45
+ enumColors?: DatoolEnumColorMap
46
+ enumOptions?: string[]
47
+ }
48
+ ) {
40
49
  if (value === null || value === undefined || value === "") {
41
50
  return <span className="text-muted-foreground">-</span>
42
51
  }
43
52
 
53
+ if (kind === "enum") {
54
+ return (
55
+ <EnumBadge
56
+ colors={options?.enumColors}
57
+ options={options?.enumOptions}
58
+ value={String(value)}
59
+ />
60
+ )
61
+ }
62
+
44
63
  if (kind === "boolean" || typeof value === "boolean") {
45
64
  return (
46
65
  <span
@@ -200,7 +219,11 @@ export function DataTableBodyCell<TData>({
200
219
  highlightTerms.length > 0
201
220
  const content = shouldHighlight
202
221
  ? renderHighlightedText(rawValue, highlightTerms, rendered)
203
- : (rendered ?? fallbackCellValue(rawValue, meta.kind))
222
+ : (rendered ??
223
+ fallbackCellValue(rawValue, meta.kind, {
224
+ enumColors: meta.enumColors,
225
+ enumOptions: meta.enumOptions,
226
+ }))
204
227
 
205
228
  return (
206
229
  <td
@@ -7,6 +7,7 @@ import {
7
7
  DataTableColIcon,
8
8
  type DataTableColumnKind,
9
9
  } from "./data-table-col-icon"
10
+ import type { DatoolEnumColorMap } from "../../shared/types"
10
11
  import { cn } from "@/lib/utils"
11
12
 
12
13
  export type DataTableAlign = "left" | "center" | "right"
@@ -14,6 +15,8 @@ export type DataTableAlign = "left" | "center" | "right"
14
15
  export type DataTableColumnMeta = {
15
16
  align?: DataTableAlign
16
17
  cellClassName?: string
18
+ enumColors?: DatoolEnumColorMap
19
+ enumOptions?: string[]
17
20
  headerClassName?: string
18
21
  highlightMatches?: boolean
19
22
  kind?: DataTableColumnKind
@@ -46,6 +46,7 @@ import {
46
46
  inferDataTableColumnKind,
47
47
  type DataTableColumnKind,
48
48
  } from "./data-table-col-icon"
49
+ import type { DatoolEnumColorMap } from "../../shared/types"
49
50
  import { Button } from "@/components/ui/button"
50
51
  import {
51
52
  ContextMenu,
@@ -148,6 +149,8 @@ export type DataTableColumnConfig<TData extends DataTableRow> = {
148
149
  enableHiding?: boolean
149
150
  enableResizing?: boolean
150
151
  enableSorting?: boolean
152
+ enumColors?: DatoolEnumColorMap
153
+ enumOptions?: string[]
151
154
  filterFn?: FilterFn<TData>
152
155
  header?: string
153
156
  headerClassName?: string
@@ -181,6 +184,7 @@ export type DataTableProps<TData extends DataTableRow> = {
181
184
  resolveColumnHighlightTerms?: (columnId: string, query: string) => string[]
182
185
  rowActions?: DataTableRowAction<TData>[]
183
186
  rowClassName?: (row: TData) => string | undefined
187
+ rowStyle?: (row: TData) => React.CSSProperties | undefined
184
188
  rowHeight?: number
185
189
  statePersistence?: "localStorage" | "none" | "url"
186
190
  }
@@ -537,6 +541,8 @@ function buildColumns<TData extends DataTableRow>(
537
541
  const meta: DataTableColumnMeta = {
538
542
  align: column.align ?? inferAlignment(kind),
539
543
  cellClassName: column.cellClassName,
544
+ enumColors: kind === "enum" ? column.enumColors : undefined,
545
+ enumOptions: kind === "enum" ? column.enumOptions : undefined,
540
546
  headerClassName: column.headerClassName,
541
547
  highlightMatches:
542
548
  column.highlightMatches ?? (kind === "text" ? true : false),
@@ -550,7 +556,10 @@ function buildColumns<TData extends DataTableRow>(
550
556
  cell: ({ getValue, row }) =>
551
557
  column.cell
552
558
  ? column.cell({ row: row.original, value: getValue() })
553
- : fallbackCellValue(getValue(), kind),
559
+ : fallbackCellValue(getValue(), kind, {
560
+ enumColors: kind === "enum" ? column.enumColors : undefined,
561
+ enumOptions: kind === "enum" ? column.enumOptions : undefined,
562
+ }),
554
563
  enableGlobalFilter: column.enableFiltering ?? true,
555
564
  filterFn: column.filterFn,
556
565
  enableHiding: column.enableHiding ?? true,
@@ -1131,9 +1140,11 @@ function DataTableView<TData extends DataTableRow>({
1131
1140
  resolveColumnHighlightTerms,
1132
1141
  rowActions,
1133
1142
  rowClassName,
1143
+ rowStyle,
1134
1144
  rowHeight = 48,
1135
1145
  statePersistence = "localStorage",
1136
1146
  }: DataTableProps<TData>) {
1147
+ const context = useOptionalDataTableContext<TData>()
1137
1148
  const persistedState = React.useMemo(
1138
1149
  () => readPersistedState(id, statePersistence),
1139
1150
  [id, statePersistence]
@@ -1190,17 +1201,45 @@ function DataTableView<TData extends DataTableRow>({
1190
1201
  () => Math.max(160, Math.min(320, rowActionButtonCount * 96)),
1191
1202
  [rowActionButtonCount]
1192
1203
  )
1204
+ const columnsWithEnumOptions = React.useMemo(() => {
1205
+ if (!columns || columns.length === 0) {
1206
+ return columns
1207
+ }
1208
+
1209
+ const fieldOptionsById = new Map(
1210
+ (context?.searchFields ?? [])
1211
+ .filter((field) => field.kind === "enum")
1212
+ .map((field) => [field.id, field.options ?? []])
1213
+ )
1214
+
1215
+ return columns.map((column, index) => {
1216
+ if (column.kind !== "enum" || column.enumOptions?.length) {
1217
+ return column
1218
+ }
1219
+
1220
+ const enumOptions = fieldOptionsById.get(resolveColumnId(column, index))
1221
+
1222
+ if (!enumOptions || enumOptions.length === 0) {
1223
+ return column
1224
+ }
1225
+
1226
+ return {
1227
+ ...column,
1228
+ enumOptions,
1229
+ } satisfies DataTableColumnConfig<TData>
1230
+ })
1231
+ }, [columns, context?.searchFields])
1193
1232
  const tableColumns = React.useMemo(
1194
1233
  () =>
1195
1234
  buildColumns(
1196
1235
  data,
1197
- columns,
1236
+ columnsWithEnumOptions,
1198
1237
  showRowSelectionColumn,
1199
1238
  showRowActionButtonsColumn,
1200
1239
  rowActionsColumnSize
1201
1240
  ),
1202
1241
  [
1203
- columns,
1242
+ columnsWithEnumOptions,
1204
1243
  data,
1205
1244
  rowActionsColumnSize,
1206
1245
  showRowActionButtonsColumn,
@@ -1886,6 +1925,7 @@ function DataTableView<TData extends DataTableRow>({
1886
1925
  }
1887
1926
  }}
1888
1927
  style={{
1928
+ ...rowStyle?.(row.original),
1889
1929
  minHeight: rowHeight,
1890
1930
  transform: `translateY(${virtualRow.start}px)`,
1891
1931
  width: table.getTotalSize(),
@@ -0,0 +1,133 @@
1
+ import * as React from "react"
2
+
3
+ import {
4
+ DATOOL_ENUM_BADGE_COLORS,
5
+ type DatoolEnumBadgeColor,
6
+ type DatoolEnumColorMap,
7
+ } from "../../shared/types"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ const ENUM_BADGE_STYLES: Record<DatoolEnumBadgeColor, string> = {
11
+ amber:
12
+ "bg-amber-50/90 text-amber-700 dark:bg-amber-400/10 dark:text-amber-200",
13
+ blue:
14
+ "bg-blue-50/90 text-blue-700 dark:bg-blue-400/10 dark:text-blue-200",
15
+ coral:
16
+ "bg-rose-50/90 text-rose-700 dark:bg-rose-400/10 dark:text-rose-200",
17
+ cyan:
18
+ "bg-cyan-50/90 text-cyan-700 dark:bg-cyan-400/10 dark:text-cyan-200",
19
+ emerald:
20
+ "bg-emerald-50/90 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-200",
21
+ fuchsia:
22
+ "bg-fuchsia-50/90 text-fuchsia-700 dark:bg-fuchsia-400/10 dark:text-fuchsia-200",
23
+ green:
24
+ "bg-green-50/90 text-green-700 dark:bg-green-400/10 dark:text-green-200",
25
+ indigo:
26
+ "bg-indigo-50/90 text-indigo-700 dark:bg-indigo-400/10 dark:text-indigo-200",
27
+ lime:
28
+ "bg-lime-50/90 text-lime-700 dark:bg-lime-400/10 dark:text-lime-200",
29
+ orange:
30
+ "bg-orange-50/90 text-orange-700 dark:bg-orange-400/10 dark:text-orange-200",
31
+ pink:
32
+ "bg-pink-50/90 text-pink-700 dark:bg-pink-400/10 dark:text-pink-200",
33
+ purple:
34
+ "bg-purple-50/90 text-purple-700 dark:bg-purple-400/10 dark:text-purple-200",
35
+ red: "bg-red-50/90 text-red-700 dark:bg-red-400/10 dark:text-red-200",
36
+ rose:
37
+ "bg-rose-50/90 text-rose-700 dark:bg-rose-400/10 dark:text-rose-200",
38
+ sky: "bg-sky-50/90 text-sky-700 dark:bg-sky-400/10 dark:text-sky-200",
39
+ stone:
40
+ "bg-stone-50/95 text-stone-700 dark:bg-white/5 dark:text-stone-100",
41
+ teal:
42
+ "bg-teal-50/90 text-teal-700 dark:bg-teal-400/10 dark:text-teal-200",
43
+ violet:
44
+ "bg-violet-50/90 text-violet-700 dark:bg-violet-400/10 dark:text-violet-200",
45
+ yellow:
46
+ "bg-yellow-50/90 text-yellow-700 dark:bg-yellow-400/10 dark:text-yellow-200",
47
+ zinc: "bg-zinc-50/95 text-zinc-700 dark:bg-white/5 dark:text-zinc-100",
48
+ }
49
+
50
+ function normalizeEnumValue(value: string) {
51
+ return value.trim().toUpperCase()
52
+ }
53
+
54
+ function getFallbackStyleIndex(value: string) {
55
+ let hash = 0
56
+
57
+ for (const character of value) {
58
+ hash = (hash * 31 + character.charCodeAt(0)) >>> 0
59
+ }
60
+
61
+ return hash % DATOOL_ENUM_BADGE_COLORS.length
62
+ }
63
+
64
+ function resolveConfiguredColor(
65
+ value: string,
66
+ colors?: DatoolEnumColorMap
67
+ ): DatoolEnumBadgeColor | null {
68
+ if (!colors) {
69
+ return null
70
+ }
71
+
72
+ const normalizedValue = normalizeEnumValue(value)
73
+
74
+ for (const [candidateValue, color] of Object.entries(colors)) {
75
+ if (color && normalizeEnumValue(candidateValue) === normalizedValue) {
76
+ return color
77
+ }
78
+ }
79
+
80
+ return null
81
+ }
82
+
83
+ function resolveEnumBadgeStyle(
84
+ value: string,
85
+ options?: string[],
86
+ colors?: DatoolEnumColorMap
87
+ ) {
88
+ const normalizedValue = normalizeEnumValue(value)
89
+ const configuredColor = resolveConfiguredColor(value, colors)
90
+
91
+ if (configuredColor) {
92
+ return ENUM_BADGE_STYLES[configuredColor]
93
+ }
94
+
95
+ const optionIndex =
96
+ options?.findIndex((option) => normalizeEnumValue(option) === normalizedValue) ??
97
+ -1
98
+
99
+ if (optionIndex >= 0) {
100
+ return ENUM_BADGE_STYLES[
101
+ DATOOL_ENUM_BADGE_COLORS[optionIndex % DATOOL_ENUM_BADGE_COLORS.length]
102
+ ]
103
+ }
104
+
105
+ return ENUM_BADGE_STYLES[
106
+ DATOOL_ENUM_BADGE_COLORS[getFallbackStyleIndex(normalizedValue)]
107
+ ]
108
+ }
109
+
110
+ export function EnumBadge({
111
+ className,
112
+ colors,
113
+ options,
114
+ value,
115
+ }: {
116
+ className?: string
117
+ colors?: DatoolEnumColorMap
118
+ options?: string[]
119
+ value: string
120
+ }) {
121
+ return (
122
+ <span
123
+ className={cn(
124
+ "inline-flex max-w-full items-center rounded-md px-2 py-1 text-sm leading-none font-medium whitespace-nowrap shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:shadow-none",
125
+ resolveEnumBadgeStyle(value, options, colors),
126
+ className
127
+ )}
128
+ title={value}
129
+ >
130
+ <span className="truncate">{value}</span>
131
+ </span>
132
+ )
133
+ }