diff --git a/src/renderer/src/components/pro/proCatalog.ts b/src/renderer/src/components/pro/proCatalog.ts index a5902eb3..9b5a942e 100644 --- a/src/renderer/src/components/pro/proCatalog.ts +++ b/src/renderer/src/components/pro/proCatalog.ts @@ -12,7 +12,7 @@ import { ShieldCheck } from '@phosphor-icons/react' import type { ComponentType } from 'react' -import { deviceNoun } from '@renderer/lib/device' +import { deviceNoun, primaryModifier } from '@renderer/lib/device' import { isMac, type DevicePlatform } from '@offgrid/core/shared/device' import { PRO_PURCHASE_URL } from '@offgrid/core/shared/product-links' @@ -190,14 +190,16 @@ export const PRO_FEATURES: ProFeature[] = [ label: 'Clipboard', icon: ClipboardText, tagline: 'Every copy, kept and searchable.', - description: - 'A local clipboard history that saves what you copy - text, images, and files - with a global hotkey (Cmd+Shift+C) quick-paste popup to drop any past copy into whatever app you are in. Stored on-device, nothing leaves your machine.', + description: `A local clipboard history that saves what you copy - text, images, and files - with a global hotkey (${primaryModifier()}+Shift+C) quick-paste popup to drop any past copy into whatever app you are in. Stored on-device, nothing leaves your machine.`, highlights: [ 'Searchable history of text, images & files', - 'Cmd+Shift+C quick-paste popup anywhere', + `${primaryModifier()}+Shift+C quick-paste popup anywhere`, 'Stored locally in your encrypted database' ], - platforms: ['darwin'] + // Ported to Windows: the store + popup + global hotkey (CommandOrControl+Shift+C) + // are all cross-platform Electron; auto-paste is synthesized per-platform in + // pro text-injection (osascript on macOS, PowerShell SendKeys on Windows). + platforms: ['darwin', 'win32'] } ] diff --git a/src/renderer/src/lib/__tests__/device.test.ts b/src/renderer/src/lib/__tests__/device.test.ts index b8b6242c..c3706c8d 100644 --- a/src/renderer/src/lib/__tests__/device.test.ts +++ b/src/renderer/src/lib/__tests__/device.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, afterEach, vi } from 'vitest' -import { deviceNoun, isMac, currentPlatform } from '../device' +import { deviceNoun, isMac, currentPlatform, primaryModifier } from '../device' // The renderer wrapper resolves the platform from the preload-bridged // `window.api.platform` and delegates the naming rule to shared/device.ts. @@ -44,4 +44,13 @@ describe('renderer deviceNoun wrapper', () => { vi.stubGlobal('window', {}) expect(currentPlatform()).toBe('unknown') }) + + it('primaryModifier() reflects the bridged platform', () => { + vi.stubGlobal('window', { api: { platform: 'darwin' } }) + expect(primaryModifier()).toBe('Cmd') + vi.stubGlobal('window', { api: { platform: 'win32' } }) + expect(primaryModifier()).toBe('Ctrl') + vi.stubGlobal('window', {}) + expect(primaryModifier()).toBe('Ctrl') + }) }) diff --git a/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts b/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts index 32222b14..6cdb8aab 100644 --- a/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts +++ b/src/renderer/src/lib/__tests__/proCatalog.lookup.test.ts @@ -76,11 +76,25 @@ describe('featureSupportsPlatform (per-feature seam)', () => { expect(featureSupportsPlatform(winPorted('x'), 'linux')).toBe(false) }) - it('every shipped feature is still macOS-only today (nothing ported yet)', () => { + // Ported-to-Windows features, by route. Grows one entry per shipped Windows port; + // asserted against the catalog so a flipped `platforms` and this list can't drift. + const WIN_PORTED = new Set(['clipboard']) + + it('clipboard is live on Windows', () => { + expect(featureSupportsPlatform(getProFeature('clipboard')!, 'win32')).toBe(true) + }) + + it('exactly the ported features are win32-supported; the rest stay macOS-only', () => { for (const f of PRO_FEATURES) { - expect(featureSupportsPlatform(f, 'win32'), `win32 support for ${f.route}`).toBe(false) + expect(featureSupportsPlatform(f, 'win32'), `win32 support for ${f.route}`).toBe( + WIN_PORTED.has(f.route) + ) } }) + + it('a Windows port does not imply Linux (each platform is explicit)', () => { + expect(featureSupportsPlatform(getProFeature('clipboard')!, 'linux')).toBe(false) + }) }) describe('proFeatureComingSoon flips PER FEATURE (the seam works one at a time)', () => { @@ -109,12 +123,19 @@ describe('proComingSoonHere', () => { }) describe('proFeatureComingSoon', () => { - it('gates every Pro route for a Windows Pro subscriber', () => { + it('does NOT gate a Windows-ported route — clipboard renders live on win32', () => { + expect(proFeatureComingSoon('clipboard', 'win32', true)).toBe(false) + expect(proFeatureComingSoon('clipboard', 'darwin', true)).toBe(false) + expect(proFeatureComingSoon('clipboard', 'win32', false)).toBe(false) + }) + + it('gates every NOT-yet-ported catalog route for a Windows Pro subscriber', () => { for (const feature of PRO_FEATURES) { + const ported = featureSupportsPlatform(feature, 'win32') expect( proFeatureComingSoon(feature.route, 'win32', true), `coming-soon for ${feature.route}` - ).toBe(true) + ).toBe(!ported) } }) diff --git a/src/renderer/src/lib/device.ts b/src/renderer/src/lib/device.ts index a64ee6b3..70fabb1b 100644 --- a/src/renderer/src/lib/device.ts +++ b/src/renderer/src/lib/device.ts @@ -1,6 +1,7 @@ import { deviceNoun as nounForPlatform, isMac as isMacForPlatform, + primaryModifier as modifierForPlatform, type DevicePlatform } from '@offgrid/core/shared/device' @@ -25,3 +26,8 @@ export function deviceNoun(opts?: { capitalize?: boolean }): string { export function isMac(): boolean { return isMacForPlatform(currentPlatform()) } + +/** The primary modifier label for shortcut copy ('Cmd' on macOS, else 'Ctrl'). */ +export function primaryModifier(): string { + return modifierForPlatform(currentPlatform()) +} diff --git a/src/shared/__tests__/device.test.ts b/src/shared/__tests__/device.test.ts index 0e4d688c..a201b41c 100644 --- a/src/shared/__tests__/device.test.ts +++ b/src/shared/__tests__/device.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { deviceNoun, isMac } from '../device' +import { deviceNoun, isMac, primaryModifier } from '../device' describe('deviceNoun', () => { it('names macOS the Mac (brand proper noun)', () => { @@ -50,3 +50,16 @@ describe('isMac', () => { expect(isMac('')).toBe(false) }) }) + +describe('primaryModifier', () => { + it('is "Cmd" on macOS', () => { + expect(primaryModifier('darwin')).toBe('Cmd') + }) + + it('is "Ctrl" on Windows/Linux/other (matches CommandOrControl)', () => { + expect(primaryModifier('win32')).toBe('Ctrl') + expect(primaryModifier('linux')).toBe('Ctrl') + expect(primaryModifier('unknown')).toBe('Ctrl') + expect(primaryModifier('')).toBe('Ctrl') + }) +}) diff --git a/src/shared/device.ts b/src/shared/device.ts index bfcfb59c..092938b0 100644 --- a/src/shared/device.ts +++ b/src/shared/device.ts @@ -34,3 +34,14 @@ export function deviceNoun(platform: DevicePlatform, opts?: { capitalize?: boole export function isMac(platform: DevicePlatform): boolean { return platform === 'darwin' } + +/** + * The primary keyboard modifier label for the platform, for showing shortcuts in + * copy: `'Cmd'` on macOS, `'Ctrl'` everywhere else. Mirrors Electron's + * `CommandOrControl` accelerator (which maps to ⌘ on Mac, Ctrl on Windows/Linux), + * so a hotkey described in the UI matches the key the app actually registers. + * Single source of truth so shortcut copy never drifts between platforms. + */ +export function primaryModifier(platform: DevicePlatform): string { + return isMac(platform) ? 'Cmd' : 'Ctrl' +}