Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,62 @@ const withNextIntl = createNextIntlPlugin()

const nextConfig: NextConfig = {
async headers() {
const isDev = process.env.NODE_ENV !== 'production'
const sharedSecurityHeaders = [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
]
const frameDenyHeaders = [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
]

return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
],
// Exclude the workbench demo (always) and, in development, the Slice
// Machine simulator route so they can be embedded in an iframe.
// Otherwise this rule's deny headers get combined onto those routes,
// leaving a stray X-Frame-Options: DENY that blocks framing.
source: isDev
? '/((?!workbench-demo|slice-simulator).*)'
: '/((?!workbench-demo).*)',
headers: [...sharedSecurityHeaders, ...frameDenyHeaders],
},
// Allow embedding the interactive workbench demo on our own pages.
// In dev, also allow the Slice Machine UI (other localhost port) so the
// demo iframe loads inside the nested simulator frames.
{
source: '/workbench-demo/:path*',
headers: isDev
? [
...sharedSecurityHeaders,
{
key: 'Content-Security-Policy',
value:
"frame-ancestors 'self' http://localhost:* http://127.0.0.1:*",
},
]
: [
...sharedSecurityHeaders,
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
],
},
// Dev-only: let the Slice Machine UI iframe the simulator.
...(isDev
? [
{
source: '/slice-simulator',
headers: [
...sharedSecurityHeaders,
{
key: 'Content-Security-Policy',
value: 'frame-ancestors http://localhost:* http://127.0.0.1:*',
},
],
},
]
: []),
]
},
async redirects() {
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
},
"scripts": {
"dev": "concurrently \"npm:dev:next\" \"npm:dev:slicemachine\" --names \"next,slicemachine\" --prefix-colors blue,magenta",
"dev:next": "next dev -p 3004",
"dev:next": "npm run sync:workbench-tour && next dev -p 3004",
"dev:slicemachine": "start-slicemachine",
"build": "next build",
"build": "npm run sync:workbench-tour && next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"sync:workbench-tour": "node scripts/sync-workbench-tour-config.mjs",
"prepare": "husky"
},
"dependencies": {
Expand Down
109 changes: 109 additions & 0 deletions prismicio-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,112 @@ export type HeroSliceExclusive = prismic.SharedSliceVariation<
never
>

/**
* Primary content in *Hero → Exclusive (interactive demo) → Primary*
*/
export interface HeroSliceExclusiveDemoPrimary {
/**
* Eyebrow field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Link
* - **Placeholder**: *None*
* - **API ID Path**: hero.exclusiveDemo.primary.eyebrow
* - **Documentation**: https://prismic.io/docs/fields/link
*/
eyebrow: prismic.LinkField<string, string, unknown, prismic.FieldState, never>

/**
* Title field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Rich Text
* - **Placeholder**: *None*
* - **API ID Path**: hero.exclusiveDemo.primary.title
* - **Documentation**: https://prismic.io/docs/fields/rich-text
*/
title: prismic.RichTextField

/**
* Description field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Rich Text
* - **Placeholder**: *None*
* - **API ID Path**: hero.exclusiveDemo.primary.description
* - **Documentation**: https://prismic.io/docs/fields/rich-text
*/
description: prismic.RichTextField

/**
* CTA field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Link
* - **Placeholder**: *None*
* - **API ID Path**: hero.exclusiveDemo.primary.cta
* - **Documentation**: https://prismic.io/docs/fields/link
*/
cta: prismic.LinkField<
string,
string,
unknown,
prismic.FieldState,
'Primary' | 'Secondary'
>

/**
* Eyebrow Emphasized field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Boolean
* - **Placeholder**: *None*
* - **Default Value**: true
* - **API ID Path**: hero.exclusiveDemo.primary.eyebrow_emphasized
* - **Documentation**: https://prismic.io/docs/fields/boolean
*/
eyebrow_emphasized: prismic.BooleanField

/**
* Full Bleed Image field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Image
* - **Placeholder**: *None*
* - **API ID Path**: hero.exclusiveDemo.primary.full_bleed_image
* - **Documentation**: https://prismic.io/docs/fields/image
*/
full_bleed_image: prismic.ImageField<never>

/**
* Is Short Image field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Boolean
* - **Placeholder**: *None*
* - **Default Value**: false
* - **API ID Path**: hero.exclusiveDemo.primary.is_short_image
* - **Documentation**: https://prismic.io/docs/fields/boolean
*/
is_short_image: prismic.BooleanField

/**
* Image Mobile field in *Hero → Exclusive (interactive demo) → Primary*
*
* - **Field Type**: Image
* - **Placeholder**: *None*
* - **API ID Path**: hero.exclusiveDemo.primary.image_mobile
* - **Documentation**: https://prismic.io/docs/fields/image
*/
image_mobile: prismic.ImageField<never>
}

/**
* Exclusive (interactive demo) variation for Hero Slice
*
* - **API ID**: `exclusiveDemo`
* - **Description**: Homepage hero with embedded workbench walkthrough
* - **Documentation**: https://prismic.io/docs/slices
*/
export type HeroSliceExclusiveDemo = prismic.SharedSliceVariation<
'exclusiveDemo',
Simplify<HeroSliceExclusiveDemoPrimary>,
never
>

/**
* Primary content in *Hero → Video → Primary*
*/
Expand Down Expand Up @@ -2782,6 +2888,7 @@ export type HeroSliceExclusiveAnimated = prismic.SharedSliceVariation<
*/
type HeroSliceVariation =
| HeroSliceExclusive
| HeroSliceExclusiveDemo
| HeroSliceVideo
| HeroSliceDefault
| HeroSliceImage
Expand Down Expand Up @@ -5068,12 +5175,14 @@ declare module '@prismicio/client' {
FeaturesSliceHorizontallyScrollable,
HeroSlice,
HeroSliceExclusivePrimary,
HeroSliceExclusiveDemoPrimary,
HeroSliceVideoPrimary,
HeroSliceDefaultPrimary,
HeroSliceImagePrimary,
HeroSliceExclusiveAnimatedPrimary,
HeroSliceVariation,
HeroSliceExclusive,
HeroSliceExclusiveDemo,
HeroSliceVideo,
HeroSliceDefault,
HeroSliceImage,
Expand Down
159 changes: 159 additions & 0 deletions public/workbench-demo/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* Marketing embed — workbench walkthrough (home → prompt → alerts → job). */

const App = () => {
const [screen, setScreen] = React.useState('home');
const [promptKey, setPromptKey] = React.useState(0);
const [jobKey, setJobKey] = React.useState(0);
const [jobPhase, setJobPhase] = React.useState('running');
const [promptReady, setPromptReady] = React.useState(false);
const screenRef = React.useRef(screen);
screenRef.current = screen;

const tour = React.useMemo(
() => window.getTourState(screen, promptReady, jobPhase),
[screen, promptReady, jobPhase]
);

React.useEffect(() => {
const r = document.documentElement;
r.style.setProperty('--purple-400', '#4A51F2');
r.style.setProperty('--purple-350', '#5D63F4');
r.style.setProperty('--purple-300', '#747AF6');
r.style.setProperty('--purple-200', '#9FA3F9');
r.style.setProperty('--action-primary', '#4A51F2');
r.style.setProperty('--action-primary-hover', '#5D63F4');
r.style.setProperty('--density', '1');
r.removeAttribute('data-theme');
}, []);

const postStep = React.useCallback(() => {
const t = window.getTourState(screen, promptReady, jobPhase);
const step = t?.index ?? 0;
try {
window.parent.postMessage(
{
type: 'workbench-demo-step',
step,
total: t?.total ?? window.TOUR_TOTAL,
title: t?.title ?? null,
barVerb: t?.barVerb ?? null,
barRest: t?.barRest ?? null,
srCaption: t?.caption ?? null,
passive: !!t?.passive,
substep: screen === 'job' ? jobPhase : null,
},
window.location.origin
);
} catch (_) {}
}, [screen, promptReady, jobPhase]);

React.useEffect(() => {
postStep();
}, [postStep]);

const resetPromptScreen = React.useCallback(() => {
setPromptReady(false);
setPromptKey((k) => k + 1);
}, []);

const openPrompt = React.useCallback(
(opts) => {
const reset = opts?.reset !== false;
if (reset) resetPromptScreen();
setScreen('prompt');
},
[resetPromptScreen]
);

const handlePromptReady = React.useCallback(() => {
setPromptReady(true);
}, []);

const openWorkbench = () => setScreen('workbench');

const viewJob = (done = false) => {
const settled = done === true;
setJobPhase(settled ? 'done' : 'running');
setJobKey((k) => k + 1);
setScreen('job');
};

const backToAlerts = () => setScreen('workbench');

const goHome = () => {
setPromptReady(false);
setScreen('home');
};

const goToTourStep = React.useCallback(
(step) => {
switch (step) {
case 0:
goHome();
break;
case 1:
openPrompt({ reset: screenRef.current !== 'prompt' });
break;
case 2:
setScreen('workbench');
break;
case 3:
viewJob(false);
break;
case 4:
viewJob(true);
break;
default:
break;
}
},
[openPrompt]
);

React.useEffect(() => {
const onMessage = (event) => {
if (event.origin !== window.location.origin) return;
if (event.source !== window.parent) return;
if (event.data?.type !== 'workbench-demo-advance') return;
const step = Number(event.data.step);
if (Number.isFinite(step) && step >= 0 && step < window.TOUR_TOTAL) {
goToTourStep(step);
}
};
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, [goToTourStep]);

return (
<window.TourProvider tour={tour}>
<div className="pc-app">
<window.CCRail active={3} onHome={goHome} />
{screen === 'home' && (
<div className="pc-main">
<window.CCHome onComposePrompt={openPrompt} />
</div>
)}
{screen === 'prompt' && (
<div className="pc-main">
<window.CCPrompt
key={promptKey}
onSend={openWorkbench}
onPromptReady={handlePromptReady}
/>
</div>
)}
{screen === 'workbench' && <window.CCWorkbench onViewJob={viewJob} />}
{screen === 'job' && (
<window.CCJob
key={jobKey}
startPhase={jobPhase}
onBack={backToAlerts}
onPhaseChange={setJobPhase}
/>
)}
</div>
</window.TourProvider>
);
};

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
Loading
Loading