feat: rebuild website on ScrewFast foundation #393
22
apps/website/.gitignore
vendored
Normal file
22
apps/website/.gitignore
vendored
Normal 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
20
apps/website/.prettierrc
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
27
apps/website/THIRD_PARTY_NOTICES.md
Normal file
27
apps/website/THIRD_PARTY_NOTICES.md
Normal 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.
|
||||
@ -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()],
|
||||
},
|
||||
});
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
5867
apps/website/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
23
apps/website/process-html.mjs
Normal file
23
apps/website/process-html.mjs
Normal 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);
|
||||
})
|
||||
);
|
||||
29
apps/website/public/banner-pattern.svg
Normal file
29
apps/website/public/banner-pattern.svg
Normal 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 |
@ -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 |
@ -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 |
@ -1,3 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: /sitemap.xml
|
||||
9
apps/website/src/assets/scripts/lenisSmoothScroll.js
Normal file
9
apps/website/src/assets/scripts/lenisSmoothScroll.js
Normal 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,
|
||||
});
|
||||
127
apps/website/src/assets/styles/global.css
Normal file
127
apps/website/src/assets/styles/global.css
Normal 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;
|
||||
}
|
||||
}
|
||||
22
apps/website/src/assets/styles/lenis.css
Normal file
22
apps/website/src/assets/styles/lenis.css
Normal 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;
|
||||
}
|
||||
191
apps/website/src/assets/styles/starlight.css
Normal file
191
apps/website/src/assets/styles/starlight.css
Normal 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;
|
||||
}
|
||||
}
|
||||
100
apps/website/src/assets/styles/starlight_main.css
Normal file
100
apps/website/src/assets/styles/starlight_main.css
Normal 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);
|
||||
}
|
||||
13
apps/website/src/components/BrandLogo.astro
Normal file
13
apps/website/src/components/BrandLogo.astro
Normal 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>
|
||||
138
apps/website/src/components/Meta.astro
Normal file
138
apps/website/src/components/Meta.astro
Normal 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" />
|
||||
59
apps/website/src/components/ThemeIcon.astro
Normal file
59
apps/website/src/components/ThemeIcon.astro
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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,
|
||||
]}
|
||||
/>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
112
apps/website/src/components/sections/landing/HeroSection.astro
Normal file
112
apps/website/src/components/sections/landing/HeroSection.astro
Normal 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>
|
||||
@ -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>
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
---
|
||||
|
||||
<PrimaryCTA title="Request walkthrough" url="/contact" />
|
||||
122
apps/website/src/components/sections/misc/ContactSection.astro
Normal file
122
apps/website/src/components/sections/misc/ContactSection.astro
Normal 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>
|
||||
79
apps/website/src/components/sections/misc/FAQ.astro
Normal file
79
apps/website/src/components/sections/misc/FAQ.astro
Normal 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>
|
||||
@ -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>
|
||||
205
apps/website/src/components/sections/navbar&footer/Navbar.astro
Normal file
205
apps/website/src/components/sections/navbar&footer/Navbar.astro
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
18
apps/website/src/components/ui/avatars/Avatar.astro
Normal file
18
apps/website/src/components/ui/avatars/Avatar.astro
Normal 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'}
|
||||
/>
|
||||
22
apps/website/src/components/ui/avatars/AvatarBlog.astro
Normal file
22
apps/website/src/components/ui/avatars/AvatarBlog.astro
Normal 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>
|
||||
22
apps/website/src/components/ui/avatars/AvatarBlogLarge.astro
Normal file
22
apps/website/src/components/ui/avatars/AvatarBlogLarge.astro
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
51
apps/website/src/components/ui/blocks/AccordionItem.astro
Normal file
51
apps/website/src/components/ui/blocks/AccordionItem.astro
Normal 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>
|
||||
66
apps/website/src/components/ui/blocks/ContactIconBlock.astro
Normal file
66
apps/website/src/components/ui/blocks/ContactIconBlock.astro
Normal 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>
|
||||
28
apps/website/src/components/ui/blocks/IconBlock.astro
Normal file
28
apps/website/src/components/ui/blocks/IconBlock.astro
Normal 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>
|
||||
49
apps/website/src/components/ui/blocks/LeftSection.astro
Normal file
49
apps/website/src/components/ui/blocks/LeftSection.astro
Normal 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>
|
||||
45
apps/website/src/components/ui/blocks/MainSection.astro
Normal file
45
apps/website/src/components/ui/blocks/MainSection.astro
Normal 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>
|
||||
61
apps/website/src/components/ui/blocks/ReviewComponent.astro
Normal file
61
apps/website/src/components/ui/blocks/ReviewComponent.astro
Normal 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>
|
||||
88
apps/website/src/components/ui/blocks/RightSection.astro
Normal file
88
apps/website/src/components/ui/blocks/RightSection.astro
Normal 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>
|
||||
17
apps/website/src/components/ui/blocks/StatsBig.astro
Normal file
17
apps/website/src/components/ui/blocks/StatsBig.astro
Normal 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>
|
||||
23
apps/website/src/components/ui/blocks/StatsGrid.astro
Normal file
23
apps/website/src/components/ui/blocks/StatsGrid.astro
Normal 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>
|
||||
15
apps/website/src/components/ui/blocks/StatsSmall.astro
Normal file
15
apps/website/src/components/ui/blocks/StatsSmall.astro
Normal 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>
|
||||
54
apps/website/src/components/ui/blocks/TabContent.astro
Normal file
54
apps/website/src/components/ui/blocks/TabContent.astro
Normal 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>
|
||||
59
apps/website/src/components/ui/blocks/TabNav.astro
Normal file
59
apps/website/src/components/ui/blocks/TabNav.astro
Normal 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>
|
||||
25
apps/website/src/components/ui/buttons/AuthBtn.astro
Normal file
25
apps/website/src/components/ui/buttons/AuthBtn.astro
Normal 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
|
||||
>
|
||||
95
apps/website/src/components/ui/buttons/Bookmark.astro
Normal file
95
apps/website/src/components/ui/buttons/Bookmark.astro
Normal 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>
|
||||
30
apps/website/src/components/ui/buttons/Btn404.astro
Normal file
30
apps/website/src/components/ui/buttons/Btn404.astro
Normal 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>
|
||||
27
apps/website/src/components/ui/buttons/GithubBtn.astro
Normal file
27
apps/website/src/components/ui/buttons/GithubBtn.astro
Normal 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>
|
||||
47
apps/website/src/components/ui/buttons/GoogleBtn.astro
Normal file
47
apps/website/src/components/ui/buttons/GoogleBtn.astro
Normal 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>
|
||||
42
apps/website/src/components/ui/buttons/LoginBtn.astro
Normal file
42
apps/website/src/components/ui/buttons/LoginBtn.astro
Normal 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
Loading…
Reference in New Issue
Block a user