merge: session work for ScrewFast website rebuild
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m27s

This commit is contained in:
Ahmed Darrazi 2026-05-20 23:34:22 +02:00
commit 725711fbbd
255 changed files with 15231 additions and 9211 deletions

22
apps/website/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# build output
dist/
# generated types
.astro/
.idea/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

20
apps/website/.prettierrc Normal file
View File

@ -0,0 +1,20 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View File

@ -0,0 +1,27 @@
# Third-Party Notices
This website app vendors and adapts portions of the ScrewFast project.
## ScrewFast
MIT License
Copyright (c) 2024 Emil Gulamov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,28 +1,122 @@
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon';
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import starlight from '@astrojs/starlight';
const publicSiteUrl = process.env.PUBLIC_SITE_URL ?? 'https://tenantial.example';
import mdx from '@astrojs/mdx';
const redirectOnlyPaths = new Set([
'/product/',
'/products/',
'/services/',
'/blog/',
'/insights/',
]);
const isRedirectOnlySitemapPath = page => {
const pathname = page.startsWith('http') ? new URL(page).pathname : page;
const normalized = pathname.endsWith('/') ? pathname : `${pathname}/`;
return redirectOnlyPaths.has(normalized);
};
// https://astro.build/config
export default defineConfig({
integrations: [icon()],
output: 'static',
redirects: {
'/product': '/platform',
},
site: publicSiteUrl,
server: {
host: true,
port: 4321,
},
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
devToolbar: {
enabled: false,
},
// https://docs.astro.build/en/guides/images/#authorizing-remote-images
site: process.env.PUBLIC_SITE_URL ?? 'https://tenantial.com',
image: {
domains: ['images.unsplash.com'],
},
// i18n: {
// defaultLocale: "en",
// locales: ["en", "fr"],
// fallback: {
// fr: "en",
// },
// routing: {
// prefixDefaultLocale: false,
// },
// },
prefetch: true,
integrations: [
sitemap({
filter: page => !isRedirectOnlySitemapPath(page),
i18n: {
defaultLocale: 'en',
locales: {
en: 'en',
},
},
},
}),
starlight({
title: 'Tenantial Docs',
// https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md
// If no Astro and Starlight i18n configurations are provided, the built-in default locale is used in Starlight and a matching Astro i18n configuration is generated/used.
// If only a Starlight i18n configuration is provided, an equivalent Astro i18n configuration is generated/used.
// If only an Astro i18n configuration is provided, the Starlight i18n configuration is updated to match it.
// If both an Astro and Starlight i18n configurations are provided, an error is thrown.
locales: {
root: {
label: 'English',
lang: 'en',
},
},
// https://starlight.astro.build/guides/sidebar/
sidebar: [
{
label: 'Evaluation Guides',
translations: {
de: 'Schnellstartanleitungen',
es: 'Guías de Inicio Rápido',
fa: 'راهنمای شروع سریع',
fr: 'Guides de Démarrage Rapide',
ja: 'クイックスタートガイド',
'zh-cn': '快速入门指南',
},
items: [{ autogenerate: { directory: 'guides' } }],
},
{
label: 'Platform Notes',
items: [{ label: 'Evidence Review', link: 'platform/evidence-review/' }],
},
],
social: [],
disable404Route: true,
customCss: ['./src/assets/styles/starlight.css'],
favicon: '/favicon.ico',
components: {
SiteTitle: './src/components/ui/starlight/SiteTitle.astro',
Head: './src/components/ui/starlight/Head.astro',
MobileMenuFooter:
'./src/components/ui/starlight/MobileMenuFooter.astro',
ThemeSelect: './src/components/ui/starlight/ThemeSelect.astro',
},
head: [
{
tag: 'meta',
attrs: {
property: 'og:image',
content: 'https://tenantial.com' + '/social.webp',
},
},
{
tag: 'meta',
attrs: {
property: 'twitter:image',
content: 'https://tenantial.com' + '/social.webp',
},
},
],
}),
mdx(),
],
experimental: {
clientPrerender: true,
},
vite: {
plugins: [tailwindcss()],
},
});

View File

@ -1,28 +1,44 @@
{
"name": "@tenantatlas/website",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"build": "astro build",
"preview": "astro preview --host 0.0.0.0",
"test": "playwright test",
"test:smoke": "playwright test"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.102",
"astro": "^6.0.0",
"astro-icon": "^1.1.5"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.7.2",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3"
}
"name": "@tenantatlas/website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"start": "astro dev --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"build": "astro check && astro build && node process-html.mjs",
"preview": "astro preview --host 0.0.0.0 --port ${WEBSITE_PORT:-4321}",
"test": "playwright test",
"test:smoke": "playwright test",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.9",
"@astrojs/mdx": "^5.0.6",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"@tailwindcss/vite": "^4.3.0",
"astro": "^6.3.3",
"clipboard": "^2.0.11",
"globby": "^16.2.0",
"gsap": "^3.15.0",
"html-minifier-terser": "^7.2.0",
"lenis": "^1.3.23",
"preline": "^4.2.0",
"rimraf": "^6.1.3",
"sharp": "^0.34.5",
"sharp-ico": "^0.1.5",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"astro-vtbot": "^2.1.12",
"prettier": "^3.8.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.8.0",
"typescript": "^6.0.3"
}
}

View File

