실제 제품을 적절한 조작 환경에서 실행해 변경 사항을 검증하고, 사용자가 확인할 수 있는 증거를 모아 인수 보고서로 게시합니다. 기존 검증 계획을 확인하거나 점검 항목을 만들고, `lh` CLI로 결과를 게시할 수 있습니다.
- 변경된 제품 동작을 실제로 확인하기
- 스크린샷 등 검증 증거 수집하기
- 검증 계획과 보고서 작성하기
React와 TSX 화면을 구현할 때 기존 UI 라이브러리와 스타일 규칙에 맞는 구성 요소를 고르고, 상태와 레이아웃을 배치하며 메모이제이션이 필요한지 판단하도록 안내합니다.
Use for TSX components, UI libraries, styling, state locality, layout, render performance and memoization.
실행 점수 미검증
설치 방법 보기이 가이드는 React나 TSX로 화면을 구현할 때 프로젝트의 기존 UI 라이브러리와 스타일 규칙을 따르도록 돕습니다. 사용할 구성 요소를 고르고 스타일과 레이아웃을 정리하며, 상태를 필요한 범위가 가장 작은 구성 요소에 두는 방법을 설명합니다. StyleX로 스타일을 옮기는 방침과 마이그레이션 전후의 스타일 차이를 확인하는 방법도 다룹니다. 렌더링 성능을 위해 memo, useMemo, useCallback을 기본으로 추가하지 않고, 실제로 줄일 수 있는 작업을 확인한 뒤 사용하도록 안내합니다. 재사용이나 업데이트 경계처럼 분명한 이유가 있을 때 구성 요소를 나눕니다.
기존 UI 패턴에 맞는 새 화면 구성 요소를 만들거나 상태를 어디에 둘지 고민할 때 참고할 수 있습니다. 렌더링이 느린 부분을 조사하고 최적화할 때도 유용합니다. 복잡한 레이아웃에서는 Flexbox와 Center의 사용법을 확인할 수 있습니다.
구성 요소 선택과 마이그레이션 절차는 @lobehub/ui, StyleX 등 프로젝트별 구성에 근거합니다. 자세한 레이아웃 예시는 references/layout-kit.md에서 확인합니다.
이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.
AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.
점수에는 넣지 않았습니다. 2026년 10월 26일 기준으로 AI(OpenAI Codex)가 판정했습니다. 참고용이므로 설치 전에 원문도 확인해 주세요.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
| Scenario | Approach |
|---|---|
| Files already on StyleX, and new SPA styles | stylex.create + @lobehub/ui/stylex/cssVar.stylex / @lobehub/ui/stylex/media.stylex. The SPA compiler is plugins/vite/stylex.ts (@stylexjs/unplugin, prefix lh). Do not copy those token modules into the app. |
| Existing styles not yet migrated | createStaticStyles + cssVar.* (zero-runtime, module-level) |
| Simple one-off | Inline style attribute |
Truly dynamic (JS color fns like readableColor/chroma) | createStyles + token — last resort |
createStaticStyles / createStyles / css / keyframes / createGlobalStyle / injectGlobal from @lobehub/ui (stylex-migration/no-emotion-styles). Files still on them are listed in scripts/stylexMigration/legacyFiles.json; after migrating one, run bun run stylex:migration --prune-legacy. Never add entries..x:not(#\#)) and beat emotion classes on the same element, but StyleX shorthand rules do not, so a half-migrated element resolves by property.className / style: {...sx([styles.root, active && styles.active], className, style)} from @/styles/sx. Spreading stylex.props next to className fails @stylexjs/no-conflicting-props.xstyle?: StyleXStyles and pass it last in the sx / stylex.props array. A plain className from outside cannot override StyleX rules. Passing a class to @lobehub/ui components (className, classNames) works, because lobe-ui CSS sits in @layer lobe-ui.@stylexjs/valid-styles: no multi-value shorthands (margin: '0 8px' → marginBlock / marginInline), and string flex (flex: '1').background, animation, border and border{Top,Right,Bottom,Left} shorthands compile to no class at all, whatever the value (border: 'none' included), without an error. Write longhands: backgroundColor (plus backgroundImage: 'none' when replacing a background shorthand), animationName / animationDuration / …, borderWidth / borderStyle / borderColor.__lobeStyleSnapshot.capture('<name>', '<root selector>'), swap the files back (git stash push -- <path>), reload, run __lobeStyleSnapshot.compare('<name>'), then git stash pop. Each pair reads [captured, current], here [migrated, original]. It lists elements and ::before / ::after whose tracked computed styles, box (relative to the root) or text differ; an empty list means those match, not that hover, focus or closed dialogs were checked, so open each state you migrated and compare it too. Keep the window size, scroll everything under the root back to the start, and move the pointer off the compared area.bun run stylex:migration --list <A|B|C|D> lists files by how hard they are to move: A flat, B self pseudo-classes, C descendant / ancestor coupling (use stylex.defaultMarker() + stylex.when.ancestor() or xstyle), D tag / third-party DOM selectors (scoped plain CSS).src/components — project-specific reusable components@lobehub/ui/base-ui — headless primitives. If the component lives here, use it. Do NOT import the same-named root export.@lobehub/ui — higher-level / antd-wrapping components (only when no base-ui equivalent)@lobehub/ui root provides itIf unsure about available components, search existing code or check node_modules/@lobehub/ui/es/index.mjs and node_modules/@lobehub/ui/es/base-ui/.
@lobehub/ui/base-ui — always prefer for these| Component | Import |
|---|---|
Alert (+ AlertProps) | import { Alert, type AlertProps } from '@lobehub/ui/base-ui'; |
Select (+ SelectProps, SelectOption) | import { Select } from '@lobehub/ui/base-ui'; |
Modal (imperative API) | import { createModal, confirmModal, useModalContext, type ModalInstance } from '@lobehub/ui/base-ui'; |
DropdownMenu | import { DropdownMenu } from '@lobehub/ui/base-ui'; |
ContextMenu | import { ContextMenu } from '@lobehub/ui/base-ui'; |
Popover | import { Popover } from '@lobehub/ui/base-ui'; |
ScrollArea | import { ScrollArea } from '@lobehub/ui/base-ui'; |
Switch | import { Switch } from '@lobehub/ui/base-ui'; |
Toast | import { Toast } from '@lobehub/ui/base-ui'; |
FloatingSheet | import { FloatingSheet } from '@lobehub/ui/base-ui'; |
Drawer | import { Drawer } from '@lobehub/ui/base-ui'; |
For Modal specifically, see the dedicated modal skill — use the imperative createModal({ content: … }) pattern over the legacy <Modal open … /> declarative pattern. base-ui has its own ModalHost already mounted in SPAGlobalProvider.
Common slip:
import { Select } from '@lobehub/ui'looks fine but it's the antd-backed Select. Use base-ui Select. Same forModal,DropdownMenu, etc.
@lobehub/ui root — use when base-ui has no equivalent| Category | Components |
|---|---|
| General | ActionIcon, ActionIconGroup, Block, Button, Icon |
| Data Display | Avatar, Collapse, Empty, Highlighter, Markdown, Tag, Tooltip |
| Data Entry | CodeEditor, CopyButton, EditableText, Form, Input, InputPassword, SearchBar, TextArea |
| Layout | Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow |
| Navigation | Burger, Menu, SideNav, Tabs |
Keep transient state in its smallest useful owner. Extract a custom hook when state transitions and handlers obscure rendering or form a reusable unit; do not extract solely because a component has a particular number of hooks.
Split a component only to establish a real state, reuse, render-update, or mountable-capability boundary. Do not split solely to make files smaller; when a file reaches the 1200-line limit in source-layout, find one of these boundaries rather than cutting at a line count. Decomposing a heavy domain feature into host-assembled atoms is owned by compose-atoms.
An action inside popover content that leaves the popover must close it, or the popover stays over the destination (for example, a plan badge in the user panel navigating to /settings/plans).
open state, calls useCloseOnNavigate(open, () => setOpen(false)) from @/hooks/useCloseOnNavigate, and passes the returned key to its Popover. This covers links added later, including links from other files. The key matters when navigation hides the host in an <Activity> (the home sidebar): a hidden tree sets up no new effects, so the base-ui popup never finishes its exit animation and stays on screen until a remount removes it.usePopoverContext().close() (or the host's setter) before opening the modal or after the pick, as NavPanel/switcher/SwitcherMenu.tsx does. Content rendered outside a popover cannot use the context, because its default close throws in development.Treat memo, useMemo, and useCallback as opt-in optimizations, not default component wrappers. Before adding one, identify the actual rerender boundary and prefer structural fixes:
Do not memoize prop-free or trivially rendered components, or a component that normally receives new objects, arrays, functions, or JSX children. Do not use memoization to compensate for state held too high in the tree.
Use memoization only when the subtree is demonstrably expensive or frequently repeated, its relevant inputs are stable during normal parent renders, and profiling or a concrete render-path analysis identifies the avoided work. State that reason in the implementation summary or PR.
Use Flexbox and Center from @lobehub/ui. See references/layout-kit.md for full props and examples.
gap instead of margin for spacing between flex childrenflex={1} to fill available spaceoverflow: 'auto' for scrollable regionsux: loading visuals (scenario → loader table in references/feedback.md §4.1) and user-facing interaction design.modal: imperative base-ui modal patterns.spa-routes: SPA navigation, route ownership, router configuration, and .desktop variants.compose-atoms: split a heavy domain feature into mountable capability atoms; each host imports only what it mounts.zustand: store structure and selector conventions.아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
실제 제품을 적절한 조작 환경에서 실행해 변경 사항을 검증하고, 사용자가 확인할 수 있는 증거를 모아 인수 보고서로 게시합니다. 기존 검증 계획을 확인하거나 점검 항목을 만들고, `lh` CLI로 결과를 게시할 수 있습니다.
LobeHub에 AI 모델 제공자를 추가하는 과정을 안내합니다. 런타임 연동, 모델 정보와 설정, 브랜드 표시, 문서화 및 인수 확인을 다루며, 기존 제공자에 모델만 추가하는 경우와 새 연동이 필요한 경우를 구분합니다.
서버 환경 변수로 사용자 설정의 기본값을 지정하도록 구현하는 절차를 안내합니다. 변수 검증, 서버 설정 구성, 사용자 저장소 반영부터 예시 환경 파일과 셀프 호스팅 문서 갱신까지 다룹니다.
에이전트 실행 단계, 도구 호출, 사람의 승인, 컨텍스트 압축, 하위 에이전트 호출에 훅을 등록하는 방법을 안내합니다. 이벤트를 관찰하고 로컬 도구 결과를 모의하거나 설정에 따라 웹훅으로 도구 호출을 허용 또는 거부할 수 있습니다.
에이전트의 이벤트 기반 백그라운드 작업을 설계하고 구현하도록 돕습니다. 소스, 시그널, 액션, 정책의 역할을 나누고 비동기 처리, 중복 실행 방지, 완료 기록과 관측 기능을 다룹니다.
실제 데스크톱 채팅 앱에서 봇 채널의 인수 테스트를 진행합니다. Discord, Slack, Telegram, WeChat, Lark, QQ에서 메시지를 보내고 응답을 기다린 뒤 결과 화면을 캡처합니다. iMessage는 BlueBubbles 브리지를 사용합니다.