├── .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 | [](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 |
83 | )
84 | }
85 |
86 | function GumroadButton({ className = '' }) {
87 | return (
88 |
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 |
127 | {navs.map((nav, i) => (
128 |
133 | {nav.content}
134 |
135 | ))}
136 |
137 |
138 | {/* */}
139 |
143 |
149 |
154 |
155 | Menu
156 |
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 |
--------------------------------------------------------------------------------