Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .storybook/polish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { readdir } from 'node:fs/promises'
import { join } from 'node:path'

const defaultVariants: Record<string, Record<string, string>> = {
avatar: {
size: 'md',
shape: 'full',
variant: 'subtle',
},
// Add more components here as needed
}

const recipesDir = join(import.meta.dir, 'styles-v2/recipes')

const files = await readdir(recipesDir)

// Convert camelCase to kebab-case
function toKebabCase(str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}

// Check if a selector is a base selector (no variant, like .avatar__root)
function isBaseSelector(selector: string): boolean {
// Matches .component__part but NOT .component__part--variant_value
return /\.[a-zA-Z][a-zA-Z0-9-]*__[a-zA-Z][a-zA-Z0-9-]*/.test(selector) && !selector.includes('--')
}

// Transform base selector to data-scope/data-part format
function transformBaseSelector(selector: string): string {
return selector.replace(/\.([a-zA-Z][a-zA-Z0-9-]*)__([a-zA-Z][a-zA-Z0-9-]*)/g, (_match, component, part) => {
const kebabComponent = toKebabCase(component)
const kebabPart = toKebabCase(part)
return `[data-scope="${kebabComponent}"][data-part="${kebabPart}"]`
})
}

// Check if a selector matches a default variant
function isDefaultVariantSelector(
selector: string,
componentName: string,
defaults: Record<string, string>,
): { isDefault: boolean; part: string } | null {
// Match pattern like .avatar__root--variant_subtle or .avatar__fallback--size_md
const variantMatch = selector.match(/\.([a-zA-Z][a-zA-Z0-9-]*)__([a-zA-Z][a-zA-Z0-9-]*)--([a-zA-Z]+)_([a-zA-Z0-9]+)/)

if (!variantMatch) return null

const [, component, part, variantType, variantValue] = variantMatch
const kebabComponent = toKebabCase(component)

if (kebabComponent !== componentName) return null

// Check if this matches a default variant
const defaultValue = defaults[variantType]
if (defaultValue && defaultValue === variantValue) {
return { isDefault: true, part: toKebabCase(part) }
}

return { isDefault: false, part: toKebabCase(part) }
}

// Check if selector is a variant selector (has --)
function isVariantSelector(selector: string): boolean {
return /\.[a-zA-Z][a-zA-Z0-9-]*__[a-zA-Z][a-zA-Z0-9-]*--/.test(selector)
}

// Transform default variant selector to data-scope/data-part format
function transformVariantToDataSelector(selector: string): string {
// Handle pseudo-elements and pseudo-classes in the selector
// e.g., .avatar__fallback--size_md :where(svg) -> [data-scope="avatar"][data-part="fallback"] :where(svg)
return selector.replace(
/\.([a-zA-Z][a-zA-Z0-9-]*)__([a-zA-Z][a-zA-Z0-9-]*)--[a-zA-Z]+_[a-zA-Z0-9]+/g,
(_match, component, part) => {
const kebabComponent = toKebabCase(component)
const kebabPart = toKebabCase(part)
return `[data-scope="${kebabComponent}"][data-part="${kebabPart}"]`
},
)
}

