├── .changeset └── config.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── README.md ├── eslint-plugin-split-classnames ├── CHANGELOG.md ├── README.md ├── package.json ├── src │ ├── index.ts │ ├── rule.ts │ └── utils.ts └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── split-classnames ├── CHANGELOG.md ├── __snapshots__ │ └── rule.test.ts.snap ├── bin.js ├── example │ ├── input.jsx │ └── output.jsx ├── package.json ├── rule.test.ts ├── src │ └── cli.ts └── tsconfig.json ├── tsconfig.base.json ├── vscode-extension ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── .yarnrc ├── README.md ├── package.json ├── scripts │ └── build.ts ├── src │ ├── extension.ts │ └── test │ │ └── runTest.ts └── tsconfig.json └── website ├── .env.example ├── .eslintrc.json ├── .gitignore ├── CHANGELOG.md ├── README.md ├── components ├── CodeEditor.tsx ├── Footer.tsx └── Link.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── index.tsx ├── success.tsx └── webhook.tsx ├── postcss.config.js ├── prism-theme.css ├── public ├── bg_gradient.svg ├── favicon.ico └── vercel.svg ├── styles.css ├── tailwind.config.js └── tsconfig.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.22.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [["eslint-plugin-split-classnames", "split-classnames"]], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "onlyUpdatePeerDependentsWhenOutOfRange": true, 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | # branches: 6 | # - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 16 18 | registry-url: https://registry.npmjs.org/ 19 | # caching 20 | # - uses: actions/cache@v2 21 | # with: 22 | # path: ~/.npm 23 | # key: ${{ runner.os }}-npm- 24 | # restore-keys: | 25 | # ${{ runner.os }}-npm- 26 | - name: Cache pnpm modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | restore-keys: | 32 | ${{ runner.os }}- 33 | # setup pnpm 34 | - uses: pnpm/action-setup@v2.0.1 35 | with: 36 | version: 7 37 | run_install: false 38 | # scripts 39 | - run: pnpm i --store-dir ~/.pnpm-store 40 | - run: pnpm build 41 | - run: pnpm test 42 | # - name: Create Release 43 | # id: changesets 44 | # uses: changesets/action@master 45 | # # with: 46 | # # publish: pnpm release 47 | # env: 48 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | esm 4 | .DS_Store 5 | *.tsbuildinfo 6 | .ultra.cache.json 7 | .env* 8 | !.env.example -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "jsxSingleQuote": true, 4 | "tabWidth": 4, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Splits long className attributes to make them more readable

