Mantine Editor Plugins
Connect Mantine Select, DatePicker, ColorPicker, and TimePicker controls to BeautifulGrid columns with @beautifuljs/grid-mantine.
import { t } from './i18n';
import * as React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
import {
createMantineColorPickerEditorPlugin,
createMantineDatePickerEditorPlugin,
createMantineSelectEditorPlugin,
createMantineTimePickerEditorPlugin,
} from '@beautifuljs/grid-mantine';
import '@beautifuljs/grid-mantine/style.css';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import { CalendarIcon, ChevronDownIcon, ClockIcon } from './editing/editorIcons';
import {
applyEditingDataChange,
cloneEditingOrders,
type EditingOrder,
withEditingCellClasses,
} from './editing/shared';
type MantineEditorOrder = EditingOrder & {
labelColor: string;
deliveryTime: string;
};
const statusEditor = createMantineSelectEditorPlugin<MantineEditorOrder, EditingOrder['status']>({
id: 'mantine-status',
ariaLabel: t('Mantine 주문 상태 선택', 'Mantine Select Order Status'),
searchable: true,
options: [
{ value: t('접수', 'Receipt'), label: t('접수', 'Receipt') },
{ value: t('진행', 'In Progress'), label: t('진행', 'In Progress') },
{ value: t('완료', 'Completed'), label: t('완료', 'Completed') },
],
});
const deliveryDateEditor = createMantineDatePickerEditorPlugin<MantineEditorOrder>({
id: 'mantine-delivery-date',
ariaLabel: t('Mantine 납기일 선택', 'Mantine Select Delivery Date'),
});
const labelColorEditor = createMantineColorPickerEditorPlugin<MantineEditorOrder>({
id: 'mantine-label-color',
ariaLabel: t('Mantine 라벨 색상 선택', 'Mantine Select Label Color'),
colors: ['#228BE6', '#12B886', '#7950F2', '#FD7E14'],
});
const deliveryTimeEditor = createMantineTimePickerEditorPlugin<MantineEditorOrder>({
id: 'mantine-delivery-time',
ariaLabel: t('Mantine 배송 시간 선택', 'Mantine Select Delivery Time'),
minuteStep: 5,
});
const initialColors = ['#228BE6', '#12B886', '#7950F2', '#FD7E14'];
const initialDeliveryTimes = ['09:30', '11:00', '14:30', '16:00'];
const cloneMantineEditorOrders = (): BGridDataItem<MantineEditorOrder>[] =>
cloneEditingOrders().map((item, index) => ({
...item,
values: {
...item.values,
labelColor: initialColors[index % initialColors.length],
deliveryTime: initialDeliveryTimes[index % initialDeliveryTimes.length],
},
}));
export default function ExternalMantineEditorPluginExample() {
const [data, setData] = React.useState(cloneMantineEditorOrders);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const columns = React.useMemo<BGridColumn<MantineEditorOrder>[]>(
() =>
withEditingCellClasses<MantineEditorOrder>([
{ key: 'orderCode', label: t('주문 코드', 'Order Code'), width: 140, editable: false },
{
key: 'status',
label: 'Mantine Select',
width: 165,
editable: true,
editor: statusEditor,
editorIcon: { render: <ChevronDownIcon />, ariaLabel: t('Mantine 상태 선택', 'Mantine Select Status') },
},
{
key: 'deliveryDate',
label: 'Mantine DatePicker',
width: 175,
editable: true,
editor: deliveryDateEditor,
editorIcon: { render: <CalendarIcon />, ariaLabel: t('Mantine 납기일 선택', 'Mantine Select Delivery Date') },
},
{
key: 'labelColor',
label: 'Mantine ColorPicker',
width: 175,
editable: true,
editor: labelColorEditor,
editorIcon: {
render: ({ value }) => (
<span
className='bgrid-color-swatch'
style={{ backgroundColor: typeof value === 'string' ? value : 'transparent' }}
aria-hidden='true'
/>
),
ariaLabel: t('Mantine 라벨 색상 선택', 'Mantine Select Label Color'),
},
},
{
key: 'deliveryTime',
label: 'Mantine TimePicker',
width: 175,
editable: true,
editor: deliveryTimeEditor,
editorIcon: { render: <ClockIcon />, ariaLabel: t('Mantine 배송 시간 선택', 'Mantine Select Delivery Time') },
},
]),
[],
);
return (
<div className='flex min-h-0 flex-col gap-3'>
<p className='m-0 rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm leading-6 text-slate-700 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300'>
<code>@beautifuljs/grid-mantine</code>
{t(
'의 Select, DatePicker, ColorPicker, TimePicker를 사용합니다. Cascader와 TreeSelect는 이 통합의 지원 범위에 포함되지 않습니다.',
' provides Select, DatePicker, ColorPicker, and TimePicker. Cascader and TreeSelect are outside this integration scope.',
)}
</p>
<DataGridContainer ref={containerRef} style={{ height: 340 }}>
<BGrid<MantineEditorOrder>
width={width}
height={height}
data={data}
columns={columns}
rowKey='id'
editable
variant='vertical-bordered'
onChangeData={(sourceIndex, _columnIndex, values, _column, meta) => {
setData(current => applyEditingDataChange(current, sourceIndex, values, meta));
}}
/>
</DataGridContainer>
</div>
);
}import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}import { t } from '../i18n';
import type { BGridChangeDataMeta, BGridColumn, BGridDataItem } from 'beautiful-grid';
import './editingExamples.css';
export interface EditingOrder {
id: string;
orderCode: string;
customerCode: string;
customerName: string;
customerGrade: string;
status: string;
deliveryDate: string;
approved: boolean;
quantity: number;
unitPrice: number;
amount: number;
note: string;
mergeGroup: string;
}
export const editingOrders: BGridDataItem<EditingOrder>[] = [
{
values: {
id: 'ORDER-001',
orderCode: 'ORD-2601',
customerCode: 'C001',
customerName: t('서울상사', 'Seoul Sangsa'),
customerGrade: 'VIP',
status: t('접수', 'Receipt'),
deliveryDate: '2026-08-25',
approved: true,
quantity: 2,
unitPrice: 12000,
amount: 24000,
note: t('오전 배송', 'Morning Delivery'),
mergeGroup: 'A',
},
},
{
values: {
id: 'ORDER-002',
orderCode: 'ORD-2602',
customerCode: 'C001',
customerName: t('서울상사', 'Seoul Sangsa'),
customerGrade: 'VIP',
status: t('진행', 'In Progress'),
deliveryDate: '2026-08-26',
approved: false,
quantity: 3,
unitPrice: 18000,
amount: 54000,
note: t('담당자 확인', 'Assignee Confirmation'),
mergeGroup: 'A',
},
},
{
values: {
id: 'ORDER-003',
orderCode: 'ORD-2603',
customerCode: 'C002',
customerName: t('한빛물산', 'Hanbit Mulsan'),
customerGrade: t('우수', 'Excellent'),
status: t('완료', 'Completed'),
deliveryDate: '2026-08-28',
approved: true,
quantity: 1,
unitPrice: 32000,
amount: 32000,
note: '',
mergeGroup: 'B',
},
},
{
values: {
id: 'ORDER-004',
orderCode: 'ORD-2604',
customerCode: 'C003',
customerName: 'Northwind',
customerGrade: t('일반', 'General'),
status: t('접수', 'Receipt'),
deliveryDate: '2026-09-01',
approved: false,
quantity: 5,
unitPrice: 9000,
amount: 45000,
note: t('영문 송장', 'English Invoice'),
mergeGroup: 'C',
},
},
];
export const cloneEditingOrders = (): BGridDataItem<EditingOrder>[] =>
editingOrders.map(item => ({
...item,
values: { ...item.values },
editedColumnIds: item.editedColumnIds ? [...item.editedColumnIds] : undefined,
changedKeys: item.changedKeys ? [...item.changedKeys] : undefined,
}));
export const applyEditingDataChange = <T,>(
current: BGridDataItem<T>[],
sourceIndex: number,
values: T,
meta?: BGridChangeDataMeta<T>,
): BGridDataItem<T>[] =>
current.map((item, index) =>
index === sourceIndex ? meta?.dataItem ?? { ...item, values } : item,
);
export const withEditingCellClasses = <T,>(columns: BGridColumn<T>[]): BGridColumn<T>[] =>
columns.map(column => ({
...column,
className: [
column.className,
column.editable === false ? 'editing-example-cell-readonly' : 'editing-example-cell-editable',
]
.filter(Boolean)
.join(' '),
}));.editing-example-row-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 34px;
padding: 6px 12px;
border: 1px solid var(--site-border-strong, #cbd5e1);
border-radius: var(--site-radius-sm, 8px);
background: var(--site-control-bg, #ffffff);
color: var(--site-text-secondary, #526178);
font: inherit;
font-size: 13px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
cursor: pointer;
transition: background-color 150ms, border-color 150ms, color 150ms;
}
.editing-example-row-button-add {
border-color: var(--site-accent, #2563eb);
background: var(--site-accent, #2563eb);
color: var(--site-accent-contrast, #ffffff);
}
.editing-example-row-button-add:hover {
border-color: var(--site-accent-strong, #1d4ed8);
background: var(--site-accent-strong, #1d4ed8);
}
.editing-example-row-button-delete:not(:disabled) {
border-color: #fca5a5;
color: #dc2626;
}
.editing-example-row-button-delete:hover:not(:disabled) {
border-color: #ef4444;
background: color-mix(in srgb, #ef4444 8%, var(--site-control-bg, #ffffff));
}
.editing-example-row-button:focus-visible {
outline: 2px solid var(--site-focus-ring, #93c5fd);
outline-offset: 2px;
}
.editing-example-row-button:disabled {
border-color: var(--site-border, #e1e7ef);
background: var(--site-surface, #f7f9fc);
color: var(--site-text-muted, #8491a5);
cursor: not-allowed;
}
.editing-example-cell-editable {
--editing-example-bg: #ffffff;
--editing-example-hover-bg: #dbeafe;
}
.editing-example-cell-readonly {
--editing-example-color: #525252;
--editing-example-bg: #f5f5f5;
--editing-example-hover-bg: #e5e5e5;
}
.bgrid-body-table
td:is(.editing-example-cell-editable, .editing-example-cell-readonly):not(:is(.bgrid-cell-selected, .bgrid-cell-edited, .bgrid-cell-value-changed, .bgrid-cell-editing)) {
color: var(--editing-example-color, inherit);
background-color: var(--editing-example-bg);
}
.bgrid-body-table tr.bgrid-row-hover
> td:is(.editing-example-cell-editable, .editing-example-cell-readonly):not(:is(.bgrid-cell-selected, .bgrid-cell-edited, .bgrid-cell-value-changed, .bgrid-cell-editing)) {
background-color: var(--editing-example-hover-bg);
}import * as React from 'react';
const iconProps = {
width: 14,
height: 14,
viewBox: '0 0 16 16',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 1.5,
strokeLinecap: 'round' as const,
strokeLinejoin: 'round' as const,
focusable: false,
'aria-hidden': true,
};
export function ChevronDownIcon() {
return (
<svg {...iconProps}>
<path d='m4 6 4 4 4-4' />
</svg>
);
}
export function CalendarIcon() {
return (
<svg {...iconProps}>
<rect x='2.5' y='3.5' width='11' height='10' rx='1.5' />
<path d='M5 2.5v2M11 2.5v2M2.5 6.5h11' />
</svg>
);
}
export function ClockIcon() {
return (
<svg {...iconProps}>
<circle cx='8' cy='8' r='5.5' />
<path d='M8 4.75V8l2.25 1.5' />
</svg>
);
}
export function SearchIcon() {
return (
<svg {...iconProps}>
<circle cx='7' cy='7' r='3.75' />
<path d='m10 10 3 3' />
</svg>
);
}
export function CheckIcon() {
return (
<svg {...iconProps}>
<path d='m3 8.25 3 3L13 4.5' />
</svg>
);
}@beautifuljs/grid-mantine is the official package that connects Mantine components to the BeautifulGrid plugin-editor lifecycle. It provides Select, DatePicker, ColorPicker, and TimePicker and renders every dropdown and popover inside the Grid portal.
Install
npm install @beautifuljs/grid-mantine @mantine/core @mantine/dates dayjs
Import Mantine’s base styles and the plugin stylesheet once from the application entry point, and place MantineProvider above the application.
import { MantineProvider } from '@mantine/core';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import 'beautiful-grid/style.css';
import '@beautifuljs/grid-mantine/style.css';
root.render(
<MantineProvider>
<App />
</MantineProvider>,
);
Connect editor factories
import {
createMantineColorPickerEditorPlugin,
createMantineDatePickerEditorPlugin,
createMantineSelectEditorPlugin,
createMantineTimePickerEditorPlugin,
} from '@beautifuljs/grid-mantine';
const statusEditor = createMantineSelectEditorPlugin<Order, Order['status']>({
id: 'order-status',
ariaLabel: 'Select order status',
searchable: true,
options: [
{ value: 'ready', label: 'Ready' },
{ value: 'done', label: 'Done' },
],
});
const dateEditor = createMantineDatePickerEditorPlugin<Order>({
id: 'order-date',
ariaLabel: 'Select delivery date',
});
const colorEditor = createMantineColorPickerEditorPlugin<Order>({
id: 'order-color',
ariaLabel: 'Select label color',
colors: ['#228BE6', '#12B886', '#FD7E14'],
});
const timeEditor = createMantineTimePickerEditorPlugin<Order>({
id: 'order-time',
ariaLabel: 'Select delivery time',
minuteStep: 5,
});
const columns: BGridColumn<Order>[] = [
{ key: 'status', label: 'Status', editable: true, editor: statusEditor },
{ key: 'deliveryDate', label: 'Delivery date', editable: true, editor: dateEditor },
{ key: 'labelColor', label: 'Color', editable: true, editor: colorEditor },
{ key: 'deliveryTime', label: 'Time', editable: true, editor: timeEditor },
];
Supported values
- Select supports string and number options plus searchable mode.
- DatePicker stores an ISO-style
YYYY-MM-DDstring. - ColorPicker stores
#RRGGBBby default and can optionally enable alpha. - TimePicker stores
HH:mm, orHH:mm:sswhen seconds are enabled. - Cascader and TreeSelect are not part of the official Mantine integration scope.
Closing a popup before a value is saved cancels the editing session. After Apply or a value selection commits, the following close event cannot overwrite the saved result.
See the @beautifuljs/grid-mantine npm page for package APIs and license information.