├── .babelrc ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierrc ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest.config.ts ├── jest ├── babelTransform.js ├── cssTransform.js └── fileTransform.js ├── package.json ├── rollup.config.js ├── src ├── components │ ├── avatar │ │ ├── Avatar.stories.tsx │ │ ├── Avatar.tsx │ │ ├── StyledAvatar.ts │ │ ├── index.ts │ │ └── static │ │ │ └── image.svg │ ├── badge │ │ ├── Badge.stories.tsx │ │ ├── Badge.tsx │ │ ├── StyledContainer.ts │ │ ├── StyledIcon.ts │ │ └── index.ts │ ├── button │ │ ├── Button.stories.tsx │ │ ├── Button.test.tsx │ │ ├── Button.tsx │ │ ├── StyledButton.ts │ │ ├── StyledIcon.ts │ │ └── index.ts │ ├── checkbox │ │ ├── Checkbox.stories.tsx │ │ ├── Checkbox.tsx │ │ ├── StyledCheckbox.ts │ │ ├── StyledHiddenCheckbox.ts │ │ ├── StyledLabel.ts │ │ └── index.ts │ ├── collapse │ │ ├── Collapse.stories.tsx │ │ ├── Collapse.tsx │ │ ├── StyledCollapse.ts │ │ ├── StyledContainer.ts │ │ ├── StyledContent.ts │ │ ├── StyledHeader.ts │ │ └── index.ts │ ├── input │ │ ├── Input.stories.tsx │ │ ├── Input.tsx │ │ ├── StyledContainer.ts │ │ ├── StyledInput.ts │ │ ├── StyledLabel.ts │ │ ├── StyledTextArea.tsx │ │ └── index.ts │ ├── pagination │ │ ├── Pagination.stories.tsx │ │ ├── Pagination.tsx │ │ ├── StyledContainer.ts │ │ └── index.ts │ ├── radio │ │ ├── Radio.stories.tsx │ │ ├── Radio.tsx │ │ ├── RadioGroup.stories.tsx │ │ ├── RadioGroup.tsx │ │ ├── StyledIcon.ts │ │ ├── StyledInput.ts │ │ ├── StyledRadio.ts │ │ ├── StyledRadioGroup.ts │ │ └── index.ts │ ├── select │ │ ├── Option.tsx │ │ ├── Select.stories.tsx │ │ ├── Select.tsx │ │ ├── StyledLabel.ts │ │ ├── StyledOption.ts │ │ ├── StyledSelect.ts │ │ └── index.ts │ ├── skeleton │ │ ├── Skeleton.stories.tsx │ │ ├── Skeleton.tsx │ │ ├── StyledContainer.ts │ │ ├── StyledSkeletonRow.ts │ │ └── index.ts │ ├── sorter │ │ ├── Sorter.tsx │ │ ├── StyledArrow.ts │ │ ├── StyledContainer.ts │ │ └── index.ts │ ├── table │ │ ├── StyledTable.ts │ │ ├── StyledTableData.ts │ │ ├── StyledTableHead.ts │ │ ├── StyledTableRow.ts │ │ ├── Table.stories.tsx │ │ ├── Table.tsx │ │ ├── getDataKey.ts │ │ └── index.ts │ ├── tabs │ │ ├── StyledTab.ts │ │ ├── StyledTabs.ts │ │ ├── Tab.tsx │ │ ├── TabPane.tsx │ │ ├── Tabs.stories.tsx │ │ ├── Tabs.tsx │ │ └── index.ts │ ├── tag │ │ ├── StyledIcon.ts │ │ ├── StyledTag.ts │ │ ├── Tag.stories.tsx │ │ ├── Tag.tsx │ │ └── index.tsx │ └── typography │ │ ├── Heading.stories.tsx │ │ ├── Heading.tsx │ │ ├── StyledHeading.ts │ │ ├── StyledText.ts │ │ ├── Text.stories.tsx │ │ ├── Text.tsx │ │ └── index.ts ├── icons │ ├── AddIcon.tsx │ ├── ArrowIcon.tsx │ ├── CancelIcon.tsx │ ├── CheckedIcon.tsx │ ├── ChevronLeft.tsx │ ├── ChevronRight.tsx │ ├── EmailIcon.tsx │ └── UserIcon.tsx ├── index.ts ├── types │ └── story.d.ts └── utils │ └── with-defaults.tsx ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["solid", "@babel/typescript"], 3 | "plugins": ["twin", "macros"] 4 | } 5 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # build 26 | dist/ 27 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "importOrder": ["^[w+]", "^[@]", "^[./]"], 7 | "importOrderSeparation": true 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | // const { addDecorator, configure } = require('@storybook/html') 2 | // const { createRoot } = require('solid-js') 3 | 4 | // automatically import all files ending in *.stories.js 5 | // configure(require.context('../stories', true, /\.stories\.js$/), module) 6 | 7 | // addDecorator((story) => { 8 | // return createRoot(() => story()) 9 | // }) 10 | 11 | module.exports = { 12 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 13 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 14 | } 15 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 550 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'solid-js' 2 | import { insert, template, createComponent } from 'solid-js/web' 3 | 4 | export const decorators = [ 5 | (Story) => 6 | createRoot(() => { 7 | // Wrap the component in a
tag. 8 | const el = template(`
`, 2).cloneNode(true) 9 | insert(el, createComponent(Story, {})) 10 | return el 11 | }), 12 | ] 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Launch-UI 2 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 3 | 4 | - Reporting a bug 5 | - Discussing the current state of the code 6 | - Submitting a fix 7 | - Proposing new features 8 | - Becoming a maintainer 9 | 10 | ## We Develop with Github 11 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 12 | 13 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 14 | Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've added code that should be tested, add tests. 18 | 3. If you've changed APIs, update the documentation. 19 | 4. Ensure the test suite passes. 20 | 5. Make sure your code lints. 21 | 6. Issue that pull request! 22 | 23 | ## Report bugs using Github's [issues](https://github.com/Launch-AI/launch-solid-ui/issues) 24 | We use GitHub issues to track public bugs. Report a bug by opening a new issue; it's that easy! 25 | 26 | ## Write bug reports with detail, background, and sample code 27 | 28 | **Great Bug Reports** tend to have: 29 | 30 | - A quick summary and/or background 31 | - Steps to reproduce 32 | - Be specific! 33 | - Give sample code if you can. 34 | - What you expected would happen 35 | - What actually happens 36 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Launch AI, https://launch.ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Launch UI - Solid JS Version 2 | 3 | A UI library containing a set of beautiful Solid JS components. 4 | 5 | ## [Join Solid UI Discord channel](https://discord.gg/yEU3YUjDQU) 6 | 7 | Storybook available at [launch-ui.netlify.app](https://launch-ui.netlify.app/). 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm install @launch/solid 13 | # or 14 | yarn add @launch/solid 15 | ``` 16 | 17 | ## Prerequisites 18 | 19 | - [yarn](https://yarnpkg.com/getting-started/install) _The project uses yarn over alternatives such as NPM/PNPM/..._ 20 | - [node](https://nodejs.org/) 21 | 22 | ## Clone & Setup 23 | 24 | Clone the repository to your local machine. 25 | 26 | ```bash 27 | $ git clone git@github.com:launch-ai/launch-ui.git 28 | ``` 29 | 30 | Install dependencies & git hooks. 31 | 32 | ```bash 33 | $ yarn install 34 | ``` 35 | 36 | Run the storybook. 37 | 38 | ```bash 39 | $ yarn run storybook 40 | ``` 41 | 42 | ### Styling 43 | 44 | Styling is done with [Emotion](https://emotion.sh/) and [twin.macro](https://github.com/ben-rogerson/twin.macro) using a custom styled util. 45 | 46 | Example: 47 | 48 | 49 | ```tsx 50 | import tw from 'twin.macro' 51 | import { styled } from './src/utils/styled' 52 | 53 | type ButtonProps = { block: boolean } 54 | 55 | type StyledButtonProps = ButtonProps 56 | 57 | const baseStyles = tw`border border-black px-4 py-2` 58 | 59 | const blockStyles = ({ block }: StyledButtonProps) => block && tw`block w-full` 60 | 61 | const StyledButton = styled('button')( 62 | baseStyles, 63 | blockStyles, 64 | ) 65 | 66 | const Button: Component = (props) => { 67 | return ( 68 | 69 | {props.children} 70 | 71 | ) 72 | } 73 | 74 | export default Button 75 | ``` 76 | 77 | ### Commit messages 78 | 79 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) is used for all commit messages with the [AngularJS](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit) variation. 80 | 81 | This is enforced by a pre-commit hook with [commitlint](https://github.com/conventional-changelog/commitlint). If your commit message does not meet the conventional commits standard, the commit hook will fail. This helps with generating changelogs with version updates. 82 | 83 | In summary, the commit message header has the following format: 84 | 85 | ``` 86 | (): 87 | │ │ │ 88 | │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end. 89 | │ │ 90 | │ └─⫸ Commit Scope: products|orders|settings|... 91 | │ 92 | └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test 93 | ``` 94 | 95 | The commit type must be one of the following: 96 | 97 | - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 98 | - **ci**: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs) 99 | - **docs**: Documentation only changes 100 | - **feat**: A new feature 101 | - **fix**: A bug fix 102 | - **perf**: A code change that improves performance 103 | - **refactor**: A code change that neither fixes a bug nor adds a feature 104 | - **test**: Adding missing tests or correcting existing tests 105 | 106 | ### Pre-commit Hooks 107 | 108 | [Husky](https://typicode.github.io/husky/) and [lint-staged](https://github.com/okonet/lint-staged#readme) is used for pre-commit git hooks. 109 | 110 | When you execute a commit, the following commands will be executed: 111 | 112 | - Lint staged files: 113 | 114 | `npx lint-staged` 115 | 116 | - Validate commit message: 117 | 118 | `npx --no-install commitlint --edit ""` 119 | 120 | The configuration can be found in `.husky/pre-commit` and in package.json under `"lint-staged"`. 121 | 122 | Additional hooks can be added with: 123 | 124 | ```bash 125 | npx husky add .husky/pre-commit "yarn test" 126 | ``` 127 | 128 | 129 | ## [Join Solid UI Discord channel](https://discord.gg/yEU3YUjDQU) 130 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-angular'] } 2 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/gw/ntsjn55d3db3bf4ddl6qvwdc0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'], 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'babel', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: { 64 | // 'ts-jest': { 65 | // babelConfig: '.babelrc', 66 | // }, 67 | // }, 68 | 69 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 70 | // maxWorkers: "50%", 71 | 72 | // An array of directory names to be searched recursively up from the requiring module's location 73 | // moduleDirectories: [ 74 | // "node_modules" 75 | // ], 76 | 77 | // An array of file extensions your modules use 78 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'], 79 | 80 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 81 | moduleNameMapper: { 82 | '^solid-js$': 'solid-js/dist/solid.cjs', 83 | '^solid-js/web$': 'solid-js/web/dist/web.cjs', 84 | '^react-native$': 'react-native-web', 85 | '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', 86 | }, 87 | 88 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 89 | // modulePathIgnorePatterns: [], 90 | 91 | // Activates notifications for test results 92 | // notify: false, 93 | 94 | // An enum that specifies notification mode. Requires { notify: true } 95 | // notifyMode: "failure-change", 96 | 97 | // A preset that is used as a base for Jest's configuration 98 | // preset: undefined, 99 | 100 | // Run tests from one or more projects 101 | // projects: undefined, 102 | 103 | // Use this configuration option to add custom reporters to Jest 104 | // reporters: undefined, 105 | 106 | // Automatically reset mock state between every test 107 | // resetMocks: false, 108 | 109 | // Reset the module registry before running each individual test 110 | // resetModules: false, 111 | 112 | // A path to a custom resolver 113 | // resolver: undefined, 114 | 115 | // Automatically restore mock state between every test 116 | // restoreMocks: false, 117 | 118 | // The root directory that Jest should scan for tests and modules within 119 | // rootDir: undefined, 120 | 121 | // A list of paths to directories that Jest should use to search for files in 122 | // roots: [ 123 | // "" 124 | // ], 125 | 126 | // Allows you to use a custom runner instead of Jest's default test runner 127 | // runner: "jest-runner", 128 | 129 | // The paths to modules that run some code to configure or set up the testing environment before each test 130 | setupFiles: [ 131 | '/Users/ari/dev/launch/launch-ui/node_modules/react-app-polyfill/jsdom.js', 132 | ], 133 | 134 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 135 | // setupFilesAfterEnv: [], 136 | 137 | // The number of seconds after which a test is considered as slow and reported as such in the results. 138 | // slowTestThreshold: 5, 139 | 140 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 141 | // snapshotSerializers: [], 142 | 143 | // The test environment that will be used for testing 144 | testEnvironment: 'jest-environment-jsdom', 145 | 146 | // Options that will be passed to the testEnvironment 147 | // testEnvironmentOptions: {}, 148 | 149 | // Adds a location field to test results 150 | // testLocationInResults: false, 151 | 152 | // The glob patterns Jest uses to detect test files 153 | // testMatch: [ 154 | // "**/__tests__/**/*.[jt]s?(x)", 155 | // "**/?(*.)+(spec|test).[tj]s?(x)" 156 | // ], 157 | 158 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 159 | // testPathIgnorePatterns: [ 160 | // "/node_modules/" 161 | // ], 162 | 163 | // The regexp pattern or array of patterns that Jest uses to detect test files 164 | // testRegex: [], 165 | 166 | // This option allows the use of a custom results processor 167 | // testResultsProcessor: undefined, 168 | 169 | // This option allows use of a custom test runner 170 | // testRunner: "jest-circus/runner", 171 | 172 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 173 | // testURL: "http://localhost", 174 | 175 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 176 | // timers: "real", 177 | 178 | // A map from regular expressions to paths to transformers 179 | transform: { 180 | '^.+\\.(js|jsx|ts|tsx)$': __dirname + '/jest/babelTransform.js', 181 | '^.+\\.css$': __dirname + '/jest/cssTransform.js', 182 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': __dirname + '/jest/fileTransform.js', 183 | }, 184 | 185 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 186 | transformIgnorePatterns: [ 187 | '[/\\\\]node_modules[/\\\\].+\\.(cjs|js|jsx|ts|tsx)$', 188 | '^.+\\.module\\.(css|sass|scss)$', 189 | ], 190 | 191 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 192 | // unmockedModulePathPatterns: undefined, 193 | 194 | // Indicates whether each individual test should be reported during the run 195 | // verbose: undefined, 196 | 197 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 198 | // watchPathIgnorePatterns: [], 199 | 200 | // Whether to use watchman for file crawling 201 | // watchman: true, 202 | } 203 | -------------------------------------------------------------------------------- /jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | const babelJest = require('babel-jest').default 2 | 3 | module.exports = babelJest.createTransformer({ 4 | presets: [ 5 | require.resolve('@babel/preset-env'), 6 | require.resolve('@babel/preset-typescript'), 7 | require.resolve('babel-preset-solid'), 8 | ], 9 | babelrc: false, 10 | configFile: false, 11 | }) 12 | -------------------------------------------------------------------------------- /jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014-present, Facebook, Inc. 3 | */ 4 | 'use strict' 5 | 6 | // This is a custom Jest transformer turning style imports into empty objects. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process() { 11 | return 'module.exports = {};' 12 | }, 13 | getCacheKey() { 14 | // The output is always the same. 15 | return 'cssTransform' 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)) 11 | return `module.exports = ${assetFilename};` 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@launch/solid", 3 | "author": "Launch AI Team", 4 | "description": "A high performance UI library for Solid JS - based on the Launch Design principles.", 5 | "version": "0.1.4", 6 | "private": false, 7 | "module": "dist/index.js", 8 | "types": "dist/types.d.ts", 9 | "files": [ 10 | "dist/**" 11 | ], 12 | "keywords": [ 13 | "solid", 14 | "ui", 15 | "components", 16 | "library" 17 | ], 18 | "homepage": "https://github.com/Launch-AI/launch-solid-ui#readme", 19 | "bugs": "https://github.com/Launch-AI/launch-solid-ui/issues", 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:Launch-AI/launch-solid-ui.git" 23 | }, 24 | "license": "MIT", 25 | "contributors": [ 26 | "Ari Seyhun (https://ariseyhun.com)" 27 | ], 28 | "scripts": { 29 | "build": "rollup -c rollup.config.js", 30 | "test": "jest", 31 | "storybook": "start-storybook -p 6006", 32 | "build-storybook": "build-storybook", 33 | "format": "yarn prettier --write .", 34 | "prepare": "husky install", 35 | "prepublishOnly": "yarn build" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.14.5", 39 | "@babel/preset-env": "^7.14.5", 40 | "@babel/preset-typescript": "^7.14.5", 41 | "@commitlint/cli": "^12.1.4", 42 | "@commitlint/config-angular": "^12.1.4", 43 | "@rollup/plugin-image": "^2.0.6", 44 | "@storybook/addon-actions": "^6.3.0", 45 | "@storybook/addon-essentials": "^6.3.0", 46 | "@storybook/addon-links": "^6.3.0", 47 | "@storybook/html": "^6.3.0", 48 | "@trivago/prettier-plugin-sort-imports": "^2.0.2", 49 | "@types/jest": "^26.0.23", 50 | "@types/lodash": "^4.14.170", 51 | "@types/node": "15.12", 52 | "babel-jest": "^27.0.2", 53 | "babel-loader": "^8.2.2", 54 | "babel-plugin-macros": "^3.1.0", 55 | "babel-plugin-twin": "^1.0.2", 56 | "babel-preset-solid": "^1.0.0", 57 | "husky": "^6.0.0", 58 | "jest": "^27.0.4", 59 | "react-app-polyfill": "^2.0.0", 60 | "rollup": "^2.51.2", 61 | "rollup-plugin-babel": "^4.4.0", 62 | "rollup-plugin-commonjs": "^10.1.0", 63 | "rollup-plugin-dts": "^3.0.2", 64 | "rollup-plugin-node-resolve": "^5.2.0", 65 | "rollup-plugin-peer-deps-external": "^2.2.4", 66 | "ts-node": "^10.0.0", 67 | "twin.macro": "^2.5.0", 68 | "typescript": "4.3" 69 | }, 70 | "dependencies": { 71 | "@emotion/css": "^11.1.3", 72 | "@emotion/is-prop-valid": "^1.1.0", 73 | "@emotion/utils": "^1.0.0", 74 | "emotion-solid": "^1.1.1", 75 | "lodash": "^4.17.21", 76 | "solid-js": "^1.0.0" 77 | }, 78 | "peerDependencies": { 79 | "@emotion/css": "^11.1", 80 | "solid-js": "^1.0.0" 81 | }, 82 | "browserslist": [ 83 | "Chrome 74", 84 | "Firefox 63", 85 | "Safari 11", 86 | "Edge 17", 87 | "Node 10" 88 | ], 89 | "lint-staged": { 90 | "*.{js,jsx,ts,tsx,css,md,json}": "prettier --write" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import dts from 'rollup-plugin-dts' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import peerDepsExternal from 'rollup-plugin-peer-deps-external' 6 | 7 | import pkg from './package.json' 8 | 9 | const config = { 10 | name: 'Launch UI', 11 | extensions: ['.ts', '.tsx'], 12 | } 13 | 14 | export default [ 15 | { 16 | input: 'src/index.ts', 17 | output: [ 18 | { 19 | // ES Modules: Modern browser imports 20 | 21 | // Browser usage: 22 | // 26 | 27 | // js/tsx file usage: 28 | // import { func } from 'my-lib'; 29 | // func(); 30 | file: pkg.module, 31 | format: 'es', 32 | sourcemap: true, 33 | }, 34 | ], 35 | plugins: [ 36 | // Automatically add peerDependencies to the `external` config 37 | // https://rollupjs.org/guide/en/#external 38 | peerDepsExternal(), 39 | 40 | // External modules not to include in your bundle (eg: 'lodash', 'moment' etc.) 41 | // https://rollupjs.org/guide/en/#external 42 | // external: [] 43 | 44 | resolve({ extensions: config.extensions }), 45 | 46 | commonjs(), 47 | 48 | babel({ 49 | extensions: config.extensions, 50 | include: ['src/**/*'], 51 | exclude: 'node_modules/**', 52 | }), 53 | ], 54 | external: ['solid-js/web'], 55 | }, 56 | { 57 | input: 'src/index.ts', 58 | output: [{ file: 'dist/types.d.ts', format: 'es' }], 59 | plugins: [dts()], 60 | }, 61 | ] 62 | -------------------------------------------------------------------------------- /src/components/avatar/Avatar.stories.tsx: -------------------------------------------------------------------------------- 1 | import Avatar, { AvatarProps } from './Avatar' 2 | // @ts-ignore 3 | import imageFile from './static/image.svg' 4 | 5 | const TypeTemplate: Story = (args) => 6 | 7 | export const Primary = TypeTemplate.bind({}) 8 | Primary.args = { 9 | type: 'filled', 10 | shape: 'rounded', 11 | color: 'purple', 12 | shadow: false, 13 | } 14 | 15 | export const Secondary = TypeTemplate.bind({}) 16 | Secondary.args = { 17 | type: 'outlined', 18 | shape: 'square', 19 | color: 'green', 20 | shadow: true, 21 | characters: 'AS', 22 | } 23 | 24 | export const Tertiary = TypeTemplate.bind({}) 25 | Tertiary.args = { 26 | type: 'outlined', 27 | shape: 'rounded', 28 | color: 'primary', 29 | shadow: true, 30 | imagePath: imageFile, 31 | alt: 'Avatar', 32 | } 33 | 34 | export default { 35 | title: 'Avatar', 36 | component: Avatar, 37 | argTypes: { 38 | type: { 39 | options: ['filled', 'outlined'], 40 | control: { type: 'select' }, 41 | }, 42 | shape: { 43 | options: ['rounded', 'square'], 44 | control: { type: 'select' }, 45 | }, 46 | color: { 47 | options: [ 48 | 'default', 49 | 'primary', 50 | 'secondary', 51 | 'green', 52 | 'teal', 53 | 'grey', 54 | 'purple', 55 | ], 56 | control: { type: 'select' }, 57 | }, 58 | shadow: { 59 | control: { type: 'boolean' }, 60 | }, 61 | imagePath: { 62 | type: { name: 'string', required: false }, 63 | defaultValue: '', 64 | control: { type: 'text' }, 65 | }, 66 | alt: { 67 | type: { name: 'string', required: false }, 68 | defaultValue: '', 69 | control: { type: 'text' }, 70 | }, 71 | characters: { 72 | type: { name: 'string', required: false }, 73 | defaultValue: '', 74 | control: { type: 'text' }, 75 | }, 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /src/components/avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import type { Component, JSX } from 'solid-js' 2 | 3 | import UserIcon from '../../icons/UserIcon' 4 | import withDefaults from '../../utils/with-defaults' 5 | import StyledAvatar from './StyledAvatar' 6 | 7 | export type Color = 8 | | 'default' 9 | | 'grey' 10 | | 'purple' 11 | | 'green' 12 | | 'teal' 13 | | 'primary' 14 | | 'secondary' 15 | | undefined 16 | 17 | export type Shape = 'rounded' | 'square' 18 | 19 | export type Type = 'filled' | 'outlined' 20 | 21 | export type AvatarProps = { 22 | class?: string 23 | type?: Type 24 | shape?: Shape 25 | color?: Color 26 | shadow?: boolean 27 | imagePath?: string 28 | alt?: string 29 | characters?: string 30 | } 31 | 32 | const Avatar: Component = (props) => { 33 | let icon: JSX.Element 34 | if (props.imagePath) { 35 | icon = ( 36 | {props.alt 37 | ) 38 | } else if (props.characters) { 39 | icon = props.characters 40 | } else { 41 | icon = 42 | } 43 | 44 | return ( 45 | 54 | {icon} 55 | 56 | ) 57 | } 58 | 59 | export default withDefaults(Avatar, { 60 | type: 'filled', 61 | shape: 'square', 62 | color: 'primary', 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/avatar/StyledAvatar.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | import { AvatarProps, Type, Color } from './Avatar' 5 | 6 | export type AvatarStyledProps = { 7 | type?: Type 8 | border?: string 9 | color?: Color 10 | shape?: AvatarProps['shape'] 11 | shadow?: boolean 12 | imagePath?: string 13 | characters?: string 14 | } 15 | 16 | const baseStyles = tw`w-12 h-12 flex justify-center items-center border-0 border-solid border-transparent text-white rounded-full text-base shadow-none` 17 | 18 | const backgroundStyles = ({ color, type }: AvatarStyledProps) => [ 19 | color === 'default' && tw`bg-light-100`, 20 | color === 'green' && tw`bg-green-light`, 21 | color === 'purple' && tw`bg-purple-light`, 22 | color === 'grey' && tw`bg-grey-light`, 23 | color === 'teal' && tw`bg-teal-light`, 24 | type === 'filled' && [ 25 | color === 'primary' && tw`bg-blue`, 26 | color === 'secondary' && tw`bg-orange`, 27 | ], 28 | type === 'outlined' && [ 29 | color === 'primary' && tw`bg-blue-light`, 30 | color === 'secondary' && tw`bg-orange-light`, 31 | ], 32 | ] 33 | 34 | const borderStyles = ({ border, color, type }: AvatarStyledProps) => [ 35 | border !== '0' && tw`border-2`, 36 | type === 'outlined' && [ 37 | color === 'default' && tw`border-light-100`, 38 | color === 'green' && tw`border-green`, 39 | color === 'purple' && tw`border-purple`, 40 | color === 'grey' && tw`border-grey-dark`, 41 | color === 'primary' && tw`border-blue`, 42 | color === 'secondary' && tw`border-orange`, 43 | color === 'teal' && tw`border-teal`, 44 | ], 45 | ] 46 | 47 | const textStyles = ({ color, type }: AvatarStyledProps) => [ 48 | color === 'default' && tw`text-blue`, 49 | color === 'green' && tw`text-green`, 50 | color === 'purple' && tw`text-purple`, 51 | color === 'grey' && tw`text-grey-dark`, 52 | color === 'teal' && tw`text-teal`, 53 | type === 'outlined' && [ 54 | color === 'primary' && tw`text-blue`, 55 | color === 'secondary' && tw`text-orange`, 56 | ], 57 | type === 'filled' && [ 58 | color === 'primary' && tw`text-white`, 59 | color === 'secondary' && tw`text-white`, 60 | ], 61 | ] 62 | 63 | const radiusStyles = ({ shape }: AvatarStyledProps) => 64 | shape === 'square' && tw`rounded-2xl` 65 | 66 | const fontStyles = ({ imagePath, characters }: AvatarStyledProps) => 67 | imagePath === '' && 68 | characters !== '' && 69 | characters !== undefined && 70 | tw`text-2xl` 71 | 72 | const shadowStyles = ({ shadow }: AvatarStyledProps) => ({ 73 | boxShadow: shadow 74 | ? '0px 2px 4px rgba(18, 17, 17, 0.04), 0px 8px 16px rgba(113, 112, 112, 0.16)' 75 | : 'none', 76 | }) 77 | 78 | const StyledAvatar = styled('div')( 79 | baseStyles, 80 | backgroundStyles, 81 | borderStyles, 82 | textStyles, 83 | radiusStyles, 84 | fontStyles, 85 | shadowStyles 86 | ) 87 | 88 | export default StyledAvatar 89 | -------------------------------------------------------------------------------- /src/components/avatar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from './Avatar' 2 | export type { AvatarProps } from './Avatar' 3 | -------------------------------------------------------------------------------- /src/components/badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, BadgeProps } from '.' 2 | import EmailIcon from '../../icons/EmailIcon' 3 | import { Avatar } from '../avatar' 4 | import { Button } from '../button' 5 | 6 | const BadgeTemplate: Story = (args) => 7 | 8 | export const Normal = BadgeTemplate.bind({}) 9 | Normal.args = { 10 | count: 1, 11 | badgeColor: 'green', 12 | children: ( 13 | 14 | ), 15 | offsetX: 9, 16 | offsetY: 7, 17 | } 18 | 19 | export const Square = BadgeTemplate.bind({}) 20 | Square.args = { 21 | count: 1, 22 | badgeColor: 'danger', 23 | children: ( 24 | 25 | ), 26 | offsetX: 9, 27 | offsetY: 7, 28 | } 29 | 30 | export const Dot = BadgeTemplate.bind({}) 31 | Dot.args = { 32 | badgeColor: 'green', 33 | children: ( 34 | 35 | ), 36 | offsetX: 2, 37 | offsetY: 6, 38 | } 39 | 40 | export const BadgePlacement = BadgeTemplate.bind({}) 41 | BadgePlacement.args = { 42 | badgeColor: 'green', 43 | badgePlacement: 'top-left', 44 | children: ( 45 | 46 | ), 47 | offsetX: 7, 48 | offsetY: 7, 49 | } 50 | 51 | export const CountLimit = BadgeTemplate.bind({}) 52 | CountLimit.args = { 53 | count: 999, 54 | badgeColor: 'danger', 55 | children: ( 56 | 57 | ), 58 | offsetX: 20, 59 | offsetY: 7, 60 | } 61 | 62 | export const Icon = BadgeTemplate.bind({}) 63 | Icon.args = { 64 | count: 1, 65 | badgeColor: 'danger', 66 | children: , 67 | offsetX: 14, 68 | offsetY: 1, 69 | } 70 | 71 | export const WithButton = BadgeTemplate.bind({}) 72 | WithButton.args = { 73 | count: 1, 74 | badgeColor: 'danger', 75 | children: , 76 | offsetX: 12, 77 | offsetY: 3, 78 | } 79 | 80 | export default { 81 | title: 'Badge', 82 | component: Badge, 83 | argTypes: { 84 | badgeColor: { 85 | options: [ 86 | 'default', 87 | 'primary', 88 | 'secondary', 89 | 'green', 90 | 'teal', 91 | 'grey', 92 | 'purple', 93 | ], 94 | control: { type: 'select' }, 95 | }, 96 | color: { 97 | options: [ 98 | 'default', 99 | 'primary', 100 | 'secondary', 101 | 'green', 102 | 'teal', 103 | 'grey', 104 | 'purple', 105 | ], 106 | control: { type: 'select' }, 107 | }, 108 | badgePlacement: { 109 | options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], 110 | control: { type: 'select' }, 111 | }, 112 | }, 113 | } 114 | -------------------------------------------------------------------------------- /src/components/badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createMemo, Show } from 'solid-js' 2 | import type { JSX } from 'solid-js' 3 | 4 | import withDefaults from '../../utils/with-defaults' 5 | import StyledContainer from './StyledContainer' 6 | import StyledIcon from './StyledIcon' 7 | 8 | export type Color = 9 | | 'default' 10 | | 'grey' 11 | | 'purple' 12 | | 'green' 13 | | 'teal' 14 | | 'primary' 15 | | 'secondary' 16 | | 'danger' 17 | 18 | export type BadgePlacement = 19 | | 'top-left' 20 | | 'top-right' 21 | | 'bottom-left' 22 | | 'bottom-right' 23 | 24 | export type BadgeProps = { 25 | count?: number 26 | countLimit?: number 27 | color?: Color 28 | children?: JSX.Element 29 | badgeColor?: Color 30 | badgePlacement?: BadgePlacement 31 | offsetX?: number 32 | offsetY?: number 33 | } 34 | 35 | const Badge: Component = (props) => { 36 | const limit = createMemo(() => 37 | props.count! > props.countLimit! ? `${props.countLimit}+` : props.count 38 | ) 39 | 40 | return ( 41 | 42 | 43 | {limit()} 44 | 45 | {props.children} 46 | 47 | ) 48 | } 49 | 50 | export default withDefaults(Badge, { 51 | countLimit: 99, 52 | }) 53 | -------------------------------------------------------------------------------- /src/components/badge/StyledContainer.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import type { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | import type { BadgeProps } from './Badge' 6 | 7 | type StyledContainerProps = BadgeProps & JSX.IntrinsicElements['div'] 8 | 9 | const baseStyles = tw`relative inline-block` 10 | 11 | const backgroundStyles = ({ color }: StyledContainerProps) => [ 12 | color === 'green' && tw`bg-green`, 13 | color === 'purple' && tw`bg-purple`, 14 | color === 'grey' && tw`bg-grey-dark`, 15 | color === 'teal' && tw`bg-teal`, 16 | ] 17 | 18 | const textStyles = tw`text-white` 19 | 20 | const StyledContainer = styled('div')( 21 | baseStyles, 22 | backgroundStyles, 23 | textStyles 24 | ) 25 | 26 | export default StyledContainer 27 | -------------------------------------------------------------------------------- /src/components/badge/StyledIcon.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import type { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | import type { BadgeProps } from '.' 6 | 7 | type StyledIconProps = BadgeProps & JSX.IntrinsicElements['span'] 8 | 9 | const baseStyles = [ 10 | tw`absolute flex h-4 w-4 bg-blue justify-center items-center text-xxs font-sans border border-white rounded-full z-50`, 11 | ] 12 | 13 | const backgroundStyles = ({ badgeColor }: StyledIconProps) => [ 14 | badgeColor === 'default' && tw`bg-light-300`, 15 | badgeColor === 'primary' && tw`bg-primary`, 16 | badgeColor === 'secondary' && tw`bg-secondary`, 17 | badgeColor === 'green' && tw`bg-green`, 18 | badgeColor === 'purple' && tw`bg-purple`, 19 | badgeColor === 'grey' && tw`bg-grey-dark`, 20 | badgeColor === 'teal' && tw`bg-teal`, 21 | badgeColor === 'danger' && tw`bg-danger`, 22 | ] 23 | 24 | const iconPositionStyles = ({ 25 | badgePlacement = 'top-right', 26 | offsetX, 27 | offsetY, 28 | }: StyledIconProps) => [ 29 | { 30 | transform: `translate(calc(-50% + ${offsetX || 0}px), calc(-50% + ${ 31 | offsetY || 0 32 | }px))`, 33 | top: 34 | badgePlacement === 'top-right' || badgePlacement === 'top-left' 35 | ? '0%' 36 | : 'auto', 37 | bottom: 38 | badgePlacement === 'bottom-right' || badgePlacement === 'bottom-left' 39 | ? '0%' 40 | : 'auto', 41 | left: 42 | badgePlacement === 'top-left' || badgePlacement === 'bottom-left' 43 | ? '0%' 44 | : 'auto', 45 | right: 46 | badgePlacement === 'top-right' || badgePlacement === 'bottom-right' 47 | ? '0%' 48 | : 'auto', 49 | }, 50 | ] 51 | 52 | const dotStyles = ({ count, countLimit = 99 }: StyledIconProps) => [ 53 | !count && tw`h-2.5 w-2.5`, 54 | count && count >= countLimit && tw`w-auto px-0.5`, 55 | ] 56 | 57 | const StyledIcon = styled('span')( 58 | baseStyles, 59 | backgroundStyles, 60 | iconPositionStyles, 61 | dotStyles 62 | ) 63 | 64 | export default StyledIcon 65 | -------------------------------------------------------------------------------- /src/components/badge/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Badge } from './Badge' 2 | export type { BadgeProps } from './Badge' 3 | -------------------------------------------------------------------------------- /src/components/button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import AddIcon from '../../icons/AddIcon' 2 | import Button, { ButtonProps } from './Button' 3 | 4 | const ButtonTemplate: Story = (args) => , div) 8 | console.log(div.innerHTML) 9 | div.textContent = '' 10 | dispose() 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { Component, JSX } from 'solid-js' 2 | 3 | import withDefaults from '../../utils/with-defaults' 4 | import StyledButton from './StyledButton' 5 | import StyledIcon from './StyledIcon' 6 | 7 | export type ButtonProps = { 8 | class?: string 9 | block?: boolean 10 | variant?: 'default' | 'primary' | 'secondary' | 'ghost' 11 | type?: 'filled' | 'outlined' | 'text' 12 | size?: 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large' 13 | iconPosition?: 'before' | 'after' 14 | shape?: 'rounded' | 'circle' 15 | disabled?: boolean 16 | onClick?: JSX.IntrinsicElements['button']['onClick'] 17 | } & ( 18 | | // Must specify icon, children or both - but never none of them 19 | { 20 | icon: JSX.Element 21 | children?: never 22 | } 23 | | { icon?: never; children: any } 24 | | { icon: JSX.Element; children: any } 25 | ) 26 | 27 | const Button: Component = (props) => { 28 | const { size, icon, iconPosition, children } = props 29 | 30 | const beforeIcon = iconPosition === 'before' && icon && ( 31 | 32 | {icon} 33 | 34 | ) 35 | 36 | const afterIcon = iconPosition === 'after' && icon && ( 37 | 38 | {icon} 39 | 40 | ) 41 | 42 | return ( 43 | 44 | {beforeIcon} 45 | {children} 46 | {afterIcon} 47 | 48 | ) 49 | } 50 | 51 | export default withDefaults(Button, { 52 | variant: 'default', 53 | type: 'filled', 54 | size: 'medium', 55 | iconPosition: 'before', 56 | shape: 'rounded', 57 | }) 58 | -------------------------------------------------------------------------------- /src/components/button/StyledButton.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | import type { ButtonProps } from './Button' 5 | 6 | type StyledButtonProps = ButtonProps & { 7 | hasChildren?: boolean 8 | } 9 | 10 | const baseStyles = tw`inline-flex justify-center items-center font-medium border border-solid border-transparent rounded-xl cursor-pointer select-none transform duration-200 ease-out outline-none active:scale-95` 11 | 12 | // Block styles 13 | const blockStyles = (props: StyledButtonProps) => props.block && tw`flex w-full` 14 | 15 | // Size styles 16 | const extraSmallStyles = ({ size, hasChildren, shape }: StyledButtonProps) => 17 | size === 'extra-small' && [ 18 | tw`text-xs h-6 px-6 rounded-lg`, 19 | (!hasChildren || shape === 'circle') && tw`px-0 w-6`, 20 | ] 21 | 22 | const smallStyles = ({ size, hasChildren, shape }: StyledButtonProps) => 23 | size === 'small' && [ 24 | tw`text-xs h-8 px-5 rounded-lg`, 25 | (!hasChildren || shape === 'circle') && tw`px-0 w-8`, 26 | ] 27 | 28 | const mediumStyles = ({ size, hasChildren, shape }: StyledButtonProps) => 29 | size === 'medium' && [ 30 | tw`text-sm h-9 px-6 rounded-xl`, 31 | (!hasChildren || shape === 'circle') && tw`px-0 w-9`, 32 | ] 33 | 34 | const largeStyles = ({ size, hasChildren, shape }: StyledButtonProps) => 35 | size === 'large' && [ 36 | tw`text-base h-12 px-8 rounded-2xl`, 37 | (!hasChildren || shape === 'circle') && tw`px-0 w-12`, 38 | ] 39 | 40 | const extraLargeStyles = ({ size, hasChildren, shape }: StyledButtonProps) => 41 | size === 'extra-large' && [ 42 | tw`text-lg h-14 px-8 rounded-3xl`, 43 | (!hasChildren || shape === 'circle') && tw`px-0 w-14`, 44 | ] 45 | 46 | // Variant styles 47 | const defaultStyles = ({ variant, type }: StyledButtonProps) => 48 | variant === 'default' && [ 49 | type === 'filled' && 50 | tw`text-dark-400 bg-light-300 hover:bg-light-100 active:bg-light-100`, 51 | type === 'outlined' && 52 | tw`text-dark-400 bg-transparent border border-dark-400 hover:(text-dark-500 border-dark-500) active:(text-dark-500 border-dark-500)`, 53 | type === 'text' && 54 | tw`text-dark-400 bg-transparent hover:text-dark-500 active:text-dark-500`, 55 | ] 56 | 57 | const primaryStyles = ({ variant, type }: StyledButtonProps) => 58 | variant === 'primary' && [ 59 | type === 'filled' && 60 | tw`text-white bg-primary hover:bg-primary-light active:bg-primary-dark`, 61 | type === 'outlined' && 62 | tw`text-primary bg-transparent border border-primary hover:(text-primary-light border-primary-light) active:(text-primary-dark border-primary-dark)`, 63 | type === 'text' && 64 | tw`text-primary bg-transparent hover:text-primary-light active:text-primary-dark`, 65 | ] 66 | 67 | const secondaryStyles = (props: StyledButtonProps) => 68 | props.variant === 'secondary' && [ 69 | props.type === 'filled' && 70 | tw`text-white bg-secondary hover:bg-secondary-light active:bg-secondary-dark`, 71 | props.type === 'outlined' && 72 | tw`text-secondary bg-transparent border border-secondary hover:(text-secondary-light border-secondary-light) active:(text-secondary-dark border-secondary-dark)`, 73 | props.type === 'text' && 74 | tw`text-secondary bg-transparent hover:text-secondary-light active:text-secondary-dark`, 75 | ] 76 | 77 | const ghostStyles = ({ variant }: StyledButtonProps) => 78 | variant === 'ghost' && 79 | tw`text-dark-100 bg-transparent hover:bg-light-300 active:bg-light-100` 80 | 81 | // Disabled styles 82 | const disabledStyles = ({ disabled, variant, type }: StyledButtonProps) => 83 | disabled && [ 84 | [ 85 | type === 'filled' && 86 | tw`bg-light-300 text-dark-500 cursor-not-allowed hover:bg-light-300`, 87 | type === 'outlined' && 88 | tw`border-dark-500 text-dark-500 cursor-not-allowed hover:(text-dark-500 border-dark-500)`, 89 | type === 'text' && 90 | tw`text-dark-500 cursor-not-allowed hover:text-dark-500`, 91 | ], 92 | variant === 'ghost' && tw`hover:bg-light-300`, 93 | ] 94 | 95 | // Shape styles 96 | const circleStyles = ({ shape }: StyledButtonProps) => 97 | shape === 'circle' && tw`rounded-full` 98 | 99 | const StyledButton = styled('button')( 100 | baseStyles, 101 | blockStyles, 102 | extraSmallStyles, 103 | smallStyles, 104 | mediumStyles, 105 | largeStyles, 106 | extraLargeStyles, 107 | defaultStyles, 108 | primaryStyles, 109 | secondaryStyles, 110 | ghostStyles, 111 | disabledStyles, 112 | circleStyles 113 | ) 114 | 115 | export default StyledButton 116 | -------------------------------------------------------------------------------- /src/components/button/StyledIcon.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | import type { ButtonProps } from './Button' 5 | 6 | type StyledIconProps = { 7 | position?: ButtonProps['iconPosition'] 8 | size?: ButtonProps['size'] 9 | } 10 | 11 | const baseStyles = tw`flex justify-center items-center` 12 | 13 | const positionStyles = ({ position }: StyledIconProps) => [ 14 | position === 'before' && tw`pr-2.5`, 15 | position === 'after' && tw`pl-2.5`, 16 | ] 17 | 18 | const sizeStyles = ({ size }: StyledIconProps) => [ 19 | size === 'small' && { svg: tw`w-3 h-3` }, 20 | size === 'medium' && { svg: tw`w-3 h-3` }, 21 | size === 'large' && { svg: tw`w-3.5 h-3.5` }, 22 | size === 'extra-large' && { svg: tw`w-4 h-4` }, 23 | ] 24 | 25 | const StyledIcon = styled('span')([ 26 | baseStyles, 27 | positionStyles, 28 | sizeStyles, 29 | ]) 30 | 31 | export default StyledIcon 32 | -------------------------------------------------------------------------------- /src/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button' 2 | export type { ButtonProps } from './Button' 3 | -------------------------------------------------------------------------------- /src/components/checkbox/Checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js' 2 | 3 | import Checkbox, { CheckboxProps } from './Checkbox' 4 | 5 | const CheckboxTemplate: Story = (args) => 6 | 7 | export const Enabled = CheckboxTemplate.bind({}) 8 | Enabled.args = { 9 | checked: true, 10 | indeterminate: false, 11 | disabled: false, 12 | } 13 | 14 | export const Indeterminate = CheckboxTemplate.bind({}) 15 | Indeterminate.args = { 16 | checked: true, 17 | indeterminate: true, 18 | disabled: false, 19 | } 20 | 21 | export const Disabled = CheckboxTemplate.bind({}) 22 | Disabled.args = { 23 | checked: true, 24 | indeterminate: false, 25 | disabled: true, 26 | } 27 | 28 | export default { 29 | title: 'Checkbox', 30 | component: Checkbox, 31 | argTypes: {}, 32 | } 33 | -------------------------------------------------------------------------------- /src/components/checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createEffect, createSignal } from 'solid-js' 2 | 3 | import StyledCheckbox from './StyledCheckbox' 4 | import StyledHiddenCheckbox from './StyledHiddenCheckbox' 5 | import StyledLabel from './StyledLabel' 6 | 7 | export type CheckboxProps = { 8 | class?: string 9 | block?: boolean 10 | label?: string 11 | checked?: boolean 12 | indeterminate?: boolean 13 | disabled?: boolean 14 | onChange?: (checked: boolean) => void 15 | } 16 | 17 | const Checkbox: Component = (props) => { 18 | const [isChecked, setIsChecked] = createSignal(props.checked || false) 19 | 20 | createEffect(() => { 21 | if (props.checked != null) { 22 | setIsChecked(props.checked) 23 | } 24 | }) 25 | 26 | const handleChange = (ev: Event) => { 27 | if (props.disabled) return 28 | const newChecked = (ev.target as HTMLInputElement).checked 29 | setIsChecked(newChecked) 30 | props.onChange && props.onChange(newChecked) 31 | } 32 | 33 | return ( 34 | 35 | 41 | 46 | 47 | ) 48 | } 49 | 50 | export default Checkbox 51 | -------------------------------------------------------------------------------- /src/components/checkbox/StyledCheckbox.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | import type { CheckboxProps } from './Checkbox' 5 | 6 | type StyledCheckboxProps = CheckboxProps & JSX.IntrinsicElements['span'] 7 | 8 | const baseStyles = [ 9 | tw`relative w-4 h-4 rounded border-2 border-solid border-primary active:border-primary-dark`, 10 | { 11 | '::after': tw`content absolute top-1/2 left-1/2 pointer-events-none w-9 h-9 bg-primary bg-opacity-10 rounded-full transform -translate-x-1/2 -translate-y-1/2 scale-0 transition duration-300 ease-out`, 12 | ':hover::after': tw`transform -translate-x-1/2 -translate-y-1/2 scale-100`, 13 | }, 14 | ] 15 | 16 | const blockStyles = ({ block }: StyledCheckboxProps) => 17 | block && tw`inline-block` 18 | 19 | const checkedStyles = ({ checked }: StyledCheckboxProps) => 20 | checked && [ 21 | tw`bg-primary bg-center bg-cover bg-no-repeat active:bg-primary-dark`, 22 | { 23 | backgroundImage: `url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M2 9.00002L7 14L16 5.00002L14.59 3.58002L7 11.17L3.41 7.59002L2 9.00002Z' fill='white'/%3E%3C/svg%3E%0A")`, 24 | }, 25 | ] 26 | 27 | const indeterminateStyles = ({ indeterminate }: StyledCheckboxProps) => 28 | indeterminate && { 29 | backgroundImage: `url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='4' y='8' width='10' height='2' fill='white'/%3E%3C/svg%3E%0A")`, 30 | } 31 | 32 | const disabledStyles = ({ disabled, checked }: StyledCheckboxProps) => 33 | disabled && [ 34 | tw`border-dark-500 cursor-default active:border-dark-500`, 35 | checked && tw`bg-dark-500 active:bg-dark-500`, 36 | { '::after': tw`hidden` }, 37 | ] 38 | 39 | const StyledCheckbox = styled('span')( 40 | baseStyles, 41 | blockStyles, 42 | checkedStyles, 43 | indeterminateStyles, 44 | disabledStyles 45 | ) 46 | 47 | export default StyledCheckbox 48 | -------------------------------------------------------------------------------- /src/components/checkbox/StyledHiddenCheckbox.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import type { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | type StyledCheckboxProps = JSX.IntrinsicElements['input'] 6 | 7 | const baseStyles = tw`absolute w-0 h-0 opacity-0` 8 | 9 | const StyledCheckbox = styled('input')(baseStyles) 10 | 11 | export default StyledCheckbox 12 | -------------------------------------------------------------------------------- /src/components/checkbox/StyledLabel.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | type StyledLabelProps = { block?: boolean } 5 | 6 | const baseStyles = tw`relative inline-flex` 7 | 8 | const blockStyles = ({ block }: StyledLabelProps) => block && tw`flex` 9 | 10 | const StyledLabel = styled('label')(baseStyles, blockStyles) 11 | 12 | export default StyledLabel 13 | -------------------------------------------------------------------------------- /src/components/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Checkbox } from './Checkbox' 2 | export type { CheckboxProps } from './Checkbox' 3 | -------------------------------------------------------------------------------- /src/components/collapse/Collapse.stories.tsx: -------------------------------------------------------------------------------- 1 | import Collapse from './Collapse' 2 | 3 | const CollapseTemplate: Story = (args) => 4 | 5 | export const Default = CollapseTemplate.bind({}) 6 | Default.args = { 7 | options: [ 8 | { 9 | title: 'Collapse One', 10 | content: 11 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam culpa natus molestias est aliquam tempora accusamus incidunt, voluptate, vero explicabo cumque cupiditate? Voluptate totam ea minima reprehenderit omnis porro officiis?', 12 | }, 13 | { 14 | title: 'Collapse Two', 15 | content: 16 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam culpa natus molestias est aliquam tempora accusamus incidunt, voluptate, vero explicabo cumque cupiditate? Voluptate totam ea minima reprehenderit omnis porro officiis? Lorem ipsum dolor sit amet consectetur adipisicing elit. Quibusdam culpa natus molestias est aliquam tempora accusamus incidunt, voluptate, vero explicabo cumque cupiditate? Voluptate totam ea minima reprehenderit omnis porro officiis?', 17 | }, 18 | ], 19 | } 20 | 21 | export default { 22 | title: 'Collapse', 23 | component: Collapse, 24 | argTypes: {}, 25 | } 26 | -------------------------------------------------------------------------------- /src/components/collapse/Collapse.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createSignal, For, JSX } from 'solid-js' 2 | 3 | import StyledCollapse from './StyledCollapse' 4 | import StyledContainer from './StyledContainer' 5 | import StyledContent from './StyledContent' 6 | import StyledHeader from './StyledHeader' 7 | 8 | type Option = { 9 | title: JSX.Element 10 | content: JSX.Element 11 | } 12 | 13 | export type CollapseProps = { 14 | options?: Option[] 15 | } 16 | 17 | const Collapse: Component = (props) => { 18 | const [activeItem, setActiveItem] = createSignal(-1) 19 | 20 | const handleToggle = (idx: number) => { 21 | idx === activeItem() ? setActiveItem(-1) : setActiveItem(idx) 22 | } 23 | 24 | return ( 25 | 26 | 27 | {(option, index) => ( 28 | 29 | handleToggle(index())} 32 | > 33 | {option.title} 34 | 35 | 36 | {option.content} 37 | 38 | 39 | )} 40 | 41 | 42 | ) 43 | } 44 | 45 | export default Collapse 46 | -------------------------------------------------------------------------------- /src/components/collapse/StyledCollapse.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | import type { CollapseProps } from './Collapse' 5 | 6 | const baseStyles = [tw`border p-8 rounded`, { 'border-radius': '15px' }] 7 | 8 | const StyledCollapse = styled('div')(baseStyles) 9 | 10 | export default StyledCollapse 11 | -------------------------------------------------------------------------------- /src/components/collapse/StyledContainer.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | type StyledContainerProps = JSX.IntrinsicElements['div'] 6 | 7 | const baseStyles = tw`border-b` 8 | 9 | const StyledContainer = styled('div')(baseStyles) 10 | 11 | export default StyledContainer 12 | -------------------------------------------------------------------------------- /src/components/collapse/StyledContent.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | type StyledContentProps = JSX.IntrinsicElements['div'] & { 6 | isActive?: boolean 7 | } 8 | 9 | const baseStyles = tw`border-t py-0 h-0 opacity-0 invisible transition-all` 10 | 11 | const activeStyles = (props: StyledContentProps) => [ 12 | props.isActive && tw`h-auto visible py-5 opacity-100`, 13 | ] 14 | 15 | const StyledContent = styled('div')( 16 | baseStyles, 17 | activeStyles 18 | ) 19 | 20 | export default StyledContent 21 | -------------------------------------------------------------------------------- /src/components/collapse/StyledHeader.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import type { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | import type { CollapseProps } from './Collapse' 6 | 7 | type StyledHeaderProps = JSX.IntrinsicElements['div'] & { 8 | isFirst?: boolean 9 | } 10 | 11 | const baseStyles = tw`py-7 cursor-pointer` 12 | 13 | const firstElementStyle = (props: StyledHeaderProps) => [ 14 | props.isFirst && tw`pt-1.5`, 15 | ] 16 | 17 | const StyledHeader = styled('div')( 18 | baseStyles, 19 | firstElementStyle 20 | ) 21 | 22 | export default StyledHeader 23 | -------------------------------------------------------------------------------- /src/components/collapse/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Collapse } from './Collapse' 2 | -------------------------------------------------------------------------------- /src/components/input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import Input from './Input' 2 | 3 | const InputTemplate: Story = (args) => 4 | 5 | export const Default = InputTemplate.bind({}) 6 | Default.args = { 7 | placeholder: 'Placeholder', 8 | block: false, 9 | } 10 | 11 | export const Filled = InputTemplate.bind({}) 12 | Filled.args = { 13 | placeholder: 'Placeholder', 14 | filled: true, 15 | block: false, 16 | } 17 | 18 | export const WithLabel = InputTemplate.bind({}) 19 | WithLabel.args = { 20 | placeholder: 'Placeholder', 21 | label: 'Label', 22 | block: false, 23 | } 24 | 25 | export const WithFloatLabel = InputTemplate.bind({}) 26 | WithFloatLabel.args = { 27 | label: 'Label', 28 | labelFloat: true, 29 | block: false, 30 | } 31 | 32 | export const Block = InputTemplate.bind({}) 33 | Block.args = { 34 | label: 'Label', 35 | labelFloat: true, 36 | block: true, 37 | } 38 | 39 | export default { 40 | title: 'Input', 41 | component: Input, 42 | argTypes: {}, 43 | } 44 | -------------------------------------------------------------------------------- /src/components/input/Input.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createSignal, JSX, onMount, Show } from 'solid-js' 2 | 3 | import StyledContainer from './StyledContainer' 4 | import StyledInput from './StyledInput' 5 | import StyledLabel from './StyledLabel' 6 | import StyledTextArea from './StyledTextArea' 7 | 8 | export type InputProps = { 9 | filled?: boolean 10 | label?: JSX.Element 11 | placeholder?: string 12 | labelFloat?: boolean 13 | onFocus?: (e: Event) => void 14 | onBlur?: (e: Event) => void 15 | block?: boolean 16 | type?: 'textarea' | 'number' 17 | } 18 | 19 | const Input: Component = (props) => { 20 | const [isFocused, setIsFocused] = createSignal(false) 21 | const handleFocus = (e: Event) => { 22 | props.onFocus && props.onFocus(e) 23 | setIsFocused(true) 24 | } 25 | const handleBlur = (e: Event) => { 26 | props.onBlur && props.onBlur(e) 27 | const target = e.target as HTMLInputElement 28 | if (!target?.value) { 29 | setIsFocused(false) 30 | } 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | {props.label} 38 | 39 | 40 | 44 | } 45 | > 46 | 47 | 48 | 49 | ) 50 | } 51 | 52 | export default Input 53 | -------------------------------------------------------------------------------- /src/components/input/StyledContainer.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | import { InputProps } from './Input' 6 | 7 | type StyledContainerProps = JSX.IntrinsicElements['div'] & { 8 | focused?: boolean 9 | } 10 | 11 | const baseStyles = tw`relative` 12 | 13 | const floatLabelStyle = (props: InputProps & StyledContainerProps) => [ 14 | props.labelFloat && 15 | props.focused && { 16 | ':hover label': tw`text-blue-dark`, 17 | }, 18 | ] 19 | 20 | const StyledContainer = styled('div')( 21 | baseStyles, 22 | floatLabelStyle 23 | ) 24 | 25 | export default StyledContainer 26 | -------------------------------------------------------------------------------- /src/components/input/StyledInput.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | import type { InputProps } from './Input' 6 | 7 | type StyledInputProps = JSX.IntrinsicElements['input'] 8 | 9 | const baseStyles = tw`border-2 w-full border-grey-light rounded-xl h-12 focus:outline-none px-3 text-base text-dark-100 focus:border-primary hover:border-blue-dark focus:border-2` 10 | 11 | const filledStyle = (props: InputProps) => [props.filled && tw`bg-light-300`] 12 | 13 | const blockStyle = (props: InputProps) => [props.block === false && tw`w-auto`] 14 | 15 | const StyledInput = styled('input')( 16 | baseStyles, 17 | filledStyle, 18 | blockStyle 19 | ) 20 | 21 | export default StyledInput 22 | -------------------------------------------------------------------------------- /src/components/input/StyledLabel.ts: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import tw from 'twin.macro' 3 | 4 | import { InputProps } from './Input' 5 | 6 | type StyledLabelProps = { 7 | focused?: boolean 8 | } 9 | 10 | const baseStyles = tw`block text-base text-dark-100 ml-3 mb-1 transition-all pointer-events-none` 11 | 12 | const floatLabelStyle = (props: InputProps & StyledLabelProps) => [ 13 | props.labelFloat && [tw`absolute inline-block`, { 'line-height': '48px' }], 14 | props.labelFloat && 15 | props.focused && [tw`text-xs bg-white text-primary px-px`, { top: '-7px' }], 16 | ] 17 | 18 | const StyledLabel = styled('label')( 19 | baseStyles, 20 | floatLabelStyle 21 | ) 22 | 23 | export default StyledLabel 24 | -------------------------------------------------------------------------------- /src/components/input/StyledTextArea.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'emotion-solid' 2 | import { JSX } from 'solid-js' 3 | import tw from 'twin.macro' 4 | 5 | import type { InputProps } from './Input' 6 | 7 | type StyledTextAreaProps = JSX.IntrinsicElements['textarea'] 8 | 9 | const baseStyles = [ 10 | tw`border-2 w-full pt-2.5 border-grey-light rounded-xl focus:outline-none px-3 text-base text-dark-100 focus:border-primary hover:border-blue-dark focus:border-2`, 11 | { 12 | 'min-height': '100px', 13 | }, 14 | ] 15 | 16 | const filledStyle = (props: InputProps) => [props.filled && tw`bg-light-300`] 17 | 18 | const blockStyle = (props: InputProps) => [props.block === false && tw`w-auto`] 19 | 20 | const StyledTextArea = styled('textarea')( 21 | baseStyles, 22 | filledStyle, 23 | blockStyle 24 | ) 25 | 26 | export default StyledTextArea 27 | -------------------------------------------------------------------------------- /src/components/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input' 2 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.stories.tsx: -------------------------------------------------------------------------------- 1 | import Pagination, { PaginationProps } from './Pagination' 2 | 3 | const PaginationTemplate: Story = (args) => ( 4 | 5 | ) 6 | 7 | export const Normal = PaginationTemplate.bind({}) 8 | Normal.args = { 9 | pages: 5, 10 | } 11 | 12 | export default { 13 | title: 'Pagination', 14 | component: Pagination, 15 | argTypes: {}, 16 | } 17 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createEffect, createSignal, Show, For } from 'solid-js' 2 | 3 | import ChevronLeft from '../../icons/ChevronLeft' 4 | import ChevronRight from '../../icons/ChevronRight' 5 | import { Button } from '../button' 6 | import StyledContainer from './StyledContainer' 7 | 8 | export type PaginationProps = { 9 | class?: string 10 | currentPage?: number 11 | pages: number 12 | onChange?: (newPage: number) => void 13 | } 14 | 15 | const Pagination: Component = (props) => { 16 | const [currentPage, setCurrentPage] = createSignal(props.currentPage || 1) 17 | 18 | createEffect(() => { 19 | if (props.currentPage != null) { 20 | setCurrentPage(props.currentPage) 21 | } 22 | }) 23 | 24 | const changePage = (pageUpdate: number | ((prev: number) => number)) => { 25 | if (typeof pageUpdate === 'number') { 26 | setCurrentPage(pageUpdate) 27 | props.onChange && props.onChange(pageUpdate) 28 | } else { 29 | setCurrentPage((oldPage) => { 30 | const newPage = pageUpdate(oldPage) 31 | props.onChange && props.onChange(newPage) 32 | return newPage 33 | }) 34 | } 35 | } 36 | 37 | const previous = () => { 38 | setCurrentPage(Math.max(1, currentPage() - 1)) 39 | } 40 | 41 | const next = () => { 42 | setCurrentPage(Math.min(props.pages, currentPage() + 1)) 43 | } 44 | 45 | return ( 46 | 47 | 67 | 68 | )} 69 | 70 |