@ -1,29 +1,35 @@
import { defineConfig, devices } from '@playwright/test';
const port = Number(process.env.WEBSITE_PORT ?? '4321');
const port = Number(process.env.WEBSITE_PORT ?? 4321);
const baseURL = `http://127.0.0.1:${port}`;
export default defineConfig({
testDir: './tests/smoke',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [['list']],
use: {
baseURL,
trace: 'on-first-retry',
testDir: './tests/smoke',
timeout: 30_000,
expect: {
timeout: 5_000,
},
fullyParallel: true,
retries: process.env.CI ? 1 : 0,
reporter: 'list',
use: {
baseURL,
trace: 'on-first-retry',
},
projects: [
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
webServer: {
command: `WEBSITE_PORT=${port} corepack pnpm dev`,
port,
reuseExistingServer: !process.env.CI,
timeout: 120000,
{
name: 'mobile',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'corepack pnpm preview',
reuseExistingServer: false,
timeout: 120_000,
url: baseURL,
},
});

5867
apps/website/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
import fs from 'node:fs/promises';
import { globby } from 'globby';
import { minify } from 'html-minifier-terser';
// Get all HTML files from the output directory
const path = './dist';
const files = await globby(`${path}/**/*.html`);
await Promise.all(
files.map(async file => {
console.log('Processing file:', file);
let html = await fs.readFile(file, 'utf-8');
// Minify the HTML
html = await minify(html, {
removeComments: true,
preserveLineBreaks: true,
collapseWhitespace: true,
minifyJS: true,
});
await fs.writeFile(file, html);
})
);

View File

@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1002 285">
<g clip-path="url(#a)">
<path fill="#FDE68A" d="M132.693 132.957V65.953h67.003l-67.003 67.004Z"/>
<path fill="#FDE68A" d="M67.003 132.957V65.953h67.004l-67.004 67.004Z"/>
<path fill="#F59E0B" d="M162.91 131.643H67.003v48.61h95.907v-48.61Z"/>
<path fill="#FDE68A" d="M67.004 131.643H0v48.61h67.004v-48.61Z"/>
<path fill="#FACC15" d="M134.007 65.953v67.004H67.003l67.004-67.004Z"/>
<path fill="#FACC15" d="M67.004 65.953v67.004H0l67.004-67.004Zm132.692-1.313v67.004h-67.003l67.003-67.003Z"/>
<path fill="#FDE68A" d="M251.535 52c-28.298 0-51.238 22.94-51.238 51.239 0 28.298 22.94 51.238 51.238 51.238s51.238-22.94 51.238-51.238-22.94-51.238-51.238-51.238Z"/>
<path fill="#FACC15" d="M251.535 82.216c-11.609 0-21.021 9.412-21.021 21.021 0 11.61 9.412 21.021 21.021 21.021s21.021-9.411 21.021-21.021c0-11.61-9.412-21.02-21.021-21.02Zm47.296 72.26V87.473h67.003l-67.003 67.003Z"/>
<path fill="#FDE68A" d="M999.983 42.52c-13.301 0-26.057 5.34-35.462 14.848-9.405 9.507-14.688 22.401-14.688 35.847 0 13.445 5.283 26.339 14.688 35.847 9.405 9.507 22.161 14.848 35.462 14.848v-23.535a26.726 26.726 0 0 1-18.999-7.955c-5.039-5.094-7.87-12.002-7.87-19.205 0-7.204 2.831-14.112 7.87-19.206a26.724 26.724 0 0 1 18.999-7.955V42.52Z"/>
<path fill="#FACC15" d="M999.982 0c-24.577 0-48.147 9.82-65.526 27.302-17.379 17.48-27.142 41.19-27.142 65.912 0 24.721 9.763 48.431 27.142 65.912 17.379 17.48 40.949 27.301 65.526 27.301v-43.274a49.5 49.5 0 0 1-35.106-14.627c-9.31-9.365-14.541-22.068-14.541-35.312 0-13.245 5.231-25.948 14.541-35.313a49.502 49.502 0 0 1 35.106-14.627V0Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.475 162.988c-33.116 0-59.962 26.846-59.962 59.962s26.846 59.962 59.962 59.962 59.962-26.846 59.962-59.962-26.846-59.962-59.962-59.962Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.476 173.89c-27.095 0-49.06 21.965-49.06 49.06 0 27.095 21.965 49.06 49.06 49.06 27.095 0 49.06-21.965 49.06-49.06 0-27.095-21.965-49.06-49.06-49.06Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.475 183.702c-21.676 0-39.248 17.572-39.248 39.247 0 21.676 17.572 39.248 39.248 39.248s39.248-17.572 39.248-39.248c0-21.675-17.572-39.247-39.248-39.247Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.476 194.604c-15.655 0-28.346 12.69-28.346 28.345s12.691 28.346 28.346 28.346 28.346-12.691 28.346-28.346-12.691-28.345-28.346-28.345Z"/>
<path stroke="#F59E0B" stroke-width="3.271" d="M939.475 205.506c-9.634 0-17.444 7.809-17.444 17.443s7.81 17.444 17.444 17.444 17.443-7.81 17.443-17.444-7.809-17.443-17.443-17.443Z"/>
<path stroke="#FDE68A" stroke-width="3.271" d="M939.476 215.317a7.632 7.632 0 1 0 0 15.264 7.632 7.632 0 0 0 0-15.264Z"/>
<path fill="#F59E0B" d="M862.615 180.977h55.601v-40.338h-55.601v40.338Z"/>
<path fill="#FACC15" d="M862.615 235.488v-55.601h55.601l-55.601 55.601Z"/>
<path stroke="#FACC15" stroke-width="2.18" d="m778.311 199.75 55.165-89.232 55.166 89.232H778.311Z"/>
<path fill="#FACC15" d="m790.958 91 57.122 91.578H733.837L790.958 91Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h1002v285H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" fill="none">
<rect width="96" height="96" rx="22" fill="#111516" />
<rect x="14" y="14" width="20" height="20" rx="5.5" fill="#FFF7E1" />
<path
d="M40 84H23C18.0294 84 14 79.9706 14 75V56.5C14 25.2959 39.2959 0 70.5 0H84C89.5229 0 94 4.47715 94 10V21C94 26.5228 89.5229 31 84 31H70.5C56.4167 31 45 42.4167 45 56.5V79C45 81.7614 42.7614 84 40 84Z"
fill="#FFF7E1"
/>
<circle cx="78" cy="72" r="12" fill="#FFF7E1" />
</svg>

Before

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@ -1,46 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 420" fill="none">
<defs>
<linearGradient id="rowFade" x1="0" x2="1600" y1="0" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFF7E1" stop-opacity="0" />
<stop offset="0.18" stop-color="#FFF7E1" stop-opacity="0.18" />
<stop offset="0.5" stop-color="#FFF7E1" stop-opacity="0.46" />
<stop offset="0.82" stop-color="#FFF7E1" stop-opacity="0.2" />
<stop offset="1" stop-color="#FFF7E1" stop-opacity="0" />
</linearGradient>
<linearGradient id="mintFade" x1="0" x2="1600" y1="0" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#6FE5BF" stop-opacity="0" />
<stop offset="0.42" stop-color="#6FE5BF" stop-opacity="0.2" />
<stop offset="0.7" stop-color="#6FE5BF" stop-opacity="0.08" />
<stop offset="1" stop-color="#6FE5BF" stop-opacity="0" />
</linearGradient>
<mask id="surfaceFade">
<rect width="1600" height="420" fill="url(#maskGradient)" />
</mask>
<linearGradient id="maskGradient" x1="800" x2="800" y1="0" y2="420" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0" />
<stop offset="0.2" stop-color="white" stop-opacity="0.45" />
<stop offset="0.62" stop-color="white" stop-opacity="1" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</linearGradient>
</defs>
<g mask="url(#surfaceFade)" stroke-linecap="round" stroke-width="3.8" stroke-dasharray="0.1 23">
<path stroke="url(#rowFade)" d="M-80 52 C160 10 300 86 520 42 C770 -8 910 106 1160 58 C1360 20 1490 42 1680 8" />
<path stroke="url(#rowFade)" d="M-80 77 C170 31 308 113 532 68 C774 19 930 132 1170 82 C1378 39 1518 71 1680 33" />
<path stroke="url(#rowFade)" d="M-80 103 C174 58 330 136 548 93 C786 46 946 159 1190 107 C1406 60 1516 100 1680 61" />
<path stroke="url(#rowFade)" d="M-80 130 C180 85 344 158 572 120 C804 81 954 182 1204 132 C1414 90 1520 126 1680 92" />
<path stroke="url(#rowFade)" d="M-80 158 C194 116 362 180 590 150 C826 119 972 207 1224 162 C1426 126 1538 154 1680 126" />
<path stroke="url(#rowFade)" d="M-80 188 C214 149 382 206 620 182 C850 158 1000 234 1240 196 C1444 164 1550 188 1680 164" />
<path stroke="url(#rowFade)" d="M-80 220 C230 184 410 236 648 218 C884 198 1028 266 1270 232 C1468 204 1560 224 1680 204" />
<path stroke="url(#rowFade)" d="M-80 254 C252 222 436 270 682 256 C918 242 1054 298 1306 270 C1494 250 1574 266 1680 248" />
<path stroke="url(#rowFade)" d="M-80 290 C278 264 472 306 724 296 C960 286 1094 332 1344 310 C1512 296 1586 306 1680 294" />
<path stroke="url(#rowFade)" d="M-80 328 C304 306 514 346 772 336 C1006 328 1136 366 1390 350 C1536 340 1600 350 1680 340" />
<path stroke="url(#mintFade)" d="M-80 198 C250 160 450 214 710 190 C960 168 1110 240 1370 204 C1510 184 1594 194 1680 184" />
<path stroke="url(#mintFade)" d="M-80 250 C270 222 500 260 768 242 C1030 224 1180 284 1426 262 C1548 252 1610 260 1680 252" />
</g>
<g mask="url(#surfaceFade)" stroke="url(#rowFade)" stroke-linecap="round" stroke-width="1.2" opacity="0.3">
<path d="M-80 130 C180 85 344 158 572 120 C804 81 954 182 1204 132 C1414 90 1520 126 1680 92" />
<path d="M-80 188 C214 149 382 206 620 182 C850 158 1000 234 1240 196 C1444 164 1550 188 1680 164" />
<path d="M-80 254 C252 222 436 270 682 256 C918 242 1054 298 1306 270 C1494 250 1574 266 1680 248" />
<path d="M-80 328 C304 306 514 346 772 336 C1006 328 1136 366 1390 350 C1536 340 1600 350 1680 340" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,3 +0,0 @@
User-agent: *
Allow: /
Sitemap: /sitemap.xml

View File

@ -0,0 +1,9 @@
import '@styles/lenis.css';
import Lenis from 'lenis';
// Script to handle Lenis library settings for smooth scrolling
// https://github.com/darkroomengineering/lenis
const lenis = new Lenis({
autoRaf: true,
});

View File

@ -0,0 +1,127 @@
@import 'tailwindcss';
/* Preline UI */
@source '../../../node_modules/preline/dist/*.js';
@import '../../../node_modules/preline/variants.css';
/* Plugins */
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:is(.dark *));
@theme {
/* https://tailwindcss.com/docs/colors#customizing-your-colors */
--color-*: initial;
--color-transparent: transparent;
--color-current: currentColor;
--color-black: #000;
--color-white: #fff;
--color-gray-50: oklch(0.985 0.002 247.839);
--color-gray-100: oklch(0.967 0.003 264.542);
--color-gray-200: oklch(0.928 0.006 264.531);
--color-gray-300: oklch(0.872 0.01 258.338);
--color-gray-400: oklch(0.707 0.022 261.325);
--color-gray-500: oklch(0.551 0.027 264.364);
--color-gray-600: oklch(0.446 0.03 256.802);
--color-gray-700: oklch(0.373 0.034 259.733);
--color-gray-800: oklch(0.278 0.033 256.848);
--color-gray-900: oklch(0.21 0.034 264.665);
--color-gray-950: oklch(0.13 0.028 261.692);
--color-indigo-50: oklch(0.962 0.018 272.314);
--color-indigo-100: oklch(0.93 0.034 272.788);
--color-indigo-200: oklch(0.87 0.065 274.039);
--color-indigo-300: oklch(0.785 0.115 274.713);
--color-indigo-400: oklch(0.673 0.182 276.935);
--color-indigo-500: oklch(0.585 0.233 277.117);
--color-indigo-600: oklch(0.511 0.262 276.966);
--color-indigo-700: oklch(0.457 0.24 277.023);
--color-indigo-800: oklch(0.398 0.195 277.366);
--color-indigo-900: oklch(0.359 0.144 278.697);
--color-indigo-950: oklch(0.257 0.09 281.288);
--color-neutral-50: oklch(0.985 0 0);
--color-neutral-100: oklch(0.97 0 0);
--color-neutral-200: oklch(0.922 0 0);
--color-neutral-300: oklch(0.87 0 0);
--color-neutral-400: oklch(0.708 0 0);
--color-neutral-500: oklch(0.556 0 0);
--color-neutral-600: oklch(0.439 0 0);
--color-neutral-700: oklch(0.371 0 0);
--color-neutral-800: oklch(0.269 0 0);
--color-neutral-900: oklch(0.205 0 0);
--color-neutral-950: oklch(0.145 0 0);
--color-yellow-50: oklch(0.987 0.026 102.212);
--color-yellow-100: oklch(0.973 0.071 103.193);
--color-yellow-200: oklch(0.945 0.129 101.54);
--color-yellow-300: oklch(0.905 0.182 98.111);
--color-yellow-400: oklch(0.852 0.199 91.936);
--color-yellow-500: oklch(0.795 0.184 86.047);
--color-yellow-600: oklch(0.681 0.162 75.834);
--color-yellow-700: oklch(0.554 0.135 66.442);
--color-yellow-800: oklch(0.476 0.114 61.907);
--color-yellow-900: oklch(0.421 0.095 57.708);
--color-yellow-950: oklch(0.286 0.066 53.813);
--color-orange-50: oklch(0.98 0.016 73.684);
--color-orange-100: oklch(0.954 0.038 75.164);
--color-orange-200: oklch(0.901 0.076 70.697);
--color-orange-300: oklch(70.72% 0.182 40.56);
--color-orange-400: oklch(67.4% 0.2072 39.23);
--color-orange-500: oklch(61.86% 0.1946 38.88);
--color-orange-600: oklch(0.646 0.222 41.116);
--color-orange-700: oklch(0.553 0.195 38.402);
--color-orange-800: oklch(0.47 0.157 37.304);
--color-orange-900: oklch(0.408 0.123 38.172);
--color-orange-950: oklch(0.266 0.079 36.259);
--color-red-50: oklch(0.971 0.013 17.38);
--color-red-100: oklch(0.936 0.032 17.717);
--color-red-200: oklch(0.885 0.062 18.334);
--color-red-300: oklch(0.808 0.114 19.571);
--color-red-400: oklch(0.704 0.191 22.216);
--color-red-500: oklch(0.637 0.237 25.331);
--color-red-600: oklch(0.577 0.245 27.325);
--color-red-700: oklch(0.505 0.213 27.518);
--color-red-800: oklch(0.444 0.177 26.899);
--color-red-900: oklch(0.396 0.141 25.723);
--color-red-950: oklch(0.258 0.092 26.042);
--color-zinc-50: oklch(0.985 0 0);
--color-zinc-100: oklch(0.967 0.001 286.375);
--color-zinc-200: oklch(0.92 0.004 286.32);
--color-zinc-300: oklch(0.871 0.006 286.286);
--color-zinc-400: oklch(0.705 0.015 286.067);
--color-zinc-500: oklch(0.552 0.016 285.938);
--color-zinc-600: oklch(0.442 0.017 285.786);
--color-zinc-700: oklch(0.37 0.013 285.805);
--color-zinc-800: oklch(0.274 0.006 286.033);
--color-zinc-900: oklch(0.21 0.006 285.885);
--color-zinc-950: oklch(0.141 0.005 285.823);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}

View File

@ -0,0 +1,22 @@
html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}

View File

@ -0,0 +1,191 @@
@import 'tailwindcss';
@layer base {
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
/* Dark mode colors. */
:root {
--border: hsla(var(--border-neutral), 0.4);
--backdrop-color: #272727cc;
--sl-color-accent: #ff801f;
--sl-color-accent-high: #ffa057;
--sl-color-accent-low: #562800;
--sl-color-black: #181818;
--sl-color-gray-1: #eee;
--sl-color-gray-2: #c2c2c2;
--sl-color-gray-3: #8b8b8b;
--sl-color-gray-4: #585858;
--sl-color-gray-5: #383838;
--sl-color-gray-6: #272727;
--sl-color-white: #fff;
--list-marker-color: #fb923c;
--border-neutral: 0, 0%, 25.1%;
}
/* Light mode colors. */
:root[data-theme='light'] {
--border: hsla(var(--border-yellow), 0.4);
--backdrop-color: #f6f6f699;
--sl-color-accent: #b73d00;
--sl-color-accent-high: #562800;
--sl-color-accent-low: #ffa057;
--sl-color-black: #fff;
--sl-color-gray-1: #272727;
--sl-color-gray-2: #383838;
--sl-color-gray-3: #585858;
--sl-color-gray-4: #8b8b8b;
--sl-color-gray-5: #c2c2c2;
--sl-color-gray-6: #eee;
--sl-color-gray-7: #f6f6f6;
--sl-color-white: #181818;
--list-marker-color: #fb923c;
--border-yellow: 54.9, 96.7%, 88%;
}
header {
border: none !important;
padding: 0 !important;
}
header.header {
background-color: transparent !important;
height: 4.5rem !important;
margin-inline: auto !important;
padding-block: 0 !important;
padding-inline: 2rem !important;
}
header > div:first-of-type {
backdrop-filter: blur(12px) !important;
background-color: var(--backdrop-color) !important;
border: 1px var(--border) solid;
border-radius: 36px;
height: 100% !important;
margin-inline: auto !important;
margin-top: 1rem !important;
max-width: 1536px;
padding-inline: 2rem !important;
width: auto !important;
}
#starlight__sidebar {
border-radius: 1rem;
margin-top: 2rem !important;
}
.content-panel:first-of-type {
margin-top: 2rem !important;
}
.right-sidebar {
top: 2rem !important;
}
#starlight__on-this-page--mobile {
border: none !important;
}
mobile-starlight-toc > nav {
border: none !important;
border-radius: 1rem;
margin-top: 2rem !important;
}
select {
background-image: none;
box-shadow: none;
}
select:focus-visible {
outline: -webkit-focus-ring-color auto 1px;
}
article.card {
border-radius: 0.5rem;
}
.pagination-links a:hover {
border-color: var(--sl-color-accent);
}
.sl-link-card:hover {
border-color: var(--sl-color-gray-4) !important;
}
.starlight-aside--tip {
background: linear-gradient(45deg, #ff512f, #f09819);
border: none;
border-radius: 0.5rem;
color: #66350c;
}
.starlight-aside--note {
background: linear-gradient(45deg, #00b4db, #2193b0);
border: none;
border-radius: 0.5rem;
color: #004558;
}
.starlight-aside__icon {
transform: scale(0.8);
}
.starlight-aside--tip .starlight-aside__title {
color: #ffe0c2;
}
.starlight-aside--note .starlight-aside__title {
color: #bbf3fef7;
}
.sl-markdown-content ul:not(:where(.not-content *)) {
list-style-type: none;
padding-left: 0;
}
.sl-markdown-content ul:not(:where(.not-content *)) > li {
padding-left: 1.75rem;
position: relative;
}
.sl-markdown-content li:not(:where(.not-content *)) > ul,
.sl-markdown-content li + li:not(:where(.not-content *)) {
margin-top: 0.625rem;
}
.sl-markdown-content ul:not(:where(.not-content *)) > li:before {
background: var(--list-marker-color);
border-radius: 1px;
content: '';
height: 2px;
left: 2px;
position: absolute;
top: 13px;
width: 0.875rem;
}
@media screen and (max-width: 800px) {
mobile-starlight-toc > nav {
border-radius: 1rem;
margin-top: 3rem !important;
}
header > div:first-of-type {
padding-inline-end: 5rem !important;
}
starlight-menu-button > button {
right: 3rem !important;
top: 2.2rem !important;
}
}
@media screen and (max-width: 1280px) {
header.header {
padding-inline: 1.5rem !important;
}
}

View File

@ -0,0 +1,100 @@
@import 'tailwindcss';
@layer base {
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
/* Dark mode colors. */
:root {
--primary-button-hover: #ff801f;
--backdrop-color: #272727cc;
--sl-color-accent: #ff801f;
--sl-color-accent-high: #ffa057;
--sl-color-accent-low: #562800;
--sl-color-black: #181818;
--sl-color-gray-1: #eee;
--sl-color-gray-2: #c2c2c2;
--sl-color-gray-3: #8b8b8b;
--sl-color-gray-4: #585858;
--sl-color-gray-5: #383838;
--sl-color-gray-6: #272727;
--sl-color-white: #fff;
--yellow-hsl: 43.3, 96.4%, 56.3%;
--overlay-yellow: hsla(var(--yellow-hsl), 0.2);
--border: hsla(var(--border-neutral), 0.4);
--border-neutral: 0, 0%, 25.1%;
}
/* Light mode colors. */
:root[data-theme='light'] {
--primary-button-hover: #ff801f;
--backdrop-color: #f6f6f699;
--sl-color-accent: #f76b15;
--sl-color-accent-high: #562800;
--sl-color-accent-low: #ffa057;
--sl-color-black: #fff;
--sl-color-gray-1: #272727;
--sl-color-gray-2: #383838;
--sl-color-gray-3: #585858;
--sl-color-gray-4: #8b8b8b;
--sl-color-gray-5: #c2c2c2;
--sl-color-gray-6: #eee;
--sl-color-gray-7: #f6f6f6;
--sl-color-white: #181818;
--yellow-hsl: 47.9, 95.8%, 53.1%;
--border-yellow: 54.9, 96.7%, 88%;
--border: hsla(var(--border-yellow), 0.4);
}
.page {
background:
linear-gradient(215deg, var(--overlay-yellow), transparent 40%),
radial-gradient(var(--overlay-yellow), transparent 40%) no-repeat center
center / cover,
radial-gradient(var(--overlay-yellow), transparent 65%) no-repeat center
center / cover;
background-blend-mode: overlay;
}
header {
border: none !important;
padding: 0 !important;
}
header.header {
background-color: transparent !important;
height: 4.5rem !important;
margin-inline: auto !important;
padding-block: 0 !important;
padding-inline: 2rem !important;
}
header > div:first-of-type {
backdrop-filter: blur(12px) !important;
background-color: var(--backdrop-color) !important;
border: 1px var(--border) solid;
border-radius: 36px;
height: 100% !important;
margin-inline: auto !important;
margin-top: 1rem !important;
max-width: 1536px;
padding-inline: 2rem !important;
width: auto !important;
}
select {
background-image: none;
box-shadow: none;
}
.sl-link-button.primary:hover {
background-color: var(--primary-button-hover);
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.sl-link-button.primary:hover svg {
transform: translateX(0.25rem);
transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@ -0,0 +1,13 @@
<span
{...Astro.props}
class:list={[
'inline-flex items-center gap-2 text-neutral-800 dark:text-neutral-100',
Astro.props.class,
]}
>
<span
class="inline-grid size-9 place-items-center rounded-xl bg-neutral-800 text-sm font-black text-yellow-400 dark:bg-yellow-400 dark:text-neutral-900"
>T</span
>
<span class="text-xl font-bold tracking-tight">Tenantial</span>
</span>

View File

@ -0,0 +1,138 @@
---
import { getImage } from 'astro:assets';
import { OG, SEO, SITE } from '@data/constants';
import faviconSvgSrc from '@images/icon.svg';
import faviconSrc from '@images/icon.png';
// Default properties for the Meta component. These values are used if props are not provided.
// 'meta' sets a default description meta tag to describe the page content.
// 'structuredData' defines default structured data in JSON-LD format to enhance search engine understanding of the page (for SEO purposes).
const defaultProps = {
meta: SITE.description,
structuredData: SEO.structuredData,
customDescription: null,
customOgTitle: null,
};
// Extract props with default values assigned from defaultProps. Values can be overridden when the component is used.
// For example:
// <MainLayout title="Custom Title" meta="Custom description." />
const {
meta = defaultProps.meta,
structuredData = defaultProps.structuredData,
customDescription = defaultProps.customDescription,
customOgTitle = defaultProps.customOgTitle,
} = Astro.props;
// Use custom description if provided, otherwise use default meta
const description = customDescription || meta;
// Use custom OG title if provided, otherwise use default OG title
const ogTitle = customOgTitle || OG.title;
const ogDescription = customDescription || OG.description;
// Define the metadata for your website and individual pages
const siteURL = `${Astro.site}`; // Set the website URL in astro.config.mjs
const author = SITE.author;
const canonical = new URL(Astro.url.pathname, Astro.site || Astro.url.origin)
.href;
const basePath = Astro.url.pathname;
const socialImageRes = await getImage({
src: OG.image,
width: 1200,
height: 600,
});
const socialImage = Astro.url.origin + socialImageRes.src; // Get the full URL of the image (https://stackoverflow.com/a/9858694)
const languages: { [key: string]: string } = {
en: '',
};
function createHref(lang: string, prefix: string, path: string): string {
// Remove any existing language prefix
const cleanPath = path.replace(/^\/(en)\//, '/');
// Add the correct language prefix if needed
const basePath = prefix ? `/${prefix}${cleanPath}` : cleanPath;
const normalizedBasePath = basePath.replace(/\/\/+/g, '/');
return `${siteURL.slice(0, -1)}${normalizedBasePath}`;
}
const fullPath: string = Astro.url.pathname;
// Generate and optimize the favicon images
const faviconSvg = await getImage({
src: faviconSvgSrc,
format: 'svg',
});
const appleTouchIcon = await getImage({
src: faviconSrc,
width: 180,
height: 180,
format: 'png',
});
---
{
/* Inject structured data into the page if provided. This data is formatted as JSON-LD, a method recommended by Google for structured data pass:
https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data */
}{
structuredData && (
<script
type="application/ld+json"
set:html={JSON.stringify(structuredData)}
/>
)
}
{/* Define the character set, description, author, and viewport settings */}
<meta charset="utf-8" />
<meta content={description} name="description" />
<meta name="web_author" content={author} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="canonical" href={canonical} />
{
Object.entries(languages).map(([lang, prefix]) => {
const cleanPath = fullPath.replace(/^\/(en)\//, '/');
const href = createHref(lang, prefix, cleanPath);
return <link rel="alternate" hreflang={lang} href={href} />;
})
}
{/* Facebook Meta Tags */}
<meta
property="og:locale"
content="en_US"
/>
<meta property="og:url" content={siteURL} />
<meta property="og:type" content="website" />
<meta property="og:title" content={ogTitle} />
<meta property="og:site_name" content={SITE.title} />
<meta property="og:description" content={ogDescription} />
<meta property="og:image" content={socialImage} />
<meta content="1200" property="og:image:width" />
<meta content="600" property="og:image:height" />
<meta content="image/png" property="og:image:type" />
{/* Twitter Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content={siteURL} />
<meta property="twitter:url" content={siteURL} />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={ogDescription} />
<meta name="twitter:image" content={socialImage} />
{/* Links to the webmanifest and sitemap */}
<link rel="manifest" href="/manifest.json" />
{/* https://docs.astro.build/en/guides/integrations-guide/sitemap/ */}
<link rel="sitemap" href="/sitemap-index.xml" />
{/* Links for favicons */}
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
<meta name="mobile-web-app-capable" content="yes" />
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
<link href={appleTouchIcon.src} rel="shortcut icon" />
{/* Set theme color */}
<meta name="theme-color" content="#facc15" />

View File

@ -0,0 +1,59 @@
{/* Dark Theme Toggle Button */}
{
/* This button is shown when the light theme is active, and when clicked, it switches the theme to dark */
}
<button
type="button"
aria-label="Dark Theme Toggle"
title="Toggle theme"
class="hs-dark-mode group hs-dark-mode-active:hidden flex h-8 w-8 items-center justify-center rounded-full font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:bg-neutral-200 hover:text-orange-400 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-orange-300 dark:focus:outline-hidden"
data-hs-theme-click-value="dark"
>
{
/* The SVG displayed shows an abstract icon that represents the moon (dark theme) */
}
<svg
class="size-4 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></svg
>
{/* Light Theme Toggle Button */}
{
/* This button is hidden by default and only appears when the dark theme is active, when clicked, it switches to the light theme */
}
</button>
<button
type="button"
aria-label="Light Theme Toggle"
title="Toggle theme"
class="hs-dark-mode group hs-dark-mode-active:flex hidden h-8 w-8 items-center justify-center rounded-full font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-orange-400 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-orange-300 dark:focus:outline-hidden"
data-hs-theme-click-value="light"
>
{
/* The SVG displayed shows a standard sun icon that stands for the light theme */
}
<svg
class="size-4.5 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="12" r="4"></circle><path d="M12 8a2 2 0 1 0 4 4"
></path><path d="M12 2v2"></path><path d="M12 20v2"></path><path
d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"
></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path
d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"
></path></svg
>
</button>

View File

@ -1,38 +0,0 @@
---
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import Card from '@/components/primitives/Card.astro';
import type { AudienceRowContent } from '@/types/site';
interface Props {
item: AudienceRowContent;
}
const { item } = Astro.props;
---
<Card class="h-full">
<Eyebrow>{item.audience}</Eyebrow>
<Headline as="h3" size="card" class="mt-4">
{item.title}
</Headline>
<Lead class="mt-3" size="body">
{item.description}
</Lead>
<ul class="mt-5 space-y-3 p-0">
{
item.bullets.map((bullet) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-border-subtle)] bg-white/70 px-4 py-3 text-sm text-[var(--color-ink-800)]">
{bullet}
</li>
))
}
</ul>
{item.cta && (
<div class="mt-6">
<SecondaryCTA cta={item.cta} />
</div>
)}
</Card>

View File

@ -1,27 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { CalloutContent } from '@/types/site';
interface Props {
content: CalloutContent;
}
const { content } = Astro.props;
const variant = content.tone === 'accent' ? 'accent' : content.tone === 'subtle' ? 'subtle' : 'default';
const barTone = content.tone === 'accent' ? undefined : content.tone === 'subtle' ? 'trust' : undefined;
---
<Card variant={variant} hoverable>
<div class="callout-bar" data-bar-tone={barTone}>
{content.eyebrow && <Eyebrow>{content.eyebrow}</Eyebrow>}
<Headline as="h3" size="card" class="mt-4">
{content.title}
</Headline>
<Lead class="mt-3" size="body">
{content.description}
</Lead>
</div>
</Card>

View File

@ -1,34 +0,0 @@
---
import Button from '@/components/primitives/Button.astro';
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
points: string[];
title: string;
}
const { cta, points, title } = Astro.props;
---
<Card variant="accent">
<Eyebrow>Qualified outreach</Eyebrow>
<Headline as="h3" size="card" class="mt-4 text-3xl">
{title}
</Headline>
<ul class="mt-5 space-y-3 p-0">
{
points.map((point) => (
<li class="list-none rounded-[1rem] bg-white/72 px-4 py-3 text-sm text-[var(--color-ink-800)]">
{point}
</li>
))
}
</ul>
<div class="mt-6">
<Button href={cta.href} variant={cta.variant ?? 'primary'}>{cta.label}</Button>
</div>
</Card>

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
interface Props {
description: string;
title: string;
}
const { description, title } = Astro.props;
---
<Card>
<Eyebrow>Conversation focus</Eyebrow>
<Headline as="h3" size="card" class="mt-4">
{title}
</Headline>
<Lead class="mt-3" size="body">
{description}
</Lead>
</Card>

View File

@ -1,23 +0,0 @@
---
interface Props {
class?: string;
tone?: 'accent' | 'neutral' | 'signal';
}
const { class: className = '', tone = 'accent' } = Astro.props;
const toneClasses = {
accent: 'text-[var(--color-brand)]',
neutral: 'text-[var(--color-muted-foreground)]',
signal: 'text-[var(--color-signal)]',
};
---
<p
class:list={[
'm-0 text-[var(--type-eyebrow-size)] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
toneClasses[tone],
className,
]}
>
<slot />
</p>

View File

@ -1,66 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { FeatureItemContent } from '@/types/site';
interface Props {
item: FeatureItemContent;
}
const { item } = Astro.props;
const lucideMap: Record<string, string> = {
'shield': 'lucide:shield',
'database': 'lucide:database',
'refresh': 'lucide:refresh-cw',
'eye': 'lucide:eye',
'file-check': 'lucide:file-check',
'layers': 'lucide:layers',
'search': 'lucide:search',
'lock': 'lucide:lock',
'zap': 'lucide:zap',
'clipboard': 'lucide:clipboard-list',
'git-branch': 'lucide:git-branch',
'bar-chart': 'lucide:bar-chart-3',
'activity': 'lucide:activity',
'settings': 'lucide:settings',
'globe': 'lucide:globe',
'users': 'lucide:users',
'check-circle': 'lucide:check-circle',
'archive': 'lucide:archive',
'trending-up': 'lucide:trending-up',
'cpu': 'lucide:cpu',
};
const iconName = item.icon ? lucideMap[item.icon] : undefined;
---
<Card class="h-full" hoverable>
<div class="space-y-3">
{iconName && (
<div class="feature-icon">
<Icon name={iconName} size={20} />
</div>
)}
{item.eyebrow && <Eyebrow>{item.eyebrow}</Eyebrow>}
<Headline as="h3" size="card">
{item.title}
</Headline>
<Lead size="body">
{item.description}
</Lead>
{(item.meta || item.href) && (
<div class="mt-2 flex flex-wrap items-center gap-3 text-sm">
{item.meta && <span class="text-[var(--color-brand)]">{item.meta}</span>}
{item.href && (
<a class="text-link font-semibold" href={item.href}>
Learn more
</a>
)}
</div>
)}
</div>
</Card>

View File

@ -1,22 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
size?: 'card' | 'display' | 'page' | 'section';
}
const { as = 'h2', class: className = '', size = 'section' } = Astro.props;
const Tag = as;
const sizeClasses = {
display:
'font-[var(--font-display)] font-bold text-[length:var(--type-display-size)] leading-[var(--line-display)] tracking-[var(--tracking-display)]',
page: 'font-[var(--font-display)] font-semibold text-[length:var(--type-page-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
section:
'font-[var(--font-display)] font-semibold text-[length:var(--type-section-size)] leading-[var(--line-heading)] tracking-[var(--tracking-tight)]',
card: 'font-medium text-[length:var(--type-card-size)] leading-[1.18] tracking-[var(--tracking-tight)]',
};
---
<Tag class:list={['m-0 text-[var(--color-foreground)] [&>.accent]:text-[var(--color-primary)]', sizeClasses[size], className]}>
<slot />
</Tag>

View File

@ -1,26 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { IntegrationEntry } from '@/types/site';
interface Props {
item: IntegrationEntry;
}
const { item } = Astro.props;
---
<Card class="h-full px-4 py-4" variant="subtle">
<div class="flex items-center gap-3">
<Badge tone="neutral">{item.category}</Badge>
<Headline as="h3" size="card" class="text-base">
{item.name}
</Headline>
</div>
<Lead class="mt-3 max-w-72" size="small">
{item.summary}
</Lead>
{item.note && <p class="mt-2 text-xs font-medium uppercase tracking-[0.14em] text-[var(--color-brand)]">{item.note}</p>}
</Card>

View File

@ -1,19 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
size?: 'body' | 'lead' | 'small';
}
const { as = 'p', class: className = '', size = 'lead' } = Astro.props;
const Tag = as;
const sizeClasses = {
lead: 'text-[length:var(--type-body-size)] leading-[var(--line-body)] sm:text-lg',
body: 'text-[length:var(--type-body-size)] leading-[var(--line-body)]',
small: 'text-[length:var(--type-small-size)] leading-[1.65]',
};
---
<Tag class:list={['m-0 text-[var(--color-copy)]', sizeClasses[size], className]}>
<slot />
</Tag>

View File

@ -1,22 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Eyebrow from '@/components/content/Eyebrow.astro';
import Lead from '@/components/content/Lead.astro';
import type { MetricItem } from '@/types/site';
interface Props {
item: MetricItem;
}
const { item } = Astro.props;
---
<Card variant="subtle">
<p class="m-0 text-3xl font-semibold tracking-[-0.04em] text-[var(--color-ink-900)]">{item.value}</p>
<Eyebrow class="mt-2">
{item.label}
</Eyebrow>
<Lead class="mt-2" size="small">
{item.description}
</Lead>
</Card>

View File

@ -1,20 +0,0 @@
---
import Button from '@/components/primitives/Button.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
class?: string;
showHelper?: boolean;
size?: 'lg' | 'md' | 'sm';
}
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
---
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="primary">
<Button href={cta.href} variant={cta.variant ?? 'primary'} size={size}>
{cta.label}
</Button>
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
</div>

