diff --git a/.github/workflows/info.yml b/.github/workflows/info.yml index cf63ea7f..16929bdf 100644 --- a/.github/workflows/info.yml +++ b/.github/workflows/info.yml @@ -5,15 +5,6 @@ on: affected_projects: description: 'JSON array of affected projects' value: ${{ jobs.Info.outputs.affected_projects }} - should_run_e2e: - description: 'Whether to run the e2e because of e2e change' - value: ${{ contains(fromJSON(jobs.Info.outputs.affected_projects), '@ageorgedev/ageorgedev-e2e') }} - should_deploy_site: - description: 'Whether to deploy the site' - value: ${{ contains(fromJSON(jobs.Info.outputs.affected_projects), '@ageorgedev/ageorgedev') }} - should_deploy_design_system: - description: 'Whether to deploy the design-system storybook' - value: ${{ contains(fromJSON(jobs.Info.outputs.affected_projects), '@ageorgedev/design-docs')}} production_deploy_matrix: description: 'JSON array of projects to deploy to production' value: ${{ jobs.Info.outputs.production_deploy_matrix }} @@ -42,6 +33,7 @@ jobs: AFFECTED: ${{ steps.affected_projects.outputs.affected_projects }} SITE_PROJECT: ${{ vars.VERCEL_PROJECT_AGEORGEDEV }} DESIGN_PROJECT: ${{ vars.VERCEL_PROJECT_DESIGN_SYSTEM }} + GAME_TOOLS_PROJECT: ${{ vars.VERCEL_PROJECT_GAME_TOOLS }} run: | matrix=$(echo "$AFFECTED" | jq -c ' . as $p | @@ -51,6 +43,9 @@ jobs: else . end | if ($p | contains(["@ageorgedev/design-docs"])) then . + [{"vercel_project_name": env.DESIGN_PROJECT}] + else . end | + if ($p | contains(["@ageorgedev/game-tools"])) then + . + [{"vercel_project_name": env.GAME_TOOLS_PROJECT}] else . end ') echo "production_deploy_matrix=$matrix" >> "$GITHUB_OUTPUT" @@ -60,6 +55,7 @@ jobs: AFFECTED: ${{ steps.affected_projects.outputs.affected_projects }} SITE_PROJECT: ${{ vars.VERCEL_PROJECT_AGEORGEDEV }} DESIGN_PROJECT: ${{ vars.VERCEL_PROJECT_DESIGN_SYSTEM }} + GAME_TOOLS_PROJECT: ${{ vars.VERCEL_PROJECT_GAME_TOOLS }} run: | matrix=$(echo "$AFFECTED" | jq -c ' . as $p | @@ -69,6 +65,9 @@ jobs: else . end | if ($p | contains(["@ageorgedev/design-docs"])) then . + [{"vercel_project_name": env.DESIGN_PROJECT, "label": "Design System"}] + else . end | + if ($p | contains(["@ageorgedev/game-tools"])) then + . + [{"vercel_project_name": env.GAME_TOOLS_PROJECT, "label": "Game Tools"}] else . end ') echo "pr_deploy_matrix=$matrix" >> "$GITHUB_OUTPUT" diff --git a/.vscode/settings.json b/.vscode/settings.json index cb336edb..c7d4cdbf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,20 @@ "editor.defaultFormatter": "biomejs.biome", "nxConsole.generateAiAgentRules": true, "js/ts.preferences.preferTypeOnlyAutoImports": true, - "js/ts.tsdk.path": "node_modules/typescript/lib" + "js/ts.tsdk.path": "node_modules/typescript/lib", + "workbench.colorCustomizations": { + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#8f491a", + "statusBar.background": "#643312", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#8f491a", + "statusBarItem.remoteBackground": "#643312", + "statusBarItem.remoteForeground": "#e7e7e7", + "tab.activeBorder": "#8f491a", + "titleBar.activeBackground": "#643312", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#64331299", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#643312" } diff --git a/apps/ageorgedev-e2e/playwright.config.ts b/apps/ageorgedev-e2e/playwright.config.ts index c21707e6..21150f9e 100644 --- a/apps/ageorgedev-e2e/playwright.config.ts +++ b/apps/ageorgedev-e2e/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test' +import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', @@ -11,4 +11,4 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, ], -}) +}); diff --git a/apps/ageorgedev-e2e/tests/smoke.spec.ts b/apps/ageorgedev-e2e/tests/smoke.spec.ts index 45ef1c2f..d786006a 100644 --- a/apps/ageorgedev-e2e/tests/smoke.spec.ts +++ b/apps/ageorgedev-e2e/tests/smoke.spec.ts @@ -1,20 +1,20 @@ -import { expect, test } from '@playwright/test' +import { expect, test } from '@playwright/test'; test('home page renders', async ({ page }) => { - await page.goto('/') - await expect(page.getByRole('heading', { name: 'Anish' })).toBeVisible() - await expect(page.getByRole('heading', { name: 'George' })).toBeVisible() + await page.goto('/'); + await expect(page.getByRole('heading', { name: 'Anish' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'George' })).toBeVisible(); await expect( page.getByRole('heading', { name: 'Web Developer' }) - ).toBeVisible() -}) + ).toBeVisible(); +}); test('resume page renders', async ({ page }) => { - await page.goto('/resume') - await expect(page.locator('body')).not.toBeEmpty() -}) + await page.goto('/resume'); + await expect(page.locator('body')).not.toBeEmpty(); +}); test('talks page renders', async ({ page }) => { - await page.goto('/talks') - await expect(page.locator('body')).not.toBeEmpty() -}) + await page.goto('/talks'); + await expect(page.locator('body')).not.toBeEmpty(); +}); diff --git a/apps/ageorgedev/package.json b/apps/ageorgedev/package.json index 8fcce42d..c112b506 100644 --- a/apps/ageorgedev/package.json +++ b/apps/ageorgedev/package.json @@ -22,7 +22,7 @@ "@phosphor-icons/react": "2.1.10", "@tanstack/react-router": "1.170.11", "@tanstack/react-start": "1.168.19", - "ramda": "0.28.0", + "ramda": "0.32.0", "react": "19.2.7", "react-dom": "19.2.7", "remark-frontmatter": "5.0.0", diff --git a/apps/ageorgedev/postcss.config.js b/apps/ageorgedev/postcss.config.js index 60bf9096..8984cd25 100644 --- a/apps/ageorgedev/postcss.config.js +++ b/apps/ageorgedev/postcss.config.js @@ -8,4 +8,4 @@ export default { 'postcss-nesting': {}, '@tailwindcss/postcss': {}, }, -} +}; diff --git a/apps/ageorgedev/src/components/GlobalProviders.tsx b/apps/ageorgedev/src/components/GlobalProviders.tsx index f2af2fb8..9f7816de 100644 --- a/apps/ageorgedev/src/components/GlobalProviders.tsx +++ b/apps/ageorgedev/src/components/GlobalProviders.tsx @@ -1,5 +1,5 @@ -import { ThemeProvider } from '@ageorgedev/design-system/theming/ThemeProvider' +import { ThemeProvider } from '@ageorgedev/design-system/theming/ThemeProvider'; export function GlobalProviders({ children }: React.PropsWithChildren) { - return {children} + return {children}; } diff --git a/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.spec.tsx b/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.spec.tsx index 64f526e6..b2e5edd8 100644 --- a/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.spec.tsx +++ b/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.spec.tsx @@ -1,25 +1,25 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { HomeAboveFold } from './HomeAboveFold' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { HomeAboveFold } from './HomeAboveFold'; describe('HomeAboveFold', () => { it('renders a heading with the title', () => { - render() + render(); expect( screen.getByRole('heading', { name: /web developer/i }) - ).toBeInTheDocument() - }) + ).toBeInTheDocument(); + }); it('renders the subtitle text', () => { - render() + render(); expect( screen.getByText(/architecting web experiences since 2016/i) - ).toBeInTheDocument() - }) + ).toBeInTheDocument(); + }); it('renders social links to GitHub and LinkedIn', () => { - render() - expect(screen.getByRole('link', { name: /github/i })).toBeInTheDocument() - expect(screen.getByRole('link', { name: /linkedin/i })).toBeInTheDocument() - }) -}) + render(); + expect(screen.getByRole('link', { name: /github/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /linkedin/i })).toBeInTheDocument(); + }); +}); diff --git a/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.tsx b/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.tsx index d5422362..9aae3d17 100644 --- a/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.tsx +++ b/apps/ageorgedev/src/components/HomeAboveFold/HomeAboveFold.tsx @@ -1,11 +1,11 @@ -import { NameLogo } from '@ageorgedev/design-system/logo/NameLogo' +import { NameLogo } from '@ageorgedev/design-system/logo/NameLogo'; import { Heading3, Heading4, -} from '@ageorgedev/design-system/typography/typography-components' -import { cn } from '@ageorgedev/toolbelt/cn' -import { GithubLogoIcon, LinkedinLogoIcon } from '@phosphor-icons/react/ssr' -import styles from './HomeAboveFold.module.css' +} from '@ageorgedev/design-system/typography/typography-components'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import { GithubLogoIcon, LinkedinLogoIcon } from '@phosphor-icons/react/ssr'; +import styles from './HomeAboveFold.module.css'; export function HomeAboveFold() { return ( @@ -62,5 +62,5 @@ export function HomeAboveFold() { - ) + ); } diff --git a/apps/ageorgedev/src/components/Resume/Skill.spec.tsx b/apps/ageorgedev/src/components/Resume/Skill.spec.tsx index 920b0f43..e84a4c2f 100644 --- a/apps/ageorgedev/src/components/Resume/Skill.spec.tsx +++ b/apps/ageorgedev/src/components/Resume/Skill.spec.tsx @@ -1,28 +1,28 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { Skill } from './Skill' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Skill } from './Skill'; describe('Skill', () => { it('renders the skill name', () => { - render() - expect(screen.getByText('TypeScript')).toBeInTheDocument() - }) + render(); + expect(screen.getByText('TypeScript')).toBeInTheDocument(); + }); it('renders for each skill type without throwing', () => { - const { rerender } = render() - expect(screen.getByText('React')).toBeInTheDocument() + const { rerender } = render(); + expect(screen.getByText('React')).toBeInTheDocument(); - rerender() - expect(screen.getByText('Performance')).toBeInTheDocument() + rerender(); + expect(screen.getByText('Performance')).toBeInTheDocument(); - rerender() - expect(screen.getByText('Mentoring')).toBeInTheDocument() - }) + rerender(); + expect(screen.getByText('Mentoring')).toBeInTheDocument(); + }); it('renders for all valid levels without throwing', () => { - const { rerender } = render() - rerender() - rerender() - expect(screen.getByText('skill')).toBeInTheDocument() - }) -}) + const { rerender } = render(); + rerender(); + rerender(); + expect(screen.getByText('skill')).toBeInTheDocument(); + }); +}); diff --git a/apps/ageorgedev/src/components/Resume/Skill.tsx b/apps/ageorgedev/src/components/Resume/Skill.tsx index 4bda756e..03061105 100644 --- a/apps/ageorgedev/src/components/Resume/Skill.tsx +++ b/apps/ageorgedev/src/components/Resume/Skill.tsx @@ -1,18 +1,18 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { map } from 'ramda' -import styles from './Skill.module.css' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { map } from 'ramda'; +import styles from './Skill.module.css'; -type SkillType = 'tool' | 'technique' | 'human' -type SkillLevel = 3 | 4 | 5 +type SkillType = 'tool' | 'technique' | 'human'; +type SkillLevel = 3 | 4 | 5; type SkillProps = { - name: string - type: SkillType - level: SkillLevel -} + name: string; + type: SkillType; + level: SkillLevel; +}; export function Skill(props: SkillProps) { - const styleClasses = levelClasses[props.type][props.level] + const styleClasses = levelClasses[props.type][props.level]; return (
{props.name}
- ) + ); } -type SkillClassMap = Record +type SkillClassMap = Record; const primaryAccentColorClasses: SkillClassMap = { 3: 'bg-primary-surface text-primary-onsurface', 4: 'bg-primary-surface-2 text-primary-onsurface-2', 5: `${styles.highSkill} bg-primary-surface-2 text-primary-onsurface-2`, // TODO -} +}; const secondaryAccentColorClasses: SkillClassMap = { 3: 'bg-secondary-surface text-secondary-onsurface', 4: 'bg-secondary-surface-2 text-secondary-onsurface-2', 5: `${styles.highSkill} bg-secondary-surface-2 text-secondary-onsurface-2`, -} +}; const levelClassesForHuman: SkillClassMap = { 3: 'bg-primary-surface text-primary-onsurface', 4: 'bg-primary-surface-2 text-primary-onsurface-2', 5: `${styles.highSkill} bg-primary-surface-2 text-primary-onsurface-2`, // TODO -} +}; const levelClasses: Record = { tool: primaryAccentColorClasses, technique: secondaryAccentColorClasses, human: levelClassesForHuman, -} +}; const skillMapper = (type: SkillType) => @@ -58,7 +58,7 @@ const skillMapper = name, level, type, - }) + }); export const toolSkills: SkillProps[] = map(skillMapper('tool'), [ ['React', 5], @@ -73,7 +73,7 @@ export const toolSkills: SkillProps[] = map(skillMapper('tool'), [ ['AgGrid', 3], ['Node', 4], ['Ramda', 4], -]) +]); export const techSkills: SkillProps[] = map(skillMapper('technique'), [ ['Performance', 4], @@ -89,7 +89,7 @@ export const techSkills: SkillProps[] = map(skillMapper('technique'), [ ['Functional programming', 5], ['Accessibility', 4], ['CSS animations', 5], -]) +]); export const humanSkills: SkillProps[] = map(skillMapper('human'), [ ['Mentoring', 4], @@ -100,10 +100,10 @@ export const humanSkills: SkillProps[] = map(skillMapper('human'), [ ['Leadership', 3], ['Stakeholder management', 5], ['Training', 5], -]) +]); export const AllSkills: SkillProps[] = [ ...toolSkills, ...techSkills, ...humanSkills, -] +]; diff --git a/apps/ageorgedev/src/components/Resume/SocialLink.spec.tsx b/apps/ageorgedev/src/components/Resume/SocialLink.spec.tsx index a41341b8..d98f0c61 100644 --- a/apps/ageorgedev/src/components/Resume/SocialLink.spec.tsx +++ b/apps/ageorgedev/src/components/Resume/SocialLink.spec.tsx @@ -1,47 +1,50 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { SocialLink } from './SocialLink' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { SocialLink } from './SocialLink'; describe('SocialLink', () => { it('renders an anchor element', () => { - const { container } = render() - expect(container.querySelector('a')).toBeInTheDocument() - }) + const { container } = render(); + expect(container.querySelector('a')).toBeInTheDocument(); + }); it('has target="_blank"', () => { - const { container } = render() - expect(container.querySelector('a')).toHaveAttribute('target', '_blank') - }) + const { container } = render(); + expect(container.querySelector('a')).toHaveAttribute('target', '_blank'); + }); it('has a rel containing noreferrer', () => { - const { container } = render() - const rel = container.querySelector('a')?.getAttribute('rel') ?? '' - expect(rel).toContain('noreferrer') - }) + const { container } = render(); + const rel = container.querySelector('a')?.getAttribute('rel') ?? ''; + expect(rel).toContain('noreferrer'); + }); it('links to the correct URL for each type', () => { const cases: Array<[Parameters[0]['type'], string]> = [ ['github', 'https://github.com/bassoGeorge'], ['linkedin', 'https://linkedin.com/in/anishbassogeorge'], ['web', 'https://ageorge.dev'], - ] + ]; for (const [type, expectedHref] of cases) { - const { container, unmount } = render() - expect(container.querySelector('a')).toHaveAttribute('href', expectedHref) - unmount() + const { container, unmount } = render(); + expect(container.querySelector('a')).toHaveAttribute( + 'href', + expectedHref + ); + unmount(); } - }) + }); it('shows display text when full=true', () => { - render() - expect(screen.getByText('anishgeorgehb@gmail.com')).toBeInTheDocument() - }) + render(); + expect(screen.getByText('anishgeorgehb@gmail.com')).toBeInTheDocument(); + }); it('hides display text when full is omitted', () => { - render() + render(); expect( screen.queryByText('anishgeorgehb@gmail.com') - ).not.toBeInTheDocument() - }) -}) + ).not.toBeInTheDocument(); + }); +}); diff --git a/apps/ageorgedev/src/components/Resume/SocialLink.tsx b/apps/ageorgedev/src/components/Resume/SocialLink.tsx index 645e7e45..22081bd6 100644 --- a/apps/ageorgedev/src/components/Resume/SocialLink.tsx +++ b/apps/ageorgedev/src/components/Resume/SocialLink.tsx @@ -1,5 +1,5 @@ -import { BodySm } from '@ageorgedev/design-system/typography/typography-components' -import type { Icon } from '@phosphor-icons/react' +import { BodySm } from '@ageorgedev/design-system/typography/typography-components'; +import type { Icon } from '@phosphor-icons/react'; import { EnvelopeIcon, GithubLogoIcon, @@ -7,7 +7,7 @@ import { LinkedinLogoIcon, MapPinIcon, PhoneIcon, -} from '@phosphor-icons/react/ssr' +} from '@phosphor-icons/react/ssr'; type SocialLinkType = | 'github' @@ -15,17 +15,17 @@ type SocialLinkType = | 'email' | 'web' | 'phone' - | 'location' + | 'location'; type SocialLinkProps = { - type: SocialLinkType - full?: boolean -} + type: SocialLinkType; + full?: boolean; +}; export function SocialLink({ type, full }: SocialLinkProps) { - const link = LINKS[type] - const displayLink = DISPLAY_LINKS[type] ?? link - const Icon = ICONS[type] + const link = LINKS[type]; + const displayLink = DISPLAY_LINKS[type] ?? link; + const Icon = ICONS[type]; return ( {full && {displayLink}} - ) + ); } -const email = 'anishgeorgehb@gmail.com' -const phone = '+91-999999' +const email = 'anishgeorgehb@gmail.com'; +const phone = '+91-999999'; const LINKS: Record = { github: 'https://github.com/bassoGeorge', @@ -49,13 +49,13 @@ const LINKS: Record = { web: 'https://ageorge.dev', phone: `tel:${phone}`, location: 'https://goo.gl/maps/Q7mUm5VQ5ZuLGV4v7', -} +}; const DISPLAY_LINKS: Partial> = { email, phone, location: 'Bengaluru, India', -} +}; const ICONS: Record = { github: GithubLogoIcon, @@ -64,4 +64,4 @@ const ICONS: Record = { web: GlobeIcon, phone: PhoneIcon, location: MapPinIcon, -} +}; diff --git a/apps/ageorgedev/src/mdx-components.tsx b/apps/ageorgedev/src/mdx-components.tsx index 486273a7..da5eb21b 100644 --- a/apps/ageorgedev/src/mdx-components.tsx +++ b/apps/ageorgedev/src/mdx-components.tsx @@ -3,8 +3,8 @@ import { Heading2, Heading3, Heading4, -} from '@ageorgedev/design-system/typography/typography-components' -import type { MDXComponents } from 'mdx/types' +} from '@ageorgedev/design-system/typography/typography-components'; +import type { MDXComponents } from 'mdx/types'; export function useMDXComponents(components: MDXComponents): MDXComponents { return { @@ -13,5 +13,5 @@ export function useMDXComponents(components: MDXComponents): MDXComponents { h2: Heading2, h3: Heading3, h4: Heading4, - } + }; } diff --git a/apps/ageorgedev/src/router.tsx b/apps/ageorgedev/src/router.tsx index 60f36e5a..c8b5b987 100644 --- a/apps/ageorgedev/src/router.tsx +++ b/apps/ageorgedev/src/router.tsx @@ -1,5 +1,5 @@ -import { createRouter as createTanStackRouter } from '@tanstack/react-router' -import { routeTree } from './routeTree.gen' +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; // import { getContext } from './integrations/tanstack-query/root-provider'; @@ -10,12 +10,12 @@ export function getRouter() { scrollRestoration: true, defaultPreload: 'intent', defaultPreloadStaleTime: 0, - }) - return router + }); + return router; } declare module '@tanstack/react-router' { interface Register { - router: ReturnType + router: ReturnType; } } diff --git a/apps/ageorgedev/src/routes/__root.tsx b/apps/ageorgedev/src/routes/__root.tsx index a4065322..7abe5d5f 100644 --- a/apps/ageorgedev/src/routes/__root.tsx +++ b/apps/ageorgedev/src/routes/__root.tsx @@ -1,18 +1,18 @@ -import { THEME_INIT_SCRIPT } from '@ageorgedev/design-system/theming/theme-init-script' +import { THEME_INIT_SCRIPT } from '@ageorgedev/design-system/theming/theme-init-script'; import { createRootRouteWithContext, HeadContent, Scripts, -} from '@tanstack/react-router' -import { GlobalProviders } from '../components/GlobalProviders' -import appCss from '../styles.css?url' +} from '@tanstack/react-router'; +import { GlobalProviders } from '../components/GlobalProviders'; +import appCss from '../styles.css?url'; // import type { QueryClient } from '@tanstack/react-query'; type MyRouterContext = { // queryClient?: QueryClient; - temp?: string -} + temp?: string; +}; export const Route = createRootRouteWithContext()({ head: () => ({ @@ -36,7 +36,7 @@ export const Route = createRootRouteWithContext()({ ], }), shellComponent: RootDocument, -}) +}); function RootDocument({ children }: { children: React.ReactNode }) { return ( @@ -67,5 +67,5 @@ function RootDocument({ children }: { children: React.ReactNode }) { - ) + ); } diff --git a/apps/ageorgedev/src/routes/_noLayout/resume.tsx b/apps/ageorgedev/src/routes/_noLayout/resume.tsx index 2fdb504a..f56cb7d4 100644 --- a/apps/ageorgedev/src/routes/_noLayout/resume.tsx +++ b/apps/ageorgedev/src/routes/_noLayout/resume.tsx @@ -1,4 +1,4 @@ -import { NameLogo } from '@ageorgedev/design-system/logo/NameLogo' +import { NameLogo } from '@ageorgedev/design-system/logo/NameLogo'; import { Body, BodyLg, @@ -8,27 +8,27 @@ import { Heading4, Heading5, Heading6, -} from '@ageorgedev/design-system/typography/typography-components' -import { cn } from '@ageorgedev/toolbelt/cn' -import { ArrowSquareOutIcon } from '@phosphor-icons/react/ssr' -import { createFileRoute } from '@tanstack/react-router' -import { Fragment } from 'react' +} from '@ageorgedev/design-system/typography/typography-components'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import { ArrowSquareOutIcon } from '@phosphor-icons/react/ssr'; +import { createFileRoute } from '@tanstack/react-router'; +import { Fragment } from 'react'; import { humanSkills, Skill, techSkills, toolSkills, -} from '../../components/Resume/Skill' -import { SocialLink } from '../../components/Resume/SocialLink' -import styles from './resume.module.css' -import './resume.global.css' +} from '../../components/Resume/Skill'; +import { SocialLink } from '../../components/Resume/SocialLink'; +import styles from './resume.module.css'; +import './resume.global.css'; export const Route = createFileRoute('/_noLayout/resume')({ component: RouteComponent, -}) +}); function RouteComponent() { - const socialLinksBlockClasses = 'flex flex-col gap-2' + const socialLinksBlockClasses = 'flex flex-col gap-2'; return (
@@ -281,13 +281,13 @@ function RouteComponent() {
- ) + ); } const education: Array<{ - period: string - institute: string - programme: string + period: string; + institute: string; + programme: string; }> = [ { period: '2012-2015', @@ -304,8 +304,8 @@ const education: Array<{ programme: 'Higher Secondary', institute: 'Fr. Agnel School | New Delhi', }, -] +]; function Imp({ children }: React.PropsWithChildren) { - return {children} + return {children}; } diff --git a/apps/ageorgedev/src/routes/_public.tsx b/apps/ageorgedev/src/routes/_public.tsx index 8710d29f..96365238 100644 --- a/apps/ageorgedev/src/routes/_public.tsx +++ b/apps/ageorgedev/src/routes/_public.tsx @@ -1,13 +1,13 @@ -import { TiltCard } from '@ageorgedev/design-system/cards/TiltCard' -import { ShortNameLogo } from '@ageorgedev/design-system/logo/ShortNameLogo' -import { ThemeSwitcher } from '@ageorgedev/design-system/theming/ThemeSwitcher' -import { InterfaceXl } from '@ageorgedev/design-system/typography/typography-components' -import { cn } from '@ageorgedev/toolbelt/cn' -import { createFileRoute, Link, Outlet } from '@tanstack/react-router' +import { TiltCard } from '@ageorgedev/design-system/cards/TiltCard'; +import { ShortNameLogo } from '@ageorgedev/design-system/logo/ShortNameLogo'; +import { ThemeSwitcher } from '@ageorgedev/design-system/theming/ThemeSwitcher'; +import { InterfaceXl } from '@ageorgedev/design-system/typography/typography-components'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import { createFileRoute, Link, Outlet } from '@tanstack/react-router'; export const Route = createFileRoute('/_public')({ component: RouteComponent, -}) +}); const links = [ { @@ -15,7 +15,7 @@ const links = [ to: '/talks', label: 'Talks', }, -] +]; function RouteComponent() { return ( @@ -55,5 +55,5 @@ function RouteComponent() { - ) + ); } diff --git a/apps/ageorgedev/src/routes/_public/index.tsx b/apps/ageorgedev/src/routes/_public/index.tsx index 7a90a033..121687e3 100644 --- a/apps/ageorgedev/src/routes/_public/index.tsx +++ b/apps/ageorgedev/src/routes/_public/index.tsx @@ -1,10 +1,10 @@ -import { createFileRoute } from '@tanstack/react-router' -import { HomeAboveFold } from '../../components/HomeAboveFold/HomeAboveFold' +import { createFileRoute } from '@tanstack/react-router'; +import { HomeAboveFold } from '../../components/HomeAboveFold/HomeAboveFold'; export const Route = createFileRoute('/_public/')({ component: HomePage, -}) +}); function HomePage() { - return + return ; } diff --git a/apps/ageorgedev/src/routes/_public/talks.index.tsx b/apps/ageorgedev/src/routes/_public/talks.index.tsx index 0edc911e..5d60ff3c 100644 --- a/apps/ageorgedev/src/routes/_public/talks.index.tsx +++ b/apps/ageorgedev/src/routes/_public/talks.index.tsx @@ -1,15 +1,15 @@ -import { TiltCard } from '@ageorgedev/design-system/cards/TiltCard' +import { TiltCard } from '@ageorgedev/design-system/cards/TiltCard'; import { Body, BodyXl, Heading1, Heading2, -} from '@ageorgedev/design-system/typography/typography-components' -import { createFileRoute, Link } from '@tanstack/react-router' +} from '@ageorgedev/design-system/typography/typography-components'; +import { createFileRoute, Link } from '@tanstack/react-router'; export const Route = createFileRoute('/_public/talks/')({ component: RouteComponent, -}) +}); function RouteComponent() { return ( @@ -35,5 +35,5 @@ function RouteComponent() { - ) + ); } diff --git a/apps/ageorgedev/src/routes/_public/talks.tailwind.tsx b/apps/ageorgedev/src/routes/_public/talks.tailwind.tsx index 82e5de1b..9e7138a8 100644 --- a/apps/ageorgedev/src/routes/_public/talks.tailwind.tsx +++ b/apps/ageorgedev/src/routes/_public/talks.tailwind.tsx @@ -1,11 +1,11 @@ -import { TalkTailwind } from '@ageorgedev/talk-tailwind' -import { createFileRoute } from '@tanstack/react-router' -import '@ageorgedev/reveal-framework/globals' +import { TalkTailwind } from '@ageorgedev/talk-tailwind'; +import { createFileRoute } from '@tanstack/react-router'; +import '@ageorgedev/reveal-framework/globals'; export const Route = createFileRoute('/_public/talks/tailwind')({ component: RouteComponent, -}) +}); function RouteComponent() { - return + return ; } diff --git a/apps/ageorgedev/tsconfig.json b/apps/ageorgedev/tsconfig.json index 7c4ba92d..d9e82867 100644 --- a/apps/ageorgedev/tsconfig.json +++ b/apps/ageorgedev/tsconfig.json @@ -4,6 +4,7 @@ "exclude": ["node_modules", "dist"], "compilerOptions": { "outDir": "dist", + "rootDir": "./src", "types": ["@testing-library/jest-dom"] } } diff --git a/apps/ageorgedev/vite.config.ts b/apps/ageorgedev/vite.config.ts index 181585af..541b373a 100644 --- a/apps/ageorgedev/vite.config.ts +++ b/apps/ageorgedev/vite.config.ts @@ -1,9 +1,9 @@ -import process from 'node:process' -import tailwindcss from '@tailwindcss/vite' -import { tanstackStart } from '@tanstack/react-start/plugin/vite' -import react from '@vitejs/plugin-react' -import { defineConfig, searchForWorkspaceRoot } from 'vite' -import dts from 'vite-plugin-dts' +import process from 'node:process'; +import tailwindcss from '@tailwindcss/vite'; +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig, searchForWorkspaceRoot } from 'vite'; +import dts from 'vite-plugin-dts'; const config = defineConfig({ cacheDir: '../../node_modules/.vite/ageorgedev', @@ -44,6 +44,6 @@ const config = defineConfig({ allow: [searchForWorkspaceRoot(process.cwd())], }, }, -}) +}); -export default config +export default config; diff --git a/apps/ageorgedev/vitest.config.ts b/apps/ageorgedev/vitest.config.ts index 9b6bd972..c641e500 100644 --- a/apps/ageorgedev/vitest.config.ts +++ b/apps/ageorgedev/vitest.config.ts @@ -1,5 +1,5 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], @@ -8,4 +8,4 @@ export default defineConfig({ setupFiles: ['@ageorgedev/testing-config/react-jsdom-test-setup.ts'], include: ['src/**/*.spec.{ts,tsx}'], }, -}) +}); diff --git a/apps/design-docs/.storybook/main.ts b/apps/design-docs/.storybook/main.ts index 0cde29c7..d3eafff8 100644 --- a/apps/design-docs/.storybook/main.ts +++ b/apps/design-docs/.storybook/main.ts @@ -1,17 +1,17 @@ // This file has been automatically migrated to valid ESM format by Storybook. -import { createRequire } from 'node:module' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import type { StorybookConfig } from '@storybook/react-vite' +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { StorybookConfig } from '@storybook/react-vite'; // import { mergeConfig } from 'vite' -const fileGlob = 'src/**/*.stories.@(js|jsx|ts|tsx|mdx)' -const projects = ['foundation-styles', 'design-system'] +const fileGlob = 'src/**/*.stories.@(js|jsx|ts|tsx|mdx)'; +const projects = ['foundation-styles', 'design-system']; -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) -const require = createRequire(import.meta.url) +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const require = createRequire(import.meta.url); const config: StorybookConfig = { stories: [ @@ -36,14 +36,14 @@ const config: StorybookConfig = { // } // }) // }, -} +}; -export default config +export default config; // To customize your Vite configuration you can use the viteFinal field. // Check https://storybook.js.org/docs/react/builders/vite#configuration // and https://nx.dev/packages/storybook/documents/custom-builder-configs function getAbsolutePath(value: string): string { - return dirname(require.resolve(join(value, 'package.json'))) + return dirname(require.resolve(join(value, 'package.json'))); } diff --git a/apps/design-docs/.storybook/preview.tsx b/apps/design-docs/.storybook/preview.tsx index 432a6809..7e0edf84 100644 --- a/apps/design-docs/.storybook/preview.tsx +++ b/apps/design-docs/.storybook/preview.tsx @@ -1,7 +1,7 @@ -import './storybook.css' -import { ThemeProvider } from '@ageorgedev/design-system/theming/ThemeProvider' -import { withThemeByClassName } from '@storybook/addon-themes' -import type { Preview } from '@storybook/react-vite' +import './storybook.css'; +import { ThemeProvider } from '@ageorgedev/design-system/theming/ThemeProvider'; +import { withThemeByClassName } from '@storybook/addon-themes'; +import type { Preview } from '@storybook/react-vite'; const preview: Preview = { decorators: [ @@ -25,6 +25,6 @@ const preview: Preview = { }, }, }, -} +}; -export default preview +export default preview; diff --git a/apps/design-docs/postcss.config.js b/apps/design-docs/postcss.config.js index 60bf9096..8984cd25 100644 --- a/apps/design-docs/postcss.config.js +++ b/apps/design-docs/postcss.config.js @@ -8,4 +8,4 @@ export default { 'postcss-nesting': {}, '@tailwindcss/postcss': {}, }, -} +}; diff --git a/apps/design-docs/stories/colors.stories.tsx b/apps/design-docs/stories/colors.stories.tsx index a41d6e69..f91ffc5d 100644 --- a/apps/design-docs/stories/colors.stories.tsx +++ b/apps/design-docs/stories/colors.stories.tsx @@ -3,16 +3,16 @@ import { BodyXl, BodyXs, InterfaceMd, -} from '@ageorgedev/design-system/typography/typography-components' -import { cn } from '@ageorgedev/toolbelt/cn' -import type { StoryObj } from '@storybook/react-vite' -import { createContext, type HTMLAttributes, useContext } from 'react' +} from '@ageorgedev/design-system/typography/typography-components'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import type { StoryObj } from '@storybook/react-vite'; +import { createContext, type HTMLAttributes, useContext } from 'react'; export default { title: 'Foundation/Colors', -} +}; -type Story = StoryObj +type Story = StoryObj; export const LightTheme: Story = { globals: { theme: 'light' }, @@ -35,7 +35,7 @@ export const LightTheme: Story = { ), -} +}; export const DarkTheme: Story = { globals: { theme: 'dark' }, @@ -58,35 +58,35 @@ export const DarkTheme: Story = { ), -} +}; function Layout({ children }: React.PropsWithChildren) { return (
{children}
- ) + ); } function Section({ className, ...attributes }: HTMLAttributes) { return (
- ) + ); } function SwatchSection({ className, ...props }: HTMLAttributes) { - return
+ return
; } function TextRow({ className, ...attributes }: Omit, 'children'>) { - const a11yFailures = useContext(A11yFailureContext) - const label = className?.replace('text-', '') ?? '' + const a11yFailures = useContext(A11yFailureContext); + const label = className?.replace('text-', '') ?? ''; return (
@@ -96,13 +96,13 @@ function TextRow({ {className}
- ) + ); } function Swatch({ className }: HTMLAttributes) { - const classes = className?.split(' ') - const bgClass = classes?.find((c) => c.startsWith('bg-')) - const fgClass = classes?.find((c) => c.startsWith('text-')) + const classes = className?.split(' '); + const bgClass = classes?.find((c) => c.startsWith('bg-')); + const fgClass = classes?.find((c) => c.startsWith('text-')); return (
) { > {bgClass}
{fgClass}
- ) + ); } function PageSample({ @@ -125,7 +125,7 @@ function PageSample({ {children}
- ) + ); } function ColorSamples({ a11yFailures = [] }: { a11yFailures?: string[] }) { @@ -206,7 +206,7 @@ function ColorSamples({ a11yFailures = [] }: { a11yFailures?: string[] }) {
- ) + ); } -const A11yFailureContext = createContext([]) +const A11yFailureContext = createContext([]); diff --git a/apps/design-docs/stories/elevations.stories.tsx b/apps/design-docs/stories/elevations.stories.tsx index 619f0a90..aba2be02 100644 --- a/apps/design-docs/stories/elevations.stories.tsx +++ b/apps/design-docs/stories/elevations.stories.tsx @@ -1,13 +1,13 @@ -import { BodyMd } from '@ageorgedev/design-system/typography/typography-components' -import { cn } from '@ageorgedev/toolbelt/cn' -import type { StoryObj } from '@storybook/react-vite' -import type { HTMLAttributes } from 'react' +import { BodyMd } from '@ageorgedev/design-system/typography/typography-components'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import type { StoryObj } from '@storybook/react-vite'; +import type { HTMLAttributes } from 'react'; export default { title: 'Foundation/Elevations', -} +}; -type Story = StoryObj +type Story = StoryObj; export const Elevations: Story = { render: () => ( @@ -25,7 +25,7 @@ export const Elevations: Story = {
), -} +}; function Box({ className, ...otherProps }: HTMLAttributes) { return ( @@ -45,5 +45,5 @@ function Box({ className, ...otherProps }: HTMLAttributes) { ))} - ) + ); } diff --git a/apps/design-docs/stories/spacing.stories.tsx b/apps/design-docs/stories/spacing.stories.tsx index 7b406db8..f01bb7df 100644 --- a/apps/design-docs/stories/spacing.stories.tsx +++ b/apps/design-docs/stories/spacing.stories.tsx @@ -1,12 +1,12 @@ -import { Interface } from '@ageorgedev/design-system/typography/typography-components' -import { cn } from '@ageorgedev/toolbelt/cn' -import type { StoryObj } from '@storybook/react-vite' +import { Interface } from '@ageorgedev/design-system/typography/typography-components'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import type { StoryObj } from '@storybook/react-vite'; export default { title: 'Foundation/Spacing', -} +}; -type Story = StoryObj +type Story = StoryObj; const SPACING_VALUES: Array<[string, string, string]> = [ ['w-0', '0', '0'], @@ -31,7 +31,7 @@ const SPACING_VALUES: Array<[string, string, string]> = [ ['w-thin-line', 'thin-line', '1px'], ['w-medium-line', 'medium-line', '2px'], ['w-thick-line', 'thick-line', '4px'], -] +]; function SpaceBar({ className, name }: { className: string; name: string }) { return ( @@ -39,7 +39,7 @@ function SpaceBar({ className, name }: { className: string; name: string }) { {name}
- ) + ); } export const SpacingScale: Story = { @@ -57,4 +57,4 @@ export const SpacingScale: Story = { ))} ), -} +}; diff --git a/apps/design-docs/tsconfig.json b/apps/design-docs/tsconfig.json index 060bc0aa..84c561f2 100644 --- a/apps/design-docs/tsconfig.json +++ b/apps/design-docs/tsconfig.json @@ -3,6 +3,7 @@ "include": ["stories"], "exclude": ["node_modules", "dist", "storybook-static"], "compilerOptions": { + "rootDir": "./stories", "outDir": "dist" } } diff --git a/apps/design-docs/vite.config.ts b/apps/design-docs/vite.config.ts index 262ad46b..bcb63a61 100644 --- a/apps/design-docs/vite.config.ts +++ b/apps/design-docs/vite.config.ts @@ -1,7 +1,7 @@ -import tailwindcss from '@tailwindcss/vite' -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; export default defineConfig({ plugins: [tailwindcss(), react()], -}) +}); diff --git a/apps/dnd-cli/src/App.tsx b/apps/dnd-cli/src/App.tsx index d768a045..3ea8b389 100644 --- a/apps/dnd-cli/src/App.tsx +++ b/apps/dnd-cli/src/App.tsx @@ -1,58 +1,63 @@ /** biome-ignore-all lint/style/noNonNullAssertion: not important at the moment */ -import { Box, Text, useApp } from 'ink' -import { useState } from 'react' -import { AbilitiesStep } from './steps/AbilitiesStep.js' -import type { CombatValues } from './steps/CombatStep.js' -import { CombatStep } from './steps/CombatStep.js' -import { FilenameStep } from './steps/FilenameStep.js' -import type { IdentityValues } from './steps/IdentityStep.js' -import { IdentityStep } from './steps/IdentityStep.js' -import type { ProficiencyValues } from './steps/ProficienciesStep.js' -import { ProficienciesStep } from './steps/ProficienciesStep.js' -import type { AbilityValues, WizardState } from './types.js' +import { Box, Text, useApp } from 'ink'; +import { useState } from 'react'; +import { AbilitiesStep } from './steps/AbilitiesStep.js'; +import type { CombatValues } from './steps/CombatStep.js'; +import { CombatStep } from './steps/CombatStep.js'; +import { FilenameStep } from './steps/FilenameStep.js'; +import type { IdentityValues } from './steps/IdentityStep.js'; +import { IdentityStep } from './steps/IdentityStep.js'; +import type { ProficiencyValues } from './steps/ProficienciesStep.js'; +import { ProficienciesStep } from './steps/ProficienciesStep.js'; +import type { AbilityValues, WizardState } from './types.js'; -type StepId = 'identity' | 'abilities' | 'combat' | 'proficiencies' | 'filename' +type StepId = + | 'identity' + | 'abilities' + | 'combat' + | 'proficiencies' + | 'filename'; function toKebabCase(name: string): string { return name .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, '') + .replace(/^-|-$/g, ''); } export function App({ onComplete, }: { - onComplete: (state: WizardState) => void + onComplete: (state: WizardState) => void; }) { - const { exit } = useApp() - const [step, setStep] = useState('identity') - const [identity, setIdentity] = useState(null) - const [abilities, setAbilities] = useState(null) - const [combat, setCombat] = useState(null) + const { exit } = useApp(); + const [step, setStep] = useState('identity'); + const [identity, setIdentity] = useState(null); + const [abilities, setAbilities] = useState(null); + const [combat, setCombat] = useState(null); const [proficiencies, setProficiencies] = useState( null - ) + ); function handleIdentity(values: IdentityValues) { - setIdentity(values) - setStep('abilities') + setIdentity(values); + setStep('abilities'); } function handleAbilities(values: AbilityValues) { - setAbilities(values) - setStep('combat') + setAbilities(values); + setStep('combat'); } function handleCombat(values: CombatValues) { - setCombat(values) - setStep('proficiencies') + setCombat(values); + setStep('proficiencies'); } function handleProficiencies(values: ProficiencyValues) { - setProficiencies(values) - setStep('filename') + setProficiencies(values); + setStep('filename'); } function handleFilename({ filename }: { filename: string }) { @@ -72,14 +77,14 @@ export function App({ savingThrowProficiencies: proficiencies!.savingThrowProficiencies, skillProficiencies: proficiencies!.skillProficiencies, filename, - } - onComplete(state) - exit() + }; + onComplete(state); + exit(); } const defaultFilename = identity ? `${toKebabCase(identity.name)}.data.ts` - : 'character.data.ts' + : 'character.data.ts'; return ( @@ -102,5 +107,5 @@ export function App({ /> )} - ) + ); } diff --git a/apps/dnd-cli/src/index.tsx b/apps/dnd-cli/src/index.tsx index d0cc63b6..c01673f9 100644 --- a/apps/dnd-cli/src/index.tsx +++ b/apps/dnd-cli/src/index.tsx @@ -1,34 +1,34 @@ -import { mkdirSync, writeFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { render } from 'ink' -import { App } from './App.js' -import { generateFile } from './lib/generateFile.js' -import type { WizardState } from './types.js' +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { render } from 'ink'; +import { App } from './App.js'; +import { generateFile } from './lib/generateFile.js'; +import type { WizardState } from './types.js'; -const __dirname = dirname(fileURLToPath(import.meta.url)) +const __dirname = dirname(fileURLToPath(import.meta.url)); const CHARACTERS_DIR = join( __dirname, '../../../packages/dnd-character-sheet/src/characters' -) +); -const result: { state: WizardState | null } = { state: null } +const result: { state: WizardState | null } = { state: null }; const { waitUntilExit } = render( { - result.state = state + result.state = state; }} /> -) +); -await waitUntilExit() +await waitUntilExit(); if (result.state) { - mkdirSync(CHARACTERS_DIR, { recursive: true }) - const outputPath = join(CHARACTERS_DIR, result.state.filename) - writeFileSync(outputPath, generateFile(result.state)) + mkdirSync(CHARACTERS_DIR, { recursive: true }); + const outputPath = join(CHARACTERS_DIR, result.state.filename); + writeFileSync(outputPath, generateFile(result.state)); console.log( `\nāœ“ Written: packages/dnd-character-sheet/src/characters/${result.state.filename}` - ) + ); } diff --git a/apps/dnd-cli/src/lib/defaults.ts b/apps/dnd-cli/src/lib/defaults.ts index b9d8ac1b..a8454242 100644 --- a/apps/dnd-cli/src/lib/defaults.ts +++ b/apps/dnd-cli/src/lib/defaults.ts @@ -1,4 +1,4 @@ -import type { HitDiceType } from '../types.js' +import type { HitDiceType } from '../types.js'; export const DEFAULTS = { level: 1, @@ -7,4 +7,4 @@ export const DEFAULTS = { armorClass: 10, speed: 30, hitDiceType: 'd10' as HitDiceType, -} as const +} as const; diff --git a/apps/dnd-cli/src/lib/generateFile.ts b/apps/dnd-cli/src/lib/generateFile.ts index 4cd174c8..a59b35cc 100644 --- a/apps/dnd-cli/src/lib/generateFile.ts +++ b/apps/dnd-cli/src/lib/generateFile.ts @@ -1,5 +1,5 @@ -import { Ability } from '@ageorgedev/dnd-character-sheet/models' -import type { WizardState } from '../types.js' +import { Ability } from '@ageorgedev/dnd-character-sheet/models'; +import type { WizardState } from '../types.js'; function toExportName(name: string): string { return name @@ -7,28 +7,28 @@ function toExportName(name: string): string { .split(/\s+/) .filter(Boolean) .map((word, i) => { - const clean = word.replace(/[^a-zA-Z0-9]/g, '') - if (!clean) return '' - if (i === 0) return clean.charAt(0).toLowerCase() + clean.slice(1) - return clean.charAt(0).toUpperCase() + clean.slice(1).toLowerCase() + const clean = word.replace(/[^a-zA-Z0-9]/g, ''); + if (!clean) return ''; + if (i === 0) return clean.charAt(0).toLowerCase() + clean.slice(1); + return clean.charAt(0).toUpperCase() + clean.slice(1).toLowerCase(); }) - .join('') + .join(''); } function formatSkillArray(items: string[]): string { - if (items.length === 0) return '[]' - return `[${items.map((s) => `Skill.${s}`).join(', ')}]` + if (items.length === 0) return '[]'; + return `[${items.map((s) => `Skill.${s}`).join(', ')}]`; } function formatAbilityArray(items: string[]): string { - if (items.length === 0) return '[]' - return `[${items.map((s) => `"${s}"`).join(', ')}]` + if (items.length === 0) return '[]'; + return `[${items.map((s) => `"${s}"`).join(', ')}]`; } export function generateFile(state: WizardState): string { - const exportName = toExportName(state.name) || 'character' + const exportName = toExportName(state.name) || 'character'; const subclassLine = state.subclass ? `\n subclass: "${state.subclass}",` - : '' + : ''; return `import type { Character } from '../lib/models/character'; import type { Skill } from '../lib/models/skills'; @@ -79,5 +79,5 @@ export const ${exportName}: Character = { currency: { cp: 0, sp: 0, ep: 0, gp: 0, pp: 0 }, features: [], }; -` +`; } diff --git a/apps/dnd-cli/src/steps/AbilitiesStep.tsx b/apps/dnd-cli/src/steps/AbilitiesStep.tsx index b8529ae3..4856f960 100644 --- a/apps/dnd-cli/src/steps/AbilitiesStep.tsx +++ b/apps/dnd-cli/src/steps/AbilitiesStep.tsx @@ -1,8 +1,8 @@ -import { Ability } from '@ageorgedev/dnd-character-sheet/models' -import { Box, Text, useInput } from 'ink' -import { useState } from 'react' -import { DEFAULTS } from '../lib/defaults.js' -import type { AbilityValues } from '../types.js' +import { Ability } from '@ageorgedev/dnd-character-sheet/models'; +import { Box, Text, useInput } from 'ink'; +import { useState } from 'react'; +import { DEFAULTS } from '../lib/defaults.js'; +import type { AbilityValues } from '../types.js'; const FIELDS: Array<{ key: Ability; label: string }> = [ { key: Ability.Strength, label: 'Strength (STR)' }, @@ -11,24 +11,24 @@ const FIELDS: Array<{ key: Ability; label: string }> = [ { key: Ability.Intelligence, label: 'Intelligence (INT)' }, { key: Ability.Wisdom, label: 'Wisdom (WIS)' }, { key: Ability.Charisma, label: 'Charisma (CHA)' }, -] +]; -const LABEL_WIDTH = 22 +const LABEL_WIDTH = 22; export function AbilitiesStep({ onComplete, }: { - onComplete: (values: AbilityValues) => void + onComplete: (values: AbilityValues) => void; }) { - const [fieldIndex, setFieldIndex] = useState(0) - const [collected, setCollected] = useState>({}) - const [buffer, setBuffer] = useState(String(DEFAULTS.abilityScore)) + const [fieldIndex, setFieldIndex] = useState(0); + const [collected, setCollected] = useState>({}); + const [buffer, setBuffer] = useState(String(DEFAULTS.abilityScore)); - const current = FIELDS[fieldIndex] + const current = FIELDS[fieldIndex]; useInput((input, key) => { if (key.return) { - const raw = buffer.trim() + const raw = buffer.trim(); const value = Math.max( 1, Math.min( @@ -36,22 +36,22 @@ export function AbilitiesStep({ parseInt(raw || String(DEFAULTS.abilityScore), 10) || DEFAULTS.abilityScore ) - ) - const next = { ...collected, [current.key]: value } + ); + const next = { ...collected, [current.key]: value }; if (fieldIndex < FIELDS.length - 1) { - setCollected(next) - setBuffer(String(DEFAULTS.abilityScore)) - setFieldIndex((i) => i + 1) + setCollected(next); + setBuffer(String(DEFAULTS.abilityScore)); + setFieldIndex((i) => i + 1); } else { - onComplete(next as AbilityValues) + onComplete(next as AbilityValues); } } else if (key.backspace || key.delete) { - setBuffer((b) => b.slice(0, -1)) + setBuffer((b) => b.slice(0, -1)); } else if (input && !key.ctrl && !key.meta && /[0-9]/.test(input)) { - setBuffer((b) => (b.length < 2 ? b + input : b)) + setBuffer((b) => (b.length < 2 ? b + input : b)); } - }) + }); return ( @@ -78,5 +78,5 @@ export function AbilitiesStep({ Enter to confirm (default: {DEFAULTS.abilityScore}, range 1–30) - ) + ); } diff --git a/apps/dnd-cli/src/steps/CombatStep.tsx b/apps/dnd-cli/src/steps/CombatStep.tsx index d38b9a33..6ec9375f 100644 --- a/apps/dnd-cli/src/steps/CombatStep.tsx +++ b/apps/dnd-cli/src/steps/CombatStep.tsx @@ -1,18 +1,18 @@ -import { Box, Text, useInput } from 'ink' -import { useState } from 'react' -import { DEFAULTS } from '../lib/defaults.js' -import type { HitDiceType } from '../types.js' +import { Box, Text, useInput } from 'ink'; +import { useState } from 'react'; +import { DEFAULTS } from '../lib/defaults.js'; +import type { HitDiceType } from '../types.js'; export interface CombatValues { - armorClass: number - speed: number - hitPointsMax: number - hitDiceType: HitDiceType + armorClass: number; + speed: number; + hitPointsMax: number; + hitDiceType: HitDiceType; } -const HIT_DICE: HitDiceType[] = ['d6', 'd8', 'd10', 'd12'] +const HIT_DICE: HitDiceType[] = ['d6', 'd8', 'd10', 'd12']; -type Phase = 'ac' | 'speed' | 'hp' | 'dice' +type Phase = 'ac' | 'speed' | 'hp' | 'dice'; const PHASE_CONFIG: Record< Exclude, @@ -29,80 +29,80 @@ const PHASE_CONFIG: Record< hint: `default: ${DEFAULTS.speed}`, }, hp: { label: 'Hit Points (max)', defaultValue: 0, hint: 'required' }, -} +}; -const LABEL_WIDTH = 24 +const LABEL_WIDTH = 24; export function CombatStep({ onComplete, }: { - onComplete: (values: CombatValues) => void + onComplete: (values: CombatValues) => void; }) { - const [phase, setPhase] = useState('ac') - const [ac, setAc] = useState() - const [speed, setSpeed] = useState() - const [hp, setHp] = useState() + const [phase, setPhase] = useState('ac'); + const [ac, setAc] = useState(); + const [speed, setSpeed] = useState(); + const [hp, setHp] = useState(); const [diceIndex, setDiceIndex] = useState( HIT_DICE.indexOf(DEFAULTS.hitDiceType) - ) - const [buffer, setBuffer] = useState(String(DEFAULTS.armorClass)) - const [error, setError] = useState('') + ); + const [buffer, setBuffer] = useState(String(DEFAULTS.armorClass)); + const [error, setError] = useState(''); useInput((input, key) => { if (phase === 'dice') { if (key.leftArrow) - setDiceIndex((i) => (i - 1 + HIT_DICE.length) % HIT_DICE.length) - else if (key.rightArrow) setDiceIndex((i) => (i + 1) % HIT_DICE.length) + setDiceIndex((i) => (i - 1 + HIT_DICE.length) % HIT_DICE.length); + else if (key.rightArrow) setDiceIndex((i) => (i + 1) % HIT_DICE.length); else if (key.return) { onComplete({ armorClass: ac ?? DEFAULTS.armorClass, speed: speed ?? DEFAULTS.speed, hitPointsMax: hp ?? 0, hitDiceType: HIT_DICE[diceIndex], - }) + }); } - return + return; } if (key.return) { - const cfg = PHASE_CONFIG[phase] - const raw = buffer.trim() + const cfg = PHASE_CONFIG[phase]; + const raw = buffer.trim(); if (phase === 'hp' && !raw) { - setError('Required') - return + setError('Required'); + return; } const value = - parseInt(raw || String(cfg.defaultValue), 10) || cfg.defaultValue - setError('') + parseInt(raw || String(cfg.defaultValue), 10) || cfg.defaultValue; + setError(''); if (phase === 'ac') { - setAc(value) - setBuffer(String(DEFAULTS.speed)) - setPhase('speed') + setAc(value); + setBuffer(String(DEFAULTS.speed)); + setPhase('speed'); } else if (phase === 'speed') { - setSpeed(value) - setBuffer('') - setPhase('hp') + setSpeed(value); + setBuffer(''); + setPhase('hp'); } else if (phase === 'hp') { - setHp(value) - setPhase('dice') + setHp(value); + setPhase('dice'); } } else if (key.backspace || key.delete) { - setBuffer((b) => b.slice(0, -1)) - setError('') + setBuffer((b) => b.slice(0, -1)); + setError(''); } else if (input && !key.ctrl && !key.meta && /[0-9]/.test(input)) { - setBuffer((b) => b + input) - setError('') + setBuffer((b) => b + input); + setError(''); } - }) + }); - const completedPhases: Array<{ label: string; value: string }> = [] + const completedPhases: Array<{ label: string; value: string }> = []; if (ac !== undefined) - completedPhases.push({ label: 'Armor Class (AC)', value: String(ac) }) + completedPhases.push({ label: 'Armor Class (AC)', value: String(ac) }); if (speed !== undefined) - completedPhases.push({ label: 'Speed (ft)', value: String(speed) }) + completedPhases.push({ label: 'Speed (ft)', value: String(speed) }); if (hp !== undefined) - completedPhases.push({ label: 'Hit Points (max)', value: String(hp) }) + completedPhases.push({ label: 'Hit Points (max)', value: String(hp) }); return ( @@ -143,5 +143,5 @@ export function CombatStep({ Enter to confirm ({PHASE_CONFIG[phase].hint}) )} - ) + ); } diff --git a/apps/dnd-cli/src/steps/FilenameStep.tsx b/apps/dnd-cli/src/steps/FilenameStep.tsx index 37f6c42c..11cb9cba 100644 --- a/apps/dnd-cli/src/steps/FilenameStep.tsx +++ b/apps/dnd-cli/src/steps/FilenameStep.tsx @@ -1,36 +1,36 @@ -import { Box, Text, useInput } from 'ink' -import { useState } from 'react' +import { Box, Text, useInput } from 'ink'; +import { useState } from 'react'; export interface FilenameValues { - filename: string + filename: string; } export function FilenameStep({ defaultFilename, onComplete, }: { - defaultFilename: string - onComplete: (values: FilenameValues) => void + defaultFilename: string; + onComplete: (values: FilenameValues) => void; }) { - const [buffer, setBuffer] = useState(defaultFilename) - const [error, setError] = useState('') + const [buffer, setBuffer] = useState(defaultFilename); + const [error, setError] = useState(''); useInput((input, key) => { if (key.return) { - const filename = buffer.trim() || defaultFilename + const filename = buffer.trim() || defaultFilename; if (!filename) { - setError('Filename is required') - return + setError('Filename is required'); + return; } - onComplete({ filename }) + onComplete({ filename }); } else if (key.backspace || key.delete) { - setBuffer((b) => b.slice(0, -1)) - setError('') + setBuffer((b) => b.slice(0, -1)); + setError(''); } else if (input && !key.ctrl && !key.meta) { - setBuffer((b) => b + input) - setError('') + setBuffer((b) => b + input); + setError(''); } - }) + }); return ( @@ -54,5 +54,5 @@ export function FilenameStep({ Enter to write the file )} - ) + ); } diff --git a/apps/dnd-cli/src/steps/IdentityStep.tsx b/apps/dnd-cli/src/steps/IdentityStep.tsx index f6ecd209..a82a99c3 100644 --- a/apps/dnd-cli/src/steps/IdentityStep.tsx +++ b/apps/dnd-cli/src/steps/IdentityStep.tsx @@ -1,24 +1,24 @@ -import { Box, Text, useInput } from 'ink' -import { useState } from 'react' -import { DEFAULTS } from '../lib/defaults.js' +import { Box, Text, useInput } from 'ink'; +import { useState } from 'react'; +import { DEFAULTS } from '../lib/defaults.js'; export interface IdentityValues { - name: string - characterClass: string - subclass: string - level: number - species: string - background: string - experiencePoints: number + name: string; + characterClass: string; + subclass: string; + level: number; + species: string; + background: string; + experiencePoints: number; } interface FieldDef { - key: keyof IdentityValues - label: string - hint: string - defaultValue: string - required: boolean - numeric: boolean + key: keyof IdentityValues; + label: string; + hint: string; + defaultValue: string; + required: boolean; + numeric: boolean; } const FIELDS: FieldDef[] = [ @@ -78,48 +78,48 @@ const FIELDS: FieldDef[] = [ required: true, numeric: true, }, -] +]; -const LABEL_WIDTH = 24 +const LABEL_WIDTH = 24; export function IdentityStep({ onComplete, }: { - onComplete: (values: IdentityValues) => void + onComplete: (values: IdentityValues) => void; }) { - const [fieldIndex, setFieldIndex] = useState(0) - const [collected, setCollected] = useState>({}) - const [buffer, setBuffer] = useState(FIELDS[0].defaultValue) - const [error, setError] = useState('') + const [fieldIndex, setFieldIndex] = useState(0); + const [collected, setCollected] = useState>({}); + const [buffer, setBuffer] = useState(FIELDS[0].defaultValue); + const [error, setError] = useState(''); - const current = FIELDS[fieldIndex] + const current = FIELDS[fieldIndex]; useInput((input, key) => { if (key.return) { - const raw = buffer.trim() !== '' ? buffer.trim() : current.defaultValue + const raw = buffer.trim() !== '' ? buffer.trim() : current.defaultValue; if (current.required && !raw) { - setError('Required') - return + setError('Required'); + return; } - const value = current.numeric ? parseInt(raw || '0', 10) : raw - const next = { ...collected, [current.key]: value } + const value = current.numeric ? parseInt(raw || '0', 10) : raw; + const next = { ...collected, [current.key]: value }; if (fieldIndex < FIELDS.length - 1) { - setCollected(next) - setBuffer(FIELDS[fieldIndex + 1].defaultValue) - setFieldIndex((i) => i + 1) - setError('') + setCollected(next); + setBuffer(FIELDS[fieldIndex + 1].defaultValue); + setFieldIndex((i) => i + 1); + setError(''); } else { - onComplete(next as IdentityValues) + onComplete(next as IdentityValues); } } else if (key.backspace || key.delete) { - setBuffer((b) => b.slice(0, -1)) - setError('') + setBuffer((b) => b.slice(0, -1)); + setError(''); } else if (input && !key.ctrl && !key.meta) { - setBuffer((b) => b + input) - setError('') + setBuffer((b) => b + input); + setError(''); } - }) + }); return ( @@ -146,5 +146,5 @@ export function IdentityStep({ Enter to confirm )} - ) + ); } diff --git a/apps/dnd-cli/src/steps/ProficienciesStep.tsx b/apps/dnd-cli/src/steps/ProficienciesStep.tsx index 798b6a3c..0de2342e 100644 --- a/apps/dnd-cli/src/steps/ProficienciesStep.tsx +++ b/apps/dnd-cli/src/steps/ProficienciesStep.tsx @@ -2,73 +2,73 @@ import { ABILITY_DETAILS, ALL_ABILITIES, Skill, -} from '@ageorgedev/dnd-character-sheet/models' -import { Box, Text, useInput } from 'ink' -import { useState } from 'react' +} from '@ageorgedev/dnd-character-sheet/models'; +import { Box, Text, useInput } from 'ink'; +import { useState } from 'react'; const SAVING_THROW_LABELS = Object.fromEntries( ALL_ABILITIES.map((ability) => [ability, ABILITY_DETAILS[ability].label]) -) +); -const AllSkills = Object.keys(Skill) +const AllSkills = Object.keys(Skill); export interface ProficiencyValues { - savingThrowProficiencies: string[] - skillProficiencies: string[] + savingThrowProficiencies: string[]; + skillProficiencies: string[]; } export function ProficienciesStep({ onComplete, }: { - onComplete: (values: ProficiencyValues) => void + onComplete: (values: ProficiencyValues) => void; }) { const [phase, setPhase] = useState<'saving-throws' | 'skills'>( 'saving-throws' - ) - const [cursor, setCursor] = useState(0) - const [selectedST, setSelectedST] = useState>(new Set()) - const [selectedSkills, setSelectedSkills] = useState>(new Set()) + ); + const [cursor, setCursor] = useState(0); + const [selectedST, setSelectedST] = useState>(new Set()); + const [selectedSkills, setSelectedSkills] = useState>(new Set()); - const isST = phase === 'saving-throws' - const list = isST ? ALL_ABILITIES : AllSkills - const labels = (isST ? SAVING_THROW_LABELS : Skill) as Record - const selected = isST ? selectedST : selectedSkills + const isST = phase === 'saving-throws'; + const list = isST ? ALL_ABILITIES : AllSkills; + const labels = (isST ? SAVING_THROW_LABELS : Skill) as Record; + const selected = isST ? selectedST : selectedSkills; const setSelected = isST ? (fn: (s: Set) => Set) => setSelectedST(fn) - : (fn: (s: Set) => Set) => setSelectedSkills(fn) + : (fn: (s: Set) => Set) => setSelectedSkills(fn); useInput((input, key) => { if (key.upArrow) { - setCursor((c) => Math.max(0, c - 1)) + setCursor((c) => Math.max(0, c - 1)); } else if (key.downArrow) { - setCursor((c) => Math.min(list.length - 1, c + 1)) + setCursor((c) => Math.min(list.length - 1, c + 1)); } else if (input === ' ') { - const item = list[cursor] + const item = list[cursor]; setSelected((s) => { - const next = new Set(s) - if (next.has(item)) next.delete(item) - else next.add(item) - return next - }) + const next = new Set(s); + if (next.has(item)) next.delete(item); + else next.add(item); + return next; + }); } else if (key.return) { if (phase === 'saving-throws') { - setPhase('skills') - setCursor(0) + setPhase('skills'); + setCursor(0); } else { onComplete({ savingThrowProficiencies: Array.from(selectedST), skillProficiencies: Array.from(selectedSkills), - }) + }); } } - }) + }); const title = isST ? '── Step 4a: Saving Throw Proficiencies ──────────' - : '── Step 4b: Skill Proficiencies ─────────────────' + : '── Step 4b: Skill Proficiencies ─────────────────'; - const count = selected.size - const countLabel = count > 0 ? ` (${count} selected)` : ' (none selected)' + const count = selected.size; + const countLabel = count > 0 ? ` (${count} selected)` : ' (none selected)'; return ( @@ -77,8 +77,8 @@ export function ProficienciesStep({ {list.map((item, i) => { - const isCursor = i === cursor - const isSelected = selected.has(item) + const isCursor = i === cursor; + const isSelected = selected.has(item); return ( @@ -89,7 +89,7 @@ export function ProficienciesStep({ {labels[item]} - ) + ); })} @@ -99,5 +99,5 @@ export function ProficienciesStep({ - ) + ); } diff --git a/apps/dnd-cli/src/types.ts b/apps/dnd-cli/src/types.ts index b8dd6672..b70fedf0 100644 --- a/apps/dnd-cli/src/types.ts +++ b/apps/dnd-cli/src/types.ts @@ -1,25 +1,25 @@ -import type { Ability } from '@ageorgedev/dnd-character-sheet/models' +import type { Ability } from '@ageorgedev/dnd-character-sheet/models'; -export type HitDiceType = 'd6' | 'd8' | 'd10' | 'd12' +export type HitDiceType = 'd6' | 'd8' | 'd10' | 'd12'; export interface WizardState { - name: string - characterClass: string - subclass: string - level: number - species: string - background: string - experiencePoints: number - abilities: AbilityValues - armorClass: number - speed: number - hitPointsMax: number - hitDiceType: HitDiceType - savingThrowProficiencies: string[] - skillProficiencies: string[] - filename: string + name: string; + characterClass: string; + subclass: string; + level: number; + species: string; + background: string; + experiencePoints: number; + abilities: AbilityValues; + armorClass: number; + speed: number; + hitPointsMax: number; + hitDiceType: HitDiceType; + savingThrowProficiencies: string[]; + skillProficiencies: string[]; + filename: string; } export type AbilityValues = { - [key in Ability]: number -} + [key in Ability]: number; +}; diff --git a/apps/game-tools/src/components/GlobalProviders.tsx b/apps/game-tools/src/components/GlobalProviders.tsx index f2af2fb8..9f7816de 100644 --- a/apps/game-tools/src/components/GlobalProviders.tsx +++ b/apps/game-tools/src/components/GlobalProviders.tsx @@ -1,5 +1,5 @@ -import { ThemeProvider } from '@ageorgedev/design-system/theming/ThemeProvider' +import { ThemeProvider } from '@ageorgedev/design-system/theming/ThemeProvider'; export function GlobalProviders({ children }: React.PropsWithChildren) { - return {children} + return {children}; } diff --git a/apps/game-tools/src/routeTree.gen.ts b/apps/game-tools/src/routeTree.gen.ts index 6bea8bb8..6560f199 100644 --- a/apps/game-tools/src/routeTree.gen.ts +++ b/apps/game-tools/src/routeTree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as PublicRouteImport } from './routes/_public' import { Route as PublicIndexRouteImport } from './routes/_public/index' import { Route as PublicDndCharactersIndexRouteImport } from './routes/_public/dnd/characters/index' +import { Route as PublicDndCharactersOmarinKenateRouteImport } from './routes/_public/dnd/characters/omarin-kenate' import { Route as PublicDndCharactersExampleRouteImport } from './routes/_public/dnd/characters/example' const PublicRoute = PublicRouteImport.update({ @@ -29,6 +30,12 @@ const PublicDndCharactersIndexRoute = path: '/dnd/characters/', getParentRoute: () => PublicRoute, } as any) +const PublicDndCharactersOmarinKenateRoute = + PublicDndCharactersOmarinKenateRouteImport.update({ + id: '/dnd/characters/omarin-kenate', + path: '/dnd/characters/omarin-kenate', + getParentRoute: () => PublicRoute, + } as any) const PublicDndCharactersExampleRoute = PublicDndCharactersExampleRouteImport.update({ id: '/dnd/characters/example', @@ -39,11 +46,13 @@ const PublicDndCharactersExampleRoute = export interface FileRoutesByFullPath { '/': typeof PublicIndexRoute '/dnd/characters/example': typeof PublicDndCharactersExampleRoute + '/dnd/characters/omarin-kenate': typeof PublicDndCharactersOmarinKenateRoute '/dnd/characters/': typeof PublicDndCharactersIndexRoute } export interface FileRoutesByTo { '/': typeof PublicIndexRoute '/dnd/characters/example': typeof PublicDndCharactersExampleRoute + '/dnd/characters/omarin-kenate': typeof PublicDndCharactersOmarinKenateRoute '/dnd/characters': typeof PublicDndCharactersIndexRoute } export interface FileRoutesById { @@ -51,18 +60,28 @@ export interface FileRoutesById { '/_public': typeof PublicRouteWithChildren '/_public/': typeof PublicIndexRoute '/_public/dnd/characters/example': typeof PublicDndCharactersExampleRoute + '/_public/dnd/characters/omarin-kenate': typeof PublicDndCharactersOmarinKenateRoute '/_public/dnd/characters/': typeof PublicDndCharactersIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/dnd/characters/example' | '/dnd/characters/' + fullPaths: + | '/' + | '/dnd/characters/example' + | '/dnd/characters/omarin-kenate' + | '/dnd/characters/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/dnd/characters/example' | '/dnd/characters' + to: + | '/' + | '/dnd/characters/example' + | '/dnd/characters/omarin-kenate' + | '/dnd/characters' id: | '__root__' | '/_public' | '/_public/' | '/_public/dnd/characters/example' + | '/_public/dnd/characters/omarin-kenate' | '/_public/dnd/characters/' fileRoutesById: FileRoutesById } @@ -93,6 +112,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PublicDndCharactersIndexRouteImport parentRoute: typeof PublicRoute } + '/_public/dnd/characters/omarin-kenate': { + id: '/_public/dnd/characters/omarin-kenate' + path: '/dnd/characters/omarin-kenate' + fullPath: '/dnd/characters/omarin-kenate' + preLoaderRoute: typeof PublicDndCharactersOmarinKenateRouteImport + parentRoute: typeof PublicRoute + } '/_public/dnd/characters/example': { id: '/_public/dnd/characters/example' path: '/dnd/characters/example' @@ -106,12 +132,14 @@ declare module '@tanstack/react-router' { interface PublicRouteChildren { PublicIndexRoute: typeof PublicIndexRoute PublicDndCharactersExampleRoute: typeof PublicDndCharactersExampleRoute + PublicDndCharactersOmarinKenateRoute: typeof PublicDndCharactersOmarinKenateRoute PublicDndCharactersIndexRoute: typeof PublicDndCharactersIndexRoute } const PublicRouteChildren: PublicRouteChildren = { PublicIndexRoute: PublicIndexRoute, PublicDndCharactersExampleRoute: PublicDndCharactersExampleRoute, + PublicDndCharactersOmarinKenateRoute: PublicDndCharactersOmarinKenateRoute, PublicDndCharactersIndexRoute: PublicDndCharactersIndexRoute, } diff --git a/apps/game-tools/src/router.tsx b/apps/game-tools/src/router.tsx index ff5154f2..dade4ff0 100644 --- a/apps/game-tools/src/router.tsx +++ b/apps/game-tools/src/router.tsx @@ -1,5 +1,5 @@ -import { createRouter as createTanStackRouter } from '@tanstack/react-router' -import { routeTree } from './routeTree.gen' +import { createRouter as createTanStackRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; export function getRouter() { const router = createTanStackRouter({ @@ -8,12 +8,12 @@ export function getRouter() { scrollRestoration: true, defaultPreload: 'intent', defaultPreloadStaleTime: 0, - }) - return router + }); + return router; } declare module '@tanstack/react-router' { interface Register { - router: ReturnType + router: ReturnType; } } diff --git a/apps/game-tools/src/routes/__root.tsx b/apps/game-tools/src/routes/__root.tsx index 6d543b1f..b7c8bd24 100644 --- a/apps/game-tools/src/routes/__root.tsx +++ b/apps/game-tools/src/routes/__root.tsx @@ -1,15 +1,15 @@ -import { THEME_INIT_SCRIPT } from '@ageorgedev/design-system/theming/theme-init-script' +import { THEME_INIT_SCRIPT } from '@ageorgedev/design-system/theming/theme-init-script'; import { createRootRouteWithContext, HeadContent, Scripts, -} from '@tanstack/react-router' -import { GlobalProviders } from '../components/GlobalProviders' -import appCss from '../styles.css?url' +} from '@tanstack/react-router'; +import { GlobalProviders } from '../components/GlobalProviders'; +import appCss from '../styles.css?url'; type MyRouterContext = { - temp?: string -} + temp?: string; +}; export const Route = createRootRouteWithContext()({ head: () => ({ @@ -33,7 +33,7 @@ export const Route = createRootRouteWithContext()({ ], }), shellComponent: RootDocument, -}) +}); function RootDocument({ children }: { children: React.ReactNode }) { return ( @@ -48,5 +48,5 @@ function RootDocument({ children }: { children: React.ReactNode }) { - ) + ); } diff --git a/apps/game-tools/src/routes/_public.tsx b/apps/game-tools/src/routes/_public.tsx index 2a25ebf8..69d3b6c0 100644 --- a/apps/game-tools/src/routes/_public.tsx +++ b/apps/game-tools/src/routes/_public.tsx @@ -1,9 +1,9 @@ -import { ThemeSwitcher } from '@ageorgedev/design-system/theming/ThemeSwitcher' -import { createFileRoute, Link, Outlet } from '@tanstack/react-router' +import { ThemeSwitcher } from '@ageorgedev/design-system/theming/ThemeSwitcher'; +import { createFileRoute, Link, Outlet } from '@tanstack/react-router'; export const Route = createFileRoute('/_public')({ component: RouteComponent, -}) +}); function RouteComponent() { return ( @@ -26,5 +26,5 @@ function RouteComponent() { - ) + ); } diff --git a/apps/game-tools/src/routes/_public/dnd/characters/example.tsx b/apps/game-tools/src/routes/_public/dnd/characters/example.tsx index 824b4f31..21927003 100644 --- a/apps/game-tools/src/routes/_public/dnd/characters/example.tsx +++ b/apps/game-tools/src/routes/_public/dnd/characters/example.tsx @@ -1,10 +1,17 @@ -import { ExampleSheet } from '@ageorgedev/dnd-character-sheet' -import { createFileRoute } from '@tanstack/react-router' +import { ExampleSheet } from '@ageorgedev/dnd-character-sheet'; +import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/_public/dnd/characters/example')({ component: RouteComponent, -}) + staticData: { + character: { + name: 'Seraphina Ashveil', + description: 'Just an example wizard', + level: 7, + }, + }, +}); function RouteComponent() { - return + return ; } diff --git a/apps/game-tools/src/routes/_public/dnd/characters/index.tsx b/apps/game-tools/src/routes/_public/dnd/characters/index.tsx index 69c16990..0baa5364 100644 --- a/apps/game-tools/src/routes/_public/dnd/characters/index.tsx +++ b/apps/game-tools/src/routes/_public/dnd/characters/index.tsx @@ -1,22 +1,53 @@ -import { createFileRoute, Link } from '@tanstack/react-router' +import { + createFileRoute, + type Route as RouteType, + useRouter, +} from '@tanstack/react-router'; +import { useMemo } from 'react'; export const Route = createFileRoute('/_public/dnd/characters/')({ component: RouteComponent, -}) +}); function RouteComponent() { + const router = useRouter(); + const currentRouteMatch = Route.useMatch(); + + const allCharacters = useMemo(() => { + const currentId = currentRouteMatch.routeId; + return ( + Object.values(router.routesById).filter( + (o) => o.id.startsWith(currentId) && o.id !== currentId + ) as RouteType[] + ).filter((r) => r.options.staticData?.character); + }, [router.routesById, currentRouteMatch.routeId]); + return (
-

Characters

- -

Seraphina Ashveil

-

- Wizard / Warlock Ā· Level 7 -

- +

Characters

+ +
+ {allCharacters.map(({ Link, ...r }) => { + const characterInfo = r.options.staticData?.character; + if (!characterInfo) { + return null; + } + + return ( + +

+ {characterInfo.name} +

+

+ Level {characterInfo.level} - {characterInfo.description} +

+ + ); + })} +
- ) + ); } diff --git a/apps/game-tools/src/routes/_public/dnd/characters/omarin-kenate.tsx b/apps/game-tools/src/routes/_public/dnd/characters/omarin-kenate.tsx new file mode 100644 index 00000000..4628d96b --- /dev/null +++ b/apps/game-tools/src/routes/_public/dnd/characters/omarin-kenate.tsx @@ -0,0 +1,264 @@ +import { + Ability, + type Character, + CharacterClass, + Skill, + StandardCharacterSheet, +} from '@ageorgedev/dnd-character-sheet'; +import { createFileRoute } from '@tanstack/react-router'; + +const OmarinData: Character = { + name: 'Omarin Kenate', + species: 'Drow Elf', + background: 'Farmer', + size: 'Medium', + classes: [ + { + name: CharacterClass.Monk, + level: 2, + }, + { + name: CharacterClass.Fighter, + level: 3, + subclass: 'Battle master', + }, + ], + experiencePoints: 0, + abilities: { + [Ability.Strength]: 8, + [Ability.Dexterity]: 17, + [Ability.Constitution]: 14, + [Ability.Intelligence]: 12, + [Ability.Wisdom]: 15, + [Ability.Charisma]: 8, + }, + savingThrowProficiencies: [Ability.Strength, Ability.Dexterity], + skillProficiencies: [ + Skill.Stealth, + Skill.Nature, + Skill.AnimalHandling, + Skill.Insight, + Skill.Perception, + ], + skillExpertise: [], + armorClass: 15, + speed: 40, + hitPoints: { + maximum: 52, + }, + attacks: [ + { + name: 'Shortsword', + kind: 'weapon', + ability: Ability.Dexterity, + damage: [{ dice: '1d6', type: 'Piercing' }], + masteryProperty: 'Vex', + notes: 'Light, Finesse', + }, + { + name: 'Scimitar', + kind: 'weapon', + ability: Ability.Dexterity, + damage: [{ dice: '1d6', type: 'Slashing' }], + masteryProperty: 'Nick', + notes: 'Light, Finesse', + }, + { + name: 'Unarmed Stike', + kind: 'weapon', + ability: Ability.Dexterity, + damage: [{ dice: '1d6', type: 'Bludgeoning' }], + }, + { + name: 'Daggers (x5)', + kind: 'weapon', + ability: Ability.Dexterity, + damage: [{ dice: '1d4', type: 'Piercing' }], + masteryProperty: 'Nick', + notes: 'Light, Finesse, Thrown (range 20/60)', + }, + { + name: 'Light Crossbow', + kind: 'weapon', + ability: Ability.Dexterity, + damage: [{ dice: '1d8', type: 'Piercing' }], + notes: 'Ammo (80/20, Bolt), Loading, 2-handed', + }, + ], + equipment: ['Bedroll, Backpack, Lantern', 'Flasks of oil (x4)'], + armorProficiencies: ['Light armor', 'Medium armor', 'Shields'], + weaponProficiencies: [ + 'Simple', + 'Martial', + 'Monk Weapons (Simple Melee, or Martial weapons with Light property)', + ], + toolProficiencies: ["Tinker's Tools", "Carpenter's Tools"], + languages: [], + features: [ + { + name: 'Martial Arts', + description: + 'On using only monk weapons, wearing no armor, and holding no shield, you gain the following benefits.
  1. Unarmed strikes as Bonus Actions
  2. Can use Dex. modifier for attacks and damage rolls; 1d6 instead of normal damage -- for unarmed strikes and Monk weapons
', + }, + { + name: 'Unarmored Defence + Movement', + description: + 'When not wearing armor and not holding Shield: AC = 10 + Dex. + Wis. modifiers, Speed + 10', + }, + { + name: 'Weapon Mastery', + description: 'You have mastery over 3 different weapons', + }, + { + name: "Monk's Focus", + description: 'You have 2 Focus Points to fuel monk features', + resource: { + name: 'Focus Points', + count: { + kind: 'fixed', + value: 2, + }, + refresh: { + kind: 'any-rest', + }, + }, + }, + { + name: 'Second Wind', + duration: 'Bonus Action', + // TODO: Fighter level + description: 'Regain Hit Points equal to 1d10+{{level.Fighter}}', + cost: '1 Second Wind charge', + resource: { + name: 'Second Wind', + count: { kind: 'fixed', value: 2 }, + refresh: { + kind: 'short-and-long-rest', + numberOfRefreshesOnShortRest: 1, + }, + }, + }, + { + name: 'Tactical Mind', + cost: '1 Second Wind charge', + description: + 'When you fail an Ability check, you can roll 1d10 and add to the roll. Second Wind charge not spent if check still unsuccessful', + }, + { + name: 'Combat Superiority', + description: + 'You learn manuevers that are fueled by Superiority Dice. Your Superiority Dice is a D8', + resource: { + name: 'Superiority Dice (d8)', + count: { kind: 'fixed', value: 4 }, + refresh: { kind: 'any-rest' }, + }, + }, + { + name: 'Action Surge', + description: 'Take 1 extra Action (except magic)', + resource: { + name: 'Action Surge', + count: { kind: 'fixed', value: 1 }, + refresh: { kind: 'any-rest' }, + }, + }, + { + name: 'Uncanny Metabolism', + description: + 'On rolling Initiative, regain all expended Focus Points, and regain 1d6+{{level.Monk}} HP', + resource: { + name: 'Uncanny Metabolism', + count: { kind: 'fixed', value: 1 }, + refresh: { kind: 'long-rest' }, + }, + }, + ], + speciesTraits: [ + { + name: 'Darkvision', + description: '120ft', + }, + { + name: 'Fey Ancestry', + description: + 'You have Advantage on saving throws you make to avoid or end the Charmed condition', + }, + { + name: 'Keen Sense', + description: 'Proficiency in Perception skill', + }, + { + name: 'Trance', + description: + "You don't need sleep and magic can't put you to sleep. You can finish a Long Rest in 4 hourse if you spend that time in a trance like state", + }, + { + name: 'Elven Lineage', + description: + 'You know the Faerie Fire, and Darkness spells. You can cast them once without a spell slot. Refresh on Long Rest. Wisdom is your spell-casting ability for this', + }, + ], + feats: [ + { + name: 'Tough', + description: + 'Whenever you gain character levels, your Hit Point maximum increases by an additional 2 points', + }, + { + name: 'Two-Weapon Fighting', + description: + 'When making the extra attack of the Light property, you can add the modifier to the damage', + }, + ], + + spellcasting: { + ability: Ability.Wisdom, + spells: [ + { + name: 'Faerie Fire', + level: 1, + castingTime: 'Action', + range: '60ft', + duration: '1min', + concentration: true, + alwaysPrepared: true, + freeUses: 1, + }, + { + name: 'Darkness', + level: 2, + castingTime: 'Action', + range: '60ft', + duration: '10min', + concentration: true, + alwaysPrepared: true, + freeUses: 1, + notes: '15ft Sphere', + }, + ], + slots: {}, + numberOfCantrips: 0, + numberOfPreparedSpells: 0, + }, + + backstory: ` +

This is the story of Omarin

+

Early years Had a lot of fun

+ `, +}; + +export const Route = createFileRoute('/_public/dnd/characters/omarin-kenate')({ + component: RouteComponent, + staticData: { + character: { + name: OmarinData.name, + level: OmarinData.classes.reduce((acc, { level }) => acc + level, 0), + description: 'Drow battle master with inner peace', + }, + }, +}); + +function RouteComponent() { + return ; +} diff --git a/apps/game-tools/src/routes/_public/index.tsx b/apps/game-tools/src/routes/_public/index.tsx index f2812d7f..28af8e50 100644 --- a/apps/game-tools/src/routes/_public/index.tsx +++ b/apps/game-tools/src/routes/_public/index.tsx @@ -1,8 +1,8 @@ -import { createFileRoute, Link } from '@tanstack/react-router' +import { createFileRoute, Link } from '@tanstack/react-router'; export const Route = createFileRoute('/_public/')({ component: RouteComponent, -}) +}); function RouteComponent() { return ( @@ -21,5 +21,5 @@ function RouteComponent() {
- ) + ); } diff --git a/apps/game-tools/src/type-enhancements.d.ts b/apps/game-tools/src/type-enhancements.d.ts new file mode 100644 index 00000000..323d656d --- /dev/null +++ b/apps/game-tools/src/type-enhancements.d.ts @@ -0,0 +1,12 @@ +declare module '@tanstack/react-router' { + interface StaticDataRouteOption { + character?: { + name: string; + description: string; + level: number; + }; + } +} + +// important, makes this file a module for ts, not an ambient script which will replace the above module entirely +export {}; diff --git a/apps/game-tools/tsconfig.json b/apps/game-tools/tsconfig.json index 9edbced5..2274edf9 100644 --- a/apps/game-tools/tsconfig.json +++ b/apps/game-tools/tsconfig.json @@ -3,6 +3,7 @@ "include": ["src"], "exclude": ["node_modules", "dist"], "compilerOptions": { + "rootDir": "./src", "outDir": "dist" } } diff --git a/apps/game-tools/vite.config.ts b/apps/game-tools/vite.config.ts index a1feeac3..ad2eac1b 100644 --- a/apps/game-tools/vite.config.ts +++ b/apps/game-tools/vite.config.ts @@ -1,7 +1,7 @@ -import tailwindcss from '@tailwindcss/vite' -import { tanstackStart } from '@tanstack/react-start/plugin/vite' -import react from '@vitejs/plugin-react' -import { defineConfig, searchForWorkspaceRoot } from 'vite' +import tailwindcss from '@tailwindcss/vite'; +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig, searchForWorkspaceRoot } from 'vite'; const config = defineConfig({ cacheDir: '../../node_modules/.vite/game-tools', @@ -20,6 +20,6 @@ const config = defineConfig({ allow: [searchForWorkspaceRoot(process.cwd())], }, }, -}) +}); -export default config +export default config; diff --git a/apps/game-tools/vitest.config.ts b/apps/game-tools/vitest.config.ts index 9b6bd972..c641e500 100644 --- a/apps/game-tools/vitest.config.ts +++ b/apps/game-tools/vitest.config.ts @@ -1,5 +1,5 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], @@ -8,4 +8,4 @@ export default defineConfig({ setupFiles: ['@ageorgedev/testing-config/react-jsdom-test-setup.ts'], include: ['src/**/*.spec.{ts,tsx}'], }, -}) +}); diff --git a/biome.json b/biome.json index 6117eb23..8ca73da0 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "includes": ["**", "!!**/dist", "!package.json"] }, "formatter": { @@ -23,11 +23,17 @@ }, "linter": { "enabled": true, + "domains": { + "types": "recommended" + }, "rules": { "recommended": true, "suspicious": { "noDuplicateProperties": "off", "noDuplicateCustomProperties": "off" + }, + "nursery": { + "useExhaustiveSwitchCases": "on" } } }, @@ -36,12 +42,8 @@ "jsxQuoteStyle": "double", "quoteProperties": "asNeeded", "trailingCommas": "es5", - "semicolons": "asNeeded", - "arrowParentheses": "always", "bracketSameLine": false, - "quoteStyle": "single", - "attributePosition": "auto", - "bracketSpacing": true + "quoteStyle": "single" } }, "html": { diff --git a/packages/brand-components/src/TwLogo/TwLogo.spec.tsx b/packages/brand-components/src/TwLogo/TwLogo.spec.tsx index c89b5b8f..372b33b9 100644 --- a/packages/brand-components/src/TwLogo/TwLogo.spec.tsx +++ b/packages/brand-components/src/TwLogo/TwLogo.spec.tsx @@ -1,26 +1,26 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { TwLogo } from './TwLogo' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { TwLogo } from './TwLogo'; describe('TwLogo', () => { it('renders an SVG element', () => { - const { container } = render() - expect(container.querySelector('svg')).toBeInTheDocument() - }) + const { container } = render(); + expect(container.querySelector('svg')).toBeInTheDocument(); + }); it('contains an accessible title', () => { - const { container } = render() - const title = container.querySelector('title') - expect(title?.textContent).toBe('ThoughtWorks') - }) + const { container } = render(); + const title = container.querySelector('title'); + expect(title?.textContent).toBe('ThoughtWorks'); + }); it('forwards className to the root SVG', () => { - const { container } = render() - expect(container.querySelector('svg')).toHaveClass('my-logo') - }) + const { container } = render(); + expect(container.querySelector('svg')).toHaveClass('my-logo'); + }); it('forwards data-testid so the element is queryable', () => { - render() - expect(screen.getByTestId('tw-logo')).toBeInTheDocument() - }) -}) + render(); + expect(screen.getByTestId('tw-logo')).toBeInTheDocument(); + }); +}); diff --git a/packages/brand-components/src/TwLogo/TwLogo.tsx b/packages/brand-components/src/TwLogo/TwLogo.tsx index 866ecbac..b1f8500a 100644 --- a/packages/brand-components/src/TwLogo/TwLogo.tsx +++ b/packages/brand-components/src/TwLogo/TwLogo.tsx @@ -1,4 +1,4 @@ -import { bar, text } from './styles' +import { bar, text } from './styles'; export function TwLogo(props: React.SVGProps) { return ( @@ -103,5 +103,5 @@ export function TwLogo(props: React.SVGProps) { - ) + ); } diff --git a/packages/brand-components/src/TwLogo/styles.ts b/packages/brand-components/src/TwLogo/styles.ts index 75e71730..8a41025b 100644 --- a/packages/brand-components/src/TwLogo/styles.ts +++ b/packages/brand-components/src/TwLogo/styles.ts @@ -1,2 +1,2 @@ -export const bar = 'fill-[#F2617A]' -export const text = 'fill-[#003D4F] dark:fill-[#EDF1F3]' +export const bar = 'fill-[#F2617A]'; +export const text = 'fill-[#003D4F] dark:fill-[#EDF1F3]'; diff --git a/packages/brand-components/src/index.ts b/packages/brand-components/src/index.ts index f9d0122e..dccac1ac 100644 --- a/packages/brand-components/src/index.ts +++ b/packages/brand-components/src/index.ts @@ -1 +1 @@ -export * from './TwLogo/TwLogo' +export * from './TwLogo/TwLogo'; diff --git a/packages/brand-components/vitest.config.ts b/packages/brand-components/vitest.config.ts index 9b6bd972..c641e500 100644 --- a/packages/brand-components/vitest.config.ts +++ b/packages/brand-components/vitest.config.ts @@ -1,5 +1,5 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], @@ -8,4 +8,4 @@ export default defineConfig({ setupFiles: ['@ageorgedev/testing-config/react-jsdom-test-setup.ts'], include: ['src/**/*.spec.{ts,tsx}'], }, -}) +}); diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 5222e83b..b61e7454 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -19,7 +19,7 @@ "@phosphor-icons/react": "2.1.10", "@radix-ui/react-slot": "1.2.3", "class-variance-authority": "0.7.1", - "ramda": "0.28.0", + "ramda": "0.32.0", "react-syntax-highlighter": "16.1.1" }, "devDependencies": { diff --git a/packages/design-system/src/CodeBlock/CodeBlock.spec.tsx b/packages/design-system/src/CodeBlock/CodeBlock.spec.tsx index 4a388def..a508087f 100644 --- a/packages/design-system/src/CodeBlock/CodeBlock.spec.tsx +++ b/packages/design-system/src/CodeBlock/CodeBlock.spec.tsx @@ -1,11 +1,11 @@ -import { act, render } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' -import type { Theme } from '../theming/models' -import { ThemeContext } from '../theming/ThemeContext' -import { ThemeProvider } from '../theming/ThemeProvider' -import { CodeBlock } from './CodeBlock' +import { act, render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import type { Theme } from '../theming/models'; +import { ThemeContext } from '../theming/ThemeContext'; +import { ThemeProvider } from '../theming/ThemeProvider'; +import { CodeBlock } from './CodeBlock'; -vi.mock('../theming/ThemeProvider') +vi.mock('../theming/ThemeProvider'); async function setupTestSubject(ui: React.ReactElement, theme: Theme = 'dark') { return act(() => @@ -21,57 +21,57 @@ async function setupTestSubject(ui: React.ReactElement, theme: Theme = 'dark') { {ui} ) - ) + ); } describe('CodeBlock', () => { const defaultProps = { text: 'const hello = "world";', lang: 'typescript' as const, - } + }; it('renders code with default props', async () => { const { container } = await setupTestSubject( - ) - const codeElement = container.querySelector('code') - expect(codeElement).toBeInTheDocument() - }) + ); + const codeElement = container.querySelector('code'); + expect(codeElement).toBeInTheDocument(); + }); it('renders with different font sizes', async () => { const { container, rerender } = await setupTestSubject( - ) - const codeElement = container.querySelector('code') - expect(codeElement?.parentElement?.parentElement).toHaveClass('text-md') + ); + const codeElement = container.querySelector('code'); + expect(codeElement?.parentElement?.parentElement).toHaveClass('text-md'); rerender( - ) - const codeElementLarge = container.querySelector('code') + ); + const codeElementLarge = container.querySelector('code'); expect(codeElementLarge?.parentElement?.parentElement).toHaveClass( 'text-xl' - ) - }) + ); + }); it('renders with different languages', async () => { const { container } = await setupTestSubject( - ) - const codeElement = container.querySelector('code') - expect(codeElement).toBeInTheDocument() - expect(codeElement?.textContent).toBe('1const hello = "world";') - }) + ); + const codeElement = container.querySelector('code'); + expect(codeElement).toBeInTheDocument(); + expect(codeElement?.textContent).toBe('1const hello = "world";'); + }); it('renders with dark theme', async () => { const { container } = await setupTestSubject( , 'dark' - ) - const codeElement = container.querySelector('code') - expect(codeElement).toBeInTheDocument() - expect(codeElement?.textContent).toBe('1const hello = "world";') - }) -}) + ); + const codeElement = container.querySelector('code'); + expect(codeElement).toBeInTheDocument(); + expect(codeElement?.textContent).toBe('1const hello = "world";'); + }); +}); diff --git a/packages/design-system/src/CodeBlock/CodeBlock.stories.tsx b/packages/design-system/src/CodeBlock/CodeBlock.stories.tsx index f360466e..93c4dcaf 100644 --- a/packages/design-system/src/CodeBlock/CodeBlock.stories.tsx +++ b/packages/design-system/src/CodeBlock/CodeBlock.stories.tsx @@ -1,5 +1,5 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import { CodeBlock } from './CodeBlock' +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { CodeBlock } from './CodeBlock'; export default { title: 'Molecules/CodeBlock', @@ -17,9 +17,9 @@ export default { control: 'text', }, }, -} satisfies Meta +} satisfies Meta; -type Story = StoryObj +type Story = StoryObj; const typescriptExample = `interface User { id: string; @@ -30,7 +30,7 @@ const typescriptExample = `interface User { function greetUser(user: User): string { return \`Hello \${user.name}!\`; -}` +}`; const htmlExample = ` @@ -42,7 +42,7 @@ const htmlExample = `

Welcome

This is an example HTML page.

-` +`; const cssExample = `.container { display: flex; @@ -57,7 +57,7 @@ const cssExample = `.container { border-radius: 0.25rem; background-color: #007bff; color: white; -}` +}`; const jsxExample = `function Button({ text, onClick }) { return ( @@ -68,7 +68,7 @@ const jsxExample = `function Button({ text, onClick }) { {text} ); -}` +}`; export const TypeScript: Story = { args: { @@ -76,7 +76,7 @@ export const TypeScript: Story = { text: typescriptExample, fontSize: 'normal', }, -} +}; export const HTML: Story = { args: { @@ -84,7 +84,7 @@ export const HTML: Story = { text: htmlExample, fontSize: 'normal', }, -} +}; export const CSS: Story = { args: { @@ -92,7 +92,7 @@ export const CSS: Story = { text: cssExample, fontSize: 'normal', }, -} +}; export const JSX: Story = { args: { @@ -100,7 +100,7 @@ export const JSX: Story = { text: jsxExample, fontSize: 'normal', }, -} +}; export const SmallFont: Story = { args: { @@ -108,7 +108,7 @@ export const SmallFont: Story = { text: typescriptExample, fontSize: 'small', }, -} +}; export const LargeFont: Story = { args: { @@ -116,7 +116,7 @@ export const LargeFont: Story = { text: typescriptExample, fontSize: 'large', }, -} +}; export const WithCustomClassName: Story = { args: { @@ -124,7 +124,7 @@ export const WithCustomClassName: Story = { text: typescriptExample, className: 'max-w-2xl', }, -} +}; export const InteractiveExample: Story = { args: { @@ -132,4 +132,4 @@ export const InteractiveExample: Story = { text: '// Try changing the language, font size, and code!', fontSize: 'normal', }, -} +}; diff --git a/packages/design-system/src/CodeBlock/CodeBlock.tsx b/packages/design-system/src/CodeBlock/CodeBlock.tsx index 257f1200..ef79ac26 100644 --- a/packages/design-system/src/CodeBlock/CodeBlock.tsx +++ b/packages/design-system/src/CodeBlock/CodeBlock.tsx @@ -1,20 +1,20 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { darcula as darkTheme, solarizedlight as lightTheme, -} from 'react-syntax-highlighter/dist/esm/styles/prism' -import { Card } from '../cards/Card' -import { useTheme } from '../theming/ThemeContext' +} from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { Card } from '../cards/Card'; +import { useTheme } from '../theming/ThemeContext'; type CodeBlockProps = { - text: string - lang?: 'typescript' | 'html' | 'css' | 'jsx' | 'tsx' | 'javascript' - fontSize?: 'normal' | 'small' | 'large' -} & React.HTMLProps + text: string; + lang?: 'typescript' | 'html' | 'css' | 'jsx' | 'tsx' | 'javascript'; + fontSize?: 'normal' | 'small' | 'large'; +} & React.HTMLProps; -const fixedLightTheme = fixFont(lightTheme) -const fixedDarkTheme = fixFont(darkTheme) +const fixedLightTheme = fixFont(lightTheme); +const fixedDarkTheme = fixFont(darkTheme); export function CodeBlock({ lang, @@ -23,8 +23,8 @@ export function CodeBlock({ className, ...otherProps }: CodeBlockProps) { - const { theme } = useTheme() - const codeTheme = theme === 'light' ? fixedLightTheme : fixedDarkTheme + const { theme } = useTheme(); + const codeTheme = theme === 'light' ? fixedLightTheme : fixedDarkTheme; return ( - ) + ); } const fontSizeMap: Record['fontSize'], string> = { large: 'text-xl large-desktop:text-2xl', normal: 'text-lg large-desktop:text-xl', small: 'text-md large-desktop:text-lg', -} +}; function fixFont(theme: { [k: string]: React.CSSProperties }) { - const styleKey1 = 'code[class*="language-"]' - const styleKey2 = 'pre[class*="language-"]' + const styleKey1 = 'code[class*="language-"]'; + const styleKey2 = 'pre[class*="language-"]'; return { ...theme, [styleKey1]: { @@ -62,5 +62,5 @@ function fixFont(theme: { [k: string]: React.CSSProperties }) { ...theme[styleKey2], fontFamily: '"Source Code Pro", monospace', }, - } + }; } diff --git a/packages/design-system/src/callouts/CalloutBadge.spec.tsx b/packages/design-system/src/callouts/CalloutBadge.spec.tsx index b9f62686..f764e4e8 100644 --- a/packages/design-system/src/callouts/CalloutBadge.spec.tsx +++ b/packages/design-system/src/callouts/CalloutBadge.spec.tsx @@ -1,81 +1,83 @@ -import { InfoIcon } from '@phosphor-icons/react' -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { CalloutBadge } from './CalloutBadge' +import { InfoIcon } from '@phosphor-icons/react'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { CalloutBadge } from './CalloutBadge'; describe('CalloutBadge', () => { const defaultProps = { text: 'Test Badge', icon: InfoIcon, - } + }; it('renders with default props', () => { - render() - expect(screen.getByText('Test Badge')).toBeInTheDocument() + render(); + expect(screen.getByText('Test Badge')).toBeInTheDocument(); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'before:bg-primary-surface-2' - ) + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'text-primary-onsurface-2' - ) - }) + ); + }); it('applies custom className', () => { - render() + render(); expect( screen.getByText('Test Badge').parentElement?.parentElement?.parentElement - ).toHaveClass('custom-class') - }) + ).toHaveClass('custom-class'); + }); it('renders with different types', () => { - const { rerender } = render() + const { rerender } = render( + + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'before:bg-primary-surface-2' - ) + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'text-primary-onsurface-2' - ) + ); - rerender() + rerender(); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'before:bg-primary-surface' - ) + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'text-primary-onsurface' - ) + ); - rerender() + rerender(); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'before:bg-primary-surface-0' - ) + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'text-primary-onsurface-0' - ) + ); - rerender() + rerender(); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'before:bg-info-surface' - ) + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'text-info-onsurface' - ) + ); - rerender() + rerender(); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'before:bg-destructive-surface' - ) + ); expect(screen.getByText('Test Badge').parentElement).toHaveClass( 'text-destructive-onsurface' - ) - }) + ); + }); it('renders with default padding when no className is provided', () => { - render() + render(); expect( screen.getByText('Test Badge').parentElement?.parentElement?.parentElement - ).toHaveClass('inline-block') + ).toHaveClass('inline-block'); expect( screen.getByText('Test Badge').parentElement?.parentElement?.parentElement - ).toHaveClass('p-5') - }) -}) + ).toHaveClass('p-5'); + }); +}); diff --git a/packages/design-system/src/callouts/CalloutBadge.stories.tsx b/packages/design-system/src/callouts/CalloutBadge.stories.tsx index 56f81653..d16c9e9e 100644 --- a/packages/design-system/src/callouts/CalloutBadge.stories.tsx +++ b/packages/design-system/src/callouts/CalloutBadge.stories.tsx @@ -13,9 +13,9 @@ import { StarIcon, TrophyIcon, UserIcon, -} from '@phosphor-icons/react/ssr' -import type { Meta, StoryObj } from '@storybook/react-vite' -import { CalloutBadge } from './CalloutBadge' +} from '@phosphor-icons/react/ssr'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { CalloutBadge } from './CalloutBadge'; export default { title: 'Atoms/Callout Badge', @@ -64,9 +64,9 @@ export default { }, }, }, -} satisfies Meta +} satisfies Meta; -type Story = StoryObj +type Story = StoryObj; export const Default: Story = { args: { @@ -74,7 +74,7 @@ export const Default: Story = { icon: AddressBookIcon, text: 'Default Badge', }, -} +}; export const Info1: Story = { args: { @@ -82,7 +82,7 @@ export const Info1: Story = { icon: AddressBookIcon, text: 'Info 1 Badge', }, -} +}; export const Info2: Story = { args: { @@ -90,7 +90,7 @@ export const Info2: Story = { icon: AirplaneInFlightIcon, text: 'Info 2 Badge', }, -} +}; export const Info3: Story = { args: { @@ -98,7 +98,7 @@ export const Info3: Story = { icon: SealWarningIcon, text: 'Info 3 Badge', }, -} +}; export const Danger: Story = { args: { @@ -106,7 +106,7 @@ export const Danger: Story = { icon: SkullIcon, text: 'Danger Badge', }, -} +}; export const Neutral: Story = { args: { @@ -114,7 +114,7 @@ export const Neutral: Story = { icon: PenNibIcon, text: 'Neutral Badge', }, -} +}; export const WithDifferentIcons: Story = { render: () => ( @@ -126,7 +126,7 @@ export const WithDifferentIcons: Story = { ), -} +}; export const WithLongText: Story = { args: { @@ -134,7 +134,7 @@ export const WithLongText: Story = { icon: HeartIcon, text: 'This is a callout badge with a longer text that might wrap', }, -} +}; export const WithCustomSize: Story = { args: { @@ -143,7 +143,7 @@ export const WithCustomSize: Story = { text: 'Custom Size', className: 'p-8', }, -} +}; export const InteractiveExample: Story = { args: { @@ -151,4 +151,4 @@ export const InteractiveExample: Story = { icon: TrophyIcon, text: 'Try changing the type, icon, and text!', }, -} +}; diff --git a/packages/design-system/src/callouts/CalloutBadge.tsx b/packages/design-system/src/callouts/CalloutBadge.tsx index 7d11a861..17f026b1 100644 --- a/packages/design-system/src/callouts/CalloutBadge.tsx +++ b/packages/design-system/src/callouts/CalloutBadge.tsx @@ -1,13 +1,13 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import type { Icon } from '@phosphor-icons/react' -import { InterfaceXl } from '../typography/typography-components' +import { cn } from '@ageorgedev/toolbelt/cn'; +import type { Icon } from '@phosphor-icons/react'; +import { InterfaceXl } from '../typography/typography-components'; type CalloutBadgeProps = { - text: string - icon: Icon - className?: string - type?: CalloutType -} + text: string; + icon: Icon; + className?: string; + type?: CalloutType; +}; export function CalloutBadge({ text, @@ -15,7 +15,7 @@ export function CalloutBadge({ type, className, }: CalloutBadgeProps) { - const typeClasses = StyleClasses[type ?? 'info1'] + const typeClasses = StyleClasses[type ?? 'info1']; return (
@@ -30,10 +30,10 @@ export function CalloutBadge({
- ) + ); } -type CalloutType = 'info1' | 'info2' | 'info3' | 'danger' | 'neutral' +type CalloutType = 'info1' | 'info2' | 'info3' | 'danger' | 'neutral'; const StyleClasses: Record = { info1: 'before:bg-primary-surface-2 text-primary-onsurface-2', @@ -41,4 +41,4 @@ const StyleClasses: Record = { info3: 'before:bg-primary-surface-0 text-primary-onsurface-0', neutral: 'before:bg-info-surface text-info-onsurface', // TODO danger: 'before:bg-destructive-surface text-destructive-onsurface', -} +}; diff --git a/packages/design-system/src/cards/Card.spec.tsx b/packages/design-system/src/cards/Card.spec.tsx index 9238ab7d..7a11d017 100644 --- a/packages/design-system/src/cards/Card.spec.tsx +++ b/packages/design-system/src/cards/Card.spec.tsx @@ -1,31 +1,31 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { Card } from './Card' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Card } from './Card'; describe('Card', () => { it('renders with default styles', () => { - render(Card content) - const card = screen.getByText('Card content') - expect(card).toBeInTheDocument() - expect(card).toHaveClass('shadow-normal') - expect(card).toHaveClass('border-line') - expect(card).toHaveClass('border-2') - expect(card).toHaveClass('mb-4') - }) + render(Card content); + const card = screen.getByText('Card content'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('shadow-normal'); + expect(card).toHaveClass('border-line'); + expect(card).toHaveClass('border-2'); + expect(card).toHaveClass('mb-4'); + }); it('applies custom className', () => { - render(Card content) - const card = screen.getByText('Card content') - expect(card).toHaveClass('custom-class') - }) + render(Card content); + const card = screen.getByText('Card content'); + expect(card).toHaveClass('custom-class'); + }); it('forwards HTML props', () => { render( Card content - ) - const card = screen.getByTestId('test-card') - expect(card).toHaveAttribute('aria-label', 'Test Card') - }) -}) + ); + const card = screen.getByTestId('test-card'); + expect(card).toHaveAttribute('aria-label', 'Test Card'); + }); +}); diff --git a/packages/design-system/src/cards/Card.tsx b/packages/design-system/src/cards/Card.tsx index e455faf9..4aab817c 100644 --- a/packages/design-system/src/cards/Card.tsx +++ b/packages/design-system/src/cards/Card.tsx @@ -3,7 +3,7 @@ * Rectangular in shape */ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; export function Card({ className, @@ -14,5 +14,5 @@ export function Card({ {...otherProps} className={cn('border-line mb-4 border-2 shadow-normal', className)} > - ) + ); } diff --git a/packages/design-system/src/cards/TiltCard.spec.tsx b/packages/design-system/src/cards/TiltCard.spec.tsx index b412ea3c..8981bf0b 100644 --- a/packages/design-system/src/cards/TiltCard.spec.tsx +++ b/packages/design-system/src/cards/TiltCard.spec.tsx @@ -1,106 +1,108 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { TiltCard } from './TiltCard' -import styles from './TiltCard.module.css' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { TiltCard } from './TiltCard'; +import styles from './TiltCard.module.css'; describe('TiltCard', () => { it.skip('renders with default styles', () => { - render(Card content) - const card = screen.getByText('Card content') - expect(card).toBeInTheDocument() - expect(card.parentElement?.parentElement).toHaveClass('elv-raised-md') - expect(card.parentElement?.parentElement).toHaveClass(styles['skew-medium']) - }) + render(Card content); + const card = screen.getByText('Card content'); + expect(card).toBeInTheDocument(); + expect(card.parentElement?.parentElement).toHaveClass('elv-raised-md'); + expect(card.parentElement?.parentElement).toHaveClass( + styles['skew-medium'] + ); + }); it('applies custom className', () => { - render(Card content) - const card = screen.getByText('Card content') - expect(card).toHaveClass('custom-class') - }) + render(Card content); + const card = screen.getByText('Card content'); + expect(card).toHaveClass('custom-class'); + }); it('applies outerClassName', () => { - render(Card content) - const card = screen.getByText('Card content') - expect(card.parentElement?.parentElement).toHaveClass('outer-class') - }) + render(Card content); + const card = screen.getByText('Card content'); + expect(card.parentElement?.parentElement).toHaveClass('outer-class'); + }); it.skip('applies different shapes', () => { const { rerender } = render( Card content - ) + ); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles.trapRight) + ).toHaveClass(styles.trapRight); - rerender(Card content) + rerender(Card content); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles.trapLeft) + ).toHaveClass(styles.trapLeft); - rerender(Card content) + rerender(Card content); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles.triUpperRight) + ).toHaveClass(styles.triUpperRight); - rerender(Card content) + rerender(Card content); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles.triUpperLeft) - }) + ).toHaveClass(styles.triUpperLeft); + }); it.skip('applies different skew strengths', () => { const { rerender } = render( Card content - ) + ); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles['skew-small']) + ).toHaveClass(styles['skew-small']); - rerender(Card content) + rerender(Card content); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles['skew-medium']) + ).toHaveClass(styles['skew-medium']); - rerender(Card content) + rerender(Card content); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass(styles['skew-large']) - }) + ).toHaveClass(styles['skew-large']); + }); it('applies interactive styles when interactive is true', () => { - render(Card content) + render(Card content); expect( screen.getByText('Card content').parentElement?.parentElement - ).toHaveClass('hover:elv-raised-lg') - }) + ).toHaveClass('hover:elv-raised-lg'); + }); it('applies different border styles', () => { - const { rerender } = render(Card content) + const { rerender } = render(Card content); expect(screen.getByText('Card content').parentElement).toHaveClass( 'p-thick-line' - ) + ); - rerender(Card content) + rerender(Card content); expect(screen.getByText('Card content').parentElement).toHaveClass( 'pb-thick-line' - ) + ); - rerender(Card content) + rerender(Card content); expect(screen.getByText('Card content').parentElement).not.toHaveClass( 'p-thick-line' - ) + ); expect(screen.getByText('Card content').parentElement).not.toHaveClass( 'pb-thick-line' - ) - }) + ); + }); it('forwards HTML props', () => { render( Card content - ) - const card = screen.getByTestId('test-card') - expect(card).toHaveAttribute('aria-label', 'Test Card') - }) -}) + ); + const card = screen.getByTestId('test-card'); + expect(card).toHaveAttribute('aria-label', 'Test Card'); + }); +}); diff --git a/packages/design-system/src/cards/TiltCard.stories.tsx b/packages/design-system/src/cards/TiltCard.stories.tsx index b12d3150..a71d75e6 100644 --- a/packages/design-system/src/cards/TiltCard.stories.tsx +++ b/packages/design-system/src/cards/TiltCard.stories.tsx @@ -1,5 +1,5 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import { TiltCard } from './TiltCard' +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { TiltCard } from './TiltCard'; export default { title: 'Atoms/Tilt Card', @@ -21,9 +21,9 @@ export default { control: 'boolean', }, }, -} satisfies Meta +} satisfies Meta; -type Story = StoryObj +type Story = StoryObj; export const Default: Story = { args: { @@ -33,7 +33,7 @@ export const Default: Story = { border: 'all', children: 'Default Tilt Card', }, -} +}; export const TrapLeft: Story = { args: { @@ -41,7 +41,7 @@ export const TrapLeft: Story = { interactive: true, children: 'Trap Left Shape', }, -} +}; export const TriUpperRight: Story = { args: { @@ -49,7 +49,7 @@ export const TriUpperRight: Story = { interactive: true, children: 'Triangle Upper Right Shape', }, -} +}; export const TriUpperLeft: Story = { args: { @@ -57,7 +57,7 @@ export const TriUpperLeft: Story = { interactive: true, children: 'Triangle Upper Left Shape', }, -} +}; export const SmallSkew: Story = { args: { @@ -65,7 +65,7 @@ export const SmallSkew: Story = { skewStrength: 'small', children: 'Small Skew Strength', }, -} +}; export const LargeSkew: Story = { args: { @@ -73,7 +73,7 @@ export const LargeSkew: Story = { skewStrength: 'large', children: 'Large Skew Strength', }, -} +}; export const BottomBorderOnly: Story = { args: { @@ -81,7 +81,7 @@ export const BottomBorderOnly: Story = { border: 'bottom', children: 'Bottom Border Only', }, -} +}; export const NoBorder: Story = { args: { @@ -89,7 +89,7 @@ export const NoBorder: Story = { border: 'none', children: 'No Border', }, -} +}; export const NonInteractive: Story = { args: { @@ -97,7 +97,7 @@ export const NonInteractive: Story = { interactive: false, children: 'Non-Interactive Card', }, -} +}; export const WithCustomContent: Story = { args: { @@ -119,4 +119,4 @@ export const WithCustomContent: Story = { ), }, -} +}; diff --git a/packages/design-system/src/cards/TiltCard.tsx b/packages/design-system/src/cards/TiltCard.tsx index 82f48a6e..fd39de56 100644 --- a/packages/design-system/src/cards/TiltCard.tsx +++ b/packages/design-system/src/cards/TiltCard.tsx @@ -1,13 +1,13 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import styles from './TiltCard.module.css' +import { cn } from '@ageorgedev/toolbelt/cn'; +import styles from './TiltCard.module.css'; export type TiltCardProps = { - outerClassName?: string - shape?: 'trapRight' | 'trapLeft' | 'triUpperRight' | 'triUpperLeft' - interactive?: boolean - skewStrength?: 'small' | 'medium' | 'large' - border?: 'all' | 'bottom' | 'none' -} & React.HTMLProps + outerClassName?: string; + shape?: 'trapRight' | 'trapLeft' | 'triUpperRight' | 'triUpperLeft'; + interactive?: boolean; + skewStrength?: 'small' | 'medium' | 'large'; + border?: 'all' | 'bottom' | 'none'; +} & React.HTMLProps; export function TiltCard({ children, @@ -19,10 +19,10 @@ export function TiltCard({ border, ...htmlProps }: TiltCardProps) { - const skewClass = shape ? styles[shape] : '' - const skewStrengthClass = styles[`skew-${skewStrength ?? 'medium'}`] - const interC = interactive ? interactiveClasses : '' - const borderC = borderClassMap[border ?? 'all'] + const skewClass = shape ? styles[shape] : ''; + const skewStrengthClass = styles[`skew-${skewStrength ?? 'medium'}`]; + const interC = interactive ? interactiveClasses : ''; + const borderC = borderClassMap[border ?? 'all']; return (
- ) + ); } const interactiveClasses = - 'transition-all hover:elv-raised-lg hover:-translate-y-2 active:elv-raised-sm' + 'transition-all hover:elv-raised-lg hover:-translate-y-2 active:elv-raised-sm'; const borderClassMap: Record['border'], string> = { all: 'p-thick-line', bottom: 'pb-thick-line', none: '', -} +}; diff --git a/packages/design-system/src/logo/NameLogo.spec.tsx b/packages/design-system/src/logo/NameLogo.spec.tsx index b3b0ef4d..e46684b7 100644 --- a/packages/design-system/src/logo/NameLogo.spec.tsx +++ b/packages/design-system/src/logo/NameLogo.spec.tsx @@ -1,6 +1,6 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { NameLogo } from './NameLogo' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { NameLogo } from './NameLogo'; import { bottomLeftShadow, bottomRightShadow, @@ -9,47 +9,47 @@ import { logoFont, logoWrapper, shadowColor, -} from './styles' +} from './styles'; describe('NameLogo', () => { it('renders with default props', () => { - render() - const wrapper = screen.getByText('Anish').parentElement - expect(wrapper).toHaveClass(logoWrapper) + render(); + const wrapper = screen.getByText('Anish').parentElement; + expect(wrapper).toHaveClass(logoWrapper); expect(screen.getByText('Anish')).toHaveClass( logoFont, bottomRightShadow, shadowColor, firstNameColor - ) + ); expect(screen.getByText('George')).toHaveClass( logoFont, bottomRightShadow, shadowColor, lastNameColor - ) - }) + ); + }); it('applies custom className', () => { - render() - const wrapper = screen.getByText('Anish').parentElement - expect(wrapper).toHaveClass('custom-class') - }) + render(); + const wrapper = screen.getByText('Anish').parentElement; + expect(wrapper).toHaveClass('custom-class'); + }); it('applies left shadow direction', () => { - render() - expect(screen.getByText('Anish')).toHaveClass(bottomLeftShadow) - expect(screen.getByText('George')).toHaveClass(bottomLeftShadow) - }) + render(); + expect(screen.getByText('Anish')).toHaveClass(bottomLeftShadow); + expect(screen.getByText('George')).toHaveClass(bottomLeftShadow); + }); it('applies right shadow direction', () => { - render() - expect(screen.getByText('Anish')).toHaveClass(bottomRightShadow) - expect(screen.getByText('George')).toHaveClass(bottomRightShadow) - }) + render(); + expect(screen.getByText('Anish')).toHaveClass(bottomRightShadow); + expect(screen.getByText('George')).toHaveClass(bottomRightShadow); + }); it('renders both names with correct spacing', () => { - render() - expect(screen.getByText('George')).toHaveClass('pl-[.5em]') - }) -}) + render(); + expect(screen.getByText('George')).toHaveClass('pl-[.5em]'); + }); +}); diff --git a/packages/design-system/src/logo/NameLogo.stories.tsx b/packages/design-system/src/logo/NameLogo.stories.tsx index da768dc1..f6439ce0 100644 --- a/packages/design-system/src/logo/NameLogo.stories.tsx +++ b/packages/design-system/src/logo/NameLogo.stories.tsx @@ -1,5 +1,5 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import { NameLogo } from './NameLogo' +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { NameLogo } from './NameLogo'; export default { component: NameLogo, @@ -13,44 +13,44 @@ export default { control: 'text', }, }, -} satisfies Meta +} satisfies Meta; -type Story = StoryObj +type Story = StoryObj; export const Default: Story = { args: { className: 'text-[100px]', shadowDirection: 'right', }, -} +}; export const Small: Story = { args: { className: 'text-[50px]', shadowDirection: 'right', }, -} +}; export const Large: Story = { args: { className: 'text-[150px]', shadowDirection: 'right', }, -} +}; export const LeftShadow: Story = { args: { className: 'text-[100px]', shadowDirection: 'left', }, -} +}; export const CustomSize: Story = { args: { className: 'text-[75px]', shadowDirection: 'right', }, -} +}; export const WithCustomBackground: Story = { render: () => ( @@ -58,4 +58,4 @@ export const WithCustomBackground: Story = { ), -} +}; diff --git a/packages/design-system/src/logo/NameLogo.tsx b/packages/design-system/src/logo/NameLogo.tsx index 5030bb0c..8f174a41 100644 --- a/packages/design-system/src/logo/NameLogo.tsx +++ b/packages/design-system/src/logo/NameLogo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; import { bottomLeftShadow, bottomRightShadow, @@ -7,16 +7,16 @@ import { logoFont, logoWrapper, shadowColor, -} from './styles' +} from './styles'; export type NameLogoProps = { - className?: string - shadowDirection?: 'left' | 'right' -} + className?: string; + shadowDirection?: 'left' | 'right'; +}; export function NameLogo({ className, shadowDirection }: NameLogoProps) { const shadowClass = - shadowDirection === 'left' ? bottomLeftShadow : bottomRightShadow + shadowDirection === 'left' ? bottomLeftShadow : bottomRightShadow; return (

@@ -34,5 +34,5 @@ export function NameLogo({ className, shadowDirection }: NameLogoProps) { George

- ) + ); } diff --git a/packages/design-system/src/logo/ShortNameLogo.spec.tsx b/packages/design-system/src/logo/ShortNameLogo.spec.tsx index af57c292..10f594de 100644 --- a/packages/design-system/src/logo/ShortNameLogo.spec.tsx +++ b/packages/design-system/src/logo/ShortNameLogo.spec.tsx @@ -1,25 +1,25 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { Heading3 } from '../typography/typography-components' -import { ShortNameLogo } from './ShortNameLogo' -import { firstNameColor, lastNameColor } from './styles' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Heading3 } from '../typography/typography-components'; +import { ShortNameLogo } from './ShortNameLogo'; +import { firstNameColor, lastNameColor } from './styles'; describe('ShortNameLogo', () => { it('renders with correct structure and styles', () => { - render() - const wrapper = screen.getByText('A').parentElement - expect(wrapper).toHaveClass(Heading3.classes) - expect(wrapper).toHaveClass('font-bold') - }) + render(); + const wrapper = screen.getByText('A').parentElement; + expect(wrapper).toHaveClass(Heading3.classes); + expect(wrapper).toHaveClass('font-bold'); + }); it('renders first initial with correct color', () => { - render() - expect(screen.getByText('A')).toHaveClass(firstNameColor) - }) + render(); + expect(screen.getByText('A')).toHaveClass(firstNameColor); + }); it('renders second initial with correct color and size', () => { - render() - expect(screen.getByText('G')).toHaveClass(lastNameColor) - expect(screen.getByText('G')).toHaveClass('text-[0.85em]') - }) -}) + render(); + expect(screen.getByText('G')).toHaveClass(lastNameColor); + expect(screen.getByText('G')).toHaveClass('text-[0.85em]'); + }); +}); diff --git a/packages/design-system/src/logo/ShortNameLogo.tsx b/packages/design-system/src/logo/ShortNameLogo.tsx index 1e95c898..42a38858 100644 --- a/packages/design-system/src/logo/ShortNameLogo.tsx +++ b/packages/design-system/src/logo/ShortNameLogo.tsx @@ -1,6 +1,6 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { Heading3 } from '../typography/typography-components' -import { firstNameColor, lastNameColor } from './styles' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { Heading3 } from '../typography/typography-components'; +import { firstNameColor, lastNameColor } from './styles'; export function ShortNameLogo() { return ( @@ -8,5 +8,5 @@ export function ShortNameLogo() { A G - ) + ); } diff --git a/packages/design-system/src/logo/styles.ts b/packages/design-system/src/logo/styles.ts index f22ae6d4..5aa8a67c 100644 --- a/packages/design-system/src/logo/styles.ts +++ b/packages/design-system/src/logo/styles.ts @@ -1,14 +1,14 @@ // export const firstNameColor = 'text-rc-timber-400 dark:text-rc-parchment-500'; -export const firstNameColor = 'text-secondary-foreground' -export const lastNameColor = 'text-primary-foreground' +export const firstNameColor = 'text-secondary-foreground'; +export const lastNameColor = 'text-primary-foreground'; -export const logoFont = `tracking-wide leading-[.75em]` +export const logoFont = `tracking-wide leading-[.75em]`; -export const bottomRightShadow = `drop-shadow-[.03em_.06em]` -export const bottomLeftShadow = `drop-shadow-[-.03em_.06em]` -export const shadowColor = `drop-shadow-rc-shadow-2 dark:drop-shadow-rc-shadow-4` +export const bottomRightShadow = `drop-shadow-[.03em_.06em]`; +export const bottomLeftShadow = `drop-shadow-[-.03em_.06em]`; +export const shadowColor = `drop-shadow-rc-shadow-2 dark:drop-shadow-rc-shadow-4`; export const logoWrapper = ` inline-block font-heading font-bold text-left pt-[.1em] pb-[.25em] px-[.1em] -` +`; diff --git a/packages/design-system/src/theming/ThemeContext.tsx b/packages/design-system/src/theming/ThemeContext.tsx index 0ee50f8d..912d45cc 100644 --- a/packages/design-system/src/theming/ThemeContext.tsx +++ b/packages/design-system/src/theming/ThemeContext.tsx @@ -1,18 +1,18 @@ -'use client' -import { createContext, useContext } from 'react' -import type { Theme } from './models' +'use client'; +import { createContext, useContext } from 'react'; +import type { Theme } from './models'; export type ThemeContext = { - theme: Theme - setTheme(newTheme: Theme): void -} + theme: Theme; + setTheme(newTheme: Theme): void; +}; export const ThemeContext = createContext({ theme: 'dark', setTheme: () => {}, -}) +}); /** Hooks */ export function useTheme() { - return useContext(ThemeContext) + return useContext(ThemeContext); } diff --git a/packages/design-system/src/theming/ThemeProvider.tsx b/packages/design-system/src/theming/ThemeProvider.tsx index aa543998..6a559e9f 100644 --- a/packages/design-system/src/theming/ThemeProvider.tsx +++ b/packages/design-system/src/theming/ThemeProvider.tsx @@ -1,41 +1,46 @@ -import { type PropsWithChildren, useCallback, useEffect, useState } from 'react' -import type { Theme } from './models' -import { ThemeContext } from './ThemeContext' +import { + type PropsWithChildren, + useCallback, + useEffect, + useState, +} from 'react'; +import type { Theme } from './models'; +import { ThemeContext } from './ThemeContext'; /** TODO: * 1. Make the switcher button do auto as well * 2. Improve the logic */ export function ThemeProvider({ children }: PropsWithChildren) { - const [mode, setMode] = useState('auto') - const [resolvedTheme, setResolvedTheme] = useState('dark') + const [mode, setMode] = useState('auto'); + const [resolvedTheme, setResolvedTheme] = useState('dark'); useEffect(() => { - const initialMode = getInitialMode() - setMode(initialMode) - setResolvedTheme(applyThemeMode(initialMode)) - }, []) + const initialMode = getInitialMode(); + setMode(initialMode); + setResolvedTheme(applyThemeMode(initialMode)); + }, []); /** Sync auto mode */ useEffect(() => { if (mode !== 'auto') { - return + return; } - const media = window.matchMedia('(prefers-color-scheme: dark)') + const media = window.matchMedia('(prefers-color-scheme: dark)'); const onChange = () => { - setResolvedTheme(applyThemeMode('auto')) - } + setResolvedTheme(applyThemeMode('auto')); + }; - media.addEventListener('change', onChange) + media.addEventListener('change', onChange); return () => { - media.removeEventListener('change', onChange) - } - }, [mode]) + media.removeEventListener('change', onChange); + }; + }, [mode]); const manuallySetTheme = useCallback((newTheme) => { - setMode(newTheme) - setResolvedTheme(applyThemeMode(newTheme)) - window.localStorage.setItem(STORAGE_THEME_KEY, newTheme) - }, []) + setMode(newTheme); + setResolvedTheme(applyThemeMode(newTheme)); + window.localStorage.setItem(STORAGE_THEME_KEY, newTheme); + }, []); return ( {children} - ) + ); } -type ThemeMode = Theme | 'auto' -const STORAGE_THEME_KEY = 'theme' +type ThemeMode = Theme | 'auto'; +const STORAGE_THEME_KEY = 'theme'; function getInitialMode(): ThemeMode { if (typeof window === 'undefined') { - return 'auto' + return 'auto'; } - const stored = window.localStorage.getItem(STORAGE_THEME_KEY) + const stored = window.localStorage.getItem(STORAGE_THEME_KEY); if (stored === 'light' || stored === 'dark' || stored === 'auto') { - return stored + return stored; } - return 'auto' + return 'auto'; } function applyThemeMode(mode: ThemeMode): Theme { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode; - document.documentElement.classList.remove('light', 'dark') - document.documentElement.classList.add(resolved) + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add(resolved); if (mode === 'auto') { - document.documentElement.removeAttribute('data-theme') + document.documentElement.removeAttribute('data-theme'); } else { - document.documentElement.setAttribute('data-theme', mode) + document.documentElement.setAttribute('data-theme', mode); } - document.documentElement.style.colorScheme = resolved - return resolved + document.documentElement.style.colorScheme = resolved; + return resolved; } diff --git a/packages/design-system/src/theming/ThemeSwitcher.spec.tsx b/packages/design-system/src/theming/ThemeSwitcher.spec.tsx index 123e9a2b..c7d4afb9 100644 --- a/packages/design-system/src/theming/ThemeSwitcher.spec.tsx +++ b/packages/design-system/src/theming/ThemeSwitcher.spec.tsx @@ -1,43 +1,43 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { describe, expect, it, vi } from 'vitest' -import { ThemeContext } from './ThemeContext' -import { ThemeSwitcher } from './ThemeSwitcher' +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; +import { ThemeContext } from './ThemeContext'; +import { ThemeSwitcher } from './ThemeSwitcher'; function renderWithTheme(theme: 'light' | 'dark', setTheme = vi.fn()) { return render( - ) + ); } describe('ThemeSwitcher', () => { it('shows moon icon in light mode', () => { - renderWithTheme('light') + renderWithTheme('light'); expect( screen.getByRole('button', { name: 'Switch to dark theme' }) - ).toBeInTheDocument() - }) + ).toBeInTheDocument(); + }); it.skip('shows sun icon in dark mode', () => { - renderWithTheme('dark') + renderWithTheme('dark'); expect( screen.getByRole('button', { name: 'Switch to light theme' }) - ).toBeInTheDocument() - }) + ).toBeInTheDocument(); + }); it('calls setTheme with "light" when in dark mode', async () => { - const setTheme = vi.fn() - renderWithTheme('dark', setTheme) - await userEvent.click(screen.getByRole('button')) - expect(setTheme).toHaveBeenCalledWith('light') - }) + const setTheme = vi.fn(); + renderWithTheme('dark', setTheme); + await userEvent.click(screen.getByRole('button')); + expect(setTheme).toHaveBeenCalledWith('light'); + }); it('calls setTheme with "dark" when in light mode', async () => { - const setTheme = vi.fn() - renderWithTheme('light', setTheme) - await userEvent.click(screen.getByRole('button')) - expect(setTheme).toHaveBeenCalledWith('dark') - }) -}) + const setTheme = vi.fn(); + renderWithTheme('light', setTheme); + await userEvent.click(screen.getByRole('button')); + expect(setTheme).toHaveBeenCalledWith('dark'); + }); +}); diff --git a/packages/design-system/src/theming/ThemeSwitcher.tsx b/packages/design-system/src/theming/ThemeSwitcher.tsx index aa01b6f4..633f2a5a 100644 --- a/packages/design-system/src/theming/ThemeSwitcher.tsx +++ b/packages/design-system/src/theming/ThemeSwitcher.tsx @@ -1,16 +1,16 @@ -'use client' +'use client'; -import { MoonIcon, SunIcon } from '@phosphor-icons/react/ssr' -import { useCallback } from 'react' -import { useTheme } from './ThemeContext' +import { MoonIcon, SunIcon } from '@phosphor-icons/react/ssr'; +import { useCallback } from 'react'; +import { useTheme } from './ThemeContext'; export function ThemeSwitcher() { - const { theme, setTheme } = useTheme() - const nextTheme = theme === 'dark' ? 'light' : 'dark' + const { theme, setTheme } = useTheme(); + const nextTheme = theme === 'dark' ? 'light' : 'dark'; const switchTheme = useCallback(() => { - setTheme(nextTheme) - }, [nextTheme, setTheme]) + setTheme(nextTheme); + }, [nextTheme, setTheme]); return ( - ) + ); } diff --git a/packages/design-system/src/theming/__mocks__/ThemeProvider.tsx b/packages/design-system/src/theming/__mocks__/ThemeProvider.tsx index 72abccf9..8cc0c929 100644 --- a/packages/design-system/src/theming/__mocks__/ThemeProvider.tsx +++ b/packages/design-system/src/theming/__mocks__/ThemeProvider.tsx @@ -1,30 +1,30 @@ -'use client' -import { useEffect, useState } from 'react' -import type { Theme } from '../models' -import { ThemeContext } from '../ThemeContext' +'use client'; +import { useEffect, useState } from 'react'; +import type { Theme } from '../models'; +import { ThemeContext } from '../ThemeContext'; export function ThemeProvider({ children, startingTheme = 'dark', }: React.PropsWithChildren<{ startingTheme?: Theme }>) { - const [theme, setThemeState] = useState(startingTheme) + const [theme, setThemeState] = useState(startingTheme); // Attach theme to class useEffect(() => { - const classList = document.documentElement.classList - classList.remove('light', 'dark') + const classList = document.documentElement.classList; + classList.remove('light', 'dark'); if (theme === 'dark') { - classList.add(theme) + classList.add(theme); } - }, [theme]) + }, [theme]); const setTheme = (newTheme: Theme) => { - setThemeState(newTheme) - } + setThemeState(newTheme); + }; return ( {children} - ) + ); } diff --git a/packages/design-system/src/theming/models.ts b/packages/design-system/src/theming/models.ts index 5f20d6ba..937ef152 100644 --- a/packages/design-system/src/theming/models.ts +++ b/packages/design-system/src/theming/models.ts @@ -1 +1 @@ -export type Theme = 'light' | 'dark' +export type Theme = 'light' | 'dark'; diff --git a/packages/design-system/src/theming/theme-init-script.ts b/packages/design-system/src/theming/theme-init-script.ts index b7c8a0b4..7459351f 100644 --- a/packages/design-system/src/theming/theme-init-script.ts +++ b/packages/design-system/src/theming/theme-init-script.ts @@ -1 +1 @@ -export const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();` +export const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();`; diff --git a/packages/design-system/src/theming/theme-state.spec.ts b/packages/design-system/src/theming/theme-state.spec.ts index e4cea103..a95de143 100644 --- a/packages/design-system/src/theming/theme-state.spec.ts +++ b/packages/design-system/src/theming/theme-state.spec.ts @@ -1,51 +1,51 @@ -import { describe, expect, it } from 'vitest' -import { initialThemeState, themeReducer } from './theme-state' +import { describe, expect, it } from 'vitest'; +import { initialThemeState, themeReducer } from './theme-state'; describe('themeReducer', () => { it('setByUser sets theme and marks under user control', () => { const state = themeReducer(initialThemeState, { type: 'setByUser', theme: 'dark', - }) - expect(state.theme).toBe('dark') - expect(state.underUserControl).toBe(true) - }) + }); + expect(state.theme).toBe('dark'); + expect(state.underUserControl).toBe(true); + }); it('setBySystem updates theme when not under user control', () => { const state = themeReducer(initialThemeState, { type: 'setBySystem', theme: 'dark', - }) - expect(state.theme).toBe('dark') - expect(state.underUserControl).toBe(false) - }) + }); + expect(state.theme).toBe('dark'); + expect(state.underUserControl).toBe(false); + }); it('setBySystem is ignored when under user control', () => { const userState = themeReducer(initialThemeState, { type: 'setByUser', theme: 'dark', - }) + }); const state = themeReducer(userState, { type: 'setBySystem', theme: 'light', - }) - expect(state.theme).toBe('dark') - expect(state.underUserControl).toBe(true) - }) + }); + expect(state.theme).toBe('dark'); + expect(state.underUserControl).toBe(true); + }); it('setByDom updates theme when theme is non-null', () => { const state = themeReducer(initialThemeState, { type: 'setByDom', theme: 'dark', - }) - expect(state.theme).toBe('dark') - }) + }); + expect(state.theme).toBe('dark'); + }); it('setByDom is ignored when theme is null', () => { const state = themeReducer(initialThemeState, { type: 'setByDom', theme: null, - }) - expect(state).toBe(initialThemeState) - }) -}) + }); + expect(state).toBe(initialThemeState); + }); +}); diff --git a/packages/design-system/src/theming/theme-state.ts b/packages/design-system/src/theming/theme-state.ts index 304fa936..be7cd8f3 100644 --- a/packages/design-system/src/theming/theme-state.ts +++ b/packages/design-system/src/theming/theme-state.ts @@ -1,24 +1,24 @@ -import type { Theme } from './models' +import type { Theme } from './models'; export type ThemeState = { - theme: Theme - underUserControl: boolean -} + theme: Theme; + underUserControl: boolean; +}; export type ThemeAction = | { - type: 'setByUser' | 'setBySystem' - theme: Theme + type: 'setByUser' | 'setBySystem'; + theme: Theme; } | { - type: 'setByDom' - theme: Theme | null - } + type: 'setByDom'; + theme: Theme | null; + }; export const initialThemeState: ThemeState = { theme: 'light', underUserControl: false, -} +}; export function themeReducer(state: ThemeState, action: ThemeAction) { switch (action.type) { @@ -26,24 +26,24 @@ export function themeReducer(state: ThemeState, action: ThemeAction) { return { theme: action.theme, underUserControl: true, - } + }; case 'setBySystem': if (state.underUserControl) { - return state + return state; } else { return { ...state, theme: action.theme, - } + }; } case 'setByDom': if (action.theme !== null) { return { ...state, theme: action.theme, - } + }; } else { - return state + return state; } } } diff --git a/packages/design-system/src/typography/Text.spec.tsx b/packages/design-system/src/typography/Text.spec.tsx index 4c891e48..abf5eb67 100644 --- a/packages/design-system/src/typography/Text.spec.tsx +++ b/packages/design-system/src/typography/Text.spec.tsx @@ -1,77 +1,77 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' -import { TYPOGRAPHY_CLASSES } from './core-type-classes' -import { Text, TextBuilder } from './Text' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { TYPOGRAPHY_CLASSES } from './core-type-classes'; +import { Text, TextBuilder } from './Text'; describe('Text', () => { it('renders with default tag based on variant', () => { - render(Heading 1) - const el = screen.getByText('Heading 1') - expect(el).toBeInTheDocument() - expect(el.tagName).toBe('H1') - expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1) - }) + render(Heading 1); + const el = screen.getByText('Heading 1'); + expect(el).toBeInTheDocument(); + expect(el.tagName).toBe('H1'); + expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1); + }); it('renders with custom tag', () => { render( Heading 1 as span - ) - const el = screen.getByText('Heading 1 as span') - expect(el).toBeInTheDocument() - expect(el.tagName).toBe('SPAN') - expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1) - }) + ); + const el = screen.getByText('Heading 1 as span'); + expect(el).toBeInTheDocument(); + expect(el.tagName).toBe('SPAN'); + expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1); + }); it('renders body variant with paragraph tag by default', () => { - render(Body text) - const el = screen.getByText('Body text') - expect(el).toBeInTheDocument() - expect(el.tagName).toBe('P') - expect(el).toHaveClass(TYPOGRAPHY_CLASSES.body) - }) + render(Body text); + const el = screen.getByText('Body text'); + expect(el).toBeInTheDocument(); + expect(el.tagName).toBe('P'); + expect(el).toHaveClass(TYPOGRAPHY_CLASSES.body); + }); it('renders interface variant with span tag by default', () => { - render(Interface text) - const el = screen.getByText('Interface text') - expect(el).toBeInTheDocument() - expect(el.tagName).toBe('SPAN') - expect(el).toHaveClass(TYPOGRAPHY_CLASSES.interface) - }) + render(Interface text); + const el = screen.getByText('Interface text'); + expect(el).toBeInTheDocument(); + expect(el.tagName).toBe('SPAN'); + expect(el).toHaveClass(TYPOGRAPHY_CLASSES.interface); + }); it('applies custom className', () => { render( Heading 1 - ) - const el = screen.getByText('Heading 1') - expect(el).toHaveClass('custom-class') - }) -}) + ); + const el = screen.getByText('Heading 1'); + expect(el).toHaveClass('custom-class'); + }); +}); describe('TextBuilder', () => { it('creates a component with the specified variant', () => { - const H1Text = TextBuilder('h1') - render(Heading 1) - const el = screen.getByText('Heading 1') - expect(el).toBeInTheDocument() - expect(el.tagName).toBe('H1') - expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1) - }) + const H1Text = TextBuilder('h1'); + render(Heading 1); + const el = screen.getByText('Heading 1'); + expect(el).toBeInTheDocument(); + expect(el.tagName).toBe('H1'); + expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1); + }); it('allows overriding the tag', () => { - const H1Text = TextBuilder('h1') - render(Heading 1 as span) - const el = screen.getByText('Heading 1 as span') - expect(el).toBeInTheDocument() - expect(el.tagName).toBe('SPAN') - expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1) - }) + const H1Text = TextBuilder('h1'); + render(Heading 1 as span); + const el = screen.getByText('Heading 1 as span'); + expect(el).toBeInTheDocument(); + expect(el.tagName).toBe('SPAN'); + expect(el).toHaveClass(TYPOGRAPHY_CLASSES.h1); + }); it('exposes the typography classes', () => { - const H1Text = TextBuilder('h1') - expect(H1Text.classes).toBe(TYPOGRAPHY_CLASSES.h1) - }) -}) + const H1Text = TextBuilder('h1'); + expect(H1Text.classes).toBe(TYPOGRAPHY_CLASSES.h1); + }); +}); diff --git a/packages/design-system/src/typography/Text.tsx b/packages/design-system/src/typography/Text.tsx index 79ab120c..a05e2b7a 100644 --- a/packages/design-system/src/typography/Text.tsx +++ b/packages/design-system/src/typography/Text.tsx @@ -1,6 +1,9 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { test } from 'ramda' -import { TYPOGRAPHY_CLASSES, type TypographyVariant } from './core-type-classes' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { test } from 'ramda'; +import { + TYPOGRAPHY_CLASSES, + type TypographyVariant, +} from './core-type-classes'; export type TextTag = | 'h1' @@ -14,12 +17,12 @@ export type TextTag = | 'em' | 'b' | 'small' - | 'li' + | 'li'; type TextProps = { - variant: TypographyVariant - as?: TextTag -} + variant: TypographyVariant; + as?: TextTag; +}; export function Text({ variant, @@ -28,15 +31,15 @@ export function Text({ children, ...otherProps }: React.PropsWithChildren & React.HTMLAttributes) { - const Tag = tag ?? getDefaultTag(variant) + const Tag = tag ?? getDefaultTag(variant); - const classes = cn(TYPOGRAPHY_CLASSES[variant], className) + const classes = cn(TYPOGRAPHY_CLASSES[variant], className); return ( {children} - ) + ); } export function TextBuilder(variant: TypographyVariant) { @@ -48,33 +51,33 @@ export function TextBuilder(variant: TypographyVariant) { {children} - ) + ); } - Component.classes = TYPOGRAPHY_CLASSES[variant] - return Component + Component.classes = TYPOGRAPHY_CLASSES[variant]; + return Component; } function getDefaultTag(variant: TypographyVariant): TextTag { - const initial = defaultTag[variant] + const initial = defaultTag[variant]; if (initial) { - return initial + return initial; } if (isBodyVariant(variant) || isPBodyVariant(variant)) { - return 'p' + return 'p'; } if (isInterfaceVariant(variant)) { - return 'span' + return 'span'; } - return 'span' + return 'span'; } -const isBodyVariant = test(/^body.*$/) -const isPBodyVariant = test(/^p-body.*$/) -const isInterfaceVariant = test(/^interface.*$/) +const isBodyVariant = test(/^body.*$/); +const isPBodyVariant = test(/^p-body.*$/); +const isInterfaceVariant = test(/^interface.*$/); const defaultTag: Partial> = { h1: 'h1', @@ -83,4 +86,4 @@ const defaultTag: Partial> = { h4: 'h4', h5: 'h5', h6: 'h6', -} +}; diff --git a/packages/design-system/src/typography/core-type-classes.ts b/packages/design-system/src/typography/core-type-classes.ts index 69d1684c..f771f08b 100644 --- a/packages/design-system/src/typography/core-type-classes.ts +++ b/packages/design-system/src/typography/core-type-classes.ts @@ -1,10 +1,10 @@ -import { mapObjIndexed } from 'ramda' +import { mapObjIndexed } from 'ramda'; -const headingFont = 'font-heading' -const bodyFont = 'font-body' -const interfaceFont = 'font-interface' +const headingFont = 'font-heading'; +const bodyFont = 'font-body'; +const interfaceFont = 'font-interface'; -const withFont = (font: string) => (v: string) => `${font} ${v}` +const withFont = (font: string) => (v: string) => `${font} ${v}`; const headingClasses = mapObjIndexed(withFont(headingFont), { h1: 'text-4xl tablet:text-5xl desktop:text-6xl', @@ -13,7 +13,7 @@ const headingClasses = mapObjIndexed(withFont(headingFont), { h4: 'text-xl tablet:text-2xl desktop:text-3xl', h5: 'text-lg tablet:text-xl desktop:text-2xl', h6: 'text-md tablet:text-lg desktop:text-xl', -}) +}); const bodyClasses = mapObjIndexed(withFont(bodyFont), { 'body-xl': 'text-xl', @@ -22,7 +22,7 @@ const bodyClasses = mapObjIndexed(withFont(bodyFont), { body: 'text-base', 'body-sm': 'text-sm', 'body-xs': 'text-xs', -}) +}); const presentationBodyClasses = mapObjIndexed(withFont(bodyFont), { 'p-body-lg': 'text-4xl', @@ -30,7 +30,7 @@ const presentationBodyClasses = mapObjIndexed(withFont(bodyFont), { 'p-body': 'text-2xl', 'p-body-sm': 'text-xl', 'p-body-xs': 'text-lg', -}) +}); // TODO: cn will remove leading-none if added before the font-size class. Refactor this file to work correctly with cn const interfaceClasses = mapObjIndexed(withFont(interfaceFont), { @@ -40,13 +40,13 @@ const interfaceClasses = mapObjIndexed(withFont(interfaceFont), { 'interface-md': 'text-md leading-none', interface: 'leading-none', 'interface-sm': 'text-sm leading-none', -}) +}); export const TYPOGRAPHY_CLASSES = { ...headingClasses, ...bodyClasses, ...interfaceClasses, ...presentationBodyClasses, -} +}; -export type TypographyVariant = keyof typeof TYPOGRAPHY_CLASSES +export type TypographyVariant = keyof typeof TYPOGRAPHY_CLASSES; diff --git a/packages/design-system/src/typography/typography-components.spec.tsx b/packages/design-system/src/typography/typography-components.spec.tsx index 996e6b05..11843a70 100644 --- a/packages/design-system/src/typography/typography-components.spec.tsx +++ b/packages/design-system/src/typography/typography-components.spec.tsx @@ -1,5 +1,5 @@ -import { render } from '@testing-library/react' -import { describe, expect, it } from 'vitest' +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; import { Body, BodyLg, @@ -19,7 +19,7 @@ import { InterfaceMd, InterfaceSm, InterfaceXl, -} from './typography-components' +} from './typography-components'; describe('Typography', () => { it('should add add correct styling for heading types', async () => { @@ -32,10 +32,10 @@ describe('Typography', () => { Heading 5 Heading 6 - ) + ); - expect(baseElement).toMatchSnapshot() - }) + expect(baseElement).toMatchSnapshot(); + }); it('should add add correct styling for body types', async () => { const { baseElement } = await render( @@ -47,10 +47,10 @@ describe('Typography', () => { Body Sm Body Xs - ) + ); - expect(baseElement).toMatchSnapshot() - }) + expect(baseElement).toMatchSnapshot(); + }); it('should add add correct styling for interface types', async () => { const { baseElement } = await render( @@ -62,8 +62,8 @@ describe('Typography', () => { Interface Interface Sm - ) + ); - expect(baseElement).toMatchSnapshot() - }) -}) + expect(baseElement).toMatchSnapshot(); + }); +}); diff --git a/packages/design-system/src/typography/typography-components.tsx b/packages/design-system/src/typography/typography-components.tsx index e6e28426..b469a1c2 100644 --- a/packages/design-system/src/typography/typography-components.tsx +++ b/packages/design-system/src/typography/typography-components.tsx @@ -1,28 +1,28 @@ -import { TextBuilder } from './Text' +import { TextBuilder } from './Text'; -export const Heading1 = TextBuilder('h1') -export const Heading2 = TextBuilder('h2') -export const Heading3 = TextBuilder('h3') -export const Heading4 = TextBuilder('h4') -export const Heading5 = TextBuilder('h5') -export const Heading6 = TextBuilder('h6') +export const Heading1 = TextBuilder('h1'); +export const Heading2 = TextBuilder('h2'); +export const Heading3 = TextBuilder('h3'); +export const Heading4 = TextBuilder('h4'); +export const Heading5 = TextBuilder('h5'); +export const Heading6 = TextBuilder('h6'); -export const BodyXl = TextBuilder('body-xl') -export const BodyLg = TextBuilder('body-lg') -export const BodyMd = TextBuilder('body-md') -export const Body = TextBuilder('body') -export const BodySm = TextBuilder('body-sm') -export const BodyXs = TextBuilder('body-xs') +export const BodyXl = TextBuilder('body-xl'); +export const BodyLg = TextBuilder('body-lg'); +export const BodyMd = TextBuilder('body-md'); +export const Body = TextBuilder('body'); +export const BodySm = TextBuilder('body-sm'); +export const BodyXs = TextBuilder('body-xs'); -export const Interface2Xl = TextBuilder('interface-2xl') -export const InterfaceXl = TextBuilder('interface-xl') -export const InterfaceLg = TextBuilder('interface-lg') -export const InterfaceMd = TextBuilder('interface-md') -export const Interface = TextBuilder('interface') -export const InterfaceSm = TextBuilder('interface-sm') +export const Interface2Xl = TextBuilder('interface-2xl'); +export const InterfaceXl = TextBuilder('interface-xl'); +export const InterfaceLg = TextBuilder('interface-lg'); +export const InterfaceMd = TextBuilder('interface-md'); +export const Interface = TextBuilder('interface'); +export const InterfaceSm = TextBuilder('interface-sm'); -export const PBodyLg = TextBuilder('p-body-lg') -export const PBodyMd = TextBuilder('p-body-md') -export const PBody = TextBuilder('p-body') -export const PBodySm = TextBuilder('p-body-sm') -export const PBodyXs = TextBuilder('p-body-xs') +export const PBodyLg = TextBuilder('p-body-lg'); +export const PBodyMd = TextBuilder('p-body-md'); +export const PBody = TextBuilder('p-body'); +export const PBodySm = TextBuilder('p-body-sm'); +export const PBodyXs = TextBuilder('p-body-xs'); diff --git a/packages/design-system/src/typography/typography.stories.tsx b/packages/design-system/src/typography/typography.stories.tsx index 9b139e38..50f69b18 100644 --- a/packages/design-system/src/typography/typography.stories.tsx +++ b/packages/design-system/src/typography/typography.stories.tsx @@ -1,5 +1,5 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import { Text } from './Text' +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Text } from './Text'; import { Body, BodyLg, @@ -24,7 +24,7 @@ import { PBodyMd, PBodySm, PBodyXs, -} from './typography-components' +} from './typography-components'; export default { title: 'Atoms/Typography', @@ -63,9 +63,9 @@ export default { options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'div'], }, }, -} satisfies Meta +} satisfies Meta; -type Story = StoryObj +type Story = StoryObj; export const Headings: Story = { render: () => ( @@ -78,7 +78,7 @@ export const Headings: Story = { Heading Level 6 ), -} +}; export const BodyText: Story = { render: () => ( @@ -91,7 +91,7 @@ export const BodyText: Story = { Body Extra Small Text ), -} +}; export const InterfaceText: Story = { render: () => ( @@ -116,7 +116,7 @@ export const InterfaceText: Story = { ), -} +}; export const ParagraphText: Story = { render: () => ( @@ -147,7 +147,7 @@ export const ParagraphText: Story = { ), -} +}; export const TextWithCustomTag: Story = { render: () => ( @@ -169,7 +169,7 @@ export const TextWithCustomTag: Story = { ), -} +}; export const InteractiveExample: Story = { args: { @@ -177,4 +177,4 @@ export const InteractiveExample: Story = { children: 'This is an interactive example. Try changing the variant and tag!', }, -} +}; diff --git a/packages/design-system/src/ui/button.stories.tsx b/packages/design-system/src/ui/button.stories.tsx index 60616b59..b4b2ba8d 100644 --- a/packages/design-system/src/ui/button.stories.tsx +++ b/packages/design-system/src/ui/button.stories.tsx @@ -1,12 +1,12 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import { Button } from './button' +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Button } from './button'; export default { title: 'UI/Button', component: Button, -} satisfies Meta +} satisfies Meta; -type Story = StoryObj +type Story = StoryObj; export const Default: Story = { args: { @@ -14,7 +14,7 @@ export const Default: Story = { variant: 'default', size: 'default', }, -} +}; export const Destructive: Story = { args: { @@ -22,7 +22,7 @@ export const Destructive: Story = { variant: 'destructive', size: 'default', }, -} +}; export const Outline: Story = { args: { @@ -30,7 +30,7 @@ export const Outline: Story = { variant: 'outline', size: 'default', }, -} +}; export const Secondary: Story = { args: { @@ -38,7 +38,7 @@ export const Secondary: Story = { variant: 'secondary', size: 'default', }, -} +}; export const Ghost: Story = { args: { @@ -46,7 +46,7 @@ export const Ghost: Story = { variant: 'ghost', size: 'default', }, -} +}; export const Link: Story = { args: { @@ -54,7 +54,7 @@ export const Link: Story = { variant: 'link', size: 'default', }, -} +}; export const SizeSmall: Story = { args: { @@ -62,7 +62,7 @@ export const SizeSmall: Story = { variant: 'default', size: 'sm', }, -} +}; export const SizeLarge: Story = { args: { @@ -70,7 +70,7 @@ export const SizeLarge: Story = { variant: 'default', size: 'lg', }, -} +}; // TODO export const SizeIcon: Story = { @@ -79,4 +79,4 @@ export const SizeIcon: Story = { variant: 'default', size: 'icon', }, -} +}; diff --git a/packages/design-system/src/ui/button.tsx b/packages/design-system/src/ui/button.tsx index c0c4d2d3..821195bf 100644 --- a/packages/design-system/src/ui/button.tsx +++ b/packages/design-system/src/ui/button.tsx @@ -1,7 +1,7 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' -import { Body, BodyLg, BodySm } from '../typography/typography-components' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { Body, BodyLg, BodySm } from '../typography/typography-components'; // TODO: I think it would be better to separate color and style? we may need, for example, a link styled destructive const buttonVariants = cva( @@ -33,7 +33,7 @@ const buttonVariants = cva( size: 'default', }, } -) +); function Button({ className, @@ -43,9 +43,9 @@ function Button({ ...props }: React.ComponentProps<'button'> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : 'button' + const Comp = asChild ? Slot : 'button'; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/packages/design-system/vitest.config.ts b/packages/design-system/vitest.config.ts index 9b6bd972..c641e500 100644 --- a/packages/design-system/vitest.config.ts +++ b/packages/design-system/vitest.config.ts @@ -1,5 +1,5 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], @@ -8,4 +8,4 @@ export default defineConfig({ setupFiles: ['@ageorgedev/testing-config/react-jsdom-test-setup.ts'], include: ['src/**/*.spec.{ts,tsx}'], }, -}) +}); diff --git a/packages/dnd-character-sheet/package.json b/packages/dnd-character-sheet/package.json index 1f46d310..232de05c 100644 --- a/packages/dnd-character-sheet/package.json +++ b/packages/dnd-character-sheet/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@ageorgedev/ts-config": "workspace:*", + "@types/mustache": "^4", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "concurrently": "10.0.3", @@ -30,6 +31,9 @@ "typescript": "5.8.3" }, "dependencies": { - "@ageorgedev/toolbelt": "workspace:*" + "@ageorgedev/toolbelt": "workspace:*", + "dompurify": "3.4.11", + "mustache": "4.2.0", + "ramda": "0.32.0" } } diff --git a/packages/dnd-character-sheet/src/characters/example-wizard.data.ts b/packages/dnd-character-sheet/src/characters/example-wizard.data.ts index 9fd6e572..5985b845 100644 --- a/packages/dnd-character-sheet/src/characters/example-wizard.data.ts +++ b/packages/dnd-character-sheet/src/characters/example-wizard.data.ts @@ -1,12 +1,22 @@ -import { Ability } from '../lib/models/abilities' -import type { Character } from '../lib/models/character' -import { Skill } from '../lib/models/skills' +import { Ability } from '../lib/models/abilities'; +import type { Character } from '../lib/models/character'; +import { Skill } from '../lib/models/skills'; export const exampleWizard: Character = { name: 'Seraphina Ashveil', - class: 'Wizard / Warlock', - subclass: 'School of Evocation / The Archfey', - level: 7, + classes: [ + { + name: 'Sorcerer', + subclass: 'Wild Magic', + level: 4, + }, + { + name: 'Warlock', + subclass: 'The Archfey', + level: 3, + }, + ], + species: 'High Elf', background: 'Sage', alignment: 'Neutral Good', @@ -37,11 +47,6 @@ export const exampleWizard: Character = { current: 45, }, - hitDice: { - dieType: 'd6', - total: 7, - }, - attacks: [ { name: 'Quarterstaff', @@ -74,15 +79,25 @@ export const exampleWizard: Character = { 'Dagger', 'Robes', ], - currency: { - cp: 0, - sp: 0, - ep: 0, - gp: 120, - pp: 0, - }, features: [ + { + name: 'Font of Magic', + source: 'Sorcerer 2', + description: + 'You can use a bonus action to convert sorcery points into spell slots, or vice versa.', + resource: { + name: 'Sorcery Points', + count: { + kind: 'class-level', + class: 'Sorcerer', + multiplier: 1, + }, + refresh: { + kind: 'long-rest', + }, + }, + }, { name: 'Arcane Recovery', source: 'Wizard 1', @@ -114,16 +129,6 @@ export const exampleWizard: Character = { toolProficiencies: ["Calligrapher's Supplies"], languages: ['Common', 'Elvish', 'Draconic', 'Sylvan'], - personality: { - traits: - 'I use polysyllabic words that convey the impression of great erudition.', - ideals: - 'Knowledge. The path to power and self-improvement is through knowledge.', - bonds: - "My life's work is a series of tomes related to the history of magic.", - flaws: 'I overlook obvious solutions in favour of complicated ones.', - }, - backstory: "Seraphina studied at the Arcane Academy for fifteen years before striking out on her own to research the ancient Ashveil bloodline's connection to the Weave.", @@ -142,7 +147,7 @@ export const exampleWizard: Character = { name: 'Fire Bolt', level: 0, school: 'Evocation', - castingTime: '1 action', + castingTime: 'Action', range: '120 ft', duration: 'Instantaneous', components: { verbal: true, somatic: true }, @@ -151,7 +156,7 @@ export const exampleWizard: Character = { name: 'Mage Hand', level: 0, school: 'Conjuration', - castingTime: '1 action', + castingTime: 'Action', range: '30 ft', duration: '1 minute', components: { verbal: true, somatic: true }, @@ -160,7 +165,7 @@ export const exampleWizard: Character = { name: 'Prestidigitation', level: 0, school: 'Transmutation', - castingTime: '1 action', + castingTime: 'Action', range: '10 ft', duration: 'Up to 1 hour', components: { verbal: true, somatic: true }, @@ -170,9 +175,9 @@ export const exampleWizard: Character = { name: 'Detect Magic', level: 1, school: 'Divination', - castingTime: '1 action', + castingTime: 'Action', range: 'Self', - duration: 'Concentration, up to 10 minutes', + duration: '10 minutes', concentration: true, ritual: true, components: { verbal: true, somatic: true }, @@ -182,7 +187,7 @@ export const exampleWizard: Character = { name: 'Magic Missile', level: 1, school: 'Evocation', - castingTime: '1 action', + castingTime: 'Action', range: '120 ft', duration: 'Instantaneous', components: { verbal: true, somatic: true }, @@ -191,7 +196,7 @@ export const exampleWizard: Character = { name: 'Shield', level: 1, school: 'Abjuration', - castingTime: '1 reaction', + castingTime: 'Reaction', range: 'Self', duration: '1 round', components: { verbal: true, somatic: true }, @@ -200,7 +205,7 @@ export const exampleWizard: Character = { name: 'Misty Step', level: 2, school: 'Conjuration', - castingTime: '1 bonus action', + castingTime: 'Bonus Action', range: 'Self', duration: 'Instantaneous', components: { verbal: true }, @@ -209,7 +214,7 @@ export const exampleWizard: Character = { name: 'Scorching Ray', level: 2, school: 'Evocation', - castingTime: '1 action', + castingTime: 'Action', range: '120 ft', duration: 'Instantaneous', components: { verbal: true, somatic: true }, @@ -218,9 +223,9 @@ export const exampleWizard: Character = { name: 'Hypnotic Pattern', level: 3, school: 'Illusion', - castingTime: '1 action', + castingTime: 'Action', range: '120 ft', - duration: 'Concentration, up to 1 minute', + duration: '1 minute', concentration: true, components: { verbal: false, somatic: true, materialConsumed: false }, notes: @@ -230,7 +235,7 @@ export const exampleWizard: Character = { name: 'Fireball', level: 3, school: 'Evocation', - castingTime: '1 action', + castingTime: 'Action', range: '150 ft', duration: 'Instantaneous', components: { verbal: true, somatic: true, materialConsumed: true }, @@ -240,9 +245,9 @@ export const exampleWizard: Character = { name: 'Banishment', level: 4, school: 'Abjuration', - castingTime: '1 action', + castingTime: 'Action', range: '60 ft', - duration: 'Concentration, up to 1 minute', + duration: '1 minute', concentration: true, components: { verbal: true, somatic: true, materialConsumed: false }, alternativeAbility: Ability.Charisma, @@ -252,4 +257,4 @@ export const exampleWizard: Character = { }, ], }, -} +}; diff --git a/packages/dnd-character-sheet/src/characters/example.tsx b/packages/dnd-character-sheet/src/characters/example.tsx index 56f323e0..a280c67a 100644 --- a/packages/dnd-character-sheet/src/characters/example.tsx +++ b/packages/dnd-character-sheet/src/characters/example.tsx @@ -1,6 +1,6 @@ -import { StandardCharacterSheet } from '../components/StandardCharacterSheet/StandardCharacterSheet' -import { exampleWizard } from './example-wizard.data' +import { StandardCharacterSheet } from '../components/StandardCharacterSheet/StandardCharacterSheet'; +import { exampleWizard } from './example-wizard.data'; export function ExampleSheet() { - return + return ; } diff --git a/packages/dnd-character-sheet/src/components/AbilityBox/AbilityBox.tsx b/packages/dnd-character-sheet/src/components/AbilityBox/AbilityBox.tsx index 50c49363..6076a10b 100644 --- a/packages/dnd-character-sheet/src/components/AbilityBox/AbilityBox.tsx +++ b/packages/dnd-character-sheet/src/components/AbilityBox/AbilityBox.tsx @@ -1,36 +1,33 @@ -import { ABILITY_DETAILS, type Ability } from '../../lib/models/abilities' -import { AbilitySkillGrouping } from '../../lib/models/skills' -import { useCharacter } from '../CharacterSheet' -import { BigNumber } from '../layout/BigNumber' -import type { CheckedState } from '../layout/checkables' -import { CircleCheck } from '../layout/checkables' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -function formatMod(mod: number): string { - return mod >= 0 ? `+${mod}` : `${mod}` -} +import { ABILITY_DETAILS, type Ability } from '../../lib/models/abilities'; +import { AbilitySkillGrouping } from '../../lib/models/skills'; +import { formatMod } from '../../lib/utils'; +import { useCharacter } from '../CharacterSheet'; +import { BigNumber } from '../layout/BigNumber'; +import type { CheckedState } from '../layout/checkables'; +import { CircleCheck } from '../layout/checkables'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; function proficiencyState( isProficient: boolean, hasExpertise: boolean ): CheckedState { - if (hasExpertise) return 'special' - if (isProficient) return true - return false + if (hasExpertise) return 'special'; + if (isProficient) return true; + return false; } interface AbilityBoxProps { - ability: Ability + ability: Ability; } export function AbilityBox({ ability }: AbilityBoxProps) { - const { character, derived } = useCharacter() - const score = character.abilities[ability] - const mod = derived.abilityModifiers[ability] - const savingThrow = derived.savingThrows[ability] - const isSaveProficient = character.savingThrowProficiencies.includes(ability) - const skills = AbilitySkillGrouping[ability] + const { character, derived } = useCharacter(); + const score = character.abilities[ability]; + const mod = derived.abilityModifiers[ability]; + const savingThrow = derived.savingThrows[ability]; + const isSaveProficient = character.savingThrowProficiencies.includes(ability); + const skills = AbilitySkillGrouping[ability]; return ( {skills.map((skill) => { - const isProficient = character.skillProficiencies.includes(skill) - const hasExpertise = character.skillExpertise.includes(skill) - const total = derived.skills[skill] + const isProficient = character.skillProficiencies.includes(skill); + const hasExpertise = character.skillExpertise.includes(skill); + const total = derived.skills[skill]; return ( {skill} - ) + ); })} )} - ) + ); } function SkillGrid({ children }: React.PropsWithChildren) { return (
{children}
- ) + ); } function SkillRow({ @@ -86,8 +83,8 @@ function SkillRow({ modifier, children, }: React.PropsWithChildren<{ - checkedState: CheckedState - modifier: number + checkedState: CheckedState; + modifier: number; }>) { return ( <> @@ -95,5 +92,5 @@ function SkillRow({ {formatMod(modifier)} {children} - ) + ); } diff --git a/packages/dnd-character-sheet/src/components/AttackList/AttackList.tsx b/packages/dnd-character-sheet/src/components/AttackList/AttackList.tsx index 2848419d..cbe33726 100644 --- a/packages/dnd-character-sheet/src/components/AttackList/AttackList.tsx +++ b/packages/dnd-character-sheet/src/components/AttackList/AttackList.tsx @@ -1,25 +1,27 @@ -import { ABILITY_DETAILS } from '../../lib/models/abilities' -import type { Attack, AttackDamage } from '../../lib/models/attacks' -import type { DerivedStats } from '../../lib/models/derived-stats' -import { useCharacter } from '../CharacterSheet' -import { DiamondCheck } from '../layout/checkables' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { ABILITY_DETAILS } from '../../lib/models/abilities'; +import type { Attack, AttackDamage } from '../../lib/models/attacks'; +import type { DerivedStats } from '../../lib/models/derived-stats'; +import { formatModIgnoreZero } from '../../lib/utils'; +import { useCharacter } from '../CharacterSheet'; +import { DiamondCheck } from '../layout/checkables'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; export function AttackList() { - const { character, derived } = useCharacter() + const { character, derived } = useCharacter(); - if (character.attacks.length === 0) return null + if (character.attacks.length === 0) return null; return ( - + Weapons & Damage Cantrips
- + @@ -30,7 +32,7 @@ export function AttackList() { const { bonusText, damageBonus } = calcAttackBonus( attack, derived - ) + ); return ( {formatDamage(attack.damage, damageBonus)} {attack.masteryProperty ? ( ) : ( - + )} - + - ) + ); })}
NameAttack Bonus / DCAtk / DC Damage & Type Mastery Notes
-   {attack.masteryProperty} +  {' '} + {attack.masteryProperty} —{attack.notes ?? '—'}{attack.notes}
- ) + ); } -function Th({ children }: { children: React.ReactNode }) { +function Th({ + className, + ...props +}: React.ThHTMLAttributes) { return ( - - {children} - - ) + + ); } function Td({ - children, - className = '', -}: { - children: React.ReactNode - className?: string -}) { - return {children} + className, + ...props +}: React.TdHTMLAttributes) { + return ; } function calcAttackBonus( @@ -84,40 +91,40 @@ function calcAttackBonus( ): { bonusText: string; damageBonus: number } { switch (attack.kind) { case 'weapon': { - const abilityMod = derived.abilityModifiers[attack.ability] - const profBonus = attack.notProficient ? 0 : derived.proficiencyBonus - const base = abilityMod + profBonus + (attack.attackBonusMod ?? 0) + const abilityMod = derived.abilityModifiers[attack.ability]; + const profBonus = attack.notProficient ? 0 : derived.proficiencyBonus; + const base = abilityMod + profBonus + (attack.attackBonusMod ?? 0); return { - bonusText: formatBonus(base), + bonusText: formatModIgnoreZero(base), damageBonus: abilityMod + (attack.attackBonusMod ?? 0), - } + }; } case 'spell-with-attack': { - let spellAttackBonus = derived.spellAttackBonus ?? 0 + let spellAttackBonus = derived.spellAttackBonus ?? 0; if (attack.ability) { - const abilityMod = derived.abilityModifiers[attack.ability] - const profBonus = attack.notProficient ? 0 : derived.proficiencyBonus - spellAttackBonus = abilityMod + profBonus + const abilityMod = derived.abilityModifiers[attack.ability]; + const profBonus = attack.notProficient ? 0 : derived.proficiencyBonus; + spellAttackBonus = abilityMod + profBonus; } - const totalAttackBonus = spellAttackBonus + (attack.attackBonusMod ?? 0) + const totalAttackBonus = spellAttackBonus + (attack.attackBonusMod ?? 0); return { - bonusText: formatBonus(totalAttackBonus), + bonusText: formatModIgnoreZero(totalAttackBonus), damageBonus: 0 + (attack.attackBonusMod ?? 0), - } + }; } case 'spell-with-save': { - let spellSaveDC = derived.spellSaveDC ?? 0 + let spellSaveDC = derived.spellSaveDC ?? 0; if (attack.ability) { - const abilityMod = derived.abilityModifiers[attack.ability] - const profBonus = attack.notProficient ? 0 : derived.proficiencyBonus - spellSaveDC = 8 + abilityMod + profBonus + const abilityMod = derived.abilityModifiers[attack.ability]; + const profBonus = attack.notProficient ? 0 : derived.proficiencyBonus; + spellSaveDC = 8 + abilityMod + profBonus; } return { bonusText: `${ABILITY_DETAILS[attack.saveAbility].shortName} save, DC ${spellSaveDC}`, damageBonus: 0 + (attack.attackBonusMod ?? 0), - } + }; } } } @@ -125,12 +132,8 @@ function calcAttackBonus( function formatDamage(entries: AttackDamage[], mod: number): string { return entries .map((entry) => { - const suffix = entry.disableModifier ? '' : formatBonus(mod) - return `${entry.dice}${suffix} ${entry.type}` + const suffix = entry.disableModifier ? '' : formatModIgnoreZero(mod); + return `${entry.dice}${suffix} ${entry.type}`; }) - .join(' + ') -} - -function formatBonus(n: number): string { - return n === 0 ? '' : n > 0 ? `+${n}` : `${n}` + .join(' + '); } diff --git a/packages/dnd-character-sheet/src/components/BackstoryBlock/BackstoryBlock.tsx b/packages/dnd-character-sheet/src/components/BackstoryBlock/BackstoryBlock.tsx deleted file mode 100644 index 1b13a722..00000000 --- a/packages/dnd-character-sheet/src/components/BackstoryBlock/BackstoryBlock.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -const PERSONALITY_FIELDS = [ - { key: 'traits', label: 'Personality Traits' }, - { key: 'ideals', label: 'Ideals' }, - { key: 'bonds', label: 'Bonds' }, - { key: 'flaws', label: 'Flaws' }, -] as const - -export function BackstoryBlock() { - const { character } = useCharacter() - - return ( - - - Backstory & Personality - -
- {PERSONALITY_FIELDS.map(({ key, label }) => ( -
-

- {label} -

-

- {character.personality[key]} -

-
- ))} - - {character.backstory && ( -
-

- Backstory -

-

- {character.backstory} -

-
- )} - - {character.notes && ( -
-

- Notes -

-

- {character.notes} -

-
- )} - - {character.languages.length > 0 && ( -
-

- Languages -

-

- {character.languages.join(', ')} -

-
- )} -
-
- ) -} diff --git a/packages/dnd-character-sheet/src/components/CharacterSheet.tsx b/packages/dnd-character-sheet/src/components/CharacterSheet.tsx index 82cbb7b9..11547c3d 100644 --- a/packages/dnd-character-sheet/src/components/CharacterSheet.tsx +++ b/packages/dnd-character-sheet/src/components/CharacterSheet.tsx @@ -1,39 +1,47 @@ -import { createContext, useContext, useMemo } from 'react' -import { calculateStats } from '../lib/calculate-derived-stats' -import type { Character } from '../lib/models/character' -import type { DerivedStats } from '../lib/models/derived-stats' +import { createContext, useContext, useMemo } from 'react'; +import { calculateStats } from '../lib/calculate-derived-stats'; +import type { Character } from '../lib/models/character'; +import type { DerivedStats } from '../lib/models/derived-stats'; +import { enrichCharacterData } from '../lib/text-enrichment'; interface CharacterContextValue { - character: Character - derived: DerivedStats + character: Character; + derived: DerivedStats; } -const CharacterContext = createContext(null) +const CharacterContext = createContext(null); export function useCharacter(): CharacterContextValue { - const ctx = useContext(CharacterContext) + const ctx = useContext(CharacterContext); if (!ctx) { throw new Error( 'useCharacter must be used inside a provider' - ) + ); } - return ctx + return ctx; } interface CharacterSheetProps { - data: Character - children: React.ReactNode + data: Character; + children: React.ReactNode; } export function CharacterSheet({ data, children }: CharacterSheetProps) { + const stats = useMemo(() => calculateStats(data), [data]); + + const character = useMemo( + () => enrichCharacterData(data, stats), + [data, stats] + ); + const value = useMemo( - () => ({ character: data, derived: calculateStats(data) }), - [data] - ) + () => ({ character, derived: stats }), + [character, stats] + ); return ( {children} - ) + ); } diff --git a/packages/dnd-character-sheet/src/components/ClassFeatures/ClassFeatures.tsx b/packages/dnd-character-sheet/src/components/ClassFeatures/ClassFeatures.tsx deleted file mode 100644 index 6cc0b1ab..00000000 --- a/packages/dnd-character-sheet/src/components/ClassFeatures/ClassFeatures.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -export function ClassFeatures() { - const { character } = useCharacter() - - return ( - - Class Features -
- {character.features.map((feature) => ( -
-

- {feature.name} -

-

- {feature.source} -

-

- {feature.description} -

-
- ))} -
-
- ) -} diff --git a/packages/dnd-character-sheet/src/components/CoinBlock/CoinBlock.tsx b/packages/dnd-character-sheet/src/components/CoinBlock/CoinBlock.tsx new file mode 100644 index 00000000..219bf0ff --- /dev/null +++ b/packages/dnd-character-sheet/src/components/CoinBlock/CoinBlock.tsx @@ -0,0 +1,30 @@ +import { HandWrittenNotes } from '../HandwrittenNotes/HandwrittenNotes'; +import { HorizontalDivider } from '../layout/dividers'; +import { Panel, type PanelProps } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; + +export function CoinBlock(props: PanelProps) { + return ( + + Coins & Treasure + +
+ + + + + +
+ +
+ ); +} + +function CoinSlot({ name }: { name: string }) { + return ( +
+ {name} +
+
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/CombatRow/CombatRow.tsx b/packages/dnd-character-sheet/src/components/CombatRow/CombatRow.tsx index dd1a7087..0b1e6609 100644 --- a/packages/dnd-character-sheet/src/components/CombatRow/CombatRow.tsx +++ b/packages/dnd-character-sheet/src/components/CombatRow/CombatRow.tsx @@ -1,28 +1,25 @@ -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -function formatMod(mod: number): string { - return mod >= 0 ? `+${mod}` : `${mod}` -} +import { formatMod } from '../../lib/utils'; +import { useCharacter } from '../CharacterSheet'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; function StatCell({ label, children, }: { - label: string - children: React.ReactNode + label: string; + children: React.ReactNode; }) { return ( {label} {children} - ) + ); } export function CombatRow() { - const { character, derived } = useCharacter() + const { character, derived } = useCharacter(); return (
@@ -33,5 +30,5 @@ export function CombatRow() { {derived.passivePerception}
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/EquipmentBlock/EquipmentBlock.tsx b/packages/dnd-character-sheet/src/components/EquipmentBlock/EquipmentBlock.tsx deleted file mode 100644 index a303338c..00000000 --- a/packages/dnd-character-sheet/src/components/EquipmentBlock/EquipmentBlock.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -const CURRENCY = [ - { key: 'pp', label: 'PP' }, - { key: 'gp', label: 'GP' }, - { key: 'ep', label: 'EP' }, - { key: 'sp', label: 'SP' }, - { key: 'cp', label: 'CP' }, -] as const - -export function EquipmentBlock() { - const { character } = useCharacter() - - return ( - - Equipment - -
-

- Currency -

-
- {CURRENCY.map(({ key, label }) => ( -
- - {character.currency[key]} - - - {label} - -
- ))} -
-
- -
    - {character.equipment.map((item) => ( -
  • - • - {item} -
  • - ))} -
-
- ) -} diff --git a/packages/dnd-character-sheet/src/components/EquipmentTraining/EquipmentTraining.tsx b/packages/dnd-character-sheet/src/components/EquipmentTraining/EquipmentTraining.tsx index 576afca7..001b294e 100644 --- a/packages/dnd-character-sheet/src/components/EquipmentTraining/EquipmentTraining.tsx +++ b/packages/dnd-character-sheet/src/components/EquipmentTraining/EquipmentTraining.tsx @@ -1,48 +1,53 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { useCharacter } from '../CharacterSheet' -import { DiamondCheck } from '../layout/checkables' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { useCharacter } from '../CharacterSheet'; +import { DiamondCheck } from '../layout/checkables'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; const ARMOR_TYPES: { key: string; label: string }[] = [ { key: 'Light armor', label: 'Light' }, { key: 'Medium armor', label: 'Medium' }, { key: 'Heavy armor', label: 'Heavy' }, { key: 'Shields', label: 'Shields' }, -] +]; export function EquipmentTraining() { - const { character } = useCharacter() + const { character } = useCharacter(); return ( - + Equipment Training & Proficiencies
Armor Training {ARMOR_TYPES.map(({ key, label }) => { - const trained = character.armorProficiencies.includes(key) + const trained = character.armorProficiencies.includes(key); return (
{label}
- ) + ); })}
Weapons -

{character.weaponProficiencies.join(', ')}

+

{character.weaponProficiencies.join(', ')}

Tools -

{character.toolProficiencies.join(', ')}

+

{character.toolProficiencies.join(', ')}

+
+ +
+ Languages +

{character.languages.join(', ') || 'Common'}

- ) + ); } function SectionTitle({ @@ -57,5 +62,5 @@ function SectionTitle({ className )} /> - ) + ); } diff --git a/packages/dnd-character-sheet/src/components/Feats/Feats.tsx b/packages/dnd-character-sheet/src/components/Feats/Feats.tsx deleted file mode 100644 index 180f9c26..00000000 --- a/packages/dnd-character-sheet/src/components/Feats/Feats.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -export function Feats() { - const { character } = useCharacter() - const feats = character.feats ?? [] - - return ( - - Feats -
- {feats.length === 0 ? ( -

- No feats -

- ) : ( - feats.map((feat) => ( -
-

- {feat.name} -

-

- {feat.source} -

-

- {feat.description} -

-
- )) - )} -
-
- ) -} diff --git a/packages/dnd-character-sheet/src/components/GenericPanel/GenericPanel.tsx b/packages/dnd-character-sheet/src/components/GenericPanel/GenericPanel.tsx new file mode 100644 index 00000000..8eedb827 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/GenericPanel/GenericPanel.tsx @@ -0,0 +1,23 @@ +import { HorizontalDivider } from '../layout/dividers'; +import { Panel, type PanelProps } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; +import { RichTextDisplay } from '../RichTextDisplay/RichTextDisplay'; + +export type GenericPanelProps = { + heading: string; + htmlContent: string; +} & PanelProps; + +export function GenericPanel({ + heading, + htmlContent, + ...props +}: GenericPanelProps) { + return ( + + {heading} + + + + ); +} diff --git a/packages/dnd-character-sheet/src/components/HandwrittenNotes/HandwrittenNotes.tsx b/packages/dnd-character-sheet/src/components/HandwrittenNotes/HandwrittenNotes.tsx new file mode 100644 index 00000000..c8fbaa0b --- /dev/null +++ b/packages/dnd-character-sheet/src/components/HandwrittenNotes/HandwrittenNotes.tsx @@ -0,0 +1,25 @@ +import { cn } from '@ageorgedev/toolbelt/cn'; + +type HandWrittenNotesProps = { + lineCount: number; +} & Omit, 'children'>; + +export function HandWrittenNotes({ + lineCount, + className, + ...props +}: HandWrittenNotesProps) { + return ( +
+ {Array.from({ length: lineCount }, (_, i) => ( +
+   +
+ ))} +
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/HeroicInspiration/HeroicInspiration.tsx b/packages/dnd-character-sheet/src/components/HeroicInspiration/HeroicInspiration.tsx index ccde51f3..44e358d0 100644 --- a/packages/dnd-character-sheet/src/components/HeroicInspiration/HeroicInspiration.tsx +++ b/packages/dnd-character-sheet/src/components/HeroicInspiration/HeroicInspiration.tsx @@ -1,6 +1,6 @@ -import { DiamondCheck } from '../layout/checkables' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' +import { DiamondCheck } from '../layout/checkables'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; export function HeroicInspiration() { return ( @@ -14,5 +14,5 @@ export function HeroicInspiration() {
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/Inventory/Inventory.tsx b/packages/dnd-character-sheet/src/components/Inventory/Inventory.tsx new file mode 100644 index 00000000..a34666f6 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/Inventory/Inventory.tsx @@ -0,0 +1,37 @@ +import { useCharacter } from '../CharacterSheet'; +import { HandWrittenNotes } from '../HandwrittenNotes/HandwrittenNotes'; +import { DiamondCheck } from '../layout/checkables'; +import { HorizontalDivider } from '../layout/dividers'; +import { Panel, type PanelProps } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; + +export function Inventory(props: PanelProps) { + const { character } = useCharacter(); + return ( + + Equipment + + {character.equipment.map((e) => ( +
{e}
+ ))} + +
+

+ Magic Item Attunement +

+ +
+ {Array.from({ length: 3 }, (_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: Not important +
+ +
+   +
+
+ ))} +
+
+
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/ProficiencyBlock/ProficiencyBlock.tsx b/packages/dnd-character-sheet/src/components/ProficiencyBlock/ProficiencyBlock.tsx index fa9e87a8..390693aa 100644 --- a/packages/dnd-character-sheet/src/components/ProficiencyBlock/ProficiencyBlock.tsx +++ b/packages/dnd-character-sheet/src/components/ProficiencyBlock/ProficiencyBlock.tsx @@ -1,10 +1,10 @@ -import { useCharacter } from '../CharacterSheet' -import { BigNumber } from '../layout/BigNumber' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' +import { useCharacter } from '../CharacterSheet'; +import { BigNumber } from '../layout/BigNumber'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; export function ProficiencyBlock() { - const { derived } = useCharacter() + const { derived } = useCharacter(); return ( Proficiency Bonus -
+
+{derived.proficiencyBonus}
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/Resources/Resources.tsx b/packages/dnd-character-sheet/src/components/Resources/Resources.tsx new file mode 100644 index 00000000..6c94bd97 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/Resources/Resources.tsx @@ -0,0 +1,131 @@ +// biome-ignore-all lint/suspicious/noArrayIndexKey: Not important +import { useMemo } from 'react'; +import type { Character } from '../../lib/models/character'; +import type { DerivedStats } from '../../lib/models/derived-stats'; +import type { Feature } from '../../lib/models/feature'; +import { useCharacter } from '../CharacterSheet'; +import { EmptyCheckList } from '../layout/checkables'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; + +type Resource = NonNullable; + +export function Resources() { + const { character, derived } = useCharacter(); + const resources = useMemo(() => { + const allResources = pullAllResourcesFromFeatures(character); + return allResources + .map(resourceDigester(character, derived)) + .filter(Boolean) as ResourceDisplay[]; + }, [character, derived]); + + if (!resources.length) { + return null; + } + + return ( + + Resources +
+ {resources.map((r) => ( +
+ {r.name} + {r.refresh} +
+ {r.display === 'dots' ? ( + // Array.from({ length: r.value }, (_, i) => ) + + ) : ( + <> +
+ / {r.value} + + )} +
+ ))} +
+
+ ); +} + +function pullAllResourcesFromFeatures(character: Character) { + return [ + ...character.features, + ...(character.speciesTraits ?? []), + ...(character.feats ?? []), + ] + .map((feature) => feature.resource) + .filter(Boolean) as Resource[]; +} + +type ResourceDisplay = { + value: number; + name: string; + display: 'dots' | 'numeric'; + refresh: string; +}; + +function resourceDigester(character: Character, stats: DerivedStats) { + return function digest(resource: Resource): ResourceDisplay | null { + const base = { + name: resource.name, + display: resource.count.display ?? 'dots', + refresh: `[${getRefreshText(resource.refresh)}]`, + }; + + const config = resource.count; + + switch (config.kind) { + case 'fixed': + return { + ...base, + value: config.value, + }; + + case 'character-level': { + return { + ...base, + value: stats.characterLevel * (config.multiplier ?? 1), + }; + } + + case 'class-level': { + const cls = character.classes.find((c) => c.name === config.class); + if (!cls) { + console.warn( + `Resource ${resource.name} depends on class ${config.class} which the character doesn't have.` + ); + return null; + } + return { + ...base, + value: cls.level * (config.multiplier ?? 1), + }; + } + + case 'ability': { + const mod = stats.abilityModifiers[config.ability]; + return { + ...base, + value: Math.max(mod * (config.multiplier ?? 1), config.min ?? 0), + }; + } + } + }; +} + +function getRefreshText(refresh: Resource['refresh']) { + switch (refresh.kind) { + case 'any-rest': + return 'Any Rest'; + + case 'long-rest': + return 'Long Rest'; + + case 'short-rest': + return 'Short Rest'; + + case 'short-and-long-rest': + return `${refresh.numberOfRefreshesOnShortRest} / Short Rest, all on Long Rest`; + } +} diff --git a/packages/dnd-character-sheet/src/components/RichTextDisplay/RichTextDisplay.module.css b/packages/dnd-character-sheet/src/components/RichTextDisplay/RichTextDisplay.module.css new file mode 100644 index 00000000..0f89077b --- /dev/null +++ b/packages/dnd-character-sheet/src/components/RichTextDisplay/RichTextDisplay.module.css @@ -0,0 +1,5 @@ +@reference "@ageorgedev/foundation-styles/theme.css"; + +.Container ol { + @apply list-decimal list-inside; +} diff --git a/packages/dnd-character-sheet/src/components/RichTextDisplay/RichTextDisplay.tsx b/packages/dnd-character-sheet/src/components/RichTextDisplay/RichTextDisplay.tsx new file mode 100644 index 00000000..a290ce42 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/RichTextDisplay/RichTextDisplay.tsx @@ -0,0 +1,29 @@ +/** biome-ignore-all lint/security/noDangerouslySetInnerHtml: handled through dompurify */ +import { cn } from '@ageorgedev/toolbelt/cn'; +import dompurify from 'dompurify'; +import { useMemo } from 'react'; +import styles from './RichTextDisplay.module.css'; + +type RichTextDisplayProps = Omit< + React.HTMLAttributes, + 'children' +> & { + content: string; + element?: 'span' | 'div'; +}; + +export function RichTextDisplay({ + className, + element: Element = 'div', + content, + ...props +}: RichTextDisplayProps) { + const cleanHtml = useMemo(() => dompurify.sanitize(content), [content]); + return ( + + ); +} diff --git a/packages/dnd-character-sheet/src/components/SheetHeader/SheetHeader.tsx b/packages/dnd-character-sheet/src/components/SheetHeader/SheetHeader.tsx index 3460425a..b403854e 100644 --- a/packages/dnd-character-sheet/src/components/SheetHeader/SheetHeader.tsx +++ b/packages/dnd-character-sheet/src/components/SheetHeader/SheetHeader.tsx @@ -1,11 +1,11 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { useCharacter } from '../CharacterSheet' -import { BigNumber } from '../layout/BigNumber' -import { DiamondCheck } from '../layout/checkables' -import { VerticalDivider } from '../layout/dividers' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' -import { VerticalSubPanel } from '../layout/SubPanel' +import { useCharacter } from '../CharacterSheet'; +import { BigNumber } from '../layout/BigNumber'; +import { DiamondCheck, EmptyCheckList } from '../layout/checkables'; +import { VerticalDivider } from '../layout/dividers'; +import { BasicLabel, LabelUnder } from '../layout/labels'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; +import { VerticalSubPanel } from '../layout/SubPanel'; export function SheetHeader() { return ( @@ -15,11 +15,11 @@ export function SheetHeader() {
- ) + ); } function NameBlock() { - const { character } = useCharacter() + const { character } = useCharacter(); return (

{character.name}

{character.background} - {character.class} + + {joinItems(character.classes, 'name')} + {character.species} - {character.subclass} + + {joinItems(character.classes, 'subclass')} +
- ) + ); } function LevelBlock() { - const { character } = useCharacter() + const { character } = useCharacter(); return (
- {character.level} + + {character.classes.reduce((total, cls) => total + cls.level, 0)} + Level
Xp
- ) + ); } function ArmorBlock() { - const { character } = useCharacter() + const { character } = useCharacter(); return ( Shield - ) + ); } function HealthAndDeathBlock() { - const { character } = useCharacter() + const { character, derived } = useCharacter(); return ( @@ -89,8 +95,7 @@ function HealthAndDeathBlock() {
Spent - {character.hitDice.total} - {character.hitDice.dieType} + {derived.hitDice.map((d) => `${d.count}${d.dice}`).join(' + ')} Max
@@ -102,24 +107,16 @@ function HealthAndDeathBlock() { Saves
-
- - - -
+ Successes
-
- - - -
+ Failures
- ) + ); } /** Smaller utils */ @@ -128,32 +125,23 @@ function NameField({ label, children, }: { - label: string - children: React.ReactNode + label: string; + children: React.ReactNode; }) { return (
{children} {label}
- ) + ); } -function LabelUnder({ - className, - ...props -}: React.HTMLAttributes) { - return -} - -function BasicLabel({ - children, - className, - ...props -}: React.HTMLAttributes) { - return ( -
- {children} -
- ) +function joinItems< + T extends Record, + K extends keyof T, +>(items: T[], key: K) { + return items + .map((item) => item[key]) + .filter((value) => value != null) + .join(' / '); } diff --git a/packages/dnd-character-sheet/src/components/SpeciesTraits/SpeciesTraits.tsx b/packages/dnd-character-sheet/src/components/SpeciesTraits/SpeciesTraits.tsx deleted file mode 100644 index 57fdf7e7..00000000 --- a/packages/dnd-character-sheet/src/components/SpeciesTraits/SpeciesTraits.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' - -export function SpeciesTraits() { - const { character } = useCharacter() - const traits = character.speciesTraits ?? [] - - return ( - - Species Traits -
- {traits.length === 0 ? ( -

- No species traits -

- ) : ( - traits.map((trait) => ( -
-

- {trait.name} -

-

- {trait.source} -

-

- {trait.description} -

-
- )) - )} -
-
- ) -} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx new file mode 100644 index 00000000..f6f4c75f --- /dev/null +++ b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx @@ -0,0 +1,45 @@ +import { ABILITY_DETAILS } from '../../lib/models'; +import { formatMod } from '../../lib/utils'; +import { useCharacter } from '../CharacterSheet'; +import { BigNumber } from '../layout/BigNumber'; +import { HorizontalDivider } from '../layout/dividers'; +import { LabelUnder } from '../layout/labels'; +import { Panel, type PanelProps } from '../layout/Panel'; + +export function SpellAbilityPanel(props: PanelProps) { + const { character, derived } = useCharacter(); + + if (!character.spellcasting) { + return null; + } + + return ( + +

+ {ABILITY_DETAILS[character.spellcasting.ability].label} +

+ Spellcasting Ability + +
+ Spellcasting Modifier + + {formatMod(derived.abilityModifiers[character.spellcasting.ability])} + + + + + Spellcasting Save DC + + {derived.spellSaveDC} + + + + + Spellcasting Attack Bonus + + {formatMod(derived.spellAttackBonus ?? 0)} + +
+
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellLevelSection.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellLevelSection.tsx deleted file mode 100644 index 135e947d..00000000 --- a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellLevelSection.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Spell } from '../../lib/models/spellcasting' -import { SpellRow } from './SpellRow' - -interface SpellLevelSectionProps { - level: number - spells: Spell[] -} - -export function SpellLevelSection({ level, spells }: SpellLevelSectionProps) { - const label = level === 0 ? 'Cantrips' : `Level ${level}` - - return ( -
-
- - {label} - -
- {spells.length > 0 && ( -
- {spells.map((spell) => ( - - ))} -
- )} -
- ) -} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellList.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellList.tsx new file mode 100644 index 00000000..ff7d2b73 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellList.tsx @@ -0,0 +1,196 @@ +import { cn } from '@ageorgedev/toolbelt/cn'; +import { ABILITY_DETAILS, type Spell } from '../../lib/models'; +import { formatMod } from '../../lib/utils'; +import { useCharacter } from '../CharacterSheet'; +import { + CircleCheck, + DiamondCheck, + EmptyCheckList, +} from '../layout/checkables'; +import { HorizontalDivider } from '../layout/dividers'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; + +export function SpellList() { + const { + character: { spellcasting }, + } = useCharacter(); + + const totalRows = 30; // problematic, does not work all the time. if we don't find a css solution, might have to prop drill from StandardCharacter sheet + const emptyRows = totalRows - (spellcasting?.spells.length ?? 0); + + return ( + + Spells + + + + + + + + + + + + + + + + {spellcasting?.spells.map((spell) => ( + + ))} + {Array.from({ length: emptyRows }, (_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: not required for this + + ))} + +
PrepLevel  NameCasting TimeRangeDurationConcentration, Ritual & Material consumedNotes
+
+ ); +} + +export function SpellRow({ spell }: { spell: Spell }) { + const { derived } = useCharacter(); + const vs = [spell.components?.verbal && 'V', spell.components?.somatic && 'S'] + .filter(Boolean) + .join(','); + + const freeUsesNode = !spell.freeUses ? null : ( + + free x{spell.freeUses}{' '} + + + ); + + const notesSection = [ + vs && {vs}, + spell.notes && {spell.notes}, + ].filter(Boolean); + + const castingTimeText = (() => { + switch (spell.castingTime.toLowerCase()) { + case 'action': + return 'Action'; + case 'bonus action': + return 'Bonus'; + default: + return spell.duration; + } + })(); + + const altAbilityNode = !spell.alternativeAbility + ? null + : (() => { + const name = ABILITY_DETAILS[spell.alternativeAbility].shortName; + const abilityMod = derived.abilityModifiers[spell.alternativeAbility]; + const toHit = formatMod(abilityMod + derived.proficiencyBonus); + const dc = abilityMod + derived.proficiencyBonus + 8; + return ( + + ({name} spell, {toHit} to Hit, DC {dc}) + + ); + })(); + + return ( + + + {spell.alwaysPrepared ? ( + AP + ) : ( + + )} + + {spell.level} + +
+ {spell.name} + {freeUsesNode} + {altAbilityNode} +
+ + {castingTimeText} + {spell.range} + {spell.duration} + + + {notesSection.map((node, idx) => ( + <> + {node} + {idx === notesSection.length - 1 ? '' : ' | '} + + ))} + + + ); +} + +function EmptyRow() { + return ( + + +   {/* Adds the text height required */} + + + + + + + + ); +} + +function CRMcell({ c, r, m }: { c?: boolean; r?: boolean; m?: boolean }) { + return ( + +
+ + C + + + R + + + M + +
+ + ); +} + +/** Utils */ + +function Th({ + className, + ...props +}: React.ThHTMLAttributes) { + return ( + + ); +} + +function Td({ + className, + ...props +}: React.ThHTMLAttributes) { + return ( + + ); +} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellRow.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellRow.tsx deleted file mode 100644 index 4d6b7a1a..00000000 --- a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellRow.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { ABILITY_DETAILS } from '../../lib/models/abilities' -import type { Spell } from '../../lib/models/spellcasting' -import { CircleCheck } from '../layout/checkables' - -function ComponentFlags({ spell }: { spell: Spell }) { - const { components } = spell - if (!components) return null - const flags: string[] = [] - if (components.verbal) flags.push('V') - if (components.somatic) flags.push('S') - if (components.materialConsumed) flags.push('M*') - else if ( - 'materialConsumed' in components && - components.materialConsumed === false - ) - flags.push('M') - if (flags.length === 0) return null - return {flags.join(' ')} -} - -function Badge({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -interface SpellRowProps { - spell: Spell - showPrepared: boolean -} - -export function SpellRow({ spell, showPrepared }: SpellRowProps) { - return ( - <> - {showPrepared ? ( -
- -
- ) : null} -
- {spell.name} -
-
- -
-
- {spell.castingTime} -
-
- {spell.range} -
-
- {spell.duration} -
-
-
- {spell.concentration && C} - {spell.ritual && R} - {spell.alwaysPrepared && ā˜…} - {spell.freeUses != null && freeƗ{spell.freeUses}} - {spell.alternativeAbility && ( - {ABILITY_DETAILS[spell.alternativeAbility].shortName} - )} -
-
- - ) -} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellSlotsPanel.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellSlotsPanel.tsx new file mode 100644 index 00000000..5bbd3879 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellSlotsPanel.tsx @@ -0,0 +1,82 @@ +import { cn } from '@ageorgedev/toolbelt/cn'; +import { useMemo } from 'react'; +import { useCharacter } from '../CharacterSheet'; +import { EmptyCheckList } from '../layout/checkables'; +import { Panel, type PanelProps } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; + +export function SpellSlotsPanel(props: PanelProps) { + const { + character: { spellcasting }, + } = useCharacter(); + + const allSlots = useMemo(() => { + const spellSlots = spellcasting?.slots + ? (Object.entries(spellcasting.slots) as [string, number][]) + .sort(([a], [b]) => Number(a) - Number(b)) + .map(([level, slots]) => [`Level ${level}`, slots] as const) + : []; + + const pactMagicSlots = spellcasting?.pactMagic + ? [ + [ + `Pact Magic level ${spellcasting.pactMagic.level}`, + spellcasting.pactMagic.slots, + ] as const, + ] + : []; + + return [...pactMagicSlots, ...spellSlots]; + }, [spellcasting]); + + const columnStyle = useMemo(() => { + switch (allSlots.length) { + case 0: + case 1: + return ''; + case 2: + case 4: + return 'columns-2'; + default: + return 'columns-3'; + } + }, [allSlots]); + + if (!spellcasting) return null; + + return ( + + Spell Slots +
+ Cantrips:{' '} + + {spellcasting.numberOfCantrips ?? 0} + + , Prepared Spells:{' '} + + {spellcasting.numberOfPreparedSpells ?? 0} + +
+ +
+ {allSlots.map(([name, count]) => ( +
+ {name} +
+ +
+ ))} + {!allSlots.length && ( +
+ No Slots +
+ )} +
+
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingBlock.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingBlock.tsx index e49f9c3e..4f17e450 100644 --- a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingBlock.tsx +++ b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingBlock.tsx @@ -1,49 +1,15 @@ -import type { Spell } from '../../lib/models/spellcasting' -import { useCharacter } from '../CharacterSheet' -import { Panel } from '../layout/Panel' -import { PanelTitle } from '../layout/PanelTitle' -import { SpellcastingHeader } from './SpellcastingHeader' -import { SpellLevelSection } from './SpellLevelSection' +import { SpellAbilityPanel } from './SpellAbilityPanel'; +import { SpellList } from './SpellList'; +import { SpellSlotsPanel } from './SpellSlotsPanel'; export function SpellcastingBlock() { - const { character } = useCharacter() - const { spellcasting } = character - if (!spellcasting) return null - - // Collect all levels that have spells or defined slots - const spellsByLevel = new Map() - - for (const spell of spellcasting.spells) { - const bucket = spellsByLevel.get(spell.level) ?? [] - bucket.push(spell) - spellsByLevel.set(spell.level, bucket) - } - - // Ensure levels with slots but no spells still appear - if (spellcasting.slots) { - for (const levelStr of Object.keys(spellcasting.slots)) { - const level = Number(levelStr) - if (!spellsByLevel.has(level)) { - spellsByLevel.set(level, []) - } - } - } - - const sortedLevels = [...spellsByLevel.keys()].sort((a, b) => a - b) - return ( - - Spellcasting - -
- {sortedLevels.map((level) => ( - - ))} +
+
+ +
- - ) + +
+ ); } diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingHeader.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingHeader.tsx deleted file mode 100644 index 3e5ae4ca..00000000 --- a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellcastingHeader.tsx +++ /dev/null @@ -1,101 +0,0 @@ -// biome-ignore-all lint/suspicious/noArrayIndexKey: Not important -import { useCharacter } from '../CharacterSheet' -import { CircleCheck } from '../layout/checkables' - -const ABILITY_LABELS: Record = { - strength: 'Strength', - dexterity: 'Dexterity', - constitution: 'Constitution', - intelligence: 'Intelligence', - wisdom: 'Wisdom', - charisma: 'Charisma', -} - -function formatBonus(n: number): string { - return n >= 0 ? `+${n}` : `${n}` -} - -export function SpellcastingHeader() { - const { character, derived } = useCharacter() - const { spellcasting } = character - if (!spellcasting) return null - - const abilityLabel = - ABILITY_LABELS[spellcasting.ability] ?? spellcasting.ability - const slotLevels = spellcasting.slots - ? (Object.entries(spellcasting.slots) as [string, number][]).sort( - ([a], [b]) => Number(a) - Number(b) - ) - : [] - - return ( -
-
-
- - Ability - - - {abilityLabel} - -
-
- - Save DC - - - {derived.spellSaveDC ?? '—'} - -
-
- - Spell Attack - - - {derived.spellAttackBonus != null - ? formatBonus(derived.spellAttackBonus) - : '—'} - -
-
- - {slotLevels.length > 0 && ( -
- - Slots - - {slotLevels.map(([level, count]) => ( -
- - {level}: - -
- {Array.from({ length: count }, (_, i) => ( - - ))} -
-
- ))} -
- )} - - {spellcasting.pactMagic && ( -
- - Pact - -
- - {spellcasting.pactMagic.level}: - -
- {Array.from({ length: spellcasting.pactMagic.slots }, (_, i) => ( - - ))} -
-
-
- )} -
- ) -} diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/index.ts b/packages/dnd-character-sheet/src/components/SpellcastingBlock/index.ts deleted file mode 100644 index 9195fbd0..00000000 --- a/packages/dnd-character-sheet/src/components/SpellcastingBlock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SpellcastingBlock } from './SpellcastingBlock' diff --git a/packages/dnd-character-sheet/src/components/StandardCharacterSheet/StandardCharacterSheet.tsx b/packages/dnd-character-sheet/src/components/StandardCharacterSheet/StandardCharacterSheet.tsx index 8dc563d9..8f1e0a2e 100644 --- a/packages/dnd-character-sheet/src/components/StandardCharacterSheet/StandardCharacterSheet.tsx +++ b/packages/dnd-character-sheet/src/components/StandardCharacterSheet/StandardCharacterSheet.tsx @@ -1,41 +1,45 @@ -import { Ability } from '../../lib/models/abilities' -import type { Character } from '../../lib/models/character' -import { AbilityBox } from '../AbilityBox/AbilityBox' -import { AttackList } from '../AttackList/AttackList' -import { BackstoryBlock } from '../BackstoryBlock/BackstoryBlock' -import { CharacterSheet } from '../CharacterSheet' -import { ClassFeatures } from '../ClassFeatures/ClassFeatures' -import { CombatRow } from '../CombatRow/CombatRow' -import { EquipmentBlock } from '../EquipmentBlock/EquipmentBlock' -import { EquipmentTraining } from '../EquipmentTraining/EquipmentTraining' -import { Feats } from '../Feats/Feats' -import { HeroicInspiration } from '../HeroicInspiration/HeroicInspiration' -import { Page } from '../layout/Page' -import { ProficiencyBlock } from '../ProficiencyBlock/ProficiencyBlock' -import { SheetHeader } from '../SheetHeader/SheetHeader' -import { SpeciesTraits } from '../SpeciesTraits/SpeciesTraits' -import { SpellcastingBlock } from '../SpellcastingBlock' +import { Ability } from '../../lib/models/abilities'; +import type { Character } from '../../lib/models/character'; +import { AbilityBox } from '../AbilityBox/AbilityBox'; +import { AttackList } from '../AttackList/AttackList'; +import { CharacterSheet } from '../CharacterSheet'; +import { CoinBlock } from '../CoinBlock/CoinBlock'; +import { CombatRow } from '../CombatRow/CombatRow'; +import { EquipmentTraining } from '../EquipmentTraining/EquipmentTraining'; +import { ClassFeatures } from '../feature-blocks/ClassFeatures'; +import { Feats } from '../feature-blocks/Feats'; +import { SpeciesTraits } from '../feature-blocks/SpeciesTraits'; +import { GenericPanel } from '../GenericPanel/GenericPanel'; +import { HeroicInspiration } from '../HeroicInspiration/HeroicInspiration'; +import { Inventory } from '../Inventory/Inventory'; +import { Page } from '../layout/Page'; +import { ProficiencyBlock } from '../ProficiencyBlock/ProficiencyBlock'; +import { Resources } from '../Resources/Resources'; +import { SheetHeader } from '../SheetHeader/SheetHeader'; +import { SpellcastingBlock } from '../SpellcastingBlock/SpellcastingBlock'; interface Props { - data: Character + data: Character; } export function StandardCharacterSheet({ data }: Props) { return ( - + + {' '} + {/** Should not be margin here, for print reasons */} -
+
-
+
-
+
@@ -47,6 +51,7 @@ export function StandardCharacterSheet({ data }: Props) {
+
@@ -57,16 +62,27 @@ export function StandardCharacterSheet({ data }: Props) { -
-
+
+
{data.spellcasting && }
-
- - +
+ + + +
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/feature-blocks/ClassFeatures.tsx b/packages/dnd-character-sheet/src/components/feature-blocks/ClassFeatures.tsx new file mode 100644 index 00000000..ddc85d39 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/feature-blocks/ClassFeatures.tsx @@ -0,0 +1,18 @@ +import { useCharacter } from '../CharacterSheet'; +import { FeatureList } from '../feature-blocks/FeatureList'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; + +export function ClassFeatures() { + const { character } = useCharacter(); + + return ( + + Class Features + + + ); +} diff --git a/packages/dnd-character-sheet/src/components/feature-blocks/Feats.tsx b/packages/dnd-character-sheet/src/components/feature-blocks/Feats.tsx new file mode 100644 index 00000000..05984d30 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/feature-blocks/Feats.tsx @@ -0,0 +1,15 @@ +import { useCharacter } from '../CharacterSheet'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; +import { FeatureList } from './FeatureList'; + +export function Feats() { + const { character } = useCharacter(); + + return ( + + Feats + + + ); +} diff --git a/packages/dnd-character-sheet/src/components/feature-blocks/FeatureEntry.tsx b/packages/dnd-character-sheet/src/components/feature-blocks/FeatureEntry.tsx new file mode 100644 index 00000000..f6705bc7 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/feature-blocks/FeatureEntry.tsx @@ -0,0 +1,16 @@ +import type { Feature } from '../../lib/models/feature'; +import { RichTextDisplay } from '../RichTextDisplay/RichTextDisplay'; + +export function FeatureEntry({ data: feat }: { data: Feature }) { + return ( +
+ {feat.name}. + {feat.cost && ( + (Cost: {feat.cost}) + )} +   + {/*  {feat.description} */} + +
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/feature-blocks/FeatureList.tsx b/packages/dnd-character-sheet/src/components/feature-blocks/FeatureList.tsx new file mode 100644 index 00000000..6cf00a49 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/feature-blocks/FeatureList.tsx @@ -0,0 +1,28 @@ +import { cn } from '@ageorgedev/toolbelt/cn'; +import type { Feature } from '../../lib/models/feature'; +import { FeatureEntry } from './FeatureEntry'; + +type FeatureListProps = React.HTMLAttributes & { + features: Feature[]; +}; + +export function FeatureList({ + className, + features, + ...props +}: FeatureListProps) { + if (!features.length) { + return ( +
+ No features +
+ ); + } + return ( +
+ {features.map((feature) => ( + + ))} +
+ ); +} diff --git a/packages/dnd-character-sheet/src/components/feature-blocks/SpeciesTraits.tsx b/packages/dnd-character-sheet/src/components/feature-blocks/SpeciesTraits.tsx new file mode 100644 index 00000000..0989e5a8 --- /dev/null +++ b/packages/dnd-character-sheet/src/components/feature-blocks/SpeciesTraits.tsx @@ -0,0 +1,15 @@ +import { useCharacter } from '../CharacterSheet'; +import { Panel } from '../layout/Panel'; +import { PanelTitle } from '../layout/PanelTitle'; +import { FeatureList } from './FeatureList'; + +export function SpeciesTraits() { + const { character } = useCharacter(); + + return ( + + Species Traits + + + ); +} diff --git a/packages/dnd-character-sheet/src/components/layout/BigNumber.tsx b/packages/dnd-character-sheet/src/components/layout/BigNumber.tsx index dae6b214..ed9e7058 100644 --- a/packages/dnd-character-sheet/src/components/layout/BigNumber.tsx +++ b/packages/dnd-character-sheet/src/components/layout/BigNumber.tsx @@ -1,4 +1,4 @@ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; export function BigNumber({ className, @@ -9,5 +9,5 @@ export function BigNumber({ className={cn('text-4xl leading-none lining-nums', className)} {...props} /> - ) + ); } diff --git a/packages/dnd-character-sheet/src/components/layout/Page.module.css b/packages/dnd-character-sheet/src/components/layout/Page.module.css index 1773d9a7..673e264e 100644 --- a/packages/dnd-character-sheet/src/components/layout/Page.module.css +++ b/packages/dnd-character-sheet/src/components/layout/Page.module.css @@ -1,5 +1,15 @@ +@reference "@ageorgedev/foundation-styles/theme.css"; + @media print { - .Page { + .PageOuter { break-inside: avoid; } } + +@media print { + .PageInner { + @apply py-6; + height: 100vh; + overflow: hidden; + } +} diff --git a/packages/dnd-character-sheet/src/components/layout/Page.tsx b/packages/dnd-character-sheet/src/components/layout/Page.tsx index ada584b7..ad295065 100644 --- a/packages/dnd-character-sheet/src/components/layout/Page.tsx +++ b/packages/dnd-character-sheet/src/components/layout/Page.tsx @@ -1,12 +1,23 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import styles from './Page.module.css' +import { cn } from '@ageorgedev/toolbelt/cn'; +import styles from './Page.module.css'; -export function Page({ children }: React.PropsWithChildren) { +export function Page({ + children, + className, + ...props +}: React.HTMLAttributes) { return ( -
-
+
+
{children}
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/layout/Panel.tsx b/packages/dnd-character-sheet/src/components/layout/Panel.tsx index 8522966d..43c82aed 100644 --- a/packages/dnd-character-sheet/src/components/layout/Panel.tsx +++ b/packages/dnd-character-sheet/src/components/layout/Panel.tsx @@ -1,17 +1,17 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { useMemo } from 'react' -import styles from './Panel.module.css' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { useMemo } from 'react'; +import styles from './Panel.module.css'; -type Corner = 'scooped' | 'regular' +type Corner = 'scooped' | 'regular'; export type PanelProps = { - outerClasses?: string + outerClasses?: string; - topLeftCorner?: Corner - topRightCorner?: Corner - bottomRightCorner?: Corner - bottomLeftCorner?: Corner -} & React.HTMLAttributes + topLeftCorner?: Corner; + topRightCorner?: Corner; + bottomRightCorner?: Corner; + bottomLeftCorner?: Corner; +} & React.HTMLAttributes; export function Panel({ topLeftCorner = 'regular', @@ -38,7 +38,7 @@ export function Panel({ : styles.regularBottomLeft, ], [topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner] - ) + ); return (
@@ -52,5 +52,5 @@ export function Panel({ )} />
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/layout/PanelTitle.tsx b/packages/dnd-character-sheet/src/components/layout/PanelTitle.tsx index 18006e18..9c9d4929 100644 --- a/packages/dnd-character-sheet/src/components/layout/PanelTitle.tsx +++ b/packages/dnd-character-sheet/src/components/layout/PanelTitle.tsx @@ -1,4 +1,4 @@ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; export function PanelTitle({ children, @@ -15,5 +15,5 @@ export function PanelTitle({ > {children} - ) + ); } diff --git a/packages/dnd-character-sheet/src/components/layout/SubPanel.tsx b/packages/dnd-character-sheet/src/components/layout/SubPanel.tsx index f67a97a7..baa81ae4 100644 --- a/packages/dnd-character-sheet/src/components/layout/SubPanel.tsx +++ b/packages/dnd-character-sheet/src/components/layout/SubPanel.tsx @@ -1,5 +1,5 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import styles from './SubPanel.module.css' +import { cn } from '@ageorgedev/toolbelt/cn'; +import styles from './SubPanel.module.css'; export function VerticalSubPanel({ className, @@ -7,7 +7,7 @@ export function VerticalSubPanel({ }: React.HtmlHTMLAttributes) { return (
- ) + ); } export function HorizontalSubPanel({ @@ -16,5 +16,5 @@ export function HorizontalSubPanel({ }: React.HtmlHTMLAttributes) { return (
- ) + ); } diff --git a/packages/dnd-character-sheet/src/components/layout/checkables.tsx b/packages/dnd-character-sheet/src/components/layout/checkables.tsx index d63beef8..35404e9d 100644 --- a/packages/dnd-character-sheet/src/components/layout/checkables.tsx +++ b/packages/dnd-character-sheet/src/components/layout/checkables.tsx @@ -1,35 +1,61 @@ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; export type CheckableProps = { - checked?: CheckedState -} & React.HTMLAttributes + checked?: CheckedState; +} & React.HTMLAttributes; export function CircleCheck({ className, ...props }: CheckableProps) { - return + return ; } export function DiamondCheck({ className, ...props }: CheckableProps) { - return + return ( + + ); } function BaseCheck({ checked = false, className, ...props }: CheckableProps) { - const fill = CHECK_FILL[String(checked) as keyof typeof CHECK_FILL] + const fill = CHECK_FILL[String(checked) as keyof typeof CHECK_FILL]; return ( - ) + ); } -export type CheckedState = boolean | 'special' | undefined +export type CheckedState = boolean | 'special' | undefined; const CHECK_FILL: Record<'true' | 'false' | 'special', string> = { true: 'bg-primary-surface border-primary-foreground', special: 'bg-secondary-surface-2 border-secondary-foreground', false: '', +}; + +export function EmptyCheckList({ + className, + count, + kind = 'circle', + ...props +}: React.HtmlHTMLAttributes & { + count: number; + kind?: 'circle' | 'diamond'; +}) { + const Comp = kind === 'circle' ? CircleCheck : DiamondCheck; + + return ( +
+ {Array.from({ length: count }, (_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: Empty checkboxes, will be fine + + ))} +
+ ); } diff --git a/packages/dnd-character-sheet/src/components/layout/dividers.tsx b/packages/dnd-character-sheet/src/components/layout/dividers.tsx index 02f0b7bf..260cb705 100644 --- a/packages/dnd-character-sheet/src/components/layout/dividers.tsx +++ b/packages/dnd-character-sheet/src/components/layout/dividers.tsx @@ -1,7 +1,23 @@ -export function VerticalDivider() { - return
+import { cn } from '@ageorgedev/toolbelt/cn'; + +export function VerticalDivider({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); } -export function HorizontalDivider() { - return
+export function HorizontalDivider({ + className, + ...props +}: React.HTMLAttributes) { + return
; } diff --git a/packages/dnd-character-sheet/src/components/layout/labels.tsx b/packages/dnd-character-sheet/src/components/layout/labels.tsx new file mode 100644 index 00000000..040119be --- /dev/null +++ b/packages/dnd-character-sheet/src/components/layout/labels.tsx @@ -0,0 +1,20 @@ +import { cn } from '@ageorgedev/toolbelt/cn'; + +export function BasicLabel({ + children, + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ {children} +
+ ); +} + +export function LabelUnder({ + className, + ...props +}: React.HTMLAttributes) { + return ; +} diff --git a/packages/dnd-character-sheet/src/index.ts b/packages/dnd-character-sheet/src/index.ts index 0c4bf30f..591c11da 100644 --- a/packages/dnd-character-sheet/src/index.ts +++ b/packages/dnd-character-sheet/src/index.ts @@ -1,5 +1,12 @@ // Assembled character sheets -export { ExampleSheet } from './characters/example' +export { ExampleSheet } from './characters/example'; // Standard assembled sheet -export { StandardCharacterSheet } from './components/StandardCharacterSheet/StandardCharacterSheet' +export { StandardCharacterSheet } from './components/StandardCharacterSheet/StandardCharacterSheet'; + +// Other models +export { Ability } from './lib/models/abilities'; +export type { Character } from './lib/models/character'; +export { CharacterClass } from './lib/models/character-classes'; +export { Skill } from './lib/models/skills'; +export type { Spell } from './lib/models/spellcasting'; diff --git a/packages/dnd-character-sheet/src/lib/calculate-derived-stats.ts b/packages/dnd-character-sheet/src/lib/calculate-derived-stats.ts index f4ab6b6d..93809696 100644 --- a/packages/dnd-character-sheet/src/lib/calculate-derived-stats.ts +++ b/packages/dnd-character-sheet/src/lib/calculate-derived-stats.ts @@ -1,54 +1,60 @@ -import { Ability, ALL_ABILITIES } from './models/abilities' -import type { Character } from './models/character' -import type { DerivedStats } from './models/derived-stats' -import { AbilitySkillGrouping, Skill } from './models/skills' +import { groupBy, map, mapObjIndexed, pipe, reduce, toPairs } from 'ramda'; +import { HIT_DICE_TABLE } from './character-class-constants'; +import { Ability, ALL_ABILITIES } from './models/abilities'; +import type { Character } from './models/character'; +import type { DerivedStats } from './models/derived-stats'; +import { AbilitySkillGrouping, Skill } from './models/skills'; function abilityModifier(score: number): number { - return Math.floor((score - 10) / 2) + return Math.floor((score - 10) / 2); } function proficiencyBonus(level: number): number { - if (level <= 4) return 2 - if (level <= 8) return 3 - if (level <= 12) return 4 - if (level <= 16) return 5 - return 6 + if (level <= 4) return 2; + if (level <= 8) return 3; + if (level <= 12) return 4; + if (level <= 16) return 5; + return 6; } export function calculateStats(character: Character): DerivedStats { - const profBonus = proficiencyBonus(character.level) + const characterLevel = character.classes.reduce( + (sum, cls) => sum + cls.level, + 0 + ); + const profBonus = proficiencyBonus(characterLevel); const abilityModifiers = Object.fromEntries( ALL_ABILITIES.map((name) => [ name, abilityModifier(character.abilities[name]), ]) - ) as DerivedStats['abilityModifiers'] + ) as DerivedStats['abilityModifiers']; const savingThrows = Object.fromEntries( ALL_ABILITIES.map((name) => { - const isProficient = character.savingThrowProficiencies.includes(name) - return [name, abilityModifiers[name] + (isProficient ? profBonus : 0)] + const isProficient = character.savingThrowProficiencies.includes(name); + return [name, abilityModifiers[name] + (isProficient ? profBonus : 0)]; }) - ) as DerivedStats['savingThrows'] + ) as DerivedStats['savingThrows']; const skills = Object.fromEntries( Object.entries(AbilitySkillGrouping).flatMap(([ability, skills]) => { return skills.map((skill) => { - const isProficient = character.skillProficiencies.includes(skill) - const hasExpertise = character.skillExpertise.includes(skill) + const isProficient = character.skillProficiencies.includes(skill); + const hasExpertise = character.skillExpertise.includes(skill); const bonus = abilityModifiers[ability as Ability] + (isProficient ? profBonus : 0) + - (hasExpertise ? profBonus : 0) - return [skill, bonus] as const - }) + (hasExpertise ? profBonus : 0); + return [skill, bonus] as const; + }); }) - ) as DerivedStats['skills'] + ) as DerivedStats['skills']; - const initiative = abilityModifiers[Ability.Dexterity] + const initiative = abilityModifiers[Ability.Dexterity]; - const passivePerception = 10 + skills[Skill.Perception] + const passivePerception = 10 + skills[Skill.Perception]; const spellcasting = character.spellcasting ? { @@ -57,7 +63,9 @@ export function calculateStats(character: Character): DerivedStats { spellAttackBonus: profBonus + abilityModifiers[character.spellcasting.ability], } - : {} + : {}; + + const hitDice = computeHitDice(character.classes); return { abilityModifiers, @@ -66,6 +74,26 @@ export function calculateStats(character: Character): DerivedStats { skills, initiative, passivePerception, + characterLevel, + hitDice, ...spellcasting, - } + }; } + +// function hitDice(charClasses: Character['classes']) { +// return charClasses.map(({ name, level }) => ({ +// count: level, +// dice: HIT_DICE_TABLE[name] +// })) +// } + +const computeHitDice = pipe( + map((config: Character['classes'][number]) => ({ + count: config.level, + dice: HIT_DICE_TABLE[config.name], + })), + groupBy(({ dice }) => dice), + mapObjIndexed(reduce((acc, { count }) => count + acc, 0)), + toPairs, + map(([dice, count]) => ({ dice, count })) +); diff --git a/packages/dnd-character-sheet/src/lib/character-class-constants.ts b/packages/dnd-character-sheet/src/lib/character-class-constants.ts new file mode 100644 index 00000000..41f57a06 --- /dev/null +++ b/packages/dnd-character-sheet/src/lib/character-class-constants.ts @@ -0,0 +1,17 @@ +import { CharacterClass } from './models/character-classes'; + +export const HIT_DICE_TABLE: Record = { + [CharacterClass.Artificer]: 'd8', + [CharacterClass.Barbarian]: 'd12', + [CharacterClass.Bard]: 'd8', + [CharacterClass.Cleric]: 'd8', + [CharacterClass.Druid]: 'd8', + [CharacterClass.Fighter]: 'd10', + [CharacterClass.Monk]: 'd8', + [CharacterClass.Paladin]: 'd10', + [CharacterClass.Ranger]: 'd10', + [CharacterClass.Rogue]: 'd8', + [CharacterClass.Sorcerer]: 'd6', + [CharacterClass.Warlock]: 'd8', + [CharacterClass.Wizard]: 'd6', +}; diff --git a/packages/dnd-character-sheet/src/lib/models/abilities.ts b/packages/dnd-character-sheet/src/lib/models/abilities.ts index cbea3dce..c418d710 100644 --- a/packages/dnd-character-sheet/src/lib/models/abilities.ts +++ b/packages/dnd-character-sheet/src/lib/models/abilities.ts @@ -5,9 +5,9 @@ export const Ability = { Intelligence: 'INT', Wisdom: 'WIS', Charisma: 'CHA', -} as const +} as const; -export type Ability = (typeof Ability)[keyof typeof Ability] +export type Ability = (typeof Ability)[keyof typeof Ability]; export const ALL_ABILITIES: Ability[] = [ Ability.Strength, @@ -16,7 +16,7 @@ export const ALL_ABILITIES: Ability[] = [ Ability.Intelligence, Ability.Wisdom, Ability.Charisma, -] +]; export const ABILITY_DETAILS: Record< Ability, @@ -28,4 +28,4 @@ export const ABILITY_DETAILS: Record< [Ability.Intelligence]: { label: 'Intelligence', shortName: 'Int.' }, [Ability.Wisdom]: { label: 'Wisdom', shortName: 'Wis.' }, [Ability.Charisma]: { label: 'Charisma', shortName: 'Cha.' }, -} +}; diff --git a/packages/dnd-character-sheet/src/lib/models/attacks.ts b/packages/dnd-character-sheet/src/lib/models/attacks.ts index 7618d0bb..8961034e 100644 --- a/packages/dnd-character-sheet/src/lib/models/attacks.ts +++ b/packages/dnd-character-sheet/src/lib/models/attacks.ts @@ -1,33 +1,33 @@ -import type { Ability } from './abilities' +import type { Ability } from './abilities'; export type Attack = { - name: string - damage: AttackDamage[] - masteryProperty?: string - attackBonusMod?: number - damageMod?: number - notProficient?: boolean - notes?: string -} & (WeaponAttack | SpellWithAttack | SpellWithSave) + name: string; + damage: AttackDamage[]; + masteryProperty?: string; + attackBonusMod?: number; + damageMod?: number; + notProficient?: boolean; + notes?: string; +} & (WeaponAttack | SpellWithAttack | SpellWithSave); export type SpellWithSave = { - kind: 'spell-with-save' - ability?: Ability - saveAbility: Ability -} + kind: 'spell-with-save'; + ability?: Ability; + saveAbility: Ability; +}; export type SpellWithAttack = { - kind: 'spell-with-attack' - ability?: Ability -} + kind: 'spell-with-attack'; + ability?: Ability; +}; export type WeaponAttack = { - kind: 'weapon' - ability: typeof Ability.Strength | typeof Ability.Dexterity -} + kind: 'weapon'; + ability: typeof Ability.Strength | typeof Ability.Dexterity; +}; export type AttackDamage = { - dice: string - type: string - disableModifier?: boolean -} + dice: string; + type: string; + disableModifier?: boolean; +}; diff --git a/packages/dnd-character-sheet/src/lib/models/character-classes.ts b/packages/dnd-character-sheet/src/lib/models/character-classes.ts new file mode 100644 index 00000000..00f9502b --- /dev/null +++ b/packages/dnd-character-sheet/src/lib/models/character-classes.ts @@ -0,0 +1,18 @@ +export const CharacterClass = { + Artificer: 'Artificer', + Barbarian: 'Barbarian', + Bard: 'Bard', + Cleric: 'Cleric', + Druid: 'Druid', + Fighter: 'Fighter', + Monk: 'Monk', + Paladin: 'Paladin', + Ranger: 'Ranger', + Rogue: 'Rogue', + Sorcerer: 'Sorcerer', + Warlock: 'Warlock', + Wizard: 'Wizard', +} as const; + +export type CharacterClass = + (typeof CharacterClass)[keyof typeof CharacterClass]; diff --git a/packages/dnd-character-sheet/src/lib/models/character.ts b/packages/dnd-character-sheet/src/lib/models/character.ts index 0a7fad95..934e5642 100644 --- a/packages/dnd-character-sheet/src/lib/models/character.ts +++ b/packages/dnd-character-sheet/src/lib/models/character.ts @@ -1,82 +1,66 @@ -import type { Ability } from './abilities' -import type { Attack } from './attacks' -import type { Feature } from './feature' -import type { Skill } from './skills' -import type { Spellcasting } from './spellcasting' +import type { Ability } from './abilities'; +import type { Attack } from './attacks'; +import type { CharacterClass } from './character-classes'; +import type { Feature } from './feature'; +import type { Skill } from './skills'; +import type { Spellcasting } from './spellcasting'; export interface Character { // Identity - name: string - class: string - subclass?: string - level: number - species: string - background: string - alignment?: string - experiencePoints: number + name: string; + species: string; + background: string; + + classes: { + name: CharacterClass; + subclass?: string; + level: number; + }[]; + + alignment?: string; + experiencePoints?: number; // Ability scores - abilities: Record + abilities: Record; // Proficiencies - savingThrowProficiencies: Ability[] - skillProficiencies: Skill[] - skillExpertise: Skill[] + savingThrowProficiencies: Ability[]; + skillProficiencies: Skill[]; + skillExpertise: Skill[]; // Combat - armorClass: number - speed: number + armorClass: number; + speed: number; hitPoints: { - maximum: number - current?: number - temporary?: number - } - - // Hit dice - hitDice: { - dieType: 'd6' | 'd8' | 'd10' | 'd12' - total: number - } + maximum: number; + current?: number; + temporary?: number; + }; // Attacks - attacks: Attack[] + attacks: Attack[]; // Equipment - equipment: string[] - currency: { - cp: number - sp: number - ep: number - gp: number - pp: number - } + equipment: string[]; // Features - features: Feature[] - speciesTraits?: Feature[] - feats?: Feature[] + features: Feature[]; + speciesTraits?: Feature[]; + feats?: Feature[]; // Size - size?: string + size?: string; // Proficiency text lists - armorProficiencies: string[] - weaponProficiencies: string[] - toolProficiencies: string[] - languages: string[] - - // Personality - personality: { - traits: string - ideals: string - bonds: string - flaws: string - } + armorProficiencies: string[]; + weaponProficiencies: string[]; + toolProficiencies: string[]; + languages: string[]; // Freeform - backstory?: string - notes?: string + backstory?: string; + appearance?: string; // Spellcasting - spellcasting?: Spellcasting + spellcasting?: Spellcasting; } diff --git a/packages/dnd-character-sheet/src/lib/models/derived-stats.ts b/packages/dnd-character-sheet/src/lib/models/derived-stats.ts index 6c3c79e8..79e1c27b 100644 --- a/packages/dnd-character-sheet/src/lib/models/derived-stats.ts +++ b/packages/dnd-character-sheet/src/lib/models/derived-stats.ts @@ -1,13 +1,15 @@ -import type { Ability } from './abilities' -import type { Skill } from './skills' +import type { Ability } from './abilities'; +import type { Skill } from './skills'; export interface DerivedStats { - abilityModifiers: Record - proficiencyBonus: number - savingThrows: Record - skills: Record - passivePerception: number - initiative: number - spellSaveDC?: number - spellAttackBonus?: number + abilityModifiers: Record; + proficiencyBonus: number; + savingThrows: Record; + skills: Record; + passivePerception: number; + initiative: number; + characterLevel: number; + hitDice: { dice: string; count: number }[]; + spellSaveDC?: number; + spellAttackBonus?: number; } diff --git a/packages/dnd-character-sheet/src/lib/models/feature.ts b/packages/dnd-character-sheet/src/lib/models/feature.ts index 2b0f0105..ca1da9af 100644 --- a/packages/dnd-character-sheet/src/lib/models/feature.ts +++ b/packages/dnd-character-sheet/src/lib/models/feature.ts @@ -1,5 +1,54 @@ +import type { Ability } from './abilities'; + export interface Feature { - name: string - source: string - description: string + name: string; + source?: string; + description: string; + + duration?: 'Action' | 'Bonus Action'; + + // Either it uses a resource + cost?: Cost; + + // or introduces a new resource + resource?: { + name: string; + count: ResourceCount; + refresh: Refresh; + }; } + +type ResourceCount = { + display?: 'dots' | 'numeric'; +} & ( + | { + kind: 'character-level'; + multiplier?: number; + } + | { + kind: 'class-level'; + class: string; + multiplier?: number; + } + | { + kind: 'ability'; + ability: Ability; + min?: number; + multiplier?: number; + } + | { + kind: 'fixed'; + value: number; + } +); + +type Cost = string; + +type Refresh = + | { + kind: 'short-rest' | 'long-rest' | 'any-rest'; + } + | { + kind: 'short-and-long-rest'; + numberOfRefreshesOnShortRest: number; + }; diff --git a/packages/dnd-character-sheet/src/lib/models/index.ts b/packages/dnd-character-sheet/src/lib/models/index.ts index 2ecb6d6c..1acfe9f1 100644 --- a/packages/dnd-character-sheet/src/lib/models/index.ts +++ b/packages/dnd-character-sheet/src/lib/models/index.ts @@ -1,4 +1,5 @@ -export { ABILITY_DETAILS, Ability, ALL_ABILITIES } from './abilities' -export type { Character } from './character' -export { AbilitySkillGrouping, Skill } from './skills' -export type { Spell } from './spellcasting' +export { ABILITY_DETAILS, Ability, ALL_ABILITIES } from './abilities'; +export type { Character } from './character'; +export type { CharacterClass } from './character-classes'; +export { AbilitySkillGrouping, Skill } from './skills'; +export type { Spell } from './spellcasting'; diff --git a/packages/dnd-character-sheet/src/lib/models/skills.ts b/packages/dnd-character-sheet/src/lib/models/skills.ts index eefcc142..2afc931d 100644 --- a/packages/dnd-character-sheet/src/lib/models/skills.ts +++ b/packages/dnd-character-sheet/src/lib/models/skills.ts @@ -1,4 +1,4 @@ -import { Ability } from './abilities' +import { Ability } from './abilities'; export const Skill = { Acrobatics: 'Acrobatics', @@ -19,9 +19,9 @@ export const Skill = { SleightOfHand: 'Sleight of Hand', Stealth: 'Stealth', Survival: 'Survival', -} as const +} as const; -export type Skill = (typeof Skill)[keyof typeof Skill] +export type Skill = (typeof Skill)[keyof typeof Skill]; export const AbilitySkillGrouping: Record = { [Ability.Strength]: [Skill.Athletics], @@ -47,4 +47,4 @@ export const AbilitySkillGrouping: Record = { Skill.Performance, Skill.Persuasion, ], -} +}; diff --git a/packages/dnd-character-sheet/src/lib/models/spellcasting.ts b/packages/dnd-character-sheet/src/lib/models/spellcasting.ts index d7662ba8..3bef6997 100644 --- a/packages/dnd-character-sheet/src/lib/models/spellcasting.ts +++ b/packages/dnd-character-sheet/src/lib/models/spellcasting.ts @@ -1,4 +1,4 @@ -import type { Ability } from './abilities' +import type { Ability } from './abilities'; type SpellSchool = | 'Abjuration' @@ -8,33 +8,35 @@ type SpellSchool = | 'Evocation' | 'Illusion' | 'Necromancy' - | 'Transmutation' + | 'Transmutation'; interface SpellComponents { - verbal?: boolean - somatic?: boolean - materialConsumed?: boolean + verbal?: boolean; + somatic?: boolean; + materialConsumed?: boolean; } export interface Spell { - name: string - level: number - castingTime: string - range: string - duration: string - school?: SpellSchool - components?: SpellComponents - concentration?: boolean - ritual?: boolean - freeUses?: number - alwaysPrepared?: boolean - alternativeAbility?: Ability - notes?: string + name: string; + level: number; + castingTime: string; + range: string; + duration: string; + school?: SpellSchool; + components?: SpellComponents; + concentration?: boolean; + ritual?: boolean; + freeUses?: number; + alwaysPrepared?: boolean; + alternativeAbility?: Ability; + notes?: string; } export interface Spellcasting { - ability: Ability - slots?: Partial> - pactMagic?: { level: number; slots: number } - spells: Spell[] + ability: Ability; + slots?: Partial>; + pactMagic?: { level: number; slots: number }; + spells: Spell[]; + numberOfCantrips?: number; + numberOfPreparedSpells?: number; } diff --git a/packages/dnd-character-sheet/src/lib/text-enrichment.ts b/packages/dnd-character-sheet/src/lib/text-enrichment.ts new file mode 100644 index 00000000..7fafd365 --- /dev/null +++ b/packages/dnd-character-sheet/src/lib/text-enrichment.ts @@ -0,0 +1,29 @@ +import Mustache from 'mustache'; +import type { Character } from './models'; +import type { DerivedStats } from './models/derived-stats'; +import type { Feature } from './models/feature'; + +export function enrichCharacterData( + character: Character, + stats: DerivedStats +): Character { + const dataValues = { + level: { + total: stats.characterLevel, + ...Object.fromEntries(character.classes.map((c) => [c.name, c.level])), + }, + }; + + const enricher = (str: string) => Mustache.render(str, dataValues); + const enrichFeature = (f: Feature): Feature => ({ + ...f, + description: enricher(f.description), + }); + + return { + ...character, + feats: character.feats?.map(enrichFeature), + features: character.features.map(enrichFeature), + speciesTraits: character.speciesTraits?.map(enrichFeature), + }; +} diff --git a/packages/dnd-character-sheet/src/lib/utils.ts b/packages/dnd-character-sheet/src/lib/utils.ts new file mode 100644 index 00000000..db37ea1c --- /dev/null +++ b/packages/dnd-character-sheet/src/lib/utils.ts @@ -0,0 +1,7 @@ +export function formatMod(mod: number): string { + return mod >= 0 ? `+${mod}` : `${mod}`; +} + +export function formatModIgnoreZero(n: number): string { + return n === 0 ? '' : n > 0 ? `+${n}` : `${n}`; +} diff --git a/packages/foundation-styles/postcss.config.js b/packages/foundation-styles/postcss.config.js index 60bf9096..8984cd25 100644 --- a/packages/foundation-styles/postcss.config.js +++ b/packages/foundation-styles/postcss.config.js @@ -8,4 +8,4 @@ export default { 'postcss-nesting': {}, '@tailwindcss/postcss': {}, }, -} +}; diff --git a/packages/reveal-framework/src/components/deck-styles.ts b/packages/reveal-framework/src/components/deck-styles.ts index 30d4962d..971be68d 100644 --- a/packages/reveal-framework/src/components/deck-styles.ts +++ b/packages/reveal-framework/src/components/deck-styles.ts @@ -1,14 +1,14 @@ -import styles from './deck.module.css' +import styles from './deck.module.css'; -export const slideMainGap = 'gap-5' -export const slideXMargin = 'px-6' -export const slideWithFooterBottomMargin = 'pb-6' -export const slideWithoutFooterBottomMargin = 'pb-9' -export const slideMainReset = `h-full ${slideXMargin} relative` -export const slideContentReset = 'h-full' +export const slideMainGap = 'gap-5'; +export const slideXMargin = 'px-6'; +export const slideWithFooterBottomMargin = 'pb-6'; +export const slideWithoutFooterBottomMargin = 'pb-9'; +export const slideMainReset = `h-full ${slideXMargin} relative`; +export const slideContentReset = 'h-full'; -export const slideCenterStyles = `flex flex-col justify-center items-center` +export const slideCenterStyles = `flex flex-col justify-center items-center`; -export const slideHeaderGrid = `grid place-items-center gap-6 large-desktop:gap-7 ${styles.slideHeaderGrid}` +export const slideHeaderGrid = `grid place-items-center gap-6 large-desktop:gap-7 ${styles.slideHeaderGrid}`; -export const middleDivider = styles.middleDivider +export const middleDivider = styles.middleDivider; diff --git a/packages/reveal-framework/src/components/slide-components.tsx b/packages/reveal-framework/src/components/slide-components.tsx index 9d4556a1..253b233d 100644 --- a/packages/reveal-framework/src/components/slide-components.tsx +++ b/packages/reveal-framework/src/components/slide-components.tsx @@ -1,9 +1,9 @@ -import { CalloutBadge } from '@ageorgedev/design-system/callouts/CalloutBadge' +import { CalloutBadge } from '@ageorgedev/design-system/callouts/CalloutBadge'; import { TiltCard, type TiltCardProps, -} from '@ageorgedev/design-system/cards/TiltCard' -import { cn } from '@ageorgedev/toolbelt/cn' +} from '@ageorgedev/design-system/cards/TiltCard'; +import { cn } from '@ageorgedev/toolbelt/cn'; import { BracketsAngleIcon, ExamIcon, @@ -12,19 +12,19 @@ import { MegaphoneIcon, PenNibIcon, WarningIcon, -} from '@phosphor-icons/react' +} from '@phosphor-icons/react'; type ImportantNoteProps = React.PropsWithChildren<{ - shape?: TiltCardProps['shape'] - className?: string - type?: NoteType -}> + shape?: TiltCardProps['shape']; + className?: string; + type?: NoteType; +}>; -type NoteType = 'tip' | 'danger' +type NoteType = 'tip' | 'danger'; export function ImportantNote(props: ImportantNoteProps) { - const noteType = props.type ?? 'tip' - const { iconClasses, mainClasses, Icon } = NoteConfigs[noteType] + const noteType = props.type ?? 'tip'; + const { iconClasses, mainClasses, Icon } = NoteConfigs[noteType]; return (
@@ -32,7 +32,7 @@ export function ImportantNote(props: ImportantNoteProps) {
{props.children}
- ) + ); } const NoteConfigs: Record< @@ -49,18 +49,18 @@ const NoteConfigs: Record< iconClasses: '', Icon: WarningIcon, }, -} +}; export function CalloutForVD() { - return + return ; } export function CalloutForAdvancedTopic() { - return + return ; } export function CalloutForPersonalOpinion() { - return + return ; } export function PointSeperator({ className }: { className?: string }) { @@ -71,5 +71,5 @@ export function PointSeperator({ className }: { className?: string }) { className )} /> - ) + ); } diff --git a/packages/reveal-framework/src/components/slide-layout-builders.spec.tsx b/packages/reveal-framework/src/components/slide-layout-builders.spec.tsx index f2ae4ec7..9cd5bb89 100644 --- a/packages/reveal-framework/src/components/slide-layout-builders.spec.tsx +++ b/packages/reveal-framework/src/components/slide-layout-builders.spec.tsx @@ -1,33 +1,33 @@ -import { render, screen } from '@testing-library/react' -import { describe, expect, it } from 'vitest' +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; import { ComparisonRow, DeckFooter, SlideMediaRow, -} from './slide-layout-builders' +} from './slide-layout-builders'; describe('SlideMediaRow', () => { it('renders children', () => { - render(slide content) - expect(screen.getByText('slide content')).toBeInTheDocument() - }) + render(slide content); + expect(screen.getByText('slide content')).toBeInTheDocument(); + }); it('merges extra className with base styles', () => { const { container } = render( content - ) - expect(container.firstChild).toHaveClass('extra-class') - }) -}) + ); + expect(container.firstChild).toHaveClass('extra-class'); + }); +}); describe('DeckFooter', () => { it('renders a footer element', () => { - render(footer text) - const footer = screen.getByRole('contentinfo') - expect(footer).toBeInTheDocument() - expect(footer).toHaveTextContent('footer text') - }) -}) + render(footer text); + const footer = screen.getByRole('contentinfo'); + expect(footer).toBeInTheDocument(); + expect(footer).toHaveTextContent('footer text'); + }); +}); describe('ComparisonRow', () => { it('renders left and right content', () => { @@ -36,15 +36,15 @@ describe('ComparisonRow', () => { left={Left side} right={Right side} /> - ) - expect(screen.getByText('Left side')).toBeInTheDocument() - expect(screen.getByText('Right side')).toBeInTheDocument() - }) + ); + expect(screen.getByText('Left side')).toBeInTheDocument(); + expect(screen.getByText('Right side')).toBeInTheDocument(); + }); it('merges extra className', () => { const { container } = render( - ) - expect(container.firstChild).toHaveClass('custom-row') - }) -}) + ); + expect(container.firstChild).toHaveClass('custom-row'); + }); +}); diff --git a/packages/reveal-framework/src/components/slide-layout-builders.tsx b/packages/reveal-framework/src/components/slide-layout-builders.tsx index 4d8c9539..58304c46 100644 --- a/packages/reveal-framework/src/components/slide-layout-builders.tsx +++ b/packages/reveal-framework/src/components/slide-layout-builders.tsx @@ -1,5 +1,5 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { middleDivider, slideMainGap } from './deck-styles' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { middleDivider, slideMainGap } from './deck-styles'; export function SlideMediaRow({ className, @@ -17,7 +17,7 @@ export function SlideMediaRow({ > {children}
- ) + ); } export function DeckFooter(props: React.PropsWithChildren) { @@ -25,7 +25,7 @@ export function DeckFooter(props: React.PropsWithChildren) {
{props.children}
- ) + ); } export function ComparisonRow({ @@ -34,8 +34,8 @@ export function ComparisonRow({ right, ...otherProps }: { - left: React.ReactNode - right: React.ReactNode + left: React.ReactNode; + right: React.ReactNode; } & Omit, 'children'>) { return (
{left}
{right}
- ) + ); } diff --git a/packages/reveal-framework/src/components/slide-types.tsx b/packages/reveal-framework/src/components/slide-types.tsx index 6eee8710..d2f58930 100644 --- a/packages/reveal-framework/src/components/slide-types.tsx +++ b/packages/reveal-framework/src/components/slide-types.tsx @@ -1,6 +1,6 @@ -import { cn } from '@ageorgedev/toolbelt/cn' -import { Slide } from '@revealjs/react' -import React from 'react' +import { cn } from '@ageorgedev/toolbelt/cn'; +import { Slide } from '@revealjs/react'; +import React from 'react'; import { slideCenterStyles, slideContentReset, @@ -8,18 +8,18 @@ import { slideMainGap, slideMainReset, slideWithoutFooterBottomMargin, -} from './deck-styles' +} from './deck-styles'; import { CalloutForAdvancedTopic, CalloutForPersonalOpinion, CalloutForVD, -} from './slide-components' +} from './slide-components'; -type CalloutType = 'UX' | 'Advanced' | 'Opinion' +type CalloutType = 'UX' | 'Advanced' | 'Opinion'; type SlideWithCallout = { - callout?: CalloutType | CalloutType[] -} + callout?: CalloutType | CalloutType[]; +}; export function SlideTypeCenter( props: React.PropsWithChildren @@ -38,7 +38,7 @@ export function SlideTypeCenter( {props.children}
- ) + ); } export function SlideTypeRegular( @@ -62,16 +62,16 @@ export function SlideTypeRegular(
- ) + ); } function TopLeftPosition({ callout, }: React.PropsWithChildren) { - const calloutTypes = Array.isArray(callout) ? callout : [callout] + const calloutTypes = Array.isArray(callout) ? callout : [callout]; const calloutComponents = calloutTypes.map((c) => c ? CalloutMap[c] : React.Fragment - ) + ); return (
{calloutComponents.map((Comp, i) => ( @@ -79,11 +79,11 @@ function TopLeftPosition({ ))}
- ) + ); } const CalloutMap = { UX: CalloutForVD, Advanced: CalloutForAdvancedTopic, Opinion: CalloutForPersonalOpinion, -} +}; diff --git a/packages/reveal-framework/src/globals.ts b/packages/reveal-framework/src/globals.ts index f0740205..d2227fb7 100644 --- a/packages/reveal-framework/src/globals.ts +++ b/packages/reveal-framework/src/globals.ts @@ -1,2 +1,2 @@ -import 'reveal.js/reveal.css' -import './styles/reveal-theme.css' +import 'reveal.js/reveal.css'; +import './styles/reveal-theme.css'; diff --git a/packages/reveal-framework/src/hooks/useRevealFramework.spec.ts b/packages/reveal-framework/src/hooks/useRevealFramework.spec.ts index 7a72e7e5..1bf3cb32 100644 --- a/packages/reveal-framework/src/hooks/useRevealFramework.spec.ts +++ b/packages/reveal-framework/src/hooks/useRevealFramework.spec.ts @@ -1,46 +1,46 @@ -import { renderHook, waitFor } from '@testing-library/react' -import type { RefObject } from 'react' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { renderHook, waitFor } from '@testing-library/react'; +import type { RefObject } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -const mockInitialize = vi.fn().mockResolvedValue(undefined) +const mockInitialize = vi.fn().mockResolvedValue(undefined); vi.mock('reveal.js', () => ({ // Regular function (not arrow) so it can be called with `new` // biome-ignore lint/suspicious/noExplicitAny: constructor mock requires any default: vi.fn(function (this: any) { - this.initialize = mockInitialize + this.initialize = mockInitialize; }), -})) +})); // Import after mock is hoisted -const { useRevealFramework } = await import('./useRevealFramework') +const { useRevealFramework } = await import('./useRevealFramework'); -import Reveal from 'reveal.js' +import Reveal from 'reveal.js'; describe('useRevealFramework', () => { beforeEach(() => { - vi.mocked(Reveal as unknown as () => void).mockClear() - mockInitialize.mockClear() - }) + vi.mocked(Reveal as unknown as () => void).mockClear(); + mockInitialize.mockClear(); + }); it('initialises a Reveal deck when the ref element is available', async () => { - const div = document.createElement('div') - const ref = { current: div } as RefObject + const div = document.createElement('div'); + const ref = { current: div } as RefObject; - renderHook(() => useRevealFramework(ref)) + renderHook(() => useRevealFramework(ref)); await waitFor(() => { - expect(Reveal).toHaveBeenCalledWith(div, expect.any(Object)) - }) - expect(mockInitialize).toHaveBeenCalled() - }) + expect(Reveal).toHaveBeenCalledWith(div, expect.any(Object)); + }); + expect(mockInitialize).toHaveBeenCalled(); + }); it('does nothing when the ref has no element', async () => { - const ref = { current: null } as unknown as RefObject + const ref = { current: null } as unknown as RefObject; - renderHook(() => useRevealFramework(ref)) + renderHook(() => useRevealFramework(ref)); - await new Promise((r) => setTimeout(r, 50)) - expect(Reveal).not.toHaveBeenCalled() - }) -}) + await new Promise((r) => setTimeout(r, 50)); + expect(Reveal).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/reveal-framework/src/hooks/useRevealFramework.ts b/packages/reveal-framework/src/hooks/useRevealFramework.ts index e83c651a..08bbf568 100644 --- a/packages/reveal-framework/src/hooks/useRevealFramework.ts +++ b/packages/reveal-framework/src/hooks/useRevealFramework.ts @@ -1,29 +1,29 @@ -import { type RefObject, useEffect, useRef } from 'react' -import type { RevealApi } from 'reveal.js' +import { type RefObject, useEffect, useRef } from 'react'; +import type { RevealApi } from 'reveal.js'; export function useRevealFramework(elementRef: RefObject) { - const revealInstance = useRef(null) + const revealInstance = useRef(null); // biome-ignore lint/correctness/useExhaustiveDependencies: thats how it works useEffect(() => { - ;(async () => { - if (!elementRef.current) return + (async () => { + if (!elementRef.current) return; - const Reveal = (await import('reveal.js')).default + const Reveal = (await import('reveal.js')).default; const deck = new Reveal(elementRef.current, { embedded: true, keyboardCondition: 'focused', controlsTutorial: false, disableLayout: true, transition: 'fade', - }) + }); deck.initialize().then((_) => { // Hack for dev // deck.slide(6, 1); - revealInstance.current = deck - }) - })() - }, []) + revealInstance.current = deck; + }); + })(); + }, []); - return [revealInstance] as const + return [revealInstance] as const; } diff --git a/packages/reveal-framework/src/index.ts b/packages/reveal-framework/src/index.ts index bc9ab171..fda56c57 100644 --- a/packages/reveal-framework/src/index.ts +++ b/packages/reveal-framework/src/index.ts @@ -1,8 +1,8 @@ -export * from './components/slide-components' -export * from './components/slide-layout-builders' -export * from './components/slide-types' -export * from './hooks/useRevealFramework' +export * from './components/slide-components'; +export * from './components/slide-layout-builders'; +export * from './components/slide-types'; +export * from './hooks/useRevealFramework'; -import * as Presentation from '@revealjs/react' +import * as Presentation from '@revealjs/react'; -export { Presentation } +export { Presentation }; diff --git a/packages/reveal-framework/vitest.config.ts b/packages/reveal-framework/vitest.config.ts index 9b6bd972..c641e500 100644 --- a/packages/reveal-framework/vitest.config.ts +++ b/packages/reveal-framework/vitest.config.ts @@ -1,5 +1,5 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [react()], @@ -8,4 +8,4 @@ export default defineConfig({ setupFiles: ['@ageorgedev/testing-config/react-jsdom-test-setup.ts'], include: ['src/**/*.spec.{ts,tsx}'], }, -}) +}); diff --git a/packages/talk-tailwind/src/diagrams/D01TailwindFilesize.tsx b/packages/talk-tailwind/src/diagrams/D01TailwindFilesize.tsx index 30fd967a..6d067c0f 100644 --- a/packages/talk-tailwind/src/diagrams/D01TailwindFilesize.tsx +++ b/packages/talk-tailwind/src/diagrams/D01TailwindFilesize.tsx @@ -1,4 +1,4 @@ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; export function D01TailwindFilesize({ className, @@ -103,5 +103,5 @@ export function D01TailwindFilesize({ - ) + ); } diff --git a/packages/talk-tailwind/src/diagrams/D02CssPipeline.tsx b/packages/talk-tailwind/src/diagrams/D02CssPipeline.tsx index 1c76b1d5..d39851cf 100644 --- a/packages/talk-tailwind/src/diagrams/D02CssPipeline.tsx +++ b/packages/talk-tailwind/src/diagrams/D02CssPipeline.tsx @@ -1,4 +1,4 @@ -import { cn } from '@ageorgedev/toolbelt/cn' +import { cn } from '@ageorgedev/toolbelt/cn'; export function D02CssPipeline({ className, @@ -186,5 +186,5 @@ export function D02CssPipeline({ - ) + ); } diff --git a/packages/talk-tailwind/src/index.tsx b/packages/talk-tailwind/src/index.tsx index 4ebc4a8d..79122961 100644 --- a/packages/talk-tailwind/src/index.tsx +++ b/packages/talk-tailwind/src/index.tsx @@ -1,18 +1,18 @@ -import { TwLogo } from '@ageorgedev/brand-components' -import { BodyXl } from '@ageorgedev/design-system/typography/typography-components' -import { DeckFooter, Presentation } from '@ageorgedev/reveal-framework' -import { IconContext } from '@phosphor-icons/react' -import { S01Intro } from './slides/s01-intro' -import { S02Fundamentals } from './slides/s02-fundamentals' -import { S03AntiPatterns } from './slides/s03-anti-patterns' -import { S04LeanConfig } from './slides/s04-lean-config' -import { S04BCognitiveLoad } from './slides/s04b-cognitive-load' -import { S05ClassManagement } from './slides/s05-class-mgt' -import { S06Plugins } from './slides/s06-plugins' -import { S07OverrideTheming } from './slides/s07-override-theming' -import { S08DarkMode } from './slides/s08-dark-mode' -import { S09CssInJs } from './slides/s09-css-in-js' -import { S10Outro } from './slides/s10-outro' +import { TwLogo } from '@ageorgedev/brand-components'; +import { BodyXl } from '@ageorgedev/design-system/typography/typography-components'; +import { DeckFooter, Presentation } from '@ageorgedev/reveal-framework'; +import { IconContext } from '@phosphor-icons/react'; +import { S01Intro } from './slides/s01-intro'; +import { S02Fundamentals } from './slides/s02-fundamentals'; +import { S03AntiPatterns } from './slides/s03-anti-patterns'; +import { S04LeanConfig } from './slides/s04-lean-config'; +import { S04BCognitiveLoad } from './slides/s04b-cognitive-load'; +import { S05ClassManagement } from './slides/s05-class-mgt'; +import { S06Plugins } from './slides/s06-plugins'; +import { S07OverrideTheming } from './slides/s07-override-theming'; +import { S08DarkMode } from './slides/s08-dark-mode'; +import { S09CssInJs } from './slides/s09-css-in-js'; +import { S10Outro } from './slides/s10-outro'; export function TalkTailwind() { return ( @@ -42,7 +42,7 @@ export function TalkTailwind() {
- ) + ); } const presentationConfig: Presentation.DeckProps['config'] = { @@ -51,6 +51,6 @@ const presentationConfig: Presentation.DeckProps['config'] = { controlsTutorial: false, disableLayout: true, transition: 'fade', -} +}; -export default TalkTailwind +export default TalkTailwind; diff --git a/packages/talk-tailwind/src/slides/s01-intro.tsx b/packages/talk-tailwind/src/slides/s01-intro.tsx index 2c104906..94b8c29f 100644 --- a/packages/talk-tailwind/src/slides/s01-intro.tsx +++ b/packages/talk-tailwind/src/slides/s01-intro.tsx @@ -1,4 +1,4 @@ -import { NameLogo } from '@ageorgedev/design-system/logo/NameLogo' +import { NameLogo } from '@ageorgedev/design-system/logo/NameLogo'; import { Heading1, Heading2, @@ -7,7 +7,7 @@ import { PBody, PBodyLg, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { CalloutForAdvancedTopic, CalloutForPersonalOpinion, @@ -16,7 +16,7 @@ import { Presentation, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' +} from '@ageorgedev/reveal-framework'; export function S01Intro() { return ( @@ -124,5 +124,5 @@ export function S01Intro() { CSS-in-JS - ) + ); } diff --git a/packages/talk-tailwind/src/slides/s02-fundamentals.tsx b/packages/talk-tailwind/src/slides/s02-fundamentals.tsx index d5934474..e4197e77 100644 --- a/packages/talk-tailwind/src/slides/s02-fundamentals.tsx +++ b/packages/talk-tailwind/src/slides/s02-fundamentals.tsx @@ -1,10 +1,10 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading3, PBody, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { ComparisonRow, ImportantNote, @@ -12,8 +12,8 @@ import { SlideMediaRow, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' -import { D01TailwindFilesize } from '../diagrams/D01TailwindFilesize' +} from '@ageorgedev/reveal-framework'; +import { D01TailwindFilesize } from '../diagrams/D01TailwindFilesize'; export function S02Fundamentals() { return ( @@ -172,7 +172,7 @@ export function S02Fundamentals() { - ) + ); } // Code snippets @@ -180,12 +180,12 @@ export function S02Fundamentals() { const simpleButtonTraditional = ` -` +`; const simpleButtonTailwind = `` +`; const simpleButtonComputedStyles = `.text-white { color: white; } @@ -206,7 +206,7 @@ const simpleButtonComputedStyles = `.text-white { color: white; } .mb-2 { margin-bottom: 0.5rem; -}` +}`; const tailwindConfigFile = `// tailwind.config.js @@ -229,7 +229,7 @@ export default { massive: "70px", }, }, -}` +}`; const configDerivedHtml = ` @@ -237,18 +237,18 @@ const configDerivedHtml = ` -` +`; const exampleHtml = `

Hello world!

-` +`; const sourceStyleFile = `/* styles.css */ @tailwind base; @tailwind components; -@tailwind utilities;` +@tailwind utilities;`; const computedStyleFile = `/* dist/styles.css */ @@ -262,4 +262,4 @@ const computedStyleFile = `/* dist/styles.css */ .text-purple { --tw-text-opacity: 1; color: rgb(63 60 187 / var(--tw-text-opacity)); -}` +}`; diff --git a/packages/talk-tailwind/src/slides/s03-anti-patterns.tsx b/packages/talk-tailwind/src/slides/s03-anti-patterns.tsx index 18d8e4c1..f052c94b 100644 --- a/packages/talk-tailwind/src/slides/s03-anti-patterns.tsx +++ b/packages/talk-tailwind/src/slides/s03-anti-patterns.tsx @@ -1,16 +1,16 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, PBody, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { PointSeperator, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' -import { ArrowRightIcon } from '@phosphor-icons/react' +} from '@ageorgedev/reveal-framework'; +import { ArrowRightIcon } from '@phosphor-icons/react'; export function S03AntiPatterns() { return ( @@ -90,7 +90,7 @@ export function S03AntiPatterns() { > - ) + ); } // Code blocks @@ -104,7 +104,7 @@ const applyAntiPatternSourceStyles = `/* style.css */ .my-btn:hover { @apply bg-blue-900; -}` +}`; const applyAntiPatternSourceComputedStyles = `/* dist/style.css */ @@ -125,21 +125,21 @@ const applyAntiPatternSourceComputedStyles = `/* dist/style.css */ .my-btn:hover { --tw-bg-opacity: 1; background-color: rgb(30 58 138 / var(--tw-bg-opacity)); -}` +}`; const applyAntiPatternHtml = ` -` +`; const oneOffHtml = `
Search -
` +
`; const oneOffComputedCss = `.mt-\\[3px\\] { margin-top: 3px; -}` +}`; const oneOffBadHtml = `
Oh my god, my eyes!! -
` +
`; diff --git a/packages/talk-tailwind/src/slides/s04-lean-config.tsx b/packages/talk-tailwind/src/slides/s04-lean-config.tsx index 64273e0d..16bb46be 100644 --- a/packages/talk-tailwind/src/slides/s04-lean-config.tsx +++ b/packages/talk-tailwind/src/slides/s04-lean-config.tsx @@ -1,16 +1,16 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, Heading3, Interface, PBody, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { ComparisonRow, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' +} from '@ageorgedev/reveal-framework'; export function S04LeanConfig() { return ( @@ -199,7 +199,7 @@ export function S04LeanConfig() { > - ) + ); } // Code blocks @@ -216,7 +216,7 @@ export default { }, }, }, -};` +};`; const overridingConfig = `/* tailwind.config.js */ @@ -233,7 +233,7 @@ export default { // ... non-specified sets picked up // from tailwind defaults }, -};` +};`; const removingCorePlugins = `/* tailwind.config.js */ @@ -244,7 +244,7 @@ export default { zIndex: false, // we don't mess with z-index animation: false // will be custom anyway }, -};` +};`; function RSpaceBar({ className, name }: { className?: string; name: string }) { return ( @@ -252,7 +252,7 @@ function RSpaceBar({ className, name }: { className?: string; name: string }) { {name} - ) + ); } function LSpaceBar({ className, name }: { className?: string; name: string }) { @@ -261,9 +261,9 @@ function LSpaceBar({ className, name }: { className?: string; name: string }) { {name} - ) + ); } function Bar({ className }: { className?: string }) { - return
+ return
; } diff --git a/packages/talk-tailwind/src/slides/s04b-cognitive-load.tsx b/packages/talk-tailwind/src/slides/s04b-cognitive-load.tsx index e39245b0..981115d0 100644 --- a/packages/talk-tailwind/src/slides/s04b-cognitive-load.tsx +++ b/packages/talk-tailwind/src/slides/s04b-cognitive-load.tsx @@ -1,4 +1,4 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, @@ -6,9 +6,12 @@ import { PBody, PBodyLg, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' -import { SlideTypeCenter, SlideTypeRegular } from '@ageorgedev/reveal-framework' -import { ArrowRightIcon } from '@phosphor-icons/react' +} from '@ageorgedev/design-system/typography/typography-components'; +import { + SlideTypeCenter, + SlideTypeRegular, +} from '@ageorgedev/reveal-framework'; +import { ArrowRightIcon } from '@phosphor-icons/react'; export function S04BCognitiveLoad() { return ( @@ -69,7 +72,7 @@ export function S04BCognitiveLoad() { Over time, devs adapt to it, but the cost is present - ) + ); } const imaginaryCSS = `.imaginary-selector { @@ -82,9 +85,9 @@ const imaginaryCSS = `.imaginary-selector { display: flex; justify-content: center; -}` +}`; const imaginaryHTML = `
-
` +
`; diff --git a/packages/talk-tailwind/src/slides/s05-class-mgt.tsx b/packages/talk-tailwind/src/slides/s05-class-mgt.tsx index dc1bff95..3e05dbd5 100644 --- a/packages/talk-tailwind/src/slides/s05-class-mgt.tsx +++ b/packages/talk-tailwind/src/slides/s05-class-mgt.tsx @@ -1,16 +1,16 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, Heading3, Interface2Xl, PBody, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { ComparisonRow, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' +} from '@ageorgedev/reveal-framework'; export function S05ClassManagement() { return ( @@ -90,7 +90,7 @@ export function S05ClassManagement() {
- ) + ); } // Code blocks @@ -104,7 +104,7 @@ const MoreStyles = cls("shadow-sm px-2", otherStyles); const Variants = { primary: 'bg-primary-300 hover:bg-primary-400', secondary: 'bg-secondary-200 hover:text-white' -}` +}`; const reactComponent = `export function Card({ className, ...otherProps @@ -117,16 +117,16 @@ const reactComponent = `export function Card({ \${className ?? ''}\`} /> ); -}` +}`; const angularComponent = `@Directive({ selector: '[appCard]', }) export class CardDirective { @HostBinding('class') classes = 'shadow border-gray border-2 mb-4'; -}` +}`; -const angularComponentExample = `
Notes
` +const angularComponentExample = `
Notes
`; const advancedComponent = `type Heading1Props = React.PropsWithChildren & React.HTMLAttributes; @@ -143,7 +143,7 @@ function Heading1({ ); } -Heading1.classes = 'text-2xl font-bold md:text-3xl';` +Heading1.classes = 'text-2xl font-bold md:text-3xl';`; const advancedComponentUsage = `Hello There @@ -152,7 +152,7 @@ const advancedComponentUsage = `Hello There

