diff --git a/site/app/app.vue b/site/app/app.vue
index 7867829..9301c2e 100644
--- a/site/app/app.vue
+++ b/site/app/app.vue
@@ -146,15 +146,7 @@ const mobileDocsNav = computed(
-
+
@@ -199,7 +191,11 @@ const mobileDocsNav = computed(
-
+
+
+
diff --git a/site/app/assets/css/main.css b/site/app/assets/css/main.css
index 77351e7..b4ab5bc 100644
--- a/site/app/assets/css/main.css
+++ b/site/app/assets/css/main.css
@@ -39,6 +39,15 @@
border-radius: 0.5rem;
}
+.landing-code {
+ border-color: color-mix(in oklab, var(--ui-primary) 40%, transparent);
+ box-shadow:
+ 0 0 0 1px color-mix(in oklab, var(--ui-primary) 54%, transparent),
+ 0 0 14px color-mix(in oklab, var(--ui-primary) 45%, transparent),
+ 0 0 36px color-mix(in oklab, var(--ui-primary) 29%, transparent),
+ 0 0 72px color-mix(in oklab, var(--ui-primary) 16%, transparent);
+}
+
.landing-code :where(pre) {
overflow-x: auto;
margin: 0;
diff --git a/site/app/components/AppGitHubButton.vue b/site/app/components/AppGitHubButton.vue
new file mode 100644
index 0000000..9e97a97
--- /dev/null
+++ b/site/app/components/AppGitHubButton.vue
@@ -0,0 +1,22 @@
+
+
+
+
+ {{ formattedStarCount }}
+
+
diff --git a/site/app/components/AppLogo.vue b/site/app/components/AppLogo.vue
index 102c87e..f59d5a3 100644
--- a/site/app/components/AppLogo.vue
+++ b/site/app/components/AppLogo.vue
@@ -56,7 +56,7 @@ const props = defineProps({
font-weight="700"
letter-spacing="-0.03em"
>
- AutoFixture
+ AutoFixture
diff --git a/site/app/components/AppTopGlow.vue b/site/app/components/AppTopGlow.vue
new file mode 100644
index 0000000..d18dfb3
--- /dev/null
+++ b/site/app/components/AppTopGlow.vue
@@ -0,0 +1,65 @@
+
+
+
diff --git a/site/app/composables/useGitHubStars.ts b/site/app/composables/useGitHubStars.ts
new file mode 100644
index 0000000..f9066e7
--- /dev/null
+++ b/site/app/composables/useGitHubStars.ts
@@ -0,0 +1,21 @@
+export function useGitHubStars() {
+ const { data } = useFetch(GITHUB_REPO_API, {
+ key: 'github-stars',
+ server: false,
+ lazy: true,
+ headers: {
+ Accept: 'application/vnd.github+json',
+ },
+ transform: (repo: { stargazers_count: number }) => repo.stargazers_count,
+ })
+
+ const formattedStarCount = computed(() => {
+ if (data.value == null) return undefined
+ return formatCompactCount(data.value)
+ })
+
+ return {
+ repoUrl: GITHUB_REPO_URL,
+ formattedStarCount,
+ }
+}
diff --git a/site/app/composables/usePageSeo.ts b/site/app/composables/usePageSeo.ts
index 5ed6e53..e1ed9bf 100644
--- a/site/app/composables/usePageSeo.ts
+++ b/site/app/composables/usePageSeo.ts
@@ -18,7 +18,7 @@ export function usePageSeo(options: PageSeoOptions = {}) {
const route = useRoute()
const { public: { siteUrl } } = useRuntimeConfig()
- const title = computed(() => toValue(options.title) ?? 'AutoFixture')
+ const title = computed(() => formatDocumentTitle(toValue(options.title)))
const description = computed(() => toValue(options.description) ?? DEFAULT_DESCRIPTION)
const image = computed(() => `${siteUrl}${toValue(options.image) ?? DEFAULT_OG_IMAGE}`)
const url = computed(() => `${siteUrl}${route.path}`)
diff --git a/site/app/plugins/favicon-color-scheme.client.ts b/site/app/plugins/favicon-color-scheme.client.ts
new file mode 100644
index 0000000..4546f7c
--- /dev/null
+++ b/site/app/plugins/favicon-color-scheme.client.ts
@@ -0,0 +1,21 @@
+export default defineNuxtPlugin(() => {
+ const media = window.matchMedia('(prefers-color-scheme: dark)')
+ const prefersDark = ref(media.matches)
+
+ function syncPrefersDark(event?: MediaQueryListEvent) {
+ prefersDark.value = event?.matches ?? media.matches
+ }
+
+ media.addEventListener('change', syncPrefersDark)
+
+ useHead({
+ link: computed(() => [
+ {
+ key: 'app-favicon',
+ rel: 'icon',
+ type: 'image/svg+xml',
+ href: faviconHref(prefersDark.value ? 'dark' : 'light'),
+ },
+ ]),
+ })
+})
diff --git a/site/app/utils/favicon.ts b/site/app/utils/favicon.ts
new file mode 100644
index 0000000..96620e2
--- /dev/null
+++ b/site/app/utils/favicon.ts
@@ -0,0 +1,8 @@
+export const FAVICON_VERSION = '2'
+
+export type FaviconColorScheme = 'light' | 'dark'
+
+export function faviconHref(colorScheme: FaviconColorScheme) {
+ const fileName = colorScheme === 'dark' ? 'favicon-dark.svg' : 'favicon-light.svg'
+ return `/${fileName}?v=${FAVICON_VERSION}`
+}
diff --git a/site/app/utils/formatCompactCount.ts b/site/app/utils/formatCompactCount.ts
new file mode 100644
index 0000000..f8a774a
--- /dev/null
+++ b/site/app/utils/formatCompactCount.ts
@@ -0,0 +1,17 @@
+export function formatCompactCount(count: number): string {
+ if (count < 1000) {
+ return String(Math.round(count))
+ }
+
+ if (count < 1_000_000) {
+ return `${compactWithSuffix(count / 1000)}K`
+ }
+
+ return `${compactWithSuffix(count / 1_000_000)}M`
+}
+
+function compactWithSuffix(value: number): string {
+ const rounded = Math.round(value * 10) / 10
+ if (Number.isInteger(rounded)) return String(rounded)
+ return rounded.toFixed(1)
+}
diff --git a/site/app/utils/formatDocumentTitle.ts b/site/app/utils/formatDocumentTitle.ts
new file mode 100644
index 0000000..4dce892
--- /dev/null
+++ b/site/app/utils/formatDocumentTitle.ts
@@ -0,0 +1,6 @@
+const SITE_NAME = 'AutoFixture'
+
+export function formatDocumentTitle(pageTitle?: string | null): string {
+ if (!pageTitle || pageTitle === SITE_NAME) return SITE_NAME
+ return `${pageTitle} · ${SITE_NAME}`
+}
diff --git a/site/app/utils/github.ts b/site/app/utils/github.ts
new file mode 100644
index 0000000..350325a
--- /dev/null
+++ b/site/app/utils/github.ts
@@ -0,0 +1,2 @@
+export const GITHUB_REPO_URL = 'https://github.com/AutoFixture/AutoFixture'
+export const GITHUB_REPO_API = 'https://api.github.com/repos/AutoFixture/AutoFixture'
diff --git a/site/nuxt.config.ts b/site/nuxt.config.ts
index 5af740e..e61ba40 100644
--- a/site/nuxt.config.ts
+++ b/site/nuxt.config.ts
@@ -37,6 +37,10 @@ export default defineNuxtConfig({
'@nuxt/content',
'@nuxt/fonts',
],
+ colorMode: {
+ preference: 'system',
+ fallback: 'dark',
+ },
css: ['~/assets/css/main.css'],
fonts: {
families: [
@@ -63,7 +67,12 @@ export default defineNuxtConfig({
{ name: 'twitter:image', content: ogImage, tagPriority: seoTagPriority },
],
link: [
- { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
+ {
+ key: 'app-favicon',
+ rel: 'icon',
+ type: 'image/svg+xml',
+ href: '/favicon.svg?v=2',
+ },
],
},
},
diff --git a/site/package.json b/site/package.json
index e143922..68700a2 100644
--- a/site/package.json
+++ b/site/package.json
@@ -8,7 +8,8 @@
"prepare-agent-assets": "node scripts/prepare-agent-assets.mjs",
"pregenerate": "node scripts/prepare-agent-assets.mjs",
"generate": "nuxt generate",
- "preview": "nuxt preview"
+ "preview": "nuxt preview",
+ "test": "node --experimental-strip-types --test tests/*.test.ts"
},
"dependencies": {
"@nuxt/content": "^3.15.2",
diff --git a/site/public/favicon-dark.svg b/site/public/favicon-dark.svg
new file mode 100644
index 0000000..7c42b2a
--- /dev/null
+++ b/site/public/favicon-dark.svg
@@ -0,0 +1,7 @@
+
diff --git a/site/public/favicon-light.svg b/site/public/favicon-light.svg
new file mode 100644
index 0000000..ff0a31a
--- /dev/null
+++ b/site/public/favicon-light.svg
@@ -0,0 +1,7 @@
+
diff --git a/site/public/favicon.svg b/site/public/favicon.svg
index ff0a31a..89654ca 100644
--- a/site/public/favicon.svg
+++ b/site/public/favicon.svg
@@ -1,7 +1,13 @@
diff --git a/site/public/og-image.png b/site/public/og-image.png
index 704d04d..89a75dd 100644
Binary files a/site/public/og-image.png and b/site/public/og-image.png differ
diff --git a/site/public/og-image.svg b/site/public/og-image.svg
index 2583a6b..91cebf8 100644
--- a/site/public/og-image.svg
+++ b/site/public/og-image.svg
@@ -1 +1,43 @@
-
+
diff --git a/site/tests/formatCompactCount.test.ts b/site/tests/formatCompactCount.test.ts
new file mode 100644
index 0000000..eb3bc19
--- /dev/null
+++ b/site/tests/formatCompactCount.test.ts
@@ -0,0 +1,30 @@
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+import { formatCompactCount } from '../app/utils/formatCompactCount.ts'
+
+describe('formatCompactCount', () => {
+ it('formatCompactCount_WhenBelowOneThousand_ReturnsExactCount', () => {
+ assert.equal(formatCompactCount(0), '0')
+ assert.equal(formatCompactCount(999), '999')
+ })
+
+ it('formatCompactCount_WhenExactlyOneThousand_Returns1K', () => {
+ assert.equal(formatCompactCount(1000), '1K')
+ })
+
+ it('formatCompactCount_When3600_Returns3Point6K', () => {
+ assert.equal(formatCompactCount(3600), '3.6K')
+ })
+
+ it('formatCompactCount_When3537_Returns3Point5K', () => {
+ assert.equal(formatCompactCount(3537), '3.5K')
+ })
+
+ it('formatCompactCount_WhenExactlyThreeThousand_Returns3K', () => {
+ assert.equal(formatCompactCount(3000), '3K')
+ })
+
+ it('formatCompactCount_WhenOnePointFiveMillion_Returns1Point5M', () => {
+ assert.equal(formatCompactCount(1_500_000), '1.5M')
+ })
+})
diff --git a/site/tests/formatDocumentTitle.test.ts b/site/tests/formatDocumentTitle.test.ts
new file mode 100644
index 0000000..0093fa5
--- /dev/null
+++ b/site/tests/formatDocumentTitle.test.ts
@@ -0,0 +1,20 @@
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+import { formatDocumentTitle } from '../app/utils/formatDocumentTitle.ts'
+
+describe('formatDocumentTitle', () => {
+ it('formatDocumentTitle_WhenMissing_ReturnsAutoFixture', () => {
+ assert.equal(formatDocumentTitle(undefined), 'AutoFixture')
+ assert.equal(formatDocumentTitle(null), 'AutoFixture')
+ assert.equal(formatDocumentTitle(''), 'AutoFixture')
+ })
+
+ it('formatDocumentTitle_WhenAlreadySiteName_ReturnsAutoFixture', () => {
+ assert.equal(formatDocumentTitle('AutoFixture'), 'AutoFixture')
+ })
+
+ it('formatDocumentTitle_WhenPageTitle_AppendsAutoFixture', () => {
+ assert.equal(formatDocumentTitle('Introduction'), 'Introduction · AutoFixture')
+ assert.equal(formatDocumentTitle('Fixture'), 'Fixture · AutoFixture')
+ })
+})