├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── feature-request.yml └── workflows │ └── doc-deploy.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── dev ├── .eslintrc.cjs ├── app.config.ts ├── package.json ├── postcss.config.cjs ├── public │ └── favicon.ico ├── src │ ├── app.css │ ├── app.tsx │ ├── components │ │ ├── auto-animate.tsx │ │ ├── index.ts │ │ ├── somo.tsx │ │ ├── somoto.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ └── ripple │ │ │ ├── ripple.tsx │ │ │ └── use-ripple.ts │ ├── entry-client.tsx │ ├── entry-server.tsx │ ├── routes │ │ ├── [...404].tsx │ │ └── index.tsx │ ├── types │ │ ├── global.d.ts │ │ └── index.d.ts │ └── utils │ │ └── index.ts ├── tailwind.config.cjs └── tsconfig.json ├── docs ├── .eslintrc.cjs ├── .vscode │ └── launch.json ├── astro.config.mjs ├── package.json ├── postcss.config.js ├── public │ └── favicon.ico ├── src │ ├── app.css │ ├── components │ │ ├── demo-template │ │ │ └── index.astro │ │ ├── layouts │ │ │ └── page-layout.astro │ │ ├── solid │ │ │ ├── code-block │ │ │ │ └── index.tsx │ │ │ ├── copy │ │ │ │ └── index.tsx │ │ │ ├── demo │ │ │ │ ├── expand │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── installation │ │ │ │ │ └── index.tsx │ │ │ │ ├── other │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── other.module.css │ │ │ │ ├── positions │ │ │ │ │ └── index.tsx │ │ │ │ ├── types │ │ │ │ │ └── index.tsx │ │ │ │ └── usage │ │ │ │ │ └── index.tsx │ │ │ ├── hero │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ ├── icons │ │ │ │ ├── check.tsx │ │ │ │ ├── clipboard.tsx │ │ │ │ └── index.ts │ │ │ └── layout │ │ │ │ └── index.tsx │ │ └── theme │ │ │ ├── theme-provider │ │ │ └── index.astro │ │ │ └── theme-select │ │ │ └── index.astro │ ├── constants │ │ └── index.ts │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── getting-started.mdx │ │ │ ├── styling.mdx │ │ │ ├── toast.mdx │ │ │ └── toaster.mdx │ ├── env.d.ts │ ├── pages │ │ └── index.astro │ └── utils │ │ ├── getDemoCode.ts │ │ ├── helper.ts │ │ └── index.ts ├── tailwind.config.ts └── tsconfig.json ├── package.json ├── packages ├── config-eslint │ ├── index.js │ └── package.json ├── config-tailwind │ ├── package.json │ ├── tailwind.config.ts │ └── tsconfig.json ├── shared │ ├── .eslintrc.cjs │ ├── env.d.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── button.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── utils │ │ │ └── index.ts │ ├── tailwind.config.cjs │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── somo │ ├── .eslintrc.cjs │ ├── env.d.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── auto-layout │ │ │ │ ├── base.ts │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── motion.tsx │ │ │ └── presence.tsx │ │ ├── context.ts │ │ ├── easing │ │ │ ├── index.ts │ │ │ └── spring.ts │ │ ├── index.ts │ │ ├── primitives.ts │ │ ├── types │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ └── interface.ts │ │ └── utils │ │ │ ├── defaults.ts │ │ │ └── helper.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts └── somoto │ ├── .eslintrc.cjs │ ├── README.md │ ├── env.d.ts │ ├── package.json │ ├── src │ ├── components │ │ ├── icons.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx │ ├── constants │ │ └── index.ts │ ├── hooks │ │ ├── use-is-document-hidden.ts │ │ ├── use-is-mounted.ts │ │ └── use-somoto.ts │ ├── index.ts │ ├── primitives │ │ └── create-timer.ts │ ├── state.ts │ ├── styles.css │ ├── types │ │ └── index.ts │ └── utils │ │ ├── cn.ts │ │ ├── get-document-direction.ts │ │ ├── helper.ts │ │ └── unwrap-accessor.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── scripts └── index.ts ├── tsconfig.json ├── turbo.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug report" 2 | description: Create a report to help us improve 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue :pray:. 8 | 9 | The more information you fill in, the better the community can help you. 10 | - type: textarea 11 | id: description 12 | attributes: 13 | label: Describe the bug 14 | description: Provide a clear and concise description of the challenge you are running into. 15 | validations: 16 | required: true 17 | - type: input 18 | id: link 19 | attributes: 20 | label: Minimal Reproduction Link 21 | description: | 22 | Please provide a link to a minimal reproduction of the bug you are running into. 23 | It makes the process of verifying and fixing the bug much easier. 24 | Note: 25 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the solid-js and solid-primitives. 26 | - To create a shareable code example you can use [Stackblitz](https://stackblitz.com/) (https://solid.new). Please no localhost URLs. 27 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. 28 | placeholder: | 29 | e.g. https://stackblitz.com/edit/...... OR Github Repo 30 | validations: 31 | required: true 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature Request" 2 | description: For feature/enhancement requests. Please search for existing issues first. 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for bringing your ideas here :pray:. 8 | 9 | The more information you fill in, the better the community can understand your idea. 10 | - type: textarea 11 | id: problem 12 | attributes: 13 | label: Describe The Problem To Be Solved 14 | description: Provide a clear and concise description of the challenge you are running into. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: solution 19 | attributes: 20 | label: Suggest A Solution 21 | description: | 22 | A concise description of your preferred solution. Things to address include: 23 | - Details of the technical implementation 24 | - Tradeoffs made in design decisions 25 | - Caveats and considerations for the future 26 | validations: 27 | required: true 28 | -------------------------------------------------------------------------------- /.github/workflows/doc-deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - gh-pages 7 | # https://docs.github.com/zh/actions/using-workflows/workflow-syntax-for-github-actions#on 8 | 9 | jobs: 10 | build: 11 | name: build docs 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: yarn 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | cache: yarn 27 | 28 | - name: install dependencies 29 | run: yarn install --frozen-lockfile 30 | - name: build 31 | run: yarn build 32 | 33 | - name: upload artifact 34 | uses: actions/upload-pages-artifact@v3 35 | with: 36 | path: docs/dist 37 | 38 | deploy: 39 | name: 部署到 GitHub Pages 40 | needs: build 41 | 42 | permissions: 43 | pages: write # to deploy to Pages 44 | id-token: write # validate source 45 | 46 | # 部署到 Github Pages 环境 47 | environment: 48 | name: github-pages 49 | url: ${{ steps.deployment.outputs.page_url }} 50 | 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # IDEs and editors 4 | /.idea 5 | .project 6 | .classpath 7 | *.launch 8 | .settings/ 9 | 10 | # dependencies 11 | node_modules 12 | .pnp 13 | .pnp.js 14 | 15 | # testing 16 | coverage 17 | 18 | out/ 19 | build 20 | .swc/ 21 | dist 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | # System Files 27 | Thumbs.db 28 | 29 | # debug 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | pnpm-debug.log* 34 | 35 | # local env files 36 | .env*.local 37 | 38 | # turbo 39 | .turbo 40 | 41 | # tsup 42 | tsup.config.bundled_*.{m,c,}s 43 | 44 | # generated types 45 | .astro/ 46 | 47 | .solid 48 | .output 49 | .vercel 50 | .netlify 51 | .vinxi 52 | app.config.timestamp_*.js 53 | 54 | # Temp 55 | gitignore 56 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "semi": true, 6 | "singleQuote": true, 7 | "useTabs": false, 8 | "arrowParens": "avoid", 9 | "bracketSpacing": true, 10 | "plugins": ["prettier-plugin-tailwindcss"], 11 | "tailwindFunctions": ["cva"] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "astro-build.astro-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.experimental.classRegex": [ 3 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 4 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Oc1s 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 | # somoto 2 | 3 | > A SolidJS port for [Sonner](https://github.com/emilkowalski/sonner). 4 | 5 | somoto is a taost library for SolidJS. 6 | 7 | For demonstration, please visit The [site](https://oc1s.github.io/somo/). 8 | 9 | ## Quick start 10 | 11 | ### Install: 12 | 13 | ```bash 14 | npm i somoto 15 | # or 16 | yarn add somoto 17 | # or 18 | pnpm add somoto 19 | # or 20 | bun add smoto 21 | ``` 22 | 23 | ### Usage: 24 | 25 | ```jsx 26 | import { Toaster, toast } from 'somoto'; 27 | 28 | function App() { 29 | return ( 30 |
31 | 32 | 33 |
34 | ); 35 | } 36 | ``` 37 | 38 | ## Documentation 39 | 40 | Find API references in the [doc](https://oc1s.github.io/somo/getting-started/). 41 | -------------------------------------------------------------------------------- /dev/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@repo/eslint-config'], 3 | }; 4 | -------------------------------------------------------------------------------- /dev/app.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@solidjs/start/config"; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somo-dev", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vinxi dev", 8 | "build": "vinxi build", 9 | "start": "vinxi start" 10 | }, 11 | "dependencies": { 12 | "@solid-primitives/props": "^3.1.11", 13 | "@solidjs/router": "^0.14.7", 14 | "@solidjs/start": "^1.0.8", 15 | "autoprefixer": "^10.4.19", 16 | "clsx": "^2.1.1", 17 | "postcss": "^8.4.38", 18 | "solid-js": "^1.9.1", 19 | "somo": "*", 20 | "somoto": "*", 21 | "tailwindcss": "^3.4.3", 22 | "vinxi": "^0.4.3" 23 | }, 24 | "devDependencies": { 25 | "@repo/tailwind-config": "*" 26 | }, 27 | "engines": { 28 | "node": ">=18" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dev/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /dev/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oc1S/somo/116d25d2e0de0a07f220dfe0f53f07ef96d70987/dev/public/favicon.ico -------------------------------------------------------------------------------- /dev/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: iiter; 7 | src: url(https://nextui.org/_next/static/media/a34f9d1faa5f3315-s.p.woff2); 8 | } 9 | 10 | :root { 11 | font-family: 12 | iiter, 13 | ui-sans-serif, 14 | system-ui, 15 | -apple-system, 16 | BlinkMacSystemFont, 17 | 'Segoe UI', 18 | Roboto, 19 | 'Helvetica Neue', 20 | Arial, 21 | 'Noto Sans', 22 | sans-serif, 23 | 'Apple Color Emoji', 24 | 'Segoe UI Emoji', 25 | 'Segoe UI Symbol', 26 | 'Noto Color Emoji'; 27 | -webkit-text-size-adjust: 100%; 28 | -webkit-font-smoothing: auto; 29 | -webkit-tap-highlight-color: transparent; 30 | --background-rgb: 0, 0, 0; 31 | --foreground-rgb: 255, 255, 255; 32 | } 33 | 34 | body { 35 | background: rgb(var(--background-rgb)); 36 | color: rgb(var(--foreground-rgb)); 37 | } 38 | -------------------------------------------------------------------------------- /dev/src/app.tsx: -------------------------------------------------------------------------------- 1 | import './app.css'; 2 | 3 | import { Suspense } from 'solid-js'; 4 | import { Router } from '@solidjs/router'; 5 | import { FileRoutes } from '@solidjs/start/router'; 6 | 7 | export default function App() { 8 | return ( 9 | ( 11 | <> 12 | {props.children} 13 | 14 | )} 15 | > 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /dev/src/components/auto-animate.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For, Show } from 'solid-js'; 2 | import { createAutoAnimate } from 'somo'; 3 | 4 | /* 目前只发现监听了childList变化的情况,resizeObserver待观察 */ 5 | export const AutoAnimateTest = () => { 6 | const [parent] = createAutoAnimate(/* optional config */); 7 | 8 | const [list, setList] = createSignal(['Home', 'Settings', 'Logout']); 9 | const [expand, setExpand] = createSignal(false); 10 | const [isExpanded, setIsExpanded] = createSignal(true); 11 | 12 | return ( 13 |
20 | 21 | 24 | 25 | 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /dev/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auto-animate'; 2 | export * from './somo'; 3 | -------------------------------------------------------------------------------- /dev/src/components/somo.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, Show } from 'solid-js'; 2 | import { Motion, Presence } from 'somo'; 3 | 4 | export default function SomoTest() { 5 | const [visible, setVisible] = createSignal(true); 6 | 7 | return ( 8 |
9 | 10 | 11 | { 39 | setVisible(false); 40 | }} 41 | /> 42 | 43 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /dev/src/components/somoto.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For } from 'solid-js'; 2 | import { toast, Toaster } from 'somoto'; 3 | 4 | import { Button } from './ui/button'; 5 | 6 | const duration = 300_000; 7 | let index = 0; 8 | const types = ['default', 'action', 'success', 'info', 'warning', 'error', 'loading'] as const; 9 | const positions = [ 10 | 'bottom-right', 11 | 'bottom-center', 12 | 'bottom-left', 13 | 'top-right', 14 | 'top-center', 15 | 'top-left', 16 | ] as const; 17 | export const Somoto = () => { 18 | const [type, setType] = createSignal<(typeof types)[number]>(types[0]); 19 | const [position, setPosition] = createSignal<(typeof positions)[number]>(positions[0]); 20 | 21 | const message = () => `Hello World_${index++}!`; 22 | 23 | const showToast = () => { 24 | switch (type()) { 25 | case 'default': 26 | toast(message(), { 27 | position: position(), 28 | duration, 29 | }); 30 | break; 31 | case 'info': 32 | toast.info(message(), { 33 | position: position(), 34 | duration, 35 | }); 36 | break; 37 | case 'success': 38 | toast.success(message(), { 39 | position: position(), 40 | duration, 41 | }); 42 | break; 43 | case 'warning': 44 | toast.warning(message(), { 45 | position: position(), 46 | duration, 47 | }); 48 | break; 49 | case 'error': 50 | toast.error(message(), { 51 | position: position(), 52 | duration, 53 | }); 54 | break; 55 | case 'action': 56 | toast(message(), { 57 | position: position(), 58 | duration, 59 | action: { 60 | label: 'hi', 61 | onClick: () => console.log('hi there'), 62 | }, 63 | }); 64 | break; 65 | } 66 | }; 67 | 68 | return ( 69 | <> 70 |
71 |
72 | 73 | {type => { 74 | return ( 75 | 83 | ); 84 | }} 85 | 86 |
87 | 88 |
89 | 90 | {position => { 91 | return ( 92 | 100 | ); 101 | }} 102 | 103 |
104 |
105 | 106 | 107 | ); 108 | }; 109 | 110 | export default function SomotoTest() { 111 | return ( 112 | <> 113 | 114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /dev/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, ParentComponent } from 'solid-js'; 2 | import { createSignal, splitProps } from 'solid-js'; 3 | import clsx from 'clsx'; 4 | import { Motion } from 'somo'; 5 | 6 | export const Button: ParentComponent> = props => { 7 | const [, rest] = splitProps(props, ['class', 'children']); 8 | const [pressed, setPressed] = createSignal(false); 9 | return ( 10 | setPressed(true)} 20 | onPointerUp={() => setPressed(false)} 21 | onPointerLeave={() => setPressed(false)} 22 | {...rest} 23 | > 24 | {props.children} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /dev/src/components/ui/ripple/ripple.tsx: -------------------------------------------------------------------------------- 1 | import type { Component, JSX } from 'solid-js'; 2 | import { For, mergeProps, splitProps } from 'solid-js'; 3 | import clsx from 'clsx'; 4 | import type { MotionProps } from 'somo'; 5 | import { Motion } from 'somo'; 6 | 7 | import { clamp } from '~/utils'; 8 | 9 | import type { RippleType } from './use-ripple'; 10 | 11 | export interface RippleProps extends MotionProps<'span'> { 12 | ripples: RippleType[]; 13 | color?: string; 14 | style: JSX.CSSProperties; 15 | onClear: (key: Key) => void; 16 | } 17 | 18 | const Ripple: Component = p => { 19 | const props = mergeProps( 20 | { 21 | ripples: [], 22 | color: 'currentColor', 23 | }, 24 | p, 25 | ); 26 | const [, domProps] = splitProps(props, ['class', 'style']); 27 | 28 | return ( 29 | 30 | {ripple => { 31 | const duration = clamp(0.01 * ripple.size, 0.2, ripple.size > 100 ? 0.75 : 0.5); 32 | 33 | return ( 34 | { 53 | props.onClear(ripple.key); 54 | }} 55 | /> 56 | ); 57 | }} 58 | 59 | ); 60 | }; 61 | 62 | export default Ripple; 63 | -------------------------------------------------------------------------------- /dev/src/components/ui/ripple/use-ripple.ts: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | import { createSignal } from 'solid-js'; 3 | 4 | import { getUniqueID } from '~/utils'; 5 | 6 | export type RippleType = { 7 | key: Key; 8 | x: number; 9 | y: number; 10 | size: number; 11 | }; 12 | 13 | export interface UseRippleProps {} 14 | 15 | export function useRipple(props: UseRippleProps = {}) { 16 | const [ripples, setRipples] = createSignal([]); 17 | 18 | const onClick: JSX.EventHandler = event => { 19 | const trigger = event.currentTarget; 20 | 21 | const size = Math.max(trigger.clientWidth, trigger.clientHeight); 22 | const rect = trigger.getBoundingClientRect(); 23 | 24 | setRipples(prevRipples => [ 25 | ...prevRipples, 26 | { 27 | key: getUniqueID(prevRipples.length.toString()), 28 | size, 29 | x: event.clientX - rect.left - size / 2, 30 | y: event.clientY - rect.top - size / 2, 31 | }, 32 | ]); 33 | }; 34 | 35 | const onClear = (key: Key) => { 36 | setRipples(prevState => prevState.filter(ripple => ripple.key !== key)); 37 | }; 38 | 39 | return { ripples, onClick, onClear, ...props }; 40 | } 41 | 42 | export type UseRippleReturn = ReturnType; 43 | -------------------------------------------------------------------------------- /dev/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { mount, StartClient } from "@solidjs/start/client"; 3 | 4 | mount(() => , document.getElementById("app")!); 5 | -------------------------------------------------------------------------------- /dev/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | // @refresh reload 2 | import { createHandler, StartServer } from '@solidjs/start/server'; 3 | 4 | export default createHandler(() => ( 5 | ( 7 | 8 | 9 | 10 | 11 | 12 | {assets} 13 | 14 | 15 |
{children}
16 | {scripts} 17 | 18 | 19 | )} 20 | /> 21 | )); 22 | -------------------------------------------------------------------------------- /dev/src/routes/[...404].tsx: -------------------------------------------------------------------------------- 1 | import { A } from "@solidjs/router"; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Not Found

7 |

8 | Visit{" "} 9 | 10 | solidjs.com 11 | {" "} 12 | to learn how to build Solid apps. 13 |

14 |

15 | 16 | Home 17 | 18 | {" - "} 19 | 20 | About Page 21 | 22 |

23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /dev/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For, Match, Switch } from 'solid-js'; 2 | 3 | import Somo from '~/components/somo'; 4 | import Somoto from '~/components/somoto'; 5 | import { Button } from '~/components/ui/button'; 6 | 7 | export default function Home() { 8 | const demoList = ['somo', 'somoto'] as const; 9 | const [type, setType] = createSignal<'somo' | 'somoto'>('somoto'); 10 | 11 | return ( 12 |
13 |
14 | 15 | {item => { 16 | return ( 17 | 20 | ); 21 | }} 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /dev/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dev/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | type Key = number | string; 2 | -------------------------------------------------------------------------------- /dev/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function getUniqueID(prefix: string) { 2 | return `${prefix}-${Math.floor(Math.random() * 1000000)}`; 3 | } 4 | 5 | /** 6 | * Clamps a value between a minimum and maximum range. 7 | * 8 | * @param value - The value to be clamped. 9 | * @param min - The minimum value of the range. 10 | * @param max - The maximum value of the range. 11 | * @returns The clamped value. 12 | */ 13 | export function clamp(value: number, min: number, max: number) { 14 | return Math.min(Math.max(value, min), max); 15 | } 16 | -------------------------------------------------------------------------------- /dev/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | import sharedConfig from '@repo/tailwind-config'; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | presets: [sharedConfig], 6 | content: ['./src/**/*.{html,js,jsx,ts,tsx}'], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "jsx": "preserve", 9 | "jsxImportSource": "solid-js", 10 | "allowJs": true, 11 | "noEmit": true, 12 | "strict": false, 13 | "types": ["vinxi/types/client"], 14 | "isolatedModules": true, 15 | "paths": { 16 | "~/*": ["./src/*"] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@repo/eslint-config'], 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import solidJs from '@astrojs/solid-js'; 2 | import starlight from '@astrojs/starlight'; 3 | import tailwind from '@astrojs/tailwind'; 4 | import { defineConfig } from 'astro/config'; 5 | 6 | import pkg from '../package.json'; 7 | console.log(`🚀 ${pkg.name} v${pkg.version} - ${pkg.description}`); 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | site: 'https://oc1s.github.io', 12 | base: 'somo', 13 | integrations: [ 14 | starlight({ 15 | title: 'Somoto', 16 | components: { 17 | ThemeProvider: './src/components/theme/theme-provider/index.astro', 18 | ThemeSelect: './src/components/theme/theme-select/index.astro', 19 | }, 20 | social: { 21 | github: 'https://github.com/Oc1S/somo', 22 | }, 23 | customCss: ['./src/app.css'], 24 | // sidebar: [ 25 | // { 26 | // label: 'Guides', 27 | // items: [ 28 | // // Each item here is one entry in the navigation menu. 29 | // { label: 'Example Guide', link: '/guides/example/' }, 30 | // ], 31 | // }, 32 | // { 33 | // label: 'Reference', 34 | // autogenerate: { directory: 'reference' }, 35 | // }, 36 | // ], 37 | }), 38 | solidJs(), 39 | tailwind({ 40 | applyBaseStyles: false, 41 | }), 42 | ], 43 | }); 44 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro check && astro build", 10 | "preview": "astro preview", 11 | "astro": "astro", 12 | "type-check": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@astrojs/check": "^0.7.0", 16 | "@astrojs/solid-js": "^4.2.0", 17 | "@astrojs/starlight": "^0.23.1", 18 | "@astrojs/starlight-tailwind": "^2.0.3", 19 | "@astrojs/tailwind": "^5.1.0", 20 | "@repo/shared": "*", 21 | "@solid-primitives/props": "^3.1.11", 22 | "astro": "^4.8.6", 23 | "copy-to-clipboard": "^3.3.3", 24 | "sharp": "^0.32.5", 25 | "solid-js": "^1.8.17", 26 | "somo": "*", 27 | "tailwind-merge": "^2.5.4", 28 | "tailwindcss": "^3.4.3" 29 | }, 30 | "devDependencies": { 31 | "@repo/eslint-config": "*", 32 | "@repo/tailwind-config": "*" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oc1S/somo/116d25d2e0de0a07f220dfe0f53f07ef96d70987/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/src/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer base { 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | p { 14 | margin: 0; 15 | } 16 | 17 | :root { 18 | --background: 240 10% 3.9%; 19 | --foreground: 0 0% 98%; 20 | 21 | --muted: 240 3.7% 15.9%; 22 | --muted-foreground: 240 5% 64.9%; 23 | 24 | --accent: 240 3.7% 15.9%; 25 | --accent-foreground: 0 0% 98%; 26 | 27 | --popover: 240 10% 3.9%; 28 | --popover-foreground: 0 0% 98%; 29 | 30 | --border: 240 3.7% 15.9%; 31 | --input: 240 3.7% 15.9%; 32 | 33 | --card: 240 10% 3.9%; 34 | --card-foreground: 0 0% 98%; 35 | 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | 39 | --secondary: 240 3.7% 15.9%; 40 | --secondary-foreground: 0 0% 98%; 41 | 42 | --destructive: 0 62.8% 30.6%; 43 | --destructive-foreground: 0 0% 98%; 44 | 45 | --info: 204 94% 94%; 46 | --info-foreground: 199 89% 48%; 47 | 48 | --success: 149 80% 90%; 49 | --success-foreground: 160 84% 39%; 50 | 51 | --warning: 48 96% 89%; 52 | --warning-foreground: 25 95% 53%; 53 | 54 | --error: 0 93% 94%; 55 | --error-foreground: 0 84% 60%; 56 | 57 | --ring: 240 4.9% 83.9%; 58 | 59 | --radius: 0.5rem; 60 | } 61 | } 62 | 63 | @media (max-width: 640px) { 64 | .container { 65 | @apply px-4; 66 | } 67 | } 68 | 69 | * { 70 | box-sizing: border-box; 71 | } 72 | 73 | :root { 74 | --font-mono: 'SF Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, 75 | Courier New, monospace; 76 | --gray0: #000; 77 | --gray1: hsl(0, 0%, 9.5%); 78 | --gray2: hsl(0, 0%, 10.5%); 79 | --gray3: hsl(0, 0%, 15.8%); 80 | --gray4: hsl(0, 0%, 18.9%); 81 | --gray5: hsl(0, 0%, 21.7%); 82 | --gray6: hsl(0, 0%, 24.7%); 83 | --gray7: hsl(0, 0%, 29.1%); 84 | --gray8: hsl(0, 0%, 37.5%); 85 | --gray9: hsl(0, 0%, 43%); 86 | --gray10: hsl(0, 0%, 50.7%); 87 | --gray11: hsl(0, 0%, 69.5%); 88 | --gray12: hsl(0, 0%, 93.5%); 89 | } 90 | 91 | body { 92 | font-family: 'Noto Sans', sans-serif; 93 | font-optical-sizing: auto; 94 | font-size: 16px; 95 | color: #fff; 96 | } 97 | 98 | button { 99 | font-family: 'Noto Sans'; 100 | } 101 | 102 | pre { 103 | margin: 0; 104 | } 105 | 106 | code { 107 | font-size: 14px !important; 108 | } 109 | 110 | .sl-flex.social-icons::after { 111 | display: none; 112 | } 113 | 114 | a { 115 | color: #fff; 116 | } 117 | -------------------------------------------------------------------------------- /docs/src/components/demo-template/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getDemoCode } from '../../utils'; 3 | import { Code } from 'astro:components'; 4 | const { path } = Astro.props; 5 | const pathArr = path.split('///')[1].split('/'); 6 | pathArr.splice(pathArr.length - 1, 0, 'demos'); 7 | const res = pathArr.join('/').replace('.mdx', '.tsx'); 8 | const demoCode = getDemoCode(res); 9 | --- 10 | 11 |

Demo

12 |
13 | 14 |
15 | 16 |

Code Example

17 | 18 | -------------------------------------------------------------------------------- /docs/src/components/layouts/page-layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title?: string; 4 | description?: string; 5 | } 6 | import '../../app.css'; 7 | const { title = 'Somoto', description = 'Somoto,SolidJS,toast,component' } = Astro.props; 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/src/components/solid/code-block/index.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | import { type Component } from 'solid-js'; 3 | 4 | import { cn } from '../../../utils'; 5 | import { Copy } from '../copy'; 6 | 7 | export const CodeBlock: Component<{ 8 | children: string; 9 | wrapperProps?: JSX.HTMLAttributes; 10 | }> = props => { 11 | return ( 12 |
19 | 25 |
26 |         {/* {props.children} */}
27 |         {props.children}
28 |       
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /docs/src/components/solid/copy/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Component, JSX } from 'solid-js'; 2 | import { createSignal, onCleanup, Show, splitProps } from 'solid-js'; 3 | import { combineProps } from '@solid-primitives/props'; 4 | import copy from 'copy-to-clipboard'; 5 | import { m, Presence } from 'somo'; 6 | 7 | import { Check, Clipboard } from '../icons'; 8 | 9 | const variants = { 10 | visible: { opacity: 1, scale: 1 }, 11 | hidden: { opacity: 0, scale: 0.5 }, 12 | }; 13 | 14 | export const Copy: Component< 15 | JSX.ButtonHTMLAttributes & { 16 | content: string; 17 | } 18 | > = props => { 19 | const [, domProps] = splitProps(props, ['content']); 20 | const [copying, setCopying] = createSignal(false); 21 | 22 | let timer: ReturnType; 23 | const onCopy = () => { 24 | copy(props.content); 25 | if (copying()) return; 26 | setCopying(true); 27 | timer = setTimeout(() => { 28 | setCopying(false); 29 | }, 2000); 30 | }; 31 | onCleanup(() => { 32 | clearTimeout(timer); 33 | }); 34 | 35 | const combined = combineProps( 36 | { 37 | class: 38 | 'flex h-[26px] w-[26px] items-center cursor-pointer justify-center rounded-md border text-[#eeeeee] transition duration-200 bg-transparent border-[#303030] focus-visible:opacity-100 focus-visible:shadow-[0_0_0_1px_#303030]', 39 | onClick: onCopy, 40 | }, 41 | domProps, 42 | ); 43 | 44 | return ( 45 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/expand/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Setter } from 'solid-js'; 2 | import { Button } from '@repo/shared'; 3 | import { toast } from 'somoto'; 4 | 5 | import { CodeBlock } from '../../code-block'; 6 | import { ContentLayout } from '../../layout'; 7 | 8 | export const Expand = (props: { expand: boolean; setExpand: Setter }) => { 9 | return ( 10 | 11 | 12 | You can change the amount of toasts visible through the visibleToasts prop. 13 | 14 | 15 | 26 | 37 | 38 | {``} 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/index.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js'; 2 | import { Toaster } from 'somoto'; 3 | 4 | import { Hero } from '../hero'; 5 | import { Expand } from './expand'; 6 | import { Installation } from './installation'; 7 | import { Other } from './other'; 8 | import { type Position, Positions } from './positions'; 9 | import { Types } from './types'; 10 | import { Usage } from './usage'; 11 | 12 | export const Somoto = () => { 13 | const [theme] = createSignal<'light' | 'dark'>('light'); 14 | const [currentPosition, setCurrentPosition] = createSignal('bottom-right'); 15 | 16 | const [expand, setExpand] = createSignal(false); 17 | const [richColors, setRichColors] = createSignal(false); 18 | const [closeButton, setCloseButton] = createSignal(false); 19 | 20 | return ( 21 |
22 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/installation/index.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlock } from '../../code-block'; 2 | import { ContentLayout } from '../../layout'; 3 | 4 | export const Installation = () => { 5 | const content = 'npm install somoto'; 6 | return ( 7 | 8 | {content} 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/other/index.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For, type Setter } from 'solid-js'; 2 | import { Button } from '@repo/shared'; 3 | import { toast } from 'somoto'; 4 | 5 | import { CodeBlock } from '../../code-block'; 6 | import { ContentLayout } from '../../layout'; 7 | import styles from './other.module.css'; 8 | 9 | export const Other = (props: { 10 | setRichColors: Setter; 11 | setCloseButton: Setter; 12 | }) => { 13 | const allTypes = [ 14 | { 15 | name: 'Rich Colors Success', 16 | snippet: `toast.success('Event has been created')`, 17 | action: () => { 18 | toast.success('Event has been created'); 19 | props.setRichColors(true); 20 | }, 21 | }, 22 | { 23 | name: 'Rich Colors Error', 24 | snippet: `toast.error('Event has not been created')`, 25 | action: () => { 26 | toast.error('Event has not been created'); 27 | props.setRichColors(true); 28 | }, 29 | }, 30 | { 31 | name: 'Rich Colors Info', 32 | snippet: `toast.info('Be at the area 10 minutes before the event time')`, 33 | action: () => { 34 | toast.info('Be at the area 10 minutes before the event time'); 35 | props.setRichColors(true); 36 | }, 37 | }, 38 | { 39 | name: 'Rich Colors Warning', 40 | snippet: `toast.warning('Event start time cannot be earlier than 8am')`, 41 | action: () => { 42 | toast.warning('Event start time cannot be earlier than 8am'); 43 | props.setRichColors(true); 44 | }, 45 | }, 46 | { 47 | name: 'Close Button', 48 | snippet: `toast('Event has been created', { 49 | description: 'Monday, January 3rd at 6:00pm', 50 | })`, 51 | action: () => { 52 | toast('Event has been created', { 53 | description: 'Monday, January 3rd at 6:00pm', 54 | }); 55 | props.setCloseButton(t => !t); 56 | }, 57 | }, 58 | { 59 | name: 'Headless', 60 | snippet: `toast.custom((t) => ( 61 |
62 |

Custom toast

63 | 64 |
65 | ));`, 66 | action: () => { 67 | toast.custom( 68 | t => ( 69 |
70 |

Event Created

71 |

Today at 4:00pm - "Louvre Museum"

72 | 77 |
78 | ), 79 | { duration: 99999 }, 80 | ); 81 | props.setCloseButton(t => !t); 82 | }, 83 | }, 84 | ]; 85 | 86 | const [activeType, setActiveType] = createSignal(allTypes[0]); 87 | 88 | const richColorsActive = () => activeType().name.includes('Rich'); 89 | const closeButtonActive = () => activeType().name.includes('Close'); 90 | 91 | return ( 92 | 93 | 94 | 95 | {type => ( 96 | 104 | )} 105 | 106 | 107 | 108 | {`${activeType().snippet || ''} 109 | 110 | // ... 111 | 112 | `} 113 | 114 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/other/other.module.css: -------------------------------------------------------------------------------- 1 | .headless { 2 | padding: 16px; 3 | width: 356px; 4 | box-sizing: border-box; 5 | border-radius: 8px; 6 | background: var(--gray1); 7 | border: 1px solid var(--gray4); 8 | position: relative; 9 | } 10 | 11 | .headless .headlessDescription { 12 | margin: 0; 13 | color: var(--gray10); 14 | font-size: 14px; 15 | line-height: 1; 16 | } 17 | 18 | .headless .headlessTitle { 19 | font-size: 14px; 20 | margin: 0 0 8px; 21 | color: var(--gray12); 22 | font-weight: 500; 23 | line-height: 1; 24 | } 25 | 26 | .headlessClose { 27 | position: absolute; 28 | cursor: pointer; 29 | top: 6px; 30 | height: 24px; 31 | width: 24px; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | right: 6px; 36 | color: var(--gray10); 37 | padding: 0; 38 | background: transparent; 39 | border: none; 40 | transition: color 200ms; 41 | } 42 | 43 | .headlessClose:hover { 44 | color: var(--gray12); 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/positions/index.tsx: -------------------------------------------------------------------------------- 1 | import { type Component, For, type Setter } from 'solid-js'; 2 | import { Button } from '@repo/shared'; 3 | import { toast, useSomoto } from 'somoto'; 4 | 5 | import { CodeBlock } from '../../code-block'; 6 | import { ContentLayout } from '../../layout'; 7 | 8 | const positions = [ 9 | 'bottom-right', 10 | 'bottom-center', 11 | 'bottom-left', 12 | 'top-right', 13 | 'top-center', 14 | 'top-left', 15 | ] as const; 16 | export type Position = (typeof positions)[number]; 17 | 18 | export const Positions: Component<{ 19 | position: Position; 20 | setPosition: Setter; 21 | }> = props => { 22 | const { toasts } = useSomoto(); 23 | 24 | function removeAllToasts() { 25 | toasts().forEach(t => toast.dismiss(t.id)); 26 | } 27 | return ( 28 | 29 | 30 |

