├── .editorconfig ├── .github └── workflows │ ├── pubilsh.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .markdownlint.json ├── .prettierignore ├── .release-it.json ├── .vscode └── settings.json ├── assets ├── assets.sketch ├── onco.svg ├── onno-react.svg ├── onno-vue.svg ├── onno.svg ├── social-dark.png ├── social-light.png └── tailwindcss-vscode.png ├── changelog.md ├── license ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── readme.md ├── src ├── app.css ├── app.d.ts ├── app.html ├── codemirror │ ├── extensions.ts │ ├── themes.ts │ └── utils.ts ├── components │ ├── Editor.svelte │ ├── Footer.svelte │ ├── Head.svelte │ ├── Header.svelte │ └── Logo.svelte ├── examples │ └── basic.example.ts ├── lib │ ├── chars.ts │ ├── text.ts │ ├── types.ts │ └── utils.ts ├── onno │ ├── index.ts │ └── types.ts ├── routes │ ├── +layout.svelte │ └── +page.svelte └── stores │ ├── metadata.store.ts │ ├── pointer.store.ts │ ├── screen.store.ts │ └── theme.store.ts ├── static ├── favicon.svg ├── icons │ ├── icon-192x192.png │ ├── icon-384x384.png │ └── icon-512x 512.png └── manifest.json ├── svelte.config.js ├── tailwind.config.js ├── tests └── onno.spec.ts ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.github/workflows/pubilsh.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v3 14 | - name: Setup PNPM 15 | uses: pnpm/action-setup@v2 16 | with: 17 | version: 8 18 | - name: Install Node 19 | uses: actions/setup-node@v3 20 | with: 21 | cache: pnpm 22 | registry-url: https://registry.npmjs.org 23 | - name: Install Dependencies 24 | run: pnpm install 25 | - name: Build Package 26 | run: pnpm build 27 | # - name: Release Package 28 | # run: pnpm release 29 | # - name: Publish Package to NPM 30 | # run: npm publish 31 | # env: 32 | # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | contents: read 15 | pull-requests: write 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x] 20 | 21 | steps: 22 | - name: Checkout Repository 23 | uses: actions/checkout@v3 24 | - name: Setup PNPM 25 | uses: pnpm/action-setup@v2 26 | with: 27 | version: 8 28 | - name: Install Node ${{ matrix.node-version }} 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: pnpm 33 | - name: Install Dependencies 34 | run: pnpm install 35 | - name: Run Unit Tests 36 | run: pnpm test 37 | - name: Report Coverage 38 | uses: davelosert/vitest-coverage-report-action@v2 39 | if: always() # Also generate report if tests fail 40 | - name: Upload Coverage to Codecov 41 | uses: codecov/codecov-action@v3 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | 5 | .svelte 6 | .vercel 7 | 8 | .DS_Store 9 | .env* 10 | 11 | *.log 12 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD046": false 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | coverage 3 | .svelte 4 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "requireBranch": "main", 4 | "commitMessage": "chore: release v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": [ 8 | "git pull", 9 | "pnpm clean", 10 | "pnpm sync", 11 | "pnpm package", 12 | "pnpm lint", 13 | "pnpm test" 14 | ], 15 | "after:bump": "pnpm changelog" 16 | }, 17 | "github": { 18 | "release": true 19 | }, 20 | "npm": { 21 | "publish": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "testing.automaticallyOpenPeekView": "never", 4 | "typescript.tsdk": "node_modules/typescript/lib", 5 | "tailwindCSS.experimental.classRegex": [ 6 | ["onno\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /assets/assets.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/assets/assets.sketch -------------------------------------------------------------------------------- /assets/onco.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/onno-react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/onno-vue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/onno.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/social-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/assets/social-dark.png -------------------------------------------------------------------------------- /assets/social-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/assets/social-light.png -------------------------------------------------------------------------------- /assets/tailwindcss-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/assets/tailwindcss-vscode.png -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [2.2.0](https://github.com/wagerfield/onno/compare/2.1.2...2.2.0) 8 | 9 | - build: lint compiled code before releasing to npm [`37da38a`](https://github.com/wagerfield/onno/commit/37da38aad61b3d3ae5f2b4ed99027a01dd697c91) 10 | 11 | #### [2.1.2](https://github.com/wagerfield/onno/compare/2.1.1...2.1.2) 12 | 13 | > 9 August 2023 14 | 15 | - chore: add keywords [`22cd4ee`](https://github.com/wagerfield/onno/commit/22cd4ee5695e16a7e0c67779207a73f5e6ba43c2) 16 | - chore: release v2.1.2 [`5cdb05b`](https://github.com/wagerfield/onno/commit/5cdb05b9a064d1dc018b4622956c69415b9304da) 17 | - docs: intellisense case refinements [`03f2a87`](https://github.com/wagerfield/onno/commit/03f2a87d8e5f784fdeb715bf89ca56cf24155ac4) 18 | 19 | #### [2.1.1](https://github.com/wagerfield/onno/compare/2.1.0...2.1.1) 20 | 21 | > 9 August 2023 22 | 23 | - chore: release v2.1.1 [`53965fc`](https://github.com/wagerfield/onno/commit/53965fc8858c51cb5a828da697a79d150a7dd995) 24 | - docs: remove cache from bundle phobia and codecov shield links [`dd1d1a0`](https://github.com/wagerfield/onno/commit/dd1d1a04e239348d8df95fa7c83dd24dfb740242) 25 | 26 | #### [2.1.0](https://github.com/wagerfield/onno/compare/2.0.0...2.1.0) 27 | 28 | > 9 August 2023 29 | 30 | - chore: release v2.1.0 [`ad5b380`](https://github.com/wagerfield/onno/commit/ad5b380f3657861281074809f6b87eb6fb94e51c) 31 | - chore: add files to package.json [`c881da2`](https://github.com/wagerfield/onno/commit/c881da2bf02af6c547fb6d8107acfc2c2ca82d9b) 32 | - build: configure before release hook [`c68bf88`](https://github.com/wagerfield/onno/commit/c68bf8879caf3203229f541dffccb616489963e8) 33 | 34 | ### [2.0.0](https://github.com/wagerfield/onno/compare/2.0.0-2...2.0.0) 35 | 36 | > 9 August 2023 37 | 38 | - docs: update tailwind css section [`556933b`](https://github.com/wagerfield/onno/commit/556933b509cc11a6ea780f03782775f20ae54672) 39 | - chore: release v2.0.0 [`7351b90`](https://github.com/wagerfield/onno/commit/7351b90b081243f2d99257c0416b01deffbfc916) 40 | - docs: add import to first example [`99fbfa4`](https://github.com/wagerfield/onno/commit/99fbfa4270df2d5e32e262fedfc3ae185cf86eca) 41 | 42 | #### [2.0.0-2](https://github.com/wagerfield/onno/compare/2.0.0-1...2.0.0-2) 43 | 44 | > 7 August 2023 45 | 46 | - docs: fix confetti emoji [`2f4f3de`](https://github.com/wagerfield/onno/commit/2f4f3dede529c350e876bb1de0181673af55e86c) 47 | - docs: refine feature emojis [`129a54e`](https://github.com/wagerfield/onno/commit/129a54e6fae0a3730d16eb0faec860f70152c839) 48 | - docs: add shield caching [`f9dcf3a`](https://github.com/wagerfield/onno/commit/f9dcf3aa101ff2d3d7008604375cfd2c14b6884b) 49 | 50 | #### [2.0.0-1](https://github.com/wagerfield/onno/compare/2.0.0-0...2.0.0-1) 51 | 52 | > 7 August 2023 53 | 54 | - docs: add config documentation [`fa15487`](https://github.com/wagerfield/onno/commit/fa154874cf60d81febdca83ebaa344572d422f8f) 55 | - docs: add class composition section [`2f3cdc0`](https://github.com/wagerfield/onno/commit/2f3cdc0895c6ff9b2384239e0a7ad34f0d82cd34) 56 | - docs: write compound classes section [`589425e`](https://github.com/wagerfield/onno/commit/589425effd73b6a4697c11166ec98bd083ae3eef) 57 | 58 | #### [2.0.0-0](https://github.com/wagerfield/onno/compare/1.0.0...2.0.0-0) 59 | 60 | > 7 August 2023 61 | 62 | - ci: add npm publish workflow [`#469`](https://github.com/wagerfield/onno/pull/469) 63 | - feat: setup codecov [`#466`](https://github.com/wagerfield/onno/pull/466) 64 | - fix(deps): update dependency csstype to v3 [`#421`](https://github.com/wagerfield/onno/pull/421) 65 | - chore(deps): bump ini from 1.3.5 to 1.3.7 [`#433`](https://github.com/wagerfield/onno/pull/433) 66 | - chore(deps): bump y18n from 4.0.0 to 4.0.1 [`#435`](https://github.com/wagerfield/onno/pull/435) 67 | - chore(deps): bump ssri from 6.0.1 to 6.0.2 [`#436`](https://github.com/wagerfield/onno/pull/436) 68 | - chore(deps): bump lodash from 4.17.11 to 4.17.21 [`#439`](https://github.com/wagerfield/onno/pull/439) 69 | - chore(deps): bump hosted-git-info from 2.7.1 to 2.8.9 [`#440`](https://github.com/wagerfield/onno/pull/440) 70 | - chore(deps): bump browserslist from 4.6.0 to 4.16.6 [`#441`](https://github.com/wagerfield/onno/pull/441) 71 | - chore(deps): bump ws from 6.2.1 to 6.2.2 [`#442`](https://github.com/wagerfield/onno/pull/442) 72 | - chore(deps): bump path-parse from 1.0.6 to 1.0.7 [`#445`](https://github.com/wagerfield/onno/pull/445) 73 | - chore(deps): bump tmpl from 1.0.4 to 1.0.5 [`#447`](https://github.com/wagerfield/onno/pull/447) 74 | - chore(deps): bump node-fetch from 2.6.0 to 2.6.7 [`#448`](https://github.com/wagerfield/onno/pull/448) 75 | - chore(deps): bump ajv from 6.10.0 to 6.12.6 [`#450`](https://github.com/wagerfield/onno/pull/450) 76 | - chore(deps): bump trim-off-newlines from 1.0.1 to 1.0.3 [`#451`](https://github.com/wagerfield/onno/pull/451) 77 | - chore(deps): bump nanoid from 3.1.10 to 3.3.4 [`#452`](https://github.com/wagerfield/onno/pull/452) 78 | - chore(deps): bump postcss from 7.0.16 to 7.0.39 [`#453`](https://github.com/wagerfield/onno/pull/453) 79 | - chore(deps): bump shell-quote from 1.6.1 to 1.7.3 [`#454`](https://github.com/wagerfield/onno/pull/454) 80 | - chore(deps): bump ua-parser-js from 0.7.21 to 0.7.35 [`#464`](https://github.com/wagerfield/onno/pull/464) 81 | - chore(deps): bump terser from 4.1.2 to 4.8.1 [`#456`](https://github.com/wagerfield/onno/pull/456) 82 | - chore(deps): bump decode-uri-component from 0.2.0 to 0.2.2 [`#459`](https://github.com/wagerfield/onno/pull/459) 83 | - chore(deps): bump qs from 6.5.2 to 6.5.3 [`#460`](https://github.com/wagerfield/onno/pull/460) 84 | - chore(deps): bump express from 4.17.0 to 4.18.2 [`#461`](https://github.com/wagerfield/onno/pull/461) 85 | - chore(deps): bump flat from 5.0.0 to 5.0.2 [`#462`](https://github.com/wagerfield/onno/pull/462) 86 | - chore(deps): bump json5 from 1.0.1 to 1.0.2 [`#463`](https://github.com/wagerfield/onno/pull/463) 87 | - chore: reset repo [`87fd6fa`](https://github.com/wagerfield/onno/commit/87fd6fa4bf5b31707017055e2a8be68729a80994) 88 | - fix(deps): update dependency nuxt to v2.13.0 [`f63f0bd`](https://github.com/wagerfield/onno/commit/f63f0bd5c7b59f9da88a95b35f61c781b6c32b25) 89 | - feat: add release-it [`59fd3f8`](https://github.com/wagerfield/onno/commit/59fd3f8bc7539bf2c9a9d1cb5278fb2511287a12) 90 | 91 | ### [1.0.0](https://github.com/wagerfield/onno/compare/0.6.0...1.0.0) 92 | 93 | > 25 June 2019 94 | 95 | - chore: update stats [`2145680`](https://github.com/wagerfield/onno/commit/2145680265b89c4830abd18b364c26016c8e1fba) 96 | - docs: update credits [`bcbd61c`](https://github.com/wagerfield/onno/commit/bcbd61cde9bea5923f4e7fbcdff84921f0ea8d9f) 97 | - chore(release): publish 1.0.0 [`efb5406`](https://github.com/wagerfield/onno/commit/efb54065674ceea706e27d76da433ac36dfe33c1) 98 | 99 | #### [0.6.0](https://github.com/wagerfield/onno/compare/0.5.4...0.6.0) 100 | 101 | > 14 June 2019 102 | 103 | - feat: remove default "theme" key from omit [`fb5e2f5`](https://github.com/wagerfield/onno/commit/fb5e2f5b5ca206b513c3e0af4f751fb86745a287) 104 | - chore(release): publish 0.6.0 [`7b2461c`](https://github.com/wagerfield/onno/commit/7b2461c415e9a8a902515f82bf734805d7b4aa6d) 105 | - chore(deps): update dependency lint-staged to v8.2.1 [`005f336`](https://github.com/wagerfield/onno/commit/005f336022ec8703aa7468ad4ecbdc2a5427064c) 106 | 107 | #### [0.5.4](https://github.com/wagerfield/onno/compare/0.5.3...0.5.4) 108 | 109 | > 13 June 2019 110 | 111 | - chore: update stats [`fe930e1`](https://github.com/wagerfield/onno/commit/fe930e184533ce4142e50561e4f4d51078690ad2) 112 | - feat: add isFunction type checker [`ba58c21`](https://github.com/wagerfield/onno/commit/ba58c21cbe29eaa5b69818dcaca8273bf8652e85) 113 | - chore(release): publish 0.5.4 [`ec06cad`](https://github.com/wagerfield/onno/commit/ec06cad8206dcca01624ecfb1708fb4917adf593) 114 | 115 | #### [0.5.3](https://github.com/wagerfield/onno/compare/0.5.2...0.5.3) 116 | 117 | > 13 June 2019 118 | 119 | - revert: theme types [`9a8aea7`](https://github.com/wagerfield/onno/commit/9a8aea71fda80825ddefa9183ce9a7eeab90a423) 120 | - chore(release): publish 0.5.3 [`be2154c`](https://github.com/wagerfield/onno/commit/be2154c634b965ddaa15d98eed0e2b4419755c2d) 121 | 122 | #### [0.5.2](https://github.com/wagerfield/onno/compare/0.5.1...0.5.2) 123 | 124 | > 13 June 2019 125 | 126 | - chore: update stats [`4518da0`](https://github.com/wagerfield/onno/commit/4518da035fb4f59a618f4344f90286caa3f49eae) 127 | - feat: make Theme generic [`6e2e00a`](https://github.com/wagerfield/onno/commit/6e2e00a387aeff0510e010f64e769c059513e5bc) 128 | - chore(release): publish 0.5.2 [`5db438b`](https://github.com/wagerfield/onno/commit/5db438b1058429cbebb229448ff78cad548d5a53) 129 | 130 | #### [0.5.1](https://github.com/wagerfield/onno/compare/0.5.0...0.5.1) 131 | 132 | > 10 June 2019 133 | 134 | - chore(release): publish 0.5.1 [`0578a6d`](https://github.com/wagerfield/onno/commit/0578a6db4615d389f48ad31fda6388e49dab0c1b) 135 | - fix: add transformer to globalStyle renderer [`5b32328`](https://github.com/wagerfield/onno/commit/5b32328a74e9d54408a59d331ad7ecc4a3ca67ea) 136 | 137 | #### [0.5.0](https://github.com/wagerfield/onno/compare/0.4.8...0.5.0) 138 | 139 | > 10 June 2019 140 | 141 | - chore: add onco-react and onco-vue placeholders [`1baa78d`](https://github.com/wagerfield/onno/commit/1baa78d8d9fb51ce2d2d6d9ff06bd8fad5ef7503) 142 | - chore: update stats [`dcd35bc`](https://github.com/wagerfield/onno/commit/dcd35bc4cbf7c7d7866f8a011be2713fa593b1d9) 143 | - chore(release): publish 0.5.0 [`506e3d9`](https://github.com/wagerfield/onno/commit/506e3d988147d8eac78ecdabca32c2f0930de928) 144 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Matthew Wagerfield (matthew.wagerfield.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onno", 3 | "license": "MIT", 4 | "version": "2.2.0", 5 | "homepage": "https://onnojs.com", 6 | "repository": "https://github.com/wagerfield/onno.git", 7 | "bugs": "https://github.com/wagerfield/onno/issues", 8 | "author": { 9 | "name": "Matthew Wagerfield", 10 | "email": "matthew@wagerfield.com", 11 | "url": "https://matthew.wagerfield.com" 12 | }, 13 | "keywords": [ 14 | "onno", 15 | "clsx", 16 | "classes", 17 | "classnames", 18 | "variants", 19 | "tailwind", 20 | "tailwindcss" 21 | ], 22 | "type": "module", 23 | "sideEffects": false, 24 | "main": "dist/index.js", 25 | "types": "dist/index.d.ts", 26 | "files": [ 27 | "dist/index.js", 28 | "dist/index.d.ts", 29 | "dist/types.d.ts" 30 | ], 31 | "exports": { 32 | "default": "./dist/index.js", 33 | "types": "./dist/index.d.ts" 34 | }, 35 | "scripts": { 36 | "pkg": "pkgroll", 37 | "dev": "vite dev", 38 | "build": "vite build", 39 | "start": "vite preview", 40 | "sync": "svelte-kit sync", 41 | "clean": "rm -rf coverage dist", 42 | "test": "vitest run --coverage", 43 | "lint": "prettier --write --ignore-path .prettierignore .", 44 | "package": "svelte-package --input src/onno", 45 | "prepare": "husky install && pnpm sync", 46 | "changelog": "pnpm auto-changelog", 47 | "release": "release-it" 48 | }, 49 | "prettier": { 50 | "semi": false 51 | }, 52 | "commitlint": { 53 | "extends": [ 54 | "@commitlint/config-conventional" 55 | ] 56 | }, 57 | "auto-changelog": { 58 | "package": true, 59 | "output": "changelog.md", 60 | "startingVersion": "0.5" 61 | }, 62 | "lint-staged": { 63 | "**/*.ts": [ 64 | "prettier --write", 65 | "vitest related --run" 66 | ], 67 | "**/*.{json,md}": [ 68 | "prettier --write" 69 | ] 70 | }, 71 | "dependencies": { 72 | "clsx": "2.0.0" 73 | }, 74 | "devDependencies": { 75 | "@codemirror/autocomplete": "6.9.0", 76 | "@codemirror/commands": "6.2.4", 77 | "@codemirror/lang-javascript": "6.1.9", 78 | "@codemirror/language": "6.9.0", 79 | "@codemirror/lint": "6.4.0", 80 | "@codemirror/search": "6.5.1", 81 | "@codemirror/state": "6.2.1", 82 | "@codemirror/theme-one-dark": "6.1.2", 83 | "@codemirror/view": "6.16.0", 84 | "@commitlint/cli": "17.7.1", 85 | "@commitlint/config-conventional": "17.7.0", 86 | "@fontsource-variable/inter": "5.0.8", 87 | "@fontsource-variable/jetbrains-mono": "5.0.9", 88 | "@lezer/highlight": "1.1.6", 89 | "@sveltejs/adapter-vercel": "3.0.3", 90 | "@sveltejs/kit": "1.22.6", 91 | "@sveltejs/package": "2.2.1", 92 | "@vitest/coverage-v8": "0.34.1", 93 | "auto-changelog": "2.4.0", 94 | "autoprefixer": "10.4.15", 95 | "husky": "8.0.3", 96 | "lint-staged": "13.2.3", 97 | "pkgroll": "1.11.0", 98 | "postcss": "8.4.28", 99 | "prettier": "3.0.1", 100 | "release-it": "16.1.4", 101 | "svelte": "4.2.0", 102 | "tailwindcss": "3.3.3", 103 | "typescript": "5.1.6", 104 | "vite": "4.4.9", 105 | "vitest": "0.34.1" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [][onno] 2 | 3 | [][onno-bundlephobia] 4 | [][onno-codecov] 5 | [][onno-workflow] 6 | [][onno-license] 7 | 8 | Tiny ([596B][onno-bundlephobia]) utility for composing class variants using `clsx` 9 | 10 | pnpm add onno 11 | 12 | ## Features 13 | 14 | - :rocket: Framework agnostic 15 | - :microscope: Single _tiny_ dependency on `clsx` ([330B][clsx-bundlephobia]) 16 | - :yum: Written in [TypeScript][typescript] with delicious [type helpers](#typescript) 17 | - :100: Rigorously tested with [100% code coverage][onno-codecov] 18 | - :confetti_ball: Perfect companion to [Tailwind CSS](#tailwind-css) 19 | 20 | ## Usage 21 | 22 | ```js 23 | import { onno } from "onno" 24 | 25 | const button = onno({ 26 | variants: { 27 | size: { 28 | sm: "h-8 px-1", 29 | md: "h-10 px-2", 30 | lg: "h-12 px-3", 31 | }, 32 | intent: { 33 | primary: "bg-blue-600 text-white", 34 | secondary: "bg-gray-200 text-black", 35 | }, 36 | disabled: "opacity-50", 37 | }, 38 | }) 39 | 40 | // "h-10 px-2 bg-blue-600 text-white opacity-50" 41 | const classes = button({ size: "md", intent: "primary", disabled: true }) 42 | ``` 43 | 44 | ### Variants 45 | 46 | Define variant names and the classes to be applied to them using the `variants` config option: 47 | 48 | ```js 49 | const button = onno({ 50 | variants: { 51 | // This `boolean` variant is applied when `disabled === true` 52 | disabled: "access denied", // Classes can be defined as a `string` 53 | 54 | // This `boolean` variant is applied when `hidden === true` 55 | hidden: ["barely", "visible"], // Classes can also be a `string[]` 56 | 57 | // This `enum` variant is applied when `size === "sm" || "lg"` 58 | size: { 59 | sm: ["pretty", "small"], // Here we are using a `string[]` class list 60 | lg: "really large", // ...and here we are using a `string` class list 61 | }, 62 | }, 63 | }) 64 | 65 | button() // "" 66 | button({}) // "" 67 | button({ size: "sm" }) // "pretty small" 68 | button({ disabled: true }) // "access denied" 69 | button({ hidden: true, size: "lg" }) // "barely visible really large" 70 | ``` 71 | 72 | Note that you cannot use `className` as a variant key since it is _reserved_ for applying [additional classes](#additional-classes): 73 | 74 | ```js 75 | const button = onno({ 76 | variants: { 77 | className: "not allowed", // Error: "className" cannot be used as a variant name 78 | }, 79 | }) 80 | ``` 81 | 82 | ### Defaults 83 | 84 | Default variants can be set using the `defaults` config option: 85 | 86 | ```js 87 | const button = onno({ 88 | defaults: { 89 | hidden: true, 90 | intent: "secondary", 91 | }, 92 | variants: { 93 | hidden: "barely visible", 94 | intent: { 95 | primary: "super punchy", 96 | secondary: "quite bland", 97 | }, 98 | size: { 99 | sm: "pretty small", 100 | lg: "really large", 101 | }, 102 | }, 103 | }) 104 | 105 | button() // "barely visible quite bland" 106 | button({}) // "barely visible quite bland" 107 | button({ hidden: false }) // "quite bland" 108 | button({ intent: "primary" }) // "barely visible super punchy" 109 | button({ size: "sm" }) // "barely visible quite bland pretty small" 110 | ``` 111 | 112 | ### Base Classes 113 | 114 | Base classes can be applied using the `base` config option: 115 | 116 | ```js 117 | const button = onno({ 118 | base: "solid base", // Can also use a `string[]` class list 119 | variants: { 120 | size: { 121 | sm: "pretty small", 122 | lg: "really large", 123 | }, 124 | }, 125 | }) 126 | 127 | button() // "solid base" 128 | button({}) // "solid base" 129 | button({ size: "lg" }) // "solid base really large" 130 | ``` 131 | 132 | ### Compound Classes 133 | 134 | Apply classes when certain variants are combined using the `compounds` config option: 135 | 136 | ```js 137 | const button = onno({ 138 | variants: { 139 | hidden: "barely visible", 140 | size: { 141 | sm: "pretty small", 142 | md: "kinda normal", 143 | lg: "really large", 144 | }, 145 | }, 146 | compounds: [ 147 | { 148 | size: ["sm", "lg"], 149 | className: ["compound", "one"], // Applied when `size === "sm" || "lg"` 150 | }, 151 | { 152 | size: "md", 153 | hidden: true, 154 | className: "compound two", // Applied when `size === "md" && hidden === true` 155 | }, 156 | ], 157 | }) 158 | 159 | button() // "" 160 | button({}) // "" 161 | button({ size: "md" }) // "kinda normal" 162 | button({ hidden: true }) // "barely visible" 163 | button({ size: "lg" }) // "really large compound one" 164 | button({ size: "md", hidden: true }) // "barely visible kinda normal compound two" 165 | ``` 166 | 167 | ### Additional Classes 168 | 169 | Additional classes can be applied using the `className` option: 170 | 171 | ```js 172 | const button = onno({ 173 | base: "solid base", 174 | variants: { 175 | size: { 176 | sm: "pretty small", 177 | lg: "really large", 178 | }, 179 | }, 180 | }) 181 | 182 | button() // "solid base" 183 | button({ className: "with more" }) // "solid base with more" 184 | button({ className: "with more", size: "sm" }) // "solid base pretty small with more" 185 | ``` 186 | 187 | ### Class Composition 188 | 189 | Classes are applied in the following order: 190 | 191 | 1. `base` 192 | 2. `variants` 193 | 3. `compounds` 194 | 4. `className` 195 | 196 | Under the hood `onno` uses `clsx` to build the class list (see [`clsx` docs][clsx]) 197 | 198 | For convenience `clsx` is exported from `onno` so you can use it to compose classes: 199 | 200 | ```js 201 | import { onno, clsx } from "onno" 202 | 203 | const button = onno({ 204 | variants: { 205 | size: { 206 | sm: "pretty small", 207 | lg: "really large", 208 | }, 209 | }, 210 | }) 211 | 212 | clsx("foo", ["bar", { baz: true }], button({ size: "sm" })) // "foo bar baz pretty small" 213 | ``` 214 | 215 | Note that onno's `className` option also accepts any `clsx.ClassValue` so you can do: 216 | 217 | ```js 218 | import { onno, clsx } from "onno" 219 | 220 | const button = onno({ 221 | variants: { 222 | size: { 223 | sm: "pretty small", 224 | lg: "really large", 225 | }, 226 | }, 227 | }) 228 | 229 | button({ size: "lg", className: ["foo", ["bar"], { baz: true }] }) // "really large foo bar baz" 230 | ``` 231 | 232 | ## TypeScript 233 | 234 | Use the `OnnoProps` type to infer variant props from an `OnnoFunction` 235 | 236 | ```ts 237 | import { onno, type OnnoProps } from "onno" 238 | 239 | export const button = onno({ 240 | variants: { 241 | disabled: "not allowed", 242 | size: { 243 | sm: "pretty small", 244 | lg: "really large", 245 | }, 246 | }, 247 | }) 248 | 249 | export type ButtonProps = OnnoProps 250 | export type ButtonSizeType = ButtonProps["size"] // "sm" | "lg" | undefined 251 | export type ButtonDisabledType = ButtonProps["disabled"] // boolean | undefined 252 | ``` 253 | 254 | Note that inferred `OnnoProps` also include the `className` option alongside the variants: 255 | 256 | ```ts 257 | export type ButtonClassNameType = ButtonProps["className"] // clsx.ClassValue 258 | ``` 259 | 260 | By default all variants inferred by `OnnoProps` are _optional_. To require one or more variants, pass a union of _required_ variant keys as the second argument to the `OnnoProps` generic type: 261 | 262 | ```ts 263 | import { onno, type OnnoProps } from "onno" 264 | 265 | export const button = onno({ 266 | variants: { 267 | disabled: "not allowed", 268 | intent: { 269 | primary: "super punchy", 270 | secondary: "quite bland", 271 | }, 272 | size: { 273 | sm: "pretty small", 274 | lg: "really large", 275 | }, 276 | }, 277 | }) 278 | 279 | // Require both the `intent` and `size` variants 280 | export type ButtonProps = OnnoProps 281 | 282 | // Error: Property 'intent' is missing in type '{ size: "md" }' 283 | const buttonProps: ButtonProps = { size: "md" } 284 | ``` 285 | 286 | ## Tailwind CSS 287 | 288 | If you are using the [Tailwind CSS VSCode extension][tailwindcss-vscode], add the following configuration to your workspace `.vscode/settings.json` file: 289 | 290 | ```json 291 | { 292 | "tailwindCSS.experimental.classRegex": [ 293 | ["onno|clsx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"] 294 | ] 295 | } 296 | ``` 297 | 298 | This will enable Tailwind's IntelliSense for both `onno` and `clsx` within your project! :tada: 299 | 300 |  301 | 302 | ## License 303 | 304 | [MIT][onno-license] © [Matthew Wagerfield][wagerfield] 305 | 306 | [wagerfield]: https://github.com/wagerfield 307 | [onno]: https://github.com/wagerfield/onno#readme 308 | [onno-workflow]: https://github.com/wagerfield/onno/actions/workflows/test.yml 309 | [onno-license]: https://github.com/wagerfield/onno/blob/main/license 310 | [onno-codecov]: https://codecov.io/gh/wagerfield/onno 311 | [clsx-bundlephobia]: https://bundlephobia.com/package/clsx 312 | [onno-bundlephobia]: https://bundlephobia.com/package/onno 313 | [clsx]: https://github.com/lukeed/clsx#readme 314 | [typescript]: https://www.typescriptlang.org 315 | [tailwindcss-vscode]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss 316 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // https://kit.svelte.dev/docs/types#app 2 | declare global { 3 | namespace App { 4 | // interface Error {} 5 | // interface Locals {} 6 | // interface PageData {} 7 | // interface Platform {} 8 | } 9 | } 10 | 11 | export {} 12 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %sveltekit.head% 6 | 7 | 8 | 9 | 10 | %sveltekit.body% 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/codemirror/extensions.ts: -------------------------------------------------------------------------------- 1 | import { history, historyKeymap, defaultKeymap } from "@codemirror/commands" 2 | import { indentOnInput, bracketMatching } from "@codemirror/language" 3 | import { EditorState, type Extension } from "@codemirror/state" 4 | import { lintKeymap } from "@codemirror/lint" 5 | import { 6 | closeBrackets, 7 | autocompletion, 8 | completionKeymap, 9 | closeBracketsKeymap, 10 | } from "@codemirror/autocomplete" 11 | import { 12 | keymap, 13 | dropCursor, 14 | lineNumbers, 15 | drawSelection, 16 | crosshairCursor, 17 | highlightActiveLine, 18 | rectangularSelection, 19 | highlightSpecialChars, 20 | highlightActiveLineGutter, 21 | } from "@codemirror/view" 22 | import { getTheme, type CodeMirrorThemeName } from "./themes" 23 | 24 | export interface ExtensionOptions { 25 | extensions?: Extension 26 | theme?: CodeMirrorThemeName 27 | } 28 | 29 | export const getExtensions = ({ 30 | extensions = [], 31 | theme = "light", 32 | }: ExtensionOptions = {}): Extension => [ 33 | autocompletion(), 34 | indentOnInput(), 35 | lineNumbers(), 36 | history(), 37 | 38 | // Language 39 | closeBrackets(), 40 | bracketMatching(), 41 | 42 | // Cursor 43 | dropCursor(), 44 | crosshairCursor(), 45 | 46 | // Selection 47 | drawSelection(), 48 | rectangularSelection(), 49 | EditorState.allowMultipleSelections.of(true), 50 | 51 | // Highlighting 52 | highlightSpecialChars(), 53 | highlightActiveLineGutter(), 54 | highlightActiveLine(), 55 | 56 | // Theme 57 | getTheme(theme), 58 | 59 | // Keymap 60 | keymap.of([ 61 | ...defaultKeymap, 62 | ...historyKeymap, 63 | ...completionKeymap, 64 | ...closeBracketsKeymap, 65 | ...lintKeymap, 66 | ]), 67 | 68 | // External 69 | extensions, 70 | ] 71 | -------------------------------------------------------------------------------- /src/codemirror/themes.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "@lezer/highlight" 2 | import { oneDark } from "@codemirror/theme-one-dark" 3 | import { createTheme, type EditorTheme } from "./utils" 4 | 5 | const baseEditorTheme: EditorTheme = { 6 | focusedOutline: "1px solid #000", 7 | 8 | // Typography 9 | fontFamily: "JetBrains Mono Variable", 10 | fontSize: "15px", 11 | lineHeight: "20px", 12 | 13 | // Cursor 14 | cursorWidth: 2, 15 | 16 | // Padding 17 | contentPadding: "0.5em 0", 18 | gutterPadding: "0 2em", 19 | linePadding: "0", 20 | 21 | // Gutter 22 | gutterBorder: "none", 23 | gutterMinWidth: "0", 24 | } 25 | 26 | const themes = { 27 | light: createTheme({ 28 | dark: false, 29 | editor: { 30 | ...baseEditorTheme, 31 | background: "#fff", 32 | textColor: "#111827", 33 | cursorColor: "#111827", 34 | gutterBackground: "#fffd", 35 | gutterBackdropFilter: "blur(4px)", 36 | gutterTextColor: "#9ca3af", 37 | activeGutterBackground: "none", 38 | activeGutterTextColor: "#111827", 39 | activeLineBackground: "#cbd5e144", 40 | selectionBackground: "#cbd5e144", 41 | focusedSelectionBackground: "#cbd5e144", 42 | matchingBracketBackground: "#cbd5e1aa", 43 | hoverGutterTextColor: "#111827", 44 | }, 45 | syntax: [ 46 | { tag: tags.comment, color: "#9ca3af" }, 47 | { tag: tags.keyword, color: "#e11d48" }, 48 | { tag: tags.variableName, color: "#7c3aed" }, 49 | { tag: tags.string, color: "#2563eb" }, 50 | { tag: tags.bool, color: "#10b981" }, 51 | { tag: tags.function(tags.variableName), color: "#f97316" }, 52 | ], 53 | }), 54 | dark: [ 55 | oneDark, 56 | createTheme({ 57 | dark: true, 58 | editor: { ...baseEditorTheme, cursorColor: "#528bff" }, 59 | syntax: [], 60 | }), 61 | ], 62 | } 63 | 64 | export type CodeMirrorThemeName = keyof typeof themes 65 | 66 | export const getTheme = (theme: CodeMirrorThemeName) => themes[theme] 67 | -------------------------------------------------------------------------------- /src/codemirror/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Extension } from "@codemirror/state" 2 | import type { TagStyle } from "@codemirror/language" 3 | 4 | import { HighlightStyle, syntaxHighlighting } from "@codemirror/language" 5 | import { EditorView } from "@codemirror/view" 6 | 7 | export interface EditorTheme { 8 | focusedOutline?: string 9 | background?: string 10 | textColor?: string 11 | 12 | // Typography 13 | fontSize?: string 14 | fontFamily?: string 15 | lineHeight?: string | number 16 | 17 | // Cursor 18 | cursorColor?: string 19 | cursorWidth?: number 20 | 21 | // Padding 22 | contentPadding?: string 23 | gutterPadding?: string 24 | linePadding?: string 25 | 26 | // Line 27 | activeLineBackground?: string 28 | 29 | // Gutter 30 | gutterBorder?: string 31 | gutterMinWidth?: string 32 | gutterTextColor?: string 33 | gutterBackground?: string 34 | gutterBackdropFilter?: string 35 | hoverGutterTextColor?: string 36 | hoverGutterBackground?: string 37 | activeGutterTextColor?: string 38 | activeGutterBackground?: string 39 | 40 | // Selection 41 | selectionBackground?: string 42 | focusedSelectionBackground?: string 43 | 44 | // Brackets 45 | matchingBracketBackground?: string 46 | matchingBracketTextColor?: string 47 | } 48 | 49 | export interface CodeMirrorTheme { 50 | editor: EditorTheme 51 | syntax: TagStyle[] 52 | dark: boolean 53 | } 54 | 55 | export interface StyleSpec { 56 | [key: string]: StyleSpec | string | number | null 57 | } 58 | 59 | export interface EditorSpec { 60 | [selector: string]: StyleSpec 61 | } 62 | 63 | export const createEditorTheme = ({ 64 | cursorColor = "black", 65 | cursorWidth = 2, 66 | ...theme 67 | }: EditorTheme): EditorSpec => ({ 68 | "&": { 69 | background: theme.background ?? null, 70 | color: theme.textColor ?? null, 71 | }, 72 | "&.cm-focused": { 73 | outline: theme.focusedOutline ?? null, 74 | }, 75 | ".cm-scroller": { 76 | fontSize: theme.fontSize ?? null, 77 | fontFamily: theme.fontFamily ?? null, 78 | lineHeight: theme.lineHeight ?? null, 79 | }, 80 | ".cm-content": { 81 | caretColor: cursorColor, 82 | padding: theme.contentPadding ?? null, 83 | }, 84 | 85 | // Gutter 86 | ".cm-gutters": { 87 | backdropFilter: theme.gutterBackdropFilter ?? null, 88 | background: theme.gutterBackground ?? null, 89 | borderRight: theme.gutterBorder ?? null, 90 | color: theme.gutterTextColor ?? null, 91 | }, 92 | ".cm-lineNumbers .cm-gutterElement": { 93 | minWidth: theme.gutterMinWidth ?? null, 94 | padding: theme.gutterPadding ?? null, 95 | cursor: "pointer", 96 | }, 97 | ".cm-gutterElement:hover": { 98 | background: theme.hoverGutterBackground ?? null, 99 | color: theme.hoverGutterTextColor ?? null, 100 | }, 101 | 102 | // Line 103 | ".cm-line": { 104 | padding: theme.linePadding ?? null, 105 | }, 106 | ".cm-activeLine": { 107 | background: theme.activeLineBackground ?? null, 108 | }, 109 | ".cm-activeLineGutter": { 110 | background: theme.activeGutterBackground ?? null, 111 | color: theme.activeGutterTextColor ?? null, 112 | }, 113 | 114 | // Cursor 115 | "&.cm-focused .cm-cursor": { 116 | borderLeft: `${cursorColor} solid ${cursorWidth}px`, 117 | marginLeft: `-${cursorWidth}px`, 118 | }, 119 | 120 | // Selection 121 | ".cm-selectionMatch": { 122 | backgroundColor: "#f00", // TODO: Make this configurable 123 | }, 124 | ".cm-selectionBackground": { 125 | background: theme.selectionBackground ?? null, 126 | }, 127 | "&.cm-focused .cm-scroller .cm-selectionLayer .cm-selectionBackground": { 128 | background: theme.focusedSelectionBackground ?? null, 129 | }, 130 | 131 | // Brackets 132 | "&.cm-focused .cm-matchingBracket": { 133 | background: theme.matchingBracketBackground ?? null, 134 | color: theme.matchingBracketTextColor ?? null, 135 | }, 136 | }) 137 | 138 | export const createTheme = (theme: CodeMirrorTheme): Extension => [ 139 | EditorView.theme(createEditorTheme(theme.editor), { dark: theme.dark }), 140 | syntaxHighlighting(HighlightStyle.define(theme.syntax), { fallback: true }), 141 | ] 142 | -------------------------------------------------------------------------------- /src/components/Editor.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/components/Head.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {$metadata.title} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {#each theme as [key, color] (key)} 35 | 40 | {/each} 41 | 42 | -------------------------------------------------------------------------------- /src/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 18 | {$metadata.description} 19 | screen: {$screen.width}px x {$screen.height}px 20 | pointer: {$pointer.x}px x {$pointer.y}px 21 | pnpm add onno 22 | 23 | size: 24 | 25 | {size} 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Logo.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | {text} 26 | {#each chars as { tag, attrs }} 27 | 28 | {/each} 29 | 30 | -------------------------------------------------------------------------------- /src/examples/basic.example.ts: -------------------------------------------------------------------------------- 1 | export default `import { onno } from "onno" 2 | 3 | // Define button component variants 4 | const button = onno({ 5 | base: "flex items-center cursor-pointer rounded", 6 | variants: { 7 | disabled: "pointer-events-none opacity-50", 8 | hidden: "hidden", 9 | intent: { 10 | primary: "bg-indigo-500 text-white", 11 | secondary: "bg-slate-100 text-slate-800", 12 | }, 13 | size: { 14 | sm: "h-8 px-2 text-sm", 15 | md: "h-10 px-3 text-md", 16 | lg: "h-12 px-4 text-md", 17 | }, 18 | }, 19 | defaults: { 20 | intent: "primary", 21 | size: "md", 22 | }, 23 | }) 24 | 25 | // Generate button class list 26 | const classes = button({ 27 | hidden: false, 28 | intent: "primary", 29 | size: "sm" 30 | }) 31 | ` 32 | -------------------------------------------------------------------------------- /src/lib/chars.ts: -------------------------------------------------------------------------------- 1 | export const CHARS = { 2 | // LETTERS 3 | // "a": "M0,1 L1,0 L7,0 L8,1 L8,8 L1,8 L0,7 L0,1 Z M2,2 L2,6 L4,6 L4,2 L2,2 Z", 4 | // "b": "M0,1 L1,0 L4,0 L4,3 L7,3 L8,4 L8,10 L7,11 L1,11 L0,10 L0,1 Z M4,5 L4,9 L6,9 L6,5 L4,5 Z", 5 | c: "8 3 6 3 6 2 4 2 4 6 6 6 6 5 8 5 8 7 7 8 1 8 0 7 0 1 1 0 7 0 8 1", 6 | // "d": "M8,1 L8,10 L7,11 L1,11 L0,10 L0,4 L1,3 L4,3 L4,0 L7,0 L8,1 Z M4,5 L2,5 L2,9 L4,9 L4,5 Z", 7 | // "e": "M8,5 L4,5 L4,6 L8,6 L8,7 L7,8 L1,8 L0,7 L0,1 L1,0 L7,0 L8,1 L8,5 Z M4,2 L4,4 L6,4 L6,2 L4,2 Z", 8 | // "f": "6 0 6 2 4 2 4 6 6 6 6 8 4 8 4 11 0 11 0 1 1 0", 9 | // "g": "M8,10 L7,11 L2,11 L2,9 L4,9 L4,8 L1,8 L0,7 L0,1 L1,0 L7,0 L8,1 L8,10 Z M4,6 L4,2 L2,2 L2,6 L4,6 Z", 10 | // "h": "4 3 7 3 8 4 8 11 6 11 6 7 4 7 4 11 0 11 0 1 1 0 4 0", 11 | // "i": "4 0 4 7 3 8 0 8 0 1 1 0", 12 | // "j": "6 0 6 10 5 11 0 11 0 9 2 9 2 1 3 0", 13 | // "k": "4 5 6 3 9 3 9 4 6 7 9 10 9 11 6 11 4 9 4 11 0 11 0 1 1 0 4 0", 14 | // "l": "4 0 4 10 3 11 0 11 0 1 1 0", 15 | // "m": "1 0 11 0 12 1 12 8 10 8 10 4 8 4 8 8 6 8 6 4 4 4 4 8 0 8 0 1", 16 | n: "6 8 6 4 4 4 4 8 0 8 0 1 1 0 7 0 8 1 8 8", 17 | o: "M0,1 L1,0 L7,0 L8,1 L8,7 L7,8 L1,8 L0,7 L0,1 Z M4,2 L4,6 L6,6 L6,2 L4,2 Z", 18 | // "p": "M0,10 L0,1 L1,0 L7,0 L8,1 L8,7 L7,8 L4,8 L4,11 L1,11 L0,10 Z M4,6 L6,6 L6,2 L4,2 L4,6 Z", 19 | // "q": "M8,10 L7,11 L4,11 L4,8 L1,8 L0,7 L0,1 L1,0 L7,0 L8,1 L8,10 Z M4,6 L4,2 L2,2 L2,6 L4,6 Z", 20 | // "r": "8 3 6 3 6 2 4 2 4 8 0 8 0 1 1 0 7 0 8 1", 21 | // "s": "1 5 0 4 0 1 1 0 7 0 8 1 8 2 3 2 3 3 7 3 8 4 8 7 7 8 1 8 0 7 0 6 5 6 5 5", 22 | // "t": "6 11 1 11 0 10 0 0 4 0 4 3 6 3 6 5 4 5 4 9 6 9", 23 | // "u": "6 0 8 0 8 7 7 8 1 8 0 7 0 0 4 0 4 4 6 4", 24 | // "v": "6 0 8 0 8 2 5 8 3 8 0 2 0 0 2 0 4 4", 25 | // "w": "1 8 0 7 0 0 4 0 4 4 6 4 6 0 8 0 8 4 10 4 10 0 12 0 12 7 11 8", 26 | // "x": "8 0 8 2 6 4 8 6 8 8 6 8 4 6 2 8 0 8 0 6 1.95 4.05 0 2 0 0 2 0 4 2 6 0", 27 | // "y": "2 0 2 4 4 4 4 0 8 0 8 10 7 11 2 11 2 9 4 9 4 8 1 8 0 7 0 0", 28 | // "z": "8 0 8 2 4 6 8 6 8 8 0 8 0 6 4 2 0 2 0 0", 29 | 30 | // SYMBOLS 31 | // " ": "M4,0", 32 | // "-": "0 0 6 0 6 2 0 2", 33 | // "+": "2 2 2 0 4 0 4 2 6 2 6 4 4 4 4 6 2 6 2 4 0 4 0 2", 34 | // "=": "M0,0 L6,0 L6,2 L0,2 L0,0 Z M0,4 L6,4 L6,6 L0,6 L0,4 Z", 35 | // ":": "M0,4 L2,4 L2,6 L0,6 L0,4 Z M0,0 L2,0 L2,2 L0,2 L0,0 Z", 36 | // ".": "0 0 2 0 2 2 0 2", 37 | // "|": "0 0 2 0 2 8 0 8", 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/text.ts: -------------------------------------------------------------------------------- 1 | import { CHARS } from "./chars" 2 | 3 | type Tag = "path" | "polygon" 4 | 5 | export interface Attrs { 6 | transform: string 7 | points?: string 8 | d?: string 9 | } 10 | 11 | export interface Char { 12 | attrs: Attrs 13 | char: string 14 | tag: Tag 15 | x: number 16 | w: number 17 | h: number 18 | } 19 | 20 | const NUM = /\d+/g 21 | const MAP: Record = { 22 | polygon: "points", 23 | path: "d", 24 | } 25 | 26 | export const mapChar = (char: string, size: number, x: number): Char | null => { 27 | const path = (CHARS as Record)[char] 28 | 29 | if (!path) return null 30 | 31 | const tag = isNaN(+path[0]) ? "path" : "polygon" 32 | 33 | const scale = (v: string) => Math[+v < 4 ? "ceil" : "floor"](+v * size) 34 | 35 | return path.split(" ").reduce( 36 | (props, value, index, array) => { 37 | const match = value.match(NUM) 38 | if (tag === "path" && match) { 39 | props.w = Math.max(props.w, scale(match[0])) 40 | props.h = Math.max(props.h, scale(match[1])) 41 | } else if (tag === "polygon" && index % 2) { 42 | props.w = Math.max(props.w, scale(array[index - 1])) 43 | props.h = Math.max(props.h, scale(value)) 44 | } 45 | return props 46 | }, 47 | { 48 | x, 49 | w: 0, 50 | h: 0, 51 | tag, 52 | char, 53 | attrs: { 54 | transform: `translate(${x})`, 55 | [MAP[tag]]: path.replace(NUM, scale as any), 56 | }, 57 | }, 58 | ) 59 | } 60 | 61 | export function mapText(text: string, size: number, x = 0): Char[] { 62 | return text.split("").reduce((chars, char) => { 63 | const data = mapChar(char, size, x) 64 | if (data) { 65 | chars.push(data) 66 | x += data.w + Math.ceil(size) 67 | } 68 | return chars 69 | }, []) 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | export type Nil = null | undefined 4 | 5 | export type Obj = Record 6 | 7 | export type Void = T | undefined 8 | 9 | export type Func = (...args: any[]) => any 10 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { browser } from "$app/environment" 4 | import type { Nil, Obj } from "./types" 5 | 6 | // Flags 7 | 8 | export const isClient = browser ? true : false 9 | 10 | export const isServer = browser ? false : true 11 | 12 | // General 13 | 14 | export const noop = () => {} 15 | 16 | export const identity = (x: T): T => x 17 | 18 | // Guards 19 | 20 | export const isNil = (x: unknown): x is Nil => x === null || x === undefined 21 | 22 | export const isBoolean = (x: unknown): x is boolean => typeof x === "boolean" 23 | 24 | export const isNumber = (x: unknown): x is number => typeof x === "number" 25 | 26 | export const isString = (x: unknown): x is string => typeof x === "string" 27 | 28 | export const isArray = (x: unknown): x is any[] => Array.isArray(x) 29 | 30 | export const isObject = (x: unknown): x is Obj => 31 | x != null && typeof x === "object" && !isArray(x) 32 | 33 | // Aliases 34 | 35 | export const now = isClient ? performance.now : Date.now 36 | 37 | export const raf = isClient ? requestAnimationFrame : noop 38 | 39 | export const caf = isClient ? cancelAnimationFrame : noop 40 | -------------------------------------------------------------------------------- /src/onno/index.ts: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx" 2 | export { clsx } from "clsx" 3 | 4 | export * from "./types" 5 | 6 | import type { OnnoFactory, OnnoClassValue } from "./types" 7 | 8 | const matches = (c: any, o: any) => (Array.isArray(c) ? c.includes(o) : c === o) 9 | 10 | export const onno: OnnoFactory = ({ 11 | base, 12 | defaults, 13 | variants, 14 | compounds = [], 15 | }) => { 16 | if (variants?.className) { 17 | throw new Error(`"className" cannot be used as a variant name`) 18 | } 19 | 20 | const cl = compounds.length || 1 21 | const cn = compounds.map((c) => c.className) 22 | 23 | return (options) => { 24 | const vc: OnnoClassValue = [] 25 | const cc = [...cn] 26 | 27 | for (let i = 0; i < cl; i++) { 28 | for (const k in variants) { 29 | const o = options?.[k] ?? defaults?.[k] 30 | const c = compounds[i]?.[k] 31 | const v = variants[k] 32 | 33 | if (!i && o && v) vc.push((v as any)[o] ?? v) // variant classes 34 | if (c && cc[i] && !matches(c, o)) cc[i] = "" // compound classes 35 | } 36 | } 37 | 38 | return clsx(base, vc, cc, options?.className) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/onno/types.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx" 2 | 3 | // Class Types 4 | 5 | export type { ClassValue } 6 | 7 | export type ClassKey = "className" 8 | 9 | export type ClassProps = Partial> 10 | 11 | // Utility Types 12 | 13 | export type Flatten = T extends object ? {} & { [P in keyof T]: T[P] } : T 14 | 15 | // Onno Class Types 16 | 17 | export type OnnoClassValue = string | string[] 18 | 19 | export type OnnoClassProps = Record 20 | 21 | export type OnnoClassMap = Record 22 | 23 | // Onno Config Types 24 | 25 | export type OnnoVariants = Record 26 | 27 | export type OnnoDefaults = { 28 | [K in keyof T]?: T[K] extends OnnoClassMap ? keyof T[K] : boolean 29 | } 30 | 31 | export type OnnoCompound = { 32 | [K in keyof T]?: T[K] extends OnnoClassMap 33 | ? keyof T[K] | Array 34 | : boolean 35 | } & OnnoClassProps 36 | 37 | export interface OnnoConfig { 38 | base?: OnnoClassValue 39 | compounds?: Flatten>[] 40 | defaults?: Flatten> 41 | variants: T 42 | } 43 | 44 | export type OnnoOptions = OnnoDefaults & ClassProps 45 | 46 | // Onno Function Types 47 | 48 | export type OnnoFunction = ( 49 | options?: Flatten>, 50 | ) => string 51 | 52 | export type OnnoFactory = ( 53 | config: OnnoConfig, 54 | ) => OnnoFunction 55 | 56 | // Onno Prop Types 57 | 58 | export type OnnoVariantProps> = Omit< 59 | Exclude[0], undefined>, 60 | ClassKey 61 | > 62 | 63 | export type OnnoProps< 64 | F extends OnnoFunction, 65 | K extends keyof OnnoVariantProps = never, 66 | > = Flatten< 67 | OnnoVariantProps & Required, K>> & ClassProps 68 | > 69 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | {value} 23 | 24 | -------------------------------------------------------------------------------- /src/stores/metadata.store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store" 2 | 3 | export const metadata = writable({ 4 | title: "onno", 5 | url: "https://onnojs.com", 6 | description: "Tiny (596B) utility for composing class variants using clsx", 7 | statusBarStyle: "black-translucent", 8 | viewport: "width=device-width", 9 | themeLight: "#FFFFFF", 10 | themeDark: "#000000", 11 | }) 12 | -------------------------------------------------------------------------------- /src/stores/pointer.store.ts: -------------------------------------------------------------------------------- 1 | import { isServer } from "$lib/utils" 2 | import { readable } from "svelte/store" 3 | 4 | export const pointer = readable({ x: 0, y: 0 }, (set) => { 5 | if (isServer) return 6 | 7 | const onPointerMove = (event: PointerEvent) => { 8 | set({ 9 | x: Math.round(event.clientX), 10 | y: Math.round(event.clientY), 11 | }) 12 | } 13 | 14 | window.addEventListener("pointermove", onPointerMove) 15 | 16 | return () => { 17 | window.removeEventListener("pointermove", onPointerMove) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/stores/screen.store.ts: -------------------------------------------------------------------------------- 1 | import { isServer } from "$lib/utils" 2 | import { readable } from "svelte/store" 3 | 4 | export const screen = readable({ width: 0, height: 0 }, (set) => { 5 | if (isServer) return 6 | 7 | const onWindowResize = () => { 8 | set({ 9 | width: window.innerWidth, 10 | height: window.innerHeight, 11 | }) 12 | } 13 | 14 | window.addEventListener("resize", onWindowResize) 15 | 16 | onWindowResize() 17 | 18 | return () => { 19 | window.removeEventListener("resize", onWindowResize) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/stores/theme.store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store" 2 | 3 | export const theme = writable({ 4 | accent: "coral", 5 | light: "ivory", 6 | dark: "#202428", 7 | }) 8 | -------------------------------------------------------------------------------- /static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/static/icons/icon-192x192.png -------------------------------------------------------------------------------- /static/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/static/icons/icon-384x384.png -------------------------------------------------------------------------------- /static/icons/icon-512x 512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagerfield/onno/e08a0a863c8fb04ef15dab8c3d9866f17c2d3bae/static/icons/icon-512x 512.png -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onno", 3 | "short_name": "onno", 4 | "theme_color": "#FFFFFF", 5 | "background_color": "#FFFFFF", 6 | "orientation": "portrait", 7 | "display": "standalone", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "/icons/icon-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png", 14 | "purpose": "any maskable" 15 | }, 16 | { 17 | "src": "/icons/icon-384x384.png", 18 | "sizes": "384x384", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/icons/icon-512x512.png", 23 | "sizes": "512x512", 24 | "type": "image/png" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/kit/vite" 2 | import vercelAdapter from "@sveltejs/adapter-vercel" 3 | 4 | // https://kit.svelte.dev/docs/configuration 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | preprocess: vitePreprocess(), 8 | vitePlugin: { 9 | inspector: false, 10 | }, 11 | kit: { 12 | appDir: "app", 13 | outDir: ".svelte", 14 | adapter: vercelAdapter({ runtime: "edge" }), 15 | alias: { 16 | $codemirror: "./src/codemirror", 17 | $components: "./src/components", 18 | $examples: "./src/examples", 19 | $stores: "./src/stores", 20 | onno: "./src/onno", 21 | }, 22 | }, 23 | } 24 | 25 | export default config 26 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme" 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: ["./src/**/*.{html,svelte,ts}"], 6 | plugins: [], 7 | theme: { 8 | fontFamily: { 9 | sans: ["Inter Variable", ...fontFamily.sans], 10 | mono: ["JetBrains Mono Variable", ...fontFamily.mono], 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /tests/onno.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { onno, type OnnoProps } from "onno" 3 | 4 | describe("onno(config)", () => { 5 | it("expects one argument", () => { 6 | expect(onno).toHaveLength(1) 7 | expect(onno).toEqual(expect.any(Function)) 8 | }) 9 | 10 | it("returns a factory function", () => { 11 | const fn = onno({ variants: {} }) 12 | 13 | expect(fn).toHaveLength(1) 14 | expect(fn).toEqual(expect.any(Function)) 15 | }) 16 | 17 | it("throws an error for 'className' variant", () => { 18 | const fn = () => onno({ variants: { className: "not allowed" } }) 19 | 20 | expect(fn).toThrow(`"className" cannot be used as a variant name`) 21 | }) 22 | 23 | it("returns base class list string", () => { 24 | const fn = onno({ 25 | base: "base classes", 26 | variants: {}, 27 | }) 28 | 29 | expect(fn()).toBe("base classes") 30 | }) 31 | 32 | it("returns base class list string[]", () => { 33 | const fn = onno({ 34 | base: ["base", "classes"], 35 | variants: {}, 36 | }) 37 | 38 | expect(fn()).toBe("base classes") 39 | }) 40 | 41 | it("adds className from options", () => { 42 | const fn = onno({ 43 | base: "base", 44 | variants: {}, 45 | }) 46 | 47 | expect(fn({ className: "with className" })).toBe("base with className") 48 | expect(fn({ className: ["with", "className"] })).toBe("base with className") 49 | expect(fn({ className: ["foo", ["bar"], { baz: true }] })).toBe( 50 | "base foo bar baz", 51 | ) 52 | }) 53 | 54 | it("supports boolean variants", () => { 55 | const fn = onno({ 56 | variants: { 57 | hidden: "invisible", 58 | disabled: ["not", "allowed"], 59 | }, 60 | }) 61 | 62 | expect(fn()).toBe("") 63 | expect(fn({ hidden: false })).toBe("") 64 | expect(fn({ hidden: true })).toBe("invisible") 65 | expect(fn({ disabled: true })).toBe("not allowed") 66 | expect(fn({ hidden: true, disabled: true })).toBe("invisible not allowed") 67 | }) 68 | 69 | it("supports mapped variants", () => { 70 | const fn = onno({ 71 | variants: { 72 | size: { 73 | sm: "pretty small", 74 | lg: ["really", "large"], 75 | }, 76 | }, 77 | }) 78 | 79 | expect(fn()).toBe("") 80 | expect(fn({ size: "sm" })).toBe("pretty small") 81 | expect(fn({ size: "lg" })).toBe("really large") 82 | }) 83 | 84 | it("combines boolean and mapped variants", () => { 85 | const fn = onno({ 86 | variants: { 87 | hidden: "invisible", 88 | size: { 89 | sm: "small", 90 | lg: "large", 91 | }, 92 | }, 93 | }) 94 | 95 | expect(fn({ size: "sm", hidden: true })).toBe("invisible small") 96 | }) 97 | 98 | it("combines baseline and variant classes", () => { 99 | const fn = onno({ 100 | base: "base", 101 | variants: { 102 | hidden: "invisible", 103 | size: { 104 | sm: "small", 105 | lg: "large", 106 | }, 107 | }, 108 | }) 109 | 110 | expect(fn({ size: "lg", hidden: true })).toBe("base invisible large") 111 | }) 112 | 113 | it("supports default variants", () => { 114 | const fn = onno({ 115 | base: "base", 116 | defaults: { 117 | size: "sm", 118 | jazzy: true, 119 | }, 120 | variants: { 121 | hidden: "invisible", 122 | jazzy: "party", 123 | size: { 124 | sm: "small", 125 | lg: "large", 126 | }, 127 | }, 128 | }) 129 | 130 | expect(fn()).toBe("base party small") 131 | expect(fn({ jazzy: false })).toBe("base small") 132 | expect(fn({ size: "lg" })).toBe("base party large") 133 | expect(fn({ hidden: true })).toBe("base invisible party small") 134 | expect(fn({ size: "lg", hidden: true })).toBe("base invisible party large") 135 | }) 136 | 137 | it("supports compound variants", () => { 138 | const fn = onno({ 139 | base: "base", 140 | defaults: { 141 | size: "md", 142 | }, 143 | variants: { 144 | hidden: "invisible", 145 | intent: { 146 | primary: "bold", 147 | secondary: "muted", 148 | }, 149 | size: { 150 | sm: "small", 151 | md: "medium", 152 | lg: "large", 153 | }, 154 | }, 155 | compounds: [ 156 | { 157 | size: "sm", 158 | hidden: true, 159 | className: "hide", 160 | }, 161 | { 162 | size: ["sm", "md"], 163 | intent: "primary", 164 | className: "highlight", 165 | }, 166 | ], 167 | }) 168 | 169 | expect(fn()).toBe("base medium") 170 | expect(fn({ intent: "primary" })).toBe("base bold medium highlight") 171 | expect(fn({ size: "sm", hidden: true })).toBe("base invisible small hide") 172 | expect(fn({ size: "sm", intent: "primary", hidden: true })).toBe( 173 | "base invisible bold small hide highlight", 174 | ) 175 | }) 176 | 177 | it("provides expected OnnoProps interface with optional variants", () => { 178 | const fn = onno({ 179 | variants: { 180 | hidden: "invisible", 181 | intent: { 182 | primary: "bold", 183 | secondary: "muted", 184 | }, 185 | size: { 186 | sm: "small", 187 | md: "medium", 188 | lg: "large", 189 | }, 190 | }, 191 | }) 192 | 193 | type Props = OnnoProps 194 | 195 | // Valid props 196 | const a: Props = { intent: "primary", size: "sm", hidden: true } 197 | 198 | // @ts-expect-error ts(2322) 199 | // Type 'string' is not assignable to type 'boolean | undefined'. 200 | const b: Props = { hidden: "yes" } 201 | 202 | // @ts-expect-error ts(2322) 203 | // Type '"xs"' is not assignable to type '"sm" | "lg" | "md" | undefined'. 204 | const c: Props = { size: "xs" } 205 | }) 206 | 207 | it("provides expected OnnoProps interface with required variants", () => { 208 | const fn = onno({ 209 | variants: { 210 | hidden: "invisible", 211 | intent: { 212 | primary: "bold", 213 | secondary: "muted", 214 | }, 215 | size: { 216 | sm: "small", 217 | md: "medium", 218 | lg: "large", 219 | }, 220 | }, 221 | }) 222 | 223 | type Props = OnnoProps 224 | 225 | // @ts-expect-error ts(2322) 226 | // Property 'intent' is missing in type '{ size: "md" }' 227 | const a: Props = { size: "md" } 228 | }) 229 | }) 230 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "noUnusedLocals": true, 5 | "skipLibCheck": true, 6 | "strict": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | import { sveltekit } from "@sveltejs/kit/vite" 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ["tests/**/*.spec.ts"], 8 | coverage: { 9 | reporter: ["text", "html", "json", "json-summary"], 10 | statements: 100, 11 | functions: 100, 12 | branches: 100, 13 | lines: 100, 14 | }, 15 | }, 16 | }) 17 | --------------------------------------------------------------------------------
{$metadata.description}
screen: {$screen.width}px x {$screen.height}px
pointer: {$pointer.x}px x {$pointer.y}px
pnpm add onno
{value}