Skip to content

Commit f448967

Browse files
authored
Add ds-react package (#635)
1 parent 5fbec51 commit f448967

File tree

136 files changed

+8496
-68
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+8496
-68
lines changed

packages/react/.eslintrc.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* eslint-env node */
2+
module.exports = {
3+
extends: ['../../.eslintrc.js', 'plugin:storybook/recommended'],
4+
parserOptions: {
5+
project: './tsconfig.json',
6+
tsconfigRootDir: __dirname,
7+
},
8+
};

packages/react/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.css
2+
!src/**/*.css

packages/react/.storybook/main.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { join, dirname } from 'path';
2+
import type { StorybookConfig } from '@storybook/react-vite';
3+
4+
/**
5+
* This function is used to resolve the absolute path of a package.
6+
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
7+
*/
8+
function getAbsolutePath(value: string) {
9+
return dirname(require.resolve(join(value, 'package.json')));
10+
}
11+
12+
const config: StorybookConfig = {
13+
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
14+
addons: [
15+
getAbsolutePath('@storybook/addon-onboarding'),
16+
getAbsolutePath('@storybook/addon-essentials'),
17+
getAbsolutePath('@chromatic-com/storybook'),
18+
getAbsolutePath('@storybook/addon-interactions'),
19+
getAbsolutePath('@storybook/addon-mdx-gfm'),
20+
],
21+
framework: {
22+
name: getAbsolutePath('@storybook/react-vite'),
23+
options: {},
24+
},
25+
async viteFinal(config) {
26+
// Add your configuration here
27+
config.optimizeDeps = {
28+
exclude: ['node_modules/.cache/sb-vite'],
29+
};
30+
return config;
31+
},
32+
};
33+
export default config;

packages/react/.storybook/preview.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Preview } from '@storybook/react';
2+
import '@utilitywarehouse/fontsource';
3+
import '@utilitywarehouse/css-reset';
4+
import '@utilitywarehouse/colour-system/css/colours.css';
5+
import '../styles.css';
6+
import '../src/storybook/styles.css';
7+
8+
const preview: Preview = {
9+
parameters: {},
10+
};
11+
12+
export default preview;

packages/react/.stylelintrc.cjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = {
2+
extends: 'stylelint-config-standard',
3+
plugins: ['stylelint-media-use-custom-media'],
4+
rules: {
5+
'csstools/media-use-custom-media': ['known', { importFrom: ['./src/styles/breakpoints.css'] }],
6+
'at-rule-no-unknown': [true, { ignoreAtRules: ['breakpoints'] }],
7+
// Enforce prefixes on classnames and keyframes
8+
'selector-class-pattern': /^((mobile|tablet|desktop|wide):)?-?uwp-([a-zA-Z\d]|-)+$/,
9+
'custom-property-pattern': /([a-zA-Z\d]|-)+$/,
10+
'keyframes-name-pattern': /^uwp-([a-z]|-)+$/,
11+
},
12+
};

packages/react/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# React

