From ab2ce3b81906a4c4a974d108389b77472f9283f4 Mon Sep 17 00:00:00 2001 From: dprevoznik <58714078+dprevoznik@users.noreply.github.com> Date: Mon, 4 May 2026 10:12:06 +0000 Subject: [PATCH] feat(demo): add Netflix-skinned showcase package Drops onto a Netflix-style backdrop (red bloom, black gradient, Bebas Neue wordmark) with theming applied via the appearance API. Two modes: - preview (default): scrub through every UI step against the Netflix appearance, no backend needed - live: pass ?session=&code= to render the all-in-one component against a real session Theming lives in src/netflixAppearance.ts as pure data so it can be lifted directly into a customer app. Co-Authored-By: Claude Opus 4.7 --- bun.lock | 19 + packages/netflix-demo/README.md | 49 ++ packages/netflix-demo/index.html | 18 + packages/netflix-demo/package.json | 24 + packages/netflix-demo/src/App.tsx | 474 ++++++++++++++++++ packages/netflix-demo/src/globals.css | 49 ++ packages/netflix-demo/src/main.tsx | 12 + .../netflix-demo/src/netflixAppearance.ts | 183 +++++++ packages/netflix-demo/tsconfig.json | 17 + packages/netflix-demo/vite.config.ts | 10 + 10 files changed, 855 insertions(+) create mode 100644 packages/netflix-demo/README.md create mode 100644 packages/netflix-demo/index.html create mode 100644 packages/netflix-demo/package.json create mode 100644 packages/netflix-demo/src/App.tsx create mode 100644 packages/netflix-demo/src/globals.css create mode 100644 packages/netflix-demo/src/main.tsx create mode 100644 packages/netflix-demo/src/netflixAppearance.ts create mode 100644 packages/netflix-demo/tsconfig.json create mode 100644 packages/netflix-demo/vite.config.ts diff --git a/bun.lock b/bun.lock index 6778b3b..f0378ec 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "managed-auth-react-workspace", @@ -44,6 +45,22 @@ "react-dom": ">=18", }, }, + "packages/netflix-demo": { + "name": "@onkernel/managed-auth-react-netflix-demo", + "version": "0.0.0", + "dependencies": { + "@onkernel/managed-auth-react": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1", + }, + "devDependencies": { + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.6.0", + "vite": "^5.4.0", + }, + }, }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], @@ -150,6 +167,8 @@ "@onkernel/managed-auth-react-demo": ["@onkernel/managed-auth-react-demo@workspace:packages/demo"], + "@onkernel/managed-auth-react-netflix-demo": ["@onkernel/managed-auth-react-netflix-demo@workspace:packages/netflix-demo"], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], diff --git a/packages/netflix-demo/README.md b/packages/netflix-demo/README.md new file mode 100644 index 0000000..6e0fc90 --- /dev/null +++ b/packages/netflix-demo/README.md @@ -0,0 +1,49 @@ +# `@onkernel/managed-auth-react-netflix-demo` + +Netflix-skinned demo of [`@onkernel/managed-auth-react`](../managed-auth-react). Drops `` onto a Netflix-style backdrop (red bloom, black gradient, Bebas-Neue wordmark) so you can see the customization API in action against a recognizable brand. + +## Run it + +From the repo root: + +```bash +bun install +bun run --filter @onkernel/managed-auth-react-netflix-demo dev +``` + +Opens at `http://localhost:5174`. + +## Two modes + +### Preview mode (default) + +The page loads in preview mode — no backend required. A state picker below the auth card lets you scrub through every UI step (`prime`, `discovering`, `awaiting_input`, `success`, `error`, etc.) so you can audit the Netflix theming against every state. + +### Live mode + +Pass a real session ID and handoff code via URL params and the page renders the all-in-one `` component instead: + +``` +http://localhost:5174/?session=&code= +``` + +Generate those on your backend with the Kernel SDK: + +```ts +import Kernel from "@onkernel/sdk"; + +const kernel = new Kernel({ apiKey: process.env.KERNEL_API_KEY }); + +const connection = await kernel.auth.connections.create({ + domain: "netflix.com", + profile_name: "demo-user", +}); +const { id, handoff_code } = await kernel.auth.connections.login(connection.id); + +// Open the demo with these in the URL: +console.log(`http://localhost:5174/?session=${id}&code=${handoff_code}`); +``` + +## Where the theming lives + +`src/netflixAppearance.ts` — exports `netflixAppearance` and `netflixLocalization`. Pure data; no component logic. Lift it into your own app to reproduce the look. diff --git a/packages/netflix-demo/index.html b/packages/netflix-demo/index.html new file mode 100644 index 0000000..d0b2934 --- /dev/null +++ b/packages/netflix-demo/index.html @@ -0,0 +1,18 @@ + + + + + + Kernel Managed Auth — Netflix demo + + + + + +
+ + + diff --git a/packages/netflix-demo/package.json b/packages/netflix-demo/package.json new file mode 100644 index 0000000..4ce5181 --- /dev/null +++ b/packages/netflix-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "@onkernel/managed-auth-react-netflix-demo", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@onkernel/managed-auth-react": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.6.0", + "vite": "^5.4.0" + } +} diff --git a/packages/netflix-demo/src/App.tsx b/packages/netflix-demo/src/App.tsx new file mode 100644 index 0000000..e4779c3 --- /dev/null +++ b/packages/netflix-demo/src/App.tsx @@ -0,0 +1,474 @@ +import { useMemo, useState } from "react"; +import { + AppearanceProvider, + ExternalActionWaiting, + KernelManagedAuth, + LoadingState, + LocalizationProvider, + Shell, + StepError, + StepExpired, + StepPrime, + StepSuccess, + UnifiedAuthForm, + type DiscoveredField, + type MFAOption, + type SignInOption, + type SSOButton, +} from "@onkernel/managed-auth-react"; +import "@onkernel/managed-auth-react/styles.css"; +import { netflixAppearance, netflixLocalization } from "./netflixAppearance"; + +const TARGET_DOMAIN = "netflix.com"; + +type Step = + | "prime" + | "discovering" + | "awaiting_input" + | "awaiting_input_sso" + | "awaiting_input_mfa" + | "awaiting_external_action" + | "submitting" + | "success" + | "expired" + | "error"; + +const allSteps: { id: Step; label: string }[] = [ + { id: "prime", label: "Prime / consent" }, + { id: "discovering", label: "Discovering" }, + { id: "awaiting_input", label: "Awaiting input" }, + { id: "awaiting_input_sso", label: "+ SSO" }, + { id: "awaiting_input_mfa", label: "MFA select" }, + { id: "awaiting_external_action", label: "External action" }, + { id: "submitting", label: "Submitting" }, + { id: "success", label: "Success" }, + { id: "expired", label: "Expired" }, + { id: "error", label: "Error" }, +]; + +const mockFields: DiscoveredField[] = [ + { + name: "email", + label: "Email or phone number", + type: "email", + required: true, + placeholder: "Email or phone number", + }, + { + name: "password", + label: "Password", + type: "password", + required: true, + placeholder: "Password", + }, +]; + +const mockSSO: SSOButton[] = [ + { provider: "google", label: "Continue with Google", selector: "" }, + { provider: "facebook", label: "Continue with Facebook", selector: "" }, +]; + +const mockMFA: MFAOption[] = [ + { type: "sms", target: "•••• 8421" }, + { type: "email", target: "n••••@netflix.com" }, + { type: "totp", description: "Use your authenticator app" }, +]; + +const mockSignInOptions: SignInOption[] = [ + { id: "personal", label: "Personal", description: "you@example.com" }, + { id: "kids", label: "Kids", description: "Profile for younger viewers" }, +]; + +function renderStep(step: Step) { + switch (step) { + case "prime": + return {}} />; + case "discovering": + return ( + + ); + case "awaiting_input": + return ( + {}} + onSSOClick={() => {}} + onMFASelect={() => {}} + onSignInOptionSelect={() => {}} + /> + ); + case "awaiting_input_sso": + return ( + {}} + onSSOClick={() => {}} + onMFASelect={() => {}} + onSignInOptionSelect={() => {}} + /> + ); + case "awaiting_input_mfa": + return ( + {}} + onSSOClick={() => {}} + onMFASelect={() => {}} + onSignInOptionSelect={() => {}} + /> + ); + case "awaiting_external_action": + return ( + + ); + case "submitting": + return ( + + ); + case "success": + return ; + case "expired": + return ; + case "error": + return ( + + ); + } +} + +interface LiveCredentials { + sessionId: string; + handoffCode: string; +} + +function readLiveCredentialsFromUrl(): LiveCredentials | null { + if (typeof window === "undefined") return null; + const params = new URLSearchParams(window.location.search); + const sessionId = params.get("session") ?? params.get("sessionId"); + const handoffCode = params.get("code") ?? params.get("handoffCode"); + if (!sessionId || !handoffCode) return null; + return { sessionId, handoffCode }; +} + +export function App() { + const live = useMemo(readLiveCredentialsFromUrl, []); + const [step, setStep] = useState("prime"); + + return ( +
+ +
+ + + + {!live && } + +
+ ); +} + +function AuthCard({ + live, + step, +}: { + live: LiveCredentials | null; + step: Step; +}) { + if (live) { + return ( + { + // eslint-disable-next-line no-console + console.log("[netflix-demo] success", { profileName, domain }); + }} + onError={({ code, message }) => { + // eslint-disable-next-line no-console + console.error("[netflix-demo] error", code, message); + }} + /> + ); + } + + return ( + + + {renderStep(step)} + + + ); +} + +function BackdropPoster() { + return ( + <> + {/* The Netflix-y backdrop: dark base + crimson radial bloom + bottom + black gradient that fades into the form area. Three layered + fixed-position divs so the auth card always reads as if it's on a + movie poster. */} +
+
+
+ + ); +} + +function Header() { + return ( +
+
NETFLIX
+ +
+ ); +} + +function Hero({ children }: { children: React.ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +function StatePicker({ + step, + onChange, +}: { + step: Step; + onChange: (s: Step) => void; +}) { + return ( + + ); +} + +function DemoFooter({ live }: { live: LiveCredentials | null }) { + return ( +
+
+
+ Demo of{" "} + + @onkernel/managed-auth-react + + . Netflix branding is illustrative only — Netflix is not affiliated. +
+
+ {live ? ( + <> + Running live flow against session{" "} + + {live.sessionId.slice(0, 8)}… + + . + + ) : ( + <> + Currently in preview mode. Use the state picker + below to scrub through every UI state. + + )} +
+
+
+ ); +} + +const pageStyles: Record = { + page: { + position: "relative", + minHeight: "100vh", + color: "#fff", + overflowX: "hidden", + paddingBottom: 80, + }, + backdropBase: { + position: "fixed", + inset: 0, + background: + "linear-gradient(180deg, #000 0%, #0a0a0a 40%, #000 100%)", + zIndex: -3, + }, + backdropBloom: { + position: "fixed", + inset: 0, + background: + "radial-gradient(60% 50% at 50% 30%, rgba(229, 9, 20, 0.35) 0%, rgba(229, 9, 20, 0.08) 35%, transparent 70%)", + zIndex: -2, + }, + backdropGradient: { + position: "fixed", + inset: 0, + background: + "linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.0) 25%, rgba(0,0,0,0.0) 60%, rgba(0,0,0,0.85) 100%)", + zIndex: -1, + pointerEvents: "none", + }, + header: { + position: "relative", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "24px 56px", + zIndex: 2, + }, + brand: { + fontFamily: "'Bebas Neue', 'Inter', sans-serif", + fontSize: 36, + fontWeight: 900, + letterSpacing: "0.04em", + color: "#e50914", + textShadow: "0 1px 2px rgba(0,0,0,0.6)", + }, + langButton: { + background: "rgba(0, 0, 0, 0.5)", + color: "#fff", + border: "1px solid rgba(255, 255, 255, 0.4)", + padding: "6px 14px", + fontSize: 13, + fontWeight: 500, + borderRadius: 2, + cursor: "pointer", + }, + heroWrap: { + position: "relative", + zIndex: 1, + display: "flex", + justifyContent: "center", + padding: "40px 16px 24px", + }, + cardSlot: { + width: "100%", + maxWidth: 480, + }, + footer: { + position: "relative", + zIndex: 1, + marginTop: 40, + padding: "0 24px", + }, + footerInner: { + maxWidth: 720, + margin: "0 auto", + fontSize: 12, + lineHeight: 1.6, + color: "#808080", + textAlign: "center", + display: "flex", + flexDirection: "column", + gap: 6, + }, + footerLine: {}, + footerLink: { color: "#b3b3b3", textDecoration: "underline" }, + footerCode: { + fontFamily: "ui-monospace, monospace", + fontSize: 11, + color: "#b3b3b3", + }, +}; + +const pickerStyles: Record = { + panel: { + position: "relative", + zIndex: 1, + maxWidth: 720, + margin: "32px auto 0", + padding: "16px 20px", + background: "rgba(0, 0, 0, 0.55)", + border: "1px solid rgba(255, 255, 255, 0.08)", + borderRadius: 6, + }, + header: { + display: "flex", + alignItems: "baseline", + justifyContent: "space-between", + gap: 12, + marginBottom: 12, + flexWrap: "wrap", + }, + headerLabel: { + fontSize: 13, + fontWeight: 600, + textTransform: "uppercase", + letterSpacing: "0.08em", + color: "#fff", + }, + headerHint: { fontSize: 12, color: "#808080" }, + code: { + fontFamily: "ui-monospace, monospace", + background: "rgba(255, 255, 255, 0.06)", + padding: "1px 5px", + borderRadius: 3, + color: "#b3b3b3", + }, + chips: { + display: "flex", + flexWrap: "wrap", + gap: 6, + }, + chip: { + background: "rgba(255, 255, 255, 0.06)", + color: "#b3b3b3", + border: "1px solid rgba(255, 255, 255, 0.1)", + padding: "6px 12px", + fontSize: 12, + fontWeight: 500, + borderRadius: 4, + cursor: "pointer", + }, + chipActive: { + background: "#e50914", + color: "#fff", + border: "1px solid #e50914", + padding: "6px 12px", + fontSize: 12, + fontWeight: 600, + borderRadius: 4, + cursor: "pointer", + }, +}; diff --git a/packages/netflix-demo/src/globals.css b/packages/netflix-demo/src/globals.css new file mode 100644 index 0000000..b870dbd --- /dev/null +++ b/packages/netflix-demo/src/globals.css @@ -0,0 +1,49 @@ +/* + * Demo chrome only — these tokens are scoped to the Netflix-style page + * surrounding the auth component. The auth component itself is themed via + * the `--kma-*` tokens emitted by `appearance.variables` (see netflixAppearance + * in App.tsx). + */ +:root { + --nflx-red: #e50914; + --nflx-red-hover: #f6121d; + --nflx-red-deep: #b1060f; + --nflx-black: #000000; + --nflx-near-black: #141414; + --nflx-charcoal: #181818; + --nflx-gray: #2f2f2f; + --nflx-text: #ffffff; + --nflx-text-muted: #b3b3b3; + --nflx-text-dim: #808080; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + padding: 0; + min-height: 100vh; + background: var(--nflx-black); + color: var(--nflx-text); + font-family: + "Inter", + "Netflix Sans", + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + sans-serif; + -webkit-font-smoothing: antialiased; +} + +a { + color: inherit; +} + +button { + font-family: inherit; +} diff --git a/packages/netflix-demo/src/main.tsx b/packages/netflix-demo/src/main.tsx new file mode 100644 index 0000000..78014d4 --- /dev/null +++ b/packages/netflix-demo/src/main.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./globals.css"; + +const container = document.getElementById("root"); +if (!container) throw new Error("#root not found"); +createRoot(container).render( + + + , +); diff --git a/packages/netflix-demo/src/netflixAppearance.ts b/packages/netflix-demo/src/netflixAppearance.ts new file mode 100644 index 0000000..27b76d6 --- /dev/null +++ b/packages/netflix-demo/src/netflixAppearance.ts @@ -0,0 +1,183 @@ +import type { Appearance, Localization } from "@onkernel/managed-auth-react"; + +/** + * Netflix-style appearance for ``. + * + * Pulls from Netflix's public sign-in page: black surface, charcoal card, + * white type, signature red primary CTA (#e50914), tight 4px radius, and + * the floating-label input pattern via a thicker dark gray fill. + * + * `kernelLogoColor: "white"` keeps the Powered-by row legible on the + * near-black card. + */ +export const netflixAppearance: Appearance = { + theme: "dark", + variables: { + colorBackground: "transparent", + colorCard: "rgba(0, 0, 0, 0.75)", + colorCardForeground: "#ffffff", + colorForeground: "#ffffff", + colorMuted: "#2f2f2f", + colorMutedForeground: "#b3b3b3", + colorBorder: "rgba(255, 255, 255, 0.08)", + colorInput: "#333333", + colorInputForeground: "#ffffff", + colorPrimary: "#e50914", + colorPrimaryForeground: "#ffffff", + colorRing: "#e50914", + colorSuccess: "#46d369", + colorSuccessForeground: "#000000", + colorDanger: "#e87c03", + colorDangerForeground: "#000000", + fontFamily: + "'Inter', 'Netflix Sans', ui-sans-serif, system-ui, sans-serif", + fontWeightNormal: 400, + fontWeightMedium: 500, + fontWeightSemibold: 700, + borderRadius: "4px", + borderRadiusSm: "2px", + borderRadiusLg: "6px", + }, + elements: { + card: { + style: { + background: "rgba(0, 0, 0, 0.75)", + border: "none", + boxShadow: "none", + }, + }, + title: { + style: { + fontSize: 32, + fontWeight: 700, + letterSpacing: "-0.01em", + marginBottom: 28, + }, + }, + subtitle: { + style: { color: "#b3b3b3" }, + }, + description: { + style: { color: "#b3b3b3" }, + }, + label: { + style: { color: "#b3b3b3", fontWeight: 500 }, + }, + input: { + style: { + background: "#333333", + border: "none", + color: "#ffffff", + height: 50, + fontSize: 16, + padding: "16px 20px 0", + borderRadius: 4, + "::placeholder": { color: "#8c8c8c", opacity: 1 }, + ":focus-visible": { + outline: "none", + background: "#454545", + boxShadow: "inset 0 -2px 0 #e50914", + }, + ":hover": { background: "#454545" }, + }, + }, + buttonPrimary: { + style: { + background: "#e50914", + color: "#ffffff", + fontSize: 16, + fontWeight: 700, + height: 48, + textTransform: "none", + letterSpacing: "0.01em", + borderRadius: 4, + ":hover": { + background: "#f6121d", + opacity: 1, + }, + ":active": { background: "#b1060f" }, + ":disabled": { background: "rgba(229, 9, 20, 0.5)", opacity: 1 }, + }, + }, + buttonSecondary: { + style: { + background: "rgba(255, 255, 255, 0.08)", + color: "#ffffff", + border: "none", + ":hover": { + background: "rgba(255, 255, 255, 0.16)", + opacity: 1, + }, + }, + }, + ssoButton: { + style: { + background: "rgba(255, 255, 255, 0.08)", + color: "#ffffff", + border: "none", + height: 48, + fontWeight: 500, + ":hover": { + background: "rgba(255, 255, 255, 0.16)", + opacity: 1, + }, + }, + }, + mfaOption: { + style: { + background: "rgba(255, 255, 255, 0.04)", + border: "1px solid rgba(255, 255, 255, 0.1)", + ":hover": { + background: "rgba(255, 255, 255, 0.08)", + opacity: 1, + }, + }, + }, + signInOption: { + style: { + background: "rgba(255, 255, 255, 0.04)", + border: "1px solid rgba(255, 255, 255, 0.1)", + ":hover": { + background: "rgba(255, 255, 255, 0.08)", + opacity: 1, + }, + }, + }, + divider: { style: { color: "#808080" } }, + dividerLine: { style: { borderColor: "rgba(255, 255, 255, 0.1)" } }, + dividerText: { style: { color: "#808080", fontSize: 13 } }, + securityCard: { + style: { + background: "rgba(255, 255, 255, 0.04)", + border: "1px solid rgba(255, 255, 255, 0.08)", + }, + }, + securityText: { style: { color: "#b3b3b3", fontSize: 13 } }, + legalText: { style: { color: "#808080", fontSize: 13 } }, + legalLink: { style: { color: "#0071eb" } }, + errorBanner: { + style: { + background: "#e87c03", + color: "#000000", + borderRadius: 4, + padding: "10px 20px", + }, + }, + successIcon: { style: { color: "#46d369" } }, + poweredBy: { style: { color: "#808080" } }, + }, + layout: { + poweredByKernel: true, + kernelLogoColor: "white", + showSecurityCard: true, + }, +}; + +export const netflixLocalization: Localization = { + loginTitle: () => "Sign In", + primeContinueButton: "Sign In", + submitButton: "Sign In", + submittingButton: "Signing in…", + ssoButtonLabel: (provider) => `Continue with ${provider}`, + orDivider: "OR", +}; diff --git a/packages/netflix-demo/tsconfig.json b/packages/netflix-demo/tsconfig.json new file mode 100644 index 0000000..42e0521 --- /dev/null +++ b/packages/netflix-demo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"] +} diff --git a/packages/netflix-demo/vite.config.ts b/packages/netflix-demo/vite.config.ts new file mode 100644 index 0000000..2f32457 --- /dev/null +++ b/packages/netflix-demo/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5174, + open: true, + }, +});