View File

@ -1,32 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { LegalSection } from '@/types/site';
interface Props {
sections: LegalSection[];
}
const { sections } = Astro.props;
---
<div class="space-y-6">
{
sections.map((section) => (
<Card as="section" class="rounded-[var(--radius-lg)]" variant="subtle">
<Headline as="h2" size="card">
{section.title}
</Headline>
<div class="legal-prose mt-4">
{section.body.map((paragraph) => <Lead size="body">{paragraph}</Lead>)}
{section.bullets && section.bullets.length > 0 && (
<ul>
{section.bullets.map((bullet) => <li>{bullet}</li>)}
</ul>
)}
</div>
</Card>
))
}
</div>

View File

@ -1,20 +0,0 @@
---
import Button from '@/components/primitives/Button.astro';
import type { CtaLink } from '@/types/site';
interface Props {
cta: CtaLink;
class?: string;
showHelper?: boolean;
size?: 'lg' | 'md' | 'sm';
}
const { cta, class: className = '', showHelper = false, size = 'md' } = Astro.props;
---
<div class:list={['flex flex-col gap-2', className]} data-cta-slot="secondary">
<Button href={cta.href} variant={cta.variant ?? 'secondary'} size={size}>
{cta.label}
</Button>
{showHelper && cta.helper && <p class="m-0 text-sm text-[var(--color-copy)]">{cta.helper}</p>}
</div>

View File

@ -1,35 +0,0 @@
---
interface Props {
class?: string;
imageClass?: string;
invert?: boolean;
label?: string;
markClass?: string;
}
const {
class: className = '',
imageClass,
invert = true,
label = 'Tenantial',
markClass = 'h-9 w-9',
} = Astro.props;
const resolvedImageClass = imageClass ?? markClass;
---
<span class:list={['inline-flex items-center', className]} data-brand-lockup>
<img
src="/images/tenantial-logo-transparent-clean.png"
alt={label}
width="3720"
height="920"
decoding="async"
class:list={[
'block shrink-0 object-contain',
resolvedImageClass,
invert ? 'brightness-0 invert' : '',
]}
data-brand-mark
/>
</span>

View File

@ -1,24 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import type { TrustPrincipleContent } from '@/types/site';
interface Props {
item: TrustPrincipleContent;
}
const { item } = Astro.props;
---
<Card class="h-full" hoverable>
<div class="callout-bar" data-bar-tone="trust">
<Headline as="h3" size="card">
{item.title}
</Headline>
<Lead class="mt-3" size="body">
{item.description}
</Lead>
{item.note && <Lead class="mt-4 text-[var(--color-brand)]" size="small">{item.note}</Lead>}
</div>
</Card>

View File

@ -1,95 +0,0 @@
---
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import TenantialLogo from '@/components/content/TenantialLogo.astro';
import Container from '@/components/primitives/Container.astro';
import { getFooterLead, getFooterNavigationGroups, siteMetadata } from '@/lib/site';
interface Props {
currentPath: string;
}
const { currentPath: _currentPath } = Astro.props;
const currentYear = new Date().getFullYear();
const footerLead = getFooterLead(_currentPath);
const footerNavigationGroups = await getFooterNavigationGroups();
const isQuietFooter = _currentPath === '/platform';
---
<footer class="section-divider px-[var(--space-page-x)] pt-10 sm:pt-12" data-footer-intent={isQuietFooter ? 'quiet' : footerLead.intent}>
<Container width="wide">
{isQuietFooter ? (
<div class="grid gap-8 py-3 lg:grid-cols-[0.85fr_1.15fr] lg:items-start">
<div class="space-y-4">
<TenantialLogo imageClass="h-[2.25rem] w-auto" />
<p class="m-0 max-w-sm text-[0.95rem] leading-7 text-[var(--color-copy)]">
Evidence-first governance for Microsoft tenants.
</p>
</div>
<div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-5">
{
footerNavigationGroups.map((group) => (
<div>
<p class="m-0 text-[0.88rem] font-semibold uppercase text-[var(--color-foreground)]">
{group.title}
</p>
<ul class="mt-4 space-y-3 p-0 text-[0.92rem] text-[var(--color-copy)]">
{group.items.map((item) => (
<li class="list-none">
<a class="transition hover:text-[var(--color-brand)]" href={item.href}>
{item.label}
</a>
</li>
))}
</ul>
</div>
))
}
</div>
</div>
) : (
<div class="surface-card-muted grid gap-8 rounded-[var(--radius-panel)] p-6 lg:grid-cols-[1.3fr,1fr] lg:p-8">
<div class="space-y-5">
<p class="m-0 text-sm font-semibold uppercase text-[var(--color-brand)]">
{footerLead.eyebrow}
</p>
<h2 class="m-0 max-w-xl font-[var(--font-display)] text-3xl leading-tight text-[var(--color-foreground)] sm:text-4xl">
{footerLead.title}
</h2>
<p class="m-0 max-w-xl text-base leading-7 text-[var(--color-copy)]">
{footerLead.description}
</p>
<PrimaryCTA cta={footerLead.primaryCta} size="sm" />
</div>
<div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-4">
{
footerNavigationGroups.map((group) => (
<div>
<p class="m-0 text-sm font-semibold uppercase text-[var(--color-foreground)]">
{group.title}
</p>
<ul class="mt-4 space-y-3 p-0 text-sm text-[var(--color-copy)]">
{group.items.map((item) => (
<li class="list-none">
<a class="transition hover:text-[var(--color-brand)]" href={item.href}>
{item.label}
</a>
</li>
))}
</ul>
</div>
))
}
</div>
</div>
)}
<div class="flex flex-col gap-3 py-6 text-[0.9rem] text-[var(--color-copy)] sm:flex-row sm:items-center sm:justify-between">
<p class="m-0">© {currentYear} {siteMetadata.siteName}. Evidence-first governance for Microsoft tenants.</p>
<p class="m-0">
Static public website with sample preview values only.
</p>
</div>
</Container>
</footer>

View File

@ -1,116 +0,0 @@
---
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import TenantialLogo from '@/components/content/TenantialLogo.astro';
import Container from '@/components/primitives/Container.astro';
import { getHeaderCta, getPrimaryNavigation, isActiveNavigationPath, siteMetadata } from '@/lib/site';
interface Props {
currentPath: string;
}
const { currentPath } = Astro.props;
const headerCta = getHeaderCta(currentPath);
const primaryNavigation = await getPrimaryNavigation();
---
<header class="relative z-30 px-[var(--space-page-x)] pt-5 sm:pt-7">
<Container width="wide" class="!max-w-[96rem]">
<div
class="flex items-center justify-between gap-4 py-1"
data-shell-surface="header"
data-visual-tone="dark"
>
<a href="/" class="group flex min-w-0 no-underline" aria-label="Tenantial home">
<TenantialLogo
label={siteMetadata.siteName}
imageClass="h-[2.75rem] w-auto sm:h-12"
/>
</a>
<nav class="hidden items-center gap-9 lg:flex" aria-label="Primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
class:list={[
'text-sm font-medium transition',
isActiveNavigationPath(currentPath, item.href)
? 'text-[var(--color-foreground)]'
: 'text-[var(--color-stone-200)] hover:text-[var(--color-primary)]',
]}
data-nav-link="primary"
>
{item.label}
</a>
))
}
</nav>
<div class="hidden items-center gap-5 lg:flex">
<span
class="text-sm font-medium text-[var(--color-stone-200)]"
data-nav-state="deferred"
title="Sign in is not available from the public website yet."
>
Sign in
</span>
<div data-header-cta>
<PrimaryCTA cta={headerCta} size="sm" />
</div>
</div>
<details class="relative lg:hidden" data-mobile-nav>
<summary
aria-label="Open navigation menu"
class="flex h-11 w-11 cursor-pointer list-none items-center justify-center rounded-full border border-[color:var(--color-line)] bg-[rgba(255,247,225,0.06)] text-[var(--color-foreground)]"
data-mobile-nav-trigger
>
<span class="sr-only">Open navigation menu</span>
<span class="flex flex-col gap-1">
<span class="block h-0.5 w-4 bg-current"></span>
<span class="block h-0.5 w-4 bg-current"></span>
<span class="block h-0.5 w-4 bg-current"></span>
</span>
</summary>
<div
class="glass-panel absolute right-0 top-[calc(100%+0.75rem)] w-[min(18rem,88vw)] rounded-[1.5rem] border border-white/80 p-3"
>
<nav class="flex flex-col gap-1" aria-label="Mobile primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
class:list={[
'rounded-[1rem] px-4 py-3 text-sm',
isActiveNavigationPath(currentPath, item.href)
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
: 'text-[var(--color-muted-foreground)] hover:bg-[var(--surface-muted)] hover:text-[var(--color-foreground)]',
]}
data-nav-link="mobile-primary"
>
<span class="block font-semibold">{item.label}</span>
{item.description && (
<span class="mt-1 block text-xs text-[var(--color-copy)]">
{item.description}
</span>
)}
</a>
))
}
<span
class="rounded-[1rem] px-4 py-3 text-sm font-medium text-[var(--color-muted-foreground)] opacity-75"
data-nav-state="deferred"
>
Sign in
</span>
<div class="mt-2 rounded-[1rem] bg-[var(--surface-accent)] p-3" data-header-cta>
<PrimaryCTA cta={headerCta} size="sm" showHelper />
</div>
</nav>
</div>
</details>
</div>
</Container>
</header>

View File

@ -1,55 +0,0 @@
---
import Footer from '@/components/layout/Footer.astro';
import Navbar from '@/components/layout/Navbar.astro';
import { getPageDefinition } from '@/lib/site';
import { resolveSeo } from '@/lib/seo';
import BaseLayout from '@/layouts/BaseLayout.astro';
interface Props {
currentPath: string;
description?: string;
title?: string;
}
const { currentPath, description, title } = Astro.props;
const seo =
title && description
? resolveSeo({ description, path: currentPath, title })
: undefined;
const pageDefinition = getPageDefinition(currentPath);
---
<BaseLayout
title={title}
description={description}
canonicalUrl={seo?.canonicalUrl}
openGraphTitle={seo?.ogTitle}
openGraphDescription={seo?.ogDescription}
robots={seo?.robots}
>
<div
class="foundation-page site-shell"
data-canonical-path={pageDefinition.canonicalPath}
data-page-family={pageDefinition.family}
data-page-priority={pageDefinition.priority}
data-page-role={pageDefinition.pageRole}
data-shell-tone={pageDefinition.shellTone}
data-surface-group={pageDefinition.surfaceGroup}
data-journey-stage={pageDefinition.journeyStage}
>
<div class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[36rem] bg-[linear-gradient(180deg,rgba(255,247,225,0.018),transparent_74%)]"></div>
{
pageDefinition.pageRole === 'home' && (
<>
<div class="home-ambient-wash" aria-hidden="true"></div>
<div class="home-dot-field" aria-hidden="true"></div>
</>
)
}
<Navbar currentPath={currentPath} />
<main id="content" class="foundation-main pb-20 sm:pb-24">
<slot />
</main>
<Footer currentPath={currentPath} />
</div>
</BaseLayout>

