diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--dark-mobile.png new file mode 100644 index 000000000..aaf67f539 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--dark-tablet.png new file mode 100644 index 000000000..e5ecb9e1f Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-desktop.png new file mode 100644 index 000000000..ba3012aea Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-mobile.png new file mode 100644 index 000000000..27951e138 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-tablet.png new file mode 100644 index 000000000..dc2a13f50 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy.png new file mode 100644 index 000000000..ab0b45f7b Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-inputs-switch--privacy.png differ diff --git a/apps/storybook/src/stories/components/inputs/switch/Switch.stories.js b/apps/storybook/src/stories/components/inputs/switch/Switch.stories.js index ded5f9992..ec1fb2ce0 100644 --- a/apps/storybook/src/stories/components/inputs/switch/Switch.stories.js +++ b/apps/storybook/src/stories/components/inputs/switch/Switch.stories.js @@ -145,3 +145,39 @@ export const Types = { } } } + +const PRIVACY_SCRIPT = [ + IMPORT, + "import { ref } from 'vue'", + '', + 'const off = ref(false)', + 'const on = ref(true)' +] + +const PRIVACY_TEMPLATE = `
+ + +
` + +/** @type {import('@storybook/vue3').StoryObj} */ +export const Privacy = { + render: () => ({ + components: { Switch }, + setup() { + const off = ref(false) + const on = ref(true) + return { off, on } + }, + template: PRIVACY_TEMPLATE + }), + parameters: { + docs: { + controls: { disable: true }, + description: { + story: + 'Privacy variant in both states. The off handle carries a closed lock (`pi-lock`) tinted `var(--bg-surface)` against the muted handle; the on handle carries an open lock (`pi-lock-open`) tinted `var(--text-default)` against the canvas handle — so the icon contrasts against the handle rather than blending into the surrounding `var(--success-contrast)` track.' + }, + source: { code: toSfc(PRIVACY_SCRIPT, PRIVACY_TEMPLATE) } + } + } +} diff --git a/packages/webkit/src/components/inputs/switch/switch.test.ts b/packages/webkit/src/components/inputs/switch/switch.test.ts index 10f628902..a9f90b4ee 100644 --- a/packages/webkit/src/components/inputs/switch/switch.test.ts +++ b/packages/webkit/src/components/inputs/switch/switch.test.ts @@ -186,6 +186,49 @@ describe('Switch', () => { }) }) + describe('privacy lock icon', () => { + it('renders no lock icon when kind=default', () => { + const { getByTestId } = render(Switch, { + props: { kind: 'default', modelValue: true }, + attrs: { 'aria-label': 'Toggle setting' } + }) + + const handle = getByTestId('input-switch__handle') + expect(handle.querySelector('i.pi')).toBeNull() + }) + + it('renders pi-lock tinted var(--bg-surface) when privacy is off', () => { + const { getByTestId } = render(Switch, { + props: { kind: 'privacy', modelValue: false }, + attrs: { 'aria-label': 'Toggle setting' } + }) + + const icon = getByTestId('input-switch__handle').querySelector('i.pi') as HTMLElement | null + expect(icon).not.toBeNull() + expect(icon!.classList.contains('pi-lock')).toBe(true) + expect(icon!.classList.contains('pi-lock-open')).toBe(false) + expect(icon!.className).toContain('text-[var(--bg-surface)]') + expect(icon!.getAttribute('aria-hidden')).toBe('true') + }) + + it('renders pi-lock-open tinted var(--text-default) when privacy is on (contrasts against handle, not track)', () => { + const { getByTestId } = render(Switch, { + props: { kind: 'privacy', modelValue: true }, + attrs: { 'aria-label': 'Toggle setting' } + }) + + const icon = getByTestId('input-switch__handle').querySelector('i.pi') as HTMLElement | null + expect(icon).not.toBeNull() + expect(icon!.classList.contains('pi-lock-open')).toBe(true) + expect(icon!.classList.contains('pi-lock')).toBe(false) + // The on icon must NOT reuse --success-contrast (the surrounding track color) + // — that made the icon "blend" into the track visually. It uses --text-default + // so it contrasts against the handle's --bg-canvas fill in both themes. + expect(icon!.className).toContain('text-[var(--text-default)]') + expect(icon!.className).not.toContain('text-[var(--success-contrast)]') + }) + }) + describe('smoke over type variants', () => { it.each([['default'], ['privacy']] as const)('renders data-kind=%s', (type) => { const { getByTestId } = render(Switch, { diff --git a/packages/webkit/src/components/inputs/switch/switch.vue b/packages/webkit/src/components/inputs/switch/switch.vue index 6ff8ed5cf..3183e5c0f 100644 --- a/packages/webkit/src/components/inputs/switch/switch.vue +++ b/packages/webkit/src/components/inputs/switch/switch.vue @@ -72,7 +72,7 @@ const LOCK_ICON_CLASS = 'pi text-body-xxs leading-none' const LOCK_OFF_CLASS = `${LOCK_ICON_CLASS} pi-lock text-[var(--bg-surface)]` - const LOCK_ON_CLASS = `${LOCK_ICON_CLASS} pi-lock-open text-[var(--success-contrast)]` + const LOCK_ON_CLASS = `${LOCK_ICON_CLASS} pi-lock-open text-[var(--text-default)]` const rootClass = computed(() => cn(ROOT_CLASS, attrs.class as string | undefined))