@lovett/ui 0.0.1
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/README.md +28 -0
- package/package.json +53 -0
- package/src/accordion.tsx +153 -0
- package/src/atoms.tsx +197 -0
- package/src/badge.tsx +91 -0
- package/src/brand-logo-tile.tsx +200 -0
- package/src/bulk-action-bar.tsx +104 -0
- package/src/button-group.tsx +46 -0
- package/src/button.tsx +93 -0
- package/src/calculator-shell-v2.tsx +380 -0
- package/src/calculator-shell.tsx +310 -0
- package/src/card.tsx +369 -0
- package/src/checkbox.tsx +114 -0
- package/src/chip-nav.tsx +347 -0
- package/src/choropleth.tsx +62 -0
- package/src/code-block.tsx +235 -0
- package/src/completion-ring.tsx +119 -0
- package/src/copy-field.tsx +169 -0
- package/src/data-grid/column-options-menu.tsx +127 -0
- package/src/data-grid/data-grid.tsx +391 -0
- package/src/data-grid/drawer-panel.tsx +146 -0
- package/src/data-grid/index.ts +29 -0
- package/src/data-grid/sortable-parts.tsx +215 -0
- package/src/data-grid/table-body.tsx +238 -0
- package/src/data-grid/table-elements.tsx +135 -0
- package/src/data-grid/table-header.tsx +95 -0
- package/src/data-grid/table-overlays.tsx +78 -0
- package/src/data-grid/table-parts.tsx +15 -0
- package/src/data-grid/table-summary-footer.tsx +101 -0
- package/src/data-grid/table-view.tsx +176 -0
- package/src/data-grid/types.ts +141 -0
- package/src/data-grid/use-grid-columns.ts +311 -0
- package/src/data-grid/use-grid-editing.ts +154 -0
- package/src/device-frame.tsx +274 -0
- package/src/dropdown-button.tsx +221 -0
- package/src/dropdown-menu.tsx +450 -0
- package/src/empty-state.tsx +85 -0
- package/src/file-upload/file-drop-zone.tsx +156 -0
- package/src/file-upload/file-preview-grid.tsx +39 -0
- package/src/file-upload/file-preview-item.tsx +175 -0
- package/src/file-upload/file-thumbnail.tsx +98 -0
- package/src/file-upload/file-upload-button.tsx +95 -0
- package/src/file-upload/index.ts +13 -0
- package/src/file-upload/use-file-upload.ts +371 -0
- package/src/filter-bar.tsx +125 -0
- package/src/filter-dropdown.tsx +289 -0
- package/src/floating-drawer.tsx +420 -0
- package/src/folder-card.tsx +188 -0
- package/src/folder-tree-picker.tsx +511 -0
- package/src/format.ts +117 -0
- package/src/frame-stack.tsx +73 -0
- package/src/hero-form-card.tsx +471 -0
- package/src/index.ts +372 -0
- package/src/input.tsx +130 -0
- package/src/kbd.tsx +41 -0
- package/src/lib/layout.ts +22 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown.tsx +214 -0
- package/src/menu-button.tsx +142 -0
- package/src/meta-previews/CreativeMedia.tsx +119 -0
- package/src/meta-previews/InstagramExplorePreview.tsx +340 -0
- package/src/meta-previews/InstagramFeedCarousel.tsx +245 -0
- package/src/meta-previews/InstagramFeedPreview.tsx +259 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +228 -0
- package/src/meta-previews/MetaFeedPreview.tsx +294 -0
- package/src/meta-previews/MetaInstreamPreview.tsx +135 -0
- package/src/meta-previews/MetaMessengerPreview.tsx +795 -0
- package/src/meta-previews/MetaReelPreview.tsx +750 -0
- package/src/meta-previews/MetaRightColumnPreview.tsx +245 -0
- package/src/meta-previews/MetaSearchPreview.tsx +113 -0
- package/src/meta-previews/MetaStoryPreview.tsx +1132 -0
- package/src/meta-previews/ProfileAvatar.tsx +79 -0
- package/src/meta-previews/defaults.ts +27 -0
- package/src/meta-previews/index.ts +37 -0
- package/src/meta-previews/types.ts +219 -0
- package/src/metric-card.tsx +117 -0
- package/src/modal.tsx +131 -0
- package/src/option-tile.tsx +252 -0
- package/src/page-hero.tsx +78 -0
- package/src/page-shell.tsx +440 -0
- package/src/pagination.tsx +86 -0
- package/src/pill-button.tsx +103 -0
- package/src/profile-section.tsx +389 -0
- package/src/progress-bar.tsx +84 -0
- package/src/radio.tsx +172 -0
- package/src/range-slider.tsx +161 -0
- package/src/resizable.tsx +282 -0
- package/src/search-bar.tsx +81 -0
- package/src/segmented-pill.tsx +143 -0
- package/src/shell.tsx +464 -0
- package/src/slider.tsx +107 -0
- package/src/sortable-table.tsx +198 -0
- package/src/sortable.tsx +137 -0
- package/src/stat-row.tsx +132 -0
- package/src/stats-grid.tsx +38 -0
- package/src/step-loader.tsx +141 -0
- package/src/styles.css +562 -0
- package/src/tabs.tsx +143 -0
- package/src/tag-chip-input.tsx +394 -0
- package/src/textarea.tsx +39 -0
- package/src/toast.tsx +59 -0
- package/src/tokens.css +579 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompletionRing — small SVG ring showing a 0–100 percentage.
|
|
3
|
+
*
|
|
4
|
+
* Used wherever the app wants to communicate "X% of this thing is
|
|
5
|
+
* done" in a compact, glanceable widget — currently on the Brand
|
|
6
|
+
* Profile rail and (queued) on the dashboard onboarding card.
|
|
7
|
+
*
|
|
8
|
+
* Sizing is configurable so the same primitive can render at 30px
|
|
9
|
+
* (rail) and 38–48px (dashboard) without duplicating the stroke maths.
|
|
10
|
+
*
|
|
11
|
+
* <CompletionRing percent={80} />
|
|
12
|
+
* <CompletionRing percent={pct} size={38} strokeWidth={3} />
|
|
13
|
+
* <CompletionRing percent={pct} showLabel={false} />
|
|
14
|
+
* <CompletionRing percent={pct} label={<Check className="h-3 w-3" />} />
|
|
15
|
+
*
|
|
16
|
+
* SVG is the right tool for ring geometry — the "no inline SVG" guidance
|
|
17
|
+
* applies to icons (use Lucide), not to chart primitives like this one.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
21
|
+
import { cn } from './lib/utils'
|
|
22
|
+
|
|
23
|
+
export interface CompletionRingProps {
|
|
24
|
+
/** 0–100. Values outside the range are clamped. */
|
|
25
|
+
percent: number
|
|
26
|
+
/** Outer dimension in px. Default 30. */
|
|
27
|
+
size?: number
|
|
28
|
+
/** Ring stroke width in px. Default scales with size (size / 12). */
|
|
29
|
+
strokeWidth?: number
|
|
30
|
+
/**
|
|
31
|
+
* What to render in the middle. Defaults to the percent number with
|
|
32
|
+
* a `%` suffix. Pass `false`/`null` to hide. Pass a node (icon, text)
|
|
33
|
+
* to fully override.
|
|
34
|
+
*/
|
|
35
|
+
label?: ReactNode | false
|
|
36
|
+
/** Convenience: false hides the default label. Same as `label={false}`. */
|
|
37
|
+
showLabel?: boolean
|
|
38
|
+
/** Track colour CSS value. Defaults to `--surface-overlay-strong`. */
|
|
39
|
+
trackColor?: string
|
|
40
|
+
/** Fill (progress arc) colour. Defaults to `--accent`. */
|
|
41
|
+
fillColor?: string
|
|
42
|
+
/** Extra classes / inline style on the wrapper. */
|
|
43
|
+
className?: string
|
|
44
|
+
style?: CSSProperties
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function CompletionRing({
|
|
48
|
+
percent,
|
|
49
|
+
size = 30,
|
|
50
|
+
strokeWidth,
|
|
51
|
+
label,
|
|
52
|
+
showLabel = true,
|
|
53
|
+
trackColor = 'rgb(var(--surface-overlay-strong))',
|
|
54
|
+
fillColor = 'rgb(var(--accent))',
|
|
55
|
+
className,
|
|
56
|
+
style,
|
|
57
|
+
}: CompletionRingProps) {
|
|
58
|
+
const clamped = Math.max(0, Math.min(100, percent))
|
|
59
|
+
const sw = strokeWidth ?? Math.max(2, Math.round(size / 12))
|
|
60
|
+
const r = (size - sw) / 2
|
|
61
|
+
const cx = size / 2
|
|
62
|
+
const cy = size / 2
|
|
63
|
+
const circ = 2 * Math.PI * r
|
|
64
|
+
const dashoff = circ * (1 - clamped / 100)
|
|
65
|
+
|
|
66
|
+
// `label` (when provided) wins over `showLabel`. The default renders
|
|
67
|
+
// the percentage in tabular-nums at a size that fits comfortably in
|
|
68
|
+
// the ring at any sane diameter.
|
|
69
|
+
const explicitLabel = label === undefined ? undefined : label
|
|
70
|
+
const renderedLabel =
|
|
71
|
+
explicitLabel !== undefined
|
|
72
|
+
? explicitLabel || null
|
|
73
|
+
: showLabel
|
|
74
|
+
? `${Math.round(clamped)}%`
|
|
75
|
+
: null
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={cn('relative shrink-0', className)}
|
|
80
|
+
style={{ width: size, height: size, ...style }}
|
|
81
|
+
>
|
|
82
|
+
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
83
|
+
<circle
|
|
84
|
+
cx={cx}
|
|
85
|
+
cy={cy}
|
|
86
|
+
r={r}
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke={trackColor}
|
|
89
|
+
strokeWidth={sw}
|
|
90
|
+
/>
|
|
91
|
+
<circle
|
|
92
|
+
cx={cx}
|
|
93
|
+
cy={cy}
|
|
94
|
+
r={r}
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke={fillColor}
|
|
97
|
+
strokeWidth={sw}
|
|
98
|
+
strokeLinecap="round"
|
|
99
|
+
strokeDasharray={circ}
|
|
100
|
+
strokeDashoffset={dashoff}
|
|
101
|
+
transform={`rotate(-90 ${cx} ${cy})`}
|
|
102
|
+
style={{ transition: 'stroke-dashoffset var(--dur-base) var(--ease-out)' }}
|
|
103
|
+
/>
|
|
104
|
+
</svg>
|
|
105
|
+
{renderedLabel !== null && (
|
|
106
|
+
<div
|
|
107
|
+
className="absolute inset-0 flex items-center justify-center font-bold tabular-nums leading-none"
|
|
108
|
+
style={{
|
|
109
|
+
color: 'rgb(var(--foreground))',
|
|
110
|
+
// Auto-size the label to the ring so 30px and 48px both look right.
|
|
111
|
+
fontSize: Math.max(8, Math.round(size * 0.3)),
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{renderedLabel}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CopyField — an input/output field with a copy button pinned to the
|
|
3
|
+
* top-right corner.
|
|
4
|
+
*
|
|
5
|
+
* The workbench input/output pattern from the tool prototypes: a single
|
|
6
|
+
* field a user types into and that a transform writes its result back
|
|
7
|
+
* into ("output replaces input"), with a one-click copy affordance
|
|
8
|
+
* always in reach.
|
|
9
|
+
*
|
|
10
|
+
* Wraps <Textarea> (`multiline`, the default) or <Input>. Owns the copy
|
|
11
|
+
* interaction end-to-end — clipboard write, a transient ✓ confirmation,
|
|
12
|
+
* and a success/error toast. The field content is right-padded so text
|
|
13
|
+
* never slides under the button.
|
|
14
|
+
*
|
|
15
|
+
* CopyField does NOT render a char/line count — that is caller state;
|
|
16
|
+
* render it in the surrounding footer.
|
|
17
|
+
*
|
|
18
|
+
* Workspace ADR: ADR-031 Decision 2.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
useId,
|
|
23
|
+
useState,
|
|
24
|
+
type ChangeEvent,
|
|
25
|
+
type CSSProperties,
|
|
26
|
+
} from 'react'
|
|
27
|
+
import { Check, Copy } from 'lucide-react'
|
|
28
|
+
import { Textarea } from './textarea'
|
|
29
|
+
import { Input } from './input'
|
|
30
|
+
import { toast } from './toast'
|
|
31
|
+
import { cn } from './lib/utils'
|
|
32
|
+
|
|
33
|
+
export interface CopyFieldProps {
|
|
34
|
+
/** Field value (controlled). */
|
|
35
|
+
value: string
|
|
36
|
+
/** Change handler. Omit for a read-only output field. */
|
|
37
|
+
onChange?: (value: string) => void
|
|
38
|
+
/** Multi-line textarea (default) or single-line input. */
|
|
39
|
+
multiline?: boolean
|
|
40
|
+
/** Textarea row count. Ignored when `multiline` is false. */
|
|
41
|
+
rows?: number
|
|
42
|
+
placeholder?: string
|
|
43
|
+
readOnly?: boolean
|
|
44
|
+
/** Visible field label — wires `<label htmlFor>`. */
|
|
45
|
+
label?: string
|
|
46
|
+
/** Keep the label for assistive tech but hide it visually. */
|
|
47
|
+
hideLabel?: boolean
|
|
48
|
+
/** Field id. Auto-generated when omitted. */
|
|
49
|
+
id?: string
|
|
50
|
+
/** Toast message on a successful copy. */
|
|
51
|
+
copyToastMessage?: string
|
|
52
|
+
className?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function CopyField({
|
|
56
|
+
value,
|
|
57
|
+
onChange,
|
|
58
|
+
multiline = true,
|
|
59
|
+
rows = 10,
|
|
60
|
+
placeholder,
|
|
61
|
+
readOnly,
|
|
62
|
+
label,
|
|
63
|
+
hideLabel,
|
|
64
|
+
id,
|
|
65
|
+
copyToastMessage = 'Copied to clipboard',
|
|
66
|
+
className,
|
|
67
|
+
}: CopyFieldProps) {
|
|
68
|
+
const reactId = useId()
|
|
69
|
+
const fieldId = id ?? reactId
|
|
70
|
+
const [copied, setCopied] = useState(false)
|
|
71
|
+
|
|
72
|
+
const handleCopy = async () => {
|
|
73
|
+
if (value === '') return
|
|
74
|
+
let ok = false
|
|
75
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
76
|
+
try {
|
|
77
|
+
await navigator.clipboard.writeText(value)
|
|
78
|
+
ok = true
|
|
79
|
+
} catch {
|
|
80
|
+
ok = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (ok) {
|
|
84
|
+
setCopied(true)
|
|
85
|
+
setTimeout(() => setCopied(false), 1500)
|
|
86
|
+
toast.success(copyToastMessage)
|
|
87
|
+
} else {
|
|
88
|
+
toast.error('Clipboard unavailable')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Reserve room on the right so text never slides under the button.
|
|
93
|
+
const fieldStyle: CSSProperties = { paddingRight: 44 }
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={cn('flex flex-col gap-1.5', className)}>
|
|
97
|
+
{label && (
|
|
98
|
+
<label
|
|
99
|
+
htmlFor={fieldId}
|
|
100
|
+
className={cn(
|
|
101
|
+
'text-[12px] font-semibold tracking-[0.02em]',
|
|
102
|
+
hideLabel && 'sr-only',
|
|
103
|
+
)}
|
|
104
|
+
style={{ color: 'rgb(var(--foreground))' }}
|
|
105
|
+
>
|
|
106
|
+
{label}
|
|
107
|
+
</label>
|
|
108
|
+
)}
|
|
109
|
+
<div className="relative">
|
|
110
|
+
{multiline ? (
|
|
111
|
+
<Textarea
|
|
112
|
+
id={fieldId}
|
|
113
|
+
value={value}
|
|
114
|
+
{...(onChange
|
|
115
|
+
? {
|
|
116
|
+
onChange: (e: ChangeEvent<HTMLTextAreaElement>) =>
|
|
117
|
+
onChange(e.target.value),
|
|
118
|
+
}
|
|
119
|
+
: {})}
|
|
120
|
+
placeholder={placeholder}
|
|
121
|
+
readOnly={readOnly}
|
|
122
|
+
rows={rows}
|
|
123
|
+
style={fieldStyle}
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<Input
|
|
127
|
+
id={fieldId}
|
|
128
|
+
value={value}
|
|
129
|
+
{...(onChange
|
|
130
|
+
? {
|
|
131
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) =>
|
|
132
|
+
onChange(e.target.value),
|
|
133
|
+
}
|
|
134
|
+
: {})}
|
|
135
|
+
placeholder={placeholder}
|
|
136
|
+
readOnly={readOnly}
|
|
137
|
+
style={fieldStyle}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={handleCopy}
|
|
143
|
+
disabled={value === ''}
|
|
144
|
+
aria-label={copied ? 'Copied' : 'Copy'}
|
|
145
|
+
className="absolute inline-flex items-center justify-center transition-colors disabled:opacity-40 disabled:pointer-events-none"
|
|
146
|
+
style={{
|
|
147
|
+
top: 8,
|
|
148
|
+
right: 8,
|
|
149
|
+
height: 28,
|
|
150
|
+
width: 28,
|
|
151
|
+
borderRadius: 'var(--radius-xs)',
|
|
152
|
+
background: 'rgb(var(--bg-card))',
|
|
153
|
+
border: '1px solid rgb(var(--border))',
|
|
154
|
+
boxShadow: 'var(--shadow-sm)',
|
|
155
|
+
color: copied
|
|
156
|
+
? 'rgb(var(--success))'
|
|
157
|
+
: 'rgb(var(--text-secondary))',
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{copied ? (
|
|
161
|
+
<Check className="h-3.5 w-3.5" />
|
|
162
|
+
) : (
|
|
163
|
+
<Copy className="h-3.5 w-3.5" />
|
|
164
|
+
)}
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataGridColumnOptionsMenu — toolbar dropdown for showing summary
|
|
3
|
+
* row + toggling column visibility + reordering columns via dnd-kit.
|
|
4
|
+
*
|
|
5
|
+
* Ported 2026-05-18 from gray-ui-csm
|
|
6
|
+
* https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/column-options-menu.tsx
|
|
7
|
+
* License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
|
|
8
|
+
* Workspace ADR: ADR-025
|
|
9
|
+
*
|
|
10
|
+
* BRING_WITH_REFACTOR:
|
|
11
|
+
* • Token reskin per ADR-025 D7
|
|
12
|
+
* • @tabler/icons-react → lucide-react per CLAUDE.md §6 (DEV-4)
|
|
13
|
+
* • DropdownMenu* imports come from the workspace BUILD_FRESH
|
|
14
|
+
* primitive (DEV-3). Trigger inherits the secondary Button visual
|
|
15
|
+
* per ADR-026 D4 (DQ-4) — by default the workspace
|
|
16
|
+
* DropdownMenuTrigger applies `.btn .btn-secondary`, so we just
|
|
17
|
+
* spread the icon + label inside it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { closestCenter, DndContext } from '@dnd-kit/core'
|
|
21
|
+
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
|
22
|
+
import { SlidersHorizontal } from 'lucide-react'
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
DropdownMenu,
|
|
26
|
+
DropdownMenuCheckboxItem,
|
|
27
|
+
DropdownMenuContent,
|
|
28
|
+
DropdownMenuGroup,
|
|
29
|
+
DropdownMenuLabel,
|
|
30
|
+
DropdownMenuSeparator,
|
|
31
|
+
DropdownMenuTrigger,
|
|
32
|
+
} from '../dropdown-menu'
|
|
33
|
+
import { SortableColumnOptionItem } from './sortable-parts'
|
|
34
|
+
import type {
|
|
35
|
+
DataGridColumn,
|
|
36
|
+
DataGridToolbarRenderProps,
|
|
37
|
+
} from './types'
|
|
38
|
+
|
|
39
|
+
type DataGridColumnOptionsMenuProps<ColumnId extends string> = Pick<
|
|
40
|
+
DataGridToolbarRenderProps<ColumnId>,
|
|
41
|
+
| 'showSummaries'
|
|
42
|
+
| 'onShowSummariesChange'
|
|
43
|
+
| 'visibleColumns'
|
|
44
|
+
| 'visibleColumnIds'
|
|
45
|
+
| 'hiddenColumns'
|
|
46
|
+
| 'toggleColumnVisibility'
|
|
47
|
+
| 'optionsSensors'
|
|
48
|
+
| 'onOptionColumnDragEnd'
|
|
49
|
+
| 'optionsDndContextId'
|
|
50
|
+
> & {
|
|
51
|
+
triggerLabel?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function DataGridColumnOptionsMenu<ColumnId extends string>({
|
|
55
|
+
showSummaries,
|
|
56
|
+
onShowSummariesChange,
|
|
57
|
+
visibleColumns,
|
|
58
|
+
visibleColumnIds,
|
|
59
|
+
hiddenColumns,
|
|
60
|
+
toggleColumnVisibility,
|
|
61
|
+
optionsSensors,
|
|
62
|
+
onOptionColumnDragEnd,
|
|
63
|
+
optionsDndContextId,
|
|
64
|
+
triggerLabel = 'Options',
|
|
65
|
+
}: DataGridColumnOptionsMenuProps<ColumnId>) {
|
|
66
|
+
return (
|
|
67
|
+
<DropdownMenu>
|
|
68
|
+
<DropdownMenuTrigger aria-label="Open table options">
|
|
69
|
+
<SlidersHorizontal className="h-4 w-4" />
|
|
70
|
+
<span>{triggerLabel}</span>
|
|
71
|
+
</DropdownMenuTrigger>
|
|
72
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
73
|
+
<DropdownMenuGroup>
|
|
74
|
+
<DropdownMenuLabel>Options</DropdownMenuLabel>
|
|
75
|
+
</DropdownMenuGroup>
|
|
76
|
+
<DropdownMenuSeparator />
|
|
77
|
+
<DropdownMenuCheckboxItem
|
|
78
|
+
checked={showSummaries}
|
|
79
|
+
onCheckedChange={(checked) => onShowSummariesChange(checked === true)}
|
|
80
|
+
>
|
|
81
|
+
Show summary row
|
|
82
|
+
</DropdownMenuCheckboxItem>
|
|
83
|
+
|
|
84
|
+
<DropdownMenuSeparator />
|
|
85
|
+
<DropdownMenuGroup>
|
|
86
|
+
<DropdownMenuLabel>Columns</DropdownMenuLabel>
|
|
87
|
+
</DropdownMenuGroup>
|
|
88
|
+
<DndContext
|
|
89
|
+
id={optionsDndContextId}
|
|
90
|
+
sensors={optionsSensors}
|
|
91
|
+
collisionDetection={closestCenter}
|
|
92
|
+
onDragEnd={onOptionColumnDragEnd}
|
|
93
|
+
>
|
|
94
|
+
<SortableContext
|
|
95
|
+
items={visibleColumns.map((column) => column.id)}
|
|
96
|
+
strategy={verticalListSortingStrategy}
|
|
97
|
+
>
|
|
98
|
+
{visibleColumns.map((column) => (
|
|
99
|
+
<SortableColumnOptionItem
|
|
100
|
+
key={column.id}
|
|
101
|
+
column={column}
|
|
102
|
+
checked
|
|
103
|
+
disabled={visibleColumnIds.length === 1}
|
|
104
|
+
onCheckedChange={(checked) =>
|
|
105
|
+
toggleColumnVisibility(column.id, checked)
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</SortableContext>
|
|
110
|
+
</DndContext>
|
|
111
|
+
|
|
112
|
+
{hiddenColumns.length > 0 ? <DropdownMenuSeparator /> : null}
|
|
113
|
+
{hiddenColumns.map((column: DataGridColumn<ColumnId>) => (
|
|
114
|
+
<DropdownMenuCheckboxItem
|
|
115
|
+
key={column.id}
|
|
116
|
+
checked={false}
|
|
117
|
+
onCheckedChange={(checked) =>
|
|
118
|
+
toggleColumnVisibility(column.id, checked === true)
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
{column.label}
|
|
122
|
+
</DropdownMenuCheckboxItem>
|
|
123
|
+
))}
|
|
124
|
+
</DropdownMenuContent>
|
|
125
|
+
</DropdownMenu>
|
|
126
|
+
)
|
|
127
|
+
}
|