View File

@ -1,26 +0,0 @@
---
interface Props {
class?: string;
tone?: 'accent' | 'neutral' | 'signal' | 'warm';
}
const { class: className = '', tone = 'accent' } = Astro.props;
const toneClasses = {
accent: 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]',
neutral: 'bg-white/78 text-[var(--color-ink-800)]',
signal: 'bg-[var(--surface-trust)] text-[var(--color-signal)]',
warm: 'bg-[rgba(175,109,67,0.14)] text-[var(--color-warm)]',
};
---
<span
class:list={[
'inline-flex w-fit items-center rounded-[var(--radius-pill)] px-3 py-1 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)]',
toneClasses[tone],
className,
]}
data-badge-tone={tone}
>
<slot />
</span>

View File

@ -1,72 +0,0 @@
---
import type { ButtonVariant } from '@/types/site';
interface Props {
ariaLabel?: string;
class?: string;
href?: string;
rel?: string;
size?: 'lg' | 'md' | 'sm';
target?: '_blank' | '_self';
type?: 'button' | 'reset' | 'submit';
variant?: ButtonVariant;
}
const {
ariaLabel,
class: className = '',
href,
rel,
size = 'md',
target,
type = 'button',
variant = 'primary',
} = Astro.props;
const baseClass =
'inline-flex items-center justify-center rounded-[var(--radius-pill)] border font-semibold tracking-[var(--tracking-tight)] transition-all duration-200 cursor-pointer';
const sizeClasses = {
sm: 'min-h-10 px-5 text-sm',
md: 'min-h-12 px-6 text-[0.95rem]',
lg: 'min-h-14 px-8 text-base',
};
const variantClasses = {
primary:
'border-transparent bg-[var(--color-primary)] text-[var(--color-primary-foreground)] shadow-[0_2px_14px_rgba(111,229,191,0.2)] hover:bg-[var(--color-mint-300)] hover:shadow-[0_4px_20px_rgba(111,229,191,0.26)] active:scale-[0.98]',
secondary:
'border-[color:var(--color-border)] bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)] hover:border-[var(--color-border-strong)] hover:bg-[var(--surface-muted-strong)] active:scale-[0.98]',
ghost: 'border-transparent bg-transparent text-[var(--color-muted-foreground)] hover:bg-[var(--surface-muted)] hover:text-[var(--color-foreground)]',
};
const classes = [baseClass, sizeClasses[size], variantClasses[variant], className];
---
{
href ? (
<a
href={href}
target={target}
rel={rel}
aria-label={ariaLabel}
class:list={classes}
data-button-variant={variant}
data-cta-weight={variant}
data-interaction="button"
>
<slot />
</a>
) : (
<button
type={type}
aria-label={ariaLabel}
class:list={classes}
data-button-variant={variant}
data-cta-weight={variant}
data-interaction="button"
>
<slot />
</button>
)
}

View File

@ -1,27 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
hoverable?: boolean;
variant?: 'accent' | 'default' | 'subtle';
[key: string]: unknown;
}
const { as = 'article', class: className = '', hoverable = false, variant = 'default', ...rest } = Astro.props;
const variantClasses = {
default: 'surface-card',
accent: 'surface-card-accent',
subtle: 'surface-card-muted',
};
const Tag = as;
---
<Tag
class:list={['rounded-[1.65rem] p-6 sm:p-7', variantClasses[variant], hoverable && 'card-hoverable', className]}
data-surface={variant}
{...rest}
>
<slot />
</Tag>

View File

@ -1,30 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
gap?: 'lg' | 'md' | 'sm';
justify?: 'between' | 'end' | 'start';
}
const {
as = 'div',
class: className = '',
gap = 'md',
justify = 'start',
} = Astro.props;
const Tag = as;
const gapClasses = {
sm: 'gap-[var(--space-cluster-sm)]',
md: 'gap-[var(--space-cluster)]',
lg: 'gap-[var(--space-cluster-lg)]',
};
const justifyClasses = {
start: 'justify-start',
between: 'justify-between',
end: 'justify-end',
};
---
<Tag class:list={['flex flex-wrap items-center', gapClasses[gap], justifyClasses[justify], className]}>
<slot />
</Tag>

View File

@ -1,32 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
width?: 'content' | 'measure' | 'wide';
wide?: boolean;
}
const {
as = 'div',
class: className = '',
width,
wide = false,
} = Astro.props;
const Tag = as;
const resolvedWidth = width ?? (wide ? 'wide' : 'content');
const widthClasses = {
content: 'max-w-[var(--content-max-width)]',
measure: 'max-w-[var(--reading-max-width)]',
wide: 'max-w-[var(--wide-max-width)]',
};
---
<Tag
class:list={[
'mx-auto w-full px-5 sm:px-6 lg:px-8',
widthClasses[resolvedWidth],
className,
]}
>
<slot />
</Tag>

View File

@ -1,23 +0,0 @@
---
interface Props {
class?: string;
cols?: '2' | '3' | '4';
gap?: 'lg' | 'md';
}
const { class: className = '', cols = '3', gap = 'md' } = Astro.props;
const colClasses = {
'2': 'grid-cols-1 md:grid-cols-2',
'3': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-3',
'4': 'grid-cols-1 md:grid-cols-2 xl:grid-cols-4',
};
const gapClasses = {
md: 'gap-[var(--space-grid)] lg:gap-[var(--space-grid-lg)]',
lg: 'gap-6 lg:gap-8',
};
---
<div class:list={['grid', gapClasses[gap], colClasses[cols], className]}>
<slot />
</div>

View File

@ -1,33 +0,0 @@
---
interface Props {
class?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
type?: string;
value?: string;
}
const {
class: className = '',
name,
placeholder,
readonly = false,
type = 'text',
value,
} = Astro.props;
---
<input
type={type}
name={name}
value={value}
placeholder={placeholder}
readonly={readonly}
data-interaction="input"
class:list={[
'min-h-12 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
readonly ? 'cursor-default' : '',
className,
]}
/>

View File

@ -1,47 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
density?: 'base' | 'compact' | 'spacious';
id?: string;
layer?: '1' | '2' | '3';
tone?: 'default' | 'emphasis' | 'muted' | 'tinted' | 'warm';
[key: string]: unknown;
}
const {
as = 'section',
class: className = '',
density = 'base',
id,
layer = '2',
tone = 'default',
...rest
} = Astro.props;
const Tag = as;
const densityClasses = {
compact: 'section-density-compact',
base: 'section-density-base',
spacious: 'section-density-spacious',
};
const toneClasses = {
default: '',
muted: 'section-shell-muted px-3 sm:px-4',
emphasis: 'section-shell-emphasis px-3 sm:px-4',
tinted: 'section-tinted px-3 sm:px-4',
warm: 'section-warm px-3 sm:px-4',
};
---
<Tag
id={id}
data-disclosure-layer={layer}
class:list={[
densityClasses[density],
toneClasses[tone],
className,
]}
{...rest}
>
<slot />
</Tag>

View File

@ -1,36 +0,0 @@
---
import Eyebrow from '@/components/content/Eyebrow.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
interface Props {
align?: 'center' | 'left';
class?: string;
description?: string;
eyebrow?: string;
title: string;
titleHtml?: string;
width?: 'default' | 'measure' | 'wide';
}
const {
align = 'left',
class: className = '',
description,
eyebrow,
title,
titleHtml,
width = 'default',
} = Astro.props;
const widthClasses = {
default: 'max-w-3xl',
measure: 'max-w-[var(--reading-max-width)]',
wide: 'max-w-4xl',
};
---
<div class:list={[widthClasses[width], align === 'center' ? 'mx-auto text-center' : '', className]}>
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
{titleHtml ? <Headline><Fragment set:html={titleHtml} /></Headline> : <Headline>{title}</Headline>}
{description && <Lead class="mt-4">{description}</Lead>}
</div>

View File

@ -1,20 +0,0 @@
---
interface Props {
as?: keyof HTMLElementTagNameMap;
class?: string;
gap?: 'lg' | 'md' | 'sm' | 'xl';
}
const { as = 'div', class: className = '', gap = 'md' } = Astro.props;
const gapClasses = {
sm: 'flex flex-col gap-[var(--space-stack-sm)]',
md: 'flex flex-col gap-[var(--space-stack)]',
lg: 'flex flex-col gap-[var(--space-stack-lg)]',
xl: 'flex flex-col gap-10',
};
const Tag = as;
---
<Tag class:list={[gapClasses[gap], className]}>
<slot />
</Tag>

View File

@ -1,32 +0,0 @@
---
interface Props {
class?: string;
name?: string;
placeholder?: string;
readonly?: boolean;
rows?: number;
value?: string;
}
const {
class: className = '',
name,
placeholder,
readonly = false,
rows = 5,
value,
} = Astro.props;
---
<textarea
name={name}
rows={rows}
placeholder={placeholder}
readonly={readonly}
data-interaction="textarea"
class:list={[
'min-h-32 w-full rounded-[var(--radius-md)] border border-[color:var(--color-border)] bg-[var(--color-input)] px-4 py-3 text-[0.97rem] text-[var(--color-ink-900)] shadow-[var(--shadow-soft)] placeholder:text-[var(--color-copy)]/70',
readonly ? 'cursor-default' : '',
className,
]}
>{value}</textarea>

View File

@ -1,33 +0,0 @@
---
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { CtaLink } from '@/types/site';
interface Props {
description: string;
eyebrow?: string;
primary: CtaLink;
secondary?: CtaLink;
title: string;
}
const { description, eyebrow, primary, secondary, title } = Astro.props;
---
<Section>
<Container width="wide">
<Card variant="accent" class="overflow-hidden" data-cta-section>
<div class="grid gap-6 lg:grid-cols-[1.35fr,0.85fr] lg:items-end">
<SectionHeader eyebrow={eyebrow} title={title} description={description} />
<div class="flex flex-col gap-3 sm:flex-row lg:justify-end">
<PrimaryCTA cta={primary} />
{secondary && <SecondaryCTA cta={secondary} />}
</div>
</div>
</Card>
</Container>
</Section>

View File

@ -1,60 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { CapabilityClusterContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: CapabilityClusterContent[];
title: string;
titleHtml?: string;
}
const { description, eyebrow, items, title, titleHtml } = Astro.props;
---
<Section layer="2" tone="tinted" data-section="capability">
<Container width="wide">
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} titleHtml={titleHtml} description={description} />
<Grid cols="2" gap="lg">
{items.map((cluster) => (
<Card class="h-full" hoverable>
<div class="space-y-4">
<div class="flex items-start justify-between gap-3">
<Headline as="h3" size="card">
{cluster.title}
</Headline>
{cluster.meta && (
<Badge tone="signal">{cluster.meta}</Badge>
)}
</div>
<Lead size="body">
{cluster.description}
</Lead>
<ul class="flex flex-wrap gap-2 p-0">
{cluster.capabilities.map((cap) => (
<li class="list-none rounded-full border border-[color:var(--color-line)] bg-white/60 px-3 py-1.5 text-sm text-[var(--color-ink-800)]">
{cap}
</li>
))}
</ul>
{cluster.href && (
<a class="text-link mt-2 inline-block text-sm font-semibold" href={cluster.href}>
Learn more →
</a>
)}
</div>
</Card>
))}
</Grid>
</div>
</Container>
</Section>

View File

@ -1,30 +0,0 @@
---
import FeatureItem from '@/components/content/FeatureItem.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { FeatureItemContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: FeatureItemContent[];
title: string;
titleHtml?: string;
tone?: 'default' | 'tinted';
}
const { description, eyebrow, items, title, titleHtml, tone = 'default' } = Astro.props;
---
<Section layer="2" tone={tone === 'tinted' ? 'tinted' : 'default'}>
<Container width="wide">
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} titleHtml={titleHtml} description={description} />
<Grid cols="3">
{items.map((item) => <FeatureItem item={item} />)}
</Grid>
</div>
</Container>
</Section>

View File

@ -1,70 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { FeatureItemContent } from '@/types/site';
interface Props {
items: FeatureItemContent[];
}
const { items } = Astro.props;
const lucideMap: Record<string, string> = {
archive: 'lucide:archive',
refresh: 'lucide:refresh-cw',
'git-branch': 'lucide:git-branch',
'file-check': 'lucide:file-check',
clipboard: 'lucide:clipboard-list',
shield: 'lucide:shield-check',
};
---
<Section data-section="feature-pillars">
<Container width="wide">
<div class="grid gap-8 lg:grid-cols-[0.78fr_1.22fr] lg:items-start">
<SectionHeader
eyebrow="Tenantial capabilities"
title="The operating loop buyers need to verify."
description="Each pillar stays tied to evidence, safety, and review context instead of unsupported proof claims."
/>
<div class="grid gap-4 sm:grid-cols-2">
{
items.map((item) => {
const iconName = item.icon ? lucideMap[item.icon] : undefined;
return (
<article
class="rounded-[var(--radius-lg)] border border-[color:var(--color-border)] bg-[var(--color-card)] p-5 shadow-[var(--shadow-soft)]"
data-surface="tenantial-pillar"
>
<div class="flex items-start gap-4">
{iconName && (
<div class="feature-icon" aria-hidden="true">
<Icon name={iconName} size={20} />
</div>
)}
<div class="min-w-0">
<h2 class="m-0 text-lg font-semibold text-[var(--color-foreground)]">
{item.title}
</h2>
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">
{item.description}
</p>
{item.meta && (
<p class="mt-3 text-sm font-semibold text-[var(--color-primary)]">
{item.meta}
</p>
)}
</div>
</div>
</article>
);
})
}
</div>
</div>
</Container>
</Section>

View File

@ -1,44 +0,0 @@
---
import IntegrationBadge from '@/components/content/IntegrationBadge.astro';
import Badge from '@/components/primitives/Badge.astro';
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import type { IntegrationEntry, LogoStripItem } from '@/types/site';
interface Props {
eyebrow?: string;
items: (IntegrationEntry | LogoStripItem)[];
title: string;
}
const { eyebrow, items, title } = Astro.props;
---
<Section class="pt-8 sm:pt-10" density="compact" layer="2">
<Container width="wide">
<div class="surface-card-muted rounded-[var(--radius-lg)] px-5 py-6">
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
<div class="space-y-3">
{eyebrow && <Badge tone="signal">{eyebrow}</Badge>}
<h2 class="m-0 max-w-2xl font-[var(--font-display)] text-3xl leading-tight text-[var(--color-ink-900)]">
{title}
</h2>
</div>
<div class="flex flex-wrap gap-3">
{
items.map((item) => (
<IntegrationBadge
item={{
category: 'category' in item ? item.category : 'Ecosystem',
name: item.label ?? item.name,
note: item.note,
summary: 'summary' in item ? item.summary : `${item.label} aligns with the launch story.`,
}}
/>
))
}
</div>
</div>
</div>
</Container>
</Section>

View File

@ -1,42 +0,0 @@
---
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { OutcomeSectionContent } from '@/types/site';
interface Props {
content: OutcomeSectionContent;
titleHtml?: string;
}
const { content, titleHtml } = Astro.props;
---
<Section layer="2" data-section="outcome">
<Container width="wide">
<div class="space-y-8">
<SectionHeader
eyebrow="Why it matters"
title={content.title}
titleHtml={titleHtml}
description={content.description}
/>
<Grid cols="3">
{content.outcomes.map((outcome) => (
<Card class="h-full" hoverable>
<Headline as="h3" size="card">
{outcome.title}
</Headline>
<Lead class="mt-3" size="body">
{outcome.description}
</Lead>
</Card>
))}
</Grid>
</div>
</Container>
</Section>

View File

