From a0df5d818ec27e5cc5b07ba4fd4a6322be04cf79 Mon Sep 17 00:00:00 2001 From: Masoud Soroush Date: Sun, 16 Aug 2026 04:57:00 +0200 Subject: [PATCH 1/2] chore(tooling): plain ASCII punctuation everywhere, and a guard that keeps it scripts/check-typography.mjs walks every tracked text file and fails on invisible characters (zero-width space, word joiner, byte order mark, soft hyphen), on typographic punctuation (en and em dashes, curly quotes, the ellipsis character, non-breaking spaces), on U+FFFD, and on UTF-8 that was read as Latin-1 somewhere upstream. It runs from the pre-commit hook and the CI lint job, and its own character table is written as code points, so it does not report itself. U+200C, U+200D and the bidi marks are deliberately left alone: Persian needs the first between the parts of a word, emoji sequences need the second, and mixed right-to-left text needs the last two. The sweep it demanded came to 3,451 characters across 674 files, and two parts of it are worth reading rather than skimming. A curly apostrophe inside a single-quoted string ends that string. Twenty-one test names and story descriptions had one, so those literals now use double quotes. The two tests that assert on typography build their fixtures from code points instead, or the sweep would have rewritten both sides of the assertion and left them passing while proving nothing. --- .claude/commands/component.md | 76 ++++++------- .claude/commands/design_system.md | 50 ++++----- .claude/commands/manage-skill.md | 22 ++-- .claude/commands/task.md | 16 +-- .claude/skills/Electron/SKILL.md | 22 ++-- .claude/skills/ci-cd/SKILL.md | 38 +++---- .claude/skills/code-style/SKILL.md | 74 ++++++------ .claude/skills/commit/SKILL.md | 22 ++-- .claude/skills/css-in-js/SKILL.md | 14 +-- .claude/skills/github-issues/SKILL.md | 14 +-- .claude/skills/naming-conventions/SKILL.md | 52 ++++----- .claude/skills/operating-manual/SKILL.md | 26 ++--- .claude/skills/release-notes/SKILL.md | 46 ++++---- .claude/skills/seo/SKILL.md | 16 +-- .claude/skills/testing/SKILL.md | 20 ++-- .claude/skills/theme-usage/SKILL.md | 30 ++--- .claude/skills/wcag/SKILL.md | 66 +++++------ .codecov.yml | 8 +- .git-blame-ignore-revs | 2 +- .gitattributes | 2 +- .github/ISSUE_TEMPLATE/1.bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/2.feature_request.yml | 2 +- .github/ISSUE_TEMPLATE/3.rfc.yml | 2 +- .github/ISSUE_TEMPLATE/4.epic.yml | 2 +- .github/ISSUE_TEMPLATE/5.user_story.yml | 2 +- .github/ISSUE_TEMPLATE/6.task.yml | 2 +- .../7.documentation_feedback.yml | 2 +- .github/actions/setup/action.yml | 10 +- .github/workflows/README.md | 48 ++++---- .github/workflows/cd-editor.md | 36 +++--- .github/workflows/cd-editor.yml | 18 +-- .github/workflows/cd-packages.md | 53 ++++----- .github/workflows/cd-packages.yml | 26 ++--- .github/workflows/cd-web.md | 10 +- .github/workflows/cd-web.yml | 6 +- .github/workflows/cd-worker-api.md | 12 +- .github/workflows/cd-worker-api.yml | 6 +- .github/workflows/cd-worker-bench.md | 4 +- .github/workflows/cd-worker-bench.yml | 4 +- .github/workflows/chromatic.md | 17 +-- .github/workflows/chromatic.yml | 6 +- .github/workflows/ci-app.md | 10 +- .github/workflows/ci-app.yml | 4 +- .github/workflows/ci-editor.md | 14 +-- .github/workflows/ci-editor.yml | 16 +-- .github/workflows/ci-packages.md | 14 +-- .github/workflows/ci-packages.yml | 8 +- .github/workflows/ci-web.md | 12 +- .github/workflows/ci-web.yml | 22 ++-- .github/workflows/ci-worker.md | 10 +- .github/workflows/ci-worker.yml | 2 +- .github/workflows/ci.md | 90 +++++++-------- .github/workflows/ci.yml | 22 ++-- .github/workflows/label-area.md | 18 +-- .github/workflows/label-area.yml | 6 +- .gitignore | 4 +- .husky/pre-commit | 2 +- CLAUDE.md | 24 ++-- README.md | 88 +++++++-------- apps/editor/.gitignore | 2 +- apps/editor/README.md | 18 +-- apps/editor/electron-builder.yml | 10 +- apps/editor/electron.vite.config.ts | 2 +- apps/editor/release-notes/0.2.0.md | 18 +-- apps/editor/release-notes/0.2.1.md | 2 +- apps/editor/src/main/bootstrap.ts | 8 +- apps/editor/src/main/claude/parseStream.ts | 8 +- apps/editor/src/main/claude/runEdit.test.ts | 10 +- apps/editor/src/main/claude/runEdit.ts | 23 +++- apps/editor/src/main/github/const.ts | 6 +- .../editor/src/main/github/createGist.test.ts | 4 +- apps/editor/src/main/github/createGist.ts | 2 +- .../src/main/github/credentialStore.test.ts | 2 +- .../editor/src/main/github/credentialStore.ts | 8 +- .../editor/src/main/github/draftStore.test.ts | 4 +- apps/editor/src/main/github/draftStore.ts | 16 +-- apps/editor/src/main/github/fetchAccount.ts | 6 +- .../src/main/github/fetchGistFiles.test.ts | 4 +- apps/editor/src/main/github/fetchGistFiles.ts | 4 +- .../editor/src/main/github/fetchGists.test.ts | 4 +- apps/editor/src/main/github/fetchGists.ts | 8 +- .../src/main/github/gistService.test.ts | 2 +- apps/editor/src/main/github/gistService.ts | 10 +- apps/editor/src/main/github/patchGist.test.ts | 2 +- apps/editor/src/main/github/patchGist.ts | 6 +- apps/editor/src/main/github/toGistId.ts | 4 +- .../src/main/ipc/claudeHandlers.test.ts | 4 +- apps/editor/src/main/ipc/claudeHandlers.ts | 2 +- apps/editor/src/main/ipc/fileHandlers.test.ts | 2 +- apps/editor/src/main/ipc/fileHandlers.ts | 2 +- apps/editor/src/main/ipc/gistHandlers.test.ts | 2 +- apps/editor/src/main/ipc/gistHandlers.ts | 8 +- apps/editor/src/main/ipc/githubHandlers.ts | 4 +- apps/editor/src/main/menu.ts | 11 +- apps/editor/src/main/updater.test.ts | 2 +- apps/editor/src/main/updater.ts | 2 +- apps/editor/src/preload/index.test.ts | 4 +- apps/editor/src/preload/index.ts | 6 +- apps/editor/src/renderer/src/App.e2e.ts | 18 +-- apps/editor/src/renderer/src/App.test.tsx | 12 +- apps/editor/src/renderer/src/App.tsx | 16 +-- .../src/renderer/src/assets/PenMark.tsx | 2 +- .../src/renderer/src/assets/PlusMark.tsx | 2 +- .../src/renderer/src/assets/TrashMark.tsx | 2 +- .../common/ClaudePanel/ClaudePanel.test.tsx | 46 +++++++- .../src/common/ClaudePanel/ClaudePanel.tsx | 37 +++--- .../utils/normalizeTypography.test.ts | 59 ++++++++++ .../ClaudePanel/utils/normalizeTypography.ts | 49 ++++++++ .../common/ClaudePanel/utils/toGistLabel.ts | 2 +- .../ClaudePanel/utils/toPreview.test.ts | 2 +- .../src/common/ClaudePanel/utils/toPreview.ts | 2 +- .../DocumentEditor/DocumentEditor.test.tsx | 2 +- .../common/DocumentEditor/DocumentEditor.tsx | 8 +- .../src/common/DraftList/DraftList.tsx | 4 +- .../DraftList/utils/describeDraft.test.ts | 2 +- .../common/DraftList/utils/describeDraft.ts | 4 +- .../EditorSidebar/EditorSidebar.test.tsx | 4 +- .../common/EditorSidebar/EditorSidebar.tsx | 8 +- .../common/EditorSidebar/SidebarPanelItem.tsx | 2 +- .../src/common/GistFiles/DescriptionField.tsx | 6 +- .../src/common/GistFiles/GistFiles.test.tsx | 16 +-- .../src/common/GistFiles/GistFiles.tsx | 6 +- .../src/common/GistFiles/utils/mergeDraft.ts | 2 +- .../src/renderer/src/common/GistList/const.ts | 2 +- .../src/common/GitHubAuth/GitHubAuth.tsx | 6 +- .../src/renderer/src/hooks/useAutosave.ts | 2 +- .../renderer/src/hooks/useClaudeEdit.test.ts | 4 +- .../src/renderer/src/hooks/useClaudeEdit.ts | 4 +- .../src/renderer/src/hooks/useDocument.ts | 10 +- .../src/renderer/src/hooks/useDrafts.ts | 6 +- .../renderer/src/hooks/useGistDraft.test.ts | 2 +- .../src/renderer/src/hooks/useGistDraft.ts | 16 +-- .../src/renderer/src/hooks/useGistFiles.ts | 6 +- .../editor/src/renderer/src/hooks/useGists.ts | 2 +- .../src/renderer/src/hooks/useGitHubAuth.ts | 2 +- .../src/renderer/src/hooks/useUndoRedo.ts | 6 +- .../renderer/src/hooks/useWindowTitle.test.ts | 8 +- .../src/renderer/src/hooks/useWindowTitle.ts | 4 +- .../src/renderer/src/theme/GlobalStyles.tsx | 2 +- .../src/renderer/src/theme/editorTheme.ts | 2 +- .../editor/src/renderer/src/utils/gistDrag.ts | 2 +- apps/editor/src/shared/ipc.ts | 14 +-- apps/editor/src/test/e2e/coverage.ts | 2 +- apps/editor/src/test/e2e/fixtures.ts | 4 +- apps/editor/vitest.config.ts | 2 +- apps/web/README.md | 26 ++--- apps/web/playwright.config.ts | 4 +- apps/web/scripts/coverage-check.ts | 4 +- apps/web/scripts/gen-experienceGraph.ts | 8 +- apps/web/scripts/setup-env.mjs | 6 +- .../common/Blueprint/Blueprint.stories.tsx | 2 +- apps/web/src/common/Blueprint/README.md | 4 +- apps/web/src/common/CommandSnippet/README.md | 8 +- .../src/common/CookieNotice/CookieNotice.tsx | 4 +- apps/web/src/common/CookieNotice/README.md | 18 +-- apps/web/src/common/DomainCard/DomainCard.tsx | 2 +- apps/web/src/common/DomainCard/README.md | 18 +-- .../src/common/Eyebrow/Eyebrow.stories.tsx | 6 +- apps/web/src/common/Eyebrow/README.md | 6 +- apps/web/src/common/Field/Field.tsx | 14 +-- apps/web/src/common/Field/README.md | 20 ++-- .../src/common/Flicker/Flicker.stories.tsx | 2 +- apps/web/src/common/Flicker/Flicker.tsx | 6 +- apps/web/src/common/Flicker/README.md | 8 +- apps/web/src/common/FontStyle/README.md | 2 +- apps/web/src/common/Footer/Footer.tsx | 10 +- apps/web/src/common/Footer/README.md | 4 +- .../common/GlitchText/GlitchText.stories.tsx | 6 +- apps/web/src/common/GlitchText/GlitchText.tsx | 4 +- apps/web/src/common/GlitchText/README.md | 10 +- apps/web/src/common/Header/README.md | 10 +- apps/web/src/common/Headline/README.md | 2 +- apps/web/src/common/IconCard/IconCard.tsx | 2 +- apps/web/src/common/IconCard/README.md | 12 +- apps/web/src/common/Layout/README.md | 4 +- .../src/common/NavLink/NavLink.stories.tsx | 2 +- apps/web/src/common/NavLink/README.md | 10 +- apps/web/src/common/Navbar/Navbar.test.tsx | 2 +- apps/web/src/common/Navbar/README.md | 14 +-- .../NetworkGraph.browser.test.tsx | 10 +- .../common/NetworkGraph/NetworkGraph.test.tsx | 4 +- .../common/NetworkGraph/NetworkGraph.types.ts | 20 ++-- apps/web/src/common/NetworkGraph/README.md | 10 +- .../GraphContainer/GraphContainer.tsx | 4 +- .../GraphControls/GraphControls.tsx | 4 +- apps/web/src/common/NetworkGraph/const.ts | 30 ++--- .../NetworkGraph/hooks/useGraphSimulation.ts | 24 ++-- .../NetworkGraph/hooks/useGraphState.ts | 2 +- .../utils/anchorExpandedNodes.test.ts | 2 +- .../NetworkGraph/utils/anchorExpandedNodes.ts | 4 +- .../common/NetworkGraph/utils/buildNodes.ts | 8 +- .../utils/computeVisibleIds.test.ts | 6 +- .../NetworkGraph/utils/computeVisibleIds.ts | 4 +- .../utils/forceAreaSeparation.test.ts | 2 +- .../NetworkGraph/utils/forceAreaSeparation.ts | 4 +- .../utils/forceMaxGroupDistance.ts | 2 +- .../common/NetworkGraph/utils/linkStyle.ts | 6 +- .../common/NetworkGraph/utils/prunePins.ts | 2 +- .../common/PageHeader/PageHeader.stories.tsx | 2 +- apps/web/src/common/PageHeader/README.md | 16 +-- apps/web/src/common/common.md | 10 +- apps/web/src/hooks/hooks.md | 38 +++---- apps/web/src/hooks/useContactInquire.ts | 4 +- apps/web/src/hooks/useTurnstile.ts | 2 +- apps/web/src/pages/_error/+Page.tsx | 4 +- apps/web/src/pages/_error/_error.e2e.ts | 4 +- apps/web/src/pages/about/+config.ts | 2 +- apps/web/src/pages/article/@id/+config.ts | 2 +- apps/web/src/pages/article/@id/article.e2e.ts | 6 +- apps/web/src/pages/articles/articles.e2e.ts | 2 +- apps/web/src/pages/contact/+config.ts | 2 +- apps/web/src/pages/contact/contact.e2e.ts | 4 +- apps/web/src/pages/design-system/+config.ts | 2 +- apps/web/src/pages/design/system/+Page.tsx | 2 +- apps/web/src/pages/design/system/+config.ts | 2 +- .../system/components/ButtonGroupCard.tsx | 2 +- .../components/CircularProgressCard.test.tsx | 2 +- .../components/CircularProgressCard.tsx | 2 +- .../design/system/components/DrawerCard.tsx | 2 +- .../design/system/components/FlexCard.tsx | 2 +- .../system/components/FormCard.test.tsx | 2 +- .../design/system/components/FormCard.tsx | 4 +- .../design/system/components/ImageCard.tsx | 2 +- .../design/system/components/ModalCard.tsx | 2 +- .../system/components/TableCard.test.tsx | 8 +- .../web/src/pages/design/system/system.e2e.ts | 6 +- apps/web/src/pages/domain/+Page.tsx | 2 +- apps/web/src/pages/domain/+config.ts | 2 +- apps/web/src/pages/domain/domain.data.ts | 4 +- .../src/pages/experience/experience.e2e.ts | 8 +- apps/web/src/pages/markdown/+config.ts | 2 +- apps/web/src/pages/npm/+config.ts | 2 +- apps/web/src/pages/oxfmt-quick/+config.ts | 2 +- .../src/pages/oxfmt-quick/oxfmt-quick.data.ts | 2 +- .../pages/oxfmt-quick/oxfmt-quick.readme.md | 12 +- apps/web/src/pages/pages.md | 30 ++--- apps/web/src/pages/styled-system/+config.ts | 2 +- .../vite-plugin-msw-server.data.ts | 4 +- .../web/src/renderer/+onRenderClient.test.tsx | 2 +- apps/web/src/renderer/buildHead.ts | 4 +- apps/web/src/renderer/head.ts | 4 +- apps/web/src/section/AboutHero/AboutHero.tsx | 4 +- apps/web/src/section/AboutHero/README.md | 12 +- apps/web/src/section/Article/README.md | 4 +- apps/web/src/section/Articles/README.md | 6 +- apps/web/src/section/Articles/utils.ts | 2 +- apps/web/src/section/CallToAction/README.md | 2 +- .../ContactInquire/ContactInquire.data.ts | 4 +- .../ContactInquire/ContactInquire.test.tsx | 2 +- .../section/ContactInquire/ContactInquire.tsx | 14 +-- apps/web/src/section/ContactInquire/README.md | 18 +-- .../web/src/section/CoreEngine/CoreEngine.tsx | 2 +- apps/web/src/section/CoreEngine/README.md | 14 +-- apps/web/src/section/CorePhilosophy/README.md | 10 +- apps/web/src/section/CoreValues/README.md | 4 +- apps/web/src/section/CurrentFocus/README.md | 2 +- .../web/src/section/DeliveryDomains/README.md | 4 +- .../ExperienceGraph.data.generated.ts | 2 +- .../ExperienceGraph/ExperienceGraph.data.ts | 14 +-- .../web/src/section/ExperienceGraph/README.md | 24 ++-- .../GraphLegend/GraphLegend.test.tsx | 22 ++-- .../components/GraphLegend/GraphLegend.tsx | 14 +-- .../ExperienceGraph/utils/buildGraph.test.ts | 12 +- .../ExperienceGraph/utils/buildGraph.ts | 32 +++--- .../src/section/ExperienceSummary/README.md | 2 +- apps/web/src/section/Hero/README.md | 4 +- apps/web/src/section/Methodology/README.md | 2 +- apps/web/src/section/NotFound/README.md | 4 +- .../src/section/PackageHero/PackageHero.tsx | 4 +- .../section/PackageIndex/PackageIndex.data.ts | 2 +- apps/web/src/section/PackageIndex/README.md | 2 +- apps/web/src/section/PackageReadme/utils.ts | 4 +- .../src/section/SystemArchitectures/README.md | 4 +- .../SystemArchitectures.tsx | 10 +- apps/web/src/section/TechStack/README.md | 2 +- apps/web/src/section/TechStack/TechStack.tsx | 8 +- apps/web/src/section/section.md | 28 ++--- apps/web/src/setupTests.browser.ts | 2 +- apps/web/src/test/e2e/coverage.ts | 6 +- apps/web/src/test/e2e/seo.e2e.ts | 8 +- apps/web/src/test/utils/wrapper.tsx | 4 +- apps/web/src/theme/ColorPalette.stories.tsx | 2 +- .../theme/ColorPalette/ColorPalette.test.tsx | 2 +- apps/web/src/theme/ColorPalette/README.md | 6 +- apps/web/src/theme/GlobalStyles.tsx | 6 +- apps/web/src/theme/ThemeModeProvider.tsx | 2 +- apps/web/src/theme/colors/deepCrimson.ts | 4 +- apps/web/src/theme/colors/forestGreen.ts | 4 +- apps/web/src/theme/colors/lightSurface.ts | 2 +- apps/web/src/theme/themes.ts | 2 +- apps/web/src/vite-env.d.ts | 4 +- apps/web/vite.config.ts | 4 +- apps/web/vitest.config.ts | 2 +- design.md | 2 +- docs/cloudflare.md | 22 ++-- labs/README.md | 4 +- packages/README.md | 8 +- packages/bench/Dockerfile | 6 +- packages/bench/README.md | 106 +++++++++--------- packages/bench/package.json | 2 +- packages/bench/release-notes/1.0.0.md | 14 +-- packages/bench/release-notes/1.0.2.md | 2 +- packages/bench/release-notes/2.0.0.md | 2 +- packages/bench/release-notes/2.1.0.md | 8 +- packages/bench/release-notes/2.2.0.md | 2 +- packages/bench/release-notes/2.2.1.md | 2 +- packages/bench/release-notes/2.2.2.md | 4 +- packages/bench/release-notes/2.2.3.md | 12 ++ packages/bench/src/docker.ts | 6 +- packages/bench/src/harness.ts | 18 +-- packages/bench/src/index.ts | 12 +- packages/bench/src/runner.test.ts | 16 +-- packages/bench/src/runner.ts | 22 ++-- packages/bench/tsdown.config.ts | 2 +- packages/bench/vitest.config.ts | 2 +- packages/design-system/.storybook/main.ts | 2 +- packages/design-system/.storybook/preview.tsx | 2 +- packages/design-system/README.md | 56 ++++----- packages/design-system/design-system.md | 106 +++++++++--------- packages/design-system/docs/customization.md | 32 +++--- packages/design-system/docs/theming.md | 24 ++-- packages/design-system/package.json | 4 +- packages/design-system/release-notes/1.0.0.md | 30 ++--- packages/design-system/release-notes/1.1.0.md | 22 ++-- packages/design-system/release-notes/1.2.0.md | 6 +- packages/design-system/release-notes/1.3.0.md | 2 +- packages/design-system/release-notes/1.3.1.md | 6 +- packages/design-system/release-notes/1.3.2.md | 4 +- packages/design-system/release-notes/1.3.3.md | 10 ++ .../design-system/scripts/audit-styled.mjs | 12 +- packages/design-system/scripts/check-dts.mjs | 14 +-- .../src/AppBar/AppBar.stories.tsx | 10 +- .../design-system/src/AppBar/AppBar.test.tsx | 4 +- packages/design-system/src/AppBar/AppBar.tsx | 6 +- packages/design-system/src/AppBar/README.md | 16 +-- .../src/Avatar/Avatar.stories.tsx | 8 +- .../design-system/src/Avatar/Avatar.test.tsx | 10 +- packages/design-system/src/Avatar/Avatar.tsx | 10 +- packages/design-system/src/Avatar/README.md | 12 +- .../src/Button/Button.stories.tsx | 12 +- .../design-system/src/Button/Button.test.tsx | 8 +- packages/design-system/src/Button/Button.tsx | 24 ++-- packages/design-system/src/Button/README.md | 38 +++---- .../src/ButtonGroup/ButtonGroup.stories.tsx | 8 +- .../src/ButtonGroup/ButtonGroup.test.tsx | 2 +- .../src/ButtonGroup/ButtonGroup.tsx | 10 +- .../design-system/src/ButtonGroup/README.md | 6 +- .../design-system/src/Card/Card.stories.tsx | 4 +- packages/design-system/src/Card/README.md | 24 ++-- .../src/Checkbox/Checkbox.stories.tsx | 2 +- .../src/Checkbox/Checkbox.test.tsx | 8 +- .../design-system/src/Checkbox/Checkbox.tsx | 8 +- packages/design-system/src/Checkbox/README.md | 24 ++-- .../CircularProgress.stories.tsx | 8 +- .../CircularProgress.test.tsx | 8 +- .../src/CircularProgress/CircularProgress.tsx | 6 +- .../src/CircularProgress/README.md | 22 ++-- .../src/Drawer/Drawer.stories.tsx | 4 +- packages/design-system/src/Drawer/Drawer.tsx | 2 +- packages/design-system/src/Drawer/README.md | 12 +- packages/design-system/src/Flex/Flex.test.tsx | 2 +- packages/design-system/src/Flex/Flex.tsx | 4 +- packages/design-system/src/Flex/README.md | 2 +- .../src/FocusTrap/FocusTrap.stories.tsx | 2 +- .../src/FocusTrap/FocusTrap.test.tsx | 10 +- .../design-system/src/FocusTrap/FocusTrap.tsx | 2 +- .../design-system/src/FocusTrap/README.md | 2 +- .../design-system/src/Form/Form.stories.tsx | 2 +- packages/design-system/src/Form/Form.tsx | 8 +- .../design-system/src/Form/FormContext.ts | 2 +- packages/design-system/src/Form/README.md | 16 +-- .../src/FormControl/FormControl.stories.tsx | 12 +- .../src/FormControl/FormControl.tsx | 4 +- .../src/FormControl/FormControlContext.ts | 6 +- .../design-system/src/FormControl/README.md | 22 ++-- .../src/FormControl/useFormControl.ts | 4 +- .../FormHelperText/FormHelperText.stories.tsx | 2 +- .../src/FormHelperText/FormHelperText.tsx | 2 +- .../src/FormHelperText/README.md | 12 +- .../src/FormLabel/FormLabel.stories.tsx | 2 +- .../design-system/src/FormLabel/FormLabel.tsx | 2 +- .../design-system/src/FormLabel/README.md | 8 +- .../design-system/src/Grid/Grid.stories.tsx | 22 ++-- packages/design-system/src/Grid/Grid.test.tsx | 2 +- packages/design-system/src/Grid/Grid.tsx | 4 +- packages/design-system/src/Grid/README.md | 16 +-- .../design-system/src/Icon/Icon.stories.tsx | 4 +- packages/design-system/src/Icon/Icon.test.tsx | 2 +- packages/design-system/src/Icon/Icon.tsx | 2 +- packages/design-system/src/Icon/README.md | 12 +- packages/design-system/src/Icon/icons.ts | 2 +- .../design-system/src/Image/Image.stories.tsx | 6 +- packages/design-system/src/Image/Image.tsx | 2 +- packages/design-system/src/Image/README.md | 10 +- .../LinearProgress/LinearProgress.stories.tsx | 10 +- .../LinearProgress/LinearProgress.test.tsx | 8 +- .../src/LinearProgress/LinearProgress.tsx | 12 +- .../src/LinearProgress/README.md | 24 ++-- .../design-system/src/Link/Link.stories.tsx | 4 +- packages/design-system/src/Link/Link.test.tsx | 2 +- packages/design-system/src/Link/README.md | 24 ++-- .../src/MenuItem/MenuItem.stories.tsx | 8 +- .../design-system/src/MenuItem/MenuItem.tsx | 14 +-- packages/design-system/src/MenuItem/README.md | 24 ++-- .../design-system/src/Modal/Modal.stories.tsx | 6 +- .../design-system/src/Modal/Modal.test.tsx | 2 +- packages/design-system/src/Modal/Modal.tsx | 2 +- .../src/Modal/ModalManager.browser.test.ts | 2 +- .../src/Modal/ModalManager.test.ts | 14 +-- .../design-system/src/Modal/ModalManager.ts | 4 +- packages/design-system/src/Modal/README.md | 12 +- .../src/Modal/hooks/useEventCallback.ts | 2 +- .../design-system/src/Modal/useModal.test.tsx | 8 +- packages/design-system/src/Modal/useModal.ts | 2 +- .../src/NativeSelect/NativeSelect.stories.tsx | 12 +- .../src/NativeSelect/NativeSelect.test.tsx | 2 +- .../src/NativeSelect/NativeSelect.tsx | 24 ++-- .../design-system/src/NativeSelect/README.md | 28 ++--- .../src/Pagination/Pagination.test.tsx | 2 +- .../src/Pagination/Pagination.tsx | 8 +- .../PaginationItem/PaginationItem.stories.tsx | 4 +- .../PaginationItem/PaginationItem.test.tsx | 2 +- .../PaginationItem/PaginationItem.tsx | 10 +- .../src/Pagination/PaginationItem/README.md | 6 +- .../design-system/src/Pagination/README.md | 38 +++---- .../Pagination/hooks/usePagination.test.ts | 2 +- .../src/Pagination/hooks/usePagination.ts | 6 +- .../design-system/src/Paper/Paper.stories.tsx | 4 +- packages/design-system/src/Paper/Paper.tsx | 8 +- packages/design-system/src/Paper/README.md | 12 +- .../src/Popover/Popover.stories.tsx | 4 +- .../design-system/src/Popover/Popover.tsx | 8 +- packages/design-system/src/Popover/README.md | 20 ++-- .../utils/computePopoverPosition.test.ts | 6 +- .../src/Portal/Portal.stories.tsx | 2 +- packages/design-system/src/Portal/Portal.tsx | 2 +- packages/design-system/src/Portal/README.md | 6 +- .../src/Pressable/Pressable.stories.tsx | 10 +- .../src/Pressable/Pressable.test.tsx | 4 +- .../design-system/src/Pressable/Pressable.tsx | 22 ++-- .../design-system/src/Pressable/README.md | 32 +++--- .../design-system/src/Quote/Quote.stories.tsx | 2 +- packages/design-system/src/Quote/Quote.tsx | 2 +- packages/design-system/src/Quote/README.md | 4 +- packages/design-system/src/Radio/README.md | 24 ++-- .../design-system/src/Radio/Radio.stories.tsx | 2 +- .../design-system/src/Radio/Radio.test.tsx | 8 +- packages/design-system/src/Radio/Radio.tsx | 8 +- packages/design-system/src/Select/README.md | 42 +++---- .../src/Select/Select.stories.tsx | 16 +-- .../design-system/src/Select/Select.test.tsx | 2 +- packages/design-system/src/Select/Select.tsx | 28 ++--- .../src/Sidebar/Sidebar/README.md | 36 +++--- .../src/Sidebar/Sidebar/Sidebar.stories.tsx | 14 +-- .../src/Sidebar/Sidebar/Sidebar.test.tsx | 16 +-- .../src/Sidebar/Sidebar/Sidebar.tsx | 26 ++--- .../src/Sidebar/SidebarContext.ts | 8 +- .../src/Sidebar/SidebarItem/README.md | 24 ++-- .../SidebarItem/SidebarItem.stories.tsx | 6 +- .../Sidebar/SidebarItem/SidebarItem.test.tsx | 4 +- .../src/Sidebar/SidebarItem/SidebarItem.tsx | 40 +++---- packages/design-system/src/Skeleton/README.md | 8 +- .../src/Skeleton/Skeleton.stories.tsx | 12 +- .../src/Skeleton/Skeleton.test.tsx | 2 +- .../design-system/src/Skeleton/Skeleton.tsx | 8 +- packages/design-system/src/Switch/README.md | 40 +++---- .../src/Switch/Switch.stories.tsx | 4 +- .../design-system/src/Switch/Switch.test.tsx | 18 +-- packages/design-system/src/Switch/Switch.tsx | 8 +- .../design-system/src/Table/Table.spec.tsx | 8 +- .../design-system/src/Table/Table/README.md | 30 ++--- .../src/Table/Table/Table.stories.tsx | 18 +-- .../src/Table/Table/Table.test.tsx | 2 +- .../design-system/src/Table/Table/Table.tsx | 32 +++--- .../src/Table/TableBody/README.md | 6 +- .../src/Table/TableBody/TableBody.stories.tsx | 2 +- .../src/Table/TableBody/TableBody.tsx | 8 +- .../src/Table/TableCell/README.md | 25 +++-- .../src/Table/TableCell/TableCell.stories.tsx | 26 ++--- .../src/Table/TableCell/TableCell.tsx | 24 ++-- .../src/Table/TableContainer/README.md | 4 +- .../Table/TableContainer/TableContainer.tsx | 2 +- .../design-system/src/Table/TableContext.ts | 2 +- .../src/Table/TableControl/README.md | 22 ++-- .../TableControl/TableControl.stories.tsx | 4 +- .../Table/TableControl/TableControl.test.tsx | 2 +- .../src/Table/TableControl/TableControl.tsx | 12 +- .../src/Table/TableFooter/README.md | 6 +- .../Table/TableFooter/TableFooter.stories.tsx | 2 +- .../src/Table/TableFooter/TableFooter.tsx | 8 +- .../src/Table/TableHead/README.md | 4 +- .../src/Table/TableHead/TableHead.stories.tsx | 2 +- .../src/Table/TableHead/TableHead.tsx | 8 +- .../src/Table/TablePagination/README.md | 14 +-- .../TablePagination.stories.tsx | 8 +- .../TablePagination/TablePagination.test.tsx | 10 +- .../Table/TablePagination/TablePagination.tsx | 8 +- .../Table/TablePaginationActions/README.md | 6 +- .../TablePaginationActions.stories.tsx | 4 +- .../TablePaginationActions.tsx | 4 +- .../src/Table/TableRow/README.md | 4 +- .../src/Table/TableRow/TableRow.stories.tsx | 2 +- .../src/Table/TableRow/TableRow.test.tsx | 2 +- .../src/Table/TableRow/TableRow.tsx | 10 +- .../src/Table/TableSectionContext.ts | 2 +- .../src/Table/TableSortLabel/README.md | 18 +-- .../TableSortLabel/TableSortLabel.stories.tsx | 4 +- .../Table/TableSortLabel/TableSortLabel.tsx | 8 +- .../design-system/src/Table/hooks/README.md | 22 ++-- .../src/Table/hooks/useTablePagination.ts | 4 +- .../src/Table/hooks/useTableSelection.ts | 8 +- .../src/Table/hooks/useTableSort.test.ts | 2 +- .../src/Table/hooks/useTableSort.ts | 6 +- .../design-system/src/TextInput/README.md | 34 +++--- .../src/TextInput/TextInput.stories.tsx | 30 ++--- .../design-system/src/TextInput/TextInput.tsx | 24 ++-- .../src/ToggleButton/ToggleButton/README.md | 10 +- .../ToggleButton/ToggleButton.stories.tsx | 8 +- .../ToggleButton/ToggleButton.tsx | 4 +- .../ToggleButton/ToggleButtonGroup/README.md | 8 +- .../ToggleButtonGroup.stories.tsx | 4 +- .../ToggleButtonGroup/ToggleButtonGroup.tsx | 4 +- .../ToggleButton/ToggleButtonGroupContext.ts | 4 +- .../design-system/src/Typography/README.md | 30 ++--- .../src/Typography/Typography.spec.md | 30 ++--- .../src/Typography/Typography.stories.tsx | 8 +- .../src/Typography/Typography.tsx | 8 +- packages/design-system/src/View/README.md | 8 +- packages/design-system/src/View/View.test.tsx | 2 +- packages/design-system/src/View/View.tsx | 14 +-- .../design-system/src/engine/globalStyles.ts | 4 +- packages/design-system/src/engine/index.ts | 2 +- packages/design-system/src/index.ts | 4 +- .../design-system/src/packageExports.test.ts | 2 +- .../design-system/src/style/styled.test.tsx | 2 +- packages/design-system/src/style/styled.ts | 14 +-- .../src/theme/Customization.stories.tsx | 8 +- packages/design-system/src/theme/README.md | 14 +-- .../src/theme/ThemeProvider.test.tsx | 2 +- .../design-system/src/theme/ThemeProvider.tsx | 2 +- packages/design-system/src/theme/emotion.ts | 4 +- .../src/theme/hooks/useTheme.browser.test.tsx | 2 +- .../design-system/src/theme/hooks/useTheme.ts | 2 +- packages/design-system/src/theme/themes.ts | 44 ++++---- .../src/theme/utils/createTheme.ts | 2 +- .../src/themeComponents.spec.tsx | 2 +- .../src/utils/test/renderWithTheme.tsx | 2 +- .../src/utils/test/storiesArgs.ts | 30 ++--- .../src/utils/test/storiesOptions.ts | 4 +- .../src/utils/test/storiesToggle.tsx | 2 +- packages/design-system/styled-audit.md | 18 +-- packages/design-system/tsdown.config.ts | 4 +- .../design-system/type-tests/augmentation.ts | 6 +- .../design-system/type-tests/styleProps.ts | 8 +- .../plugins/no-import-extensions.js | 2 +- packages/hooks/src/useCopyToClipboard.test.ts | 2 +- packages/hooks/src/useCopyToClipboard.ts | 2 +- packages/markdown/README.md | 54 ++++----- packages/markdown/markdown.md | 14 +-- packages/markdown/package.json | 4 +- packages/markdown/release-notes/1.0.0.md | 8 +- packages/markdown/release-notes/1.1.0.md | 6 +- packages/markdown/release-notes/1.2.0.md | 8 +- packages/markdown/release-notes/1.2.1.md | 6 +- packages/markdown/release-notes/1.2.2.md | 11 ++ .../markdown/src/CodeBlock/CodeBlock.data.ts | 8 +- packages/markdown/src/CodeBlock/CodeBlock.tsx | 4 +- packages/markdown/src/CodeBlock/README.md | 12 +- .../markdown/src/Control/Control.stories.tsx | 2 +- packages/markdown/src/Control/Control.tsx | 6 +- packages/markdown/src/Control/README.md | 4 +- .../markdown/src/Editor/Editor.stories.tsx | 18 +-- packages/markdown/src/Editor/Editor.test.tsx | 4 +- packages/markdown/src/Editor/Editor.tsx | 24 ++-- packages/markdown/src/Editor/README.md | 28 ++--- .../src/LiveEdit/LiveEdit.stories.tsx | 4 +- .../markdown/src/LiveEdit/LiveEdit.test.tsx | 4 +- packages/markdown/src/LiveEdit/LiveEdit.tsx | 20 ++-- packages/markdown/src/LiveEdit/README.md | 22 ++-- .../src/MarkdownContext/MarkdownContext.ts | 4 +- .../src/Mermaid/DiagramViewer.README.md | 6 +- .../src/Mermaid/DiagramViewer.stories.tsx | 2 +- .../src/Mermaid/DiagramViewer.test.tsx | 2 +- .../markdown/src/Mermaid/DiagramViewer.tsx | 6 +- .../markdown/src/Mermaid/Mermaid.stories.tsx | 2 +- .../markdown/src/Mermaid/Mermaid.test.tsx | 4 +- packages/markdown/src/Mermaid/Mermaid.tsx | 14 +-- packages/markdown/src/Mermaid/README.md | 6 +- .../markdown/src/Preview/Preview.stories.tsx | 6 +- .../markdown/src/Preview/Preview.test.tsx | 4 +- packages/markdown/src/Preview/Preview.tsx | 8 +- packages/markdown/src/Preview/README.md | 8 +- packages/markdown/src/Preview/mermaidBlock.ts | 4 +- packages/markdown/src/Toolbar/README.md | 10 +- packages/markdown/src/Toolbar/TablePicker.tsx | 4 +- packages/markdown/src/Toolbar/Toolbar.tsx | 2 +- packages/markdown/src/const.ts | 24 ++-- packages/markdown/src/index.ts | 10 +- .../markdown/src/themeComponents.spec.tsx | 2 +- packages/markdown/src/utils/applyAction.ts | 14 +-- .../markdown/src/utils/blockToMarkdown.ts | 2 +- .../markdown/src/utils/splitBlocks.test.ts | 2 +- packages/markdown/src/utils/splitBlocks.ts | 6 +- packages/markdown/type-tests/augmentation.ts | 2 +- packages/packages.md | 34 +++--- packages/playwright-coverage/README.md | 38 +++---- packages/playwright-coverage/package.json | 2 +- .../release-notes/1.0.0.md | 10 +- .../release-notes/2.0.0.md | 2 +- .../release-notes/3.0.0.md | 2 +- .../release-notes/3.1.0.md | 2 +- .../release-notes/3.1.1.md | 4 +- .../release-notes/3.1.2.md | 3 + packages/playwright-coverage/src/collector.ts | 2 +- .../playwright-coverage/src/report.test.ts | 6 +- packages/playwright-coverage/src/types.ts | 4 +- packages/schema/src/contact.ts | 6 +- packages/styled-system/README.md | 14 +-- packages/styled-system/docs/README.md | 28 ++--- packages/styled-system/docs/css.md | 4 +- .../styled-system/docs/guides/color-modes.md | 2 +- .../styled-system/docs/guides/migrating.md | 4 +- .../styled-system/docs/responsive-styles.md | 2 +- packages/styled-system/docs/typescript.md | 12 +- packages/styled-system/package.json | 4 +- packages/styled-system/release-notes/5.2.0.md | 14 +-- packages/styled-system/release-notes/5.4.0.md | 4 +- packages/styled-system/release-notes/5.5.0.md | 6 +- packages/styled-system/release-notes/5.6.0.md | 12 +- packages/styled-system/release-notes/5.7.0.md | 6 +- packages/styled-system/release-notes/5.7.1.md | 2 +- packages/styled-system/release-notes/5.8.0.md | 10 +- packages/styled-system/release-notes/5.8.1.md | 2 +- packages/styled-system/release-notes/5.8.2.md | 2 +- packages/styled-system/release-notes/5.8.3.md | 4 + packages/styled-system/src/core/core.ts | 4 +- packages/styled-system/src/css/css.ts | 2 +- .../styled-system/src/space/space.test.ts | 2 +- packages/styled-system/src/space/space.ts | 2 +- packages/styled-system/src/types.ts | 16 +-- packages/styled-system/vitest.config.ts | 2 +- packages/vite-plugin-msw-server/README.md | 52 ++++----- packages/vite-plugin-msw-server/package.json | 2 +- .../release-notes/1.0.1.md | 2 +- .../release-notes/1.0.2.md | 2 +- .../release-notes/1.0.3.md | 4 +- .../release-notes/1.0.4.md | 4 +- .../release-notes/1.0.5.md | 4 + packages/vite-plugin-msw-server/src/index.ts | 4 +- .../vite-plugin-sitemap/src/index.test.ts | 2 +- packages/vite-plugin-sitemap/src/index.ts | 2 +- packages/vite-plugin-watch/src/index.ts | 2 +- packages/wrangler-tools/src/gen-wrangler.ts | 6 +- packages/wrangler-tools/src/setup-env.ts | 6 +- renovate.json | 4 +- scripts/assemble-changes.mjs | 52 ++++----- scripts/check-release-notes.mjs | 30 ++--- scripts/gen-publish-options.mjs | 10 +- socket.yml | 6 +- sonar-project.properties | 2 +- workers/README.md | 6 +- workers/api/README.md | 20 ++-- workers/api/src/app.ts | 6 +- workers/api/src/db/contacts.schema.sql | 2 +- workers/api/src/env.ts | 2 +- workers/api/src/jobs/retention.test.ts | 2 +- workers/api/src/jobs/retention.ts | 2 +- workers/api/src/routes/contact.test.ts | 4 +- workers/api/src/routes/contact.ts | 4 +- workers/api/src/services/turnstile.ts | 2 +- workers/api/src/utils/requestId.ts | 2 +- workers/api/src/utils/sanitize.ts | 8 +- workers/api/src/utils/tables.ts | 2 +- workers/api/worker.md | 26 ++--- workers/bench/README.md | 16 +-- workers/bench/default.env | 2 +- workers/bench/src/app.ts | 6 +- workers/bench/src/openapi.ts | 2 +- workers/bench/src/routes/report.ts | 4 +- workers/bench/src/services/githubApp.ts | 2 +- workers/bench/src/services/githubComment.ts | 6 +- workers/bench/src/services/githubOidc.ts | 4 +- workers/bench/src/utils/jwt.test.ts | 2 +- workers/bench/src/utils/jwt.ts | 6 +- workers/bench/worker.md | 22 ++-- 685 files changed, 3513 insertions(+), 3285 deletions(-) create mode 100644 apps/editor/src/renderer/src/common/ClaudePanel/utils/normalizeTypography.test.ts create mode 100644 apps/editor/src/renderer/src/common/ClaudePanel/utils/normalizeTypography.ts create mode 100644 packages/bench/release-notes/2.2.3.md create mode 100644 packages/design-system/release-notes/1.3.3.md create mode 100644 packages/markdown/release-notes/1.2.2.md create mode 100644 packages/playwright-coverage/release-notes/3.1.2.md create mode 100644 packages/styled-system/release-notes/5.8.3.md create mode 100644 packages/vite-plugin-msw-server/release-notes/1.0.5.md diff --git a/.claude/commands/component.md b/.claude/commands/component.md index a1f63ab9..f8616409 100644 --- a/.claude/commands/component.md +++ b/.claude/commands/component.md @@ -1,7 +1,7 @@ Parse `$ARGUMENTS` as two space-separated words: -- **Word 1** — component name (``). Must be PascalCase. -- **Word 2** — target directory (``). Must be one of: `theme` · `common` · `page` · `section`. +- **Word 1** - component name (``). Must be PascalCase. +- **Word 2** - target directory (``). Must be one of: `theme` · `common` · `page` · `section`. If either word is missing or invalid, stop and say: @@ -20,46 +20,46 @@ If either word is missing or invalid, stop and say: --- -## Step 0 — Read reference files +## Step 0 - Read reference files -Read ALL of these before doing anything else — they are the authoritative source of truth: +Read ALL of these before doing anything else - they are the authoritative source of truth: -1. `packages/design-system/design-system.md` — architecture rules -2. `.claude/skills/theme-usage/SKILL.md` — rules for consuming theme primitives in non-theme components -3. `.claude/skills/css-in-js/SKILL.md` — CSS-in-JS and styled-system conventions -4. `.claude/skills/code-style/SKILL.md` — TypeScript and JavaScript coding conventions -5. `.claude/skills/wcag/SKILL.md` — accessibility rules (ARIA, contrast, keyboard, landmarks) -6. `src/section/section.md` — section conventions (read only when `` is `section`) +1. `packages/design-system/design-system.md` - architecture rules +2. `.claude/skills/theme-usage/SKILL.md` - rules for consuming theme primitives in non-theme components +3. `.claude/skills/css-in-js/SKILL.md` - CSS-in-JS and styled-system conventions +4. `.claude/skills/code-style/SKILL.md` - TypeScript and JavaScript coding conventions +5. `.claude/skills/wcag/SKILL.md` - accessibility rules (ARIA, contrast, keyboard, landmarks) +6. `src/section/section.md` - section conventions (read only when `` is `section`) --- -## Step 1 — Detect mode +## Step 1 - Detect mode Check whether `` already exists. -- **Exists** → **rework**. Read all existing files in the folder, then go to Step 4 (rework rules apply). Skip Steps 2–3. +- **Exists** → **rework**. Read all existing files in the folder, then go to Step 4 (rework rules apply). Skip Steps 2-3. - **Does not exist** → **new component**. Follow all steps in order. --- -## Step 2 — Clarify intent (new components only) +## Step 2 - Clarify intent (new components only) If the task spec (everything in `$ARGUMENTS` after the two required words) already makes the component's purpose, base element, and props clear, state your interpretation explicitly and proceed. Otherwise ask: > What is ``'s primary purpose and base HTML element? -> (e.g. `"section — a page region with a heading and body"`) +> (e.g. `"section - a page region with a heading and body"`) Do not generate any files until you have enough information to make concrete decisions. --- -## Step 3 — Audit for reuse +## Step 3 - Audit for reuse Before writing any code, scan for existing work that should be reused instead of duplicated: -**Theme primitives** — check `@soroush.tech/design-system/` for components that cover the layout or styling need: +**Theme primitives** - check `@soroush.tech/design-system/` for components that cover the layout or styling need: - Block container → `View` - Flex layout → `Flex` @@ -69,41 +69,41 @@ Before writing any code, scan for existing work that should be reused instead of - Clickable → `Button` - Input → `TextInput` -If a theme primitive covers the need, use it as a composed child — never reimplement it. +If a theme primitive covers the need, use it as a composed child - never reimplement it. -**Utils** — grep `src/utils/` and all sibling component folders for functions that match the logic needed. If a match exists, import it. If it lives in another component's folder (`src/common/OtherComponent/utils.ts`), present a proposal to move it to `src/utils/` and wait for approval before touching that file. +**Utils** - grep `src/utils/` and all sibling component folders for functions that match the logic needed. If a match exists, import it. If it lives in another component's folder (`src/common/OtherComponent/utils.ts`), present a proposal to move it to `src/utils/` and wait for approval before touching that file. -**Hooks** — grep `src/hooks/` and sibling component folders for hooks that match. Same rule: if a hook is generic enough to share, propose moving it to `src/hooks/` first. +**Hooks** - grep `src/hooks/` and sibling component folders for hooks that match. Same rule: if a hook is generic enough to share, propose moving it to `src/hooks/` first. Present a short summary of what you found and what you plan to reuse before proceeding. --- -## Step 4 — Generate files +## Step 4 - Generate files Every component lives in its own folder regardless of ``. Create `` with these files: -### `index.ts` — barrel re-export only +### `index.ts` - barrel re-export only ```ts export * from './' ``` -### `.tsx` — component +### `.tsx` - component Rules that apply to **all** `` values: -- Compose using theme primitives (`View`, `Flex`, `Typography`, `Button`, etc.) — pass layout, color, and spacing via their token props, not inline styles. +- Compose using theme primitives (`View`, `Flex`, `Typography`, `Button`, etc.) - pass layout, color, and spacing via their token props, not inline styles. - Never pass raw hex values, pixel literals, or hardcoded font-family strings. - Extract reusable pure functions to `/utils.ts`. Extract stateful logic to `/use.ts` (or a more specific name). Keep `.tsx` a thin composition layer. - Do **not** write custom CSS (`styled`, template literals, or `css` prop) without first presenting a proposal that explains why a theme primitive cannot cover the case. Wait for approval before implementing. - **Assets:** SVG icons and images used by the component must be placed in `src/assets/` (icons in `src/assets/icons/`), not inlined or co-located next to the component. -**If `` is `theme`** — additionally follow all `packages/design-system/design-system.md` rules: +**If `` is `theme`** - additionally follow all `packages/design-system/design-system.md` rules: - `styled` base with `createShouldForwardProp([...props, ...customProps])` - Custom props wired via `system()` against theme scales -- Prop types derived from `Theme` (`keyof Theme['scaleName']`) — no manual unions +- Prop types derived from `Theme` (`keyof Theme['scaleName']`) - no manual unions - Export all prop types **If `` is `common`, `page`, or `section`:** @@ -120,8 +120,8 @@ For components where a prop selects a palette (not a raw CSS property), use a ne button: Record<'primary' | 'secondary' | ..., { main, hover, active, contrast }> ``` -- Derive type via `keyof Theme['button']` — never a manual union -- Use a `variantStyles` function, not `system()` — `system()` handles 1:1 prop→CSS; nested/conditional mappings need a function +- Derive type via `keyof Theme['button']` - never a manual union +- Use a `variantStyles` function, not `system()` - `system()` handles 1:1 prop→CSS; nested/conditional mappings need a function - Hover/active states: hex-opacity suffix `${main}14` (8%) or `${main}20` (12%) **Kinetic OS design tokens for interactive elements:** @@ -132,11 +132,11 @@ Buttons use `borderRadius: 0`, `textTransform: uppercase`, `fontWeight: bold`, ` When a layout calls for a specific font size, weight, or line height, match it to the nearest `Typography` variant or theme token prop (`fontSize`, `fontWeight`, `lineHeight` from `theme.fontSizes`, `theme.fontWeights`, `theme.lineHeights`). State the mapping explicitly: -> "I'm using `variant="body2"` (14px, normal weight) — does that match the intended style?" +> "I'm using `variant="body2"` (14px, normal weight) - does that match the intended style?" Wait for confirmation before committing to a specific variant or size token. -### `.test.tsx` — unit tests +### `.test.tsx` - unit tests - Wrap every render in `renderWithTheme` (or `ThemeProvider`). - Cover: children render, each meaningful prop produces the correct CSS or DOM output, HTML attribute passthrough (`className`, `data-*`, `aria-*`), element mapping if a variant prop exists. @@ -145,22 +145,22 @@ Wait for confirmation before committing to a specific variant or size token. **jsdom caveats (theme components):** - `backgroundColor` transparent value will convert to rgba -- Elements with `visibility: hidden` are excluded from accessible name — use `data-testid` selectors +- Elements with `visibility: hidden` are excluded from accessible name - use `data-testid` selectors -### `README.md` — prop documentation +### `README.md` - prop documentation - Document every prop: type, default, description. -- Color / bg / border tables use palette constant names only — `kineticGreen[500]`, never hex. +- Color / bg / border tables use palette constant names only - `kineticGreen[500]`, never hex. - Include a usage example. - Keep in sync with actual token values in `themes.ts`. -### `.stories.tsx` — Storybook (required for `theme`; optional but recommended for `common` / `section`) +### `.stories.tsx` - Storybook (required for `theme`; optional but recommended for `common` / `section`) -- Import option arrays from `@soroush.tech/design-system/utils/test/storiesArgs.ts` and `@soroush.tech/design-system/utils/test/storiesOptions.ts` — never hardcode inline. -- `controls.include` whitelist — no autodiscovery. +- Import option arrays from `@soroush.tech/design-system/utils/test/storiesArgs.ts` and `@soroush.tech/design-system/utils/test/storiesOptions.ts` - never hardcode inline. +- `controls.include` whitelist - no autodiscovery. - Every prop in `controls.include` must have a matching `argType` with `control`, `description`, and `table.category`. - Category names: Content · Typography · Layout · Visual · Spacing. -- No top-level `name:` in any argType. `table.name` inside `table:` is safe — only affects autodocs display. +- No top-level `name:` in any argType. `table.name` inside `table:` is safe - only affects autodocs display. - When adding new token arrays to the options file, constrain with `satisfies`: `export const myTokens = [...] satisfies MyToken[]` - Control types: `opacity` → `{ type: 'range', min: 0, max: 1, step: 0.05 }` · space props → `{ type: 'select' }, options: spaceTokens` · booleans → `'boolean'` @@ -178,7 +178,7 @@ Static data the component renders. Co-locate it here and import it into both `/` already exists. @@ -16,15 +16,15 @@ Check whether `packages/design-system/src//` already exists. --- -## Step 1 — Read reference files +## Step 1 - Read reference files Read these files in order before doing anything else: -1. `@soroush.tech/design-system/Typography/Typography.tsx` — canonical component implementation -2. `@soroush.tech/design-system/Typography/README.md` — canonical documentation format -3. `@soroush.tech/design-system/Typography/Typography.stories.tsx` — canonical story structure -4. `@soroush.tech/design-system/storybookOptions.ts` — existing token arrays and import patterns -5. `packages/design-system/design-system.md` — full architecture rules (authoritative source of truth) +1. `@soroush.tech/design-system/Typography/Typography.tsx` - canonical component implementation +2. `@soroush.tech/design-system/Typography/README.md` - canonical documentation format +3. `@soroush.tech/design-system/Typography/Typography.stories.tsx` - canonical story structure +4. `@soroush.tech/design-system/storybookOptions.ts` - existing token arrays and import patterns +5. `packages/design-system/design-system.md` - full architecture rules (authoritative source of truth) If reworking an existing component, also read its current files: @@ -35,14 +35,14 @@ If reworking an existing component, also read its current files: --- -## Step 2 — Clarify base element (new components only) +## Step 2 - Clarify base element (new components only) If the task spec already makes the base HTML element and props clear, state your interpretation and proceed without asking. Otherwise ask: > What is the base HTML element for ``, and what is its primary purpose? -> (e.g. `"div — a layout container"` or `"button — an interactive action trigger"`) +> (e.g. `"div - a layout container"` or `"button - an interactive action trigger"`) Use the answer to decide: @@ -54,13 +54,13 @@ Do not generate any files until confirmed. --- -## Step 3 — Generate or update files +## Step 3 - Generate or update files ### New component Create the folder `packages/design-system/src//` with these five files: -**`index.ts`** — re-export only: +**`index.ts`** - re-export only: ```ts export * from './' @@ -71,19 +71,19 @@ export * from './' - `styled` base with `createShouldForwardProp([...props, ...customProps])` - Only include styled-system groups relevant to this component - Wire `color` → `scale: 'text'` and `bg` → `scale: 'background'` via `system()` if color props are needed -- Derive all prop types from `Theme` via `keyof Theme['scaleName']` — no manual unions +- Derive all prop types from `Theme` via `keyof Theme['scaleName']` - no manual unions - Export named prop types (e.g. `Props`) **`README.md`** - Document every prop: type, default, description -- Color/bg tables use palette constant names only — `kineticGreen[500]`, never hex +- Color/bg tables use palette constant names only - `kineticGreen[500]`, never hex - Mirror the section structure of `@soroush.tech/design-system/Typography/README.md` **`.stories.tsx`** -- Import all option arrays from `@soroush.tech/design-system/storybookOptions.ts` — never hardcode inline -- `controls.include` whitelist — no autodiscovery +- Import all option arrays from `@soroush.tech/design-system/storybookOptions.ts` - never hardcode inline +- `controls.include` whitelist - no autodiscovery - Every prop in `controls.include` must have a matching `argType` with `control`, `description`, and `table.category` - Category names: Content · Typography · Layout · Color · Spacing - No top-level `name:` in any argType @@ -93,22 +93,22 @@ export * from './' - Cover: CSS output for each prop, HTML attribute passthrough, element mapping if a variant prop exists - Wrap renders in `ThemeProvider` -Update `@soroush.tech/design-system/storybookOptions.ts` only if new token arrays are needed — never duplicate existing ones. +Update `@soroush.tech/design-system/storybookOptions.ts` only if new token arrays are needed - never duplicate existing ones. ### Rework (existing component) Apply only the changes required by the task spec. For every change made: -- **`.tsx`** — update the component -- **`.test.tsx`** — add or update tests to cover the change; remove any tests made obsolete -- **`README.md`** — update prop documentation to reflect the change; fix any stale references -- **`.stories.tsx`** — add the new prop to `controls.include` and `argTypes`; add a story if the change warrants one +- **`.tsx`** - update the component +- **`.test.tsx`** - add or update tests to cover the change; remove any tests made obsolete +- **`README.md`** - update prop documentation to reflect the change; fix any stale references +- **`.stories.tsx`** - add the new prop to `controls.include` and `argTypes`; add a story if the change warrants one Do not touch unrelated parts of any file. --- -## Step 4 — Cross-component impact +## Step 4 - Cross-component impact After any change, check whether other components are affected: @@ -118,9 +118,9 @@ After any change, check whether other components are affected: > **Impact on other components** > -> - `Flex` — inherits `cursor` automatically, no changes needed -> - `Grid` — inherits `cursor` automatically, no changes needed -> - `Paper` — not yet implemented; `cursor` will be available when built +> - `Flex` - inherits `cursor` automatically, no changes needed +> - `Grid` - inherits `cursor` automatically, no changes needed +> - `Paper` - not yet implemented; `cursor` will be available when built Wait for the user to confirm before making changes to other components. diff --git a/.claude/commands/manage-skill.md b/.claude/commands/manage-skill.md index 2261a96d..7433f27d 100644 --- a/.claude/commands/manage-skill.md +++ b/.claude/commands/manage-skill.md @@ -4,7 +4,7 @@ The first word of `$ARGUMENTS` is the skill name (kebab-case). The rest is an op --- -## Step 1 — Detect mode +## Step 1 - Detect mode Check whether `.claude/skills/$0/SKILL.md` already exists. @@ -13,41 +13,41 @@ Check whether `.claude/skills/$0/SKILL.md` already exists. --- -## Step 2 — Determine frontmatter (create only) +## Step 2 - Determine frontmatter (create only) Decide the correct frontmatter based on what the skill does: -**`description`** — one sentence: what the skill does + when Claude should auto-load it. Put the key trigger phrase first (budget is 1,536 chars). +**`description`** - one sentence: what the skill does + when Claude should auto-load it. Put the key trigger phrase first (budget is 1,536 chars). -**`paths`** — set only if the skill is scoped to specific files. Use glob patterns: +**`paths`** - set only if the skill is scoped to specific files. Use glob patterns: - Design system / theme work → `@soroush.tech/design-system/**` - All TypeScript → `**/*.ts,**/*.tsx` - Omit for project-wide knowledge -**`argument-hint`** — short label shown in autocomplete, e.g. `[filename]` or `[component-name]`. +**`argument-hint`** - short label shown in autocomplete, e.g. `[filename]` or `[component-name]`. -**`disable-model-invocation: true`** — add only if this is a manual workflow with side effects (deploy, commit, send). Reference knowledge should NOT have this. +**`disable-model-invocation: true`** - add only if this is a manual workflow with side effects (deploy, commit, send). Reference knowledge should NOT have this. -**`user-invocable: false`** — add only if users should never call it directly (pure background context). +**`user-invocable: false`** - add only if users should never call it directly (pure background context). --- -## Step 3 — Write content +## Step 3 - Write content Rules for skill content: -- **Reference skills** (conventions, patterns, style guides): state the rule + one ✗/✓ code pair. No prose explaining why — the example shows it. +- **Reference skills** (conventions, patterns, style guides): state the rule + one ✗/✓ code pair. No prose explaining why - the example shows it. - **Task skills** (step-by-step actions): numbered steps, imperative verbs, no explanation. - Keep SKILL.md under 500 lines. -- Every line is a recurring token cost — cut anything that doesn't change Claude's behavior. +- Every line is a recurring token cost - cut anything that doesn't change Claude's behavior. - End with: `If $ARGUMENTS names a file, read it and apply the rules. Otherwise apply to the code being discussed.` (for reference skills only). **Update mode**: add the new rule or change to the appropriate section. Do not touch unrelated rules. Remove any rule made obsolete by the change. --- -## Step 4 — Write the file +## Step 4 - Write the file For **create**: run `mkdir -p .claude/skills/$0` then write `.claude/skills/$0/SKILL.md`. diff --git a/.claude/commands/task.md b/.claude/commands/task.md index d0455d86..5820a709 100644 --- a/.claude/commands/task.md +++ b/.claude/commands/task.md @@ -4,7 +4,7 @@ The task topic or description is: $ARGUMENTS --- -## Step 1 — Gather context +## Step 1 - Gather context Run these commands in parallel to build context: @@ -14,24 +14,24 @@ If `$ARGUMENTS` is empty, infer the task title and domain from the branch name a --- -## Step 2 — Draft the issue +## Step 2 - Draft the issue -Using the gathered context, write a complete task issue in GitHub Markdown. Follow the structure below exactly — use the same section headings as the template (`Task Description ✏️`, `Acceptance Criteria ✅`, etc.). +Using the gathered context, write a complete task issue in GitHub Markdown. Follow the structure below exactly - use the same section headings as the template (`Task Description ✏️`, `Acceptance Criteria ✅`, etc.). Guidelines for each section: -**Title** — action-oriented, starts with a verb, e.g. "Implement Box component" or "Migrate API auth to JWT". Prepend `[Task]` internally but do NOT include it in the `## Title` line of the output. +**Title** - action-oriented, starts with a verb, e.g. "Implement Box component" or "Migrate API auth to JWT". Prepend `[Task]` internally but do NOT include it in the `## Title` line of the output. **Task Description ✏️** -- 2–4 sentences: what is being built/changed, why it matters, which epic or story it belongs to (reference issue number if inferable from context). -- Be concrete — mention the component, module, or area being touched. +- 2-4 sentences: what is being built/changed, why it matters, which epic or story it belongs to (reference issue number if inferable from context). +- Be concrete - mention the component, module, or area being touched. **Acceptance Criteria ✅** - One checkbox per verifiable condition. - Cover: implementation, props/API, edge cases, tests (unit + Storybook if relevant), lint/types/CI. -- Match the granularity of the Avatar example — each item should be independently testable. +- Match the granularity of the Avatar example - each item should be independently testable. **Dependencies 🔗** @@ -52,7 +52,7 @@ Guidelines for each section: --- -## Step 3 — Output the draft +## Step 3 - Output the draft Print the full issue content to the conversation in a single markdown code block so the user can copy and paste it directly into GitHub's new-issue form under the **Task 🛠️** template. diff --git a/.claude/skills/Electron/SKILL.md b/.claude/skills/Electron/SKILL.md index 812c0fd2..b867db72 100644 --- a/.claude/skills/Electron/SKILL.md +++ b/.claude/skills/Electron/SKILL.md @@ -28,13 +28,13 @@ The preload path must point at the **built** output, not the source file. ### All renderer↔main traffic flows through `contextBridge` -Never expose `ipcRenderer` itself — wrap each channel in a named function. +Never expose `ipcRenderer` itself - wrap each channel in a named function. ```ts -// ✗ preload.ts — renderer gets full IPC access +// ✗ preload.ts - renderer gets full IPC access contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer) -// ✓ preload.ts — typed, named surface +// ✓ preload.ts - typed, named surface contextBridge.exposeInMainWorld('electronAPI', { loadPreferences: () => ipcRenderer.invoke('load-prefs'), saveFile: (content: string) => ipcRenderer.invoke('save-file', content), @@ -71,7 +71,7 @@ session.defaultSession.webRequest.onHeadersReceived((details, callback) => { `send`/`on` has no return value and no error propagation. Use `invoke`/`handle` for anything that produces a result. ```ts -// ✗ — no result, no error path +// ✗ - no result, no error path ipcRenderer.send('save-file', content) ipcMain.on('save-file', (_e, content) => fs.writeFile(...)) @@ -93,15 +93,15 @@ For larger apps, prefer `electron-trpc` (tRPC router + Zod input validation) ove --- -### Wrap every IPC response in a Result type — Electron only serializes `Error.message` +### Wrap every IPC response in a Result type - Electron only serializes `Error.message` ```ts -// ✗ — stack, cause, custom fields all lost crossing the IPC boundary +// ✗ - stack, cause, custom fields all lost crossing the IPC boundary ipcMain.handle('save-file', async (_e, content: string) => { await fs.writeFile(filePath, content) // throws raw Error }) -// ✓ — full error context preserved as data +// ✓ - full error context preserved as data ipcMain.handle('save-file', async (_e, content: string) => { try { await fs.writeFile(filePath, content) @@ -112,7 +112,7 @@ ipcMain.handle('save-file', async (_e, content: string) => { }) ``` -Validate every argument received from the renderer (Zod or manual checks) — it crossed a trust boundary. +Validate every argument received from the renderer (Zod or manual checks) - it crossed a trust boundary. Group related handlers into one module per domain: @@ -127,7 +127,7 @@ export function registerFileHandlers(): void { ## React integration: IPC listeners always return a cleanup -Strict Mode double-invokes effects — an IPC listener without cleanup leaks a duplicate handler per mount. +Strict Mode double-invokes effects - an IPC listener without cleanup leaks a duplicate handler per mount. ```ts // ✗ @@ -142,7 +142,7 @@ useEffect(() => { }, []) ``` -For multi-window apps, the main process is the single source of truth for shared state (`electron-store` + IPC broadcast to all windows) — never mutate state window-to-window directly. +For multi-window apps, the main process is the single source of truth for shared state (`electron-store` + IPC broadcast to all windows) - never mutate state window-to-window directly. --- @@ -161,7 +161,7 @@ src/ └── index.html ``` -Use `electron-vite` for dev (unified main/preload/renderer config, instant HMR) and Electron Forge for packaging/signing/notarizing — not webpack-based toolchains or manual packaging. +Use `electron-vite` for dev (unified main/preload/renderer config, instant HMR) and Electron Forge for packaging/signing/notarizing - not webpack-based toolchains or manual packaging. --- diff --git a/.claude/skills/ci-cd/SKILL.md b/.claude/skills/ci-cd/SKILL.md index 467de1bd..fc46ae9b 100644 --- a/.claude/skills/ci-cd/SKILL.md +++ b/.claude/skills/ci-cd/SKILL.md @@ -1,11 +1,11 @@ --- -description: GitHub Actions CI/CD conventions for this repo — the CI entry workflow calling one workflow per area (prepare → lint → packages/worker/app → ci-ok, with app → web/editor), the action-pinning rule (version tags for `actions/*`, commit SHAs for everything else including our own org), per-workspace Codecov flags with tokenless-OIDC uploads, the CI-environment approval gate (with env-scoped vars forwarded to environment-less jobs via job outputs), Cloudflare deploys via cloudflare/wrangler-action, and the standalone Chromatic workflow. Use when adding, editing, or debugging any workflow under .github/workflows/. +description: GitHub Actions CI/CD conventions for this repo - the CI entry workflow calling one workflow per area (prepare → lint → packages/worker/app → ci-ok, with app → web/editor), the action-pinning rule (version tags for `actions/*`, commit SHAs for everything else including our own org), per-workspace Codecov flags with tokenless-OIDC uploads, the CI-environment approval gate (with env-scoped vars forwarded to environment-less jobs via job outputs), Cloudflare deploys via cloudflare/wrangler-action, and the standalone Chromatic workflow. Use when adding, editing, or debugging any workflow under .github/workflows/. paths: .github/workflows/** --- # CI/CD (GitHub Actions) -Each workflow has a per-file deep-dive doc next to it (`ci.md`, `cd-*.md`, `chromatic.md`) with the full step-by-step — **read it before editing that workflow**. This skill is the rulebook, not a second copy of those docs. +Each workflow has a per-file deep-dive doc next to it (`ci.md`, `cd-*.md`, `chromatic.md`) with the full step-by-step - **read it before editing that workflow**. This skill is the rulebook, not a second copy of those docs. ## Workflow files @@ -19,55 +19,55 @@ Each workflow has a per-file deep-dive doc next to it (`ci.md`, `cd-*.md`, `chro | `ci-editor.yml` | `CI · Editor` | `workflow_call` from `ci-app.yml` | | `cd-web.yml` | `CD · Web (Pages + Storybook)` | `workflow_run` of CI (success, `main`) + dispatch | | `cd-worker-api.yml` | `CD · Worker (api)` | `workflow_run` of CI (success, `main`) + dispatch | -| `cd-packages.yml` | `CD · Packages (npm)` | manual `workflow_dispatch` only — see the `release-notes` skill | -| `cd-editor.yml` | `CD · Editor (release)` | manual `workflow_dispatch` only — draft GitHub Release of the installers | +| `cd-packages.yml` | `CD · Packages (npm)` | manual `workflow_dispatch` only - see the `release-notes` skill | +| `cd-editor.yml` | `CD · Editor (release)` | manual `workflow_dispatch` only - draft GitHub Release of the installers | | `chromatic.yml` | `Chromatic` | `pull_request` + `push` to `main` + `workflow_dispatch` (main), non-blocking | | `label-area.yml` | `Label Affected Area` | `issues: opened` | -One CI entry workflow calling one per area; CD is separate and **gated on CI success** — never deploy on a raw `push`. +One CI entry workflow calling one per area; CD is separate and **gated on CI success** - never deploy on a raw `push`. -**Naming.** Every workflow is `CI · ` or `CD · ()`, so the Actions sidebar groups into two blocks; the entry workflow is plain `CI`. Chromatic and the labeller stay **unprefixed on purpose** — neither is part of `ci-ok`, and prefixing them would say they gate PRs. **Renaming a workflow is never a one-file edit**: `workflow_run` matches on the workflow's `name:`, not its filename, so `cd-web` / `cd-worker-api` / `cd-worker-bench` all pin `workflows: ['CI']` and a rename that misses one silently stops that deploy for good. Branch protection is unaffected — it matches the **job** name `ci-ok`. +**Naming.** Every workflow is `CI · ` or `CD · ()`, so the Actions sidebar groups into two blocks; the entry workflow is plain `CI`. Chromatic and the labeller stay **unprefixed on purpose** - neither is part of `ci-ok`, and prefixing them would say they gate PRs. **Renaming a workflow is never a one-file edit**: `workflow_run` matches on the workflow's `name:`, not its filename, so `cd-web` / `cd-worker-api` / `cd-worker-bench` all pin `workflows: ['CI']` and a rename that misses one silently stops that deploy for good. Branch protection is unaffected - it matches the **job** name `ci-ok`. -## Action pinning convention — the load-bearing rule +## Action pinning convention - the load-bearing rule Pin every `uses:` by the action's **origin**. Getting this wrong fails review: CodeRabbit flags SHA-pinned `actions/*`; SonarQube flags anything else on a version tag. -- **GitHub's own** — `actions/*` (checkout, setup-node, cache, upload-artifact, github-script) → **version tag**: `actions/checkout@v5`. +- **GitHub's own** - `actions/*` (checkout, setup-node, cache, upload-artifact, github-script) → **version tag**: `actions/checkout@v5`. - **Everything else, our own org included** (`soroush-tech/bench-action`, `pnpm/action-setup`, `codecov/codecov-action`, `cloudflare/wrangler-action`, `chromaui/action`, `dorny/paths-filter`) → **commit SHA** + `# vX` comment. -Own-org used to sit with `actions/*` on a tag. It does not any more: what SHA-pinning defends against is a tag being moved, and our own tags move like anyone's. The SHA is bumped when the action releases, which is the point — the upgrade is a reviewed line, not a silent one. +Own-org used to sit with `actions/*` on a tag. It does not any more: what SHA-pinning defends against is a tag being moved, and our own tags move like anyone's. The SHA is bumped when the action releases, which is the point - the upgrade is a reviewed line, not a silent one. ## CI job shape `prepare` → `lint` → three **caller jobs** (`packages`, `worker`, `app`) → `ci-ok`. Each caller `uses:` an area workflow; `ci-app.yml` calls one workflow per app in turn, so adding an app never touches the entry file. Nesting is three of the four levels GitHub allows, and it stays one run with one `ci-ok`. - **Detect once in `prepare`** (node version from `.nvmrc`, package manager, runner, changed areas), reuse via `needs.prepare.outputs.*`. Never hard-code the node version. -- **Every job that installs starts from `./.github/actions/setup`** — the composite action holding pnpm, Node and the install. **Never re-inline those steps**: the whole point is that the `pnpm/action-setup` pin and the store cache are a one-file edit. A new job is a checkout (the action cannot carry it — a local action is resolved from the working tree it checks out) plus a call with `node_version`/`manager`/`command`. Per-area caches (Playwright binaries, the Electron binary) stay in the job, just after the call. The action carries its own `# ci:validates all` marker, so editing it re-runs every job without setting `changes.root` — nothing deploys off a CI edit. -- **A file per area, so the gate can be narrower than everything.** Each workflow declares its scope on line 1 (`# ci:validates pkg__*`), read by `scripts/assemble-changes.mjs`; unmarked or unparseable means the whole workspace. A caller job cannot set `environment:`/`timeout-minutes:`/`runs-on:` (those belong to the inner jobs), and **`secrets: inherit` is mandatory, per hop** — naming an environment-scoped secret at the call site passes an empty string, and a middle layer that omits it starves the workflow below. -- **One job per shape, not per member.** Packages, workers and the editor's unit tier are the same job — install, `test:coverage`, upload the lcov — so packages are one matrix and workers another, both built from the tree in `scripts/assemble-changes.mjs`. **Adding a workspace member must need no edit to `ci.yml`**: if a new area needs a job, ask first whether it is really a different shape (`web` builds; `editor-e2e` drives Electron) or just another row. -- Heavy jobs are **change-gated** (`dorny/paths-filter`, no Nx/Turbo) so a package-only PR stays cheap. Dependency edges are **derived, never listed**: a member runs when it changed or when a package it declares as a `workspace:` dependency changed. Do not add a hand-written consumer list — it is a list to forget the day a dependency moves. -- **A workflow file validates what it runs**, and says so itself on line 1. `ci.yml` is `all`; a `cd-*`, Chromatic or the labeller is `nothing`, because CI never executes them. Same for `labs/*`, which has no job at all. The marker line takes tokens only — prose on the line below, or a stray `nothing` in it silently narrows the file to zero. +- **Every job that installs starts from `./.github/actions/setup`** - the composite action holding pnpm, Node and the install. **Never re-inline those steps**: the whole point is that the `pnpm/action-setup` pin and the store cache are a one-file edit. A new job is a checkout (the action cannot carry it - a local action is resolved from the working tree it checks out) plus a call with `node_version`/`manager`/`command`. Per-area caches (Playwright binaries, the Electron binary) stay in the job, just after the call. The action carries its own `# ci:validates all` marker, so editing it re-runs every job without setting `changes.root` - nothing deploys off a CI edit. +- **A file per area, so the gate can be narrower than everything.** Each workflow declares its scope on line 1 (`# ci:validates pkg__*`), read by `scripts/assemble-changes.mjs`; unmarked or unparseable means the whole workspace. A caller job cannot set `environment:`/`timeout-minutes:`/`runs-on:` (those belong to the inner jobs), and **`secrets: inherit` is mandatory, per hop** - naming an environment-scoped secret at the call site passes an empty string, and a middle layer that omits it starves the workflow below. +- **One job per shape, not per member.** Packages, workers and the editor's unit tier are the same job - install, `test:coverage`, upload the lcov - so packages are one matrix and workers another, both built from the tree in `scripts/assemble-changes.mjs`. **Adding a workspace member must need no edit to `ci.yml`**: if a new area needs a job, ask first whether it is really a different shape (`web` builds; `editor-e2e` drives Electron) or just another row. +- Heavy jobs are **change-gated** (`dorny/paths-filter`, no Nx/Turbo) so a package-only PR stays cheap. Dependency edges are **derived, never listed**: a member runs when it changed or when a package it declares as a `workspace:` dependency changed. Do not add a hand-written consumer list - it is a list to forget the day a dependency moves. +- **A workflow file validates what it runs**, and says so itself on line 1. `ci.yml` is `all`; a `cd-*`, Chromatic or the labeller is `nothing`, because CI never executes them. Same for `labs/*`, which has no job at all. The marker line takes tokens only - prose on the line below, or a stray `nothing` in it silently narrows the file to zero. - **`web` is ubuntu-only** (build + unit/browser/storybook coverage). **`e2e` is the only multi-OS matrix** (one Playwright engine per native OS; macOS ≈10× cost → WebKit only) and **`needs: web`**, so a `web` failure skips it instead of re-running three OSes. - **`ci-ok`** is the single branch-protection check: `if: always()`, fails only on a needed job's `failure`/`cancelled` (change-gated skips pass). **Add every new job to its `needs`.** ## Coverage → Codecov - Each workspace emits `coverage/lcov.info` and uploads under its **own flag** (`codecov/codecov-action`, SHA-pinned); register each area as a `.codecov.yml` component. Vitest configs set `reporter: ['text', 'lcov']`; 100% is enforced in `vitest.config` (`thresholds: { 100: true }`), Codecov is reporting only. -- The **`web` flag is the single merged `test:coverage` pass** — that's the patch gate. The per-tier `unit`/`browser`/`storybook` flags run `all: true` and stay **informational** (don't gate on them — phantom-uncovered lines). `e2e` is the chromium-only page-coverage flag. +- The **`web` flag is the single merged `test:coverage` pass** - that's the patch gate. The per-tier `unit`/`browser`/`storybook` flags run `all: true` and stay **informational** (don't gate on them - phantom-uncovered lines). `e2e` is the chromium-only page-coverage flag. - A matrix row's flag is the **unscoped package name**, not its directory: two members are called `bench`, and their flags are `bench` (the package) and `bench-api` (the worker). The editor splits the same way as the web app: `editor` for the unit tier, `editor-e2e` for the Electron run. ## The `environment: CI` approval gate -`environment: CI` on `web`/`packages`/`worker` is a **required-reviewer gate**. One approval covers every job **already waiting** in that wave — but a job reaching the gate **later** (e.g. `e2e`, which `needs: web`) prompts a **second** approval. Keep such a job **off** the environment. When it still needs an env-scoped value, **forward it through an already-gated job's `outputs`** rather than joining the environment: `e2e` needs the CI-environment-scoped `VITE_BASE_URL` (its dev-server SSR fetch needs an absolute base — else the relative `/gists/:id` URL throws in Node and the article-page e2e fails on a placeholder title), so `web` reads it (`outputs.vite_base_url: ${{ vars.VITE_BASE_URL }}`) and `e2e` consumes `needs.web.outputs.vite_base_url`. Only works for **non-secret** values (job outputs redact secrets). e2e also uploads to Codecov **tokenlessly via OIDC** (`use_oidc: true` + job `permissions: { id-token: write }`, works on this public repo), so its coverage upload doesn't depend on the env-scoped `CODECOV_TOKEN` either. +`environment: CI` on `web`/`packages`/`worker` is a **required-reviewer gate**. One approval covers every job **already waiting** in that wave - but a job reaching the gate **later** (e.g. `e2e`, which `needs: web`) prompts a **second** approval. Keep such a job **off** the environment. When it still needs an env-scoped value, **forward it through an already-gated job's `outputs`** rather than joining the environment: `e2e` needs the CI-environment-scoped `VITE_BASE_URL` (its dev-server SSR fetch needs an absolute base - else the relative `/gists/:id` URL throws in Node and the article-page e2e fails on a placeholder title), so `web` reads it (`outputs.vite_base_url: ${{ vars.VITE_BASE_URL }}`) and `e2e` consumes `needs.web.outputs.vite_base_url`. Only works for **non-secret** values (job outputs redact secrets). e2e also uploads to Codecov **tokenlessly via OIDC** (`use_oidc: true` + job `permissions: { id-token: write }`, works on this public repo), so its coverage upload doesn't depend on the env-scoped `CODECOV_TOKEN` either. ## Deploys -- **Gated on CI success**: `workflow_run` of CI + `workflow_dispatch`; a `changes` job re-derives what changed from CI's `changes.json` artifact (workflow_run carries no diff base). `concurrency: cancel-in-progress: false` — never abort an in-flight deploy. -- **Cloudflare deploys go through `cloudflare/wrangler-action` (SHA-pinned), not the wrangler CLI** — both the Worker (`command: deploy`) and Storybook Pages (`command: pages deploy`), with `apiToken`/`accountId` inputs. The worker's `wrangler.json` is generated **before** the action (`pnpm --filter @soroush/api config:gen`), since the action runs `wrangler deploy` directly and won't fire the package's `predeploy` hook. +- **Gated on CI success**: `workflow_run` of CI + `workflow_dispatch`; a `changes` job re-derives what changed from CI's `changes.json` artifact (workflow_run carries no diff base). `concurrency: cancel-in-progress: false` - never abort an in-flight deploy. +- **Cloudflare deploys go through `cloudflare/wrangler-action` (SHA-pinned), not the wrangler CLI** - both the Worker (`command: deploy`) and Storybook Pages (`command: pages deploy`), with `apiToken`/`accountId` inputs. The worker's `wrangler.json` is generated **before** the action (`pnpm --filter @soroush/api config:gen`), since the action runs `wrangler deploy` directly and won't fire the package's `predeploy` hook. ## Chromatic -Visual review is its **own non-blocking workflow** (`chromatic.yml`), split out of CI so an exhausted plan can't block PR CI — **not** part of `ci-ok`. A `workflow_dispatch` is restricted to the main ref. +Visual review is its **own non-blocking workflow** (`chromatic.yml`), split out of CI so an exhausted plan can't block PR CI - **not** part of `ci-ok`. A `workflow_dispatch` is restricted to the main ref. ## Cost & speed defaults diff --git a/.claude/skills/code-style/SKILL.md b/.claude/skills/code-style/SKILL.md index 2eacfb2e..3e9b813f 100644 --- a/.claude/skills/code-style/SKILL.md +++ b/.claude/skills/code-style/SKILL.md @@ -5,7 +5,7 @@ argument-hint: [filename] ## Rules -### Extract the varying part — never repeat surrounding structure +### Extract the varying part - never repeat surrounding structure When a conditional changes only one part of a value, extract that part. Never repeat the template. @@ -20,7 +20,7 @@ const borderColor = variant === 'outlined' ? main : 'transparent' border: `${theme.borderWidths.thin} solid ${borderColor}` ``` -Applies everywhere: template literals, object spreads, JSX props — anywhere the surrounding structure is identical across branches. +Applies everywhere: template literals, object spreads, JSX props - anywhere the surrounding structure is identical across branches. --- @@ -48,12 +48,12 @@ if (error) { When a prop maps one-to-one to a CSS property + theme scale, always use `system()`. Never use a raw theme function or `variant()` for this. -`variant()` is for one prop → **multiple** CSS properties across **multiple** theme scales. However, `variant()` calls `css(styles)(props)` internally, which only resolves theme scale indices correctly when a ThemeProvider is present — it silently falls back to raw pixel values without one. +`variant()` is for one prop → **multiple** CSS properties across **multiple** theme scales. However, `variant()` calls `css(styles)(props)` internally, which only resolves theme scale indices correctly when a ThemeProvider is present - it silently falls back to raw pixel values without one. For one prop → multiple CSS properties → multiple theme scales, use a **raw theme function** instead. It receives `props.theme` directly from Emotion and always resolves correctly: ```ts -// ✗ — variant(): theme scale resolution breaks without ThemeProvider +// ✗ - variant(): theme scale resolution breaks without ThemeProvider const sizeVariant = variant({ prop: 'size', variants: { @@ -62,7 +62,7 @@ const sizeVariant = variant({ }, }) -// ✓ — raw theme function: reads active theme directly, always works +// ✓ - raw theme function: reads active theme directly, always works const sizeVariant = ({ theme, size = 'md' }: { theme?: Theme; size?: AppBarSize }) => { const { paddingTop, paddingBottom, paddingLeft, paddingRight, fontSize } = sizes[size] return { @@ -75,7 +75,7 @@ const sizeVariant = ({ theme, size = 'md' }: { theme?: Theme; size?: AppBarSize } ``` -Destructure the size config at the top so each return line only references a simple variable. Use `theme?.space?.[key]` — optional chaining handles undefined theme without an intermediate variable or type cast. +Destructure the size config at the top so each return line only references a simple variable. Use `theme?.space?.[key]` - optional chaining handles undefined theme without an intermediate variable or type cast. Never hardcode a theme scale value in a raw theme function. Use `system()` instead: @@ -92,12 +92,12 @@ const elevationVariant = system({ ``` ```ts -// ✗ — raw theme function: hardcoded, not prop-driven +// ✗ - raw theme function: hardcoded, not prop-driven const elevationStyle = ({ theme }: { theme?: Theme }) => ({ boxShadow: theme?.shadows[4] ?? 'none', }) -// ✗ — variant(): hardcoded index list, can't read string[] scale directly +// ✗ - variant(): hardcoded index list, can't read string[] scale directly const elevationVariant = variant({ prop: 'elevation', variants: { @@ -107,7 +107,7 @@ const elevationVariant = variant({ }, }) -// ✓ — system(): prop-driven, reads theme.shadows[elevation] directly +// ✓ - system(): prop-driven, reads theme.shadows[elevation] directly const elevationVariant = system({ elevation: { property: 'boxShadow', scale: 'shadows' }, }) @@ -129,10 +129,10 @@ const colorSystem = system({ Every `index.ts` that exists solely to re-export a module must use `export *`. Named re-exports in barrels require manual upkeep and silently omit new exports until someone notices. ```ts -// ✗ — named re-export: new exports must be added by hand +// ✗ - named re-export: new exports must be added by hand export { StylesConsumer, withStyles, withTheme } from './withTheme' -// ✓ — wildcard: all exports picked up automatically +// ✓ - wildcard: all exports picked up automatically export * from './withTheme' ``` @@ -140,19 +140,19 @@ This applies to every barrel in the project (`src///index.ts`, `sr --- -### `react-refresh/only-export-components` — extract, never disable +### `react-refresh/only-export-components` - extract, never disable When a file mixes React components with non-component exports (HOFs, factories, plain functions) and triggers `react-refresh/only-export-components`, extract each non-component export into its own colocated file. Never suppress with `eslint-disable`. ```ts -// ✗ — withTheme.tsx: mixes component + HOFs, requires eslint-disable +// ✗ - withTheme.tsx: mixes component + HOFs, requires eslint-disable // eslint-disable-next-line react-refresh/only-export-components export function withStyles(...) { ... } // eslint-disable-next-line react-refresh/only-export-components export function withTheme(...) { ... } export function StylesConsumer(...) { ... } // component -// ✓ — three colocated files, no suppression needed +// ✓ - three colocated files, no suppression needed // StylesConsumer.tsx → exports only the component // withStyles.ts → exports only withStyles // withTheme.ts → exports only withTheme @@ -167,7 +167,7 @@ Name each file after what it exports. Keep all files in the same directory. An inline `renderX` function inside a component is fine while it stays a few lines. Once it closes over a hook/instance value (so it can't move to `utils.ts`) **and** its return is a self-contained, nameable unit of UI, extract it into a sibling component instead. The inline closure is re-created every render, can't be unit-tested in isolation, and inflates the parent until it reads as internals rather than layout + composition. ```tsx -// ✗ — inline render closure: captures `form`, grown to a cohesive UI block, only testable through the parent +// ✗ - inline render closure: captures `form`, grown to a cohesive UI block, only testable through the parent function Parent() { const form = useParentForm() const renderItem = (item: ItemDef) => ( @@ -176,7 +176,7 @@ function Parent() { return <>{items.map(renderItem)} } -// ✓ — colocated component: the captured instance + item passed as props, unit-testable, parent stays compositional +// ✓ - colocated component: the captured instance + item passed as props, unit-testable, parent stays compositional // Item.tsx export function Item({ form, item }: { form: ReturnType; item: ItemDef }) { return {(api) => /* label + control + error */} @@ -185,28 +185,28 @@ export function Item({ form, item }: { form: ReturnType; i {items.map((item) => )} ``` -Trigger: it captures something component-local (so `utils.ts` is out) **and** the render body is a cohesive UI unit. A short formatter or a one-line wrapper stays inline. Watch for the gradual case — each addition is individually reasonable, the sum crosses the line. This is a threshold, not a hard rule. +Trigger: it captures something component-local (so `utils.ts` is out) **and** the render body is a cohesive UI unit. A short formatter or a one-line wrapper stays inline. Watch for the gradual case - each addition is individually reasonable, the sum crosses the line. This is a threshold, not a hard rule. --- ### Section data lives in `SectionName.data.ts`, shared by component and test -A section's content data (arrays of cards, steps, tags, stats) goes in a sibling `SectionName.data.ts` file — never inline in the `.tsx`, and never `export`ed from it (that trips `react-refresh/only-export-components`). The component **and** its `*.test.tsx` both import from the data file, so the test iterates over the real data instead of a hardcoded copy that silently drifts when the data changes. +A section's content data (arrays of cards, steps, tags, stats) goes in a sibling `SectionName.data.ts` file - never inline in the `.tsx`, and never `export`ed from it (that trips `react-refresh/only-export-components`). The component **and** its `*.test.tsx` both import from the data file, so the test iterates over the real data instead of a hardcoded copy that silently drifts when the data changes. ```ts -// SectionName.data.ts — single source of truth +// SectionName.data.ts - single source of truth export const steps = [ - { number: '01', title: 'AGILE INFRASTRUCTURE', body: '…' }, - // … + { number: '01', title: 'AGILE INFRASTRUCTURE', body: '...' }, + // ... ] // SectionName.tsx import { steps } from './SectionName.data' -// SectionName.test.tsx — iterate, don't hardcode +// SectionName.test.tsx - iterate, don't hardcode import { steps } from './SectionName.data' it.each(steps)('renders step $number ($title)', ({ number, title }) => { - /* … */ + /* ... */ }) ``` @@ -214,18 +214,18 @@ Assert flat string lists with `getAllByText(x).length > 0` (safe against repeats --- -### Images and SVGs belong in `src/assets/` — never inline, never from `public/` +### Images and SVGs belong in `src/assets/` - never inline, never from `public/` All static images (PNG, JPG, WebP) and SVG files live in `src/assets/`. Import as Vite module URLs and render with `` or the theme `` component. `public/` is reserved for browser-served files that need a fixed URL (favicon, service workers, MSW worker) and must never be imported in source code. ```ts -// ✗ — importing from public/ +// ✗ - importing from public/ import logo from '/soroush.svg' -// ✗ — inline SVG JSX in a component file +// ✗ - inline SVG JSX in a component file const SunIcon = () => ... -// ✓ — SVG file in src/assets/, imported as URL +// ✓ - SVG file in src/assets/, imported as URL import sunSvg from 'src/assets/sun.svg' ``` @@ -236,16 +236,16 @@ Note: `stroke="currentColor"` / `fill="currentColor"` in an SVG file does not in ### Responsive images with `vite-imagetools` -Raster images that the browser scales (any photo/mascot rendered smaller than its file, or served to varying viewports) must be imported through `vite-imagetools` query params — never as a bare URL that ships one oversized file. +Raster images that the browser scales (any photo/mascot rendered smaller than its file, or served to varying viewports) must be imported through `vite-imagetools` query params - never as a bare URL that ships one oversized file. -**Multi-format `` — the default for any meaningful image.** Import with `?w=…&format=avif;webp;png&as=picture`, listing modern formats first and the fallback format last. The import resolves to `{ sources, img }`: `sources` is keyed by the non-fallback formats, `img` is the last format's fallback descriptor. Render a `` per format with the **fallback as the ``** — the `` _is_ the png; old browsers that ignore `` fall through to it. Never give the fallback format its own ``. +**Multi-format `` - the default for any meaningful image.** Import with `?w=...&format=avif;webp;png&as=picture`, listing modern formats first and the fallback format last. The import resolves to `{ sources, img }`: `sources` is keyed by the non-fallback formats, `img` is the last format's fallback descriptor. Render a `` per format with the **fallback as the ``** - the `` _is_ the png; old browsers that ignore `` fall through to it. Never give the fallback format its own ``. ```tsx -// ✗ — bare import ships one fixed-size file, one format +// ✗ - bare import ships one fixed-size file, one format import portrait from 'src/assets/portrait.png' -// ✓ — multi-format, multi-width +// ✓ - multi-format, multi-width import portrait from 'src/assets/portrait.png?w=320;480;640;960&format=avif;webp;png&as=picture' const SIZES = '(min-width: 832px) min(40vw, 520px), 100vw' @@ -254,24 +254,24 @@ const SIZES = '(min-width: 832px) min(40vw, 520px), 100vw' {Object.entries(portrait.sources).map(([format, srcSet]) => ( ))} - … + ... ``` -Single-format responsive (no ``) uses `?w=…&as=srcset&format=avif`, which resolves to a `srcSet` string. Each query shape needs a `declare module` in `src/vite-env.d.ts` — `*as=picture` returns `{ sources: Record; img: { src: string; w: number; h: number } }`, `*format=avif` returns a `string`. +Single-format responsive (no ``) uses `?w=...&as=srcset&format=avif`, which resolves to a `srcSet` string. Each query shape needs a `declare module` in `src/vite-env.d.ts` - `*as=picture` returns `{ sources: Record; img: { src: string; w: number; h: number } }`, `*format=avif` returns a `string`. -**`sizes` must match the real rendered width**, computed from the layout — not guessed. Work out the slot width at each styled-system breakpoint (defaults: **40em / 52em / 64em**; a responsive prop array `['a','b','c']` maps to base / 40em / 52em). For an `objectFit: cover` box with a square source, the **taller** side binds, so size for that. +**`sizes` must match the real rendered width**, computed from the layout - not guessed. Work out the slot width at each styled-system breakpoint (defaults: **40em / 52em / 64em**; a responsive prop array `['a','b','c']` maps to base / 40em / 52em). For an `objectFit: cover` box with a square source, the **taller** side binds, so size for that. ```tsx // grid switches to two columns at 52em (832px); portrait is the ~520px 5fr column above it const SIZES = '(min-width: 832px) min(40vw, 520px), 100vw' ``` -**Never request widths above the source's intrinsic size** — imagetools does not upscale, so larger entries just clamp to the source and waste descriptors. Check the source dimensions first; a 512px source can't serve a 2× retina slot — that's an asset problem, flag it rather than padding the width list. +**Never request widths above the source's intrinsic size** - imagetools does not upscale, so larger entries just clamp to the source and waste descriptors. Check the source dimensions first; a 512px source can't serve a 2× retina slot - that's an asset problem, flag it rather than padding the width list. -**LCP images get `fetchPriority="high"`.** But `theme/Image` filters props through `@styled-system/should-forward-prop`, which pins an old `@emotion/is-prop-valid` that strips `fetchPriority`. It is already allow-listed in `Image.tsx`'s `shouldForwardProp` — keep it there; if a similar valid attribute is dropped, extend that allow-list rather than working around it. +**LCP images get `fetchPriority="high"`.** But `theme/Image` filters props through `@styled-system/should-forward-prop`, which pins an old `@emotion/is-prop-valid` that strips `fetchPriority`. It is already allow-listed in `Image.tsx`'s `shouldForwardProp` - keep it there; if a similar valid attribute is dropped, extend that allow-list rather than working around it. -**A `` is `display: inline` and static**, so an absolutely-positioned `` inside it still resolves against the nearest positioned ancestor (the layout box), not the ``. Put `position`/`top`/`left` on the `` as before — the `` wrapper changes nothing about positioning. +**A `` is `display: inline` and static**, so an absolutely-positioned `` inside it still resolves against the nearest positioned ancestor (the layout box), not the ``. Put `position`/`top`/`left` on the `` as before - the `` wrapper changes nothing about positioning. --- diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md index 3e66568d..638015e7 100644 --- a/.claude/skills/commit/SKILL.md +++ b/.claude/skills/commit/SKILL.md @@ -1,5 +1,5 @@ --- -description: How to write git commits in this repo that auto-close their GitHub task — a conventional-commits header with the closing keyword appended to the end of the subject line. Use when committing work that resolves a task or issue. +description: How to write git commits in this repo that auto-close their GitHub task - a conventional-commits header with the closing keyword appended to the end of the subject line. Use when committing work that resolves a task or issue. --- # Commit @@ -17,7 +17,7 @@ structure: [optional footer] ``` -### Types — use only these +### Types - use only these `fix`, `feat`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`. @@ -25,12 +25,12 @@ structure: | ---------- | ------------------------------------------------------- | ------ | | `feat` | A new feature | MINOR | | `fix` | A bug fix | PATCH | -| `docs` | Documentation only | — | -| `style` | Formatting, whitespace — no code-behavior change | — | -| `refactor` | Code change that neither fixes a bug nor adds a feature | — | -| `perf` | Performance improvement | — | -| `test` | Adding or correcting tests | — | -| `chore` | Maintenance that fits no other type | — | +| `docs` | Documentation only | - | +| `style` | Formatting, whitespace - no code-behavior change | - | +| `refactor` | Code change that neither fixes a bug nor adds a feature | - | +| `perf` | Performance improvement | - | +| `test` | Adding or correcting tests | - | +| `chore` | Maintenance that fits no other type | - | No other types. Things like CI, build, or deps are expressed as a **scope** on one of these types, never as the type itself. @@ -38,7 +38,7 @@ of these types, never as the type itself. ### Scope A scope in parentheses adds context: `feat(parser): add ability to parse arrays`. -Use a scope for the area touched — e.g. CI/CD changes are `fix(CI):` / +Use a scope for the area touched - e.g. CI/CD changes are `fix(CI):` / `chore(CI):`, **not** `ci:`. ### Breaking changes @@ -65,7 +65,7 @@ chore(CI): gate lint, packages, and worker jobs behind the CI environment review ## One commit per task; closing keyword at the end of the header -A commit that completes a GitHub task ends its **subject line** (the header — the +A commit that completes a GitHub task ends its **subject line** (the header - the first line) with the closing keyword and the task number: ```text @@ -80,7 +80,7 @@ feat(schema): extract contact validation into shared @soroush.tech/schema - clos - One task → one commit. Commit tasks separately, in dependency order. - The keyword goes at the **end of the header**, never in a footer line. -- `close` / `closes` / `closed` all work — keep `#` immediately after. +- `close` / `closes` / `closed` all work - keep `#` immediately after. - Several tasks in one commit: `... - close #155, close #156`. ## Don't close issues by hand diff --git a/.claude/skills/css-in-js/SKILL.md b/.claude/skills/css-in-js/SKILL.md index a4323eea..2b3b68cc 100644 --- a/.claude/skills/css-in-js/SKILL.md +++ b/.claude/skills/css-in-js/SKILL.md @@ -6,13 +6,13 @@ paths: @soroush.tech/design-system/** ## Canonical reference -Read `@soroush.tech/design-system/Avatar/Avatar.tsx` before writing any new component — it is the correct styled-system pattern. +Read `@soroush.tech/design-system/Avatar/Avatar.tsx` before writing any new component - it is the correct styled-system pattern. --- ## Rules -### 0. All engine imports come from `@soroush.tech/design-system` — never from the underlying packages +### 0. All engine imports come from `@soroush.tech/design-system` - never from the underlying packages Inside `@soroush.tech/design-system/`, every styling primitive must be imported from the barrel, not from its original package: @@ -40,11 +40,11 @@ This applies to every file in `@soroush.tech/design-system/` and `src/common/`. | Custom prop → theme scale | `system({ gap: { property: 'gap', scale: 'space' } })` | | Shape / state variants | `variant({ prop, variants: { key: { cssProperty: themeKey } } })` | | Font weight, letter spacing | `typography()` + prop default in wrapper | -| Static styles (no theme access) | Plain object — no function | +| Static styles (no theme access) | Plain object - no function | `variant()` resolves string keys against theme scales automatically: `{ borderRadius: 'md' }` → `theme.radii.md`. -### 2. Static base styles — no function unless theme access is required +### 2. Static base styles - no function unless theme access is required ```ts // ✗ @@ -59,7 +59,7 @@ Component-specific config stays in the component file. If only one component use ### 4. All variants must occupy equal space -Use transparent borders on borderless variants — never remove the border entirely. +Use transparent borders on borderless variants - never remove the border entirely. ```ts contained: { @@ -81,12 +81,12 @@ text: { const safeLayout = ({ size: _size, ...rest }: object) => layout(rest) ``` -Do not rename the public API — fix the conflict internally. +Do not rename the public API - fix the conflict internally. ### 6. Typography defaults as overridable props ```ts -// ✓ — resolved through typography() against theme scales; user can override +// ✓ - resolved through typography() against theme scales; user can override export function Button({ fontWeight = 'bold', letterSpacing = 'tight', ...rest }) { return } diff --git a/.claude/skills/github-issues/SKILL.md b/.claude/skills/github-issues/SKILL.md index a3d4d6a5..308e86a7 100644 --- a/.claude/skills/github-issues/SKILL.md +++ b/.claude/skills/github-issues/SKILL.md @@ -1,5 +1,5 @@ --- -description: Drafting, referencing, and creating GitHub issues in this repo — templates, issue-reference style, and the show-before-create rule. Use when drafting or filing any epic, task, RFC, bug, story, feature, or doc-feedback issue, or when referencing issues in issue/PR bodies. +description: Drafting, referencing, and creating GitHub issues in this repo - templates, issue-reference style, and the show-before-create rule. Use when drafting or filing any epic, task, RFC, bug, story, feature, or doc-feedback issue, or when referencing issues in issue/PR bodies. --- # GitHub issues @@ -7,13 +7,13 @@ description: Drafting, referencing, and creating GitHub issues in this repo — ## Use the issue templates Any epic, task, RFC, bug report, user story, feature request, or doc-feedback -item — as an `issues/` file or a GitHub issue — must follow the matching template in +item - as an `issues/` file or a GitHub issue - must follow the matching template in `.github/ISSUE_TEMPLATE/` (`4.epic.yml`, `6.task.yml`, `3.rfc.yml`, `1.bug_report.yml`, `5.user_story.yml`, `2.feature_request.yml`, `7.documentation_feedback.yml`). Read the template first; use its exact section -headings, order, and title prefix (`[Epic]`, `[Task]`, …). +headings, order, and title prefix (`[Epic]`, `[Task]`, ...). -## Issue hierarchy — set parent up the chain +## Issue hierarchy - set parent up the chain Each level's **parent** is the level above it: @@ -26,11 +26,11 @@ RFC → Epic → Task - For rework discovered later, add a **subtask** whose parent is the Task it reworks. -## Reference issues bare — GitHub renders the title +## Reference issues bare - GitHub renders the title GitHub auto-renders the title for a bare `#123`, so don't hand-write it. -✗ `- [ ] #136 — [RFC] Restructure Repository into a pnpm Workspace Monorepo` +✗ `- [ ] #136 - [RFC] Restructure Repository into a pnpm Workspace Monorepo` ✓ `- #136` ## Show before creating @@ -40,7 +40,7 @@ verification before creating it on GitHub or writing the file. ## Mandatory metadata on every created issue -When creating an issue on GitHub, all three are required — never omit: +When creating an issue on GitHub, all three are required - never omit: - a **label** - a **parent** diff --git a/.claude/skills/naming-conventions/SKILL.md b/.claude/skills/naming-conventions/SKILL.md index 9cc6ea13..0ea239c8 100644 --- a/.claude/skills/naming-conventions/SKILL.md +++ b/.claude/skills/naming-conventions/SKILL.md @@ -1,11 +1,11 @@ --- -description: JavaScript and TypeScript naming conventions — variables, booleans, functions, classes, React components, constants, privates. Auto-load when naming or reviewing identifiers in .ts/.tsx files. +description: JavaScript and TypeScript naming conventions - variables, booleans, functions, classes, React components, constants, privates. Auto-load when naming or reviewing identifiers in .ts/.tsx files. paths: '**/*.ts,**/*.tsx,**/*.js,**/*.jsx' --- # Naming Conventions -## Variables — camelCase +## Variables - camelCase ```ts // ✗ @@ -17,7 +17,7 @@ var val = 'Robin' var firstName = 'Robin' ``` -## Booleans — is / are / has prefix +## Booleans - is / are / has prefix ```ts // ✗ @@ -31,14 +31,14 @@ var areEqual = false var hasEncryption = true ``` -## Functions & Methods — camelCase + verb prefix +## Functions & Methods - camelCase + verb prefix ```ts // ✗ -function name(firstName, lastName) { … } +function name(firstName, lastName) { ... } // ✓ -function getName(firstName, lastName) { … } +function getName(firstName, lastName) { ... } ``` Common verbs: `get`, `set`, `fetch`, `create`, `update`, `delete`, `calculate`, `handle`, `is`, `has`. @@ -46,54 +46,54 @@ Common verbs: `get`, `set`, `fetch`, `create`, `update`, `delete`, `calculate`, Function names describe **what the function does**, not what it blocks or how it works internally. ```ts -// ✗ — describes what it blocks, not what it does +// ✗ - describes what it blocks, not what it does const blockSize = (prop: string) => prop !== 'size' -// ✗ — describes the implementation detail +// ✗ - describes the implementation detail const isNotSize = (prop: string) => prop !== 'size' -// ✓ — describes the function's purpose in its context +// ✓ - describes the function's purpose in its context const shouldForwardInputProps = (prop: string) => prop !== 'size' ``` -## Classes — PascalCase +## Classes - PascalCase ```ts // ✗ -class softwareDeveloper { … } +class softwareDeveloper { ... } // ✓ -class SoftwareDeveloper { … } +class SoftwareDeveloper { ... } ``` -## React Components — PascalCase +## React Components - PascalCase ```tsx // ✗ function userProfile(user) { - return
+ return
...
} // ✓ function UserProfile(user) { - return
+ return
...
} ``` -Use descriptive, meaningful names. Don't append a redundant `Component` suffix — the JSX usage already makes it a component. +Use descriptive, meaningful names. Don't append a redundant `Component` suffix - the JSX usage already makes it a component. ```tsx -// ✗ — camelCase start -function markdownComponent(props) { … } +// ✗ - camelCase start +function markdownComponent(props) { ... } -// ✗ — redundant "Component" suffix -function MarkdownComponent(props) { … } +// ✗ - redundant "Component" suffix +function MarkdownComponent(props) { ... } -// ✓ — PascalCase, concise -function Markdown(props) { … } +// ✓ - PascalCase, concise +function Markdown(props) { ... } ``` -## Constants — UPPER_SNAKE_CASE +## Constants - UPPER_SNAKE_CASE ```ts // ✗ @@ -105,10 +105,10 @@ const SECONDS_IN_DAY = 86400 Use `UPPER_SNAKE_CASE` for truly fixed, non-reassignable values. Mutable module-level variables stay camelCase. -## Private — \_ prefix +## Private - \_ prefix ```ts -// ✓ — signals internal use only +// ✓ - signals internal use only class Foo { _computeName(first, last) { return `${first} ${last}` @@ -123,6 +123,6 @@ Unused destructured params also use `_` prefix (`{ size: _size }`). - `snake_case` for variables or functions - `kebab-case` in identifiers (`first-name` is a syntax error) - Single-letter or cryptic names (`val`, `tmp`, `x`) outside loop counters -- Adding a comment to explain what a name means — rename instead +- Adding a comment to explain what a name means - rename instead If the arguments name a file, read it and apply these rules. Otherwise apply to the code being discussed. diff --git a/.claude/skills/operating-manual/SKILL.md b/.claude/skills/operating-manual/SKILL.md index 101f7cec..0e6840e7 100644 --- a/.claude/skills/operating-manual/SKILL.md +++ b/.claude/skills/operating-manual/SKILL.md @@ -1,6 +1,6 @@ --- name: operating-manual -description: Working doctrine for any nontrivial task — diagnosing bugs, reviewing code, answering hard questions, planning changes, or delivering conclusions. Load before starting investigation or analysis work, and run the five-question self-test before sending any answer or verdict. +description: Working doctrine for any nontrivial task - diagnosing bugs, reviewing code, answering hard questions, planning changes, or delivering conclusions. Load before starting investigation or analysis work, and run the five-question self-test before sending any answer or verdict. argument-hint: [optional: answer or claim to self-test] --- @@ -23,7 +23,7 @@ Eight procedures. Apply during work; run the self-test before sending. 1. Split by what can be checked independently, not by narrative sequence. 2. Give each piece a pass/fail observation independent of the others. 3. Run the cheapest invalidating checks first. -4. A piece with no independent check is a guess — label it (§5). +4. A piece with no independent check is a guess - label it (§5). ✗ One theory-driven fix for "stale data after save." ✓ Check write reaches DB → read query returns row → cache serves old value. Each observed alone. @@ -42,33 +42,33 @@ Risk = likelihood wrong × cost wrong × how late it surfaces. ## 4. Verify by re-deriving, not re-reading 1. Reconstruct the claim from ground truth by an independent route: read source not docs, run code not mental traces, grep call sites not type signatures, compute the number a second way. -2. Never re-read your own reasoning as verification — it re-executes the same bug. +2. Never re-read your own reasoning as verification - it re-executes the same bug. 3. Two independent routes agree → believe it. One route → provisional, label it. -✗ "Never called with null — the type is non-nullable." +✗ "Never called with null - the type is non-nullable." ✓ Grep call sites; find the `as any` caller. ## 5. Label known vs guessed For every load-bearing statement, tier it: -- **observed** — ran it, read the line, saw the output -- **inferred** — one step from observed -- **assumed** — needed, never checked +- **observed** - ran it, read the line, saw the output +- **inferred** - one step from observed +- **assumed** - needed, never checked State assumed/inferred claims in the output with what would confirm them and what changes if wrong. Never let assumed claims wear observed language. -✓ "Pages results (verified in response). Assuming cap is 100 (docs, untested) — if lower, only the loop limit changes." +✓ "Pages results (verified in response). Assuming cap is 100 (docs, untested) - if lower, only the loop limit changes." ## 6. Attack your own conclusion -1. Construct the case that breaks it — do not re-confirm the motivating case. +1. Construct the case that breaks it - do not re-confirm the motivating case. 2. Ask: what input/state falsifies this? What does a skeptic point at first? Where is wrongness most likely hiding? 3. Run the counterexample: empty, one element, boundary value, concurrent call. 4. If no concrete attack is possible, name the untested boundary. ✗ Reported case passes → done. -✓ Off-by-one fix: run empty, single-item, exactly-one-page. One fails — the bug moved. +✓ Off-by-one fix: run empty, single-item, exactly-one-page. One fails - the bug moved. ## 7. Communicate answer → reasoning → risk @@ -90,11 +90,11 @@ State assumed/inferred claims in the output with what would confirm them and wha When in doubt: worse theater, better engineering. -## Self-test — before sending any answer +## Self-test - before sending any answer 1. Does the first sentence answer the question actually asked? -2. Which single claim, if wrong, most changes the outcome — verified by an independent route? -3. What is assumed unchecked — and does the text label it? +2. Which single claim, if wrong, most changes the outcome - verified by an independent route? +3. What is assumed unchecked - and does the text label it? 4. What concrete counterexample or edge was run, and what happened? 5. If wrong anyway, does the reader find out cheaply and early? If late, add the one risk sentence that moves it earlier. diff --git a/.claude/skills/release-notes/SKILL.md b/.claude/skills/release-notes/SKILL.md index 98e179ab..e30c3a8c 100644 --- a/.claude/skills/release-notes/SKILL.md +++ b/.claude/skills/release-notes/SKILL.md @@ -1,5 +1,5 @@ --- -description: How to write and cut a GitHub Release for an @soroush.tech/* package published by cd-packages.yml — notes live in an in-repo file `packages//release-notes/.md`, the semver bump rule, a linked PR/issue reference as an absolute URL (required for feature/fix releases, waived for dependency-bump-only ones which instead list every bump), and the breaking-change, new-API-doc-link, and packaging side-note sections. Use when releasing a package or drafting its release notes. +description: How to write and cut a GitHub Release for an @soroush.tech/* package published by cd-packages.yml - notes live in an in-repo file `packages//release-notes/.md`, the semver bump rule, a linked PR/issue reference as an absolute URL (required for feature/fix releases, waived for dependency-bump-only ones which instead list every bump), and the breaking-change, new-API-doc-link, and packaging side-note sections. Use when releasing a package or drafting its release notes. paths: packages/** --- @@ -7,27 +7,27 @@ paths: packages/** Publishing is **manual**, and notes live in a **versioned file committed to the package**: `packages//release-notes/.md`, where `` matches `package.json`. The -workflow requires that file to exist before it publishes — a package can never ship with empty +workflow requires that file to exist before it publishes - a package can never ship with empty notes. See [`cd-packages.md`](../../../.github/workflows/cd-packages.md) for the workflow. This skill is about writing that file. -The file is plain multi-line markdown — **no `\n` escaping** (that was the old dispatch-input +The file is plain multi-line markdown - **no `\n` escaping** (that was the old dispatch-input flow; it's gone). Notes files never ship to npm: every package uses a `files: ["dist"]` allowlist, so `release-notes/` is excluded from the tarball automatically. -Every file **starts with a `## @` heading** — it's the only title the file +Every file **starts with a `## @` heading** - it's the only title the file carries (a GitHub Release shows the tag separately; the standalone file doesn't). The directory -is the package's full per-version history — one file per released version, browsable and linked +is the package's full per-version history - one file per released version, browsable and linked from the package README. -**Repo links** use `soroush-tech/core`: `blob/main/…` for a file, `tree/main/…` for a +**Repo links** use `soroush-tech/core`: `blob/main/...` for a file, `tree/main/...` for a directory. Never the old `soroush-tech/soroush.tech`. ## Before you write notes: bump the version (semver) A release is bumping `version` in the package's `package.json` on `main` **and** adding the matching `release-notes/.md`, in the same PR. The publish step skips a version already -on npm, so the **version number is the release**. Follow [semver](https://semver.org) — the +on npm, so the **version number is the release**. Follow [semver](https://semver.org) - the bump decides which sections the notes need. | Bump | `x.y.z` → | Use for | @@ -37,36 +37,36 @@ bump decides which sections the notes need. | **MAJOR** | `X+1.0.0` | **Any change that breaks backward compatibility** (reset minor+patch) | > Breaking a **dependency contract** (raising a peer-dependency floor consumers must meet, -> dropping a supported runtime) is a breaking change — bump **MAJOR**, even if your own code +> dropping a supported runtime) is a breaking change - bump **MAJOR**, even if your own code > is untouched. A backward-compatible dep bump is PATCH. ## Required contents Every release body **must** have: -1. **A PR or issue reference, as a full link** — `[#](https://github.com/soroush-tech/core/issues/)` +1. **A PR or issue reference, as a full link** - `[#](https://github.com/soroush-tech/core/issues/)` somewhere in the notes (lead line or a bullet). Ties a **feature or fix** release to its change history. Required for any release that changes behavior or API. Write the **absolute URL**, never a bare `#`. A bare reference only autolinks in - GitHub's issue/PR/Release UI — these files are also browsed in-repo at `blob/main/…`, where + GitHub's issue/PR/Release UI - these files are also browsed in-repo at `blob/main/...`, where it renders as plain text. Same reason the doc links in rule 3 are absolute. (Note this is the opposite of the [`github-issues`](../github-issues/SKILL.md) rule: inside an _issue or PR body_, reference issues bare so GitHub renders the title.) **Exception:** a maintenance / dependency-bump-only release (a PATCH that only refreshes - dependencies) may have no owning issue — the reference is **not required** there; instead + dependencies) may have no owning issue - the reference is **not required** there; instead **list exactly what was bumped** (see rule 4). -2. **Breaking changes**, if any — a `### BREAKING CHANGES` section spelling out what broke and +2. **Breaking changes**, if any - a `### BREAKING CHANGES` section spelling out what broke and the migration. Its presence means the bump must be MAJOR. -3. **New public API**, if any — name each new export **and link its doc**. API docs live under +3. **New public API**, if any - name each new export **and link its doc**. API docs live under `packages//docs/*.md`. Link the repo-hosted file (a `blob/main` URL) so it resolves from the GitHub Release page, where relative links don't: `https://github.com/soroush-tech/core/blob/main/packages//docs/.md#`. -4. **A packaging side note** — a `### Packaging` section for packaging-level changes: +4. **A packaging side note** - a `### Packaging` section for packaging-level changes: dependency-floor raises, peer/optional-peer changes, engine/runtime requirements, repo moves. - For a dependency-bump-only release this section **is** the release notes — **name every + For a dependency-bump-only release this section **is** the release notes - **name every package bumped with its `old → new` version**, don't just say "dependency bumps". Omit the section only when there were genuinely no packaging changes. @@ -82,7 +82,7 @@ Every release body **must** have: ### Added -- **`newExport`** — one line on what it does. +- **`newExport`** - one line on what it does. [docs](https://github.com/soroush-tech/core/blob/main/packages//docs/.md#anchor) ### Changed @@ -95,7 +95,7 @@ Every release body **must** have: ### BREAKING CHANGES -- . +- - . ### Packaging @@ -109,13 +109,13 @@ release: read the previous `release-notes/*.md`, or `gh release view "@@ -Maintenance release — dependency refresh only. No public API or behavior changes. +Maintenance release - dependency refresh only. No public API or behavior changes. ### Packaging @@ -128,18 +128,18 @@ Maintenance release — dependency refresh only. No public API or behavior chang 1. In **one PR to `main`** (CI must pass): bump `version` in `package.json` **and** add `packages//release-notes/.md` with the notes. The filename must equal the new - version — `pnpm check:release-notes` (pre-commit hook + CI lint job) fails the commit/build + version - `pnpm check:release-notes` (pre-commit hook + CI lint job) fails the commit/build if a publishable package's version has no matching notes file, and the workflow's Validate step fails again before publishing. That same check also fails a commit whose **staged** package isn't ahead of the version already on npm, so an edit can't land without its bump. A package you didn't touch may stay at its published version. -2. Dispatch — Actions → **CD · Packages (npm)** → pick `package`, **Run**. CLI: +2. Dispatch - Actions → **CD · Packages (npm)** → pick `package`, **Run**. CLI: `gh workflow run cd-packages.yml -f package=`. 3. The job publishes to npm (skips if that version already exists) and cuts a GitHub Release tagged/titled `@` from the notes file. Re-running repairs a missing Release without republishing. -> This machine can't `git push` and has no `gh` CLI — commit the version bump + notes file +> This machine can't `git push` and has no `gh` CLI - commit the version bump + notes file > locally, then ask the user to push and to run the dispatch. ## Backfilling notes for past releases @@ -148,4 +148,4 @@ To seed `release-notes/` for versions already published, copy each GitHub Releas `release-notes/.md` (read them with the `mcp__github__list_releases` tool, or `gh release view "@"`). Add the `## @` heading if the body lacks one, and normalize any links to `soroush-tech/core`. The guards only look at each package's -current version, so backfilling is optional — but it makes the directory a complete history. +current version, so backfilling is optional - but it makes the directory a complete history. diff --git a/.claude/skills/seo/SKILL.md b/.claude/skills/seo/SKILL.md index 2449a327..c6620aec 100644 --- a/.claude/skills/seo/SKILL.md +++ b/.claude/skills/seo/SKILL.md @@ -5,7 +5,7 @@ paths: src/**/*.tsx,src/**/*.ts argument-hint: [page or component] --- -# SEO — Heading Structure +# SEO - Heading Structure Headings describe the page's structure, not visual emphasis. Search engines use them to filter, order, and display results; screen readers use them to navigate. Same markup serves both. @@ -16,7 +16,7 @@ Headings describe the page's structure, not visual emphasis. Search engines use Exactly one `

