feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
+1
View File
@@ -13,6 +13,7 @@
"storybook:publish": "pnpm build-storybook && pnpm storybook:upload"
},
"dependencies": {
"@cio/certificates": "workspace:*",
"@cio/ui": "workspace:*",
"@cio/utils": "workspace:*",
"svelte-inview": "^4.0.4",
@@ -0,0 +1,54 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { NumberBadge } from '@cio/ui/base/number-badge';
const { Story } = defineMeta({
title: 'Atom/NumberBadge',
component: NumberBadge,
parameters: {
layout: 'centered'
},
tags: ['autodocs']
});
</script>
<Story name="Inactive">
{#snippet template()}
<NumberBadge number={3} />
{/snippet}
</Story>
<Story name="Active">
{#snippet template()}
<NumberBadge number={3} active />
{/snippet}
</Story>
<Story name="Locked">
{#snippet template()}
<NumberBadge number={3} locked />
{/snippet}
</Story>
<Story name="With group hover">
{#snippet template()}
<button
type="button"
class="group hover:border-border flex cursor-pointer items-center gap-2 rounded-md border border-transparent px-2 py-1.5"
>
<NumberBadge number={2} />
<span class="text-sm">Hover the row to see badge emphasis</span>
</button>
{/snippet}
</Story>
<Story name="Sequence">
{#snippet template()}
<div class="flex items-center gap-2">
<NumberBadge number={1} />
<NumberBadge number={2} active />
<NumberBadge number={3} locked />
<NumberBadge number={4} />
</div>
{/snippet}
</Story>
@@ -16,7 +16,8 @@
ShinnyText,
Sparkle,
SquareCardBody,
SquareCardIcon
SquareCardIcon,
Waves
} from '@cio/ui/custom/animation';
const { Story } = defineMeta({
@@ -72,6 +73,19 @@
{/snippet}
</Story>
<Story name="Waves">
{#snippet template()}
<div
class="ui:relative ui:mx-auto ui:h-[500px] ui:w-full ui:max-w-3xl ui:overflow-hidden ui:rounded-xl ui:border ui:bg-background"
>
<Waves class="ui:absolute ui:inset-0 ui:h-full ui:w-full" />
<div class="ui:relative ui:z-10 ui:flex ui:h-full ui:items-center ui:justify-center ui:text-sm">
Move the cursor — animated waves use the theme primary stroke
</div>
</div>
{/snippet}
</Story>
<Story name="CardContainer">
{#snippet template()}
<CardContainer className="ui:max-w-lg">
@@ -1,4 +1,4 @@
<script module>
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { ModelPicker } from '@cio/ui/custom/model-picker';
@@ -12,7 +12,7 @@
});
</script>
<script>
<script lang="ts">
import { DEFAULT_PICKER_MODEL_ID, type AgentModelId } from '@cio/utils/agent-models';
let selected = $state<AgentModelId>(DEFAULT_PICKER_MODEL_ID);
@@ -44,7 +44,7 @@
onPrev={() => console.log('prev')}
onNext={() => console.log('next')}
>
<PublicCourse.PublicLessonView lesson={LESSON_FIXTURE} callout={CALLOUT_FIXTURE} />
<PublicCourse.PublicLessonView lesson={LESSON_FIXTURE} videoCaptionsLabel="Captions" callout={CALLOUT_FIXTURE} />
</PublicCourse.PublicCourseShell>
{/snippet}
</Story>
@@ -68,7 +68,11 @@
hasPrev
hasNext={false}
>
<PublicCourse.PublicLessonView lesson={LESSON_LOCKED_FIXTURE} callout={CALLOUT_FIXTURE} />
<PublicCourse.PublicLessonView
lesson={LESSON_LOCKED_FIXTURE}
videoCaptionsLabel="Captions"
callout={CALLOUT_FIXTURE}
/>
</PublicCourse.PublicCourseShell>
{/snippet}
</Story>
@@ -105,12 +109,26 @@
{#snippet template()}
<div class="ui:mx-auto ui:max-w-2xl ui:space-y-8 ui:p-8">
<div>
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline (at bottom of lessons)</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" />
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline · waves (default)</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" animation="waves" />
</div>
<div>
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Full (locked item replacement)</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="full" />
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline · dotted</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" animation="dotted" />
</div>
<div>
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">
Full (locked item replacement) · waves
</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="full" animation="waves" />
</div>
<div>
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Full · dotted</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="full" animation="dotted" />
</div>
<div>
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline · none (no motion)</h3>
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" animation="none" />
</div>
<div>
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Full (no callout configured)</h3>
@@ -0,0 +1,141 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { Certificate } from '@cio/ui';
import {
CERTIFICATE_TEMPLATE_LIST,
LONG_RENDER_DATA,
MINIMAL_RENDER_DATA,
SAMPLE_RENDER_DATA,
STORY_ACCENT_VARIATIONS,
buildDesign
} from './fixtures';
const { Story } = defineMeta({
title: 'Templates/Certificate',
component: Certificate.Preview,
parameters: {
layout: 'fullscreen'
},
tags: ['autodocs']
});
</script>
<Story name="Gallery · all templates">
{#snippet template()}
<div class="grid grid-cols-1 gap-6 bg-zinc-100 p-6 md:grid-cols-2 xl:grid-cols-3">
{#each CERTIFICATE_TEMPLATE_LIST as template (template.id)}
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
<div class="aspect-[1.4/1] w-full">
<Certificate.Preview design={buildDesign(template.id)} data={SAMPLE_RENDER_DATA} />
</div>
<figcaption class="border-t border-zinc-200 px-4 py-3">
<h3 class="text-sm font-medium text-zinc-900">{template.label}</h3>
<p class="mt-0.5 text-xs text-zinc-500">{template.description}</p>
</figcaption>
</figure>
{/each}
</div>
{/snippet}
</Story>
<Story name="Classique · full-size">
{#snippet template()}
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
<div class="aspect-[1.4/1] w-full max-w-5xl">
<Certificate.Preview design={buildDesign('classique')} data={SAMPLE_RENDER_DATA} showControls />
</div>
</div>
{/snippet}
</Story>
<Story name="Brutalist · full-size">
{#snippet template()}
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
<div class="aspect-[1.4/1] w-full max-w-5xl">
<Certificate.Preview
design={buildDesign('brutalist', { accentColor: '#ff4500' })}
data={SAMPLE_RENDER_DATA}
showControls
/>
</div>
</div>
{/snippet}
</Story>
<Story name="Noir · full-size">
{#snippet template()}
<div class="flex h-screen items-center justify-center bg-zinc-950 p-8">
<div class="aspect-[1.4/1] w-full max-w-5xl">
<Certificate.Preview
design={buildDesign('noir', { accentColor: '#d4af37' })}
data={SAMPLE_RENDER_DATA}
showControls
/>
</div>
</div>
{/snippet}
</Story>
<Story name="Poster · full-size">
{#snippet template()}
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
<div class="aspect-[1.4/1] w-full max-w-5xl">
<Certificate.Preview
design={buildDesign('poster', { accentColor: '#ff5722' })}
data={SAMPLE_RENDER_DATA}
showControls
/>
</div>
</div>
{/snippet}
</Story>
<Story name="Minimal · full-size">
{#snippet template()}
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
<div class="aspect-[1.4/1] w-full max-w-5xl">
<Certificate.Preview design={buildDesign('minimal')} data={SAMPLE_RENDER_DATA} showControls />
</div>
</div>
{/snippet}
</Story>
<Story name="Classique · accent variations">
{#snippet template()}
<div class="grid grid-cols-1 gap-5 bg-zinc-100 p-6 md:grid-cols-2 xl:grid-cols-3">
{#each STORY_ACCENT_VARIATIONS as accent (accent)}
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
<div class="aspect-[1.4/1] w-full">
<Certificate.Preview design={buildDesign('classique', { accentColor: accent })} data={SAMPLE_RENDER_DATA} />
</div>
<figcaption class="border-t border-zinc-200 px-4 py-2 text-xs text-zinc-600">
<code>{accent}</code>
</figcaption>
</figure>
{/each}
</div>
{/snippet}
</Story>
<Story name="Edge cases · long names + short data">
{#snippet template()}
<div class="grid grid-cols-1 gap-5 bg-zinc-100 p-6 xl:grid-cols-2">
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
<div class="aspect-[1.4/1] w-full">
<Certificate.Preview design={buildDesign('poster')} data={LONG_RENDER_DATA} />
</div>
<figcaption class="border-t border-zinc-200 px-4 py-3 text-xs text-zinc-600">
Long names · poster template
</figcaption>
</figure>
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
<div class="aspect-[1.4/1] w-full">
<Certificate.Preview design={buildDesign('minimal')} data={MINIMAL_RENDER_DATA} />
</div>
<figcaption class="border-t border-zinc-200 px-4 py-3 text-xs text-zinc-600">
Minimal copy · minimal template
</figcaption>
</figure>
</div>
{/snippet}
</Story>
@@ -0,0 +1,58 @@
import {
ACCENT_COLORS,
CERTIFICATE_TEMPLATES,
DEFAULT_CERTIFICATE_DESIGN,
type CertificateDesign,
type CertificateRenderData,
type CertificateTemplateId
} from '@cio/certificates';
export const CERTIFICATE_TEMPLATE_LIST = CERTIFICATE_TEMPLATES;
export const SAMPLE_RENDER_DATA: CertificateRenderData = {
recipientName: 'Eleanor Vance',
courseName: 'Certificate of Excellence',
courseDescription:
'In recognition of outstanding achievement and exemplary dedication to the craft of design and storytelling.',
orgName: 'The Royal Academy of Arts',
date: 'October 12, 2026',
certificateId: 'N° 0247'
};
export const LONG_RENDER_DATA: CertificateRenderData = {
recipientName: 'Maximilian Alexandros Constantinopoulos',
courseName: 'Advanced Editorial Layout & Typography for the Digital Era',
courseDescription:
'Awarded after sustained mastery of advanced editorial layout, typography, color theory, motion craft, and the careful coordination of a multi-stage production.',
orgName: 'The International Atelier of Design & Storytelling',
date: 'February 03, 2026',
certificateId: 'N° 9991'
};
export const MINIMAL_RENDER_DATA: CertificateRenderData = {
recipientName: 'Jo',
courseName: 'Intro',
courseDescription: 'Successfully completed the introductory programme.',
orgName: 'CIO',
date: 'May 11, 2026',
certificateId: 'N° 0001'
};
export function buildDesign(
templateId: CertificateTemplateId,
overrides: Partial<CertificateDesign> = {}
): CertificateDesign {
return {
templateId,
accentColor: overrides.accentColor ?? DEFAULT_CERTIFICATE_DESIGN.accentColor,
subtitle: overrides.subtitle ?? DEFAULT_CERTIFICATE_DESIGN.subtitle,
descriptionOverride: overrides.descriptionOverride,
signatories: overrides.signatories ?? [
{ name: 'Margaret W. Holloway', role: 'Director of Studies' },
{ name: 'James K. Reeves', role: 'Dean of Faculty' }
],
idFormat: overrides.idFormat ?? DEFAULT_CERTIFICATE_DESIGN.idFormat
};
}
export const STORY_ACCENT_VARIATIONS = ACCENT_COLORS;
@@ -118,16 +118,71 @@ export const mockProps: OrgLandingPageProps = {
href: '#contact'
}
},
links: {
heading: 'Resources',
description: 'Everything you need beyond the catalog.',
boldVisitLabel: 'Visit',
classicLearnMoreLabel: 'Learn more',
cards: [
{
icon: 'help-circle',
title: 'Help Center',
description: 'Guides, FAQs, and how-to articles for admins and learners.',
href: 'https://example.com/help'
},
{
icon: 'video',
title: 'Webinars',
description: 'Live sessions and recordings on compliance and product updates.',
href: 'https://example.com/webinars'
},
{
icon: 'users',
title: 'Community',
description: 'Connect with peers, share templates, and ask questions.',
href: 'https://example.com/community'
}
]
},
callout: {
heading: 'Ready to simplify compliance?',
description:
'Launch your first certification program in minutes. Automate tracking, reduce risk, and keep your team certified.',
action: { label: 'Start Free', href: '#courses' }
},
footerLinks: [
{ label: 'Terms of Service', href: '#terms' },
{ label: 'Privacy Policy', href: '#privacy' },
{ label: 'Support', href: '#support' }
],
footerText: '© 2026 Certifi Academy. All rights reserved.'
footer: {
brand: {
socials: [
{ platform: 'linkedin', href: 'https://linkedin.com/company/example' },
{ platform: 'x', href: 'https://x.com/example' }
]
},
columns: [
{
id: 'col-company',
heading: 'Company',
links: [
{ id: 'col-company-about', label: 'About', href: '#about' },
{ id: 'col-company-careers', label: 'Careers', href: '#careers' }
],
cta: { label: 'Explore more', href: '#more' }
},
{
id: 'col-resources',
heading: 'Resources',
links: [
{ id: 'col-resources-help', label: 'Help Center', href: '#help' },
{ id: 'col-resources-blog', label: 'Blog', href: '#blog' }
]
}
],
bottom: {
text: '© 2026 Certifi Academy. All rights reserved.',
links: [
{ id: 'bottom-terms', label: 'Terms of Service', href: '#terms' },
{ id: 'bottom-privacy', label: 'Privacy Policy', href: '#privacy' },
{ id: 'bottom-support', label: 'Support', href: '#support' }
]
}
}
};
@@ -1,6 +1,15 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { MinimalLandingPage, BoldLandingPage, ClassicLandingPage } from '@cio/ui/custom/org-landing-page';
import {
MinimalLandingPage,
BoldLandingPage,
ClassicLandingPage,
SaasLandingPage,
TechLandingPage,
StudioLandingPage,
CorporateLandingPage,
TerminalLandingPage
} from '@cio/ui/custom/org-landing-page';
import { mockProps } from './fixtures';
const { Story } = defineMeta({
@@ -23,3 +32,23 @@
<Story name="Classic">
<ClassicLandingPage {...mockProps} />
</Story>
<Story name="SaaS">
<SaasLandingPage {...mockProps} />
</Story>
<Story name="Tech">
<TechLandingPage {...mockProps} />
</Story>
<Story name="Studio">
<StudioLandingPage {...mockProps} />
</Story>
<Story name="Corporate">
<CorporateLandingPage {...mockProps} />
</Story>
<Story name="Terminal">
<TerminalLandingPage {...mockProps} />
</Story>