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))