diff --git a/.changeset/brown-rocks-wonder.md b/.changeset/brown-rocks-wonder.md new file mode 100644 index 00000000..6be643f2 --- /dev/null +++ b/.changeset/brown-rocks-wonder.md @@ -0,0 +1,5 @@ +--- +'@soramitsu-ui/ui': patch +--- + +**refactor**(`SButton`): refactor BEM usage diff --git a/.changeset/dirty-peaches-cry.md b/.changeset/dirty-peaches-cry.md new file mode 100644 index 00000000..a9dcbd5c --- /dev/null +++ b/.changeset/dirty-peaches-cry.md @@ -0,0 +1,5 @@ +--- +'@soramitsu-ui/ui': patch +--- + +**refactor**: move `type-fest` to prod dependencies and update it to `3.1.0` diff --git a/.changeset/shaggy-donkeys-swim.md b/.changeset/shaggy-donkeys-swim.md new file mode 100644 index 00000000..4520739d --- /dev/null +++ b/.changeset/shaggy-donkeys-swim.md @@ -0,0 +1,5 @@ +--- +'@soramitsu-ui/ui': patch +--- + +**chore**: add `@soramitsu-ui/bem` to deps diff --git a/Jenkinsfile b/Jenkinsfile index ba040b31..d2442661 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,6 +5,7 @@ def pipeline = new org.js.LibPipeline( buildDockerImage: 'build-tools/node:14-ubuntu-cypress', npmLoginEmail: 'admin@soramitsu.co.jp', dockerImageName: 'soramitsu/soramitsu-js-ui-library', + preBuildCmds: ['npm install -g n','n 16.17.0', 'n prune', "yarn install"], testCmds: ['yarn test:all'], pushCmds: ['yarn publish-workspaces --no-verify-access'], libPushBranches: ['master', 'next'], diff --git a/package.json b/package.json index dfc93ea6..64eabdba 100755 --- a/package.json +++ b/package.json @@ -7,13 +7,15 @@ "scripts": { "sb:serve": "yarn --cwd packages/ui sb:serve", "sb:build": "yarn --cwd packages/ui sb:build", - "test:all": "run-s lint:check test:theme:unit build:vite-plugin-svg test:ui:unit build:theme test:ui:cy build:ui:only-vite test:ui:after-build", + "test:all": "run-s lint:check test:bem:unit test:theme:unit build:bem build:vite-plugin-svg test:ui:unit build:theme test:ui:cy build:ui:only-vite test:ui:after-build", "test:theme:unit": "yarn --cwd packages/theme test", + "test:bem:unit": "yarn --cwd packages/bem test", "test:ui:unit": "yarn --cwd packages/ui test:unit", "test:ui:cy": "yarn --cwd packages/ui cy:ci:component", "test:ui:after-build": "yarn --cwd packages/ui test:after-build", - "build": "run-s build:theme build:vite-plugin-svg build:ui", + "build": "run-s build:bem build:theme build:vite-plugin-svg build:ui", "build:theme": "yarn --cwd packages/theme build", + "build:bem": "yarn --cwd packages/bem build", "build:vite-plugin-svg": "yarn --cwd packages/vite-plugin-svg build", "build:ui": "yarn --cwd packages/ui build", "build:ui:only-vite": "yarn --cwd packages/ui build:vite", @@ -28,19 +30,19 @@ "devDependencies": { "@changesets/cli": "^2.17.0", "@types/node": "^17.0.14", - "@typescript-eslint/eslint-plugin": "^5.12.1", - "@typescript-eslint/parser": "^5.12.1", + "@typescript-eslint/eslint-plugin": "^5.40.1", + "@typescript-eslint/parser": "^5.40.1", "esbuild-jest": "^0.5.0", - "eslint": "^8.16.0", - "eslint-config-alloy": "^4.5.1", + "eslint": "^8.25.0", + "eslint-config-alloy": "^4.7.0", "eslint-plugin-cypress": "^2.12.1", - "eslint-plugin-vue": "^9.0.1", - "eslint-plugin-vuejs-accessibility": "^1.1.1", + "eslint-plugin-vue": "^9.6.0", + "eslint-plugin-vuejs-accessibility": "^1.2.0", "lerna": "^4.0.0", "npm-run-all": "^4.1.5", - "prettier": "^2.6.2", - "prettier-eslint": "^15.0.0", + "prettier": "^2.7.1", + "prettier-eslint": "^15.0.1", "prettier-eslint-cli": "^6.0.1", - "typescript": "4.6.4" + "typescript": "4.8.4" } } diff --git a/packages/bem/README.md b/packages/bem/README.md new file mode 100644 index 00000000..7c555f16 --- /dev/null +++ b/packages/bem/README.md @@ -0,0 +1,154 @@ +# @soramitsu-ui/bem + +Type-level [BEM](https://en.bem.info/methodology/naming-convention/) notation. + +## Features + +- Statically typed BEM schema +- Less boilerplate - no need to repeat root block name +- Less possibility to make a typo +- Support of classic BEM style (`block__elem_mod-name_mod-key`) and two-dashes (`block__elem--mod-name--mod-key`) + +## Example + +### Classic style + +```ts +const bem = defineBem('v-btn') + // Block modifiers + .mod('loading') + .mod('show-icon') + .mod('icon-size', ['small', 'very-small'] as const) + .mod('icon-size', 'little') + // Block elements + .elem('spinner') + .elem('left-icon', (el) => + el + // Element modifiers + .mod('active') + .mod('has-stroke') + .mod('right-span', ['big', 'very-big'] as const) + .mod('right-span', 'huge'), + ) + .build() + +type test = Expect< + Equal< + typeof bem, + { + _: 'v-btn' + '_icon-size_little': 'v-btn_icon-size_little' + '_icon-size_small': 'v-btn_icon-size_small' + '_icon-size_very-small': 'v-btn_icon-size_very-small' + _iconSize_little: 'v-btn_icon-size_little' + _iconSize_small: 'v-btn_icon-size_small' + _iconSize_verySmall: 'v-btn_icon-size_very-small' + _loading: 'v-btn_loading' + '_show-icon': 'v-btn_show-icon' + _showIcon: 'v-btn_show-icon' + 'left-icon': 'v-btn__left-icon' + 'left-icon_active': 'v-btn__left-icon_active' + 'left-icon_has-stroke': 'v-btn__left-icon_has-stroke' + 'left-icon_right-span_big': 'v-btn__left-icon_right-span_big' + 'left-icon_right-span_huge': 'v-btn__left-icon_right-span_huge' + 'left-icon_right-span_very-big': 'v-btn__left-icon_right-span_very-big' + leftIcon: 'v-btn__left-icon' + leftIcon_active: 'v-btn__left-icon_active' + leftIcon_hasStroke: 'v-btn__left-icon_has-stroke' + leftIcon_rightSpan_big: 'v-btn__left-icon_right-span_big' + leftIcon_rightSpan_huge: 'v-btn__left-icon_right-span_huge' + leftIcon_rightSpan_veryBig: 'v-btn__left-icon_right-span_very-big' + spinner: 'v-btn__spinner' + } + > +> +``` + +### Two-dashes style + +```ts +const bem = defineBem('v-btn') + .mod('loading') + .mod('show-icon') + .mod('icon-size', ['small', 'very-small'] as const) + .mod('icon-size', 'little') + .elem('spinner') + .elem('left-icon', (el) => + el + // + .mod('active') + .mod('has-stroke') + .mod('right-span', ['big', 'very-big'] as const) + .mod('right-span', 'huge'), + ) + .build('two-dashes') + +type test = Expect< + Equal< + typeof bem, + { + _: 'v-btn' + '_icon-size_little': 'v-btn--icon-size--little' + '_icon-size_small': 'v-btn--icon-size--small' + '_icon-size_very-small': 'v-btn--icon-size--very-small' + _iconSize_little: 'v-btn--icon-size--little' + _iconSize_small: 'v-btn--icon-size--small' + _iconSize_verySmall: 'v-btn--icon-size--very-small' + _loading: 'v-btn--loading' + '_show-icon': 'v-btn--show-icon' + _showIcon: 'v-btn--show-icon' + 'left-icon': 'v-btn__left-icon' + 'left-icon_active': 'v-btn__left-icon--active' + 'left-icon_has-stroke': 'v-btn__left-icon--has-stroke' + 'left-icon_right-span_big': 'v-btn__left-icon--right-span--big' + 'left-icon_right-span_huge': 'v-btn__left-icon--right-span--huge' + 'left-icon_right-span_very-big': 'v-btn__left-icon--right-span--very-big' + leftIcon: 'v-btn__left-icon' + leftIcon_active: 'v-btn__left-icon--active' + leftIcon_hasStroke: 'v-btn__left-icon--has-stroke' + leftIcon_rightSpan_big: 'v-btn__left-icon--right-span--big' + leftIcon_rightSpan_huge: 'v-btn__left-icon--right-span--huge' + leftIcon_rightSpan_veryBig: 'v-btn__left-icon--right-span--very-big' + spinner: 'v-btn__spinner' + } + > +> +``` + +## Install + +```bash +npm i @soramitsu-ui/bem +``` + +## Questions + +### Why keys are duplicated in both `camelCase` and `kebab-case`? + +`camelCase` is used to make access shorter: + +```ts +// shorter +bem.leftIcon_active + +// longer +bem['left-icon_active'] +``` + +However, in some cases we need access a key dynamically with some value: + +```ts +const bem = defineBem('tooltip') + .mod('position', ['left-bottom', 'right-top'] as const) + .build() + +function getClassByPosition(value: 'left-bottom' | 'right-top') { + return bem[`_position_${value}`] +} +``` + +Without preserving the initial case of `position` modifier, we wouldn't be able to implement `getClassByPosition()` so easy. + +### Well, mostly I don't need to preserve keys case. How to avoid extra generation? + +For now the implementation is naive and not focused on such an optimisation. However, it might be possible to be optimised with `Proxy` and lazy-keys-computation under the hood. But it is a question if it is possible to implement this without even bigger performance overhead. diff --git a/packages/bem/package.json b/packages/bem/package.json new file mode 100644 index 00000000..e9c1ae38 --- /dev/null +++ b/packages/bem/package.json @@ -0,0 +1,34 @@ +{ + "name": "@soramitsu-ui/bem", + "version": "1.0.0", + "description": "Type-level BEM notation", + "type": "module", + "exports": { + ".": { + "import": "./dist/lib.mjs", + "require": "./dist/lib.cjs", + "types": "./dist/lib.d.ts" + } + }, + "main": "./dist/lib.cjs", + "types": "./dist/lib.d.ts", + "files": [ + "dist" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "fast-case": "^1.7.0", + "type-fest": "^3.1.0" + }, + "devDependencies": { + "unbuild": "^0.9.4", + "vitest": "^0.24.3" + }, + "scripts": { + "build": "unbuild", + "test": "vitest run" + } +} diff --git a/packages/bem/src/lib.spec.ts b/packages/bem/src/lib.spec.ts new file mode 100644 index 00000000..2111c5a7 --- /dev/null +++ b/packages/bem/src/lib.spec.ts @@ -0,0 +1,181 @@ +import { test, expect, describe } from 'vitest' +import { defineBem } from './lib' +import { Expect, Equal } from './types' + +describe('defineBem', () => { + const complexBem = defineBem('v-btn') + // Block modifiers + .mod('loading') + .mod('show-icon') + .mod('icon-size', ['small', 'very-small'] as const) + .mod('icon-size', 'little') + // Block elements + .elem('spinner') + .elem('left-icon', (el) => + el + // Element modifiers + .mod('active') + .mod('has-stroke') + .mod('right-span', ['big', 'very-big'] as const) + .mod('right-span', 'huge'), + ) + + test('Only block', () => { + const bem = defineBem('s-table').build() + + type test = Expect> + + expect(bem).toMatchInlineSnapshot(` + { + "_": "s-table", + } + `) + }) + + test('Simple block', () => { + const bem = defineBem('block').elem('elem').build() + + type test = Expect< + Equal< + typeof bem, + { + _: 'block' + elem: 'block__elem' + } + > + > + + expect(bem).toMatchInlineSnapshot(` + { + "_": "block", + "elem": "block__elem", + } + `) + }) + + test('Complex classic build', () => { + const bem = complexBem.build() + + type test = Expect< + Equal< + typeof bem, + { + _: 'v-btn' + '_icon-size_little': 'v-btn_icon-size_little' + '_icon-size_small': 'v-btn_icon-size_small' + '_icon-size_very-small': 'v-btn_icon-size_very-small' + _iconSize_little: 'v-btn_icon-size_little' + _iconSize_small: 'v-btn_icon-size_small' + _iconSize_verySmall: 'v-btn_icon-size_very-small' + _loading: 'v-btn_loading' + '_show-icon': 'v-btn_show-icon' + _showIcon: 'v-btn_show-icon' + 'left-icon': 'v-btn__left-icon' + 'left-icon_active': 'v-btn__left-icon_active' + 'left-icon_has-stroke': 'v-btn__left-icon_has-stroke' + 'left-icon_right-span_big': 'v-btn__left-icon_right-span_big' + 'left-icon_right-span_huge': 'v-btn__left-icon_right-span_huge' + 'left-icon_right-span_very-big': 'v-btn__left-icon_right-span_very-big' + leftIcon: 'v-btn__left-icon' + leftIcon_active: 'v-btn__left-icon_active' + leftIcon_hasStroke: 'v-btn__left-icon_has-stroke' + leftIcon_rightSpan_big: 'v-btn__left-icon_right-span_big' + leftIcon_rightSpan_huge: 'v-btn__left-icon_right-span_huge' + leftIcon_rightSpan_veryBig: 'v-btn__left-icon_right-span_very-big' + spinner: 'v-btn__spinner' + } + > + > + + expect(bem).toMatchInlineSnapshot(` + { + "_": "v-btn", + "_icon-size_little": "v-btn_icon-size_little", + "_icon-size_small": "v-btn_icon-size_small", + "_icon-size_very-small": "v-btn_icon-size_very-small", + "_iconSize_little": "v-btn_icon-size_little", + "_iconSize_small": "v-btn_icon-size_small", + "_iconSize_verySmall": "v-btn_icon-size_very-small", + "_loading": "v-btn_loading", + "_show-icon": "v-btn_show-icon", + "_showIcon": "v-btn_show-icon", + "left-icon": "v-btn__left-icon", + "left-icon_active": "v-btn__left-icon_active", + "left-icon_has-stroke": "v-btn__left-icon_has-stroke", + "left-icon_right-span_big": "v-btn__left-icon_right-span_big", + "left-icon_right-span_huge": "v-btn__left-icon_right-span_huge", + "left-icon_right-span_very-big": "v-btn__left-icon_right-span_very-big", + "leftIcon": "v-btn__left-icon", + "leftIcon_active": "v-btn__left-icon_active", + "leftIcon_hasStroke": "v-btn__left-icon_has-stroke", + "leftIcon_rightSpan_big": "v-btn__left-icon_right-span_big", + "leftIcon_rightSpan_huge": "v-btn__left-icon_right-span_huge", + "leftIcon_rightSpan_veryBig": "v-btn__left-icon_right-span_very-big", + "spinner": "v-btn__spinner", + } + `) + }) + + test('Complex two-dashes build', () => { + const bem = complexBem.build('two-dashes') + + type test = Expect< + Equal< + typeof bem, + { + _: 'v-btn' + '_icon-size_little': 'v-btn--icon-size--little' + '_icon-size_small': 'v-btn--icon-size--small' + '_icon-size_very-small': 'v-btn--icon-size--very-small' + _iconSize_little: 'v-btn--icon-size--little' + _iconSize_small: 'v-btn--icon-size--small' + _iconSize_verySmall: 'v-btn--icon-size--very-small' + _loading: 'v-btn--loading' + '_show-icon': 'v-btn--show-icon' + _showIcon: 'v-btn--show-icon' + 'left-icon': 'v-btn__left-icon' + 'left-icon_active': 'v-btn__left-icon--active' + 'left-icon_has-stroke': 'v-btn__left-icon--has-stroke' + 'left-icon_right-span_big': 'v-btn__left-icon--right-span--big' + 'left-icon_right-span_huge': 'v-btn__left-icon--right-span--huge' + 'left-icon_right-span_very-big': 'v-btn__left-icon--right-span--very-big' + leftIcon: 'v-btn__left-icon' + leftIcon_active: 'v-btn__left-icon--active' + leftIcon_hasStroke: 'v-btn__left-icon--has-stroke' + leftIcon_rightSpan_big: 'v-btn__left-icon--right-span--big' + leftIcon_rightSpan_huge: 'v-btn__left-icon--right-span--huge' + leftIcon_rightSpan_veryBig: 'v-btn__left-icon--right-span--very-big' + spinner: 'v-btn__spinner' + } + > + > + + expect(bem).toMatchInlineSnapshot(` + { + "_": "v-btn", + "_icon-size_little": "v-btn--icon-size--little", + "_icon-size_small": "v-btn--icon-size--small", + "_icon-size_very-small": "v-btn--icon-size--very-small", + "_iconSize_little": "v-btn--icon-size--little", + "_iconSize_small": "v-btn--icon-size--small", + "_iconSize_verySmall": "v-btn--icon-size--very-small", + "_loading": "v-btn--loading", + "_show-icon": "v-btn--show-icon", + "_showIcon": "v-btn--show-icon", + "left-icon": "v-btn__left-icon", + "left-icon_active": "v-btn__left-icon--active", + "left-icon_has-stroke": "v-btn__left-icon--has-stroke", + "left-icon_right-span_big": "v-btn__left-icon--right-span--big", + "left-icon_right-span_huge": "v-btn__left-icon--right-span--huge", + "left-icon_right-span_very-big": "v-btn__left-icon--right-span--very-big", + "leftIcon": "v-btn__left-icon", + "leftIcon_active": "v-btn__left-icon--active", + "leftIcon_hasStroke": "v-btn__left-icon--has-stroke", + "leftIcon_rightSpan_big": "v-btn__left-icon--right-span--big", + "leftIcon_rightSpan_huge": "v-btn__left-icon--right-span--huge", + "leftIcon_rightSpan_veryBig": "v-btn__left-icon--right-span--very-big", + "spinner": "v-btn__spinner", + } + `) + }) +}) diff --git a/packages/bem/src/lib.ts b/packages/bem/src/lib.ts new file mode 100644 index 00000000..a32f9940 --- /dev/null +++ b/packages/bem/src/lib.ts @@ -0,0 +1,8 @@ +import type { BlockBuilder } from './types' +import { BemBlock } from './unchecked-builder' + +export const defineBem: (blockName: r) => BlockBuilder = (blockName) => { + return new BemBlock(blockName) as unknown as BlockBuilder +} + +export type { BlockBuilder, BemStyle } from './types' diff --git a/packages/bem/src/types.ts b/packages/bem/src/types.ts new file mode 100644 index 00000000..3e862cdd --- /dev/null +++ b/packages/bem/src/types.ts @@ -0,0 +1,281 @@ +import type { Simplify, CamelCase } from 'type-fest' + +export type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false + +export type NotEqual = Equal extends true ? false : true + +export type Expect = T + +export type BemStyle = 'classic' | 'two-dashes' + +type ApplyStyleToModifier