尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證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.