Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cda60840d8 | ||
|
|
75fdcb4775 | ||
|
|
22fef414f5 | ||
|
|
f5ae748573 | ||
|
|
e555afc64f | ||
|
|
c099b9eff3 |
No files matched your search
@@ -4,7 +4,7 @@ Designer Playground는 내부 개발용 `internal` 태그로 기본 검수 메
|
|||||||
|
|
||||||
1. 처음 열면 `themes/default.json` 기반 HM Default 컬러가 표시됩니다. **Load Default JSON**, **Load Service Example JSON**으로 repository JSON을 URL에서 불러오거나 **Theme JSON URL**에 CDN URL을 입력한 뒤 **Load URL**을 누릅니다. 같은 탭에 저장값이 있다면 마지막 유효 Theme를 다시 표시합니다. 다른 Story로 이동해도 편집 데이터는 세션에 보관되지만 해당 Story의 색상은 HM Default이며, 편집기로 돌아오면 보관된 편집값을 다시 적용합니다. **Reset to HM Default**로 기본값을 다시 불러옵니다.
|
1. 처음 열면 `themes/default.json` 기반 HM Default 컬러가 표시됩니다. **Load Default JSON**, **Load Service Example JSON**으로 repository JSON을 URL에서 불러오거나 **Theme JSON URL**에 CDN URL을 입력한 뒤 **Load URL**을 누릅니다. 같은 탭에 저장값이 있다면 마지막 유효 Theme를 다시 표시합니다. 다른 Story로 이동해도 편집 데이터는 세션에 보관되지만 해당 Story의 색상은 HM Default이며, 편집기로 돌아오면 보관된 편집값을 다시 적용합니다. **Reset to HM Default**로 기본값을 다시 불러옵니다.
|
||||||
2. Primary / Active / Focus / Accent / Destructive 등 역할의 컬러 피커 또는 HEX 입력을 수정합니다. 불투명 6자리 HEX(`#123abc`)를 사용합니다. 유효한 값은 입력 즉시 적용되며 별도의 Apply 버튼이 필요 없습니다. 미완성 입력은 마지막 유효 색을 유지합니다.
|
2. Primary / Active / Focus / Accent / Destructive 등 역할의 컬러 피커 또는 HEX 입력을 수정합니다. 불투명 6자리 HEX(`#123abc`)를 사용합니다. 유효한 값은 입력 즉시 적용되며 별도의 Apply 버튼이 필요 없습니다. 미완성 입력은 마지막 유효 색을 유지합니다.
|
||||||
3. 오른쪽 Live Preview에서 여러 컴포넌트를 함께 확인합니다. 화면이 좁으면 편집기 아래에 Preview가 이어집니다. Focus 확인은 Tab 키로 Checkbox/Input 등에 이동합니다. Select, Date Picker, Combobox 및 메뉴를 열면 Global Theme가 팝업에도 적용됩니다.
|
3. 오른쪽 실시간 미리보기에서 여러 컴포넌트를 함께 확인합니다. 화면이 좁으면 편집기 아래에 미리보기가 이어집니다. Focus 확인은 Tab 키로 Checkbox/Input 등에 이동합니다. Select, Date Picker, Combobox 및 메뉴를 열면 Global Theme가 팝업에도 적용됩니다.
|
||||||
4. 임시 Theme는 Designer Playground 내부에만 적용됩니다. 다른 Component / Pattern Story로 이동하면 override를 해제하고 HM Default로 검수합니다. 편집기 내부 값 변경에는 reload, 파일 저장, Vite HMR, build, Storybook 재시작이 필요 없습니다.
|
4. 임시 Theme는 Designer Playground 내부에만 적용됩니다. 다른 Component / Pattern Story로 이동하면 override를 해제하고 HM Default로 검수합니다. 편집기 내부 값 변경에는 reload, 파일 저장, Vite HMR, build, Storybook 재시작이 필요 없습니다.
|
||||||
5. **Reset to HM Default**는 현재 override를 해제하고 기본값을 즉시 복원합니다. 컴포넌트 API와 레이아웃은 바꾸지 않습니다.
|
5. **Reset to HM Default**는 현재 override를 해제하고 기본값을 즉시 복원합니다. 컴포넌트 API와 레이아웃은 바꾸지 않습니다.
|
||||||
6. **Show JSON**으로 현재 데이터를 표시하고 **Copy JSON**으로 복사하거나 **Export JSON**으로 `hm-color-theme.json`을 내려받습니다. 클립보드 접근이 막히면 JSON 입력란에서 직접 복사합니다. 수정한 키만 저장하므로 나머지는 HM Default입니다.
|
6. **Show JSON**으로 현재 데이터를 표시하고 **Copy JSON**으로 복사하거나 **Export JSON**으로 `hm-color-theme.json`을 내려받습니다. 클립보드 접근이 막히면 JSON 입력란에서 직접 복사합니다. 수정한 키만 저장하므로 나머지는 HM Default입니다.
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table
|
|||||||
|
|
||||||
## 9. Storybook
|
## 9. Storybook
|
||||||
|
|
||||||
- `Patterns/Data/Data Table` → `Data Table`(대표: `Advanced Search` 구역 하나. 추가 조건 목록 검색 + 선택·정렬·열 표시·행 액션·페이지 크기·페이지), 숨김 verification Story `Overview`(기본 표, `patterns-data-data-table--overview`), `Basic Search`(기본 목록 검색, `patterns-data-data-table--basic-search`), `With Filter Bar`(추가 조건 연동 검증). 목록 검색 구역은 흰 Card 하나에 CardTitle(`사용자 목록`) → 검색·열 표시 도구 → 표 순서다. 페이지 제목 PageHeader를 쓰는 화면에서는 PageHeader를 이 Card 밖에 둔다. verification: `WithFilterBar`, `Loading`, `Empty`.
|
- `Patterns/Data/Data Table` → `Data Table`(대표: `고급 검색` 구역 하나. 추가 조건 목록 검색 + 선택·정렬·열 표시·행 액션·페이지 크기·페이지), 숨김 verification Story `Overview`(기본 표, `patterns-data-data-table--overview`), `Basic Search`(기본 목록 검색, `patterns-data-data-table--basic-search`), `With Filter Bar`(추가 조건 연동 검증). 목록 검색 구역은 흰 Card 하나에 CardTitle(`사용자 목록`) → 검색·열 표시 도구 → 표 순서다. 페이지 제목 PageHeader를 쓰는 화면에서는 PageHeader를 이 Card 밖에 둔다. verification: `WithFilterBar`, `Loading`, `Empty`.
|
||||||
- 샘플 데이터는 `src/patterns/sample-users.ts`(47행, 네트워크 없음).
|
- 샘플 데이터는 `src/patterns/sample-users.ts`(47행, 네트워크 없음).
|
||||||
|
|
||||||
## 10. Accessibility
|
## 10. Accessibility
|
||||||
|
|||||||
@@ -51,11 +51,26 @@ await goto('sw-header--overview')
|
|||||||
const docButton = page.locator('.hm-sw-header__document-button').first()
|
const docButton = page.locator('.hm-sw-header__document-button').first()
|
||||||
await stableOnHover('Header file name', docButton, docButton.locator('.hm-sw-header__document-name'))
|
await stableOnHover('Header file name', docButton, docButton.locator('.hm-sw-header__document-name'))
|
||||||
// SWHeader More (narrow): fields moved into the More menu are still SWHeader fields (400).
|
// SWHeader More (narrow): fields moved into the More menu are still SWHeader fields (400).
|
||||||
await goto('sw-header--responsive-more')
|
// Only below 760px does the header move fields into More (sw.css `@container hm-sw-header (max-width: 759.98px)`: 선/문자/치수
|
||||||
const narrow = page.locator('.hm-sw-header').filter({ visible: true }).nth(2)
|
// 스타일, 속성Bar / 속성창, 색상; 인쇄 / 팝업 가이드 only below 640). From 760px the More button is hidden and the 화면 너비 Story
|
||||||
|
// rows stop at 760, so the 760 frame is narrowed to 700px, the way the Viewer More check below narrows its header.
|
||||||
|
const narrowHeaderMore = async () => {
|
||||||
|
await goto('sw-header--responsive-more')
|
||||||
|
const frame = page.locator('[data-frame-width="760"]')
|
||||||
|
await frame.evaluate(e => { e.style.maxWidth = '700px' }); await page.waitForTimeout(200)
|
||||||
|
return frame.locator('.hm-sw-header')
|
||||||
|
}
|
||||||
|
const narrow = await narrowHeaderMore()
|
||||||
|
const narrowWidth = await narrow.evaluate(e => Math.round(e.getBoundingClientRect().width))
|
||||||
const moreTrigger = narrow.locator('.hm-sw-header__more-trigger')
|
const moreTrigger = narrow.locator('.hm-sw-header__more-trigger')
|
||||||
if (await moreTrigger.count()) { await moreTrigger.click(); await page.waitForTimeout(200) }
|
check('Header More: the 700px header shows the More button', narrowWidth === 700 && await moreTrigger.isVisible(), { narrowWidth })
|
||||||
const moreValues = await styles('.hm-sw-header__field-value')
|
const movedOut = await narrow.locator('.hm-sw-header__fields > :is([data-tier="aux"], [data-menu="color"])').evaluateAll(list => list.map(e => getComputedStyle(e).display))
|
||||||
|
check('Header More: 선/문자/치수 스타일 and 색상 leave the 700px header', movedOut.length === 4 && movedOut.every(d => d === 'none'), movedOut)
|
||||||
|
await moreTrigger.click(); await page.waitForTimeout(200)
|
||||||
|
const moreLabels = (await styles('[data-frame-width="760"] .hm-sw-header__more-item > span:first-child')).map(s => s.text)
|
||||||
|
const wantMore = ['색상', '선스타일', '문자스타일', '치수스타일', '속성Bar', '속성창']
|
||||||
|
check('Header More: the menu lists 색상 · 선/문자/치수 스타일 · 속성Bar · 속성창, not 인쇄 / 팝업 가이드 (below 640 only)', wantMore.every(w => moreLabels.includes(w)) && !moreLabels.includes('인쇄') && !moreLabels.includes('팝업 가이드'), moreLabels)
|
||||||
|
const moreValues = await styles('[data-frame-width="760"] :is(.hm-sw-header__field-value, .hm-sw-header__more-value)')
|
||||||
check('Header More: values shown with the More menu open stay 400 (none turn Bold)', moreValues.length > 0 && moreValues.every(v => v.weight === '400'), moreValues.filter(v => v.weight !== '400'))
|
check('Header More: values shown with the More menu open stay 400 (none turn Bold)', moreValues.length > 0 && moreValues.every(v => v.weight === '400'), moreValues.filter(v => v.weight !== '400'))
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
// Composed Controls: standalone menu fields (outside SWHeader) are Bold; labels and the font sample unchanged.
|
// Composed Controls: standalone menu fields (outside SWHeader) are Bold; labels and the font sample unchanged.
|
||||||
@@ -118,9 +133,8 @@ await page.screenshot({ path: path.join(out, 'footer.png'), clip: { x: 0, y: 940
|
|||||||
// SWHeader file-name button), 레이어 · 설정 · 속성 and Model / Layout; the two menus follow the SWHeader rules, measured
|
// SWHeader file-name button), 레이어 · 설정 · 속성 and Model / Layout; the two menus follow the SWHeader rules, measured
|
||||||
// on SWHeader itself below and compared row by row.
|
// on SWHeader itself below and compared row by row.
|
||||||
const typo = s => ({ weight: s.weight, size: s.size, family: s.family, outline: s.outline })
|
const typo = s => ({ weight: s.weight, size: s.size, family: s.family, outline: s.outline })
|
||||||
// SWHeader references: the responsive `...` menu rows and the file-name list rows.
|
// SWHeader references: the responsive `...` menu rows (the same 700px header: More shows only below 760) and the file-name list rows.
|
||||||
await goto('sw-header--responsive-more')
|
const refTrigger = (await narrowHeaderMore()).locator('.hm-sw-header__more-trigger')
|
||||||
const refTrigger = page.locator('.hm-sw-header__more-trigger').filter({ visible: true }).first()
|
|
||||||
await refTrigger.click(); await page.waitForTimeout(200)
|
await refTrigger.click(); await page.waitForTimeout(200)
|
||||||
const refMore = await styles('.hm-sw-header__more-menu .hm-sw-header__more-item > span:first-child')
|
const refMore = await styles('.hm-sw-header__more-menu .hm-sw-header__more-item > span:first-child')
|
||||||
check('SWHeader ... menu rows (reference): Regular 400 without outline', refMore.length > 0 && refMore.every(r => r.weight === '400' && r.outline === 'none'), refMore)
|
check('SWHeader ... menu rows (reference): Regular 400 without outline', refMore.length > 0 && refMore.every(r => r.weight === '400' && r.outline === 'none'), refMore)
|
||||||
@@ -172,7 +186,9 @@ await stableOnHover('Viewer 레이어', lab.locator('xpath=ancestor::button[1]')
|
|||||||
const tab = vh.locator('.hm-sw-viewer-header__tab', { hasText: 'Layout' }).first()
|
const tab = vh.locator('.hm-sw-viewer-header__tab', { hasText: 'Layout' }).first()
|
||||||
await stableOnHover('Viewer Layout tab', tab, tab)
|
await stableOnHover('Viewer Layout tab', tab, tab)
|
||||||
// More stays: narrow header shows the More button, it opens and lists the moved items.
|
// More stays: narrow header shows the More button, it opens and lists the moved items.
|
||||||
await vh.evaluate(e => { e.style.width = '700px' })
|
// 2026-10-26 (cabd2f6): the viewer header keeps every menu down to 520px (measured steps) and moves items into More only
|
||||||
|
// below that, so the header is narrowed to 400px as in viewer.browser.mjs (the 520 row narrowed to 400).
|
||||||
|
await vh.evaluate(e => { e.style.width = '400px' })
|
||||||
const more = vh.getByRole('button', { name: /추가 도구/ })
|
const more = vh.getByRole('button', { name: /추가 도구/ })
|
||||||
// 2026-10-20: the header re-measures through a ResizeObserver; wait for More instead of a fixed 200ms (the check below
|
// 2026-10-20: the header re-measures through a ResizeObserver; wait for More instead of a fixed 200ms (the check below
|
||||||
// still fails when it never appears). The fixed wait failed intermittently on the same build.
|
// still fails when it never appears). The fixed wait failed intermittently on the same build.
|
||||||
@@ -203,10 +219,11 @@ const rowBefore = await rowGeo()
|
|||||||
await stableOnHover('Recent file name', row, row.locator('.hm-sw-recent-files__name'))
|
await stableOnHover('Recent file name', row, row.locator('.hm-sw-recent-files__name'))
|
||||||
await row.hover(); await page.waitForTimeout(250)
|
await row.hover(); await page.waitForTimeout(250)
|
||||||
const preview = await page.locator('.hm-sw-recent-files__preview').evaluate(p => { const t = p.querySelector('.hm-sw-recent-files__preview-title'), c = getComputedStyle(t), b = p.getBoundingClientRect(), body = p.querySelector('.hm-sw-recent-files__preview-body').getBoundingClientRect()
|
const preview = await page.locator('.hm-sw-recent-files__preview').evaluate(p => { const t = p.querySelector('.hm-sw-recent-files__preview-title'), c = getComputedStyle(t), b = p.getBoundingClientRect(), body = p.querySelector('.hm-sw-recent-files__preview-body').getBoundingClientRect()
|
||||||
return { title: t.textContent, weight: c.fontWeight, size: c.fontSize, color: c.color, outline: c.textShadow.split(', rgb')[0], titleW: t.getBoundingClientRect().width, overflow: c.textOverflow, w: b.width, h: b.height, bodyH: body.height, bodyEmpty: p.querySelector('.hm-sw-recent-files__preview-body').childElementCount === 0 && !p.querySelector('.hm-sw-recent-files__preview-body').textContent } })
|
return { title: t.textContent, weight: c.fontWeight, size: c.fontSize, color: c.color, outline: c.textShadow.split(', rgb')[0], titleW: t.getBoundingClientRect().width, overflow: c.textOverflow, w: b.width, h: b.height, bodyH: body.height, bodyContent: [...p.querySelector('.hm-sw-recent-files__preview-body').children].map(e => `${e.tagName.toLowerCase()}.${e.getAttribute('class')}`), bodyImages: p.querySelectorAll('.hm-sw-recent-files__preview-body img').length } })
|
||||||
report.preview = preview
|
report.preview = preview
|
||||||
// 2026-10-21 (user decision): the preview frame stays on hover; its 243px body is empty (no image / thumbnail).
|
// 2026-10-21 (user decision): the preview frame stays on hover; its 243px body shows only the red no-preview sign
|
||||||
check('Recent preview: title 14px 700 + 1.3px outline, width 294 with ellipsis, empty body 243px', preview.bodyEmpty && preview.weight === '700' && preview.size === '14px' && preview.outline === OUTLINE_14 && preview.titleW === 294 && preview.overflow === 'ellipsis' && preview.bodyH === 243, preview)
|
// (DESIGN_RULES 최근파일 패널), never an image / thumbnail.
|
||||||
|
check('Recent preview: title 14px 700 + 1.3px outline, width 294 with ellipsis, body 243px with only the no-preview sign', JSON.stringify(preview.bodyContent) === '["svg.hm-sw-recent-files__preview-none"]' && preview.bodyImages === 0 && preview.weight === '700' && preview.size === '14px' && preview.outline === OUTLINE_14 && preview.titleW === 294 && preview.overflow === 'ellipsis' && preview.bodyH === 243, preview)
|
||||||
await page.screenshot({ path: path.join(out, 'recent-files.png'), clip: { x: 0, y: 0, width: 1100, height: 900 } })
|
await page.screenshot({ path: path.join(out, 'recent-files.png'), clip: { x: 0, y: 0, width: 1100, height: 900 } })
|
||||||
check('Recent files: rows keep 50px and their text widths', rowBefore === await rowGeo() && rowBefore.startsWith('50,'), rowBefore)
|
check('Recent files: rows keep 50px and their text widths', rowBefore === await rowGeo() && rowBefore.startsWith('50,'), rowBefore)
|
||||||
await row.press('Enter').catch(() => {})
|
await row.press('Enter').catch(() => {})
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export const All: Story = {
|
|||||||
render: () => (
|
render: () => (
|
||||||
<StoryPage>
|
<StoryPage>
|
||||||
<StoryPageHeader title="Data Table" />
|
<StoryPageHeader title="Data Table" />
|
||||||
<StorySection title="Advanced Search"><WithFilterBar /></StorySection>
|
<StorySection title="고급 검색"><WithFilterBar /></StorySection>
|
||||||
</StoryPage>
|
</StoryPage>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,13 +112,13 @@ function ThemeEditor() {
|
|||||||
return <StoryPage><main className="hm-designer">
|
return <StoryPage><main className="hm-designer">
|
||||||
<header className="hm-designer-header">
|
<header className="hm-designer-header">
|
||||||
<div><p>HM DESIGN SYSTEM / COLOR THEME</p><h1>디자인팀 Theme Playground</h1>
|
<div><p>HM DESIGN SYSTEM / COLOR THEME</p><h1>디자인팀 Theme Playground</h1>
|
||||||
<p>임시 컬러는 이 편집기와 Live Preview에만 적용됩니다. 편집값은 현재 탭의 세션 동안 보관하며 다른 Story는 HM Default로 검수합니다.</p></div>
|
<p>임시 컬러는 이 편집기와 실시간 미리보기에만 적용됩니다. 편집값은 현재 탭의 세션 동안 보관하며 다른 Story는 HM Default로 검수합니다.</p></div>
|
||||||
<button type="button" onClick={reset}>Reset to HM Default</button>
|
<button type="button" onClick={reset}>Reset to HM Default</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="hm-designer-layout">
|
<div className="hm-designer-layout">
|
||||||
<section className="hm-designer-editor" aria-label="Theme color editor">
|
<section className="hm-designer-editor" aria-label="Theme color editor">
|
||||||
<section className="hm-designer-source" aria-label="Theme JSON source">
|
<section className="hm-designer-source" aria-label="Theme JSON source">
|
||||||
<h2>Theme JSON Preview</h2><p>원본은 themes/*.json입니다. 파일을 Import하거나 서비스의 CDN URL을 입력하세요.</p>
|
<h2>Theme JSON 미리보기</h2><p>원본은 themes/*.json입니다. 파일을 Import하거나 서비스의 CDN URL을 입력하세요.</p>
|
||||||
<div className="hm-designer-actions"><button type="button" disabled={loading} onClick={() => void loadUrl("/themes/default.json")}>Load Default JSON</button>
|
<div className="hm-designer-actions"><button type="button" disabled={loading} onClick={() => void loadUrl("/themes/default.json")}>Load Default JSON</button>
|
||||||
<button type="button" disabled={loading} onClick={() => void loadUrl("/themes/service-example.json")}>Load Service Example JSON</button></div>
|
<button type="button" disabled={loading} onClick={() => void loadUrl("/themes/service-example.json")}>Load Service Example JSON</button></div>
|
||||||
<label htmlFor="hm-theme-url">Theme JSON URL</label><input id="hm-theme-url" value={themeUrl} onChange={event => setThemeUrl(event.target.value)} />
|
<label htmlFor="hm-theme-url">Theme JSON URL</label><input id="hm-theme-url" value={themeUrl} onChange={event => setThemeUrl(event.target.value)} />
|
||||||
@@ -146,7 +146,7 @@ function ThemeEditor() {
|
|||||||
<output aria-live="polite">{status}</output>
|
<output aria-live="polite">{status}</output>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<section className="hm-designer-preview" aria-label="Live component preview"><h2>Live Preview</h2>
|
<section className="hm-designer-preview" aria-label="Live component preview"><h2>실시간 미리보기</h2>
|
||||||
<p>Primary · Active · Focus · Accent · Destructive의 영향을 여러 컴포넌트에서 함께 확인하세요.</p>
|
<p>Primary · Active · Focus · Accent · Destructive의 영향을 여러 컴포넌트에서 함께 확인하세요.</p>
|
||||||
<label htmlFor="hm-preview-navigation-mode">Navigation Mode</label>
|
<label htmlFor="hm-preview-navigation-mode">Navigation Mode</label>
|
||||||
<select id="hm-preview-navigation-mode" value={navigationMode} onChange={event => setNavigationMode(event.target.value === "LNB" ? "LNB" : "GNB")}>
|
<select id="hm-preview-navigation-mode" value={navigationMode} onChange={event => setNavigationMode(event.target.value === "LNB" ? "LNB" : "GNB")}>
|
||||||
|
|||||||
Reference in new issue
Block a user