Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/side-self-contained-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sipe-team/side": patch
---

Build a self-contained dist by bundling the workspace components and compiling their vanilla-extract styles, so the umbrella package can be consumed by non-vanilla-extract bundlers. A precompiled `dist/index.css` is emitted alongside the JS bundle.
5 changes: 5 additions & 0 deletions .changeset/tokens-drop-theme-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sipe-team/tokens": patch
---

Drop the `@layer theme` wrapper from the generated theme CSS so the token variables resolve as plain `:root` declarations. The cascade layer was unused by any consumer, and the wrapper prevented non-vanilla-extract bundlers from preserving the variables. The variable contract is unchanged; theme switching via `[data-theme]` and runtime `assignInlineVars` are unaffected.
1 change: 1 addition & 0 deletions packages/side/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@sipe-team/flex": "workspace:*"
},
"devDependencies": {
"@vanilla-extract/esbuild-plugin": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/side/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { vanillaExtractPlugin } from '@vanilla-extract/esbuild-plugin';

import { defineConfig } from 'tsup';

export default defineConfig({
entry: ['src/index.ts'],
clean: true,
dts: true,
format: ['esm', 'cjs'],
noExternal: [/@sipe-team\//],
esbuildPlugins: [vanillaExtractPlugin()],
});
4 changes: 1 addition & 3 deletions packages/tokens/src/theme/contract.css.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { createGlobalThemeContract, globalLayer } from '@vanilla-extract/css';

export const themeLayer = globalLayer('theme');
import { createGlobalThemeContract } from '@vanilla-extract/css';

export const vars = createGlobalThemeContract(
{
Expand Down
3 changes: 1 addition & 2 deletions packages/tokens/src/theme/themes.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { radius } from '../effects/radius';
import { shadows } from '../effects/shadows';
import { spacing } from '../layout/spacing';
import { fontSize, fontWeight, lineHeight } from '../typography/fonts';
import { themeLayer, vars } from './contract.css';
import { vars } from './contract.css';

const baseTheme = {
'@layer': themeLayer,
spacing: {
component: {
xs: `${spacing[1]}px`,
Expand Down
483 changes: 369 additions & 114 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions www/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto-generated props metadata (build-time extraction output)
src/.generated/
25 changes: 25 additions & 0 deletions www/docs/components/button.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Button
---

# Button

## Setup

```sh
npm install @sipe-team/button
```

## Usage

```tsx
import { Button } from '@sipe-team/button';
```

```tsx
<Button variant="filled" size="lg">Click me</Button>
```

## Playground

<Playground component="button" />
32 changes: 31 additions & 1 deletion www/docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import type * as Preset from '@docusaurus/preset-classic';
import type { Config } from '@docusaurus/types';
import tailwindcssPostcss from '@tailwindcss/postcss';
import { themes as prismThemes } from 'prism-react-renderer';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sideDist = path.resolve(__dirname, '../packages/side/dist/index.js');
const sideCss = path.resolve(__dirname, '../packages/side/dist/index.css');

export default {
title: 'Side',
tagline: 'Sipe Design System',
Expand All @@ -26,12 +34,34 @@ export default {
},
blog: false,
theme: {
customCss: './src/custom.css',
customCss: ['./src/custom.css', sideCss],
},
} satisfies Preset.Options,
],
],

plugins: [
() => ({
name: 'playground-workspace-dist-aliases',
configureWebpack() {
return {
resolve: {
alias: {
'@sipe-team/side': sideDist,
},
},
};
},
}),
() => ({
name: 'playground-tailwind',
configurePostCss(postcssOptions) {
postcssOptions.plugins.push(tailwindcssPostcss());
return postcssOptions;
},
}),
],

themeConfig: {
image: 'img/docusaurus-social-card.jpg',
navbar: {
Expand Down
12 changes: 10 additions & 2 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,36 @@
"scripts": {
"dev": "docusaurus start",
"build": "docusaurus build",
"prebuild": "pnpm --filter @sipe-team/side build && pnpm run extract:props",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"clean": "rm -rf node_modules build .docusaurus"
"clean": "rm -rf node_modules build .docusaurus",
"extract:props": "node scripts/extract-props.mjs --component button"
},
"dependencies": {
"@docusaurus/core": "3.6.3",
"@docusaurus/preset-classic": "3.6.3",
"@mdx-js/react": "^3.0.0",
"@sipe-team/button": "workspace:*",
"@sipe-team/tokens": "workspace:^",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-live": "^4.1.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.6.3",
"@docusaurus/tsconfig": "3.6.3",
"@docusaurus/types": "3.6.3",
"@tailwindcss/postcss": "^4.3.0",
"react-docgen-typescript": "^2.2.2",
"tailwindcss": "^4.3.0",
"typescript": "~5.6.2"
},
"browserslist": {
Expand Down
70 changes: 70 additions & 0 deletions www/scripts/extract-props.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env node
/**
* Build-time props extraction.
* Usage: node scripts/extract-props.mjs --component <name>
*/

import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..', '..');
const outputDir = path.resolve(__dirname, '..', 'src', '.generated', 'props');

function toPascalCase(name) {
return name
.split('-')
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
}

function formatType(type) {
if (!type) return { name: 'unknown' };
if (Array.isArray(type.value)) {
return {
name: type.name,
value: type.value.map((v) => (typeof v === 'object' ? v.value : v)),
};
}
return { name: type.name };
}

function extractProps(name) {
const reactDocgenTypescript = require('react-docgen-typescript');
const source = path.resolve(repoRoot, 'packages', name, 'src', `${toPascalCase(name)}.tsx`);
if (!fs.existsSync(source)) throw new Error(`Source file not found: ${source}`);

const parser = reactDocgenTypescript.withDefaultConfig({
savePropValueAsString: true,
shouldExtractValuesFromUnion: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) => (prop.parent ? !prop.parent.fileName.includes('node_modules') : true),
});
const [doc] = parser.parse(source);
if (!doc) return [];

return Object.values(doc.props).map((p) => ({
name: p.name,
type: formatType(p.type),
defaultValue: p.defaultValue ? { value: p.defaultValue.value } : null,
required: p.required,
description: p.description || '',
}));
}

const args = process.argv.slice(2);
const idx = args.indexOf('--component');
const name = args[idx + 1];
// Restrict to lowercase alphanumeric+hyphen so the resolved source path cannot escape packages/.
if (idx === -1 || !name || !/^[a-z][a-z0-9-]*$/.test(name)) {
console.error('Usage: node scripts/extract-props.mjs --component <name>');
process.exit(1);
}

const props = extractProps(name);
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.resolve(outputDir, `${name}.json`), JSON.stringify(props, null, 2));
console.log(`[extract-props] ${name}: ${props.length} props`);
106 changes: 106 additions & 0 deletions www/src/components/Playground/ControlsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Fragment } from 'react';

import { unquote } from './generateCode';

type PropDescriptor = {
name: string;
type: { name: string; value?: string[] };
defaultValue: { value: string } | null;
required: boolean;
description: string;
};

type ControlsPanelProps = {
propsSchema: readonly PropDescriptor[];
values: Record<string, string>;
onChange: (name: string, value: string) => void;
};

const fieldClass =
'rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900/30 focus-visible:border-gray-400';

const checkboxClass = 'h-4 w-4 accent-[var(--side-color-accent-default,#ffb24d)]';

function isBooleanEnum(type: PropDescriptor['type']): boolean {
return (
type.name === 'enum' &&
type.value?.length === 2 &&
type.value.every((v) => unquote(v) === 'true' || unquote(v) === 'false')
);
}

function isControllable(prop: PropDescriptor): boolean {
const { type } = prop;
return (type.name === 'enum' && !!type.value) || type.name === 'boolean' || type.name === 'string';
}

export function ControlsPanel({ propsSchema, values, onChange }: ControlsPanelProps) {
const controls = propsSchema.filter(isControllable);

return (
<div className="border-t border-gray-200">
<div className="px-4 py-2.5 text-sm font-medium text-gray-500">Controls</div>
{controls.length === 0 ? (
<p className="px-4 pb-4 text-sm text-gray-400">No adjustable props</p>
) : (
<div className="grid grid-cols-[7rem_1fr] items-center gap-x-4 gap-y-3 px-4 pb-4 text-sm">
{controls.map((prop) => {
const { name, type } = prop;
const id = `ctrl-${name}`;
const checked = values[name] === 'true';

let control: JSX.Element;
if (isBooleanEnum(type) || type.name === 'boolean') {
control = (
<input
id={id}
type="checkbox"
className={checkboxClass}
checked={checked}
onChange={(e) => onChange(name, e.target.checked ? 'true' : 'false')}
/>
);
} else if (type.name === 'enum' && type.value) {
control = (
<select
id={id}
className={`${fieldClass} w-full max-w-xs`}
value={values[name] ?? ''}
onChange={(e) => onChange(name, e.target.value)}
>
{type.value.map((v) => {
const clean = unquote(v);
return (
<option key={clean} value={clean}>
{clean}
</option>
);
})}
</select>
);
} else {
control = (
<input
id={id}
type="text"
className={`${fieldClass} w-full max-w-xs`}
value={values[name] ?? ''}
onChange={(e) => onChange(name, e.target.value)}
/>
);
}

return (
<Fragment key={name}>
<label htmlFor={id} className="truncate font-medium text-gray-700">
{name}
</label>
{control}
</Fragment>
);
})}
</div>
)}
</div>
);
}
Loading
Loading