4 |
5 |
6 |
7 | 8 | Subscribe to my [Newsletter](https://xmorse.xyz) if you want to get notified about other cool projects and updates. 9 | 10 | ## Example 11 | 12 | The following code 13 | 14 | ```tsx 15 | function Component() { 16 | return ( 17 | 18 |

19 | 20 | ) 21 | } 22 | ``` 23 | 24 | becomes 25 | 26 | ```tsx 27 | import classNames from 'classnames' 28 | function Component() { 29 | return ( 30 | 31 |

40 | 41 | ) 42 | } 43 | ``` 44 | 45 | ## Usage as a cli 46 | 47 | ```sh 48 | npm i -g split-classnames 49 | # split classnames on all js files in the src directory 50 | split-classnames --dry --max 30 'src/**' 51 | ``` 52 | 53 | ## Usage as an eslint plugin 54 | 55 | Install the plugin: 56 | 57 | ```sh 58 | npm i -D eslint-plugin-split-classnames 59 | ``` 60 | 61 | Add the plugin to your eslint config: 62 | 63 | ```json 64 | // .eslintrc.json 65 | { 66 | "plugins": ["split-classnames"], 67 | "rules": { 68 | "split-classnames/split-classnames": [ 69 | "error", 70 | { 71 | "maxClassNameCharacters": 40, 72 | "functionName": "classnames" 73 | } 74 | ] 75 | } 76 | } 77 | ``` 78 | 79 | Then run eslint with `--fix` to split long classnames 80 | 81 | ```sh 82 | eslint --fix ./src 83 | ``` 84 | 85 | ## Features 86 | 87 | - Works bot on typescript and javascript jsx 88 | - Works on string literals (`className='something'`) 89 | - Works on template literals (`className={`something ${anotherClass}`}`) 90 | - Works on existing classnames calls (`className={clsx('very long classNames are slitted in groups')}`) 91 | - Regroups already existing classnames calls 92 | - Sorts the classes for tailwind (variants like `sm:` and `lg:` are put last) 93 | 94 | 95 | ## Sponsors 96 | 97 | [**Notaku**](https://notaku.website) 98 | 99 | 100 | [![Notaku](https://preview.notaku.website/github_banner.jpg)](https://notaku.website) 101 | 102 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-split-classnames 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - Add version in cli 8 | 9 | ## 0.2.0 10 | 11 | ### Minor Changes 12 | 13 | - Sorting is now deterministic 14 | 15 | ## 0.1.1 16 | 17 | ### Patch Changes 18 | 19 | - Do not report on already organized classnames calls 20 | 21 | ## 0.1.0 22 | 23 | ### Minor Changes 24 | 25 | - Split cs calls 26 | 27 | ## 0.0.4 28 | 29 | ### Patch Changes 30 | 31 | - Fixed template literals case 32 | 33 | ## 0.0.3 34 | 35 | ### Patch Changes 36 | 37 | - Fix typescript support, using jscodeshift minimally 38 | 39 | ## 0.0.2 40 | 41 | ### Patch Changes 42 | 43 | - Added tsx parser to j 44 | 45 | ## 0.0.1 46 | 47 | ### Patch Changes 48 | 49 | - Init 50 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Install the plugin with 4 | 5 | ``` 6 | npm i -D eslint-plugin-split-classnames 7 | ``` 8 | 9 | ## Eslint config 10 | 11 | Add the following code into your `.eslintrc.json` config 12 | 13 | ```json 14 | { 15 | "plugins": ["split-classnames"], 16 | "rules": { 17 | "split-classnames/split-classnames": [ 18 | "error", 19 | { 20 | "maxClassNameCharacters": 60, 21 | "functionName": "classNames" 22 | } 23 | ] 24 | } 25 | } 26 | ``` 27 | 28 | To automatically split classnames run the following command or use the Vscode ESlint extension with `Ctrl - Shift - P` and `Eslint - Fix all auto fixable problems` 29 | 30 | ```sh 31 | eslint --fix . 32 | ``` 33 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-split-classnames", 3 | "version": "0.3.0", 4 | "description": "Eslint plugin to keep className attributes short and readable", 5 | "main": "dist/index.js", 6 | "sideEffects": false, 7 | "types": "dist/index.d.ts", 8 | "repository": "remorses/split-classnames", 9 | "scripts": { 10 | "build": "tsc", 11 | "play": "esno src/cli", 12 | "watch": "tsc -w" 13 | }, 14 | "files": [ 15 | "dist", 16 | "src" 17 | ], 18 | "keywords": [ 19 | "eslint", 20 | "eslintplugin", 21 | "classname" 22 | ], 23 | "author": "Tommaso De Rossi, morse ", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "@types/eslint": "^8.2.0", 27 | "eslint": "^8.3.0", 28 | "estree-jsx": "^0.0.1" 29 | }, 30 | "peerDependencies": { 31 | "eslint": "*" 32 | }, 33 | "dependencies": { 34 | "ast-types": "^0.14.2", 35 | "jscodeshift": "^0.13.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import path from 'path' 3 | import { rule } from './rule' 4 | 5 | const plugin = { 6 | rules: { 7 | 'split-classnames': rule, 8 | }, 9 | 10 | configs: { 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | config: { 17 | plugins: ['split-classnames'], 18 | rules: { 19 | 'split-classnames/split-classnames': 'error', 20 | }, 21 | }, 22 | }, 23 | } 24 | export default plugin 25 | module.exports = plugin 26 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/src/rule.ts: -------------------------------------------------------------------------------- 1 | import _jscodeshift, { JSCodeshift } from 'jscodeshift' 2 | 3 | const j: JSCodeshift = _jscodeshift.withParser('tsx') 4 | 5 | const CLASSNAMES_IDENTIFIER_NAME = 'classNames' 6 | 7 | // TODO make a sorter that does not sort based on chars length but instead creates groups, like group for md, lg, base, dark, hover, ... 8 | function tailwindRank(a: string) { 9 | // a before b 10 | let rank = 0 11 | 12 | // a after b 13 | if (a.includes(':')) { 14 | rank += 1 15 | } 16 | 17 | // a after b 18 | if (a.includes('[')) { 19 | rank += 2 20 | } 21 | // keep order 22 | return rank 23 | } 24 | 25 | export function splitClassNames( 26 | className: string, 27 | maxClassLength: number = 60, 28 | ) { 29 | className = className.trim() 30 | if (className.length <= maxClassLength) { 31 | return null 32 | } 33 | const classes = className 34 | .split(/\s+/) 35 | .filter((name) => name.length > 0) 36 | .sort((a, b) => { 37 | const diff = tailwindRank(a) - tailwindRank(b) 38 | if (diff === 0) { 39 | return a < b ? 1 : -1 40 | } 41 | return diff 42 | }) 43 | // console.log(classes) 44 | 45 | const classGroups: string[] = [] 46 | let currentSize = 0 47 | let lastAddedIndex = 0 48 | 49 | for (let i = 0; i < classes.length; i += 1) { 50 | currentSize += classes[i].length 51 | if (currentSize >= maxClassLength || i === classes.length - 1) { 52 | classGroups.push(classes.slice(lastAddedIndex, i + 1).join(' ')) 53 | lastAddedIndex = i + 1 54 | currentSize = 0 55 | } 56 | } 57 | 58 | if (classGroups.length <= 1) { 59 | return null 60 | } 61 | 62 | return classGroups 63 | } 64 | 65 | // TODO you can also use https://github.com/dcastil/tailwind-merge to merge tailwind stuff 66 | 67 | const possibleClassNamesImportNames = new Set([ 68 | 'classnames', 69 | 'classNames', 70 | 'clsx', 71 | 'cc', 72 | 'cx', 73 | 'cs', 74 | 'classcat', 75 | ]) 76 | 77 | const possibleClassNamesImportSources = new Set([ 78 | 'classnames', 79 | 'clsx', 80 | 'classcat', 81 | ]) 82 | 83 | // TODO custom function import source 84 | const CLASSNAMES_IMPORT_SOURCE = 'classnames' 85 | 86 | const meta: import('eslint').Rule.RuleMetaData = { 87 | type: 'problem', 88 | 89 | docs: { 90 | description: 'suggest using className() or clsx() in JSX className', 91 | category: 'Stylistic Issues', 92 | recommended: true, 93 | // url: documentUrl('prefer-classnames-function'), 94 | }, 95 | 96 | fixable: 'code', 97 | 98 | schema: [ 99 | { 100 | type: 'object', 101 | functionName: false, 102 | properties: { 103 | maxClassNameCharacters: { 104 | type: 'number', 105 | }, 106 | functionName: { 107 | type: 'string', 108 | }, 109 | }, 110 | }, 111 | ], 112 | } 113 | 114 | export type Opts = { 115 | maxClassNameCharacters?: number 116 | functionName?: string 117 | } 118 | 119 | export const rule: import('eslint').Rule.RuleModule = { 120 | meta, 121 | create(context) { 122 | const [params = {}] = context.options 123 | const { functionName, maxClassNameCharacters = 40 } = params 124 | let addedImport = false 125 | function report({ 126 | replaceWith: replaceWith, 127 | message = '', 128 | node, 129 | }: { 130 | node: import('ast-types').ASTNode 131 | message?: string 132 | replaceWith?: import('ast-types').ASTNode 133 | }) { 134 | context.report({ 135 | node: node as any, 136 | message: message || 'The className is too long.', 137 | data: { 138 | functionName: params.functionName || 'clsx', 139 | }, 140 | 141 | *fix(fixer) { 142 | if ( 143 | !addedImport && 144 | shouldInsertCXImport && 145 | !existingClassNamesImportIdentifier && 146 | classNamesImportName 147 | ) { 148 | addedImport = true 149 | 150 | yield fixer.insertTextBeforeRange( 151 | [0, 0], 152 | `import ${classNamesImportName} from '${CLASSNAMES_IMPORT_SOURCE}'\n`, 153 | ) 154 | } 155 | if (replaceWith) { 156 | const newSource = j(replaceWith as any).toSource({ 157 | wrapColumn: 1000 * 10, 158 | quote: 'single', 159 | }) 160 | yield fixer.replaceText(node as any, newSource) 161 | } 162 | }, 163 | }) 164 | } 165 | 166 | let existingClassNamesImportIdentifier: string 167 | let classNamesImportName: string 168 | let shouldInsertCXImport = false 169 | 170 | return { 171 | Program: (program) => { 172 | const ast = context.getSourceCode().ast 173 | }, 174 | ImportDeclaration: (importDeclaration) => { 175 | // check if user has already imported the classnames function 176 | if ( 177 | possibleClassNamesImportSources.has( 178 | importDeclaration.source?.value as string, 179 | ) 180 | ) { 181 | const defaultImport = j(importDeclaration as any) 182 | .find(j.ImportDefaultSpecifier) 183 | .get() 184 | existingClassNamesImportIdentifier = 185 | defaultImport.node.local.name 186 | } 187 | }, 188 | JSXAttribute: function reportAndReset(node) { 189 | classNamesImportName = 190 | existingClassNamesImportIdentifier || 191 | functionName || 192 | CLASSNAMES_IDENTIFIER_NAME 193 | try { 194 | if ( 195 | node.name.name !== 'className' && 196 | node.name.name !== 'class' 197 | ) { 198 | return 199 | } 200 | 201 | // simple literals or literals inside expressions 202 | if (node?.value?.type === 'Literal') { 203 | const literal = j(node).find(j.Literal).get() 204 | // const literal = path.value. 205 | 206 | const splitted = splitClassNames( 207 | literal.value?.value, 208 | maxClassNameCharacters, 209 | ) 210 | if (!splitted) { 211 | return 212 | } 213 | const cxArguments = splitted.map((s) => j.literal(s)) 214 | // don't add the classnames if className attr is short enough 215 | if (cxArguments.length <= 1) { 216 | return 217 | } 218 | shouldInsertCXImport = true 219 | report({ 220 | node: literal.node, 221 | 222 | replaceWith: j.jsxExpressionContainer( 223 | j.callExpression( 224 | j.identifier(classNamesImportName), 225 | cxArguments, 226 | ), 227 | ), 228 | }) 229 | } 230 | // string literal inside expressions 231 | if ( 232 | node?.value?.type === 'JSXExpressionContainer' && 233 | node?.value?.expression?.type === 'Literal' 234 | ) { 235 | shouldInsertCXImport = true 236 | const literal = j(node).find(j.Literal).get() 237 | 238 | const cxArguments = splitClassNames( 239 | literal.value?.value, 240 | maxClassNameCharacters, 241 | )?.map((s) => j.literal(s)) 242 | if (!cxArguments) { 243 | return 244 | } 245 | report({ 246 | node: literal.node, 247 | 248 | replaceWith: j.callExpression( 249 | j.identifier(classNamesImportName), 250 | cxArguments, 251 | ), 252 | }) 253 | } 254 | 255 | // template literal 256 | 257 | if ( 258 | node.value.type === 'JSXExpressionContainer' && 259 | node.value.expression.type === 'TemplateLiteral' 260 | ) { 261 | shouldInsertCXImport = true 262 | const templateLiteral = j(node) 263 | .find(j.TemplateLiteral) 264 | .get() 265 | const { quasis, expressions } = templateLiteral.node 266 | let cxArguments: any[] = [] 267 | let shouldReport = false 268 | quasis.forEach((quasi, index) => { 269 | if (quasi.value.raw.trim()) { 270 | const classNames = splitClassNames( 271 | quasi.value.raw, 272 | maxClassNameCharacters, 273 | ) 274 | if (classNames) { 275 | shouldReport = true 276 | cxArguments.push( 277 | ...classNames.map((className) => 278 | j.literal(className), 279 | ), 280 | ) 281 | } else { 282 | cxArguments.push(j.literal(quasi.value.raw)) 283 | } 284 | } 285 | if (expressions[index] !== undefined) { 286 | cxArguments.push(expressions[index]) 287 | } 288 | }) 289 | if (shouldReport) { 290 | report({ 291 | node: node.value, 292 | 293 | replaceWith: fixArguments( 294 | j.jsxExpressionContainer( 295 | j.callExpression( 296 | j.identifier(classNamesImportName), 297 | cxArguments, 298 | ), 299 | ), 300 | ), 301 | }) 302 | } 303 | } 304 | 305 | // classnames arguments too long 306 | // regroups together classnames literal arguments and splits them in groups of maxClassNameCharacters (reordered using tailwindSort) 307 | const usedClassNameFn = 308 | node?.value?.expression?.callee?.name 309 | if ( 310 | node?.value?.type === 'JSXExpressionContainer' && 311 | node?.value?.expression?.type === 'CallExpression' && 312 | possibleClassNamesImportNames.has(usedClassNameFn) 313 | ) { 314 | const replaceWith = fixArguments(node) 315 | if (replaceWith) { 316 | report({ 317 | message: `The ${usedClassNameFn} arguments are not canonically organized.`, 318 | node: node.value, 319 | replaceWith, 320 | }) 321 | } 322 | } 323 | 324 | function fixArguments(node) { 325 | const callExpression = j(node) 326 | .find(j.CallExpression) 327 | .get() 328 | 329 | const classNamesImportName = 330 | callExpression.value.callee.name 331 | let literalParts: string[] = [] 332 | let nonLiteralParts: string[] = [] 333 | callExpression.value.arguments.forEach((arg) => { 334 | if (arg.type === 'Literal') { 335 | literalParts.push(arg.value) 336 | } else { 337 | // TODO maybe support template literals in function arguments 338 | nonLiteralParts.push(arg) 339 | } 340 | }) 341 | const splitted = splitClassNames( 342 | literalParts.join(' '), 343 | maxClassNameCharacters, 344 | ) 345 | 346 | if (!splitted) { 347 | return 348 | } 349 | const changed = 350 | splitted.length !== literalParts.length || 351 | splitted.some((x, i) => { 352 | const diff = 353 | literalParts[i] && x !== literalParts[i] 354 | // if (diff) { 355 | // console.log(literalParts[i], '-', x) 356 | // } 357 | return diff 358 | }) 359 | 360 | if (changed) { 361 | const newArgs: any[] = [ 362 | ...splitted.map((s) => j.literal(s)), 363 | ...nonLiteralParts, 364 | ] 365 | const replaceWith = j.jsxExpressionContainer( 366 | j.callExpression( 367 | j.identifier(classNamesImportName), 368 | newArgs, 369 | ), 370 | ) 371 | return replaceWith 372 | } 373 | } 374 | } catch (e) { 375 | throw new Error(`could not report for class names, ` + e) 376 | } 377 | }, 378 | } 379 | }, 380 | } 381 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ESLint } from 'eslint' 2 | import { Opts } from './rule' 3 | 4 | let eslint: ESLint 5 | export async function runRule(code: string, options: Opts = {}) { 6 | if (!eslint) { 7 | eslint = new ESLint({ 8 | // resolvePluginsRelativeTo: __dirname, 9 | baseConfig: {}, 10 | allowInlineConfig: true, 11 | 12 | overrideConfig: { 13 | plugins: ['split-classnames'], 14 | parser: '@typescript-eslint/parser', 15 | 16 | rules: { 17 | 'split-classnames/split-classnames': [ 18 | 'error', 19 | { ...options }, 20 | ], 21 | }, 22 | parserOptions: { 23 | ecmaVersion: 2018, 24 | 25 | sourceType: 'module', 26 | ecmaFeatures: { 27 | jsx: true, 28 | }, 29 | }, 30 | }, 31 | }) 32 | } 33 | const result = await eslint.lintText(code, { filePath: 'test.tsx' }) 34 | 35 | if (!result[0]?.messages?.length) { 36 | return '' 37 | } 38 | let fixedCode = result[0].source 39 | let incrementRanges = 0 40 | for (let message of result[0].messages) { 41 | // console.log(message.message) 42 | if (message.fix) { 43 | fixedCode = replaceRange( 44 | fixedCode, 45 | message.fix.range[0] + incrementRanges, 46 | message.fix.range[1] + incrementRanges, 47 | message.fix.text, 48 | ) 49 | incrementRanges += 50 | message.fix.text.length - 51 | (message.fix.range[1] - message.fix.range[0]) 52 | } 53 | } 54 | return fixedCode 55 | } 56 | 57 | function replaceRange(s, start, end, substitute) { 58 | return s.substring(0, start) + substitute + s.substring(end) 59 | } 60 | -------------------------------------------------------------------------------- /eslint-plugin-split-classnames/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "checkJs": false, 6 | "allowJs": false, 7 | "outDir": "dist" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "test": "FORCE_COLOR=1 pnpm -r test", 6 | "watch": "pnpm -r watch", 7 | "build": "pnpm -r build", 8 | "play": "esno eslint-plugin-split-classnames/src/cli", 9 | "release": "pnpm build && changeset" 10 | }, 11 | "devDependencies": { 12 | "@changesets/cli": "^2.22.0", 13 | "@types/node": "14.17.27", 14 | "esbuild": "^0.13.12", 15 | "esno": "^0.12.1", 16 | "prettier": "^2.4.1", 17 | "typescript": "^4.4.4", 18 | "ultra-runner": "^3.10.5", 19 | "vite": "^2.9.9", 20 | "vitest": "^0.12.9" 21 | }, 22 | "author": "remorses ", 23 | "license": "" 24 | } 25 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - website 3 | - eslint-plugin-split-classnames 4 | - vscode-extension 5 | - split-classnames 6 | -------------------------------------------------------------------------------- /split-classnames/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # split-classnames 2 | 3 | ## 0.3.0 4 | 5 | ### Minor Changes 6 | 7 | - Add version in cli 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - eslint-plugin-split-classnames@0.3.0 13 | 14 | ## 0.2.0 15 | 16 | ### Minor Changes 17 | 18 | - Sorting is now deterministic 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies 23 | - eslint-plugin-split-classnames@0.2.0 24 | 25 | ## 0.1.1 26 | 27 | ### Patch Changes 28 | 29 | - Do not report on already organized classnames calls 30 | - Updated dependencies 31 | - eslint-plugin-split-classnames@0.1.1 32 | 33 | ## 0.1.0 34 | 35 | ### Minor Changes 36 | 37 | - Split cs calls 38 | 39 | ### Patch Changes 40 | 41 | - Updated dependencies 42 | - eslint-plugin-split-classnames@0.1.0 43 | -------------------------------------------------------------------------------- /split-classnames/__snapshots__/rule.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`test eslint > alreadySplitted > fixed 1`] = ` 4 | "import { Fragment } from 'react'; 5 | import cs from 'classnames'; 6 | function Component() { 7 | return ( 8 | 9 |