A section with common styling

Pretty nifty

-` +`; const fullTailwindWithoutComponents = `
User Avatar @@ -175,7 +175,7 @@ const fullTailwindWithoutComponents = `
@@ -197,4 +197,4 @@ const fixedTailwindWithComponents = ` Photographer
-` +`; diff --git a/packages/talk-tailwind/src/slides/s06-plugins.tsx b/packages/talk-tailwind/src/slides/s06-plugins.tsx index 63f5479c..6bbabee9 100644 --- a/packages/talk-tailwind/src/slides/s06-plugins.tsx +++ b/packages/talk-tailwind/src/slides/s06-plugins.tsx @@ -1,17 +1,17 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, Heading3, PBody, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { ComparisonRow, ImportantNote, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' +} from '@ageorgedev/reveal-framework'; export function S06Plugins() { return ( @@ -161,7 +161,7 @@ export function S06Plugins() { > - ) + ); } const basicPluginInstallation = `/* tailwind.config.js */ @@ -174,7 +174,7 @@ module.exports = { // Add your custom styles here }), ] -}` +}`; const typographyPlugin = `plugin(({ addBase, theme }) => { addBase({ @@ -195,11 +195,11 @@ const typographyPlugin = `plugin(({ addBase, theme }) => { textTransform: 'uppercase', } }) -})` +})`; const typographyUsage = ` Some content -` +`; const ngInvalidPlugin = `plugin(({ addVariant }) => { // Overwrite the default invalid:* modifier to support angular classes @@ -209,9 +209,9 @@ const ngInvalidPlugin = `plugin(({ addVariant }) => { addVariant('group-invalid', [':merge(.group).ng-invalid.ng-touched &', ':merge(.group):invalid &']); addVariant('peer-invalid', [':merge(.peer).ng-invalid.ng-touched ~ &', ':merge(.peer):invalid ~ &']); }); -` +`; -const ngInvalidPluginUsage = `` +const ngInvalidPluginUsage = ``; const layerStylesCss = `@tailwind base; @tailwind components; @@ -235,14 +235,14 @@ const layerStylesCss = `@tailwind base; .area-header { grid-area: header; } .area-main { grid-area: main; } -}` +}`; const layerStylesHtml = `
header
main
footer
-
` +`; const matchComponentsPlugin = `const gridPlugin = plugin(({ matchComponents, theme }) => { matchComponents( @@ -256,7 +256,7 @@ const matchComponentsPlugin = `const gridPlugin = plugin(({ matchComponents, the }, { values: theme("layouts") } ); -});` +});`; const matchComponentsLayout = `plugins: [gridPlugin], theme: { @@ -277,4 +277,4 @@ theme: { ], }, }, -},` +},`; diff --git a/packages/talk-tailwind/src/slides/s07-override-theming.tsx b/packages/talk-tailwind/src/slides/s07-override-theming.tsx index cd2a7d0a..2b447238 100644 --- a/packages/talk-tailwind/src/slides/s07-override-theming.tsx +++ b/packages/talk-tailwind/src/slides/s07-override-theming.tsx @@ -1,17 +1,17 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, Heading3, PBody, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { ComparisonRow, ImportantNote, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' +} from '@ageorgedev/reveal-framework'; export function S07OverrideTheming() { return ( @@ -132,7 +132,7 @@ export function S07OverrideTheming() { > - ) + ); } const plainColorsConfig = `/* tailwind.config.js */ @@ -152,7 +152,7 @@ export default { } } }, -};` +};`; const colorVarsRoot = `/* styles.css */ @layer base { @@ -161,7 +161,7 @@ const colorVarsRoot = `/* styles.css */ --color-primary-base: hsl(321 12% 29%); --color-primary-dark: hsl(119 22% 44%); } -}` +}`; const colorVarsUsage = `/* tailwind.config.js */ @@ -180,23 +180,23 @@ export default { } } }, -};` +};`; const colorVarsWithDefaults = `primary: { DEFAULT: "var(--color-primary-base, hsl(321 12% 29%))", light: "var(--color-primary-light, hsl(321 12% 48%))", dark: "var(--color-primary-dark, hsl(119 22% 44%))", -}` +}`; -const tailwindColorOpacityModifiers = `
...
` +const tailwindColorOpacityModifiers = `
...
`; const colorVarsRootChannels = `:root { --color-primary-base: 207 49% 65%; -}` +}`; const colorVarsWithDefaultsChannels = `primary: { DEFAULT: "hsl( var(--color-primary-base) / )", -}` +}`; const modernHslSyntax = `.selector { /* hsl( ) */ @@ -204,4 +204,4 @@ const modernHslSyntax = `.selector { /* hsl( / ) */ background: hsl(205 30% 90% / .4) -}` +}`; diff --git a/packages/talk-tailwind/src/slides/s08-dark-mode.tsx b/packages/talk-tailwind/src/slides/s08-dark-mode.tsx index cea4e950..2fafd679 100644 --- a/packages/talk-tailwind/src/slides/s08-dark-mode.tsx +++ b/packages/talk-tailwind/src/slides/s08-dark-mode.tsx @@ -1,4 +1,4 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, @@ -7,10 +7,13 @@ import { InterfaceXl, PBodyLg, PBodyXs, -} from '@ageorgedev/design-system/typography/typography-components' -import { SlideTypeCenter, SlideTypeRegular } from '@ageorgedev/reveal-framework' -import { cn } from '@ageorgedev/toolbelt/cn' -import { WarningIcon } from '@phosphor-icons/react' +} from '@ageorgedev/design-system/typography/typography-components'; +import { + SlideTypeCenter, + SlideTypeRegular, +} from '@ageorgedev/reveal-framework'; +import { cn } from '@ageorgedev/toolbelt/cn'; +import { WarningIcon } from '@phosphor-icons/react'; export function S08DarkMode() { return ( @@ -180,13 +183,13 @@ export function S08DarkMode() { - ) + ); } function Swatch({ className }: { className?: string }) { return (
- ) + ); } // Code blocks @@ -194,15 +197,15 @@ const darkModeConfig = `/* tailwind.config.js */ export default { darkMode: 'class' // ... rest of the config -}` +}`; const darkModeHtml = ` -` +`; const darkModeBasicUsage = `` +`; const rawColorsRoot = `:root { --rc-parchment-500: hsl(39 100% 89%); @@ -213,21 +216,21 @@ const rawColorsRoot = `:root { --rc-d-neutral-500: hsl(189 67% 6%); --rc-l-neutral-500: hsl(45 100% 99%); -}` +}`; const contextualColorsRootLight = `:root { --page-far: var(--rc-parchment-500); --accent: var(--rc-p-accent-500); --neutral: var(--rc-d-neutral-500); --neutral-inverse: var(--rc-l-neutral-500); -}` +}`; const contextualColorsRootDark = `:root.dark { --page-far: var(--rc-timber-300); --accent: var(--rc-p-accent-300); --neutral: var(--rc-l-neutral-500); --neutral-inverse: var(--rc-d-neutral-500); -}` +}`; const contextualColorsTailwindConfig = `/* tailwind.config.js */ @@ -240,13 +243,13 @@ export default { //... others } } -}` +}`; const contextualColorsUsage = `
-
` +`; const contextualColorsProblem = `
-
` +
`; diff --git a/packages/talk-tailwind/src/slides/s09-css-in-js.tsx b/packages/talk-tailwind/src/slides/s09-css-in-js.tsx index 2206a3cb..98caf4d3 100644 --- a/packages/talk-tailwind/src/slides/s09-css-in-js.tsx +++ b/packages/talk-tailwind/src/slides/s09-css-in-js.tsx @@ -1,19 +1,19 @@ -import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock' +import { CodeBlock } from '@ageorgedev/design-system/CodeBlock/CodeBlock'; import { Heading1, Heading2, Heading3, PBody, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { ImportantNote, PointSeperator, SlideMediaRow, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' -import { D02CssPipeline } from '../diagrams/D02CssPipeline' +} from '@ageorgedev/reveal-framework'; +import { D02CssPipeline } from '../diagrams/D02CssPipeline'; export function S09CssInJs() { return ( @@ -101,7 +101,7 @@ export function S09CssInJs() { - ) + ); } // Code blocks @@ -115,4 +115,4 @@ fullConfig.theme.width[4] // => '1rem' fullConfig.theme.screens.md -// => '768px'` +// => '768px'`; diff --git a/packages/talk-tailwind/src/slides/s10-outro.tsx b/packages/talk-tailwind/src/slides/s10-outro.tsx index 96a1ec78..b2de7fd6 100644 --- a/packages/talk-tailwind/src/slides/s10-outro.tsx +++ b/packages/talk-tailwind/src/slides/s10-outro.tsx @@ -5,12 +5,12 @@ import { PBody, PBodyLg, PBodyMd, -} from '@ageorgedev/design-system/typography/typography-components' +} from '@ageorgedev/design-system/typography/typography-components'; import { PointSeperator, SlideTypeCenter, SlideTypeRegular, -} from '@ageorgedev/reveal-framework' +} from '@ageorgedev/reveal-framework'; export function S10Outro() { return ( @@ -58,5 +58,5 @@ export function S10Outro() { - ) + ); } diff --git a/packages/testing-config/react-jsdom-test-setup.ts b/packages/testing-config/react-jsdom-test-setup.ts index db5e6424..7a5898c3 100644 --- a/packages/testing-config/react-jsdom-test-setup.ts +++ b/packages/testing-config/react-jsdom-test-setup.ts @@ -1,7 +1,7 @@ // import '@testing-library/jest-dom/vitest'; -import * as matchers from '@testing-library/jest-dom/matchers' -import { cleanup } from '@testing-library/react' -import { afterEach, expect } from 'vitest' +import * as matchers from '@testing-library/jest-dom/matchers'; +import { cleanup } from '@testing-library/react'; +import { afterEach, expect } from 'vitest'; -expect.extend(matchers) -afterEach(cleanup) +expect.extend(matchers); +afterEach(cleanup); diff --git a/packages/toolbelt/package.json b/packages/toolbelt/package.json index 9aa648fa..a5787624 100644 --- a/packages/toolbelt/package.json +++ b/packages/toolbelt/package.json @@ -18,7 +18,7 @@ }, "dependencies": { "clsx": "2.1.1", - "ramda": "0.28.0", + "ramda": "0.32.0", "tailwind-merge": "3.3.1" }, "devDependencies": { diff --git a/packages/toolbelt/src/cn.spec.ts b/packages/toolbelt/src/cn.spec.ts index 14e148ef..a8b4aa0f 100644 --- a/packages/toolbelt/src/cn.spec.ts +++ b/packages/toolbelt/src/cn.spec.ts @@ -1,36 +1,36 @@ -import { describe, expect, it } from 'vitest' -import { cn } from './cn' +import { describe, expect, it } from 'vitest'; +import { cn } from './cn'; describe('cn', () => { it('merges multiple class strings', () => { - expect(cn('foo', 'bar')).toBe('foo bar') - }) + expect(cn('foo', 'bar')).toBe('foo bar'); + }); it('handles a single class', () => { - expect(cn('only')).toBe('only') - }) + expect(cn('only')).toBe('only'); + }); it('omits false conditions', () => { - expect(cn('base', false && 'conditional')).toBe('base') - }) + expect(cn('base', false && 'conditional')).toBe('base'); + }); it('includes truthy conditions', () => { - expect(cn('base', true && 'active')).toBe('base active') - }) + expect(cn('base', true && 'active')).toBe('base active'); + }); it('resolves Tailwind padding conflicts — last wins', () => { - expect(cn('p-4', 'p-8')).toBe('p-8') - }) + expect(cn('p-4', 'p-8')).toBe('p-8'); + }); it('resolves Tailwind text colour conflicts — last wins', () => { - expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500') - }) + expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500'); + }); it('accepts object syntax', () => { - expect(cn({ active: true, disabled: false })).toBe('active') - }) + expect(cn({ active: true, disabled: false })).toBe('active'); + }); it('accepts array syntax', () => { - expect(cn(['foo', 'bar'])).toBe('foo bar') - }) -}) + expect(cn(['foo', 'bar'])).toBe('foo bar'); + }); +}); diff --git a/packages/toolbelt/src/cn.ts b/packages/toolbelt/src/cn.ts index d32b0fe6..9ad0df42 100644 --- a/packages/toolbelt/src/cn.ts +++ b/packages/toolbelt/src/cn.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/packages/toolbelt/src/ramda-additions.spec.ts b/packages/toolbelt/src/ramda-additions.spec.ts index c24f0f91..0a9d389e 100644 --- a/packages/toolbelt/src/ramda-additions.spec.ts +++ b/packages/toolbelt/src/ramda-additions.spec.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest' -import { mapKeys } from './ramda-additions' +import { describe, expect, it } from 'vitest'; +import { mapKeys } from './ramda-additions'; describe('mapKeys', () => { it('works directly', () => { @@ -8,11 +8,11 @@ describe('mapKeys', () => { name__test: 'test', age__test: 10, } - ) - }) + ); + }); it('works in curried format', () => { - const pref = mapKeys((k) => `test--${k}`) + const pref = mapKeys((k) => `test--${k}`); expect( pref({ a: { @@ -25,6 +25,6 @@ describe('mapKeys', () => { name: 'test', }, 'test--b': 'something', - }) - }) -}) + }); + }); +}); diff --git a/packages/toolbelt/src/ramda-additions.ts b/packages/toolbelt/src/ramda-additions.ts index e9deeac0..f047aeec 100644 --- a/packages/toolbelt/src/ramda-additions.ts +++ b/packages/toolbelt/src/ramda-additions.ts @@ -1,6 +1,6 @@ -import { compose, curryN, fromPairs, map, toPairs } from 'ramda' +import { compose, curryN, fromPairs, map, toPairs } from 'ramda'; -type KeyT = string +type KeyT = string; function _mapKeys( mapper: (key: KeyT) => string, @@ -10,6 +10,6 @@ function _mapKeys( fromPairs, map(([key, value]: [KeyT, S]) => [mapper(key), value] as const), toPairs - )(obj) + )(obj); } -export const mapKeys = curryN(2, _mapKeys) +export const mapKeys = curryN(2, _mapKeys); diff --git a/packages/toolbelt/vitest.config.ts b/packages/toolbelt/vitest.config.ts index daf21ceb..aacc4e15 100644 --- a/packages/toolbelt/vitest.config.ts +++ b/packages/toolbelt/vitest.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', include: ['src/**/*.spec.ts'], }, -}) +}); diff --git a/packages/ts-config/css-modules.d.ts b/packages/ts-config/css-modules.d.ts index f5e4b38d..5a6040ca 100644 --- a/packages/ts-config/css-modules.d.ts +++ b/packages/ts-config/css-modules.d.ts @@ -1,13 +1,13 @@ declare module '*.module.css' { - const classes: { [key: string]: string } - export default classes + const classes: { [key: string]: string }; + export default classes; } declare module '*.css' { - const value: string - export default value + const value: string; + export default value; } declare module '*.css?url' { - const value: string - export default value + const value: string; + export default value; } diff --git a/packages/ts-config/react-globals.d.ts b/packages/ts-config/react-globals.d.ts index 10ad759a..38b48d57 100644 --- a/packages/ts-config/react-globals.d.ts +++ b/packages/ts-config/react-globals.d.ts @@ -1,14 +1,14 @@ // biome-ignore-all lint/suspicious/noExplicitAny: Easiest way to handle svg -import 'react' +import 'react'; declare module '*.svg' { - const content: any - export const ReactComponent: any - export default content + const content: any; + export const ReactComponent: any; + export default content; } declare module 'react' { interface CSSProperties { - [key: `--${string}`]: string | number + [key: `--${string}`]: string | number; } } diff --git a/syncpackrc.cjs b/syncpackrc.cjs index 4707154c..4f99f737 100644 --- a/syncpackrc.cjs +++ b/syncpackrc.cjs @@ -29,6 +29,6 @@ const config = { packages: ['**'], }, ], -} +}; -module.exports = config +module.exports = config; diff --git a/yarn.lock b/yarn.lock index 8372dc27..30d4d7a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -60,7 +60,7 @@ __metadata: concurrently: "npm:10.0.3" postcss: "npm:8.5.15" postcss-nesting: "npm:12.1.5" - ramda: "npm:0.28.0" + ramda: "npm:0.32.0" react: "npm:19.2.7" react-dom: "npm:19.2.7" remark-frontmatter: "npm:5.0.0" @@ -138,7 +138,7 @@ __metadata: concurrently: "npm:10.0.3" cpx2: "npm:9.0.0" jsdom: "npm:29.1.1" - ramda: "npm:0.28.0" + ramda: "npm:0.32.0" react-syntax-highlighter: "npm:16.1.1" typescript: "npm:5.8.3" typescript-plugin-css-modules: "npm:5.2.0" @@ -156,10 +156,14 @@ __metadata: dependencies: "@ageorgedev/toolbelt": "workspace:*" "@ageorgedev/ts-config": "workspace:*" + "@types/mustache": "npm:^4" "@types/react": "npm:19.2.17" "@types/react-dom": "npm:19.2.3" concurrently: "npm:10.0.3" cpx2: "npm:9.0.0" + dompurify: "npm:3.4.11" + mustache: "npm:4.2.0" + ramda: "npm:0.32.0" typescript: "npm:5.8.3" peerDependencies: react: 19.2.7 @@ -297,7 +301,7 @@ __metadata: "@types/ramda": "npm:0.28.23" "@vitest/ui": "npm:4.1.8" clsx: "npm:2.1.1" - ramda: "npm:0.28.0" + ramda: "npm:0.32.0" tailwind-merge: "npm:3.3.1" typescript: "npm:5.8.3" vitest: "npm:4.1.8" @@ -3837,6 +3841,13 @@ __metadata: languageName: node linkType: hard +"@types/mustache@npm:^4": + version: 4.2.6 + resolution: "@types/mustache@npm:4.2.6" + checksum: 10c0/f49a83b189e92c962e9b61094c80c979f115ea876bd746bdb9f725c38ab8981a6691e3d1d5cd008ade24b6e5040602970bc0bc60ade71772501d1df8adc7adad + languageName: node + linkType: hard + "@types/node@npm:18.16.9": version: 18.16.9 resolution: "@types/node@npm:18.16.9" @@ -3919,6 +3930,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^3.0.0": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" @@ -5054,6 +5072,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:3.4.11": + version: 3.4.11 + resolution: "dompurify@npm:3.4.11" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10c0/31439481c7e8fc3805d40c376936fd66936620fb1b1a31a2ec097f6165412c37f2d868e082c9ceba62bb37661c1ea132a5db4d5213434317e30df68d4aca9cc9 + languageName: node + linkType: hard + "dotenv@npm:^16.4.2": version: 16.6.1 resolution: "dotenv@npm:16.6.1" @@ -7742,6 +7772,15 @@ __metadata: languageName: node linkType: hard +"mustache@npm:4.2.0": + version: 4.2.0 + resolution: "mustache@npm:4.2.0" + bin: + mustache: bin/mustache + checksum: 10c0/1f8197e8a19e63645a786581d58c41df7853da26702dbc005193e2437c98ca49b255345c173d50c08fe4b4dbb363e53cb655ecc570791f8deb09887248dd34a2 + languageName: node + linkType: hard + "nanoid@npm:^3.3.12": version: 3.3.12 resolution: "nanoid@npm:3.3.12" @@ -8443,10 +8482,10 @@ __metadata: languageName: node linkType: hard -"ramda@npm:0.28.0": - version: 0.28.0 - resolution: "ramda@npm:0.28.0" - checksum: 10c0/0f9dc0cc3b0432ff047f1e2a5e58860c531a84574674c0f52fef535efc6e1e07fa3851102fff3da7dd551a592c743f6f6fa521379a6aa5fe50266f8af8f0b570 +"ramda@npm:0.32.0": + version: 0.32.0 + resolution: "ramda@npm:0.32.0" + checksum: 10c0/6ad24338813cbf91932f7f160e33af9159a540580a43386b974365a8c98b8aa63e2a9d98affa9b2ac4b6628ff34705ddf9ced8f2987c04e3562f58f202c41cf9 languageName: node linkType: hard