Swipe direction changes depending on the position.

31 |
32 | 33 | 34 | {position => { 35 | const active = () => position === props.position; 36 | return ( 37 | 49 | ); 50 | }} 51 | 52 | 53 | {``} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/types/index.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For } from 'solid-js'; 2 | import { Button } from '@repo/shared'; 3 | import { toast } from 'somoto'; 4 | 5 | import { CodeBlock } from '../../code-block'; 6 | import { ContentLayout } from '../../layout'; 7 | 8 | const promiseCode = '`${data.name} has been done`'; 9 | const types = [ 10 | { 11 | name: 'Default', 12 | snippet: `toast('Event has been created')`, 13 | action: () => toast('Event has been created'), 14 | }, 15 | { 16 | name: 'Description', 17 | snippet: `toast.message('Event has been created', { 18 | description: 'Monday, January 3rd at 6:00pm', 19 | })`, 20 | action: () => 21 | toast('Event has been created', { 22 | description: 'Monday, January 3rd at 6:00pm', 23 | }), 24 | }, 25 | { 26 | name: 'Success', 27 | snippet: `toast.success('Event has been created')`, 28 | action: () => toast.success('Event has been created'), 29 | }, 30 | { 31 | name: 'Info', 32 | snippet: `toast.info('Be at the area 10 minutes before the event time')`, 33 | action: () => toast.info('Be at the area 10 minutes before the event time'), 34 | }, 35 | { 36 | name: 'Warning', 37 | snippet: `toast.warning('Event start time cannot be earlier than 8am')`, 38 | action: () => toast.warning('Event start time cannot be earlier than 8am'), 39 | }, 40 | { 41 | name: 'Error', 42 | snippet: `toast.error('Event has not been created')`, 43 | action: () => toast.error('Event has not been created'), 44 | }, 45 | { 46 | name: 'Action', 47 | snippet: `toast('Event has been created', { 48 | action: { 49 | label: 'Undo', 50 | onClick: () => console.log('Undo') 51 | }, 52 | })`, 53 | action: () => 54 | toast.message('Event has been created', { 55 | action: { 56 | label: 'Undo', 57 | onClick: () => console.log('Undo'), 58 | }, 59 | }), 60 | }, 61 | { 62 | name: 'Promise', 63 | snippet: `toast.promise<{ name: string }>( 64 | () => 65 | new Promise(resolve => { 66 | setTimeout(() => { 67 | resolve({ name: 'Cook' }); 68 | }, 2000); 69 | }), 70 | { 71 | loading: 'Loading...', 72 | success: data => { 73 | return ${promiseCode}; 74 | }, 75 | error: 'Error', 76 | }, 77 | ),`, 78 | action: () => 79 | toast.promise<{ name: string }>( 80 | () => 81 | new Promise(resolve => { 82 | setTimeout(() => { 83 | resolve({ name: 'Cook' }); 84 | }, 2000); 85 | }), 86 | { 87 | loading: 'Loading...', 88 | success: data => { 89 | return `${data.name} has been done`; 90 | }, 91 | error: 'Error', 92 | }, 93 | ), 94 | }, 95 | { 96 | name: 'Custom', 97 | snippet: `toast(
A custom toast with default styling
)`, 98 | action: () => toast(
A custom toast with default styling
, { duration: 1000000 }), 99 | }, 100 | ]; 101 | 102 | export const Types = () => { 103 | const [currentType, setCurrentType] = createSignal<(typeof types)[number]>(types[0]); 104 | return ( 105 | 106 | 107 |

Swipe direction changes depending on the position.

108 |
109 | 110 | 111 | {type => { 112 | const active = () => type === currentType(); 113 | return ( 114 | 123 | ); 124 | }} 125 | 126 | 127 | {`${currentType().snippet}`} 128 |
129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /docs/src/components/solid/demo/usage/index.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlock } from '../../code-block'; 2 | import { ContentLayout } from '../../layout'; 3 | 4 | const snipper = `import { Toaster, toast } from 'somoto' 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 | 13 |
14 | ) 15 | }`; 16 | 17 | export const Usage = () => { 18 | return ( 19 | 20 | 21 | Render {''} in the root 22 | of your app. 23 | 24 | {snipper} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /docs/src/components/solid/hero/index.css: -------------------------------------------------------------------------------- 1 | .toast-wrapper { 2 | mask-image: linear-gradient(to top, transparent 0%, #000 35%); 3 | } 4 | 5 | .toast:nth-child(1) { 6 | transform: translateY(-60%) translateX(-50%) scale(0.9); 7 | } 8 | 9 | .toast:nth-child(2) { 10 | transform: translateY(-30%) translateX(-50%) scale(0.95); 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/solid/hero/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | 3 | import { For } from 'solid-js'; 4 | import { Button } from '@repo/shared'; 5 | import { toast } from 'somoto'; 6 | 7 | export const Hero = () => { 8 | return ( 9 |
10 |
11 | 12 | {() => ( 13 |
14 | )} 15 | 16 |
17 |

Somoto

18 |
19 | All you need for 🍞toast in SolidJS. 20 |
21 |
22 | A SolidJS port of  23 | 24 | Sonner 25 | 26 |
27 |
28 | 35 | 38 |
39 | 43 | Documentation 44 | 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /docs/src/components/solid/icons/check.tsx: -------------------------------------------------------------------------------- 1 | export const Check = () => { 2 | return ( 3 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/src/components/solid/icons/clipboard.tsx: -------------------------------------------------------------------------------- 1 | export const Clipboard = () => { 2 | return ( 3 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/src/components/solid/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check'; 2 | export * from './clipboard'; 3 | -------------------------------------------------------------------------------- /docs/src/components/solid/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX, ParentComponent } from 'solid-js'; 2 | import { combineProps } from '@solid-primitives/props'; 3 | 4 | const Wrapper: ParentComponent<{ 5 | title: string; 6 | }> = props => { 7 | return ( 8 |
9 |
{props.title}
10 | {props.children} 11 |
12 | ); 13 | }; 14 | 15 | const ButtonGroup: ParentComponent> = props => { 16 | const combined = combineProps(props, { 17 | class: 'my-4 flex flex-wrap gap-4', 18 | }); 19 | return
{props.children}
; 20 | }; 21 | 22 | const Description: ParentComponent> = props => { 23 | const combined = combineProps(props, { 24 | class: 'my-2', 25 | }); 26 | return

{props.children}

; 27 | }; 28 | 29 | export const ContentLayout = Object.assign({}, { ButtonGroup, Wrapper, Description }); 30 | -------------------------------------------------------------------------------- /docs/src/components/theme/theme-provider/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 | 10 | -------------------------------------------------------------------------------- /docs/src/components/theme/theme-select/index.astro: -------------------------------------------------------------------------------- 1 | <> 2 | -------------------------------------------------------------------------------- /docs/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const isProd = location.hostname.includes('github.io'); 2 | -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { docsSchema } from '@astrojs/starlight/schema'; 2 | import { defineCollection } from 'astro:content'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | import { toast } from 'somoto' 5 | import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; 6 | 7 | This guide will help you get started with Somoto. 8 | Sonner is good. So i think there should be a toast library for SolidJS. 9 | 10 | ### Install 11 | 12 | 13 | 14 | ```bash 15 | pnpm i somoto 16 | ``` 17 | 18 | 19 | ```bash 20 | npm i somoto 21 | ``` 22 | 23 | 24 | ```bash 25 | yarn add somoto 26 | ``` 27 | 28 | 29 | ```bash 30 | bun add somoto 31 | ``` 32 | 33 | 34 | 35 | ### Add Toaster to your app 36 | 37 | It can be placed anywhere, even in server components such as `layout.tsx`. 38 | 39 | ```tsx 40 | import { Toaster } from 'somoto'; 41 | 42 | export default function RootLayout({ children }: { children: JSX.Element }) { 43 | return ( 44 | 45 | 46 | {children} 47 | 48 | 49 | 50 | ); 51 | } 52 | ``` 53 | 54 | ### Render a toast 55 | 56 | ```tsx 57 | import { toast } from 'somoto'; 58 | 59 | function MyToast() { 60 | return ; 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/src/content/docs/styling.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Styling 3 | --- 4 | 5 | Styling can be done globally via `toastOptions`, this way every toast will have the same styling. 6 | 7 | ```jsx 8 | 16 | ``` 17 | 18 | You can also use the same props when calling `toast` to style a specific toast. 19 | 20 | ```jsx 21 | toast('Hello World', { 22 | style: { 23 | background: 'red', 24 | }, 25 | className: 'class', 26 | }); 27 | ``` 28 | 29 | ## Tailwind CSS 30 | 31 | The preferred way to style the toasts with tailwind is by using the `unstyled` prop. That will give you an unstyled toast which you can then style with tailwind. 32 | 33 | ```jsx 34 | 47 | ``` 48 | 49 | You can do the same when calling `toast()`. 50 | 51 | ```jsx 52 | toast('Hello World', { 53 | unstyled: true, 54 | classNames: { 55 | toast: 'bg-blue-400', 56 | title: 'text-red-400 text-2xl', 57 | description: 'text-red-400', 58 | actionButton: 'bg-zinc-400', 59 | cancelButton: 'bg-orange-400', 60 | closeButton: 'bg-lime-400', 61 | }, 62 | }); 63 | ``` 64 | 65 | Styling per toast type is also possible. 66 | 67 | ```jsx 68 | 79 | ``` 80 | 81 | ## Changing Icons 82 | 83 | You can change the default icons using the `icons` prop: 84 | 85 | ```jsx 86 | , 89 | info: , 90 | warning: , 91 | error: , 92 | loading: , 93 | }} 94 | /> 95 | ``` 96 | 97 | You can also set an icon for each toast: 98 | 99 | ```jsx 100 | toast('Hello World', { 101 | icon: , 102 | }); 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/src/content/docs/toast.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: toast 3 | --- 4 | 5 | import { toast } from 'somoto' 6 | 7 | Use it to render a toast. You can call it from anywhere, even outside of Solid. 8 | 9 | ## Rendering the toast 10 | 11 | You can call it with just a string. 12 | 13 | ```jsx 14 | import { toast } from 'somoto'; 15 | 16 | toast('Hello World!'); 17 | ``` 18 | 19 | Or provide an object as the second argument with more options. They will overwrite the options passed to [``](/toaster) if you have provided any. 20 | 21 | ```jsx 22 | import { toast } from 'somoto'; 23 | 24 | toast('My toast', { 25 | className: 'my-classname', 26 | description: 'My description', 27 | duration: 5000, 28 | icon: , 29 | }); 30 | ``` 31 | 32 | ### Render toast on page load 33 | 34 | To render a toast on initial page load it is required that the function `toast()` is called inside of a `setTimeout` or `requestAnimationFrame`. 35 | 36 | ```jsx 37 | setTimeout(() => { 38 | toast('My toast on a page load'); 39 | }); 40 | ``` 41 | 42 | ## Creating toasts 43 | 44 | ### Success 45 | 46 | Renders a checkmark icon in front of the message. 47 | 48 | ```jsx 49 | toast.success('My success toast'); 50 | ``` 51 | 52 | ### Error 53 | 54 | Renders an error icon in front of the message. 55 | 56 | ```jsx 57 | toast.error('My error toast'); 58 | ``` 59 | 60 | ### Action 61 | 62 | Renders a primary button, clicking it will close the toast and run the callback passed via `onClick`. You can prevent the toast from closing by calling `event.preventDefault()` in the `onClick` callback. 63 | 64 | ```jsx 65 | toast('My action toast', { 66 | action: { 67 | label: 'Action', 68 | onClick: () => console.log('Action!'), 69 | }, 70 | }); 71 | ``` 72 | 73 | You can also render jsx as your action. 74 | 75 | ```jsx 76 | toast('My action toast', { 77 | action: , 78 | }); 79 | ``` 80 | 81 | ### Cancel 82 | 83 | Renders a secondary button, clicking it will close the toast and run the callback passed via `onClick`. 84 | 85 | ```jsx 86 | toast('My cancel toast', { 87 | cancel: { 88 | label: 'Cancel', 89 | onClick: () => console.log('Cancel!'), 90 | }, 91 | }); 92 | ``` 93 | 94 | You can also render jsx in the cancel option. 95 | 96 | ```jsx 97 | toast('My cancel toast', { 98 | cancel: , 99 | }); 100 | ``` 101 | 102 | ### Promise 103 | 104 | Starts in a loading state and will update automatically after the promise resolves or fails. 105 | You can pass a function to the success/error messages to incorporate the result/error of the promise. 106 | 107 | ```jsx 108 | toast.promise(myPromise, { 109 | loading: 'Loading...', 110 | success: data => { 111 | return `${data.name} toast has been added`; 112 | }, 113 | error: 'Error', 114 | }); 115 | ``` 116 | 117 | ### Loading 118 | 119 | Renders a toast with a loading spinner. Useful when you want to handle various states yourself instead of using a promise toast. 120 | 121 | ```jsx 122 | toast.loading('Loading data'); 123 | ``` 124 | 125 | ### Custom 126 | 127 | You can pass jsx as the first argument instead of a string to render a custom toast while maintaining default styling. 128 | 129 | ```jsx 130 | toast(
A custom toast with default styling
, { duration: 5000 }); 131 | ``` 132 | 133 | ### Headless 134 | 135 | Use it to render an unstyled toast with custom jsx while maintaining the functionality. This function receives the `Toast` as an argument, giving you access to all properties. 136 | 137 | ```jsx 138 | toast.custom(t => ( 139 |
140 | This is a custom component 141 |
142 | )); 143 | ``` 144 | 145 | ### Dynamic Position 146 | 147 | You can change the position of the toast dynamically by passing a `position` prop to the toast 148 | function. It will not affect the positioning of other toasts. 149 | 150 | ```jsx 151 | // Available positions: 152 | // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right 153 | toast('Hello World', { 154 | position: 'top-center', 155 | }); 156 | ``` 157 | 158 | ## Other 159 | 160 | ### Updating toasts 161 | 162 | You can update a toast by using the `toast` function and passing it the id of the toast you want to update, the rest stays the same. 163 | 164 | ```jsx 165 | const toastId = toast('somoto'); 166 | 167 | toast.success('Toast has been updated', { 168 | id: toastId, 169 | }); 170 | ``` 171 | 172 | ### On Close Callback 173 | 174 | You can pass `onDismiss` and `onAutoClose` callbacks to each toast. `onDismiss` gets fired when either the close button gets clicked or the toast is swiped. `onAutoClose` fires when the toast disappears automatically after it's timeout (`duration` prop). 175 | 176 | ```jsx 177 | toast('Event has been created', { 178 | onDismiss: t => console.log(`Toast with id ${t.id} has been dismissed`), 179 | onAutoClose: t => console.log(`Toast with id ${t.id} has been closed automatically`), 180 | }); 181 | ``` 182 | 183 | ### Persisting toasts 184 | 185 | If you want a toast to stay on screen forever, you can set the `duration` to [`Infinity`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity). 186 | 187 | ```js 188 | toast('This toast will stay on screen forever', { 189 | duration: Infinity, 190 | }); 191 | ``` 192 | 193 | ### Dismissing toasts programmatically 194 | 195 | To remove a toast programmatically use `toast.dismiss(id)`. The `toast()` function return the id of the toast. 196 | 197 | ```jsx 198 | const toastId = toast('Event has been created'); 199 | 200 | toast.dismiss(toastId); 201 | ``` 202 | 203 | You can also dismiss all toasts at once by calling `toast.dismiss()` without an id. 204 | 205 | ```jsx 206 | toast.dismiss(); 207 | ``` 208 | 209 | ### Rendering custom elements 210 | 211 | You can render custom elements inside the toast like `` or custom components by passing a function instead of a string. This work for both the title and description. 212 | 213 | ```jsx 214 | toast( 215 | () => ( 216 | <> 217 | View 218 | 219 | Animation on the Web 220 | 221 | 222 | ), 223 | { 224 | description: () => , 225 | }, 226 | ); 227 | ``` 228 | 229 | ## API Reference 230 | 231 | | Property | Description | Default | 232 | | :---------------- | :----------------------------------------------------------------------------------------------------: | -------------: | 233 | | description | Toast's description, renders underneath the title. | `-` | 234 | | closeButton | Adds a close button. | `false` | 235 | | invert | Dark toast in light mode and vice versa. | `false` | 236 | | duration | Time in milliseconds that should elapse before automatically closing the toast. | `4000` | 237 | | position | Position of the toast. | `bottom-right` | 238 | | dismissible | If `false`, it'll prevent the user from dismissing the toast. | `true` | 239 | | icon | Icon displayed in front of toast's text, aligned vertically. | `-` | 240 | | action | Renders a primary button, clicking it will close the toast. | `-` | 241 | | cancel | Renders a secondary button, clicking it will close the toast. | `-` | 242 | | id | Custom id for the toast. | `-` | 243 | | onDismiss | The function gets called when either the close button is clicked, or the toast is swiped. | `-` | 244 | | onAutoClose | Function that gets called when the toast disappears automatically after it's timeout (duration` prop). | `-` | 245 | | unstyled | Removes the default styling, which allows for easier customization. | `false` | 246 | | actionButtonStyle | Styles for the action button | `{}` | 247 | | cancelButtonStyle | Styles for the cancel button | `{}` | 248 | -------------------------------------------------------------------------------- /docs/src/content/docs/toaster.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Toaster 3 | --- 4 | 5 | This component renders all the toasts, you can place it anywhere in your app. 6 | 7 | ## Customization 8 | 9 | You can see examples of most of the scenarios described below on the [homepage](/). 10 | 11 | ### Expand 12 | 13 | When you hover on one of the toasts, they will expand. You can make that the default behavior by setting the `expand` prop to `true`, and customize it even further with the `visibleToasts` prop. 14 | 15 | ```jsx 16 | // 9 toasts will be visible instead of the default, which is 3. 17 | 18 | ``` 19 | 20 | ### Position 21 | 22 | Changes the place where all toasts will be rendered. 23 | 24 | ```jsx 25 | // Available positions: 26 | // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right 27 | 28 | ``` 29 | 30 | ### Styling all toasts 31 | 32 | You can customize all toasts at once with `toastOptions` prop. These options will act as the default for all toasts. 33 | 34 | ```jsx 35 | 41 | ``` 42 | 43 | ### dir 44 | 45 | Changes the directionality of the toast's text. 46 | 47 | ```jsx 48 | // rtl, ltr, auto 49 | 50 | ``` 51 | 52 | ## API Reference 53 | 54 | | Property | Description | Default | 55 | | :--- | :-----------: | ------------: | 56 | | theme | Toast's theme, either `light`, `dark`, or `system` | `light` | 57 | | richColors | Makes error and success state more colorful | `false` | 58 | | expand | Toasts will be expanded by default | `false` | 59 | | visibleToasts | Amount of visible toasts | `3` | 60 | | position | Place where the toasts will be rendered | `bottom-right` | 61 | | closeButton | Adds a close button to all toasts | `false` | 62 | | offset | Offset from the edges of the screen. | `32px` | 63 | | dir | Directionality of toast's text | `ltr` | 64 | | hotkey | Keyboard shortcut that will move focus to the toaster area. | `⌥/alt + T` | 65 | | invert | Dark toasts in light mode and vice versa. | `false` | 66 | | toastOptions | These will act as default options for all toasts. See [toast()](/toast) for all available options. | `4000` | 67 | | gap | Gap between toasts when expanded | `14` | 68 | | loadingIcon | Changes the default loading icon | `-` | 69 | | pauseWhenPageIsHidden | Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. | `false` | 70 | | icons | Changes the default icons | `-` | 71 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import PageLayout from '../components/layouts/page-layout.astro'; 3 | import { Somoto } from '../components/solid/demo'; 4 | --- 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/src/utils/getDemoCode.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | export const getDemoCode = (filename: string) => { 4 | return fs.readFileSync(filename, 'utf-8'); 5 | }; 6 | -------------------------------------------------------------------------------- /docs/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from 'clsx'; 2 | import { clsx } from 'clsx'; 3 | import { twMerge } from 'tailwind-merge'; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getDemoCode'; 2 | export * from './helper'; 3 | -------------------------------------------------------------------------------- /docs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import starlightPlugin from '@astrojs/starlight-tailwind'; 2 | import sharedConfig from '@repo/tailwind-config'; 3 | import type { Config } from 'tailwindcss'; 4 | 5 | const accent = { 200: '#b3c7ff', 600: '#364bff', 900: '#182775', 950: '#131e4f' }; 6 | const gray = { 7 | 100: '#f5f6f8', 8 | 200: '#eceef2', 9 | 300: '#c0c2c7', 10 | 400: '#888b96', 11 | 500: '#545861', 12 | 700: '#353841', 13 | 800: '#24272f', 14 | 900: '#17181c', 15 | }; 16 | 17 | const config: Config = { 18 | presets: [sharedConfig], 19 | content: [ 20 | './src/**/*.{astro,html,js,jsx,md,mdx,ts,tsx}', 21 | '../node_modules/@repo/shared/dist/**/*.{js,ts,jsx,tsx}', 22 | ], 23 | plugins: [starlightPlugin()], 24 | theme: { 25 | extend: { 26 | colors: { accent, gray }, 27 | }, 28 | }, 29 | }; 30 | 31 | export default config; 32 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js" 6 | }, 7 | "include": ["."], 8 | "files": [".eslintrc.cjs"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo-root", 3 | "private": true, 4 | "description": "root of the monorepo, for somo & somoto", 5 | "author": { 6 | "name": "Oc1s", 7 | "email": "ocis.chan@gmail.com" 8 | }, 9 | "scripts": { 10 | "build": "turbo build", 11 | "dev": "turbo dev --filter=somoto --filter=somo --filter=somo-dev --filter=@repo/shared", 12 | "dev:doc": "turbo dev --filter=somoto --filter=somo --filter=docs --filter=@repo/shared", 13 | "lint": "turbo lint", 14 | "type-check": "turbo type-check", 15 | "clean": "turbo clean", 16 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 17 | }, 18 | "devDependencies": { 19 | "concurrently": "^8.2.2", 20 | "eslint": "^8.57.0", 21 | "postcss": "^8.4.47", 22 | "prettier": "^3.2.5", 23 | "prettier-plugin-tailwindcss": "^0.5.11", 24 | "turbo": "2.0.4" 25 | }, 26 | "engines": { 27 | "node": ">=18" 28 | }, 29 | "license": "MIT", 30 | "packageManager": "yarn@1.22.22", 31 | "workspaces": [ 32 | "docs", 33 | "dev", 34 | "packages/*" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/config-eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | '@typescript-eslint', 5 | 'no-only-tests', 6 | 'eslint-comments', 7 | 'simple-import-sort', 8 | 'solid', 9 | ], 10 | ignorePatterns: ['node_modules', 'dist', 'dev', 'tsup.config.ts', 'vitest.config.ts'], 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:solid/recommended', 15 | ], 16 | rules: { 17 | 'simple-import-sort/imports': [ 18 | 'warn', 19 | { 20 | groups: [ 21 | // Side effect imports. 22 | ['^\\u0000'], 23 | // Node.js builtins prefixed with `node:`. 24 | ['^node:'], 25 | // Packages. 26 | // Things that start with a letter (or digit or underscore), or `@` followed by a letter. 27 | ['^solid', '^@solid', '^@?\\w'], 28 | // Absolute imports and other imports such as Vue-style `@/foo`. 29 | // Anything not matched in another group. 30 | ['^'], 31 | // Relative imports. 32 | // Anything that starts with a dot. 33 | ['^\\.'], 34 | ], 35 | }, 36 | ], 37 | 'simple-import-sort/exports': 'warn', 38 | 'no-debugger': 'warn', 39 | '@typescript-eslint/consistent-type-imports': 'warn', 40 | '@typescript-eslint/no-unused-vars': [ 41 | 'warn', 42 | { 43 | argsIgnorePattern: '^_', 44 | varsIgnorePattern: '^_', 45 | caughtErrorsIgnorePattern: '^_', 46 | }, 47 | ], 48 | '@typescript-eslint/no-useless-empty-export': 'warn', 49 | 'no-only-tests/no-only-tests': 'warn', 50 | 'eslint-comments/no-unused-disable': 'warn', 51 | 'solid/reactivity': 'off', 52 | 'no-undef': 'off', 53 | '@typescript-eslint/no-unused-expressions': 'off', 54 | '@typescript-eslint/no-explicit-any': 'off', 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/config-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "clear": "rimraf ./node_modules" 8 | }, 9 | "dependencies": { 10 | "@typescript-eslint/eslint-plugin": "^8.12.2", 11 | "@typescript-eslint/parser": "^8.12.2", 12 | "eslint-plugin-eslint-comments": "^3.2.0", 13 | "eslint-plugin-no-only-tests": "^3.3.0", 14 | "eslint-plugin-simple-import-sort": "^12.1.1", 15 | "eslint-plugin-solid": "^0.14.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/config-tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/tailwind-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | ".": "./tailwind.config.ts" 7 | }, 8 | "devDependencies": { 9 | "tailwindcss": "^3.4.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/config-tailwind/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | // each package is responsible for its own content. 4 | const config: Omit = { 5 | darkMode: ['class'], 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: '2rem', 10 | screens: { 11 | '2xl': '1400px', 12 | }, 13 | }, 14 | extend: { 15 | // colors: { 16 | // border: 'hsl(var(--border))', 17 | // input: 'hsl(var(--input))', 18 | // ring: 'hsl(var(--ring))', 19 | // background: 'hsl(var(--background))', 20 | // foreground: 'hsl(var(--foreground))', 21 | // primary: { 22 | // DEFAULT: 'hsl(var(--primary))', 23 | // foreground: 'hsl(var(--primary-foreground))', 24 | // }, 25 | // secondary: { 26 | // DEFAULT: 'hsl(var(--secondary))', 27 | // foreground: 'hsl(var(--secondary-foreground))', 28 | // }, 29 | // destructive: { 30 | // DEFAULT: 'hsl(var(--destructive))', 31 | // foreground: 'hsl(var(--destructive-foreground))', 32 | // }, 33 | // info: { 34 | // DEFAULT: 'hsl(var(--info))', 35 | // foreground: 'hsl(var(--info-foreground))', 36 | // }, 37 | // success: { 38 | // DEFAULT: 'hsl(var(--success))', 39 | // foreground: 'hsl(var(--success-foreground))', 40 | // }, 41 | // warning: { 42 | // DEFAULT: 'hsl(var(--warning))', 43 | // foreground: 'hsl(var(--warning-foreground))', 44 | // }, 45 | // error: { 46 | // DEFAULT: 'hsl(var(--error))', 47 | // foreground: 'hsl(var(--error-foreground))', 48 | // }, 49 | // muted: { 50 | // DEFAULT: 'hsl(var(--muted))', 51 | // foreground: 'hsl(var(--muted-foreground))', 52 | // }, 53 | // accent: { 54 | // DEFAULT: 'hsl(var(--accent))', 55 | // foreground: 'hsl(var(--accent-foreground))', 56 | // }, 57 | // popover: { 58 | // DEFAULT: 'hsl(var(--popover))', 59 | // foreground: 'hsl(var(--popover-foreground))', 60 | // }, 61 | // card: { 62 | // DEFAULT: 'hsl(var(--card))', 63 | // foreground: 'hsl(var(--card-foreground))', 64 | // }, 65 | // }, 66 | // borderRadius: { 67 | // xl: 'calc(var(--radius) + 4px)', 68 | // lg: 'var(--radius)', 69 | // md: 'calc(var(--radius) - 2px)', 70 | // sm: 'calc(var(--radius) - 4px)', 71 | // }, 72 | // keyframes: { 73 | // 'accordion-down': { 74 | // from: { height: 0 }, 75 | // to: { height: 'var(--kb-accordion-content-height)' }, 76 | // }, 77 | // 'accordion-up': { 78 | // from: { height: 'var(--kb-accordion-content-height)' }, 79 | // to: { height: 0 }, 80 | // }, 81 | // 'content-show': { 82 | // from: { opacity: 0, transform: 'scale(0.96)' }, 83 | // to: { opacity: 1, transform: 'scale(1)' }, 84 | // }, 85 | // 'content-hide': { 86 | // from: { opacity: 1, transform: 'scale(1)' }, 87 | // to: { opacity: 0, transform: 'scale(0.96)' }, 88 | // }, 89 | // }, 90 | // animation: { 91 | // 'accordion-down': 'accordion-down 0.2s ease-out', 92 | // 'accordion-up': 'accordion-up 0.2s ease-out', 93 | // 'content-show': 'content-show 0.2s ease-out', 94 | // 'content-hide': 'content-hide 0.2s ease-out', 95 | // }, 96 | }, 97 | }, 98 | }; 99 | export default config; 100 | -------------------------------------------------------------------------------- /packages/config-tailwind/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/shared/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@repo/eslint-config'], 4 | parserOptions: { 5 | project: './tsconfig.json', 6 | tsconfigRootDir: __dirname, 7 | sourceType: 'module', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/shared/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | env: { 4 | NODE_ENV: 'production' | 'development'; 5 | PROD: boolean; 6 | DEV: boolean; 7 | }; 8 | } 9 | namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: 'production' | 'development'; 12 | PROD: boolean; 13 | DEV: boolean; 14 | } 15 | } 16 | } 17 | 18 | export {}; 19 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/shared", 3 | "version": "0.0.0", 4 | "description": "Somo shared", 5 | "license": "MIT", 6 | "author": "Oc1s", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/oc1s/somo.git" 10 | }, 11 | "homepage": "https://github.com/oc1s/somo#readme", 12 | "bugs": { 13 | "url": "https://github.com/oc1s/somo/issues" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "private": false, 19 | "sideEffects": false, 20 | "type": "module", 21 | "main": "./dist/server.js", 22 | "module": "./dist/server.js", 23 | "types": "./dist/index.d.ts", 24 | "browser": { 25 | "./dist/server.js": "./dist/index.js" 26 | }, 27 | "exports": { 28 | "worker": { 29 | "import": { 30 | "types": "./dist/index.d.ts", 31 | "default": "./dist/server.js" 32 | } 33 | }, 34 | "browser": { 35 | "development": { 36 | "import": { 37 | "types": "./dist/index.d.ts", 38 | "default": "./dist/dev.js" 39 | } 40 | }, 41 | "import": { 42 | "types": "./dist/index.d.ts", 43 | "default": "./dist/index.js" 44 | } 45 | }, 46 | "deno": { 47 | "import": { 48 | "types": "./dist/index.d.ts", 49 | "default": "./dist/server.js" 50 | } 51 | }, 52 | "node": { 53 | "import": { 54 | "types": "./dist/index.d.ts", 55 | "default": "./dist/server.js" 56 | } 57 | }, 58 | "development": { 59 | "import": { 60 | "types": "./dist/index.d.ts", 61 | "default": "./dist/dev.js" 62 | } 63 | }, 64 | "import": { 65 | "types": "./dist/index.d.ts", 66 | "default": "./dist/index.js" 67 | } 68 | }, 69 | "typesVersions": {}, 70 | "scripts": { 71 | "dev": "tsup --watch", 72 | "build": "tsup", 73 | "test": "concurrently pnpm:test:*", 74 | "test:client": "vitest", 75 | "test:ssr": "vitest --mode ssr", 76 | "prepublishOnly": "npm run build", 77 | "format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"docs/**/*.{js,ts,json,css,tsx,jsx}\"", 78 | "lint": "concurrently pnpm:lint:*", 79 | "lint:code": "eslint --ignore-path .gitignore --max-warnings 0 src/**/*.{js,ts,tsx,jsx}", 80 | "lint:types": "tsc --noEmit", 81 | "update-deps": "pnpm up -Li" 82 | }, 83 | "dependencies": { 84 | "@solid-primitives/props": "^3.1.11", 85 | "cva": "npm:class-variance-authority", 86 | "somo": "*" 87 | }, 88 | "devDependencies": { 89 | "@types/node": "^22.7.4", 90 | "autoprefixer": "^10.4.19", 91 | "esbuild": "^0.21.3", 92 | "jsdom": "^24.0.0", 93 | "postcss": "^8.4.38", 94 | "solid-js": "^1.9.1", 95 | "tsup": "^8.0.2", 96 | "tsup-preset-solid": "^2.2.0", 97 | "typescript": "^5.4.5", 98 | "vite": "^5.2.11", 99 | "vite-plugin-solid": "^2.10.2", 100 | "vitest": "^1.6.0" 101 | }, 102 | "peerDependencies": { 103 | "solid-js": "^1.6.0" 104 | }, 105 | "keywords": [ 106 | "solid", 107 | "hooks" 108 | ], 109 | "engines": { 110 | "node": ">=18" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/shared/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from 'solid-js'; 2 | import { createSignal, splitProps } from 'solid-js'; 3 | import { Dynamic } from 'solid-js/web'; 4 | import { combineProps } from '@solid-primitives/props'; 5 | import type { VariantProps } from 'cva'; 6 | import { cva } from 'cva'; 7 | import { m } from 'somo'; 8 | const buttonVariants = cva( 9 | 'relative flex h-9 cursor-pointer appearance-none items-center gap-2 overflow-hidden whitespace-nowrap text-nowrap rounded-lg px-4 text-sm text-black no-underline outline-none transition duration-200 hover:opacity-90 focus:opacity-95 data-[pressed=true]:scale-[0.97]', 10 | { 11 | variants: { 12 | variant: { 13 | default: 'bg-white', 14 | bordered: 'border bg-transparent', 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: 'default', 19 | }, 20 | }, 21 | ); 22 | 23 | type ButtonType = 'a' | 'button'; 24 | export type ButtonProps = VariantProps & 25 | ComponentProps; 26 | 27 | export const Button = ( 28 | props: ButtonProps & { 29 | as?: T; 30 | }, 31 | ) => { 32 | const [, domProps] = splitProps(props, ['class', 'variant', 'as']); 33 | const combined = combineProps( 34 | { 35 | onPointerDown: () => setPressed(true), 36 | onPointerUp: () => setPressed(false), 37 | onPointerLeave: () => setPressed(false), 38 | }, 39 | domProps, 40 | ) as unknown as ButtonProps; 41 | const [pressed, setPressed] = createSignal(false); 42 | 43 | const component = () => (props.as === 'a' ? m.a : m.button); 44 | return ( 45 | [0])} 48 | {...combined} 49 | component={component()} 50 | /> 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/shared/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | -------------------------------------------------------------------------------- /packages/shared/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function cn(...classes: (string | undefined)[]) { 2 | return classes.filter(Boolean).join(' '); 3 | } 4 | -------------------------------------------------------------------------------- /packages/shared/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | import sharedConfig from '@repo/tailwind-config'; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | presets: [sharedConfig], 6 | content: ['./src/**/*.{html,js,jsx,ts,tsx}'], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "strict": false, 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "jsx": "preserve", 9 | "jsxImportSource": "solid-js", 10 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 11 | "types": ["node"], 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true, 15 | "noEmit": true, 16 | "isolatedModules": true, 17 | "skipLibCheck": true, 18 | "allowSyntheticDefaultImports": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "noUncheckedIndexedAccess": false, 21 | "strictNullChecks": true, 22 | "suppressImplicitAnyIndexErrors": false, 23 | "noImplicitAny": true, 24 | "noImplicitThis": true, 25 | "noImplicitUseStrict": false, 26 | "baseUrl": ".", 27 | "paths": {} 28 | }, 29 | "include": ["."], 30 | "files": [".eslintrc.cjs"], 31 | "exclude": ["node_modules", "dist"] 32 | } 33 | -------------------------------------------------------------------------------- /packages/shared/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import * as preset from 'tsup-preset-solid'; 3 | 4 | const preset_options: preset.PresetOptions = { 5 | // array or single object 6 | entries: [ 7 | { 8 | // entries with '.ts' extension will have `solid` export condition generated 9 | entry: 'src/index.ts', 10 | // generate a separate development entry 11 | dev_entry: true, 12 | server_entry: true, 13 | }, 14 | ], 15 | // Set to `true` to remove all `console.*` calls and `debugger` statements in prod builds 16 | drop_console: true, 17 | // Set to `true` to generate a CommonJS build alongside ESM 18 | // cjs: true, 19 | }; 20 | 21 | const CI = 22 | process.env['CI'] === 'true' || 23 | process.env['GITHUB_ACTIONS'] === 'true' || 24 | process.env['CI'] === '"1"' || 25 | process.env['GITHUB_ACTIONS'] === '"1"'; 26 | 27 | export default defineConfig(config => { 28 | const watching = !!config.watch; 29 | 30 | const parsed_options = preset.parsePresetOptions(preset_options, watching); 31 | 32 | if (!watching && !CI) { 33 | const package_fields = preset.generatePackageExports(parsed_options); 34 | 35 | console.log(`package.json: \n\n${JSON.stringify(package_fields, null, 2)}\n\n`); 36 | 37 | // will update ./package.json with the correct export fields 38 | preset.writePackageJson(package_fields); 39 | } 40 | 41 | return preset.generateTsupOptions(parsed_options).map(options => ({ 42 | ...options, 43 | clean: true, 44 | })); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/shared/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | // to test in server environment, run with "--mode ssr" or "--mode test:ssr" flag 6 | // loads only server.test.ts file 7 | const testSSR = mode === 'test:ssr' || mode === 'ssr'; 8 | 9 | return { 10 | plugins: [ 11 | solidPlugin({ 12 | // https://github.com/solidjs/solid-refresh/issues/29 13 | hot: false, 14 | // For testing SSR we need to do a SSR JSX transform 15 | solid: { generate: testSSR ? 'ssr' : 'dom' }, 16 | }), 17 | ], 18 | test: { 19 | watch: false, 20 | isolate: !testSSR, 21 | env: { 22 | NODE_ENV: testSSR ? 'production' : 'development', 23 | DEV: testSSR ? '' : '1', 24 | SSR: testSSR ? '1' : '', 25 | PROD: testSSR ? '1' : '', 26 | }, 27 | environment: testSSR ? 'node' : 'jsdom', 28 | transformMode: { web: [/\.[jt]sx$/] }, 29 | ...(testSSR 30 | ? { 31 | include: ['test/server.test.{ts,tsx}'], 32 | } 33 | : { 34 | include: ['test/*.test.{ts,tsx}'], 35 | exclude: ['test/server.test.{ts,tsx}'], 36 | }), 37 | }, 38 | resolve: { 39 | conditions: testSSR ? ['node'] : ['browser', 'development'], 40 | }, 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /packages/somo/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@repo/eslint-config'], 4 | parserOptions: { 5 | project: './tsconfig.json', 6 | tsconfigRootDir: __dirname, 7 | sourceType: 'module', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/somo/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | env: { 4 | NODE_ENV: 'production' | 'development' 5 | PROD: boolean 6 | DEV: boolean 7 | } 8 | } 9 | namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: 'production' | 'development' 12 | PROD: boolean 13 | DEV: boolean 14 | } 15 | } 16 | } 17 | 18 | export {} 19 | -------------------------------------------------------------------------------- /packages/somo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somo", 3 | "version": "0.0.0", 4 | "description": "SolidJS Motion", 5 | "license": "MIT", 6 | "author": "Oc1s", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/oc1s/somo.git" 10 | }, 11 | "homepage": "https://github.com/oc1s/somo#readme", 12 | "bugs": { 13 | "url": "https://github.com/oc1s/somo/issues" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "private": false, 19 | "sideEffects": false, 20 | "type": "module", 21 | "main": "./dist/server.cjs", 22 | "module": "./dist/server.js", 23 | "types": "./dist/index.d.ts", 24 | "browser": { 25 | "./dist/server.js": "./dist/index.js", 26 | "./dist/server.cjs": "./dist/index.cjs" 27 | }, 28 | "exports": { 29 | "worker": { 30 | "import": { 31 | "types": "./dist/index.d.ts", 32 | "default": "./dist/server.js" 33 | }, 34 | "require": { 35 | "types": "./dist/index.d.cts", 36 | "default": "./dist/server.cjs" 37 | } 38 | }, 39 | "browser": { 40 | "development": { 41 | "import": { 42 | "types": "./dist/index.d.ts", 43 | "default": "./dist/dev.js" 44 | }, 45 | "require": { 46 | "types": "./dist/index.d.cts", 47 | "default": "./dist/dev.cjs" 48 | } 49 | }, 50 | "import": { 51 | "types": "./dist/index.d.ts", 52 | "default": "./dist/index.js" 53 | }, 54 | "require": { 55 | "types": "./dist/index.d.cts", 56 | "default": "./dist/index.cjs" 57 | } 58 | }, 59 | "deno": { 60 | "import": { 61 | "types": "./dist/index.d.ts", 62 | "default": "./dist/server.js" 63 | }, 64 | "require": { 65 | "types": "./dist/index.d.cts", 66 | "default": "./dist/server.cjs" 67 | } 68 | }, 69 | "node": { 70 | "import": { 71 | "types": "./dist/index.d.ts", 72 | "default": "./dist/server.js" 73 | }, 74 | "require": { 75 | "types": "./dist/index.d.cts", 76 | "default": "./dist/server.cjs" 77 | } 78 | }, 79 | "development": { 80 | "import": { 81 | "types": "./dist/index.d.ts", 82 | "default": "./dist/dev.js" 83 | }, 84 | "require": { 85 | "types": "./dist/index.d.cts", 86 | "default": "./dist/dev.cjs" 87 | } 88 | }, 89 | "import": { 90 | "types": "./dist/index.d.ts", 91 | "default": "./dist/index.js" 92 | }, 93 | "require": { 94 | "types": "./dist/index.d.cts", 95 | "default": "./dist/index.cjs" 96 | } 97 | }, 98 | "typesVersions": {}, 99 | "scripts": { 100 | "dev": "tsup --watch", 101 | "build": "tsup", 102 | "test": "concurrently pnpm:test:*", 103 | "test:client": "vitest", 104 | "test:ssr": "vitest --mode ssr", 105 | "prepublishOnly": "npm run build", 106 | "format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"docs/**/*.{js,ts,json,css,tsx,jsx}\"", 107 | "lint": "concurrently pnpm:lint:*", 108 | "lint:code": "eslint --ignore-path .gitignore --max-warnings 0 src/**/*.{js,ts,tsx,jsx}", 109 | "lint:types": "tsc --noEmit", 110 | "update-deps": "pnpm up -Li" 111 | }, 112 | "dependencies": { 113 | "@motionone/dom": "^10.18.0", 114 | "@solid-primitives/props": "^3.1.11", 115 | "@solid-primitives/refs": "^1.0.8", 116 | "@solid-primitives/transition-group": "^1.0.5", 117 | "lodash-es": "^4.17.21" 118 | }, 119 | "devDependencies": { 120 | "@motionone/types": "^10.17.1", 121 | "@types/lodash-es": "^4.17.12", 122 | "@types/node": "^22.7.4", 123 | "esbuild": "^0.21.3", 124 | "esbuild-plugin-solid": "^0.6.0", 125 | "jsdom": "^24.0.0", 126 | "solid-js": "^1.9.1", 127 | "tsup": "^8.0.2", 128 | "tsup-preset-solid": "^2.2.0", 129 | "typescript": "^5.4.5", 130 | "vite": "^5.2.11", 131 | "vite-plugin-solid": "^2.10.2", 132 | "vitest": "^1.6.0" 133 | }, 134 | "peerDependencies": { 135 | "solid-js": ">=1.6.0" 136 | }, 137 | "keywords": [ 138 | "solid", 139 | "hooks" 140 | ], 141 | "engines": { 142 | "node": ">=18" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/somo/src/components/auto-layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Accessor, createSignal, onMount, Setter } from 'solid-js'; 2 | 3 | import autoAnimate, { AnimationController, AutoAnimateOptions, AutoAnimationPlugin } from './base'; 4 | 5 | declare module 'solid-js' { 6 | namespace JSX { 7 | interface Directives { 8 | autoAnimate: Partial | AutoAnimationPlugin | true; 9 | } 10 | } 11 | } 12 | 13 | export const createAutoAnimate = ( 14 | options: Partial | AutoAnimationPlugin = {}, 15 | ): [Setter, (enabled: boolean) => void] => { 16 | const [element, setElement] = createSignal(null); 17 | 18 | let controller: AnimationController | undefined; 19 | // Will help us set enabled even before the element is mounted 20 | let active = true; 21 | 22 | onMount(() => { 23 | const el = element(); 24 | if (el) { 25 | controller = autoAnimate(el, options); 26 | if (active) controller.enable(); 27 | else controller.disable(); 28 | } 29 | }); 30 | 31 | const setEnabled = (enabled: boolean) => { 32 | active = enabled; 33 | if (controller) { 34 | enabled ? controller.enable() : controller.disable(); 35 | } 36 | }; 37 | 38 | return [setElement, setEnabled]; 39 | }; 40 | 41 | export const createAutoAnimateDirective = () => { 42 | return ( 43 | el: HTMLElement, 44 | options: Accessor | AutoAnimationPlugin | true>, 45 | ) => { 46 | let optionsValue = options(); 47 | let resolvedOptions: Partial | AutoAnimationPlugin = {}; 48 | if (optionsValue !== true) resolvedOptions = optionsValue; 49 | autoAnimate(el, resolvedOptions); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/somo/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './auto-layout'; 2 | export * from './motion'; 3 | export * from './presence'; 4 | -------------------------------------------------------------------------------- /packages/somo/src/components/motion.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | import { createContext, splitProps, useContext } from 'solid-js'; 3 | import { Dynamic } from 'solid-js/web'; 4 | import { combineStyle } from '@solid-primitives/props'; 5 | import type { MotionState } from '@motionone/dom'; 6 | 7 | import { createAndBindMotionState } from '../primitives.js'; 8 | import type { HElements, IMotionComponent, MotionProps, MotionProxy } from '../types/index.js'; 9 | import { PresenceContext } from './presence.jsx'; 10 | 11 | const OPTION_KEYS = [ 12 | 'initial', 13 | 'animate', 14 | 'exit', 15 | 'inView', 16 | 'inViewOptions', 17 | 'hover', 18 | 'press', 19 | 'variants', 20 | 'transition', 21 | ] as const; 22 | 23 | const EXCLUDE_KEYS = ['tag'] as const; 24 | 25 | export const ParentContext = createContext(); 26 | 27 | /** @internal */ 28 | const MotionComponent = (props: MotionProps): JSX.Element => { 29 | const [options, , domProps] = splitProps(props, OPTION_KEYS, EXCLUDE_KEYS); 30 | 31 | const [state, style] = createAndBindMotionState( 32 | () => root, 33 | () => ({ ...options }), 34 | useContext(PresenceContext), 35 | useContext(ParentContext), 36 | ); 37 | 38 | let root!: Element; 39 | return ( 40 | 41 | { 44 | root = el; 45 | props.ref?.(el); 46 | }} 47 | component={props.tag || 'div'} 48 | style={combineStyle(props.style, style)} 49 | /> 50 | 51 | ); 52 | }; 53 | 54 | /** 55 | * Renders an animatable HTML or SVG element. 56 | * 57 | * @component 58 | * Animation props: 59 | * - `animate` a target of values to animate to. Accepts all the same values and keyframes as Motion One's [animate function](https://motion.dev/dom/animate). This prop is **reactive** – changing it will animate the transition element to the new state. 60 | * - `transition` for changing type of animation 61 | * - `initial` a target of values to animate from when the element is first rendered. 62 | * - `exit` a target of values to animate to when the element is removed. The element must be a direct child of the `` component. 63 | * 64 | * @example 65 | * ```tsx 66 | * 67 | * ``` 68 | * 69 | * Interaction animation props: 70 | * 71 | * - `inView` animation target for when the element is in view 72 | * - `hover` animate when hovered 73 | * - `press` animate when pressed 74 | * 75 | * @example 76 | * ```tsx 77 | * 78 | * ``` 79 | */ 80 | export const Motion = new Proxy(MotionComponent, { 81 | get: 82 | (_: any, tag: T): IMotionComponent => 83 | props => { 84 | return ; 85 | }, 86 | }) as MotionProxy; 87 | 88 | /** 89 | * Alias of `Motion` 90 | * 91 | * Renders an animatable HTML or SVG element. 92 | * 93 | * @component 94 | * Animation props: 95 | * - `animate` a target of values to animate to. Accepts all the same values and keyframes as Motion One's [animate function](https://motion.dev/dom/animate). This prop is **reactive** – changing it will animate the transition element to the new state. 96 | * - `transition` for changing type of animation 97 | * - `initial` a target of values to animate from when the element is first rendered. 98 | * - `exit` a target of values to animate to when the element is removed. The element must be a direct child of the `` component. 99 | * 100 | * @example 101 | * ```tsx 102 | * 103 | * ``` 104 | * 105 | * Interaction animation props: 106 | * 107 | * - `inView` animation target for when the element is in view 108 | * - `hover` animate when hovered 109 | * - `press` animate when pressed 110 | * 111 | * @example 112 | * ```tsx 113 | * 114 | * ``` 115 | */ 116 | export const m = Motion; 117 | -------------------------------------------------------------------------------- /packages/somo/src/components/presence.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type Accessor, 3 | batch, 4 | createContext, 5 | createSignal, 6 | type FlowComponent, 7 | type JSX, 8 | } from 'solid-js'; 9 | import { resolveFirst } from '@solid-primitives/refs'; 10 | import { createSwitchTransition } from '@solid-primitives/transition-group'; 11 | import { mountedStates } from '@motionone/dom'; 12 | 13 | export type PresenceContextState = { 14 | initial: boolean; 15 | mount: Accessor; 16 | }; 17 | export const PresenceContext = createContext(); 18 | 19 | /** 20 | * Perform exit/enter trantisions of children `` components. 21 | * 22 | * accepts props: 23 | * - `initial` – *(Defaults to `true`)* – If `false`, will disable the first animation on all child `Motion` elements the first time `Presence` is rendered. 24 | * - `exitBeforeEnter` – *(Defaults to `false`)* – If `true`, `Presence` will wait for the exiting element to finish animating out before animating in the next one. 25 | * 26 | * @example 27 | * ```tsx 28 | * 29 | * 30 | * 35 | * 36 | * 37 | * ``` 38 | */ 39 | export const Presence: FlowComponent<{ 40 | initial?: boolean; 41 | mode?: 'parallel' | 'out-in' | 'in-out'; 42 | }> = props => { 43 | const [mount, setMount] = createSignal(true), 44 | state = { initial: props.initial ?? true, mount }, 45 | render = ( 46 | 47 | { 48 | createSwitchTransition( 49 | resolveFirst(() => props.children), 50 | { 51 | appear: state.initial, 52 | mode: props.mode, 53 | onEnter(_, done) { 54 | batch(() => { 55 | setMount(true); 56 | done(); 57 | }); 58 | }, 59 | onExit(el, done) { 60 | /* setMount & done */ 61 | batch(() => { 62 | setMount(false); 63 | mountedStates.get(el)?.getOptions().exit 64 | ? el.addEventListener('motioncomplete', done) 65 | : done(); 66 | }); 67 | }, 68 | }, 69 | ) as any as JSX.Element 70 | } 71 | 72 | ); 73 | 74 | state.initial = true; 75 | return render; 76 | }; 77 | -------------------------------------------------------------------------------- /packages/somo/src/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'solid-js'; 2 | import type { AnimationOptionsWithOverrides } from '@motionone/dom'; 3 | 4 | export const MotionConfig = createContext<{ 5 | transition?: AnimationOptionsWithOverrides; 6 | }>({}); 7 | -------------------------------------------------------------------------------- /packages/somo/src/easing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spring'; 2 | -------------------------------------------------------------------------------- /packages/somo/src/easing/spring.ts: -------------------------------------------------------------------------------- 1 | export { spring } from '@motionone/dom'; 2 | -------------------------------------------------------------------------------- /packages/somo/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './easing'; 3 | export { createMotion, motion } from './primitives'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/somo/src/primitives.ts: -------------------------------------------------------------------------------- 1 | import type { Accessor } from 'solid-js'; 2 | import { createEffect, createMemo, onCleanup, useContext } from 'solid-js'; 3 | import type { MotionState } from '@motionone/dom'; 4 | import { createMotionState, createStyles, style } from '@motionone/dom'; 5 | import type { KeyframeOptions } from '@motionone/types'; 6 | import { isObject } from 'lodash-es'; 7 | 8 | import type { PresenceContextState } from './components/presence.jsx'; 9 | import { PresenceContext } from './components/presence.jsx'; 10 | import { MotionConfig } from './context.js'; 11 | import type { Options } from './types/index.js'; 12 | import { defaultTransitionKeys, defaultTransitions } from './utils/defaults.js'; 13 | import { objectKeys } from './utils/helper.js'; 14 | 15 | const generateTransition = (options: Options) => { 16 | const keys = new Set(); 17 | objectKeys(options).forEach(key => { 18 | const variantDef = options[key]; 19 | isObject(variantDef) && 20 | objectKeys(variantDef as object).forEach(k => { 21 | keys.add(k); 22 | }); 23 | }); 24 | 25 | const defaultTransition = [...keys].reduce( 26 | (obj, key: keyof typeof defaultTransitions) => { 27 | if (defaultTransitionKeys.has(key)) { 28 | obj[key] = defaultTransitions[key](); 29 | } 30 | return obj; 31 | }, 32 | {} as Record, 33 | ); 34 | 35 | return defaultTransition; 36 | }; 37 | 38 | /** @internal */ 39 | export function createAndBindMotionState( 40 | el: () => Element, 41 | options: Accessor, 42 | presenceState?: PresenceContextState, 43 | parentState?: MotionState, 44 | ): [MotionState, ReturnType] { 45 | const contextConfig = useContext(MotionConfig); 46 | 47 | const computedOptions = createMemo(() => { 48 | const $options = { ...options() }; 49 | $options.transition = 50 | $options.transition || contextConfig.transition || generateTransition($options); 51 | return $options; 52 | }); 53 | 54 | const motionState = createMotionState( 55 | presenceState?.initial === false ? { ...computedOptions(), initial: false } : computedOptions(), 56 | parentState, 57 | ); 58 | 59 | createEffect(() => { 60 | /* 61 | Motion components under should wait before animating in this is done with additional signal, because effects will still run immediately 62 | */ 63 | if (presenceState && !presenceState.mount()) return; 64 | 65 | const element = el(), 66 | unmount = motionState.mount(element); 67 | 68 | /* 触发状态变化 */ 69 | createEffect(() => motionState.update(computedOptions())); 70 | 71 | onCleanup(() => { 72 | /* 需要等到dom消失的情况 */ 73 | if (presenceState && computedOptions().exit) { 74 | motionState.setActive('exit', true); 75 | element.addEventListener('motioncomplete', unmount); 76 | } else { 77 | /* 直接调用motionState unmount */ 78 | unmount(); 79 | } 80 | }); 81 | }); 82 | 83 | return [motionState, createStyles(motionState.getTarget())] as const; 84 | } 85 | 86 | /** 87 | * createMotion provides MotionOne as a compact Solid primitive. 88 | * 89 | * @param target Target Element to animate. 90 | * @param options Options to effect the animation. 91 | * @param presenceState Optional PresenceContext override, defaults to current parent. 92 | * @returns Object to access MotionState 93 | */ 94 | export function createMotion( 95 | target: Element, 96 | options: Accessor | Options, 97 | presenceState?: PresenceContextState, 98 | ): MotionState { 99 | const [state, styles] = createAndBindMotionState( 100 | () => target, 101 | typeof options === 'function' ? options : () => options, 102 | presenceState, 103 | ); 104 | 105 | for (const key in styles) { 106 | style.set(target, key, styles[key]); 107 | } 108 | 109 | return state; 110 | } 111 | 112 | /** 113 | * motion is a Solid directive that makes binding to elements easier. 114 | * 115 | * @param el Target Element to bind to. 116 | * @param props Options to effect the animation. 117 | */ 118 | export function motion(el: Element, props: Accessor): void { 119 | createMotion(el, props, useContext(PresenceContext)); 120 | } 121 | -------------------------------------------------------------------------------- /packages/somo/src/types/helper.ts: -------------------------------------------------------------------------------- 1 | import { Accessor } from 'solid-js'; 2 | 3 | export type MaybeAccessor = T | Accessor; 4 | -------------------------------------------------------------------------------- /packages/somo/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { JSX, ParentProps } from 'solid-js'; 2 | import type * as motionone from '@motionone/dom'; 3 | import type { PropertiesHyphen } from 'csstype'; 4 | export type { Options } from '@motionone/dom'; 5 | 6 | export interface MotionEventHandlers { 7 | onMotionStart?: (event: motionone.MotionEvent) => void; 8 | onMotionComplete?: (event: motionone.MotionEvent) => void; 9 | onHoverStart?: (event: motionone.CustomPointerEvent) => void; 10 | onHoverEnd?: (event: motionone.CustomPointerEvent) => void; 11 | onPressStart?: (event: motionone.CustomPointerEvent) => void; 12 | onPressEnd?: (event: motionone.CustomPointerEvent) => void; 13 | onViewEnter?: (event: motionone.ViewEvent) => void; 14 | onViewLeave?: (event: motionone.ViewEvent) => void; 15 | } 16 | 17 | declare module '@motionone/dom' { 18 | /* 19 | Solid style attribute supports only kebab-case properties. 20 | While @motionone/dom supports both camelCase and kebab-case, 21 | but provides only camelCase properties in the types. 22 | */ 23 | interface CSSStyleDeclarationWithTransform 24 | extends Omit {} 25 | 26 | /* 27 | exit is missing in types in motionone core 28 | because it is only used in the Presence implementations 29 | */ 30 | interface Options { 31 | exit?: motionone.VariantDefinition; 32 | } 33 | } 34 | 35 | export type HElements = keyof JSX.IntrinsicElements; 36 | // export only here so the `JSX` import won't be shaken off the tree: 37 | export type E = JSX.Element; 38 | 39 | export type MotionProps = ParentProps< 40 | JSX.IntrinsicElements[T] & 41 | MotionEventHandlers & 42 | motionone.Options & { 43 | ref?: any; 44 | tag?: string; 45 | /* TODO:whiles */ 46 | whileTap?: motionone.VariantDefinition; 47 | whileFocus?: motionone.VariantDefinition; 48 | whileHover?: motionone.VariantDefinition; 49 | whileInView?: motionone.VariantDefinition; 50 | whileDrag?: motionone.VariantDefinition; 51 | } 52 | >; 53 | 54 | // export type MotionComponent = { 55 | // // 56 | // (props: JSX.IntrinsicElements['div'] & MotionComponentProps): JSX.Element; 57 | // // 58 | // ( 59 | // props: JSX.IntrinsicElements[T] & MotionComponentProps & { tag: T }, 60 | // ): JSX.Element; 61 | // }; 62 | 63 | export type IMotionComponent = (props: MotionProps) => JSX.Element; 64 | 65 | /* proxy type defination */ 66 | export type MotionProxy = IMotionComponent & { 67 | // form as 68 | [K in HElements]: IMotionComponent; 69 | }; 70 | 71 | declare module 'solid-js' { 72 | namespace JSX { 73 | interface Directives { 74 | motion: motionone.Options; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/somo/src/types/interface.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oc1S/somo/116d25d2e0de0a07f220dfe0f53f07ef96d70987/packages/somo/src/types/interface.ts -------------------------------------------------------------------------------- /packages/somo/src/utils/defaults.ts: -------------------------------------------------------------------------------- 1 | // import type { 2 | // Keyframes, 3 | // KeyframesTarget, 4 | // PopmotionTransitionProps, 5 | // SingleTarget, 6 | // Spring, 7 | // ValueTarget, 8 | // } from '../types'; 9 | 10 | import { spring } from '@motionone/dom'; 11 | import type { KeyframeOptions } from '@motionone/types'; 12 | 13 | import { objectKeys } from './helper'; 14 | 15 | type ValueTarget = string | number; 16 | 17 | export function isKeyframesTarget(v: ValueTarget) { 18 | return Array.isArray(v); 19 | } 20 | 21 | export function underDampedSpring(): KeyframeOptions { 22 | return { 23 | easing: spring({ 24 | stiffness: 500, 25 | damping: 25, 26 | restSpeed: 10, 27 | }), 28 | }; 29 | } 30 | 31 | export function criticallyDampedSpring(): KeyframeOptions { 32 | return { 33 | easing: spring({ 34 | stiffness: 550, 35 | damping: 30, 36 | restSpeed: 10, 37 | }), 38 | }; 39 | } 40 | 41 | export function overDampedSpring(): KeyframeOptions { 42 | return { 43 | easing: spring({ 44 | stiffness: 550, 45 | damping: 30, 46 | restSpeed: 10, 47 | }), 48 | }; 49 | } 50 | 51 | export function linear(): KeyframeOptions { 52 | return { 53 | easing: 'linear', 54 | }; 55 | } 56 | 57 | const defaultSpring = () => ({ 58 | easing: spring({ 59 | stiffness: 140, 60 | damping: 14, 61 | restSpeed: 5, 62 | }), 63 | }); 64 | 65 | export const defaultTransitions = { 66 | // default: overDampedSpring, 67 | // x: underDampedSpring, 68 | // y: underDampedSpring, 69 | // z: underDampedSpring, 70 | // rotate: underDampedSpring, 71 | // rotateX: underDampedSpring, 72 | // rotateY: underDampedSpring, 73 | // rotateZ: underDampedSpring, 74 | // scaleX: criticallyDampedSpring, 75 | // scaleY: criticallyDampedSpring, 76 | // scale: criticallyDampedSpring, 77 | // default: linear, 78 | x: defaultSpring, 79 | y: defaultSpring, 80 | z: defaultSpring, 81 | rotate: defaultSpring, 82 | rotateX: defaultSpring, 83 | rotateY: defaultSpring, 84 | rotateZ: defaultSpring, 85 | scaleX: defaultSpring, 86 | scaleY: defaultSpring, 87 | scale: defaultSpring, 88 | // backgroundColor: linear, 89 | // color: linear, 90 | // opacity: linear, 91 | }; 92 | export const defaultTransitionKeys = new Set(objectKeys(defaultTransitions)); 93 | 94 | // export function getDefaultTransition(valueKey: string) { 95 | // return ( 96 | // defaultTransitions[valueKey as keyof typeof defaultTransitions] || defaultTransitions.default 97 | // ); 98 | // } 99 | -------------------------------------------------------------------------------- /packages/somo/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | export function objectEntries(obj: T) { 2 | return Object.entries(obj) as Array<[keyof T, T[keyof T]]>; 3 | } 4 | 5 | export function objectKeys(obj: T) { 6 | return Object.keys(obj) as Array; 7 | } 8 | -------------------------------------------------------------------------------- /packages/somo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "compilerOptions": { 4 | "allowJs": true, 5 | // "strict": true, 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "isolatedModules": true, 14 | "skipLibCheck": true, 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noUncheckedIndexedAccess": false, 18 | "strictNullChecks": true, 19 | "suppressImplicitAnyIndexErrors": false, 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "noImplicitUseStrict": false, 23 | "jsx": "preserve", 24 | "jsxImportSource": "solid-js", 25 | "types": ["node"], 26 | "baseUrl": ".", 27 | "paths": {} 28 | }, 29 | "include": ["."], 30 | "files": [".eslintrc.cjs"], 31 | "exclude": ["node_modules", "dist"] 32 | } 33 | -------------------------------------------------------------------------------- /packages/somo/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import * as preset from 'tsup-preset-solid'; 3 | 4 | const preset_options: preset.PresetOptions = { 5 | // array or single object 6 | entries: [ 7 | { 8 | // entries with '.ts' extension will have `solid` export condition generated 9 | entry: 'src/index.ts', 10 | // will generate a separate development entry 11 | dev_entry: true, 12 | server_entry: true, 13 | }, 14 | ], 15 | // Set to `true` to remove all `console.*` calls and `debugger` statements in prod builds 16 | drop_console: true, 17 | // Set to `true` to generate a CommonJS build alongside ESM 18 | cjs: true, 19 | }; 20 | 21 | const CI = 22 | process.env['CI'] === 'true' || 23 | process.env['GITHUB_ACTIONS'] === 'true' || 24 | process.env['CI'] === '"1"' || 25 | process.env['GITHUB_ACTIONS'] === '"1"'; 26 | 27 | export default defineConfig(config => { 28 | const watching = !!config.watch; 29 | 30 | const parsed_options = preset.parsePresetOptions(preset_options, watching); 31 | 32 | if (!watching && !CI) { 33 | const package_fields = preset.generatePackageExports(parsed_options); 34 | 35 | console.log(`package.json: \n\n${JSON.stringify(package_fields, null, 2)}\n\n`); 36 | 37 | // will update ./package.json with the correct export fields 38 | preset.writePackageJson(package_fields); 39 | } 40 | 41 | return preset.generateTsupOptions(parsed_options); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/somo/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import solidPlugin from 'vite-plugin-solid' 3 | 4 | export default defineConfig(({ mode }) => { 5 | // to test in server environment, run with "--mode ssr" or "--mode test:ssr" flag 6 | // loads only server.test.ts file 7 | const testSSR = mode === 'test:ssr' || mode === 'ssr' 8 | 9 | return { 10 | plugins: [ 11 | solidPlugin({ 12 | // https://github.com/solidjs/solid-refresh/issues/29 13 | hot: false, 14 | // For testing SSR we need to do a SSR JSX transform 15 | solid: { generate: testSSR ? 'ssr' : 'dom' }, 16 | }), 17 | ], 18 | test: { 19 | watch: false, 20 | isolate: !testSSR, 21 | env: { 22 | NODE_ENV: testSSR ? 'production' : 'development', 23 | DEV: testSSR ? '' : '1', 24 | SSR: testSSR ? '1' : '', 25 | PROD: testSSR ? '1' : '', 26 | }, 27 | environment: testSSR ? 'node' : 'jsdom', 28 | transformMode: { web: [/\.[jt]sx$/] }, 29 | ...(testSSR 30 | ? { 31 | include: ['test/server.test.{ts,tsx}'], 32 | } 33 | : { 34 | include: ['test/*.test.{ts,tsx}'], 35 | exclude: ['test/server.test.{ts,tsx}'], 36 | }), 37 | }, 38 | resolve: { 39 | conditions: testSSR ? ['node'] : ['browser', 'development'], 40 | }, 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /packages/somoto/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@repo/eslint-config'], 4 | parserOptions: { 5 | project: './tsconfig.json', 6 | tsconfigRootDir: __dirname, 7 | sourceType: 'module', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/somoto/README.md: -------------------------------------------------------------------------------- 1 | # somoto 2 | 3 | > A SolidJS port for [Sonner](https://github.com/emilkowalski/sonner). 4 | 5 | somoto is a taost library for SolidJS. 6 | 7 | For demonstration, please visit The [site](https://oc1s.github.io/somo/). 8 | 9 | ## Quick start 10 | 11 | ### Install: 12 | 13 | ```bash 14 | npm i somoto 15 | # or 16 | yarn add somoto 17 | # or 18 | pnpm add somoto 19 | # or 20 | bun add smoto 21 | ``` 22 | 23 | ### Usage: 24 | 25 | ```jsx 26 | import { Toaster, toast } from 'somoto'; 27 | 28 | function App() { 29 | return ( 30 |
31 | 32 | 33 |
34 | ); 35 | } 36 | ``` 37 | 38 | ## Documentation 39 | 40 | Find API references in the [doc](https://oc1s.github.io/somo/getting-started/). 41 | -------------------------------------------------------------------------------- /packages/somoto/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ImportMeta { 3 | env: { 4 | NODE_ENV: 'production' | 'development'; 5 | PROD: boolean; 6 | DEV: boolean; 7 | }; 8 | } 9 | namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: 'production' | 'development'; 12 | PROD: boolean; 13 | DEV: boolean; 14 | } 15 | } 16 | } 17 | 18 | export {}; 19 | -------------------------------------------------------------------------------- /packages/somoto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "somoto", 3 | "version": "0.0.2", 4 | "description": "SolidJS Motion Toast", 5 | "license": "MIT", 6 | "author": "Oc1s", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/oc1s/somo.git" 10 | }, 11 | "homepage": "https://oc1s.github.io/somo/", 12 | "bugs": { 13 | "url": "https://github.com/oc1s/somo/issues" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "private": false, 19 | "sideEffects": true, 20 | "type": "module", 21 | "main": "./dist/server.cjs", 22 | "module": "./dist/server.js", 23 | "types": "./dist/index.d.ts", 24 | "browser": { 25 | "./dist/server.js": "./dist/index.js", 26 | "./dist/server.cjs": "./dist/index.cjs" 27 | }, 28 | "exports": { 29 | "worker": { 30 | "import": { 31 | "types": "./dist/index.d.ts", 32 | "default": "./dist/server.js" 33 | }, 34 | "require": { 35 | "types": "./dist/index.d.cts", 36 | "default": "./dist/server.cjs" 37 | } 38 | }, 39 | "browser": { 40 | "development": { 41 | "import": { 42 | "types": "./dist/index.d.ts", 43 | "default": "./dist/dev.js" 44 | }, 45 | "require": { 46 | "types": "./dist/index.d.cts", 47 | "default": "./dist/dev.cjs" 48 | } 49 | }, 50 | "import": { 51 | "types": "./dist/index.d.ts", 52 | "default": "./dist/index.js" 53 | }, 54 | "require": { 55 | "types": "./dist/index.d.cts", 56 | "default": "./dist/index.cjs" 57 | } 58 | }, 59 | "deno": { 60 | "import": { 61 | "types": "./dist/index.d.ts", 62 | "default": "./dist/server.js" 63 | }, 64 | "require": { 65 | "types": "./dist/index.d.cts", 66 | "default": "./dist/server.cjs" 67 | } 68 | }, 69 | "node": { 70 | "import": { 71 | "types": "./dist/index.d.ts", 72 | "default": "./dist/server.js" 73 | }, 74 | "require": { 75 | "types": "./dist/index.d.cts", 76 | "default": "./dist/server.cjs" 77 | } 78 | }, 79 | "development": { 80 | "import": { 81 | "types": "./dist/index.d.ts", 82 | "default": "./dist/dev.js" 83 | }, 84 | "require": { 85 | "types": "./dist/index.d.cts", 86 | "default": "./dist/dev.cjs" 87 | } 88 | }, 89 | "import": { 90 | "types": "./dist/index.d.ts", 91 | "default": "./dist/index.js" 92 | }, 93 | "require": { 94 | "types": "./dist/index.d.cts", 95 | "default": "./dist/index.cjs" 96 | } 97 | }, 98 | "typesVersions": {}, 99 | "scripts": { 100 | "dev": "tsup --watch", 101 | "build": "tsup", 102 | "test": "concurrently pnpm:test:*", 103 | "test:client": "vitest", 104 | "test:ssr": "vitest --mode ssr", 105 | "prepublishOnly": "npm run build", 106 | "format": "prettier --ignore-path .gitignore -w \"src/**/*.{js,ts,json,css,tsx,jsx}\" \"docs/**/*.{js,ts,json,css,tsx,jsx}\"", 107 | "lint": "concurrently pnpm:lint:*", 108 | "lint:code": "eslint --ignore-path .gitignore --max-warnings 0 src/**/*.{js,ts,tsx,jsx}", 109 | "lint:types": "tsc --noEmit", 110 | "update-deps": "pnpm up -Li" 111 | }, 112 | "devDependencies": { 113 | "@types/node": "^22.7.4", 114 | "esbuild": "^0.21.3", 115 | "esbuild-plugin-solid": "^0.6.0", 116 | "jsdom": "^24.0.0", 117 | "solid-js": "^1.9.1", 118 | "tsup": "^8.0.2", 119 | "tsup-preset-solid": "^2.2.0", 120 | "typescript": "^5.4.5", 121 | "vite": "^5.2.11", 122 | "vite-plugin-solid": "^2.10.2", 123 | "vitest": "^1.6.0" 124 | }, 125 | "peerDependencies": { 126 | "solid-js": ">=1.6.0" 127 | }, 128 | "keywords": [ 129 | "solid", 130 | "hooks", 131 | "notifications", 132 | "toast", 133 | "snackbar", 134 | "message" 135 | ], 136 | "engines": { 137 | "node": ">=18" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/somoto/src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | import { For } from 'solid-js'; 3 | 4 | import type { ToastVariants } from '../types'; 5 | 6 | export const getIcon = (type: ToastVariants): JSX.Element | null => { 7 | switch (type) { 8 | case 'success': 9 | return ; 10 | case 'info': 11 | return ; 12 | case 'warning': 13 | return ; 14 | case 'error': 15 | return ; 16 | default: 17 | return null; 18 | } 19 | }; 20 | 21 | export const Loading = (props: { visible: boolean }) => { 22 | return ( 23 |
24 |
25 | {() =>
} 26 |
27 |
28 | ); 29 | }; 30 | 31 | const SuccessIcon = () => ( 32 | 39 | 44 | 45 | ); 46 | 47 | const WarningIcon = () => ( 48 | 55 | 60 | 61 | ); 62 | 63 | const InfoIcon = () => ( 64 | 71 | 76 | 77 | ); 78 | 79 | const ErrorIcon = () => ( 80 | 87 | 92 | 93 | ); 94 | 95 | export const CloseIcon = () => ( 96 | 107 | 108 | 109 | 110 | ); 111 | -------------------------------------------------------------------------------- /packages/somoto/src/components/toast.tsx: -------------------------------------------------------------------------------- 1 | import type { JSXElement } from 'solid-js'; 2 | import { 3 | createEffect, 4 | createMemo, 5 | createSignal, 6 | Match, 7 | mergeProps, 8 | onCleanup, 9 | Show, 10 | Switch, 11 | } from 'solid-js'; 12 | import { createTimer } from 'src/primitives/create-timer'; 13 | 14 | import { GAP, SWIPE_THRESHOLD, TIME_BEFORE_UNMOUNT, TOAST_LIFETIME } from '../constants'; 15 | import { useIsDocumentHidden } from '../hooks/use-is-document-hidden'; 16 | import { useIsMounted } from '../hooks/use-is-mounted'; 17 | import { toast } from '../state'; 18 | import type { Action, ToastClassnames, ToastIcons, ToastVariants } from '../types'; 19 | import { isAction, type ToastProps } from '../types'; 20 | import { cn } from '../utils/cn'; 21 | import { CloseIcon, getIcon, Loading } from './icons'; 22 | 23 | export const Toast = (p: ToastProps) => { 24 | const props = mergeProps( 25 | { 26 | gap: GAP, 27 | closeButtonAriaLabel: 'Close toast', 28 | descriptionClassName: '', 29 | }, 30 | p, 31 | ); 32 | 33 | const mounted = useIsMounted(); 34 | const [removed, setRemoved] = createSignal(false); 35 | const [swiping, setSwiping] = createSignal(false); 36 | const [swipeOut, setSwipeOut] = createSignal(false); 37 | const [offsetBeforeRemove, setOffsetBeforeRemove] = createSignal(0); 38 | const [initialHeight, setInitialHeight] = createSignal(0); 39 | const [toastElement, setToastElement] = createSignal(); 40 | 41 | const isFront = () => props.index === 0; 42 | const invert = () => props.toast.invert || props.invert; 43 | const isVisible = () => props.index + 1 <= props.visibleAmount; 44 | const toastType = () => props.toast.type; 45 | const disabled = () => toastType() === 'loading'; 46 | const dismissible = () => props.toast.dismissible !== false; 47 | const closeButton = () => props.toast.closeButton ?? props.closeButton; 48 | const duration = () => props.toast.duration || props.duration || TOAST_LIFETIME; 49 | 50 | let dragStartTime: Date | null = null; 51 | let pointerStartRef: { x: number; y: number } | null = null; 52 | 53 | const position = createMemo(() => props.position.split('-')); 54 | const isDocumentHidden = useIsDocumentHidden(); 55 | 56 | // Height index is used to calculate the offset as it gets updated before the toast array, which means we can calculate the new layout faster. 57 | const heightIndex = createMemo(() => { 58 | const index = props.heights.findIndex(height => height.toastId === props.toast.id); 59 | return index === -1 ? 0 : index; 60 | }); 61 | 62 | const toastsHeightBefore = createMemo(() => { 63 | return props.heights.reduce((prev, curr, reducerIndex) => { 64 | // Calculate offset up until current toast 65 | if (reducerIndex >= heightIndex()) { 66 | return prev; 67 | } 68 | return prev + curr.height; 69 | }, 0); 70 | }); 71 | 72 | const offset = createMemo(() => { 73 | // console.log( 74 | // '@id', 75 | // props.toast.id, 76 | // '@index', 77 | // props.index, 78 | // '@height-index', 79 | // heightIndex(), 80 | // '@gap', 81 | // props.gap, 82 | // '@height-before', 83 | // toastsHeightBefore(), 84 | // '@offset', 85 | // heightIndex() * props.gap + toastsHeightBefore(), 86 | // '@heights', 87 | // props.heights, 88 | // ); 89 | return heightIndex() * props.gap + toastsHeightBefore(); 90 | }); 91 | 92 | /* calc heights */ 93 | createEffect(() => { 94 | const toastNode = toastElement(); 95 | if (!mounted() || !toastNode) return; 96 | const originalHeight = toastNode.style.height; 97 | toastNode.style.height = 'auto'; 98 | const newHeight = toastNode.getBoundingClientRect().height; 99 | toastNode.style.height = originalHeight; 100 | 101 | setInitialHeight(newHeight); 102 | 103 | // Add toast height to heights array after the toast is mounted 104 | props.setHeights(heights => { 105 | const alreadyExists = heights.find(height => height.toastId === props.toast.id); 106 | if (alreadyExists) { 107 | return heights.map(height => 108 | height.toastId === props.toast.id ? { ...height, height: newHeight } : height, 109 | ); 110 | } else { 111 | return [ 112 | { toastId: props.toast.id, height: newHeight, position: props.toast.position! }, 113 | ...heights, 114 | ]; 115 | } 116 | }); 117 | 118 | onCleanup(() => { 119 | props.setHeights(heights => heights.filter(height => height.toastId !== props.toast.id)); 120 | }); 121 | }); 122 | 123 | const deleteToast = () => { 124 | // Save the offset for the exit swipe animation 125 | setRemoved(true); 126 | setOffsetBeforeRemove(offset()); 127 | props.setHeights(h => h.filter(height => height.toastId !== props.toast.id)); 128 | 129 | setTimeout(() => { 130 | props.removeToast(props.toast); 131 | }, TIME_BEFORE_UNMOUNT); 132 | }; 133 | 134 | /* auto close */ 135 | createEffect(() => { 136 | if (duration() === Infinity || toastType() === 'loading') return; 137 | const { startTimer, pauseTimer } = createTimer(duration(), () => { 138 | props.toast.onAutoClose?.(props.toast); 139 | deleteToast(); 140 | }); 141 | 142 | if ( 143 | props.expanded || 144 | props.interacting || 145 | (props.pauseWhenPageIsHidden && isDocumentHidden()) 146 | ) { 147 | pauseTimer(); 148 | } else { 149 | startTimer(); 150 | } 151 | }); 152 | 153 | /* delete */ 154 | createEffect(() => { 155 | if (props.toast.delete) { 156 | deleteToast(); 157 | } 158 | }); 159 | 160 | function getLoadingIcon() { 161 | return ( 162 | }> 163 | {loading => ( 164 |
165 | {loading()} 166 |
167 | )} 168 |
169 | ); 170 | } 171 | 172 | const renderCloseButton = (): JSXElement => { 173 | return ( 174 | 175 | 193 | 194 | ); 195 | }; 196 | 197 | const renderIcon = (): JSXElement => { 198 | return ( 199 | 200 |
201 | {/* loading & promise */} 202 | 203 | 204 | {props.toast.icon} 205 | 206 | 207 | 208 | 209 | {_ => { 210 | return ( 211 | 212 | {props.toast.icon} 213 | 214 | {icon => icon()} 215 | 216 | {icon => icon()} 217 | 218 | ); 219 | }} 220 | 221 |
222 |
223 | ); 224 | }; 225 | 226 | /* content:title + description */ 227 | const renderContent = (): JSXElement => { 228 | return ( 229 |
230 |
231 | {props.toast.title} 232 |
233 | 234 | {description => ( 235 |
244 | {description()} 245 |
246 | )} 247 |
248 |
249 | ); 250 | }; 251 | 252 | /* cancel */ 253 | const renderCancel = (): JSXElement => { 254 | return ( 255 | {cancel => <>{cancel()}}} 258 | > 259 | 274 | 275 | ); 276 | }; 277 | 278 | /* action */ 279 | const renderAction = (): JSXElement => { 280 | return ( 281 | {action => <>{action()}}} 284 | > 285 | 300 | 301 | ); 302 | }; 303 | 304 | return ( 305 |
  • { 347 | if (disabled() || !dismissible()) return; 348 | dragStartTime = new Date(); 349 | setOffsetBeforeRemove(offset()); 350 | // Ensure we maintain correct pointer capture even when going outside of the toast (e.g. when swiping) 351 | (event.target as HTMLElement).setPointerCapture(event.pointerId); 352 | if ((event.target as HTMLElement).tagName === 'BUTTON') return; 353 | setSwiping(true); 354 | pointerStartRef = { x: event.clientX, y: event.clientY }; 355 | }} 356 | onPointerUp={() => { 357 | if (swipeOut() || !dismissible()) return; 358 | pointerStartRef = null; 359 | const toastNode = toastElement(); 360 | const swipeAmount = Number( 361 | toastNode?.style.getPropertyValue('--swipe-amount').replace('px', '') || 0, 362 | ); 363 | const timeTaken = new Date().getTime() - (dragStartTime as Date)?.getTime(); 364 | const velocity = Math.abs(swipeAmount) / timeTaken; 365 | 366 | // Remove only if threshold is met 367 | if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) { 368 | setOffsetBeforeRemove(offset()); 369 | props.toast.onDismiss?.(props.toast); 370 | deleteToast(); 371 | setSwipeOut(true); 372 | return; 373 | } 374 | 375 | toastNode?.style.setProperty('--swipe-amount', '0px'); 376 | setSwiping(false); 377 | }} 378 | onPointerMove={event => { 379 | if (!pointerStartRef || !dismissible()) return; 380 | 381 | const yPosition = event.clientY - pointerStartRef.y; 382 | const xPosition = event.clientX - pointerStartRef.x; 383 | 384 | const clamp = position()[0] === 'top' ? Math.min : Math.max; 385 | const clampedY = clamp(0, yPosition); 386 | const swipeStartThreshold = event.pointerType === 'touch' ? 10 : 2; 387 | const isAllowedToSwipe = Math.abs(clampedY) > swipeStartThreshold; 388 | 389 | if (isAllowedToSwipe) { 390 | const toastNode = toastElement(); 391 | toastNode?.style.setProperty('--swipe-amount', `${yPosition}px`); 392 | } else if (Math.abs(xPosition) > swipeStartThreshold) { 393 | // User is swiping in wrong direction so we disable swipe gesture 394 | // for the current pointer down interaction 395 | pointerStartRef = null; 396 | } 397 | }} 398 | > 399 | {renderCloseButton()} 400 | 404 | {renderIcon()} 405 | {renderContent()} 406 | {renderCancel()} 407 | {renderAction()} 408 | 409 | } 410 | > 411 | {props.toast.jsx || props.toast.title} 412 | 413 |
  • 414 | ); 415 | }; 416 | -------------------------------------------------------------------------------- /packages/somoto/src/components/toaster.tsx: -------------------------------------------------------------------------------- 1 | import '../styles.css'; 2 | 3 | import type { Component } from 'solid-js'; 4 | import { 5 | createEffect, 6 | createMemo, 7 | createSignal, 8 | For, 9 | mergeProps, 10 | onCleanup, 11 | onMount, 12 | Show, 13 | } from 'solid-js'; 14 | 15 | import { GAP, TOAST_WIDTH, VIEWPORT_OFFSET, VISIBLE_TOASTS_AMOUNT } from '../constants'; 16 | import { ToastState } from '../state'; 17 | import type { 18 | HeightT, 19 | Position, 20 | ToasterProps, 21 | ToastOptions, 22 | ToastToDismiss, 23 | ToastType, 24 | } from '../types'; 25 | import { getDocumentDirection } from '../utils/get-document-direction'; 26 | import { Toast } from './toast'; 27 | 28 | export const Toaster: Component = p => { 29 | const props = mergeProps( 30 | { 31 | invert: false, 32 | expand: false, 33 | pauseWhenPageIsHidden: false, 34 | closeButton: false, 35 | gap: GAP, 36 | position: 'bottom-right', 37 | hotkey: ['altKey', 'KeyT'], 38 | theme: 'light', 39 | offset: VIEWPORT_OFFSET, 40 | toastOptions: {} as ToastOptions, 41 | visibleAmount: VISIBLE_TOASTS_AMOUNT, 42 | dir: getDocumentDirection(), 43 | containerAriaLabel: 'Notifications', 44 | } satisfies Partial, 45 | p, 46 | ); 47 | const [toasts, setToasts] = createSignal([]); 48 | 49 | const possiblePositions = createMemo(() => { 50 | return Array.from( 51 | new Set( 52 | [props.position].concat( 53 | toasts() 54 | .filter(toast => toast.position) 55 | .map(toast => toast.position) as Position[], 56 | ), 57 | ), 58 | ); 59 | }); 60 | 61 | const [heights, setHeights] = createSignal([]); 62 | const [expanded, setExpanded] = createSignal(false); 63 | const [interacting, setInteracting] = createSignal(false); 64 | const [actualTheme, setActualTheme] = createSignal( 65 | props.theme !== 'system' 66 | ? props.theme 67 | : typeof window !== 'undefined' 68 | ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches 69 | ? 'dark' 70 | : 'light' 71 | : 'light', 72 | ); 73 | 74 | const [listRef, setListRef] = createSignal(); 75 | const hotkeyLabel = () => props.hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, ''); 76 | let lastFocusedElementRef: HTMLElement | null = null; 77 | let isFocusWithinRef = false; 78 | 79 | const removeToast = (toastToRemove: ToastType) => { 80 | setToasts(toasts => { 81 | if (!toasts.find(toast => toast.id === toastToRemove.id)?.delete) { 82 | ToastState.dismiss(toastToRemove.id); 83 | } 84 | 85 | return toasts.filter(({ id }) => id !== toastToRemove.id); 86 | }); 87 | }; 88 | 89 | onMount(() => { 90 | const unSubscribe = ToastState.subscribe(toast => { 91 | if ((toast as ToastToDismiss).dismiss) { 92 | setToasts(toasts => toasts.map(t => (t.id === toast.id ? { ...t, delete: true } : t))); 93 | return; 94 | } 95 | 96 | setToasts(toasts => { 97 | const indexOfExistingToast = toasts.findIndex(t => t.id === toast.id); 98 | 99 | // Update the toast if it already exists 100 | if (indexOfExistingToast !== -1) { 101 | return [ 102 | ...toasts.slice(0, indexOfExistingToast), 103 | { ...toasts[indexOfExistingToast], ...toast }, 104 | ...toasts.slice(indexOfExistingToast + 1), 105 | ]; 106 | } 107 | 108 | return [toast, ...toasts]; 109 | }); 110 | }); 111 | onCleanup(unSubscribe); 112 | }); 113 | 114 | /* sync actualTheme with theme */ 115 | createEffect(() => { 116 | if (props.theme !== 'system') { 117 | setActualTheme(props.theme); 118 | return; 119 | } 120 | 121 | if (props.theme === 'system') { 122 | // check if current preference is dark 123 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 124 | setActualTheme('dark'); 125 | } else { 126 | setActualTheme('light'); 127 | } 128 | } 129 | 130 | if (typeof window === 'undefined') return; 131 | 132 | const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 133 | 134 | try { 135 | // Chrome & Firefox 136 | darkMediaQuery.addEventListener('change', ({ matches }) => { 137 | if (matches) { 138 | setActualTheme('dark'); 139 | } else { 140 | setActualTheme('light'); 141 | } 142 | }); 143 | } catch { 144 | // Safari < 14 145 | darkMediaQuery.addListener(({ matches }) => { 146 | try { 147 | if (matches) { 148 | setActualTheme('dark'); 149 | } else { 150 | setActualTheme('light'); 151 | } 152 | } catch (e) { 153 | console.error(e); 154 | } 155 | }); 156 | } 157 | }); 158 | 159 | // Ensure expanded is always false when no toasts are present / only one left 160 | createEffect(() => { 161 | if (toasts().length <= 1) { 162 | setExpanded(false); 163 | } 164 | }); 165 | 166 | createEffect(() => { 167 | const handleKeyDown = (event: KeyboardEvent) => { 168 | const isHotkeyPressed = props.hotkey.every(key => (event as any)[key] || event.code === key); 169 | 170 | if (isHotkeyPressed) { 171 | setExpanded(true); 172 | listRef()?.focus(); 173 | } 174 | 175 | if ( 176 | event.code === 'Escape' && 177 | (document.activeElement === listRef() || listRef()?.contains(document.activeElement)) 178 | ) { 179 | setExpanded(false); 180 | } 181 | }; 182 | document.addEventListener('keydown', handleKeyDown); 183 | onCleanup(() => { 184 | document.removeEventListener('keydown', handleKeyDown); 185 | }); 186 | }); 187 | 188 | createEffect(() => { 189 | if (listRef()) { 190 | onCleanup(() => { 191 | if (lastFocusedElementRef) { 192 | lastFocusedElementRef.focus({ preventScroll: true }); 193 | lastFocusedElementRef = null; 194 | isFocusWithinRef = false; 195 | } 196 | }); 197 | } 198 | }); 199 | 200 | return ( 201 | 202 | {/* Remove item from normal navigation flow, only available via hotkey */} 203 |
    { 207 | props.ref = r; 208 | }} 209 | > 210 | 211 | {(position, index) => { 212 | const [y, x] = position.split('-'); 213 | return ( 214 |
      { 231 | if ( 232 | isFocusWithinRef && 233 | !event.currentTarget.contains(event.relatedTarget as Node) 234 | ) { 235 | isFocusWithinRef = false; 236 | if (lastFocusedElementRef) { 237 | lastFocusedElementRef.focus({ preventScroll: true }); 238 | lastFocusedElementRef = null; 239 | } 240 | } 241 | }} 242 | onFocus={event => { 243 | const isNotDismissible = 244 | event.target instanceof HTMLElement && 245 | event.target.dataset.dismissible === 'false'; 246 | 247 | if (isNotDismissible) return; 248 | 249 | if (!isFocusWithinRef) { 250 | isFocusWithinRef = true; 251 | lastFocusedElementRef = event.relatedTarget as HTMLElement; 252 | } 253 | }} 254 | onMouseEnter={() => setExpanded(true)} 255 | onMouseMove={() => setExpanded(true)} 256 | onMouseLeave={() => { 257 | // Avoid setting expanded to false when interacting with a toast, e.g. swiping 258 | if (!interacting()) { 259 | setExpanded(false); 260 | } 261 | }} 262 | onPointerDown={event => { 263 | const isNotDismissible = 264 | event.target instanceof HTMLElement && 265 | event.target.dataset.dismissible === 'false'; 266 | 267 | if (isNotDismissible) return; 268 | setInteracting(true); 269 | }} 270 | onPointerUp={() => setInteracting(false)} 271 | > 272 | 275 | (!toast.position && 276 | index() === 0) /* case that don't have position but as first */ || 277 | toast.position === position, 278 | )} 279 | > 280 | {(toast, index) => ( 281 | t.position == toast.position)} 302 | heights={heights().filter(h => h.position == toast.position)} 303 | setHeights={setHeights} 304 | expandByDefault={props.expand} 305 | expanded={expanded()} 306 | pauseWhenPageIsHidden={props.pauseWhenPageIsHidden} 307 | /> 308 | )} 309 | 310 |
    311 | ); 312 | }} 313 |
    314 |
    315 |
    316 | ); 317 | }; 318 | -------------------------------------------------------------------------------- /packages/somoto/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | // Visible toasts amount 2 | export const VISIBLE_TOASTS_AMOUNT = 3; 3 | 4 | // Viewport padding 5 | export const VIEWPORT_OFFSET = '32px'; 6 | 7 | // Default lifetime of a toasts (in ms) 8 | export const TOAST_LIFETIME = 4000; 9 | 10 | // Default toast width 11 | export const TOAST_WIDTH = 356; 12 | 13 | // Default gap between toasts 14 | export const GAP = 14; 15 | 16 | // Threshold to dismiss a toast 17 | export const SWIPE_THRESHOLD = 20; 18 | 19 | // Equal to exit animation duration 20 | export const TIME_BEFORE_UNMOUNT = 200; 21 | -------------------------------------------------------------------------------- /packages/somoto/src/hooks/use-is-document-hidden.ts: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup, onMount } from 'solid-js'; 2 | 3 | export const useIsDocumentHidden = () => { 4 | const [isDocumentHidden, setIsDocumentHidden] = createSignal(document?.hidden); 5 | 6 | onMount(() => { 7 | const callback = () => { 8 | setIsDocumentHidden(document.hidden); 9 | }; 10 | document.addEventListener('visibilitychange', callback); 11 | onCleanup(() => { 12 | document.removeEventListener('visibilitychange', callback); 13 | }); 14 | }); 15 | 16 | return isDocumentHidden; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/somoto/src/hooks/use-is-mounted.ts: -------------------------------------------------------------------------------- 1 | import { createSignal, onMount } from 'solid-js'; 2 | 3 | export const useIsMounted = () => { 4 | const [isMounted, setIsMounted] = createSignal(false); 5 | onMount(() => { 6 | setIsMounted(true); 7 | }); 8 | return isMounted; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/somoto/src/hooks/use-somoto.ts: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup, onMount } from 'solid-js'; 2 | 3 | import { ToastState } from '../state'; 4 | import { ToastType } from '../types'; 5 | 6 | export const useSomoto = () => { 7 | const [activeToasts, setActiveToasts] = createSignal([]); 8 | 9 | onMount(() => { 10 | const unSubscribe = ToastState.subscribe(toast => { 11 | setActiveToasts(currentToasts => { 12 | if ('dismiss' in toast && toast.dismiss) { 13 | return currentToasts.filter(t => t.id !== toast.id); 14 | } 15 | 16 | const existingToastIndex = currentToasts.findIndex(t => t.id === toast.id); 17 | if (existingToastIndex !== -1) { 18 | const updatedToasts = [...currentToasts]; 19 | updatedToasts[existingToastIndex] = { ...updatedToasts[existingToastIndex], ...toast }; 20 | return updatedToasts; 21 | } else { 22 | return [toast, ...currentToasts]; 23 | } 24 | }); 25 | }); 26 | 27 | onCleanup(unSubscribe); 28 | }); 29 | 30 | return { 31 | toasts: activeToasts, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/somoto/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Toaster } from './components/toaster'; 2 | import { toast } from './state'; 3 | import type { ExternalToast, ToasterProps, ToastType } from './types'; 4 | 5 | export { toast, Toaster }; 6 | export { useSomoto } from './hooks/use-somoto'; 7 | export type { ExternalToast, ToasterProps, ToastType }; 8 | export type { Action, ToastClassnames, ToastToDismiss } from './types'; 9 | -------------------------------------------------------------------------------- /packages/somoto/src/primitives/create-timer.ts: -------------------------------------------------------------------------------- 1 | import { onCleanup } from 'solid-js'; 2 | 3 | export const createTimer = (ms: number, onTimeout: () => void) => { 4 | let closeTimerStartTime = 0; 5 | let lastCloseTimerStartTime = 0; 6 | let timeoutId: ReturnType; 7 | let remainingTime = ms; 8 | 9 | // Pause the timer on each hover 10 | const pauseTimer = () => { 11 | if (lastCloseTimerStartTime < closeTimerStartTime) { 12 | // Get the elapsed time since the timer started 13 | const elapsedTime = Date.now() - closeTimerStartTime; 14 | remainingTime = remainingTime - elapsedTime; 15 | } 16 | 17 | lastCloseTimerStartTime = Date.now(); 18 | }; 19 | 20 | const startTimer = () => { 21 | // setTimeout(callback, Infinity) behaves as if the delay is 0. 22 | // As a result, the toast would be closed immediately, giving the appearance that it was never rendered. 23 | if (remainingTime === Infinity) return; 24 | closeTimerStartTime = Date.now(); 25 | // Let the toast know it has started 26 | timeoutId = setTimeout(onTimeout, remainingTime); 27 | }; 28 | 29 | onCleanup(() => clearTimeout(timeoutId)); 30 | return { 31 | startTimer, 32 | pauseTimer, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/somoto/src/state.ts: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js'; 2 | 3 | import type { 4 | ExternalToast, 5 | PromiseData, 6 | PromiseT, 7 | ToastToDismiss, 8 | ToastType, 9 | ToastVariants, 10 | } from './types'; 11 | import { isHttpResponse } from './utils/helper'; 12 | 13 | let toastsCounter = 1; 14 | 15 | class Observer { 16 | subscribers: Array<(toast: ExternalToast | ToastToDismiss) => void>; 17 | toasts: Array; 18 | 19 | constructor() { 20 | this.subscribers = []; 21 | this.toasts = []; 22 | } 23 | 24 | // We use arrow functions to maintain the correct `this` reference 25 | subscribe = (subscriber: (toast: ToastType | ToastToDismiss) => void) => { 26 | this.subscribers.push(subscriber); 27 | 28 | return () => { 29 | const index = this.subscribers.indexOf(subscriber); 30 | this.subscribers.splice(index, 1); 31 | }; 32 | }; 33 | 34 | publish = (data: ToastType) => { 35 | this.subscribers.forEach(subscriber => subscriber(data)); 36 | }; 37 | 38 | addToast = (data: ToastType) => { 39 | this.publish(data); 40 | this.toasts = [...this.toasts, data]; 41 | }; 42 | 43 | create = ( 44 | data: ExternalToast & { 45 | message?: JSX.Element; 46 | type?: ToastVariants; 47 | promise?: PromiseT; 48 | jsx?: JSX.Element; 49 | }, 50 | ) => { 51 | const { message, ...rest } = data; 52 | const id = 53 | typeof data?.id === 'number' || (data.id?.length || 0) > 0 54 | ? (data.id as string | number) 55 | : toastsCounter++; 56 | const alreadyExists = this.toasts.find(toast => { 57 | return toast.id === id; 58 | }); 59 | const dismissible = data.dismissible ?? true; 60 | 61 | if (alreadyExists) { 62 | this.toasts = this.toasts.map(toast => { 63 | if (toast.id === id) { 64 | this.publish({ ...toast, ...data, id, title: message }); 65 | return { 66 | ...toast, 67 | ...data, 68 | id, 69 | dismissible, 70 | title: message, 71 | }; 72 | } 73 | 74 | return toast; 75 | }); 76 | } else { 77 | this.addToast({ title: message, ...rest, dismissible, id }); 78 | } 79 | 80 | return id; 81 | }; 82 | 83 | dismiss = (id?: number | string) => { 84 | if (!id) { 85 | this.toasts.forEach(toast => { 86 | this.subscribers.forEach(subscriber => subscriber({ id: toast.id, dismiss: true })); 87 | }); 88 | } 89 | 90 | this.subscribers.forEach(subscriber => subscriber({ id, dismiss: true })); 91 | return id; 92 | }; 93 | 94 | message = (message: JSX.Element, data?: ExternalToast) => { 95 | return this.create({ ...data, message }); 96 | }; 97 | 98 | error = (message: JSX.Element, data?: ExternalToast) => { 99 | return this.create({ ...data, message, type: 'error' }); 100 | }; 101 | 102 | success = (message: JSX.Element, data?: ExternalToast) => { 103 | return this.create({ ...data, type: 'success', message }); 104 | }; 105 | 106 | info = (message: JSX.Element, data?: ExternalToast) => { 107 | return this.create({ ...data, type: 'info', message }); 108 | }; 109 | 110 | warning = (message: JSX.Element, data?: ExternalToast) => { 111 | return this.create({ ...data, type: 'warning', message }); 112 | }; 113 | 114 | loading = (message: JSX.Element, data?: ExternalToast) => { 115 | return this.create({ ...data, type: 'loading', message }); 116 | }; 117 | 118 | promise = (promise: PromiseT, data?: PromiseData) => { 119 | if (!data) { 120 | // Nothing to show 121 | return; 122 | } 123 | 124 | let id: string | number | undefined = undefined; 125 | if (data.loading !== undefined) { 126 | id = this.create({ 127 | ...data, 128 | promise, 129 | type: 'loading', 130 | message: data.loading, 131 | description: typeof data.description !== 'function' ? data.description : undefined, 132 | }); 133 | } 134 | 135 | const p = promise instanceof Promise ? promise : promise(); 136 | 137 | let shouldDismiss = id !== undefined; 138 | let result: ['resolve', ToastData] | ['reject', unknown]; 139 | 140 | const originalPromise = p 141 | .then(async response => { 142 | result = ['resolve', response]; 143 | if (isHttpResponse(response) && !response.ok) { 144 | shouldDismiss = false; 145 | const message = 146 | typeof data.error === 'function' 147 | ? await data.error(`HTTP error! status: ${response.status}`) 148 | : data.error; 149 | const description = 150 | typeof data.description === 'function' 151 | ? await data.description(`HTTP error! status: ${response.status}`) 152 | : data.description; 153 | this.create({ id, type: 'error', message, description }); 154 | } else if (data.success !== undefined) { 155 | shouldDismiss = false; 156 | const message = 157 | typeof data.success === 'function' ? await data.success(response) : data.success; 158 | const description = 159 | typeof data.description === 'function' 160 | ? await data.description(response) 161 | : data.description; 162 | this.create({ id, type: 'success', message, description }); 163 | } 164 | }) 165 | .catch(async error => { 166 | result = ['reject', error]; 167 | if (data.error !== undefined) { 168 | shouldDismiss = false; 169 | const message = typeof data.error === 'function' ? await data.error(error) : data.error; 170 | const description = 171 | typeof data.description === 'function' 172 | ? await data.description(error) 173 | : data.description; 174 | this.create({ id, type: 'error', message, description }); 175 | } 176 | }) 177 | .finally(() => { 178 | if (shouldDismiss) { 179 | // Toast is still in load state (and will be indefinitely — dismiss it) 180 | this.dismiss(id); 181 | id = undefined; 182 | } 183 | 184 | data.finally?.(); 185 | }); 186 | 187 | const unwrap = () => 188 | new Promise((resolve, reject) => 189 | originalPromise 190 | .then(() => (result[0] === 'reject' ? reject(result[1]) : resolve(result[1]))) 191 | .catch(reject), 192 | ); 193 | 194 | if (typeof id !== 'string' && typeof id !== 'number') { 195 | // cannot Object.assign on undefined 196 | return { unwrap }; 197 | } else { 198 | return Object.assign(id, { unwrap }); 199 | } 200 | }; 201 | 202 | custom = (jsx: (id: number | string) => JSX.Element, data?: ExternalToast) => { 203 | const id = data?.id || toastsCounter++; 204 | this.create({ jsx: jsx(id), id, ...data }); 205 | return id; 206 | }; 207 | } 208 | 209 | export const ToastState = new Observer(); 210 | 211 | const basicToast = (message: JSX.Element, data?: ExternalToast) => { 212 | const id = data?.id || toastsCounter++; 213 | ToastState.addToast({ 214 | title: message, 215 | ...data, 216 | id, 217 | }); 218 | return id; 219 | }; 220 | 221 | const getHistory = () => ToastState.toasts; 222 | 223 | // use `Object.assign` to maintain the correct types as we would lose them otherwise 224 | export const toast = Object.assign( 225 | basicToast, 226 | { 227 | success: ToastState.success, 228 | info: ToastState.info, 229 | warning: ToastState.warning, 230 | error: ToastState.error, 231 | custom: ToastState.custom, 232 | message: ToastState.message, 233 | promise: ToastState.promise, 234 | dismiss: ToastState.dismiss, 235 | loading: ToastState.loading, 236 | }, 237 | { getHistory }, 238 | ); 239 | -------------------------------------------------------------------------------- /packages/somoto/src/styles.css: -------------------------------------------------------------------------------- 1 | :where(html[dir='ltr']), 2 | :where([data-somoto-toaster][dir='ltr']) { 3 | --toast-icon-margin-start: -3px; 4 | --toast-icon-margin-end: 4px; 5 | --toast-svg-margin-start: -1px; 6 | --toast-svg-margin-end: 0px; 7 | --toast-button-margin-start: auto; 8 | --toast-button-margin-end: 0; 9 | --toast-close-button-start: 0; 10 | --toast-close-button-end: unset; 11 | --toast-close-button-transform: translate(-35%, -35%); 12 | } 13 | 14 | :where(html[dir='rtl']), 15 | :where([data-somoto-toaster][dir='rtl']) { 16 | --toast-icon-margin-start: 4px; 17 | --toast-icon-margin-end: -3px; 18 | --toast-svg-margin-start: 0px; 19 | --toast-svg-margin-end: -1px; 20 | --toast-button-margin-start: 0; 21 | --toast-button-margin-end: auto; 22 | --toast-close-button-start: unset; 23 | --toast-close-button-end: 0; 24 | --toast-close-button-transform: translate(35%, -35%); 25 | } 26 | 27 | :where([data-somoto-toaster]) { 28 | --gray1: hsl(0, 0%, 99%); 29 | --gray2: hsl(0, 0%, 97.3%); 30 | --gray3: hsl(0, 0%, 95.1%); 31 | --gray4: hsl(0, 0%, 93%); 32 | --gray5: hsl(0, 0%, 90.9%); 33 | --gray6: hsl(0, 0%, 88.7%); 34 | --gray7: hsl(0, 0%, 85.8%); 35 | --gray8: hsl(0, 0%, 78%); 36 | --gray9: hsl(0, 0%, 56.1%); 37 | --gray10: hsl(0, 0%, 52.3%); 38 | --gray11: hsl(0, 0%, 43.5%); 39 | --gray12: hsl(0, 0%, 9%); 40 | --border-radius: 8px; 41 | box-sizing: border-box; 42 | position: fixed; 43 | margin: 0; 44 | padding: 0; 45 | width: var(--width); 46 | list-style: none; 47 | outline: none; 48 | z-index: 999999999; 49 | } 50 | 51 | :where([data-somoto-toaster][data-x-position='right']) { 52 | right: max(var(--offset), env(safe-area-inset-right)); 53 | } 54 | 55 | :where([data-somoto-toaster][data-x-position='left']) { 56 | left: max(var(--offset), env(safe-area-inset-left)); 57 | } 58 | 59 | :where([data-somoto-toaster][data-x-position='center']) { 60 | left: 50%; 61 | transform: translateX(-50%); 62 | } 63 | 64 | :where([data-somoto-toaster][data-y-position='top']) { 65 | top: max(var(--offset), env(safe-area-inset-top)); 66 | } 67 | 68 | :where([data-somoto-toaster][data-y-position='bottom']) { 69 | bottom: max(var(--offset), env(safe-area-inset-bottom)); 70 | } 71 | 72 | :where([data-somoto-toast]) { 73 | --y: translateY(100%); 74 | --lift-amount: calc(var(--lift) * var(--gap)); 75 | z-index: var(--z-index); 76 | position: absolute; 77 | opacity: 0; 78 | transform: var(--y); 79 | filter: blur(0); 80 | /* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */ 81 | touch-action: none; 82 | transition: 83 | transform 400ms, 84 | opacity 400ms, 85 | height 400ms, 86 | box-shadow 200ms; 87 | box-sizing: border-box; 88 | outline: none; 89 | overflow-wrap: anywhere; 90 | } 91 | 92 | :where([data-somoto-toast][data-styled='true']) { 93 | padding: 16px; 94 | background: var(--normal-bg); 95 | border: 1px solid var(--normal-border); 96 | color: var(--normal-text); 97 | border-radius: var(--border-radius); 98 | box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); 99 | width: var(--width); 100 | font-size: 13px; 101 | display: flex; 102 | align-items: center; 103 | gap: 6px; 104 | } 105 | 106 | :where([data-somoto-toast]:focus-visible) { 107 | box-shadow: 108 | 0px 4px 12px rgba(0, 0, 0, 0.1), 109 | 0 0 0 2px rgba(0, 0, 0, 0.2); 110 | } 111 | 112 | :where([data-somoto-toast][data-y-position='top']) { 113 | top: 0; 114 | --y: translateY(-100%); 115 | --lift: 1; 116 | --lift-amount: calc(1 * var(--gap)); 117 | } 118 | 119 | :where([data-somoto-toast][data-y-position='bottom']) { 120 | bottom: 0; 121 | --y: translateY(100%); 122 | --lift: -1; 123 | --lift-amount: calc(var(--lift) * var(--gap)); 124 | } 125 | 126 | :where([data-somoto-toast]) :where([data-description]) { 127 | font-weight: 400; 128 | line-height: 1.4; 129 | color: inherit; 130 | } 131 | 132 | :where([data-somoto-toast]) :where([data-title]) { 133 | font-weight: 500; 134 | line-height: 1.5; 135 | color: inherit; 136 | } 137 | 138 | :where([data-somoto-toast]) :where([data-icon]) { 139 | display: flex; 140 | height: 16px; 141 | width: 16px; 142 | position: relative; 143 | justify-content: flex-start; 144 | align-items: center; 145 | flex-shrink: 0; 146 | margin-left: var(--toast-icon-margin-start); 147 | margin-right: var(--toast-icon-margin-end); 148 | } 149 | 150 | :where([data-somoto-toast][data-promise='true']) :where([data-icon]) > svg { 151 | opacity: 0; 152 | transform: scale(0.8); 153 | transform-origin: center; 154 | animation: somoto-fade-in 300ms ease forwards; 155 | } 156 | 157 | :where([data-somoto-toast]) :where([data-icon]) > * { 158 | flex-shrink: 0; 159 | } 160 | 161 | :where([data-somoto-toast]) :where([data-icon]) svg { 162 | margin-left: var(--toast-svg-margin-start); 163 | margin-right: var(--toast-svg-margin-end); 164 | } 165 | 166 | :where([data-somoto-toast]) :where([data-content]) { 167 | display: flex; 168 | flex-direction: column; 169 | gap: 2px; 170 | } 171 | 172 | [data-somoto-toast][data-styled='true'] [data-button] { 173 | border-radius: 4px; 174 | padding-left: 8px; 175 | padding-right: 8px; 176 | height: 24px; 177 | font-size: 12px; 178 | color: var(--normal-bg); 179 | background: var(--normal-text); 180 | margin-left: var(--toast-button-margin-start); 181 | margin-right: var(--toast-button-margin-end); 182 | border: none; 183 | cursor: pointer; 184 | outline: none; 185 | display: flex; 186 | align-items: center; 187 | flex-shrink: 0; 188 | transition: 189 | opacity 400ms, 190 | box-shadow 200ms; 191 | } 192 | 193 | :where([data-somoto-toast]) :where([data-button]):focus-visible { 194 | box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4); 195 | } 196 | 197 | :where([data-somoto-toast]) :where([data-button]):first-of-type { 198 | margin-left: var(--toast-button-margin-start); 199 | margin-right: var(--toast-button-margin-end); 200 | } 201 | 202 | :where([data-somoto-toast]) :where([data-cancel]) { 203 | color: var(--normal-text); 204 | background: rgba(0, 0, 0, 0.08); 205 | } 206 | 207 | :where([data-somoto-toast][data-theme='dark']) :where([data-cancel]) { 208 | background: rgba(255, 255, 255, 0.3); 209 | } 210 | 211 | :where([data-somoto-toast]) :where([data-close-button]) { 212 | position: absolute; 213 | left: var(--toast-close-button-start); 214 | right: var(--toast-close-button-end); 215 | top: 0; 216 | height: 20px; 217 | width: 20px; 218 | display: flex; 219 | justify-content: center; 220 | align-items: center; 221 | padding: 0; 222 | background: var(--gray1); 223 | color: var(--gray12); 224 | border: 1px solid var(--gray4); 225 | transform: var(--toast-close-button-transform); 226 | border-radius: 50%; 227 | cursor: pointer; 228 | z-index: 1; 229 | transition: 230 | opacity 100ms, 231 | background 200ms, 232 | border-color 200ms; 233 | } 234 | 235 | :where([data-somoto-toast]) :where([data-close-button]):focus-visible { 236 | box-shadow: 237 | 0px 4px 12px rgba(0, 0, 0, 0.1), 238 | 0 0 0 2px rgba(0, 0, 0, 0.2); 239 | } 240 | 241 | :where([data-somoto-toast]) :where([data-disabled='true']) { 242 | cursor: not-allowed; 243 | } 244 | 245 | :where([data-somoto-toast]):hover :where([data-close-button]):hover { 246 | background: var(--gray2); 247 | border-color: var(--gray5); 248 | } 249 | 250 | /* Leave a ghost div to avoid setting hover to false when swiping out */ 251 | :where([data-somoto-toast][data-swiping='true'])::before { 252 | content: ''; 253 | position: absolute; 254 | left: 0; 255 | right: 0; 256 | height: 100%; 257 | z-index: -1; 258 | } 259 | 260 | :where([data-somoto-toast][data-y-position='top'][data-swiping='true'])::before { 261 | /* y 50% needed to distribute height additional height evenly */ 262 | bottom: 50%; 263 | transform: scaleY(3) translateY(50%); 264 | } 265 | 266 | :where([data-somoto-toast][data-y-position='bottom'][data-swiping='true'])::before { 267 | /* y -50% needed to distribute height additional height evenly */ 268 | top: 50%; 269 | transform: scaleY(3) translateY(-50%); 270 | } 271 | 272 | /* Leave a ghost div to avoid setting hover to false when transitioning out */ 273 | :where([data-somoto-toast][data-swiping='false'][data-removed='true'])::before { 274 | content: ''; 275 | position: absolute; 276 | inset: 0; 277 | transform: scaleY(2); 278 | } 279 | 280 | /* Needed to avoid setting hover to false when inbetween toasts */ 281 | :where([data-somoto-toast])::after { 282 | content: ''; 283 | position: absolute; 284 | left: 0; 285 | height: calc(var(--gap) + 1px); 286 | bottom: 100%; 287 | width: 100%; 288 | } 289 | 290 | :where([data-somoto-toast][data-mounted='true']) { 291 | --y: translateY(0); 292 | opacity: 1; 293 | } 294 | 295 | :where([data-somoto-toast][data-expanded='false'][data-front='false']) { 296 | --scale: var(--toasts-before) * 0.05 + 1; 297 | --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale))); 298 | height: var(--front-toast-height); 299 | } 300 | 301 | :where([data-somoto-toast]) > * { 302 | transition: opacity 400ms; 303 | } 304 | 305 | :where([data-somoto-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * { 306 | opacity: 0; 307 | } 308 | 309 | :where([data-somoto-toast][data-visible='false']) { 310 | opacity: 0; 311 | pointer-events: none; 312 | } 313 | 314 | :where([data-somoto-toast][data-mounted='true'][data-expanded='true']) { 315 | --y: translateY(calc(var(--lift) * var(--offset))); 316 | height: var(--initial-height); 317 | } 318 | 319 | :where([data-somoto-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) { 320 | --y: translateY(calc(var(--lift) * -100%)); 321 | opacity: 0; 322 | } 323 | 324 | :where( 325 | [data-somoto-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true'] 326 | ) { 327 | --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%)); 328 | opacity: 0; 329 | } 330 | 331 | :where( 332 | [data-somoto-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false'] 333 | ) { 334 | --y: translateY(40%); 335 | opacity: 0; 336 | transition: 337 | transform 500ms, 338 | opacity 200ms; 339 | } 340 | 341 | /* Bump up the height to make sure hover state doesn't get set to false */ 342 | :where([data-somoto-toast][data-removed='true'][data-front='false'])::before { 343 | height: calc(var(--initial-height) + 20%); 344 | } 345 | 346 | [data-somoto-toast][data-swiping='true'] { 347 | transform: var(--y) translateY(var(--swipe-amount, 0px)); 348 | transition: none; 349 | } 350 | 351 | [data-somoto-toast][data-swipe-out='true'][data-y-position='bottom'], 352 | [data-somoto-toast][data-swipe-out='true'][data-y-position='top'] { 353 | animation: swipe-out 200ms ease-out forwards; 354 | } 355 | 356 | @keyframes swipe-out { 357 | from { 358 | transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount))); 359 | opacity: 1; 360 | } 361 | 362 | to { 363 | transform: translateY( 364 | calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%) 365 | ); 366 | opacity: 0; 367 | } 368 | } 369 | 370 | @media (max-width: 600px) { 371 | [data-somoto-toaster] { 372 | position: fixed; 373 | --mobile-offset: 16px; 374 | right: var(--mobile-offset); 375 | left: var(--mobile-offset); 376 | width: 100%; 377 | } 378 | 379 | [data-somoto-toaster][dir='rtl'] { 380 | left: calc(var(--mobile-offset) * -1); 381 | } 382 | 383 | [data-somoto-toaster] [data-somoto-toast] { 384 | left: 0; 385 | right: 0; 386 | width: calc(100% - var(--mobile-offset) * 2); 387 | } 388 | 389 | [data-somoto-toaster][data-x-position='left'] { 390 | left: var(--mobile-offset); 391 | } 392 | 393 | [data-somoto-toaster][data-y-position='bottom'] { 394 | bottom: 20px; 395 | } 396 | 397 | [data-somoto-toaster][data-y-position='top'] { 398 | top: 20px; 399 | } 400 | 401 | [data-somoto-toaster][data-x-position='center'] { 402 | left: var(--mobile-offset); 403 | right: var(--mobile-offset); 404 | transform: none; 405 | } 406 | } 407 | 408 | [data-somoto-toaster][data-theme='light'] { 409 | --normal-bg: #fff; 410 | --normal-border: var(--gray4); 411 | --normal-text: var(--gray12); 412 | 413 | --success-bg: hsl(143, 85%, 96%); 414 | --success-border: hsl(145, 92%, 91%); 415 | --success-text: hsl(140, 100%, 27%); 416 | 417 | --info-bg: hsl(208, 100%, 97%); 418 | --info-border: hsl(221, 91%, 91%); 419 | --info-text: hsl(210, 92%, 45%); 420 | 421 | --warning-bg: hsl(49, 100%, 97%); 422 | --warning-border: hsl(49, 91%, 91%); 423 | --warning-text: hsl(31, 92%, 45%); 424 | 425 | --error-bg: hsl(359, 100%, 97%); 426 | --error-border: hsl(359, 100%, 94%); 427 | --error-text: hsl(360, 100%, 45%); 428 | } 429 | 430 | [data-somoto-toaster][data-theme='light'] [data-somoto-toast][data-invert='true'] { 431 | --normal-bg: #000; 432 | --normal-border: hsl(0, 0%, 20%); 433 | --normal-text: var(--gray1); 434 | } 435 | 436 | [data-somoto-toaster][data-theme='dark'] [data-somoto-toast][data-invert='true'] { 437 | --normal-bg: #fff; 438 | --normal-border: var(--gray3); 439 | --normal-text: var(--gray12); 440 | } 441 | 442 | [data-somoto-toaster][data-theme='dark'] { 443 | --normal-bg: #000; 444 | --normal-border: hsl(0, 0%, 20%); 445 | --normal-text: var(--gray1); 446 | 447 | --success-bg: hsl(150, 100%, 6%); 448 | --success-border: hsl(147, 100%, 12%); 449 | --success-text: hsl(150, 86%, 65%); 450 | 451 | --info-bg: hsl(215, 100%, 6%); 452 | --info-border: hsl(223, 100%, 12%); 453 | --info-text: hsl(216, 87%, 65%); 454 | 455 | --warning-bg: hsl(64, 100%, 6%); 456 | --warning-border: hsl(60, 100%, 12%); 457 | --warning-text: hsl(46, 87%, 65%); 458 | 459 | --error-bg: hsl(358, 76%, 10%); 460 | --error-border: hsl(357, 89%, 16%); 461 | --error-text: hsl(358, 100%, 81%); 462 | } 463 | 464 | [data-rich-colors='true'][data-somoto-toast][data-type='success'] { 465 | background: var(--success-bg); 466 | border-color: var(--success-border); 467 | color: var(--success-text); 468 | } 469 | 470 | [data-rich-colors='true'][data-somoto-toast][data-type='success'] [data-close-button] { 471 | background: var(--success-bg); 472 | border-color: var(--success-border); 473 | color: var(--success-text); 474 | } 475 | 476 | [data-rich-colors='true'][data-somoto-toast][data-type='info'] { 477 | background: var(--info-bg); 478 | border-color: var(--info-border); 479 | color: var(--info-text); 480 | } 481 | 482 | [data-rich-colors='true'][data-somoto-toast][data-type='info'] [data-close-button] { 483 | background: var(--info-bg); 484 | border-color: var(--info-border); 485 | color: var(--info-text); 486 | } 487 | 488 | [data-rich-colors='true'][data-somoto-toast][data-type='warning'] { 489 | background: var(--warning-bg); 490 | border-color: var(--warning-border); 491 | color: var(--warning-text); 492 | } 493 | 494 | [data-rich-colors='true'][data-somoto-toast][data-type='warning'] [data-close-button] { 495 | background: var(--warning-bg); 496 | border-color: var(--warning-border); 497 | color: var(--warning-text); 498 | } 499 | 500 | [data-rich-colors='true'][data-somoto-toast][data-type='error'] { 501 | background: var(--error-bg); 502 | border-color: var(--error-border); 503 | color: var(--error-text); 504 | } 505 | 506 | [data-rich-colors='true'][data-somoto-toast][data-type='error'] [data-close-button] { 507 | background: var(--error-bg); 508 | border-color: var(--error-border); 509 | color: var(--error-text); 510 | } 511 | 512 | .somoto-loading-wrapper { 513 | --size: 16px; 514 | height: var(--size); 515 | width: var(--size); 516 | position: absolute; 517 | inset: 0; 518 | z-index: 10; 519 | } 520 | 521 | .somoto-loading-wrapper[data-visible='false'] { 522 | transform-origin: center; 523 | animation: somoto-fade-out 0.2s ease forwards; 524 | } 525 | 526 | .somoto-spinner { 527 | position: relative; 528 | top: 50%; 529 | left: 50%; 530 | height: var(--size); 531 | width: var(--size); 532 | } 533 | 534 | .somoto-loading-bar { 535 | animation: somoto-spin 1.2s linear infinite; 536 | background: var(--gray11); 537 | border-radius: 6px; 538 | height: 8%; 539 | left: -10%; 540 | position: absolute; 541 | top: -3.9%; 542 | width: 24%; 543 | } 544 | 545 | .somoto-loading-bar:nth-child(1) { 546 | animation-delay: -1.2s; 547 | transform: rotate(0.0001deg) translate(146%); 548 | } 549 | 550 | .somoto-loading-bar:nth-child(2) { 551 | animation-delay: -1.1s; 552 | transform: rotate(30deg) translate(146%); 553 | } 554 | 555 | .somoto-loading-bar:nth-child(3) { 556 | animation-delay: -1s; 557 | transform: rotate(60deg) translate(146%); 558 | } 559 | 560 | .somoto-loading-bar:nth-child(4) { 561 | animation-delay: -0.9s; 562 | transform: rotate(90deg) translate(146%); 563 | } 564 | 565 | .somoto-loading-bar:nth-child(5) { 566 | animation-delay: -0.8s; 567 | transform: rotate(120deg) translate(146%); 568 | } 569 | 570 | .somoto-loading-bar:nth-child(6) { 571 | animation-delay: -0.7s; 572 | transform: rotate(150deg) translate(146%); 573 | } 574 | 575 | .somoto-loading-bar:nth-child(7) { 576 | animation-delay: -0.6s; 577 | transform: rotate(180deg) translate(146%); 578 | } 579 | 580 | .somoto-loading-bar:nth-child(8) { 581 | animation-delay: -0.5s; 582 | transform: rotate(210deg) translate(146%); 583 | } 584 | 585 | .somoto-loading-bar:nth-child(9) { 586 | animation-delay: -0.4s; 587 | transform: rotate(240deg) translate(146%); 588 | } 589 | 590 | .somoto-loading-bar:nth-child(10) { 591 | animation-delay: -0.3s; 592 | transform: rotate(270deg) translate(146%); 593 | } 594 | 595 | .somoto-loading-bar:nth-child(11) { 596 | animation-delay: -0.2s; 597 | transform: rotate(300deg) translate(146%); 598 | } 599 | 600 | .somoto-loading-bar:nth-child(12) { 601 | animation-delay: -0.1s; 602 | transform: rotate(330deg) translate(146%); 603 | } 604 | 605 | @keyframes somoto-fade-in { 606 | 0% { 607 | opacity: 0; 608 | transform: scale(0.8); 609 | } 610 | 100% { 611 | opacity: 1; 612 | transform: scale(1); 613 | } 614 | } 615 | 616 | @keyframes somoto-fade-out { 617 | 0% { 618 | opacity: 1; 619 | transform: scale(1); 620 | } 621 | 100% { 622 | opacity: 0; 623 | transform: scale(0.8); 624 | } 625 | } 626 | 627 | @keyframes somoto-spin { 628 | 0% { 629 | opacity: 1; 630 | } 631 | 100% { 632 | opacity: 0.15; 633 | } 634 | } 635 | 636 | @media (prefers-reduced-motion) { 637 | [data-somoto-toast], 638 | [data-somoto-toast] > *, 639 | .somoto-loading-bar { 640 | transition: none !important; 641 | animation: none !important; 642 | } 643 | } 644 | 645 | .somoto-loader { 646 | position: absolute; 647 | top: 50%; 648 | left: 50%; 649 | transform: translate(-50%, -50%); 650 | transform-origin: center; 651 | transition: 652 | opacity 200ms, 653 | transform 200ms; 654 | } 655 | 656 | .somoto-loader[data-visible='false'] { 657 | opacity: 0; 658 | transform: scale(0.8) translate(-50%, -50%); 659 | } 660 | -------------------------------------------------------------------------------- /packages/somoto/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Accessor, JSX, Ref, Setter } from 'solid-js'; 2 | 3 | export type MaybeAccessor = T | Accessor; 4 | 5 | export type ToastVariants = 6 | | 'default' 7 | | 'action' 8 | | 'success' 9 | | 'info' 10 | | 'warning' 11 | | 'error' 12 | | 'loading'; 13 | 14 | export type PromiseT = Promise | (() => Promise); 15 | 16 | export type PromiseTResult = 17 | | JSX.Element 18 | | ((data: Data) => JSX.Element | Promise); 19 | 20 | export type PromiseExternalToast = Omit; 21 | 22 | export type PromiseData = PromiseExternalToast & { 23 | loading?: string | JSX.Element; 24 | success?: PromiseTResult; 25 | error?: PromiseTResult; 26 | description?: PromiseTResult; 27 | finally?: () => void | Promise; 28 | }; 29 | 30 | export interface ToastClassnames { 31 | toast?: string; 32 | title?: string; 33 | description?: string; 34 | loader?: string; 35 | closeButton?: string; 36 | cancelButton?: string; 37 | actionButton?: string; 38 | default?: string; 39 | success?: string; 40 | error?: string; 41 | info?: string; 42 | warning?: string; 43 | loading?: string; 44 | content?: string; 45 | icon?: string; 46 | } 47 | 48 | export interface ToastIcons { 49 | success?: JSX.Element; 50 | info?: JSX.Element; 51 | warning?: JSX.Element; 52 | error?: JSX.Element; 53 | loading?: JSX.Element; 54 | close?: JSX.Element; 55 | } 56 | 57 | export interface Action { 58 | label: JSX.Element; 59 | onClick: JSX.EventHandler; 60 | actionButtonStyle?: JSX.CSSProperties; 61 | } 62 | 63 | export interface ToastType { 64 | id: number | string; 65 | title?: string | JSX.Element; 66 | type?: ToastVariants; 67 | icon?: JSX.Element; 68 | jsx?: JSX.Element; 69 | richColors?: boolean; 70 | invert?: boolean; 71 | closeButton?: boolean; 72 | dismissible?: boolean; 73 | description?: JSX.Element; 74 | duration?: number; 75 | delete?: boolean; 76 | important?: boolean; 77 | action?: Action | JSX.Element; 78 | cancel?: Action | JSX.Element; 79 | onDismiss?: (toast: ToastType) => void; 80 | onAutoClose?: (toast: ToastType) => void; 81 | promise?: PromiseT; 82 | cancelButtonStyle?: JSX.CSSProperties; 83 | actionButtonStyle?: JSX.CSSProperties; 84 | style?: JSX.CSSProperties; 85 | unstyled?: boolean; 86 | className?: string; 87 | classNames?: ToastClassnames; 88 | descriptionClassName?: string; 89 | position?: Position; 90 | } 91 | 92 | export function isAction(action: Action | JSX.Element): action is Action { 93 | return (action as Action).label !== undefined; 94 | } 95 | 96 | export type Position = 97 | | 'top-left' 98 | | 'top-right' 99 | | 'bottom-left' 100 | | 'bottom-right' 101 | | 'top-center' 102 | | 'bottom-center'; 103 | export interface HeightT { 104 | height: number; 105 | toastId: number | string; 106 | position: Position; 107 | } 108 | 109 | export interface ToastOptions { 110 | className?: string; 111 | closeButton?: boolean; 112 | descriptionClassName?: string; 113 | style?: JSX.CSSProperties; 114 | cancelButtonStyle?: JSX.CSSProperties; 115 | actionButtonStyle?: JSX.CSSProperties; 116 | duration?: number; 117 | unstyled?: boolean; 118 | classNames?: ToastClassnames; 119 | } 120 | 121 | export type Direction = 'rtl' | 'ltr' | 'auto'; 122 | 123 | export interface ToasterProps { 124 | invert?: boolean; 125 | theme?: 'light' | 'dark' | 'system'; 126 | position?: Position; 127 | hotkey?: string[]; 128 | richColors?: boolean; 129 | expand?: boolean; 130 | duration?: number; 131 | gap?: number; 132 | visibleAmount?: number; 133 | closeButton?: boolean; 134 | toastOptions?: ToastOptions; 135 | className?: string; 136 | style?: JSX.CSSProperties; 137 | offset?: string | number; 138 | dir?: Direction; 139 | icons?: ToastIcons; 140 | containerAriaLabel?: string; 141 | pauseWhenPageIsHidden?: boolean; 142 | ref?: Ref; 143 | } 144 | 145 | export interface ToastProps { 146 | toast: ToastType; 147 | toasts: ToastType[]; 148 | index: number; 149 | expanded: boolean; 150 | invert: boolean; 151 | heights: HeightT[]; 152 | setHeights: Setter; 153 | removeToast: (toast: ToastType) => void; 154 | gap?: number; 155 | position: Position; 156 | visibleAmount: number; 157 | expandByDefault: boolean; 158 | closeButton: boolean; 159 | interacting: boolean; 160 | style?: JSX.CSSProperties; 161 | cancelButtonStyle?: JSX.CSSProperties; 162 | actionButtonStyle?: JSX.CSSProperties; 163 | duration?: number; 164 | class?: string; 165 | classNames?: ToastClassnames; 166 | unstyled?: boolean; 167 | descriptionClassName?: string; 168 | icons?: ToastIcons; 169 | closeButtonAriaLabel?: string; 170 | pauseWhenPageIsHidden: boolean; 171 | defaultRichColors?: boolean; 172 | } 173 | 174 | export enum SwipeStateTypes { 175 | SwipedOut = 'SwipedOut', 176 | SwipedBack = 'SwipedBack', 177 | NotSwiped = 'NotSwiped', 178 | } 179 | 180 | export type Theme = 'light' | 'dark'; 181 | 182 | export interface ToastToDismiss { 183 | id: number | string; 184 | dismiss: boolean; 185 | } 186 | 187 | export type ExternalToast = Omit< 188 | ToastType, 189 | 'id' | 'type' | 'title' | 'jsx' | 'delete' | 'promise' 190 | > & { 191 | id?: number | string; 192 | }; 193 | -------------------------------------------------------------------------------- /packages/somoto/src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | export function cn(...classes: (string | undefined)[]) { 2 | return classes.filter(Boolean).join(' '); 3 | } 4 | -------------------------------------------------------------------------------- /packages/somoto/src/utils/get-document-direction.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from '../types'; 2 | 3 | export function getDocumentDirection(): Direction { 4 | if (typeof window === 'undefined' || typeof document === 'undefined') return 'ltr'; 5 | 6 | const dirAttribute = document.documentElement.getAttribute('dir'); 7 | if (dirAttribute === 'auto' || !dirAttribute) { 8 | return window.getComputedStyle(document.documentElement).direction as Direction; 9 | } 10 | 11 | return dirAttribute as Direction; 12 | } 13 | -------------------------------------------------------------------------------- /packages/somoto/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | export const isHttpResponse = (data: any): data is Response => { 2 | return ( 3 | data && 4 | typeof data === 'object' && 5 | 'ok' in data && 6 | typeof data.ok === 'boolean' && 7 | 'status' in data && 8 | typeof data.status === 'number' 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/somoto/src/utils/unwrap-accessor.ts: -------------------------------------------------------------------------------- 1 | import type { Accessor } from 'solid-js'; 2 | 3 | import type { MaybeAccessor } from '../types'; 4 | 5 | export const unwrapAccessor = (target: MaybeAccessor) => { 6 | return typeof target === 'function' ? (target as Accessor)() : target; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/somoto/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "isolatedModules": true, 13 | "skipLibCheck": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noUncheckedIndexedAccess": false, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": false, 19 | "noImplicitAny": true, 20 | "noImplicitThis": true, 21 | "noImplicitUseStrict": false, 22 | "jsx": "preserve", 23 | "jsxImportSource": "solid-js", 24 | "types": ["node"], 25 | "baseUrl": ".", 26 | "paths": {} 27 | }, 28 | "include": ["."], 29 | "files": [".eslintrc.cjs"], 30 | "exclude": ["node_modules", "dist"] 31 | } 32 | -------------------------------------------------------------------------------- /packages/somoto/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import * as preset from 'tsup-preset-solid'; 3 | 4 | const presetOptions: preset.PresetOptions = { 5 | // array or single object 6 | entries: [ 7 | { 8 | // entries with '.ts' extension will have `solid` export condition generated 9 | entry: 'src/index.ts', 10 | // will generate a separate development entry 11 | dev_entry: true, 12 | server_entry: true, 13 | }, 14 | ], 15 | // Set to `true` to remove all `console.*` calls and `debugger` statements in prod builds 16 | drop_console: true, 17 | // Set to `true` to generate a CommonJS build alongside ESM 18 | cjs: true, 19 | }; 20 | 21 | const CI = 22 | process.env['CI'] === 'true' || 23 | process.env['GITHUB_ACTIONS'] === 'true' || 24 | process.env['CI'] === '"1"' || 25 | process.env['GITHUB_ACTIONS'] === '"1"'; 26 | 27 | export default defineConfig(config => { 28 | const watching = !!config.watch; 29 | 30 | const parsedOptions = preset.parsePresetOptions(presetOptions, watching); 31 | 32 | if (!watching && !CI) { 33 | const packageFields = preset.generatePackageExports(parsedOptions); 34 | 35 | console.log(`package.json: \n\n${JSON.stringify(packageFields, null, 2)}\n\n`); 36 | 37 | // will update ./package.json with the correct export fields 38 | preset.writePackageJson(packageFields); 39 | } 40 | 41 | return preset.generateTsupOptions(parsedOptions).map(options => ({ 42 | ...options, 43 | injectStyle: true, 44 | })); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/somoto/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import solidPlugin from 'vite-plugin-solid'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | // to test in server environment, run with "--mode ssr" or "--mode test:ssr" flag 6 | // loads only server.test.ts file 7 | const testSSR = mode === 'test:ssr' || mode === 'ssr'; 8 | 9 | return { 10 | plugins: [ 11 | solidPlugin({ 12 | // https://github.com/solidjs/solid-refresh/issues/29 13 | hot: false, 14 | // For testing SSR we need to do a SSR JSX transform 15 | solid: { generate: testSSR ? 'ssr' : 'dom' }, 16 | }), 17 | ], 18 | test: { 19 | watch: false, 20 | isolate: !testSSR, 21 | env: { 22 | NODE_ENV: testSSR ? 'production' : 'development', 23 | DEV: testSSR ? '' : '1', 24 | SSR: testSSR ? '1' : '', 25 | PROD: testSSR ? '1' : '', 26 | }, 27 | environment: testSSR ? 'node' : 'jsdom', 28 | transformMode: { web: [/\.[jt]sx$/] }, 29 | ...(testSSR 30 | ? { 31 | include: ['test/server.test.{ts,tsx}'], 32 | } 33 | : { 34 | include: ['test/*.test.{ts,tsx}'], 35 | exclude: ['test/server.test.{ts,tsx}'], 36 | }), 37 | }, 38 | resolve: { 39 | conditions: testSSR ? ['node'] : ['browser', 'development'], 40 | }, 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /scripts/index.ts: -------------------------------------------------------------------------------- 1 | const syncReadMe = () => {}; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "allowImportingTsExtensions": true, 11 | "noEmit": true, 12 | "inlineSources": false, 13 | "isolatedModules": true, 14 | "module": "ESNext", 15 | "moduleResolution": "Bundler", 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "preserveWatchOutput": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "strictNullChecks": true 22 | }, 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "lint": {}, 10 | "type-check": {}, 11 | "dev": { 12 | "cache": false, 13 | "persistent": true 14 | }, 15 | "clean": { 16 | "cache": false 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------