diff --git a/.github/workflows/store-release.yml b/.github/workflows/store-release.yml index ba4dd67..b170865 100644 --- a/.github/workflows/store-release.yml +++ b/.github/workflows/store-release.yml @@ -21,13 +21,28 @@ permissions: jobs: package: + name: Package ${{ matrix.productName }} runs-on: windows-latest timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - variant: prompterpro + productName: PrompterPro + identityName: ReleasedPtyLtd.PrompterPro + productId: "" + - variant: simpleprompt + productName: SimplePrompt + identityName: ReleasedPtyLtd.SimplePrompt + productId: 9MT1X5BNTHQS env: - MS_STORE_PRODUCT_ID: ${{ vars.MS_STORE_PRODUCT_ID }} - MS_STORE_IDENTITY_NAME: ${{ vars.MS_STORE_IDENTITY_NAME }} - MS_STORE_PUBLISHER: ${{ vars.MS_STORE_PUBLISHER }} - MS_STORE_PUBLISHER_DISPLAY_NAME: ${{ vars.MS_STORE_PUBLISHER_DISPLAY_NAME }} + PROMPTER_VARIANT: ${{ matrix.variant }} + VITE_APP_VARIANT: ${{ matrix.variant }} + MS_STORE_PRODUCT_ID: ${{ matrix.productId || vars.MS_STORE_PRODUCT_ID }} + MS_STORE_IDENTITY_NAME: ${{ matrix.identityName }} + MS_STORE_PUBLISHER: CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897 + MS_STORE_PUBLISHER_DISPLAY_NAME: Released Pty Ltd steps: - name: Check out repository @@ -80,8 +95,8 @@ jobs: - name: Upload MSIX bundle artifact uses: actions/upload-artifact@v7 with: - name: PrompterPro-${{ env.MS_STORE_VERSION }}-x64-arm64 - path: out/store/*.msixbundle + name: ${{ matrix.productName }}-${{ env.MS_STORE_VERSION }}-x64-arm64 + path: out/store/${{ matrix.productName }}_*.msixbundle if-no-files-found: error compression-level: 0 @@ -129,6 +144,6 @@ jobs: --sellerId $env:SELLER_ID ` --clientId $env:AZURE_AD_APPLICATION_CLIENT_ID ` --clientSecret $env:AZURE_AD_APPLICATION_SECRET - $package = Get-ChildItem "out/store/*.msixbundle" | + $package = Get-ChildItem "out/store/${{ matrix.productName }}_*.msixbundle" | Select-Object -First 1 msstore publish $package.FullName -id $env:MS_STORE_PRODUCT_ID diff --git a/desktop/main.mjs b/desktop/main.mjs index eecb05f..2a87c85 100644 --- a/desktop/main.mjs +++ b/desktop/main.mjs @@ -15,6 +15,22 @@ const MODEL_DIRECTORY_NAME = "sherpa-onnx-streaming-zipformer-en-20M-2023-02-17"; const BACKGROUND_COLOR = "#0b0d0f"; const LEGACY_USER_DATA_DIRECTORY_NAME = "Prompter"; +const executableName = path.basename( + process.execPath, + path.extname(process.execPath), +); +const desktopBrand = + executableName.toLowerCase() === "simpleprompt" + ? { + name: "SimplePrompt", + appUserModelId: "ReleasedPtyLtd.SimplePrompt", + userDataDirectoryName: "SimplePrompt", + } + : { + name: "PrompterPro", + appUserModelId: "ReleasedPtyLtd.PrompterPro", + userDataDirectoryName: LEGACY_USER_DATA_DIRECTORY_NAME, + }; let mainWindow = null; let localServer = null; @@ -27,7 +43,7 @@ let readyToQuit = false; // scripts, device preferences, and other local browser data survive upgrades. app.setPath( "userData", - path.join(app.getPath("appData"), LEGACY_USER_DATA_DIRECTORY_NAME), + path.join(app.getPath("appData"), desktopBrand.userDataDirectoryName), ); function isLoopbackUrl(value) { @@ -68,13 +84,17 @@ function configurePermissions() { function configureScriptStorage() { ipcMain.handle("prompter:scripts:load", (event) => { if (!isAppUrl(event.senderFrame?.url)) { - throw new Error("Script storage is only available to PrompterPro."); + throw new Error( + `Script storage is only available to ${desktopBrand.name}.`, + ); } return loadScriptStore(app.getPath("userData")); }); ipcMain.handle("prompter:scripts:save", (event, scripts) => { if (!isAppUrl(event.senderFrame?.url)) { - throw new Error("Script storage is only available to PrompterPro."); + throw new Error( + `Script storage is only available to ${desktopBrand.name}.`, + ); } const saveOperation = scriptSaveQueue .catch(() => undefined) @@ -87,6 +107,7 @@ function configureScriptStorage() { async function startLocalServer() { process.env.PORT = "0"; process.env.PROMPTER_PRODUCTION = "1"; + process.env.PROMPTER_PRODUCT_NAME = desktopBrand.name; process.env.SHERPA_ONNX_MODEL_DIR = app.isPackaged ? path.join(process.resourcesPath, MODEL_DIRECTORY_NAME) : path.join(app.getAppPath(), ".models", MODEL_DIRECTORY_NAME); @@ -104,7 +125,9 @@ async function startLocalServer() { const address = localServer.address(); if (!address || typeof address === "string") { - throw new Error("PrompterPro could not reserve a local server port."); + throw new Error( + `${desktopBrand.name} could not reserve a local server port.`, + ); } appUrl = `http://127.0.0.1:${address.port}`; } @@ -169,10 +192,12 @@ async function startArm64Sidecar() { } async function createWindow() { - if (!appUrl) throw new Error("The local PrompterPro server is not ready."); + if (!appUrl) { + throw new Error(`The local ${desktopBrand.name} server is not ready.`); + } mainWindow = new BrowserWindow({ - title: "PrompterPro", + title: desktopBrand.name, width: 1440, height: 960, minWidth: 1040, @@ -215,7 +240,7 @@ if (!hasSingleInstanceLock) { app.whenReady() .then(async () => { - app.setAppUserModelId("ReleasedPtyLtd.PrompterPro"); + app.setAppUserModelId(desktopBrand.appUserModelId); configurePermissions(); await startLocalServer(); configureScriptStorage(); @@ -224,7 +249,7 @@ if (!hasSingleInstanceLock) { .catch((error) => { const message = error instanceof Error ? error.message : "Unknown startup error"; - dialog.showErrorBox("PrompterPro could not start", message); + dialog.showErrorBox(`${desktopBrand.name} could not start`, message); app.quit(); }); diff --git a/package.json b/package.json index 5b50af1..dc01054 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "main": "desktop/main.mjs", "scripts": { "dev": "concurrently -k -n API,WEB -c cyan,magenta \"tsx watch server/index.ts\" \"vite\"", + "site:render": "tsx site/render-static.tsx", + "site:dev": "npm run site:render && vite site", + "site:build": "npm run site:render && tsc -p site/tsconfig.json --pretty false && vite build site", "build": "tsc -b && vite build", "build:server": "node scripts/build-server.mjs", "build:desktop": "npm run build && npm run build:server", diff --git a/public/simpleprompt-mark.svg b/public/simpleprompt-mark.svg new file mode 100644 index 0000000..4fe2ac0 --- /dev/null +++ b/public/simpleprompt-mark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/package-windows.mjs b/scripts/package-windows.mjs index a9a5a6a..1b62db1 100644 --- a/scripts/package-windows.mjs +++ b/scripts/package-windows.mjs @@ -17,6 +17,7 @@ import { DEFAULT_STORE_IDENTITY, renderStoreManifest, } from "./store-manifest.mjs"; +import { getProductVariant } from "./product-variants.mjs"; const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); const rootDirectory = path.resolve(scriptDirectory, ".."); @@ -34,6 +35,13 @@ const outputDirectory = process.env.PROMPTER_PACKAGE_OUTPUT_DIR const packageJson = JSON.parse( await readFile(path.join(rootDirectory, "package.json"), "utf8"), ); +const productVariant = getProductVariant( + process.env.PROMPTER_VARIANT || process.env.VITE_APP_VARIANT, +); +const productAssetsDirectory = path.join( + storeDirectory, + productVariant.assetDirectory, +); const allowedAppEntries = new Set([ "desktop", "dist", @@ -121,25 +129,25 @@ for (const architecture of architectures) { console.log(`Packaging the ${architecture.name} Electron application...`); const packagePaths = await packager({ dir: rootDirectory, - name: packageJson.productName, - executableName: packageJson.productName, + name: productVariant.productName, + executableName: productVariant.productName, platform: "win32", arch: architecture.name, out: outputDirectory, overwrite: true, prune: true, asar: architecture.asar, - icon: path.join(storeDirectory, "assets", "AppIcon.ico"), + icon: path.join(productAssetsDirectory, "AppIcon.ico"), extraResource: [modelDirectory], afterPrune: [removeUnneededRuntimeModules], appVersion: packageJson.version, buildVersion: packageJson.version, win32metadata: { CompanyName: packageJson.author, - FileDescription: packageJson.description, - ProductName: packageJson.productName, - InternalName: packageJson.productName, - OriginalFilename: `${packageJson.productName}.exe`, + FileDescription: productVariant.description, + ProductName: productVariant.productName, + InternalName: productVariant.productName, + OriginalFilename: `${productVariant.productName}.exe`, "requested-execution-level": "asInvoker", }, ignore: shouldIgnorePackageFile, @@ -153,7 +161,7 @@ for (const architecture of architectures) { const packageDirectory = packagePaths[0]; await assertPeArchitecture( - path.join(packageDirectory, `${packageJson.productName}.exe`), + path.join(packageDirectory, `${productVariant.productName}.exe`), architecture.name, ); await assertPackagedAppAllowList( @@ -161,7 +169,7 @@ for (const architecture of architectures) { architecture.name, ); await cp( - path.join(storeDirectory, "assets"), + productAssetsDirectory, path.join(packageDirectory, "Assets"), { recursive: true }, ); @@ -178,7 +186,7 @@ const msixOutputDirectory = path.join(outputDirectory, "store"); await mkdir(msixOutputDirectory, { recursive: true }); const bundlePath = path.join( msixOutputDirectory, - `${packageJson.productName}_${toSafeVersion(version)}_x64_arm64.msixbundle`, + `${productVariant.productName}_${toSafeVersion(version)}_x64_arm64.msixbundle`, ); const winAppCli = path.join( rootDirectory, @@ -358,15 +366,21 @@ async function addStoreManifest(packageDirectory, architecture) { architecture: architecture.name, identityName: process.env.MS_STORE_IDENTITY_NAME || - DEFAULT_STORE_IDENTITY.identityName, + productVariant.store.identityName || DEFAULT_STORE_IDENTITY.identityName, minimumWindowsVersion: architecture.minimumWindowsVersion, publisher: process.env.MS_STORE_PUBLISHER || - DEFAULT_STORE_IDENTITY.publisher, + productVariant.store.publisher || DEFAULT_STORE_IDENTITY.publisher, publisherDisplayName: process.env.MS_STORE_PUBLISHER_DISPLAY_NAME || + productVariant.store.publisherDisplayName || DEFAULT_STORE_IDENTITY.publisherDisplayName, version: process.env.MS_STORE_VERSION || packageJson.version, + displayName: productVariant.productName, + executableName: productVariant.productName, + applicationId: productVariant.applicationId, + description: productVariant.description, + backgroundColor: productVariant.backgroundColor, }); await writeFile( path.join(packageDirectory, "Package.appxmanifest"), diff --git a/scripts/product-variants.d.mts b/scripts/product-variants.d.mts new file mode 100644 index 0000000..6eb95a5 --- /dev/null +++ b/scripts/product-variants.d.mts @@ -0,0 +1,22 @@ +export interface ProductVariant { + readonly key: "prompterpro" | "simpleprompt"; + readonly productName: "PrompterPro" | "SimplePrompt"; + readonly assetDirectory: string; + readonly applicationId: string; + readonly description: string; + readonly backgroundColor: string; + readonly store: Readonly<{ + identityName: string; + publisher: string; + publisherDisplayName: string; + productId: string; + packageFamilyName?: string; + packageSid?: string; + }>; +} + +export const PRODUCT_VARIANTS: Readonly< + Record<"prompterpro" | "simpleprompt", ProductVariant> +>; + +export function getProductVariant(value?: string): ProductVariant; diff --git a/scripts/product-variants.mjs b/scripts/product-variants.mjs new file mode 100644 index 0000000..1761e02 --- /dev/null +++ b/scripts/product-variants.mjs @@ -0,0 +1,49 @@ +const sharedPublisher = Object.freeze({ + publisher: "CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897", + publisherDisplayName: "Released Pty Ltd", +}); + +export const PRODUCT_VARIANTS = Object.freeze({ + prompterpro: Object.freeze({ + key: "prompterpro", + productName: "PrompterPro", + assetDirectory: "assets", + applicationId: "Prompter", + description: + "An offline, voice-following teleprompter for polished video recording.", + backgroundColor: "#0b0d0f", + store: Object.freeze({ + ...sharedPublisher, + identityName: "ReleasedPtyLtd.PrompterPro", + productId: "", + }), + }), + simpleprompt: Object.freeze({ + key: "simpleprompt", + productName: "SimplePrompt", + assetDirectory: "simpleprompt/assets", + applicationId: "SimplePrompt", + description: + "A private, voice-following teleprompter for clear video recording.", + backgroundColor: "#0b0d0f", + store: Object.freeze({ + ...sharedPublisher, + identityName: "ReleasedPtyLtd.SimplePrompt", + productId: "9MT1X5BNTHQS", + packageFamilyName: "ReleasedPtyLtd.SimplePrompt_q0b077qanz1d8", + packageSid: + "S-1-15-2-2676728462-2596204801-2700890954-455437082-720273692-286854053-1939976512", + }), + }), +}); + +export function getProductVariant(value = "prompterpro") { + const key = value.trim().toLowerCase(); + const variant = PRODUCT_VARIANTS[key]; + if (!variant) { + throw new Error( + `Unknown product variant "${value}". Expected prompterpro or simpleprompt.`, + ); + } + return variant; +} diff --git a/scripts/smoke-arm64-sidecar.mjs b/scripts/smoke-arm64-sidecar.mjs index 3662b9f..9cc6c31 100644 --- a/scripts/smoke-arm64-sidecar.mjs +++ b/scripts/smoke-arm64-sidecar.mjs @@ -5,17 +5,21 @@ import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import WebSocket from "ws"; +import { getProductVariant } from "./product-variants.mjs"; const require = createRequire(import.meta.url); const sherpa = require("sherpa-onnx-node"); const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); const rootDirectory = path.resolve(scriptDirectory, ".."); +const productVariant = getProductVariant( + process.env.PROMPTER_VARIANT || process.env.VITE_APP_VARIANT, +); const packageOutputDirectory = process.env.PROMPTER_PACKAGE_OUTPUT_DIR ? path.resolve(rootDirectory, process.env.PROMPTER_PACKAGE_OUTPUT_DIR) : path.join(rootDirectory, "out"); const arm64Directory = path.join( packageOutputDirectory, - "PrompterPro-win32-arm64", + `${productVariant.productName}-win32-arm64`, ); const nodeExecutable = path.join( arm64Directory, @@ -44,6 +48,7 @@ const sidecar = spawn(nodeExecutable, [serverEntry], { ...process.env, PORT: "0", PROMPTER_PRODUCTION: "1", + PROMPTER_PRODUCT_NAME: productVariant.productName, SHERPA_ONNX_MODEL_DIR: modelDirectory, }, stdio: ["ignore", "pipe", "pipe", "ipc"], diff --git a/scripts/store-manifest.d.mts b/scripts/store-manifest.d.mts index 94d61c2..3436124 100644 --- a/scripts/store-manifest.d.mts +++ b/scripts/store-manifest.d.mts @@ -5,12 +5,17 @@ export interface StoreManifestValues { publisher: string; publisherDisplayName: string; version: string; + displayName: string; + executableName: string; + applicationId: string; + description: string; + backgroundColor: string; } export const DEFAULT_STORE_IDENTITY: Readonly<{ identityName: "ReleasedPtyLtd.PrompterPro"; publisher: "CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897"; - publisherDisplayName: "Released Pty Ltd"; + publisherDisplayName: string; }>; export function toWindowsVersion(version: string): string; diff --git a/scripts/store-manifest.mjs b/scripts/store-manifest.mjs index 419f3fb..ee6317a 100644 --- a/scripts/store-manifest.mjs +++ b/scripts/store-manifest.mjs @@ -1,11 +1,9 @@ const WINDOWS_VERSION_PATTERN = /^\d+(?:\.\d+){0,3}$/; const WINDOWS_ARCHITECTURES = new Set(["x64", "arm64"]); -export const DEFAULT_STORE_IDENTITY = Object.freeze({ - identityName: "ReleasedPtyLtd.PrompterPro", - publisher: "CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897", - publisherDisplayName: "Released Pty Ltd", -}); +import { PRODUCT_VARIANTS } from "./product-variants.mjs"; + +export const DEFAULT_STORE_IDENTITY = PRODUCT_VARIANTS.prompterpro.store; export function toWindowsVersion(version) { const numericVersion = version.split("-", 1)[0]; @@ -45,6 +43,11 @@ export function renderStoreManifest(template, values) { "__VERSION__": toWindowsVersion(values.version), "__PROCESSOR_ARCHITECTURE__": values.architecture, "__MIN_WINDOWS_VERSION__": values.minimumWindowsVersion, + "__DISPLAY_NAME__": values.displayName, + "__EXECUTABLE_NAME__": values.executableName, + "__APPLICATION_ID__": values.applicationId, + "__DESCRIPTION__": values.description, + "__BACKGROUND_COLOR__": values.backgroundColor, }; let manifest = template; diff --git a/server/index.ts b/server/index.ts index e517114..1700025 100644 --- a/server/index.ts +++ b/server/index.ts @@ -227,9 +227,10 @@ server.listen(port, "127.0.0.1", () => { type: "prompter:server-ready", port: activePort, }); + const productName = process.env.PROMPTER_PRODUCT_NAME || "PrompterPro"; console.log( production - ? `PrompterPro is running at http://127.0.0.1:${activePort}` - : `PrompterPro API is running at http://127.0.0.1:${activePort}`, + ? `${productName} is running at http://127.0.0.1:${activePort}` + : `${productName} API is running at http://127.0.0.1:${activePort}`, ); }); diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..a1951d7 --- /dev/null +++ b/site/README.md @@ -0,0 +1,19 @@ +# SimplePrompt website + +This folder contains the standalone SimplePrompt marketing website. Its entry +point uses plain HTML, CSS, and JavaScript, so it works directly with VS Code +Live Server. The React source remains available for typed component development, +and the site builds separately from the desktop application. + +From the repository root: + +```powershell +npm.cmd run site:dev +npm.cmd run site:build +``` + +The production site is written to `site/dist/`. + +`site:dev` and `site:build` regenerate the Live Server-compatible `index.html` +from `src/App.tsx` before starting or building, keeping the static entry point +and typed source in sync. diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..ead26a1 --- /dev/null +++ b/site/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + SimplePrompt — Your words. Your pace. + + +
Built for natural delivery

Your words.
Your pace.

The private teleprompter that listens as you speak, follows naturally, and helps every take feel like you.

$1.99 per month Billed monthly
SimplePrompt
ScriptsStudio
Local-first
“The best ideas don’t need to sound rehearsed.They just need the space to land.Take a breath, look into the lens, and make it yours.”
Following your voice
00:18
VOICE PACENatural & steady
Prompt hidden from recording
Voice-following that keeps up
100% local speech recognition
Clean MP4 recordings
Ready in under a minute
Less managing. More connecting.

A teleprompter that gets
out of your way.

Everything you need to sound prepared—without looking or feeling scripted.

01

It follows your voice

SimplePrompt tracks where you are in the script and moves at your pace. Pause, improvise, or take a breath—it waits for you.

02

Record a clean take

Capture crisp camera and microphone footage. Your prompt stays on screen for you, never burned into the final video.

03

Private by design

Scripts, recordings, and voice recognition stay on your computer. Your work remains yours—full stop.

BRAND STORY · TAKE 02
Product launch2:30 min

Great products start with a simple idea: make something people genuinely want to use.

You
Your new pre-record ritual

From blank page
to brilliant take.

1

Write or generate

Paste your script or turn a few talking points into a polished first draft with AI.

2

Set your eye-line

Place your words near the camera and choose the type size that feels natural.

3

Speak. It follows.

SimplePrompt listens locally and keeps your next words right where you need them.

Your voice is nobody else's business

Private means
private.

Voice recognition and recording stay on your computer. Optional AI drafting sends only the details you enter in the draft form to OpenAI. Your local script library is never uploaded.

On-device speech recognition Local script library No account required
Stays on
this device
Voice Scripts Video
Your next take is the one

Sound like yourself.
Only better prepared.

Get SimplePrompt for $1.99 per month and make camera confidence your new default.

Get SimplePromptWindows 11 · $1.99 per month · No account needed
+ + + diff --git a/site/render-static.tsx b/site/render-static.tsx new file mode 100644 index 0000000..8513c09 --- /dev/null +++ b/site/render-static.tsx @@ -0,0 +1,32 @@ +import { writeFile } from "node:fs/promises"; +import path from "node:path"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +(globalThis as typeof globalThis & { React: typeof React }).React = React; +const { default: App } = await import("./src/App"); +const markup = renderToStaticMarkup(); +const outputPath = path.join(import.meta.dirname, "index.html"); + +const document = ` + + + + + + + + + SimplePrompt — Your words. Your pace. + + + ${markup} + + + +`; + +await writeFile(outputPath, document, "utf8"); diff --git a/site/simpleprompt-mark.svg b/site/simpleprompt-mark.svg new file mode 100644 index 0000000..4fe2ac0 --- /dev/null +++ b/site/simpleprompt-mark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/site/site.js b/site/site.js new file mode 100644 index 0000000..87ab092 --- /dev/null +++ b/site/site.js @@ -0,0 +1,23 @@ +const menuButton = document.querySelector(".menu-button"); +const navigation = document.querySelector(".site-header nav"); + +menuButton?.addEventListener("click", () => { + const open = navigation?.classList.toggle("nav-open") ?? false; + menuButton.setAttribute("aria-expanded", String(open)); + menuButton.setAttribute("aria-label", open ? "Close menu" : "Open menu"); +}); + +navigation?.querySelectorAll("a").forEach((link) => { + link.addEventListener("click", () => { + navigation.classList.remove("nav-open"); + menuButton?.setAttribute("aria-expanded", "false"); + menuButton?.setAttribute("aria-label", "Open menu"); + }); +}); + +const demoButton = document.querySelector(".studio-controls button"); +demoButton?.addEventListener("click", () => { + const isPaused = demoButton.getAttribute("aria-label") === "Play demo"; + demoButton.setAttribute("aria-label", isPaused ? "Pause demo" : "Play demo"); + demoButton.classList.toggle("is-paused", !isPaused); +}); diff --git a/site/src/App.tsx b/site/src/App.tsx new file mode 100644 index 0000000..3e0f9b3 --- /dev/null +++ b/site/src/App.tsx @@ -0,0 +1,242 @@ +import { useState } from "react"; +import { + ArrowRight, + AudioLines, + Check, + ChevronRight, + Clapperboard, + Download, + FileText, + Menu, + Mic2, + MonitorDown, + MousePointer2, + Pause, + Play, + ShieldCheck, + Sparkles, + Video, + WandSparkles, + X, + Zap, +} from "lucide-react"; + +const features = [ + { + icon: Mic2, + number: "01", + title: "It follows your voice", + copy: "SimplePrompt tracks where you are in the script and moves at your pace. Pause, improvise, or take a breath—it waits for you.", + tone: "lime", + }, + { + icon: Video, + number: "02", + title: "Record a clean take", + copy: "Capture crisp camera and microphone footage. Your prompt stays on screen for you, never burned into the final video.", + tone: "violet", + }, + { + icon: ShieldCheck, + number: "03", + title: "Private by design", + copy: "Scripts, recordings, and voice recognition stay on your computer. Your work remains yours—full stop.", + tone: "blue", + }, +]; + +export default function App() { + const [menuOpen, setMenuOpen] = useState(false); + const [demoPlaying, setDemoPlaying] = useState(true); + + return ( +
+
+ + + SimplePrompt + + + + + + Get SimplePrompt + + +
+ +
+
+
+ + Built for natural delivery + + +

Your words.
Your pace.

+

+ The private teleprompter that listens as you speak, follows naturally, and helps every take feel like you. +

+ +
+ $1.99 per month + Billed monthly +
+
+ +
+
+
+
+
+
+ + SimplePrompt +
+
ScriptsStudio
+ Local-first +
+
+
+ “The best ideas don’t need to sound rehearsed. + They just need the space to land. + Take a breath, look into the lens, and make it yours.” +
+
+ +
+
Voice-following that keeps up
+
100% local speech recognition
+
Clean MP4 recordings
+
Ready in under a minute
+
+ +
+
+ Less managing. More connecting. +

A teleprompter that gets
out of your way.

+

Everything you need to sound prepared—without looking or feeling scripted.

+
+
+ {features.map(({ icon: Icon, number, title, copy, tone }) => ( +
+
{number}
+

{title}

+

{copy}

+
+
+ ))} +
+
+ +
+
+
BRAND STORY · TAKE 02
+
+
Product launch2:30 min
+

Great products start with a simple idea: make something people genuinely want to use.

+
+
+
You
+
+
+ Your new pre-record ritual +

From blank page
to brilliant take.

+
+
1

Write or generate

Paste your script or turn a few talking points into a polished first draft with AI.

+
2

Set your eye-line

Place your words near the camera and choose the type size that feels natural.

+
3

Speak. It follows.

SimplePrompt listens locally and keeps your next words right where you need them.

+
+
+
+ +
+
+ + Your voice is nobody else's business +

Private means
private.

+

Voice recognition and recording stay on your computer. Optional AI drafting sends only the details you enter in the draft form to OpenAI. Your local script library is never uploaded.

+
+ On-device speech recognition + Local script library + No account required +
+
+
+
+
+
Stays on
this device
+ Voice + Scripts + +
+
+ +
+
+
+ Your next take is the one +

Sound like yourself.
Only better prepared.

+

Get SimplePrompt for $1.99 per month and make camera confidence your new default.

+ + Get SimplePrompt + + Windows 11 · $1.99 per month · No account needed +
+ + +
+ + +
+ ); +} diff --git a/site/src/main.tsx b/site/src/main.tsx new file mode 100644 index 0000000..693ac2e --- /dev/null +++ b/site/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/site/src/styles.css b/site/src/styles.css new file mode 100644 index 0000000..6c659a0 --- /dev/null +++ b/site/src/styles.css @@ -0,0 +1,159 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&display=swap'); + +:root { + font-family: "DM Sans", system-ui, sans-serif; + color: #f6f7f2; + background: #0a0c0d; + font-synthesis: none; + text-rendering: optimizeLegibility; + --ink: #0a0c0d; + --paper: #f4f5ef; + --lime: #55dc82; + --muted: #9ca29c; + --line: rgba(255,255,255,.11); +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; min-width: 320px; min-height: 100vh; background: #0a0c0d; overflow-x: hidden; } +button, a { font: inherit; } +a { color: inherit; text-decoration: none; } +button, a { -webkit-tap-highlight-color: transparent; } +button:focus-visible, a:focus-visible { outline: 2px solid var(--lime); outline-offset: 4px; } + +.site-header { + position: absolute; inset: 0 0 auto; z-index: 20; height: 82px; max-width: 1240px; margin: auto; + padding: 0 28px; display: flex; align-items: center; justify-content: space-between; +} +.logo { display: inline-flex; align-items: center; gap: 10px; font: 700 17px "Manrope", sans-serif; letter-spacing: -.04em; } +.logo > span:last-child > span { color: var(--lime); } +.logo-mark { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; color: #101309; background: var(--lime); box-shadow: 0 0 30px rgba(85,220,130,.12); } +.site-header nav { position: absolute; left: 50%; transform: translateX(-50%); display: flex; gap: 34px; color: #a9aea9; font-size: 13px; } +.site-header nav a { transition: color .2s; } +.site-header nav a:hover { color: #fff; } +.header-cta { display: flex; align-items: center; gap: 9px; padding: 10px 15px; border: 1px solid rgba(255,255,255,.18); border-radius: 9px; font-size: 12px; font-weight: 600; transition: .2s; } +.header-cta:hover { background: #fff; color: #111; } +.menu-button { display: none; border: 0; background: none; color: #fff; } + +.hero { position: relative; min-height: 820px; overflow: hidden; display: grid; grid-template-columns: .84fr 1.16fr; align-items: center; gap: 34px; padding: 142px max(28px, calc((100vw - 1184px)/2)) 92px; background: radial-gradient(circle at 74% 33%, #1d2521 0, #0d1110 29%, #0a0c0d 55%); } +.hero::before { content: ""; position: absolute; inset: 0; opacity: .035; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.7'/%3E%3C/svg%3E"); } +.hero-glow { position: absolute; width: 520px; height: 520px; right: 10%; top: 20%; border-radius: 50%; background: rgba(207,255,92,.05); filter: blur(70px); } +.hero-copy { position: relative; z-index: 2; max-width: 570px; padding-top: 10px; } +.announce { display: inline-flex; align-items: center; gap: 10px; border: 1px solid rgba(85,220,130,.18); border-radius: 999px; padding: 5px 8px 5px 5px; color: #c9cec8; font-size: 11px; } +.announce span { display: flex; align-items: center; gap: 6px; border-radius: 999px; padding: 5px 10px; color: #171a0d; background: var(--lime); font-weight: 700; } +.hero h1, .section h2 { font-family: "Manrope", sans-serif; } +.hero h1 { margin: 26px 0 23px; font-size: clamp(58px, 6.15vw, 91px); line-height: .94; letter-spacing: -.075em; font-weight: 700; } +.hero h1 em, .download h2 em { color: var(--lime); font-style: normal; font-weight: 500; } +.hero-subtitle { max-width: 520px; margin: 0; color: #a7ada7; font-size: 18px; line-height: 1.65; } +.hero-actions { display: flex; align-items: center; gap: 13px; margin-top: 32px; } +.button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; padding: 0 20px; border-radius: 10px; font-size: 13px; font-weight: 700; transition: transform .2s, box-shadow .2s, background .2s; } +.button:hover { transform: translateY(-2px); } +.button-primary { color: #111408; background: var(--lime); box-shadow: 0 12px 35px rgba(85,220,130,.12); } +.button-secondary { border: 1px solid var(--line); color: #e6e9e4; background: rgba(255,255,255,.025); } +.hero-note { display: flex; gap: 20px; margin-top: 18px; color: #737a75; font-size: 10px; } +.hero-note span { display: flex; align-items: center; gap: 5px; } +.hero-note svg { color: var(--lime); } + +.product-stage { position: relative; z-index: 2; min-width: 0; height: 590px; display: flex; align-items: center; justify-content: center; perspective: 1200px; } +.stage-orbit { position: absolute; border: 1px solid rgba(85,220,130,.09); border-radius: 50%; } +.orbit-one { width: 560px; height: 560px; } +.orbit-two { width: 720px; height: 720px; } +.app-window { position: relative; z-index: 2; width: min(680px, 100%); aspect-ratio: 1.33; overflow: hidden; border: 1px solid rgba(255,255,255,.15); border-radius: 16px; background: #111416; box-shadow: 0 55px 100px rgba(0,0,0,.58), 0 0 0 9px rgba(255,255,255,.018); transform: rotateY(-4deg) rotateX(2deg); } +.window-bar { height: 44px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 13px; color: #7c837e; background: #101314; border-bottom: 1px solid rgba(255,255,255,.08); font-size: 8px; } +.window-brand { display: flex; align-items: center; gap: 6px; color: #dadeda; font-size: 9px; font-weight: 700; } +.mini-mark { width: 20px; height: 20px; display: grid; place-items: center; border-radius: 6px; color: #111; background: var(--lime); } +.window-nav { display: flex; align-items: center; gap: 4px; padding: 3px; border: 1px solid rgba(255,255,255,.07); border-radius: 6px; } +.window-nav span, .window-nav b { padding: 4px 10px; font-weight: 500; } +.window-nav b { color: #e8ebe6; border-radius: 4px; background: rgba(255,255,255,.08); } +.local-pill { justify-self: end; display: flex; align-items: center; gap: 4px; } +.local-pill svg { color: #79e89c; } +.studio-view { position: relative; height: calc(100% - 44px); overflow: hidden; background: radial-gradient(ellipse at 50% 28%, #4f5d50 0, #253027 34%, #121713 72%); } +.studio-view::before { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(0,0,0,.4), transparent 25%, transparent 75%, rgba(0,0,0,.4)), linear-gradient(0deg, rgba(0,0,0,.55), transparent 35%); } +.camera-copy { position: absolute; z-index: 4; top: 70px; left: 50%; width: 86%; transform: translateX(-50%); text-align: center; font: 500 clamp(14px, 1.45vw, 20px)/1.62 "Manrope", sans-serif; letter-spacing: -.025em; text-shadow: 0 2px 10px #000; } +.camera-copy span, .camera-copy strong { display: block; } +.camera-copy span { color: rgba(255,255,255,.42); } +.camera-copy strong { margin: 2px -5px; padding: 2px 10px; color: #fff; background: rgba(85,220,130,.13); border-radius: 5px; font-weight: 600; } +.camera-person { position: absolute; left: 50%; bottom: -80px; width: 250px; height: 320px; transform: translateX(-50%); filter: drop-shadow(0 12px 20px rgba(0,0,0,.3)); } +.person-head { position: absolute; z-index: 2; width: 112px; height: 140px; left: 69px; top: 31px; border-radius: 47% 47% 42% 42%; background: linear-gradient(110deg, #bd876b, #e1a685 55%, #9a634e); } +.person-hair { position: absolute; z-index: 3; width: 130px; height: 83px; left: 61px; top: 19px; border-radius: 58% 50% 36% 34%; background: #29201b; transform: rotate(-4deg); clip-path: polygon(0 0,100% 0,100% 76%,77% 56%,65% 80%,50% 50%,33% 72%,14% 55%,0 80%); } +.person-head::before, .person-head::after { content: ""; position: absolute; top: 64px; width: 9px; height: 4px; border-radius: 50%; background: #36251f; } +.person-head::before { left: 27px; }.person-head::after { right: 27px; } +.person-neck { position: absolute; z-index: 1; left: 97px; top: 150px; width: 56px; height: 70px; background: #b8785d; } +.person-body { position: absolute; left: 18px; bottom: 0; width: 215px; height: 165px; border-radius: 48% 48% 12px 12px; background: linear-gradient(120deg, #243d34, #162a23); } +.focus-line { position: absolute; z-index: 3; left: 4%; right: 4%; top: 45%; height: 1px; background: linear-gradient(90deg, transparent, rgba(85,220,130,.6), transparent); } +.following-badge { position: absolute; z-index: 5; bottom: 59px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; padding: 6px 10px; border: 1px solid rgba(255,255,255,.12); border-radius: 999px; background: rgba(10,14,11,.7); backdrop-filter: blur(8px); color: #cbd2ca; font-size: 8px; } +.following-badge span, .live-dot { width: 6px; height: 6px; border-radius: 50%; background: #79e89c; box-shadow: 0 0 0 3px rgba(121,232,156,.12); } +.studio-controls { position: absolute; z-index: 5; left: 13px; right: 13px; bottom: 11px; height: 40px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; border: 1px solid rgba(255,255,255,.09); border-radius: 8px; background: rgba(15,18,16,.82); backdrop-filter: blur(12px); } +.studio-controls button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 50%; color: #10130b; background: var(--lime); } +.control-left { display: flex; align-items: center; gap: 8px; padding-left: 13px; color: #d5d9d5; font-size: 8px; } +.control-left i { width: 5px; height: 5px; border-radius: 50%; background: #ff645d; } +.audio-meter { justify-self: end; display: flex; align-items: center; gap: 2px; padding-right: 14px; height: 18px; } +.audio-meter i { display: block; width: 2px; height: 25%; background: #7be89c; animation: meter .8s ease-in-out infinite alternate; } +.audio-meter i:nth-child(2), .audio-meter i:nth-child(5) { height: 65%; animation-delay: -.3s; }.audio-meter i:nth-child(3) { height: 90%; animation-delay: -.6s; }.audio-meter i:nth-child(4) { height: 45%; animation-delay: -.1s; } +@keyframes meter { to { transform: scaleY(1.8); } } +.floating-card { position: absolute; z-index: 5; display: flex; align-items: center; border: 1px solid rgba(255,255,255,.13); background: rgba(23,28,25,.88); backdrop-filter: blur(18px); box-shadow: 0 18px 45px rgba(0,0,0,.34); } +.pace-card { top: 55px; right: -18px; gap: 10px; padding: 12px 14px; border-radius: 11px; } +.pace-card > svg { color: var(--lime); }.pace-card div { display: grid; gap: 2px; }.pace-card small { color: #68706a; font-size: 6px; letter-spacing: .15em; }.pace-card strong { color: #dfe3df; font-size: 9px; }.pace-card .live-dot { margin-left: 7px; } +.clean-card { left: 12px; bottom: 60px; gap: 8px; padding: 10px 13px; border-radius: 9px; color: #cbd0cb; font-size: 8px; }.clean-card svg { padding: 2px; border-radius: 50%; color: #152111; background: var(--lime); } + +.trust-strip { min-height: 78px; display: grid; grid-template-columns: repeat(4, 1fr); max-width: 1184px; margin: auto; border-bottom: 1px solid rgba(255,255,255,.08); } +.trust-strip div { display: flex; align-items: center; justify-content: center; gap: 11px; color: #717872; font-size: 11px; }.trust-strip div + div { border-left: 1px solid rgba(255,255,255,.08); }.trust-strip svg { color: var(--lime); }.trust-strip strong { color: #c4c9c4; font-weight: 600; } +.section { max-width: 1240px; margin: auto; padding: 132px 28px; } +.section-heading { max-width: 660px; margin: 0 auto 60px; text-align: center; } +.kicker { color: #9aad70; font-size: 11px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; } +.section h2 { margin: 18px 0; font-size: clamp(40px, 4vw, 59px); line-height: 1.05; letter-spacing: -.06em; } +.section-heading p { color: #858c86; font-size: 15px; line-height: 1.7; } +.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } +.feature-card { position: relative; min-height: 335px; overflow: hidden; padding: 29px; border: 1px solid rgba(255,255,255,.09); border-radius: 16px; background: #101314; } +.feature-card::after { content: ""; position: absolute; right: -80px; bottom: -110px; width: 230px; height: 230px; border-radius: 50%; filter: blur(50px); opacity: .13; } +.feature-card.lime::after { background: var(--lime); }.feature-card.violet::after { background: #a88cff; }.feature-card.blue::after { background: #5db8ff; } +.feature-top { display: flex; justify-content: space-between; align-items: flex-start; color: #4d544e; font: 500 10px "Manrope"; letter-spacing: .12em; } +.feature-icon { display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid rgba(255,255,255,.09); border-radius: 12px; color: var(--lime); background: rgba(255,255,255,.025); }.violet .feature-icon { color: #aa9cff; }.blue .feature-icon { color: #78c4ff; } +.feature-card h3 { margin: 75px 0 12px; font: 600 23px "Manrope"; letter-spacing: -.04em; }.feature-card p { max-width: 310px; color: #7f8680; font-size: 13px; line-height: 1.65; }.feature-line { position: absolute; right: 29px; bottom: 29px; left: 29px; height: 1px; background: linear-gradient(90deg, rgba(85,220,130,.65), transparent); }.violet .feature-line { background: linear-gradient(90deg, rgba(170,156,255,.65), transparent); }.blue .feature-line { background: linear-gradient(90deg, rgba(120,196,255,.65), transparent); } + +.workflow { display: grid; grid-template-columns: 1fr 1fr; align-items: center; gap: 110px; max-width: none; padding-left: max(28px, calc((100vw - 1184px)/2)); padding-right: max(28px, calc((100vw - 1184px)/2)); background: #f0f1eb; color: #151714; } +.workflow-visual { position: relative; height: 530px; display: grid; place-items: center; } +.workflow-visual::before { content: ""; position: absolute; width: 440px; height: 440px; border-radius: 50%; background: #e3e5db; } +.script-card { position: absolute; width: min(470px, 92%); border-radius: 17px; background: #111513; box-shadow: 0 35px 70px rgba(33,38,30,.22); } +.script-back { height: 310px; transform: rotate(-7deg) translate(-20px, -12px); color: #737a73; padding: 25px; font-size: 8px; letter-spacing: .15em; background: #dfe3d7; box-shadow: none; } +.script-front { z-index: 2; min-height: 305px; padding: 25px; color: #eff1ed; transform: rotate(2deg); } +.script-card-bar { display: flex; align-items: center; gap: 9px; padding-bottom: 22px; border-bottom: 1px solid rgba(255,255,255,.09); color: #b5bbb6; font-size: 10px; }.script-card-bar svg { color: var(--lime); }.script-card-bar i { margin-left: auto; color: #626a63; font-style: normal; } +.script-card p { margin: 50px 10px 30px; font: 500 22px/1.55 "Manrope"; letter-spacing: -.035em; }.script-card mark { color: #0f120a; background: var(--lime); padding: 2px 3px; } +.script-progress { height: 3px; margin: 0 10px; border-radius: 3px; background: #2b312c; }.script-progress i { display: block; width: 61%; height: 100%; border-radius: inherit; background: var(--lime); } +.cursor-bubble { position: absolute; z-index: 4; right: -5px; top: 53%; display: flex; align-items: center; gap: 6px; padding: 8px 11px; border-radius: 7px 7px 7px 0; color: #111; background: var(--lime); font-size: 9px; font-weight: 700; box-shadow: 0 10px 20px rgba(0,0,0,.15); } +.workflow-copy h2 { margin-bottom: 45px; } +.steps { display: grid; gap: 25px; }.steps > div { display: grid; grid-template-columns: 35px 1fr; gap: 16px; }.steps > div > span { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid #cdd0c8; border-radius: 50%; color: #71804d; font-size: 11px; font-weight: 700; }.steps section { padding-bottom: 21px; border-bottom: 1px solid #d7d9d2; }.steps h3 { margin: 2px 0 7px; font: 700 15px "Manrope"; }.steps p { margin: 0; color: #737871; font-size: 12px; line-height: 1.6; } + +.privacy { display: grid; grid-template-columns: 1fr 1fr; align-items: center; gap: 80px; min-height: 740px; } +.privacy-copy { max-width: 530px; }.privacy-icon { width: 50px; height: 50px; display: grid; place-items: center; margin-bottom: 35px; border-radius: 14px; color: #13170b; background: var(--lime); }.privacy h2 em { color: #6f7e48; font-style: normal; font-weight: 500; }.privacy-copy > p { color: #89908a; font-size: 15px; line-height: 1.75; }.privacy-points { display: grid; gap: 12px; margin-top: 27px; color: #b5bbb5; font-size: 12px; }.privacy-points span { display: flex; align-items: center; gap: 9px; }.privacy-points svg { color: var(--lime); } +.privacy-graphic { position: relative; height: 500px; display: grid; place-items: center; } +.privacy-ring { position: absolute; border: 1px solid rgba(85,220,130,.15); border-radius: 50%; }.ring-outer { width: 440px; height: 440px; }.ring-inner { width: 300px; height: 300px; border-style: dashed; animation: spin 30s linear infinite; }@keyframes spin { to { transform: rotate(360deg); } } +.privacy-core { position: relative; z-index: 2; width: 176px; height: 176px; display: grid; place-content: center; gap: 13px; text-align: center; border: 1px solid rgba(85,220,130,.24); border-radius: 50%; color: var(--lime); background: radial-gradient(circle, #1b2417, #101410); box-shadow: 0 0 80px rgba(85,220,130,.07); }.privacy-core strong { color: #dce1db; font: 600 14px/1.35 "Manrope"; } +.privacy-chip { position: absolute; z-index: 3; display: flex; align-items: center; gap: 7px; padding: 9px 12px; border: 1px solid rgba(255,255,255,.1); border-radius: 8px; color: #aeb5af; background: #141817; font-size: 10px; }.privacy-chip svg { color: var(--lime); }.chip-one { top: 60px; left: 25%; }.chip-two { right: 8%; top: 44%; }.chip-three { bottom: 65px; left: 22%; } + +.download { position: relative; max-width: 1184px; min-height: 490px; overflow: hidden; display: grid; place-items: center; margin-bottom: 80px; padding: 90px 28px; border-radius: 24px; color: #111409; background: var(--lime); text-align: center; } +.download::before, .download::after { content: ""; position: absolute; width: 350px; height: 350px; border: 1px solid rgba(12,15,7,.12); border-radius: 50%; }.download::before { left: -170px; top: -140px; }.download::after { right: -160px; bottom: -190px; } +.download-noise { position: absolute; inset: 0; opacity: .05; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } +.download-copy { position: relative; z-index: 2; }.download .kicker { color: #64732a; }.download h2 { margin: 17px 0; font-size: clamp(42px, 5vw, 67px); }.download h2 em { color: #576622; }.download p { margin: 0 0 28px; color: #4a531f; font-size: 14px; }.button-dark { color: #f4f6f0; background: #11140e; box-shadow: 0 15px 30px rgba(29,34,13,.18); }.download small { display: block; margin-top: 16px; color: #64702f; font-size: 9px; }.download-spark { position: absolute; color: rgba(14,18,8,.18); }.spark-one { top: 80px; left: 12%; width: 34px; height: 34px; }.spark-two { right: 11%; bottom: 80px; width: 25px; height: 25px; transform: rotate(140deg); } + +footer { max-width: 1184px; min-height: 120px; display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: 40px; margin: auto; padding: 0 0 45px; color: #676d68; font-size: 10px; }.footer-logo { color: #f5f6f2; }.footer-links { display: flex; gap: 24px; }.footer-links a:hover { color: #fff; }.copyright { padding-left: 30px; border-left: 1px solid rgba(255,255,255,.09); } + +@media (max-width: 1000px) { + .hero { grid-template-columns: 1fr; padding-top: 145px; text-align: center; }.hero-copy { margin: auto; }.announce { margin: auto; }.hero-actions, .hero-note { justify-content: center; }.product-stage { width: min(730px, 100%); margin: auto; }.hero h1 { font-size: clamp(60px, 10vw, 88px); }.trust-strip { grid-template-columns: repeat(2, 1fr); }.trust-strip div { border-bottom: 1px solid rgba(255,255,255,.08); }.workflow { gap: 50px; }.privacy { gap: 25px; } + footer { padding: 0 28px 40px; } +} + +@media (max-width: 760px) { + .site-header { height: 72px; padding: 0 20px; }.site-header nav { position: absolute; top: 65px; left: 20px; right: 20px; transform: none; display: none; flex-direction: column; gap: 0; padding: 10px; border: 1px solid var(--line); border-radius: 12px; background: rgba(17,20,19,.97); box-shadow: 0 15px 40px rgba(0,0,0,.4); }.site-header nav.nav-open { display: flex; }.site-header nav a { padding: 13px; }.header-cta { display: none; }.menu-button { display: grid; place-items: center; } + .hero { min-height: auto; padding: 125px 20px 80px; }.hero h1 { font-size: clamp(51px, 16vw, 75px); }.hero-subtitle { font-size: 16px; }.hero-actions { flex-direction: column; }.hero-actions .button { width: 100%; }.hero-note { flex-wrap: wrap; }.product-stage { height: 420px; margin-top: 35px; }.app-window { width: 110%; transform: none; }.pace-card { right: -5px; }.clean-card { left: -5px; }.orbit-one { width: 400px; height: 400px; }.orbit-two { display: none; } + .trust-strip { padding: 16px 20px; grid-template-columns: 1fr; }.trust-strip div { min-height: 44px; justify-content: flex-start; border: 0 !important; } + .section { padding: 90px 20px; }.feature-grid { grid-template-columns: 1fr; }.feature-card { min-height: 300px; }.feature-card h3 { margin-top: 55px; } + .workflow { grid-template-columns: 1fr; padding-left: 20px; padding-right: 20px; }.workflow-visual { height: 430px; order: 2; }.script-card p { font-size: 18px; }.workflow-copy { order: 1; } + .privacy { grid-template-columns: 1fr; }.privacy-graphic { height: 420px; }.ring-outer { width: 340px; height: 340px; }.ring-inner { width: 250px; height: 250px; }.chip-one { left: 10%; }.chip-three { left: 8%; } + .download { width: calc(100% - 40px); margin-bottom: 65px; border-radius: 18px; }.download h2 br { display: none; } + footer { grid-template-columns: 1fr 1fr; gap: 24px; }.footer-links { justify-self: end; }.copyright { padding: 0; border: 0; justify-self: end; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; } +} diff --git a/site/src/vite-env.d.ts b/site/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/site/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 0000000..e935337 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/src/components/AIGenerator.tsx b/src/components/AIGenerator.tsx index 55efb18..a946a40 100644 --- a/src/components/AIGenerator.tsx +++ b/src/components/AIGenerator.tsx @@ -1,6 +1,7 @@ import { useState, type FormEvent } from "react"; import { ArrowRight, Sparkles, X } from "lucide-react"; import type { GenerateScriptInput } from "../types"; +import { appBrand } from "../lib/appBrand"; interface AIGeneratorProps { onClose: () => void; @@ -69,7 +70,7 @@ export function AIGenerator({ onClose, onGenerated }: AIGeneratorProps) {

Turn an idea into something worth saying.

- Give PrompterPro the substance. You stay in control of every word. + Give {appBrand.name} the substance. You stay in control of every word.

diff --git a/src/components/Brand.tsx b/src/components/Brand.tsx index 80b826c..059bee4 100644 --- a/src/components/Brand.tsx +++ b/src/components/Brand.tsx @@ -1,12 +1,13 @@ import { AudioLines } from "lucide-react"; +import { appBrand } from "../lib/appBrand"; export function Brand() { return ( -
+
- PrompterPro + {appBrand.name}
); } diff --git a/src/hooks/useLocalScripts.ts b/src/hooks/useLocalScripts.ts index e25a28c..bb982b0 100644 --- a/src/hooks/useLocalScripts.ts +++ b/src/hooks/useLocalScripts.ts @@ -1,13 +1,14 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { loadScripts, parseScripts, saveScripts } from "../lib/scriptStorage"; import type { PrompterScript, ScriptSource } from "../types"; +import { appBrand } from "../lib/appBrand"; const sampleScript: PrompterScript = { id: "welcome-to-prompter", - title: "Welcome to PrompterPro", + title: `Welcome to ${appBrand.name}`, body: "Great delivery starts with a clear idea and the confidence to say it naturally.\n\n" + - "PrompterPro keeps your next words close to the camera, follows your voice, and waits when you improvise. " + + `${appBrand.name} keeps your next words close to the camera, follows your voice, and waits when you improvise. ` + "The recording keeps running, so you can stay present instead of worrying about the controls.\n\n" + "Take a breath, look toward the lens, and make the message your own.", source: "manual", diff --git a/src/hooks/useSpeechFollower.ts b/src/hooks/useSpeechFollower.ts index e33b1ad..86a478e 100644 --- a/src/hooks/useSpeechFollower.ts +++ b/src/hooks/useSpeechFollower.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { alignTranscript, wordsFromText } from "../lib/alignment"; +import { appBrand } from "../lib/appBrand"; import { stabilizeAdvance, type PendingAdvance, @@ -198,7 +199,7 @@ export function useSpeechFollower( if (!disposed && !failureReported) { failureReported = true; setErrorMessage( - "The local speech engine could not be reached. Keep the PrompterPro API running.", + `The local speech engine could not be reached. Keep the ${appBrand.name} API running.`, ); setStatus("error"); } diff --git a/src/lib/appBrand.ts b/src/lib/appBrand.ts new file mode 100644 index 0000000..61e4c7c --- /dev/null +++ b/src/lib/appBrand.ts @@ -0,0 +1,18 @@ +export type AppVariant = "prompterpro" | "simpleprompt"; + +const requestedVariant = import.meta.env.VITE_APP_VARIANT?.toLowerCase(); + +export const appVariant: AppVariant = + requestedVariant === "simpleprompt" ? "simpleprompt" : "prompterpro"; + +export const appBrand = Object.freeze( + appVariant === "simpleprompt" + ? { + name: "SimplePrompt", + icon: "/simpleprompt-mark.svg", + } + : { + name: "PrompterPro", + icon: "/prompter-mark.svg", + }, +); diff --git a/src/main.tsx b/src/main.tsx index d89e75f..d8a0ec5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,15 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; +import { appBrand, appVariant } from "./lib/appBrand"; import "./styles.css"; +document.documentElement.dataset.appVariant = appVariant; +document.title = appBrand.name; +document + .querySelector('link[rel="icon"]') + ?.setAttribute("href", appBrand.icon); + if ("serviceWorker" in navigator && import.meta.env.PROD) { window.addEventListener("load", () => { void navigator.serviceWorker.register("/sw.js"); diff --git a/src/styles.css b/src/styles.css index 930cef5..3ec3d1e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -15,12 +15,26 @@ --muted: #8c949b; --muted-2: #626a71; --accent: #e8ff67; + --accent-rgb: 232, 255, 103; --accent-ink: #111508; --amber: #ffbd66; --red: #ff6259; --green: #79e89c; } +:root[data-app-variant="simpleprompt"] { + --accent: #55dc82; + --accent-rgb: 85, 220, 130; + --accent-ink: #07150c; + --green: #55dc82; +} + +:root[data-app-variant="simpleprompt"] body { + background: + radial-gradient(circle at 82% -15%, rgba(85, 220, 130, 0.08), transparent 28%), + var(--bg); +} + * { box-sizing: border-box; } @@ -36,7 +50,7 @@ body, body { min-height: 100vh; background: - radial-gradient(circle at 82% -15%, rgba(232, 255, 103, 0.06), transparent 28%), + radial-gradient(circle at 82% -15%, rgba(var(--accent-rgb), 0.06), transparent 28%), var(--bg); overflow: hidden; } @@ -111,7 +125,7 @@ a:focus-visible { border-radius: 10px; color: var(--accent-ink); background: var(--accent); - box-shadow: 0 7px 24px rgba(232, 255, 103, 0.12); + box-shadow: 0 7px 24px rgba(var(--accent-rgb), 0.12); } .app-header nav { @@ -235,19 +249,19 @@ p { align-items: center; gap: 11px; padding: 12px; - border: 1px solid rgba(232, 255, 103, 0.16); + border: 1px solid rgba(var(--accent-rgb), 0.16); border-radius: 12px; text-align: left; color: var(--text); background: - linear-gradient(120deg, rgba(232, 255, 103, 0.08), transparent 62%), + linear-gradient(120deg, rgba(var(--accent-rgb), 0.08), transparent 62%), #14181a; transition: border-color 160ms ease, transform 160ms ease; } .ai-draft-button:hover { transform: translateY(-1px); - border-color: rgba(232, 255, 103, 0.32); + border-color: rgba(var(--accent-rgb), 0.32); } .ai-draft-icon { @@ -353,7 +367,7 @@ p { .source-dot[data-source="ai"] { background: var(--accent); - box-shadow: 0 0 0 3px rgba(232, 255, 103, 0.09); + box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.09); } .card-menu-button { @@ -620,7 +634,7 @@ p { border: 1px solid var(--line-strong); border-radius: 18px; background: - radial-gradient(circle at 100% 0, rgba(232, 255, 103, 0.08), transparent 30%), + radial-gradient(circle at 100% 0, rgba(var(--accent-rgb), 0.08), transparent 30%), #15191c; box-shadow: 0 26px 90px rgba(0, 0, 0, 0.56); } @@ -698,7 +712,7 @@ p { .generator-form input:focus, .generator-form textarea:focus, .generator-form select:focus { - border-color: rgba(232, 255, 103, 0.42); + border-color: rgba(var(--accent-rgb), 0.42); } .field-row { @@ -782,7 +796,7 @@ p { grid-template-rows: 58px minmax(0, 1fr) 82px; padding: 0 26px 18px; background: - radial-gradient(circle at 50% 18%, rgba(232, 255, 103, 0.045), transparent 34%), + radial-gradient(circle at 50% 18%, rgba(var(--accent-rgb), 0.045), transparent 34%), #090b0c; } @@ -910,7 +924,7 @@ p { } .devices-popover select:focus { - border-color: rgba(232, 255, 103, 0.42); + border-color: rgba(var(--accent-rgb), 0.42); } .devices-popover p { @@ -958,7 +972,7 @@ p { .tool-button.active { color: var(--accent); - border-color: rgba(232, 255, 103, 0.25); + border-color: rgba(var(--accent-rgb), 0.25); } .font-control button { @@ -1016,10 +1030,10 @@ p { display: grid; place-items: center; margin-bottom: 18px; - border: 1px solid rgba(232, 255, 103, 0.16); + border: 1px solid rgba(var(--accent-rgb), 0.16); border-radius: 17px; color: var(--accent); - background: rgba(232, 255, 103, 0.055); + background: rgba(var(--accent-rgb), 0.055); } .device-setup h2 { @@ -1105,7 +1119,7 @@ p { .prompt-word.current, .prompt-word.current-line { color: var(--accent); - text-shadow: 0 2px 18px rgba(232, 255, 103, 0.2); + text-shadow: 0 2px 18px rgba(var(--accent-rgb), 0.2); } .prompt-word.upcoming, @@ -1123,7 +1137,7 @@ p { transform: translateY(-50%); border-radius: 4px; background: var(--accent); - box-shadow: 0 0 16px rgba(232, 255, 103, 0.35); + box-shadow: 0 0 16px rgba(var(--accent-rgb), 0.35); } .prompt-fade { diff --git a/store/Package.appxmanifest.template b/store/Package.appxmanifest.template index 53b1889..c515001 100644 --- a/store/Package.appxmanifest.template +++ b/store/Package.appxmanifest.template @@ -10,7 +10,7 @@ Version="__VERSION__" ProcessorArchitecture="__PROCESSOR_ARCHITECTURE__" /> - PrompterPro + __DISPLAY_NAME__ __PUBLISHER_DISPLAY_NAME__ Assets\StoreLogo.png @@ -25,22 +25,22 @@ + BackgroundColor="__BACKGROUND_COLOR__" /> diff --git a/store/README.md b/store/README.md index 28361a2..54689b9 100644 --- a/store/README.md +++ b/store/README.md @@ -1,8 +1,8 @@ # Microsoft Store release guide -PrompterPro is packaged as native x64 and ARM64 Electron desktop applications and -then combined into an unsigned MSIX bundle. Microsoft Store signs the accepted -bundle during submission. +PrompterPro and SimplePrompt are packaged as native x64 and ARM64 Electron +desktop applications and then combined into separate unsigned MSIX bundles. +Microsoft Store signs each accepted bundle during submission. The ARM64 shell uses a bundled x64 Node sidecar through Windows 11's compatibility layer because the offline sherpa runtime and FFmpeg do not publish @@ -26,9 +26,10 @@ The Store bundle therefore targets Windows 11 for both architecture slices. Repository variables: - `MS_STORE_PRODUCT_ID` -- `MS_STORE_IDENTITY_NAME` -- `MS_STORE_PUBLISHER` -- `MS_STORE_PUBLISHER_DISPLAY_NAME` + +`MS_STORE_PRODUCT_ID` is the PrompterPro Store ID. SimplePrompt's Store ID and +both products' Partner Center identities are versioned in the release matrix and +[`simpleprompt/STORE_DETAILS.md`](simpleprompt/STORE_DETAILS.md). The packager defaults to the production `ReleasedPtyLtd.PrompterPro` identity, its Partner Center publisher certificate subject and `Released Pty Ltd`. @@ -76,6 +77,26 @@ submitted as-is. Sideloading requires a certificate whose subject matches the manifest publisher; use the identity environment variables only when a deliberate development identity is needed. +To build SimplePrompt locally, set the flavour and its Store identity before +running the same packaging command: + +```powershell +$env:PROMPTER_VARIANT = "simpleprompt" +$env:VITE_APP_VARIANT = "simpleprompt" +$env:MS_STORE_IDENTITY_NAME = "ReleasedPtyLtd.SimplePrompt" +$env:MS_STORE_PUBLISHER = "CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897" +$env:MS_STORE_PUBLISHER_DISPLAY_NAME = "Released Pty Ltd" +npm.cmd run package:windows +``` + +This writes `SimplePrompt__x64_arm64.msixbundle`. Use x64 Node.js for +both product builds; the packager uses it to assemble the x64 application and +the compatibility sidecar included in the ARM64 application. + +Each product has its own Windows artwork. PrompterPro uses `store/assets/` and +SimplePrompt uses the green assets in `store/simpleprompt/assets/`; the +packager selects the matching icon, tile, splash and Store logo automatically. + The server build bundles ordinary JavaScript dependencies. Packaging retains only the FFmpeg executable and sherpa-onnx loader/native files outside that bundle, avoiding thousands of development and transitive dependency artifacts @@ -93,11 +114,12 @@ npm.cmd run speech:smoke:arm64 ## Automated releases -The Store release workflow validates and uploads an MSIX bundle artifact for a -`vMAJOR.MINOR.PATCH` tag or a manual run. Publishing is attempted for a tag, or -when **Publish to Microsoft Store** is selected manually. Store identity -variables enable the publish steps; missing authentication secrets then fail -the workflow with an explicit configuration error. +The Store release workflow runs a two-product matrix and uploads separate +PrompterPro and SimplePrompt MSIX bundle artifacts for a `vMAJOR.MINOR.PATCH` +tag or manual run. Publishing is attempted for a tag, or when **Publish to +Microsoft Store** is selected manually. A Store ID enables publishing for each +matrix entry; missing authentication secrets then fail that entry with an +explicit configuration error. Before a production release: diff --git a/store/simpleprompt/PRIVACY.md b/store/simpleprompt/PRIVACY.md new file mode 100644 index 0000000..6b96654 --- /dev/null +++ b/store/simpleprompt/PRIVACY.md @@ -0,0 +1,48 @@ +# SimplePrompt privacy policy + +Effective date: 6 August 2026 + +SimplePrompt is designed to keep your recording workflow on your Windows device. + +## Data the app handles + +SimplePrompt can access your camera and microphone after you grant Windows +permission. It uses them to show a preview, record a take and follow your spoken +position in a script. + +Scripts are stored in local application storage. Camera and microphone media, +speech samples and recognised words are processed locally. A recording may be +converted to MP4 in a temporary local folder, which is removed after export. + +SimplePrompt does not include advertising, analytics or telemetry. It does not +collect account information, contacts, location, payment information, your +script library, recordings or transcripts. + +## Network access + +The Microsoft Store build does not require a SimplePrompt cloud service for its +core script, speech-following and recording features. Windows and Microsoft +Store may make their own network requests under Microsoft's terms. + +If optional AI drafting is configured, the topic, audience, tone, duration and +key points entered in that form are sent to the configured OpenAI service to +create a draft. Camera media, microphone audio, recordings, transcripts and the +local script library are not included in that request. + +## Your choices + +You can deny or revoke camera and microphone permission in Windows Settings. +You can delete scripts inside SimplePrompt and delete exported recordings using +Windows File Explorer. Uninstalling the app removes its local application +storage, subject to Windows behaviour and any backups you control. + +## Children + +SimplePrompt does not knowingly collect personal information from children. + +## Changes and contact + +Material changes will be published in this file with a new effective date. For +privacy questions or support, open an issue at: + +https://github.com/ReleasedGroup/Prompter/issues diff --git a/store/simpleprompt/STORE_DETAILS.md b/store/simpleprompt/STORE_DETAILS.md new file mode 100644 index 0000000..30eba55 --- /dev/null +++ b/store/simpleprompt/STORE_DETAILS.md @@ -0,0 +1,17 @@ +# SimplePrompt Microsoft Store details + +## Partner Center identity + +| Field | Value | +| --- | --- | +| Package/Identity/Name | `ReleasedPtyLtd.SimplePrompt` | +| Package/Identity/Publisher | `CN=E3BDC624-0B0A-4256-85B0-5AE714EA8897` | +| Package/Properties/PublisherDisplayName | `Released Pty Ltd` | +| Package Family Name (PFN) | `ReleasedPtyLtd.SimplePrompt_q0b077qanz1d8` | +| Package SID | `S-1-15-2-2676728462-2596204801-2700890954-455437082-720273692-286854053-1939976512` | +| Store ID | `9MT1X5BNTHQS` | +| Store deep link | Available after the product is live | +| Web Store URL | Available after the product is live | + +The PFN and Package SID are derived Partner Center values and are recorded here +for validation; they are not written directly into the MSIX manifest. diff --git a/store/simpleprompt/assets/AppIcon.ico b/store/simpleprompt/assets/AppIcon.ico new file mode 100644 index 0000000..a3dc9c3 Binary files /dev/null and b/store/simpleprompt/assets/AppIcon.ico differ diff --git a/store/simpleprompt/assets/AppIcon.png b/store/simpleprompt/assets/AppIcon.png new file mode 100644 index 0000000..33dc7c7 Binary files /dev/null and b/store/simpleprompt/assets/AppIcon.png differ diff --git a/store/simpleprompt/assets/SplashScreen.png b/store/simpleprompt/assets/SplashScreen.png new file mode 100644 index 0000000..81019c9 Binary files /dev/null and b/store/simpleprompt/assets/SplashScreen.png differ diff --git a/store/simpleprompt/assets/Square150x150Logo.png b/store/simpleprompt/assets/Square150x150Logo.png new file mode 100644 index 0000000..f550cc5 Binary files /dev/null and b/store/simpleprompt/assets/Square150x150Logo.png differ diff --git a/store/simpleprompt/assets/Square310x310Logo.png b/store/simpleprompt/assets/Square310x310Logo.png new file mode 100644 index 0000000..f93f19a Binary files /dev/null and b/store/simpleprompt/assets/Square310x310Logo.png differ diff --git a/store/simpleprompt/assets/Square44x44Logo.png b/store/simpleprompt/assets/Square44x44Logo.png new file mode 100644 index 0000000..3c606c6 Binary files /dev/null and b/store/simpleprompt/assets/Square44x44Logo.png differ diff --git a/store/simpleprompt/assets/StoreLogo.png b/store/simpleprompt/assets/StoreLogo.png new file mode 100644 index 0000000..d01cb15 Binary files /dev/null and b/store/simpleprompt/assets/StoreLogo.png differ diff --git a/store/simpleprompt/assets/Wide310x150Logo.png b/store/simpleprompt/assets/Wide310x150Logo.png new file mode 100644 index 0000000..7c26133 Binary files /dev/null and b/store/simpleprompt/assets/Wide310x150Logo.png differ diff --git a/store/simpleprompt/listing/en-AU.md b/store/simpleprompt/listing/en-AU.md new file mode 100644 index 0000000..faf2ade --- /dev/null +++ b/store/simpleprompt/listing/en-AU.md @@ -0,0 +1,62 @@ +# Microsoft Store listing — en-AU + +## Product name + +SimplePrompt + +## Short description + +Record naturally with a private teleprompter that follows your voice. + +## Description + +Stay on message without sounding scripted. + +SimplePrompt keeps your words close to the camera while you record. As you +speak, the prompt follows automatically. If you improvise, it waits; when you +return to the script, it picks up from the right place. + +Speech following and video export run on your Windows computer. Camera video, +microphone audio, recordings, transcripts and your script library are not +uploaded. + +Key features: + +- Write, organise, search and duplicate scripts. +- Preview your camera and microphone before recording. +- Follow scripts hands-free with offline voice recognition. +- Pause prompt movement automatically when you go off script. +- Adjust text size, mirror the prompt and move manually when needed. +- Review each take and save it as an MP4. +- Keep scripts and recordings under your control on this device. + +The included speech model currently recognises English. + +## What's new + +Initial Microsoft Store release with offline voice following, local script +management, camera and microphone recording, and MP4 export. + +## Search terms + +teleprompter, autocue, video recording, presenter, script, speech, creator + +## Category + +Productivity + +## Minimum system requirements + +- Windows 11 on an x64 or ARM64 processor +- Camera and microphone for recording +- Internet connection only for installation and optional online features + +## Suggested age rating + +Suitable for all ages. The app has no user-to-user communication, gambling, +violence or mature content. + +## Support and policy links + +- Support: https://github.com/ReleasedGroup/Prompter/issues +- Privacy policy: https://github.com/ReleasedGroup/Prompter/blob/main/store/simpleprompt/PRIVACY.md diff --git a/tests/storeManifest.test.ts b/tests/storeManifest.test.ts index 09d02e9..0352348 100644 --- a/tests/storeManifest.test.ts +++ b/tests/storeManifest.test.ts @@ -6,6 +6,15 @@ import { renderStoreManifest, toWindowsVersion, } from "../scripts/store-manifest.mjs"; +import { PRODUCT_VARIANTS } from "../scripts/product-variants.mjs"; + +const prompterProManifestValues = { + displayName: PRODUCT_VARIANTS.prompterpro.productName, + executableName: PRODUCT_VARIANTS.prompterpro.productName, + applicationId: PRODUCT_VARIANTS.prompterpro.applicationId, + description: PRODUCT_VARIANTS.prompterpro.description, + backgroundColor: PRODUCT_VARIANTS.prompterpro.backgroundColor, +}; describe("Windows Store manifest", () => { it("uses the Partner Center identity and PrompterPro product metadata", () => { @@ -18,6 +27,7 @@ describe("Windows Store manifest", () => { ...DEFAULT_STORE_IDENTITY, minimumWindowsVersion: "10.0.22000.0", version: "1.2.3", + ...prompterProManifestValues, }); expect(manifest).toContain('Name="ReleasedPtyLtd.PrompterPro"'); @@ -33,6 +43,40 @@ describe("Windows Store manifest", () => { expect(manifest).toContain('ShortName="PrompterPro"'); }); + it("renders the SimplePrompt Store identity and product metadata", () => { + const template = readFileSync( + new URL("../store/Package.appxmanifest.template", import.meta.url), + "utf8", + ); + const simplePrompt = PRODUCT_VARIANTS.simpleprompt; + const manifest = renderStoreManifest(template, { + architecture: "arm64", + identityName: simplePrompt.store.identityName, + publisher: simplePrompt.store.publisher, + publisherDisplayName: simplePrompt.store.publisherDisplayName, + minimumWindowsVersion: "10.0.22000.0", + version: "1.2.3", + displayName: simplePrompt.productName, + executableName: simplePrompt.productName, + applicationId: simplePrompt.applicationId, + description: simplePrompt.description, + backgroundColor: simplePrompt.backgroundColor, + }); + + expect(manifest).toContain('Name="ReleasedPtyLtd.SimplePrompt"'); + expect(manifest).toContain('SimplePrompt'); + expect(manifest).toContain('Executable="SimplePrompt.exe"'); + expect(manifest).toContain('Id="SimplePrompt"'); + expect(simplePrompt.store.productId).toBe("9MT1X5BNTHQS"); + expect(simplePrompt.assetDirectory).toBe("simpleprompt/assets"); + expect(simplePrompt.store.packageFamilyName).toBe( + "ReleasedPtyLtd.SimplePrompt_q0b077qanz1d8", + ); + expect(simplePrompt.store.packageSid).toBe( + "S-1-15-2-2676728462-2596204801-2700890954-455437082-720273692-286854053-1939976512", + ); + }); + it("converts semantic versions to four-part MSIX versions", () => { expect(toWindowsVersion("2.7.3")).toBe("2.7.3.0"); expect(toWindowsVersion("2.7.3-beta.1")).toBe("2.7.3.0"); @@ -60,6 +104,7 @@ describe("Windows Store manifest", () => { publisher: "CN=Released Group", publisherDisplayName: "Released Pty Ltd", version: "1.2.3", + ...prompterProManifestValues, }, ), ).toBe( @@ -76,6 +121,7 @@ describe("Windows Store manifest", () => { publisher: "CN=Released Group", publisherDisplayName: "Released Pty Ltd", version: "1.2.3", + ...prompterProManifestValues, }), ).toThrow(/x64 or arm64/); });