From 899a9f2b51d23aa1a15f617705547ccac8f3073c Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 11 Jun 2026 23:18:40 +0100 Subject: [PATCH] fix: apply ui: prefix to all packages/ui components and automate it --- .github/workflows/ui-prefix-check.yml | 50 +++ AGENTS.md | 1 + .../lib/features/course/pages/settings.svelte | 4 +- apps/embeds/package.json | 1 + lefthook.yml | 6 + packages/ui/README.md | 5 +- packages/ui/add-ui-prefix.cjs | 371 +++++++++++++++--- packages/ui/package.json | 4 + .../ui/src/base/chart/chart-container.svelte | 7 +- .../ui/src/base/chart/chart-tooltip.svelte | 2 +- .../ui/src/base/dialog/dialog-content.svelte | 6 +- .../dropdown-menu-checkbox-item.svelte | 2 +- .../dropdown-menu/dropdown-menu-item.svelte | 2 +- packages/ui/src/base/field/field.svelte | 6 +- .../base/menubar/menubar-checkbox-item.svelte | 2 +- .../ui/src/base/menubar/menubar-item.svelte | 2 +- .../navigation-menu-content.svelte | 2 +- .../navigation-menu-trigger.svelte | 4 +- .../navigation-menu-viewport.svelte | 2 +- .../base/pagination/pagination-link.svelte | 2 +- packages/ui/src/base/sidebar/sidebar.svelte | 2 +- .../src/custom/animation/blurin/blurin.svelte | 3 +- .../ui/src/custom/animation/card/body.svelte | 2 +- .../custom/animation/card/container.svelte | 14 +- .../src/custom/animation/card/ellipses.svelte | 12 +- .../animation/dot-field/dot-field.svelte | 6 +- .../animation/dot-pattern/dot-pattern.svelte | 2 +- .../gradual-spacing/gradual-spacing.svelte | 4 +- .../hero-video-dialog.svelte | 31 +- .../animation/magic-card/magic-card.svelte | 10 +- .../animation/shinny-text/shinny-text.svelte | 6 +- .../animation/sparkle/sparkle-particle.svelte | 4 +- .../animation/sparkle/sparkle-text.svelte | 2 +- .../custom/animation/square-card/body.svelte | 2 +- .../custom/animation/square-card/icon.svelte | 2 +- .../certificate-preview.svelte | 6 +- .../checkbox-option-card.svelte | 2 +- .../circular-progress.svelte | 6 +- .../ui/src/custom/code/code-overflow.svelte | 2 +- .../custom/document-card/document-card.svelte | 4 +- .../editor/ui/components/CodeBlock.svelte | 2 +- .../editor/ui/components/MediaExtended.svelte | 18 +- .../ui/components/SlashCommandList.svelte | 2 +- .../editor/ui/components/ToolBarIcon.svelte | 2 +- .../ui/components/toolbar/QuickColors.svelte | 8 +- .../toolbar/SearchAndReplace.svelte | 4 +- .../editor/ui/drag-handle-extended.svelte | 2 +- .../question-navigation.svelte | 2 +- .../renderers/file-upload/edit.svelte | 2 +- .../renderers/option-review-utils.ts | 6 +- .../renderers/star/star-row.svelte | 2 +- .../true-false-review-toggle.svelte | 4 +- .../renderers/word-bank/edit.svelte | 2 +- .../renderers/word-bank/take.svelte | 4 +- .../section-navigation-sidebar.svelte | 8 +- .../exercise-question/section-overview.svelte | 2 +- .../file-drop-zone-trigger.svelte | 2 +- .../image-cropper/image-cropper-dialog.svelte | 2 +- .../src/custom/input-field/input-field.svelte | 2 +- .../mention-popover/mention-popover.svelte | 2 +- .../custom/model-picker/model-picker.svelte | 22 +- .../ui/src/custom/moving-icons/course.svelte | 4 +- .../course-info-blocks.svelte | 8 +- .../landing-page-footer.tokens.ts | 18 +- .../percent-ring-progress.svelte | 14 +- .../custom/public-course/lesson-view.svelte | 2 +- .../question-type-picker.svelte | 2 +- .../src/custom/radio-item/radio-item.svelte | 2 +- .../resource-list-row.svelte | 2 +- .../textarea-field/textarea-field.svelte | 2 +- .../underline-tabs-trigger.svelte | 6 +- .../src/custom/user-avatar/user-avatar.svelte | 4 +- packages/ui/src/output.css | 2 +- 73 files changed, 541 insertions(+), 228 deletions(-) create mode 100644 .github/workflows/ui-prefix-check.yml diff --git a/.github/workflows/ui-prefix-check.yml b/.github/workflows/ui-prefix-check.yml new file mode 100644 index 00000000..012662f0 --- /dev/null +++ b/.github/workflows/ui-prefix-check.yml @@ -0,0 +1,50 @@ +name: UI Tailwind prefix check + +on: + pull_request: + paths: + - 'packages/ui/**' + - '.github/workflows/ui-prefix-check.yml' + push: + branches: + - main + paths: + - 'packages/ui/**' + - '.github/workflows/ui-prefix-check.yml' + +jobs: + ui-prefix: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Resolve git base ref + id: base + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "ref=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT" + elif [ -n "${{ github.event.before }}" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + echo "ref=${{ github.event.before }}" >> "$GITHUB_OUTPUT" + else + echo "ref=origin/main" >> "$GITHUB_OUTPUT" + fi + + - name: Check ui: prefix on changed UI files + env: + UI_PREFIX_GIT_BASE: ${{ steps.base.outputs.ref }} + run: pnpm --filter @cio/ui prefix:check:changed diff --git a/AGENTS.md b/AGENTS.md index 929180b2..e3270796 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,6 +304,7 @@ Use `.server.ts` files for server-side code to isolate API keys. ### UI Components - Add new UI components under `packages/ui/src` following existing folder patterns. +- **All Tailwind utility classes in `packages/ui/src/**` must use the `ui:` prefix** (see `packages/ui/README.md`). Pre-commit and CI run `pnpm --filter @cio/ui prefix:check` on touched UI files; fix with `pnpm --filter @cio/ui prefix`. - Document component usage and props in `packages/ui/README.md`. - Add example usages and variants in Storybook stories under `packages/storybook/src`. - See `packages/ui/README.md` and `packages/storybook/README.md` for full guidance. diff --git a/apps/dashboard/src/lib/features/course/pages/settings.svelte b/apps/dashboard/src/lib/features/course/pages/settings.svelte index 35a0395d..a480b911 100644 --- a/apps/dashboard/src/lib/features/course/pages/settings.svelte +++ b/apps/dashboard/src/lib/features/course/pages/settings.svelte @@ -369,10 +369,10 @@
Course cover
diff --git a/apps/embeds/package.json b/apps/embeds/package.json index 175120c3..cf83db43 100644 --- a/apps/embeds/package.json +++ b/apps/embeds/package.json @@ -19,6 +19,7 @@ "@cio/tsconfig": "workspace:*", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@tailwindcss/vite": "^4.1.16", + "tailwindcss": "^4.1.18", "svelte": "^5.39.11", "dotenv": "^16.3.1", "tsx": "^4.7.1", diff --git a/lefthook.yml b/lefthook.yml index e207bc08..9e45e000 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -9,3 +9,9 @@ pre-commit: fail_text: | ❌ Prettier formatting check failed! Please run 'pnpm format:changed' or 'pnpm format' to fix formatting issues and try again. + ui-prefix: + glob: 'packages/ui/src/**/*.{svelte,ts,tsx,js,jsx}' + run: pnpm --filter @cio/ui prefix:check:staged + fail_text: | + ❌ packages/ui Tailwind classes must use the ui: prefix. + Run 'pnpm --filter @cio/ui prefix' to fix staged UI files, then commit again. diff --git a/packages/ui/README.md b/packages/ui/README.md index 10034b88..c9f0bfb3 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -249,7 +249,10 @@ When adding a new component to this package, follow these steps: - `pnpm build` - Build the package - `pnpm dev` - Watch mode for CSS generation - `pnpm format` - Format code with Prettier -- `pnpm prefix:list` - List files that need the `ui:` prefix (dry-run) +- `pnpm prefix:list` - List files that need the `ui:` prefix (dry-run, exit 0) +- `pnpm prefix:check` - Fail if any file under `src/` needs the `ui:` prefix +- `pnpm prefix:check:staged` - Fail if **staged** `packages/ui` files need the prefix (pre-commit) +- `pnpm prefix:check:changed` - Fail if **changed** `packages/ui` files need the prefix (CI; set `UI_PREFIX_GIT_BASE`) - `pnpm prefix` - Add `ui:` prefix to all Tailwind classes and format ### Configuration diff --git a/packages/ui/add-ui-prefix.cjs b/packages/ui/add-ui-prefix.cjs index a0aca4d3..fbf55eb2 100755 --- a/packages/ui/add-ui-prefix.cjs +++ b/packages/ui/add-ui-prefix.cjs @@ -2,25 +2,46 @@ /** * Script to add 'ui:' prefix to Tailwind classes in packages/ui - * Usage: node add-ui-prefix.cjs [--dry-run] + * Usage: + * node add-ui-prefix.cjs # fix all files + * node add-ui-prefix.cjs --dry-run # list fixes without writing (exit 0) + * node add-ui-prefix.cjs --check # fail if fixes are needed (exit 1) + * node add-ui-prefix.cjs --check --staged # check staged packages/ui files only + * node add-ui-prefix.cjs --check --changed # check changed packages/ui files vs base ref + * node add-ui-prefix.cjs --audit # fail on known prefix corruption patterns * - * This script will: - * - Find all .svelte, .ts, .tsx, .js, .jsx files in packages/ui/src - * - Add 'ui:' prefix to Tailwind utility classes - * - Skip classes that already have the prefix - * - Preserve existing code structure + * Env: + * UI_PREFIX_GIT_BASE - optional git ref for --changed (default: origin/main) */ const fs = require('fs'); const path = require('path'); +const { spawnSync } = require('child_process'); -const DRY_RUN = process.argv.includes('--dry-run'); +const CHECK_MODE = process.argv.includes('--check'); +const AUDIT_MODE = process.argv.includes('--audit'); +const DRY_RUN = process.argv.includes('--dry-run') || CHECK_MODE; +const STAGED_ONLY = process.argv.includes('--staged'); +const CHANGED_ONLY = process.argv.includes('--changed'); const UI_PREFIX = 'ui:'; +const REPO_ROOT = path.join(__dirname, '../..'); +const UI_SRC_DIR = path.join(__dirname, 'src'); +const UI_SOURCE_EXTENSIONS = new Set(['.svelte', '.ts', '.tsx', '.js', '.jsx']); + +// Collapse duplicate ui: markers (e.g. ui:hover:ui:bg-accent → ui:hover:bg-accent) +function normalizeUiClass(className) { + if (!className.includes(UI_PREFIX)) { + return className; + } + + const stripped = className.split(UI_PREFIX).filter(Boolean).join(''); + return `${UI_PREFIX}${stripped}`; +} // Check if a class looks like a Tailwind utility class function isTailwindClass(className) { - // Skip if already has ui: prefix - if (className.startsWith(UI_PREFIX)) { + // Skip if already prefixed (including malformed mid-chain ui: after a variant) + if (className.startsWith(UI_PREFIX) || className.includes(':ui:')) { return false; } @@ -66,30 +87,126 @@ function isTailwindClass(className) { // Arbitrary variants (like [&_svg]:pointer-events-none, after:absolute, etc.) /^\[.*\]:|^(after|before|first|last|only|nth|marker|placeholder|selection|backdrop):/, // Common utilities - /^(rounded|border|outline|shadow|ring|overflow|z-|order-|col-|row-|justify-|items-|content-|self-|place-|object-|cursor-|select-|resize-|appearance-|pointer-events-|visibility-|whitespace-|break-|overflow-|overscroll-|scroll-|will-change-|transform|transition|top|right|bottom|left)/ + /^(rounded|border|outline|shadow|ring|overflow|z-|order-|col-|row-|justify-|items-|content-|self-|place-|object-|cursor-|select-|resize-|appearance-|pointer-events-|visibility-|whitespace-|break-|overflow-|overscroll-|scroll-|will-change-|transform|transition|top-|right-|bottom-|left-)/ ]; return patterns.some((pattern) => pattern.test(className)); } -// Process a single class string -function processClassString(classString) { - // Split by whitespace and process each class - const classes = classString.split(/\s+/).filter((c) => c.trim()); - const processed = classes.map((cls) => { - if (isTailwindClass(cls)) { - return `${UI_PREFIX}${cls}`; +function extractScopedClassNames(content) { + const scoped = new Set(); + const styleBlocks = content.match(/]*>[\s\S]*?<\/style>/gi); + + if (!styleBlocks) { + return scoped; + } + + for (const block of styleBlocks) { + const css = block.replace(/<\/?style[^>]*>/gi, ''); + + for (const match of css.matchAll(/\.([a-zA-Z_][\w-]*)/g)) { + scoped.add(match[1]); } - return cls; + } + + return scoped; +} + +function isQuotedJsLiteral(token) { + return ( + (token.startsWith("'") && token.endsWith("'") && token.length > 1) || + (token.startsWith('"') && token.endsWith('"') && token.length > 1) + ); +} + +function resolveClassToken(cls, scopedClasses = new Set()) { + if (isQuotedJsLiteral(cls)) { + const quote = cls[0]; + const inner = cls.slice(1, -1); + const processed = processClassTokens(inner, scopedClasses); + + return `${quote}${processed}${quote}`; + } + + const bareName = cls.startsWith(UI_PREFIX) ? cls.slice(UI_PREFIX.length) : cls; + + // Component-scoped hook classes (defined in