`, opening the main content. No headings before it. ```tsx -// ✗ — no h1, or h1 buried below other headings +// ✗ - no h1, or h1 buried below other headings

Section

Page Title

@@ -25,12 +25,12 @@ Exactly one `

`, opening the main content. No headings before it.

Section

``` -## Descend sequentially — never skip a level +## Descend sequentially - never skip a level The level after `

` is `

`, then `

`, and so on. Don't jump `h2 → h4`. ```tsx -// ✗ — skips h2 +// ✗ - skips h2

Setting Exposure Manually

Set the ISO

@@ -42,19 +42,19 @@ The level after `

` is `

`, then `

`, and so on. Don't jump `h2 → h4` ## Heading markup only for headings -Use `

`–`

` _if and only if_ the text is a heading. Never apply heading tags to make text large or bold. +Use `

`-`

` _if and only if_ the text is a heading. Never apply heading tags to make text large or bold. ```tsx -// ✗ — heading tag used for visual weight on non-heading text +// ✗ - heading tag used for visual weight on non-heading text

Sale ends Friday!

-// ✓ — style a real element instead +// ✓ - style a real element instead Sale ends Friday! ``` ## Headings are brief, clear, unique -Skim only the headings: do they convey the page's contents? If not, rewrite them. Outline the page first (title → sections → subsections), then map that outline onto `h1`–`h6`. +Skim only the headings: do they convey the page's contents? If not, rewrite them. Outline the page first (title → sections → subsections), then map that outline onto `h1`-`h6`. --- diff --git a/.claude/skills/testing/SKILL.md b/.claude/skills/testing/SKILL.md index 63a3ad04..98fd50c8 100644 --- a/.claude/skills/testing/SKILL.md +++ b/.claude/skills/testing/SKILL.md @@ -1,5 +1,5 @@ --- -description: How this project runs tests and coverage — the three test tiers and their placement, the vitest projects, the test:coverage:* commands, where coverage output lands (coverage/lcov.info), the Codecov/CI file, and the `pnpm coverage:check` gate. Use when running tests or coverage, checking per-file coverage, or verifying 100% on touched files. +description: How this project runs tests and coverage - the three test tiers and their placement, the vitest projects, the test:coverage:* commands, where coverage output lands (coverage/lcov.info), the Codecov/CI file, and the `pnpm coverage:check` gate. Use when running tests or coverage, checking per-file coverage, or verifying 100% on touched files. --- ## Test tiers (all co-located next to source) @@ -15,9 +15,9 @@ description: How this project runs tests and coverage — the three test tiers a ## Vitest projects (`vitest.config.ts`) -- `unit` — jsdom, runs `**/*.{test,spec}.*` (excludes `*.browser.test.*`) -- `unit-browser` — headless Chromium, runs only `*.browser.test.*` -- `storybook` — runs `*.stories.tsx` via the Storybook addon +- `unit` - jsdom, runs `**/*.{test,spec}.*` (excludes `*.browser.test.*`) +- `unit-browser` - headless Chromium, runs only `*.browser.test.*` +- `storybook` - runs `*.stories.tsx` via the Storybook addon Scope a run with `--project=unit` (etc.) and pass path filters: `vitest run --project=unit src/section/Article`. @@ -27,7 +27,7 @@ Scope a run with `--project=unit` (etc.) and pass path filters: `vitest run --pr | ------------------------------ | ------------------------------------------ | | `pnpm test:coverage` | every workspace, recursively | | `pnpm test:coverage:web` | the web app alone, all its vitest projects | -| `pnpm test:coverage:unit` | `unit` project — **this is what CI runs** | +| `pnpm test:coverage:unit` | `unit` project - **this is what CI runs** | | `pnpm test:coverage:browser` | `unit-browser` project | | `pnpm test:coverage:storybook` | `storybook` project | | `pnpm test:coverage:e2e` | Playwright (`E2E_COVERAGE=true`) | @@ -47,12 +47,12 @@ must be addressable without dragging the web app's along: Default `reportsDirectory` is **`coverage/`** at repo root (reporters: `text` + `lcov`): -- `coverage/lcov.info` — machine-readable report. **The file CI uploads to Codecov** (`.github/workflows/ci.yml`) and the file `pnpm coverage:check` parses. -- `coverage/lcov-report/index.html` — browsable HTML report (`test:coverage:ui` opens it). -- `text` reporter prints to stdout — no file. +- `coverage/lcov.info` - machine-readable report. **The file CI uploads to Codecov** (`.github/workflows/ci.yml`) and the file `pnpm coverage:check` parses. +- `coverage/lcov-report/index.html` - browsable HTML report (`test:coverage:ui` opens it). +- `text` reporter prints to stdout - no file. - **E2E exception:** writes to `coverage/e2e/lcov.info`. -All vitest projects write to the **same** `coverage/lcov.info` — each run overwrites the last, so the file reflects whatever you ran most recently. `coverage/` is gitignored. +All vitest projects write to the **same** `coverage/lcov.info` - each run overwrites the last, so the file reflects whatever you ran most recently. `coverage/` is gitignored. ## Per-file coverage gate: `pnpm coverage:check` @@ -66,4 +66,4 @@ pnpm coverage:check --threshold=90 src/common # custom threshold (default pnpm coverage:check --file=coverage/e2e/lcov.info # parse the e2e report instead ``` -It reports only what the **last** coverage run generated — a file whose tests didn't run shows 0%. So generate coverage with the relevant tests (full `test:coverage:unit`, or scoped to the touched tests) before checking. +It reports only what the **last** coverage run generated - a file whose tests didn't run shows 0%. So generate coverage with the relevant tests (full `test:coverage:unit`, or scoped to the touched tests) before checking. diff --git a/.claude/skills/theme-usage/SKILL.md b/.claude/skills/theme-usage/SKILL.md index 6bc4f2c4..6ca44cb8 100644 --- a/.claude/skills/theme-usage/SKILL.md +++ b/.claude/skills/theme-usage/SKILL.md @@ -1,5 +1,5 @@ --- -description: How to consume design system components in pages and features — when to use View/Flex/Typography/Button, how to pass theme tokens, and what to avoid. Auto-load when writing or reviewing code in src/pages/, src/section/, or src/common/. +description: How to consume design system components in pages and features - when to use View/Flex/Typography/Button, how to pass theme tokens, and what to avoid. Auto-load when writing or reviewing code in src/pages/, src/section/, or src/common/. argument-hint: [component or file name] paths: src/pages/**,src/section/**,src/common/** --- @@ -10,7 +10,7 @@ paths: src/pages/**,src/section/**,src/common/** | ------------------------- | ------------ | --------------------------------------------- | | Block container / wrapper | `View` | `div`, `section`, `article` | | Flex layout | `Flex` | `View` with inline `display: flex`, raw `div` | -| All text | `Typography` | `p`, `span`, `h1`–`h6` | +| All text | `Typography` | `p`, `span`, `h1`-`h6` | | Clickable element | `Button` | `div onClick`, `a` for actions | | Navigation link | `Link` | `a href`, `Button` | @@ -18,7 +18,7 @@ paths: src/pages/**,src/section/**,src/common/** --- -## Rule 1 — Theme tokens for color and spacing, never literals +## Rule 1 - Theme tokens for color and spacing, never literals ```tsx // ✗ @@ -32,7 +32,7 @@ Color props (`bg`, `color`, `borderColor`) accept keys from `Theme['background'] --- -## Rule 2 — Use `Flex` for any flex layout +## Rule 2 - Use `Flex` for any flex layout ```tsx // ✗ @@ -46,7 +46,7 @@ Color props (`bg`, `color`, `borderColor`) accept keys from `Theme['background'] --- -## Rule 3 — Typography for all text, with variant +## Rule 3 - Typography for all text, with variant ```tsx // ✗ @@ -56,11 +56,11 @@ Color props (`bg`, `color`, `borderColor`) accept keys from `Theme['background'] Heading ``` -The `variant` prop maps to the correct HTML element automatically (via `theme.typography[variant].element`) — no need to set `as` unless you want to override the element while keeping the visual style. Always prefer a semantic `variant` over raw `fontSize`/`fontWeight` props. +The `variant` prop maps to the correct HTML element automatically (via `theme.typography[variant].element`) - no need to set `as` unless you want to override the element while keeping the visual style. Always prefer a semantic `variant` over raw `fontSize`/`fontWeight` props. --- -## Rule 4 — Import from the barrel index +## Rule 4 - Import from the barrel index ```ts // ✗ @@ -77,9 +77,9 @@ Always use the `src/` alias, never relative paths up the tree. --- -## Rule 5 — Never recreate what already exists +## Rule 5 - Never recreate what already exists -Before adding a layout div, spinner, input, or icon wrapper — check `@soroush.tech/design-system/`. If it exists, use it: +Before adding a layout div, spinner, input, or icon wrapper - check `@soroush.tech/design-system/`. If it exists, use it: | If you need | Check | | ------------------------- | ----------------------------- | @@ -93,9 +93,9 @@ Before adding a layout div, spinner, input, or icon wrapper — check `@soroush. --- -## Rule 6 — Responsive props over conditional renders +## Rule 6 - Responsive props over conditional renders -styled-system props accept responsive arrays — prefer them over JS ternaries or media query CSS strings: +styled-system props accept responsive arrays - prefer them over JS ternaries or media query CSS strings: ```tsx // ✗ @@ -109,11 +109,11 @@ styled-system props accept responsive arrays — prefer them over JS ternaries o ## What NOT to do -- No inline `style={{}}` for colors, spacing, or typography — use theme props -- No hardcoded hex values (`'#fff'`, `'rgba(0,0,0,0.5)'`) — reference palette tokens +- No inline `style={{}}` for colors, spacing, or typography - use theme props +- No hardcoded hex values (`'#fff'`, `'rgba(0,0,0,0.5)'`) - reference palette tokens - No raw `div`/`span`/`p` when a theme component exists -- No `className` + external CSS for layout — use `View`/`Flex` props +- No `className` + external CSS for layout - use `View`/`Flex` props - No custom wrapper components that just rename a theme primitive with no added logic -- Do not import theme components from inside `packages/design-system/src/ComponentName/ComponentName.tsx` — always go through the barrel `index.ts` +- Do not import theme components from inside `packages/design-system/src/ComponentName/ComponentName.tsx` - always go through the barrel `index.ts` If `$ARGUMENTS` names a file or component, read it and flag every violation above with a corrected snippet. Otherwise apply to the code being discussed. diff --git a/.claude/skills/wcag/SKILL.md b/.claude/skills/wcag/SKILL.md index a575c9e5..039f70e4 100644 --- a/.claude/skills/wcag/SKILL.md +++ b/.claude/skills/wcag/SKILL.md @@ -7,7 +7,7 @@ argument-hint: [component or violation] # WCAG Accessibility Patterns -WCAG 2.2 AA is the target. Four principles: **Perceivable · Operable · Understandable · Robust** (POUR). Automated tools catch ~30% of issues — always verify manually with keyboard navigation and screen reader. +WCAG 2.2 AA is the target. Four principles: **Perceivable · Operable · Understandable · Robust** (POUR). Automated tools catch ~30% of issues - always verify manually with keyboard navigation and screen reader. --- @@ -18,20 +18,20 @@ WCAG 2.2 AA is the target. Four principles: **Perceivable · Operable · Underst - Normal text (< 18pt / < 14pt bold): **4.5:1 minimum** - Large text (≥ 18pt / ≥ 14pt bold): **3:1 minimum** - UI components and focus indicators: **3:1 minimum** -- **Disabled UI components are exempt** — but only when actually disabled, not when reusing the disabled color token on regular text +- **Disabled UI components are exempt** - but only when actually disabled, not when reusing the disabled color token on regular text -### `opacity` compounds with the background — compute the effective color +### `opacity` compounds with the background - compute the effective color ``` effective = alpha × foreground + (1 - alpha) × background ``` ```tsx -// ✗ — opacity: 0.5 on secondary (#444748) over #e2e2e2 → effective #939595 → 2.32:1 -© 2026 … +// ✗ - opacity: 0.5 on secondary (#444748) over #e2e2e2 → effective #939595 → 2.32:1 +© 2026 ... -// ✓ — solid secondary (#444748) → 7.22:1 -© 2026 … +// ✓ - solid secondary (#444748) → 7.22:1 +© 2026 ... ``` When fixing hex-alpha tokens (e.g. `#1a1c1c4D`): compute the effective color on every background it appears on, then raise the alpha until the weakest background clears 4.5:1. @@ -41,10 +41,10 @@ When fixing hex-alpha tokens (e.g. `#1a1c1c4D`): compute the effective color on `visibility: hidden` strips an element from the AT; `opacity: 0` keeps it. ```tsx -// ✗ — button has no accessible name while loading (label hidden from AT) +// ✗ - button has no accessible name while loading (label hidden from AT) ...(invisible && { visibility: 'hidden' }) -// ✓ — text stays in AT; spinner overlays via position: absolute +// ✓ - text stays in AT; spinner overlays via position: absolute ...(invisible && { opacity: 0 }) ``` @@ -61,10 +61,10 @@ Also add `aria-busy` to the interactive root when it is loading: ### Keyboard: never remove focus indicators ```tsx -// ✗ — removes all focus styling +// ✗ - removes all focus styling button:focus { outline: none; } -// ✓ — visible on keyboard focus only +// ✓ - visible on keyboard focus only button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } ``` @@ -78,22 +78,22 @@ Tab order must follow DOM order. Every interactive element must be reachable and Every `` needs an accessible name via one of: -1. **Wrapping `