Compare commits

..

35 Commits

Author SHA1 Message Date
55338a88c6 merge: platform-dev into dev (#311)
Some checks failed
Main Confidence / confidence (push) Failing after 59s
PR Fast Feedback / fast-feedback (pull_request) Failing after 46s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
## Summary
- sync platform-dev back into dev with the latest integrated feature and spec work
- include the customer review workspace productization flow and its related review, review-pack, evidence, audit, and test updates
- carry forward the recent governance and roadmap/spec updates already merged on platform-dev

## Included highlights
- customer review workspace productization and customer-safe released-review drilldown
- governance decision convergence work
- cross-tenant compare and promotion work
- external support desk handoff work
- product, roadmap, permissions, and spec artifact updates

## Validation context
- platform-dev currently contains the already-validated feature work from the merged branch PRs
- latest customer review workspace batch included focused Pest suites, one bounded browser smoke, and Pint

## Notes
- this is an integration PR from platform-dev into dev
- no separate provider-registration or asset-strategy expansion is introduced by the customer review workspace slice

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #311
2026-04-30 18:33:56 +00:00
e1136ac6e9 Merge platform-dev into dev (automated) (#309)
Some checks failed
Main Confidence / confidence (push) Failing after 54s
Automatischer Commit und PR erstellt auf Anfrage.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #309
2026-04-30 14:41:01 +00:00
61feb48d8a chore(platform): merge platform-dev into dev (#308)
Some checks failed
Main Confidence / confidence (push) Failing after 54s
Integrates latest TenantPilot platform changes from `platform-dev` into `dev`.

Refresh method in this update: merge from `origin/dev` into `platform-dev` on explicit user request.

This PR was created by agent on user request; do not merge automatically.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #308
2026-04-30 07:52:08 +00:00
905b595880 chore(sync): platform-dev → dev (#306)
Some checks failed
Main Confidence / confidence (push) Failing after 55s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
Automatisch erstellter PR: Synchronisiere `platform-dev` nach `dev`.

Enthält alle Änderungen, die aktuell in `platform-dev` vorhanden sind. Bitte Review und Merge gegen `dev`.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #306
2026-04-29 22:44:27 +00:00
7b394918ce chore(platform): merge platform-dev into dev (#302)
Some checks failed
Main Confidence / confidence (push) Failing after 1m48s
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m43s
Integrates latest TenantPilot platform changes from `platform-dev` into `dev`.

This PR was created by agent on user request; do not merge automatically.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #302
2026-04-29 20:53:36 +00:00
4b36d2c64f Automated PR: platform-dev → dev (#300)
Some checks failed
Main Confidence / confidence (push) Failing after 1m0s
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m50s
Automated PR created by Copilot. Commit: 4b0dc2a62e

This PR merges branch `platform-dev` into `dev`.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #300
2026-04-29 13:01:43 +00:00
ab9c36f21e Automatische PR: platform-dev → dev (#299)
Some checks failed
Main Confidence / confidence (push) Failing after 59s
Automatisch erstellt: Merge `platform-dev` into `dev` (via MCP)

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #299
2026-04-29 12:37:48 +00:00
54fb65a63a chore: promote platform-dev to dev (#297)
Some checks failed
Main Confidence / confidence (push) Failing after 54s
This pull request promotes the current state of `platform-dev` to the main integration branch `dev`. It includes recent features, fixes, and architectural refinements validated on the platform development track.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #297
2026-04-29 07:50:16 +00:00
29ad8852ca merge: platform-dev into dev (#295)
Some checks failed
Main Confidence / confidence (push) Failing after 1m1s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
## Summary
- integrate the current `platform-dev` branch into `dev`
- bring the latest platform work from the integration branch into the main development branch
- include the recent findings lifecycle backfill removal slice together with the already accumulated `platform-dev` changes

## Scope
- source branch: `platform-dev`
- target branch: `dev`
- branch role: integration PR, not a single-feature PR

## Validation
- branch state reviewed before PR creation
- `platform-dev` is ahead of `dev` with the expected integration history
- this PR intentionally carries the accumulated `platform-dev` commits into `dev`

## Notes
- this is the correct merge direction for the current workflow, where feature branches land in `platform-dev` first and `platform-dev` is then merged into `dev`
- after merging, `platform-dev` can be recreated fresh from `dev` as usual

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #295
2026-04-28 22:11:20 +00:00
7613e339c4 feat: implement platform localization v1 (#293)
Some checks failed
Main Confidence / confidence (push) Failing after 56s
## Summary
- add the localization v1 foundation with request-time locale resolution and workspace or user preference handling
- localize the first-wave platform surfaces for auth, shell, dashboards, findings, baseline compare, and review workspace chrome
- add Pest coverage for locale resolution, preference flows, fallback behavior, notifications, and governance surface localization

## Scope
- active spec: specs/252-platform-localization-v1
- target branch: dev

## Notes
- machine-readable artifacts remain invariant and are not localized in this slice
- the branch includes the related spec kit artifacts for the feature

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #293
2026-04-28 19:45:03 +00:00
7ee4909212 feat: commercial lifecycle overlay for workspace entitlements (#292)
Some checks failed
Main Confidence / confidence (push) Failing after 1m45s
## Summary
- add the bounded workspace commercial lifecycle overlay from spec 251 on top of the existing entitlement substrate
- expose audited commercial state inspection and mutation on the system workspace detail surface
- gate onboarding activation and review-pack start actions through the shared lifecycle decision while preserving suspended read-only access to existing review, evidence, and generated-pack history
- add focused Pest coverage plus the spec/plan/tasks/data-model/contract artifacts for the feature

## Validation
- targeted Pest unit and feature lanes for lifecycle resolution, system-plane mutation, onboarding gating, review-pack enforcement, download preservation, customer review workspace access, and evidence snapshot access
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- integrated browser smoke on the system workspace detail and the preserved read-only review/evidence/review-pack surfaces

## Notes
- branch: `251-commercial-entitlements-billing-state`
- base: `dev`
- commit: `606e9760`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #292
2026-04-28 13:39:33 +00:00
72bfb37ba7 feat: add decision-based governance inbox (#291)
Some checks failed
Main Confidence / confidence (push) Failing after 57s
## Summary
- add a read-first governance inbox page at `/admin/governance/inbox`
- aggregate assigned findings, intake, stale operations, alert-delivery failures, and review follow-up into one canonical routing surface
- add focused coverage for inbox authorization, navigation context, page behavior, and section builder logic
- include the Spec Kit artifacts for spec 250

## Notes
- branch is synced with `dev`
- this PR supersedes #290 for the governance inbox work

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #291
2026-04-28 10:13:09 +00:00
aacd82849a feat(reviews): add CustomerReviewWorkspace with audit logging and RBAC enforcement (#289)
Some checks failed
Main Confidence / confidence (push) Failing after 54s
Add `CustomerReviewWorkspace` page for tenant pre-filtered reviews
Add customer workspace links to `EvidenceSnapshotResource`, `ReviewPackResource`, and `TenantReviewResource`
Implement audit logging for `TenantReviewOpened` and `ReviewPackDownloaded` actions
Update ReviewPack download controller to enforce tenant-scoped RBAC
Add tests for ReviewPack download authorization and audit logging

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #289
2026-04-28 07:15:41 +00:00
ff3392892b Merge 248-private-ai-policy-foundation into dev (#288)
Some checks failed
Main Confidence / confidence (push) Failing after 56s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
Automated PR: merge branch 248-private-ai-policy-foundation into dev (created by Copilot)

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #288
2026-04-27 21:18:37 +00:00
e222845a36 247: plans entitlements billing readiness (#287)
Some checks failed
Main Confidence / confidence (push) Failing after 53s
Automated commit and PR created by Copilot per user request.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #287
2026-04-27 17:35:04 +00:00
6e3736a53f Add in-app support request with context (#285)
Some checks failed
Main Confidence / confidence (push) Failing after 1m29s
## Summary
- add the first in-app support request flow with an immutable `SupportRequest` record, canonical context builder, submission service, and generated internal reference
- expose contextual support-request actions from the tenant dashboard and operation run surfaces, including audit logging and support-safe diagnostic capture rules
- add Pest coverage plus the `specs/246-support-request-context` artifacts for the new support-request slice

## Testing
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/SupportRequests/OperationRunSupportRequestActionTest.php tests/Feature/SupportRequests/SupportRequestAuditTest.php tests/Feature/SupportRequests/SupportRequestAuthorizationTest.php tests/Feature/SupportRequests/TenantSupportRequestActionTest.php tests/Unit/Support/SupportRequests/SupportRequestContextBuilderTest.php tests/Unit/Support/SupportRequests/SupportRequestReferenceTest.php`

## Notes
- this PR supersedes the earlier session-branch PR opened from `246-support-request-context-session-1777289015`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #285
2026-04-27 12:51:39 +00:00
86505483bf feat(customer-health): add decision card to tenant/workspace detail (spec 245) (#283)
Some checks failed
Main Confidence / confidence (push) Failing after 52s
Add Customer Health decision card to tenant & workspace detail pages (spec 245).

What I changed:
- Render a decision-first Customer Health card on tenant and workspace detail pages.
- Reuse `WorkspaceHealthSummaryQuery` and preserve `window` query param.
- Update attention widget link text to "Review health details" and include `?window=`.
- Add/adjust tests to cover new behavior and explainability.
- Run Pint formatting.

Compare URL: https://git.cloudarix.de/ahmido/TenantAtlas/compare/dev...245-customer-health-score

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #283
2026-04-27 08:30:01 +00:00
bf43e55848 feat(onboarding): decision-first verify-step & contextual-help callout fix (#282)
Some checks failed
Main Confidence / confidence (push) Failing after 53s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
This PR implements the onboarding verify-step changes and ProductKnowledge contextual-help fixes:

- Hide the top-level "Permission diagnostics" when a stored verification report exists.
- Move permission details to "Supporting evidence" / "View required permissions" and into stored report technical details.
- Rename "Current checkpoint" to "Step" in onboarding readiness.
- Rename the inner verification card title to "Stored verification details" to avoid duplicate headings.
- Keep "Grant admin consent" as primary CTA when admin consent is the dominant blocker by deriving the CTA from the verification primary reason.
- Replace the custom Safe Next Action with Filament `Callout` for correct dark-mode styling.
- Add/adjust focused feature tests proving the above behaviors.

Verification:
- Tests: 36 passed (173 assertions) locally.
- Pint: pass.

Created from local session branch `244-product-knowledge-contextual-help-session-1777248340`. Please review and merge into `dev` when ready.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #282
2026-04-27 00:09:46 +00:00
6053d87b99 feat: implement product usage adoption telemetry (#281)
Some checks failed
Main Confidence / confidence (push) Failing after 48s
## Summary
- implement spec 243 product usage adoption telemetry end-to-end
- add bounded product usage event capture, aggregation, retention pruning, and system dashboard KPIs
- add unit and feature coverage for telemetry capture, authorization, retention, privacy, and dashboard window behavior

## Validation
- ran focused Pest test suites for telemetry and system dashboard behavior
- ran Laravel Pint formatting
- verified the system dashboard telemetry widget in the integrated browser

## Notes
- branch: `243-product-usage-adoption-telemetry`
- target: `dev`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #281
2026-04-26 20:52:38 +00:00
d96abc65fb Remove Findings lifecycle backfill operational surface (controls slice) (#280)
Some checks failed
Main Confidence / confidence (push) Failing after 1m23s
Removes the Findings lifecycle backfill from the Operational Controls UI and OperationalControlCatalog.

This patch is a safe, controls-only change; runbooks, jobs and other runtime artifacts are NOT removed yet. Follow-up work will delete the runbook service/scope, jobs, commands, and update tests.

Files changed:
- apps/platform/app/Filament/System/Pages/Ops/Controls.php
- apps/platform/app/Support/OperationalControls/OperationalControlCatalog.php
- apps/platform/tests/Feature/System/OpsControls/OperationalControlManagementTest.php
- apps/platform/tests/Unit/Support/OperationalControls/OperationalControlCatalogTest.php
- apps/platform/tests/Unit/Support/OperationalControls/OperationalControlScopeResolutionTest.php

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #280
2026-04-26 15:43:47 +00:00
17d3ca8313 feat(support-diagnostics): guardrail refactor and UI polish (agent) (#278)
Some checks failed
Main Confidence / confidence (push) Failing after 45s
Implements support diagnostics bundle, moves audit writes to action mountUsing to avoid side-effects during render, replaces custom slide-over with Filament-native schema, updates tests and adds spec docs.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #278
2026-04-25 23:32:30 +00:00
ab6eccaf40 feat: add onboarding readiness workflow (#277)
Some checks failed
Main Confidence / confidence (push) Failing after 48s
## Summary
- add derived onboarding readiness to the managed tenant onboarding workflow and multi-draft picker
- keep provider-specific permission diagnostics secondary while preserving canonical `Open operation` and existing onboarding action semantics
- add spec-kit artifacts for `240-tenant-onboarding-readiness` and align roadmap/spec-candidate planning notes
- unify the required-permissions empty state copy to English

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- browser smoke exercised the onboarding picker, route-bound mismatch readiness state, canonical `Open operation` path, and local fixture cleanup

## Notes
- branch includes the generated spec artifacts under `specs/240-tenant-onboarding-readiness/`
- temporary browser smoke tenants/drafts/runs were cleaned from the local environment after validation

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #277
2026-04-25 21:17:31 +00:00
fb32e9bfa5 feat: canonical operation type source of truth (#276)
Some checks failed
Main Confidence / confidence (push) Failing after 49s
## Summary
- implement the canonical operation type source-of-truth slice across operation writers, monitoring surfaces, onboarding flows, and supporting services
- add focused contract and regression coverage for canonical operation type handling
- include the generated spec 239 artifacts for the feature slice

## Validation
- browser smoke PASS for `/admin` -> workspace overview -> operations -> operation detail -> tenant-scoped operations drilldown
- spec/plan/tasks/quickstart artifact analysis cleaned up to a no-findings state
- automated test suite not run in this session

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #276
2026-04-25 18:11:23 +00:00
58f9bb7355 chore: commit all workspace changes (#275)
Some checks failed
Main Confidence / confidence (push) Failing after 1m34s
Auto-generated PR: commit all workspace changes (includes .github/skills addition).

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #275
2026-04-25 09:13:54 +00:00
110245a9ec feat: neutralize provider connection target-scope surfaces (#274)
Some checks are pending
Main Confidence / confidence (push) Waiting to run
## Summary
- add a shared provider target-scope descriptor, normalizer, identity-context metadata, and surface-summary layer
- update provider connection list, detail, create, edit, and onboarding surfaces to use neutral target-scope vocabulary while keeping Microsoft identity contextual
- align provider connection audit and resolver output with the neutral target-scope contract and add focused guard/unit/feature coverage for regressions

## Validation
- browser smoke: opened the tenant-scoped provider connection list, drilled into detail, and verified the edit/create surfaces in local admin context

## Notes
- this PR comes from the session branch created for the active feature work
- no additional runtime or persistence layer was introduced in this slice

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #274
2026-04-25 09:07:40 +00:00
bd26e209de feat: harden provider boundaries (#273)
Some checks failed
Main Confidence / confidence (push) Failing after 57s
## Summary
- add the provider boundary catalog, boundary support types, and guardrails for platform-core versus provider-owned seams
- harden provider gateway, identity resolution, operation registry, and start-gate behavior to require explicit provider bindings
- add unit and feature coverage for boundary classification, runtime preservation, unsupported paths, and platform-core leakage guards
- add the full Spec Kit artifact set for spec 237 and update roadmap/spec-candidate tracking

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderGatewayTest.php tests/Unit/Providers/ProviderIdentityResolverTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- browser smoke: `http://localhost/admin/provider-connections?tenant_id=18000000-0000-4000-8000-000000000180` loaded with the local smoke user, the empty-state CTA reached the canonical create route, and cancel returned to the scoped list

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #273
2026-04-24 21:05:37 +00:00
6a5b8a3a11 feat: canonical control catalog foundation (#272)
Some checks failed
Main Confidence / confidence (push) Failing after 50s
## Summary
- add a config-seeded canonical control catalog plus shared resolution primitives and Microsoft subject bindings
- propagate canonical control references into findings-derived evidence snapshots and tenant review composition
- add the feature spec artifacts and focused Pest coverage, plus the supporting workspace and Sail helper adjustments included in this branch

## Testing
- cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Governance/CanonicalControlCatalogTest.php tests/Unit/Governance/CanonicalControlResolverTest.php tests/Feature/Governance/CanonicalControlResolutionIntegrationTest.php tests/Feature/Evidence/EvidenceSnapshotCanonicalControlReferenceTest.php tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/PlatformRelocation/CommandModelSmokeTest.php
- cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #272
2026-04-24 12:26:02 +00:00
2752515da5 Spec 235: harden baseline truth and onboarding flows (#271)
Some checks failed
Main Confidence / confidence (push) Failing after 54s
## Summary
- harden baseline capture truth, compare readiness, and monitoring explanations around latest inventory eligibility, blocked prerequisites, and zero-subject outcomes
- improve onboarding verification and bootstrap recovery handling, including admin-consent callback invalidation and queued execution legitimacy/report behavior
- align workspace findings/workspace overview signals and refresh the related spec, roadmap, and spec-candidate artifacts

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/BaselineDriftEngine/BaselineCaptureAuditEventsTest.php tests/Feature/BaselineDriftEngine/BaselineSnapshotNoTenantIdentifiersTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineFullContentOnDemandTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineMetaFallbackTest.php tests/Feature/Baselines/BaselineCaptureTest.php tests/Feature/Baselines/BaselineCompareFindingsTest.php tests/Feature/Baselines/BaselineSnapshotBackfillTest.php tests/Feature/Filament/BaselineCaptureResultExplanationSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Monitoring/AuditCoverageGovernanceTest.php tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php tests/Feature/Notifications/OperationRunNotificationTest.php tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/AdminConsentCallbackTest.php tests/Feature/Filament/WorkspaceOverviewDbOnlyTest.php tests/Feature/Guards/Spec194GovernanceActionSemanticsGuardTest.php tests/Feature/ManagedTenantOnboardingWizardTest.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/Operations/QueuedExecutionAuditTrailTest.php tests/Unit/Operations/QueuedExecutionLegitimacyGateTest.php`

## Notes
- browser validation was not re-run in this pass

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #271
2026-04-24 05:44:54 +00:00
603d509b8f cleanup: retire dead transitional residue (#270)
Some checks failed
Main Confidence / confidence (push) Failing after 58s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
## Summary
- remove deprecated baseline profile status alias constants and keep baseline lifecycle semantics on the canonical enum path
- retire the dead tenant app-status badge/default-fixture residue from the active runtime support path
- add the `234-dead-transitional-residue` spec, plan, research, data-model, quickstart, checklist, and task artifacts plus focused regression assertions

## Validation
- not rerun during this PR creation step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #270
2026-04-23 16:54:48 +00:00
6fdd45fb02 feat: surface stale active operation runs (#269)
Some checks failed
Main Confidence / confidence (push) Failing after 53s
## Summary
- keep stale active operation runs visible in the tenant progress overlay and polling state
- align tenant and canonical operation surfaces around the shared stale-active presentation contract
- add Spec 233 artifacts and clean the promoted-candidate backlog entries

## Validation
- browser smoke: `/admin/t/18000000-0000-4000-8000-000000000180` -> stale dashboard CTA -> `/admin/operations?tenant_id=7&activeTab=active_stale_attention&problemClass=active_stale_attention` -> `/admin/operations/15`
- verified healthy vs likely-stale tenant cards, canonical stale list row, and canonical run detail consistency

## Notes
- local smoke fixture seeded with one fresh and one stale running `baseline_compare` operation for browser validation
- Pest suite was not re-run in this session before opening this PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #269
2026-04-23 15:10:06 +00:00
2bf53f6337 Enforce operation run link contract (#268)
Some checks failed
Main Confidence / confidence (push) Failing after 44s
## Summary
- enforce shared operation run link generation across admin and system surfaces
- add guard coverage to block new raw operation route bypasses outside explicit exceptions
- harden Filament theme asset resolution so stale or wrong-stack hot files fall back to built assets

## Testing
- export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
- export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/InventoryCoverageRunContinuityTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php tests/Feature/078/RelatedLinksOnDetailTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/Guards/OperationRunLinkContractGuardTest.php tests/Unit/Filament/PanelThemeAssetTest.php

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #268
2026-04-23 13:09:53 +00:00
421261a517 feat: implement finding outcome taxonomy (#267)
Some checks failed
Main Confidence / confidence (push) Failing after 48s
## Summary
- implement the finding outcome taxonomy end-to-end with canonical resolve, close, reopen, and verification semantics
- align finding UI, filters, audit metadata, review summaries, and export/read-model consumers to the shared outcome semantics
- add focused Pest coverage and complete the spec artifacts for feature 231

## Details
- manual resolve is limited to the canonical `remediated` outcome
- close and reopen flows now use bounded canonical reasons
- trusted system clear and reopen distinguish verified-clear from verification-failed and recurrence paths
- duplicate lifecycle backfill now closes findings canonically as `duplicate`
- accepted-risk recording now uses the canonical `accepted_risk` reason
- finding detail and list surfaces now expose terminal outcome and verification summaries
- review, snapshot, and review-pack consumers now propagate the same outcome buckets

## Filament / Platform Contract
- Livewire v4.0+ compatibility remains intact
- provider registration is unchanged and remains in `bootstrap/providers.php`
- no new globally searchable resource was introduced; `FindingResource` still has a View page and `TenantReviewResource` remains globally searchable false
- lifecycle mutations still run through confirmed Filament actions with capability enforcement
- no new asset family was added; the existing `filament:assets` deploy step is unchanged

## Verification
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingWorkflowServiceTest.php tests/Feature/Findings/FindingRecurrenceTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Filament/FindingResolvedReferencePresentationTest.php tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php tests/Feature/Findings/FindingRiskGovernanceProjectionTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings tests/Feature/Filament/FindingResolvedReferencePresentationTest.php tests/Feature/Models/FindingResolvedTest.php tests/Unit/Findings/FindingWorkflowServiceTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewRegisterTest.php tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php`
- browser smoke: `/admin/findings/my-work` -> finding detail resolve flow -> queue regression check passed

## Notes
- this commit also includes the existing `.github/agents/copilot-instructions.md` workspace change that was already present in the worktree when all changes were committed

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #267
2026-04-23 07:29:05 +00:00
76334cb096 chore: migrate repo to managed spec-kit (#266)
Some checks failed
Main Confidence / confidence (push) Failing after 54s
Heavy Governance Lane / heavy-governance (push) Has been skipped
Browser Lane / browser (push) Has been skipped
## Summary

Selective migration to the managed Spec Kit project structure.

## Included

- add managed Spec Kit integration metadata under `.specify/`
- add bundled `speckit` workflow registry
- add bundled `git` extension, scripts, and config
- add new `speckit.git.*` command surfaces for Copilot, Gemini, and `.agents`
- add the Spec Kit plan marker block to `.github/copilot-instructions.md`

## Intentionally excluded

- no replacement of the existing customized core `speckit.*.agent.md` files
- no `.vscode/settings.json` commit; the copied manifest was adjusted accordingly
- no changes to the active `specs/231-finding-outcome-taxonomy` work

## Validation

- `specify integration list`
- `specify workflow list`
- `specify extension list`
- focused managed-file diff review

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #266
2026-04-22 22:29:05 +00:00
742d65f0d9 feat: converge findings notification presentation (#265)
Some checks failed
Main Confidence / confidence (push) Failing after 51s
## Summary
- converge finding, queued, and completed database notifications on one shared `OperationUxPresenter` presentation contract
- preserve existing finding and operation deep-link authorities while standardizing title, body, status/icon treatment, and single primary action
- add focused notification, findings, and guard coverage plus the full feature 230 spec artifacts

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Notifications/SharedDatabaseNotificationContractTest.php tests/Feature/Notifications/OperationRunNotificationTest.php tests/Feature/Notifications/FindingNotificationLinkTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingsNotificationEventTest.php tests/Feature/Findings/FindingsNotificationRoutingTest.php tests/Feature/OpsUx/Constitution/LegacyNotificationGuardTest.php`

## Filament / Platform Notes
- Livewire v4.0+ compliance preserved on Filament v5 primitives
- provider registration remains unchanged in `apps/platform/bootstrap/providers.php`
- no globally searchable resource behavior changed in this feature
- no destructive actions were introduced
- asset strategy is unchanged; the existing `cd apps/platform && php artisan filament:assets` deploy step remains sufficient

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #265
2026-04-22 20:26:18 +00:00
12fb5ebb30 feat: add findings hygiene report and control catalog layering (#264)
Some checks failed
Main Confidence / confidence (push) Failing after 1m20s
## Summary
- add the workspace-scoped findings hygiene report, overview signal, and supporting classification service for broken assignments and stale in-progress work
- add Spec 225 artifacts and focused findings hygiene test coverage alongside the new Filament page and workspace overview wiring
- align product roadmap and spec candidates around the layered canonical control catalog, CIS library, and readiness model
- extend SpecKit constitution and templates with the XCUT-001 shared-pattern reuse guidance

## Notes
- validation commands and implementation close-out notes are documented in `specs/225-assignment-hygiene/plan.md` and `specs/225-assignment-hygiene/quickstart.md`
- this PR targets `dev` from `225-assignment-hygiene`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #264
2026-04-22 12:26:18 +00:00
394 changed files with 6095 additions and 39691 deletions

View File

@ -266,11 +266,6 @@ ## Active Technologies
- PostgreSQL via existing `workspace_settings` rows plus existing audit log records; no new table or billing/account model (251-commercial-entitlements-billing-state)
- PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities` (253-remove-findings-backfill-runtime-surfaces)
- PostgreSQL existing `findings`, `operation_runs`, `audit_logs`, and related runtime tables only; no new persistence, migration, or data backfill is planned (253-remove-findings-backfill-runtime-surfaces)
- TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0 + Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwrigh (404-public-content-messaging)
- N/A - static website content and generated build output only; no database or product persistence (404-public-content-messaging)
- TypeScript 6.0.3 and Astro 6.3.3 content/runtime files + Astro, Playwright, Tailwind CSS v4 (`@tailwindcss/vite`), Starlight docs stack (408-review-evidence-decision)
- N/A (static public website content only) (408-review-evidence-decision)
- TypeScript 6.0.3, Astro 6.3.3, MDX content files rendered through Starlight 0.39.2 + Astro, `@astrojs/starlight`, `@astrojs/mdx`, Tailwind CSS v4 via `@tailwindcss/vite`, Playwright 1.59.1 (410-public-docs-ia)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -305,9 +300,9 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 410-public-docs-ia: Added TypeScript 6.0.3, Astro 6.3.3, MDX content files rendered through Starlight 0.39.2 + Astro, `@astrojs/starlight`, `@astrojs/mdx`, Tailwind CSS v4 via `@tailwindcss/vite`, Playwright 1.59.1
- 409-evaluation-procurement-rollout: Added TypeScript 6.0.3 and Astro 6.3.3 content/runtime files + Astro, Playwright, Tailwind CSS v4 (`@tailwindcss/vite`), Starlight docs stack
- 408-review-evidence-decision: Added TypeScript 6.0.3 and Astro 6.3.3 content/runtime files + Astro, Playwright, Tailwind CSS v4 (`@tailwindcss/vite`), Starlight docs stack
- 253-remove-findings-backfill-runtime-surfaces: Added PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities`
- 251-commercial-entitlements-billing-state: Added PHP 8.4 (Laravel 12) + Filament v5 + Livewire v4, existing workspace settings stack (`SettingsRegistry`, `SettingsResolver`, `SettingsWriter`), `WorkspaceEntitlementResolver`, `ReviewPackService`, system directory detail page
- 249-customer-review-workspace: Added PHP 8.4, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing review/evidence/review-pack/audit/RBAC support services
<!-- MANUAL ADDITIONS START -->
### Pre-production compatibility check

View File

@ -942,14 +942,6 @@ ## Active Technologies
- PHP 8.4 (Laravel 12) + Filament v5 + Livewire v4
- PostgreSQL (Sail)
- Tailwind CSS v4
- TypeScript 5.9, Astro 6 static components, HTML, CSS + Astro 6.0.0, Tailwind CSS 4.2.2 through CSS-first `@theme` and `@tailwindcss/vite`, `astro-icon`, `@iconify-json/lucide`, Playwright 1.59.1 (400-tenantial-homepage-visual-rebuild)
- TypeScript 5.9.3, Astro 6.0.0, Tailwind CSS v4.2.2 via `@tailwindcss/vite`, `astro-icon`, `@iconify-json/lucide`, and Playwright smoke tests for the static website; no database, CMS, API, customer data, tenant data, or runtime persistence. (401-tenantial-platform-page)
- TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0 + Astro, `@astrojs/starlight`, `@astrojs/sitemap`, `@astrojs/mdx`, Tailwind CSS v4, `@tailwindcss/vite`, Preline 4, Lenis, GSAP, Sharp, Playwright; static website content only, no database or product persistence. (feat/403-public-website-launch-readiness)
- No new technology; reuses TypeScript 6.0.3, Astro 6.3.3, Node.js >=20.0.0, pnpm 10.33.0, Starlight, Tailwind CSS v4, Preline, Lenis, GSAP, Sharp, and Playwright for static website content, docs content, route metadata, and generated build output only; no database or product persistence. (404-public-content-messaging)
- TypeScript 6, Astro 6, Node.js `>=20.0.0` + Astro, Tailwind CSS v4, Starlight, Playwrigh (405-dach-trust-datenschutz-security-website-surface)
- N/A - static Astro pages and centralized locale copy in TypeScrip (405-dach-trust-datenschutz-security-website-surface)
- TypeScript 6.0.3, Astro 6.3.3, Tailwind CSS 4.3.0 + Astro, `@astrojs/check`, `@astrojs/sitemap`, Tailwind CSS v4, Playwright smoke tests (406-provider-policy-domain-public-taxonomy)
- N/A - static public website content only; no runtime persistence (406-provider-policy-domain-public-taxonomy)
## Recent Changes
- 066-rbac-ui-enforcement-helper-v2-session-1769732329: Planned UiEnforcement v2 (spec + plan + design artifacts)

View File

@ -1,35 +0,0 @@
# build output
dist/
build/
# generated types
.astro/
.idea/
.vscode/
# dependencies
node_modules/
# logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# test artifacts
coverage/
playwright-report/
test-results/
blob-report/
# environment variables
.env
.env.*
.env.production
# macOS-specific files
.DS_Store
Thumbs.db
*.tmp
*.swp

View File

@ -1,14 +0,0 @@
node_modules/
dist/
build/
coverage/
.astro/
playwright-report/
test-results/
blob-report/
package-lock.json
yarn.lock
pnpm-lock.yaml
*.log
.env
.env.*

View File

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

View File

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

View File

@ -1,141 +1,25 @@
import { defineConfig } from 'astro/config';
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap';
import starlight from '@astrojs/starlight';
import icon from 'astro-icon';
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
const publicSiteUrl = process.env.PUBLIC_SITE_URL ?? 'https://tenantatlas.example';
const redirectOnlyPaths = new Set([
'/product/',
'/products/',
'/services/',
'/blog/',
'/insights/',
'/welcome-to-docs/',
'/guides/intro/',
'/guides/getting-started/',
'/guides/first-project-checklist/',
'/platform/evidence-review/',
'/en/product/',
'/en/products/',
'/en/services/',
'/en/blog/',
'/en/insights/',
'/en/welcome-to-docs/',
'/en/guides/intro/',
'/en/guides/getting-started/',
'/en/guides/first-project-checklist/',
'/en/platform/evidence-review/',
]);
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({
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'],
},
prefetch: true,
integrations: [
sitemap({
filter: page => !isRedirectOnlySitemapPath(page),
i18n: {
defaultLocale: 'de',
locales: {
de: 'de',
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: 'Deutsch',
lang: 'de',
},
en: {
label: 'English',
lang: 'en',
},
},
// https://starlight.astro.build/guides/sidebar/
sidebar: [
{
label: 'Dokumentationsstart',
translations: {
en: 'Documentation Start',
},
items: ['docs', 'docs/getting-started', 'docs/evaluation-pilot'],
},
{
label: 'Provider & Zugriff',
translations: {
en: 'Provider & Access',
},
items: [
'docs/microsoft-365-provider',
'docs/permissions-data-access',
'docs/data-processing-trust',
],
},
{
label: 'Evidence & Recovery',
translations: {
en: 'Evidence & Recovery',
},
items: [
'docs/policy-evidence',
'docs/drift-detection',
'docs/backups-versioning-recovery',
'docs/findings-exceptions-accepted-risk',
],
},
{
label: 'Review & Grenzen',
translations: {
en: 'Review & Boundaries',
},
items: [
'docs/review-packs-decisions',
'docs/known-limitations',
'docs/faq',
],
},
],
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',
},
}),
mdx(),
],
experimental: {
clientPrerender: true,
integrations: [icon()],
output: 'static',
site: publicSiteUrl,
server: {
host: true,
port: 4321,
},
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
},
});

View File

@ -1,44 +1,28 @@
{
"name": "@tenantatlas/website",
"version": "0.0.0",
"private": true,
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=20.0.0"
},
"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}",
"build": "astro build",
"preview": "astro preview --host 0.0.0.0",
"test": "playwright test",
"test:smoke": "playwright test",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"astro": "astro"
"test:smoke": "playwright test"
},
"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"
"@iconify-json/lucide": "^1.2.102",
"astro": "^6.0.0",
"astro-icon": "^1.1.5"
},
"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"
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^24.7.2",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3"
}
}

View File

@ -1,35 +1,29 @@
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',
timeout: 30_000,
expect: {
timeout: 5_000,
},
fullyParallel: true,
retries: process.env.CI ? 1 : 0,
reporter: 'list',
retries: process.env.CI ? 2 : 0,
reporter: [['list']],
use: {
baseURL,
trace: 'on-first-retry',
},
projects: [
{
name: 'desktop',
use: { ...devices['Desktop Chrome'] },
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
{
name: 'mobile',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'corepack pnpm preview',
reuseExistingServer: false,
timeout: 120_000,
url: baseURL,
command: `WEBSITE_PORT=${port} corepack pnpm dev`,
port,
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,29 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect width="64" height="64" rx="18" fill="#17120F" />
<path
d="M17 18H47V25H36V46H28V25H17V18Z"
fill="#FFF7F1"
/>
<path
d="M44 17C50.0751 17 55 21.9249 55 28C55 34.0751 50.0751 39 44 39H39V32H44C46.2091 32 48 30.2091 48 28C48 25.7909 46.2091 24 44 24H39V17H44Z"
fill="#CC5F2C"
/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

View File

@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500" fill="none">
<defs>
<linearGradient id="bg" x1="60" y1="40" x2="740" y2="460" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#F8FAFC" />
<stop offset="1" stop-color="#E2E8F0" />
</linearGradient>
<linearGradient id="topbar" x1="0" y1="0" x2="800" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1D4ED8" />
<stop offset="1" stop-color="#4F46E5" />
</linearGradient>
</defs>
<rect x="8" y="8" width="784" height="484" rx="20" fill="url(#bg)" stroke="#D7E2EE" stroke-width="2" />
<rect x="8" y="8" width="784" height="52" rx="20" fill="url(#topbar)" />
<rect x="8" y="30" width="784" height="30" fill="url(#topbar)" />
<text x="32" y="40" font-family="system-ui" font-size="15" font-weight="700" fill="#FFFFFF">TenantAtlas</text>
<circle cx="742" cy="34" r="6" fill="#FFFFFF" fill-opacity="0.7" />
<circle cx="762" cy="34" r="6" fill="#FFFFFF" fill-opacity="0.45" />
<rect x="24" y="76" width="168" height="396" rx="18" fill="#F3F6FB" stroke="#D7E2EE" />
<text x="44" y="108" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">WORKSPACE</text>
<rect x="36" y="128" width="144" height="34" rx="10" fill="#DBEAFE" />
<text x="52" y="149" font-family="system-ui" font-size="12" font-weight="700" fill="#1D4ED8">Change history</text>
<text x="52" y="194" font-family="system-ui" font-size="12" fill="#475569">Restore preview</text>
<text x="52" y="230" font-family="system-ui" font-size="12" fill="#475569">Review queue</text>
<text x="52" y="266" font-family="system-ui" font-size="12" fill="#475569">Evidence</text>
<text x="52" y="302" font-family="system-ui" font-size="12" fill="#475569">Assignments</text>
<rect x="36" y="344" width="144" height="96" rx="14" fill="#FFFFFF" stroke="#D7E2EE" />
<text x="52" y="370" font-family="system-ui" font-size="11" font-weight="700" fill="#334155">Current tenant</text>
<text x="52" y="394" font-family="system-ui" font-size="12" fill="#0F172A">Northwind Services</text>
<text x="52" y="418" font-family="system-ui" font-size="11" fill="#64748B">Inventory linked to reviewable history</text>
<rect x="212" y="76" width="564" height="396" rx="18" fill="#FFFFFF" stroke="#D7E2EE" />
<text x="236" y="108" font-family="system-ui" font-size="12" font-weight="700" fill="#334155">Recent tenant changes</text>
<rect x="236" y="124" width="90" height="24" rx="12" fill="#EFF6FF" />
<text x="252" y="140" font-family="system-ui" font-size="10" font-weight="700" fill="#2563EB">Policies</text>
<rect x="336" y="124" width="82" height="24" rx="12" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="356" y="140" font-family="system-ui" font-size="10" font-weight="700" fill="#475569">Drift</text>
<rect x="428" y="124" width="108" height="24" rx="12" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="448" y="140" font-family="system-ui" font-size="10" font-weight="700" fill="#475569">Assignments</text>
<rect x="236" y="164" width="320" height="236" rx="16" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="256" y="192" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">CHANGE RECORD</text>
<line x1="256" y1="210" x2="536" y2="210" stroke="#E2E8F0" />
<text x="256" y="236" font-family="system-ui" font-size="13" font-weight="700" fill="#0F172A">Windows Compliance Baseline</text>
<text x="256" y="256" font-family="system-ui" font-size="11" fill="#475569">Version diff prepared for review</text>
<rect x="454" y="222" width="78" height="22" rx="11" fill="#DCFCE7" />
<text x="469" y="237" font-family="system-ui" font-size="10" font-weight="700" fill="#15803D">Ready</text>
<line x1="256" y1="274" x2="536" y2="274" stroke="#E2E8F0" />
<text x="256" y="300" font-family="system-ui" font-size="13" font-weight="700" fill="#0F172A">Conditional Access MFA</text>
<text x="256" y="320" font-family="system-ui" font-size="11" fill="#475569">Assignment drift surfaced before rollout</text>
<rect x="438" y="286" width="94" height="22" rx="11" fill="#FEF3C7" />
<text x="452" y="301" font-family="system-ui" font-size="10" font-weight="700" fill="#B45309">Needs review</text>
<line x1="256" y1="338" x2="536" y2="338" stroke="#E2E8F0" />
<text x="256" y="364" font-family="system-ui" font-size="13" font-weight="700" fill="#0F172A">BitLocker policy</text>
<text x="256" y="384" font-family="system-ui" font-size="11" fill="#475569">Restore candidate linked to prior snapshot</text>
<rect x="420" y="350" width="112" height="22" rx="11" fill="#DBEAFE" />
<text x="436" y="365" font-family="system-ui" font-size="10" font-weight="700" fill="#1D4ED8">Snapshot linked</text>
<rect x="576" y="164" width="176" height="112" rx="16" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="596" y="192" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">RESTORE PREVIEW</text>
<text x="596" y="220" font-family="system-ui" font-size="12" fill="#0F172A">Scope validated</text>
<text x="596" y="242" font-family="system-ui" font-size="12" fill="#0F172A">Assignments included</text>
<text x="596" y="264" font-family="system-ui" font-size="12" fill="#0F172A">Confirmation required</text>
<circle cx="580" cy="217" r="4" fill="#16A34A" />
<circle cx="580" cy="239" r="4" fill="#16A34A" />
<circle cx="580" cy="261" r="4" fill="#F59E0B" />
<rect x="576" y="292" width="176" height="108" rx="16" fill="#F8FAFC" stroke="#D7E2EE" />
<text x="596" y="320" font-family="system-ui" font-size="11" font-weight="700" fill="#64748B">REVIEW QUEUE</text>
<text x="596" y="346" font-family="system-ui" font-size="12" fill="#0F172A">Conditional Access drift</text>
<text x="596" y="368" font-family="system-ui" font-size="12" fill="#0F172A">Restore plan awaiting approval</text>
<text x="596" y="390" font-family="system-ui" font-size="12" fill="#0F172A">Evidence attached to change record</text>
<rect x="236" y="420" width="516" height="28" rx="14" fill="#EEF2FF" />
<text x="256" y="438" font-family="system-ui" font-size="11" font-weight="700" fill="#4338CA">Change history, restore preview, and review queue stay connected on one screen.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,54 +0,0 @@
---
import logoLockupMask from '@images/tenantial-logo-lockup-mask.png';
const {
class: className,
style: inlineStyle,
'aria-label': ariaLabel = 'Tenantial',
...attrs
} = Astro.props;
const logoMaskUrl =
typeof logoLockupMask === 'string' ? logoLockupMask : logoLockupMask.src;
const logoStyle = `--tenantial-logo-mask: url("${logoMaskUrl}");${inlineStyle ?? ''}`;
---
<span
{...attrs}
class:list={[
'tenantial-wordmark inline-block shrink-0 align-middle text-neutral-700 transition-colors duration-300 dark:text-white',
className,
]}
style={logoStyle}
role="img"
aria-label={ariaLabel}
>
<span class="tenantial-wordmark__shape" aria-hidden="true"></span>
</span>
<style>
.tenantial-wordmark {
color: #4b4b4f;
}
:global(.dark) .tenantial-wordmark,
:global([data-theme='dark']) .tenantial-wordmark {
color: #ffffff;
}
.tenantial-wordmark__shape {
display: block;
width: 100%;
aspect-ratio: 898 / 167;
background: currentColor;
mask-image: var(--tenantial-logo-mask);
mask-mode: alpha;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-image: var(--tenantial-logo-mask);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
}
</style>

View File

@ -1,147 +0,0 @@
---
import { getImage } from 'astro:assets';
import { OG, SEO, SITE } from '@data/constants';
import faviconSvgSrc from '@images/icon.svg';
import faviconSrc from '@images/icon.png';
import {
getLocaleFromPath,
isLocale,
localeOg,
localizedPath,
localizeRoutePath,
stripLocalePrefix,
type Locale,
} from '@/i18n';
// 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,
locale: rawLocale = getLocaleFromPath(Astro.url.pathname),
} = Astro.props;
const locale: Locale = isLocale(rawLocale) ? rawLocale : 'de';
// 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 cleanPath = stripLocalePrefix(Astro.url.pathname);
const canonical = new URL(
localizedPath(cleanPath, locale),
Astro.site || Astro.url.origin
).href;
const socialImageRes = await getImage({
src: OG.image,
width: 1200,
height: 600,
format: 'png',
});
const socialImage = new URL(socialImageRes.src, Astro.site || Astro.url.origin)
.href;
const twitterDomain = new URL(siteURL).hostname;
const alternateLocales: Locale[] = ['de', 'en'];
// 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} />
{
alternateLocales.map(lang => {
const href = new URL(
localizeRoutePath(cleanPath, locale, lang),
Astro.site || Astro.url.origin
).href;
return <link rel="alternate" hreflang={lang} href={href} />;
})
}
<link
rel="alternate"
hreflang="x-default"
href={new URL(
localizeRoutePath(cleanPath, locale, 'de'),
Astro.site || Astro.url.origin
).href}
/>
{/* Facebook Meta Tags */}
<meta property="og:locale" content={localeOg[locale]} />
<meta property="og:url" content={canonical} />
<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={twitterDomain} />
<meta property="twitter:url" content={canonical} />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={ogDescription} />
<meta name="twitter:image" content={socialImage} />
{/* Links to the webmanifest and sitemap */}
<link rel="manifest" href="/manifest.json" />
{/* https://docs.astro.build/en/guides/integrations-guide/sitemap/ */}
<link rel="sitemap" href="/sitemap-index.xml" />
{/* Links for favicons */}
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
<meta name="mobile-web-app-capable" content="yes" />
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
<link href={appleTouchIcon.src} rel="shortcut icon" />
{/* Set theme color */}
<meta name="theme-color" content="#facc15" />

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
---
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>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,22 @@
---
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-ink-900)] [&>.accent]:text-[var(--color-primary)]', sizeClasses[size], className]}>
<slot />
</Tag>

View File

@ -0,0 +1,467 @@
---
---
<div
class="hero-dashboard"
role="img"
aria-label="TenantAtlas — change history, restore preview, and a review queue with baseline drift and evidence links"
>
<div class="dashboard-chrome">
<div class="dashboard-titlebar">
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<span class="dot dot-red"></span>
<span class="dot dot-yellow"></span>
<span class="dot dot-green"></span>
</div>
<span class="titlebar-label">TenantAtlas — Governance Surface</span>
</div>
<div class="titlebar-url">
<span class="url-text">app.tenantatlas.com/admin/governance/change-history</span>
</div>
</div>
<div class="dashboard-body">
<div class="dashboard-sidebar">
<div class="sidebar-logo">TA</div>
<div class="sidebar-nav">
<div class="nav-item active">
<div class="nav-dot"></div>
<span>Change history</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Restore preview</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Review queue</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Evidence</span>
</div>
<div class="nav-item">
<div class="nav-dot"></div>
<span>Assignments</span>
</div>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-nav">
<div class="nav-item">
<div class="nav-dot muted"></div>
<span>Settings</span>
</div>
<div class="nav-item">
<div class="nav-dot muted"></div>
<span>Audit Log</span>
</div>
</div>
</div>
<div class="dashboard-main">
<div class="stats-row">
<div class="stat-card">
<span class="stat-label">Baseline drift</span>
<span class="stat-value">2 items need review</span>
<span class="stat-change neutral">Windows baseline and Conditional Access</span>
</div>
<div class="stat-card">
<span class="stat-label">Restore preview</span>
<span class="stat-value">Scope and assignments validated</span>
<span class="stat-change positive">Confirmation required before execution</span>
</div>
<div class="stat-card">
<span class="stat-label">Evidence linked</span>
<span class="stat-value">3 change records attached</span>
<span class="stat-change neutral">Review context stays with the operator path</span>
</div>
</div>
<div class="activity-grid">
<div class="activity-section">
<div class="activity-header">
<span class="activity-title">Change record</span>
<span class="activity-badge">Review active</span>
</div>
<div class="activity-table">
<div class="table-row">
<span class="row-status warning"></span>
<span class="row-name">Windows Compliance Baseline</span>
<span class="row-type">Baseline drift</span>
<span class="row-time">Needs review</span>
</div>
<div class="table-row">
<span class="row-status success"></span>
<span class="row-name">BitLocker policy restore</span>
<span class="row-type">Restore preview</span>
<span class="row-time">Assignments in scope</span>
</div>
<div class="table-row">
<span class="row-status success"></span>
<span class="row-name">Conditional Access MFA</span>
<span class="row-type">Evidence linked</span>
<span class="row-time">Ready for approval</span>
</div>
</div>
</div>
<div class="queue-column">
<div class="queue-card">
<span class="queue-label">Restore preview</span>
<p class="queue-title">Scope and assignment edges stay visible before execution.</p>
<ul class="queue-list">
<li>Scope validated</li>
<li>Assignments included</li>
<li>Confirmation required</li>
</ul>
</div>
<div class="queue-card">
<span class="queue-label">Review queue</span>
<ul class="queue-list">
<li>Baseline drift awaiting reviewer</li>
<li>Restore plan queued for approval</li>
<li>Evidence pack linked to the change record</li>
</ul>
</div>
</div>
</div>
<div class="evidence-strip">Evidence stays linked to the change record before an operator acts.</div>
</div>
</div>
</div>
</div>
<style>
.hero-dashboard {
border-radius: 1rem;
overflow: hidden;
border: 1px solid rgba(20, 20, 20, 0.08);
box-shadow:
0 24px 80px rgba(20, 20, 20, 0.1),
0 8px 24px rgba(0, 0, 0, 0.05);
background: white;
}
.dashboard-chrome {
display: flex;
flex-direction: column;
}
.dashboard-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 1rem;
background: #fafafa;
border-bottom: 1px solid rgba(20, 20, 20, 0.06);
}
.dot {
display: block;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
}
.dot-red { background: #ff5f57; }
.dot-yellow { background: #febc2e; }
.dot-green { background: #28c840; }
.titlebar-label {
font-size: 0.7rem;
font-weight: 500;
color: rgba(20, 20, 20, 0.45);
margin-left: 0.5rem;
}
.titlebar-url {
font-size: 0.65rem;
color: rgba(20, 20, 20, 0.35);
background: rgba(20, 20, 20, 0.04);
padding: 0.2rem 0.6rem;
border-radius: 0.25rem;
}
.url-text {
font-family: var(--font-mono);
}
.dashboard-body {
display: grid;
grid-template-columns: 140px 1fr;
min-height: 260px;
}
.dashboard-sidebar {
padding: 0.75rem;
background: #f8f9fb;
border-right: 1px solid rgba(20, 20, 20, 0.05);
}
.sidebar-logo {
width: 1.75rem;
height: 1.75rem;
border-radius: 0.5rem;
background: var(--color-ink-900);
color: white;
font-size: 0.6rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.75rem;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.sidebar-divider {
height: 1px;
background: rgba(20, 20, 20, 0.06);
margin: 0.5rem 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.4rem;
font-size: 0.65rem;
color: rgba(20, 20, 20, 0.5);
border-radius: 0.3rem;
}
.nav-item.active {
background: rgba(40, 60, 120, 0.06);
color: var(--color-ink-900);
font-weight: 600;
}
.nav-dot {
width: 0.35rem;
height: 0.35rem;
border-radius: 50%;
background: var(--color-brand-500);
opacity: 0.6;
}
.nav-dot.muted {
background: rgba(20, 20, 20, 0.18);
}
.nav-item.active .nav-dot {
opacity: 1;
}
.dashboard-main {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.65rem;
}
.stats-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.stat-card {
display: flex;
flex-direction: column;
gap: 0.15rem;
padding: 0.5rem 0.6rem;
border-radius: 0.5rem;
border: 1px solid rgba(20, 20, 20, 0.05);
background: #fbfbfd;
}
.stat-label {
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: rgba(20, 20, 20, 0.45);
font-weight: 500;
}
.stat-value {
font-size: 0.94rem;
font-weight: 700;
color: var(--color-ink-900);
letter-spacing: -0.02em;
line-height: 1.25;
}
.stat-change {
font-size: 0.55rem;
font-weight: 500;
line-height: 1.35;
}
.stat-change.positive {
color: var(--color-mint-700);
}
.stat-change.neutral {
color: rgba(20, 20, 20, 0.4);
}
.activity-section {
border: 1px solid rgba(20, 20, 20, 0.05);
border-radius: 0.5rem;
overflow: hidden;
}
.activity-grid {
display: grid;
grid-template-columns: minmax(0, 1.28fr) minmax(14rem, 0.92fr);
gap: 0.5rem;
}
.activity-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.45rem 0.6rem;
background: #fafafa;
border-bottom: 1px solid rgba(20, 20, 20, 0.05);
}
.activity-title {
font-size: 0.65rem;
font-weight: 600;
color: var(--color-ink-900);
}
.activity-badge {
font-size: 0.55rem;
font-weight: 600;
background: rgba(40, 60, 120, 0.07);
color: var(--color-brand-500);
padding: 0.1rem 0.4rem;
border-radius: 999px;
}
.activity-table {
display: flex;
flex-direction: column;
}
.queue-column {
display: grid;
gap: 0.5rem;
}
.queue-card {
border: 1px solid rgba(20, 20, 20, 0.05);
border-radius: 0.5rem;
background: #fbfbfd;
padding: 0.65rem 0.7rem;
}
.queue-label {
display: inline-flex;
font-size: 0.55rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(20, 20, 20, 0.42);
}
.queue-title {
margin: 0.45rem 0 0;
font-size: 0.72rem;
font-weight: 600;
line-height: 1.45;
color: var(--color-ink-900);
}
.queue-list {
margin: 0.55rem 0 0;
padding-left: 1rem;
display: grid;
gap: 0.35rem;
font-size: 0.64rem;
line-height: 1.45;
color: rgba(20, 20, 20, 0.56);
}
.evidence-strip {
border-radius: 0.5rem;
background: rgba(40, 60, 120, 0.06);
color: var(--color-ink-800);
font-size: 0.64rem;
font-weight: 600;
line-height: 1.4;
padding: 0.5rem 0.65rem;
}
.table-row {
display: grid;
grid-template-columns: 0.5rem 1fr auto auto;
gap: 0.5rem;
align-items: center;
padding: 0.4rem 0.6rem;
font-size: 0.6rem;
border-bottom: 1px solid rgba(20, 20, 20, 0.04);
}
.table-row:last-child {
border-bottom: none;
}
.row-status {
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
}
.row-status.success {
background: var(--color-mint-500);
}
.row-status.warning {
background: #febc2e;
}
.row-name {
font-weight: 500;
color: var(--color-ink-900);
}
.row-type {
color: rgba(20, 20, 20, 0.4);
font-weight: 500;
}
.row-time {
color: rgba(20, 20, 20, 0.35);
}
@media (max-width: 640px) {
.dashboard-body {
grid-template-columns: 1fr;
}
.dashboard-sidebar {
display: none;
}
.stats-row {
grid-template-columns: 1fr;
}
.titlebar-url {
display: none;
}
.activity-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,61 @@
---
import PrimaryCTA from '@/components/content/PrimaryCTA.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();
---
<footer class="section-divider px-[var(--space-page-x)] pt-10 sm:pt-12" data-footer-intent={footerLead.intent}>
<Container width="wide">
<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 tracking-[0.16em] text-[var(--color-brand)]">
{footerLead.eyebrow}
</p>
<h2 class="m-0 max-w-xl font-[var(--font-display)] text-3xl leading-[0.98] text-[var(--color-ink-900)] 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 tracking-[0.14em] text-[var(--color-ink-900)]">
{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-sm text-[var(--color-copy)] sm:flex-row sm:items-center sm:justify-between">
<p class="m-0">© {currentYear} {siteMetadata.siteName}. Core public route foundation.</p>
<p class="m-0">
Built as a static Astro track with no platform auth, session, or API coupling.
</p>
</div>
</Container>
</footer>

View File

@ -0,0 +1,108 @@
---
import SecondaryCTA from '@/components/content/SecondaryCTA.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="sticky top-0 z-30 px-[var(--space-page-x)] pt-4 sm:pt-6">
<Container width="wide">
<div
class="shell-panel flex items-center justify-between gap-4 rounded-[var(--radius-panel)] px-4 py-3 sm:px-5"
data-shell-surface="header"
>
<a href="/" class="flex min-w-0 items-center gap-3 no-underline">
<span
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-[linear-gradient(135deg,var(--color-primary),#8eaed1)] font-[var(--font-display)] text-lg text-white shadow-[var(--shadow-inline)]"
>
TA
</span>
<span class="min-w-0">
<span class="block truncate text-sm font-semibold uppercase tracking-[0.16em] text-[var(--color-ink-900)]">
{siteMetadata.siteName}
</span>
<span class="block truncate text-sm text-[var(--color-copy)]">
{siteMetadata.siteTagline}
</span>
</span>
</a>
<nav class="hidden items-center gap-1 lg:flex" aria-label="Primary">
{
primaryNavigation.map((item) => (
<a
href={item.href}
aria-current={isActiveNavigationPath(currentPath, item.href) ? 'page' : undefined}
class:list={[
'rounded-full px-4 py-2 text-sm font-medium transition',
isActiveNavigationPath(currentPath, item.href)
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)]'
: 'text-[var(--color-ink-800)] hover:bg-white/70',
]}
data-nav-link="primary"
>
{item.label}
</a>
))
}
</nav>
<div class="hidden lg:block">
<SecondaryCTA cta={headerCta} size="sm" />
</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-white/85 text-[var(--color-ink-900)]"
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-ink-800)] hover:bg-white/75',
]}
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>
))
}
<div class="mt-2 rounded-[1rem] bg-[rgba(47,111,183,0.08)] p-3">
<SecondaryCTA cta={headerCta} size="sm" showHelper />
</div>
</nav>
</div>
</details>
</div>
</Container>
</header>

View File

@ -0,0 +1,50 @@
---
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-[34rem] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.68),transparent_28%),radial-gradient(circle_at_top_right,rgba(47,111,183,0.14),transparent_26%)]"
>
</div>
<Navbar currentPath={currentPath} />
<main id="content" class="foundation-main pb-20 sm:pb-24">
<slot />
</main>
<Footer currentPath={currentPath} />
</div>
</BaseLayout>

View File

@ -1,41 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import ContactSection from '@components/sections/misc/ContactSection.astro';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import { localeHtmlLang, localizedPath, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].contact;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath('/contact', locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<ContactSection locale={locale} />
</MainLayout>

View File

@ -1,530 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Icon from '@components/ui/icons/Icon.astro';
import AccordionItem from '@components/ui/blocks/AccordionItem.astro';
import heroImage from '@images/tenantial-rollout-plan.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].evaluation;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath(copy.routePath, locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta.label}
primaryBtnURL={localizeHref(copy.primaryCta.href, locale)}
secondaryBtn={copy.secondaryCta.label}
secondaryBtnURL={localizeHref(copy.secondaryCta.href, locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroTitle}
/>
{/* Evaluation path */}
<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="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionRoute" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.pathTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.pathIntro}
</p>
</div>
</div>
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.evaluationSteps.map((step: any, index: number) => (
<li class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{String(index + 1).padStart(2, '0')}
</span>
<h3 class="mt-4 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{step.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{step.content}
</p>
</li>
))
}
</ol>
</div>
</section>
{/* Preparation */}
<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="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionPrep" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.preparationTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.preparationIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.preparationCards.map((card: any) => (
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Pilot scenarios */}
<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="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionPilot" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.pilotTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.pilotIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{
copy.pilotScenarios.map((scenario: any) => (
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{scenario.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{scenario.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Stakeholders */}
<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="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionStakeholders" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.stakeholderTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.stakeholderIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.stakeholderCards.map((card: any) => (
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</section>
{/* Security & procurement */}
<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="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/20 text-yellow-600 ring-1 ring-yellow-400/40 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionSecurity" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.securityTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.securityIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.securityChecklist.map((item: any) => (
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{item.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{item.content}
</p>
</article>
))
}
</div>
<div
class="mt-8 border-t border-neutral-300 pt-6 dark:border-neutral-700"
>
<h3
class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.securityHandoffTitle}
</h3>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 dark:text-neutral-300"
>
{copy.securityHandoffText}
</p>
<div class="mt-5">
<PrimaryCTA
title={copy.securityHandoffCta.label}
url={localizeHref(copy.securityHandoffCta.href, locale)}
/>
</div>
</div>
</div>
</section>
{/* Microsoft 365 access principles */}
<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="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionAccess" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.accessTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.accessIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2">
{
copy.accessPrinciples.map((principle: any) => (
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{principle.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{principle.content}
</p>
</article>
))
}
</div>
</section>
{/* Non-requirements */}
<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="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionLimits" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.nonRequirementTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.nonRequirementIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.nonRequirementCards.map((card: any) => (
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Example timeline */}
<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="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionTimeline" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.timelineTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.timelineIntro}
</p>
</div>
</div>
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{
copy.timelineEntries.map((entry: any, index: number) => (
<li class="rounded-3xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{String(index + 1).padStart(2, '0')}
</span>
<h3 class="mt-4 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{entry.title}
</h3>
<p class="mt-2 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{entry.content}
</p>
</li>
))
}
</ol>
</section>
{/* Buyer FAQ */}
<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="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionFaq" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.faqTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.faqIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-x-12 md:grid-cols-2">
{
[0, 1].map((column: number) => (
<div class="hs-accordion-group divide-y divide-neutral-300 dark:divide-neutral-700">
{copy.faqItems
.filter((_item: any, index: number) => index % 2 === column)
.map((item: any, index: number) => {
const itemIndex = index * 2 + column;
return (
<AccordionItem
id={`evaluation-faq-heading-${itemIndex}`}
collapseId={`evaluation-faq-collapse-${itemIndex}`}
question={item.question}
answer={item.answer}
first={index === 0}
/>
);
})}
</div>
))
}
</div>
</div>
</section>
{/* Final CTA */}
<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="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-wrap gap-4">
{
copy.finalCtas.map((cta: any, index: number) =>
index === 0 ? (
<PrimaryCTA
title={cta.label}
url={localizeHref(cta.href, locale)}
/>
) : (
<SecondaryCTA
title={cta.label}
url={localizeHref(cta.href, locale)}
/>
)
)
}
</div>
</div>
</section>
</MainLayout>

View File

@ -1,263 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import FeaturesGeneral from '@components/sections/features/FeaturesGeneral.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import heroImage from '@images/tenantial-dashboard.avif';
import featureImage from '@images/tenantial-review-board.avif';
import { featuresByLocale, siteCopy } from '@data/site-copy';
import { localizeHref, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].home;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta}
primaryBtnURL={localizeHref('/contact', locale)}
secondaryBtn={copy.secondaryCta}
secondaryBtnURL={localizeHref('/platform', locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroAlt}
/>
<FeaturesGeneral
title={copy.featureTitle}
subTitle={copy.featureSubtitle}
src={featureImage}
alt={copy.featureAlt}
features={featuresByLocale[locale]}
/>
<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)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.audienceTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.audienceSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-3">
{
copy.audiences.map((audience: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{audience.title}
</h3>
<p class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400">
{audience.content}
</p>
</article>
))
}
</div>
</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"
>
<div
class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.useCasesTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.useCasesSubtitle}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{
copy.useCases.map((useCase: any) => (
<article class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 shadow-xs dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800">
<p class="text-xs font-semibold tracking-[0.22em] text-neutral-500 uppercase dark:text-neutral-400">
{useCase.eyebrow}
</p>
<h3 class="mt-4 text-2xl font-semibold text-balance text-neutral-800 dark:text-neutral-200">
{useCase.title}
</h3>
<p class="mt-4 max-w-prose text-pretty text-neutral-600 dark:text-neutral-400">
{useCase.content}
</p>
<div class="mt-6">
<SecondaryCTA
title={useCase.cta}
url={localizeHref(useCase.href, locale)}
/>
</div>
</article>
))
}
</div>
</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"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.boundaryTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.boundarySubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-3">
{
copy.boundaries.map((boundary: any) => (
<article class="rounded-2xl bg-neutral-200/80 p-5 dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-800 dark:text-neutral-200">
{boundary.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{boundary.content}
</p>
</article>
))
}
</div>
</div>
</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"
>
<div
class="grid gap-8 border-y border-neutral-300 py-10 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-center lg:py-14 dark:border-neutral-700"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.trustTeaserTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.trustTeaserSubtitle}
</p>
<div class="mt-6">
<SecondaryCTA
title={copy.trustTeaserCta}
url={localizeHref('/trust', locale)}
/>
</div>
</div>
<ul class="grid gap-3">
{
copy.trustPoints.map((point: string) => (
<li class="rounded-2xl border border-neutral-300 bg-neutral-100/70 px-5 py-4 text-sm font-medium text-neutral-700 dark:border-neutral-700 dark:bg-white/[0.04] dark:text-neutral-300">
{point}
</li>
))
}
</ul>
</div>
</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"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<span
class="text-xs font-semibold tracking-[0.18em] text-yellow-600 uppercase dark:text-yellow-400"
>
{siteCopy[locale].evaluation.discovery.homepageEyebrow}
</span>
<h2
class="mt-3 text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.homepageTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.homepageContent}
</p>
<div class="mt-6 flex flex-wrap gap-3">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.homepageCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
<SecondaryCTA
title={siteCopy[locale].docs.discovery.homepage.label}
url={localizeHref(siteCopy[locale].docs.discovery.homepage.href, locale)}
/>
</div>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] bg-linear-to-r from-neutral-900 to-neutral-700 px-6 py-8 text-neutral-50 md:px-10 md:py-12 dark:from-neutral-100 dark:to-neutral-300 dark:text-neutral-900"
>
<div class="max-w-(--breakpoint-lg)">
<h2 class="text-2xl font-bold text-balance md:text-3xl">
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-2xl text-pretty text-neutral-200 dark:text-neutral-700"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
<PrimaryCTA
title={copy.finalPrimaryCta}
url={localizeHref('/contact', locale)}
/>
<SecondaryCTA
title={copy.finalSecondaryCta}
url={localizeHref('/platform', locale)}
/>
</div>
</div>
</section>
</MainLayout>

View File

@ -1,35 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import { siteCopy } from '@data/site-copy';
import { localizeHref, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].legal;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<MainSection title={copy.heading} subTitle={copy.subtitle} />
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8">
<div class="flex flex-wrap gap-3">
<PrimaryCTA title={copy.privacy} url={localizeHref('/privacy', locale)} />
<SecondaryCTA title={copy.terms} url={localizeHref('/terms', locale)} />
<SecondaryCTA
title={copy.imprint}
url={localizeHref('/imprint', locale)}
/>
</div>
</section>
</MainLayout>

View File

@ -1,204 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import LeftSection from '@components/ui/blocks/LeftSection.astro';
import RightSection from '@components/ui/blocks/RightSection.astro';
import FeaturesStats from '@components/sections/features/FeaturesStats.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import dashboard from '@images/tenantial-dashboard.avif';
import evidenceImage from '@images/tenantial-evidence-panel.avif';
import reviewImage from '@images/tenantial-drift-workflow.avif';
import restoreImage from '@images/tenantial-rollout-plan.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].platform;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath('/platform', locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<MainSection
title={copy.heading}
subTitle={copy.subtitle}
btnExists={true}
btnTitle={siteCopy[locale].auth.walkthrough}
btnURL={localizeHref('/contact', locale)}
/>
<section
class="mx-auto max-w-[85rem] px-4 py-4 sm:px-6 lg:px-8 lg:py-8 2xl:max-w-full"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.focusTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.focusSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2">
{
copy.focusCards.map((card: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 py-4 sm:px-6 lg:px-8 lg:py-8 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.useCasesTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.useCasesSubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.useCases.map((useCase: any) => (
<article class="rounded-2xl bg-neutral-200/80 p-5 dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-800 dark:text-neutral-200">
{useCase.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{useCase.content}
</p>
<div class="mt-5">
<SecondaryCTA
title={useCase.cta}
url={localizeHref(useCase.href, locale)}
/>
</div>
</article>
))
}
</div>
</div>
</section>
<RightSection
title={copy.backupTitle}
subTitle={copy.backupSubtitle}
single={false}
imgOne={dashboard}
imgOneAlt={copy.dashboardAlt}
imgTwo={evidenceImage}
imgTwoAlt={copy.evidenceAlt}
/>
<LeftSection
title={copy.driftTitle}
subTitle={copy.driftSubtitle}
img={reviewImage}
imgAlt={copy.driftAlt}
btnExists={true}
btnTitle={copy.trustCta}
btnURL={localizeHref('/trust', locale)}
/>
<RightSection
title={copy.restoreTitle}
subTitle={copy.restoreSubtitle}
single={true}
imgOne={restoreImage}
imgOneAlt={copy.restoreAlt}
btnExists={true}
btnTitle={copy.rolloutCta}
btnURL={localizeHref('/contact', locale)}
/>
<FeaturesStats
title={copy.boundaryTitle}
subTitle={copy.boundarySubtitle}
mainStatTitle={copy.mainStatTitle}
mainStatSubTitle={copy.mainStatSubTitle}
stats={copy.stats}
/>
<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="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.platformTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.platformContent}
</p>
<div class="mt-6 flex flex-wrap gap-3">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.platformCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
<SecondaryCTA
title={siteCopy[locale].docs.discovery.platform.label}
url={localizeHref(siteCopy[locale].docs.discovery.platform.href, locale)}
/>
</div>
</div>
</div>
</section>
</MainLayout>

View File

@ -1,25 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import PricingSection from '@components/sections/pricing/PricingSection.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import { pricingByLocale, siteCopy } from '@data/site-copy';
import type { Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].pricingIntro;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<MainSection title={copy.heading} subTitle={copy.subtitle} />
<PricingSection pricing={pricingByLocale[locale]} locale={locale} />
</MainLayout>

View File

@ -1,579 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import heroImage from '@images/tenantial-review-board.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].reviewPacks;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath('/platform/review-packs', locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta}
primaryBtnURL={localizeHref('/contact', locale)}
secondaryBtn={copy.secondaryCta}
secondaryBtnURL={localizeHref('/platform', locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroAlt}
/>
<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)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.problemTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.problemSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-4">
{
copy.problemCards.map((card: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/80 p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-3 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</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"
>
<div
class="grid gap-8 lg:grid-cols-[minmax(0,0.7fr)_minmax(0,1.3fr)] lg:items-start"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.workflowTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.workflowSubtitle}
</p>
</div>
<div class="grid gap-4 md:grid-cols-2">
{
copy.workflowSteps.map((step: any) => (
<article class="rounded-3xl border border-neutral-300 bg-neutral-100/70 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{step.step}
</span>
<h3 class="mt-4 text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{step.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{step.content}
</p>
</article>
))
}
</div>
</div>
</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"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-neutral-200/70 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.anatomyTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.anatomySubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{
copy.reviewPackCards.map((card: any) => (
<article class="flex flex-col rounded-3xl border border-neutral-200/50 bg-white p-6 shadow-xs transition-shadow hover:shadow-sm dark:border-neutral-800/50 dark:bg-neutral-950/80">
<div class="mb-4 flex flex-wrap items-start justify-between gap-3">
<span
class:list={[
'inline-flex shrink-0 items-center justify-center rounded-lg px-2.5 py-1 text-[11px] font-semibold tracking-[0.16em] uppercase',
card.availabilityTone === 'soft-availability'
? 'border border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400'
: 'border border-yellow-200/50 bg-yellow-100/50 text-yellow-800 dark:border-yellow-900/30 dark:bg-yellow-500/10 dark:text-yellow-500',
]}
>
{card.availabilityTone === 'soft-availability'
? copy.softAvailabilityLabel
: copy.availableNowLabel}
</span>
</div>
<h3 class="mb-2 text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</div>
</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"
>
<div class="grid gap-8 lg:grid-cols-2">
<div>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.evidenceTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.evidenceSubtitle}
</p>
</div>
<div class="mt-8 grid gap-4">
{
copy.evidenceCards.map((card: any) => (
<article class="flex gap-4 rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 dark:border-neutral-700 dark:bg-white/[0.04]">
<div class="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-800 text-white dark:bg-neutral-200 dark:text-neutral-900">
<svg
class="h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-2 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</div>
</article>
))
}
</div>
</div>
<div>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.decisionTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.decisionSubtitle}
</p>
</div>
<figure
class="mt-8 overflow-hidden rounded-3xl border border-neutral-300 bg-white shadow-sm dark:border-neutral-700 dark:bg-neutral-950/80"
>
<figcaption
class="flex flex-wrap items-center justify-between gap-3 border-b border-neutral-200 bg-neutral-50 px-6 py-4 dark:border-neutral-800 dark:bg-neutral-900/70"
>
<span
class="text-[11px] font-semibold tracking-[0.16em] text-neutral-500 uppercase dark:text-neutral-400"
>
{copy.decisionSampleBadge}
</span>
<span
class="inline-flex items-center gap-2 rounded-full border border-yellow-200/60 bg-yellow-100/70 px-3 py-1 text-xs font-semibold text-yellow-800 dark:border-yellow-900/40 dark:bg-yellow-500/10 dark:text-yellow-300"
>
<span
class="h-1.5 w-1.5 rounded-full bg-yellow-500 dark:bg-yellow-400"
></span>
{copy.decisionSampleStatusValue}
</span>
</figcaption>
<div class="px-6 py-5">
<h3
class="text-base font-semibold text-balance text-neutral-800 dark:text-neutral-200"
>
{copy.decisionSampleTitle}
</h3>
<dl class="mt-4 grid gap-3">
{
copy.decisionSampleRows.map((row: any) => (
<div class="grid gap-1 border-t border-neutral-100 pt-3 first:border-0 first:pt-0 sm:grid-cols-[7rem_1fr] sm:gap-4 dark:border-neutral-800">
<dt class="text-xs font-semibold tracking-[0.14em] text-neutral-500 uppercase dark:text-neutral-400">
{row.label}
</dt>
<dd class="text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{row.value}
</dd>
</div>
))
}
</dl>
</div>
</figure>
<div class="mt-8 grid gap-4 sm:grid-cols-2">
{
copy.decisionCards.map((card: any) => (
<article class="flex gap-4 rounded-3xl border border-neutral-300 bg-neutral-100/70 p-6 dark:border-neutral-700 dark:bg-white/[0.04]">
<div class="mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-200">
<svg
class="h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-2 text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</div>
</article>
))
}
</div>
</div>
</div>
</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"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.boundaryTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.boundarySubtitle}
</p>
</div>
<div class="mt-8 grid gap-4 lg:grid-cols-2">
{
copy.boundaryColumns.map((column: any) => (
<article class="rounded-3xl bg-neutral-200/80 p-6 dark:bg-neutral-900/70">
<h3 class="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{column.title}
</h3>
<ul class="mt-4 grid gap-3">
{column.items.map((item: string) => (
<li class="rounded-2xl border border-neutral-300 bg-white/70 px-4 py-3 text-sm leading-6 text-neutral-700 dark:border-neutral-700 dark:bg-neutral-950/50 dark:text-neutral-300">
{item}
</li>
))}
</ul>
</article>
))
}
</div>
<p
class="mt-6 max-w-3xl text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.boundaryNote}
</p>
</div>
</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"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.audienceTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.audienceSubtitle}
</p>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
{
copy.audienceCards.map((card: any) => (
<article class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-neutral-200/70 p-6 shadow-xs dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800">
<h3 class="text-2xl font-semibold text-balance text-neutral-800 dark:text-neutral-200">
{card.title}
</h3>
<p class="mt-4 max-w-prose text-pretty text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</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"
>
<div
class="rounded-[2rem] bg-linear-to-br from-neutral-900 to-neutral-700 p-6 md:p-10 dark:from-neutral-950 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2 class="text-2xl font-bold text-balance text-neutral-50 md:text-3xl">
{copy.comparisonTitle}
</h2>
<p class="mt-3 max-w-prose text-pretty text-neutral-300 md:text-lg">
{copy.comparisonSubtitle}
</p>
</div>
<div class="mt-8 hidden gap-4 px-2 md:grid md:grid-cols-[9rem_1fr_1fr]">
<span></span>
<span
class="text-xs font-semibold tracking-[0.18em] text-neutral-400 uppercase"
>
{copy.comparisonRawLabel}
</span>
<span
class="text-xs font-semibold tracking-[0.18em] text-yellow-300 uppercase"
>
{copy.comparisonStoryLabel}
</span>
</div>
<div class="mt-3 grid gap-3">
{
copy.comparisonRows.map((row: any) => (
<article class="grid gap-3 rounded-3xl border border-white/10 bg-white/[0.03] p-4 md:grid-cols-[9rem_1fr_1fr] md:items-stretch">
<h3 class="self-center text-sm font-semibold text-neutral-100">
{row.title}
</h3>
<div class="rounded-2xl bg-white/[0.04] p-4">
<p class="text-[11px] font-semibold tracking-[0.18em] text-neutral-400 uppercase md:hidden">
{copy.comparisonRawLabel}
</p>
<p class="mt-2 text-sm leading-6 text-neutral-400 md:mt-0">
{row.rawExport}
</p>
</div>
<div class="rounded-2xl border border-yellow-400/30 bg-yellow-500/10 p-4">
<p class="text-[11px] font-semibold tracking-[0.18em] text-yellow-300 uppercase md:hidden">
{copy.comparisonStoryLabel}
</p>
<div class="flex gap-2.5 md:items-start">
<span class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-yellow-400 text-neutral-900">
<svg
class="h-3 w-3"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</span>
<p class="text-sm leading-6 text-neutral-100">
{row.reviewStory}
</p>
</div>
</div>
</article>
))
}
</div>
</div>
</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"
>
<div
class="grid gap-8 border-y border-neutral-300 py-10 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:items-center lg:py-14 dark:border-neutral-700"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{copy.trustTeaserTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{copy.trustTeaserSubtitle}
</p>
<div class="mt-6">
<SecondaryCTA
title={copy.trustTeaserCta}
url={localizeHref('/trust', locale)}
/>
</div>
</div>
<ul class="grid gap-3">
{
copy.trustPoints.map((point: string) => (
<li class="rounded-2xl border border-neutral-300 bg-neutral-100/70 px-5 py-4 text-sm font-medium text-neutral-700 dark:border-neutral-700 dark:bg-white/[0.04] dark:text-neutral-300">
{point}
</li>
))
}
</ul>
</div>
</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"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.reviewPackTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.reviewPackContent}
</p>
<div class="mt-6 flex flex-wrap gap-3">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.reviewPackCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
<SecondaryCTA
title={siteCopy[locale].docs.discovery.reviewPack.label}
url={localizeHref(siteCopy[locale].docs.discovery.reviewPack.href, locale)}
/>
</div>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] bg-linear-to-r from-neutral-900 to-neutral-700 px-6 py-8 text-neutral-50 md:px-10 md:py-12 dark:from-neutral-100 dark:to-neutral-300 dark:text-neutral-900"
>
<div class="max-w-(--breakpoint-lg)">
<h2 class="text-2xl font-bold text-balance md:text-3xl">
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-2xl text-pretty text-neutral-200 dark:text-neutral-700"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
<PrimaryCTA
title={copy.finalPrimaryCta}
url={localizeHref('/contact', locale)}
/>
<SecondaryCTA
title={copy.finalSecondaryCta}
url={localizeHref('/platform', locale)}
/>
</div>
</div>
</section>
</MainLayout>

View File

@ -1,24 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import MainSection from '@components/ui/blocks/MainSection.astro';
import { siteCopy } from '@data/site-copy';
import type { Locale } from '@/i18n';
const { locale, page } = Astro.props;
interface Props {
locale: Locale;
page: 'privacy' | 'terms' | 'imprint';
}
const copy = siteCopy[locale][page];
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<MainSection title={copy.heading} subTitle={copy.subtitle} />
</MainLayout>

View File

@ -1,360 +0,0 @@
---
import MainLayout from '@/layouts/MainLayout.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import { siteCopy } from '@data/site-copy';
import { localizeHref, type Locale } from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].trust;
const statusByKey = new Map<string, any>(
copy.statusLegend.map((status: any) => [status.key, status])
);
const statusLabel = (key: string) => statusByKey.get(key)?.label ?? key;
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
>
<section
class="mx-auto max-w-[85rem] px-4 pt-16 pb-10 sm:px-6 lg:px-8 lg:pt-24 lg:pb-14 2xl:max-w-full"
>
<div
class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,0.42fr)] lg:items-start"
>
<div class="max-w-4xl">
<p class="text-sm font-semibold text-orange-500 dark:text-orange-300">
{copy.heroEyebrow}
</p>
<h1
class="mt-4 text-4xl font-bold text-balance text-neutral-900 md:text-5xl dark:text-neutral-100"
>
{copy.heading}
</h1>
<p
class="mt-5 max-w-3xl text-lg leading-8 text-pretty text-neutral-700 dark:text-neutral-300"
>
{copy.subtitle}
</p>
<p
class="mt-4 max-w-3xl text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.primaryHandoff.summary}
</p>
<div class="mt-8 flex flex-col gap-3 sm:flex-row">
<PrimaryCTA
title={copy.primaryCta}
url={localizeHref(copy.primaryHandoff.href, locale)}
/>
<SecondaryCTA
title={copy.secondaryCta}
url={localizeHref('/platform', locale)}
/>
</div>
</div>
<aside
class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]"
aria-labelledby="trust-status-legend"
>
<h2
id="trust-status-legend"
class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.statusLegendTitle}
</h2>
<p
class="mt-2 text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.statusLegendIntro}
</p>
<dl class="mt-5 grid gap-3">
{
copy.statusLegend.map((status: any) => (
<div class="rounded-lg bg-white/70 p-3 dark:bg-neutral-900/60">
<dt class="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{status.label}
</dt>
<dd class="mt-1 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{status.description}
</dd>
</div>
))
}
</dl>
</aside>
</div>
</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"
>
<div class="max-w-3xl">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.principlesTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.principlesIntro}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.principles.map((principle: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{principle.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{principle.content}
</p>
</article>
))
}
</div>
</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"
>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.topicsTitle}
</h2>
<div class="mt-8 grid gap-5 lg:grid-cols-2">
{
copy.topics.map((topic: any) => (
<article
id={topic.slug}
class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]"
>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{topic.title}
</h3>
<span class="inline-flex w-fit rounded-full border border-orange-300 bg-orange-100 px-3 py-1 text-xs font-semibold text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-200">
{statusLabel(topic.claimStatus)}
</span>
</div>
<p class="mt-4 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{topic.summary}
</p>
<ul class="mt-4 grid gap-2 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{topic.points.map((point: string) => (
<li class="flex gap-2">
<span
class="mt-2 size-1.5 shrink-0 rounded-full bg-orange-400"
aria-hidden="true"
/>
<span>{point}</span>
</li>
))}
</ul>
</article>
))
}
</div>
</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"
>
<div class="max-w-3xl">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.documentReadinessTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.documentReadinessIntro}
</p>
</div>
<div class="mt-8 grid gap-4 lg:grid-cols-2">
{
copy.documentReadiness.map((item: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{item.documentType}
</h3>
<span class="inline-flex w-fit rounded-full border border-neutral-300 bg-white px-3 py-1 text-xs font-semibold text-neutral-700 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200">
{statusLabel(item.claimStatus)}
</span>
</div>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{item.availabilitySummary}
</p>
{item.requestPath ? (
<a
class="mt-4 inline-flex rounded-lg text-sm font-semibold text-orange-600 ring-zinc-500 outline-hidden hover:text-orange-700 focus-visible:ring-3 dark:text-orange-300 dark:ring-zinc-200 dark:hover:text-orange-200"
href={localizeHref(item.requestPath, locale)}
>
{copy.primaryHandoff.label}
</a>
) : null}
</article>
))
}
</div>
</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"
>
<div
class="grid gap-8 lg:grid-cols-[minmax(0,0.7fr)_minmax(0,1fr)] lg:items-start"
>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.dataCategoriesTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.dataCategoriesIntro}
</p>
<div
class="mt-6 rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]"
>
<h3
class="text-base font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.avoidanceTitle}
</h3>
<p
class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400"
>
{copy.avoidanceText}
</p>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
{
copy.dataCategories.map((category: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<p class="text-xs font-semibold text-orange-600 uppercase dark:text-orange-300">
{category.boundary}
</p>
<h3 class="mt-2 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{category.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{category.description}
</p>
</article>
))
}
</div>
</div>
</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"
>
<div class="max-w-3xl">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.permissionTitle}
</h2>
<p
class="mt-3 text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.permissionIntro}
</p>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{
copy.permissionCards.map((card: any) => (
<article class="rounded-lg border border-neutral-300 bg-neutral-100/80 p-5 dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-600 dark:text-neutral-400">
{card.content}
</p>
</article>
))
}
</div>
</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"
>
<div
class="rounded-lg border border-neutral-300 bg-neutral-900 p-6 text-neutral-50 md:p-8 dark:border-neutral-700 dark:bg-neutral-100 dark:text-neutral-900"
>
<h2 class="text-2xl font-bold text-balance md:text-3xl">
{copy.handoffTitle}
</h2>
<p
class="mt-3 max-w-3xl text-pretty text-neutral-200 dark:text-neutral-700"
>
{copy.handoffText}
</p>
<p
class="mt-3 max-w-3xl text-sm leading-6 text-neutral-300 dark:text-neutral-600"
>
{copy.hardClaimNote}
</p>
<div class="mt-6">
<PrimaryCTA
title={copy.handoffCta}
url={localizeHref(copy.primaryHandoff.href, locale)}
/>
</div>
</div>
</section>
<section
class="mx-auto max-w-[85rem] px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-800 md:text-3xl dark:text-neutral-200"
>
{siteCopy[locale].evaluation.discovery.trustTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-600 md:text-lg dark:text-neutral-400"
>
{siteCopy[locale].evaluation.discovery.trustContent}
</p>
<div class="mt-6 flex flex-wrap gap-3">
<SecondaryCTA
title={siteCopy[locale].evaluation.discovery.trustCta}
url={localizeHref(siteCopy[locale].evaluation.routePath, locale)}
/>
<SecondaryCTA
title={siteCopy[locale].docs.discovery.trust.label}
url={localizeHref(siteCopy[locale].docs.discovery.trust.href, locale)}
/>
</div>
</div>
</div>
</section>
</MainLayout>

View File

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

View File

@ -0,0 +1,72 @@
---
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-ink-900)] text-white shadow-[0_2px_8px_rgba(0,0,0,0.12)] hover:bg-[var(--color-ink-800)] hover:shadow-[0_4px_16px_rgba(0,0,0,0.18)] active:scale-[0.98]',
secondary:
'border-[color:var(--color-border)] bg-white text-[var(--color-secondary-foreground)] hover:border-[var(--color-border-strong)] hover:bg-[var(--surface-muted)] active:scale-[0.98]',
ghost: 'border-transparent bg-transparent text-[var(--color-ink-800)] hover:bg-white/70',
};
const classes = [baseClass, sizeClasses[size], variantClasses[variant], className];
---
{
href ? (
<a
href={href}
target={target}
rel={rel}
aria-label={ariaLabel}
class:list={classes}
data-button-variant={variant}
data-cta-weight={variant}
data-interaction="button"
>
<slot />
</a>
) : (
<button
type={type}
aria-label={ariaLabel}
class:list={classes}
data-button-variant={variant}
data-cta-weight={variant}
data-interaction="button"
>
<slot />
</button>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,239 @@
---
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 Headline from '@/components/content/Headline.astro';
import HeroDashboard from '@/components/content/HeroDashboard.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 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';
---
<section
class:list={[
isHomepageHero ? 'hero-gradient pt-2 sm:pt-8 lg:pt-14' : '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">
{isHomepageHero ? (
<div class="space-y-6 sm:space-y-8 lg:space-y-10" data-disclosure-layer="1" data-hero-layout>
<div class="grid gap-6 lg:grid-cols-[minmax(0,0.82fr)_minmax(22rem,1.18fr)] lg:items-start lg:gap-10">
<div class="motion-rise flex flex-col gap-5 sm:gap-6 lg:max-w-[40rem]" data-hero-panel="text">
<div class="space-y-5" 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-[13ch] text-balance text-[length:clamp(2.7rem,4.6vw,4.8rem)] leading-[0.94] tracking-[-0.045em]"
>
{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-[36rem] text-[1.02rem] leading-8 text-[var(--color-copy)] sm:text-[1.08rem]">
{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)]">
<PrimaryCTA cta={hero.primaryCta} size="lg" />
{hero.secondaryCta && (
<SecondaryCTA cta={hero.secondaryCta} />
)}
</Cluster>
</div>
)}
</div>
</div>
<div
class="motion-rise lg:pt-1"
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"
>
<div class="overflow-hidden rounded-[2rem] border border-[color:var(--color-border)] bg-[linear-gradient(180deg,rgba(255,255,255,0.9),rgba(246,248,252,0.94))] p-3 shadow-[var(--shadow-panel-strong)] sm:p-4">
{hero.visualFocus && (
<div class="mb-3 rounded-[1.45rem] border border-[color:var(--color-border-subtle)] bg-white/88 px-4 py-4 sm:px-5">
<p class="m-0 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)] text-[var(--color-brand-500)]">
{hero.visualFocus.eyebrow}
</p>
<p class="mt-2 max-w-[42rem] text-sm font-semibold leading-6 text-[var(--color-ink-900)] sm:text-[0.98rem]">
{hero.visualFocus.title}
</p>
<ul class="mt-3 grid gap-2 p-0 sm:grid-cols-3">
{hero.visualFocus.points.map((point) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-border-subtle)] bg-[var(--surface-muted)] px-3 py-2 text-sm font-medium leading-5 text-[var(--color-ink-800)]">
{point}
</li>
))}
</ul>
</div>
)}
<HeroDashboard />
</div>
</div>
</div>
{hero.trustSubclaims && hero.trustSubclaims.length > 0 && (
<div class="motion-rise space-y-2" data-hero-segment="trust-subclaims" data-hero-trust-signals>
<ul class="flex flex-wrap gap-3 p-0">
{hero.trustSubclaims.map((claim) => (
<li class="list-none rounded-full border border-[color:var(--color-border)] bg-white/80 px-4 py-1.5 text-sm font-medium text-[var(--color-ink-800)]">
{claim}
</li>
))}
</ul>
</div>
)}
</div>
) : (
<!-- Subpage hero: card-based 2-col layout -->
<div
class="grid gap-5 sm:gap-6 lg:grid-cols-[minmax(0,1.08fr)_minmax(20rem,0.92fr)] lg:items-start"
data-disclosure-layer="1"
data-hero-layout
>
<Card class="motion-rise overflow-hidden" data-hero-panel="text">
<div class="space-y-4 sm:space-y-6">
<div data-hero-text-core>
<div data-hero-eyebrow data-hero-segment="eyebrow">
<Badge>{hero.eyebrow}</Badge>
</div>
<div class="mt-3 space-y-4 sm:mt-4 sm:space-y-5">
<div data-hero-heading data-hero-segment="headline">
<Headline
as="h1"
size={heroHeadlineSize}
class="max-w-3xl text-balance"
>
{hero.titleHtml ? <Fragment set:html={hero.titleHtml} /> : hero.title}
</Headline>
</div>
<div data-hero-copy-role="supporting" data-hero-supporting-copy data-hero-segment="supporting-copy">
<Lead class="max-w-2xl" size={heroLeadSize}>
{hero.description}
</Lead>
</div>
</div>
</div>
{(hero.primaryCta || hero.secondaryCta) && (
<div data-hero-cta-pair data-hero-segment="cta-pair">
<Cluster data-cta-cluster gap="sm" class="sm:gap-[var(--space-cluster)]">
<PrimaryCTA cta={hero.primaryCta} />
{hero.secondaryCta && (
<SecondaryCTA cta={hero.secondaryCta} />
)}
</Cluster>
</div>
)}
{hero.highlights && hero.highlights.length > 0 && !hero.trustSubclaims?.length && (
<ul class="grid gap-3 p-0 sm:grid-cols-3">
{hero.highlights.map((highlight) => (
<li class="list-none rounded-[1.1rem] border border-[color:var(--color-line)] bg-white/70 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{highlight}
</li>
))}
</ul>
)}
</div>
</Card>
<div class="grid gap-4 sm:gap-5" data-hero-panel="supporting">
{hero.productVisual && (
<Card
variant="accent"
class="motion-rise overflow-hidden"
data-hero-segment="product-near-visual"
data-hero-visual
>
<img
src={hero.productVisual.src}
alt={hero.productVisual.alt}
class="max-h-[22rem] w-full rounded-[var(--radius-lg)] object-cover object-top"
loading="eager"
/>
</Card>
)}
{hero.trustSubclaims && hero.trustSubclaims.length > 0 && (
<Card variant="subtle" class="motion-rise" data-hero-segment="trust-subclaims">
<div class="space-y-3" data-hero-trust-signals>
<p class="m-0 text-[0.72rem] font-semibold uppercase tracking-[var(--tracking-eyebrow)] text-[var(--color-copy)]">
Early trust
</p>
<ul class="grid gap-2.5 p-0">
{hero.trustSubclaims.map((claim) => (
<li class="list-none rounded-[1rem] border border-[color:var(--color-line)] bg-white/82 px-4 py-3 text-sm font-medium text-[var(--color-ink-800)]">
{claim}
</li>
))}
</ul>
</div>
</Card>
)}
{!hero.productVisual && (calloutTitle || calloutDescription) && (
<Card variant="accent" class="motion-rise">
<p class="m-0 text-sm font-semibold uppercase tracking-[0.15em] text-[var(--color-brand)]">
Trust-first launch surface
</p>
{calloutTitle && (
<h2 class="mt-4 font-[var(--font-display)] text-3xl font-bold leading-tight text-[var(--color-ink-900)]">
{calloutTitle}
</h2>
)}
{calloutDescription && (
<p class="mt-3 text-base leading-7 text-[var(--color-copy)]">{calloutDescription}</p>
)}
</Card>
)}
{metrics.length > 0 && (
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
{metrics.map((metric) => <Metric item={metric} />)}
</div>
)}
</div>
</div>
)}
</Container>
</section>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +0,0 @@
---
// 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[];
}
const visiblePartners = partners.filter(
partner => partner.href && partner.icon
);
---
<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 */}
{
visiblePartners.map(partner => (
<a
href={partner.href}
target="_blank"
rel="noopener noreferrer"
aria-label={partner.name}
>
<div set:html={partner.icon} />
</a>
))
}
</div>
</section>

View File

@ -1,114 +0,0 @@
---
// 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,
supportingLine,
primaryBtn,
primaryBtnURL,
secondaryBtn,
secondaryBtnURL,
withReview,
avatars,
rating,
reviews,
src,
alt,
} = Astro.props;
// Define TypeScript interface for props
interface Props {
title: string;
subTitle?: string;
supportingLine?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
withReview?: boolean;
avatars?: Array<string>;
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>
)
}
{
supportingLine && (
<p class="mt-4 max-w-2xl text-sm font-semibold leading-6 text-neutral-800 dark:text-neutral-300">
{supportingLine}
</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} rating={rating} reviews={reviews} />
) : (
''
)
}
</div>
{/* Hero Image Section */}
<div class="flex w-full">
<div class="top-12 overflow-hidden">
{
src && alt && (
<Image
src={src}
alt={alt}
class="h-full w-full scale-110 object-cover object-center"
draggable={'false'}
loading={'eager'}
format={'avif'}
/>
)
}
</div>
</div>
</section>

View File

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

View File

@ -1,36 +0,0 @@
---
import { localizeHref, type Locale } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const { locale = 'de' } = Astro.props;
interface Props {
locale?: Locale;
}
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>`;
const copy = siteCopy[locale].auth;
---
<a
href={localizeHref('/contact', locale)}
data-astro-prefetch
class="flex items-center gap-x-2 text-base font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:text-orange-400 focus-visible:ring-3 md:my-6 md:border-s md:border-neutral-300 md:ps-6 md:text-sm 2xl:text-base dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:text-orange-300 dark:focus:outline-hidden"
>
<Fragment set:html={userSVG} />
{copy.walkthrough}
</a>

View File

@ -1,128 +0,0 @@
---
// 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';
import { localizeHref, type Locale } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const { locale = 'de' } = Astro.props;
interface Props {
locale?: Locale;
}
const copy = siteCopy[locale].contact;
---
{/* 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"
>
{copy.title}
</h1>
<p class="mt-1 text-pretty text-neutral-600 dark:text-neutral-400">
{copy.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"
>
{copy.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
action="mailto:hello@tenantial.com"
method="post"
enctype="text/plain"
>
<div class="grid gap-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<TextInput
id="hs-firstname-contacts"
label={copy.firstName}
name="hs-firstname-contacts"
/>
<TextInput
id="hs-lastname-contacts"
label={copy.lastName}
name="hs-lastname-contacts"
/>
</div>
<EmailContactInput id="hs-email-contacts" label={copy.email} />
<PhoneInput id="hs-phone-number" label={copy.phone} />
<TextAreaInput
id="hs-about-contacts"
label={copy.details}
name="hs-about-contacts"
/>
</div>
<div class="mt-4 grid">
<AuthBtn title={copy.submit} />
</div>
<div class="mt-3 text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
{copy.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={copy.blocks.docsHeading}
content={copy.blocks.docsContent}
isLinkVisible={true}
linkTitle={copy.blocks.docsLink}
linkURL={localizeHref('/docs/', locale)}
isArrowVisible={true}
><Icon name="question" />
</ContactIconBlock>
<ContactIconBlock
heading={copy.blocks.faqHeading}
content={copy.blocks.faqContent}
isLinkVisible={true}
linkTitle={copy.blocks.faqLink}
linkURL={localizeHref('/#faq', locale)}
isArrowVisible={true}
><Icon name="chatBubble" />
</ContactIconBlock>
<ContactIconBlock
heading={copy.blocks.boundaryHeading}
content={copy.blocks.boundaryContent}
isAddressVisible={false}
><Icon name="mapPin" />
</ContactIconBlock>
<ContactIconBlock
heading={copy.blocks.emailHeading}
content={copy.blocks.emailContent}
isLinkVisible={true}
linkTitle="hello@tenantial.com"
linkURL="mailto:hello@tenantial.com"
><Icon name="envelopeOpen" />
</ContactIconBlock>
</div>
</div>
</div>
</section>

View File

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

View File

@ -1,86 +0,0 @@
---
// Import the necessary dependencies
import EmailFooterInput from '@components/ui/forms/input/EmailFooterInput.astro';
import BrandLogo from '@components/BrandLogo.astro';
import { SITE } from '@data/constants';
import { getLocaleFromPath, localizeHref } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const locale = getLocaleFromPath(Astro.url.pathname);
const copy = siteCopy[locale].footer;
type FooterSection = {
section: string;
links: Array<{ name: string; url: string }>;
};
---
<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-40" />
</div>
{/* An array of links for Product and Company sections */}
{
copy.sections.map((section: FooterSection) => (
<div class="col-span-1 min-w-0">
<h3
class="break-words font-bold text-neutral-800 dark:text-neutral-200"
>
{section.section}
</h3>
<ul class="mt-3 grid space-y-3">
{section.links.map(link => (
<li>
<a
href={localizeHref(link.url, locale)}
class="inline-flex max-w-full gap-x-2 rounded-lg break-words whitespace-normal 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 min-w-0">
<h3 class="break-words font-bold text-neutral-800 dark:text-neutral-200">
{copy.conversationTitle}
</h3>
<div>
<EmailFooterInput
title={copy.contactButton}
url={localizeHref('/contact', locale)}
/>
<p class="mt-3 break-words text-sm text-neutral-600 dark:text-neutral-400">
{copy.conversationContent}
</p>
</div>
</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="break-words text-sm text-neutral-600 dark:text-neutral-400">
© <span id="current-year"></span>
{SITE.title}. {copy.copyrightSuffix}
</p>
</div>
</div>
<script>
const year = new Date().getFullYear();
const element = document.getElementById('current-year');
element!.innerText = year.toString();
</script>
</div>
</footer>

View File

@ -1,213 +0,0 @@
---
//Import relevant dependencies
import ThemeIcon from '@components/ThemeIcon.astro';
import NavLink from '@components/ui/links/NavLink.astro';
import Authentication from '../misc/Authentication.astro';
import BrandLogo from '@components/BrandLogo.astro';
import LanguagePicker from '@components/ui/LanguagePicker.astro';
import { getLocaleFromPath, localizeHref } from '@/i18n';
import { siteCopy } from '@data/site-copy';
const locale = getLocaleFromPath(Astro.url.pathname);
const strings = siteCopy[locale];
const homeUrl = localizeHref('/', locale);
type NavItem = {
name: string;
url: string;
};
---
{/* 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/60 bg-yellow-50/95 px-4 py-3 shadow-sm backdrop-blur-lg md:flex md:items-center md:justify-between md:px-6 md:py-0 lg:px-8 xl:mx-auto dark:border-neutral-700/60 dark:bg-neutral-900/95 dark:backdrop-blur-lg"
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-36 sm:w-40" />
</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.nav.map((link: NavItem) => (
<NavLink url={localizeHref(link.url, locale)} name={link.name} />
))
}
<Authentication locale={locale} />
<LanguagePicker />
{/* ThemeIcon component specifically for larger screens */}
<span class="hidden md:inline-block">
<ThemeIcon />
</span>
</div>
</div>
</nav>
</header>
{/* Theme Appearance script to manage light/dark modes */}
<script is:inline>
const HSThemeAppearance = {
init() {
const defaultTheme = 'default';
let theme = localStorage.getItem('hs_theme') || defaultTheme;
if (document.querySelector('html').classList.contains('dark')) return;
this.setAppearance(theme);
},
_resetStylesOnLoad() {
const $resetStyles = document.createElement('style');
$resetStyles.innerText = `*{transition: unset !important;}`;
$resetStyles.setAttribute('data-hs-appearance-onload-styles', '');
document.head.appendChild($resetStyles);
return $resetStyles;
},
setAppearance(theme, saveInStore = true, dispatchEvent = true) {
const $resetStylesEl = this._resetStylesOnLoad();
if (saveInStore) {
localStorage.setItem('hs_theme', theme);
}
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
}
document.querySelector('html').classList.remove('dark');
document.querySelector('html').classList.remove('default');
document.querySelector('html').classList.remove('auto');
document
.querySelector('html')
.classList.add(this.getOriginalAppearance());
setTimeout(() => {
$resetStylesEl.remove();
});
if (dispatchEvent) {
window.dispatchEvent(
new CustomEvent('on-hs-appearance-change', { detail: theme })
);
}
},
getAppearance() {
let theme = this.getOriginalAppearance();
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'default';
}
return theme;
},
getOriginalAppearance() {
const defaultTheme = 'default';
return localStorage.getItem('hs_theme') || defaultTheme;
},
};
HSThemeAppearance.init();
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (HSThemeAppearance.getOriginalAppearance() === 'auto') {
HSThemeAppearance.setAppearance('auto', false);
}
});
window.addEventListener('load', () => {
const $clickableThemes = document.querySelectorAll(
'[data-hs-theme-click-value]'
);
const $switchableThemes = document.querySelectorAll(
'[data-hs-theme-switch]'
);
$clickableThemes.forEach($item => {
$item.addEventListener('click', () =>
HSThemeAppearance.setAppearance(
$item.getAttribute('data-hs-theme-click-value'),
true,
$item
)
);
});
$switchableThemes.forEach($item => {
$item.addEventListener('change', e => {
HSThemeAppearance.setAppearance(e.target.checked ? 'dark' : 'default');
});
$item.checked = HSThemeAppearance.getAppearance() === 'dark';
});
window.addEventListener('on-hs-appearance-change', e => {
$switchableThemes.forEach($item => {
$item.checked = e.detail === 'dark';
});
});
});
</script>

View File

@ -1,163 +0,0 @@
---
// Import SecondaryCTA component for use in this module
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Icon from '@components/ui/icons/Icon.astro';
import { localizeHref, type Locale } from '@/i18n';
// Define props from Astro
const { pricing, locale = 'de' } = 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;
};
type Pricing = {
title: string;
subTitle: string;
badge: string;
thirdOption: string;
btnText: string;
starterKit: Product;
professionalToolbox: Product;
};
interface Props {
pricing: Pricing;
locale?: Locale;
}
---
<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={localizeHref(pricing.starterKit.purchaseLink, locale)}
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={localizeHref(pricing.professionalToolbox.purchaseLink, locale)}
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={localizeHref('/contact', locale)}
/>
</div>
</section>

View File

@ -1,43 +0,0 @@
---
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="Illustrative testimonial avatar"
loading={'eager'}
inferSize
/>
</div>
<div class="ms-4 grow">
<div class="font-bold text-neutral-800 dark:text-neutral-200">
{author}
</div>
<div class="text-xs text-neutral-500">{role}</div>
</div>
</div>
</div>
</blockquote>

View File

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

View File

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

View File

@ -1,62 +0,0 @@
---
import {
getLocaleFromPath,
localeLabels,
localizeRoutePath,
stripLocalePrefix,
} from '@/i18n';
import Icon from './icons/Icon.astro';
const currentLocale = getLocaleFromPath(Astro.url.pathname);
const currentPath = stripLocalePrefix(Astro.url.pathname);
---
<div class="hs-dropdown relative inline-flex">
<button
id="hs-language-dropdown"
type="button"
aria-label="Sprache wechseln"
title="Sprache wechseln"
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-1.5 py-1.5 text-sm font-medium text-neutral-600 ring-zinc-500 outline-hidden transition duration-300 hover:bg-neutral-200 hover:text-orange-400 dark:border-neutral-700 dark:text-neutral-400 dark:ring-zinc-200 dark:hover:bg-neutral-700 dark:hover:text-orange-300 dark:focus:outline-hidden"
>
<Icon name="earth" />
<span class="sr-only">{localeLabels[currentLocale]}</span>
<svg
class="hs-dropdown-open:rotate-180 size-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg
>
</button>
<div
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 top-[98%]! left-[20%]! mt-2 hidden transform-none! rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] before:absolute before:start-0 before:-top-4 before:h-4 before:w-full after:absolute after:start-0 after:-bottom-4 after:h-4 after:w-full md:top-[80%]! md:left-[90%]! dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
aria-labelledby="hs-language-dropdown"
>
{
Object.entries(localeLabels).map(([locale, label]) => (
<a
class:list={[
'flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-hidden dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700',
locale === currentLocale
? 'font-semibold text-orange-500 dark:text-orange-300'
: 'text-neutral-800 dark:text-neutral-400',
]}
href={localizeRoutePath(
currentPath,
currentLocale,
locale as 'de' | 'en'
)}
aria-current={locale === currentLocale ? 'true' : undefined}
>
{label}
</a>
))
}
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +0,0 @@
---
// 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 */}
<div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-neutral-200/80 text-orange-500 dark:bg-white/[0.06] dark:text-orange-300 [&>svg]:!m-0 [&>svg]:!size-6 [&>svg]:!shrink-0 [&>svg]:!fill-current">
<slot />
</div>
<div class="grow">
{/* Heading of the section */}
<h3 class={headingClasses}>
{heading}
</h3>
{/* Content text of the section */}
<p class={contentClasses}>{content}</p>
</div>
</div>

View File

@ -1,49 +0,0 @@
---
// 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-3xl font-extrabold tracking-tight break-words text-balance text-neutral-800 md:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{/* The subtitle of the section */}
<p
class="mb-4 max-w-prose font-normal text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{/* Conditionally render the Primary CTA button if btnExists is true */}
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
</section>

View File

@ -1,45 +0,0 @@
---
// 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="min-w-0 max-w-(--breakpoint-md)">
{/* Section title */}
<h1
class="mb-4 text-4xl font-extrabold tracking-tight break-words text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h1>
{/* Section subtitle */}
<p
class="mb-8 max-w-prose font-normal break-words text-pretty text-neutral-600 sm:text-xl dark:text-neutral-400"
>
{subTitle}
</p>
{
/* Conditional rendering of PrimaryCTA component if 'btnExists' property is truthy */
}
{
btnExists ? (
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
<PrimaryCTA title={btnTitle} url={btnURL} />
</div>
) : null
}
</div>
</section>

View File

@ -1,51 +0,0 @@
---
import Avatar from '@components/ui/avatars/Avatar.astro';
const { avatars, rating, reviews } = Astro.props;
interface Props {
avatars?: Array<string>;
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="Illustrative reviewer avatar" />
))
}
<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"
>Demo</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>
<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">
<p class="text-neutral-800 dark:text-neutral-200">
<Fragment set:html={rating || 'Illustrative review preview'} />
</p>
</div>
<div class="text-sm text-neutral-800 sm:ps-5 dark:text-neutral-200">
<p>
<Fragment set:html={reviews || 'Static example only'} />
</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,88 +0,0 @@
---
// 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-3xl font-extrabold tracking-tight break-words text-balance text-neutral-800 md:text-4xl dark:text-neutral-200"
>
{title}
</h2>
{/* Subtitle of the section */}
<p
class="mb-4 max-w-prose font-normal text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{
/* Conditional rendering of the Primary Call-To-Action button if 'btnExists' is true */
}
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
{/* Conditionally render one or two images based on 'single' property */}
{
single ? (
<div class="mt-8">
{/* Single image */}
<Image
class="w-full rounded-lg"
src={imgOne}
alt={imgOneAlt}
format={'avif'}
/>
</div>
) : (
<div class="mt-8 grid grid-cols-2 gap-4">
{/* First image in a two-image layout */}
<Image
class="w-full rounded-xl"
src={imgOne}
alt={imgOneAlt}
draggable={'false'}
format={'avif'}
/>
{/* Second image in a two-image layout */}
<Image
class="mt-4 w-full rounded-xl lg:mt-10"
src={imgTwo}
alt={imgTwoAlt}
draggable={'false'}
format={'avif'}
/>
</div>
)
}
</section>

View File

@ -1,19 +0,0 @@
---
// 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="min-w-0 lg:pe-6 xl:pe-12">
<p class="text-6xl leading-10 font-bold break-words text-balance text-orange-400 dark:text-orange-300">
{title}
</p>
<p class="mt-2 break-words text-neutral-600 sm:mt-3 dark:text-neutral-400">
{subTitle}
</p>
</div>

View File

@ -1,23 +0,0 @@
---
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>

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