diff --git a/apps/storybook/.storybook/preview.js b/apps/storybook/.storybook/preview.js index e09d33157..d3974643b 100644 --- a/apps/storybook/.storybook/preview.js +++ b/apps/storybook/.storybook/preview.js @@ -14,6 +14,7 @@ function applyThemeClass(name) { docElement.classList.remove('azion', 'azion-light', 'azion-dark') docElement.classList.add(...(THEME_CLASSES[name] || THEME_CLASSES.dark)) + docElement.dataset.theme = THEME_CLASSES[name] ? name : 'dark' } applyThemeClass('dark') @@ -93,6 +94,7 @@ export const parameters = { 'Documentation', ['Overview', 'Authoring', 'Example Page'], 'Templates', + ['Marketing', ['Overview', 'Social Proof', 'Heroes', 'Content', 'Network', 'Media']], 'Marketing', 'Site' ] diff --git a/apps/storybook/src/stories/_shared/code-samples.js b/apps/storybook/src/stories/_shared/code-samples.js new file mode 100644 index 000000000..87473b15e --- /dev/null +++ b/apps/storybook/src/stories/_shared/code-samples.js @@ -0,0 +1,84 @@ +const IMPORT = 'import' + +const FILE_UPLOAD = `${IMPORT} type { AzionBucketObject, AzionStorageResponse } from "azion/storage"; +${IMPORT} { createObject } from "azion/storage"; +${IMPORT} { Hono } from "hono"; + +const app = new Hono(); + +app.post("/upload", async (c) => { + const body = await c.req.parseBody(); + const file = body["file"]; // File | string + + // First check if file is a valid File object + if ( + !file || + typeof file !== "object" || + typeof file.arrayBuffer !== "function" + ) { + return c.json({ message: "Invalid file" }, 400); + } + + const { data: newObject } = (await createObject({ + bucket: "uploads", + key: file.name, + content: await file.arrayBuffer(), + })) as AzionStorageResponse; + + return c.json({ key: newObject?.key }, 201, { + "Content-Type": file.type, + "Content-Disposition": \`attachment; filename="\${newObject?.key}"\`, + "Content-Length": newObject?.size?.toString() ?? "0", + }); +}); + +export default app;` + +const TRANSACTIONAL_EMAIL = `${IMPORT} { Hono } from "hono"; + +const app = new Hono(); + +app.post("/welcome", async (c) => { + const { email, name } = await c.req.json(); + + // The API key lives in the workload's environment, never in the bundle + const sent = await fetch("https://api.provider.com/v1/messages", { + method: "POST", + headers: { + Authorization: \`Bearer \${Azion.env.get("EMAIL_API_KEY")}\`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + to: email, + subject: "Welcome aboard", + text: \`Hi \${name}, your account is ready.\`, + }), + }); + + if (!sent.ok) { + return c.json({ message: "Could not send the email" }, 502); + } + + return c.json({ queued: true }, 202); +}); + +export default app;` + +export const FUNCTION_SAMPLE_TABS = [ + { + label: 'File upload', + value: 'file-upload', + language: 'typescript', + code: FILE_UPLOAD, + fileName: 'github.com/aziontech/azion-samples', + fileIcon: 'pi pi-github' + }, + { + label: 'Send transactional emails', + value: 'transactional-email', + language: 'typescript', + code: TRANSACTIONAL_EMAIL, + fileName: 'github.com/aziontech/azion-samples', + fileIcon: 'pi pi-github' + } +] diff --git a/apps/storybook/src/stories/_shared/markup.js b/apps/storybook/src/stories/_shared/markup.js new file mode 100644 index 000000000..9c876a149 --- /dev/null +++ b/apps/storybook/src/stories/_shared/markup.js @@ -0,0 +1,31 @@ +export const indent = (markup, depth = 1) => + markup + .split('\n') + .map((line) => (line ? `${' '.repeat(depth)}${line}` : line)) + .join('\n') + +export const each = (items, render, depth = 0) => + items.map((item, index) => indent(render(item, index), depth)).join('\n') + +export const inColumn = (markup) => + `\n \n\n${indent(markup)}\n\n \n` + +export const COLUMN_IMPORTS = [ + "import SectionContainer from '@aziontech/webkit/section-container'", + "import SectionGap from '@aziontech/webkit/section-gap'" +] + +const text = (value) => `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` + +const multiline = (value) => + `\`${value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\`` + +export const declareTabs = (name, tabs) => + `const ${name} = [\n${tabs + .map( + (tab) => + ` {\n${Object.entries(tab) + .map(([key, value]) => ` ${key}: ${key === 'code' ? multiline(value) : text(value)}`) + .join(',\n')}\n }` + ) + .join(',\n')}\n]` diff --git a/apps/storybook/src/stories/templates/marketing/CapabilityGrid.stories.js b/apps/storybook/src/stories/templates/marketing/CapabilityGrid.stories.js new file mode 100644 index 000000000..ced857582 --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/CapabilityGrid.stories.js @@ -0,0 +1,155 @@ +import CardGrid from '@aziontech/webkit/card-grid' +import FrameBox from '@aziontech/webkit/frame-box' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' +import Topic from '@aziontech/webkit/topic' + +import { COLUMN_IMPORTS, each, inColumn } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const IMPORTS = [ + "import CardGrid from '@aziontech/webkit/card-grid'", + "import FrameBox from '@aziontech/webkit/frame-box'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + "import Topic from '@aziontech/webkit/topic'" +] + +const components = { + CardGrid, + 'CardGrid.Cell': CardGrid.Cell, + FrameBox, + SectionContainer, + SectionGap, + SectionModule, + Topic +} + +const RETAIL = [ + { + icon: 'pi pi-bolt', + title: 'Speed that converts', + description: + 'Deliver fast storefront experiences with consistent page speed and Core Web Vitals, lifting conversion, SEO visibility, and average order value across web and mobile.' + }, + { + icon: 'pi pi-chart-line', + title: 'Built for peak season', + description: + 'Stay online during Black Friday, Cyber Monday, and flash sales with automatic scaling and a distributed architecture that absorbs traffic surges without overprovisioning.' + }, + { + icon: 'pi pi-shield', + title: 'Security built in', + description: + 'Protect transactions, customer data, and storefront availability with DDoS mitigation, WAF, and zero trust controls integrated into the same distributed architecture.' + }, + { + icon: 'pi pi-lock', + title: 'Fraud and bots blocked', + description: + 'Block bots, scraping, credential stuffing, account takeover, and payment fraud with bot management and application security applied before requests reach your origin.' + }, + { + icon: 'pi pi-sparkles', + title: 'AI-powered personalization', + description: + 'Deliver tailored recommendations, search, and content with AI Inference and Functions executed at request time, improving conversion without round-tripping to centralized origins.' + }, + { + icon: 'pi pi-credit-card', + title: 'Lower costs at scale', + description: + 'Offload origin traffic, optimize images, and serve cached responses from a distributed architecture to cut cloud, egress, and CDN costs while scaling automatically with demand.' + } +] + +const FINANCIAL = [ + { + icon: 'pi pi-server', + title: 'High availability', + description: + 'Ensure operational continuity for latency-sensitive financial applications and APIs, with automatic scalability and real-time responses, even during transaction peaks.' + }, + { + icon: 'pi pi-shield', + title: 'Advanced security', + description: + 'Protect financial data and transactions with multilayer security integrated into the application, mitigating API attacks, DDoS, and bots without increasing operational complexity.' + }, + { + icon: 'pi pi-check-circle', + title: 'Continuous compliance', + description: + 'Meet regulatory requirements with consistent security policies and continuous auditing across distributed environments, without compromising performance or development agility.' + } +] + +const capabilityGrid = (items) => + inColumn(` + + +${each( + items, + (item) => ` + +`, + 3 +)} + + +`) + +const SIX_TEMPLATE = capabilityGrid(RETAIL) +const THREE_TEMPLATE = capabilityGrid(FINANCIAL) + +const meta = { + title: 'Templates/Marketing/Content/CapabilityGrid', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + backgrounds: { default: 'dark' }, + controls: { disable: true }, + docs: { + description: { + component: + 'The band that answers "what does this do for me" right under a solution page’s hero: three or six capabilities, each an icon, a title and one sentence, on a hairline frame grid. Every solution page opens its column with it (Retail, Web Apps, AI Workloads, Security, Performance, Streaming, Financial Services, Technology). Built from `SectionModule`, `FrameBox`, `CardGrid` and `Topic`.' + }, + canvas: { sourceState: 'shown' } + } + } +} + +export default meta + +export const SixCapabilities = { + render: () => ({ components, template: SIX_TEMPLATE }), + parameters: { + docs: { + description: { + story: + 'Two rows of three, the Retail page’s set. Six is the count a page uses when it argues on breadth.' + }, + source: { code: toSfc(IMPORTS, SIX_TEMPLATE) } + } + } +} + +export const ThreeCapabilities = { + render: () => ({ components, template: THREE_TEMPLATE }), + parameters: { + docs: { + description: { + story: + 'One row of three, the Financial Services page’s set, for a page that argues on a few strong claims.' + }, + source: { code: toSfc(IMPORTS, THREE_TEMPLATE) } + } + } +} diff --git a/apps/storybook/src/stories/templates/marketing/ClientMosaic.stories.js b/apps/storybook/src/stories/templates/marketing/ClientMosaic.stories.js new file mode 100644 index 000000000..f2e8e8e6d --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/ClientMosaic.stories.js @@ -0,0 +1,254 @@ +import agibankExtendedReversed from '@aziontech/webkit/assets/agibank-extended-reversed.svg' +import dafitiExtendedMono from '@aziontech/webkit/assets/dafiti-extended-mono.svg' +import exameExtendedMono from '@aziontech/webkit/assets/exame-extended-mono.svg' +import fourbankExtendedReversed from '@aziontech/webkit/assets/fourbank-extended-reversed.svg' +import gpaExtendedReversed from '@aziontech/webkit/assets/gpa-extended-reversed.svg' +import gpaPhoto from '@aziontech/webkit/assets/gpa-photo.jpg' +import netshoesExtendedColor from '@aziontech/webkit/assets/netshoes-extended-color.svg' +import netshoesPhoto from '@aziontech/webkit/assets/netshoes-photo.jpg' +import nznExtendedReversed from '@aziontech/webkit/assets/nzn-extended-reversed.svg' +import rennerExtendedReversed from '@aziontech/webkit/assets/renner-extended-reversed.svg' +import BentoGrid from '@aziontech/webkit/bento-grid' +import FrameBox from '@aziontech/webkit/frame-box' +import MiniButton from '@aziontech/webkit/mini-button' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' + +import { COLUMN_IMPORTS, each, inColumn, indent } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const IMPORTS = [ + "import agibankExtendedReversed from '@aziontech/webkit/assets/agibank-extended-reversed.svg'", + "import dafitiExtendedMono from '@aziontech/webkit/assets/dafiti-extended-mono.svg'", + "import exameExtendedMono from '@aziontech/webkit/assets/exame-extended-mono.svg'", + "import fourbankExtendedReversed from '@aziontech/webkit/assets/fourbank-extended-reversed.svg'", + "import gpaExtendedReversed from '@aziontech/webkit/assets/gpa-extended-reversed.svg'", + "import gpaPhoto from '@aziontech/webkit/assets/gpa-photo.jpg'", + "import netshoesExtendedColor from '@aziontech/webkit/assets/netshoes-extended-color.svg'", + "import netshoesPhoto from '@aziontech/webkit/assets/netshoes-photo.jpg'", + "import nznExtendedReversed from '@aziontech/webkit/assets/nzn-extended-reversed.svg'", + "import rennerExtendedReversed from '@aziontech/webkit/assets/renner-extended-reversed.svg'", + "import BentoGrid from '@aziontech/webkit/bento-grid'", + "import FrameBox from '@aziontech/webkit/frame-box'", + "import MiniButton from '@aziontech/webkit/mini-button'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'" +] + +const components = { + BentoGrid, + 'BentoGrid.Cell': BentoGrid.Cell, + FrameBox, + MiniButton, + SectionContainer, + SectionGap, + SectionModule +} + +const setup = () => ({ + agibankExtendedReversed, + dafitiExtendedMono, + exameExtendedMono, + fourbankExtendedReversed, + gpaExtendedReversed, + gpaPhoto, + netshoesExtendedColor, + netshoesPhoto, + nznExtendedReversed, + rennerExtendedReversed +}) + +const MONOCHROME = 'brightness-0 [[data-theme=dark]_&]:invert' +const KNOCKOUT = 'brightness-0' + +const CELLS = [ + { + name: 'Netshoes', + span: '2', + rows: '2', + fill: 'surface', + logo: 'netshoesExtendedColor', + filter: MONOCHROME, + mark: 'relative z-10 h-6 w-auto max-w-32 object-contain object-left', + photo: 'netshoesPhoto', + story: 'Netshoes automatically blocks more than 4 million threats in six months', + storyClass: 'text-heading-md', + href: 'https://www.azion.com/en/success-case/netshoes/' + }, + { + name: 'Dafiti', + span: '1', + rows: '1', + fill: 'white', + logo: 'dafitiExtendedMono', + filter: KNOCKOUT, + mark: 'relative z-10 h-8 w-auto max-w-36 object-contain', + href: 'https://www.azion.com/en/success-case/dafiti/dafiti-accelerates-its-e-commerce-by-86-and-saves-45-on-data-transfer-costs-using-azion-edge-application/' + }, + { + name: 'Agibank', + span: '1', + rows: '1', + fill: 'canvas', + logo: 'agibankExtendedReversed', + filter: MONOCHROME, + mark: 'relative z-10 h-8 w-auto max-w-36 object-contain', + href: 'https://www.azion.com/en/success-case/agibank/' + }, + { + name: 'Renner', + span: '1', + rows: '1', + fill: 'canvas', + logo: 'rennerExtendedReversed', + filter: MONOCHROME, + mark: 'relative z-10 h-8 w-auto max-w-36 object-contain', + href: 'https://www.azion.com/en/success-case/renner/' + }, + { + name: 'GPA', + span: '1', + rows: '2', + fill: 'surface', + logo: 'gpaExtendedReversed', + filter: MONOCHROME, + mark: 'relative z-10 h-9 w-auto max-w-32 object-contain object-left', + photo: 'gpaPhoto', + story: 'Grupo Pão de Açúcar (GPA) stops a cyberattack and reduces costs by 30%', + storyClass: 'text-pretty text-body-lg', + href: 'https://www.azion.com/en/success-case/gpa-solved-cyberattack/' + }, + { + name: 'Fourbank', + span: '1', + rows: '1', + fill: 'primary', + logo: 'fourbankExtendedReversed', + filter: KNOCKOUT, + mark: 'relative z-10 h-8 w-auto max-w-36 object-contain', + href: 'https://www.azion.com/en/success-case/fourbank/' + }, + { + name: 'Exame', + span: '1', + rows: '1', + fill: 'canvas', + logo: 'exameExtendedMono', + filter: MONOCHROME, + mark: 'relative z-10 h-8 w-auto max-w-36 object-contain', + href: 'https://www.azion.com/en/success-case/exame/' + }, + { + name: 'NZN', + span: '1', + rows: '1', + fill: 'primary', + logo: 'nznExtendedReversed', + filter: KNOCKOUT, + mark: 'relative z-10 h-8 w-auto max-w-36 object-contain', + href: 'https://www.azion.com/en/success-case/nzn/nzn-creates-more-than-100-edge-applications-and-reduces-their-websites-loading-time-by-50-using-the-azion-platform/' + } +] + +const FACE = + 'relative flex h-full min-w-0 flex-col overflow-hidden p-(--spacing-xl) transition-colors duration-fast-02 ease-productive-entrance focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-(--ring-color) motion-reduce:transition-none data-[fill=canvas]:bg-(--bg-canvas) data-[fill=canvas]:hover:bg-(--bg-surface-raised) data-[fill=primary]:bg-(--primary) data-[fill=primary]:hover:bg-(--color-orange-600) data-[fill=surface]:bg-(--bg-surface) data-[fill=white]:bg-(--color-base-white)' + +const clientMark = (cell) => `${cell.name}` + +const storyFace = (cell) => `
+ + ` + +const markFace = (cell) => ` +${indent(clientMark(cell))} +` + +const TEMPLATE = inColumn(` + + +${each( + CELLS, + (cell) => ` +${indent(cell.story ? storyFace(cell) : markFace(cell))} +`, + 3 +)} + + +`) + +const meta = { + title: 'Templates/Marketing/Social Proof/ClientMosaic', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + backgrounds: { default: 'dark' }, + controls: { disable: true }, + docs: { + description: { + component: + 'Client stories as a mosaic of tiles on one hairline grid with no gutter: eight clients, each linking to its success case. Two tiles carry a story headline over the client’s own photograph, under a scrim that ends on the canvas so the headline keeps its contrast, and close on a “Learn more” link; the rest are the client’s logo, centred, and the whole tile is the link. Logos on the canvas are drawn as one flat ink that follows the theme; on the white and orange fills they are pinned to black. Home carries it after the analyst recognitions. Built from `SectionModule`, `FrameBox`, `BentoGrid` (`BentoGrid.Cell`) and `MiniButton`.' + }, + canvas: { sourceState: 'shown' } + } + } +} + +export default meta + +export const Default = { + render: () => ({ components, setup, template: TEMPLATE }), + parameters: { + docs: { + description: { + story: + 'Home’s mosaic, four columns (two on mobile): Netshoes leads as a two-by-two story tile, GPA runs down two rows as the second, and six logo tiles fill the rest, Dafiti on white and Fourbank and NZN on the brand orange.' + }, + source: { code: toSfc(IMPORTS, TEMPLATE) } + } + } +} diff --git a/apps/storybook/src/stories/templates/marketing/ClientQuotes.stories.js b/apps/storybook/src/stories/templates/marketing/ClientQuotes.stories.js new file mode 100644 index 000000000..58b45c318 --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/ClientQuotes.stories.js @@ -0,0 +1,153 @@ +import herosparkExtendedColor from '@aziontech/webkit/assets/herospark-extended-color.svg' +import magaluExtendedColor from '@aziontech/webkit/assets/magalu-extended-color.svg' +import zoopExtendedColor from '@aziontech/webkit/assets/zoop-extended-color.svg' +import Button from '@aziontech/webkit/button' +import FrameBox from '@aziontech/webkit/frame-box' +import QuoteTabs from '@aziontech/webkit/quote-tabs' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' + +import { COLUMN_IMPORTS, each, inColumn } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const IMPORTS = [ + "import herosparkExtendedColor from '@aziontech/webkit/assets/herospark-extended-color.svg'", + "import magaluExtendedColor from '@aziontech/webkit/assets/magalu-extended-color.svg'", + "import zoopExtendedColor from '@aziontech/webkit/assets/zoop-extended-color.svg'", + "import Button from '@aziontech/webkit/button'", + "import FrameBox from '@aziontech/webkit/frame-box'", + "import QuoteTabs from '@aziontech/webkit/quote-tabs'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'" +] + +const components = { + Button, + FrameBox, + QuoteTabs, + SectionContainer, + SectionGap, + SectionModule +} + +const setup = () => ({ herosparkExtendedColor, magaluExtendedColor, zoopExtendedColor }) + +const RETAIL_QUOTES = [ + { + mark: 'dafiti', + clientName: 'Dafiti', + text: "One of the best CDN and WAF solutions I've ever used. Easy to implement and integrate, with speed and low latency that make a real difference for our customers.", + name: 'Julian H', + jobTitle: 'IT OPS, SRE & SEC Manager at Dafiti Group' + }, + { + mark: 'axur', + text: 'With Azion, we scale proprietary AI models without managing infrastructure—inspecting millions of websites daily and automating the market’s fastest threat takedown.', + name: 'Fabio Ramos', + jobTitle: 'CEO at Axur' + }, + { + logo: 'magaluExtendedColor', + clientName: 'Magalu', + text: 'Azion shielded us from sophisticated cyberattacks and empowered us to modernize our infrastructure, reduce costs, and deliver the best shopping experiences to millions of customers across Latin America.', + name: 'Allan Monteiro', + jobTitle: 'CISO & Head of Technology at Magalu' + }, + { + logo: 'herosparkExtendedColor', + clientName: 'HeroSpark', + text: 'Azion transformed our operations, reducing costs and improving performance while freeing 200+ monthly hours for strategic development.', + name: 'Mateus Leonardi', + jobTitle: 'CTO at HeroSpark' + }, + { + logo: 'zoopExtendedColor', + clientName: 'Zoop', + text: 'Azion delivered the advanced protection and superior performance we needed, with fast implementation and immediate results.', + name: 'Ismael Aguilar', + jobTitle: 'Information Security Manager at Zoop' + }, + { + mark: 'contabilizei', + text: 'With Azion, Contabilizei improved request delivery at the Edge, reduced infrastructure costs, and gained fast access to support whenever needed.', + name: 'Fabrício Santos', + jobTitle: 'DevSecOps Manager at Contabilizei' + } +] + +const quoted = (value) => `'${value.replace(/'/g, "\\'").replace(/"/g, '"')}'` + +const FIELDS = ['logo', 'mark', 'clientName', 'text', 'name', 'jobTitle'] + +const quoteLiteral = (quote, index, list) => { + const fields = FIELDS.filter((key) => quote[key]).map((key) => + key === 'logo' ? ` logo: ${quote.logo}` : ` ${key}: ${quoted(quote[key])}` + ) + return ['{', fields.join(',\n'), `}${index < list.length - 1 ? ',' : ''}`].join('\n') +} + +const clientQuotes = (quotes) => + inColumn(` + + + + + +`) + +const RETAIL_TEMPLATE = clientQuotes(RETAIL_QUOTES) + +const meta = { + title: 'Templates/Marketing/Social Proof/ClientQuotes', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + backgrounds: { default: 'dark' }, + controls: { disable: true }, + docs: { + description: { + component: + 'The proof band of a solution page: one client quotation on screen at a time, over a wall of client cards that is the selector itself, framed edge to edge and closed by a link to every success story. Each page leads the wall with the client that speaks to its audience and keeps the rest in one fixed order. All eight solution pages render it: Retail, Web Apps, AI Workloads, Security, Performance, Streaming, Technology and Financial Services. Built from `SectionModule`, `FrameBox`, `QuoteTabs` and `Button`.' + }, + canvas: { sourceState: 'shown' } + } + } +} + +export default meta + +export const Default = { + render: () => ({ components, setup, template: RETAIL_TEMPLATE }), + parameters: { + docs: { + description: { + story: + 'The Retail page’s wall, led by Dafiti with the wording that page gives its quotation. Magalu, HeroSpark and Zoop pass their colour logo files from `@aziontech/webkit/assets`; Dafiti, Axur and Contabilizei pass a registered `mark` name and draw in the theme’s one ink.' + }, + source: { code: toSfc(IMPORTS, RETAIL_TEMPLATE) } + } + } +} diff --git a/apps/storybook/src/stories/templates/marketing/ClosingCallToAction.stories.js b/apps/storybook/src/stories/templates/marketing/ClosingCallToAction.stories.js new file mode 100644 index 000000000..e0a81bf0c --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/ClosingCallToAction.stories.js @@ -0,0 +1,145 @@ +import Button from '@aziontech/webkit/button' +import CallToAction from '@aziontech/webkit/call-to-action' +import FrameBox from '@aziontech/webkit/frame-box' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' +import TextureMaterial from '@aziontech/webkit/texture-material' + +import { COLUMN_IMPORTS, indent } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const IMPORTS = [ + "import Button from '@aziontech/webkit/button'", + "import CallToAction from '@aziontech/webkit/call-to-action'", + "import FrameBox from '@aziontech/webkit/frame-box'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + "import TextureMaterial from '@aziontech/webkit/texture-material'" +] + +const components = { + Button, + CallToAction, + FrameBox, + SectionContainer, + SectionGap, + SectionModule, + TextureMaterial +} + +const closingColumn = (markup) => + `\n \n\n${indent(markup)}\n` + +const SPLIT_TEMPLATE = closingColumn(` + + + + + + + +`) + +const PANEL_TEMPLATE = closingColumn(` + + + + + + + +`) + +const meta = { + title: 'Templates/Marketing/Content/ClosingCallToAction', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + backgrounds: { default: 'dark' }, + controls: { disable: true }, + docs: { + description: { + component: + 'The close of every landing page: one last ask after a hatched gap, then a closing spacer that draws no rules of its own, because the site footer under it opens with a full-bleed rule. Almost every page ends with the split form, among them Home, Products, Pricing, Success Cases, the Vercel Alternative page and every solution page, and only its copy varies per page (Build / Build once. / Run everywhere. on Retail, Web Apps, AI Workloads, Financial Services, Technology and Success Cases; Secure / Protected by default. / Always on. on Security and the Vercel Alternative page; Performance / Fast everywhere. / Always reliable. on Performance). Careers ends with the single-action panel and a closing frame half that height. Built from `SectionContainer`, `SectionGap`, `SectionModule`, `CallToAction`, `Button` and `FrameBox`, with `TextureMaterial` ruling each closing frame.' + }, + canvas: { sourceState: 'shown' } + } + } +} + +export default meta + +export const Split = { + render: () => ({ components, template: SPLIT_TEMPLATE }), + parameters: { + docs: { + description: { + story: + 'The solution pages’ close: a split `CallToAction` with `Start Free` in the lead cell and `Talk to our team` in the aside, then a double-height closing frame with its corner marks and the lines texture. The module’s `id="contact"` is the anchor the page’s other contact links scroll to.' + }, + source: { code: toSfc(IMPORTS, SPLIT_TEMPLATE) } + } + } +} + +export const Panel = { + render: () => ({ components, template: PANEL_TEMPLATE }), + parameters: { + docs: { + description: { + story: + 'The Careers page’s close: a panel `CallToAction` with a single action to the job listing, then the small closing frame, one `--spacing-xxl` tall: the same corner marks and lines texture as the split close, at the height of a small `SectionGap`. It draws no rules, because the footer under it opens with its own.' + }, + source: { code: toSfc(IMPORTS, PANEL_TEMPLATE) } + } + } +} diff --git a/apps/storybook/src/stories/templates/marketing/CodeSplit.stories.js b/apps/storybook/src/stories/templates/marketing/CodeSplit.stories.js new file mode 100644 index 000000000..65edea52b --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/CodeSplit.stories.js @@ -0,0 +1,385 @@ +import Button from '@aziontech/webkit/button' +import CodeBlock from '@aziontech/webkit/code-block' +import FrameBox from '@aziontech/webkit/frame-box' +import MediaSplit from '@aziontech/webkit/media-split' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' +import SectionTitle from '@aziontech/webkit/section-title' +import TextureMaterial from '@aziontech/webkit/texture-material' + +import { FUNCTION_SAMPLE_TABS } from '../../_shared/code-samples' +import { COLUMN_IMPORTS, declareTabs, each, inColumn } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const components = { + Button, + CodeBlock, + FrameBox, + MediaSplit, + SectionContainer, + SectionGap, + SectionModule, + SectionTitle, + TextureMaterial +} + +const CACHE_SETTINGS = `{ + "cache_settings": { + "name": "api-cache", + "browser_cache_settings": { + "ttl": 60 + }, + "cdn_cache_settings": { + "ttl": 30 + }, + "cache_key": { + "query_string": "whitelist", + "query_string_fields": ["user_id", "category"], + "cookie": "whitelist", + "cookie_names": ["session_id", "region"] + }, + "methods": ["GET", "POST", "OPTIONS"], + "stale_cache": true + } +}` + +const CACHE_TABS = [ + { + label: 'cache-settings.json', + value: 'cache-settings', + language: 'json', + code: CACHE_SETTINGS, + fileName: 'cache-settings.json', + fileIcon: 'ai ai-json' + } +] + +const AZION_CONFIG = `export default { + build: { preset: 'vue' }, + rules: { + request: [ + { + name: 'Deliver Static Assets', + match: '.(css|js|svg|png|webp|woff2)$', + behavior: { + setOrigin: { name: 'origin-storage-default' }, + deliver: true + } + }, + { + name: 'Redirect to index.html', + match: '^\\/', + behavior: { rewrite: '/index.html' } + } + ] + } +}` + +const HANDLER = `export default { + async fetch(request) { + const url = new URL(request.url) + + if (url.pathname === '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/api/status') { + return Response.json({ region: 'edge', ok: true }) + } + + return fetch(request) + } +}` + +const STORAGE = `export default { + async fetch(request) { + const url = new URL(request.url) + const asset = await fetch(new URL(url.pathname, 'file://assets-prod/')) + + return new Response(asset.body, { + headers: { 'cache-control': 'public, max-age=86400' } + }) + } +}` + +const WORKLOAD_TABS = [ + { + label: 'azion.config.js', + value: 'config', + language: 'javascript', + code: AZION_CONFIG, + fileName: 'azion.config.js', + fileIcon: 'pi pi-code' + }, + { + label: 'main.js', + value: 'handler', + language: 'javascript', + code: HANDLER, + fileName: 'main.js', + fileIcon: 'pi pi-code' + }, + { + label: 'assets.js', + value: 'storage', + language: 'javascript', + code: STORAGE, + fileName: 'assets.js', + fileIcon: 'pi pi-code' + } +] + +const RECIPES = [ + { + title: 'Declare the workload in code', + description: + 'The hostname, the build preset and the routing live in the repository, so shipping a change is a commit rather than a console session.' + }, + { + title: 'Answer the request yourself', + description: + 'Return a response from the handler when a route needs logic — an auth check, a redirect, a shaped payload — instead of sending it to the origin.' + }, + { + title: 'Serve objects from storage', + description: + 'Bind a bucket to the workload and let it answer with the object directly, with no server in front of it and no egress charge behind it.' + } +] + +const FULL_STACK_IMPORTS = [ + "import Button from '@aziontech/webkit/button'", + "import CodeBlock from '@aziontech/webkit/code-block'", + "import MediaSplit from '@aziontech/webkit/media-split'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + '', + declareTabs('codeTabs', FUNCTION_SAMPLE_TABS) +] + +const ACCELERATOR_IMPORTS = [ + "import Button from '@aziontech/webkit/button'", + "import CodeBlock from '@aziontech/webkit/code-block'", + "import FrameBox from '@aziontech/webkit/frame-box'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + '', + declareTabs('codeTabs', CACHE_TABS) +] + +const RECIPES_IMPORTS = [ + "import CodeBlock from '@aziontech/webkit/code-block'", + "import FrameBox from '@aziontech/webkit/frame-box'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + "import SectionTitle from '@aziontech/webkit/section-title'", + "import TextureMaterial from '@aziontech/webkit/texture-material'", + '', + declareTabs('codeTabs', WORKLOAD_TABS) +] + +const FULL_STACK_TEMPLATE = inColumn(` + + + + +`) + +const ACCELERATOR_TEMPLATE = inColumn(` + +
+
+
+

+ From basic caching to advanced acceleration +

+

+ Application Accelerator extends Cache with protocol optimizations and advanced cache + rules for dynamic content. +

+
+ +
+
+
+ +
+
+ +
+
+
+
+
`) + +const RECIPES_TEMPLATE = inColumn(` + + + +
+
+ + +
+ +
+
+ +
    +${each( + RECIPES, + (recipe) => `
  • +

    + ${recipe.title} +

    +

    + ${recipe.description} +

    +
  • `, + 4 +)} +
+
+
+
`) + +const meta = { + title: 'Templates/Marketing/Media/CodeSplit', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + backgrounds: { default: 'dark' }, + controls: { disable: true }, + docs: { + description: { + component: + 'A claim set beside the code that makes it true: copy in one cell, a `CodeBlock` laid on the other, split on one frame. Product pages use it where the argument is configuration or a handler a developer can read (Functions, Application Accelerator, Workloads). Built from `SectionModule`, a `framed` `MediaSplit` or a `FrameBox`, and `CodeBlock`, with buttons or a recipe list on the copy side. The `CodeBlock` sits in a wrapper at its own radius, so the wrapper casts the shadow the block would clip. The tabs are declared in the script so the code strings stay verbatim.' + }, + canvas: { sourceState: 'shown' } + } + } +} + +export default meta + +export const Default = { + render: () => ({ + components, + setup: () => ({ codeTabs: FUNCTION_SAMPLE_TABS }), + template: FULL_STACK_TEMPLATE + }), + parameters: { + docs: { + description: { + story: + 'The code split, from the Functions page: a framed `MediaSplit` whose media is a two-tab `CodeBlock`, inset by `media-padded` on the shared small, top-faded pixelate field and run off the frame’s bottom rule so the sample is cut mid-line. It closes on two medium actions, Docs as the secondary button and See GitHub as the outlined one. With two destinations the band sets no `media-href`: it is not a link itself and hovering it lights neither button, so the reader picks one.' + }, + source: { code: toSfc(FULL_STACK_IMPORTS, FULL_STACK_TEMPLATE) } + } + } +} + +export const Accelerator = { + render: () => ({ + components, + setup: () => ({ codeTabs: CACHE_TABS }), + template: ACCELERATOR_TEMPLATE + }), + parameters: { + docs: { + description: { + story: + 'From the Application Accelerator page: a heading, a sentence and a text `Button` on the start cell, and a one-file `CodeBlock` on a surface panel on the end cell. With one tab, the block draws a filename bar and no tab strip.' + }, + source: { code: toSfc(ACCELERATOR_IMPORTS, ACCELERATOR_TEMPLATE) } + } + } +} + +export const Recipes = { + render: () => ({ + components, + setup: () => ({ codeTabs: WORKLOAD_TABS }), + template: RECIPES_TEMPLATE + }), + parameters: { + docs: { + description: { + story: + 'From the Workloads page: a horizontal `SectionTitle` opens the module, a three-file `CodeBlock` sits on a faded pixelate `TextureMaterial`, and three recipes run down the end cell, one per file.' + }, + source: { code: toSfc(RECIPES_IMPORTS, RECIPES_TEMPLATE) } + } + } +} diff --git a/apps/storybook/src/stories/templates/marketing/ComparePlans.stories.js b/apps/storybook/src/stories/templates/marketing/ComparePlans.stories.js new file mode 100644 index 000000000..b892d3e8d --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/ComparePlans.stories.js @@ -0,0 +1,440 @@ +import Button from '@aziontech/webkit/button' +import Hint from '@aziontech/webkit/hint' +import Link from '@aziontech/webkit/link' +import Overline from '@aziontech/webkit/overline' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' +import Select, { SelectContent, SelectOption, SelectTrigger } from '@aziontech/webkit/select' +import Tag from '@aziontech/webkit/tag' +import { ref } from 'vue' + +import { COLUMN_IMPORTS, inColumn } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const PLANS = [ + { + id: 'hobby', + name: 'Hobby', + highlighted: false, + action: { label: 'Start for free', kind: 'outlined', href: '/signup' } + }, + { + id: 'pro', + name: 'Pro', + highlighted: true, + action: { label: 'Start with Pro', kind: 'secondary', href: '/signup' } + }, + { + id: 'enterprise', + name: 'Enterprise', + highlighted: false, + action: { label: 'Contact us', kind: 'outlined', href: '#contact' } + } +] + +const SECTIONS = [ + { + eyebrow: 'Managed infrastructure', + title: 'Azion Platform', + description: 'Running modern workloads on a global infrastructure.', + rows: [ + { label: 'Global infrastructure', group: true, values: ['', '', ''] }, + { label: '100+ data centers', values: [true, true, true] }, + { + label: 'Automatic routing with load balancing and failover', + values: [true, true, true] + }, + { label: 'Management', group: true, values: ['', '', ''] }, + { + label: 'Workspaces (Multi-Org)', + hint: 'A workspace is an isolated set of resources, users and billing inside your organization.', + values: ['1 Workspace', '1 Workspace', 'Custom'] + }, + { label: 'Users', values: ['Unlimited', 'Unlimited', 'Unlimited'] }, + { label: 'Access security', group: true, values: ['', '', ''] }, + { + label: 'Role-Based Access Control (RBAC)', + hint: 'Gives each user only the permissions their role needs, instead of full account access.', + values: ['Team level', 'Team level', 'Team and workspace levels'] + }, + { + label: 'Multi-Factor Authentication (MFA)', + hint: 'Requires a second factor beyond the password when signing in.', + values: [true, true, true] + }, + { label: 'Login OAuth (Google, GitHub)', values: [true, true, true] }, + { + label: 'SAML Single Sign-On (SSO)', + hint: 'Lets your team sign in through your own identity provider instead of Azion credentials.', + values: [true, true, true] + }, + { label: 'Workloads', group: true, values: ['', '', ''] }, + { + label: 'Workloads', + hint: 'A workload is the domain and TLS configuration that exposes an application or firewall to the internet.', + values: ['10 included', '20 included', 'Custom'] + }, + { label: 'Data transfer', values: ['1 TB / mo included', '2 TB / mo included', 'Custom'] }, + { label: 'Requests', values: ['10M / mo included', '20M / mo included', 'Custom'] }, + { label: 'TLS encryption', values: [true, true, true] }, + { + label: 'Mutual TLS (mTLS)', + hint: 'Requires the client to present a certificate too, not just the server.', + values: [true, true, true] + }, + { + label: 'Certificate Manager', + hint: 'Where TLS certificates are issued, imported and renewed for your workloads.', + group: true, + values: ['', '', ''] + }, + { label: "Let's Encrypt certificate", values: [true, true, true] }, + { label: 'Bring your own certificate', values: [true, true, true] }, + { label: 'Developer tools', group: true, values: ['', '', ''] }, + { label: 'CLI, API, GraphQL API, Terraform', values: [true, true, true] } + ] + }, + { + eyebrow: 'Build', + title: 'Azion Applications', + description: 'Build, run and scale applications globally.', + rows: [ + { label: 'Applications', group: true, values: ['', '', ''] }, + { + label: 'Rules Engine', + hint: 'Conditional rules that change how a request is handled, evaluated at request and response time.', + values: [true, true, true] + }, + { label: 'Rules per application', values: ['10 included', '20 included', 'Custom'] }, + { label: 'Redirects', values: [true, true, true] }, + { + label: 'Reverse proxy using Connectors', + hint: 'Connectors define the origins an application fetches from, including headers and failover.', + values: [true, true, true] + }, + { label: 'Functions', group: true, values: ['', '', ''] }, + { + label: 'Compute time', + hint: 'The total execution time your functions consume, billed by the hour.', + values: ['8 hours / mo included', '10 hours / mo included', 'Custom'] + }, + { label: 'Requests', values: ['3M / mo included', '10M / mo included', 'Custom'] }, + { label: 'Cache', group: true, values: ['', '', ''] }, + { + label: 'Purges', + hint: 'A purge invalidates cached content before its TTL expires.', + values: ['1,000 / mo included', '2,000 / mo included', 'Custom'] + }, + { + label: 'Tiered Cache', + hint: 'Adds a second cache layer between the network and your origin, so fewer requests reach it.', + values: [true, true, true] + }, + { + label: 'Application Accelerator', + hint: 'Extends caching and routing control to dynamic content and APIs.', + group: true, + values: ['', '', ''] + }, + { label: 'Data transfer', values: ['1 TB / mo included', '2 TB / mo included', 'Custom'] }, + { + label: 'Custom Cache Keys', + hint: 'Choose which parts of a request — query string, cookies, headers — make two responses distinct.', + values: [true, true, true] + }, + { label: 'Path Rewrites', values: [true, true, true] }, + { + label: 'Image Processor', + hint: 'Resizes, crops and converts images on the fly from a single stored original.', + group: true, + values: ['', '', ''] + }, + { label: 'Images', values: ['5,000 / mo included', '10,000 / mo included', 'Custom'] }, + { + label: 'AI Inference', + hint: 'Runs model inference on the same distributed network, close to the user.', + group: true, + tag: 'Preview', + values: ['', '', ''] + }, + { + label: 'Preview access', + values: ['On request (subject to approval)', 'On request (subject to approval)', 'Custom'] + } + ] + }, + { + eyebrow: 'Azion Platform', + title: 'Compliance', + description: 'Compliance that protects your business.', + rows: [ + { + label: 'PCI DSS 4.0.1 Level 1 Service Provider', + hint: 'Azion is assessed annually as a Level 1 service provider under PCI DSS 4.0.1.', + group: true, + values: ['—', '—', true] + }, + { + label: 'SOC 2 Type 2 / SOC 3', + hint: "Independent audit of Azion's security, availability and confidentiality controls over time.", + group: true, + values: ['—', '—', true] + }, + { + label: 'FIPS 140-2 Level 3', + hint: 'Cryptographic keys are held in hardware validated to FIPS 140-2 Level 3.', + group: true, + values: ['—', '—', 'Included with Secure Key Store (paid add-on)'] + } + ] + }, + { + eyebrow: 'Contractual commitments', + title: 'Commit and Save', + description: 'Commit up front and save more.', + rows: [ + { + label: 'Capacity Reservation', + hint: 'Commit to a usage volume up front in exchange for a lower unit price.', + group: true, + values: ['—', '—', 'Available'] + }, + { + label: 'Savings Plan', + hint: 'Commit to a spend amount over a 1, 2 or 3 year term in exchange for a lower unit price.', + group: true, + values: ['—', '—', 'Available'] + } + ] + } +] + +const DEFAULT_PLAN = PLANS.find((plan) => plan.highlighted).id + +const planName = (id) => PLANS.find((plan) => plan.id === id)?.name ?? '' + +const labelHead = (label) => label.slice(0, label.lastIndexOf(' ') + 1) + +const labelTail = (label) => label.slice(label.lastIndexOf(' ') + 1) + +const quoted = (text) => + text.includes("'") && !text.includes('"') ? `"${text}"` : `'${text.replaceAll("'", "\\'")}'` + +const literal = (value, depth = 0) => { + if (typeof value === 'string') return quoted(value) + if (value === null || typeof value !== 'object') return String(value) + const entries = Array.isArray(value) + ? value.map((item) => literal(item, depth + 1)) + : Object.entries(value).map(([key, item]) => `${key}: ${literal(item, depth + 1)}`) + const [open, close] = Array.isArray(value) ? ['[', ']'] : ['{ ', ' }'] + const inline = `${open}${entries.join(', ')}${close}` + if (!inline.includes('\n') && inline.length + depth * 2 <= 96) return inline + const pad = ' '.repeat(depth + 1) + return `${open.trim()}\n${entries.map((entry) => `${pad}${entry}`).join(',\n')}\n${' '.repeat(depth)}${close.trim()}` +} + +const IMPORTS = [ + "import Button from '@aziontech/webkit/button'", + "import Hint from '@aziontech/webkit/hint'", + "import Link from '@aziontech/webkit/link'", + "import Overline from '@aziontech/webkit/overline'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + "import Select from '@aziontech/webkit/select'", + "import Tag from '@aziontech/webkit/tag'", + "import { ref } from 'vue'", + '', + `const plans = ${literal(PLANS)}`, + '', + `const sections = ${literal(SECTIONS)}`, + '', + `const visiblePlan = ref(${quoted(DEFAULT_PLAN)})`, + '', + "const planName = (id) => plans.find((plan) => plan.id === id)?.name ?? ''", + '', + "const labelHead = (label) => label.slice(0, label.lastIndexOf(' ') + 1)", + '', + "const labelTail = (label) => label.slice(label.lastIndexOf(' ') + 1)" +] + +const components = { + Button, + Hint, + Link, + Overline, + SectionContainer, + SectionGap, + SectionModule, + Select, + 'Select.Trigger': SelectTrigger, + 'Select.Content': SelectContent, + 'Select.Option': SelectOption, + Tag +} + +const TEMPLATE = inColumn(` + + + + + + + + + + + + + + + + + + + + + + +
+ Feature and included-usage comparison across the Hobby, Pro and Enterprise plans. +
+ Features + + +
+ {{ section.eyebrow }} + {{ section.title }} + + {{ section.description }} + +
+ + + + + + + + + {{ row.values[planIndex] }} +
+ +
+
`) + +const meta = { + title: 'Templates/Marketing/Content/ComparePlans', + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + backgrounds: { default: 'dark' }, + controls: { disable: true }, + docs: { + description: { + component: + 'The plan-by-plan feature matrix that follows the plan cards on the Pricing page: one column per plan, one band per product, and the allowance each plan includes on every row. It is a real `table` with one `tbody` per product, so a screen reader announces each allowance with its row and its plan. A cell is a tick (included), an em dash (not included) or the stated allowance, and the two glyphs carry their meaning in `sr-only` text; a `Hint` defines the terms a row names but does not explain. The plan header is sticky at `top-14`, the height of the site’s sticky nav, with the recommended plan marked by an accent bar on its top edge. Below `lg` the matrix narrows to the feature column and one plan, picked with a `Select` in the header; the other columns are hidden, not duplicated, which is why the table is `table-auto` until `lg` and `table-fixed` from there. Built from `SectionModule`, `Select`, `Button`, `Overline`, `Hint`, `Tag` and `Link`.' + }, + canvas: { sourceState: 'shown' } + } + } +} + +export default meta + +export const PlanMatrix = { + render: () => ({ + components, + setup: () => ({ + plans: PLANS, + sections: SECTIONS, + visiblePlan: ref(DEFAULT_PLAN), + planName, + labelHead, + labelTail + }), + template: TEMPLATE + }), + parameters: { + docs: { + description: { + story: + 'Hobby, Pro and Enterprise across four of the Pricing page’s nine product sections: Azion Platform, Azion Applications, Compliance, and Commit and Save, which between them use every kind of cell. The rows are data, so a page adds the other sections to `sections` without touching the markup. Each plan’s button repeats the action of its card above the matrix, and the closing row hands off to the on-demand rates. Narrow the canvas below 1024px to see the plan picker; it opens on Pro, the recommended plan.' + }, + source: { code: toSfc(IMPORTS, TEMPLATE) } + } + } +} diff --git a/apps/storybook/src/stories/templates/marketing/ComparisonTable.stories.js b/apps/storybook/src/stories/templates/marketing/ComparisonTable.stories.js new file mode 100644 index 000000000..b7c94891b --- /dev/null +++ b/apps/storybook/src/stories/templates/marketing/ComparisonTable.stories.js @@ -0,0 +1,178 @@ +import vercelExtendedMono from '@aziontech/webkit/assets/vercel-extended-mono.svg' +import vercelExtendedReversed from '@aziontech/webkit/assets/vercel-extended-reversed.svg' +import Brand from '@aziontech/webkit/brand' +import SectionContainer from '@aziontech/webkit/section-container' +import SectionGap from '@aziontech/webkit/section-gap' +import SectionModule from '@aziontech/webkit/section-module' +import SectionTitle from '@aziontech/webkit/section-title' + +import { COLUMN_IMPORTS, each, inColumn, indent } from '../../_shared/markup' +import { toSfc } from '../../_shared/story-source' + +const IMPORTS = [ + "import vercelExtendedMono from '@aziontech/webkit/assets/vercel-extended-mono.svg'", + "import vercelExtendedReversed from '@aziontech/webkit/assets/vercel-extended-reversed.svg'", + "import Brand from '@aziontech/webkit/brand'", + ...COLUMN_IMPORTS, + "import SectionModule from '@aziontech/webkit/section-module'", + "import SectionTitle from '@aziontech/webkit/section-title'" +] + +const components = { + Brand, + SectionContainer, + SectionGap, + SectionModule, + SectionTitle +} + +const CAPABILITIES = [ + { + capability: 'Delivery, compute, storage, and security on one platform', + azion: 'full', + vercel: 'partial' + }, + { capability: 'Git-connected builds and preview deployments', azion: 'full', vercel: 'full' }, + { + capability: 'Delivery rules changed without redeploying the application', + azion: 'full', + vercel: 'partial' + }, + { capability: 'JavaScript and WebAssembly runtime', azion: 'full', vercel: 'full' }, + { capability: 'Cold-start-free function execution', azion: 'full', vercel: 'partial' }, + { capability: 'AI inference on the platform', azion: 'full', vercel: 'partial' }, + { capability: 'Distributed key-value store', azion: 'full', vercel: 'partial' }, + { capability: 'S3-compatible object storage', azion: 'full', vercel: 'partial' }, + { capability: 'Distributed SQL database with vector search', azion: 'full', vercel: 'none' }, + { capability: 'Managed WAF for applications and APIs', azion: 'full', vercel: 'full' }, + { capability: 'API discovery and protection', azion: 'full', vercel: 'partial' }, + { capability: 'Bot management', azion: 'full', vercel: 'full' }, + { capability: 'DDoS protection included with delivery', azion: 'full', vercel: 'full' }, + { capability: 'Network-layer firewall', azion: 'full', vercel: 'none' }, + { capability: 'Tiered caching and origin offload', azion: 'full', vercel: 'partial' }, + { capability: 'Image and video optimization', azion: 'full', vercel: 'partial' }, + { capability: 'Authoritative DNS', azion: 'full', vercel: 'full' }, + { capability: 'Load balancing across multi-cloud origins', azion: 'full', vercel: 'none' }, + { + capability: 'Accelerate and protect origins hosted elsewhere', + azion: 'full', + vercel: 'none' + }, + { capability: 'Real-time metrics and event search', azion: 'full', vercel: 'full' }, + { capability: 'Log streaming to third-party tools', azion: 'full', vercel: 'full' }, + { capability: 'Real-user monitoring', azion: 'full', vercel: 'full' }, + { capability: 'Published pay-as-you-go pricing', azion: 'full', vercel: 'full' } +] + +const SUPPORT_MARKUP = { + full: `