17 | 18 | ); 19 | } 20 | " 21 | `; 22 | 23 | exports[`test eslint > callArgumentTooLong > fixed 1`] = ` 24 | "import { Fragment } from 'react'; 25 | import cs from 'classnames'; 26 | function Component() { 27 | return ( 28 | 29 |

36 | 37 | ); 38 | } 39 | " 40 | `; 41 | 42 | exports[`test eslint > regression1 > fixed 1`] = ` 43 | "import cs from 'classnames'; 44 | function Component() { 45 | return ( 46 | 47 |

56 | 57 | ); 58 | } 59 | " 60 | `; 61 | 62 | exports[`test eslint > regression2 > fixed 1`] = ` 63 | "import cs from 'classnames'; 64 | function Component() { 65 | return ( 66 | 67 |

73 | 74 | ); 75 | } 76 | " 77 | `; 78 | 79 | exports[`test eslint > shouldNotRun > fixed 1`] = ` 80 | "import cs from 'classnames'; 81 | function Component() { 82 | return ( 83 | 84 |

92 | 93 | ); 94 | } 95 | " 96 | `; 97 | 98 | exports[`test eslint > simple > fixed 1`] = ` 99 | "import classNames from 'classnames'; 100 | 101 | function Component() { 102 | return ( 103 | 104 |

113 | 114 | ); 115 | } 116 | " 117 | `; 118 | 119 | exports[`test eslint > withCsImport > fixed 1`] = ` 120 | "import { Fragment } from 'react'; 121 | import cs from 'classnames'; 122 | function Component() { 123 | return ( 124 | 125 |

131 | 132 | ); 133 | } 134 | " 135 | `; 136 | 137 | exports[`test eslint > withImports > fixed 1`] = ` 138 | "import classNames from 'classnames'; 139 | 140 | import { Fragment } from 'react'; 141 | function Component() { 142 | return ( 143 | 144 |

153 | 154 | ); 155 | } 156 | " 157 | `; 158 | 159 | exports[`test eslint > withManyClassnames > fixed 1`] = ` 160 | "import classNames from 'classnames'; 161 | 162 | function Component() { 163 | return ( 164 | 165 |

174 |

183 | 184 | ); 185 | } 186 | " 187 | `; 188 | 189 | exports[`test eslint > withTemplateLiteral > fixed 1`] = ` 190 | "import classNames from 'classnames'; 191 | 192 | import { Fragment } from 'react'; 193 | function Component() { 194 | return ( 195 | 196 |

203 | 204 | ); 205 | } 206 | " 207 | `; 208 | 209 | exports[`test eslint > withTypescript > fixed 1`] = ` 210 | "import classNames from 'classnames'; 211 | 212 | function Component(x: SomeType) { 213 | return ( 214 | 215 |

224 | 225 | ); 226 | } 227 | " 228 | `; 229 | -------------------------------------------------------------------------------- /split-classnames/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./dist/cli.js') 3 | -------------------------------------------------------------------------------- /split-classnames/example/input.jsx: -------------------------------------------------------------------------------- 1 | function Component() { 2 | return ( 3 | 4 |

