fix: apply ui: prefix to all packages/ui components and automate it

This commit is contained in:
Rotimi Best
2026-06-11 23:18:40 +01:00
parent 89e44d4fab
commit 899a9f2b51
73 changed files with 541 additions and 228 deletions
+50
View File
@@ -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
+1
View File
@@ -304,6 +304,7 @@ Use `.server.ts` files for server-side code to isolate API keys.
### UI Components ### UI Components
- Add new UI components under `packages/ui/src` following existing folder patterns. - 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`. - Document component usage and props in `packages/ui/README.md`.
- Add example usages and variants in Storybook stories under `packages/storybook/src`. - 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. - See `packages/ui/README.md` and `packages/storybook/README.md` for full guidance.
@@ -369,10 +369,10 @@
<Field.Field> <Field.Field>
<div class="relative w-fit"> <div class="relative w-fit">
<img <img
style="min-width:320px; min-height:200px" style="min-width:280px; min-height:200px"
alt="Course cover" alt="Course cover"
src={$settings.logo ? $settings.logo : '/images/classroomio-course-img-template.jpg'} src={$settings.logo ? $settings.logo : '/images/classroomio-course-img-template.jpg'}
class="relative mt-2 h-[200px] w-[320px] rounded-md md:mt-0" class="relative mt-2 h-[200px] w-[280px] rounded-md md:mt-0"
/> />
</div> </div>
</Field.Field> </Field.Field>
+1
View File
@@ -19,6 +19,7 @@
"@cio/tsconfig": "workspace:*", "@cio/tsconfig": "workspace:*",
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"tailwindcss": "^4.1.18",
"svelte": "^5.39.11", "svelte": "^5.39.11",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"tsx": "^4.7.1", "tsx": "^4.7.1",
+6
View File
@@ -9,3 +9,9 @@ pre-commit:
fail_text: | fail_text: |
❌ Prettier formatting check failed! ❌ Prettier formatting check failed!
Please run 'pnpm format:changed' or 'pnpm format' to fix formatting issues and try again. 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.
+4 -1
View File
@@ -249,7 +249,10 @@ When adding a new component to this package, follow these steps:
- `pnpm build` - Build the package - `pnpm build` - Build the package
- `pnpm dev` - Watch mode for CSS generation - `pnpm dev` - Watch mode for CSS generation
- `pnpm format` - Format code with Prettier - `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 - `pnpm prefix` - Add `ui:` prefix to all Tailwind classes and format
### Configuration ### Configuration
+310 -53
View File
@@ -2,25 +2,46 @@
/** /**
* Script to add 'ui:' prefix to Tailwind classes in packages/ui * 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: * Env:
* - Find all .svelte, .ts, .tsx, .js, .jsx files in packages/ui/src * UI_PREFIX_GIT_BASE - optional git ref for --changed (default: origin/main)
* - Add 'ui:' prefix to Tailwind utility classes
* - Skip classes that already have the prefix
* - Preserve existing code structure
*/ */
const fs = require('fs'); const fs = require('fs');
const path = require('path'); 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 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 // Check if a class looks like a Tailwind utility class
function isTailwindClass(className) { function isTailwindClass(className) {
// Skip if already has ui: prefix // Skip if already prefixed (including malformed mid-chain ui: after a variant)
if (className.startsWith(UI_PREFIX)) { if (className.startsWith(UI_PREFIX) || className.includes(':ui:')) {
return false; return false;
} }
@@ -66,30 +87,126 @@ function isTailwindClass(className) {
// Arbitrary variants (like [&_svg]:pointer-events-none, after:absolute, etc.) // Arbitrary variants (like [&_svg]:pointer-events-none, after:absolute, etc.)
/^\[.*\]:|^(after|before|first|last|only|nth|marker|placeholder|selection|backdrop):/, /^\[.*\]:|^(after|before|first|last|only|nth|marker|placeholder|selection|backdrop):/,
// Common utilities // 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)); return patterns.some((pattern) => pattern.test(className));
} }
// Process a single class string function extractScopedClassNames(content) {
function processClassString(classString) { const scoped = new Set();
// Split by whitespace and process each class const styleBlocks = content.match(/<style[^>]*>[\s\S]*?<\/style>/gi);
const classes = classString.split(/\s+/).filter((c) => c.trim());
const processed = classes.map((cls) => { 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 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 <style>) are never Tailwind utilities.
if (scopedClasses.has(bareName)) {
return bareName;
}
if (cls.includes(UI_PREFIX)) {
return normalizeUiClass(cls);
}
if (isTailwindClass(cls)) { if (isTailwindClass(cls)) {
return `${UI_PREFIX}${cls}`; return `${UI_PREFIX}${cls}`;
} }
return cls; return cls;
}
function processClassTokens(classString, scopedClasses = new Set()) {
const classes = classString.split(/\s+/).filter((c) => c.trim());
return classes.map((cls) => resolveClassToken(cls, scopedClasses)).join(' ');
}
// Process a single class string
function processClassString(classString, scopedClasses = new Set()) {
return processClassTokens(classString, scopedClasses);
}
function repairCorruptedQuotedPrefixes(content) {
return content.replace(/ui:'([^']*)'/g, "'ui:$1'").replace(/ui:"([^"]*)"/g, '"ui:$1"');
}
function processInterpolatedSegments(value, scopedClasses, expressionPattern) {
const parts = value.split(expressionPattern);
return parts.reduce((result, part, index) => {
const isExpression = index % 2 === 1;
const processed = isExpression
? processQuotedClassStrings(part, scopedClasses)
: processClassString(part, scopedClasses);
if (result.length === 0) {
return processed;
}
const needsSpace = processed.length > 0 && result.length > 0 && !result.endsWith(' ') && !processed.startsWith(' ');
return `${result}${needsSpace ? ' ' : ''}${processed}`;
}, '');
}
function processClassAttributeValue(value, scopedClasses = new Set()) {
return processInterpolatedSegments(value, scopedClasses, /(\{[^}]*\})/g);
}
function processTemplateClassAttributeValue(value, scopedClasses = new Set()) {
return processInterpolatedSegments(value, scopedClasses, /(\$\{[^}]*\})/g);
}
function processQuotedClassStrings(content, scopedClasses = new Set()) {
let processed = content;
processed = processed.replace(/'([^']*)'/g, (match, quoted) => {
const result = processClassString(quoted, scopedClasses);
return result !== quoted ? `'${result}'` : match;
}); });
return processed.join(' '); processed = processed.replace(/"([^"]*)"/g, (match, quoted) => {
const result = processClassString(quoted, scopedClasses);
return result !== quoted ? `"${result}"` : match;
});
return processed;
} }
// Process file content // Process file content
function processFileContent(content) { function processFileContent(content, scopedClasses = new Set()) {
let modified = content; let modified = repairCorruptedQuotedPrefixes(content);
let changes = 0; let changes = modified !== content ? 1 : 0;
// Helper to process and count changes // Helper to process and count changes
function processAndReplace(pattern, replacer) { function processAndReplace(pattern, replacer) {
@@ -104,25 +221,25 @@ function processFileContent(content) {
// Pattern 1: class="..." or className="..." // Pattern 1: class="..." or className="..."
processAndReplace(/(class|className)="([^"]*)"/g, (match, attr, classes) => { processAndReplace(/(class|className)="([^"]*)"/g, (match, attr, classes) => {
const processed = processClassString(classes); const processed = processClassAttributeValue(classes, scopedClasses);
return processed !== classes ? `${attr}="${processed}"` : match; return processed !== classes ? `${attr}="${processed}"` : match;
}); });
// Pattern 1b: class='...' or className='...' // Pattern 1b: class='...' or className='...'
processAndReplace(/(class|className)='([^']*)'/g, (match, attr, classes) => { processAndReplace(/(class|className)='([^']*)'/g, (match, attr, classes) => {
const processed = processClassString(classes); const processed = processClassAttributeValue(classes, scopedClasses);
return processed !== classes ? `${attr}='${processed}'` : match; return processed !== classes ? `${attr}='${processed}'` : match;
}); });
// Pattern 2: class={`...`} or className={`...`} // Pattern 2: class={`...`} or className={`...`}
processAndReplace(/(class|className)=\{`([^`]+)`\}/g, (match, attr, classes) => { processAndReplace(/(class|className)=\{`([^`]+)`\}/g, (match, attr, classes) => {
const processed = processClassString(classes); const processed = processTemplateClassAttributeValue(classes, scopedClasses);
return processed !== classes ? `${attr}={\`${processed}\`}` : match; return processed !== classes ? `${attr}={\`${processed}\`}` : match;
}); });
// Pattern 3: class={'...'} or className={'...'} // Pattern 3: class={'...'} or className={'...'}
processAndReplace(/(class|className)=\{['"]([^'"]+)['"]\}/g, (match, attr, classes) => { processAndReplace(/(class|className)=\{['"]([^'"]+)['"]\}/g, (match, attr, classes) => {
const processed = processClassString(classes); const processed = processClassString(classes, scopedClasses);
return processed !== classes ? `${attr}={\`${processed}\`}` : match; return processed !== classes ? `${attr}={\`${processed}\`}` : match;
}); });
@@ -176,14 +293,14 @@ function processFileContent(content) {
processAndReplace( processAndReplace(
new RegExp(`(${tailwindFunctionNames.join('|')})\\s*\\(\\s*['"]([^'"]+)['"]\\s*([,)])`, 'g'), new RegExp(`(${tailwindFunctionNames.join('|')})\\s*\\(\\s*['"]([^'"]+)['"]\\s*([,)])`, 'g'),
(match, fn, classes, separator) => { (match, fn, classes, separator) => {
const processed = processClassString(classes); const processed = processClassString(classes, scopedClasses);
return processed !== classes ? `${fn}("${processed}"${separator}` : match; return processed !== classes ? `${fn}("${processed}"${separator}` : match;
} }
); );
// Pattern 7: Template literals in function calls: cn(`...`) // Pattern 7: Template literals in function calls: cn(`...`)
processAndReplace(/(cn|tv)\s*\(`([^`]+)`/g, (match, fn, classes) => { processAndReplace(/(cn|tv)\s*\(`([^`]+)`/g, (match, fn, classes) => {
const processed = processClassString(classes); const processed = processClassString(classes, scopedClasses);
return processed !== classes ? `${fn}(\`${processed}\`` : match; return processed !== classes ? `${fn}(\`${processed}\`` : match;
}); });
@@ -195,19 +312,19 @@ function processFileContent(content) {
// Process double-quoted strings // Process double-quoted strings
processedObj = processedObj.replace(/"([^"]*)"/g, (match, value) => { processedObj = processedObj.replace(/"([^"]*)"/g, (match, value) => {
const processed = processClassString(value); const processed = processClassString(value, scopedClasses);
return processed !== value ? `"${processed}"` : match; return processed !== value ? `"${processed}"` : match;
}); });
// Process single-quoted strings // Process single-quoted strings
processedObj = processedObj.replace(/'([^']*)'/g, (match, value) => { processedObj = processedObj.replace(/'([^']*)'/g, (match, value) => {
const processed = processClassString(value); const processed = processClassString(value, scopedClasses);
return processed !== value ? `'${processed}'` : match; return processed !== value ? `'${processed}'` : match;
}); });
// Process template literals // Process template literals
processedObj = processedObj.replace(/`([^`]*)`/g, (match, value) => { processedObj = processedObj.replace(/`([^`]*)`/g, (match, value) => {
const processed = processClassString(value); const processed = processClassString(value, scopedClasses);
return processed !== value ? `\`${processed}\`` : match; return processed !== value ? `\`${processed}\`` : match;
}); });
@@ -217,22 +334,14 @@ function processFileContent(content) {
// Pattern 8: class: cn(...) patterns (like in derived props) // Pattern 8: class: cn(...) patterns (like in derived props)
// Matches: class: cn('string1', 'string2', ...) // Matches: class: cn('string1', 'string2', ...)
processAndReplace(/class:\s*cn\s*\(([^)]+)\)/g, (match, args) => { processAndReplace(/class:\s*cn\s*\(([^)]+)\)/g, (match, args) => {
// Use a more sophisticated approach to handle multi-line strings and comments const processedArgs = processQuotedClassStrings(args, scopedClasses);
let processedArgs = args; return processedArgs !== args ? `class: cn(${processedArgs})` : match;
// Process single-quoted strings: 'content'
processedArgs = processedArgs.replace(/'([^']*)'/g, (match, content) => {
const processed = processClassString(content);
return processed !== content ? `'${processed}'` : match;
}); });
// Process double-quoted strings: "content" // Pattern 8b: class={cn(...)} in Svelte markup (supports embedded quotes in class strings)
processedArgs = processedArgs.replace(/"([^"]*)"/g, (match, content) => { processAndReplace(/class=\{cn\s*\(([\s\S]*?)\)\}/g, (match, args) => {
const processed = processClassString(content); const processedArgs = processQuotedClassStrings(args, scopedClasses);
return processed !== content ? `"${processed}"` : match; return processedArgs !== args ? `class={cn(${processedArgs})}` : match;
});
return `class: cn(${processedArgs})`;
}); });
return { content: modified, changes }; return { content: modified, changes };
@@ -242,7 +351,8 @@ function processFileContent(content) {
function processFile(filePath) { function processFile(filePath) {
try { try {
const content = fs.readFileSync(filePath, 'utf8'); const content = fs.readFileSync(filePath, 'utf8');
const { content: modified, changes } = processFileContent(content); const scopedClasses = path.extname(filePath) === '.svelte' ? extractScopedClassNames(content) : new Set();
const { content: modified, changes } = processFileContent(content, scopedClasses);
if (changes > 0) { if (changes > 0) {
if (DRY_RUN) { if (DRY_RUN) {
@@ -298,22 +408,163 @@ function findFiles(dir, extensions = ['.svelte', '.ts', '.tsx', '.js', '.jsx'])
return files; return files;
} }
// Main execution function runGitPaths(args) {
function main() { const result = spawnSync('git', args, {
const uiDir = path.join(__dirname); cwd: REPO_ROOT,
const srcDir = path.join(uiDir, 'src'); encoding: 'utf8'
});
if (!fs.existsSync(srcDir)) { if (result.status !== 0) {
console.error(`Error: ${srcDir} does not exist`); const errorOutput = result.stderr || result.stdout || `git ${args.join(' ')} failed`;
throw new Error(errorOutput.trim());
}
return result.stdout.split('\0').filter(Boolean);
}
function isUiSourceFile(relativePath) {
if (!relativePath.startsWith('packages/ui/src/')) {
return false;
}
return UI_SOURCE_EXTENSIONS.has(path.extname(relativePath));
}
function getScopedGitFiles(mode) {
let paths = [];
if (mode === 'staged') {
paths = runGitPaths(['diff', '--name-only', '-z', '--cached', '--diff-filter=ACMR']);
} else if (mode === 'changed') {
const baseRef = process.env.UI_PREFIX_GIT_BASE || 'origin/main';
paths = runGitPaths(['diff', '--name-only', '-z', '--diff-filter=ACMR', `${baseRef}...HEAD`]);
}
return paths.filter(isUiSourceFile).map((relativePath) => path.join(REPO_ROOT, relativePath));
}
function resolveFilesToProcess(srcDir) {
if (STAGED_ONLY) {
return getScopedGitFiles('staged');
}
if (CHANGED_ONLY) {
return getScopedGitFiles('changed');
}
return findFiles(srcDir);
}
const AUDIT_PATTERNS = [
{
id: 'corrupted-quoted-prefix',
label: 'ui:\'...\' / ui:"..." (prefix script broke Svelte/JS string literals)',
regex: /ui:['"]/
},
{
id: 'double-variant-prefix',
label: 'ui:variant:ui:utility (duplicate ui: inside variant chains)',
regex: /ui:[a-z0-9_-]+:ui:/
},
{
id: 'inverted-variant-prefix',
label: 'variant:ui:utility (ui: after variant instead of at start)',
regex: /(?:^|[\s"'`{])(?:hover|focus|active|disabled|dark|sm|md|lg|xl|2xl|group-hover|peer-focus):ui:/
}
];
function auditScopedHookMismatches(filePath, content) {
const findings = [];
if (!filePath.endsWith('.svelte')) {
return findings;
}
const scoped = extractScopedClassNames(content);
for (const cls of scoped) {
const prefixed = `${UI_PREFIX}${cls}`;
const pattern = new RegExp(`class=["'\`][^"'\`]*${prefixed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
if (pattern.test(content)) {
findings.push({
id: 'scoped-hook-prefixed',
label: `<style> hook .${cls} prefixed as ${prefixed} in markup`,
sample: prefixed
});
}
}
return findings;
}
function auditFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const findings = [];
for (const pattern of AUDIT_PATTERNS) {
if (pattern.regex.test(content)) {
findings.push({ id: pattern.id, label: pattern.label, sample: content.match(pattern.regex)?.[0] });
}
}
return findings.concat(auditScopedHookMismatches(filePath, content));
}
function runAudit() {
const files = resolveFilesToProcess(UI_SRC_DIR);
let issueCount = 0;
console.log(`Auditing ${files.length} file(s) in packages/ui/src for prefix corruption...\n`);
for (const file of files) {
const findings = auditFile(file);
if (findings.length === 0) {
continue;
}
issueCount += findings.length;
console.log(file);
for (const finding of findings) {
console.log(` - ${finding.label}${finding.sample ? ` (e.g. ${finding.sample})` : ''}`);
}
console.log('');
}
if (issueCount > 0) {
console.error(`❌ Found ${issueCount} prefix corruption issue(s).`);
process.exit(1); process.exit(1);
} }
console.log(`Searching for files in ${srcDir}...`); console.log('✓ No known prefix corruption patterns found.');
const files = findFiles(srcDir); }
console.log(`Found ${files.length} files to process\n`);
// Main execution
function main() {
if (AUDIT_MODE) {
runAudit();
return;
}
if (!fs.existsSync(UI_SRC_DIR)) {
console.error(`Error: ${UI_SRC_DIR} does not exist`);
process.exit(1);
}
const scopeLabel = STAGED_ONLY ? 'staged' : CHANGED_ONLY ? 'changed' : 'all';
const files = resolveFilesToProcess(UI_SRC_DIR);
if (files.length === 0) {
console.log(`No ${scopeLabel} packages/ui source files to check.`);
process.exit(0);
}
console.log(`Checking ${files.length} ${scopeLabel} file(s) in packages/ui/src...\n`);
if (DRY_RUN) { if (DRY_RUN) {
console.log('DRY RUN MODE - No files will be modified\n'); console.log(`${CHECK_MODE ? 'CHECK MODE' : 'DRY RUN MODE'} - No files will be modified\n`);
} }
let totalChanges = 0; let totalChanges = 0;
@@ -328,6 +579,12 @@ function main() {
} }
console.log(`\n${DRY_RUN ? 'Would process' : 'Processed'} ${filesChanged} files with ${totalChanges} total changes`); console.log(`\n${DRY_RUN ? 'Would process' : 'Processed'} ${filesChanged} files with ${totalChanges} total changes`);
if (CHECK_MODE && filesChanged > 0) {
console.error('\n❌ packages/ui Tailwind classes must use the ui: prefix.');
console.error("Run 'pnpm --filter @cio/ui prefix' to fix, then commit again.");
process.exit(1);
}
} }
main(); main();
+4
View File
@@ -8,6 +8,10 @@
"dev": "pnpm generate-css --watch", "dev": "pnpm generate-css --watch",
"format": "prettier --write .", "format": "prettier --write .",
"prefix:list": "node add-ui-prefix.cjs --dry-run", "prefix:list": "node add-ui-prefix.cjs --dry-run",
"prefix:check": "node add-ui-prefix.cjs --check",
"prefix:check:staged": "node add-ui-prefix.cjs --check --staged",
"prefix:check:changed": "node add-ui-prefix.cjs --check --changed",
"prefix:audit": "node add-ui-prefix.cjs --audit",
"prefix": "node add-ui-prefix.cjs && pnpm format" "prefix": "node add-ui-prefix.cjs && pnpm format"
}, },
"devDependencies": { "devDependencies": {
@@ -46,12 +46,7 @@
// of the other series, this overrides that // of the other series, this overrides that
'ui:[&_.lc-area-path]:opacity-100 ui:[&_.lc-highlight-line]:opacity-100 ui:[&_.lc-highlight-point]:opacity-100 ui:[&_.lc-spline-path]:opacity-100 ui:[&_.lc-text]:text-xs ui:[&_.lc-text-svg]:overflow-visible', 'ui:[&_.lc-area-path]:opacity-100 ui:[&_.lc-highlight-line]:opacity-100 ui:[&_.lc-highlight-point]:opacity-100 ui:[&_.lc-spline-path]:opacity-100 ui:[&_.lc-text]:text-xs ui:[&_.lc-text-svg]:overflow-visible',
// We don't want the little tick lines between the axis labels and the chart, so we remove // We don't want the little tick lines between the axis labels and the chart, so we remove // the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every // chart.'ui:[&_.lc-axis-tick]:stroke-0', // We don't want to display the rule on the x/y axis, as there is already going to be
// the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
// chart.
'ui:[&_.lc-axis-tick]:stroke-0',
// We don't want to display the rule on the x/y axis, as there is already going to be
// a grid line there and rule ends up overlapping the marks because it is rendered after // a grid line there and rule ends up overlapping the marks because it is rendered after
// the marks // the marks
'ui:[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 ui:[&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0', 'ui:[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 ui:[&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
@@ -102,7 +102,7 @@
<div <div
class={cn( class={cn(
'ui:[&>svg]:text-muted-foreground ui:flex ui:w-full ui:flex-wrap ui:items-stretch ui:gap-2 ui:[&>svg]:size-2.5', 'ui:[&>svg]:text-muted-foreground ui:flex ui:w-full ui:flex-wrap ui:items-stretch ui:gap-2 ui:[&>svg]:size-2.5',
indicator === 'dot' && 'items-center' indicator === 'dot' && 'ui:items-center'
)} )}
> >
{#if formatter && item.value !== undefined && item.name} {#if formatter && item.value !== undefined && item.name}
@@ -26,10 +26,8 @@
data-slot="dialog-content" data-slot="dialog-content"
class={cn( class={cn(
// `has-[[data-slot=dialog-footer][data-sticky=true]]:pb-0` drops the // `has-[[data-slot=dialog-footer][data-sticky=true]]:pb-0` drops the
// dialog's own bottom padding when a sticky footer is rendered — // dialog's own ui:bottom padding when a ui:sticky footer is rendered — // the footer's own `py-4` provides the bottom spacing instead, and
// the footer's own `py-4` provides the bottom spacing instead, and // it pins flush with the dialog's ui:bottom edge.'ui:bg-background ui:border ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-95 ui:fixed ui:left-[50%] ui:top-[50%] ui:z-200 ui:grid ui:w-full ui:max-w-[calc(100%-2rem)] ui:translate-x-[-50%] ui:translate-y-[-50%] ui:gap-4 ui:rounded-lg ui:p-6 ui:[&:has([data-slot=dialog-footer][data-sticky=true])]:pb-0 ui:shadow-lg ui:duration-200 ui:sm:max-w-lg',
// it pins flush with the dialog's bottom edge.
'ui:bg-background ui:border ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:fade-out-0 ui:data-[state=open]:fade-in-0 ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-95 ui:fixed ui:left-[50%] ui:top-[50%] ui:z-200 ui:grid ui:w-full ui:max-w-[calc(100%-2rem)] ui:translate-x-[-50%] ui:translate-y-[-50%] ui:gap-4 ui:rounded-lg ui:p-6 ui:[&:has([data-slot=dialog-footer][data-sticky=true])]:pb-0 ui:shadow-lg ui:duration-200 ui:sm:max-w-lg',
className className
)} )}
{...restProps} {...restProps}
@@ -33,7 +33,7 @@
{#if indeterminate} {#if indeterminate}
<MinusIcon class="ui:size-4" /> <MinusIcon class="ui:size-4" />
{:else} {:else}
<CheckIcon class={cn('ui:size-4', !checked && 'text-transparent')} /> <CheckIcon class={cn('ui:size-4', !checked && 'ui:text-transparent')} />
{/if} {/if}
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
@@ -20,7 +20,7 @@
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
class={cn( class={cn(
"ui:data-highlighted:bg-accent ui:data-highlighted:text-accent-foreground ui:data-[variant=destructive]:text-destructive ui:data-[variant=destructive]:data-highlighted:bg-destructive/10 ui:dark:ui:data-[variant=destructive]:data-highlighted:bg-destructive/20 ui:data-[variant=destructive]:data-highlighted:text-destructive ui:data-[variant=destructive]:*:[svg]:text-destructive! ui:[&_svg:not([class*='text-'])]:text-muted-foreground ui:outline-hidden ui:relative ui:flex ui:cursor-default ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-sm ui:data-disabled:pointer-events-none ui:data-inset:pl-8 ui:data-disabled:opacity-50 ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0", "ui:data-highlighted:bg-accent ui:data-highlighted:text-accent-foreground ui:data-[variant=destructive]:text-destructive ui:data-[variant=destructive]:data-highlighted:bg-destructive/10 ui:dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 ui:data-[variant=destructive]:data-highlighted:text-destructive ui:data-[variant=destructive]:*:[svg]:text-destructive! ui:[&_svg:not([class*='text-'])]:text-muted-foreground ui:outline-hidden ui:relative ui:flex ui:cursor-default ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-sm ui:data-disabled:pointer-events-none ui:data-inset:pl-8 ui:data-disabled:opacity-50 ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
+3 -3
View File
@@ -12,9 +12,9 @@
'ui:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px ui:has-[>[data-slot=field-content]]:items-start' 'ui:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px ui:has-[>[data-slot=field-content]]:items-start'
], ],
responsive: [ responsive: [
'@md/field-group:ui:flex-row @md/field-group:ui:items-center @md/field-group:ui:[&>*]:w-auto ui:flex-col ui:*:w-full ui:*.sr-only:w-auto', 'ui:@md/field-group:flex-row ui:@md/field-group:items-center ui:@md/field-group:[&>*]:w-auto ui:flex-col ui:*:w-full ui:*.sr-only:w-auto',
'@md/field-group:ui:[&>[data-slot=field-label]]:flex-auto', 'ui:@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:ui:has-[>[data-slot=field-content]]:items-start @md/field-group:ui:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px' 'ui:@md/field-group:has-[>[data-slot=field-content]]:items-start ui:@md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
] ]
} }
}, },
@@ -33,7 +33,7 @@
{#if indeterminate} {#if indeterminate}
<MinusIcon class="ui:size-4" /> <MinusIcon class="ui:size-4" />
{:else} {:else}
<CheckIcon class={cn('ui:size-4', !checked && 'text-transparent')} /> <CheckIcon class={cn('ui:size-4', !checked && 'ui:text-transparent')} />
{/if} {/if}
</span> </span>
{@render childrenProp?.()} {@render childrenProp?.()}
@@ -20,7 +20,7 @@
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
class={cn( class={cn(
"ui:focus:bg-accent ui:focus:text-accent-foreground ui:data-[variant=destructive]:text-destructive ui:data-[variant=destructive]:focus:bg-destructive/10 ui:dark:ui:data-[variant=destructive]:focus:bg-destructive/20 ui:data-[variant=destructive]:focus:text-destructive ui:data-[variant=destructive]:*:[svg]:text-destructive! ui:[&_svg:not([class*='text-'])]:text-muted-foreground ui:outline-hidden ui:relative ui:flex ui:cursor-default ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-sm ui:data-disabled:pointer-events-none ui:data-inset:pl-8 ui:data-disabled:opacity-50 ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0", "ui:focus:bg-accent ui:focus:text-accent-foreground ui:data-[variant=destructive]:text-destructive ui:data-[variant=destructive]:focus:bg-destructive/10 ui:dark:data-[variant=destructive]:focus:bg-destructive/20 ui:data-[variant=destructive]:focus:text-destructive ui:data-[variant=destructive]:*:[svg]:text-destructive! ui:[&_svg:not([class*='text-'])]:text-muted-foreground ui:outline-hidden ui:relative ui:flex ui:cursor-default ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-sm ui:data-disabled:pointer-events-none ui:data-inset:pl-8 ui:data-disabled:opacity-50 ui:[&_svg:not([class*='size-'])]:size-4 ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0",
className className
)} )}
{...restProps} {...restProps}
@@ -9,7 +9,7 @@
bind:ref bind:ref
data-slot="navigation-menu-content" data-slot="navigation-menu-content"
class={cn( class={cn(
'ui:data-[motion^=from-]:animate-in ui:data-[motion^=to-]:animate-out ui:data-[motion^=from-]:fade-in ui:data-[motion^=to-]:fade-out ui:data-[motion=from-end]:slide-in-from-end-52 ui:data-[motion=from-start]:slide-in-from-start-52 ui:data-[motion=to-end]:slide-out-to-end-52 ui:data-[motion=to-start]:slide-out-to-start-52 ui:start-0 ui:top-0 ui:w-full ui:md:ui:absolute ui:md:ui:w-auto', 'ui:data-[motion^=from-]:animate-in ui:data-[motion^=to-]:animate-out ui:data-[motion^=from-]:fade-in ui:data-[motion^=to-]:fade-out ui:data-[motion=from-end]:slide-in-from-end-52 ui:data-[motion=from-start]:slide-in-from-start-52 ui:data-[motion=to-end]:slide-out-to-end-52 ui:data-[motion=to-start]:slide-out-to-start-52 ui:start-0 ui:top-0 ui:w-full ui:md:absolute ui:md:w-auto',
'ui:group-data-[viewport=false]/navigation-menu:bg-popover ui:group-data-[viewport=false]/navigation-menu:text-popover-foreground ui:group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in ui:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out ui:group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 ui:group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 ui:group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 ui:group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 ui:**:data-[slot=navigation-menu-link]:focus:ring-0 ui:**:data-[slot=navigation-menu-link]:focus:outline-none ui:group-data-[viewport=false]/navigation-menu:top-full ui:group-data-[viewport=false]/navigation-menu:mt-1.5 ui:group-data-[viewport=false]/navigation-menu:overflow-hidden ui:group-data-[viewport=false]/navigation-menu:rounded-md ui:group-data-[viewport=false]/navigation-menu:border ui:group-data-[viewport=false]/navigation-menu:shadow ui:group-data-[viewport=false]/navigation-menu:duration-200', 'ui:group-data-[viewport=false]/navigation-menu:bg-popover ui:group-data-[viewport=false]/navigation-menu:text-popover-foreground ui:group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in ui:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out ui:group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 ui:group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 ui:group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 ui:group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 ui:**:data-[slot=navigation-menu-link]:focus:ring-0 ui:**:data-[slot=navigation-menu-link]:focus:outline-none ui:group-data-[viewport=false]/navigation-menu:top-full ui:group-data-[viewport=false]/navigation-menu:mt-1.5 ui:group-data-[viewport=false]/navigation-menu:overflow-hidden ui:group-data-[viewport=false]/navigation-menu:rounded-md ui:group-data-[viewport=false]/navigation-menu:border ui:group-data-[viewport=false]/navigation-menu:shadow ui:group-data-[viewport=false]/navigation-menu:duration-200',
className className
)} )}
@@ -3,7 +3,7 @@
import { tv } from 'tailwind-variants'; import { tv } from 'tailwind-variants';
export const navigationMenuTriggerStyle = tv({ export const navigationMenuTriggerStyle = tv({
base: 'ui:bg-background ui:hover:bg-accent ui:hover:text-accent-foreground ui:focus:bg-accent ui:focus:text-accent-foreground ui:data-[state=open]:ui:hover:bg-accent ui:data-[state=open]:ui:text-accent-foreground ui:data-[state=open]:ui:focus:bg-accent ui:data-[state=open]:ui:bg-accent/50 ui:focus-visible:ring-ring/50 ui:group ui:inline-flex ui:h-9 ui:w-max ui:items-center ui:justify-center ui:rounded-md ui:px-4 ui:py-2 ui:text-sm ui:font-medium ui:outline-none ui:transition-[color,box-shadow] ui:focus-visible:outline-1 ui:focus-visible:ring-[3px] ui:disabled:pointer-events-none ui:disabled:opacity-50' base: 'ui:bg-background ui:hover:bg-accent ui:hover:text-accent-foreground ui:focus:bg-accent ui:focus:text-accent-foreground ui:data-[state=open]:hover:bg-accent ui:data-[state=open]:text-accent-foreground ui:data-[state=open]:focus:bg-accent ui:data-[state=open]:bg-accent/50 ui:focus-visible:ring-ring/50 ui:group ui:inline-flex ui:h-9 ui:w-max ui:items-center ui:justify-center ui:rounded-md ui:px-4 ui:py-2 ui:text-sm ui:font-medium ui:outline-none ui:transition-[color,box-shadow] ui:focus-visible:outline-1 ui:focus-visible:ring-[3px] ui:disabled:pointer-events-none ui:disabled:opacity-50'
}); });
</script> </script>
@@ -28,7 +28,7 @@
{@render children?.()} {@render children?.()}
<ChevronDownIcon <ChevronDownIcon
class="ui:relative ui:top-px ui:ms-1 ui:size-3 ui:transition ui:duration-300 ui:group-data-[state=open]:ui:rotate-180" class="ui:relative ui:top-px ui:ms-1 ui:size-3 ui:transition ui:duration-300 ui:group-data-[state=open]:rotate-180"
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
@@ -10,7 +10,7 @@
bind:ref bind:ref
data-slot="navigation-menu-viewport" data-slot="navigation-menu-viewport"
class={cn( class={cn(
'ui:origin-top-center ui:bg-popover ui:text-popover-foreground ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-90 ui:relative ui:mt-1.5 ui:h-(--bits-navigation-menu-viewport-height) ui:w-full ui:overflow-hidden ui:rounded-md ui:border ui:shadow ui:md:ui:w-[var(--bits-navigation-menu-viewport-width)]', 'ui:origin-top-center ui:bg-popover ui:text-popover-foreground ui:data-[state=open]:animate-in ui:data-[state=closed]:animate-out ui:data-[state=closed]:zoom-out-95 ui:data-[state=open]:zoom-in-90 ui:relative ui:mt-1.5 ui:h-(--bits-navigation-menu-viewport-height) ui:w-full ui:overflow-hidden ui:rounded-md ui:border ui:shadow ui:md:w-[var(--bits-navigation-menu-viewport-width)]',
className className
)} )}
{...restProps} {...restProps}
@@ -29,7 +29,7 @@
data-active={isActive} data-active={isActive}
class={cn( class={cn(
buttonVariants({ buttonVariants({
variant: isActive ? 'outline' : 'ghost', variant: isActive ? 'ui:outline' : 'ghost',
size size
}), }),
className className
+1 -1
View File
@@ -78,7 +78,7 @@
data-slot="sidebar-container" data-slot="sidebar-container"
class={cn( class={cn(
'ui:w-(--sidebar-width) ui:fixed ui:inset-y-0 ui:z-100 ui:hidden ui:max-h-svh ui:transition-[left,right,width] ui:duration-200 ui:ease-linear ui:md:flex', 'ui:w-(--sidebar-width) ui:fixed ui:inset-y-0 ui:z-100 ui:hidden ui:max-h-svh ui:transition-[left,right,width] ui:duration-200 ui:ease-linear ui:md:flex',
side === 'left' side === 'ui:left'
? 'ui:left-0 ui:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]' ? 'ui:left-0 ui:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'ui:right-0 ui:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', : 'ui:right-0 ui:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants. // Adjust the padding for floating and inset variants.
@@ -16,7 +16,8 @@
let { let {
variant, variant,
duration = 1, duration = 1,
class: className = 'text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-20', class:
className = 'ui:text-4xl ui:font-bold ui:tracking-[-0.02em] ui:drop-shadow-sm ui:md:text-7xl ui:md:leading-20',
children children
}: Props = $props(); }: Props = $props();
@@ -10,6 +10,6 @@
let { children, className }: Props = $props(); let { children, className }: Props = $props();
</script> </script>
<div class={cn('p-4 text-left', className)}> <div class={cn('ui:p-4 ui:text-left', className)}>
{@render children()} {@render children()}
</div> </div>
@@ -13,15 +13,19 @@
<div <div
class={cn( class={cn(
'relative mx-auto w-full border border-dashed border-zinc-300 px-8 shadow-sm sm:px-6 md:px-8 dark:border-zinc-800 dark:bg-[#09090B]', 'ui:relative ui:mx-auto ui:w-full ui:border ui:border-dashed ui:border-zinc-300 ui:px-8 ui:shadow-sm ui:sm:px-6 ui:md:px-8 ui:dark:border-zinc-800 ui:dark:bg-[#09090B]',
className className
)} )}
> >
<div class="absolute top-4 left-0 z-0 h-px w-full bg-zinc-400 sm:top-6 md:top-8 dark:bg-zinc-700"></div> <div
<div class="absolute bottom-4 left-0 z-0 h-px w-full bg-zinc-400 sm:bottom-6 md:bottom-8 dark:bg-zinc-700"></div> class="ui:absolute ui:top-4 ui:left-0 ui:z-0 ui:h-px ui:w-full ui:bg-zinc-400 ui:sm:top-6 ui:md:top-8 ui:dark:bg-zinc-700"
<div class="relative w-full border-x border-zinc-400 dark:border-zinc-700"> ></div>
<div
class="ui:absolute ui:bottom-4 ui:left-0 ui:z-0 ui:h-px ui:w-full ui:bg-zinc-400 ui:sm:bottom-6 ui:md:bottom-8 ui:dark:bg-zinc-700"
></div>
<div class="ui:relative ui:w-full ui:border-x ui:border-zinc-400 ui:dark:border-zinc-700">
<Ellipses /> <Ellipses />
<div class="relative z-20 mx-auto py-8"> <div class="ui:relative ui:z-20 ui:mx-auto ui:py-8">
{@render children?.()} {@render children?.()}
</div> </div>
</div> </div>
@@ -3,11 +3,11 @@
'rounded-full outline outline-8 dark:outline-gray-950 sm:my-6 md:my-8 size-1 my-4 outline-gray-50 bg-green-400'; 'rounded-full outline outline-8 dark:outline-gray-950 sm:my-6 md:my-8 size-1 my-4 outline-gray-50 bg-green-400';
</script> </script>
<div class="absolute z-0 grid h-full w-full items-center gap-8 lg:grid-cols-2"> <div class="ui:absolute ui:z-0 ui:grid ui:h-full ui:w-full ui:items-center ui:gap-8 ui:lg:grid-cols-2">
<section class="absolute z-0 grid h-full w-full grid-cols-2 place-content-between"> <section class="ui:absolute ui:z-0 ui:grid ui:h-full ui:w-full ui:place-content-between grid-cols-2">
<div class={`${sharedClass} -mx-[2.5px]`}></div> <div class={`${sharedClass} ui:-mx-[2.5px]`}></div>
<div class={`${sharedClass} -mx-[2px] place-self-end`}></div> <div class={`${sharedClass} ui:-mx-[2px] ui:place-self-end`}></div>
<div class={`${sharedClass} -mx-[2.5px]`}></div> <div class={`${sharedClass} ui:-mx-[2.5px]`}></div>
<div class={`${sharedClass} -mx-[2px] place-self-end`}></div> <div class={`${sharedClass} ui:-mx-[2px] ui:place-self-end`}></div>
</section> </section>
</div> </div>
@@ -275,14 +275,14 @@
<div <div
bind:this={root} bind:this={root}
class={cn( class={cn(
'relative h-full w-full', 'ui:relative ui:h-full ui:w-full',
glowColor == null && 'ui:[--dot-field-glow:#fff] ui:dark:[--dot-field-glow:#0a0a0a]', glowColor == null && 'ui:[--dot-field-glow:#fff] ui:dark:[--dot-field-glow:#0a0a0a]',
className className
)} )}
style={glowColor != null ? `--dot-field-glow: ${glowColor}` : undefined} style={glowColor != null ? `--dot-field-glow: ${glowColor}` : undefined}
> >
<canvas bind:this={canvas} class="absolute inset-0 h-full w-full"></canvas> <canvas bind:this={canvas} class="ui:absolute ui:inset-0 ui:h-full ui:w-full"></canvas>
<svg class="pointer-events-none absolute inset-0 h-full w-full" aria-hidden="true"> <svg class="ui:pointer-events-none ui:absolute ui:inset-0 ui:h-full ui:w-full" aria-hidden="true">
<defs> <defs>
<radialGradient id={glowId}> <radialGradient id={glowId}>
<stop offset="0%" stop-color="var(--dot-field-glow, #fff)" /> <stop offset="0%" stop-color="var(--dot-field-glow, #fff)" />
@@ -32,7 +32,7 @@
<svg <svg
aria-hidden="true" aria-hidden="true"
class={cn('pointer-events-none absolute inset-0 h-full w-full', className)} class={cn('ui:pointer-events-none ui:absolute ui:inset-0 ui:h-full ui:w-full', className)}
{...restProps} {...restProps}
fill={fillColor} fill={fillColor}
> >
@@ -16,7 +16,7 @@
} }
let { let {
class: className = 'drop-shadow-sm', class: className = 'ui:drop-shadow-sm',
containerClass = '', containerClass = '',
words = 'Gradual Spacing', words = 'Gradual Spacing',
duration = 0.5, duration = 0.5,
@@ -30,7 +30,7 @@
const wordsspilit = $derived(words.split('')); const wordsspilit = $derived(words.split(''));
</script> </script>
<div class={cn('flex justify-center space-x-1', containerClass)}> <div class={cn('ui:flex ui:justify-center ui:space-x-1', containerClass)}>
<AnimatePresence let:_item list={[{ key: wordsspilit }]}> <AnimatePresence let:_item list={[{ key: wordsspilit }]}>
{#each wordsspilit as char, i} {#each wordsspilit as char, i}
<Motion <Motion
@@ -89,34 +89,34 @@
const selectedAnimation = $derived(animationVariants[animationStyle]); const selectedAnimation = $derived(animationVariants[animationStyle]);
</script> </script>
<div class="relative"> <div class="ui:relative">
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="relative cursor-pointer" onclick={openVideo} role="presentation"> <div class="ui:relative ui:cursor-pointer" onclick={openVideo} role="presentation">
<img <img
src={thumbnailSrc} src={thumbnailSrc}
alt={thumbnailAlt} alt={thumbnailAlt}
width={1920} width={1920}
height={1080} height={1080}
class="w-full rounded-2xl transition-all duration-200" class="ui:w-full ui:rounded-2xl ui:transition-all ui:duration-200"
/> />
<div class="absolute inset-0 flex items-center justify-center"> <div class="ui:absolute ui:inset-0 ui:flex ui:items-center ui:justify-center">
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="flex size-24 items-center justify-center rounded-full border border-neutral-400 bg-black backdrop-blur-md transition-transform duration-300 ease-out" class="ui:flex ui:size-24 ui:items-center ui:justify-center ui:rounded-full ui:border ui:border-neutral-400 ui:bg-black ui:backdrop-blur-md ui:transition-transform ui:duration-300 ui:ease-out"
onmouseenter={() => (isPlayHovered = true)} onmouseenter={() => (isPlayHovered = true)}
onmouseleave={() => (isPlayHovered = false)} onmouseleave={() => (isPlayHovered = false)}
role="presentation" role="presentation"
> >
<div <div
class="relative flex size-20 items-center justify-center rounded-full border border-neutral-400 bg-black backdrop-blur-2xl transition-all duration-300 ease-out" class="ui:relative ui:flex ui:size-20 ui:items-center ui:justify-center ui:rounded-full ui:border ui:border-neutral-400 ui:bg-black ui:backdrop-blur-2xl ui:transition-all ui:duration-300 ui:ease-out"
> >
<span <span
class="inline-flex size-8 items-center justify-center transition-transform duration-300 ease-out" class="ui:inline-flex ui:size-8 ui:items-center ui:justify-center ui:transition-transform ui:duration-300 ui:ease-out"
style:transform={`scale(${isPlayHovered ? 1.1 : 1})`} style:transform={`scale(${isPlayHovered ? 1.1 : 1})`}
style:color={iconColor} style:color={iconColor}
> >
<Play strokeWidth={1.7} class="size-full" /> <Play strokeWidth={1.7} class="ui:size-full" />
</span> </span>
</div> </div>
</div> </div>
@@ -126,7 +126,10 @@
<AnimatePresence let:item list={[{ key: isVideoOpen }]}> <AnimatePresence let:item list={[{ key: isVideoOpen }]}>
{#if item.key} {#if item.key}
<Motion initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} let:motion> <Motion initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} let:motion>
<div use:motion class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"> <div
use:motion
class="ui:fixed ui:inset-0 ui:z-50 ui:flex ui:items-center ui:justify-center ui:bg-black/50 ui:backdrop-blur-md"
>
<Motion <Motion
initial={selectedAnimation.initial} initial={selectedAnimation.initial}
animate={selectedAnimation.animate} animate={selectedAnimation.animate}
@@ -134,28 +137,28 @@
transition={{ type: 'spring', damping: 30, stiffness: 300 }} transition={{ type: 'spring', damping: 30, stiffness: 300 }}
let:motion let:motion
> >
<div use:motion class="relative mx-4 aspect-video w-full max-w-4xl md:mx-0"> <div use:motion class="ui:relative ui:mx-4 ui:w-full ui:max-w-4xl ui:md:mx-0 aspect-video">
<Motion let:motion whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}> <Motion let:motion whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
<button <button
type="button" type="button"
use:motion use:motion
class="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 backdrop-blur-md" class="ui:absolute ui:-top-16 ui:right-0 ui:rounded-full ui:bg-neutral-900/50 ui:p-2 ui:text-xl ui:text-white ui:ring-1 ui:backdrop-blur-md"
onclick={closeVideo} onclick={closeVideo}
onmouseenter={() => (isCloseHovered = true)} onmouseenter={() => (isCloseHovered = true)}
onmouseleave={() => (isCloseHovered = false)} onmouseleave={() => (isCloseHovered = false)}
> >
<X class="size-5 text-white" /> <X class="ui:size-5 ui:text-white" />
</button> </button>
</Motion> </Motion>
<Motion animate={{ scale: isCloseHovered ? 0.98 : 1 }} transition={{ duration: 0.2 }} let:motion> <Motion animate={{ scale: isCloseHovered ? 0.98 : 1 }} transition={{ duration: 0.2 }} let:motion>
<div <div
use:motion use:motion
class="relative isolate z-1 size-full overflow-hidden rounded-2xl border-2 border-white" class="ui:relative ui:z-1 ui:size-full ui:overflow-hidden ui:rounded-2xl ui:border-2 ui:border-white isolate"
> >
<!-- svelte-ignore a11y_missing_attribute --> <!-- svelte-ignore a11y_missing_attribute -->
<iframe <iframe
src={videoSrc} src={videoSrc}
class="size-full rounded-2xl" class="ui:size-full ui:rounded-2xl"
allowfullscreen allowfullscreen
title="Video" title="Video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
@@ -50,16 +50,16 @@
onmousemove={handleMouseMove} onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave} onmouseleave={handleMouseLeave}
class={cn( class={cn(
'group relative flex size-full justify-center overflow-hidden rounded-xl border bg-neutral-100 py-4 text-black dark:bg-neutral-900 dark:text-white', 'group ui:relative ui:flex ui:size-full ui:justify-center ui:overflow-hidden ui:rounded-xl ui:border ui:bg-neutral-100 ui:py-4 ui:text-black ui:dark:bg-neutral-900 ui:dark:text-white',
className className
)} )}
> >
<div class="relative z-10"> <div class="ui:relative ui:z-10">
{#if children} {#if children}
{@render children()} {@render children()}
{:else} {:else}
<div class="flex h-full items-center justify-center text-center"> <div class="ui:flex ui:h-full ui:items-center ui:justify-center ui:text-center">
<p class="text-2xl">Magic Card</p> <p class="ui:text-2xl">Magic Card</p>
</div> </div>
{/if} {/if}
</div> </div>
@@ -72,7 +72,7 @@
> >
<div <div
use:motion use:motion
class="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100" class="ui:pointer-events-none ui:absolute ui:-inset-px ui:rounded-xl ui:opacity-0 ui:transition-opacity ui:duration-300 ui:group-hover:opacity-100"
></div> ></div>
</Motion> </Motion>
</div> </div>
@@ -14,9 +14,9 @@
<p <p
style:--shimmer-width="{shimmerWidth}px" style:--shimmer-width="{shimmerWidth}px"
class={cn( class={cn(
'mx-auto max-w-md text-neutral-600/50 dark:text-neutral-400/50', 'ui:mx-auto ui:max-w-md ui:text-neutral-600/50 ui:dark:text-neutral-400/50',
'animate-shimmer bg-size-[var(--shimmer-width)_100%] bg-clip-text bg-position-[0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]', 'ui:animate-shimmer ui:bg-size-[var(--shimmer-width)_100%] ui:bg-clip-text ui:bg-position-[0_0] ui:bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]',
'bg-linear-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80', 'ui:bg-linear-to-r ui:from-transparent ui:via-black/80 ui:via-50% ui:to-transparent ui:dark:via-white/80',
className className
)} )}
> >
@@ -23,8 +23,8 @@
transition={{ duration: 0.8, repeat: Infinity, delay }} transition={{ duration: 0.8, repeat: Infinity, delay }}
let:motion let:motion
> >
<div {id} use:motion class="pointer-events-none absolute z-20" style:left={x} style:top={y}> <div {id} use:motion class="ui:pointer-events-none ui:absolute ui:z-20" style:left={x} style:top={y}>
<svg width="21" height="21" viewBox="0 0 21 21" class="block"> <svg width="21" height="21" viewBox="0 0 21 21" class="ui:block">
<path <path
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z" d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
fill={color} fill={color}
@@ -63,7 +63,7 @@
</script> </script>
<div style:--sparkles-first-color="{colors.first};" style:--sparkles-second-color="{colors.second};" class={className}> <div style:--sparkles-first-color="{colors.first};" style:--sparkles-second-color="{colors.second};" class={className}>
<span class="relative inline-block"> <span class="ui:relative ui:inline-block">
{#each sparkles as item} {#each sparkles as item}
<SparkleParticle {...item} /> <SparkleParticle {...item} />
{/each} {/each}
@@ -10,6 +10,6 @@
let { children, className }: Props = $props(); let { children, className }: Props = $props();
</script> </script>
<div class={cn('text-left', className)}> <div class={cn('ui:text-left', className)}>
{@render children()} {@render children()}
</div> </div>
@@ -18,7 +18,7 @@
stroke-width="1" stroke-width="1"
stroke-linecap="round" stroke-linecap="round"
stroke="currentColor" stroke="currentColor"
class={cn('absolute text-black dark:text-white', className)} class={cn('ui:absolute ui:text-black ui:dark:text-white', className)}
{...rest} {...rest}
> >
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
@@ -109,7 +109,7 @@
> >
<button <button
type="button" type="button"
class="ui:flex ui:size-7 ui:items-center ui:justify-center ui:rounded-full ui:transition-colors hover:ui:bg-muted" class="ui:flex ui:size-7 ui:items-center ui:justify-center ui:rounded-full ui:transition-colors ui:hover:bg-muted"
aria-label="Zoom out" aria-label="Zoom out"
onclick={handleZoomOut} onclick={handleZoomOut}
> >
@@ -120,7 +120,7 @@
</span> </span>
<button <button
type="button" type="button"
class="ui:flex ui:size-7 ui:items-center ui:justify-center ui:rounded-full ui:transition-colors hover:ui:bg-muted" class="ui:flex ui:size-7 ui:items-center ui:justify-center ui:rounded-full ui:transition-colors ui:hover:bg-muted"
aria-label="Zoom in" aria-label="Zoom in"
onclick={handleZoomIn} onclick={handleZoomIn}
> >
@@ -128,7 +128,7 @@
</button> </button>
<button <button
type="button" type="button"
class="ui:flex ui:size-7 ui:items-center ui:justify-center ui:rounded-full ui:transition-colors hover:ui:bg-muted" class="ui:flex ui:size-7 ui:items-center ui:justify-center ui:rounded-full ui:transition-colors ui:hover:bg-muted"
aria-label="Fit to viewport" aria-label="Fit to viewport"
onclick={handleFit} onclick={handleFit}
> >
@@ -55,7 +55,7 @@
/> />
</Field.Field> </Field.Field>
{#if footer} {#if footer}
<div class="-mt-1 min-w-0 px-4 pb-3"> <div class="ui:-mt-1 ui:min-w-0 ui:px-4 ui:pb-3">
{@render footer()} {@render footer()}
</div> </div>
{/if} {/if}
@@ -36,7 +36,7 @@
width={size} width={size}
height={size} height={size}
viewBox={`0 0 ${size} ${size}`} viewBox={`0 0 ${size} ${size}`}
class={cn('shrink-0 -rotate-90', className)} class={cn('ui:-rotate-90 ui:shrink-0', className)}
aria-valuenow={clampedValue} aria-valuenow={clampedValue}
aria-valuemin={0} aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
@@ -48,7 +48,7 @@
r={radius} r={radius}
fill="none" fill="none"
stroke-width={strokeWidth} stroke-width={strokeWidth}
class={cn('transition-all', trackClass)} class={cn('ui:transition-all', trackClass)}
/> />
<circle <circle
cx={center} cx={center}
@@ -59,6 +59,6 @@
stroke-dasharray={circumference} stroke-dasharray={circumference}
stroke-dashoffset={offset} stroke-dashoffset={offset}
stroke-linecap="round" stroke-linecap="round"
class={cn('transition-all duration-300', progressClass)} class={cn('ui:transition-all ui:duration-300', progressClass)}
/> />
</svg> </svg>
@@ -19,7 +19,7 @@
{...props} {...props}
data-code-overflow data-code-overflow
data-collapsed={collapsed} data-collapsed={collapsed}
class={cn('ui:relative ui:overflow-y-hidden ui:data-[collapsed=true]:ui:max-h-[300px]', className)} class={cn('ui:relative ui:overflow-y-hidden ui:data-[collapsed=true]:max-h-[300px]', className)}
> >
{@render children?.()} {@render children?.()}
{#if collapsed} {#if collapsed}
@@ -63,7 +63,7 @@
href={fileUrl} href={fileUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="ui:border-input ui:bg-background ui:text-foreground hover:ui:bg-accent hover:ui:text-accent-foreground ui:shrink ui:inline-flex ui:items-center ui:gap-1.5 ui:rounded-md ui:border ui:px-3 ui:py-1.5 ui:text-sm ui:font-medium" class="ui:border-input ui:bg-background ui:text-foreground ui:hover:bg-accent ui:hover:text-accent-foreground ui:shrink ui:inline-flex ui:items-center ui:gap-1.5 ui:rounded-md ui:border ui:px-3 ui:py-1.5 ui:text-sm ui:font-medium"
> >
{viewLabel} {viewLabel}
</a> </a>
@@ -72,7 +72,7 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
download={title} download={title}
class="ui:border-input ui:bg-background ui:text-foreground hover:ui:bg-accent hover:ui:text-accent-foreground ui:shrink ui:inline-flex ui:items-center ui:gap-1.5 ui:rounded-md ui:border ui:px-3 ui:py-1.5 ui:text-sm ui:font-medium" class="ui:border-input ui:bg-background ui:text-foreground ui:hover:bg-accent ui:hover:text-accent-foreground ui:shrink ui:inline-flex ui:items-center ui:gap-1.5 ui:rounded-md ui:border ui:px-3 ui:py-1.5 ui:text-sm ui:font-medium"
> >
<DownloadIcon class="ui:size-4" /> <DownloadIcon class="ui:size-4" />
{downloadLabel} {downloadLabel}
@@ -71,6 +71,6 @@
</Button> </Button>
</div> </div>
<pre bind:this={preRef} draggable={false}> <pre bind:this={preRef} draggable={false}>
<NodeViewContent as="code" class={`language-${defaultLanguage} ui:pt-3!`} {...node.attrs} /> <NodeViewContent as="code" class={`language- ${defaultLanguage} ui:pt-3!`} {...node.attrs} />
</pre> </pre>
</NodeViewWrapper> </NodeViewWrapper>
@@ -142,10 +142,10 @@
id="resizable-container-media" id="resizable-container-media"
class={cn( class={cn(
'ui:relative ui:my-2 ui:flex ui:flex-col ui:rounded-md ui:border-2 ui:border-transparent', 'ui:relative ui:my-2 ui:flex ui:flex-col ui:rounded-md ui:border-2 ui:border-transparent',
selected ? 'border-muted-foreground' : '', selected ? 'ui:border-muted-foreground' : '',
node.attrs.align === 'left' && 'left-0 -translate-x-0', node.attrs.align === 'ui:left' && 'ui:left-0 ui:-translate-x-0',
node.attrs.align === 'center' && 'left-1/2 -translate-x-1/2', node.attrs.align === 'center' && 'ui:left-1/2 ui:-translate-x-1/2',
node.attrs.align === 'right' && 'left-full -translate-x-full' node.attrs.align === 'ui:right' && 'ui:left-full ui:-translate-x-full'
)} )}
style={`width: ${node.attrs.width}`} style={`width: ${node.attrs.width}`}
> >
@@ -191,13 +191,13 @@
<div <div
class={cn( class={cn(
'ui:bg-background/50 ui:absolute ui:-top-2 ui:left-[calc(50%-3rem)] ui:flex ui:items-center ui:gap-1 ui:rounded ui:border ui:p-1 ui:opacity-0 ui:backdrop-blur-sm ui:transition-opacity', 'ui:bg-background/50 ui:absolute ui:-top-2 ui:left-[calc(50%-3rem)] ui:flex ui:items-center ui:gap-1 ui:rounded ui:border ui:p-1 ui:opacity-0 ui:backdrop-blur-sm ui:transition-opacity',
!resizing && 'group-hover:opacity-100', !resizing && 'ui:group-hover:opacity-100',
openedMore && 'opacity-100' openedMore && 'ui:opacity-100'
)} )}
> >
<Button <Button
variant="ghost" variant="ghost"
class={cn('ui:size-6 ui:p-0', node.attrs.align === 'left' && 'bg-muted')} class={cn('ui:size-6 ui:p-0', node.attrs.align === 'ui:left' && 'ui:bg-muted')}
onclick={() => updateAttributes({ align: 'left' })} onclick={() => updateAttributes({ align: 'left' })}
title="Align Left" title="Align Left"
> >
@@ -205,7 +205,7 @@
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
class={cn('ui:size-6 ui:p-0', node.attrs.align === 'center' && 'bg-muted')} class={cn('ui:size-6 ui:p-0', node.attrs.align === 'center' && 'ui:bg-muted')}
onclick={() => updateAttributes({ align: 'center' })} onclick={() => updateAttributes({ align: 'center' })}
title="Align Center" title="Align Center"
> >
@@ -213,7 +213,7 @@
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
class={cn('ui:size-6 ui:p-0', node.attrs.align === 'right' && 'bg-muted')} class={cn('ui:size-6 ui:p-0', node.attrs.align === 'ui:right' && 'ui:bg-muted')}
onclick={() => updateAttributes({ align: 'right' })} onclick={() => updateAttributes({ align: 'right' })}
title="Align Right" title="Align Right"
> >
@@ -107,7 +107,7 @@
<Button <Button
id={`${groupIndex}-${commandIndex}`} id={`${groupIndex}-${commandIndex}`}
variant="ghost" variant="ghost"
class={cn('ui:h-8 ui:w-full ui:justify-start ui:gap-2 ui:rounded-sm', isActive && 'bg-muted')} class={cn('ui:h-8 ui:w-full ui:justify-start ui:gap-2 ui:rounded-sm', isActive && 'ui:bg-muted')}
onclick={() => selectItem(groupIndex, commandIndex)} onclick={() => selectItem(groupIndex, commandIndex)}
> >
<Icon /> <Icon />
@@ -17,7 +17,7 @@
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class={cn(command.isActive?.(editor) && 'bg-muted')} class={cn(command.isActive?.(editor) && 'ui:bg-muted')}
onclick={() => command.onClick?.(editor)} onclick={() => command.onClick?.(editor)}
disabled={command.clickable ? !command.clickable(editor) : false} disabled={command.clickable ? !command.clickable(editor) : false}
> >
@@ -53,8 +53,8 @@
variant="ghost" variant="ghost"
class={cn( class={cn(
`size-6 border-0 p-0 font-normal`, `size-6 border-0 p-0 font-normal`,
editor.isActive('textStyle', { color: color.value }) && 'border-2 font-extrabold', editor.isActive('ui:textStyle', { color: color.value }) && 'ui:border-2 ui:font-extrabold',
color.value === '' && 'border' color.value === '' && 'ui:border'
)} )}
style={`color: ${color.value}; background-color: ${color.value}50; border-color: ${color.value};`} style={`color: ${color.value}; background-color: ${color.value}50; border-color: ${color.value};`}
title={color.label} title={color.label}
@@ -79,8 +79,8 @@
variant="ghost" variant="ghost"
class={cn( class={cn(
`size-6 border-0 p-0 font-normal`, `size-6 border-0 p-0 font-normal`,
editor.isActive('highlight', { color: color.value }) && 'border-2', editor.isActive('highlight', { color: color.value }) && 'ui:border-2',
color.value === '' && 'border' color.value === '' && 'ui:border'
)} )}
style={`background-color: ${color.value}50; border-color: ${color.value};`} style={`background-color: ${color.value}50; border-color: ${color.value};`}
title={color.label} title={color.label}
@@ -97,7 +97,7 @@
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class={cn('ui:transition-transform', showMore && 'bg-muted rotate-90')} class={cn('ui:transition-transform', showMore && 'ui:bg-muted ui:rotate-90')}
onclick={() => (showMore = !showMore)} onclick={() => (showMore = !showMore)}
title="Show More" title="Show More"
> >
@@ -111,7 +111,7 @@
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class={cn(caseSensitive && 'bg-muted')} class={cn(caseSensitive && 'ui:bg-muted')}
onclick={() => { onclick={() => {
caseSensitive = !caseSensitive; caseSensitive = !caseSensitive;
updateSearchTerm(); updateSearchTerm();
@@ -105,7 +105,7 @@
</script> </script>
<div class="drag-handle"> <div class="drag-handle">
<Button variant="ghost" class="!ui:size-6 ui:rounded-sm ui:p-0" onclick={() => (open = true)}> <Button variant="ghost" class="ui:!size-6 ui:rounded-sm ui:p-0" onclick={() => (open = true)}>
<GripVertical /> <GripVertical />
</Button> </Button>
<DropdownMenu.Root bind:open> <DropdownMenu.Root bind:open>
@@ -26,7 +26,7 @@
}: Props = $props(); }: Props = $props();
</script> </script>
<div class="flex justify-end gap-3"> <div class="ui:flex ui:justify-end ui:gap-3">
<Button type="button" variant="outline" size="sm" onclick={() => onPrevious()} disabled={!canGoBack || isSubmitting}> <Button type="button" variant="outline" size="sm" onclick={() => onPrevious()} disabled={!canGoBack || isSubmitting}>
{previousLabel} {previousLabel}
</Button> </Button>
@@ -85,7 +85,7 @@
<div class="ui:flex ui:max-h-64 ui:flex-col ui:gap-0 ui:overflow-y-auto"> <div class="ui:flex ui:max-h-64 ui:flex-col ui:gap-0 ui:overflow-y-auto">
{#each FILE_UPLOAD_SUPPORTED_TYPES as fileType (fileType.value)} {#each FILE_UPLOAD_SUPPORTED_TYPES as fileType (fileType.value)}
<label <label
class="ui:flex ui:cursor-pointer ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-2 ui:text-sm hover:ui:bg-accent hover:ui:text-accent-foreground {disabled class="ui:flex ui:cursor-pointer ui:select-none ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-2 ui:text-sm ui:hover:bg-accent ui:hover:text-accent-foreground {disabled
? 'ui:pointer-events-none ui:opacity-50' ? 'ui:pointer-events-none ui:opacity-50'
: ''}" : ''}"
> >
@@ -10,13 +10,13 @@ export function getOptionNumericId(option: { id?: number | string }): number | n
export function getMcqReviewRowBorderClass(isSelected: boolean, isCorrect: boolean): string { export function getMcqReviewRowBorderClass(isSelected: boolean, isCorrect: boolean): string {
if (isSelected && isCorrect) { if (isSelected && isCorrect) {
return 'ui:border-emerald-500 ui:bg-emerald-50/40 dark:ui:bg-emerald-950/30'; return 'ui:border-emerald-500 ui:bg-emerald-50/40 ui:dark:bg-emerald-950/30';
} }
if (isSelected && !isCorrect) { if (isSelected && !isCorrect) {
return 'ui:border-red-500 ui:bg-red-50/40 dark:ui:bg-red-950/30'; return 'ui:border-red-500 ui:bg-red-50/40 ui:dark:bg-red-950/30';
} }
if (!isSelected && isCorrect) { if (!isSelected && isCorrect) {
return 'ui:border-emerald-300 ui:bg-emerald-50/20 dark:ui:bg-emerald-950/20'; return 'ui:border-emerald-300 ui:bg-emerald-50/20 ui:dark:bg-emerald-950/20';
} }
return 'ui:border-muted'; return 'ui:border-muted';
} }
@@ -23,7 +23,7 @@
{#if onPick && !disabled} {#if onPick && !disabled}
<button <button
type="button" type="button"
class="ui:rounded-md ui:p-1 ui:text-amber-600 ui:transition-colors ui:hover:bg-amber-500/10 focus-visible:ui:ring-ring ui:outline-none focus-visible:ui:ring-2 ui:dark:text-amber-400" class="ui:rounded-md ui:p-1 ui:text-amber-600 ui:transition-colors ui:hover:bg-amber-500/10 ui:focus-visible:ring-ring ui:outline-none ui:focus-visible:ring-2 ui:dark:text-amber-400"
aria-label={starLabel(starIndex)} aria-label={starLabel(starIndex)}
aria-pressed={value >= starIndex} aria-pressed={value >= starIndex}
{disabled} {disabled}
@@ -20,7 +20,7 @@
<ToggleGroup.Root type="single" value={studentValue} disabled={true} variant="outline"> <ToggleGroup.Root type="single" value={studentValue} disabled={true} variant="outline">
<ToggleGroup.Item <ToggleGroup.Item
value="true" value="true"
class={correctIsTrue ? 'ui:border-emerald-500 ui:bg-emerald-50/40 dark:ui:bg-emerald-950/30' : ''} class={correctIsTrue ? 'ui:border-emerald-500 ui:bg-emerald-50/40 ui:dark:bg-emerald-950/30' : ''}
> >
{label('true_false.true_label')} {label('true_false.true_label')}
<OptionCorrectnessBadges <OptionCorrectnessBadges
@@ -31,7 +31,7 @@
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
value="false" value="false"
class={!correctIsTrue ? 'ui:border-emerald-500 ui:bg-emerald-50/40 dark:ui:bg-emerald-950/30' : ''} class={!correctIsTrue ? 'ui:border-emerald-500 ui:bg-emerald-50/40 ui:dark:bg-emerald-950/30' : ''}
> >
{label('true_false.false_label')} {label('true_false.false_label')}
<OptionCorrectnessBadges <OptionCorrectnessBadges
@@ -93,7 +93,7 @@
<p class="ui:text-muted-foreground ui:text-xs">{label('word_bank.edit.correct_answers_heading')}</p> <p class="ui:text-muted-foreground ui:text-xs">{label('word_bank.edit.correct_answers_heading')}</p>
<div class="ui:flex ui:flex-col ui:gap-2"> <div class="ui:flex ui:flex-col ui:gap-2">
{#each Array.from({ length: blankCount }, (_, i) => i) as blankIndex (blankIndex)} {#each Array.from({ length: blankCount }, (_, i) => i) as blankIndex (blankIndex)}
<div class="ui:flex ui:flex-col ui:gap-1 sm:ui:flex-row sm:ui:items-center sm:ui:gap-3"> <div class="ui:flex ui:flex-col ui:gap-1 ui:sm:flex-row ui:sm:items-center ui:sm:gap-3">
<span class="ui:text-muted-foreground ui:min-w-18 ui:text-sm" <span class="ui:text-muted-foreground ui:min-w-18 ui:text-sm"
>{label('word_bank.edit.blank_label')} {blankIndex + 1}</span >{label('word_bank.edit.blank_label')} {blankIndex + 1}</span
> >
@@ -166,7 +166,7 @@
{@const text = chipId ? (chipById.get(chipId)?.label ?? '') : ''} {@const text = chipId ? (chipById.get(chipId)?.label ?? '') : ''}
<button <button
type="button" type="button"
class="ui:inline-flex ui:align-middle ui:mx-0.5 ui:min-h-8 ui:min-w-[5rem] ui:max-w-[16rem] ui:items-center ui:justify-center ui:rounded ui:border-2 ui:px-2 ui:py-0.5 ui:text-left ui:text-sm ui:transition-all ui:duration-200 ui:border-primary disabled:ui:cursor-not-allowed" class="ui:inline-flex ui:align-middle ui:mx-0.5 ui:min-h-8 ui:min-w-[5rem] ui:max-w-[16rem] ui:items-center ui:justify-center ui:rounded ui:border-2 ui:px-2 ui:py-0.5 ui:text-left ui:text-sm ui:transition-all ui:duration-200 ui:border-primary ui:disabled:cursor-not-allowed"
class:ui:border-dashed={!text} class:ui:border-dashed={!text}
class:ui:bg-background={!text} class:ui:bg-background={!text}
class:ui:border-solid={!!text} class:ui:border-solid={!!text}
@@ -195,7 +195,7 @@
<button <button
type="button" type="button"
draggable={!disabled && !placed} draggable={!disabled && !placed}
class="ui:rounded ui:border-2 ui:border-primary ui:bg-primary ui:px-3 ui:py-1.5 ui:text-sm ui:text-primary-foreground ui:transition-opacity disabled:ui:cursor-not-allowed" class="ui:rounded ui:border-2 ui:border-primary ui:bg-primary ui:px-3 ui:py-1.5 ui:text-sm ui:text-primary-foreground ui:transition-opacity ui:disabled:cursor-not-allowed"
class:ui:opacity-40={placed} class:ui:opacity-40={placed}
class:ui:pointer-events-none={placed} class:ui:pointer-events-none={placed}
{disabled} {disabled}
@@ -30,13 +30,7 @@
<div class="ui:flex ui:flex-wrap ui:gap-1.5"> <div class="ui:flex ui:flex-wrap ui:gap-1.5">
{#each section.questions as question (question.key)} {#each section.questions as question (question.key)}
<span <span
class={`ui:inline-flex ui:h-7 ui:min-w-7 ui:items-center ui:justify-center ui:rounded ui:border ui:px-1.5 ui:text-xs ui:font-medium ${ class={`ui:inline-flex ui:h-7 ui:min-w-7 ui:items-center ui:justify-center ui:rounded ui:border ui:px-1.5 ui:text-xs ui:font-medium ${question.isCurrent ? 'ui:border-primary ui:bg-primary ui:text-primary-foreground' : question.isAnswered ? 'ui:border-primary/40 ui:bg-primary/10 ui:text-primary' : 'ui:border-border ui:text-muted-foreground'}`}
question.isCurrent
? 'ui:border-primary ui:bg-primary ui:text-primary-foreground'
: question.isAnswered
? 'ui:border-primary/40 ui:bg-primary/10 ui:text-primary'
: 'ui:border-border ui:text-muted-foreground'
}`}
> >
{question.label} {question.label}
</span> </span>
@@ -70,7 +70,7 @@
</p> </p>
<span class={`ui:mt-8 ui:h-1 ui:w-14 ui:rounded-full ${ACCENT_CLASSES[colorTheme]}`}></span> <span class={`ui:mt-8 ui:h-1 ui:w-14 ui:rounded-full ${ACCENT_CLASSES[colorTheme]}`}></span>
<h2 class="ui:mt-10 ui:font-serif ui:text-5xl ui:font-bold ui:tracking-tight ui:text-foreground md:ui:text-6xl"> <h2 class="ui:mt-10 ui:font-serif ui:text-5xl ui:font-bold ui:tracking-tight ui:text-foreground ui:md:text-6xl">
{sectionTitle} {sectionTitle}
</h2> </h2>
@@ -24,7 +24,7 @@
{@render children()} {@render children()}
{:else} {:else}
<HoverableItem <HoverableItem
class="ui:hover:bg-accent/25 ui:flex ui:h-48 ui:flex-col ui:place-items-center ui:justify-center ui:gap-2 ui:rounded-lg ui:border ui:border-dashed ui:p-6 ui:transition-all ui:group-aria-disabled/file-drop-zone-trigger:ui:opacity-50 ui:hover:cursor-pointer ui:group-aria-disabled/file-drop-zone-trigger:ui:hover:cursor-not-allowed" class="ui:hover:bg-accent/25 ui:flex ui:h-48 ui:flex-col ui:place-items-center ui:justify-center ui:gap-2 ui:rounded-lg ui:border ui:border-dashed ui:p-6 ui:transition-all ui:group-aria-disabled/file-drop-zone-trigger:opacity-50 ui:hover:cursor-pointer ui:group-aria-disabled/file-drop-zone-trigger:hover:cursor-not-allowed"
> >
{#snippet children(isHovered)} {#snippet children(isHovered)}
<div class="ui:flex ui:flex-col ui:place-items-center ui:justify-center ui:gap-2"> <div class="ui:flex ui:flex-col ui:place-items-center ui:justify-center ui:gap-2">
@@ -16,7 +16,7 @@
{...rest} {...rest}
showCloseButton={false} showCloseButton={false}
class={cn( class={cn(
'ui:min-h-96 ui:max-w-[60%] ui:rounded-none ui:border-x-0 ui:sm:ui:max-w-lg ui:sm:ui:rounded-lg ui:sm:ui:border-x', 'ui:min-h-96 ui:max-w-[60%] ui:rounded-none ui:border-x-0 ui:sm:max-w-lg ui:sm:rounded-lg ui:sm:border-x',
className className
)} )}
> >
@@ -76,7 +76,7 @@
<Field.Field class={className}> <Field.Field class={className}>
{#if label} {#if label}
<div class="flex items-center justify-between"> <div class="ui:flex ui:items-center ui:justify-between">
<Field.Label for={name || 'input-field'} class={labelClassName}> <Field.Label for={name || 'input-field'} class={labelClassName}>
{label} {label}
{#if isRequired} {#if isRequired}
@@ -75,7 +75,7 @@
aria-selected={index === selectedIndex} aria-selected={index === selectedIndex}
class={cn( class={cn(
'ui:flex ui:w-full ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-left ui:text-sm ui:transition-colors', 'ui:flex ui:w-full ui:items-center ui:gap-2 ui:rounded-sm ui:px-2 ui:py-1.5 ui:text-left ui:text-sm ui:transition-colors',
index === selectedIndex ? 'ui:bg-accent ui:text-accent-foreground' : 'hover:ui:bg-accent/50' index === selectedIndex ? 'ui:bg-accent ui:text-accent-foreground' : 'ui:hover:bg-accent/50'
)} )}
onmousedown={(e) => { onmousedown={(e) => {
e.preventDefault(); e.preventDefault();
@@ -73,37 +73,37 @@
<Popover.Root bind:open> <Popover.Root bind:open>
<Popover.Trigger <Popover.Trigger
{disabled} {disabled}
class="ui:text-muted-foreground hover:ui:bg-muted ui:hover:text-foreground flex shrink-0 items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors disabled:pointer-events-none disabled:opacity-40" class="ui:text-muted-foreground ui:hover:bg-muted ui:hover:text-foreground ui:flex ui:shrink-0 ui:items-center ui:gap-1 ui:rounded-md ui:px-2 ui:py-1 ui:text-xs ui:transition-colors ui:disabled:pointer-events-none ui:disabled:opacity-40"
> >
<span class="truncate">{AGENT_MODELS[value].label}</span> <span class="truncate">{AGENT_MODELS[value].label}</span>
{#if AGENT_MODELS[value].costTier === 'high'} {#if AGENT_MODELS[value].costTier === 'high'}
<span class="ui:text-red-500 font-medium">$$$</span> <span class="ui:text-red-500 ui:font-medium">$$$</span>
{:else} {:else}
<span class="ui:text-green-500 font-medium">$</span> <span class="ui:text-green-500 ui:font-medium">$</span>
{/if} {/if}
<ChevronDownIcon size={12} /> <ChevronDownIcon size={12} />
</Popover.Trigger> </Popover.Trigger>
<Popover.Content class="ui:p-0! ui:z-[110] w-48" align="end" side="top"> <Popover.Content class="ui:p-0! ui:z-[110] ui:w-48" align="end" side="top">
<div class="py-1"> <div class="ui:py-1">
{#each UI_PICKER_MODEL_IDS as id (id)} {#each UI_PICKER_MODEL_IDS as id (id)}
{@const isSelected = id === value} {@const isSelected = id === value}
<button <button
type="button" type="button"
onclick={() => handleSelect(id)} onclick={() => handleSelect(id)}
class="hover:ui:bg-muted flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-xs transition-colors" class="ui:hover:bg-muted ui:flex ui:w-full ui:items-center ui:justify-between ui:gap-2 ui:px-3 ui:py-2 ui:text-left ui:text-xs ui:transition-colors"
> >
<span class="flex min-w-0 items-center gap-2"> <span class="ui:flex ui:min-w-0 ui:items-center ui:gap-2">
<span class="min-w-0 truncate">{AGENT_MODELS[id].label}</span> <span class="ui:min-w-0 truncate">{AGENT_MODELS[id].label}</span>
{#if isLocked(id)} {#if isLocked(id)}
<RocketIcon size={16} class="ui:text-primary! ui:shrink-0" /> <RocketIcon size={16} class="ui:text-primary! ui:shrink-0" />
{/if} {/if}
</span> </span>
<span class="flex shrink-0 items-center gap-1"> <span class="ui:flex ui:shrink-0 ui:items-center ui:gap-1">
{#if AGENT_MODELS[id].costTier === 'high'} {#if AGENT_MODELS[id].costTier === 'high'}
<span class="ui:text-red-500 font-medium">$$$</span> <span class="ui:text-red-500 ui:font-medium">$$$</span>
{:else} {:else}
<span class="ui:text-green-500 font-medium">$</span> <span class="ui:text-green-500 ui:font-medium">$</span>
{/if} {/if}
{#if isSelected} {#if isSelected}
<CheckIcon size={14} class="ui:text-primary" /> <CheckIcon size={14} class="ui:text-primary" />
@@ -34,8 +34,8 @@
class:animate={isHovered} class:animate={isHovered}
> >
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" /> <path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" />
<path d="M8 11h8" class="ui:text-line ui:text-line-1" /> <path d="M8 11h8" class="text-line text-line-1" />
<path d="M8 7h6" class="ui:text-line ui:text-line-2" /> <path d="M8 7h6" class="text-line text-line-2" />
</svg> </svg>
</div> </div>
@@ -102,14 +102,10 @@
<EditableLandingSection sectionKey={block.id}> <EditableLandingSection sectionKey={block.id}>
<article <article
id={block.id} id={block.id}
class={`ui:border ui:border-[var(--landing-border)] ${ class={`ui:border ui:border-[var(--landing-border)] ${variant === 'terminal' ? 'ui:bg-[var(--landing-card)]' : ''}`}
variant === 'terminal' ? 'ui:bg-[var(--landing-card)]' : ''
}`}
> >
<div <div
class={`ui:flex ui:items-center ui:justify-between ui:px-4 ui:py-2 ui:border-b ui:border-[var(--landing-border)] ${ class={`ui:flex ui:items-center ui:justify-between ui:px-4 ui:py-2 ui:border-b ui:border-[var(--landing-border)] ${variant === 'terminal' ? 'ui:bg-[var(--landing-bg-section)]' : ''}`}
variant === 'terminal' ? 'ui:bg-[var(--landing-bg-section)]' : ''
}`}
> >
<span class="ui:text-xs ui:font-mono ui:text-[var(--landing-fg-muted)]"> <span class="ui:text-xs ui:font-mono ui:text-[var(--landing-fg-muted)]">
// {block.monoLabel} // {block.monoLabel}
@@ -36,7 +36,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-sm ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0', 'ui:block ui:text-sm ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0',
columnCta: columnCta:
'ui:text-sm ui:font-semibold ui:text-[var(--landing-fg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-4 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-semibold ui:text-[var(--landing-fg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-4 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:p-1.5 ui:rounded-md ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:p-1.5 ui:rounded-md ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:transition-colors ui:no-underline',
@@ -64,7 +64,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-sm ui:text-[var(--landing-bg)]/75 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0', 'ui:block ui:text-sm ui:text-[var(--landing-bg)]/75 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0',
columnCta: columnCta:
'ui:text-sm ui:font-semibold ui:text-[var(--landing-bg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-semibold ui:text-[var(--landing-bg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-4 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-4 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:size-8 ui:border ui:border-[var(--landing-bg)]/15 ui:bg-[var(--landing-bg)]/5 ui:text-[var(--landing-bg)]/75 ui:hover:text-[var(--landing-bg)] ui:hover:bg-[var(--landing-bg)]/10 ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:size-8 ui:border ui:border-[var(--landing-bg)]/15 ui:bg-[var(--landing-bg)]/5 ui:text-[var(--landing-bg)]/75 ui:hover:text-[var(--landing-bg)] ui:hover:bg-[var(--landing-bg)]/10 ui:transition-colors ui:no-underline',
@@ -92,7 +92,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-[13px] ui:text-[var(--landing-fg)] ui:hover:text-[var(--landing-fg-muted)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0', 'ui:block ui:text-[13px] ui:text-[var(--landing-fg)] ui:hover:text-[var(--landing-fg-muted)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0',
columnCta: columnCta:
'ui:text-sm ui:font-medium ui:text-[var(--landing-accent)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-medium ui:text-[var(--landing-accent)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:size-7 ui:rounded-md ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-card-soft)] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:size-7 ui:rounded-md ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-card-soft)] ui:transition-colors ui:no-underline',
@@ -119,7 +119,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-[13.5px] ui:text-[#9da1ab] ui:hover:text-[#e9eaed] ui:transition-colors ui:break-words ui:py-1 ui:md:py-0 ui:no-underline', 'ui:block ui:text-[13.5px] ui:text-[#9da1ab] ui:hover:text-[#e9eaed] ui:transition-colors ui:break-words ui:py-1 ui:md:py-0 ui:no-underline',
columnCta: columnCta:
'ui:text-sm ui:font-medium ui:text-[#e9eaed] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-medium ui:text-[#e9eaed] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-3 ui:mt-4 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-3 ui:mt-4 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:text-[#9da1ab] ui:hover:text-[#e9eaed] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:text-[#9da1ab] ui:hover:text-[#e9eaed] ui:transition-colors ui:no-underline',
@@ -174,7 +174,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-sm ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0', 'ui:block ui:text-sm ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0',
columnCta: columnCta:
'ui:text-sm ui:font-semibold ui:text-[var(--landing-accent)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-semibold ui:text-[var(--landing-accent)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-4 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-4 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:size-8 ui:rounded-full ui:border ui:border-[var(--landing-border)] ui:bg-[var(--landing-card-soft)]/40 ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-card-soft)] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:size-8 ui:rounded-full ui:border ui:border-[var(--landing-border)] ui:bg-[var(--landing-card-soft)]/40 ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-card-soft)] ui:transition-colors ui:no-underline',
@@ -203,7 +203,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-lg ui:font-medium ui:text-[var(--landing-fg)] ui:hover:text-[var(--landing-accent)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-1 ui:no-underline', 'ui:block ui:text-lg ui:font-medium ui:text-[var(--landing-fg)] ui:hover:text-[var(--landing-accent)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-1 ui:no-underline',
columnCta: columnCta:
'ui:text-sm ui:font-medium ui:text-[var(--landing-accent)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-medium ui:text-[var(--landing-accent)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-3 ui:mt-4 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-3 ui:mt-4 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:size-9 ui:rounded-md ui:border ui:border-[var(--landing-border)] ui:bg-[var(--landing-bg)] ui:text-[var(--landing-fg)] ui:hover:text-[var(--landing-accent)] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:size-9 ui:rounded-md ui:border ui:border-[var(--landing-border)] ui:bg-[var(--landing-bg)] ui:text-[var(--landing-fg)] ui:hover:text-[var(--landing-accent)] ui:transition-colors ui:no-underline',
@@ -230,7 +230,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-sm ui:text-[#2c2b29] ui:hover:text-[#1a1a1a] ui:transition-colors ui:break-words ui:py-1 ui:md:py-0 ui:no-underline', 'ui:block ui:text-sm ui:text-[#2c2b29] ui:hover:text-[#1a1a1a] ui:transition-colors ui:break-words ui:py-1 ui:md:py-0 ui:no-underline',
columnCta: columnCta:
'ui:text-sm ui:font-medium ui:text-[#c25237] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-medium ui:text-[#c25237] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-3 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-3 ui:mt-4 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-3 ui:mt-4 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:text-[#76746c] ui:hover:text-[#1a1a1a] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:text-[#76746c] ui:hover:text-[#1a1a1a] ui:transition-colors ui:no-underline',
@@ -257,7 +257,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-sm ui:font-medium ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0', 'ui:block ui:text-sm ui:font-medium ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0',
columnCta: columnCta:
'ui:text-sm ui:font-bold ui:text-[var(--landing-bg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-4 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-bold ui:text-[var(--landing-bg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-4 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:p-1.5 ui:rounded-md ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:p-1.5 ui:rounded-md ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:no-underline',
@@ -284,7 +284,7 @@ export function getFooterTokens(variant: OrgLandingPageTheme): FooterRegionToken
columnLink: columnLink:
'ui:block ui:text-sm ui:font-medium ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0', 'ui:block ui:text-sm ui:font-medium ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:break-words ui:py-1.5 ui:md:py-0',
columnCta: columnCta:
'ui:text-sm ui:font-semibold ui:text-[var(--landing-bg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-4 ui:no-underline hover:ui:underline', 'ui:text-sm ui:font-semibold ui:text-[var(--landing-bg)] ui:inline-flex ui:items-center ui:gap-1.5 ui:mt-4 ui:no-underline ui:hover:underline',
socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0', socialWrap: 'ui:flex ui:flex-wrap ui:gap-2 ui:mt-3 ui:list-none ui:m-0 ui:p-0',
socialIcon: socialIcon:
'ui:inline-flex ui:items-center ui:justify-center ui:p-1.5 ui:rounded-md ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:no-underline', 'ui:inline-flex ui:items-center ui:justify-center ui:p-1.5 ui:rounded-md ui:text-[var(--landing-bg)]/70 ui:hover:text-[var(--landing-bg)] ui:transition-colors ui:no-underline',
@@ -13,13 +13,13 @@
const clampedValue = $derived(Math.max(0, Math.min(100, value))); const clampedValue = $derived(Math.max(0, Math.min(100, value)));
const labelPercentage = $derived(Math.round(clampedValue)); const labelPercentage = $derived(Math.round(clampedValue));
const strokeDashoffset = $derived(RING_CIRCUMFERENCE * (1 - clampedValue / 100)); const strokeDashoffset = $derived(RING_CIRCUMFERENCE * (1 - clampedValue / 100));
const sizeClass = $derived(size === 'small' ? 'size-10' : 'size-14'); const sizeClass = $derived(size === 'small' ? 'ui:size-10' : 'ui:size-14');
const labelClass = $derived(size === 'small' ? 'text-[10px]' : 'text-sm'); const labelClass = $derived(size === 'small' ? 'ui:text-[10px]' : 'ui:text-sm');
</script> </script>
<div class="relative shrink-0"> <div class="ui:relative ui:shrink-0">
<svg <svg
class="{sizeClass} -rotate-90" class="{sizeClass} ui:-rotate-90"
style="transform-box: fill-box; transform-origin: center;" style="transform-box: fill-box; transform-origin: center;"
viewBox="0 0 100 100" viewBox="0 0 100 100"
aria-hidden="true" aria-hidden="true"
@@ -31,7 +31,7 @@
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="8" stroke-width="8"
class="text-gray-200 dark:text-gray-700" class="ui:text-gray-200 ui:dark:text-gray-700"
/> />
<circle <circle
cx="50" cx="50"
@@ -41,12 +41,12 @@
stroke="currentColor" stroke="currentColor"
stroke-width="8" stroke-width="8"
stroke-linecap="round" stroke-linecap="round"
class="text-green-600 transition-[stroke-dashoffset] duration-700 ease-out" class="ui:text-green-600 ui:transition-[stroke-dashoffset] ui:duration-700 ui:ease-out"
stroke-dasharray={RING_CIRCUMFERENCE} stroke-dasharray={RING_CIRCUMFERENCE}
stroke-dashoffset={strokeDashoffset} stroke-dashoffset={strokeDashoffset}
/> />
</svg> </svg>
<div class="absolute inset-0 flex flex-col items-center justify-center"> <div class="ui:absolute ui:inset-0 ui:flex ui:flex-col ui:items-center ui:justify-center">
<p class={labelClass}>{labelPercentage}%</p> <p class={labelClass}>{labelPercentage}%</p>
</div> </div>
</div> </div>
@@ -126,7 +126,7 @@
{lesson.title} {lesson.title}
</h1> </h1>
<div class="prose sm:prose-sm ui:mt-8 ui:max-w-none ui:dark:text-white"> <div class="prose ui:sm:prose-sm ui:mt-8 ui:max-w-none ui:dark:text-white">
{#if lesson.body} {#if lesson.body}
<SafeHtmlContent content={lesson.body} /> <SafeHtmlContent content={lesson.body} />
{:else} {:else}
@@ -121,7 +121,7 @@
isActive isActive
? 'ui:border-l-foreground ui:bg-secondary ui:text-secondary-foreground' ? 'ui:border-l-foreground ui:bg-secondary ui:text-secondary-foreground'
: 'ui:border-l-transparent ui:bg-transparent ui:text-foreground', : 'ui:border-l-transparent ui:bg-transparent ui:text-foreground',
!isActive && 'hover:ui:bg-muted/40' !isActive && 'ui:hover:bg-muted/40'
)} )}
aria-current={isActive ? 'true' : undefined} aria-current={isActive ? 'true' : undefined}
onclick={() => selectItem(item.key)} onclick={() => selectItem(item.key)}
@@ -31,7 +31,7 @@
<div <div
class={cn( class={cn(
'group ui:inline-flex ui:w-full ui:items-center', 'group ui:inline-flex ui:w-full ui:items-center',
disabled ? 'cursor-not-allowed' : 'cursor-pointer', disabled ? 'ui:cursor-not-allowed' : 'ui:cursor-pointer',
className className
)} )}
> >
@@ -2,7 +2,7 @@
import { tv, type VariantProps } from 'tailwind-variants'; import { tv, type VariantProps } from 'tailwind-variants';
export const resourceListRowVariants = tv({ export const resourceListRowVariants = tv({
base: 'ui:flex-nowrap! ui:rounded-none ui:border-0 ui:border-b ui:border-border last:ui:border-b-0 ui:hover:bg-muted/50', base: 'ui:flex-nowrap! ui:rounded-none ui:border-0 ui:border-b ui:border-border ui:last:border-b-0 ui:hover:bg-muted/50',
variants: { variants: {
align: { align: {
center: 'ui:items-center', center: 'ui:items-center',
@@ -33,7 +33,7 @@
<Field.Field class={className}> <Field.Field class={className}>
{#if label} {#if label}
<div class="flex items-center justify-between"> <div class="ui:flex ui:items-center ui:justify-between">
<Field.Label class={labelClassName}> <Field.Label class={labelClassName}>
{label} {label}
{#if isRequired} {#if isRequired}
@@ -30,10 +30,10 @@
bind:ref bind:ref
data-slot="underline-tabs-trigger" data-slot="underline-tabs-trigger"
class={cn( class={cn(
"dark:data-[state=active]:text-foreground data-[state=active]:text-foreground ui:text-foreground ui:z-10 ui:relative ui:inline-flex ui:h-[calc(100%-3px)] ui:flex-1 ui:items-center ui:justify-center ui:gap-1.5 ui:whitespace-nowrap ui:px-3 ui:py-1 ui:text-sm ui:transition-colors ui:focus:outline-none ui:disabled:pointer-events-none ui:disabled:opacity-50 ui:cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "ui:dark:data-[state=active]:text-foreground ui:data-[state=active]:text-foreground ui:text-foreground ui:z-10 ui:relative ui:inline-flex ui:h-[calc(100%-3px)] ui:flex-1 ui:items-center ui:justify-center ui:gap-1.5 ui:whitespace-nowrap ui:px-3 ui:py-1 ui:text-sm ui:transition-colors ui:focus:outline-none ui:disabled:pointer-events-none ui:disabled:opacity-50 ui:cursor-pointer ui:[&_svg]:pointer-events-none ui:[&_svg]:shrink-0 ui:[&_svg:not([class*='size-'])]:size-4",
state.rootState.isHovered && state.rootState.isHovered &&
state.rootState.hoveredTab === value && state.rootState.hoveredTab === value &&
'data-[state=inactive]:text-primary-foreground ui:text-primary', 'ui:data-[state=inactive]:text-primary-foreground ui:text-primary',
className className
)} )}
{...state.props} {...state.props}
@@ -44,7 +44,7 @@
{#if state.rootState.hoveredTab === value} {#if state.rootState.hoveredTab === value}
<div <div
class={cn( class={cn(
'ui:bg-primary-foreground ui:z-1 ui:absolute ui:top-0 ui:h-[calc(100%-3px)] ui:w-full ui:rounded-md ui:opacity-0 ui:transition-opacity ui:duration-300 ui:pointer-events-none ui:peer-focus-visible:ui:opacity-100', 'ui:bg-primary-foreground ui:z-1 ui:absolute ui:top-0 ui:h-[calc(100%-3px)] ui:w-full ui:rounded-md ui:opacity-0 ui:transition-opacity ui:duration-300 ui:pointer-events-none ui:peer-focus-visible:opacity-100',
state.rootState.isHovered && 'ui:opacity-100' state.rootState.isHovered && 'ui:opacity-100'
)} )}
in:receive={{ key: `${state.rootState.opts.id.current}-tab-hover`, duration: 300 }} in:receive={{ key: `${state.rootState.opts.id.current}-tab-hover`, duration: 300 }}
@@ -8,7 +8,7 @@
iconClass?: string; iconClass?: string;
} }
let { src, alt = 'User', class: className = 'size-6', iconClass = '' }: Props = $props(); let { src, alt = 'User', class: className = 'ui:size-6', iconClass = '' }: Props = $props();
</script> </script>
<Avatar.Root class={className}> <Avatar.Root class={className}>
@@ -16,6 +16,6 @@
<Avatar.Image {src} {alt} /> <Avatar.Image {src} {alt} />
{/if} {/if}
<Avatar.Fallback> <Avatar.Fallback>
<img src="/images/avatar.svg" alt="User avatar" class="size-10 rounded-md {iconClass}" /> <img src="/images/avatar.svg" alt="User avatar" class="ui:size-10 ui:rounded-md {iconClass}" />
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
File diff suppressed because one or more lines are too long