torch-glare 2.1.2 → 2.1.3

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 (27) hide show
  1. package/apps/lib/components/Avatar.tsx +1 -1
  2. package/apps/lib/components/Card.tsx +68 -54
  3. package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
  4. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +56 -45
  5. package/apps/lib/components/DataViews/DataViewsHeader.tsx +130 -28
  6. package/apps/lib/components/DataViews/DataViewsLayout.tsx +32 -2
  7. package/apps/lib/components/DataViews/FilterPanel.tsx +148 -3
  8. package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
  9. package/apps/lib/components/DataViews/InboxView.tsx +263 -282
  10. package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
  11. package/apps/lib/components/DataViews/KanbanView.tsx +264 -153
  12. package/apps/lib/components/DataViews/PanelControls.tsx +10 -41
  13. package/apps/lib/components/DataViews/TreeView.tsx +220 -191
  14. package/apps/lib/components/DataViews/index.ts +6 -0
  15. package/apps/lib/components/DataViews/types.ts +30 -1
  16. package/apps/lib/components/Radio.tsx +18 -21
  17. package/apps/lib/components/Switch.tsx +3 -1
  18. package/apps/lib/components/Table.tsx +1 -1
  19. package/apps/lib/components/TreeFolder/TreeFolder.tsx +160 -137
  20. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +221 -93
  21. package/apps/lib/components/TreeFolder/types.ts +9 -0
  22. package/apps/lib/layouts/DataViewCard.tsx +76 -0
  23. package/dist/src/shared/copyComponentsRecursively.js +9 -1
  24. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  25. package/docs/components/data-views-config-panel.md +204 -0
  26. package/docs/components/data-views-layout.md +270 -0
  27. package/package.json +1 -1
@@ -7,6 +7,7 @@ import {
7
7
  useMemo,
8
8
  useRef,
9
9
  useState,
10
+ type ReactNode,
10
11
  } from "react";
11
12
  import { List, LayoutGrid, Inbox as InboxIcon, Network } from "lucide-react";
12
13
  import type {
@@ -41,6 +42,8 @@ export type DataViewsLayoutProps = {
41
42
  inboxConfig?: InboxConfig;
42
43
  treeConfig?: TreeConfig;
43
44
  kanbanGroupBy?: string;
45
+ kanbanTitleField?: string;
46
+ onKanbanColumnAction?: (columnId: string) => void;
44
47
 
45
48
  views?: ViewVisibility;
46
49
 
@@ -57,6 +60,14 @@ export type DataViewsLayoutProps = {
57
60
  onAddNew?: () => void;
58
61
  addNewLabel?: string;
59
62
 
63
+ inboxItemHref?: (item: DynamicRecord, id: any) => string;
64
+ inboxSelectedId?: any;
65
+ inboxRenderDetail?: (item: DynamicRecord | null) => ReactNode;
66
+
67
+ searchValue?: string;
68
+ onSearchChange?: (value: string) => void;
69
+ searchPlaceholder?: string;
70
+
60
71
  className?: string;
61
72
  theme?: Themes;
62
73
  };
@@ -83,6 +94,8 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
83
94
  inboxConfig,
84
95
  treeConfig,
85
96
  kanbanGroupBy,
97
+ kanbanTitleField,
98
+ onKanbanColumnAction,
86
99
  views,
87
100
  columns,
88
101
  filters,
@@ -94,6 +107,12 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
94
107
  showTitle = true,
95
108
  onAddNew,
96
109
  addNewLabel,
110
+ inboxItemHref,
111
+ inboxSelectedId,
112
+ inboxRenderDetail,
113
+ searchValue,
114
+ onSearchChange,
115
+ searchPlaceholder,
97
116
  className,
98
117
  theme,
99
118
  } = props;
@@ -180,7 +199,10 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
180
199
  className={cn(
181
200
  // Shell is always black (matches Figma): the dark header and config
182
201
  // rail sit on it; the Master Container is the white surface inside.
183
- "flex h-screen gap-2 bg-black p-2 text-content-presentation-global-primary",
202
+ // `overflow-hidden` traps any child overflow — without it, a tall
203
+ // config-panel body escapes and creates a page-level scrollbar in
204
+ // addition to the panel's own.
205
+ "flex h-screen gap-2 overflow-hidden bg-black text-content-presentation-global-primary",
184
206
  className,
185
207
  )}
186
208
  >
@@ -197,13 +219,16 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
197
219
  onToggleSettings={togglePanel}
198
220
  onAddNew={onAddNew}
199
221
  addNewLabel={addNewLabel}
222
+ searchValue={searchValue}
223
+ onSearchChange={onSearchChange}
224
+ searchPlaceholder={searchPlaceholder}
200
225
  />
201
226
  )}
202
227
 
