torch-glare 2.2.1 → 2.4.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.
- package/apps/lib/components/Badge.tsx +6 -0
- package/apps/lib/components/ContextMenu.tsx +14 -11
- package/apps/lib/components/DataViews/DataViewRadio.tsx +9 -2
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +8 -8
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +13 -32
- package/apps/lib/components/DataViews/FilterPanel.tsx +26 -9
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +80 -0
- package/apps/lib/components/DataViews/types.ts +8 -0
- package/apps/lib/components/DatePicker.tsx +6 -1
- package/apps/lib/components/DropdownMenu.tsx +14 -11
- package/apps/lib/components/HeaderBar.tsx +127 -0
- package/apps/lib/components/SearchableSelect.tsx +42 -42
- package/apps/lib/components/SearchableTable.tsx +167 -177
- package/apps/lib/components/TabSwitch.tsx +181 -0
- package/docs/components/context-menu.md +30 -0
- package/docs/components/data-views-config-panel.md +3 -3
- package/docs/components/data-views-layout.md +9 -2
- package/docs/components/dropdown-menu.md +28 -0
- package/docs/components/header-bar.md +181 -0
- package/docs/components/searchable-table.md +44 -30
- package/docs/components/section-block.md +118 -0
- package/docs/components/tab-switch.md +163 -0
- package/docs/how-to/data-views-from-backend-response.md +1 -0
- package/package.json +1 -1
|
@@ -122,6 +122,12 @@ export const badgeStyles = cva(
|
|
|
122
122
|
"transition-all duration-200 ease-in-out",
|
|
123
123
|
"whitespace-nowrap",
|
|
124
124
|
"[&_i]:!leading-none",
|
|
125
|
+
// The subtle variants blend their label/icon with `mix-blend-luminosity`.
|
|
126
|
+
// `isolate` opens a new stacking context so that blend only composites
|
|
127
|
+
// against the badge's own background — not against arbitrary page layers
|
|
128
|
+
// behind it (semi-transparent rows, gradients, masks), which otherwise
|
|
129
|
+
// ghosts/double-strokes the text in "random" places.
|
|
130
|
+
"isolate",
|
|
125
131
|
],
|
|
126
132
|
{
|
|
127
133
|
variants: {
|
|
@@ -122,7 +122,7 @@ ContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;
|
|
|
122
122
|
const ContextMenuContent = React.forwardRef<
|
|
123
123
|
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
124
124
|
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> &
|
|
125
|
-
ContextMenuContentProps & { autoGroup?: boolean }
|
|
125
|
+
ContextMenuContentProps & { autoGroup?: boolean; maxHeight?: number }
|
|
126
126
|
>(
|
|
127
127
|
(
|
|
128
128
|
{
|
|
@@ -131,6 +131,8 @@ const ContextMenuContent = React.forwardRef<
|
|
|
131
131
|
variant = "PresentationStyle",
|
|
132
132
|
autoGroup = true,
|
|
133
133
|
collisionPadding = 8,
|
|
134
|
+
maxHeight = 320,
|
|
135
|
+
style,
|
|
134
136
|
children,
|
|
135
137
|
...props
|
|
136
138
|
},
|
|
@@ -141,13 +143,13 @@ const ContextMenuContent = React.forwardRef<
|
|
|
141
143
|
data-theme={theme}
|
|
142
144
|
ref={ref}
|
|
143
145
|
collisionPadding={collisionPadding}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)}
|
|
146
|
+
// Cap at maxHeight, but never exceed the space Radix has after collision
|
|
147
|
+
// handling. The menu scrolls (overflow on the surface) past this height.
|
|
148
|
+
style={{
|
|
149
|
+
maxHeight: `min(${maxHeight}px, var(--radix-context-menu-content-available-height))`,
|
|
150
|
+
...style,
|
|
151
|
+
}}
|
|
152
|
+
className={cn(menuContentStyles({ variant }), className)}
|
|
151
153
|
{...props}
|
|
152
154
|
>
|
|
153
155
|
{autoGroup ? autoGroupChildren(children) : children}
|
|
@@ -388,6 +390,7 @@ const MenuItemStyles = cva(
|
|
|
388
390
|
"border",
|
|
389
391
|
"border-transparent",
|
|
390
392
|
"flex",
|
|
393
|
+
"shrink-0", // keep full row height so the menu scrolls instead of squishing
|
|
391
394
|
"items-center",
|
|
392
395
|
"justify-start",
|
|
393
396
|
"text-overflow",
|
|
@@ -483,13 +486,13 @@ const menuContentStyles = cva(
|
|
|
483
486
|
"rounded-[14px]",
|
|
484
487
|
"min-w-[240px]",
|
|
485
488
|
"outline-none",
|
|
486
|
-
"overflow-
|
|
489
|
+
"overflow-y-auto",
|
|
490
|
+
"overflow-x-hidden",
|
|
487
491
|
// Only animate the OPEN (enter) state. An exit animation on [data-state=closed]
|
|
488
492
|
// holds the old DOM node during close, which breaks close/reposition on a
|
|
489
493
|
// second right-click (Radix issue #2572).
|
|
490
494
|
"data-[state=open]:animate-in",
|
|
491
495
|
"data-[state=open]:fade-in-0",
|
|
492
|
-
"overflow-x-hidden",
|
|
493
496
|
"scrollbar-hide",
|
|
494
497
|
"backdrop-blur-[21px]",
|
|
495
498
|
"flex gap-1 flex-col",
|
|
@@ -509,7 +512,7 @@ const menuContentStyles = cva(
|
|
|
509
512
|
}
|
|
510
513
|
);
|
|
511
514
|
|
|
512
|
-
const menuGroupStyles = cva(["flex", "flex-col"], {
|
|
515
|
+
const menuGroupStyles = cva(["flex", "flex-col", "shrink-0"], {
|
|
513
516
|
variants: {
|
|
514
517
|
variant: {
|
|
515
518
|
// Visually contains its items in a bordered card.
|
|
@@ -30,9 +30,16 @@ export function DataViewRadio({
|
|
|
30
30
|
<span
|
|
31
31
|
className={cn(
|
|
32
32
|
"flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full",
|
|
33
|
-
|
|
33
|
+
// Hardcoded literals so the indicator always renders dark inside the
|
|
34
|
+
// DataView regardless of the host app's data-theme (matches the
|
|
35
|
+
// always-dark panel chrome convention). Unselected ring = the spec
|
|
36
|
+
// CheckBox-Primary border (#626467) + BorderStyle fill
|
|
37
|
+
// (rgba(255,255,255,0.05)); same tokens the shared Checkbox uses.
|
|
38
|
+
"border border-[#626467] bg-white/5 transition-colors",
|
|
34
39
|
"group-data-[state=checked]:border-transparent",
|
|
35
|
-
|
|
40
|
+
// #0075FF = the previous selected-fill value (border-presentation-state-focus),
|
|
41
|
+
// now literal so the blue is theme-independent and visually unchanged.
|
|
42
|
+
"group-data-[state=checked]:bg-[#0075FF]",
|
|
36
43
|
)}
|
|
37
44
|
>
|
|
38
45
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
} from "./types";
|
|
19
19
|
|
|
20
20
|
import { RadioGroup } from "../Radio";
|
|
21
|
+
import { Button } from "../Button";
|
|
21
22
|
import { FilterPanel } from "./FilterPanel";
|
|
22
23
|
import { RadioRow, DataViewsSwitch } from "./PanelControls";
|
|
23
24
|
import { cn } from "../../utils/cn";
|
|
@@ -234,7 +235,7 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
234
235
|
type="button"
|
|
235
236
|
onClick={onClose}
|
|
236
237
|
aria-label="Close panel"
|
|
237
|
-
className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-white
|
|
238
|
+
className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-background-presentation-state-negative-primary hover:text-white"
|
|
238
239
|
>
|
|
239
240
|
<X className="h-[18px] w-[18px]" />
|
|
240
241
|
</button>
|
|
@@ -262,14 +263,16 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
262
263
|
</div>
|
|
263
264
|
))}
|
|
264
265
|
</RadioGroup>
|
|
265
|
-
<
|
|
266
|
+
<Button
|
|
266
267
|
type="button"
|
|
268
|
+
variant="BorderStyle"
|
|
269
|
+
size="M"
|
|
267
270
|
onClick={onSaveNewView}
|
|
268
|
-
className="
|
|
271
|
+
className="w-full"
|
|
269
272
|
>
|
|
270
|
-
<Plus className="h-
|
|
273
|
+
<Plus className="h-4 w-4" />
|
|
271
274
|
Save a New View
|
|
272
|
-
</
|
|
275
|
+
</Button>
|
|
273
276
|
</div>
|
|
274
277
|
|
|
275
278
|
<div className="h-px w-full bg-[#2C2D2E]" />
|
|
@@ -277,9 +280,6 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
277
280
|
{/* Table Columns */}
|
|
278
281
|
<div className="space-y-3">
|
|
279
282
|
<SectionHeader title="Table Columns" />
|
|
280
|
-
<p className="text-[12px] leading-[1.475] text-content-presentation-global-tertiary">
|
|
281
|
-
Show or hide columns in table view
|
|
282
|
-
</p>
|
|
283
283
|
{orderedColumns.length === 0 ? (
|
|
284
284
|
<p className="text-xs text-content-presentation-global-tertiary">
|
|
285
285
|
No fields detected.
|
|
@@ -4,6 +4,7 @@ import { Search, Settings } from "lucide-react";
|
|
|
4
4
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
5
5
|
import type { ViewType } from "./types";
|
|
6
6
|
import { Button } from "../Button";
|
|
7
|
+
import { TabSwitch } from "../TabSwitch";
|
|
7
8
|
import { cn } from "../../utils/cn";
|
|
8
9
|
|
|
9
10
|
export type DataViewsHeaderView = {
|
|
@@ -66,38 +67,18 @@ export function DataViewsHeader({
|
|
|
66
67
|
|
|
67
68
|
{/* Segmented view switcher */}
|
|
68
69
|
<div className="flex flex-1 items-center gap-2">
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<button
|
|
82
|
-
type="button"
|
|
83
|
-
aria-pressed={active}
|
|
84
|
-
onClick={() => onViewChange(view.id)}
|
|
85
|
-
className={cn(
|
|
86
|
-
"flex h-6 items-center gap-[6px] rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
|
|
87
|
-
active
|
|
88
|
-
? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
|
|
89
|
-
: "bg-transparent text-white hover:bg-white/5",
|
|
90
|
-
)}
|
|
91
|
-
>
|
|
92
|
-
<span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
|
|
93
|
-
{view.icon}
|
|
94
|
-
</span>
|
|
95
|
-
{view.label}
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
);
|
|
99
|
-
})}
|
|
100
|
-
</div>
|
|
70
|
+
<TabSwitch
|
|
71
|
+
// The header bar is always dark, so the switcher resolves dark-theme
|
|
72
|
+
// tokens regardless of the host app's theme.
|
|
73
|
+
theme="dark"
|
|
74
|
+
value={currentView}
|
|
75
|
+
onValueChange={onViewChange}
|
|
76
|
+
options={views.map((view) => ({
|
|
77
|
+
value: view.id,
|
|
78
|
+
label: view.label,
|
|
79
|
+
icon: view.icon,
|
|
80
|
+
}))}
|
|
81
|
+
/>
|
|
101
82
|
</div>
|
|
102
83
|
|
|
103
84
|
{/* Action bar */}
|
|
@@ -30,9 +30,11 @@ import {
|
|
|
30
30
|
resolvePresets,
|
|
31
31
|
} from "../../utils/dataViews/rangeUtils"
|
|
32
32
|
import { RangeSliderWithInputs } from "./filters/RangeSliderWithInputs"
|
|
33
|
-
import {
|
|
33
|
+
import { DatePickerRangeFilter } from "./filters/DatePickerRangeFilter"
|
|
34
34
|
import { PresetChips } from "./filters/PresetChips"
|
|
35
35
|
import { resolveBadgeVariant } from "./badgeAdapter"
|
|
36
|
+
import { SearchableSelect } from "../SearchableSelect"
|
|
37
|
+
import type { SearchableSelectOption } from "../SearchableSelect"
|
|
36
38
|
|
|
37
39
|
type FilterPanelProps = {
|
|
38
40
|
data: DynamicRecord[]
|
|
@@ -346,21 +348,36 @@ function FilterBody({
|
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
if (entry.kind === "date-range" && entry.field) {
|
|
349
|
-
const presets = resolvePresets(entry.field)
|
|
350
351
|
const dateValue: DateRangeFilter | undefined = isDateRange(value) ? value : undefined
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
onChange={onSetFilter}
|
|
355
|
-
presets={presets.length > 0 ? presets : undefined}
|
|
356
|
-
/>
|
|
357
|
-
)
|
|
352
|
+
// Glare DatePicker in range mode — pick a from→to span. (No quick-preset
|
|
353
|
+
// chips: the calendar range selection is the single intended interaction.)
|
|
354
|
+
return <DatePickerRangeFilter value={dateValue} onChange={onSetFilter} />
|
|
358
355
|
}
|
|
359
356
|
|
|
360
357
|
const opts = getCategoricalOptions(data, entry.path, entry.field, entry.legacy)
|
|
361
358
|
const selected = Array.isArray(value) ? value : []
|
|
362
359
|
const isSingle = entry.field?.filterMode === "single"
|
|
363
360
|
|
|
361
|
+
// Searchable single-select dropdown — for fields with many options. Stores
|
|
362
|
+
// the chosen value as a 1-element array (empty when cleared) to stay
|
|
363
|
+
// consistent with the categorical FilterValue shape.
|
|
364
|
+
if (entry.field?.filterVariant === "searchable-select") {
|
|
365
|
+
const selectOptions: SearchableSelectOption[] = opts.map((opt) => ({
|
|
366
|
+
value: opt,
|
|
367
|
+
label: opt,
|
|
368
|
+
}))
|
|
369
|
+
const current = selected[0] ?? null
|
|
370
|
+
return (
|
|
371
|
+
<SearchableSelect
|
|
372
|
+
options={selectOptions}
|
|
373
|
+
value={current}
|
|
374
|
+
onValueChange={(v) => onSetFilter(v ? [v] : [])}
|
|
375
|
+
placeholder={`Select ${entry.label}…`}
|
|
376
|
+
icon={<i className="ri-search-line" />}
|
|
377
|
+
/>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
364
381
|
if (isSingle) {
|
|
365
382
|
const current = selected[0] ?? ""
|
|
366
383
|
const onSingleChange = (next: string) => onSetFilter(next ? [next] : [])
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { type ChangeEventHandler } from "react"
|
|
4
|
+
import { DatePicker } from "../../DatePicker"
|
|
5
|
+
import { InputField } from "../../InputField"
|
|
6
|
+
import { ActionButton } from "../../ActionButton"
|
|
7
|
+
import type { DateRangeFilter } from "../types"
|
|
8
|
+
import { toIsoDate } from "../../../utils/dataViews/rangeUtils"
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
value: DateRangeFilter | undefined
|
|
12
|
+
onChange: (next: DateRangeFilter) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse an ISO `yyyy-MM-dd` string to a local Date (noon-safe, no TZ drift). */
|
|
16
|
+
function parseIso(iso?: string): Date | undefined {
|
|
17
|
+
return iso ? new Date(iso + "T00:00:00") : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One labeled single-date Glare DatePicker bound to one end of the range. */
|
|
21
|
+
function DateBound({
|
|
22
|
+
caption,
|
|
23
|
+
iso,
|
|
24
|
+
placeholder,
|
|
25
|
+
onPick,
|
|
26
|
+
}: {
|
|
27
|
+
caption: string
|
|
28
|
+
iso?: string
|
|
29
|
+
placeholder: string
|
|
30
|
+
onPick: (next?: string) => void
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<label className="flex items-center gap-2">
|
|
34
|
+
<span className="w-9 shrink-0 typography-body-small-regular text-content-presentation-global-secondary">
|
|
35
|
+
{caption}
|
|
36
|
+
</span>
|
|
37
|
+
<div className="flex-1 min-w-0">
|
|
38
|
+
<DatePicker
|
|
39
|
+
mode="single"
|
|
40
|
+
size="S"
|
|
41
|
+
value={parseIso(iso) ?? (undefined as never)}
|
|
42
|
+
dateFormat="yyyy-MM-dd"
|
|
43
|
+
// Single mode reports `{ target: { value: Date } }` (it casts internally).
|
|
44
|
+
onChange={((e: { target: { value: Date | undefined } }) => {
|
|
45
|
+
const d = e?.target?.value
|
|
46
|
+
onPick(d instanceof Date ? toIsoDate(d) : undefined)
|
|
47
|
+
}) as unknown as ChangeEventHandler<HTMLInputElement>}
|
|
48
|
+
>
|
|
49
|
+
<InputField
|
|
50
|
+
readOnly
|
|
51
|
+
value={iso ?? ""}
|
|
52
|
+
placeholder={placeholder}
|
|
53
|
+
childrenSide={
|
|
54
|
+
<ActionButton type="button" size="S" aria-label={`Pick ${caption} date`}>
|
|
55
|
+
<i className="ri-calendar-event-line" />
|
|
56
|
+
</ActionButton>
|
|
57
|
+
}
|
|
58
|
+
/>
|
|
59
|
+
</DatePicker>
|
|
60
|
+
</div>
|
|
61
|
+
</label>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Date-range filter as two separate Glare DatePickers — a "From" and a "To"
|
|
67
|
+
* single-date picker. Each writes only its own bound, preserving the other, so
|
|
68
|
+
* the result is still a `DateRangeFilter` ({ from, to }).
|
|
69
|
+
*/
|
|
70
|
+
export function DatePickerRangeFilter({ value, onChange }: Props) {
|
|
71
|
+
const setBound = (key: "from" | "to") => (next?: string) =>
|
|
72
|
+
onChange({ kind: "date", from: value?.from, to: value?.to, [key]: next })
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<DateBound caption="From" iso={value?.from} placeholder="Start date" onPick={setBound("from")} />
|
|
77
|
+
<DateBound caption="To" iso={value?.to} placeholder="End date" onPick={setBound("to")} />
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -160,6 +160,14 @@ export type FieldConfig = {
|
|
|
160
160
|
* - "single": radios, single-select. FilterValue is a 1-element array.
|
|
161
161
|
*/
|
|
162
162
|
filterMode?: "single" | "multi"
|
|
163
|
+
/**
|
|
164
|
+
* Categorical filter control style.
|
|
165
|
+
* - "checkbox" (default): inline list of checkboxes (multi) or radios (single).
|
|
166
|
+
* - "searchable-select": a single-select SearchableSelect dropdown — useful
|
|
167
|
+
* when a field has many options. Implies single-select; the FilterValue is
|
|
168
|
+
* a 1-element array (or empty when cleared).
|
|
169
|
+
*/
|
|
170
|
+
filterVariant?: "checkbox" | "searchable-select"
|
|
163
171
|
presets?: FieldPreset[]
|
|
164
172
|
rangeMin?: number
|
|
165
173
|
rangeMax?: number
|
|
@@ -63,7 +63,12 @@ export const DatePicker = forwardRef(
|
|
|
63
63
|
? value
|
|
64
64
|
: [value]
|
|
65
65
|
: mode == "range"
|
|
66
|
-
|
|
66
|
+
// Accept a DateRange ({ from, to }) directly, or wrap a single
|
|
67
|
+
// Date into a one-day range.
|
|
68
|
+
? value && typeof value === "object" && !(value instanceof Date) &&
|
|
69
|
+
("from" in value || "to" in value)
|
|
70
|
+
? (value as DateRange)
|
|
71
|
+
: { from: value, to: value }
|
|
67
72
|
: value;
|
|
68
73
|
const [date, setDate] = useState<Date[] | Date | DateRange | undefined>(initialDate);
|
|
69
74
|
const [pickerValue, setPickerValue] = useState<TimePickerValue>({
|
|
@@ -50,7 +50,7 @@ DropdownMenuRadioGroup.displayName =
|
|
|
50
50
|
const DropdownMenuContent = React.forwardRef<
|
|
51
51
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
52
52
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> &
|
|
53
|
-
DropdownMenuProps & { autoGroup?: boolean }
|
|
53
|
+
DropdownMenuProps & { autoGroup?: boolean; maxHeight?: number }
|
|
54
54
|
>(
|
|
55
55
|
(
|
|
56
56
|
{
|
|
@@ -60,6 +60,8 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
60
60
|
variant = "PresentationStyle",
|
|
61
61
|
autoGroup = true,
|
|
62
62
|
collisionPadding = 8,
|
|
63
|
+
maxHeight = 320,
|
|
64
|
+
style,
|
|
63
65
|
children,
|
|
64
66
|
...props
|
|
65
67
|
},
|
|
@@ -71,13 +73,13 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
71
73
|
ref={ref}
|
|
72
74
|
sideOffset={sideOffset}
|
|
73
75
|
collisionPadding={collisionPadding}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)}
|
|
76
|
+
// Cap at maxHeight, but never exceed the space Radix has after collision
|
|
77
|
+
// handling. The menu scrolls (overflow on the surface) past this height.
|
|
78
|
+
style={{
|
|
79
|
+
maxHeight: `min(${maxHeight}px, var(--radix-dropdown-menu-content-available-height))`,
|
|
80
|
+
...style,
|
|
81
|
+
}}
|
|
82
|
+
className={cn(menuContentStyles({ variant }), className)}
|
|
81
83
|
{...props}
|
|
82
84
|
>
|
|
83
85
|
{autoGroup ? autoGroupChildren(children) : children}
|
|
@@ -398,6 +400,7 @@ export const MenuItemStyles = cva(
|
|
|
398
400
|
"border",
|
|
399
401
|
"border-transparent",
|
|
400
402
|
"flex",
|
|
403
|
+
"shrink-0", // keep full row height so the menu scrolls instead of squishing
|
|
401
404
|
"items-center",
|
|
402
405
|
"justify-start",
|
|
403
406
|
"text-overflow",
|
|
@@ -493,13 +496,13 @@ export const menuContentStyles = cva(
|
|
|
493
496
|
"rounded-[14px]",
|
|
494
497
|
"min-w-[240px]",
|
|
495
498
|
"outline-none",
|
|
496
|
-
"overflow-
|
|
499
|
+
"overflow-y-auto",
|
|
500
|
+
"overflow-x-hidden",
|
|
497
501
|
// Only animate the OPEN (enter) state. An exit animation on [data-state=closed]
|
|
498
502
|
// holds the old DOM node during close, which breaks the context menu's
|
|
499
503
|
// close/reposition on a second right-click (Radix issue #2572).
|
|
500
504
|
"data-[state=open]:animate-in",
|
|
501
505
|
"data-[state=open]:fade-in-0",
|
|
502
|
-
"overflow-x-hidden",
|
|
503
506
|
"scrollbar-hide",
|
|
504
507
|
"backdrop-blur-[21px]",
|
|
505
508
|
"flex gap-1 flex-col",
|
|
@@ -519,7 +522,7 @@ export const menuContentStyles = cva(
|
|
|
519
522
|
}
|
|
520
523
|
);
|
|
521
524
|
|
|
522
|
-
export const menuGroupStyles = cva(["flex", "flex-col"], {
|
|
525
|
+
export const menuGroupStyles = cva(["flex", "flex-col", "shrink-0"], {
|
|
523
526
|
variants: {
|
|
524
527
|
variant: {
|
|
525
528
|
// Visually contains its items in a bordered card.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../utils/cn";
|
|
4
|
+
import { Themes } from "../utils/types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HeaderBar
|
|
8
|
+
*
|
|
9
|
+
* A variant-driven header chip showing two text pieces inside a single
|
|
10
|
+
* rounded container:
|
|
11
|
+
* - `label` -> the colored emphasis pill (BADGE piece)
|
|
12
|
+
* - `title` -> the plain text (PLAIN piece)
|
|
13
|
+
*
|
|
14
|
+
* The `variant` controls BOTH the colors AND the DOM order:
|
|
15
|
+
* - "new" / "edit" -> [badge(label)] [plain(title)] (badge on the LEFT)
|
|
16
|
+
* - "detail" -> [plain(title)] [badge(label)] (badge on the RIGHT)
|
|
17
|
+
*
|
|
18
|
+
* Example usage:
|
|
19
|
+
* - new: <HeaderBar variant="new" label="New" title="sales iNVOICE" />
|
|
20
|
+
* - edit: <HeaderBar variant="edit" label="edit" title="sales iNVOICE" />
|
|
21
|
+
* - detail: <HeaderBar variant="detail" label="de-344" title="sales iNVOICE" />
|
|
22
|
+
* (renders plain "sales iNVOICE" on the LEFT, colored "de-344" on the RIGHT)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Inner row: order is reversed for "detail" so the badge ends up on the right.
|
|
26
|
+
const rowStyles = cva(["flex", "items-center"], {
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
new: "flex-row",
|
|
30
|
+
edit: "flex-row",
|
|
31
|
+
detail: "flex-row-reverse",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "new",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Colored pill holding `label`.
|
|
40
|
+
const badgeStyles = cva(
|
|
41
|
+
[
|
|
42
|
+
"flex",
|
|
43
|
+
"h-8",
|
|
44
|
+
"items-center",
|
|
45
|
+
"justify-center",
|
|
46
|
+
"gap-2.5",
|
|
47
|
+
"rounded-lg",
|
|
48
|
+
"px-1",
|
|
49
|
+
],
|
|
50
|
+
{
|
|
51
|
+
variants: {
|
|
52
|
+
variant: {
|
|
53
|
+
new: "bg-blue-sparkle-alpha-50",
|
|
54
|
+
edit: "bg-orange-alpha-50",
|
|
55
|
+
detail: "bg-white-alpha-30",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultVariants: {
|
|
59
|
+
variant: "new",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Text inside the colored pill.
|
|
65
|
+
const badgeTextStyles = cva(
|
|
66
|
+
[
|
|
67
|
+
"font-sans",
|
|
68
|
+
"text-[28px]",
|
|
69
|
+
"font-[510]",
|
|
70
|
+
"leading-normal",
|
|
71
|
+
"uppercase",
|
|
72
|
+
"[font-feature-settings:'cv05'_on]",
|
|
73
|
+
],
|
|
74
|
+
{
|
|
75
|
+
variants: {
|
|
76
|
+
variant: {
|
|
77
|
+
new: "text-blue-sparkle-200",
|
|
78
|
+
edit: "text-orange-200",
|
|
79
|
+
detail: "text-white-00",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultVariants: {
|
|
83
|
+
variant: "new",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
interface HeaderBarProps
|
|
89
|
+
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof rowStyles> {
|
|
90
|
+
theme?: Themes;
|
|
91
|
+
/** The colored emphasis pill text. */
|
|
92
|
+
label: string;
|
|
93
|
+
/** The plain text. */
|
|
94
|
+
title: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const HeaderBar = forwardRef<HTMLDivElement, HeaderBarProps>(
|
|
98
|
+
({ variant = "new", label, title, theme, className, ...props }, ref) => {
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
ref={ref}
|
|
102
|
+
data-theme={theme}
|
|
103
|
+
className={cn(
|
|
104
|
+
"inline-flex flex-col items-start rounded-[14px] border border-black-600 bg-black-1000 p-1.5 shadow-[0_0_32px_2px_rgba(0,0,0,0.05),0_0_32px_2px_rgba(0,0,0,0.05)]",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<div className={cn(rowStyles({ variant }))}>
|
|
110
|
+
<div className={cn(badgeStyles({ variant }))}>
|
|
111
|
+
<p className={cn(badgeTextStyles({ variant }))}>{label}</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex h-8 items-center justify-center px-1.5">
|
|
114
|
+
<p className="font-sans text-[28px] font-[510] leading-normal uppercase text-white-00 [font-feature-settings:'cv05'_on]">
|
|
115
|
+
{title}
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
HeaderBar.displayName = "HeaderBar";
|
|
125
|
+
|
|
126
|
+
export default HeaderBar;
|
|
127
|
+
export { HeaderBar };
|