5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /split-classnames/example/output.jsx: -------------------------------------------------------------------------------- 1 | import clsx from 'classnames' 2 | function Component() { 3 | return ( 4 | 5 |

13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /split-classnames/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split-classnames", 3 | "version": "0.3.0", 4 | "description": "Split classnames in your jsx files to make them shorter and more readable", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "remorses/split-classnames", 8 | "bin": "bin.js", 9 | "scripts": { 10 | "build": "tsc", 11 | "test": "NODE_ENV=test vitest --run", 12 | "play": "./bin.js 'example/output**'", 13 | "watch": "tsc -w" 14 | }, 15 | "files": [ 16 | "bin.js", 17 | "dist", 18 | "src" 19 | ], 20 | "keywords": [ 21 | "eslint", 22 | "eslintplugin", 23 | "classname" 24 | ], 25 | "author": "Tommaso De Rossi, morse ", 26 | "license": "ISC", 27 | "devDependencies": { 28 | "@types/eslint": "^8.2.0", 29 | "@types/prettier": "^2.6.3", 30 | "estree-jsx": "^0.0.1" 31 | }, 32 | "dependencies": { 33 | "@typescript-eslint/parser": "^5.16.0", 34 | "cac": "^6.7.12", 35 | "eslint": "^8.3.0", 36 | "eslint-plugin-split-classnames": "*", 37 | "smart-glob": "^1.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /split-classnames/rule.test.ts: -------------------------------------------------------------------------------- 1 | import { ESLint, RuleTester } from 'eslint' 2 | import { runRule } from 'eslint-plugin-split-classnames/src/utils' 3 | import { test, describe, expect } from 'vitest' 4 | 5 | const tests = { 6 | simple: ` 7 | function Component() { 8 | return ( 9 | 10 |

13 | 14 | ) 15 | } 16 | `, 17 | withTypescript: ` 18 | function Component(x: SomeType) { 19 | return ( 20 | 21 |

24 | 25 | ) 26 | } 27 | `, 28 | withManyClassnames: ` 29 | function Component() { 30 | return ( 31 | 32 |

33 |

34 | 35 | ) 36 | } 37 | 38 | `, 39 | withImports: ` 40 | import { Fragment } from 'react' 41 | function Component() { 42 | return ( 43 | 44 |

47 | 48 | ) 49 | } 50 | `, 51 | withTemplateLiteral: ` 52 | import { Fragment } from 'react' 53 | function Component() { 54 | return ( 55 | 56 |

59 | 60 | ) 61 | } 62 | `, 63 | callArgumentTooLong: ` 64 | import { Fragment } from 'react' 65 | import cs from 'classnames' 66 | function Component() { 67 | return ( 68 | 69 |

72 | 73 | ) 74 | } 75 | `, 76 | withCsImport: ` 77 | import { Fragment } from 'react' 78 | import cs from 'classnames' 79 | function Component() { 80 | return ( 81 | 82 |

85 | 86 | ) 87 | } 88 | `, 89 | alreadySplitted: ` 90 | import { Fragment } from 'react' 91 | import cs from 'classnames' 92 | function Component() { 93 | return ( 94 | 95 |

104 | 105 | ) 106 | } 107 | `, 108 | shouldNotRun: ` 109 | import cs from 'classnames'; 110 | function Component() { 111 | return ( 112 | 113 |

120 | 121 | ); 122 | } 123 | `, 124 | regression1: ` 125 | import cs from 'classnames'; 126 | function Component() { 127 | return ( 128 | 129 |

137 | 138 | ); 139 | } 140 | `, 141 | regression2: ` 142 | import cs from 'classnames'; 143 | function Component() { 144 | return ( 145 | 146 |

152 | 153 | ); 154 | } 155 | `, 156 | } 157 | 158 | import prettier from 'prettier' 159 | 160 | describe('test eslint', () => { 161 | for (let testName in tests) { 162 | test(`${testName}`, async () => { 163 | const code = tests[testName] 164 | // console.log('code', code) 165 | let fixedCode = await runRule(code) 166 | let prettyFixedCode = 167 | fixedCode && 168 | prettier.format(fixedCode, { 169 | singleQuote: true, 170 | parser: 'babel', 171 | }) 172 | // console.log(fixedCode) 173 | 174 | expect(prettyFixedCode).toMatchSnapshot('fixed') 175 | let fixedCodeAgain = (await runRule(fixedCode)) || fixedCode || code 176 | expect(fixedCodeAgain.trim()).toBe(fixedCode.trim()) 177 | }) 178 | } 179 | }) 180 | -------------------------------------------------------------------------------- /split-classnames/src/cli.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { glob as globFn } from 'smart-glob' 4 | import { runRule } from 'eslint-plugin-split-classnames/dist/utils' 5 | import { Opts } from 'eslint-plugin-split-classnames/dist/rule' 6 | 7 | import cac from 'cac' 8 | 9 | const cli = cac(require('../package.json').name) 10 | 11 | cli.version(require('../package.json').version) 12 | 13 | 14 | const allowedExtensions = ['.ts', '.tsx', '.js', '.jsx'] 15 | 16 | export async function runCodemod({ glob, opts = {} as Opts, dryRun = false }) { 17 | const files = await globFn(glob, { 18 | absolute: true, 19 | gitignore: true, 20 | 21 | ignoreGlobs: ['**/node_modules/**'], 22 | }) 23 | 24 | const results: string[] = [] 25 | for (let file of files) { 26 | let source = (await fs.promises.readFile(file)).toString() 27 | if (file.endsWith('.d.ts')) { 28 | continue 29 | } 30 | if (!allowedExtensions.some((ext) => file.endsWith(ext))) { 31 | continue 32 | } 33 | const ext = path.extname(file) 34 | console.info(`=> ${dryRun ? 'Found' : 'Applying to'} [${file}]`) 35 | source = (await runRule(source, opts)) || source 36 | results.push(source) 37 | if (!dryRun) { 38 | await fs.promises.writeFile(file, source, { encoding: 'utf-8' }) 39 | } 40 | } 41 | return results 42 | } 43 | 44 | cli.command('[glob]', 'Split long classnames') 45 | .option('--dry', 'Only show what files would be changed', { 46 | type: ['boolean'], 47 | }) 48 | .option('--max', 'Max number of characters in a classname') 49 | .action((glob, args) => { 50 | // console.log(args) 51 | 52 | if (!glob) { 53 | console.error('missing required positional argument glob') 54 | process.exit(1) 55 | } 56 | runCodemod({ 57 | glob, 58 | opts: { maxClassNameCharacters: args.max || undefined }, 59 | dryRun: args.dry, 60 | }) 61 | }) 62 | 63 | cli.help() 64 | cli.parse() 65 | 66 | // console.log(JSON.stringify(argv, null, 2)) 67 | -------------------------------------------------------------------------------- /split-classnames/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "checkJs": false, 6 | "allowJs": false, 7 | "outDir": "dist" 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "moduleResolution": "Node", 7 | "lib": [ 8 | "es2017", 9 | "es7", 10 | "es6" 11 | // "dom" 12 | ], 13 | "declaration": true, 14 | "declarationMap": true, 15 | "strict": true, 16 | "esModuleInterop": true, 17 | "noImplicitAny": false, 18 | "sourceMap": true, 19 | "downlevelIteration": true, 20 | "jsx": "react", 21 | "skipLibCheck": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vscode-extension/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /vscode-extension/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}", 14 | "${workspaceFolder}/../codemod" 15 | ], 16 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 17 | }, 18 | { 19 | "name": "Extension Tests", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 25 | ], 26 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 27 | "preLaunchTask": "${defaultBuildTask}" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /vscode-extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /vscode-extension/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /vscode-extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | node_modules/** 12 | node_modules 13 | scripts/** -------------------------------------------------------------------------------- /vscode-extension/.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /vscode-extension/README.md: -------------------------------------------------------------------------------- 1 | ## Work in progress, probably will never be released because does not make any sense 2 | -------------------------------------------------------------------------------- /vscode-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-extension", 3 | "private": true, 4 | "displayName": "vscode-extension", 5 | "description": "", 6 | "version": "0.0.1", 7 | "engines": { 8 | "vscode": "^1.61.0" 9 | }, 10 | "publisher": "xmorse", 11 | "categories": [ 12 | "Other" 13 | ], 14 | "activationEvents": [ 15 | "*", 16 | "onCommand:vscode-extension.splitClassNames", 17 | "onCommand:vscode-extension.inputLicenseKey" 18 | ], 19 | "main": "./dist/extension.js", 20 | "contributes": { 21 | "commands": [ 22 | { 23 | "command": "vscode-extension.splitClassNames", 24 | "title": "Split long classNames" 25 | }, 26 | { 27 | "command": "vscode-extension.inputLicenseKey", 28 | "title": "Enter split-classnames license key" 29 | } 30 | ], 31 | "configuration": [ 32 | { 33 | "title": "classNames splitter", 34 | "properties": { 35 | "vscode-extension.runOnSave": { 36 | "type": "boolean", 37 | "items": { 38 | "type": "string" 39 | }, 40 | "default": false, 41 | "description": "Run on save" 42 | } 43 | } 44 | } 45 | ], 46 | "keybindings": [ 47 | { 48 | "key": "", 49 | "command": "vscode-extension.splitClassNames" 50 | } 51 | ] 52 | }, 53 | "scripts": { 54 | "vscode:prepublish": "pnpm run build", 55 | "build": "esno scripts/build", 56 | "watch": "WATCH=1 esno scripts/build", 57 | "play": "code --extensionDevelopmentPath=`pwd` -n `pwd`/../codemod", 58 | "pretest": "pnpm run build" 59 | }, 60 | "devDependencies": { 61 | "@types/glob": "^7.1.4", 62 | "@types/mocha": "^9.0.0", 63 | "@types/node": "14.17.27", 64 | "@types/vscode": "^1.61.0", 65 | "@vscode/test-electron": "^1.6.2", 66 | "glob": "^7.1.7", 67 | "mocha": "^9.1.3", 68 | "conf": "^10.0.3", 69 | "node-fetch": "^3.0.0", 70 | "typescript": "^4.4.4" 71 | }, 72 | "dependencies": {} 73 | } 74 | -------------------------------------------------------------------------------- /vscode-extension/scripts/build.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import { build } from 'esbuild' 4 | import fs from 'fs' 5 | 6 | async function main() { 7 | const result = await build({ 8 | entryPoints: ['src/extension.ts'], 9 | bundle: true, 10 | // splitting: true, 11 | format: 'cjs', 12 | watch: Boolean(process.env.WATCH), 13 | external: ['vscode', 'flow-parser'], 14 | plugins: [ 15 | { 16 | name: 'flow-parser', 17 | setup(build) { 18 | build.onResolve({ filter: /^flow-parser$/ }, (args) => ({ 19 | path: args.path, 20 | namespace: 'flow-parser', 21 | })) 22 | 23 | build.onLoad( 24 | { filter: /.*/, namespace: 'flow-parser' }, 25 | () => ({ 26 | contents: `module.exports = {parser: () => ''};`, 27 | loader: 'js', 28 | }), 29 | ) 30 | }, 31 | }, 32 | ], 33 | platform: 'node', 34 | target: 'node12', 35 | metafile: true, 36 | sourcemap: true, 37 | outfile: 'dist/extension.js', 38 | }) 39 | require('fs').writeFileSync( 40 | 'dist/meta.json', 41 | JSON.stringify(result.metafile), 42 | ) 43 | } 44 | 45 | main() 46 | -------------------------------------------------------------------------------- /vscode-extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import Conf from 'conf' 3 | import fetch from 'node-fetch' 4 | 5 | const config = vscode.workspace.getConfiguration() 6 | 7 | const SPLIT_CLASSNAMES_COMMAND = 'vscode-extension.splitClassNames' 8 | 9 | const allowedLanguageIds = new Set([ 10 | 'typescript', 11 | 'typescriptreact', 12 | 'javascript', 13 | 'javascriptreact', 14 | ]) 15 | 16 | const GUMROAD_PERMALINK = 'nNrvI' 17 | 18 | async function validateLicenseKey(key: string) { 19 | const response = await fetch(`https://api.gumroad.com/v2/licenses/verify`, { 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | Accept: 'application/json', 23 | }, 24 | body: JSON.stringify({ 25 | product_permalink: GUMROAD_PERMALINK, 26 | license_key: key, 27 | }), 28 | method: 'POST', 29 | }) 30 | 31 | const data: any = await response.json() 32 | if (data?.success) { 33 | return true 34 | } else { 35 | return false 36 | } 37 | } 38 | 39 | async function promptLicenseKey(extensionConfig: Conf, prompt: string) { 40 | const res = await vscode.window.showInputBox({ 41 | ignoreFocusOut: true, 42 | password: false, 43 | placeHolder: '00000000-00000000-00000000-00000000', 44 | prompt, 45 | }) 46 | const valid = await validateLicenseKey(res || '') 47 | if (valid) { 48 | extensionConfig.set('licenseKey', res) 49 | vscode.window.showInformationMessage( 50 | 'License key for split-classnames saved!', 51 | ) 52 | return true 53 | } else { 54 | vscode.window.showErrorMessage( 55 | 'Invalid license key for split-classnames', 56 | ) 57 | return false 58 | } 59 | } 60 | 61 | export function activate(context: vscode.ExtensionContext) { 62 | console.log('Extension active') 63 | const extensionConfig = new Conf({ projectName: 'vscode-split-classnames' }) 64 | const licenseKey = extensionConfig.get('licenseKey') as any 65 | let validLicense = true 66 | Promise.resolve().then(async function validateOnStartup() { 67 | if (!licenseKey) { 68 | const success = await promptLicenseKey( 69 | extensionConfig, 70 | 'Enter the Gumroad license key for vscode-split-classnames to use the extension', 71 | ) 72 | if (!success) { 73 | validLicense = false 74 | } 75 | } else { 76 | const valid = await validateLicenseKey(licenseKey) 77 | if (!valid) { 78 | const success = await promptLicenseKey( 79 | extensionConfig, 80 | 'Saved license key is invalid, please enter new license key for split-sclassnames', 81 | ) 82 | if (!success) { 83 | validLicense = false 84 | } 85 | } 86 | } 87 | }) 88 | 89 | context.subscriptions.push( 90 | vscode.commands.registerTextEditorCommand( 91 | SPLIT_CLASSNAMES_COMMAND, 92 | function (editor, edit) { 93 | if (!validLicense) { 94 | return 95 | } 96 | const editorText = editor.document.getText() 97 | const editorLangId = editor.document.languageId 98 | if (!allowedLanguageIds.has(editorLangId)) { 99 | vscode.window.showWarningMessage( 100 | `${editorLangId} is not supported by vscode-extension`, 101 | ) 102 | return 103 | } 104 | const range = new vscode.Range( 105 | editor.document.positionAt(0), 106 | editor.document.positionAt(editorText.length), 107 | ) 108 | try { 109 | const result = transformSource(editorText, {}) 110 | edit.replace(range, result) 111 | } catch (e: any) { 112 | vscode.window.showErrorMessage( 113 | `Error splitting classnames: ${e.message}`, 114 | ) 115 | } 116 | // TODO run prettier formatting if prettier config is found? Maybe try running prettier command from vscode? 117 | }, 118 | ), 119 | ) 120 | 121 | context.subscriptions.push( 122 | vscode.commands.registerCommand( 123 | 'vscode-extension.inputLicenseKey', 124 | () => { 125 | promptLicenseKey( 126 | extensionConfig, 127 | 'Enter the Gumroad license key for vscode-split-classnames', 128 | ) 129 | }, 130 | ), 131 | ) 132 | 133 | if (config.get('vscode-extension.runOnSave')) { 134 | context.subscriptions.push( 135 | vscode.workspace.onWillSaveTextDocument((_e) => { 136 | if (!validLicense) { 137 | return 138 | } 139 | if (!allowedLanguageIds.has(_e.document.languageId)) { 140 | return 141 | } 142 | vscode.commands.executeCommand(SPLIT_CLASSNAMES_COMMAND) 143 | }), 144 | ) 145 | } 146 | } 147 | 148 | export function deactivate() {} 149 | 150 | function transformSource(...args) { 151 | return '' 152 | } 153 | -------------------------------------------------------------------------------- /vscode-extension/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | // main(); 24 | -------------------------------------------------------------------------------- /vscode-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", ".vscode-test"] 9 | } 10 | -------------------------------------------------------------------------------- /website/.env.example: -------------------------------------------------------------------------------- 1 | GUMROAD_ACCESS_TOKEN= -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"], 3 | "plugins": ["split-classnames"], 4 | "rules": { 5 | "split-classnames/split-classnames": [ 6 | "error", 7 | { 8 | "maxClassNameCharacters": 30, 9 | "functionName": "classnames" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /website/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # website 2 | 3 | ## null 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - eslint-plugin-split-classnames@0.3.0 9 | - split-classnames@0.3.0 10 | 11 | ## null 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies 16 | - eslint-plugin-split-classnames@0.2.0 17 | - split-classnames@0.2.0 18 | 19 | ## null 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies 24 | - eslint-plugin-split-classnames@0.1.0 25 | - split-classnames@0.1.0 26 | 27 | ## null 28 | 29 | ### Patch Changes 30 | 31 | - Updated dependencies [undefined] 32 | - eslint-plugin-split-classnames@0.0.4 33 | 34 | ## null 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies [undefined] 39 | - eslint-plugin-split-classnames@0.0.3 40 | 41 | ## null 42 | 43 | ### Patch Changes 44 | 45 | - Updated dependencies [undefined] 46 | - eslint-plugin-split-classnames@0.0.2 47 | 48 | ## null 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies [undefined] 53 | - eslint-plugin-split-classnames@0.0.1 54 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remorses/split-classnames/99675bb804af3cea0965f0202f68a0adff67d8dc/website/README.md -------------------------------------------------------------------------------- /website/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Editor from 'react-simple-code-editor' 3 | import classNames from 'classnames' 4 | import { highlight, languages } from 'prismjs' 5 | import 'prismjs/components/prism-jsx' 6 | import 'prismjs/components/prism-tsx' 7 | // import 'prismjs/themes/prism.css' 8 | 9 | export function CodeEditor({ 10 | code, 11 | className = '', 12 | onChange = (x) => {}, 13 | readOnly = false, 14 | }) { 15 | function hl(code) { 16 | try { 17 | return highlight(code, languages.tsx, 'typescript') 18 | } catch (e) { 19 | return code 20 | } 21 | } 22 | return ( 23 |

24 | 31 | 32 | 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /website/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'classnames' 2 | import React, { ComponentPropsWithoutRef, ReactNode } from 'react' 3 | import { Link } from './Link' 4 | import NextLink from 'next/link' 5 | 6 | export type FooterProps = { 7 | columns?: { [k: string]: ReactNode[] } 8 | businessName?: string 9 | justifyAround?: boolean 10 | } & ComponentPropsWithoutRef<'div'> 11 | 12 | export function Footer({ 13 | className = '', 14 | columns = { 15 | Resources: [ 16 | 17 | Quick start 18 | , 19 | ], 20 | Company: [ 21 | 22 | Twitter 23 | , 24 | ], 25 | 'Who made this?': [ 26 | Twitter, 27 | Github, 28 | ], 29 | }, 30 | justifyAround = false, 31 | businessName = 'Notaku', 32 | ...rest 33 | }: FooterProps) { 34 | return ( 35 |
42 |
48 | {Object.keys(columns).map((k, i) => { 49 | return ( 50 |
54 |
55 | {k} 56 |
57 | {columns[k].map((x, i) => ( 58 |
59 | {x} 60 |
61 | ))} 62 |
63 | ) 64 | })} 65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /website/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link' 2 | import cs from 'classnames' 3 | import React, { useEffect, useMemo, useState } from 'react' 4 | import { useRouter } from 'next/dist/client/router' 5 | 6 | export const Link = ({ 7 | href, 8 | className, 9 | ...props 10 | }: React.ComponentPropsWithoutRef<'a'>) => { 11 | const isExternal = href.startsWith('http') 12 | const { activeClass, eventHandlers } = useActiveClass({ 13 | className: '!opacity-40', 14 | time: 400, 15 | removeOnRouteComplete: !isExternal, 16 | }) 17 | 18 | if (isExternal) { 19 | return ( 20 | 27 | ) 28 | } 29 | 30 | return ( 31 | 32 | 37 | 38 | ) 39 | } 40 | 41 | export function useActiveClass({ 42 | className = '!opacity-100', 43 | time = 400, 44 | removeOnRouteComplete = false, 45 | }) { 46 | const [activeClass, setActiveClass] = useState('') 47 | function onActive() { 48 | if (!className) { 49 | return 50 | } 51 | setActiveClass(className) 52 | if (!removeOnRouteComplete) { 53 | setTimeout(() => { 54 | setActiveClass('') 55 | }, time) 56 | } 57 | } 58 | const router = useRouter() 59 | useEffect(() => { 60 | if (!removeOnRouteComplete) { 61 | return 62 | } 63 | function onComplete() { 64 | setActiveClass('') 65 | } 66 | router.events.on('routeChangeComplete', onComplete) 67 | return () => { 68 | router.events.off('routeChangeComplete', onComplete) 69 | } 70 | }, []) 71 | return { 72 | activeClass, 73 | eventHandlers: { onPointerDown: onActive, onTouchStart: onActive }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | const withTM = require('next-transpile-modules')([]) 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const config = { 5 | webpack(config) { 6 | config.externals = config.externals.concat([ 7 | '_http_common', 8 | 'flow-parser', 9 | ]) 10 | return config 11 | }, 12 | } 13 | 14 | module.exports = withTM(config) 15 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "website", 4 | "scripts": { 5 | "dev": "next dev", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "classnames": "^2.3.1", 10 | "eslint-plugin-split-classnames": "workspace:^0.3.0", 11 | "split-classnames": "workspace:^0.3.0", 12 | "eslint-config-next": "^12.0.4", 13 | "lodash": "^4.17.21", 14 | "next": "latest", 15 | "node-fetch": "^3.0.0", 16 | "prismjs": "^1.25.0", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-simple-code-editor": "^0.11.0" 20 | }, 21 | "devDependencies": { 22 | "@types/prismjs": "^1.16.6", 23 | "@types/react": "^17.0.33", 24 | "autoprefixer": "^10.2.6", 25 | "eslint": "<8.0.0", 26 | "next-transpile-modules": "^9.0.0", 27 | "postcss": "^8.3.5", 28 | "tailwindcss": "^2.2.4" 29 | }, 30 | "version": null 31 | } 32 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'tailwindcss/tailwind.css' 2 | import '../styles.css' 3 | import '../prism-theme.css' 4 | 5 | function MyApp({ Component, pageProps }) { 6 | return 7 | } 8 | 9 | export default MyApp 10 | -------------------------------------------------------------------------------- /website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import throttle from 'lodash/throttle' 3 | import { runRule as ssrTransformSource } from 'eslint-plugin-split-classnames/dist/utils' 4 | import React, { useEffect, useMemo, useRef, useState } from 'react' 5 | import Script from 'next/script' 6 | import gradientBg from '../public/bg_gradient.svg' 7 | import { Link } from '@app/components/Link' 8 | import NextLink from 'next/link' 9 | import clsx from 'classnames' 10 | import { Footer } from '@app/components/Footer' 11 | import { CodeEditor } from '@app/components/CodeEditor' 12 | import { GetStaticProps } from 'next' 13 | console.log(gradientBg.src) 14 | 15 | export default function Home({ transformedCode }) { 16 | return ( 17 |
18 | 19 | 20 | 21 |
22 |
28 | 29 | 30 |
31 |
36 |
37 |
38 | ) 39 | } 40 | 41 | export const getStaticProps: GetStaticProps = async function getStaticProps() { 42 | try { 43 | const transformedCode = await ssrTransformSource(CODE_BEFORE, {}) 44 | return { 45 | props: { 46 | transformedCode, 47 | }, 48 | } 49 | } catch (e) { 50 | return { 51 | props: { 52 | transformedCode: '', 53 | }, 54 | } 55 | } 56 | } 57 | 58 | export function WavesBg({ top = 700, className = '' }) { 59 | return ( 60 |
73 | ) 74 | } 75 | 76 | function GumroadCheckout() { 77 | return ( 78 |
79 |
80 | Loading... 81 |
82 |
83 | ) 84 | } 85 | 86 | function GumroadButton({ className = '' }) { 87 | return ( 88 |
89 | 90 | Buy License Key 91 | 92 |
93 | ) 94 | } 95 | 96 | function Logo({}) { 97 | return ( 98 |
99 | 107 | 108 | 109 |
Flowrift
110 |
111 | ) 112 | } 113 | 114 | function Header({ navs }) { 115 | return ( 116 |
117 | {/* logo - start */} 118 | 123 | 124 | 125 |
126 | 137 | 138 | {/* */} 139 | 157 | {/* buttons - end */} 158 |
159 | ) 160 | } 161 | 162 | function Hero() { 163 | return ( 164 |
165 |

166 | Very proud to introduce 167 |

168 |

169 | Revolutionary way to build the web 170 |

171 |

172 | This is a section of some simple filler text, also known as 173 | placeholder text. It shares some characteristics of a real 174 | written text but is random. 175 |

176 |
177 | 178 |
179 |
180 | ) 181 | } 182 | 183 | function CodeComparison({ transformedCode }) { 184 | const [code, setCode] = useState(CODE_BEFORE) 185 | const [codeAfter, setCodeAfter] = useState(transformedCode) 186 | 187 | const transformSource = useRef() 188 | 189 | // useEffect(() => { 190 | // try { 191 | // import('eslint-plugin-split-classnames/dist/utils').then( 192 | // (m) => (transformSource.current = m.runRule), 193 | // ) 194 | // } catch (e) { 195 | // console.error('error importing transform', e) 196 | // } 197 | // }, []) 198 | function safeTransformSource(code = '') { 199 | try { 200 | if (!transformSource.current) { 201 | return code 202 | } 203 | const res = transformSource.current(code) 204 | return res 205 | } catch (e) { 206 | console.error('error transforming', e) 207 | return code 208 | } 209 | } 210 | const throttledEffect = throttle(() => { 211 | const res = safeTransformSource(code) 212 | // console.log({ res }) 213 | setCodeAfter(res) 214 | }, 400) 215 | useEffect(throttledEffect, [code]) 216 | 217 | return ( 218 |
219 |
220 | setCode(x)} code={code} /> 221 | 225 |
226 | 227 |
228 | ) 229 | } 230 | 231 | const CODE_BEFORE = ` 232 | 233 | function Hero() { 234 | return ( 235 |
236 |

237 | Very proud to introduce 238 |

239 |
240 | ) 241 | } 242 | ` 243 | 244 | function Arrow({ className = '', ...rest }) { 245 | return ( 246 | 259 | 260 | 273 | 274 | 275 | ) 276 | } 277 | -------------------------------------------------------------------------------- /website/pages/success.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from '@app/components/Footer' 2 | import { Link } from '@app/components/Link' 3 | import { GetServerSidePropsContext } from 'next' 4 | import { useRouter } from 'next/dist/client/router' 5 | import React from 'react' 6 | import { WavesBg } from '.' 7 | import fetch from 'node-fetch' 8 | import { CodeEditor } from '@app/components/CodeEditor' 9 | 10 | export default function Success({ licenseKey }) { 11 | const router = useRouter() 12 | return ( 13 |
14 | 15 |
16 |
17 |

18 | Thank you for purchasing! 19 |

20 |

21 | You can now download the vscode extension{' '} 22 | 23 | here 24 | 25 |

26 |

27 | Your license key is:{' '} 28 | 29 | {licenseKey} 30 | 31 |

32 |

33 | A copy of your license key has also been sent to the 34 | email you used on Gumroad 35 |

36 |
37 |
38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | export async function getServerSideProps({ query }: GetServerSidePropsContext) { 45 | const saleId = (query?.sale_id || '') as string 46 | const licenseKey = await getLicenseKey(saleId) 47 | 48 | return { 49 | props: { 50 | licenseKey, 51 | }, 52 | } 53 | } 54 | 55 | async function getLicenseKey(saleId: string) { 56 | const accessToken = process.env.GUMROAD_ACCESS_TOKEN 57 | if (!accessToken) { 58 | throw new Error('Gumroad access token is not set') 59 | } 60 | 61 | const response = await fetch(`https://api.gumroad.com/v2/sales/${saleId}`, { 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | Accept: 'application/json', 65 | Authorization: `Bearer ${accessToken}`, 66 | }, 67 | // body: JSON.stringify({ 68 | // access_token: accessToken, 69 | // }), 70 | method: 'GET', 71 | }) 72 | 73 | const data: any = await response.json() 74 | if (data?.success) { 75 | return data?.sale?.license_key || '' 76 | } else { 77 | console.error(data) 78 | return '' 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /website/pages/webhook.tsx: -------------------------------------------------------------------------------- 1 | 2 | export interface NextApiRequestWithPaddle extends NextApiRequest { 3 | isSandbox: boolean 4 | } 5 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /website/prism-theme.css: -------------------------------------------------------------------------------- 1 | /** 2 | * prism.js Twilight theme 3 | * Based (more or less) on the Twilight theme originally of Textmate fame. 4 | * @author Remy Bach 5 | */ 6 | code[class*='language-'], 7 | pre[class*='language-'] { 8 | color: white; 9 | background: none; 10 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 11 | font-size: 1em; 12 | text-align: left; 13 | /* text-shadow: 0 -0.1em 0.2em black; */ 14 | white-space: pre; 15 | word-spacing: normal; 16 | word-break: normal; 17 | word-wrap: normal; 18 | line-height: 1.5; 19 | 20 | -moz-tab-size: 4; 21 | -o-tab-size: 4; 22 | tab-size: 4; 23 | 24 | -webkit-hyphens: none; 25 | -moz-hyphens: none; 26 | -ms-hyphens: none; 27 | hyphens: none; 28 | } 29 | 30 | pre[class*='language-']::-moz-selection { 31 | /* Firefox */ 32 | background: hsl(200, 4%, 16%); /* #282A2B */ 33 | } 34 | 35 | pre[class*='language-']::selection { 36 | /* Safari */ 37 | background: hsl(200, 4%, 16%); /* #282A2B */ 38 | } 39 | 40 | /* Text Selection colour */ 41 | pre[class*='language-']::-moz-selection, 42 | pre[class*='language-'] ::-moz-selection, 43 | code[class*='language-']::-moz-selection, 44 | code[class*='language-'] ::-moz-selection { 45 | text-shadow: none; 46 | background: hsla(0, 0%, 93%, 0.15); /* #EDEDED */ 47 | } 48 | 49 | pre[class*='language-']::selection, 50 | pre[class*='language-'] ::selection, 51 | code[class*='language-']::selection, 52 | code[class*='language-'] ::selection { 53 | text-shadow: none; 54 | background: hsla(0, 0%, 93%, 0.15); /* #EDEDED */ 55 | } 56 | 57 | /* Inline code */ 58 | :not(pre) > code[class*='language-'] { 59 | border-radius: 0.3em; 60 | padding: 0.15em 0.2em 0.05em; 61 | white-space: normal; 62 | } 63 | 64 | .token.comment, 65 | .token.prolog, 66 | .token.doctype, 67 | .token.cdata { 68 | color: hsl(0, 0%, 47%); /* #777777 */ 69 | } 70 | .dark .token.comment, 71 | .token.prolog, 72 | .token.doctype, 73 | .token.cdata { 74 | color: hsl(255, 0%, 47%); /* #777777 */ 75 | } 76 | 77 | .token.punctuation { 78 | opacity: 0.7; 79 | } 80 | 81 | .token.namespace { 82 | opacity: 0.7; 83 | } 84 | 85 | .token.tag, 86 | .token.boolean, 87 | .token.number, 88 | .token.deleted { 89 | color: hsl(14, 58%, 55%); /* #CF6A4C */ 90 | } 91 | 92 | .token.keyword, 93 | .token.property, 94 | .token.selector, 95 | .token.constant, 96 | .token.symbol, 97 | .token.builtin { 98 | color: hsl(53, 89%, 79%); /* #F9EE98 */ 99 | } 100 | 101 | .token.attr-name, 102 | .token.attr-value, 103 | .token.string, 104 | .token.char, 105 | .token.operator, 106 | .token.entity, 107 | .token.url, 108 | .language-css .token.string, 109 | .style .token.string, 110 | .token.variable, 111 | .token.inserted { 112 | color: hsl(76, 21%, 52%); /* #8F9D6A */ 113 | } 114 | 115 | .token.atrule { 116 | color: hsl(218, 22%, 55%); /* #7587A6 */ 117 | } 118 | 119 | .token.regex, 120 | .token.important { 121 | color: hsl(42, 75%, 65%); /* #E9C062 */ 122 | } 123 | 124 | .token.important, 125 | .token.bold { 126 | font-weight: bold; 127 | } 128 | .token.italic { 129 | font-style: italic; 130 | } 131 | 132 | .token.entity { 133 | cursor: help; 134 | } 135 | 136 | /* Markup */ 137 | .language-markup .token.tag, 138 | .language-markup .token.attr-name, 139 | .language-markup .token.punctuation { 140 | color: hsl(33, 33%, 52%); /* #AC885B */ 141 | } 142 | 143 | /* Make the tokens sit above the line highlight so the colours don't look faded. */ 144 | .token { 145 | position: relative; 146 | z-index: 1; 147 | } 148 | 149 | .line-highlight.line-highlight { 150 | background: hsla(0, 0%, 33%, 0.25); /* #545454 */ 151 | background: linear-gradient( 152 | to right, 153 | hsla(0, 0%, 33%, 0.1) 70%, 154 | hsla(0, 0%, 33%, 0) 155 | ); /* #545454 */ 156 | border-bottom: 1px dashed hsl(0, 0%, 33%); /* #545454 */ 157 | border-top: 1px dashed hsl(0, 0%, 33%); /* #545454 */ 158 | margin-top: 0.75em; /* Same as .prism’s padding-top */ 159 | z-index: 0; 160 | } 161 | 162 | .line-highlight.line-highlight:before, 163 | .line-highlight.line-highlight[data-end]:after { 164 | background-color: hsl(215, 15%, 59%); /* #8794A6 */ 165 | color: hsl(24, 20%, 95%); /* #F5F2F0 */ 166 | } 167 | -------------------------------------------------------------------------------- /website/public/bg_gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bg_gradient 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remorses/split-classnames/99675bb804af3cea0965f0202f68a0adff67d8dc/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /website/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --pageWidth: 1200px; 3 | --pagePadding: 20px; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | border-color: theme('colors.gray.200'); 9 | } 10 | /* do not zoom on ios select */ 11 | select { 12 | font-size: 16px; 13 | } 14 | .dark * { 15 | border-color: theme('colors.gray.800'); 16 | } 17 | 18 | /* emotion adds style tags between elements, do not display them */ 19 | style { 20 | display: none !important; 21 | margin-right: 0 !important; 22 | margin-left: 0 !important; 23 | margin-top: 0 !important; 24 | margin-bottom: 0 !important; 25 | } 26 | 27 | html { 28 | min-height: 100%; 29 | background-color: theme('colors.bg.800'); 30 | /* height: 100%; */ 31 | position: relative; 32 | overflow-x: hidden !important; 33 | scroll-behavior: smooth; 34 | color: theme('colors.gray.800'); 35 | touch-action: pan-x pan-y pinch-zoom !important; 36 | -webkit-tap-highlight-color: transparent !important; 37 | -webkit-touch-callout: none !important; 38 | } 39 | 40 | html.dark { 41 | background-color: theme('colors.gray.800'); 42 | color: theme('colors.gray.200'); 43 | color-scheme: dark; 44 | } 45 | 46 | #__next { 47 | min-height: 100vh !important; 48 | margin-right: calc(-1 * (100vw - 100%)); 49 | } 50 | body { 51 | min-height: 100%; 52 | position: relative; 53 | scroll-behavior: smooth; 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | text-rendering: optimizeLegibility; 57 | } 58 | 59 | @keyframes fadeDown { 60 | from { 61 | opacity: 0; 62 | transform: translate3d(0px, -2em, 0px); 63 | } 64 | to { 65 | transform: none; 66 | } 67 | } 68 | 69 | .icon { 70 | width: 20px; 71 | height: 20px; 72 | } 73 | 74 | img { 75 | display: block !important; 76 | } -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss/tailwind-config').TailwindConfig} */ 2 | module.exports = { 3 | mode: 'jit', 4 | purge: [ 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | darkMode: false, // or 'media' or 'class' 9 | theme: { 10 | extend: { 11 | colors: { 12 | primary: '#00bcd4', 13 | bg: { 14 | 800: '#1F1144', 15 | }, 16 | }, 17 | }, 18 | }, 19 | variants: { 20 | extend: {}, 21 | }, 22 | plugins: [], 23 | } 24 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "baseUrl": "./", 18 | "paths": { 19 | "@app/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | --------------------------------------------------------------------------------