diff --git a/packages/sw/README.md b/packages/sw/README.md index 9c6d34b..0f784cc 100644 --- a/packages/sw/README.md +++ b/packages/sw/README.md @@ -57,7 +57,7 @@ SW Story는 40개입니다(`SW/Header` 20 · `SW/Footer` 6 · `SW/Popup` 3 · `S - `SW/Header`: SW Header(`sw-header--overview`), Home Menu, Logo Slot, Responsive More (+ verification 16: Dropdown, Document Open, Hidden Sub Controls, Detail Buttons, Interactive States, Window Move Motion, Document Dropdown, K Guide, Home Menu 하위 8개) - `SW/Footer`: SW Footer(`sw-footer--full-footer`), Responsive More (+ verification 4: 도구 그룹별 표시, Figma 디자인별 Variant, 도구 및 메뉴 변경 예제, 개별 부품) -- `SW/Popup`: Custom Content, Multiple Popups, Property Panel · 4개 비교(Story ID `sw-header--property-panel-compare`) +- `SW/Popup`: Custom Content, Multiple Popups, Property Panel(Story ID `sw-header--property-panel-compare`) - `SW/Viewer`: Viewer Header(기본 + 최소화·닫기 버전), Settings Popup(`sw-viewer--content-variants`), Responsive More, Logo (+ verification 6: Menu Groups, Item States, Header + Dropdown, Settings Popup(`sw-viewer--settings-panel`), Variable Size Popups, SWHeader vs Viewer Header(`sw-viewer--compare-with-sw-header`)) - `SW/Theme`: (verification 1: Theme Inheritance) diff --git a/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs b/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs index a1b787a..8265e0f 100644 --- a/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs +++ b/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs @@ -25,7 +25,8 @@ const log = () => page.locator('[data-callback-log]').first().textContent() const footer = Object.values(index.entries).filter(e => e.type === 'story' && e.title === 'SW/Footer').map(e => e.id) check('Footer: 기본 Footer / States removed, full-footer + figma-variants kept', !footer.includes('sw-footer--default') && !footer.includes('sw-footer--states') && footer.includes('sw-footer--full-footer') && footer.includes('sw-footer--figma-variants'), footer) await goto('sw-footer--full-footer') -const sections = await page.locator('h2').allTextContents() +// 2026-10-22: the composition section has no heading; its name is the section aria-label. +const sections = [...await page.locator('h2').allTextContents(), ...await page.locator('section[aria-label]').evaluateAll(list => list.map(e => e.getAttribute('aria-label')))] // 2026-10-20 (second pass): 전체 Footer — Figma 기본 구성, 상태, 참고: 실제 화면 were removed; the live composition stays. check('Footer: full-footer keeps only 실제 Footer 조합 (3 sections removed)', sections.some(s => s.startsWith('실제 Footer 조합')) && !['전체 Footer — Figma', '상태 —', '참고: 실제 화면'].some(t => sections.some(s => s.startsWith(t))) && errors.length === 0, { sections, errors }) check('Footer: one live footer, no Figma / reference image', await page.locator('.hm-sw-footer').count() === 1 && await page.locator('img[src*="composition-2026"], img[src*="narrow-2026"], [aria-label^="Figma 26:10097"]').count() === 0, await page.locator('.hm-sw-footer').count()) @@ -114,15 +115,13 @@ if (await page.locator('.hm-sw-property__drop').count()) await page.keyboard.pre const frames = await page.locator('[data-popups] .hm-sw-property').count() check('Popup: frame / title / close unchanged (3 popups in the main section)', frames === 3) -// 3. SWSlider appearances -const sliders = await page.locator('[data-sw-slider]').evaluateAll(list => list.map(e => ({ look: e.dataset.appearance, h: Math.round(e.getBoundingClientRect().height), fill: e.style.getPropertyValue('--hm-sw-range-fill'), paint: getComputedStyle(e).getPropertyValue('--hm-sw-range-track-paint').trim().length > 0, range: getComputedStyle(e).getPropertyValue('--hm-sw-slider-panel-range').trim() }))) -check('Slider: A = panel (18px, no property paint), B = property (16px, property paint), same fill', sliders[0].look === 'panel' && sliders[0].h === 18 && !sliders[0].paint && sliders[1].look === 'property' && sliders[1].h === 16 && sliders[1].paint && sliders[0].fill === sliders[1].fill && sliders[0].range === '#266651', sliders) +// 3. SWSlider (2026-10-22: Custom Content shows only style B, appearance="property"; no panel gauge on the page) +const sliders = await page.locator('[data-sw-slider]').evaluateAll(list => list.map(e => ({ look: e.dataset.appearance, h: Math.round(e.getBoundingClientRect().height), paint: getComputedStyle(e).getPropertyValue('--hm-sw-range-track-paint').trim().length > 0 }))) +const panelGauges = await page.locator('[data-popups] .hm-sw-slider[data-appearance="panel"], [data-slider-popups] .hm-sw-slider[data-appearance="panel"]').count() +check('Slider: B = property (16px, property paint); no panel (A) gauge left', sliders.length === 1 && sliders[0].look === 'property' && sliders[0].h === 16 && sliders[0].paint && panelGauges === 0, { sliders, panelGauges }) await page.getByLabel('투명도 B').focus(); await page.keyboard.press('ArrowRight'); await page.waitForTimeout(100) const afterB = await page.locator('[data-sw-slider]').evaluateAll(list => list.map(e => e.value)) -check('Slider: one shared value; keyboard change on B commits through onValueCommit', /opacity\(property\)=50/.test(await log()) && afterB.every(v => v === '50'), { log: await log(), afterB }) -await page.getByRole('combobox', { name: '게이지바 스타일' }).selectOption('property'); await page.waitForTimeout(100) -const chosen = await page.getByLabel('투명도 (선택한 스타일)').evaluate(e => e.dataset.appearance) -check('Slider: appearance selection switches the look only', chosen === 'property' && (await log()).includes('appearance=property'), chosen) +check('Slider: keyboard change on B commits through onValueCommit', /opacity\(property\)=50/.test(await log()) && afterB.every(v => v === '50'), { log: await log(), afterB }) const disabledSlider = await page.getByLabel('투명도 (비활성)').evaluate(e => ({ d: e.disabled, o: getComputedStyle(e).opacity })) check('Slider: disabled 40%', disabledSlider.d && disabledSlider.o === '0.4', disabledSlider) await page.locator('[data-slider-popups]').screenshot({ path: path.join(out, 'popup-slider-styles.png') }) @@ -210,8 +209,9 @@ await goto('sw-viewer--content-variants') const cv = await art() check('Viewer: content variants (incl. 300px) title art has no gap', cv.every(p => p.artW === 308 && p.gap < 0.5), cv) await goto('sw-viewer--settings-panel') -const sl = await page.locator('.hm-sw-viewer-slider').first().evaluate(e => ({ cls: e.className, look: e.dataset.appearance, h: Math.round(e.getBoundingClientRect().height), range: getComputedStyle(e).getPropertyValue('--hm-sw-slider-panel-range').trim(), track: getComputedStyle(e).getPropertyValue('--hm-sw-slider-panel-track').trim() })) -check('Viewer: SWViewerSlider = SWSlider panel with viewer tokens (#266651 / #e4dbc9)', sl.cls.includes('hm-sw-slider') && sl.look === 'panel' && sl.h === 18 && sl.range === '#266651' && sl.track === '#e4dbc9', sl) +// 2026-10-22: the viewer Story gauges are SWSlider style B (appearance="property"), no green panel gauge. +const sl = await page.locator('.hm-sw-viewer-panel .hm-sw-slider').evaluateAll(list => list.map(e => ({ look: e.dataset.appearance, h: Math.round(e.getBoundingClientRect().height), viewer: e.classList.contains('hm-sw-viewer-slider') }))) +check('Viewer: gauges = SWSlider style B (property, 16px), no SWViewerSlider panel gauge', sl.length > 0 && sl.every(e => e.look === 'property' && e.h === 16 && !e.viewer), sl) await writeFile(path.join(out, 'result.json'), JSON.stringify({ date: new Date().toISOString(), checks, report }, null, 1)) console.log(`${checks.filter(c => c.ok).length}/${checks.length} PASS`) diff --git a/packages/sw/tests/integrated-popup.checks.mjs b/packages/sw/tests/integrated-popup.checks.mjs index 2268aec..73a3622 100644 --- a/packages/sw/tests/integrated-popup.checks.mjs +++ b/packages/sw/tests/integrated-popup.checks.mjs @@ -25,7 +25,7 @@ export async function popupChecks({ page, goto, check, log, rect, out, report }) await settings.locator('[data-popup-slider]').focus() await page.keyboard.press('ArrowRight') await page.waitForTimeout(100) - check('popup content: SWSlider works inside', /opacity=70/.test(await log()) && await settings.locator('[data-popup-slider]').evaluate(e => e.classList.contains('hm-sw-slider') && e.dataset.appearance === 'panel' && e.value === '70'), await log()) + check('popup content: SWSlider works inside', /opacity=70/.test(await log()) && await settings.locator('[data-popup-slider]').evaluate(e => e.classList.contains('hm-sw-slider') && e.dataset.appearance === 'property' && e.value === '70'), await log()) check('popup content: no HM UI Slider left inside the popups', await page.locator('[data-popups] [data-slot="slider"]').count() === 0) await settings.locator('[data-ui-apply]').click() check('popup content: HM UI Button works inside', (await log()).includes('apply')) diff --git a/packages/sw/tests/property-panel-comparison.browser.mjs b/packages/sw/tests/property-panel-comparison.browser.mjs index 6e1e4a5..f180b90 100644 --- a/packages/sw/tests/property-panel-comparison.browser.mjs +++ b/packages/sw/tests/property-panel-comparison.browser.mjs @@ -75,7 +75,7 @@ try { JSON.stringify(recorded.ids.map(([id]) => id).sort()) === JSON.stringify(expected.map(e => e.id).sort()) && retiredPropertyPanelIds.every(id => !index.entries[id]) && stories.length === 338 - retiredUnusedDemoIds.length + approvedAddedStoryIds.length && now.unexpected.length === 0 && now.missing.length === 0 && now.retiredPresent.length === 0, { before: [before.total, before.sw.length], recorded: [recorded.total, recorded.sw], after: [stories.length, sw.length], ...now }) check('inventory: retained SW Story IDs, names, titles, tags unchanged (recorded at c8e5fbe); now only the approved tags and Popup move', JSON.stringify(recorded.ids) === JSON.stringify(expected.map(e => [e.id, e.name])) && now.changed.length === 0, { retained: sw.length, changed: now.changed }) - check('inventory: compare Story keeps its ID and name', index.entries['sw-header--property-panel-compare']?.name === 'Property Panel · 4개 비교') + check('inventory: compare Story keeps its ID and name', index.entries['sw-header--property-panel-compare']?.name === 'Property Panel') check('inventory: Dropdown Story keeps ID / name / title', index.entries['sw-header--composed-controls']?.name === 'Dropdown' && index.entries['sw-header--composed-controls']?.title === 'SW/Header') // ---- Layout at four widths @@ -89,23 +89,16 @@ try { check(`${width}: original panel sizes 314 × 256 / 361 / 658 / 754`, STATES.every(([id, h], i) => sizes[i][0] === id && sizes[i][1] === 314 && sizes[i][2] === h), sizes) const colGap = b.cell.x - a.cell.right, colGap2 = d.cell.x - c.cell.right, rowGap = c.cell.y - Math.max(a.cell.bottom, b.cell.bottom) report.data[`gaps${width}`] = { colGap, colGap2, rowGap } - check(`${width}: 2 columns × 2 rows by coordinates`, Math.abs(a.cell.y - b.cell.y) < 0.5 && Math.abs(c.cell.y - d.cell.y) < 0.5 && Math.abs(a.cell.x - c.cell.x) < 0.5 && Math.abs(b.cell.x - d.cell.x) < 0.5 && b.cell.x >= a.cell.right && c.cell.y >= Math.max(a.cell.bottom, b.cell.bottom), { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) - check(`${width}: consistent 24px column and row gaps`, [colGap, colGap2, rowGap].every(v => Math.abs(v - 24) < 0.5), { colGap, colGap2, rowGap }) - check(`${width}: page itself does not scroll (no document overflow)`, g.doc.scrollW <= g.doc.clientW && g.doc.scrollH <= g.doc.clientH, g.doc) - check(`${width}: Inner scroller scrolls vertically to the 754px panel`, g.scroller.scrollH > g.scroller.clientH, g.scroller) - await page.screenshot({ path: path.join(out, `compare-${width}-top.png`) }) - // Scroll to the very end: every panel must be reachable with its bottom inside the scroller. - const end = await page.evaluate(() => { - const s = document.querySelector('[data-compare-scroll]'); s.scrollTop = s.scrollHeight; s.scrollLeft = s.scrollWidth - const sr = s.getBoundingClientRect() - return { scrollTop: s.scrollTop, scrollLeft: s.scrollLeft, panels: [...s.querySelectorAll('.hm-sw-property')].map(p => { const r = p.getBoundingClientRect(); return { bottom: r.bottom, right: r.right } }), sBottom: sr.bottom, sRight: sr.right } - }) - report.data[`end${width}`] = end - const last = end.panels[3], right = end.panels[1] - check(`${width}: bottom of the 754px panel reachable, not clipped`, last.bottom <= end.sBottom + 0.5 && last.right <= end.sRight + 0.5, end) - if (g.scroller.scrollW > g.scroller.clientW) check(`${width}: narrow Inner scrolls horizontally, keeping 2 columns`, end.scrollLeft > 0 && right.right <= end.sRight + 0.5, { scrollW: g.scroller.scrollW, clientW: g.scroller.clientW, end }) - else check(`${width}: both columns fit without horizontal scroll`, g.scroller.scrollW <= g.scroller.clientW && b.cell.right <= g.scroller.right, g.scroller) - await page.screenshot({ path: path.join(out, `compare-${width}-end.png`) }) + // 2026-10-22 (user request): no Inner scroller; the page scrolls. 2 columns × 2 rows, one column below 720px. + const oneColumn = width < 720 + if (oneColumn) check(`${width}: one column in order`, [a, b, c, d].every((cell, i, all) => i === 0 || (Math.abs(cell.cell.x - a.cell.x) < 0.5 && cell.cell.y >= all[i - 1].cell.bottom)), { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) + else { + check(`${width}: 2 columns × 2 rows by coordinates`, Math.abs(a.cell.y - b.cell.y) < 0.5 && Math.abs(c.cell.y - d.cell.y) < 0.5 && Math.abs(a.cell.x - c.cell.x) < 0.5 && Math.abs(b.cell.x - d.cell.x) < 0.5 && b.cell.x >= a.cell.right && c.cell.y >= Math.max(a.cell.bottom, b.cell.bottom), { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) + check(`${width}: consistent 24px column and row gaps`, [colGap, colGap2, rowGap].every(v => Math.abs(v - 24) < 0.5), { colGap, colGap2, rowGap }) + } + check(`${width}: no Inner scroll (wrapper does not scroll)`, g.scroller.scrollW <= g.scroller.clientW && g.scroller.scrollH <= g.scroller.clientH, g.scroller) + check(`${width}: no horizontal page scroll; panels inside the page width`, g.doc.scrollW <= g.doc.clientW && g.cells.every(cell => cell.panel.right <= g.doc.clientW + 0.5), g.doc) + await page.screenshot({ path: path.join(out, `compare-${width}-top.png`), fullPage: true }) } // ---- Behaviour (1440 × 900) @@ -141,8 +134,8 @@ try { check(`Dropdown ${id}: opens, has items, fully inside the viewport`, info.items > 0 && info.x >= 0 && info.y >= 0 && info.right <= info.vw + 0.5 && info.bottom <= info.vh + 0.5, info) await page.screenshot({ path: path.join(out, `dropdown-${id}.png`) }) if (id === 'expanded') { - // Scroll the Inner while open: the fixed overlay must stay attached to its field (below or flipped above). - const moved = await page.locator('[data-compare-scroll]').evaluate(s => { const t = s.scrollTop; s.scrollTop += 120; return s.scrollTop - t }) + // Scroll the page while open (2026-10-22: no Inner scroller): the fixed overlay must stay attached to its field. + const moved = await page.evaluate(() => { const t = scrollY; scrollBy(0, 120); return scrollY - t }) await page.waitForTimeout(150) const after = await page.evaluate(() => { const d = document.querySelector('.hm-sw-property__drop')?.getBoundingClientRect() @@ -150,7 +143,7 @@ try { return d && f ? { dTop: d.top, dBottom: d.bottom, fTop: f.top, fBottom: f.bottom } : null }) report.data.dropScroll = { moved, before: info, after } - check('Dropdown follows the Inner scroller (stays attached to its field)', moved > 0 && after && (Math.abs(after.dTop - after.fBottom) < 1.5 || Math.abs(after.dBottom - after.fTop) < 1.5), report.data.dropScroll) + check('Dropdown follows the page scroll (stays attached to its field)', moved > 0 && after && (Math.abs(after.dTop - after.fBottom) < 1.5 || Math.abs(after.dBottom - after.fTop) < 1.5), report.data.dropScroll) } await page.keyboard.press('Escape') check(`Dropdown ${id}: Escape closes and returns focus to its field`, await page.locator('.hm-sw-property__drop').count() === 0 && await trigger.evaluate(e => document.activeElement === e || e.contains(document.activeElement))) diff --git a/packages/sw/tests/storybook-inventory.mjs b/packages/sw/tests/storybook-inventory.mjs index e9db25f..1477a51 100644 --- a/packages/sw/tests/storybook-inventory.mjs +++ b/packages/sw/tests/storybook-inventory.mjs @@ -105,6 +105,8 @@ export const menuCleanupHiddenIds = ['sw-header--interactive-states', 'sw-header // 2026-10-22 (user request): SWHeader vs Viewer Header left the sidebar the same way. 'sw-viewer--compare-with-sw-header'] export const renamedEntries = { 'sw-header--overview': 'SW Header', 'sw-footer--full-footer': 'SW Footer', + 'sw-header--logo-slot': 'Logo', // 2026-10-22 (user request): sidebar "Logo", page title "SW Header — 로고" + 'sw-header--property-panel-compare': 'Property Panel', // 2026-10-22 (user request): was "Property Panel · 4개 비교" 'sw-footer--responsive': 'Responsive More', 'sw-viewer--content-variants': 'Settings Popup', 'sw-viewer--responsive': 'Responsive More' } // Every approved SW Story ID added after the unused-demo removal, as the current index lists them. diff --git a/packages/sw/tests/storybook-unused-removal.browser.mjs b/packages/sw/tests/storybook-unused-removal.browser.mjs index d98d0f0..7159354 100644 --- a/packages/sw/tests/storybook-unused-removal.browser.mjs +++ b/packages/sw/tests/storybook-unused-removal.browser.mjs @@ -44,7 +44,7 @@ const keys = Object.keys(afterIndex.entries), firstSW = keys.findIndex(id => id. const swOrder = { previous: keys[firstSW - 1], afterCenterGraphic: afterIndex.entries[keys[firstSW - 1]]?.title.startsWith('Center Graphic/') && !keys.slice(firstSW + 1).some(id => afterIndex.entries[id].title.startsWith('Center Graphic/')), contiguous: keys.filter(id => id.startsWith('sw-')).every((id, i) => keys[firstSW + i] === id) } check('inventory: every non-removed index entry identical (docs and other packages included) except the approved SW changes', outsideSW(others(beforeIndex)) === outsideSW(others(afterIndex)) && JSON.stringify(others(beforeIndex).filter(e => e.id.startsWith('sw-')).map(withApprovedEntryChanges)) === JSON.stringify(others(beforeIndex).filter(e => e.id.startsWith('sw-')).map(e => afterIndex.entries[e.id])) && swOrder.afterCenterGraphic && swOrder.contiguous, swOrder) check('inventory: Dropdown keeps sw-header--composed-controls / Dropdown / SW/Header', afterIndex.entries['sw-header--composed-controls']?.name === 'Dropdown' && afterIndex.entries['sw-header--composed-controls']?.title === 'SW/Header') -check('inventory: comparison keeps its ID and name', afterIndex.entries['sw-header--property-panel-compare']?.name === 'Property Panel · 4개 비교') +check('inventory: comparison keeps its ID and name', afterIndex.entries['sw-header--property-panel-compare']?.name === 'Property Panel') // ---- Source: retained Story declarations byte-identical; product, AI template and other packages untouched. const sourcePath = 'src/sw/sw-header.stories.tsx' diff --git a/src/sw/sw-footer-stories.css b/src/sw/sw-footer-stories.css index ae047e5..1c790f2 100644 --- a/src/sw/sw-footer-stories.css +++ b/src/sw/sw-footer-stories.css @@ -29,6 +29,8 @@ .sw-footer-story-row--tight { gap: 0; } .sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; } .sw-footer-story-width + .sw-footer-story-width { margin-top: 16px; } +/* The hidden 1920px row (Responsive More, narrower than 1920px) takes its gap with it. */ +@container (max-width: 1919.98px) { .sw-story-responsive-wide + .sw-footer-story-width { margin-top: 0; } } .sw-footer-story-aux-compare { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 16px; } .sw-footer-story-aux-row { display: flex; align-items: center; gap: 12px; margin: 0; } .sw-footer-story-aux-row figcaption { flex: 0 0 120px; font-size: 12px; color: #3d4a45; } diff --git a/src/sw/sw-footer.stories.tsx b/src/sw/sw-footer.stories.tsx index eef1b6e..f233e0e 100644 --- a/src/sw/sw-footer.stories.tsx +++ b/src/sw/sw-footer.stories.tsx @@ -29,7 +29,15 @@ export default meta type Story = StoryObj /** A program screen bottom: drawing area above the footer (the layout tabs sit on its lower edge). */ -function Screen({ width = 1920, canvas = 160, children }: { width?: number | string; canvas?: number; children: ReactNode }) { +function Screen({ width = 1920, canvas = 160, fluid = false, children }: { width?: number | string; canvas?: number; fluid?: boolean; children: ReactNode }) { + // `fluid` (2026-10-22, Responsive More): `width` is a maximum and there is no scroller, so the screen background and + // the footer share the available width, commands that do not fit go to ▲ More, and an open menu is never clipped. + if (fluid) return ( +
+
+ {children} +
+ ) return (
@@ -81,9 +89,11 @@ export const FullFooter: Story = { render: () => ( - + {/* 2026-10-22 (user request): no section heading; the live composition follows the page title. Behaviour: command + click = select + history, category tabs, empty slot guide, aux toggles, ▲ = command window, Layout tabs, Enter runs. */} +
- +
), } @@ -379,13 +389,16 @@ export const Responsive: Story = { render: () => ( - {[1920, 1440, 1280, 950, 860, 760].map(width => ( -
- - - -
- ))} + {/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */} +
+ {[1920, 1440, 1280, 950, 860, 760].map(width => ( +
+ + + +
+ ))} +
), } diff --git a/src/sw/sw-header.stories.tsx b/src/sw/sw-header.stories.tsx index cb4169b..878aae7 100644 --- a/src/sw/sw-header.stories.tsx +++ b/src/sw/sw-header.stories.tsx @@ -106,8 +106,12 @@ export const DetailButtons: Story = { , } -/** Fixed-width frame. The outer scroller belongs to Storybook, never to the header. */ -function Frame({ width, children }: { width: number; children: ReactNode }) { +/** + * Fixed-width frame. The outer scroller belongs to Storybook, never to the header. + * `fluid` (2026-10-22, Logo Slot): `width` is a maximum and there is no scroller; the header fits the available width. + */ +function Frame({ width, children, fluid = false }: { width: number; children: ReactNode; fluid?: boolean }) { + if (fluid) return
{children}
return (
{children}
@@ -447,26 +451,28 @@ const TALL_SAMPLE_LOGO = ( ) +// 2026-10-22 (user request): sidebar "Logo" (ID sw-header--logo-slot kept), page title "SW Header — 로고", +// frames fit the page (no scroller). export const LogoSlot: Story = { - name: "Logo Slot", + name: "Logo", parameters: { controls: { disable: true } }, render: () => ( - + - + - + - + ), @@ -789,11 +795,15 @@ export const ResponsiveMore: Story = { render: () => ( - {MORE_WIDTHS.map(width => ( - - - - ))} +
+ {MORE_WIDTHS.map(width => ( +
+ + + +
+ ))} +
), } diff --git a/src/sw/sw-popup.stories.tsx b/src/sw/sw-popup.stories.tsx index 73648cc..27d130a 100644 --- a/src/sw/sw-popup.stories.tsx +++ b/src/sw/sw-popup.stories.tsx @@ -2,7 +2,7 @@ import { useId, useState } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { SWHeaderInput, SWHeaderPropertyPanel, SWPopup, SWPopupPropertyRow, SWPopupRow, SWPopupSection, SWSelect, SWSlider, SWTheme, - type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty, type SWSliderAppearance, + type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty, } from "@hm-design/sw" import "@hm-design/sw/styles.css" import { Button, Switch } from "@hm-design/ui" @@ -26,7 +26,6 @@ const COLORS: SWHeaderColorOption[] = [ const UNITS = ["mm", "cm", "m"].map(value => ({ value })) const MENUS = { layers: LAYERS, colors: COLORS } const layerProperty = (value: string): SWHeaderProperty => ({ id: "layer", name: "레이어", value, type: "select", groupId: "popup", picker: "layer", options: LAYERS.map(({ id, name }) => ({ id, name })) }) -const APPEARANCES = [{ value: "panel", label: "A · panel (기본)" }, { value: "property", label: "B · property" }] const meta = { title: "SW/Popup", @@ -71,13 +70,14 @@ function SettingsPopup({ record }: { record: (line: string) => void }) { { setColor(value); record(`color=${value}`) }} /> - {/* 2026-10-20: the gauge is the SW gauge (SWSlider). SW has no normal-mode switch, so 스냅 stays the HM UI Switch. */} + {/* 2026-10-20: the gauge is the SW gauge (SWSlider; 2026-10-22: style B, appearance="property", on this page). + SW has no normal-mode switch, so 스냅 stays the HM UI Switch. */} { setSnap(next); record(`snap=${next}`) }} data-ui-switch="" /> - record(`opacity=${next}`)} aria-label="투명도" data-popup-slider="" /> + record(`opacity=${next}`)} aria-label="투명도" data-popup-slider="" />
@@ -122,35 +122,22 @@ function PanelWithChildren({ record }: { record: (line: string) => void }) { ) } -/** SWSlider: the same value and events in both looks; the host only picks `appearance`. */ -function OpacityStylePopups({ record }: { record: (line: string) => void }) { +/** SWSlider style B (appearance="property", 2026-10-22: the only gauge look on this page): value, entry and disabled. */ +function OpacityPopup({ record }: { record: (line: string) => void }) { const [opacity, setOpacity] = useState(40) - const [appearance, setAppearance] = useState("panel") - const slider = (look: SWSliderAppearance, label: string) => ( - record(`opacity(${look})=${next}`)} /> - ) return ( - <> - - - {slider("panel", "투명도 A")} - {slider("property", "투명도 B")} - - { setOpacity(Math.min(100, Math.max(0, Number(next)))); record(`opacity=${next}`) }} /> - - - - - - - { setAppearance(next as SWSliderAppearance); record(`appearance=${next}`) }} /> - - {slider(appearance, "투명도 (선택한 스타일)")} - - - - + + + + record(`opacity(property)=${next}`)} /> + + + { setOpacity(Math.min(100, Math.max(0, Number(next)))); record(`opacity=${next}`) }} /> + + + + ) } @@ -168,9 +155,9 @@ function PopupPage() { - appearance="panel"(기본, A) = 지금까지 Popup에서 쓰던 게이지바(6px 트랙 · 18px 흰 Thumb · 녹색 Range, SWViewerSlider와 같은 모양). appearance="property"(B) = 속성창 4개 상태 비교의 게이지바(베이지 트랙 · 빨강 채움 · Figma Thumb). 값·onValueChange·onValueCommit·키보드는 같고 모양만 바뀝니다. 색은 SW 토큰(--hm-sw-slider-panel-*, --hm-sw-property-range-*)을 따릅니다.}> + - + diff --git a/src/sw/sw-property-panel.stories.tsx b/src/sw/sw-property-panel.stories.tsx index 6661701..36970dc 100644 --- a/src/sw/sw-property-panel.stories.tsx +++ b/src/sw/sw-property-panel.stories.tsx @@ -12,6 +12,6 @@ const meta = { export default meta export const PropertyPanelCompare: StoryObj = { - name: "Property Panel · 4개 비교", + name: "Property Panel", // 2026-10-22 (user request): was "Property Panel · 4개 비교"; ID unchanged render: () => , } diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css index 795d1c3..06b7917 100644 --- a/src/sw/sw-stories.css +++ b/src/sw/sw-stories.css @@ -1,5 +1,9 @@ /* Story canvas only: frames that simulate a header width. Never styles the header itself. */ .sw-story-scroller { max-width: 100%; overflow-x: auto; } +/* Responsive More pages (2026-10-22): the 1920px row shows only when the content Inner itself is 1920px or wider + (container width, not the browser width); otherwise the whole row — title, header, background, margin — is hidden. */ +.sw-story-responsive { container-type: inline-size; } +@container (max-width: 1919.98px) { .sw-story-responsive-wide { display: none; } } .sw-story-frame { background: #060606; } /* Shared drawing-area canvas of the SW Stories (2026-10-19): the SW/Viewer black grid — 40px cells, 1px lines of HM green #266651 at 18%, on #0A0A0B. Used behind the SW Header work area and dropdown room and the viewer frames. */ @@ -33,14 +37,12 @@ .sw-story-compare { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; } .sw-story-compare__cell { margin: 0; } .sw-story-compare__cell figcaption { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px; font-size: 12px; color: #3d4a45; } -/* Property panel 4-state comparison (2026-10-19): 2 columns × 2 rows at the panels' own size inside a - viewport-high page. Only the Inner scroller scrolls (both axes); panels are never shrunk or wrapped. */ -.sw-story-compare-page.sb-page { height: 100dvh; min-height: 0; } -.sw-story-compare-page > .sb-page-inner { height: 100%; display: flex; flex-direction: column; } -.sw-story-compare-page .sb-page-header { flex: 0 0 auto; } -.sw-story-compare-page .sw-story-log { flex: 0 0 auto; margin-top: 12px; } -.sw-story-compare-scroll { flex: 1 1 0; min-height: 0; overflow: auto; overscroll-behavior: contain; } -.sw-story-compare.sw-story-compare--grid { display: grid; grid-template-columns: repeat(2, max-content); align-items: start; gap: 24px; width: max-content; } +/* Property panel 4-state comparison: 2 columns × 2 rows at the panels' own size (314px wide, never shrunk). + 2026-10-22 (user request): no Inner scroller — the page scrolls normally, and a screen too narrow for two + panels (2 × 314 + 24px gap + gutters) shows one column instead of scrolling sideways. */ +.sw-story-compare-page .sw-story-log { margin-top: 12px; } +.sw-story-compare.sw-story-compare--grid { display: grid; grid-template-columns: repeat(2, max-content); align-items: start; gap: 24px; } +@media (max-width: 719.98px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: max-content; } } /* Placement-only exception, same as the Overview Stage: the product clamps a panel to the viewport height; here the scroller provides the room so the 754px panel keeps its size. */ .sw-story-compare--grid .hm-sw-property { max-height: none; } diff --git a/src/sw/sw-viewer-fixture.tsx b/src/sw/sw-viewer-fixture.tsx index 0c45eb3..fe7c3de 100644 --- a/src/sw/sw-viewer-fixture.tsx +++ b/src/sw/sw-viewer-fixture.tsx @@ -2,7 +2,7 @@ // UI state lives here (like a consuming program); no DWG/DXF engine — commands are logged, not executed. import { useMemo, useState } from "react" import { - SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch, + SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWSlider, SWViewerSwitch, type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps, } from "@hm-design/sw" import { Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react" @@ -148,10 +148,12 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP ) } +// 2026-10-22: the viewer gauges use SWSlider style B (appearance="property", as SW/Popup › Custom Content), not +// SWViewerSlider (fixed to the green panel look). function SliderRow({ label, unit, value, min, max, step, disabled, onChange }: { label: string; unit?: string; value: number; min: number; max: number; step: number; disabled?: boolean; onChange: (value: number) => void }) { return ( }> + below={}> {id => { const next = Number(event.currentTarget.value); if (Number.isFinite(next)) onChange(next) }} />} ) diff --git a/src/sw/sw-viewer.stories.tsx b/src/sw/sw-viewer.stories.tsx index 2a5a3b7..b2ba6a6 100644 --- a/src/sw/sw-viewer.stories.tsx +++ b/src/sw/sw-viewer.stories.tsx @@ -86,11 +86,13 @@ export const ViewerHeader: Story = { name: "Viewer Header", render: () => ( - - + {/* 2026-10-22 (user request): page title and the two sections only. The minimize / close version keeps + SWHeaderWindowControls at the right end of `trailing`; the program owns the real window actions. */} + + - +
@@ -335,12 +337,12 @@ export const Responsive: Story = { -
+
{[1920, 1440, 1280, 950, 860, 760, 520].map(width => ( - +
{/* The row width is a maximum (2026-10-22): on a narrower screen the header shrinks with it instead of scrolling. */}
-
+
))}
@@ -404,9 +406,10 @@ export const Logo: Story = {
{rows.map(row => ( -
+ {/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */} +
-
+
))}