diff --git a/README.md b/README.md
index 048fddc..751fdb9 100644
--- a/README.md
+++ b/README.md
@@ -49,14 +49,16 @@ button {
```jsx
<>
- {/* TS error, tertiary isn't valid */}
+
+ {/* TS error, tertiary isn't valid */}
+
>
```
Output
```jsx
- {/* Same as in Page.tsx */}
+ {/* Same as in Page.tsx */}
```
_This example demonstrates enums, but MistCSS also supports boolean and string props. For more details, see the FAQ._
@@ -67,6 +69,7 @@ MistCSS parses your `mist.css` file and generates `mist.d.ts` for type safety.
For instance, here’s the generated `mist.d.ts` for our button component:
+
```typescript
interface Mist_button extends React.DetailedHTMLProps, HTMLButtonElement> {
'data-variant'?: 'primary' | 'secondary'
@@ -78,6 +81,7 @@ declare namespace JSX {
}
}
```
+
That’s it! Simple yet powerful, built entirely on browser standards and TypeScript/JSX.
@@ -132,8 +136,7 @@ button {
#### Tailwind v4
-Tailwind v4 will support CSS variables natively (see [blog post](https://tailwindcss.com/blog/tailwindcss-v4-alpha
-)).
+Tailwind v4 will support CSS variables natively (see [blog post](https://tailwindcss.com/blog/tailwindcss-v4-alpha)).
#### Tailwind (inline style)
@@ -190,7 +193,7 @@ div[data-component='section']
&[data-size="lg"] { ... }
/* Boolean props */
- &[data-is-active] { ... }
+ &[data-is-active] { ... }
/* Condition: size="lg" && is-active */
&[data-size="lg"]&[data-is-active] { ... }
@@ -216,29 +219,32 @@ If you want both basic links and button-styled links, here’s how you can do:
```css
a:not([data-component]) { /* ... */ }
-a[data-component='button'] { /* ... */ }
+a[data-component='button'] {
&[data-variant='primary'] { /* ... */ }
}
```
+
```jsx
<>
Home
Home
Home
- Home {/* TS error, `data-variant` is only valid with `data-component="button"` */}
+
+ {/* TS error, `data-variant` is only valid with `data-component="button"` */}
+ Home
>
```
+
-> [!NOTE]
-> `data-component` is just a naming convention. Feel free to use any attribute, like `data-kind='button'` or just `data-c`. It’s simply a way to differentiate between components using the same tag.
+> [!NOTE] > `data-component` is just a naming convention. Feel free to use any attribute, like `data-kind='button'` or just `data-c`. It’s simply a way to differentiate between components using the same tag.
### How to split my code?
You can use CSS [@import](https://developer.mozilla.org/en-US/docs/Web/CSS/@import). For example, in your `mist.css` file:
```css
-@import './button.css'
+@import './button.css';
```
### How to build complex components?
@@ -246,9 +252,15 @@ You can use CSS [@import](https://developer.mozilla.org/en-US/docs/Web/CSS/@impo
`mist.css`
```css
-article[data-component='card'] { /* ... */ }
-div[data-component='card-title'] { /* ... */ }
-div[data-component='card-content'] { /* ... */ }
+article[data-component='card'] {
+ /* ... */
+}
+div[data-component='card-title'] {
+ /* ... */
+}
+div[data-component='card-content'] {
+ /* ... */
+}
```
`Card.jsx`
@@ -294,7 +306,7 @@ import 'my-ui/mist.css'
`app/mist.d.ts`
-```
+```typescript
import 'my-ui/mist.d.ts
```
diff --git a/src/index.ts b/src/index.ts
index 06b572b..01e4062 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,8 @@
import fs = require('node:fs')
import { type PluginCreator } from 'postcss'
-import selectorParser = require('postcss-selector-parser');
-import atImport = require("postcss-import")
-import path = require('node:path');
+import selectorParser = require('postcss-selector-parser')
+import atImport = require('postcss-import')
+import path = require('node:path')
const html = require('./html')
const key = require('./key')
@@ -30,49 +30,123 @@ function render(parsed: Parsed): string {
let interfaceDefinitions = ''
const jsxElements: Record = {}
- Object.entries(parsed).forEach(
- ([key, { tag, rootAttribute, attributes, booleanAttributes, properties }]) => {
- const interfaceName = `Mist_${key}`
+ // Normalize
+ type Component = {
+ rootAttribute: string
+ discriminatorAttributes: Set
+ attributes: Record>
+ booleanAttributes: Set
+ properties: Set
+ }
- const attributeEntries = Object.entries(attributes)
+ const normalized: Record<
+ string,
+ {
+ _base: Component
+ [other: string]: Component
+ }
+ > = {}
- let htmlElement = 'HTMLElement'
- if (tag in html) {
- htmlElement = html[tag as keyof typeof html]
+ console.log(parsed)
+
+ Object.entries(parsed).forEach(
+ ([
+ key,
+ { tag, rootAttribute, attributes, booleanAttributes, properties },
+ ]) => {
+ // Default base tag, always there
+ normalized[tag] ??= {
+ _base: {
+ rootAttribute: '',
+ discriminatorAttributes: new Set(),
+ attributes: {},
+ booleanAttributes: new Set(),
+ properties: new Set(),
+ },
}
- let interfaceDefinition = `interface ${interfaceName} extends React.DetailedHTMLProps, ${htmlElement}> {\n`
+ if (rootAttribute !== '') {
+ normalized[tag][key] ??= {
+ rootAttribute,
+ discriminatorAttributes: new Set(),
+ attributes,
+ booleanAttributes,
+ properties,
+ }
+ normalized[tag]['_base']['discriminatorAttributes'] ??= new Set()
+ normalized[tag]['_base']['discriminatorAttributes'].add(rootAttribute)
+ } else {
+ normalized[tag]['_base'] = {
+ rootAttribute,
+ discriminatorAttributes: new Set(),
+ attributes,
+ booleanAttributes,
+ properties,
+ }
+ }
+ },
+ )
- attributeEntries.forEach(([attr, values]) => {
- const valueType = Array.from(values)
- .map((v) => `'${v}'`)
- .join(' | ')
- // Root attribute is used to narrow type and therefore is the only attribute
- // that shouldn't be optional (i.e. attr: ... and not attr?: ...)
- interfaceDefinition += ` '${attr}'${rootAttribute === attr ? '' : '?'}: ${valueType}\n`
- })
+ console.dir(normalized, { depth: null })
+
+ Object.entries(normalized).forEach(([tag, components]) => {
+ Object.entries(components).forEach(
+ ([
+ key,
+ {
+ rootAttribute,
+ discriminatorAttributes,
+ attributes,
+ booleanAttributes,
+ properties,
+ },
+ ]) => {
+ const interfaceName = `Mist_${key === '_base' ? tag : key}`
+
+ const attributeEntries = Object.entries(attributes)
+
+ let htmlElement = 'HTMLElement'
+ if (tag in html) {
+ htmlElement = html[tag as keyof typeof html]
+ }
+
+ let interfaceDefinition = `interface ${interfaceName} extends React.DetailedHTMLProps, ${htmlElement}> {\n`
+
+ discriminatorAttributes.forEach((attr) => {
+ interfaceDefinition += ` '${attr}'?: never\n`
+ })
- booleanAttributes.forEach((attr) => {
- interfaceDefinition += ` '${attr}'?: boolean\n`
- })
+ attributeEntries.forEach(([attr, values]) => {
+ const valueType = Array.from(values)
+ .map((v) => `'${v}'`)
+ .join(' | ')
+ // Root attribute is used to narrow type and therefore is the only attribute
+ // that shouldn't be optional (i.e. attr: ... and not attr?: ...)
+ interfaceDefinition += ` '${attr}'${rootAttribute === attr ? '' : '?'}: ${valueType}\n`
+ })
- if (Array.from(properties).length > 0) {
- const propertyEntries = Array.from(properties)
- .map((prop) => `'${prop}': string`)
- .join(', ')
- interfaceDefinition += ` style?: { ${propertyEntries} } & React.CSSProperties\n`
- }
+ booleanAttributes.forEach((attr) => {
+ interfaceDefinition += ` '${attr}'?: boolean\n`
+ })
- interfaceDefinition += '}\n\n'
+ if (Array.from(properties).length > 0) {
+ const propertyEntries = Array.from(properties)
+ .map((prop) => `'${prop}': string`)
+ .join(', ')
+ interfaceDefinition += ` style?: { ${propertyEntries} } & React.CSSProperties\n`
+ }
- interfaceDefinitions += interfaceDefinition
+ interfaceDefinition += '}\n\n'
- if (!jsxElements[tag]) {
- jsxElements[tag] = []
- }
- jsxElements[tag].push(interfaceName)
- },
- )
+ interfaceDefinitions += interfaceDefinition
+
+ if (!jsxElements[tag]) {
+ jsxElements[tag] = []
+ }
+ jsxElements[tag].push(interfaceName)
+ },
+ )
+ })
// Generate the JSX namespace declaration dynamically
let jsxDeclaration =
@@ -151,7 +225,7 @@ _mistcss.postcss = true
const mistcss: PluginCreator<{}> = (_opts = {}) => {
return {
postcssPlugin: 'mistcss',
- plugins: [atImport(), _mistcss()]
+ plugins: [atImport(), _mistcss()],
}
}
diff --git a/test/card.mist.css b/test/card.mist.css
new file mode 100644
index 0000000..2c419d3
--- /dev/null
+++ b/test/card.mist.css
@@ -0,0 +1,12 @@
+/* Testing union discriminator */
+div[data-component='card'] {
+ background: gray;
+ &[data-size='sm'] {
+ }
+ &[data-size='xl'] {
+ }
+}
+
+div[data-component='card-title'] {
+ background: gray;
+}
diff --git a/test/mist.css b/test/mist.css
index 0d0d6ba..34f0466 100644
--- a/test/mist.css
+++ b/test/mist.css
@@ -1 +1,2 @@
@import './button.mist.css';
+@import './card.mist.css';
diff --git a/test/mist.d.ts b/test/mist.d.ts
index 699d8e6..b64cbfd 100644
--- a/test/mist.d.ts
+++ b/test/mist.d.ts
@@ -2,8 +2,22 @@ interface Mist_button extends React.DetailedHTMLProps, HTMLDivElement> {
+ 'data-component'?: never
+}
+
+interface Mist_div_data_component_card extends React.DetailedHTMLProps, HTMLDivElement> {
+ 'data-component': 'card'
+ 'data-size'?: 'sm' | 'xl'
+}
+
+interface Mist_div_data_component_card_title extends React.DetailedHTMLProps, HTMLDivElement> {
+ 'data-component': 'card-title'
+}
+
declare namespace JSX {
interface IntrinsicElements {
button: Mist_button
+ div: Mist_div | Mist_div_data_component_card | Mist_div_data_component_card_title
}
}