@ -1,240 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Cluster from '@/components/primitives/Cluster.astro';
import Container from '@/components/primitives/Container.astro';
import DashboardPreview from '@/components/content/DashboardPreview.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import Metric from '@/components/content/Metric.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import SecondaryCTA from '@/components/content/SecondaryCTA.astro';
import { Icon } from 'astro-icon/components';
import type { HeroContent, MetricItem } from '@/types/site';
interface Props {
calloutDescription?: string;
calloutTitle?: string;
hero: HeroContent;
metrics?: MetricItem[];
}
const { calloutDescription, calloutTitle, hero, metrics = [] } = Astro.props;
const isHomepageHero = Astro.url.pathname === '/';
const heroHeadlineSize = isHomepageHero ? 'page' : 'display';
const heroLeadSize = isHomepageHero ? 'body' : 'lead';
const heroPrimaryAnchor = hero.primaryAnchor ?? 'headline';
const trustSignalIcons = ['lucide:shield-check', 'lucide:lock', 'lucide:check-circle'];
const audienceLabels = ['MSP operators', 'Endpoint teams', 'Security reviewers', 'Audit owners', 'Cloud operations'];
---
<section
class:list={[
isHomepageHero ? 'hero-gradient pt-10 sm:pt-14 lg:pt-12' : 'pt-8 sm:pt-10 lg:pt-14',
]}
data-hero-root
data-hero-surface={isHomepageHero ? 'homepage' : 'page'}
data-homepage-hero={isHomepageHero ? 'true' : undefined}
data-hero-primary-anchor={isHomepageHero && heroPrimaryAnchor === 'composition' ? 'composition' : undefined}
data-section={isHomepageHero ? 'hero' : undefined}
>
<Container width="wide" class={isHomepageHero ? '!max-w-[96rem]' : ''}>
{isHomepageHero ? (
<div class="space-y-8 sm:space-y-10" data-disclosure-layer="1" data-hero-layout>
<div class="grid gap-8 xl:grid-cols-[minmax(24rem,0.68fr)_minmax(42rem,1.32fr)] xl:items-start xl:gap-12">
<div class="motion-rise flex flex-col gap-7 pt-1 sm:gap-8 xl:max-w-[39rem] xl:pt-14" data-hero-panel="text">
<div class="space-y-6" data-hero-anchor-group>
<div data-hero-text-core>
<div data-hero-eyebrow data-hero-segment="eyebrow">
<Badge>{hero.eyebrow}</Badge>
</div>
<div class="mt-3 space-y-4 sm:mt-5 sm:space-y-5">
<div
data-hero-heading
data-hero-primary-anchor={heroPrimaryAnchor === 'headline' ? 'headline' : undefined}
data-hero-segment="headline"
>
<Headline
as="h1"
size="page"
class="max-w-[15ch] text-balance text-[3.1rem] font-medium leading-[1.04] tracking-normal sm:text-[3.8rem] lg:text-[4.15rem]"
>
{hero.titleHtml ? <Fragment set:html={hero.titleHtml} /> : hero.title}
</Headline>
</div>
<div
data-hero-copy-role="supporting"
data-hero-supporting-copy
data-hero-segment="supporting-copy"
>
<Lead class="max-w-[35rem] text-[1.04rem] leading-8 text-[var(--color-copy)] sm:text-[1.12rem]">
{hero.description}
</Lead>
</div>
</div>
</div>
{(hero.primaryCta || hero.secondaryCta) && (
<div data-hero-cta-pair data-hero-segment="cta-pair">
<Cluster data-cta-cluster gap="sm" class="items-center sm:gap-[var(--space-cluster-lg)]">
<PrimaryCTA cta={hero.primaryCta} size="lg" />
{hero.secondaryCta && (
<SecondaryCTA cta={hero.secondaryCta} />
)}
</Cluster>
</div>
)}
</div>
{hero.trustSubclaims && hero.trustSubclaims.length > 0 && (
<div class="space-y-6" data-hero-segment="trust-subclaims" data-hero-trust-signals>
<ul class="grid gap-4 p-0 sm:grid-cols-3 lg:grid-cols-3">
{hero.trustSubclaims.map((claim, index) => (
<li class="list-none border-r border-[color:var(--color-border-strong)] pr-4 text-sm leading-6 text-[var(--color-copy)] last:border-r-0">
<Icon
name={trustSignalIcons[index] ?? 'lucide:check-circle'}
class="mb-3 h-5 w-5 text-[var(--color-stone-400)]"
aria-hidden="true"
/>
{claim}
</li>
))}
</ul>
<div class="space-y-3" data-hero-audience-strip>
<p class="m-0 text-[0.68rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)] text-[var(--color-muted-foreground)]">
Built for operator-led teams
</p>
<ul class="flex flex-wrap items-center gap-x-7 gap-y-2 p-0">
{audienceLabels.map((label) => (
<li class="list-none text-sm font-semibold text-[var(--color-muted-foreground)] opacity-70">
{label}
</li>
))}
</ul>
</div>
</div>
)}
</div>
<div
class="motion-rise min-w-0 xl:-mr-4"
style="animation-delay: 120ms;"
data-hero-panel="dashboard"
data-hero-primary-anchor={heroPrimaryAnchor === 'product-visual' ? 'product-visual' : undefined}
data-hero-visual
data-hero-visual-style="governance-surface"
data-hero-segment="product-near-visual"
>
<DashboardPreview />
</div>
</div>
</div>
) : (
<!-- Subpage hero: card-based 2-col layout -->
<div
class="grid gap-5 sm:gap-6 lg:grid-cols-[minmax(0,1.08fr)_minmax(20rem,0.92fr)] lg:items-start"
data-disclosure-layer="1"
data-hero-layout
>
<Card class="motion-rise overflow-hidden" data-hero-panel="text">
<div class="space-y-4 sm:space-y-6">
<div data-hero-text-core>
<div data-hero-eyebrow data-hero-segment="eyebrow">
<Badge>{hero.eyebrow}</Badge>
</div>
<div class="mt-3 space-y-4 sm:mt-4 sm:space-y-5">
<div data-hero-heading data-hero-segment="headline">
<Headline
as="h1"
size={heroHeadlineSize}
class="max-w-3xl text-balance"
>
{hero.titleHtml ? <Fragment set:html={hero.titleHtml} /> : hero.title}
</Headline>
</div>
<div data-hero-copy-role="supporting" data-hero-supporting-copy data-hero-segment="supporting-copy">
<Lead class="max-w-2xl" size={heroLeadSize}>
{hero.description}
</Lead>
</div>
</div>
</div>
{(hero.primaryCta || hero.secondaryCta) && (
<div data-hero-cta-pair data-hero-segment="cta-pair">
<Cluster data-cta-cluster gap="sm" class="sm:gap-[var(--space-cluster)]">
<PrimaryCTA cta={hero.primaryCta} />
{hero.secondaryCta && (
<SecondaryCTA cta={hero.secondaryCta} />
)}
</Cluster>
</div>
)}
{hero.highlights && hero.highlights.length > 0 && !hero.trustSubclaims?.length && (
<ul class="grid gap-3 p-0 sm:grid-cols-3">
{hero.highlights.map((highlight) => (
<li class="list-none rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/70 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{highlight}
</li>
))}
</ul>
)}
</div>
</Card>
<div class="grid gap-4 sm:gap-5" data-hero-panel="supporting">
{hero.productVisual && (
<Card
variant="accent"
class="motion-rise overflow-hidden"
data-hero-segment="product-near-visual"
data-hero-visual
>
<img
src={hero.productVisual.src}
alt={hero.productVisual.alt}
class="max-h-[22rem] w-full rounded-[var(--radius-lg)] object-cover object-top"
loading="eager"
/>
</Card>
)}
{hero.trustSubclaims && hero.trustSubclaims.length > 0 && (
<Card variant="subtle" class="motion-rise" data-hero-segment="trust-subclaims">
<div class="space-y-3" data-hero-trust-signals>
<p class="m-0 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)] text-[var(--color-copy)]">
Early trust
</p>
<ul class="grid gap-2.5 p-0">
{hero.trustSubclaims.map((claim) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-line)] bg-white/82 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{claim}
</li>
))}
</ul>
</div>
</Card>
)}
{!hero.productVisual && (calloutTitle || calloutDescription) && (
<Card variant="accent" class="motion-rise">
<p class="m-0 text-sm font-semibold uppercase tracking-[0.15em] text-[var(--color-brand)]">
Trust-first launch surface
</p>
{calloutTitle && (
<h2 class="mt-4 font-[var(--font-display)] text-3xl font-bold leading-tight text-[var(--color-ink-900)]">
{calloutTitle}
</h2>
)}
{calloutDescription && (
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{calloutDescription}</p>
)}
</Card>
)}
{metrics.length > 0 && (
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
{metrics.map((metric) => <Metric item={metric} />)}
</div>
)}
</div>
</div>
)}
</Container>
</section>

View File

@ -1,53 +0,0 @@
---
import Badge from '@/components/primitives/Badge.astro';
import Card from '@/components/primitives/Card.astro';
import Container from '@/components/primitives/Container.astro';
import Headline from '@/components/content/Headline.astro';
import Lead from '@/components/content/Lead.astro';
import PrimaryCTA from '@/components/content/PrimaryCTA.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { ProgressTeaserContent } from '@/types/site';
interface Props {
content: ProgressTeaserContent;
}
const { content } = Astro.props;
function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
---
<Section layer="2" data-section="progress">
<Container width="wide">
<div class="space-y-8">
<SectionHeader
eyebrow="Visible progress"
title={content.title}
description={content.description}
/>
<div class="space-y-4">
{content.entries.map((entry) => (
<Card class="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-6">
<Badge tone="signal">{formatDate(entry.date)}</Badge>
<div>
<Headline as="h3" size="card">
{entry.title}
</Headline>
<Lead class="mt-2" size="body">
{entry.description}
</Lead>
</div>
</Card>
))}
</div>
<PrimaryCTA cta={content.cta} />
</div>
</Container>
</Section>

View File

@ -1,30 +0,0 @@
---
import Container from '@/components/primitives/Container.astro';
import Section from '@/components/primitives/Section.astro';
import type { TrustPrincipleContent } from '@/types/site';
interface Props {
statements: TrustPrincipleContent[];
}
const { statements } = Astro.props;
---
<Section density="compact" data-section="trustbar">
<Container width="wide">
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{
statements.map((statement) => (
<article class="rounded-[var(--radius-lg)] border border-[color:var(--color-border)] bg-[var(--surface-muted)] p-4">
<h2 class="m-0 text-base font-semibold text-[var(--color-foreground)]">
{statement.title}
</h2>
<p class="mt-2 text-sm leading-6 text-[var(--color-copy)]">
{statement.description}
</p>
</article>
))
}
</div>
</Container>
</Section>

View File

@ -1,29 +0,0 @@
---
import TrustPrincipleCard from '@/components/content/TrustPrincipleCard.astro';
import Container from '@/components/primitives/Container.astro';
import Grid from '@/components/primitives/Grid.astro';
import Section from '@/components/primitives/Section.astro';
import SectionHeader from '@/components/primitives/SectionHeader.astro';
import type { TrustPrincipleContent } from '@/types/site';
interface Props {
description?: string;
eyebrow?: string;
items: TrustPrincipleContent[];
title: string;
titleHtml?: string;
}
const { description, eyebrow, items, title, titleHtml } = Astro.props;
---
<Section layer="2" tone="warm">
<Container width="wide">
<div class="space-y-8">
<SectionHeader eyebrow={eyebrow} title={title} titleHtml={titleHtml} description={description} />
<Grid cols="3">
{items.map((item) => <TrustPrincipleCard item={item} />)}
</Grid>
</div>
</Container>
</Section>

View File

@ -0,0 +1,80 @@
---
// Import the necessary dependencies
import { Image } from 'astro:assets';
import IconBlock from '@components/ui/blocks/IconBlock.astro';
import Icon from '@components/ui/icons/Icon.astro';
interface Feature {
heading: string;
content: string;
svg: string;
}
interface Props {
title?: string;
subTitle?: string;
features?: Feature[];
src?: any;
alt?: string;
}
// Define props from Astro
const { title, subTitle, src, alt, features } = Astro.props;
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Block to display the feature image */}
<div class="relative mb-6 overflow-hidden md:mb-8">
{
src && alt && (
<Image
src={src}
alt={alt}
class="h-full w-full object-cover object-center"
draggable={'false'}
format={'avif'}
loading={'eager'}
/>
)
}
</div>
{
/* Displaying the main content consisting of title, subtitle, and several `IconBlock` components */
}
<div class="mt-5 grid gap-8 lg:mt-16 lg:grid-cols-3 lg:gap-12">
{/* Block for title and subtitle */}
<div class="lg:col-span-1">
{/* Rendering title */}
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{title}
</h2>
{/* Rendering subtitle */}
{
subTitle && (
<p class="mt-2 text-pretty text-neutral-600 md:mt-4 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{/* Block to display the IconBlock components */}
<div class="lg:col-span-2">
<div class="grid gap-8 sm:grid-cols-2 md:gap-12">
{/* Injecting IconBlock components with different properties */}
{
features &&
features.map(feature => (
<IconBlock heading={feature.heading} content={feature.content}>
<Icon name={feature.svg} />
</IconBlock>
))
}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,97 @@
---
// Import the necessary dependencies
import TabNav from '@components/ui/blocks/TabNav.astro';
import TabContent from '@components/ui/blocks/TabContent.astro';
import Icon from '@components/ui/icons/Icon.astro';
// Define props from Astro
const { title, tabs } = Astro.props;
// Define TypeScript interface for tab object
interface Tab {
heading: string;
content: string;
svg: string;
src: any;
alt: string;
first?: boolean;
second?: boolean;
}
// Define TypeScript interface for props
interface Props {
title?: string;
tabs: Tab[];
}
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="relative p-6 md:p-16">
<div
class="relative z-10 lg:grid lg:grid-cols-12 lg:items-center lg:gap-16"
>
{/* Section's heading and tab navigation */}
<div class="mb-10 lg:order-2 lg:col-span-6 lg:col-start-8 lg:mb-0">
<h2
class="text-2xl font-bold text-neutral-800 sm:text-3xl dark:text-neutral-200"
>
{
/* About Fragment: https://docs.astro.build/en/basics/astro-syntax/#fragments */
}
<Fragment set:html={title} />
</h2>
{
/* Tab navigation - use the attribute 'first' in the first TabNav for the component to work */
}
<nav class="mt-5 grid gap-4 md:mt-10" aria-label="Tabs" role="tablist">
{
tabs.map((tab, index) => (
<TabNav
id={`tabs-with-card-item-${index + 1}`}
dataTab={`#tabs-with-card-${index + 1}`}
aria={`tabs-with-card-${index + 1}`}
heading={tab.heading}
content={tab.content}
first={tab.first}
>
<Icon name={tab.svg} />
</TabNav>
))
}
</nav>
</div>
{
/* Contents for each tab - the 'first' attribute should be used in the first tab for that tab to be initially visible, 'second' changes the styles */
}
<div class="lg:col-span-6">
<div class="relative">
<div>
{
tabs.map((tab, index) => (
<TabContent
id={`tabs-with-card-${index + 1}`}
aria={`tabs-with-card-item-${index + 1}`}
src={tab.src}
alt={tab.alt}
first={tab.first}
second={tab.second}
/>
))
}
</div>
</div>
</div>
</div>
<div class="absolute inset-0 grid h-full w-full grid-cols-12">
{/* Decorative background and sizing */}
<div
class="col-span-full h-5/6 w-full rounded-xl bg-neutral-100 sm:h-3/4 lg:col-span-7 lg:col-start-6 lg:h-full dark:bg-white/[.075]"
>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,62 @@
---
// Import the necessary components
import StatsBig from '@components/ui/blocks/StatsBig.astro';
import StatsSmall from '@components/ui/blocks/StatsSmall.astro';
const { title, subTitle, stats, mainStatTitle, mainStatSubTitle } = Astro.props;
interface Props {
title: string;
subTitle?: string;
mainStatTitle: string;
mainStatSubTitle: string;
stats?: Stat[];
}
// TypeScript type for the statistics
type Stat = {
stat: string;
description: string;
};
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
{/* Main title */}
<h2
class="mb-4 text-3xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
{/* Subtitle */}
{
subTitle && (
<p class="mb-16 max-w-prose font-normal text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{/* Grid container for statistics */}
<div class="grid items-center gap-6 lg:grid-cols-12 lg:gap-12">
{/* First grid item, showing a big statistics */}
<div class="lg:col-span-4">
<StatsBig title={mainStatTitle} subTitle={mainStatSubTitle} />
</div>
{/* Second grid item, showing multiple small statistics */}
{
stats && (
<div class="relative lg:col-span-8 lg:before:absolute lg:before:-start-12 lg:before:top-0 lg:before:h-full lg:before:w-px lg:before:bg-neutral-300 lg:dark:before:bg-neutral-700">
<div class="grid grid-cols-2 gap-6 sm:gap-8 md:grid-cols-4 lg:grid-cols-3">
{/* Iterate over the 'stats' array and create a 'StatsSmall' component for each object in the array */}
{stats.map(stat => (
<StatsSmall title={stat.stat} subTitle={stat.description} />
))}
</div>
</div>
)
}
</div>
</section>

View File

@ -0,0 +1,60 @@
---
import { Image } from 'astro:assets';
import product5 from '@images/features-image.avif';
// Define props from Astro
const { title, subTitle, benefits } = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
benefits?: Array<string>;
}
// Define SVG marker to be used in the component
const ListItemMarker: string = `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mt-0.5 h-6 w-6 text-orange-400 dark:text-orange-300 flex-none"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>`;
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Grid */}
<div class="lg:grid lg:grid-cols-12 lg:items-center lg:gap-16">
<div class="lg:col-span-7">
<Image class="rounded-xl" src={product5} alt="Mockup of floating boxes" />
</div>
<div class="mt-5 sm:mt-10 lg:col-span-5 lg:mt-0">
<div class="space-y-6 sm:space-y-8">
<div class="space-y-2 md:space-y-4">
<h2
class="text-3xl font-bold text-balance text-neutral-800 lg:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{
subTitle && (
<p class="text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{
benefits && (
<ul class="space-y-2 sm:space-y-4">
{benefits.map(item => (
<li class="flex space-x-3">
<Fragment set:html={ListItemMarker} />
<span class="text-base font-medium text-pretty text-neutral-600 dark:text-neutral-400">
{item}
</span>
</li>
))}
</ul>
)
}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,49 @@
---
// Define props from Astro
const { title, subTitle, partners } = Astro.props;
interface Partner {
icon: any;
name?: string;
href?: string;
}
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
partners: Partner[];
}
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Title and description */}
<div class="mx-auto mb-6 w-full space-y-1 text-center sm:w-1/2 lg:w-1/3">
<h2
class="text-2xl leading-tight font-bold text-balance text-neutral-800 sm:text-3xl dark:text-neutral-200"
>
{title}
</h2>
{
subTitle && (
<p class="leading-tight text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
<div
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
>
{/* Clients Group SVGs */}
{
partners.map(partner => (
<a href={partner.href} target="_blank" rel="noopener noreferrer">
<div set:html={partner.icon} />
</a>
))
}
</div>
</section>

View File

@ -0,0 +1,112 @@
---
// Import the necessary dependencies
import { Image } from 'astro:assets';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import ReviewComponent from '@components/ui/blocks/ReviewComponent.astro';
// Define props from Astro
const {
title,
subTitle,
primaryBtn,
primaryBtnURL,
secondaryBtn,
secondaryBtnURL,
withReview,
avatars,
starCount,
rating,
reviews,
src,
alt,
} = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
withReview?: boolean;
avatars?: Array<string>;
starCount?: number;
rating?: string;
reviews?: string;
src?: any;
alt?: string;
}
---
{/* Defining a grid container that holds all the content */}
<section
class="mx-auto grid max-w-[85rem] gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
>
{/* Title and description */}
<div>
{
/* Each h1 and p tag renders a portion of the title and subTitle defined above */
}
<h1
class="block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-6xl lg:leading-tight dark:text-neutral-200"
>
{
/* About Fragment: https://docs.astro.build/en/basics/astro-syntax/#fragments */
}
<Fragment set:html={title} />
</h1>
{
subTitle && (
<p class="mt-3 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-400">
{subTitle}
</p>
)
}
{
/* Action Button Section: This section includes two CTAs with their own styles and URL */
}
<div class="mt-7 grid w-full gap-3 sm:inline-flex">
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
{
secondaryBtn && (
<SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />
)
}
</div>
{
/* Review Section: This section presents avatars, review ratings and the number of reviews */
}
{
withReview ? (
<ReviewComponent
avatars={avatars}
starCount={starCount}
rating={rating}
reviews={reviews}
/>
) : (
''
)
}
</div>
{/* Hero Image Section */}
<div class="flex w-full">
<div class="top-12 overflow-hidden">
{
src && alt && (
<Image
src={src}
alt={alt}
class="h-full w-full scale-110 object-cover object-center"
draggable={'false'}
loading={'eager'}
format={'avif'}
/>
)
}
</div>
</div>
</section>

View File

@ -0,0 +1,148 @@
---
// Import the necessary dependencies
import GithubBtn from '@components/ui/buttons/GithubBtn.astro';
// Define props from Astro
const { title, subTitle, url } = Astro.props;
const btnTitle =
Astro.currentLocale === 'fr'
? 'Continuer avec Github'
: 'Continue with Github';
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
url?: string;
}
---
<section
class="relative mx-auto max-w-[85rem] px-4 pt-10 pb-24 sm:px-6 lg:px-8"
>
{/* Decorating SVG elements */}
<div
class="absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
>
<svg
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#ea580c"
viewBox="0 0 24 24"
>
<path
fill="#ea580c"
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="M12 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 18a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
></path>
<path
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="M21 7.353v9.294a.6.6 0 0 1-.309.525l-8.4 4.666a.6.6 0 0 1-.582 0l-8.4-4.666A.6.6 0 0 1 3 16.647V7.353a.6.6 0 0 1 .309-.524l8.4-4.667a.6.6 0 0 1 .582 0l8.4 4.667a.6.6 0 0 1 .309.524Z"
></path>
<path
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="m3.528 7.294 8.18 4.544a.6.6 0 0 0 .583 0l8.209-4.56M12 21v-9"
></path>
</svg>
</div>
<div class="absolute top-0 left-[85%] scale-75">
<svg
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#fbbf24"
viewBox="0 0 24 24"
>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"
></path>
<path
fill="#fbbf24"
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 10.5V9M5 15v-1.5"></path>
<path
fill="#fbbf24"
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM19 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
></path>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19H9M15 19h-1.5"></path>
</svg>
</div>
<div
class="absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
>
<svg
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#a3a3a3"
viewBox="0 0 24 24"
>
<path
stroke="#a3a3a3"
stroke-linecap="round"
stroke-linejoin="round"
d="M5.164 17c.29-1.049.67-2.052 1.132-3M11.5 7.794A16.838 16.838 0 0 1 14 6.296M4.5 22a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
></path>
<path
stroke="#a3a3a3"
stroke-linecap="round"
stroke-linejoin="round"
d="M9.5 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM19.5 7a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
></path>
</svg>
</div>
{/* Hero Section Heading */}
<div class="mx-auto mt-5 max-w-xl text-center">
<h2
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
>
{title}
</h2>
</div>
{/* Hero Section Sub-heading */}
<div class="mx-auto mt-5 max-w-3xl text-center">
{
subTitle && (
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
)
}
</div>
{/* Github Button */}
{
url && (
<div class="mt-8 flex justify-center gap-3">
<GithubBtn url={url} title={btnTitle} />
</div>
)
}
</section>

