diff --git a/features/shell_tokens_and_nav_contrast.feature b/features/shell_tokens_and_nav_contrast.feature new file mode 100644 index 0000000..c398dad --- /dev/null +++ b/features/shell_tokens_and_nav_contrast.feature @@ -0,0 +1,122 @@ +Feature: Shell design tokens and selected nav contrast + + As the maintainer of RenewCMS + I want the new --rc-* tokens in the SPA and every selected nav item drawn in sand with on-sand text + So that the sidebar and drawer stay readable in both themes and match docs/design + + Background: + Given web/src/assets/main.css declares the --rc-* tokens of docs/design/foundations.md + "New tokens", light values in :root and dark values in .dark + And DashboardShell renders the nav from web/src/components/DashboardShell.vue, with "Settings" + as the parent row (/settings) and a "Media" group holding "Variants" + (/settings/media/variants) and "Cache" (/settings/media/cache) + And the session role is "owner", so the Settings children are present + And a selected row is a link whose route is current, carrying aria-current="page" + + Scenario Outline: The new tokens exist with the values given in foundations.md + When web/src/assets/main.css is loaded in the theme + Then resolves to + + Examples: + | token | theme | value | + | --rc-sand | light | #e8dfd1 | + | --rc-sand | dark | #d4c5b0 | + | --rc-on-sand | light | #111111 | + | --rc-on-sand | dark | #111111 | + | --rc-success-soft | light | #eaefea | + | --rc-success-soft | dark | #1f241f | + | --rc-info-soft | light | #ebeef0 | + | --rc-info-soft | dark | #202428 | + | --rc-warning-soft | light | #f0ede6 | + | --rc-warning-soft | dark | #2a251e | + | --rc-error-soft | light | #f3eae9 | + | --rc-error-soft | dark | #2c201f | + | --rc-overlay | light | rgb(10 10 10 / 0.4) | + | --rc-overlay | dark | rgb(0 0 0 / 0.6) | + | --rc-shadow-pop | light | 0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06) | + | --rc-shadow-pop | dark | 0 4px 16px rgb(0 0 0 / 0.5) | + | --rc-shadow-modal | light | 0 16px 48px rgb(17 17 17 / 0.18) | + | --rc-shadow-modal | dark | 0 16px 48px rgb(0 0 0 / 0.6) | + + Scenario: No --ui-* token and no --rc-canvas are introduced + When web/src/assets/main.css is compared with docs/design/foundations.md + Then no --ui-* token value is changed + And --rc-canvas is not declared + + Scenario Outline: N1 Parent selected: Settings is filled in sand with on-sand text and icon + Given the current route is /settings + And the theme is + When DashboardShell renders the desktop sidebar + Then the "Settings" row has aria-current="page" + And its background is --rc-sand and its label and icon colour are --rc-on-sand + And the "Media" group is visible with "Variants" and "Cache" unselected + And neither child row uses --rc-sand + + Examples: + | theme | + | light | + | dark | + + Scenario Outline: N2 and N3 Child selected: the child is filled and the parent is unfilled and bold + Given the current route is + And the theme is + When DashboardShell renders the desktop sidebar + Then the "" row has aria-current="page", background --rc-sand and label and icon colour --rc-on-sand + And the "Settings" row is not filled, has no aria-current, uses --ui-text-highlighted at weight 500 and shows the chevron pointing down + And the other child row is not filled + + Examples: + | path | child | theme | + | /settings/media/variants | Variants | light | + | /settings/media/variants | Variants | dark | + | /settings/media/cache | Cache | light | + | /settings/media/cache | Cache | dark | + + Scenario Outline: Hover, focus and active on a selected row keep sand and on-sand + Given the "" row is selected on + And the theme is + When the row is + Then its background stays --rc-sand and its label and icon colour stay --rc-on-sand + And no white or --ui-text-muted colour is applied to its label or icon + + Examples: + | row | path | interaction | theme | + | Settings | /settings | hovered | dark | + | Settings | /settings | focus-visible | dark | + | Settings | /settings | pressed | dark | + | Settings | /settings | hovered | light | + | Variants | /settings/media/variants | hovered | dark | + | Variants | /settings/media/variants | pressed | light | + | Cache | /settings/media/cache | focus-visible | dark | + + Scenario: Focus on a selected row adds a 2 px --ui-primary outline + Given the "Settings" row is selected on /settings + When the row receives keyboard focus + Then it has a 2 px --ui-primary focus outline + And its background is still --rc-sand + + Scenario Outline: N4 and N5 The mobile drawer uses the same selected treatment + Given the viewport is 390 x 844 and the theme is + And the current route is + When the Menu button is activated + Then the drawer rows are 44 px tall + And the "" row has aria-current="page", background --rc-sand and label and icon colour --rc-on-sand + And + + Examples: + | path | selected | parent_state | theme | + | /settings | Settings | the Media group lists Variants and Cache, unselected | light | + | /settings | Settings | the Media group lists Variants and Cache, unselected | dark | + | /settings/media/variants | Variants | the "Settings" row is unfilled and bold | light | + | /settings/media/variants | Variants | the "Settings" row is unfilled and bold | dark | + + Scenario Outline: Other selected nav items use the same pairing + Given the current route is and the theme is dark + When DashboardShell renders + Then the "" row has background --rc-sand and label and icon colour --rc-on-sand + + Examples: + | path | row | + | /home | Home | + | /users | Users | + | /activity | Activity | diff --git a/features/shell_tokens_and_nav_contrast.qa.md b/features/shell_tokens_and_nav_contrast.qa.md new file mode 100644 index 0000000..8b0e6c0 --- /dev/null +++ b/features/shell_tokens_and_nav_contrast.qa.md @@ -0,0 +1,37 @@ +# QA: Shell design tokens and selected nav contrast (SPA only) + +No /v1 behaviour changes. There is no httptest part. + +## Automated (Vitest + Vue Test Utils, `task test:web`) + +Mount DashboardShell with a test router and a resolved session (role "owner"). Mock the /v1 calls the shell issues (/v1/auth/me, and the update-check call if the shell makes one) so none go unmocked. Do not freeze the system date. + +1. Token declarations: read web/src/assets/main.css as text; assert each token/theme/value row of the Examples table is declared in :root (light) or .dark (dark), and that --rc-canvas is absent. +2. Parent selected (N1): route /settings; the Settings link has aria-current="page" and the sand and on-sand classes (bg-(--rc-sand), text-(--rc-on-sand), or the equivalent in navUi); the Variants and Cache links have neither. +3. Child selected (N2, N3): routes /settings/media/variants and /settings/media/cache; the child link has the sand classes and aria-current; the Settings link has no sand class, no aria-current, the text-highlighted and font-medium classes, and the chevron open state (rotate-90). +4. Selected-row interaction classes: for the selected link, assert the class list includes the sand and on-sand variants for hover:, focus-visible: and active:, and includes no text-muted, text-white or text-inverted for the active state. Assert the leading icon inherits the on-sand colour. +5. Dark and light: repeat 2 and 3 with the `dark` class on the document root; assert the same class contract, since the tokens carry the theme. +6. Mobile drawer (N4, N5): open the drawer; assert 44 px row height classes and the same selected contract for /settings and /settings/media/variants. +7. Other routes: /home, /users, /activity selected links have the sand contract. + +## Visual (`task visual:check`, gate: every capture within 10 %) + +Each state's mocks reuse the board's own sample data from the "Settings navigation" frame in `RenewCMS Screens Back-office.dc.html`. + +- `task visual:check -- settings` (the Settings screen states in docs/design/screens/settings.md and states.md) +- `task visual:check -- image-variants` for the nav states N1 to N5 (docs/design/screens/image-variants.md, "Settings navigation"; the screen key in web/visual/states.ts is `image-variants`). + +Each capture is desktop 1440x900 and mobile 390x844, in light and dark. N4 and N5 are the mobile drawer states. None of these states are clock-sensitive. + +| State | Route / trigger | Must show | +|---|---|---| +| N1 | /settings | Settings filled in sand, Media group visible, children unselected | +| N2 | /settings/media/variants | Settings unfilled and bold, Variants filled | +| N3 | /settings/media/cache | Settings unfilled and bold, Cache filled | +| N4 | Menu button on /settings | Drawer, 44 px rows, Settings filled | +| N5 | Menu button on /settings/media/variants | Drawer, Variants filled | + +## Manual checks (screenshots cannot cover) + +- Keyboard: Tab through the sidebar. The focus ring is 2 px --ui-primary on a selected row, and the sand fill stays. +- Hover and pressed on a selected row in dark mode: the label and icon never turn white or muted. diff --git a/web/src/assets/main.css b/web/src/assets/main.css index e1ca6c9..1bbf0f2 100644 --- a/web/src/assets/main.css +++ b/web/src/assets/main.css @@ -170,11 +170,15 @@ --ui-info: var(--ui-color-info-700); --ui-warning: var(--ui-color-warning-700); --ui-error: var(--ui-color-error-700); - --rc-success-soft: #eaefeb; + --rc-sand: #e8dfd1; + --rc-on-sand: #111111; + --rc-success-soft: #eaefea; --rc-info-soft: #ebeef0; --rc-warning-soft: #f0ede6; --rc-error-soft: #f3eae9; --rc-overlay: rgb(10 10 10 / 0.4); + --rc-shadow-pop: var(--shadow-popover); + --rc-shadow-modal: var(--shadow-modal); } .dark { @@ -199,10 +203,12 @@ --ui-info: var(--ui-color-info-300); --ui-warning: var(--ui-color-warning-300); --ui-error: var(--ui-color-error-300); - --rc-success-soft: #202420; - --rc-info-soft: #202325; - --rc-warning-soft: #26221c; - --rc-error-soft: #28201e; + --rc-sand: #d4c5b0; + --rc-on-sand: #111111; + --rc-success-soft: #1f241f; + --rc-info-soft: #202428; + --rc-warning-soft: #2a251e; + --rc-error-soft: #2c201f; --rc-overlay: rgb(0 0 0 / 0.6); --shadow-popover: 0 4px 16px rgb(0 0 0 / 0.5); --shadow-modal: 0 16px 48px rgb(0 0 0 / 0.6); diff --git a/web/src/components/DashboardShell.vue b/web/src/components/DashboardShell.vue index 08afe89..2796a95 100644 --- a/web/src/components/DashboardShell.vue +++ b/web/src/components/DashboardShell.vue @@ -161,6 +161,9 @@ const sidebarCollapsed = useSidebarCollapsed() const sidebarUi = computed(() => ({ root: sidebarCollapsed.value ? 'min-w-14 px-1 py-3' : 'min-w-14 p-3', + content: 'w-75 gap-0.5 divide-y-0 p-3 shadow-modal', + overlay: 'bg-(--rc-overlay)', + toggle: 'hidden', header: 'mb-3 h-10 gap-2.5 px-1.5', body: 'gap-0.5 p-0', footer: 'p-0', @@ -170,12 +173,13 @@ const navUi = computed(() => ({ list: 'flex flex-col gap-0.5', link: sidebarCollapsed.value ? 'h-8 justify-center gap-0 px-0 text-sm' - : 'h-8 gap-2.5 px-2.5 py-0 text-sm [li[data-state=open]>&]:font-medium [li[data-state=open]>&]:text-highlighted', + : 'h-8 gap-2.5 px-2.5 py-0 text-sm max-lg:h-11 [li[data-state=open]>&:not([aria-current=page])]:font-medium [li[data-state=open]>&:not([aria-current=page])]:text-highlighted', linkLeadingIcon: 'size-4', linkTrailingIcon: 'size-3.5 group-data-[state=open]:rotate-90', label: 'px-2.5 pt-1.5 pb-0.5 pl-3 text-[11px] leading-4 font-semibold tracking-[.04em] text-muted uppercase', - childList: 'ms-[19px] gap-0.5 border-s border-default ps-0', - childLink: 'h-8 gap-2.5 py-0 pr-2.5 pl-3 text-sm', + childList: 'ms-[19px] mt-0.5 flex flex-col gap-0.5 border-s border-default ps-0', + childItem: 'ms-0 ps-0', + childLink: 'h-8 max-lg:h-11 gap-2.5 py-0 pr-2.5 pl-3 text-sm', childLinkIcon: 'size-3.5', })) const loggingOut = ref(false) diff --git a/web/src/components/UserMenu.vue b/web/src/components/UserMenu.vue index 8e7ee44..251facf 100644 --- a/web/src/components/UserMenu.vue +++ b/web/src/components/UserMenu.vue @@ -147,7 +147,7 @@ const items = computed(() => [ :aria-expanded="chipExpanded" :class="chipClass" :trailing-icon="collapsed ? undefined : 'i-lucide-chevrons-up-down'" - :ui="{ trailingIcon: 'ms-auto size-3.5 text-current' }" + :ui="{ trailingIcon: 'ms-auto size-3.5 text-current max-lg:hidden' }" :aria-label="triggerAriaLabel" >