packages/react/package.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"name": "@utilitywarehouse/ds-react",
3+
"version": "0.0.0",
4+
"description": "React components",
5+
"type": "module",
6+
"main": "dist/index.cjs",
7+
"module": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"sideEffects": false,
10+
"scripts": {
11+
"clean": "rm -rf .turbo node_modules dist *.css",
12+
"build": "pnpm build:css && pnpm build:js",
13+
"build:css": "postcss ./src/styles/index.css -o styles.css",
14+
"build:js": "NODE_OPTIONS='--max-old-space-size=16384' tsup",
15+
"build:storybook": "storybook build",
16+
"build:storybook:ci": "pnpm build && pnpm build:storybook",
17+
"dev": "pnpm dev:storybook & pnpm dev:css",
18+
"dev:css": "postcss --watch --verbose src/styles/index.css -o styles.css",
19+
"dev:storybook": "storybook dev -p 6006",
20+
"ts-types": "tsc",
21+
"lint": "pnpm lint:js && pnpm lint:css",
22+
"lint:fix": "pnpm lint:js:fix && pnpm lint:css:fix",
23+
"lint:js": "TIMING=1 eslint --max-warnings 0 \"src/**/*.ts*\"",
24+
"lint:js:fix": "TIMING=1 eslint --fix --max-warnings 0 \"src/**/*.ts*\" src",
25+
"lint:css": "stylelint \"src/**/*.css\"",
26+
"lint:css:fix": "stylelint \"src/**/*.css\" --fix",
27+
"format": "prettier --write \"src/**/*.ts*\"",
28+
"format:ci": "prettier --list-different \"src/**/*.ts*\""
29+
},
30+
"publishConfig": {
31+
"access": "restricted"
32+
},
33+
"files": [
34+
"dist",
35+
"*.css"
36+
],
37+
"peerDependencies": {
38+
"react": "^17.0.0 || ^18.0.0",
39+
"react-dom": "^17.0.0 || ^18.0.0"
40+
},
41+
"dependencies": {
42+
"@radix-ui/react-radio-group": "^1.2.1",
43+
"@radix-ui/react-slot": "^1.1.0",
44+
"@utilitywarehouse/react-icons": "^1.11.0",
45+
"clsx": "^2.1.1"
46+
},
47+
"devDependencies": {
48+
"@chromatic-com/storybook": "^3.2.2",
49+
"@storybook/addon-essentials": "^8.4.2",
50+
"@storybook/addon-interactions": "^8.4.2",
51+
"@storybook/addon-mdx-gfm": "^8.4.5",
52+
"@storybook/addon-onboarding": "^8.4.2",
53+
"@storybook/blocks": "^8.4.2",
54+
"@storybook/react": "^8.4.2",
55+
"@storybook/react-vite": "^8.4.2",
56+
"@storybook/test": "^8.4.2",
57+
"@types/node": "^22.9.0",
58+
"@types/react": "^18.3.12",
59+
"@utilitywarehouse/colour-system": "^0.5.0",
60+
"@utilitywarehouse/css-reset": "^0.1.0",
61+
"@utilitywarehouse/fontsource": "^0.1.0",
62+
"autoprefixer": "^10.4.20",
63+
"cssnano": "^7.0.6",
64+
"eslint-plugin-storybook": "^0.11.0",
65+
"postcss": "^8.4.49",
66+
"postcss-cli": "^11.0.0",
67+
"postcss-custom-media": "^11.0.5",
68+
"postcss-import": "^16.1.0",
69+
"postcss-nesting": "^13.0.1",
70+
"prop-types": "^15.8.1",
71+
"react": "^17.0.0 || ^18.0.0",
72+
"react-dom": "^17.0.0 || ^18.0.0",
73+
"storybook": "^8.4.2",
74+
"stylelint": "^16.10.0",
75+
"stylelint-config-standard": "^36.0.1",
76+
"stylelint-media-use-custom-media": "^4.0.0",
77+
"tsup": "^8.3.5"
78+
}
79+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const postcss = require('postcss');
4+
5+
/**
6+
*
7+
* Many thanks to Radix for blazing the trail: https://github.com/radix-ui/themes/blob/main/packages/radix-ui-themes/postcss-breakpoints.cjs
8+
*
9+
*/
10+
11+
// Build a list of breakpoints from "@custom media" rules in "breakpoints.css"
12+
const breakpointsFile = path.resolve('./src/styles/breakpoints.css');
13+
const breakpointsCss = fs.readFileSync(breakpointsFile, 'utf-8');
14+
const breakpoints = postcss
15+
.parse(breakpointsCss)
16+
.nodes.map(node => {
17+
if (node.type === 'atrule' && node.name === 'custom-media') {
18+
const [_match, name, params] = node.params.match(/--(\w+)\s+(.+)/);
19+
return { name, params };
20+
}
21+
22+
return null;
23+
})
24+
.filter(Boolean);
25+
26+
const cache = new WeakMap();
27+
28+
module.exports = () => ({
29+
postcssPlugin: 'postcss-breakpoints',
30+
Rule(rule) {
31+
if (rule.parent.name === 'breakpoints') {
32+
const breakpointsRule = rule.parent;
33+
34+
// when we first meet a given @breakpoints at-rule
35+
if (!cache.has(breakpointsRule)) {
36+
// create the final media rules for this @breakpoints at-rule
37+
const medias = breakpoints.reduce((breakpointsMedias, breakpoint) => {
38+
breakpointsMedias[breakpoint.name] = new postcss.AtRule({
39+
name: 'media',
40+
params: breakpoint.params,
41+
});
42+
return breakpointsMedias;
43+
}, {});
44+
45+
// add an entry to the cache
46+
cache.set(breakpointsRule, medias);
47+
48+
// add final media rules after the @breakpoints at-rule
49+
const mediaRules = Object.values(medias).reverse();
50+
mediaRules.forEach(media => {
51+
breakpointsRule.after(media);
52+
});
53+
}
54+
55+
// move the rule itself before @breakpoints at-rule
56+
breakpointsRule.before(rule);
57+
58+
// save clone of the rule before we modify it
59+
const originalRule = rule.clone();
60+
// clean up the extra indentation
61+
rule.selector = rule.selector.replace(/\n\s\s/g, '\n');
62+
rule.cleanRaws();
63+
64+
// add breakpoint-level rules
65+
breakpoints.forEach(breakpoint => {
66+
const clone = originalRule.clone();
67+
updateClass(clone, breakpoint.name);
68+
cache.get(breakpointsRule)[breakpoint.name].append(clone);
69+
});
70+
71+
// remove @breakpoints at-rule and clear cache if it has no rules
72+
if (breakpointsRule.nodes.length === 0) {
73+
breakpointsRule.remove();
74+
cache.delete(breakpointsRule);
75+
}
76+
}
77+
},
78+
});
79+
80+
module.exports.postcss = true;
81+
82+
function updateClass(node, prefix) {
83+
if (node.type === 'atrule') {
84+
node.each(child => updateClass(child, prefix));
85+
}
86+
87+
/**
88+
* Should match responsive classes (uwp-r- prefix):
89+
* ```
90+
* .uwp-r-size-1
91+
* .uwp-r-m-2
92+
* .-uwp-r-m-2
93+
* .uwp-Button.uwp-r-size-1 (captures "uwp-r-size-1")
94+
* ```
95+
*
96+
* Should not match:
97+
* .uwp-Button
98+
*/
99+
const classNameRegexp = /\.(-?uwp-r-[a-z0-9-]+)/g; // TODO: import class prefix?
100+
101+
// Check for rules that use compound props on a component:
102+
// - a component name (prefixed with "rt-" and pascal cased)
103+
// - followed by 2 or more prop selectors (lowercase, numbers, -)
104+
//
105+
// e.g. ".rt-DialogContent.rt-r-size-2.gray"
106+
if (/\.uwp-(?:[A-Z][a-z]+)+(?:\.[a-z0-9-]+){2,}/.test(node.selector)) {
107+
throw Error(`
108+
"${node.selector}" looks like it uses compound props on a component.
109+
"@breakpoints" does not support compound props yet.
110+
`);
111+
}
112+
113+
if (classNameRegexp.test(node.selector)) {
114+
node.selector = node.selector.replace(classNameRegexp, `.${prefix}\\:$1`);
115+
}
116+
117+
addPropertySuffixes(node, prefix);
118+
}
119+
120+
function addPropertySuffixes(propertyNode, suffix) {
121+
propertyNode.nodes.map(node => {
122+
/**
123+
* Should match custom properties with responsive --r prefix:
124+
* ```
125+
* --r-padding
126+
* ```
127+
*/
128+
const propertyNameRegexp = /(--r-[a-z0-9-]+)/g;
129+
if (propertyNameRegexp.test(node.value)) {
130+
node.value = node.value.replace(propertyNameRegexp, `$1-${suffix}`);
131+
}
132+
});
133+
}

packages/react/postcss.config.cjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module.exports = {
2+
plugins: [
3+
require('postcss-import'),
4+
require('postcss-nesting'),
5+
require('./postcss-breakpoints.cjs'),
6+
require('postcss-custom-media'),
7+
require('autoprefixer'),
8+
// require('cssnano')({
9+
// preset: 'default',
10+
// }),
11+
],
12+
};

0 commit comments

Comments
 (0)