Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/dds-internal-ark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@devslab/dds-solid": minor
---

Use Ark UI internally for accessible interactions while preserving DDS public APIs and visual tokens. Add a simple RadioGroup, retained tab panels with reactive disabled navigation, toast interaction pausing, and native form reset compatibility. Acknowledge Ark UI on DDS pages.

Add optional manual tab activation, consumer element IDs, unstyled native composition, and a DDS-owned dialog frame/portal API. Authorized external focus containers participate in the modal's focus cycle and accessibility isolation, preserving retry actions without exposing Ark APIs.

Allow native section dialog content while preserving dialog attributes, presence, and focus behavior for existing consumer markup contracts.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ packages/*/dist/
storybook-static/
test-results/
playwright-report/
.playwright-cli/
.wrangler/
dist-worker/
*storybook.log
Expand Down
1 change: 1 addition & 0 deletions brand/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ <h3>버전이 고정된 원본 자산</h3>

<footer class="site">
<span class="mono">© 2026 DevsLab</span>
<span>Built with <a href="https://ark-ui.com/">Ark UI</a> · Designed by DevsLab</span>
<span>토큰 규격: DDS 규칙 명세 (devslab-kr/dds · docs/design-system.ko.md)</span>
<span><a href="https://devslab.kr">devslab.kr</a></span>
</footer>
Expand Down
44 changes: 44 additions & 0 deletions docs/ark-migration.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# DDS의 Ark UI 내부 연결

DDS는 `@devslab/dds-solid`라는 기존 패키지에서 공개 API와 디자인 토큰을 유지한다.
Ark UI는 내부 접근성·상호작용 엔진이다. 별도 래퍼 패키지나 새로운 디자인 시스템은 만들지 않는다.

| DDS API | 내부 구현 | 소비자 계약 |
| --- | --- | --- |
| Tabs / TabList / Tab / TabPanel | Ark Tabs | 문자열 콜백, 자동 키보드 선택, 비활성 패널 인스턴스 유지 |
| Dialog | Ark Dialog | boolean 콜백, 포커스 트랩·복귀, 외부 클릭·Escape 정책 |
| Tooltip | Ark Tooltip | 기존 render prop 사용; 전달한 속성을 트리거에 모두 spread |
| Checkbox / Switch | Ark의 headless hooks + DDS 네이티브 input | boolean 콜백, name/value/form/required, DDS CSS 유지 |
| Field | Ark Field + DDS control props | 기존 함수형 children, label/help/error 연결 |
| ToastProvider / useToast | Ark toaster | show/dismiss/clear, DDS tone·message, 현지화된 닫기 이름 |
| RadioGroup | Ark RadioGroup | options 배열, 문자열 선택, 폼 제출과 그룹 방향키 탐색 |
| Radio / Button / IconButton / Select | 기존 네이티브 요소 | 기존 API 유지 |

## 소비 프로젝트 적용

애플리케이션 셸에서 `@devslab/dds-solid/styles.css`를 한 번 import한다.
Ark 패키지는 DDS의 런타임 의존성이므로 소비자가 별도로 Ark API를 사용하거나 설치 설정을 추가할 필요는 없다.
상태형 콜백은 Ark의 details 객체를 노출하지 않고 DDS의 boolean/string 값을 전달한다.
router, 권한, 서버 요청과 도메인 상태는 소비 프로젝트가 담당한다.

단독 `Radio`의 checked API를 유지하기 위해 개별 라디오를 억지로 하나짜리 Ark 그룹으로 감싸지 않는다.
그룹 키보드 동작이 필요한 신규 코드에서는 `RadioGroup`을 사용한다.
`TooltipTriggerProps`는 실제 DOM 트리거 속성을 표현한다. `children`의 인자를 모두 spread하고,
기존 무인자 trigger 콜백도 유지한다. 프로그램에서 상태를 제어할 때는 `open`/`onOpenChange`를 권장한다.

ToastProvider는 보통 앱 최상위에 두며, 여러 provider의 큐와 DOM ID도 각각 분리한다. `regionLabel`과 `dismissLabel`은 소비 로케일에 맞게 주입한다.
duration이 0 이하이면 알림을 유지하고, 포인터·포커스 상호작용 및 비활성 페이지에서는 타이머를 멈춘다.

Tabs의 기본 선택 방식은 자동이며 `activationMode="manual"`이면 방향키는 포커스만 옮기고 Enter/Space로 선택한다. `tabId`/`panelId`로 접근성 ID를 지정한다. `unstyled`와 네이티브 속성을 받는 `asChild` 콜백으로 앱의 레이아웃과 버튼을 Ark import 없이 유지한다. 비활성 패널은 hidden/inert와 display 제외를 보장한다. 작업 모달과 허용된 재시도 영역은 [Dialog 프레임·외부 포커스 안내](dialog-focus.ko.md)를 참고한다.
Tabs의 hidden 패널은 인스턴스를 유지하지만 실행 중인 effect나 서버 요청을 자동 중단하지 않는다.

## 검증

`pnpm --filter @devslab/dds-solid test`, `check`, `build`와 `node --test tests/dds-solid-contracts.test.mjs`를 실행한다.
실제 브라우저 픽스처는 패키지 디렉터리에서 `pnpm exec vite --config vite.preview.config.ts`로 실행한다.
Playwright CLI 세션을 해당 페이지에 연결하고 `run-code --filename scripts/verify-ark-browser.cjs`로
탭 상태 유지·방향키·폼 제출·Dialog·Tooltip·Toast를 확인한다. 데스크톱과 모바일 크기를 각각 확인한다.
jsdom에는 ResizeObserver와 레이아웃이 없으므로 컴포넌트 테스트는 해당 환경의 한계를 명시하고 브라우저 검증을 병행한다.

홈페이지 크레딧은 `Built with Ark UI · Designed by DevsLab`로 실제 의존성을 표시한다.
이 변경은 소스 변경이며 운영 사이트 배포나 npm 발행을 의미하지 않는다.
44 changes: 44 additions & 0 deletions docs/ark-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# DDS internal Ark UI integration

DDS retains its public API and design tokens in the existing `@devslab/dds-solid` package.
Ark UI is the internal accessibility and interaction engine. No separate wrapper package or new design system is created.

| DDS API | Implementation | Consumer contract |
| --- | --- | --- |
| Tabs / TabList / Tab / TabPanel | Ark Tabs | String callbacks, automatic keyboard selection, retained inactive panel instances |
| Dialog | Ark Dialog | Boolean callbacks, focus trap/return, outside-click and Escape policies |
| Tooltip | Ark Tooltip | Existing render prop; spread all supplied attributes onto the trigger |
| Checkbox / Switch | Ark headless hooks + DDS native input | Boolean callbacks, name/value/form/required, DDS CSS |
| Field | Ark Field + DDS control props | Existing function children and label/help/error associations |
| ToastProvider / useToast | Ark toaster | show/dismiss/clear, DDS tone/message, localized dismiss names |
| RadioGroup | Ark RadioGroup | Options array, string selection, native forms and grouped arrow navigation |
| Radio / Button / IconButton / Select | Existing native elements | Existing API |

## Consumer integration

Import `@devslab/dds-solid/styles.css` once in the application shell.
Ark is a DDS runtime dependency; consumers do not need to use Ark APIs or add separate installation configuration.
State callbacks return DDS boolean/string values rather than Ark details objects.
Routing, authorization, server requests and domain state remain consumer responsibilities.

Standalone `Radio` keeps its checked API instead of becoming a one-item Ark group.
Use `RadioGroup` for new code requiring grouped keyboard navigation.
`TooltipTriggerProps` describes actual DOM trigger attributes. Spread the entire render-prop argument;
legacy zero-argument trigger callbacks remain supported. Prefer `open`/`onOpenChange` for programmatic state changes.

Normally use an application-level ToastProvider; multiple providers keep independent queues and DOM IDs. Inject localized `regionLabel` and `dismissLabel` values.
Non-positive durations persist until dismissal; pointer/focus interaction and inactive pages pause expiry.
Hidden Tabs panels preserve instances but do not automatically suspend effects or server requests.

Tabs defaults to automatic activation; `activationMode="manual"` moves focus with arrows and activates on Enter/Space. `tabId`/`panelId` customize accessible IDs. `unstyled` and native-attribute `asChild` callbacks let Tabs/Tab retain an application's layout and buttons without Ark imports. Inactive panels own hidden/inert and display exclusion. See [Dialog frames and external focus regions](dialog-focus.md) for task dialogs and authorized retry regions.

## Verification

Run `pnpm --filter @devslab/dds-solid test`, `check`, `build`, and `node --test tests/dds-solid-contracts.test.mjs`.
Start the browser fixture from the package directory with `pnpm exec vite --config vite.preview.config.ts`.
Connect a Playwright CLI session to the page and run `run-code --filename scripts/verify-ark-browser.cjs`
to verify retained tabs, arrows, native forms, Dialog, Tooltip and Toast at desktop and mobile sizes.
jsdom lacks ResizeObserver and layout; component tests document these limits and are supplemented by real-browser checks.

Homepage credits use `Built with Ark UI · Designed by DevsLab` to acknowledge the actual dependency.
These are source changes; they do not imply production deployment or npm publication.
8 changes: 8 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@

---

## D-037 — DDS API 안에서 Ark UI 동작을 사용하고 의존성을 크레딧으로 명시 (2026-10-08)

**결정.** DDS의 공개 컴포넌트 API와 시각 토큰은 유지하고, 복합 컴포넌트의 접근성·키보드·포커스 동작은 내부에서 Ark UI로 연결한다. 소비자는 DDS를 계속 import하고 Ark의 details 객체 대신 기존 DDS 콜백 값을 받는다. 네이티브 버튼과 select의 계약은 유지한다. 별도 Ark 래퍼 패키지는 만들지 않는다.

**크레딧.** 사용자의 명시적 요청에 따라 DDS 페이지에 `Built with Ark UI · Designed by DevsLab`와 공식 링크를 추가한다. Ark UI는 headless 동작 라이브러리이므로 `inspired by Ark Design`보다 실제 의존성과 디자인 소유권을 정확히 설명한다. D-016의 외부 참고 크레딧 제한은 실제 사용 의존성의 크레딧에 한해 이 결정으로 대체한다.

**검증 기준.** controlled/uncontrolled 상태, 동적 disabled, 탭 패널 인스턴스 유지, 네이티브 폼 제출, Dialog 포커스 복원과 외부 상호작용, 타이머 정리, SSR과 접근성을 확인한 후 소비 프로젝트에 적용한다. 이 결정은 npm 발행이나 운영 사이트 배포 승인을 포함하지 않는다.

## D-036 — 헤더 가운데 정렬은 들어갈 때만: 양옆 칸은 내용 아래로 줄지 않는다 (2026-10-06)

**결정.** `.site-header__inner`의 열 템플릿을 `minmax(0, 1fr) auto minmax(0, 1fr)`에서 `minmax(max-content, 1fr) auto minmax(max-content, 1fr)`로 바꾼다. 양옆(브랜드·액션)이 다 들어가면 두 fr 칸이 똑같이 나뉘어 내비는 전처럼 정확히 가운데다. 한쪽이 남은 폭의 절반보다 크면 그 칸은 자기 내용 폭을 지키고, 내비가 모자란 만큼만 반대쪽으로 비켜난다. 그래도 모자라면 내비가 줄을 바꾼다. 양보 순서는 **가운데 → 내비 한 줄**이고, 브랜드와 액션은 줄지도 덮이지도 않는다.
Expand Down
38 changes: 38 additions & 0 deletions docs/dialog-focus.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# DDS Dialog 프레임과 외부 포커스 영역

`Dialog`는 기존 prop과 기본 동작을 유지한다. 추가 표시·포커스 prop으로 앱의 업무 프레임을 유지하면서 DDS에 모달 동작을 맡길 수 있다.

- `portal`은 `document.body` 아래에 렌더링하며 `portalMount`로 다른 마운트된 요소를 지정할 수 있다. 기본값은 기존 인라인 렌더링이다.
- `role`은 `dialog` 또는 `alertdialog`를 받는다.
- `contentAs`로 네이티브 `div`(기본값) 또는 `section`을 선택한다. 생성된 모달 속성, presence 수명과 포커스 등록은 그대로 유지한다.
- `unstyled`는 DDS 컴포넌트 클래스를 제거한다. `class`, `overlayClass`, `titleClass`, `descriptionClass`, `actionsClass`로 앱 클래스를 지정한다.
- `frame(parts)`는 제공된 `title`, `description`, `children`, `actions` 노드를 배치한다. 제목·설명 노드를 각각 한 번 렌더링하면 자동 접근성 연결을 유지한다. DDS가 제공한 노드이므로 앱에서 Ark 컴포넌트를 import할 필요가 없다.
- `contentProps`로 `aria-busy`, `onKeyDown` 같은 네이티브 콘텐츠 속성·이벤트를 전달한다.
- `initialFocus`, `finalFocus`는 앱이 선택한 포커스 요소를 반환한다. 워크스페이스 화면이 비활성화되면 `restoreFocus={false}`로 포커스 복귀를 막을 수 있다.
- `onEscapeKeyDown(event)`는 네이티브 키보드 이벤트를 받는다. 작업 중 `event.preventDefault()`를 호출하면 Escape 닫기를 취소한다. 기존 `closeOnEscape`, `closeOnOutside` 정책도 유지한다.

`additionalFocusContainers`는 외부 DOM 영역을 모달의 Tab 순환과 접근성 범위에 명시적으로 포함한다. 재시도 토스트를 키보드로 사용할 수 있게 하면서 나머지 배경은 숨긴다.

```tsx
<Dialog
open={taskOpen() && pageActive()}
onOpenChange={setTaskOpen}
title="작업 저장"
portal
closeOnOutside={false}
initialFocus={() => firstField}
finalFocus={() => usableOpener() ?? fallbackControl()}
restoreFocus={pageActive()}
onEscapeKeyDown={(event) => { if (busy()) event.preventDefault(); }}
additionalFocusContainers={() => [toastRegion]}
actions={<button onClick={save}>저장</button>}
>
<input ref={firstField} aria-label="작업 이름" />
</Dialog>
```

같은 document의 의도한 영역만 허용한다. getter에서 반응형 참조를 읽으면 참조 변경에 따라 컨테이너 목록도 갱신된다. 이미 있는 영역에 재시도 버튼을 추가·제거하는 동작은 포커스 트랩이 처리한다. 기존 수동 dialog-to-toast Tab 브리지는 필요하지 않다. 허용된 재시도 영역 클릭은 외부 클릭으로 취급하지 않는다. 영역 자체를 제거할 때는 반응형 참조/getter도 갱신해야 한다.

브라우저 전용 포커스·컨테이너 getter는 SSR에서 실행하지 않는다. Portal 콘텐츠는 클라이언트에서 마운트된다. 일반 모달은 Ark의 포커스·접근성 격리를 유지하고, 외부 영역을 허용한 모달은 Zag의 다중 컨테이너 트랩과 공유 격리 처리를 사용한다.

DOM 테스트는 네이티브 section의 presence·닫기, Tab/Shift+Tab 순환, 배경 격리, 포커스 복귀, 사용자 프레임·Portal, Escape 취소를 검증한다. SSR 테스트는 section 렌더링과 실행 시 예외를 내는 브라우저 전용 getter로 서버 실행 여부를 확인한다. 실제 브라우저 회귀 검증은 `node packages/dds-solid/src/__tests__/dialog-browser.test.mjs`로 실행한다. section 모달이 열린 뒤 재시도 버튼과 반응형 외부 팝업 영역을 추가하고 최초 포커스를 다시 설정하지 않는지, 실제 포커스 이동·허용 영역 클릭·작업 중 Escape·닫기 후 격리 정리를 확인한다.
38 changes: 38 additions & 0 deletions docs/dialog-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# DDS Dialog frames and external focus regions

`Dialog` keeps its existing props and defaults. Its optional presentation and focus props let an application keep its own task frame while DDS owns dialog interaction.

- `portal` renders under `document.body`; `portalMount` chooses another mounted element. The default remains inline rendering.
- `role` accepts `dialog` or `alertdialog`.
- `contentAs` selects the native `div` (default) or `section` while keeping the generated dialog attributes, presence lifecycle, and focus registration.
- `unstyled` removes DDS component classes. `class`, `overlayClass`, `titleClass`, `descriptionClass`, and `actionsClass` add application classes.
- `frame(parts)` arranges the supplied `title`, `description`, `children`, and `actions` nodes. Render each supplied title/description once to retain its generated accessible association. These are DDS-owned nodes; applications do not import Ark components.
- `contentProps` supplies native content attributes and handlers, such as `aria-busy` and `onKeyDown`.
- `initialFocus` and `finalFocus` return application-selected focus targets. `restoreFocus={false}` suppresses restoration when a workspace page or task is no longer active.
- `onEscapeKeyDown(event)` receives the native keyboard event. `event.preventDefault()` cancels Escape dismissal, for example while an application is busy. `closeOnEscape` and `closeOnOutside` keep their existing policies.

`additionalFocusContainers` explicitly authorizes external DOM regions to join the modal's focus cycle **and** its accessibility scope. A retry toast can then remain keyboard reachable without exposing the rest of the background:

```tsx
<Dialog
open={taskOpen() && pageActive()}
onOpenChange={setTaskOpen}
title="Save task"
portal
closeOnOutside={false}
initialFocus={() => firstField}
finalFocus={() => usableOpener() ?? fallbackControl()}
restoreFocus={pageActive()}
onEscapeKeyDown={(event) => { if (busy()) event.preventDefault(); }}
additionalFocusContainers={() => [toastRegion]}
actions={<button onClick={save}>Save</button>}
>
<input ref={firstField} aria-label="Task name" />
</Dialog>
```

Authorize only the intended regions in the same document. The getter can read reactive references; changes update the container list. Adding/removing retry controls inside an existing region is handled by the focus trap. No manual dialog-to-toast Tab bridge is needed. An authorized retry click does not count as outside interaction. Removing an authorized region should update its reference/reactive getter.

Browser-only focus/container getters are not evaluated during SSR. The portalled content mounts on the client. Normal dialogs retain Ark's focus and isolation behavior; external-scope dialogs reuse Zag's multi-container trap and shared modal isolation bookkeeping.

Validation: DOM tests cover native section presence/dismissal, the combined Tab/Shift+Tab cycle, background isolation, restoration, custom frame/portal and Escape cancellation. SSR tests cover native sections and throwing browser-only getters. Run the real-browser regression with `node packages/dds-solid/src/__tests__/dialog-browser.test.mjs`; it additionally mounts a section, adds retry controls and a reactive external popup region after opening without resetting focus, and checks real focus, authorized clicks, busy Escape and isolation cleanup.
Loading
Loading