diff --git a/.changeset/clear-avatar-initial.md b/.changeset/clear-avatar-initial.md new file mode 100644 index 00000000..7131c819 --- /dev/null +++ b/.changeset/clear-avatar-initial.md @@ -0,0 +1,4 @@ +--- +'svforge': patch +--- +Inline Dashboard avatar initials in the reactive markup without changing empty-name or variant behavior. diff --git a/graphify-out/graph.json b/graphify-out/graph.json index 7fc82124..3440e164 100644 --- a/graphify-out/graph.json +++ b/graphify-out/graph.json @@ -21916,6 +21916,14 @@ "_callable": true, "_origin": "ast" }, + { + "id": "tests_avatar_initial_test", + "label": "avatar-initial.test.ts", + "file_type": "code", + "source_file": "tests/avatar-initial.test.ts", + "source_location": "L1", + "_origin": "ast" + }, { "id": "tests_avoid_patterns_test", "label": "avoid-patterns.test.ts", @@ -26379,6 +26387,15 @@ "source_location": "L1", "_origin": "ast" }, + { + "id": "changeset_clear_avatar_initial", + "label": "clear-avatar-initial.md", + "file_type": "document", + "node_kind": "page", + "source_file": ".changeset/clear-avatar-initial.md", + "source_location": "L1", + "_origin": "ast" + }, { "id": "changeset_plain_base_class_derivations", "label": "plain-base-class-derivations.md", @@ -74057,6 +74074,39 @@ "source_file": "tests/auth-schema-check.test.ts", "_origin": "ast" }, + { + "source": "tests_avatar_initial_test", + "target": "ref_vitest", + "relation": "imports_from", + "context": "import", + "confidence": "EXTRACTED", + "source_file": "tests/avatar-initial.test.ts", + "source_location": "L2", + "weight": 1, + "_origin": "ast" + }, + { + "source": "tests_avatar_initial_test", + "target": "ref_testing_library_svelte", + "relation": "imports_from", + "context": "import", + "confidence": "EXTRACTED", + "source_file": "tests/avatar-initial.test.ts", + "source_location": "L3", + "weight": 1, + "_origin": "ast" + }, + { + "source": "tests_avatar_initial_test", + "target": "packages_svforge_templates_dashboard_src_lib_components_svforge_ui_avatarinitial", + "relation": "imports_from", + "context": "import", + "confidence": "EXTRACTED", + "source_file": "tests/avatar-initial.test.ts", + "source_location": "L4", + "weight": 1, + "_origin": "ast" + }, { "source": "tests_avoid_patterns_test", "target": "ref_vitest", diff --git a/graphify-out/manifest.json b/graphify-out/manifest.json index 85cdd71a..25c0e808 100644 --- a/graphify-out/manifest.json +++ b/graphify-out/manifest.json @@ -488,7 +488,7 @@ "semantic_hash": "" }, "packages/svforge/src/templates.ts": { - "ast_hash": "3136b546212ced01f45ec40895b95434", + "ast_hash": "af298d7862569aeb90dc421a2df9b5df", "semantic_hash": "" }, "packages/svforge/src/upgrade.ts": { @@ -740,7 +740,7 @@ "semantic_hash": "" }, "packages/svforge/templates/dashboard/src/lib/components/svforge/ui/AvatarInitial.svelte": { - "ast_hash": "2429a1b2d87321a7bcc4205edeadd459", + "ast_hash": "6824a407a2889c63bdceeb85bd4e5614", "semantic_hash": "" }, "packages/svforge/templates/dashboard/src/lib/components/svforge/ui/Feedback.svelte": { @@ -2110,5 +2110,13 @@ ".changeset/plain-base-class-derivations.md": { "ast_hash": "7778c6938a8841c5a60545fb9b168b42", "semantic_hash": "" + }, + "tests/avatar-initial.test.ts": { + "ast_hash": "53a0adb8832b105297d25494fd9bb3dc", + "semantic_hash": "" + }, + ".changeset/clear-avatar-initial.md": { + "ast_hash": "75e717999620fe7c44c536ab7aaf9e77", + "semantic_hash": "" } } diff --git a/packages/svforge/src/templates.ts b/packages/svforge/src/templates.ts index 58de035b..97ce00d2 100644 --- a/packages/svforge/src/templates.ts +++ b/packages/svforge/src/templates.ts @@ -55,7 +55,7 @@ export const dashboardFiles = { "/hooks.server.ts": "import type { Handle } from '@sveltejs/kit/hooks';\n// `building` is used to skip auth during prerender/build (SvelteKit builds pages statically).\nimport { building } from '$app/env';\nimport { getTextDirection } from '#lib/paraglide/runtime.js';\nimport { paraglideMiddleware } from '#lib/paraglide/server.js';\nimport { auth } from '#lib/server/auth.js';\nimport { db } from '#lib/server/db/index.js';\nimport { user } from '#lib/server/db/schema.js';\nimport { eq } from 'drizzle-orm';\nimport { svelteKitHandler } from 'better-auth/svelte-kit';\n\n/**\n * Single composed handle (#280).\n *\n * Order matters:\n * 1. `paraglideMiddleware` negotiates the locale and rewrites `event.request`\n * (cookie / accept-language handling).\n * 2. Better Auth reads the session from the REWRITTEN request and populates\n * `locals.session` / `locals.user`; auth paths are routed to the auth\n * handler, every other request resolves through the app.\n * 3. The Paraglide transform rewrites the `%paraglide.lang%` /\n * `%paraglide.dir%` placeholders of app.html in the FINAL html — this must\n * survive the composition: the #268 fix (composing handleBetterAuth) had\n * dropped it, leaving the placeholders in the rendered HTML (#280).\n */\nexport const handle: Handle = async ({ event, resolve }) =>\n\tparaglideMiddleware(event.request, async ({ request, locale }) => {\n\t\tconst localizedEvent = { ...event, request };\n\n\t\tconst session = await auth.api.getSession({ headers: localizedEvent.request.headers });\n\t\tif (session) {\n\t\t\t// A disabled identity may be retained for domain/audit references, but\n\t\t\t// it must never retain access through an old or newly-created session.\n\t\t\tconst [currentUser] = await db\n\t\t\t\t.select({ disabled: user.disabled })\n\t\t\t\t.from(user)\n\t\t\t\t.where(eq(user.id, session.user.id))\n\t\t\t\t.limit(1);\n\t\t\tif (!currentUser?.disabled) {\n\t\t\t\tevent.locals.session = session.session;\n\t\t\t\tevent.locals.user = session.user;\n\t\t\t}\n\t\t}\n\n\t\treturn svelteKitHandler({\n\t\t\tevent: localizedEvent,\n\t\t\tauth,\n\t\t\tbuilding,\n\t\t\t// svelteKitHandler calls resolve(event) with a single argument — the\n\t\t\t// Paraglide transform is applied here so the i18n placeholders of\n\t\t\t// app.html are rewritten in the final html.\n\t\t\tresolve: (event) =>\n\t\t\t\tresolve(event, {\n\t\t\t\t\ttransformPageChunk: ({ html }) =>\n\t\t\t\t\t\thtml\n\t\t\t\t\t\t\t.replace('%paraglide.lang%', locale)\n\t\t\t\t\t\t\t.replace('%paraglide.dir%', getTextDirection(locale))\n\t\t\t\t})\n\t\t});\n\t});\n", "/lib/client/auth.ts": "import { createAuthClient } from 'better-auth/svelte';\n\nexport const authClient = createAuthClient();\n", "/lib/components/svforge/layout/AdminLayout.svelte": "\n\n (mobileOpen = details.open)}\n\tcloseOnInteractOutside={true}\n\trestoreFocus={true}\n>\n
\n\t\n\t\n\n\t\n\t
\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t

{m.layout_admin()}

\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t{#if user}\n\t\t\t\t\t{user.name}\n\t\t\t\t\t{#if onSignOut}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t{/if}\n\t\t\t\t{/if}\n\t\t\t
\n\t\t
\n\n\n\t\t\n\t\t
\n\t\t\t{@render children()}\n\t\t
\n\t
\n
\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{m.layout_admin()}\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\n\t\t
\n\t
\n\n", - "/lib/components/svforge/ui/AvatarInitial.svelte": "\n\n
\n {initial}\n
\n", + "/lib/components/svforge/ui/AvatarInitial.svelte": "\n\n
\n {name[0]?.toUpperCase() ?? '?'}\n
\n", "/lib/components/svforge/ui/Feedback.svelte": "\n\n{#if message}\n\t\n\t
\n\t\t{#if type === 'success'}{:else}{/if}\n\t\t{message}\n\t\t{#if ondismiss}\n\t\t\t\n\t\t{/if}\n\t
\n{/if}\n", "/lib/components/svforge/ui/index.ts": "export { default as Alert } from './Alert.svelte';\nexport { default as Card } from './Card.svelte';\nexport { default as Logo } from './Logo.svelte';\nexport { default as Seo } from './Seo.svelte';\nexport { default as Table } from './Table.svelte';\nexport { default as ThemeToggle } from './ThemeToggle.svelte';\nexport { generateSitemap } from './Sitemap';\nexport { default as AvatarInitial } from './AvatarInitial.svelte';\nexport { default as Feedback } from './Feedback.svelte';\n", "/lib/icons/core.ts": "export { default as Menu } from 'phosphor-svelte/lib/List';\nexport { default as X } from 'phosphor-svelte/lib/X';\nexport { default as SquaresFour } from 'phosphor-svelte/lib/SquaresFour';\nexport { default as Sun } from 'phosphor-svelte/lib/Sun';\nexport { default as Moon } from 'phosphor-svelte/lib/Moon';\nexport { default as Users } from 'phosphor-svelte/lib/Users';\nexport { default as Gear } from 'phosphor-svelte/lib/Gear';\nexport { default as ChartBar } from 'phosphor-svelte/lib/ChartBar';\nexport { default as SignOut } from 'phosphor-svelte/lib/SignOut';\nexport { default as UserPlus } from 'phosphor-svelte/lib/UserPlus';\nexport { default as EnvelopeSimple } from 'phosphor-svelte/lib/EnvelopeSimple';\nexport { default as Power } from 'phosphor-svelte/lib/Power';\nexport { default as Pencil } from 'phosphor-svelte/lib/Pencil';\nexport { default as Check } from 'phosphor-svelte/lib/Check';\nexport { default as Warning } from 'phosphor-svelte/lib/Warning';\nexport { default as Lock } from 'phosphor-svelte/lib/Lock';\nexport { default as Clock } from 'phosphor-svelte/lib/Clock';\nexport { default as Eye } from 'phosphor-svelte/lib/Eye';\nexport { default as EyeSlash } from 'phosphor-svelte/lib/EyeSlash';\n", diff --git a/packages/svforge/templates/dashboard/src/lib/components/svforge/ui/AvatarInitial.svelte b/packages/svforge/templates/dashboard/src/lib/components/svforge/ui/AvatarInitial.svelte index 03b06950..7e8c6c1d 100644 --- a/packages/svforge/templates/dashboard/src/lib/components/svforge/ui/AvatarInitial.svelte +++ b/packages/svforge/templates/dashboard/src/lib/components/svforge/ui/AvatarInitial.svelte @@ -6,10 +6,9 @@ class?: string; } let { name, size = 'md', class: className }: Props = $props(); - const initial = $derived(name[0]?.toUpperCase() ?? '?'); const sizes = { sm: 'w-7 h-7 text-xs', md: 'w-8 h-8 text-sm', lg: 'w-12 h-12 text-lg' };
- {initial} + {name[0]?.toUpperCase() ?? '?'}
diff --git a/tests/avatar-initial.test.ts b/tests/avatar-initial.test.ts new file mode 100644 index 00000000..94c1d0ca --- /dev/null +++ b/tests/avatar-initial.test.ts @@ -0,0 +1,30 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from 'vitest'; +import { cleanup, render } from '@testing-library/svelte'; +import AvatarInitial from '../packages/svforge/templates/dashboard/src/lib/components/svforge/ui/AvatarInitial.svelte'; + +afterEach(cleanup); + +describe('Dashboard AvatarInitial', () => { + it('renders the current name initial and reacts to name changes', async () => { + const { container, rerender } = render(AvatarInitial, { + props: { name: 'Ada Lovelace' } + }); + const avatar = container.firstElementChild!; + + expect(avatar.textContent).toBe('A'); + expect(avatar.classList).toContain('w-8'); + + await rerender({ name: 'Grace Hopper', size: 'lg', class: 'team-avatar' }); + + expect(avatar.textContent).toBe('G'); + expect(avatar.classList).toContain('w-12'); + expect(avatar.classList).not.toContain('w-8'); + expect(avatar.classList).toContain('team-avatar'); + }); + + it('uses a question mark when the name is empty', () => { + const { container } = render(AvatarInitial, { props: { name: '' } }); + expect(container.firstElementChild?.textContent).toBe('?'); + }); +});