├── .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 | toast('Toast for you!')}>Give me a toast
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 |
22 | {item => {item} }
23 |
24 |
25 |
{
37 | setList(prev => [...prev.reverse()]);
38 | // setExpand(!expand());
39 | // setIsExpanded(prev => !prev);
40 | }}
41 | >
42 | Toggle
43 |
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 | {
77 | setType(type);
78 | showToast();
79 | }}
80 | >
81 | {type}
82 |
83 | );
84 | }}
85 |
86 |
87 |
88 |
89 |
90 | {position => {
91 | return (
92 | {
94 | setPosition(position);
95 | showToast();
96 | }}
97 | >
98 | {position}
99 |
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 | setType(item)}>
18 | {item}
19 |
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 |
46 |
47 |
59 |
60 |
61 | }
62 | >
63 |
72 |
73 |
74 |
75 |
76 |
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 | {
18 | props.setExpand(true);
19 | toast('Event has been created', {
20 | description: 'Monday, January 3rd at 6:00pm',
21 | });
22 | }}
23 | >
24 | Expand
25 |
26 | {
29 | props.setExpand(false);
30 | toast('Event has been created', {
31 | description: 'Monday, January 3rd at 6:00pm',
32 | });
33 | }}
34 | >
35 | Default
36 |
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 | toast.dismiss(t)}>Dismiss
64 |
65 | ));`,
66 | action: () => {
67 | toast.custom(
68 | t => (
69 |
70 |
Event Created
71 |
Today at 4:00pm - "Louvre Museum"
72 |
toast.dismiss(t)}>
73 |
74 |
75 |
76 |
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 | {
98 | setActiveType(type);
99 | type.action();
100 | }}
101 | >
102 | {type.name}
103 |
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 | {
40 | props.setPosition(position);
41 | removeAllToasts();
42 | toast('Event has been created', {
43 | description: 'Monday, January 3rd at 6:00pm',
44 | });
45 | }}
46 | >
47 | {position}
48 |
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 | {
117 | type.action();
118 | setCurrentType(type);
119 | }}
120 | >
121 | {type.name}
122 |
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 | toast('Toast for you!')}>
11 | Give me a toast
12 |
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 | {
30 | toast('Have a Toast!');
31 | }}
32 | >
33 | Render A Toast
34 |
35 |
36 | Github
37 |
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 toast('This is a somoto toast')}>Render my toast ;
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: console.log('Action!')}>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: console.log('Cancel!')}>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 toast.dismiss(t)}>close
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: () => This is a button element! ,
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 | toast('Toast for you!')}>Give me a toast
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 |
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 | {}
182 | : () => {
183 | deleteToast();
184 | props.toast.onDismiss?.(props.toast);
185 | }
186 | }
187 | class={cn(props.classNames?.closeButton, props.toast?.classNames?.closeButton)}
188 | >
189 | }>
190 | {closeElement => closeElement()}
191 |
192 |
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 | {
264 | // We need to check twice because typescript
265 | if (!isAction(props.toast.cancel)) return;
266 | if (!dismissible()) return;
267 | props.toast.cancel.onClick?.(event);
268 | deleteToast();
269 | }}
270 | class={cn(props.classNames?.cancelButton, props.toast?.classNames?.cancelButton)}
271 | >
272 | {(props.toast.cancel as Action).label}
273 |
274 |
275 | );
276 | };
277 |
278 | /* action */
279 | const renderAction = (): JSXElement => {
280 | return (
281 | {action => <>{action()}>} }
284 | >
285 | {
290 | // We need to check twice because typescript
291 | if (!isAction(props.toast.action)) return;
292 | if (event.defaultPrevented) return;
293 | props.toast.action.onClick?.(event);
294 | deleteToast();
295 | }}
296 | class={cn(props.classNames?.actionButton, props.toast.classNames?.actionButton)}
297 | >
298 | {(props.toast.action as Action).label}
299 |
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 |
--------------------------------------------------------------------------------