203
228
  <main className="flex min-h-0 flex-1 overflow-hidden ">
204
229
  {/* Master Container — white card, 16px radius, #D4D4D4 hairline
205
230
  border. Fixed surface (matches header chrome). */}
206
- <div className="flex flex-1 overflow-hidden rounded-[16px] border border-border-presentation-global-primary ">
231
+ <div className="flex flex-1 overflow-hidden rounded-[16px]">
207
232
  {/* Clip the scrollable surface to the parent radius MINUS the 1px
208
233
  border (16 − 1 = 15px). Using the full 16px here let the
209
234
  opaque view background sit flush with the parent's outer edge
@@ -234,6 +259,8 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
234
259
  config={effectiveConfig}
235
260
  onDataUpdate={onDataUpdate}
236
261
  groupByField={effectiveKanbanGroupBy}
262
+ titleField={kanbanTitleField}
263
+ onColumnAction={onKanbanColumnAction}
237
264
  />
238
265
  )}
239
266
  {currentView === "inbox" && enabledViews.inbox && (
@@ -248,6 +275,9 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
248
275
  filterState={filterState}
249
276
  onFilterChange={setFilterState}
250
277
  showFilters={false}
278
+ itemHref={inboxItemHref}
279
+ selectedItemId={inboxSelectedId}
280
+ renderDetail={inboxRenderDetail}
251
281
  />
252
282
  )}
253
283
  {currentView === "tree" && enabledViews.tree && (
@@ -1,11 +1,15 @@
1
1
  "use client"
2
2
 
3
+ import { Fragment } from "react"
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
3
5
  import { Button } from "../Button"
4
6
  import { Badge } from "../Badge"
5
7
  import { X } from "lucide-react"
6
8
  import { Checkbox } from "../Checkbox"
7
9
  import { Divider } from "../Divider"
8
10
  import { Label } from "../Label"
11
+ import { DataViewRadio } from "./DataViewRadio"
12
+ import { cn } from "../../utils/cn"
9
13
  import type {
10
14
  DynamicRecord,
11
15
  DynamicFilterConfig,
@@ -37,6 +41,13 @@ type FilterPanelProps = {
37
41
  onFilterChange: (path: string, value: FilterValue) => void
38
42
  onClearAll: () => void
39
43
  filterConfig?: DynamicFilterConfig[]
44
+ /**
45
+ * "default": standalone left-rail style (border, padding, light surface).
46
+ * "panel": matches the Config tab inside DataViewsConfigPanel — no outer
47
+ * chrome, white section headers, categorical options inside a #1C1D1F
48
+ * rounded container, sections separated by #2C2D2E dividers.
49
+ */
50
+ variant?: "default" | "panel"
40
51
  }
41
52
 
