Doop
Doop: 오픈소스 멀티플레이어 디자인 캔버스 및 AI 에이전트용 MCP
SSA Group이 제공하는 최고의 React JS 대시보드 템플릿과 관리자 UI 키트. 반응형·맞춤형 대시보드를 빠르게 만들 수 있습니다
SSA Group offers the best React JS dashboard templates and admin UI kits. Quickly create responsive and customizable dashboards with our React solutions. Start now!
디자인TypeScript · MDXGitHub @ssagroup온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기SSA UI kit is an open-source React library offering AI-friendly components to streamline the creation of dashboards and admin panels.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗SSA UI kit is an open-source React-based library combining multiple UI Components, Widgets, Blocks, and Forms classified by application area and implemented following Atomic design principles.
SSA UI kit includes Figma design, integration with Firebase and examples of implementation for different business domains.
SSA UI kit was founded and is supported by SSA Group. The SSA UI kit source code is available under the MIT License.
We regularly post updates, new components, and improvements for the SSA UI kit on our blog: https://www.ssa.group/blog/ssa-ui-kit/ Please feel free to follow along.
SSA UI kit components are designed with the following principles:
Form Controls: Most form inputs integrate with React Hook Form. Use register prop for uncontrolled inputs, or value/onChange for controlled patterns.
Compound Components: Many components like Table, Modal, Field, and Dropdown use compound component patterns for flexible composition.
Status Props: Components use consistent prop naming like status (for validation states), disabled (for disabled state), and variant (for visual variants).
Controlled & Uncontrolled: Form controls follow the React convention —
checked/defaultChecked for booleans, value/defaultValue for everything
else.
Prop renames (v3.20). The library is converging on the names the DOM, MUI, Radix and Ant use. Each old prop still works and warns once per component in development; all are removed at the next major.
Old New Components isDisableddisabled15 components isCheckedcheckedRadioinitialStatedefaultCheckedCheckboxexternalStatecheckedCheckboxexternalState,selectedItemvalueRadioGroup,ButtonGroupselectedItems/defaultSelectedItemsvalue/defaultValueTypeaheadcolor/defaultColorvalue/defaultValueColorPickerariaLabelaria-labelBreadcrumbs,PaginationariaLabelledbyaria-labelledbyCardContent,DropdownOptions,MultipleDropdownOptions,DropdownToggleariaControlsaria-controlsAccordion,Tab,LargeTab,DropdownToggleopened/defaultOpenedopen/defaultOpenDrawer,useDraweropened/defaultOpened/onOpenedChangeopen/defaultOpen/onOpenChange
Get started in 60 seconds. Here's a complete example showing Card, Button, Input, and Table working together:
import { ThemeProvider } from '@emotion/react';
import { FormProvider, useForm } from 'react-hook-form';
import {
Card,
CardHeader,
CardContent,
Button,
Input,
Typography,
Table,
TableHead,
TableBody,
TableRow,
TableCell,
TableCellHeader,
mainTheme,
Theme as T,
} from '@ssa-ui-kit/core';
// TypeScript: Extend Emotion theme types
declare module '@emotion/react' {
export interface Theme extends T {}
}
function App() {
const methods = useForm();
const users = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' },
];
return (
<ThemeProvider theme={mainTheme}>
<FormProvider {...methods}>
<Card>
<CardHeader>
<Typography variant="h2">User Management</Typography>
</CardHeader>
<CardContent>
<Input
name="search"
placeholder="Search users..."
register={methods.register}
css={{ marginBottom: 16 }}
/>
<Table>
<TableHead>
<TableRow>
<TableCellHeader>Name</TableCellHeader>
<TableCellHeader>Email</TableCellHeader>
<TableCellHeader>Actions</TableCellHeader>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Button size="small" text="Edit" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</FormProvider>
</ThemeProvider>
);
}
# Core components
npm install @ssa-ui-kit/core
# or
yarn add @ssa-ui-kit/core
# or
pnpm add @ssa-ui-kit/core
# Hooks (optional, separate package)
npm install @ssa-ui-kit/hooks
# or
yarn add @ssa-ui-kit/hooks
# or
pnpm add @ssa-ui-kit/hooks
⚠️ Vite Users: See the Vite Configuration section below for required setup to avoid
useFormContext()errors.
Make sure you have React 19 or later, Emotion, and React Hook Form installed:
npm install react@^19 react-dom@^19 @emotion/react @emotion/styled react-hook-form
Note: The UI kit requires React 19 or later (
>=19). React 18 is not supported — some components rely on React 19 features such as refs-as-props.
As of the dual ESM/CJS build, the
react-hook-formalias below should no longer be needed.@ssa-ui-kit/core,@ssa-ui-kit/hooks,@ssa-ui-kit/utilsand@ssa-ui-kit/widgetsnow ship a real ESM bundle (dist/index.mjs) alongside the CommonJS one, and theirexportsmap pointsimportat it. The ESM bundle emitsimport ... from 'react-hook-form'rather thanrequire('react-hook-form'), so Vite resolves a single shared instance.
The UI kit was previously published as a CommonJS-only bundle. When webpack externalised react-hook-form it emitted require('react-hook-form'), while Vite loaded the ESM build (index.esm.mjs) for the app. Two module instances meant two React contexts, so FormProvider wrote into one and useFormContext() read from the other — returning null inside UI kit components.
useFormContext() returned nullFirst check you are resolving the ESM entry (node_modules/@ssa-ui-kit/core/dist/index.mjs). If you are, the most likely cause is now duplicate copies of react-hook-form in your dependency tree rather than a module-format mismatch. This is common when the kit is consumed via npm link, file: or a workspace symlink, because the linked package resolves its own peer dependencies from its tree instead of yours. resolve.dedupe is the fix for that case:
export default defineConfig({
resolve: { dedupe: ['react', 'react-dom', 'react-hook-form'] },
});
Shipping both bundles introduces one trade-off worth knowing about. exports.import
resolves to dist/index.mjs and exports.require to dist/index.js, so a
dependency graph that reaches @ssa-ui-kit/core both ways loads both files —
and each gets its own module scope. React contexts are per-instance, so the kit's
own contexts then exist twice: a ThemeProvider from one copy is invisible to a
useTheme from the other, a PopoverTrigger cannot find the Popover wrapping
it, and so on. Most apps are wholly ESM or wholly CJS and never hit this; the
usual trigger is npm link, a file: dependency, or a workspace symlink giving
the linked package its own node_modules.
The kit detects this and warns once in development:
[ssa-ui-kit] Two copies of `@ssa-ui-kit/core` are loaded in this runtime.
Deduplicate the same way as above:
export default defineConfig({
resolve: { dedupe: ['@ssa-ui-kit/core', 'react', 'react-dom', 'react-hook-form'] },
});
Element type is invalid ... but got: object under vite devFixed. If you are on 3.20.1 or earlier, the symptom is a chart or colour-picker
component that renders fine after vite build but throws under vite dev —
often as a 404 and a reload loop, and only once the component actually mounts:
Element type is invalid: expected a string (for built-in components) or a
class/function (for composite components) but got: object.
The ESM bundle externalised two CommonJS-only packages (react-plotly.js,
@rc-component/color-picker) and imported them as import { default as X } from '…'. A plain ESM loader binds default to the whole module.exports of a
CommonJS module — __esModule is a bundler convention, not part of the interop
spec — so X arrived as { __esModule: true, default: Component }. Rollup
rewrites those dependencies during vite build, which is why only vite dev
was affected, and only for deps Vite chose not to pre-bundle.
Both are now bundled into the kit, so no consumer configuration is needed. On an older version, the workaround is to force Vite to pre-bundle them:
export default defineConfig({
optimizeDeps: {
exclude: ['@ssa-ui-kit/core'],
include: ['react-plotly.js', '@rc-component/color-picker'],
},
});
Add this alias to your vite.config.ts:
import path from 'node:path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
resolve: {
dedupe: ['react', 'react-dom', 'react-hook-form'],
alias: {
'@': path.resolve(__dirname, './src'),
// ⚠️ CRITICAL: Force CommonJS bundle for react-hook-form
// This ensures the UI kit (expecting CommonJS) uses the same instance as your app
'react-hook-form': path.resolve(
__dirname,
'./node_modules/react-hook-form/dist/index.cjs.js' // ← CommonJS bundle
),
},
},
optimizeDeps: {
include: ['react-hook-form'],
},
});
React contexts are instance-specific. Even with the same package version, if the library uses a different module instance (ESM vs CJS), the context won't be shared. Forcing the CommonJS bundle made both your app and the CJS-only UI kit use the same instance.
Related Issue: react-hook-form#8281
💡 Note: This only ever applied to Vite projects. Webpack projects did not need it.
Wrap your app with ThemeProvider from @emotion/react and pass mainTheme:
import { ThemeProvider } from '@emotion/react';
import { mainTheme, Theme as T } from '@ssa-ui-kit/core';
// TypeScript: Extend Emotion theme types (required for TypeScript projects)
declare module '@emotion/react' {
export interface Theme extends T {}
}
function App() {
return (
<ThemeProvider theme={mainTheme}>
{/* Your app components */}
</ThemeProvider>
);
}
In your main entry file (e.g., src/index.tsx), add the theme type augmentation:
import '@emotion/react';
import { Theme as T } from '@ssa-ui-kit/core';
declare module '@emotion/react' {
export interface Theme extends T {}
}
[!TIP] If you are using a monorepo or a custom Vite/Webpack setup, we recommend putting the
declare module '@emotion/react'block in a globaltypes/emotion.d.tsfile to keep your components clean.
Buttons come in multiple variants, sizes, and can include icons:
import { Button, Icon } from '@ssa-ui-kit/core';
// Basic button
<Button text="Click me" variant="primary" size="medium" />
// Button with icon
<Button
text="Save"
startIcon={<Icon name="check" size={16} />}
variant="secondary"
/>
// Variants: primary, secondary, tertiary, info, attention
// Sizes: small, medium, large
Form inputs with validation states and helper text:
import { Input } from '@ssa-ui-kit/core';
import { useForm, FormProvider } from 'react-hook-form';
function MyForm() {
const methods = useForm();
return (
<FormProvider {...methods}>
<Input
name="email"
placeholder="Enter your email"
status="basic" // or 'error', 'success'
helperText="We'll never share your email"
showHelperText
/>
</FormProvider>
);
}
Note: Input components work with react-hook-form. Wrap your form with FormProvider from react-hook-form.
Build data tables with sorting, styling, and row actions:
import {
Table,
TableHead,
TableBody,
TableRow,
TableCell,
TableCellHeader,
} from '@ssa-ui-kit/core';
function UserTable() {
const data = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'User' },
];
return (
<Table>
<TableHead>
<TableRow>
<TableCellHeader>Name</TableCellHeader>
<TableCellHeader>Email</TableCellHeader>
<TableCellHeader>Role</TableCellHeader>
</TableRow>
</TableHead>
<TableBody>
{data.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
Handle async data fetching with loading and error states:
import { useApi } from '@ssa-ui-kit/hooks';
function UserList() {
const { data, isLoading, error, query } = useApi(
async (userId: string) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
useEffect(() => {
query('123');
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data.name}</div>;
}
Toggle between multiple values (useful for boolean or multi-state toggles):
import { useToggle } from '@ssa-ui-kit/hooks';
function ToggleExample() {
// Boolean toggle (default)
const [isOpen, toggleOpen] = useToggle();
// Multi-state toggle
const [theme, toggleTheme] = useToggle(['light', 'dark', 'auto']);
return (
<div>
<button onClick={() => toggleOpen()}>
{isOpen ? 'Close' : 'Open'}
</button>
<button onClick={() => toggleTheme()}>
Theme: {theme}
</button>
</div>
);
}
Track window dimensions with reactive updates:
import { useWindowSize } from '@ssa-ui-kit/hooks';
function ResponsiveComponent() {
const { width, height } = useWindowSize();
const isMobile = width < 768;
return (
<div>
<p>Window size: {width}x{height}</p>
{isMobile ? <MobileView /> : <DesktopView />}
</div>
);
}
useClickOutside - Detect clicks outside an elementuseDebouncedCallback - Debounce function callsuseThrottledCallback - Throttle function callsuseClipboard - Copy to clipboard functionalityuseDeviceType - Detect device typeuseResizeObserver - Observe element size changesusePaginationRange - Generate pagination rangesThe project is built using:
For detailed component documentation, examples, and API references, visit our Storybook documentation.
Every published package ships an AI source map — a single-file index of its public API, generated from the TypeScript program so it can never drift from the real exports:
node_modules/@ssa-ui-kit/core/dist/ai/source-map.md
node_modules/@ssa-ui-kit/utils/dist/ai/source-map.md
node_modules/@ssa-ui-kit/hooks/dist/ai/source-map.md
node_modules/@ssa-ui-kit/widgets/dist/ai/source-map.md
node_modules/@ssa-ui-kit/templates/dist/ai/source-map.md
node_modules/@ssa-ui-kit/infra-dash/dist/ai/source-map.md
It lists every component grouped by area, each one's library-specific props with
their literal unions and defaults, plus the hooks, contexts and exported types.
The alternative is grepping ~940 .d.ts files, so pointing your agent at this
file first is considerably cheaper.
Agents can read node_modules, but their search tools skip it by default, so
they won't find the map unless told where it is. Add one line to your project's
CLAUDE.md (Claude Code) or AGENTS.md (Codex):
When using @ssa-ui-kit, read node_modules/@ssa-ui-kit/<package>/dist/ai/source-map.md
for each @ssa-ui-kit package the app depends on (core, utils, hooks, widgets,
templates, infra-dash) for the component and prop index before writing UI code.
Start from the Component roster near the top to see what exists, then search
the file for #### <ComponentName> to get that component's props. For full prop
documentation and usage examples, read the matching .d.ts under the package's
dist/ folder (e.g. dist/components/<ComponentName>/ in core).
Please refer to our CONTRIBUTING.md.
Thanks to Lost Pixel for providing the visual testing platform that helps us review UI changes and catch visual regressions.
useFilterMultiSelect |
initialOpen | defaultOpen | Popover, usePopover |
isOpen | open | Modal, ModalDialog |
isOpen | defaultOpen | Tooltip, useTooltip |
isOpened | open / defaultOpen | Accordion, AccordionContent |
Open state now follows one shape everywhere: open (controlled),
defaultOpen (uncontrolled seed) and onOpenChange (notification).
onOpenChange is new on Modal, Tooltip and Accordion, which previously
had no way to tell a parent they had opened or closed.
Run node scripts/codemods/prop-renames.mjs src to rename call sites
automatically (--dry to preview). It rewrites JSX attributes, kit hook
options (useDrawer({ defaultOpened })) and recognised item-object keys —
only where the tag or callee resolves, in that file, to an import from
@ssa-ui-kit/*, including aliased (Button as KitButton), qualified
(Drawer.Root) and namespaced (Kit.Button) imports. Components of the same
name from elsewhere are left alone, which matters because Chakra UI uses
isDisabled as its own real API on Button, Checkbox, Radio and
Switch.
⚠ Two groups are not pure renames, and the codemod flags every such site.
externalState / selectedItem / Modal.isOpen → checked / value /
open: the old props are synced into internal state, so the control still
moves on click even when the parent ignores the change callback; the
replacements are fully controlled. Confirm the parent writes the value back
— if it does not, use defaultChecked / defaultValue / defaultOpen.Tooltip.isOpen and Accordion.isOpened → defaultOpen: these read as
controlled but only ever seeded the initial state, so a changing value did
nothing. defaultOpen preserves that exactly; if you meant to control the
component, move to open + onOpenChange.