// Process a single CSS file
async function processFile(filePath: string, componentName: string) {
const content = await Bun.file(filePath).text()
const defaults = defaultVariants[componentName]

if (!defaults) {
console.log(`Skipping ${componentName}: no default variants defined`)
return
}

const lines = content.split('\n')
const result: string[] = []

let braceDepth = 0
let skipUntilBraceDepth = -1 // -1 means not skipping

for (let i = 0; i < lines.length; i++) {
const line = lines[i]

// Count braces to track nesting depth
const openBraces = (line.match(/\{/g) || []).length
const closeBraces = (line.match(/\}/g) || []).length

// Check if we're currently skipping a block
if (skipUntilBraceDepth >= 0) {
braceDepth += openBraces - closeBraces
if (braceDepth <= skipUntilBraceDepth) {
skipUntilBraceDepth = -1 // Stop skipping
}
continue // Skip this line
}

// Check if this line starts a new rule block (has a selector followed by {)
const selectorMatch = line.match(/^\s*([^{}]+)\{/)

if (selectorMatch && openBraces > 0) {
const selector = selectorMatch[1].trim()

// Always keep @layer declarations
if (selector.startsWith('@layer')) {
result.push(line)
braceDepth += openBraces - closeBraces
continue
}

// Check what kind of selector this is
if (isVariantSelector(selector)) {
// It's a variant selector - check if it matches default
const check = isDefaultVariantSelector(selector, componentName, defaults)
if (check?.isDefault) {
// Transform and keep default variant selectors
const transformedSelector = transformVariantToDataSelector(selector)
result.push(line.replace(selector, transformedSelector))
braceDepth += openBraces - closeBraces
} else {
// Skip non-default variant selectors
skipUntilBraceDepth = braceDepth
braceDepth += openBraces - closeBraces
}
} else if (isBaseSelector(selector)) {
// Transform base selectors like .avatar__root to [data-scope][data-part]
const transformedSelector = transformBaseSelector(selector)
result.push(line.replace(selector, transformedSelector))
braceDepth += openBraces - closeBraces
} else {
// Keep other selectors as-is
result.push(line)
braceDepth += openBraces - closeBraces
}
} else {
// Not a selector line, just keep it
result.push(line)
braceDepth += openBraces - closeBraces
}
}

// Clean up multiple consecutive empty lines
const cleaned = result.join('\n').replace(/\n{3,}/g, '\n\n')

await Bun.write(filePath, cleaned)
console.log(`Processed: ${componentName}.css`)
}

// Main execution
for (const file of files) {
if (!file.endsWith('.css')) continue

const componentName = file.replace('.css', '')
const filePath = join(recipesDir, file)

await processFile(filePath, componentName)
}

console.log(`\nDone! Processed ${files.filter((f) => f.endsWith('.css')).length} files.`)
7 changes: 7 additions & 0 deletions .storybook/styles-v2.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@layer reset, base, tokens, recipes, utilities;

@import './styles-v2/reset.css';
@import './styles-v2/global.css';
@import './styles-v2/tokens.css';
@import './styles-v2/utilities.css';
@import './styles-v2/recipes.css';
100 changes: 100 additions & 0 deletions .storybook/styles-v2/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
@layer base {
:root {
--made-with-panda: '🐼';
}

*,::before,::after,::backdrop {
--blur: /*-*/ /*-*/;
--brightness: /*-*/ /*-*/;
--contrast: /*-*/ /*-*/;
--grayscale: /*-*/ /*-*/;
--hue-rotate: /*-*/ /*-*/;
--invert: /*-*/ /*-*/;
--saturate: /*-*/ /*-*/;
--sepia: /*-*/ /*-*/;
--drop-shadow: /*-*/ /*-*/;
--backdrop-blur: /*-*/ /*-*/;
--backdrop-brightness: /*-*/ /*-*/;
--backdrop-contrast: /*-*/ /*-*/;
--backdrop-grayscale: /*-*/ /*-*/;
--backdrop-hue-rotate: /*-*/ /*-*/;
--backdrop-invert: /*-*/ /*-*/;
--backdrop-opacity: /*-*/ /*-*/;
--backdrop-saturate: /*-*/ /*-*/;
--backdrop-sepia: /*-*/ /*-*/;
--gradient-from-position: /*-*/ /*-*/;
--gradient-to-position: /*-*/ /*-*/;
--gradient-via-position: /*-*/ /*-*/;
--scroll-snap-strictness: proximity;
--border-spacing-x: 0;
--border-spacing-y: 0;
--translate-x: 0;
--translate-y: 0;
--rotate: 0;
--rotate-x: 0;
--rotate-y: 0;
--skew-x: 0;
--skew-y: 0;
--scale-x: 1;
--scale-y: 1;
}

html {
--colors-color-palette-1: var(--colors-neutral-1);
--colors-color-palette-2: var(--colors-neutral-2);
--colors-color-palette-3: var(--colors-neutral-3);
--colors-color-palette-4: var(--colors-neutral-4);
--colors-color-palette-5: var(--colors-neutral-5);
--colors-color-palette-6: var(--colors-neutral-6);
--colors-color-palette-7: var(--colors-neutral-7);
--colors-color-palette-8: var(--colors-neutral-8);
--colors-color-palette-9: var(--colors-neutral-9);
--colors-color-palette-10: var(--colors-neutral-10);
--colors-color-palette-11: var(--colors-neutral-11);
--colors-color-palette-12: var(--colors-neutral-12);
--colors-color-palette-a1: var(--colors-neutral-a1);
--colors-color-palette-a2: var(--colors-neutral-a2);
--colors-color-palette-a3: var(--colors-neutral-a3);
--colors-color-palette-a4: var(--colors-neutral-a4);
--colors-color-palette-a5: var(--colors-neutral-a5);
--colors-color-palette-a6: var(--colors-neutral-a6);
--colors-color-palette-a7: var(--colors-neutral-a7);
--colors-color-palette-a8: var(--colors-neutral-a8);
--colors-color-palette-a9: var(--colors-neutral-a9);
--colors-color-palette-a10: var(--colors-neutral-a10);
--colors-color-palette-a11: var(--colors-neutral-a11);
--colors-color-palette-a12: var(--colors-neutral-a12);
--colors-color-palette-solid-bg: var(--colors-neutral-solid-bg);
--colors-color-palette-solid-bg-hover: var(--colors-neutral-solid-bg-hover);
--colors-color-palette-solid-fg: var(--colors-neutral-solid-fg);
--colors-color-palette-subtle-bg: var(--colors-neutral-subtle-bg);
--colors-color-palette-subtle-bg-hover: var(--colors-neutral-subtle-bg-hover);
--colors-color-palette-subtle-bg-active: var(--colors-neutral-subtle-bg-active);
--colors-color-palette-subtle-fg: var(--colors-neutral-subtle-fg);
--colors-color-palette-surface-bg: var(--colors-neutral-surface-bg);
--colors-color-palette-surface-bg-hover: var(--colors-neutral-surface-bg-hover);
--colors-color-palette-surface-bg-active: var(--colors-neutral-surface-bg-active);
--colors-color-palette-surface-border: var(--colors-neutral-surface-border);
--colors-color-palette-surface-border-hover: var(--colors-neutral-surface-border-hover);
--colors-color-palette-surface-fg: var(--colors-neutral-surface-fg);
--colors-color-palette-outline-bg-hover: var(--colors-neutral-outline-bg-hover);
--colors-color-palette-outline-bg-active: var(--colors-neutral-outline-bg-active);
--colors-color-palette-outline-border: var(--colors-neutral-outline-border);
--colors-color-palette-outline-fg: var(--colors-neutral-outline-fg);
--colors-color-palette-plain-bg-hover: var(--colors-neutral-plain-bg-hover);
--colors-color-palette-plain-bg-active: var(--colors-neutral-plain-bg-active);
--colors-color-palette-plain-fg: var(--colors-neutral-plain-fg);
}

* {
--global-color-border: var(--colors-border);
--global-color-placeholder: var(--colors-fg-subtle);
--global-color-selection: var(--colors-color-palette-subtle-bg);
--global-color-focus-ring: var(--colors-color-palette-solid-bg);
}

body {
background: var(--colors-canvas);
color: var(--colors-fg-default);
}
}
42 changes: 42 additions & 0 deletions .storybook/styles-v2/recipes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import './recipes/group.css';
@import './recipes/button.css';
@import './recipes/icon.css';
@import './recipes/spinner.css';
@import './recipes/absolute-center.css';
@import './recipes/skeleton.css';
@import './recipes/accordion.css';
@import './recipes/alert.css';
@import './recipes/avatar.css';
@import './recipes/badge.css';
@import './recipes/breadcrumb.css';
@import './recipes/menu.css';
@import './recipes/card.css';
@import './recipes/input.css';
@import './recipes/carousel.css';
@import './recipes/checkbox.css';
@import './recipes/link.css';
@import './recipes/input-group.css';
@import './recipes/code.css';
@import './recipes/combobox.css';
@import './recipes/dialog.css';
@import './recipes/drawer.css';
@import './recipes/editable.css';
@import './recipes/file-upload.css';
@import './recipes/input-addon.css';
@import './recipes/kbd.css';
@import './recipes/number-input.css';
@import './recipes/pin-input.css';
@import './recipes/textarea.css';
@import './recipes/progress.css';
@import './recipes/radio-card-group.css';
@import './recipes/radio-group.css';
@import './recipes/rating-group.css';
@import './recipes/scroll-area.css';
@import './recipes/segment-group.css';
@import './recipes/select.css';
@import './recipes/slider.css';
@import './recipes/switch-recipe.css';
@import './recipes/table.css';
@import './recipes/tabs.css';
@import './recipes/tags-input.css';
@import './recipes/toggle-group.css';
34 changes: 34 additions & 0 deletions .storybook/styles-v2/recipes/absolute-center.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@layer recipes {
@layer _base {
.absolute-center {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
}

.absolute-center--axis_both {
inset-inline-start: 50%;
translate: -50% -50%;
top: 50%;
}

:where([dir=rtl], :dir(rtl)) .absolute-center--axis_both {
translate: 50% -50%;
}

.absolute-center--axis_horizontal {
inset-inline-start: 50%;
translate: -50%;
}

:where([dir=rtl], :dir(rtl)) .absolute-center--axis_horizontal {
translate: 50%;
}

.absolute-center--axis_vertical {
translate: 0 -50%;
top: 50%;
}
}
Loading