View File

@ -0,0 +1,5 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
---
<PrimaryCTA title="Request walkthrough" url="/contact" />

View File

@ -0,0 +1,122 @@
---
// Import the necessary dependencies.
import AuthBtn from '@components/ui/buttons/AuthBtn.astro';
import ContactIconBlock from '@components/ui/blocks/ContactIconBlock.astro';
import TextInput from '@components/ui/forms/input/TextInput.astro';
import EmailContactInput from '@components/ui/forms/input/EmailContactInput.astro';
import PhoneInput from '@components/ui/forms/input/PhoneInput.astro';
import TextAreaInput from '@components/ui/forms/input/TextAreaInput.astro';
import Icon from '@components/ui/icons/Icon.astro';
// Define the variables that will be used in this component
const title: string = 'Contact Tenantial';
const subTitle: string =
'Request a walkthrough or start a scoped rollout conversation. Do not send secrets, credentials, or tenant exports through this public website.';
const formTitle: string = 'Prepare a walkthrough request';
const formSubTitle: string =
'The public website form is static; use email for business contact context.';
---
{/* Contact Us */}
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<div class="mx-auto max-w-2xl lg:max-w-5xl">
<div class="text-center">
<h1
class="text-2xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
>
{title}
</h1>
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
</div>
<div class="mt-12 grid items-center gap-6 lg:grid-cols-2 lg:gap-16">
<div class="flex flex-col rounded-xl p-4 sm:p-6 lg:p-8">
<h2
class="mb-8 text-xl font-bold text-neutral-700 dark:text-neutral-300"
>
{formTitle}
</h2>
{
/* Form for user input with various input fields.-->
{/* Each field utilizes a different input component for the specific type of input (text, email, phone, and textarea)*/
}
<form>
<div class="grid gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<TextInput
id="hs-firstname-contacts"
label="First Name"
name="hs-firstname-contacts"
/>
<TextInput
id="hs-lastname-contacts"
label="Last Name"
name="hs-lastname-contacts"
/>
</div>
<EmailContactInput id="hs-email-contacts" />
<PhoneInput id="hs-phone-number" />
<TextAreaInput
id="hs-about-contacts"
label="Details"
name="hs-about-contacts"
/>
</div>
<div class="mt-4 grid">
<AuthBtn title="Prepare Email" />
</div>
<div class="mt-3 text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
{formSubTitle}
</p>
</div>
</form>
</div>
{
/*ContactIconBlocks are used to display different methods of contacting, including visiting office, email, browsing knowledgebase, and FAQ.*/
}
<div class="divide-y divide-neutral-300 dark:divide-neutral-700">
<ContactIconBlock
heading="Knowledgebase"
content="Read public notes about the Tenantial review model."
isLinkVisible={true}
linkTitle="Visit docs"
linkURL="/welcome-to-docs/"
isArrowVisible={true}
><Icon name="question" />
</ContactIconBlock>
<ContactIconBlock
heading="FAQ"
content="Explore our FAQ for quick, clear answers to common queries."
isLinkVisible={true}
linkTitle="Visit FAQ"
linkURL="/#faq"
isArrowVisible={true}
><Icon name="chatBubble" />
</ContactIconBlock>
<ContactIconBlock
heading="Product preview boundary"
content="The website does not connect to live tenant data or start backend workflows."
isAddressVisible={false}
><Icon name="mapPin" />
</ContactIconBlock>
<ContactIconBlock
heading="Contact us by email"
content="Prefer the written word? Drop us an email at"
isLinkVisible={true}
linkTitle="hello@tenantial.com"
linkURL="mailto:hello@tenantial.com"
><Icon name="envelopeOpen" />
</ContactIconBlock>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,79 @@
---
// Import the necessary AccordionItem component and JSON data
import AccordionItem from '@components/ui/blocks/AccordionItem.astro';
// Define props from Astro
const { title, faqs } = Astro.props;
// Define TypeScript interface for props
interface Faq {
question: string;
answer: string;
}
interface FaqGroup {
subTitle?: string;
faqs: Faq[];
}
interface Props {
title: string;
faqs: FaqGroup;
}
// Define a helper function to generate ids dynamically.
const makeId = (base: any, index: any) => `${base}${index + 1}`;
---
{
/* Main container that holds all content. Customized for different viewport sizes. */
}
<section
id="faq"
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="grid gap-10 md:grid-cols-5">
<div class="md:col-span-2">
<div class="max-w-xs">
<h2
class="text-2xl font-bold text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
>
<Fragment set:html={title} />
</h2>
<p class="mt-1 hidden text-neutral-600 md:block dark:text-neutral-400">
{faqs.subTitle}
</p>
</div>
</div>
{/* FAQ accordion items */}
<div class="md:col-span-3">
<div
class="hs-accordion-group divide-y divide-neutral-200 dark:divide-neutral-700"
>
{
faqs.faqs.map((question, i) => {
{
/* Generate ids dynamically for each FAQ accordion item. */
}
let id = makeId(
'hs-basic-with-title-and-arrow-stretched-heading-',
i
);
let collapseId = makeId(
'hs-basic-with-title-and-arrow-stretched-collapse',
i
);
return (
<AccordionItem
{...question}
id={id}
collapseId={collapseId}
first={i === 0}
/>
);
})
}
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,80 @@
---
// Import the necessary dependencies
import EmailFooterInput from '@components/ui/forms/input/EmailFooterInput.astro';
import enStrings from '@utils/navigation.ts';
import BrandLogo from '@components/BrandLogo.astro';
import { SITE } from '@data/constants';
const strings = enStrings;
// Define the variables that will be used in this component
const sectionThreeTitle: string =
'Start a scoped conversation';
const sectionThreeContent: string =
'Use email for business contact context only. Do not send secrets, credentials, or tenant exports through the public website.';
---
<footer class="w-full bg-neutral-300 dark:bg-neutral-900">
<div
class="mx-auto w-full max-w-[85rem] px-4 py-10 sm:px-6 lg:px-16 lg:pt-20 2xl:max-w-(--breakpoint-2xl)"
>
<div class="grid grid-cols-2 gap-6 md:grid-cols-4 lg:grid-cols-5">
<div class="col-span-full lg:col-span-1">
{/* Brand Logo */}
<BrandLogo class="h-auto w-32" />
</div>
{/* An array of links for Product and Company sections */}
{
strings.footerLinks.map(section => (
<div class="col-span-1">
<h3 class="font-bold text-neutral-800 dark:text-neutral-200">
{section.section}
</h3>
<ul class="mt-3 grid space-y-3">
{section.links.map((link, index) => (
<li>
<a
href={link.url}
class="inline-flex gap-x-2 rounded-lg text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-neutral-500 focus-visible:ring-3 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-neutral-300 dark:focus:outline-hidden"
>
{link.name}
</a>
</li>
))}
</ul>
</div>
))
}
<div class="col-span-2">
<h3 class="font-bold text-neutral-800 dark:text-neutral-200">
{sectionThreeTitle}
</h3>
<form>
<EmailFooterInput />
<p class="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
{sectionThreeContent}
</p>
</form>
</div>
</div>
<div
class="mt-9 grid gap-y-2 sm:mt-12 sm:flex sm:items-center sm:justify-between sm:gap-y-0"
>
<div class="flex items-center justify-between">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
© <span id="current-year"></span>
{SITE.title}. Public website content only.
</p>
</div>
</div>
<script>
const year = new Date().getFullYear();
const element = document.getElementById('current-year');
element!.innerText = year.toString();
</script>
</div>
</footer>

View File

@ -0,0 +1,205 @@
---
//Import relevant dependencies
import ThemeIcon from '@components/ThemeIcon.astro';
import NavLink from '@components/ui/links/NavLink.astro';
import Authentication from '../misc/Authentication.astro';
import enStrings from '@utils/navigation.ts';
import BrandLogo from '@components/BrandLogo.astro';
const strings = enStrings;
const homeUrl = '/';
---
{/* Main header component */}
<header
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm md:flex-nowrap md:justify-start"
>
{/* Navigation container */}
<nav
class="relative mx-2 w-full rounded-[36px] border border-yellow-100/40 bg-yellow-50/60 px-4 py-3 backdrop-blur-md md:flex md:items-center md:justify-between md:px-6 md:py-0 lg:px-8 xl:mx-auto dark:border-neutral-700/40 dark:bg-neutral-800/80 dark:backdrop-blur-md"
aria-label="Global"
>
<div class="flex items-center justify-between">
{/* Brand logo */}
<a
class="flex-none rounded-lg text-xl font-bold ring-zinc-500 outline-hidden focus-visible:ring-3 dark:ring-zinc-200 dark:focus:outline-hidden"
href={homeUrl}
aria-label="Brand"
>
<BrandLogo class="h-auto w-24" />
</a>
{/* Collapse toggle for smaller screens */}
<div class="mr-5 ml-auto md:hidden">
<button
type="button"
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-hidden"
data-hs-collapse="#navbar-collapse-with-animation"
aria-controls="navbar-collapse-with-animation"
aria-label="Toggle navigation"
>
<svg
class="hs-collapse-open:hidden h-[1.25rem] w-[1.25rem] shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="3" x2="21" y1="6" y2="6"></line>
<line x1="3" x2="21" y1="12" y2="12"></line>
<line x1="3" x2="21" y1="18" y2="18"></line>
</svg>
<svg
class="hs-collapse-open:block hidden h-[1.25rem] w-[1.25rem] shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
{/* ThemeIcon component specifically for smaller screens */}
<span class="inline-block md:hidden">
<ThemeIcon />
</span>
</div>
{/* Contains navigation links */}
<div
id="navbar-collapse-with-animation"
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block"
>
{/* Navigation links container */}
<div
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7"
>
{/* Navigation links and Authentication component */}
{
strings.navBarLinks.map(link => (
<NavLink url={link.url} name={link.name} />
))
}
<Authentication />
{/* ThemeIcon component specifically for larger screens */}
<span class="hidden md:inline-block">
<ThemeIcon />
</span>
</div>
</div>
</nav>
</header>
{/* Theme Appearance script to manage light/dark modes */}
<script is:inline>
const HSThemeAppearance = {
init() {
const defaultTheme = 'default';
let theme = localStorage.getItem('hs_theme') || defaultTheme;
if (document.querySelector('html').classList.contains('dark')) return;
this.setAppearance(theme);
},
_resetStylesOnLoad() {
const $resetStyles = document.createElement('style');
$resetStyles.innerText = `*{transition: unset !important;}`;
$resetStyles.setAttribute('data-hs-appearance-onload-styles', '');
document.head.appendChild($resetStyles);
return $resetStyles;
},
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
const $resetStylesEl = this._resetStylesOnLoad();
if (saveInStore) {
localStorage.setItem('hs_theme', theme);
}
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
}
document.querySelector('html').classList.remove('dark');
document.querySelector('html').classList.remove('default');
document.querySelector('html').classList.remove('auto');
document
.querySelector('html')
.classList.add(this.getOriginalAppearance());
setTimeout(() => {
$resetStylesEl.remove();
});
if (dispatchEvent) {
window.dispatchEvent(
new CustomEvent('on-hs-appearance-change', { detail: theme })
);
}
},
getAppearance() {
let theme = this.getOriginalAppearance();
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
}
return theme;
},
getOriginalAppearance() {
const defaultTheme = 'default';
return localStorage.getItem('hs_theme') || defaultTheme;
},
};
HSThemeAppearance.init();
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (HSThemeAppearance.getOriginalAppearance() === 'auto') {
HSThemeAppearance.setAppearance('auto', false);
}
});
window.addEventListener('load', () => {
const $clickableThemes = document.querySelectorAll(
'[data-hs-theme-click-value]'
);
const $switchableThemes = document.querySelectorAll(
'[data-hs-theme-switch]'
);
$clickableThemes.forEach($item => {
$item.addEventListener('click', () =>
HSThemeAppearance.setAppearance(
$item.getAttribute('data-hs-theme-click-value'),
true,
$item
)
);
});
$switchableThemes.forEach($item => {
$item.addEventListener('change', e => {
HSThemeAppearance.setAppearance(e.target.checked ? 'dark' : 'default');
});
$item.checked = HSThemeAppearance.getAppearance() === 'dark';
});
window.addEventListener('on-hs-appearance-change', e => {
$switchableThemes.forEach($item => {
$item.checked = e.detail === 'dark';
});
});
});
</script>

View File

