테마 및 스타일 커스터마이징 (Theming)
CSS 변수(--bgrid-*)를 오버라이드하여 라이트/다크 모드 및 기업 브랜드 컬러에 맞춘 커스텀 테마를 적용하는 방법을 학습합니다.
import * as React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
import { Segmented } from 'antd';
import { Building2, Moon, Sun } from 'lucide-react';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import './ThemingExample.css';
type ThemeId = 'default' | 'brand' | 'dark';
type ServiceStatus = '정상' | '점검 예정' | '주의';
interface ServiceMetric {
id: string;
service: string;
team: string;
status: ServiceStatus;
availability: number;
responseTime: number;
updatedAt: string;
}
interface ThemeOption {
id: ThemeId;
label: string;
description: string;
icon: React.ReactNode;
tokens: Array<{ name: string; value: string }>;
}
const themeOptions: ThemeOption[] = [
{
id: 'default',
label: '기본',
description: '별도 오버라이드가 없는 기본 라이트 테마',
icon: <Sun size={14} aria-hidden='true' />,
tokens: [
{ name: '--bgrid-primary-color', value: '#3073f1' },
{ name: '--bgrid-header-bg', value: '#f3f5f8' },
{ name: '--bgrid-body-bg', value: '#ffffff' },
{ name: '--bgrid-body-active-bg', value: '#e1f0ff' },
],
},
{
id: 'brand',
label: '브랜드',
description: '기업 컬러와 둥근 모서리를 적용한 커스텀 테마',
icon: <Building2 size={14} aria-hidden='true' />,
tokens: [
{ name: '--bgrid-primary-color', value: '#0f766e' },
{ name: '--bgrid-header-bg', value: '#0f766e' },
{ name: '--bgrid-body-bg', value: '#f7fffd' },
{ name: '--bgrid-body-active-bg', value: '#ccfbf1' },
],
},
{
id: 'dark',
label: '다크',
description: '어두운 업무 화면에 맞춘 고대비 다크 테마',
icon: <Moon size={14} aria-hidden='true' />,
tokens: [
{ name: '--bgrid-primary-color', value: '#38bdf8' },
{ name: '--bgrid-header-bg', value: '#1e293b' },
{ name: '--bgrid-body-bg', value: '#0f172a' },
{ name: '--bgrid-body-active-bg', value: '#164e63' },
],
},
];
const services: BGridDataItem<ServiceMetric>[] = [
['API-GW', 'API Gateway', '플랫폼', '정상', 99.99, 82, '10:42'],
['AUTH', '인증 서비스', '플랫폼', '정상', 99.97, 104, '10:41'],
['PAY', '결제 처리', '커머스', '주의', 99.82, 286, '10:40'],
['ORDER', '주문 관리', '커머스', '정상', 99.95, 127, '10:42'],
['SEARCH', '상품 검색', '프로덕트', '점검 예정', 99.91, 168, '10:39'],
['NOTIFY', '알림 발송', 'CRM', '정상', 99.98, 91, '10:41'],
['REPORT', '리포트 생성', '데이터', '주의', 99.76, 342, '10:38'],
['STORAGE', '파일 저장소', '플랫폼', '정상', 99.99, 73, '10:42'],
['PROFILE', '회원 프로필', 'CRM', '정상', 99.96, 118, '10:40'],
['CATALOG', '상품 카탈로그', '프로덕트', '점검 예정', 99.89, 194, '10:39'],
].map(([id, service, team, status, availability, responseTime, updatedAt]) => ({
values: {
id: String(id),
service: String(service),
team: String(team),
status: status as ServiceStatus,
availability: Number(availability),
responseTime: Number(responseTime),
updatedAt: String(updatedAt),
},
}));
const columns: BGridColumn<ServiceMetric>[] = [
{ key: 'service', label: '서비스', width: 190 },
{ key: 'team', label: '담당 팀', width: 110, align: 'center' },
{
key: 'status',
label: '운영 상태',
width: 120,
align: 'center',
itemRender: ({ value }) => (
<span className={`theming-example-status theming-example-status--${value === '정상' ? 'healthy' : value === '주의' ? 'warning' : 'scheduled'}`}>
{String(value)}
</span>
),
},
{
key: 'availability',
label: '가용성',
width: 110,
align: 'right',
itemRender: ({ value }) => <strong>{Number(value).toFixed(2)}%</strong>,
},
{
key: 'responseTime',
label: '응답 시간',
width: 120,
align: 'right',
itemRender: ({ value }) => `${Number(value).toLocaleString()}ms`,
},
{ key: 'updatedAt', label: '최근 확인', width: 100, align: 'center' },
];
export default function ThemingExample() {
const [theme, setTheme] = React.useState<ThemeId>('default');
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const activeTheme = themeOptions.find(option => option.id === theme) ?? themeOptions[0];
return (
<div className='theming-example'>
<div className='theming-example__toolbar'>
<div className='theming-example__intro'>
<strong>같은 데이터 · 같은 컬럼 · 다른 CSS 변수</strong>
<span>{activeTheme.description}</span>
</div>
<Segmented
aria-label='데이터그리드 테마 선택'
value={theme}
options={themeOptions.map(option => ({
value: option.id,
label: (
<span className='theming-example__option'>
{option.icon}
{option.label}
</span>
),
}))}
onChange={value => setTheme(value as ThemeId)}
/>
</div>
<div className='theming-example__tokens' aria-live='polite'>
<div className='theming-example__class-name'>
<span>적용 클래스</span>
<code>.bgrid-theme-{theme}</code>
</div>
{activeTheme.tokens.map(token => (
<div className='theming-example__token' key={token.name}>
<span className='theming-example__swatch' style={{ backgroundColor: token.value }} aria-hidden='true' />
<span>
<code>{token.name}</code>
<small>{token.value}</small>
</span>
</div>
))}
</div>
<DataGridContainer ref={containerRef} className='theming-example__grid-container'>
<BGrid<ServiceMetric>
className={`theming-example-grid bgrid-theme-${theme}`}
width={width}
height={height}
headerHeight={38}
itemHeight={22}
columns={columns}
data={services}
rowKey='id'
showLineNumber
frozenColumnIndex={1}
cellSelectionOptions={{ enabled: true }}
cellNavigationOptions={{ enabled: true, defaultActiveCell: { rowIndex: 0, columnIndex: 0 } }}
status={{ content: `${services.length}개 서비스 · ${activeTheme.label} 테마` }}
/>
</DataGridContainer>
<p className='theming-example__hint'>행에 마우스를 올리거나 셀을 클릭하면 hover·선택 색상도 테마에 맞게 바뀝니다.</p>
</div>
);
}.theming-example {
--theming-panel-bg: #f8fafc;
--theming-panel-border: #dbe2ea;
--theming-panel-color: #334155;
display: flex;
min-height: 0;
flex-direction: column;
gap: 12px;
color: var(--theming-panel-color);
}
.theming-example__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border: 1px solid var(--theming-panel-border);
border-radius: 12px;
background: var(--theming-panel-bg);
}
.theming-example__intro strong,
.theming-example__intro span {
display: block;
}
.theming-example__intro strong {
margin-bottom: 3px;
color: #0f172a;
font-size: 14px;
}
.theming-example__intro span {
color: #64748b;
font-size: 12px;
}
.theming-example__option {
display: inline-flex;
align-items: center;
gap: 5px;
}
.theming-example__tokens {
display: grid;
grid-template-columns: minmax(170px, 1.25fr) repeat(4, minmax(130px, 1fr));
gap: 8px;
}
.theming-example__class-name,
.theming-example__token {
display: flex;
min-width: 0;
align-items: center;
gap: 9px;
padding: 9px 10px;
border: 1px solid var(--theming-panel-border);
border-radius: 9px;
background: #ffffff;
}
.theming-example__class-name {
align-items: flex-start;
flex-direction: column;
justify-content: center;
gap: 2px;
}
.theming-example__class-name > span {
color: #64748b;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
}
.theming-example__class-name code {
color: #2563eb;
font-size: 12px;
font-weight: 700;
}
.theming-example__swatch {
width: 22px;
height: 22px;
flex: 0 0 auto;
border: 1px solid rgba(15, 23, 42, 0.16);
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.theming-example__token > span:last-child,
.theming-example__token code,
.theming-example__token small {
display: block;
min-width: 0;
}
.theming-example__token code {
overflow: hidden;
color: #475569;
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.theming-example__token small {
margin-top: 2px;
color: #0f172a;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
font-weight: 700;
}
.theming-example .theming-example__grid-container {
height: 350px;
}
.theming-example__hint {
margin: -3px 0 0;
color: #64748b;
font-size: 11px;
}
/* className으로 Grid 루트에 범위를 한정해 CSS 변수만 오버라이드합니다. */
.site-grid-theme .bgrid-root.theming-example-grid.bgrid-theme-brand {
--bgrid-font-size: 13px;
--bgrid-primary-color: #0f766e;
--bgrid-header-bg: #0f766e;
--bgrid-header-color: #ffffff;
--bgrid-header-font-weight: 700;
--bgrid-header-hover-bg: #115e59;
--bgrid-border-color-base: #99d5ca;
--bgrid-border-color-light: #b7e2da;
--bgrid-border-color-subtle: #d9f2ed;
--bgrid-header-separator-color: #5eead4;
--bgrid-frozen-boundary-color: #0f766e;
--bgrid-border-radius: 10px;
--bgrid-row-selector-color: #0f766e;
--bgrid-body-bg: #f7fffd;
--bgrid-body-color: #134e4a;
--bgrid-body-odd-bg: #ecfdf9;
--bgrid-body-hover-bg: #d9f8f1;
--bgrid-body-hover-odd-bg: #ccfbf1;
--bgrid-body-active-bg: #ccfbf1;
--bgrid-cell-selected-bg: #99f6e4;
--bgrid-cell-selected-border-color: #0f766e;
--bgrid-active-cell-bg: #ffffff;
--bgrid-active-cell-ring-color: #0f766e;
--bgrid-selection-axis-bg: #99f6e4;
--bgrid-selection-axis-color: #115e59;
--bgrid-selection-axis-border-color: #0f766e;
--bgrid-footer-bg: #dff7f2;
--bgrid-scroll-bg: #f0fdfa;
--bgrid-scroll-track-bg: #d5f5ed;
--bgrid-scroll-thumb-bg: #5eead4;
--bgrid-scroll-thumb-hover-bg: #2dd4bf;
--bgrid-scrollbar-modern-track-bg: #d5f5ed;
--bgrid-scrollbar-modern-thumb-bg: #5eead4;
--bgrid-scrollbar-modern-thumb-hover-bg: #2dd4bf;
--bgrid-scrollbar-modern-button-hover-bg: #ccfbf1;
--bgrid-scrollbar-modern-icon-color: #0f766e;
--bgrid-scrollbar-modern-gutter-bg: #f0fdfa;
--bgrid-scrollbar-modern-gutter-border-color: #99d5ca;
}
.site-grid-theme .bgrid-root.theming-example-grid.bgrid-theme-dark {
--bgrid-font-size: 13px;
--bgrid-primary-color: #38bdf8;
--bgrid-header-bg: #1e293b;
--bgrid-header-color: #f8fafc;
--bgrid-header-font-weight: 650;
--bgrid-header-hover-bg: #334155;
--bgrid-border-color-base: #475569;
--bgrid-border-color-light: #334155;
--bgrid-border-color-subtle: #263449;
--bgrid-header-separator-color: #64748b;
--bgrid-frozen-boundary-color: #64748b;
--bgrid-border-radius: 8px;
--bgrid-row-selector-color: #0f172a;
--bgrid-body-bg: #0f172a;
--bgrid-body-color: #e2e8f0;
--bgrid-body-odd-bg: #111c2f;
--bgrid-body-hover-bg: #1e293b;
--bgrid-body-hover-odd-bg: #263449;
--bgrid-body-active-bg: #164e63;
--bgrid-cell-selected-bg: #164e63;
--bgrid-cell-selected-border-color: #38bdf8;
--bgrid-active-cell-bg: #0f172a;
--bgrid-active-cell-ring-color: #38bdf8;
--bgrid-selection-axis-bg: #164e63;
--bgrid-selection-axis-color: #7dd3fc;
--bgrid-selection-axis-border-color: #38bdf8;
--bgrid-footer-bg: #1e293b;
--bgrid-scroll-bg: #0f172a;
--bgrid-scroll-track-bg: #1e293b;
--bgrid-scroll-thumb-bg: #475569;
--bgrid-scroll-thumb-hover-bg: #64748b;
--bgrid-scrollbar-modern-track-bg: #1e293b;
--bgrid-scrollbar-modern-thumb-bg: #475569;
--bgrid-scrollbar-modern-thumb-hover-bg: #64748b;
--bgrid-scrollbar-modern-button-hover-bg: #334155;
--bgrid-scrollbar-modern-icon-color: #94a3b8;
--bgrid-scrollbar-modern-gutter-bg: #1e293b;
--bgrid-scrollbar-modern-gutter-border-color: #475569;
}
[role='grid'].theming-example-grid .theming-example-status {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 62px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
[role='grid'].theming-example-grid .theming-example-status--healthy {
background: #dcfce7;
color: #15803d;
}
[role='grid'].theming-example-grid .theming-example-status--warning {
background: #ffedd5;
color: #c2410c;
}
[role='grid'].theming-example-grid .theming-example-status--scheduled {
background: #e0e7ff;
color: #4338ca;
}
[role='grid'].theming-example-grid.bgrid-theme-dark .theming-example-status--healthy {
background: #14532d;
color: #bbf7d0;
}
[role='grid'].theming-example-grid.bgrid-theme-dark .theming-example-status--warning {
background: #7c2d12;
color: #fed7aa;
}
[role='grid'].theming-example-grid.bgrid-theme-dark .theming-example-status--scheduled {
background: #312e81;
color: #c7d2fe;
}
@media (max-width: 900px) {
.theming-example__tokens {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.theming-example__class-name {
grid-column: 1 / -1;
}
}
@media (max-width: 640px) {
.theming-example__toolbar {
align-items: stretch;
flex-direction: column;
}
.theming-example__toolbar .ant-segmented {
width: 100%;
}
.theming-example__toolbar .ant-segmented-item {
flex: 1;
}
.theming-example__tokens {
grid-template-columns: 1fr;
}
.theming-example__class-name {
grid-column: auto;
}
.theming-example .theming-example__grid-container {
height: 330px;
}
}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,
};
}1. 개요 및 CSS 변수 아키텍처
BeautifulGrid는 현대적인 CSS 커스텀 프로퍼티(CSS Variables) 설계를 따르고 있어, 별도의 복잡한 테마 프로바이더나 무거운 CSS-in-JS 런타임 없이도 순수 CSS 변수 오버라이드만으로 폰트, 배경색, 테두리 색상, 활성 행 하이라이트 색상을 즉시 변경할 수 있습니다.
위 라이브 데모에서 기본 / 브랜드 / 다크를 전환해 보세요. 데이터와 컬럼 설정은 그대로 유지되고 Grid 루트의 클래스와 --bgrid-* 변수만 바뀝니다. 색상 칩에는 현재 적용된 핵심 변수와 실제 값이 표시되며, 셀을 클릭하거나 행에 마우스를 올리면 선택·hover 상태까지 함께 비교할 수 있습니다.
테마는 Grid 전체의 시각 토큰을 바꾸는 작업이고, 스타일 커스터마이징은 itemRender의 상태 뱃지처럼 업무 UI를 추가하는 작업입니다. 두 방식 모두 Grid 인스턴스의 className 아래로 범위를 한정하면 다른 Grid에는 영향을 주지 않습니다.
2. 주요 CSS 변수 계약 명세
/* 데이터그리드 컨테이너 또는 글로벌 CSS에서 오버라이드 가능 */
[role='grid'] {
/* 폰트 및 타이포그래피 */
--bgrid-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--bgrid-font-size: 12px;
/* 테두리 및 구분선 */
--bgrid-border-color-base: #cbd5e1;
--bgrid-border-color-light: #dbe2ea;
--bgrid-border-color-subtle: #eef2f6;
--bgrid-header-separator-color: #94a3b8;
/* 헤더 스타일 */
--bgrid-header-bg: #f8fafc;
--bgrid-header-color: #1e293b;
/* 바디 행 및 셀 */
--bgrid-body-bg: #ffffff;
--bgrid-body-color: #0f172a;
--bgrid-body-odd-bg: #f8fafc;
--bgrid-body-hover-bg: #f1f5f9;
--bgrid-body-hover-odd-bg: #e9eef5;
--bgrid-body-active-bg: #e2e8f0;
/* 별도 오버레이로 그려지는 셀 선택 범위 */
--bgrid-cell-selected-bg: var(--bgrid-body-active-bg);
--bgrid-cell-selected-overlay-opacity: 0.72;
--bgrid-cell-selected-border-color: rgba(37, 99, 235, 0.78);
--bgrid-cell-selected-border-width: var(--bgrid-active-cell-ring-width);
/* 선택 범위 안의 포커스 셀 */
--bgrid-active-cell-bg: #ffffff;
--bgrid-active-cell-ring-color: #2563eb;
--bgrid-active-cell-ring-width: 2px;
/* 선택 범위의 컬럼 헤더와 라인넘버 축 */
--bgrid-selection-axis-bg: #dbeafe;
--bgrid-selection-axis-color: #2563eb;
--bgrid-selection-axis-border-color: #2563eb;
/* 값이 변경된 셀 */
--bgrid-cell-edited-bg: #fff7ed;
--bgrid-cell-edited-color: #c2410c;
--bgrid-cell-edited-border-color: #fdba74;
--bgrid-cell-value-changed-bg: #fff7ed;
--bgrid-cell-value-changed-color: #c2410c;
--bgrid-cell-value-changed-border-color: #fdba74;
/* 포인트 컬러 (선택선, 활성 뱃지) */
--bgrid-primary-color: #2563eb;
/* 행 순서 재배치 모션과 preview */
--bgrid-row-reorder-duration: 150ms;
--bgrid-row-reorder-easing: cubic-bezier(0.2, 0, 0, 1);
--bgrid-row-reorder-guide-color: #2563eb;
--bgrid-row-reorder-preview-bg: #e2e8f0;
--bgrid-row-reorder-preview-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
/* 정렬 및 필터 Toolbox */
--bgrid-toolbox-bg: #ffffff;
--bgrid-toolbox-color: #334155;
--bgrid-toolbox-muted-color: #64748b;
--bgrid-toolbox-control-bg: #ffffff;
--bgrid-toolbox-control-color: #334155;
--bgrid-toolbox-control-border-color: #cbd5e1;
--bgrid-toolbox-control-placeholder-color: #94a3b8;
--bgrid-toolbox-hover-bg: #f1f5f9;
--bgrid-toolbox-active-bg: #dbeafe;
--bgrid-toolbox-danger-color: #dc2626;
--bgrid-toolbox-danger-bg: #fef2f2;
--bgrid-toolbox-button-bg: #f8fafc;
--bgrid-toolbox-primary-hover-color: #1d4ed8;
--bgrid-toolbox-primary-contrast-color: #ffffff;
--bgrid-toolbox-notice-bg: #f8fafc;
--bgrid-toolbox-scroll-thumb-bg: #b8c2d1;
--bgrid-toolbox-scroll-track-bg: #f1f5f9;
--bgrid-toolbox-focus-ring-color: #bfdbfe;
/* 검색 하이라이트와 셀 컨텍스트 메뉴 */
--bgrid-search-bg: #ffffff;
--bgrid-search-color: #334155;
--bgrid-search-control-bg: #f8fafc;
--bgrid-search-button-hover-bg: #f1f5f9;
--bgrid-search-match-bg: rgba(250, 204, 21, 0.28);
--bgrid-search-match-border-color: #ca8a04;
--bgrid-search-current-bg: rgba(249, 115, 22, 0.3);
--bgrid-search-current-border-color: #f97316;
--bgrid-context-menu-bg: #ffffff;
--bgrid-context-menu-color: #334155;
--bgrid-context-menu-border-color: #cbd5e1;
--bgrid-context-menu-hover-bg: #f1f5f9;
}
편집 저장 또는 다중 셀 붙여넣기가 직접 발생한 셀에는 bgrid-cell-edited가 적용됩니다. 변경된 데이터 key를 공유하는 모든 셀에는 bgrid-cell-value-changed가 적용되므로 동일 key·다른 id 컬럼도 값 변경 상태를 표시합니다. 일반 선택 셀에는 선택 테마가 우선하며, 선택을 이동하면 변경 셀 배경과 inset border가 표시됩니다.
셀 선택은 각 셀의 border를 변경하지 않고 포인터 이벤트를 통과시키는 별도 오버레이 사각형으로 표시합니다. 틀고정 행·컬럼을 가로지르는 범위는 패널별 조각으로 나누되 전체 선택의 바깥쪽 외곽선만 표시하므로, 병합셀이 일부 포함되어도 선택 영역은 직사각형을 유지하고 스크롤 위치와 동기화됩니다. 배경은 --bgrid-cell-selected-bg, 투명도는 --bgrid-cell-selected-overlay-opacity, 외곽선은 --bgrid-cell-selected-border-* 변수로 조정합니다.
선택 영역 안의 포커스 셀은 선택 배경 대신 --bgrid-active-cell-bg를 사용합니다. 단일 셀 선택에서는 --bgrid-active-cell-ring-*으로 지정한 inset 링을 표시하지만, 다중 셀 선택에서는 포커스 셀의 개별 링을 제거하고 전체 선택 범위 외곽선만 표시합니다. 외곽선 두께는 --bgrid-cell-selected-border-width로 조정하며 기본값은 단일 셀 포커스 링 두께와 같습니다.
활성 셀이나 다중 선택 범위에 포함된 컬럼 헤더에는 bgrid-column-axis-active, 라인넘버에는 bgrid-row-axis-active가 자동으로 적용됩니다. 배경·글자·강조선 색상은 --bgrid-selection-axis-* 변수로 조정할 수 있으며 frozen 컬럼과 일반 컬럼에 동일하게 적용됩니다.
정렬·필터 Toolbox, editor plugin과 셀 컨텍스트 메뉴는 문서 최상위 포털인 Grid 인스턴스별 floating root를 공유합니다. 해당 Grid의 --bgrid-* 변수가 포털에 자동으로 전달되므로 컨테이너에 지정한 테마가 팝오버와 메뉴에도 동일하게 적용됩니다. 검색 입력은 Grid 안에 배치되며 --bgrid-search-*, 검색 셀은 --bgrid-search-match-*와 --bgrid-search-current-*로 조정합니다.
3. 실무 완성형 예제: 다크 테마(Dark Theme) 적용
import React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
export default function DarkThemeGrid() {
const columns: BGridColumn<any>[] = [
{ key: 'id', label: 'ID', width: 70, align: 'center' },
{ key: 'name', label: '서비스명', width: 180 },
{ key: 'status', label: '상태', width: 100, align: 'center' },
];
const data: BGridDataItem<any>[] = [
{ values: { id: 1, name: 'Auth Gateway', status: 'Healthy' } },
{ values: { id: 2, name: 'Payment Worker', status: 'Healthy' } },
];
return (
<div style={{ padding: 16, backgroundColor: '#0f172a', borderRadius: 8 }}>
<style>{`
.custom-dark-grid {
--bgrid-border-color-base: #334155;
--bgrid-border-color-light: #475569;
--bgrid-border-color-subtle: #1e293b;
--bgrid-header-separator-color: #475569;
--bgrid-header-bg: #1e293b;
--bgrid-header-color: #f8fafc;
--bgrid-body-bg: #0f172a;
--bgrid-body-color: #e2e8f0;
--bgrid-body-odd-bg: #111c2f;
--bgrid-body-hover-bg: #1e293b;
--bgrid-body-hover-odd-bg: #263449;
--bgrid-body-active-bg: #334155;
--bgrid-primary-color: #38bdf8;
}
`}</style>
<BGrid
className="custom-dark-grid"
width={450}
height={180}
columns={columns}
data={data}
rowKey="id"
/>
</div>
);
}