42
53
  const NUMERIC_TYPES: FieldType[] = [
@@ -172,6 +183,7 @@ export function FilterPanel({
172
183
  onFilterChange,
173
184
  onClearAll,
174
185
  filterConfig,
186
+ variant = "default",
175
187
  }: FilterPanelProps) {
176
188
  const entries = buildFilterableEntries(data, fields, filterConfig)
177
189
 
@@ -198,6 +210,57 @@ export function FilterPanel({
198
210
 
199
211
  const countBadge = resolveBadgeVariant("gray")
200
212
 
213
+ if (variant === "panel") {
214
+ return (
215
+ <div className="flex flex-col gap-6 px-3 py-4">
216
+ <div className="flex items-center justify-between">
217
+ <div className="flex items-center gap-2">
218
+ <h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
219
+ Filters
220
+ </h3>
221
+ {totalFilters > 0 && (
222
+ <Badge
223
+ {...countBadge}
224
+ label={String(totalFilters)}
225
+ className="h-5 min-w-[20px] rounded-full p-0 text-xs"
226
+ size="XS"
227
+ />
228
+ )}
229
+ </div>
230
+ {totalFilters > 0 && (
231
+ <button
232
+ type="button"
233
+ onClick={onClearAll}
234
+ className="flex items-center gap-1 rounded-[4px] bg-white/[0.15] px-1.5 py-0.5 text-[12px] font-[510] text-white transition-colors hover:bg-white/25"
235
+ >
236
+ <X className="h-3 w-3" />
237
+ Clear
238
+ </button>
239
+ )}
240
+ </div>
241
+
242
+ {entries.map((entry, index) => (
243
+ <Fragment key={entry.path}>
244
+ {index > 0 && <div className="h-px w-full bg-[#2C2D2E]" />}
245
+ <div className="space-y-3">
246
+ <h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
247
+ {entry.label}
248
+ </h3>
249
+ <FilterBody
250
+ entry={entry}
251
+ data={data}
252
+ value={filters[entry.path]}
253
+ onCategoricalToggle={(opt) => toggleCategorical(entry.path, opt)}
254
+ onSetFilter={(v) => setFilter(entry.path, v)}
255
+ variant="panel"
256
+ />
257
+ </div>
258
+ </Fragment>
259
+ ))}
260
+ </div>
261
+ )
262
+ }
263
+
201
264
  return (
202
265
  <div className="w-64 border-r border-border-presentation-global-primary bg-background-presentation-body-overlay-primary p-4 overflow-y-auto">
203
266
  <div className="flex items-center justify-between mb-4">
@@ -209,7 +272,7 @@ export function FilterPanel({
209
272
  label={String(totalFilters)}
210
273
  className="h-5 min-w-[20px] rounded-full p-0 text-xs"
211
274
  size="XS"
212
-
275
+
213
276
  />
214
277
  )}
215
278
  </div>
@@ -248,12 +311,14 @@ function FilterBody({
248
311
  value,
249
312
  onCategoricalToggle,
250
313
  onSetFilter,
314
+ variant = "default",
251
315
  }: {
252
316
  entry: Entry
253
317
  data: DynamicRecord[]
254
318
  value: FilterValue | undefined
255
319
  onCategoricalToggle: (option: string) => void
256
320
  onSetFilter: (next: FilterValue) => void
321
+ variant?: "default" | "panel"
257
322
  }) {
258
323
  if (entry.kind === "numeric-range" && entry.field) {
259
324
  const extremes = computeNumericExtremes(data, entry.path)
@@ -293,12 +358,92 @@ function FilterBody({
293
358
 
294
359
  const opts = getCategoricalOptions(data, entry.path, entry.field, entry.legacy)
295
360
  const selected = Array.isArray(value) ? value : []
361
+ const isSingle = entry.field?.filterMode === "single"
362
+
363
+ if (isSingle) {
364
+ const current = selected[0] ?? ""
365
+ const onSingleChange = (next: string) => onSetFilter(next ? [next] : [])
366
+ return (
367
+ <RadioGroupPrimitive.Root
368
+ value={current}
369
+ onValueChange={onSingleChange}
370
+ className={cn(
371
+ "flex flex-col space-y-0 rounded-[12px] bg-[#1C1D1F] p-1",
372
+ // Hide the divider directly above and below the hovered row.
373
+ "[&>div:has(>[role=radio]:hover)>.dv-divider]:opacity-0",
374
+ "[&>div:has(>[role=radio]:hover)+div>.dv-divider]:opacity-0",
375
+ )}
376
+ >
377
+ {opts.map((opt, i) => {
378
+ const isSelected = current === opt
379
+ const badgeVariant = entry.field?.variants?.[opt]
380
+ const badgeProps = badgeVariant ? resolveBadgeVariant(badgeVariant) : null
381
+ return (
382
+ <div key={opt}>
383
+ {i > 0 && (
384
+ <div className="dv-divider h-px bg-[#2C2D2E]" />
385
+ )}
386
+ <DataViewRadio value={opt}>
387
+ {entry.legacy?.render
388
+ ? entry.legacy.render(opt, isSelected)
389
+ : badgeProps
390
+ ? <Badge {...badgeProps} label={opt} size="XS" />
391
+ : opt}
392
+ </DataViewRadio>
393
+ </div>
394
+ )
395
+ })}
396
+ </RadioGroupPrimitive.Root>
397
+ )
398
+ }
399
+
400
+ if (variant === "panel") {
401
+ return (
402
+ <div
403
+ className={cn(
404
+ "flex flex-col rounded-[12px] bg-[#1C1D1F] p-1",
405
+ // Hide the divider directly above and below the hovered row.
406
+ "[&>div:has(>label:hover)>.dv-divider]:opacity-0",
407
+ "[&>div:has(>label:hover)+div>.dv-divider]:opacity-0",
408
+ )}
409
+ >
410
+ {opts.map((opt, i) => {
411
+ const isSelected = selected.includes(opt)
412
+ const badgeVariant = entry.field?.variants?.[opt]
413
+ const badgeProps = badgeVariant ? resolveBadgeVariant(badgeVariant) : null
414
+ return (
415
+ <div key={opt}>
416
+ {i > 0 && <div className="dv-divider h-px bg-[#2C2D2E]" />}
417
+ <label
418
+ htmlFor={`${entry.path}-${opt}`}
419
+ className="flex cursor-pointer items-center gap-2 rounded-[8px] px-2 py-2 text-[14px] text-white hover:bg-white/5"
420
+ >
421
+ <Checkbox
422
+ id={`${entry.path}-${opt}`}
423
+ checked={isSelected}
424
+ onCheckedChange={() => onCategoricalToggle(opt)}
425
+ />
426
+ <span className="flex-1 leading-none">
427
+ {entry.legacy?.render
428
+ ? entry.legacy.render(opt, isSelected)
429
+ : badgeProps
430
+ ? <Badge {...badgeProps} label={opt} size="XS" />
431
+ : opt}
432
+ </span>
433
+ </label>
434
+ </div>
435
+ )
436
+ })}
437
+ </div>
438
+ )
439
+ }
440
+
296
441
  return (
297
442
  <div className="space-y-2">
298
443
  {opts.map((opt) => {
299
444
  const isSelected = selected.includes(opt)
300
- const variant = entry.field?.variants?.[opt]
301
- const badgeProps = variant ? resolveBadgeVariant(variant) : null
445
+ const badgeVariant = entry.field?.variants?.[opt]
446
+ const badgeProps = badgeVariant ? resolveBadgeVariant(badgeVariant) : null
302
447
  return (
303
448
  <div key={opt} className="flex items-center space-x-2">
304
449
  <Checkbox
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { Search } from "lucide-react";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { Button } from "../Button";
6
+
7
+ export type HeaderSearchProps = {
8
+ value: string;
9
+ onChange: (value: string) => void;
10
+ placeholder?: string;
11
+ };
12
+
13
+ export function HeaderSearch({
14
+ value,
15
+ onChange,
16
+ placeholder = "Search...",
17
+ }: HeaderSearchProps) {
18
+ const [open, setOpen] = useState(false);
19
+ const inputRef = useRef<HTMLInputElement>(null);
20
+ const wrapRef = useRef<HTMLDivElement>(null);
21
+
22
+ useEffect(() => {
23
+ if (open) inputRef.current?.focus();
24
+ }, [open]);
25
+
26
+ // Auto-collapse on outside click only when the input is empty — keeps the
27
+ // expanded state if the user has typed a query but clicks away.
28
+ useEffect(() => {
29
+ if (!open) return;
30
+ function onPointerDown(e: MouseEvent) {
31
+ if (!wrapRef.current) return;
32
+ if (wrapRef.current.contains(e.target as Node)) return;
33
+ if (!value) setOpen(false);
34
+ }
35
+ document.addEventListener("mousedown", onPointerDown);
36
+ return () => document.removeEventListener("mousedown", onPointerDown);
37
+ }, [open, value]);
38
+
39
+ function clearAndCollapse() {
40
+ onChange("");
41
+ setOpen(false);
42
+ }
43
+
44
+ if (!open) {
45
+ return (
46
+ <Button
47
+ variant="BluContStyle"
48
+ size="M"
49
+ buttonType="icon"
50
+ aria-label="Open search"
51
+ onClick={() => setOpen(true)}
52
+ className="shrink-0 rounded-[6px] border border-border-presentation-global-primary"
53
+ >
54
+ <Search className="h-[18px] w-[18px]" />
55
+ </Button>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <div
61
+ ref={wrapRef}
62
+ className="relative flex h-[28px] w-[260px] shrink-0 items-center justify-center rounded-[6px] border border-border-presentation-state-focus bg-background-presentation-form-field-primary px-1 shadow-[0_1px_6px_0_rgba(0,0,0,0.30)] transition-all duration-150 ease-in-out"
63
+ >
64
+ <input
65
+ ref={inputRef}
66
+ type="text"
67
+ value={value}
68
+ placeholder={placeholder}
69
+ onChange={(e) => onChange(e.target.value)}
70
+ onKeyDown={(e) => {
71
+ if (e.key === "Escape") clearAndCollapse();
72
+ }}
73
+ size={1}
74
+ className="min-w-0 flex-1 bg-transparent text-[14px] leading-none text-white caret-[#1E7AFE] placeholder:text-content-presentation-global-tertiary focus:outline-none"
75
+ />
76
+ <button
77
+ type="button"
78
+ aria-label="Clear search"
79
+ onClick={clearAndCollapse}
80
+ className="flex shrink-0 items-center justify-center self-stretch px-1"
81
+ >
82
+ <svg
83
+ xmlns="http://www.w3.org/2000/svg"
84
+ width="16"
85
+ height="16"
86
+ viewBox="0 0 16 16"
87
+ fill="none"
88
+ >
89
+ <path
90
+ d="M7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325C11.6818 1.33325 14.6666 4.31802 14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666ZM7.99992 7.05712L6.1143 5.17149L5.17149 6.1143L7.05712 7.99992L5.17149 9.88552L6.1143 10.8283L7.99992 8.94272L9.88552 10.8283L10.8283 9.88552L8.94272 7.99992L10.8283 6.1143L9.88552 5.17149L7.99992 7.05712Z"
91
+ fill="white"
92
+ />
93
+ </svg>
94
+ </button>
95
+ </div>
96
+ );
97
+ }