@ -0,0 +1,158 @@
---
// Import SecondaryCTA component for use in this module
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Icon from '@components/ui/icons/Icon.astro';
// Define props from Astro
const { pricing } = Astro.props;
// Define TypeScript type for products.
type Product = {
name: string;
description: string;
price: string;
cents: string;
billingFrequency: string;
features: Array<string>;
purchaseBtnTitle: string;
purchaseLink: string;
};
interface PricingSectionProps {
title: string;
subTitle: string;
badge: string;
thirdOption: string;
btnText: string;
pricing: {
title: string;
subTitle: string;
starterKit: Product;
professionalToolbox: Product;
};
}
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Section heading and sub-heading */}
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h2
class="text-2xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl md:leading-tight dark:text-neutral-200"
>
{pricing.title}
</h2>
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
{pricing.subTitle}
</p>
</div>
{/* Contains two main product blocks */}
<div class="flex flex-wrap items-center justify-center gap-4 sm:gap-0">
{/* Starter Kit product details */}
<div
class="w-full rounded-xl bg-gray-800 p-6 sm:w-1/2 sm:rounded-r-none sm:p-8 lg:w-1/3"
>
<div class="mb-4">
<h3 class="text-2xl font-bold text-neutral-100 sm:text-3xl">
{pricing.starterKit.name}
</h3>
<p class="text-indigo-300">{pricing.starterKit.description}</p>
</div>
<div class="mb-4">
<span class="text-4xl font-bold text-neutral-200"
>{pricing.starterKit.price}</span
>
<span class="text-lg font-bold text-neutral-300"
>{pricing.starterKit.cents}</span
>
<span class="ms-3 text-sm text-indigo-200"
>{pricing.starterKit.billingFrequency}</span
>
</div>
{
/* Features list - automatically created by mapping over `features` array */
}
<ul class="mb-6 space-y-2 text-neutral-300">
{
pricing.starterKit.features.map((feature: string) => (
<li class="flex items-center gap-1.5">
<Icon name="checkCircle" />
<span>{feature}</span>
</li>
))
}
</ul>
{/* CTA for purchasing the product */}
<a
href={pricing.starterKit.purchaseLink}
class="block rounded-lg bg-gray-500 px-8 py-3 text-center text-sm font-bold text-gray-100 ring-indigo-300 outline-hidden transition duration-100 hover:bg-gray-600 focus-visible:ring-3 active:text-gray-300 md:text-base"
>{pricing.starterKit.purchaseBtnTitle}</a
>
</div>
{/* Professional Toolbox product details */}
<div
class="w-full rounded-xl bg-linear-to-tr from-[#FF512F] to-[#F09819] p-6 shadow-xl sm:w-1/2 sm:p-8"
>
<div
class="mb-4 flex flex-col items-start justify-between gap-4 lg:flex-row"
>
<div>
<h3 class="text-2xl font-bold text-neutral-100 sm:text-3xl">
{pricing.professionalToolbox.name}
</h3>
<p class="text-orange-200">
{pricing.professionalToolbox.description}
</p>
</div>
<span
class="bg-opacity-50 order-first inline-block rounded-full bg-orange-200/60 px-3 py-1 text-center text-xs font-bold tracking-wider text-orange-600 uppercase lg:order-none"
>{pricing.badge}</span
>
</div>
<div class="mb-4">
<span class="text-6xl font-bold text-neutral-100"
>{pricing.professionalToolbox.price}</span
>
<span class="text-lg font-bold text-orange-100"
>{pricing.professionalToolbox.cents}</span
>
<span class="ms-3 text-orange-200"
>{pricing.professionalToolbox.billingFrequency}</span
>
</div>
{
/* Features list - automatically created by mapping over `features` array */
}
<ul class="mb-6 space-y-2 text-orange-100">
{
pricing.professionalToolbox.features.map((feature: string) => (
<li class="flex items-center gap-1.5">
<Icon name="checkCircle" />
<span>{feature}</span>
</li>
))
}
</ul>
{/* CTA for purchasing the product */}
<a
href={pricing.professionalToolbox.purchaseLink}
class="bg-opacity-50 block rounded-lg bg-orange-200/40 px-8 py-3 text-center text-sm font-bold text-neutral-100 ring-orange-300 outline-hidden transition duration-300 hover:bg-orange-300 focus-visible:ring-3 active:bg-orange-400 md:text-base"
>{pricing.professionalToolbox.purchaseBtnTitle}</a
>
</div>
</div>
{/* Call to action for Enterprise Solutions */}
<div class="mt-8 flex items-center justify-center gap-x-3 md:mt-12">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
{pricing.thirdOption}
</p>
<SecondaryCTA title={pricing.btnText} url="/contact" />
</div>
</section>

View File

@ -0,0 +1,43 @@
---
import { Image } from 'astro:assets';
import Icon from '../../ui/icons/Icon.astro';
const { content, author, role, avatarSrc } = Astro.props;
interface Props {
content: string;
author: string;
role: string;
avatarSrc: string;
}
---
<blockquote class="relative">
<Icon name="quotation" />
<div class="relative z-10">
<p class="text-xl text-neutral-800 italic dark:text-neutral-200">
{content}
</p>
</div>
<div class="mt-6">
<div class="flex items-center">
<div class="shrink-0">
<Image
class="h-8 w-8 rounded-full"
src={avatarSrc}
alt="Avatar Description"
loading={'eager'}
inferSize
/>
</div>
<div class="ms-4 grow">
<div class="font-bold text-neutral-800 dark:text-neutral-200">
{author}
</div>
<div class="text-xs text-neutral-500">{role}</div>
</div>
</div>
</div>
</blockquote>

View File

@ -0,0 +1,85 @@
---
import TestimonialItem from './TestimonialItem.astro';
import StatsGrid from '../../ui/blocks/StatsGrid.astro';
const { title, subTitle, testimonials, statistics } = Astro.props;
interface Props {
title: string;
subTitle?: string;
testimonials?: Testimonial[];
statistics?: StatProps[];
}
// TypeScript type for testimonials
type Testimonial = {
content: string;
author: string;
role: string;
avatarSrc: string;
};
// TypeScript type for stats.
type StatProps = {
count: string;
description: string;
};
---
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
{/* Container for the testimonials */}
<div
class="lg:grid lg:grid-cols-12 lg:items-center lg:justify-between lg:gap-16"
>
<div class="lg:col-span-5 lg:col-start-1">
{/* Title and Subtitle */}
<div class="mb-8">
<h2
class="mb-2 text-3xl font-bold text-neutral-800 lg:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{
subTitle && (
<p class="text-neutral-600 dark:text-neutral-400">{subTitle}</p>
)
}
</div>
{
/* Generate a blockquote for each testimonial in the testimonials array by mapping over the array. */
}
{
testimonials &&
testimonials.map(testimonial => (
<TestimonialItem
content={testimonial.content}
author={testimonial.author}
role={testimonial.role}
avatarSrc={testimonial.avatarSrc}
/>
))
}
</div>
{
statistics && (
<div class="mt-10 lg:col-span-6 lg:col-end-13 lg:mt-0">
<div class="space-y-6 sm:space-y-8">
<ul class="grid grid-cols-2 divide-x-2 divide-y-2 divide-neutral-300 overflow-hidden dark:divide-neutral-700">
{/* Generate a list item for each stat in the statistics array by mapping over the array. */}
{statistics.map((stat, index) => (
<StatsGrid
count={stat.count}
description={stat.description}
index={index}
/>
))}
</ul>
</div>
</div>
)
}
</div>
</section>

View File

@ -0,0 +1,73 @@
---
// Import AvatarTestimonialSection component for use in this module
import AvatarTestimonialSection from '../../ui/avatars/AvatarTestimonialSection.astro';
// Define props from Astro
const { title, testimonials } = Astro.props;
// Define TypeScript interface for Testimonial
interface Testimonial {
content: string;
author: string;
role: string;
avatarSrc: string;
avatarAlt: string;
}
// Define TypeScript interface for props
interface Props {
title: string;
testimonials: Testimonial[];
}
---
{/* Main div that wraps the testimonials section */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
id="testimonials"
>
{/* Title of the testimonials section */}
<div class="mb-6 w-3/4 max-w-2xl sm:mb-10 md:mb-16 lg:w-1/2">
<h2
class="text-2xl font-bold text-balance text-neutral-800 sm:text-3xl lg:text-4xl dark:text-neutral-200"
>
{title}
</h2>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Looping through each testimonial data and rendering it */}
{
testimonials.map(testimonial => (
<div class="flex h-auto">
<div class="flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-700">
<div class="flex-auto p-4 md:p-6">
{/* Testimonial content */}
<p class="text-base text-pretty text-neutral-600 italic md:text-lg dark:text-neutral-300">
{testimonial.content}
</p>
</div>
<div class="rounded-b-xl bg-neutral-300/30 p-4 md:px-7 dark:bg-neutral-900/30">
<div class="flex items-center">
<AvatarTestimonialSection
src={testimonial.avatarSrc}
alt={testimonial.avatarAlt}
/>
<div class="ms-3 grow">
<p class="text-sm font-bold text-neutral-800 sm:text-base dark:text-neutral-200">
{testimonial.author}
</p>
<p class="text-xs text-neutral-600 dark:text-neutral-400">
{testimonial.role}
</p>
</div>
</div>
</div>
</div>
</div>
))
}
</div>
</section>

View File

@ -0,0 +1,18 @@
---
import { Image } from 'astro:assets';
const { src, alt } = Astro.props;
interface Props {
src: string;
alt: string;
}
---
<Image
class="inline-block h-8 w-8 rounded-full ring-2 ring-neutral-50 dark:ring-zinc-800"
src={src}
alt={alt}
inferSize
loading={'eager'}
/>

View File

@ -0,0 +1,22 @@
---
// Import necessary components
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
const { blogEntry } = Astro.props;
interface Props {
blogEntry: CollectionEntry<'blog'>;
}
---
<div class="shrink-0">
<Image
class="size-[46px] rounded-full border-2 border-neutral-50"
src={blogEntry.data.authorImage}
alt={blogEntry.data.authorImageAlt}
draggable={'false'}
format={'avif'}
/>
</div>

View File

@ -0,0 +1,22 @@
---
// Import necessary components
import { Image } from 'astro:assets';
import type { CollectionEntry } from 'astro:content';
const { blogEntry } = Astro.props;
interface Props {
blogEntry: CollectionEntry<'blog'>;
}
---
<div class="shrink-0">
<Image
class="size-10 rounded-full sm:h-14 sm:w-14"
src={blogEntry.data.authorImage}
alt={blogEntry.data.authorImageAlt}
draggable={'false'}
format={'avif'}
/>
</div>

View File

@ -0,0 +1,17 @@
---
const { src, alt } = Astro.props;
interface Props {
src: string;
alt: string;
}
---
<div class="shrink-0">
<img
class="size-8 rounded-full sm:h-[2.875rem] sm:w-[2.875rem]"
src={src}
alt={alt}
loading="lazy"
/>
</div>

View File

@ -0,0 +1,84 @@
---
const { title, btnId, btnTitle, url } = Astro.props;
interface Props {
title?: string;
btnId: string;
btnTitle: string;
url: string;
}
---
<astro-banner btnId={btnId}>
<div
class="fixed start-1/2 bottom-0 z-50 mx-auto w-full -translate-x-1/2 transform p-6 sm:max-w-4xl"
role="region"
aria-label="Informational Banner"
>
<div
class="rounded-xl bg-neutral-800 bg-[url('/banner-pattern.svg')] bg-cover bg-center bg-no-repeat p-4 text-center shadow-xs dark:bg-neutral-200"
>
<div class="flex items-center justify-center">
<div class="ml-auto">
{
title && (
<p class="me-2 inline-block font-medium text-neutral-50 dark:text-neutral-700">
{title}
</p>
)
}
<a
class="group inline-flex items-center gap-x-2 rounded-full border-2 border-neutral-50 px-3 py-2 text-sm font-semibold text-neutral-50 backdrop-brightness-75 transition duration-300 hover:border-neutral-100/70 hover:text-neutral-50/70 disabled:pointer-events-none disabled:opacity-50 sm:backdrop-brightness-100 dark:border-neutral-700 dark:text-neutral-700 dark:backdrop-brightness-100 dark:hover:border-neutral-700/70 dark:hover:text-neutral-800/70 dark:focus:outline-hidden"
href={url}
target="_blank"
>
{btnTitle}
<svg
class="size-4 shrink-0 transition duration-300 group-hover:translate-x-1"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="m9 18 6-6-6-6"></path></svg
>
</a>
</div>
<button
type="button"
class="ml-auto inline-flex items-center gap-x-2 rounded-full border border-transparent bg-gray-100 p-2 text-sm font-semibold text-gray-800 hover:bg-gray-200 disabled:pointer-events-none disabled:opacity-50 dark:bg-neutral-700 dark:text-neutral-50 dark:hover:bg-neutral-700/80 dark:hover:text-neutral-50 dark:focus:outline-hidden"
id={btnId}
>
<span class="sr-only">Dismiss</span>
<svg
class="size-5 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg
>
</button>
</div>
</div>
</div>
</astro-banner>
<script>
class AstroBanner extends HTMLElement {
connectedCallback() {
const btnId = this.getAttribute('btnId');
const button = this.querySelector(`#${btnId}`);
if (button != null) {
button.addEventListener('click', () => this.remove());
}
}
}
customElements.define('astro-banner', AstroBanner);
</script>

View File

@ -0,0 +1,51 @@
---
import Icon from '@components/ui/icons/Icon.astro';
// Define props from Astro
const { id, collapseId, question, answer, first } = Astro.props;
// Define TypeScript interface for props
interface Props {
id: string;
collapseId: string;
question: string;
answer: string;
first?: boolean;
}
// Define class names for the accordion and its content
const ACCORDION_CLASS_DEFAULT = 'hs-accordion pb-3 active';
const ACCORDION_CLASS_COLLAPSED = 'hs-accordion pt-6 pb-3';
const ACCORDION_CONTENT_CLASS =
'hs-accordion-content w-full overflow-hidden transition-[height] duration-300';
// Helper function to return the correct class for the accordion
function getAccordionClass(first: boolean = false) {
return first ? ACCORDION_CLASS_DEFAULT : ACCORDION_CLASS_COLLAPSED;
}
---
{/* The main container for the accordion item */}
<div class={getAccordionClass(first)} id={id}>
{/* The accordion button, which toggles the expanded/collapsed state */}
<button
class="hs-accordion-toggle group inline-flex w-full items-center justify-between gap-x-3 rounded-lg pb-3 text-start font-bold text-balance text-neutral-800 ring-zinc-500 outline-hidden transition hover:text-neutral-500 focus-visible:ring-3 md:text-lg dark:text-neutral-200 dark:ring-zinc-200 dark:hover:text-neutral-400 dark:focus:outline-hidden"
aria-expanded={first}
aria-controls={collapseId}
>
{question}
{/* SVG Icon that is shown when the accordion is NOT active */}
<Icon name="accordionNotActive" />
{/* SVG Icon that is shown when the accordion is active */}
<Icon name="accordionActive" />
</button>
{/* The collapsible content of the accordion */}
<div
id={collapseId}
role="region"
aria-labelledby={id}
class={`${first ? ACCORDION_CONTENT_CLASS : 'hidden ' + ACCORDION_CONTENT_CLASS}`}
>
{/* The content paragraph */}
<p class="text-pretty text-neutral-600 dark:text-neutral-400">
{answer}
</p>
</div>
</div>

View File

@ -0,0 +1,66 @@
---
// Define props from Astro
const {
heading,
content,
isAddressVisible,
addressContent,
isLinkVisible,
linkTitle,
linkURL,
isArrowVisible,
} = Astro.props;
// Define TypeScript interface for props
interface Props {
heading?: string;
content?: string;
isAddressVisible?: boolean;
addressContent?: string;
isLinkVisible?: boolean;
linkTitle?: string;
linkURL?: string;
isArrowVisible?: boolean;
}
// Define SVG arrow to be used in the component
const arrowSVG: string = `<svg
class="h-4 w-4 shrink-0 transition ease-in-out group-hover:translate-x-1"
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" >
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" /> </svg>`;
---
{/* Root container, which arranges the heading and content */}
<div class="flex gap-x-7 py-6">
{/* Slot to allow for extensibility of the component */}
<slot />
<div class="grow">
{/* Heading of the section */}
<h3 class="font-bold text-neutral-700 dark:text-neutral-300">
{heading}
</h3>
{/* Content of the section */}
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">{content}</p>
{/* Conditional rendering of address content if isAddressVisible is true */}
{
isAddressVisible ? (
<p class="mt-1 text-sm text-neutral-500 italic">{addressContent}</p>
) : null
}
{
/* Conditional rendering of a link if isLinkVisible is true.
The link also conditionally includes an arrow SVG if isArrowVisible is true */
}
{
isLinkVisible ? (
<a
class="group mt-2 inline-flex items-center gap-x-2 rounded-lg text-sm font-medium text-zinc-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-zinc-800 focus-visible:ring-3 dark:text-zinc-400 dark:ring-zinc-200 dark:hover:text-zinc-200 dark:focus:ring-1 dark:focus:outline-hidden"
href={linkURL}
>
{linkTitle}
{isArrowVisible ? <Fragment set:html={arrowSVG} /> : null}
</a>
) : null
}
</div>
</div>

View File

@ -0,0 +1,28 @@
---
// Get heading and content from Astro props
const { heading, content } = Astro.props;
// Define TypeScript interface for props
interface Props {
heading?: string;
content?: string;
}
// Define classes for heading and content
const headingClasses =
'text-balance text-lg font-bold text-neutral-800 dark:text-neutral-200';
const contentClasses =
'mt-1 text-pretty text-neutral-700 dark:text-neutral-300';
---
{/* The root container that arranges your slot and the heading/content */}
<div class="flex gap-x-5">
{/* Slot to allow for extensibility of the component */}
<slot />
<div class="grow">
{/* Heading of the section */}
<h3 class={headingClasses}>
{heading}
</h3>
{/* Content text of the section */}
<p class={contentClasses}>{content}</p>
</div>
</div>

View File

@ -0,0 +1,49 @@
---
// Import the necessary modules
import { Image } from 'astro:assets';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
// Destructure the props passed to the Astro component
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } =
Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
img: any;
imgAlt: any;
}
---
{/* The root section of the component */}
<section
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
>
{/* The Image component which renders the image */}
<Image
class="w-full rounded-xl"
src={img}
alt={imgAlt}
draggable={'false'}
format={'avif'}
/>
{/* The container for title, subtitle, and optional CTA button */}
<div class="mt-4 md:mt-0">
{/* The title of the section */}
<h2
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
{/* The subtitle of the section */}
<p
class="mb-4 max-w-prose font-normal text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{/* Conditionally render the Primary CTA button if btnExists is true */}
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
</section>

View File

@ -0,0 +1,45 @@
---
// Import PrimaryCTA component
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
// Destructure the props passed to the Astro component
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
---
{/* Root section of the component */}
<section
class="mx-auto mt-10 max-w-[85rem] px-4 py-10 sm:px-6 sm:py-16 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
{/* Section title */}
<h1
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h1>
{/* Section subtitle */}
<p
class="mb-8 max-w-prose font-normal text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400"
>
{subTitle}
</p>
{
/* Conditional rendering of PrimaryCTA component if 'btnExists' property is truthy */
}
{
btnExists ? (
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
<PrimaryCTA title={btnTitle} url={btnURL} />
</div>
) : null
}
</div>
</section>

View File

@ -0,0 +1,61 @@
---
import Avatar from '@components/ui/avatars/Avatar.astro';
import FullStar from '@components/ui/stars/FullStar.astro';
import HalfStar from '@components/ui/stars/HalfStar.astro';
const { avatars, starCount = 0, rating, reviews } = Astro.props;
interface Props {
avatars?: Array<string>;
starCount?: number;
rating?: string;
reviews?: string;
}
---
<div class="mt-6 lg:mt-10">
<div class="py-5">
<div class="text-center sm:flex sm:items-center sm:text-start">
<div class="shrink-0 pb-5 sm:flex sm:pe-5 sm:pb-0">
{/* Avatar Group */}
<div class="flex justify-center -space-x-3">
{avatars?.map(src => <Avatar src={src} alt="Avatar Description" />)}
<span
class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-zinc-800 ring-2 ring-white dark:bg-zinc-900 dark:ring-zinc-800"
>
<span class="text-xs leading-none font-medium text-white uppercase"
>7k+</span
>
</span>
</div>
</div>
<div
class="mx-auto h-px w-32 border-t border-neutral-400 sm:mx-0 sm:h-8 sm:w-auto sm:border-s sm:border-t-0 dark:border-neutral-500"
>
</div>
{/* Review Ratings */}
<div class="flex flex-col items-center sm:items-start">
<div class="flex items-baseline space-x-1 pt-5 sm:ps-5 sm:pt-0">
<div class="flex space-x-1">
{/* Your star ratings */}
{
Array(starCount)
.fill(0)
.map((_, i) => <FullStar key={i} />)
}
{/* Adding additional half-star */}
<HalfStar />
</div>
<p class="text-neutral-800 dark:text-neutral-200">
<Fragment set:html={rating} />
</p>
</div>
<div class="text-sm text-neutral-800 sm:ps-5 dark:text-neutral-200">
<p>
<Fragment set:html={reviews} />
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,88 @@
---
// Import the required modules
import { Image } from 'astro:assets';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
// Extract properties from Astro.props
const {
title,
subTitle,
btnExists,
btnTitle,
btnURL,
single,
imgOne,
imgOneAlt,
imgTwo,
imgTwoAlt,
} = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
single?: boolean;
imgOne?: any;
imgOneAlt?: any;
imgTwo?: any;
imgTwoAlt?: any;
}
---
{/* Root section of the component */}
<section
class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div>
{/* Title of the section */}
<h2
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
{/* Subtitle of the section */}
<p
class="mb-4 max-w-prose font-normal text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{
/* Conditional rendering of the Primary Call-To-Action button if 'btnExists' is true */
}
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
{/* Conditionally render one or two images based on 'single' property */}
{
single ? (
<div class="mt-8">
{/* Single image */}
<Image
class="w-full rounded-lg"
src={imgOne}
alt={imgOneAlt}
format={'avif'}
/>
</div>
) : (
<div class="mt-8 grid grid-cols-2 gap-4">
{/* First image in a two-image layout */}
<Image
class="w-full rounded-xl"
src={imgOne}
alt={imgOneAlt}
draggable={'false'}
format={'avif'}
/>
{/* Second image in a two-image layout */}
<Image
class="mt-4 w-full rounded-xl lg:mt-10"
src={imgTwo}
alt={imgTwoAlt}
draggable={'false'}
format={'avif'}
/>
</div>
)
}
</section>

View File

@ -0,0 +1,17 @@
---
// Extract the properties from Astro.props
const { title, subTitle } = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title: string;
subTitle: string;
}
---
{/* Container for the title and subtitle */}
<div class="lg:pe-6 xl:pe-12">
<p class="text-6xl leading-10 font-bold text-orange-400 dark:text-orange-300">
{title}
</p>
<p class="mt-2 text-neutral-600 sm:mt-3 dark:text-neutral-400">{subTitle}</p>
</div>

View File

@ -0,0 +1,23 @@
---
import Icon from '@components/ui/icons/Icon.astro';
const { count, description, index } = Astro.props;
interface Props {
count: string;
description: string;
index: number;
}
---
<li class="-m-0.5 flex flex-col p-4 sm:p-8">
<div
class="mb-2 flex items-end gap-x-2 text-3xl font-bold text-neutral-800 sm:text-5xl dark:text-neutral-200"
>
{index === 1 || index === 2 ? <Icon name="arrowUp" /> : null}
{count}
</div>
<p class="text-sm text-neutral-600 sm:text-base dark:text-neutral-400">
{description}
</p>
</li>

View File

@ -0,0 +1,15 @@
---
// Extract the properties from Astro.props
const { title, subTitle } = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title: string;
subTitle: string;
}
---
{/* Container for title and subtitle */}
<div>
<p class="text-3xl font-bold text-orange-400 dark:text-orange-300">{title}</p>
<p class="mt-1 text-neutral-600 dark:text-neutral-400">{subTitle}</p>
</div>

View File

@ -0,0 +1,54 @@
---
// Import the Image component from astro:assets
import { Image } from 'astro:assets';
// Destructure the component properties from Astro.props
const { id, aria, src, alt, first, second } = Astro.props;
// Define TypeScript interface for the properties
interface Props {
id: string;
aria: string;
src?: any;
alt: string;
first?: boolean;
second?: boolean;
}
// Set class based on 'first' property
// If 'first' is present, show the tab content immediately
const firstClass = first ? '' : 'hidden';
// Set class based on 'second' property
// If 'second' is present, use an alternate style for the image
const secondClass = second
? 'shadow-xl aspect-video object-contain bg-neutral-300 dark:bg-neutral-600 p-3 lg:object-cover lg:aspect-square shadow-neutral-200 rounded-xl dark:shadow-neutral-900/[.2]'
: 'shadow-xl aspect-video object-cover lg:aspect-square shadow-neutral-200 rounded-xl dark:shadow-neutral-900/[.2]';
/*
first: This property should be set to true for the initial TabContent component
in your list to ensure that it's visible when the page first loads.
All subsequent TabContent components should omit this property or set it to false.
second: This property allows to control changes in the look of the Image.
If it is set to true, the Image will have different aspect ratio and background color.
If this property is not provided or is set to false, the Image will use default styling.
You can enable this for any TabContent component you want to apply these changes to.
This is the full example:
<TabContent id="" aria="" src="" alt="" first={true}/>
<TabContent id="" aria="" src="" alt="" second={true}/>
<TabContent id="" aria="" src="" alt="" />
*/
---
{/* Container for tab content that controls visibility and accessibility */}
<div id={id} role="tabpanel" class={firstClass} aria-labelledby={aria}>
<!-- Astro Image component to display the image with dynamic classes based on the 'second' property -->
<Image
src={src}
alt={alt}
class={secondClass}
draggable={'false'}
format={'avif'}
loading={'eager'}
/>
</div>

View File

@ -0,0 +1,59 @@
---
// Extract properties from Astro.props
const { aria, dataTab, id, heading, content, first } = Astro.props;
// Define TypeScript interface for properties
interface Props {
dataTab: string;
id: string;
aria: string;
heading?: string;
content?: string;
first?: boolean;
}
// Define button classes
const BUTTON_CLASS =
'dark:hover:bg-neutral-700 rounded-xl p-4 text-start outline-hidden ring-zinc-500 transition duration-300 hover:bg-neutral-200 focus-visible:ring-3 hs-tab-active:bg-neutral-50 hs-tab-active:shadow-md hs-tab-active:hover:border-transparent dark:ring-zinc-200 dark:focus:outline-hidden dark:hs-tab-active:bg-neutral-700/60 md:p-5';
/*
first: This property should be set to true for the initial TabNav component in your list
to ensure that it's visible when the page first loads. All subsequent TabNav components
should omit this property or set it to false.
Example:
<TabNav id="" dataTab="" aria="" heading="" paragraph="" first={true} />
<TabNav id="" dataTab="" aria="" heading="" paragraph="" />
<TabNav id="" dataTab="" aria="" heading="" paragraph="" />
*/
---
{
/* Tab button with dynamic class based on 'first' property, id, tab data, and aria-controls */
}
<button
type="button"
class={`${first ? 'active ' : ''}${BUTTON_CLASS}`}
id={id}
data-hs-tab={dataTab}
aria-controls={aria}
aria-selected={first ? 'true' : 'false'}
role="tab"
>
{/* Slot for additional content */}
<span class="flex">
<slot />
{/* Container for the heading and content of the tab */}
<span class="ms-6 grow">
{/* Heading of the tab, changes color when active */}
<span
class="hs-tab-active:text-orange-400 dark:hs-tab-active:text-orange-300 block text-lg font-bold text-neutral-800 dark:text-neutral-200"
>{heading}</span
>
{/* Content of the tab, changes color when active */}
<span
class="hs-tab-active:text-neutral-600 dark:hs-tab-active:text-neutral-200 mt-1 block text-neutral-500 dark:text-neutral-400"
>{content}</span
>
</span>
</span>
</button>

View File

@ -0,0 +1,25 @@
---
// Destructure the properties from Astro.props
const { title } = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title: string;
}
// Define CSS classes for styling the button
const baseClasses =
'inline-flex w-full items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-700 focus-visible:ring-3 outline-hidden transition duration-300';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-yellow-400 dark:focus:outline-hidden';
const hoverClasses = 'hover:bg-yellow-500';
const fontSizeClasses = '2xl:text-base';
const disabledClasses = 'disabled:pointer-events-none disabled:opacity-50';
const ringClasses = 'ring-zinc-500 dark:ring-zinc-200';
---
{/* Styled submit button with dynamic title */}
<button
type="submit"
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${fontSizeClasses} ${disabledClasses} ${ringClasses}`}
>{title}</button
>

View File

@ -0,0 +1,95 @@
---
import Icon from '@components/ui/icons/Icon.astro';
---
<button
type="button"
class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:bg-neutral-100 focus:outline-hidden focus-visible:ring-1 focus-visible:outline-hidden dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700"
data-bookmark-button="bookmark-button"
>
<Icon name="bookmark" />
</button>
<script>
class Bookmark {
private static readonly BOOKMARKS_KEY = 'bookmarks';
private bookmarkButton: Element | null;
constructor(private dataAttrValue: string) {
this.bookmarkButton = document.querySelector(
`[data-bookmark-button="${dataAttrValue}"]`
);
}
private getStoredBookmarks(): string[] {
const item = localStorage.getItem(Bookmark.BOOKMARKS_KEY);
return item ? JSON.parse(item) : [];
}
init(): void {
if (this.bookmarkButton && this.isStored()) {
this.markAsStored();
}
this.bookmarkButton?.addEventListener('click', () =>
this.toggleBookmark()
);
}
isStored(): boolean {
return this.getStoredBookmarks().includes(window.location.pathname);
}
markAsStored(): void {
if (this.bookmarkButton) {
this.bookmarkButton.classList.add('bookmarked');
let svgElement = this.bookmarkButton.querySelector('svg');
if (svgElement) {
svgElement.setAttribute(
'class',
'h-6 w-6 fill-red-500 dark:fill-red-500'
);
}
let pathElement = svgElement?.querySelector('path');
if (pathElement) {
pathElement.setAttribute(
'class',
'fill-current text-red-500 dark:text-red-500'
);
}
}
}
unmarkAsStored(): void {
if (this.bookmarkButton) {
this.bookmarkButton.classList.remove('bookmarked');
let svgElement = this.bookmarkButton.querySelector('svg');
if (svgElement) {
svgElement.setAttribute('class', 'h-6 w-6 fill-none');
}
let pathElement = svgElement?.querySelector('path');
if (pathElement) {
pathElement.setAttribute(
'class',
'fill-current text-neutral-500 group-hover:text-red-400 dark:text-neutral-500 dark:group-hover:text-red-400'
);
}
}
}
toggleBookmark(): void {
let storedBookmarks = this.getStoredBookmarks();
const index = storedBookmarks.indexOf(window.location.pathname);
if (index !== -1) {
storedBookmarks.splice(index, 1);
this.unmarkAsStored();
} else {
storedBookmarks.push(window.location.pathname);
this.markAsStored();
}
localStorage.setItem(
Bookmark.BOOKMARKS_KEY,
JSON.stringify(storedBookmarks)
);
}
}
new Bookmark('bookmark-button').init();
</script>

View File

@ -0,0 +1,30 @@
---
import Icon from '@components/ui/icons/Icon.astro';
// Destructure the properties from Astro.props
const { title, id, noArrow } = Astro.props;
// Define TypeScript interface for the properties
interface Props {
title?: string;
id?: string;
noArrow?: boolean;
}
// Define CSS classes for styling the button
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-zinc-500 transition duration-300 focus-visible:ring-3 outline-hidden';
const borderClasses = 'border border-transparent';
const bgColorClasses =
'bg-orange-400 hover:bg-orange-500 active:bg-orange-500 dark:focus:outline-hidden';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-zinc-200';
---
{/* Button with dynamic title, id, and optional arrow */}
<button
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
id={id}
>
{title}
{/* Display the arrow based on the 'noArrow' property */}
{noArrow ? null : <Icon name="arrowRight" />}
</button>

View File

@ -0,0 +1,27 @@
---
import Icon from '@components/ui/icons/Icon.astro';
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-3 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-700 ring-zinc-500 focus-visible:ring-3 transition duration-300 outline-hidden';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-yellow-400 dark:focus:outline-hidden';
const hoverClasses = 'hover:shadow-2xl hover:shadow-yellow-500';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-zinc-200';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${fontSizeClasses} ${ringClasses}`}
href={url}
target="_blank"
rel="noopener noreferrer"
>
<Icon name="github" />
{title}
</a>

View File

@ -0,0 +1,47 @@
---
const { title } = Astro.props;
interface Props {
title: string;
}
const baseClasses =
'inline-flex w-full items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm dark:text-neutral-400 font-medium text-neutral-600 shadow-xs transition duration-300 focus-visible:ring-3 outline-hidden';
const borderClasses = 'border border-neutral-200 dark:border-neutral-700';
const bgColorClasses =
'bg-neutral-50 dark:bg-neutral-800 hover:bg-neutral-200 dark:hover:bg-neutral-900';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const ringClasses = 'ring-zinc-500 dark:ring-zinc-200';
const googleSVG = `<svg
class="h-auto w-4"
width="46"
height="47"
viewBox="0 0 46 47"
fill="none"
>
<path
d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z"
fill="#4285F4"></path>
<path
d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z"
fill="#34A853"></path>
<path
d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z"
fill="#FBBC05"></path>
<path
d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z"
fill="#EB4335"></path>
</svg>`;
---
<button
type="button"
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${ringClasses}`}
>
{
/* About Fragment: https://docs.astro.build/en/basics/astro-syntax/#fragments */
}
<Fragment set:html={googleSVG} />
{title}
</button>

View File

@ -0,0 +1,42 @@
---
const { title = 'Log in' } = Astro.props;
interface Props {
title?: string;
}
const baseClasses =
'flex items-center gap-x-2 text-base md:text-sm font-medium text-neutral-600 ring-zinc-500 transition duration-300 focus-visible:ring-3 outline-hidden';
const hoverClasses = 'hover:text-orange-400 dark:hover:text-orange-300';
const darkClasses =
'dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:focus:outline-hidden';
const mdClasses = 'md:my-6 md:border-s md:border-neutral-300 md:ps-6';
const txtSizeClasses = '2xl:text-base';
const userSVG = `<svg
class="h-4 w-4 shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>`;
---
<button
type="button"
class={`${baseClasses} ${hoverClasses} ${darkClasses} ${mdClasses} ${txtSizeClasses}`}
data-hs-overlay="#hs-toggle-between-modals-login-modal"
>
{
/* About Fragment: https://docs.astro.build/en/basics/astro-syntax/#fragments */
}
<Fragment set:html={userSVG} />
{title}
</button>

Some files were not shown because too many files have changed in this diff Show More