├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── preview.yml ├── .gitignore ├── LICENSE ├── README.md ├── apps └── quotes-api │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── api │ │ └── quotes │ │ │ ├── quotes.json │ │ │ └── random.ts │ └── index.html │ └── tsconfig.json ├── examples ├── counter │ ├── biome.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── Main.module.css │ │ ├── Main.tsx │ │ ├── domains │ │ │ └── counter │ │ │ │ ├── Counter.ts │ │ │ │ ├── components │ │ │ │ ├── ApplyDeltaButton.tsx │ │ │ │ ├── SetCounterValue.module.css │ │ │ │ ├── SetCounterValue.tsx │ │ │ │ └── ShowValue.tsx │ │ │ │ └── index.ts │ │ ├── index.css │ │ ├── index.html │ │ └── index.ts │ ├── tailwind.config.js │ ├── tailwind.css │ ├── tests │ │ ├── index.test.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ ├── vite.config.js │ └── vitest.config.ts ├── custom-element │ ├── README.md │ ├── biome.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── Counter.module.css │ │ ├── Counter.tsx │ │ ├── external.ts │ │ ├── index.css │ │ ├── index.html │ │ └── index.ts │ ├── tailwind.config.js │ ├── tailwind.css │ ├── test.html │ ├── tests │ │ ├── index.test.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── e-commerce │ ├── biome.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ └── images │ │ │ └── categories │ │ │ ├── electronics.webp │ │ │ ├── jewelery.webp │ │ │ ├── men's clothing.webp │ │ │ └── women's clothing.webp │ ├── src │ │ ├── Main.tsx │ │ ├── components │ │ │ ├── CardList.module.css │ │ │ ├── CardList.tsx │ │ │ ├── Header.module.css │ │ │ ├── Header.tsx │ │ │ └── icon.png │ │ ├── domains │ │ │ ├── cart │ │ │ │ ├── CartDomain.ts │ │ │ │ ├── components │ │ │ │ │ ├── AddToCart.module.css │ │ │ │ │ ├── AddToCart.tsx │ │ │ │ │ ├── CartBadge.tsx │ │ │ │ │ ├── CartProductList.module.css │ │ │ │ │ ├── CartProductList.tsx │ │ │ │ │ ├── CartTooltip.module.css │ │ │ │ │ ├── CartTooltip.tsx │ │ │ │ │ └── cart-badge.module.css │ │ │ │ ├── events.ts │ │ │ │ └── index.ts │ │ │ ├── product │ │ │ │ ├── ProductDomain.ts │ │ │ │ ├── components │ │ │ │ │ ├── ProductCategoryList.module.css │ │ │ │ │ ├── ProductCategoryList.tsx │ │ │ │ │ ├── ProductItem.module.css │ │ │ │ │ └── ProductItem.tsx │ │ │ │ └── index.ts │ │ │ └── user │ │ │ │ ├── UserDomain.ts │ │ │ │ ├── components │ │ │ │ ├── UserProfileBadge.tsx │ │ │ │ └── user-profile-badge.module.css │ │ │ │ ├── events.ts │ │ │ │ └── index.ts │ │ ├── index.css │ │ ├── index.html │ │ ├── index.ts │ │ ├── pages │ │ │ ├── cart.tsx │ │ │ ├── category.tsx │ │ │ ├── checkout.tsx │ │ │ ├── home.tsx │ │ │ ├── login.tsx │ │ │ ├── logout.tsx │ │ │ └── profile.tsx │ │ ├── router.module.css │ │ ├── router.tsx │ │ ├── types.d.ts │ │ └── types.ts │ ├── tailwind.config.js │ ├── tailwind.css │ ├── tests │ │ ├── index.test.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ ├── vite.config.js │ └── vitest.config.ts ├── empty │ ├── biome.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── Main.module.css │ │ ├── Main.tsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.ts │ ├── tailwind.config.js │ ├── tailwind.css │ ├── tests │ │ ├── index.test.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ ├── vite.config.js │ └── vitest.config.ts └── random-quote │ ├── biome.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── Main.module.css │ ├── Main.tsx │ ├── domains │ │ └── quote │ │ │ ├── QuoteDomain.ts │ │ │ ├── components │ │ │ ├── Quote.module.css │ │ │ ├── Quote.tsx │ │ │ └── RefreshQuoteButton.tsx │ │ │ └── index.ts │ ├── index.css │ ├── index.html │ └── index.ts │ ├── tailwind.config.js │ ├── tailwind.css │ ├── tests │ ├── index.test.ts │ └── setupTests.ts │ ├── tsconfig.json │ ├── vite.config.js │ └── vitest.config.ts ├── package.json ├── packages ├── components │ ├── .gitignore │ ├── .npmignore │ ├── .storybook │ │ ├── main.js │ │ └── preview.js │ ├── LICENSE │ ├── README.md │ ├── biome.json │ ├── build-types.tsconfig.json │ ├── package.json │ ├── postcss.config.cjs │ ├── src │ │ ├── Alert │ │ │ ├── Alert.stories.tsx │ │ │ └── index.tsx │ │ ├── Badge │ │ │ ├── Badge.stories.tsx │ │ │ └── index.tsx │ │ ├── Button │ │ │ ├── Button.stories.tsx │ │ │ └── index.tsx │ │ ├── Card │ │ │ ├── Card.stories.tsx │ │ │ └── index.tsx │ │ ├── ChatBubble │ │ │ ├── ChatBubble.stories.tsx │ │ │ └── index.tsx │ │ ├── Checkbox │ │ │ ├── Checkbox.stories.tsx │ │ │ └── index.tsx │ │ ├── Divider │ │ │ ├── Divider.stories.tsx │ │ │ └── index.tsx │ │ ├── Dropdown │ │ │ ├── Dropdown.stories.tsx │ │ │ └── index.tsx │ │ ├── Footer │ │ │ ├── Footer.stories.tsx │ │ │ └── index.tsx │ │ ├── Form │ │ │ ├── Form.stories.tsx │ │ │ └── index.tsx │ │ ├── FormField │ │ │ ├── FormField.stories.tsx │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── Hero │ │ │ ├── Hero.stories.tsx │ │ │ └── index.tsx │ │ ├── Link │ │ │ ├── Link.stories.tsx │ │ │ └── index.tsx │ │ ├── Loading │ │ │ ├── Loading.stories.tsx │ │ │ └── index.tsx │ │ ├── Menu │ │ │ ├── Menu.stories.tsx │ │ │ └── index.tsx │ │ ├── Modal │ │ │ ├── Modal.stories.tsx │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── Navbar.stories.tsx │ │ │ └── index.tsx │ │ ├── NumberInput │ │ │ ├── NumberInput.stories.tsx │ │ │ └── index.tsx │ │ ├── Select │ │ │ ├── Select.stories.tsx │ │ │ └── index.tsx │ │ ├── Tabs │ │ │ ├── Tabs.stories.tsx │ │ │ └── index.tsx │ │ ├── TextInput │ │ │ ├── TextInput.stories.tsx │ │ │ └── index.tsx │ │ ├── Typography │ │ │ ├── Heading.stories.tsx │ │ │ ├── Prose.stories.tsx │ │ │ └── index.tsx │ │ ├── index.css │ │ └── index.tsx │ ├── stories │ │ ├── Configure.mdx │ │ ├── assets │ │ │ ├── accessibility.png │ │ │ ├── accessibility.svg │ │ │ ├── addon-library.png │ │ │ ├── assets.png │ │ │ ├── avif-test-image.avif │ │ │ ├── context.png │ │ │ ├── discord.svg │ │ │ ├── docs.png │ │ │ ├── figma-plugin.png │ │ │ ├── github.svg │ │ │ ├── share.png │ │ │ ├── styling.png │ │ │ ├── testing.png │ │ │ ├── theming.png │ │ │ ├── tutorials.svg │ │ │ └── youtube.svg │ │ └── foo.tsx │ ├── tailwind.config.js │ ├── tests │ │ ├── index.test.tsx │ │ └── test.js │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── create-seqflow │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── biome.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── main.ts │ └── vite.config.js ├── seqflow │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── biome.json │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── component.ts │ │ ├── debug.ts │ │ ├── domains.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── plugin.ts │ │ ├── router.ts │ │ └── types.ts │ ├── tests │ │ ├── inner │ │ │ ├── child.test.tsx │ │ │ ├── debug.test.tsx │ │ │ ├── dom.test.tsx │ │ │ ├── domains.test.ts │ │ │ ├── events.test.ts │ │ │ ├── lifecycle.test.tsx │ │ │ ├── onClick.test.tsx │ │ │ ├── render.test.tsx │ │ │ ├── router.test.ts │ │ │ └── unmount-checks.test.tsx │ │ ├── seqflowjs.test.tsx │ │ └── test-utils.ts │ ├── tsconfig.json │ ├── types │ │ ├── app.tsx │ │ ├── component.tsx │ │ ├── events.tsx │ │ ├── jsx.tsx │ │ └── tsconfig.json │ ├── vite.config.js │ ├── vite.d.ts │ └── vitest.config.ts ├── storybook │ ├── README.md │ ├── manager.js │ ├── package.json │ ├── preset.js │ ├── render.js │ ├── src │ │ ├── docs │ │ │ └── sourceDecorator.ts │ │ ├── entry-preview-docs.ts │ │ ├── entry-preview.ts │ │ ├── index.ts │ │ ├── manager.ts │ │ ├── preset.ts │ │ ├── render.ts │ │ ├── types.ts │ │ └── vite-plugin.ts │ ├── tests │ │ ├── fixtures │ │ │ ├── Button.ts │ │ │ ├── Enum.ts │ │ │ ├── EnumOutsidePropertyAccessPropsType.ts │ │ │ ├── ExportDefault.ts │ │ │ ├── MultipleExports.ts │ │ │ ├── Normal.ts │ │ │ ├── Or2.ts │ │ │ ├── OrSimple.ts │ │ │ ├── SimpleEnum.ts │ │ │ ├── WithFunction.ts │ │ │ └── formField.ts │ │ └── foo.test.ts │ ├── tsconfig.json │ └── vite-plugin.js └── website │ ├── .gitignore │ ├── README.md │ ├── biome.json │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── Main.module.css │ ├── Main.tsx │ ├── components │ │ ├── Arrow.module.css │ │ ├── Arrow.tsx │ │ ├── ContentWithToc.module.css │ │ ├── ContentWithToc.tsx │ │ ├── Header.module.css │ │ └── Header.tsx │ ├── index.css │ ├── index.html │ ├── index.ts │ ├── pages │ │ ├── Example.md │ │ ├── Example.tsx │ │ ├── GetStarted.md │ │ ├── GetStarted.tsx │ │ ├── GetStarted_0prerequisites.md │ │ ├── GetStarted_1fetchData.md │ │ ├── GetStarted_2splitComponents.md │ │ ├── GetStarted_3refreshQuote.md │ │ ├── GetStarted_4configuration.md │ │ ├── GetStarted_5test.md │ │ ├── GetStarted_6domain.md │ │ ├── GetStarted_7conclusion.md │ │ ├── Home.module.css │ │ ├── Home.tsx │ │ ├── Why.md │ │ ├── Why.tsx │ │ └── blog │ │ │ ├── 2024-11-24-theres-a-new-framework-in-town.md │ │ │ ├── Blog.module.css │ │ │ └── Blog.tsx │ ├── public │ │ └── images │ │ │ ├── Rectangle 5.svg │ │ │ ├── Union.svg │ │ │ ├── blog │ │ │ └── 2024-11-24-theres-a-new-framework-in-town.svg │ │ │ ├── favicon.ico │ │ │ ├── github.svg │ │ │ ├── logo-dark.svg │ │ │ └── logo.svg │ └── typings.d.ts │ ├── tailwind.config.js │ ├── tailwind.css │ ├── tsconfig.json │ ├── vercel.json │ ├── vite.config.ts │ ├── vite.d.ts │ ├── vitest.config.ts │ └── webfonts │ ├── fa-brands-400.ttf │ └── fa-brands-400.woff2 ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── scripts └── release.js └── turbo.json /.gitattributes: -------------------------------------------------------------------------------- 1 | packages/website/src/** linguist-detectable=false -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | jobs: 8 | build: 9 | name: Build and Test 10 | timeout-minutes: 15 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - uses: pnpm/action-setup@v3 18 | with: 19 | version: 8 20 | 21 | - name: Setup Node.js environment 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install 29 | 30 | - name: Install playwright 31 | run: cd packages/components && npx playwright install --with-deps 32 | 33 | - name: Build 34 | run: pnpm build 35 | 36 | - name: Test 37 | run: pnpm test 38 | 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Build & Vercel Preview Deployment 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | timeout-minutes: 15 11 | runs-on: ubuntu-latest 12 | 13 | environment: Preview 14 | 15 | env: 16 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 17 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 18 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} 19 | ENV_NAME: ${{ vars.ENV_NAME }} 20 | 21 | steps: 22 | - name: Print env 23 | run : echo $ENV_NAME 24 | 25 | - name: Check out code 26 | uses: actions/checkout@v4 27 | 28 | - uses: pnpm/action-setup@v3 29 | with: 30 | version: 9 31 | 32 | - name: Setup Node.js environment 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 20 36 | cache: 'pnpm' 37 | 38 | - name: Install dependencies 39 | run: pnpm install 40 | 41 | - name: Install playwright 42 | run: cd packages/components && npx playwright install --with-deps 43 | 44 | - name: Build 45 | run: pnpm build 46 | 47 | - name: Test 48 | run: pnpm test 49 | 50 | - name: Install Vercel CLI 51 | working-directory: packages/website 52 | run: pnpm install --global vercel@latest 53 | - name: Pull Vercel Environment Information 54 | working-directory: packages/website 55 | run: vercel pull --yes --environment=preview --token ${{ secrets.VERCEL_TOKEN }} 56 | - name: Build Project Artifacts 57 | working-directory: packages/website 58 | run: vercel build 59 | - name: Deploy Project Artifacts to Vercel 60 | working-directory: packages/website 61 | run: vercel deploy --prebuilt --token ${{ secrets.VERCEL_TOKEN }} 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | .dev 5 | .DS_Store 6 | .turbo 7 | .idea/ 8 | 9 | *storybook.log 10 | 11 | gen-ai 12 | orama-search-box 13 | apps/track-active-component 14 | tools/foo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tommaso Allevi 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 | # SeqFlow JS 2 | 3 | This framework is a lightweight, domain-driven front-end framework designed to simplify web application development, reduce complexity, and enhance user experience with an event-driven architecture. 4 | 5 | See the [documentation](https://seqflow.dev) for more information. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pnpm install @seqflow/seqflow 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```tsx 16 | import { Contexts } from "@seqflow/seqflow"; 17 | 18 | interface Quote { 19 | author: string; 20 | content: string; 21 | } 22 | 23 | async function getRandomQuote(): Promise { 24 | const res = await fetch("https://quotes.seqflow.dev/api/quotes/random") 25 | return await res.json(); 26 | } 27 | 28 | export async function Main({}, { component }: Contexts) { 29 | // Render loading message 30 | component.renderSync( 31 |

Loading...

32 | ); 33 | 34 | // Perform an async operation 35 | const quote = await getRandomQuote(); 36 | 37 | // Replace loading message with quote 38 | component.renderSync( 39 |
40 |
{quote.content}
41 |
{quote.author}
42 |
43 | ); 44 | } 45 | 46 | start(document.getElementById("root"), Main, {}, {}); 47 | ``` 48 | 49 | ## License 50 | 51 | SeqFlow JS is licensed under the [MIT License](LICENSE). 52 | -------------------------------------------------------------------------------- /apps/quotes-api/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /apps/quotes-api/README.md: -------------------------------------------------------------------------------- 1 | # Quote API for demo purposes 2 | 3 | This is a simple API that returns a random quote from a list of quotes. 4 | The quotes are stored in a JSON file and are loaded into memory when the server starts. 5 | The quotes copied from [this repo](https://github.com/quotable-io/data). 6 | 7 | ## Endpoints 8 | 9 | `GET /api/quotes/random` - Returns a random quote 10 | 11 | ## Running the server 12 | 13 | ```bash 14 | npx vercel dev 15 | ``` 16 | 17 | ## Deploying the server 18 | 19 | ```bash 20 | npx vercel 21 | ``` 22 | or, for production, 23 | ```bash 24 | npx vercel --prod 25 | ``` 26 | -------------------------------------------------------------------------------- /apps/quotes-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quotes-api", 3 | "version": "1.0.0", 4 | "description": "Random Quotes API", 5 | "scripts": { 6 | "build": "rm -rf dist && tsc && cp src/index.html dist/src/index.html", 7 | "test": "echo \"Error: no test specified\"" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "typescript": "^5.6.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/quotes-api/src/api/quotes/random.ts: -------------------------------------------------------------------------------- 1 | import quotes from './quotes.json' 2 | 3 | const ALLOW_CORS = { 4 | 'Access-Control-Allow-Origin': '*', 5 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 6 | 'Access-Control-Allow-Headers': 'Content-Type', 7 | }; 8 | 9 | export function GET(request: Request): Response { 10 | const quote = quotes[Math.floor(Math.random() * quotes.length)]; 11 | return new Response(JSON.stringify(quote), { 12 | headers: { 13 | 'content-type': 'application/json', 14 | ...ALLOW_CORS, 15 | } 16 | }); 17 | } -------------------------------------------------------------------------------- /apps/quotes-api/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SeqFlow - Quotes API 5 | 6 | 7 |

SeqFlow - Quotes API

8 |

For more information, please visit the GitHub repository.

9 | 10 |

Endpoints

11 | 22 | -------------------------------------------------------------------------------- /apps/quotes-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "WebWorker"], 4 | "strict": true, 5 | "skipLibCheck": true, 6 | "target": "ES2020", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "rootDir": "./", 12 | "outDir": "./dist", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /examples/counter/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "1.0.0", 4 | "description": "Counter example", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm-run-all biome unit build", 9 | "unit": "vitest --run", 10 | "biome": "biome ci ./src", 11 | "biome:check": "biome check --apply src tests", 12 | "start": "vite -c vite.config.js", 13 | "build": "vite build --emptyOutDir -c vite.config.js", 14 | "depcheck": "depcheck --ignores @types/css-modules,autoprefixer,typescript .", 15 | "serve:static": "serve dist" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@biomejs/biome": "1.5.3", 22 | "@tailwindcss/typography": "^0.5.13", 23 | "@testing-library/dom": "^10", 24 | "@testing-library/jest-dom": "^6.4.2", 25 | "@types/css-modules": "^1.0.5", 26 | "autoprefixer": "^10.4.2", 27 | "depcheck": "^1.4.7", 28 | "jsdom": "^25", 29 | "npm-run-all": "^4.1.5", 30 | "serve": "^14.2.3", 31 | "tailwindcss": "^3.4.6", 32 | "typescript": "^5.4.2", 33 | "vite": "^6", 34 | "vite-plugin-checker": "^0.8", 35 | "vitest": "^2" 36 | }, 37 | "dependencies": { 38 | "@seqflow/components": "workspace:*", 39 | "@seqflow/seqflow": "workspace:*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/counter/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /examples/counter/src/Main.module.css: -------------------------------------------------------------------------------- 1 | 2 | .wrapper { 3 | @apply flex flex-row items-center; 4 | } 5 | 6 | .counter { 7 | @apply grow min-w-14 text-center; 8 | } 9 | -------------------------------------------------------------------------------- /examples/counter/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Divider } from "@seqflow/components"; 2 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | import classes from "./Main.module.css"; 4 | import { 5 | ApplyDeltaButton, 6 | SetCounterValue, 7 | ShowValue, 8 | } from "./domains/counter"; 9 | 10 | export async function Main( 11 | _: ComponentProps, 12 | { component }: Contexts, 13 | ) { 14 | component.renderSync( 15 | 20 | 21 | Counter Card 22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 |
30 |
, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/counter/src/domains/counter/Counter.ts: -------------------------------------------------------------------------------- 1 | import { createDomainEventClass } from "@seqflow/seqflow"; 2 | 3 | export const CounterChanged = createDomainEventClass< 4 | { 5 | beforeValue: number; 6 | delta: number; 7 | currentValue: number; 8 | }, 9 | "changed" 10 | >("counter", "changed"); 11 | 12 | export class CounterDomain { 13 | private counter: number; 14 | 15 | constructor(private eventTarget: EventTarget) { 16 | this.counter = 0; 17 | } 18 | 19 | applyDelta(delta: number) { 20 | const beforeValue = this.counter; 21 | this.counter += delta; 22 | this.eventTarget.dispatchEvent( 23 | new CounterChanged({ 24 | beforeValue, 25 | delta, 26 | currentValue: this.counter, 27 | }), 28 | ); 29 | } 30 | 31 | set(newValue: number) { 32 | const beforeValue = this.counter; 33 | const delta = newValue - this.counter; 34 | this.counter = newValue; 35 | this.eventTarget.dispatchEvent( 36 | new CounterChanged({ 37 | beforeValue, 38 | delta, 39 | currentValue: this.counter, 40 | }), 41 | ); 42 | } 43 | 44 | get() { 45 | return this.counter; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/counter/src/domains/counter/components/ApplyDeltaButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@seqflow/components"; 2 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | 4 | export async function ApplyDeltaButton( 5 | { delta, label }: ComponentProps<{ delta: number; label: string }>, 6 | { component, app }: Contexts, 7 | ) { 8 | component.renderSync( 9 | , 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/counter/src/domains/counter/components/SetCounterValue.module.css: -------------------------------------------------------------------------------- 1 | 2 | .wrapper { 3 | @apply flex flex-row gap-2; 4 | 5 | .submitButton { 6 | @apply mb-8 self-end; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/counter/src/domains/counter/components/SetCounterValue.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, FormField, NumberInput } from "@seqflow/components"; 2 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | import classes from "./SetCounterValue.module.css"; 4 | 5 | export async function SetCounterValue( 6 | _: ComponentProps, 7 | { component, app }: Contexts, 8 | ) { 9 | component.renderSync( 10 |
11 | 12 | 13 | 14 | 22 |
, 23 | ); 24 | 25 | const events = component.waitEvents( 26 | component.domEvent("form", "submit", { preventDefault: true }), 27 | ); 28 | for await (const _ of events) { 29 | const input = component.getChild("choose-value"); 30 | const value = input.valueAsNumber; 31 | app.domains.counter.set(value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/counter/src/domains/counter/components/ShowValue.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { CounterChanged } from "../Counter"; 3 | 4 | export async function ShowValue( 5 | _: ComponentProps, 6 | { component, app }: Contexts, 7 | ) { 8 | component._el.setAttribute("aria-live", "polite"); 9 | 10 | component.renderSync(`${app.domains.counter.get()}`); 11 | 12 | const events = component.waitEvents(component.domainEvent(CounterChanged)); 13 | for await (const ev of events) { 14 | component._el.textContent = `${ev.detail.currentValue}`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/counter/src/domains/counter/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Counter"; 2 | export * from "./components/ApplyDeltaButton"; 3 | export * from "./components/SetCounterValue"; 4 | export * from "./components/ShowValue"; 5 | -------------------------------------------------------------------------------- /examples/counter/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | :root { 7 | /* background color */ 8 | --b1: 0.3 0 0; 9 | /* text color */ 10 | --bc: 0.91 0 0; 11 | 12 | /* primary color - button color */ 13 | --p: 0.6 0.22 257.22; 14 | --pc: 1 0 0; 15 | /* secondary color - button color */ 16 | --s: 0.64 0.18 146.74; 17 | --sc: 1 0 0; 18 | } 19 | -------------------------------------------------------------------------------- /examples/counter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SeqFlow JS - Counter example 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /examples/counter/src/index.ts: -------------------------------------------------------------------------------- 1 | import "@seqflow/components/style.css"; 2 | import { start } from "@seqflow/seqflow"; 3 | import { Main } from "./Main"; 4 | import { CounterDomain } from "./domains/counter/Counter"; 5 | import "./index.css"; 6 | 7 | start( 8 | document.getElementById("root")!, 9 | Main, 10 | {}, 11 | { 12 | log: console, 13 | domains: { 14 | counter: (et) => new CounterDomain(et), 15 | }, 16 | }, 17 | ); 18 | 19 | declare module "@seqflow/seqflow" { 20 | interface Domains { 21 | counter: CounterDomain; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/counter/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/domains/counter/components/*.{js,jsx,ts,tsx}', 5 | './src/**/*.{js,jsx,ts,tsx}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require("@tailwindcss/typography"), 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /examples/counter/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/counter/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { start } from "@seqflow/seqflow"; 2 | import { screen, waitFor } from "@testing-library/dom"; 3 | import { expect, test } from "vitest"; 4 | import { Main } from "../src/Main"; 5 | import { CounterDomain } from "../src/domains/counter"; 6 | 7 | test("should increment and decrement the counter", async () => { 8 | start( 9 | document.body, 10 | Main, 11 | {}, 12 | { 13 | domains: { 14 | counter: (et) => new CounterDomain(et), 15 | }, 16 | }, 17 | ); 18 | 19 | const incrementButton = await screen.findByText(/increment/i); 20 | const decrementButton = await screen.findByText(/decrement/i); 21 | const setValueButton = await screen.findByText(/Set value/i); 22 | const spinInput = await screen.findByRole("spinbutton"); 23 | const counterDiv = await screen.findByText(/0/i); 24 | 25 | expect(counterDiv?.textContent).toBe("0"); 26 | 27 | incrementButton?.click(); 28 | await waitFor(() => expect(counterDiv?.textContent).toBe("1")); 29 | incrementButton?.click(); 30 | await waitFor(() => expect(counterDiv?.textContent).toBe("2")); 31 | decrementButton?.click(); 32 | await waitFor(() => expect(counterDiv?.textContent).toBe("1")); 33 | 34 | spinInput.value = "10"; 35 | setValueButton?.click(); 36 | 37 | await waitFor(() => expect(counterDiv?.textContent).toBe("10")); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/counter/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | 3 | import "../src/index.css"; 4 | import "../tailwind.css"; 5 | -------------------------------------------------------------------------------- /examples/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | }, 18 | "include": [ 19 | "./src/**/*", 20 | "./tests/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /examples/counter/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import checker from 'vite-plugin-checker' 3 | 4 | export default defineConfig({ 5 | root: "src", 6 | build: { 7 | outDir: "../dist", 8 | }, 9 | plugins: [ 10 | checker({ 11 | typescript: true, 12 | }), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /examples/counter/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | setupFiles: './tests/setupTests.ts', 7 | }, 8 | }) -------------------------------------------------------------------------------- /examples/custom-element/README.md: -------------------------------------------------------------------------------- 1 | # Custom Element Example: Counter 2 | 3 | This project is created to demonstrate how to create a web component using SeqFlow. It is so simple! 4 | 5 | In this example, we have a simple counter that increments and decrements the value. The counter is defined as a `SeqFlow` component and used in the `CounterElement` class. 6 | 7 | The example shows how to create a web component using `SeqFlow` and use it in a simple HTML file. Moreover, the example demonstrates how to handle the attribute changes of the web component. 8 | 9 | ## How to run 10 | 11 | Because custom elements are tricky, this repo uses three kinds of tests. 12 | 13 | ### Development mode 14 | 15 | This allows you to test and develop the web component. It does not use shadow DOM, so it is easier to debug. 16 | 17 | ```bash 18 | pnpm start 19 | ``` 20 | 21 | ### Unit test 22 | The following command runs the unit tests, as normal. 23 | ```bash 24 | pnpm test 25 | ``` 26 | 27 | ### Build mode 28 | Because the production mode is different from the development mode, you need to build the web component and test it in a real environment. To do so, this repo provides a simple HTML file that uses the web component. 29 | 30 | ```bash 31 | pnpm build 32 | pnpm run serve:static 33 | ``` 34 | 35 | Open [http://localhost:3000/test.html](http://localhost:3000/test.html) in your browser. 36 | 37 | ## Project structure 38 | 39 | - `src/index.ts`: The main file that defines the web component. It defines a `CounterElement` class as `my-counter-element` custom element. It initializes the application. 40 | - `src/Counter.tsx`: A classic `SeqFlow` component that defines the counter. 41 | - `test.html`: An example usage of the `my-counter-element` custom element like a production environment. 42 | -------------------------------------------------------------------------------- /examples/custom-element/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/custom-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-element", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm-run-all biome unit build", 9 | "unit": "vitest --run", 10 | "biome": "biome ci ./src", 11 | "biome:check": "biome check --apply src tests", 12 | "start": "vite -c vite.config.js", 13 | "serve:static": "serve .", 14 | "build": "vite build --emptyOutDir -c vite.config.js" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@biomejs/biome": "1.5.3", 21 | "@tailwindcss/typography": "^0.5.15", 22 | "@testing-library/dom": "^10", 23 | "@testing-library/jest-dom": "^6.4.5", 24 | "@types/css-modules": "^1.0.5", 25 | "autoprefixer": "^10.4.2", 26 | "npm-run-all": "^4.1.5", 27 | "jsdom": "^25", 28 | "serve": "^14.2.3", 29 | "tailwindcss": "^3.4.6", 30 | "typescript": "^5.4.2", 31 | "vite": "^6", 32 | "vite-plugin-checker": "^0.8", 33 | "vitest": "^2" 34 | }, 35 | "dependencies": { 36 | "@seqflow/components": "workspace:*", 37 | "@seqflow/seqflow": "workspace:*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/custom-element/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /examples/custom-element/src/Counter.module.css: -------------------------------------------------------------------------------- 1 | 2 | .counter-card { 3 | display: flex; 4 | flex-direction: column; 5 | 6 | .buttons { 7 | display: flex; 8 | 9 | .counter { 10 | text-align: center; 11 | flex-grow: 1; 12 | align-self: center; 13 | } 14 | } 15 | } 16 | 17 | .main-counter { 18 | width: 200px; 19 | margin: auto; 20 | margin-top: 100px; 21 | } 22 | -------------------------------------------------------------------------------- /examples/custom-element/src/external.ts: -------------------------------------------------------------------------------- 1 | import { createDomainEventClass } from "@seqflow/seqflow"; 2 | 3 | export const CHANGE_VALUE_EVENT_NAME = "changeValue" as const; 4 | export const ExternalChangeValue = createDomainEventClass< 5 | { 6 | newValue: number; 7 | }, 8 | typeof CHANGE_VALUE_EVENT_NAME 9 | >("external", CHANGE_VALUE_EVENT_NAME); 10 | export type ExternalChangeValue = InstanceType; 11 | -------------------------------------------------------------------------------- /examples/custom-element/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | :root { 8 | /* background color */ 9 | --b1: 0.3 0 0; 10 | /* text color */ 11 | --bc: 0.91 0 0; 12 | 13 | /* primary color - button color */ 14 | --p: 0.6 0.22 257.22; 15 | --pc: 1 0 0; 16 | /* secondary color - button color */ 17 | --s: 0.64 0.18 146.74; 18 | --sc: 1 0 0; 19 | } 20 | -------------------------------------------------------------------------------- /examples/custom-element/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | SeqFlow example 9 | 15 | 37 | 38 | 39 |

Below the custom component:

40 |
41 |

This button change the counter value from outside:

42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/custom-element/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/domains/counter/components/*.{js,jsx,ts,tsx}', 5 | './src/**/*.{js,jsx,ts,tsx}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require("@tailwindcss/typography"), 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-element/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/custom-element/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { start } from "@seqflow/seqflow"; 2 | import { screen, waitFor } from "@testing-library/dom"; 3 | import { expect, test } from "vitest"; 4 | import { Counter, CounterDomain } from "../src/Counter"; 5 | import { ExternalChangeValue } from "../src/external"; 6 | 7 | test("should increment and decrement the counter", async () => { 8 | const externalEventTarget = new EventTarget(); 9 | start( 10 | document.body, 11 | Counter, 12 | {}, 13 | { 14 | domains: { 15 | counter: (et) => new CounterDomain(et, externalEventTarget, 0), 16 | external: () => externalEventTarget, 17 | }, 18 | }, 19 | ); 20 | 21 | const incrementButton = await screen.findByText(/increment/i); 22 | const decrementButton = await screen.findByText(/decrement/i); 23 | const counterDiv = await screen.findByText(/0/i); 24 | 25 | expect(counterDiv?.textContent).toBe("0"); 26 | 27 | incrementButton?.click(); 28 | await waitFor(() => expect(counterDiv?.textContent).toBe("1")); 29 | incrementButton?.click(); 30 | await waitFor(() => expect(counterDiv?.textContent).toBe("2")); 31 | incrementButton?.click(); 32 | await waitFor(() => expect(counterDiv?.textContent).toBe("3")); 33 | decrementButton?.click(); 34 | await waitFor(() => expect(counterDiv?.textContent).toBe("2")); 35 | externalEventTarget.dispatchEvent(new ExternalChangeValue({ newValue: 10 })); 36 | await waitFor(() => expect(counterDiv?.textContent).toBe("10")); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/custom-element/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | 3 | import "../src/index.css"; 4 | import "../tailwind.css"; 5 | -------------------------------------------------------------------------------- /examples/custom-element/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2018", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true 17 | }, 18 | "include": [ 19 | "./src/**/*", 20 | "./tests/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /examples/custom-element/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | setupFiles: './tests/setupTests.ts', 7 | }, 8 | }) -------------------------------------------------------------------------------- /examples/e-commerce/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/e-commerce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e-commerce", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm-run-all biome unit build", 9 | "unit": "vitest --run", 10 | "biome": "biome ci ./src", 11 | "biome:check": "biome check --apply src tests", 12 | "start": "vite -c vite.config.js", 13 | "build": "vite build --emptyOutDir -c vite.config.js", 14 | "depcheck": "depcheck --ignores @types/css-modules,autoprefixer,typescript .", 15 | "serve:static": "serve dist" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@biomejs/biome": "1.5.3", 22 | "@tailwindcss/typography": "^0.5.13", 23 | "@testing-library/dom": "^10", 24 | "@testing-library/jest-dom": "^6.4.2", 25 | "@testing-library/user-event": "^14.5.2", 26 | "@types/css-modules": "^1.0.5", 27 | "autoprefixer": "^10.4.2", 28 | "depcheck": "^1.4.7", 29 | "chokidar": "^4", 30 | "jsdom": "^25", 31 | "msw": "^2.2.1", 32 | "npm-run-all": "^4.1.5", 33 | "serve": "^14.2.3", 34 | "tailwindcss": "^3.4.6", 35 | "typescript": "^5.4.2", 36 | "vite": "^6", 37 | "vite-plugin-checker": "^0.8", 38 | "vitest": "^2" 39 | }, 40 | "dependencies": { 41 | "@seqflow/components": "workspace:*", 42 | "@seqflow/seqflow": "workspace:*" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/e-commerce/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /examples/e-commerce/public/images/categories/electronics.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/examples/e-commerce/public/images/categories/electronics.webp -------------------------------------------------------------------------------- /examples/e-commerce/public/images/categories/jewelery.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/examples/e-commerce/public/images/categories/jewelery.webp -------------------------------------------------------------------------------- /examples/e-commerce/public/images/categories/men's clothing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/examples/e-commerce/public/images/categories/men's clothing.webp -------------------------------------------------------------------------------- /examples/e-commerce/public/images/categories/women's clothing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/examples/e-commerce/public/images/categories/women's clothing.webp -------------------------------------------------------------------------------- /examples/e-commerce/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import type { CartDomain } from "./domains/cart"; 3 | import type { ProductDomain } from "./domains/product"; 4 | import type { UserDomain } from "./domains/user"; 5 | import { Router } from "./router"; 6 | 7 | export async function Main( 8 | _: ComponentProps, 9 | { component, app }: Contexts, 10 | ) { 11 | await app.domains.user.restoreUser(); 12 | 13 | component.renderSync(); 14 | } 15 | 16 | declare module "@seqflow/seqflow" { 17 | interface Domains { 18 | user: UserDomain; 19 | cart: CartDomain; 20 | product: ProductDomain; 21 | } 22 | 23 | interface ApplicationConfiguration { 24 | api: { 25 | baseUrl: string; 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/e-commerce/src/components/CardList.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4; 3 | /* 4 | grid-template-columns: 1fr 1fr 1fr 1fr; 5 | grid-auto-rows: 1fr; 6 | gap: 5px; 7 | */ 8 | 9 | list-style: none; 10 | 11 | /* 12 | margin-block: 0px; 13 | padding-inline: 0px; 14 | margin-left: 20px; 15 | margin-right: 20px; 16 | */ 17 | 18 | .element { 19 | > div { 20 | height: 100%; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/components/CardList.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts, SeqFlowComponent } from "@seqflow/seqflow"; 2 | import classes from "./CardList.module.css"; 3 | 4 | export async function CardList( 5 | data: ComponentProps<{ 6 | prefix: string; 7 | items: T[]; 8 | Component: SeqFlowComponent; 9 | }>, 10 | { component }: Contexts, 11 | ) { 12 | component.renderSync( 13 |
    14 | {data.items.map((item) => ( 15 |
  1. 20 | 21 |
  2. 22 | ))} 23 |
, 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/e-commerce/src/components/Header.module.css: -------------------------------------------------------------------------------- 1 | 2 | .header { 3 | background-color: #7d7d7d40; 4 | border-bottom: 1px solid #02020214; 5 | 6 | .icon { 7 | width: 100px; 8 | 9 | } 10 | } 11 | 12 | .logged { 13 | .signInButton { 14 | display: none; 15 | } 16 | } 17 | .unlogged { 18 | .userProfileBadge { 19 | display: none; 20 | } 21 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/components/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/examples/e-commerce/src/components/icon.png -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/components/AddToCart.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .firstAddToCart { 4 | display: none; 5 | height: 2rem; 6 | min-height: 2rem; 7 | padding-left: 0.5rem; 8 | padding-right: 0.5rem; 9 | } 10 | .firstAddToCart.show { 11 | display: unset; 12 | } 13 | 14 | .otherAddToCartWrapper { 15 | display: none; 16 | align-items: center; 17 | 18 | button { 19 | height: 2rem; 20 | min-height: 2rem; 21 | width: 2rem; 22 | } 23 | 24 | span { 25 | flex-grow: 1; 26 | text-align: center; 27 | } 28 | } 29 | .otherAddToCartWrapper.show { 30 | display: flex; 31 | justify-content: center; 32 | } 33 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/components/CartBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Button } from "@seqflow/components"; 2 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | import { ChangeCartEvent, CheckoutEndedCartEvent } from "../events"; 4 | import classes from "./cart-badge.module.css"; 5 | 6 | export async function CartBadge( 7 | _: ComponentProps, 8 | { component, app }: Contexts, 9 | ) { 10 | const productCount = app.domains.cart.getProductCount(); 11 | const count = productCount === 0 ? "" : productCount; 12 | 13 | component.renderSync( 14 | , 31 | ); 32 | 33 | const counter = component.getChild("counter") as HTMLSpanElement; 34 | const events = component.waitEvents( 35 | component.domainEvent(ChangeCartEvent), 36 | component.domainEvent(CheckoutEndedCartEvent), 37 | component.domEvent(component._el, "click", { 38 | preventDefault: true, 39 | }), 40 | ); 41 | for await (const ev of events) { 42 | if (ev instanceof ChangeCartEvent || ev instanceof CheckoutEndedCartEvent) { 43 | const productCount = app.domains.cart.getProductCount(); 44 | const count = productCount === 0 ? "" : productCount; 45 | counter.textContent = `${count}`; 46 | } else { 47 | // click event 48 | app.router.navigate("/cart"); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/components/CartProductList.module.css: -------------------------------------------------------------------------------- 1 | 2 | .cartProducts { 3 | list-style: none; 4 | margin: 0px; 5 | padding: 0px; 6 | display: flex; 7 | flex-direction: column; 8 | gap: 10px; 9 | } 10 | 11 | .product { 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | gap: 10px; 16 | 17 | } 18 | 19 | .left { 20 | text-align: center; 21 | } 22 | .productImage { 23 | aspect-ratio: 1; 24 | height: 100px; 25 | } 26 | .productTitle { 27 | flex: 1; 28 | } 29 | 30 | .cartCheckout { 31 | text-align: right; 32 | } 33 | .cartTotal { 34 | text-align: right; 35 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/components/CartTooltip.module.css: -------------------------------------------------------------------------------- 1 | 2 | .wrapper { 3 | z-index: 10; 4 | display: none; 5 | position: absolute; 6 | bottom: 10px; 7 | right: 10px; 8 | 9 | .cartTooltipLink { 10 | display: inline-block 11 | } 12 | } 13 | .wrapper.show { 14 | display: inline; 15 | } 16 | /* 17 | .cartTooltipLink { 18 | display: inline-block; 19 | padding: 30px; 20 | } 21 | */ -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/components/cart-badge.module.css: -------------------------------------------------------------------------------- 1 | 2 | .icon { 3 | font-size: 20px; 4 | } 5 | 6 | .numberOfProductsInCart { 7 | position: relative; 8 | font-size: 20px; 9 | } 10 | 11 | .cartProductCounter { 12 | background-color: red; 13 | border-color: transparent; 14 | color: white; 15 | padding: 1px 3px; 16 | text-align: center; 17 | border-radius: 5px; 18 | position: absolute; 19 | top: -3px; 20 | font-size: 10px; 21 | right: 4px; 22 | } 23 | 24 | .cartProductCounter:empty { 25 | display: none; 26 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/events.ts: -------------------------------------------------------------------------------- 1 | import { createDomainEventClass } from "@seqflow/seqflow"; 2 | import type { Product } from "../product"; 3 | 4 | export const ChangeCartEvent = createDomainEventClass< 5 | { 6 | product: Product; 7 | action: 8 | | "add-product" 9 | | "remove-element-product" 10 | | "remove-all-elements-of-a-product"; 11 | }, 12 | "change-cart" 13 | >("cart", "change-cart"); 14 | export const CheckoutEndedCartEvent = createDomainEventClass( 15 | "cart", 16 | "checkout-end", 17 | ); 18 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/cart/index.ts: -------------------------------------------------------------------------------- 1 | import { AddToCart } from "./components/AddToCart"; 2 | import { CartBadge } from "./components/CartBadge"; 3 | import { CartProductList } from "./components/CartProductList"; 4 | import { CartTooltip } from "./components/CartTooltip"; 5 | 6 | export * from "./CartDomain"; 7 | export * from "./events"; 8 | 9 | const components = { 10 | AddToCart, 11 | CartBadge, 12 | CartProductList, 13 | CartTooltip, 14 | }; 15 | 16 | export { components }; 17 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/product/ProductDomain.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationConfiguration } from "@seqflow/seqflow"; 2 | 3 | export class ProductDomain { 4 | constructor(private applicationConfig: Readonly) {} 5 | 6 | async fetchProductsCategories( 7 | signal: AbortSignal, 8 | ): Promise { 9 | const res = await fetch( 10 | `${this.applicationConfig.api.baseUrl}/products/categories`, 11 | { 12 | signal, 13 | }, 14 | ); 15 | const categories = (await res.json()) as string[]; 16 | 17 | return categories.map((name, id) => { 18 | const url = `/images/categories/${name}.webp`; 19 | return { id: `${id}`, name, image: { url } }; 20 | }); 21 | } 22 | 23 | async fetchProductsByCategory( 24 | { categoryId }: { categoryId: string }, 25 | signal: AbortSignal, 26 | ): Promise { 27 | const res = await fetch( 28 | `${this.applicationConfig.api.baseUrl}/products/category/${categoryId}`, 29 | { signal }, 30 | ); 31 | return (await res.json()) as Product[]; 32 | } 33 | } 34 | 35 | export interface ProductCategory { 36 | id: string; 37 | name: string; 38 | image: { 39 | url: string; 40 | }; 41 | } 42 | 43 | export interface Product { 44 | id: string; 45 | title: string; 46 | price: number; 47 | description: string; 48 | image: string; 49 | } 50 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/product/components/ProductCategoryList.module.css: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | .productList { 4 | margin-top: 30px; 5 | } 6 | 7 | .categoryAnchor { 8 | display: inline-block; 9 | padding: 5px; 10 | text-decoration: none; 11 | color: #242424; 12 | text-transform: capitalize; 13 | } 14 | 15 | .categoryImage { 16 | border-radius: 3px 4px 0px 0px; 17 | max-width: 100%; 18 | height: auto; 19 | } 20 | */ 21 | 22 | .card:hover:before { 23 | background-color: transparent !important; 24 | } 25 | 26 | .card:hover .body { 27 | --tw-bg-opacity: 0.8; 28 | } 29 | 30 | .card .body { 31 | position: absolute !important; 32 | bottom: 0px; 33 | width: 100%; 34 | padding: 0px; 35 | flex-direction: row; 36 | height: 60px; 37 | align-items: center; 38 | background-color: var(--fallback-n, oklch(var(--n) / var(--tw-bg-opacity))); 39 | border-bottom-left-radius: var(--rounded-box, 1rem); 40 | border-bottom-right-radius: var(--rounded-box, 1rem); 41 | 42 | h2 { 43 | display: inline; 44 | flex-grow: 1; 45 | margin: 0px; 46 | text-align: center; 47 | } 48 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/product/components/ProductCategoryList.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@seqflow/components"; 2 | import type { Contexts } from "@seqflow/seqflow"; 3 | import { CardList } from "../../../components/CardList"; 4 | import type { ProductCategory } from "../ProductDomain"; 5 | import classes from "./ProductCategoryList.module.css"; 6 | 7 | async function CategoryItem( 8 | data: ProductCategory, 9 | { component, app }: Contexts, 10 | ) { 11 | component.renderSync( 12 | 13 | 14 |
15 | {data.name} 20 |
21 | 22 | 23 | {data.name} 24 | 25 | 26 |
27 |
, 28 | ); 29 | 30 | const events = component.waitEvents( 31 | component.domEvent(component._el, "click", { 32 | preventDefault: true, 33 | }), 34 | ); 35 | for await (const ev of events) { 36 | app.router.navigate(`/category/${data.name}`); 37 | } 38 | } 39 | 40 | export async function ProductCategoryList( 41 | data: { categories: ProductCategory[] }, 42 | { component }: Contexts, 43 | ) { 44 | component.renderSync( 45 |
46 | 51 |
, 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/product/components/ProductItem.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .productImage { 4 | height: 100px !important; 5 | aspect-ratio: auto; 6 | } 7 | 8 | h2 { 9 | display: inline !important; 10 | white-space: nowrap; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | } 14 | 15 | 16 | .tooltipWrapper { 17 | opacity: 0; 18 | display: none; 19 | position: absolute; 20 | --tw-bg-opacity: 1; 21 | background-color: var(--fallback-n, oklch(var(--n) / var(--tw-bg-opacity))); 22 | border: 1px solid #000; 23 | border-radius: 5px; 24 | padding: 5px; 25 | z-index: 1; 26 | top: 100%; 27 | left: -7.5px; 28 | width: calc(100% + 20px); 29 | max-width: 200px; 30 | text-align: center; 31 | left: 0px; 32 | transition: opacity 0.3s; 33 | box-sizing: border-box; 34 | } 35 | .tooltipWrapper.show { 36 | opacity: 1; 37 | display: block; 38 | } 39 | 40 | /* 41 | .wrapper { 42 | padding: 5px; 43 | position: relative; 44 | } 45 | 46 | .productImage { 47 | height: 100px; 48 | aspect-ratio: auto; 49 | } 50 | 51 | .price { 52 | margin: 0px; 53 | } 54 | 55 | */ -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/product/components/ProductItem.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@seqflow/components"; 2 | import { Contexts } from "@seqflow/seqflow"; 3 | import { AddToCart } from "../../cart/components/AddToCart"; 4 | import type { Product } from "../ProductDomain"; 5 | import classes from "./ProductItem.module.css"; 6 | 7 | export async function ProductItem(data: Product, { component }: Contexts) { 8 | const tooltip = ( 9 |
{data.title}
10 | ) as HTMLDivElement; 11 | component.renderSync( 12 | 13 |
14 | {data.title} 19 |
20 | 21 | {data.title} 22 | {tooltip} 23 | 24 | 25 | 26 | 27 |
, 28 | ); 29 | 30 | const events = component.waitEvents( 31 | component.domEvent(component._el, "mouseover"), 32 | component.domEvent(component._el, "mouseout"), 33 | ); 34 | for await (const e of events) { 35 | if (e.type === "mouseover") { 36 | tooltip.classList.add(classes.show); 37 | } else { 38 | tooltip.classList.remove(classes.show); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/product/index.ts: -------------------------------------------------------------------------------- 1 | import { ProductCategoryList } from "./components/ProductCategoryList"; 2 | import { ProductItem } from "./components/ProductItem"; 3 | 4 | export * from "./ProductDomain"; 5 | 6 | const components = { 7 | ProductCategoryList, 8 | ProductItem, 9 | }; 10 | 11 | export { components }; 12 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/user/UserDomain.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationConfiguration } from "@seqflow/seqflow"; 2 | import { UserLoggedEvent, UserLoggedOutEvent } from "./events"; 3 | 4 | const LOCALSTORAGE_USER_KEY = "user"; 5 | 6 | export interface UserType { 7 | id: number; 8 | email: string; 9 | username: string; 10 | password: string; 11 | name: { 12 | firstname: string; 13 | lastname: string; 14 | }; 15 | phone: string; 16 | address: { 17 | geolocation: { 18 | lat: string; 19 | long: string; 20 | }; 21 | city: string; 22 | street: string; 23 | number: number; 24 | zipcode: string; 25 | }; 26 | } 27 | 28 | export class UserDomain { 29 | private user?: UserType; 30 | 31 | constructor( 32 | private eventTarget: EventTarget, 33 | private applicationConfig: Readonly, 34 | ) {} 35 | 36 | async restoreUser(): Promise { 37 | const str = localStorage.getItem(LOCALSTORAGE_USER_KEY); 38 | if (!str) { 39 | return undefined; 40 | } 41 | this.user = JSON.parse(str) as UserType; 42 | return this.user; 43 | } 44 | 45 | isLoggedIn() { 46 | return this.user !== undefined; 47 | } 48 | 49 | async login({ 50 | username, 51 | }: { username: string }): Promise { 52 | const r = await fetch(`${this.applicationConfig.api.baseUrl}/users`); 53 | const users = (await r.json()) as UserType[]; 54 | this.user = users.find((u) => u.username === username); 55 | if (this.user) { 56 | localStorage.setItem(LOCALSTORAGE_USER_KEY, JSON.stringify(this.user)); 57 | this.eventTarget.dispatchEvent(new UserLoggedEvent(this.user)); 58 | } 59 | 60 | return this.user; 61 | } 62 | 63 | async logout() { 64 | this.user = undefined; 65 | localStorage.removeItem(LOCALSTORAGE_USER_KEY); 66 | 67 | this.eventTarget.dispatchEvent(new UserLoggedOutEvent(null)); 68 | } 69 | 70 | async getUser(): Promise { 71 | return this.user; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/user/components/user-profile-badge.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .logo { 4 | border-radius: 50%; 5 | } 6 | 7 | .logoWrapper { 8 | display: block; 9 | height: 100%; 10 | } 11 | 12 | .profile-header-menu-wrapper { 13 | position: relative; 14 | right: 50px; 15 | } 16 | #profile-header-menu { 17 | display: none; 18 | background-color: #7d7d7d; 19 | width: 150px; 20 | 21 | padding-top: 5px; 22 | padding-bottom: 5px; 23 | 24 | ol { 25 | color: oklch(var(--bc)); 26 | list-style: none; 27 | margin-block: 0; 28 | padding-inline: 0; 29 | } 30 | } 31 | #profile-header-menu.show { 32 | position: absolute; 33 | display: unset; 34 | left: -40px; 35 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/user/events.ts: -------------------------------------------------------------------------------- 1 | import { createDomainEventClass } from "@seqflow/seqflow"; 2 | import type { UserType } from "./UserDomain"; 3 | 4 | export const UserLoggedEvent = createDomainEventClass( 5 | "user", 6 | "userLoggedIn", 7 | ); 8 | export type UserLoggedEvent = InstanceType; 9 | 10 | export const UserLoggedOutEvent = createDomainEventClass( 11 | "user", 12 | "userLoggedOut", 13 | ); 14 | export type UserLoggedOutEvent = InstanceType; 15 | -------------------------------------------------------------------------------- /examples/e-commerce/src/domains/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./UserDomain"; 2 | export * from "./events"; 3 | import { UserProfileBadge } from "./components/UserProfileBadge"; 4 | 5 | const components = { UserProfileBadge }; 6 | 7 | export { components }; 8 | -------------------------------------------------------------------------------- /examples/e-commerce/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | :root { 7 | /* background color */ 8 | --b1: 0.3 0 0; 9 | /* text color */ 10 | --bc: 0.91 0 0; 11 | 12 | /* primary color - button color */ 13 | --p: 0.6 0.22 257.22; 14 | --pc: 1 0 0; 15 | /* secondary color - button color */ 16 | --s: 0.64 0.18 146.74; 17 | --sc: 1 0 0; 18 | } 19 | -------------------------------------------------------------------------------- /examples/e-commerce/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Vite Build 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /examples/e-commerce/src/index.ts: -------------------------------------------------------------------------------- 1 | import "@seqflow/components/style.css"; 2 | import { ComponentResult, Contexts, KeyPair, start } from "@seqflow/seqflow"; 3 | import { Main } from "./Main"; 4 | import { CartDomain } from "./domains/cart"; 5 | import { ProductDomain } from "./domains/product"; 6 | import { UserDomain } from "./domains/user"; 7 | import "./index.css"; 8 | 9 | start( 10 | document.getElementById("root")!, 11 | Main, 12 | {}, 13 | { 14 | log: console, 15 | domains: { 16 | user: (eventTarget, _, config) => { 17 | return new UserDomain(eventTarget, config); 18 | }, 19 | cart: (eventTarget) => { 20 | return new CartDomain(eventTarget); 21 | }, 22 | product: (eventTarget, _, config) => { 23 | return new ProductDomain(config); 24 | }, 25 | }, 26 | config: { 27 | api: { 28 | baseUrl: "https://fakestoreapi.com", 29 | }, 30 | }, 31 | }, 32 | ); 33 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/cart.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { components } from "../domains/cart"; 3 | 4 | export async function Cart( 5 | _: ComponentProps, 6 | { component, app }: Contexts, 7 | ) { 8 | const cart = app.domains.cart.getCart(); 9 | component._el.classList.add("w-3/5"); 10 | component.renderSync(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/category.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { CardList } from "../components/CardList"; 3 | import { components } from "../domains/product"; 4 | 5 | async function Loading(_: ComponentProps, { component }: Contexts) { 6 | component.renderSync(
Loading...
); 7 | } 8 | 9 | export async function Category( 10 | _: ComponentProps, 11 | { component, app }: Contexts, 12 | ) { 13 | component.renderSync(); 14 | 15 | const categoryId = app.router.segments.pop(); 16 | 17 | if (!categoryId) { 18 | app.router.navigate("/"); 19 | return; 20 | } 21 | 22 | const products = await app.domains.product.fetchProductsByCategory( 23 | { categoryId }, 24 | component.ac.signal, 25 | ); 26 | 27 | component.renderSync( 28 | , 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/checkout.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export async function Checkout( 4 | _: ComponentProps, 5 | { component, app }: Contexts, 6 | ) { 7 | app.domains.cart.checkout(); 8 | 9 | component.renderSync( 10 | <> 11 |

Well done!

12 | 13 | Go home 14 | 15 | , 16 | ); 17 | 18 | const events = component.waitEvents(component.domEvent("go-home", "click")); 19 | for await (const ev of events) { 20 | ev.preventDefault(); 21 | app.router.navigate("/"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { components } from "../domains/product"; 3 | 4 | async function Loading(_: ComponentProps, { component }: Contexts) { 5 | component.renderSync(
Loading...
); 6 | } 7 | 8 | export async function Home( 9 | _: ComponentProps, 10 | { component, app }: Contexts, 11 | ) { 12 | component.renderSync(); 13 | 14 | const categories = await app.domains.product.fetchProductsCategories( 15 | component.ac.signal, 16 | ); 17 | 18 | component.renderSync( 19 | , 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Card, 4 | Form, 5 | type FormComponent, 6 | FormField, 7 | TextInput, 8 | type TextInputComponent, 9 | } from "@seqflow/components"; 10 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 11 | import type { UserType } from "../domains/user"; 12 | 13 | export async function Login( 14 | _: ComponentProps, 15 | { component, app }: Contexts, 16 | ) { 17 | component.renderSync( 18 |
19 | 20 | 21 | 22 | 30 | 31 | 34 | 35 | 36 |
, 37 | ); 38 | 39 | const usernameInput = component.getChild("username"); 40 | const form = component.getChild("login-form"); 41 | const events = component.waitEvents( 42 | component.domEvent("login-form", "submit", { preventDefault: true }), 43 | ); 44 | let user: UserType | undefined; 45 | for await (const _ of events) { 46 | const username = usernameInput.value; 47 | const user = await form.runAsync(async () => { 48 | return await app.domains.user.login({ username }); 49 | }); 50 | 51 | if (!user) { 52 | usernameInput.setError("User not found. Try 'johnd'"); 53 | continue; 54 | } 55 | 56 | break; 57 | } 58 | 59 | app.log.info({ 60 | message: "User logged in", 61 | data: { user }, 62 | }); 63 | 64 | app.router.navigate("/"); 65 | } 66 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/logout.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export async function Logout( 4 | _: ComponentProps, 5 | { component, app }: Contexts, 6 | ) { 7 | // blank 8 | component.renderSync(""); 9 | await app.domains.user.logout(); 10 | app.router.navigate("/"); 11 | } 12 | -------------------------------------------------------------------------------- /examples/e-commerce/src/pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@seqflow/components"; 2 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | 4 | export async function Profile( 5 | _: ComponentProps, 6 | { component, app }: Contexts, 7 | ) { 8 | const user = await app.domains.user.getUser(); 9 | if (!user) { 10 | app.router.navigate("/login"); 11 | return; 12 | } 13 | 14 | component.renderSync( 15 | 16 | 17 | Profile 18 |
19 |
Username
20 |
{user.username}
21 |
Email
22 |
{user.email}
23 |
24 |
25 |
, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/e-commerce/src/router.module.css: -------------------------------------------------------------------------------- 1 | 2 | #app { 3 | display: flex; 4 | flex-direction: column; 5 | overflow: hidden; 6 | height: 100vh; 7 | 8 | .main { 9 | flex: 1; 10 | display: flex; 11 | overflow: auto; 12 | padding-bottom: 40px; 13 | margin: auto; 14 | @apply mt-6; 15 | 16 | > div { 17 | width: 100%; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /examples/e-commerce/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /examples/e-commerce/src/types.ts: -------------------------------------------------------------------------------- 1 | import "./types.d"; 2 | -------------------------------------------------------------------------------- /examples/e-commerce/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/domains/counter/components/*.{js,jsx,ts,tsx}', 5 | './src/**/*.{js,jsx,ts,tsx}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require("@tailwindcss/typography"), 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /examples/e-commerce/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/e-commerce/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | 3 | // import "../src/index.css"; 4 | // import "../tailwind.css"; 5 | -------------------------------------------------------------------------------- /examples/e-commerce/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | }, 18 | "include": [ 19 | "./src/**/*", 20 | "./tests/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /examples/e-commerce/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import checker from 'vite-plugin-checker' 3 | 4 | export default defineConfig({ 5 | root: "src", 6 | build: { 7 | outDir: "../dist", 8 | }, 9 | publicDir: "../public", 10 | plugins: [ 11 | checker({ 12 | typescript: true, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /examples/e-commerce/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | css: true, 6 | environment: 'jsdom', 7 | setupFiles: './tests/setupTests.ts', 8 | }, 9 | }) -------------------------------------------------------------------------------- /examples/empty/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | }, 8 | "correctness": { 9 | "noEmptyPattern": "off" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/empty/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empty", 3 | "version": "1.0.0", 4 | "description": "Empty example", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm-run-all biome unit build", 9 | "unit": "vitest --run", 10 | "biome": "biome ci ./src", 11 | "biome:check": "biome check --apply src tests", 12 | "start": "vite -c vite.config.js", 13 | "build": "vite build --emptyOutDir -c vite.config.js", 14 | "depcheck": "depcheck --ignores @types/css-modules,autoprefixer,typescript .", 15 | "serve:static": "serve dist" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@biomejs/biome": "1.5.3", 22 | "@tailwindcss/typography": "^0.5.13", 23 | "@testing-library/dom": "^9.3.4", 24 | "@testing-library/jest-dom": "^6.5.0", 25 | "@types/css-modules": "^1.0.5", 26 | "autoprefixer": "^10.4.2", 27 | "daisyui": "^4.12.10", 28 | "depcheck": "^1.4.7", 29 | "jsdom": "^25.0.0", 30 | "npm-run-all": "^4.1.5", 31 | "serve": "^14.2.3", 32 | "tailwindcss": "^3.4.6", 33 | "typescript": "^5.6.3", 34 | "vite": "^6", 35 | "vite-plugin-checker": "^0.8", 36 | "vitest": "^2" 37 | }, 38 | "dependencies": { 39 | "@seqflow/components": "workspace:*", 40 | "@seqflow/seqflow": "workspace:*" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/empty/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /examples/empty/src/Main.module.css: -------------------------------------------------------------------------------- 1 | 2 | .card-wrapper { 3 | @apply m-auto 4 | } 5 | -------------------------------------------------------------------------------- /examples/empty/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from "@seqflow/components"; 2 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | import classes from "./Main.module.css"; 4 | 5 | export async function Main( 6 | _: ComponentProps, 7 | { component }: Contexts, 8 | ) { 9 | component.renderSync( 10 | 11 | 12 | Empty example 13 |

This is the empty example

14 |
15 |
, 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/empty/src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | -------------------------------------------------------------------------------- /examples/empty/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SeqFlow JS - Empty example 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /examples/empty/src/index.ts: -------------------------------------------------------------------------------- 1 | import "@seqflow/components/style.css"; 2 | import { start } from "@seqflow/seqflow"; 3 | import { Main } from "./Main"; 4 | import "./index.css"; 5 | 6 | start( 7 | document.getElementById("root")!, 8 | Main, 9 | {}, 10 | { 11 | log: console, 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /examples/empty/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/**/*.{js,jsx,ts,tsx,css}', 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [ 10 | require("@tailwindcss/typography"), 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /examples/empty/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/empty/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { start } from "@seqflow/seqflow"; 2 | import { screen } from "@testing-library/dom"; 3 | import { expect, test } from "vitest"; 4 | import { Main } from "../src/Main"; 5 | 6 | test("should increment and decrement the counter", async () => { 7 | start(document.body, Main, {}, {}); 8 | 9 | const el = await screen.findAllByText(/empty/i); 10 | 11 | expect(el.length).greaterThan(0); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/empty/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | 3 | import "../src/index.css"; 4 | import "../tailwind.css"; 5 | -------------------------------------------------------------------------------- /examples/empty/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | }, 18 | "include": [ 19 | "./src/**/*", 20 | "./tests/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /examples/empty/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import checker from 'vite-plugin-checker' 3 | 4 | export default defineConfig({ 5 | root: "src", 6 | build: { 7 | outDir: "../dist", 8 | }, 9 | plugins: [ 10 | checker({ 11 | typescript: true, 12 | }) 13 | ] 14 | }); 15 | -------------------------------------------------------------------------------- /examples/empty/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | setupFiles: './tests/setupTests.ts', 7 | }, 8 | }) -------------------------------------------------------------------------------- /examples/random-quote/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | }, 8 | "correctness": { 9 | "noEmptyPattern": "off" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/random-quote/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "random-quote", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm-run-all biome unit build", 9 | "unit": "vitest --run", 10 | "biome": "biome ci ./src", 11 | "biome:check": "biome check --apply src tests", 12 | "start": "vite -c vite.config.js", 13 | "build": "vite build --emptyOutDir -c vite.config.js", 14 | "serve:static": "serve dist" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@biomejs/biome": "1.5.3", 21 | "@tailwindcss/typography": "^0.5.13", 22 | "@testing-library/dom": "^10", 23 | "@testing-library/jest-dom": "^6.5.0", 24 | "@types/css-modules": "^1.0.5", 25 | "autoprefixer": "^10.4.2", 26 | "daisyui": "^4.12.10", 27 | "msw": "^2.2.1", 28 | "jsdom": "^25", 29 | "npm-run-all": "^4.1.5", 30 | "serve": "^14.2.3", 31 | "tailwindcss": "^3.4.6", 32 | "typescript": "^5.6.3", 33 | "vite": "^6", 34 | "vite-plugin-checker": "^0.8", 35 | "vitest": "^2" 36 | }, 37 | "dependencies": { 38 | "@seqflow/components": "workspace:*", 39 | "@seqflow/seqflow": "workspace:*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/random-quote/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /examples/random-quote/src/Main.module.css: -------------------------------------------------------------------------------- 1 | 2 | .main { 3 | @apply max-w-lg m-auto mt-10 flex flex-col items-end; 4 | 5 | .quote { 6 | @apply size-full; 7 | } 8 | } -------------------------------------------------------------------------------- /examples/random-quote/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | import classes from "./Main.module.css"; 3 | import { 4 | FetchingNewQuote, 5 | NewQuoteFetched, 6 | QuoteComponent, 7 | QuoteErrorFetched, 8 | RefreshQuoteButton, 9 | } from "./domains/quote"; 10 | 11 | function Loading({}, { component }: Contexts) { 12 | component.renderSync(

Loading...

); 13 | } 14 | function ErrorMessage(data: { error: unknown }, { component }: Contexts) { 15 | if (data.error instanceof Error) { 16 | component.renderSync(

{data.error.message}

); 17 | } else { 18 | component.renderSync(

Unknown error

); 19 | } 20 | } 21 | 22 | function Spot() {} 23 | 24 | export async function Main({}, { component }: Contexts) { 25 | component._el.classList.add(...[classes.main]); 26 | component.renderSync( 27 | <> 28 | 29 | 30 | , 31 | ); 32 | 33 | const events = component.waitEvents( 34 | component.domainEvent(FetchingNewQuote), 35 | component.domainEvent(NewQuoteFetched), 36 | component.domainEvent(QuoteErrorFetched), 37 | ); 38 | for await (const ev of events) { 39 | switch (true) { 40 | case ev instanceof FetchingNewQuote: { 41 | component.replaceChild("quote", () => ( 42 | 43 | )); 44 | break; 45 | } 46 | case ev instanceof NewQuoteFetched: { 47 | component.replaceChild("quote", () => ( 48 | 53 | )); 54 | break; 55 | } 56 | case ev instanceof QuoteErrorFetched: { 57 | component.replaceChild("quote", () => ( 58 | 59 | )); 60 | break; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/random-quote/src/domains/quote/QuoteDomain.ts: -------------------------------------------------------------------------------- 1 | import { createDomainEventClass } from "@seqflow/seqflow"; 2 | 3 | export interface Quote { 4 | content: string; 5 | author: string; 6 | } 7 | 8 | export const FetchingNewQuote = createDomainEventClass< 9 | unknown, 10 | "fetching-new-quote" 11 | >("quotes", "fetching-new-quote"); 12 | export const NewQuoteFetched = createDomainEventClass< 13 | Quote, 14 | "new-quote-fetched" 15 | >("quotes", "new-quote-fetched"); 16 | export const QuoteErrorFetched = createDomainEventClass< 17 | string, 18 | "quote-error-fetched" 19 | >("quotes", "quote-error-fetched"); 20 | 21 | export class QuoteDomain { 22 | constructor( 23 | private et: EventTarget, 24 | private baseUrl: string, 25 | ) {} 26 | 27 | async fetchNewQuote() { 28 | this.et.dispatchEvent(new FetchingNewQuote(null)); 29 | 30 | let quote: Quote; 31 | try { 32 | const ret = await Promise.all([ 33 | getRandomQuote(this.baseUrl), 34 | // Simulate a delay 35 | new Promise((r) => setTimeout(r, 500)), 36 | ]); 37 | quote = ret[0]; 38 | 39 | this.et.dispatchEvent(new NewQuoteFetched(quote)); 40 | } catch (e) { 41 | this.et.dispatchEvent(new QuoteErrorFetched("Failed to fetch quote")); 42 | } 43 | } 44 | } 45 | 46 | async function getRandomQuote(baseUrl: string): Promise { 47 | const res = await fetch(`${baseUrl}/api/quotes/random`); 48 | 49 | if (!res.ok) { 50 | throw new Error("Failed to fetch quote"); 51 | } 52 | 53 | const body = await res.json(); 54 | 55 | // TODO: validate the quote in a more robust way... 56 | if (!body.content || !body.author) { 57 | throw new Error("Invalid quote"); 58 | } 59 | 60 | return { 61 | content: body.content, 62 | author: body.author, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /examples/random-quote/src/domains/quote/components/Quote.module.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .wrapper { 4 | @apply flex flex-col justify-around !max-w-full; 5 | 6 | .quote { 7 | @apply mb-0; 8 | } 9 | } 10 | .author { 11 | @apply self-end; 12 | } -------------------------------------------------------------------------------- /examples/random-quote/src/domains/quote/components/Quote.tsx: -------------------------------------------------------------------------------- 1 | import { Prose } from "@seqflow/components"; 2 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | import type { Quote } from "../QuoteDomain"; 4 | import classes from "./Quote.module.css"; 5 | 6 | export async function QuoteComponent( 7 | data: ComponentProps<{ quote: Quote }>, 8 | { component }: Contexts, 9 | ) { 10 | component.renderSync( 11 | 12 |
13 |

{data.quote.content}

14 |
15 | {data.quote.author} 16 |
, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/random-quote/src/domains/quote/components/RefreshQuoteButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, type ButtonComponent } from "@seqflow/components"; 2 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | 4 | export async function RefreshQuoteButton( 5 | _: ComponentProps, 6 | { component, app }: Contexts, 7 | ) { 8 | const f = async () => { 9 | const refreshButton = component.getChild("button"); 10 | refreshButton.transition({ 11 | disabled: true, 12 | loading: true, 13 | loadingText: "Fetching...", 14 | }); 15 | 16 | await app.domains.quotes.fetchNewQuote(); 17 | 18 | refreshButton.transition({ 19 | loading: false, 20 | disabled: false, 21 | }); 22 | }; 23 | 24 | component.renderSync( 25 | , 28 | ); 29 | 30 | await f(); 31 | 32 | const events = component.waitEvents(component.domEvent("button", "click")); 33 | for await (const _ of events) { 34 | await f(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/random-quote/src/domains/quote/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./QuoteDomain"; 2 | export * from "./components/Quote"; 3 | export * from "./components/RefreshQuoteButton"; 4 | -------------------------------------------------------------------------------- /examples/random-quote/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | :root { 7 | /* background color */ 8 | --b1: 0.3 0 0; 9 | /* text color */ 10 | --bc: 0.91 0 0; 11 | 12 | /* primary color - button color */ 13 | --p: 0.6 0.22 257.22; 14 | --pc: 1 0 0; 15 | /* secondary color - button color */ 16 | --s: 0.64 0.18 146.74; 17 | --sc: 1 0 0; 18 | } 19 | -------------------------------------------------------------------------------- /examples/random-quote/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SeqFlow example 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /examples/random-quote/src/index.ts: -------------------------------------------------------------------------------- 1 | import "@seqflow/components/style.css"; 2 | import { debugEventTarget, start } from "@seqflow/seqflow"; 3 | import { Main } from "./Main"; 4 | import "./index.css"; 5 | 6 | import { QuoteDomain } from "./domains/quote"; 7 | 8 | start( 9 | document.getElementById("root")!, 10 | Main, 11 | {}, 12 | { 13 | log: console, 14 | config: { 15 | api: { 16 | baseUrl: "https://quotes.seqflow.dev", 17 | }, 18 | }, 19 | domains: { 20 | quotes: (et, _, config) => 21 | new QuoteDomain(debugEventTarget(et), config.api.baseUrl), 22 | }, 23 | }, 24 | ); 25 | 26 | declare module "@seqflow/seqflow" { 27 | interface ApplicationConfiguration { 28 | api: { 29 | baseUrl: string; 30 | }; 31 | } 32 | interface Domains { 33 | quotes: QuoteDomain; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/random-quote/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/**/*.{js,jsx,ts,tsx,css}', 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [ 10 | require("@tailwindcss/typography"), 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /examples/random-quote/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/random-quote/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { screen } from "@testing-library/dom"; 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | import { afterAll, afterEach, beforeAll, test } from "vitest"; 6 | 7 | import { start } from "@seqflow/seqflow"; 8 | import { Main } from "../src/Main"; 9 | import { QuoteDomain } from "../src/domains/quote"; 10 | 11 | const quotes = [ 12 | { content: "quote 1", author: "Author 1" }, 13 | { content: "quote 2", author: "Author 2" }, 14 | ]; 15 | 16 | let index = 0; 17 | const server = setupServer( 18 | http.get("/api/quotes/random", () => { 19 | return HttpResponse.json(quotes[index++ % quotes.length]); 20 | }), 21 | ); 22 | 23 | beforeAll(() => server.listen()); 24 | afterEach(() => server.resetHandlers()); 25 | afterAll(() => server.close()); 26 | 27 | test("should render the quote and refresh it", async () => { 28 | start( 29 | document.body, 30 | Main, 31 | {}, 32 | { 33 | config: { 34 | api: { 35 | // Route to the mock server 36 | baseUrl: "", 37 | }, 38 | }, 39 | domains: { 40 | quotes: (et, _, config) => new QuoteDomain(et, config.api.baseUrl), 41 | }, 42 | }, 43 | ); 44 | 45 | // When landed, the user should see the first quote 46 | await screen.findByText(new RegExp(quotes[0].content, "i")); 47 | await screen.findByText(new RegExp(quotes[0].author, "i")); 48 | 49 | // When the user clicks the refresh button, the quote should change 50 | const button = await screen.findByText("Refresh quote"); 51 | button.click(); 52 | 53 | // And the second quote should be displayed 54 | await screen.findByText(new RegExp(quotes[1].content, "i")); 55 | await screen.findByText(new RegExp(quotes[1].author, "i")); 56 | 57 | // When the user clicks the refresh button again, 58 | button.click(); 59 | 60 | // the first quote should be displayed 61 | await screen.findByText(new RegExp(quotes[0].content, "i")); 62 | await screen.findByText(new RegExp(quotes[0].author, "i")); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/random-quote/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | 3 | import "../src/index.css"; 4 | import "../tailwind.css"; 5 | -------------------------------------------------------------------------------- /examples/random-quote/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | }, 18 | "include": [ 19 | "./src/**/*", 20 | "./tests/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /examples/random-quote/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import checker from 'vite-plugin-checker' 3 | 4 | export default defineConfig({ 5 | root: "src", 6 | build: { 7 | outDir: "../dist", 8 | }, 9 | plugins: [ 10 | checker({ 11 | typescript: true, 12 | }) 13 | ] 14 | }); 15 | -------------------------------------------------------------------------------- /examples/random-quote/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | setupFiles: './tests/setupTests.ts', 7 | }, 8 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seqflow-root", 3 | "version": "unpublished", 4 | "description": "SeqFlow: A lightweight, domain-driven front-end framework designed to simplify web application development, reduce complexity, and enhance user experience with an event-driven architecture.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "turbo run --concurrency 1 --log-order=stream build", 9 | "test": "turbo run --concurrency 1 --log-order=stream test", 10 | "biome:check": "turbo run biome:check", 11 | "postinstall": "rm -rf node_modules/@types/react && rm -rf node_modules/.pnpm/@types+react*" 12 | }, 13 | "keywords": [], 14 | "homepage": "https://seqflow.dev", 15 | "repository": "https://github.com/allevo/seqflow-js", 16 | "bugs": "https://github.com/allevo/seqflow-js/issues", 17 | "author": "Tommaso Allevi ", 18 | "engines": { 19 | "node": ">=20.11.1" 20 | }, 21 | "license": "MIT", 22 | "dependencies": { 23 | "turbo": "^2.3.3" 24 | }, 25 | "devDependencies": { 26 | "execa": "^9.5.1" 27 | }, 28 | "packageManager": "pnpm@9.9.0" 29 | } 30 | -------------------------------------------------------------------------------- /packages/components/.gitignore: -------------------------------------------------------------------------------- 1 | src/output.css 2 | storybook-static -------------------------------------------------------------------------------- /packages/components/.npmignore: -------------------------------------------------------------------------------- 1 | 2 | .turbo 3 | node_modules 4 | npm-debug.log 5 | coverage 6 | storybook-static -------------------------------------------------------------------------------- /packages/components/.storybook/main.js: -------------------------------------------------------------------------------- 1 | import { join, dirname } from "path"; 2 | 3 | /** 4 | * This function is used to resolve the absolute path of a package. 5 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 6 | */ 7 | function getAbsolutePath(value) { 8 | return dirname(require.resolve(join(value, "package.json"))); 9 | } 10 | 11 | /** @type { import('@storybook/html-vite').StorybookConfig } */ 12 | const config = { 13 | stories: [ 14 | "../stories/**/*.mdx", 15 | "../src/**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)", 16 | ], 17 | addons: [ 18 | // getAbsolutePath("@storybook/addon-links"), 19 | // getAbsolutePath("@storybook/addon-essentials"), 20 | // getAbsolutePath("@chromatic-com/storybook"), 21 | // getAbsolutePath("@storybook/addon-interactions"), 22 | '@storybook/addon-a11y', 23 | '@storybook/addon-interactions', 24 | { 25 | name: '@storybook/addon-themes', 26 | options: { 27 | implementation: require("postcss"), 28 | }, 29 | }, 30 | { 31 | name: '@storybook/addon-essentials', 32 | options: { 33 | backgrounds: false 34 | } 35 | }, 36 | ], 37 | core: { 38 | builder: '@storybook/builder-vite', // 👈 The builder enabled here. 39 | }, 40 | framework: { 41 | name: getAbsolutePath("@seqflow/storybook"), 42 | options: {}, 43 | }, 44 | }; 45 | export default config; 46 | -------------------------------------------------------------------------------- /packages/components/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/html').Preview } */ 2 | 3 | import '../dist/index.css'; 4 | 5 | const preview = { 6 | parameters: { 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i, 11 | }, 12 | }, 13 | // darkMode: { 14 | // classTarget: 'html', 15 | // // Override the default dark theme 16 | // dark: { ...themes.dark, appBg: 'black' }, 17 | // // Override the default light theme 18 | // light: { ...themes.normal, appBg: 'red' } 19 | // } 20 | }, 21 | }; 22 | 23 | export default preview; 24 | -------------------------------------------------------------------------------- /packages/components/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tommaso Allevi 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 | -------------------------------------------------------------------------------- /packages/components/README.md: -------------------------------------------------------------------------------- 1 | # SeqFlow Component Library 2 | 3 | A collection of reusable components for building SeqFlow apps. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | pnpm install @seqflow/components 9 | ``` 10 | 11 | ## Local Development 12 | 13 | To run the storybook locally, run the following command: 14 | 15 | ```bash 16 | pnpm run build 17 | pnpm run storybook 18 | ``` 19 | 20 | This is a work in progress. More components will be added soon. 21 | -------------------------------------------------------------------------------- /packages/components/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/components/build-types.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDeclarationOnly": true, 5 | "outFile": "./dist/all.d.ts", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "target": "ES6", 11 | "module": "ES6", 12 | "moduleResolution": "node", 13 | "jsx": "react", 14 | "jsxFactory": "component.createDOMElement", 15 | "jsxFragmentFactory": "component.createDOMFragment" 16 | }, 17 | "include": [ 18 | "src/**/*.tsx" 19 | ], 20 | "exclude": [ 21 | "./src/**/*.stories.tsx" 22 | ] 23 | } -------------------------------------------------------------------------------- /packages/components/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | }; -------------------------------------------------------------------------------- /packages/components/src/Alert/Alert.stories.tsx: -------------------------------------------------------------------------------- 1 | import { expect, userEvent, within } from "@storybook/test"; 2 | 3 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 4 | import type { StoryFn } from "@seqflow/storybook"; 5 | import { Alert, type AlertPropsType } from "."; 6 | 7 | async function AlertStory( 8 | props: ComponentProps, 9 | { component }: Contexts, 10 | ) { 11 | component.renderSync(This is an alert); 12 | } 13 | // biome-ignore lint/suspicious/noExplicitAny: storybook 14 | AlertStory.__storybook = (Alert as any).__storybook; 15 | 16 | export default { 17 | title: "Example/Alert", 18 | tags: ["autodocs"], 19 | component: AlertStory, 20 | args: { 21 | children: "This is an alert", 22 | }, 23 | }; 24 | 25 | export const Empty = {}; 26 | 27 | export const AllAlert: StoryFn = async (_, { component }: Contexts) => { 28 | component.renderSync( 29 |
35 |
Color
36 |
37 | No color 38 |
39 |
40 | info 41 |
42 |
43 | success 44 |
45 |
46 | warning 47 |
48 |
49 | error 50 |
51 |
, 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/components/src/Alert/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface AlertPropsType { 4 | color?: "info" | "success" | "warning" | "error"; 5 | } 6 | 7 | export async function Alert( 8 | { color, children }: ComponentProps, 9 | { component, app }: Contexts, 10 | ) { 11 | const classNames = ["alert"]; 12 | if (color) { 13 | /* 14 | alert-info 15 | alert-success 16 | alert-warning 17 | alert-error 18 | */ 19 | classNames.push(`alert-${color}`); 20 | } 21 | component._el.role = "alert"; 22 | 23 | component._el.classList.add(...classNames); 24 | 25 | if (!children || (Array.isArray(children) && children.length === 0)) { 26 | app.log.error({ 27 | message: "Alert component requires children", 28 | }); 29 | return; 30 | } 31 | 32 | component.renderSync(children); 33 | } 34 | -------------------------------------------------------------------------------- /packages/components/src/Badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import { expect, userEvent, within } from "@storybook/test"; 2 | 3 | import type { Contexts } from "@seqflow/seqflow"; 4 | import type { StoryFn } from "@seqflow/storybook"; 5 | import { Badge } from "."; 6 | 7 | export default { 8 | title: "Example/Badge", 9 | tags: ["autodocs"], 10 | component: Badge, 11 | args: { 12 | children: "1", 13 | }, 14 | }; 15 | 16 | export const Empty = {}; 17 | 18 | export const AllBadge: StoryFn = async (_, { component }: Contexts) => { 19 | component.renderSync( 20 |
26 |
Color
27 |
28 | neutral 29 |
30 |
31 | primary 32 |
33 |
34 | secondary 35 |
36 |
37 | accent 38 |
39 |
40 | ghost 41 |
42 |
43 | info 44 |
45 |
46 | success 47 |
48 |
49 | warning 50 |
51 |
52 | error 53 |
54 |
55 | outline 56 |
57 |
, 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/components/src/Badge/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface BadgePropsType { 4 | color?: 5 | | "neutral" 6 | | "primary" 7 | | "secondary" 8 | | "accent" 9 | | "ghost" 10 | | "info" 11 | | "success" 12 | | "warning" 13 | | "error" 14 | | "outline"; 15 | size?: "normal" | "lg" | "md" | "sm" | "xs"; 16 | } 17 | 18 | export async function Badge( 19 | { color, size, children }: ComponentProps, 20 | { component, app }: Contexts, 21 | ) { 22 | const classNames = ["badge"]; 23 | if (color) { 24 | /* 25 | badge-neutral 26 | badge-primary 27 | badge-secondary 28 | badge-accent 29 | badge-ghost 30 | badge-info 31 | badge-success 32 | badge-warning 33 | badge-error 34 | badge-outline 35 | */ 36 | classNames.push(`badge-${color}`); 37 | } 38 | if (size && size !== "normal") { 39 | /* 40 | badge-lg 41 | badge-md 42 | badge-sm 43 | badge-xs 44 | */ 45 | classNames.push(`badge-${size}`); 46 | } 47 | component._el.classList.add(...classNames); 48 | 49 | component._el.setAttribute("aria-live", "polite"); 50 | 51 | if (!children) { 52 | app.log.error({ 53 | message: "Badge component requires children", 54 | }); 55 | return; 56 | } 57 | 58 | component.renderSync(children); 59 | } 60 | -------------------------------------------------------------------------------- /packages/components/src/Card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | import type { StoryFn } from "@seqflow/storybook"; 3 | import { Body, Card, type CardProps } from "."; 4 | import { Button } from "../Button"; 5 | 6 | async function CardStory(props: CardProps, { component }: Contexts) { 7 | component.renderSync( 8 | 9 | 10 | Card title 11 |

The content of the Card

12 | 13 | 14 | 15 | 16 |
, 17 | ); 18 | } 19 | 20 | // biome-ignore lint/suspicious/noExplicitAny: storybook 21 | CardStory.__storybook = (Card as any).__storybook; 22 | 23 | export default { 24 | title: "Example/Card", 25 | tags: ["autodocs"], 26 | component: CardStory, 27 | args: { 28 | shadow: "md", 29 | }, 30 | }; 31 | 32 | export const Empty = {}; 33 | 34 | export const Centered: StoryFn = async (_, { component }: Contexts) => { 35 | component.renderSync( 36 | 37 | 38 | Card title 39 |

The content of the Card

40 | 41 | 42 | 43 | 44 |
, 45 | ); 46 | }; 47 | 48 | export const ActionOnTop: StoryFn = async ( 49 | _, 50 | { component }: Contexts, 51 | ) => { 52 | component.renderSync( 53 | 54 | 55 | 56 | 57 | 58 | Card title 59 |

The content of the Card

60 |
61 |
, 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/components/src/ChatBubble/ChatBubble.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | import type { StoryFn } from "@seqflow/storybook"; 3 | import { type BubbleProps, ChatBubble, type ChatBubbleProps } from "."; 4 | 5 | async function ChatBubbleStory( 6 | props: ChatBubbleProps, 7 | { component }: Contexts, 8 | ) { 9 | component.renderSync( 10 | <> 11 | 12 | 13 |

It's over Anakin,I have the high ground.

14 |
15 |
16 | 17 | 18 |

You underestimate my power!

19 |
20 |
21 | , 22 | ); 23 | } 24 | // biome-ignore lint/suspicious/noExplicitAny: 25 | ChatBubbleStory.__storybook = (ChatBubble as any).__storybook; 26 | 27 | export default { 28 | title: "Example/ChatBubble", 29 | tags: ["autodocs"], 30 | component: ChatBubbleStory, 31 | args: { 32 | spot: "start", 33 | }, 34 | }; 35 | 36 | export const Empty = {}; 37 | 38 | export const AllColors: StoryFn = async ( 39 | _, 40 | { component }: Contexts, 41 | ) => { 42 | const colors: BubbleProps["color"][] = [ 43 | "primary", 44 | "secondary", 45 | "accent", 46 | "info", 47 | "success", 48 | "warning", 49 | "error", 50 | ]; 51 | const elements: JSX.Element[] = []; 52 | for (const color of colors) { 53 | elements.push( 54 | 55 | 56 |

{color} bubble

57 |
58 |
, 59 | ); 60 | } 61 | 62 | component.renderSync(elements); 63 | }; 64 | -------------------------------------------------------------------------------- /packages/components/src/ChatBubble/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface ChatBubbleProps { 4 | spot: "start" | "end"; 5 | } 6 | 7 | export async function ChatBubble( 8 | { children, spot }: ComponentProps, 9 | { component, app }: Contexts, 10 | ) { 11 | const classes = ["chat"]; 12 | // chat-start 13 | // chat-end 14 | classes.push(`chat-${spot}`); 15 | for (const c of classes) { 16 | component._el.classList.add(c); 17 | } 18 | 19 | if (!children) { 20 | app.log.error({ 21 | message: "ChatBubble component requires children", 22 | }); 23 | return; 24 | } 25 | 26 | component.renderSync(children); 27 | } 28 | 29 | export interface BubbleProps { 30 | color?: 31 | | "primary" 32 | | "secondary" 33 | | "accent" 34 | | "info" 35 | | "success" 36 | | "warning" 37 | | "error"; 38 | } 39 | 40 | async function Bubble( 41 | { children, color }: ComponentProps, 42 | { component, app }: Contexts, 43 | ) { 44 | component._el.classList.add("chat-bubble"); 45 | if (color) { 46 | // chat-bubble-primary 47 | // chat-bubble-secondary 48 | // chat-bubble-accent 49 | // chat-bubble-info 50 | // chat-bubble-success 51 | // chat-bubble-warning 52 | // chat-bubble-error 53 | component._el.classList.add(`chat-bubble-${color}`); 54 | } 55 | 56 | if (!children) { 57 | app.log.error({ 58 | message: "ChatBubble.Bubble component requires children", 59 | }); 60 | return; 61 | } 62 | component.renderSync(children); 63 | } 64 | ChatBubble.Bubble = Bubble; 65 | -------------------------------------------------------------------------------- /packages/components/src/Checkbox/Checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | import type { StoryFn } from "@seqflow/storybook"; 3 | import { Checkbox } from "."; 4 | 5 | export default { 6 | title: "Example/Checkbox", 7 | tags: ["autodocs"], 8 | component: Checkbox, 9 | }; 10 | 11 | export const Empty = {}; 12 | 13 | export const AllColors: StoryFn = async ( 14 | _, 15 | { component }: Contexts, 16 | ) => { 17 | component.renderSync( 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
, 28 | ); 29 | }; 30 | 31 | export const AllSizes: StoryFn = async (_, { component }: Contexts) => { 32 | component.renderSync( 33 |
34 | 35 | 36 | 37 | 38 | 39 |
, 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/components/src/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface CheckboxPropsType { 4 | color?: 5 | | "primary" 6 | | "secondary" 7 | | "accent" 8 | | "success" 9 | | "warning" 10 | | "info" 11 | | "error"; 12 | size?: "lg" | "md" | "sm" | "xs"; 13 | disabled?: boolean; 14 | defaultChecked?: boolean; 15 | name?: string; 16 | } 17 | 18 | export async function Checkbox( 19 | { color, size, disabled, name, defaultChecked }: CheckboxPropsType, 20 | { component }: Contexts, 21 | ) { 22 | const classNames = ["checkbox"]; 23 | if (color) { 24 | // checkbox-primary 25 | // checkbox-secondary 26 | // checkbox-accent 27 | // checkbox-success 28 | // checkbox-warning 29 | // checkbox-info 30 | // checkbox-error 31 | classNames.push(`checkbox-${color}`); 32 | } 33 | if (size) { 34 | // checkbox-lg 35 | // checkbox-md 36 | // checkbox-sm 37 | // checkbox-xs 38 | classNames.push(`checkbox-${size}`); 39 | } 40 | component._el.classList.add(...classNames); 41 | 42 | const el = component._el as HTMLInputElement; 43 | el.type = "checkbox"; 44 | if (disabled !== undefined) { 45 | el.disabled = disabled; 46 | } 47 | if (defaultChecked !== undefined) { 48 | el.defaultChecked = defaultChecked; 49 | el.checked = defaultChecked; 50 | } 51 | if (name !== undefined) { 52 | el.name = name; 53 | } 54 | } 55 | Checkbox.tagName = () => "input"; 56 | -------------------------------------------------------------------------------- /packages/components/src/Divider/Divider.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | import type { StoryFn } from "@seqflow/storybook"; 3 | import { Divider } from "."; 4 | 5 | export default { 6 | title: "Example/Divider", 7 | tags: ["autodocs"], 8 | component: Divider, 9 | }; 10 | 11 | export const Empty = {}; 12 | 13 | export const WithoutText: StoryFn = async function OR( 14 | _: unknown, 15 | { component }: Contexts, 16 | ) { 17 | component.renderSync( 18 |
19 |
20 | content 21 |
22 | 23 |
24 | content 25 |
26 |
, 27 | ); 28 | }; 29 | 30 | export const WithText: StoryFn = async function OR( 31 | _: unknown, 32 | { component }: Contexts, 33 | ) { 34 | component.renderSync( 35 |
36 |
37 | content 38 |
39 | 40 |
41 | content 42 |
43 |
, 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/components/src/Divider/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface DividerPropsType { 4 | /** The text */ 5 | label?: string; 6 | } 7 | 8 | export async function Divider( 9 | { label }: DividerPropsType, 10 | { component }: Contexts, 11 | ) { 12 | component._el.classList.add("divider"); 13 | 14 | if (label) { 15 | component._el.textContent = label; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/components/src/Dropdown/Dropdown.stories.tsx: -------------------------------------------------------------------------------- 1 | import { expect, userEvent, within } from "@storybook/test"; 2 | 3 | import type { Contexts } from "@seqflow/seqflow"; 4 | import type { StoryFn } from "@seqflow/storybook"; 5 | import { Dropdown, type DropdownPropsType } from "."; 6 | import { Button } from "../Button"; 7 | import { Link } from "../Link"; 8 | import { Menu } from "../Menu"; 9 | import { Navbar } from "../Navbar"; 10 | 11 | async function DropdownStory( 12 | props: DropdownPropsType, 13 | { component }: Contexts, 14 | ) { 15 | component._el.classList.add(...["pt-48", "pl-48"]); 16 | component.renderSync( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | This is a link 25 | 26 | 27 | 28 | , 29 | ); 30 | } 31 | // biome-ignore lint/suspicious/noExplicitAny: storybook 32 | DropdownStory.__storybook = (Dropdown as any).__storybook; 33 | 34 | export default { 35 | title: "Example/Dropdown", 36 | tags: ["autodocs"], 37 | component: DropdownStory, 38 | args: { 39 | label: "Dropdown", 40 | }, 41 | }; 42 | 43 | export const Empty = {}; 44 | -------------------------------------------------------------------------------- /packages/components/src/Dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface DropdownPropsType { 4 | label?: string | JSX.Element; 5 | openOn?: "hover" | "click"; 6 | align?: 7 | | "end" 8 | | "top" 9 | | "top-end" 10 | | "bottom" 11 | | "bottom-end" 12 | | "left" 13 | | "left-end" 14 | | "right" 15 | | "right-end"; 16 | } 17 | 18 | export async function Dropdown( 19 | { label, openOn, align, children }: ComponentProps, 20 | { component, app }: Contexts, 21 | ) { 22 | const classes = ["dropdown"]; 23 | if (openOn === "hover") { 24 | classes.push("dropdown-hover"); 25 | } 26 | if (align) { 27 | /* 28 | dropdown-end 29 | dropdown-top 30 | dropdown-bottom 31 | dropdown-left 32 | dropdown-right 33 | */ 34 | classes.push(...align.split("-").map((a) => `dropdown-${a}`)); 35 | } 36 | component._el.classList.add(...classes); 37 | 38 | if (!children) { 39 | app.log.error({ 40 | message: "Dropdown component must have children", 41 | }); 42 | return; 43 | } 44 | 45 | if (Array.isArray(children)) { 46 | for (const child of children) { 47 | if (child instanceof DocumentFragment) { 48 | continue; 49 | } 50 | child.classList.add("dropdown-content"); 51 | child.classList.add("z-[90]"); 52 | } 53 | } 54 | 55 | const btn = 56 | typeof label === "string" ? ( 57 | 60 | ) : ( 61 | label 62 | ); 63 | 64 | if (btn instanceof HTMLElement) { 65 | // This is needed for Safari, otherwise the dropdown won't open 66 | // See: https://github.com/allevo/seqflow-js/issues/29 67 | // See: https://stackoverflow.com/questions/61652836/why-does-focus-within-not-work-in-safari 68 | btn.tabIndex = 0; 69 | } 70 | 71 | component.renderSync( 72 | <> 73 | {btn} 74 | {/**/} 75 | {children} 76 | , 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /packages/components/src/Footer/Footer.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { Footer, type FooterProps } from "."; 3 | 4 | async function FooterExample( 5 | props: ComponentProps, 6 | { component }: Contexts, 7 | ) { 8 | component.renderSync( 9 |
10 | 16 |
, 17 | ); 18 | } 19 | 20 | // biome-ignore lint/suspicious/noExplicitAny: storybook 21 | FooterExample.__storybook = (Footer as any).__storybook; 22 | 23 | export default { 24 | title: "Example/Footer", 25 | tags: ["autodocs"], 26 | component: FooterExample, 27 | args: {}, 28 | }; 29 | 30 | export const Empty = {}; 31 | -------------------------------------------------------------------------------- /packages/components/src/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface FooterProps { 4 | center?: boolean; 5 | } 6 | 7 | export async function Footer( 8 | { children, center }: ComponentProps, 9 | { component, app }: Contexts, 10 | ) { 11 | if (!children || children.length === 0) { 12 | app.log.error({ 13 | message: "Footer component must have children", 14 | }); 15 | return; 16 | } 17 | 18 | const classes = ["footer"]; 19 | if (center) { 20 | classes.push("footer-center"); 21 | } 22 | component._el.classList.add(...classes); 23 | 24 | component.renderSync(children); 25 | } 26 | Footer.tagName = () => "footer"; 27 | -------------------------------------------------------------------------------- /packages/components/src/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import type { ButtonComponent } from "../Button"; 3 | 4 | export type FormComponent = HTMLFormElement & { 5 | runAsync: (fn: (c: Contexts) => Promise) => Promise; 6 | }; 7 | 8 | export async function Form( 9 | { children }: ComponentProps, 10 | { component, app }: Contexts, 11 | ) { 12 | if (!children) { 13 | app.log.error({ 14 | message: "Form component must have children", 15 | }); 16 | return; 17 | } 18 | 19 | const form = component._el as HTMLFormElement; 20 | form.noValidate = true; 21 | 22 | form.addEventListener( 23 | "submit", 24 | (ev) => { 25 | if (!form.checkValidity()) { 26 | ev.preventDefault(); 27 | ev.stopPropagation(); 28 | ev.stopImmediatePropagation(); 29 | } 30 | }, 31 | { 32 | signal: component.ac.signal, 33 | }, 34 | ); 35 | component.renderSync(children); 36 | 37 | const el = component._el as FormComponent; 38 | el.runAsync = async (fn) => { 39 | const button = component._el.querySelector("button[type=submit]"); 40 | if ( 41 | button && 42 | button instanceof HTMLButtonElement && 43 | "transition" in button 44 | ) { 45 | const b = button as ButtonComponent; 46 | b.transition({ 47 | disabled: true, 48 | loading: true, 49 | loadingText: "Loading...", 50 | }); 51 | try { 52 | return await fn({ component, app }); 53 | } finally { 54 | b.transition({ 55 | disabled: false, 56 | loading: false, 57 | }); 58 | } 59 | } else { 60 | return await fn({ component, app }); 61 | } 62 | }; 63 | } 64 | Form.tagName = () => "form"; 65 | -------------------------------------------------------------------------------- /packages/components/src/FormField/index.css: -------------------------------------------------------------------------------- 1 | .form-control { 2 | .text-error { 3 | /* "text-red-500" is bad here. We should put in configuration */ 4 | @apply text-red-500 hidden; 5 | } 6 | 7 | .hint:empty:before { 8 | content: "\200b"; 9 | } 10 | } 11 | 12 | .form-control.form-control-error { 13 | input { 14 | @apply input-error; 15 | } 16 | 17 | .text-error { 18 | @apply block; 19 | } 20 | .hint { 21 | @apply hidden; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/components/src/Hero/Hero.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | import { Hero } from "."; 3 | import { Button } from "../Button"; 4 | import { Heading } from "../Typography"; 5 | 6 | async function HeroStory(_: unknown, { component }: Contexts) { 7 | component.renderSync( 8 | 9 | 10 |
11 | 16 |

17 | Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda 18 | excepturi exercitationem quasi. In deleniti eaque aut repudiandae et 19 | a id nisi. 20 |

21 | 22 |
23 |
24 |
, 25 | ); 26 | } 27 | // biome-ignore lint/suspicious/noExplicitAny: storybook 28 | HeroStory.__storybook = (Hero as any).__storybook; 29 | 30 | export default { 31 | title: "Example/Hero", 32 | tags: ["autodocs"], 33 | component: HeroStory, 34 | args: {}, 35 | }; 36 | 37 | export const Empty = {}; 38 | -------------------------------------------------------------------------------- /packages/components/src/Hero/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export async function Hero( 4 | { children }: ComponentProps, 5 | { component, app }: Contexts, 6 | ) { 7 | component._el.classList.add("hero"); 8 | 9 | if (!children) { 10 | app.log.error({ 11 | message: "Hero component must have children", 12 | }); 13 | return; 14 | } 15 | 16 | component.renderSync(children); 17 | } 18 | 19 | export async function HeroContent( 20 | { children }: ComponentProps, 21 | { component, app }: Contexts, 22 | ) { 23 | component._el.classList.add("hero-content"); 24 | 25 | if (!children) { 26 | app.log.error({ 27 | message: "HeroContent component must have children", 28 | }); 29 | return; 30 | } 31 | 32 | component.renderSync(children); 33 | } 34 | 35 | Hero.Content = HeroContent; 36 | Object.assign(Hero, { 37 | Content: HeroContent, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/components/src/Link/Link.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { Link, type LinkPropsType } from "."; 3 | 4 | async function LinkStory( 5 | { children, ...props }: ComponentProps, 6 | { component }: Contexts, 7 | ) { 8 | component.renderSync(This is a link); 9 | } 10 | // biome-ignore lint/suspicious/noExplicitAny: storybook 11 | LinkStory.__storybook = (Link as any).__storybook; 12 | 13 | export default { 14 | title: "Example/Link", 15 | tags: ["autodocs"], 16 | component: LinkStory, 17 | args: {}, 18 | }; 19 | 20 | export const Empty = {}; 21 | 22 | export const AllLinks = async function AllLinks( 23 | _: ComponentProps, 24 | { component }: Contexts, 25 | ) { 26 | component.renderSync( 27 |
28 | Link 29 | 30 | Link 31 | 32 | 33 | Link 34 | 35 | 36 | Link 37 | 38 | 39 | Link 40 | 41 | 42 | Link 43 | 44 | 45 | Link 46 | 47 | 48 | Link 49 | 50 | 51 | Link 52 | 53 | 54 | Link 55 | 56 |
, 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/components/src/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface LoadingPropsType { 4 | type?: "spinner" | "dots" | "ring" | "ball" | "bars" | "infinity"; 5 | size?: "xs" | "sm" | "md" | "lg"; 6 | } 7 | 8 | export async function Loading( 9 | { type, size }: ComponentProps, 10 | { component }: Contexts, 11 | ) { 12 | component._el.classList.add("loading"); 13 | component._el.classList.add("loading-spinner"); 14 | 15 | component._el.role = "progressbar"; 16 | 17 | if (type) { 18 | // loading-spinner 19 | // loading-dots 20 | // loading-ring 21 | // loading-ball 22 | // loading-bars 23 | // loading-infinity 24 | component._el.classList.add(`loading-${type}`); 25 | } 26 | if (size) { 27 | // loading-xs 28 | // loading-sm 29 | // loading-md 30 | // loading-lg 31 | component._el.classList.add(`loading-${size}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/components/src/Menu/Menu.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { Menu, type MenuPropsType } from "."; 3 | 4 | async function MenuStory( 5 | { children, ...props }: ComponentProps, 6 | { component }: Contexts, 7 | ) { 8 | component.renderSync( 9 | 10 | 11 | Item 1 12 | Item 2 13 | Item 3 14 | 15 | , 16 | ); 17 | const events = component.waitEvents( 18 | component.domEvent(component._el, "click"), 19 | ); 20 | for await (const ev of events) { 21 | } 22 | } 23 | // biome-ignore lint/suspicious/noExplicitAny: storybook 24 | MenuStory.__storybook = (Menu as any).__storybook; 25 | 26 | export default { 27 | title: "Example/Menu", 28 | tags: ["autodocs"], 29 | component: MenuStory, 30 | args: {}, 31 | }; 32 | 33 | export const Empty = {}; 34 | -------------------------------------------------------------------------------- /packages/components/src/Modal/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { domEvent } from "@seqflow/seqflow/dist/src/events"; 3 | import { Modal, type ModalType } from "."; 4 | import { Button } from "../Button"; 5 | 6 | async function ModalStory( 7 | { children, ...props }: ComponentProps, 8 | { component }: Contexts, 9 | ) { 10 | component.renderSync( 11 | <> 12 | 15 | 16 | 17 | 25 | 26 | This is a modal 27 | 28 |
29 | 32 |
33 |
34 |
35 | , 36 | ); 37 | 38 | const modal = component.getChild("modal"); 39 | 40 | const events = component.waitEvents( 41 | component.domEvent("open-modal", "click"), 42 | ); 43 | for await (const event of events) { 44 | modal.showModal(); 45 | } 46 | } 47 | // biome-ignore lint/suspicious/noExplicitAny: storybook 48 | ModalStory.__storybook = (ModalStory as any).__storybook; 49 | 50 | export default { 51 | title: "Example/Modal", 52 | tags: ["autodocs"], 53 | component: ModalStory, 54 | args: {}, 55 | }; 56 | 57 | export const Empty = {}; 58 | -------------------------------------------------------------------------------- /packages/components/src/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface ModalType { 4 | closeOnClickOutside?: boolean; 5 | modalBoxClassName?: string | string[]; 6 | } 7 | 8 | export async function Modal( 9 | { 10 | children, 11 | closeOnClickOutside, 12 | modalBoxClassName, 13 | }: ComponentProps, 14 | { component, app }: Contexts, 15 | ) { 16 | component._el.classList.add("modal"); 17 | 18 | let outside = undefined; 19 | if (closeOnClickOutside) { 20 | outside = ( 21 | 24 | ); 25 | } 26 | 27 | const modalBoxClasses = ["modal-box"]; 28 | if (modalBoxClassName) { 29 | if (Array.isArray(modalBoxClassName)) { 30 | modalBoxClasses.push(...modalBoxClassName); 31 | } else { 32 | modalBoxClasses.push(modalBoxClassName); 33 | } 34 | } 35 | 36 | component.renderSync( 37 | <> 38 |
{children}
39 | {outside} 40 | , 41 | ); 42 | } 43 | Modal.tagName = () => "dialog"; 44 | 45 | export async function ModalAction( 46 | { children }: ComponentProps, 47 | { component, app }: Contexts, 48 | ) { 49 | component._el.classList.add("modal-action"); 50 | 51 | component.renderSync(children); 52 | } 53 | 54 | export async function ModalClose( 55 | { children }: ComponentProps, 56 | { component, app }: Contexts, 57 | ) { 58 | const form = component._el as HTMLFormElement; 59 | form.method = "dialog"; 60 | component.renderSync(children); 61 | } 62 | ModalClose.tagName = () => "form"; 63 | 64 | Modal.Action = ModalAction; 65 | Modal.Close = ModalClose; 66 | Object.assign(Modal, { 67 | Action: ModalAction, 68 | Close: ModalClose, 69 | }); 70 | -------------------------------------------------------------------------------- /packages/components/src/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export async function Navbar( 4 | { children }: ComponentProps, 5 | { component, app }: Contexts, 6 | ) { 7 | const classes = ["navbar"]; 8 | component._el.classList.add(...classes); 9 | 10 | if (!children) { 11 | app.log.error({ 12 | message: "Navbar component requires children", 13 | }); 14 | return; 15 | } 16 | 17 | component.renderSync(children); 18 | } 19 | 20 | export async function NavbarStart( 21 | { children }: ComponentProps, 22 | { component, app }: Contexts, 23 | ) { 24 | const classes = ["navbar-start"]; 25 | component._el.classList.add(...classes); 26 | 27 | if (!children) { 28 | app.log.error({ 29 | message: "NavbarStart component requires children", 30 | }); 31 | return; 32 | } 33 | 34 | component.renderSync(children); 35 | } 36 | 37 | export async function NavbarCenter( 38 | { children }: ComponentProps, 39 | { component, app }: Contexts, 40 | ) { 41 | const classes = ["navbar-center"]; 42 | component._el.classList.add(...classes); 43 | 44 | if (!children) { 45 | app.log.error({ 46 | message: "NavbarCenter component requires children", 47 | }); 48 | return; 49 | } 50 | 51 | component.renderSync(children); 52 | } 53 | 54 | export async function NavbarEnd( 55 | { children }: ComponentProps, 56 | { component, app }: Contexts, 57 | ) { 58 | const classes = ["navbar-end"]; 59 | component._el.classList.add(...classes); 60 | 61 | if (!children) { 62 | app.log.error({ 63 | message: "NavbarEnd component requires children", 64 | }); 65 | return; 66 | } 67 | 68 | component.renderSync(children); 69 | } 70 | 71 | Navbar.Start = NavbarStart; 72 | Navbar.Center = NavbarCenter; 73 | Navbar.End = NavbarEnd; 74 | Object.assign(Navbar, { 75 | Start: NavbarStart, 76 | Center: NavbarCenter, 77 | End: NavbarEnd, 78 | }); 79 | -------------------------------------------------------------------------------- /packages/components/src/NumberInput/NumberInput.stories.tsx: -------------------------------------------------------------------------------- 1 | import { expect, userEvent, within } from "@storybook/test"; 2 | 3 | import type { StoryFn } from "@seqflow/storybook"; 4 | import { NumberInput } from "."; 5 | 6 | export default { 7 | title: "Example/NumberInput", 8 | tags: ["autodocs"], 9 | component: NumberInput, 10 | args: { 11 | placeholder: "Type a number", 12 | name: "number", 13 | withBorder: true, 14 | color: "normal", 15 | }, 16 | }; 17 | 18 | export const Typing: StoryFn = { 19 | play: async ({ canvasElement }) => { 20 | await new Promise((resolve) => setTimeout(resolve, 100)); 21 | const canvas = within(canvasElement); 22 | 23 | const input = canvas.getByRole("spinbutton"); 24 | 25 | await userEvent.type(input, "3"); 26 | 27 | expect(input).toHaveValue(3); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/components/src/NumberInput/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface NumberInputPropsType { 4 | name: string; 5 | placeholder?: string; 6 | withBorder?: boolean; 7 | color?: 8 | | "normal" 9 | | "primary" 10 | | "secondary" 11 | | "accent" 12 | | "info" 13 | | "success" 14 | | "warning" 15 | | "error" 16 | | "ghost"; 17 | disabled?: boolean; 18 | required?: boolean; 19 | } 20 | 21 | export async function NumberInput( 22 | { 23 | name, 24 | placeholder, 25 | withBorder, 26 | color, 27 | disabled, 28 | required, 29 | }: NumberInputPropsType, 30 | { component }: Contexts, 31 | ) { 32 | const classNames = ["input"]; 33 | if (withBorder !== false) { 34 | classNames.push("input-bordered"); 35 | } 36 | if (color && color !== "normal") { 37 | // input-primary 38 | // input-secondary 39 | // input-accent 40 | // input-info 41 | // input-success 42 | // input-warning 43 | // input-error 44 | // input-ghost 45 | classNames.push(`input-${color}`); 46 | } 47 | component._el.classList.add(...classNames); 48 | 49 | const el = component._el as HTMLInputElement; 50 | el.type = "number"; 51 | el.name = name; 52 | el.disabled = Boolean(disabled); 53 | if (placeholder) { 54 | el.placeholder = placeholder; 55 | } 56 | if (required) { 57 | el.required = true; 58 | el.ariaRequired = "true"; 59 | } 60 | 61 | const ev = component.waitEvents(component.domEvent(component._el, "input")); 62 | for await (const _ of ev) { 63 | if (el.validity.valid) { 64 | el.dispatchEvent(new Event("valid")); 65 | } 66 | } 67 | } 68 | NumberInput.tagName = () => "input"; 69 | -------------------------------------------------------------------------------- /packages/components/src/Select/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | import { Select, type SelectPropsType } from "."; 3 | 4 | async function SelectStory(props: SelectPropsType, { component }: Contexts) { 5 | component.renderSync( 6 | , 12 | ); 13 | } 14 | // biome-ignore lint/suspicious/noExplicitAny: storybook 15 | SelectStory.__storybook = (Select as any).__storybook; 16 | 17 | export default { 18 | title: "Example/Select", 19 | tags: ["autodocs"], 20 | component: SelectStory, 21 | args: { 22 | bordered: true, 23 | }, 24 | }; 25 | 26 | export const Empty = {}; 27 | -------------------------------------------------------------------------------- /packages/components/src/Select/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface SelectPropsType { 4 | name?: string; 5 | size?: "xs" | "sm" | "md" | "lg"; 6 | bordered?: boolean; 7 | disabled?: boolean; 8 | color?: 9 | | "ghost" 10 | | "primary" 11 | | "secondary" 12 | | "accent" 13 | | "info" 14 | | "success" 15 | | "warning" 16 | | "error"; 17 | required?: boolean; 18 | } 19 | 20 | export async function Select( 21 | { 22 | size, 23 | bordered, 24 | color, 25 | children, 26 | name, 27 | disabled, 28 | required, 29 | }: ComponentProps, 30 | { component, app }: Contexts, 31 | ) { 32 | const classes = ["select"]; 33 | if (bordered === true) { 34 | classes.push("select-bordered"); 35 | } 36 | if (size) { 37 | // select-xs 38 | // select-sm 39 | // select-md 40 | // select-lg 41 | classes.push(`select-${size}`); 42 | } 43 | if (color) { 44 | // select-ghost 45 | // select-primary 46 | // select-secondary 47 | // select-accent 48 | // select-info 49 | // select-success 50 | // select-warning 51 | // select-error 52 | classes.push(`select-${color}`); 53 | } 54 | component._el.classList.add(...classes); 55 | 56 | if (name) { 57 | component._el.setAttribute("name", name); 58 | } 59 | if (disabled === true) { 60 | component._el.setAttribute("disabled", ""); 61 | } 62 | 63 | if (!children) { 64 | app.log.error({ 65 | message: "Select component requires children", 66 | }); 67 | return; 68 | } 69 | 70 | component.renderSync(children); 71 | 72 | if (required) { 73 | component._el.setAttribute("required", ""); 74 | } 75 | } 76 | Select.tagName = () => "select"; 77 | -------------------------------------------------------------------------------- /packages/components/src/Typography/Heading.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import type { StoryFn } from "@seqflow/storybook"; 3 | import { Heading, type HeadingProps, Prose } from "."; 4 | 5 | async function HeadingStory( 6 | { level }: ComponentProps, 7 | { component }: Contexts, 8 | ) { 9 | component.renderSync( 10 | 11 | The title of the heading 12 | , 13 | ); 14 | } 15 | // biome-ignore lint/suspicious/noExplicitAny: storybook 16 | HeadingStory.__storybook = (Heading as any).__storybook; 17 | 18 | export default { 19 | title: "Example/Heading", 20 | tags: ["autodocs"], 21 | component: HeadingStory, 22 | args: {}, 23 | }; 24 | 25 | export const Empty = {}; 26 | 27 | export const AllHeading: StoryFn = async ( 28 | _, 29 | { component }: Contexts, 30 | ) => { 31 | const title = "The heading"; 32 | component.renderSync( 33 | 34 | {`${title} 1`} 35 | {`${title} 2`} 36 | {`${title} 3`} 37 | {`${title} 4`} 38 |

This is a paragraph

39 |
, 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/components/src/Typography/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export async function Prose( 4 | { children }: ComponentProps, 5 | { component, app }: Contexts, 6 | ) { 7 | component._el.classList.add("prose"); 8 | if (!children) { 9 | app.log.error({ 10 | message: "Prose component requires children", 11 | }); 12 | return; 13 | } 14 | component.renderSync(children); 15 | } 16 | 17 | export interface HeadingProps { 18 | /** Level */ 19 | level?: 1 | 2 | 3 | 4; 20 | } 21 | 22 | export async function Heading( 23 | { children }: ComponentProps, 24 | { component, app }: Contexts, 25 | ) { 26 | if (!children) { 27 | app.log.error({ 28 | message: "Heading component requires children", 29 | }); 30 | return; 31 | } 32 | 33 | component.renderSync(children); 34 | } 35 | Heading.tagName = (props: HeadingProps) => { 36 | return `h${props.level || 1}`; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/components/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/components/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | export * from "./Alert/index.js"; 3 | export * from "./Badge/index.js"; 4 | export * from "./Button/index.js"; 5 | export * from "./Card/index.js"; 6 | export * from "./ChatBubble/index.js"; 7 | export * from "./Checkbox/index.js"; 8 | export * from "./Divider/index.js"; 9 | export * from "./Dropdown/index.js"; 10 | export * from "./Form/index.js"; 11 | export * from "./Footer/index.js"; 12 | export * from "./FormField/index.js"; 13 | export * from "./Loading/index.js"; 14 | export * from "./Hero/index.js"; 15 | export * from "./Menu/index.js"; 16 | export * from "./Modal/index.js"; 17 | export * from "./Navbar/index.js"; 18 | export * from "./NumberInput/index.js"; 19 | export * from "./Link/index.js"; 20 | export * from "./Select/index.js"; 21 | export * from "./Tabs/index.js"; 22 | export * from "./TextInput/index.js"; 23 | export * from "./Typography/index.js"; 24 | -------------------------------------------------------------------------------- /packages/components/stories/Configure.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from "@storybook/blocks"; 2 | 3 | import Github from "./assets/github.svg"; 4 | import Discord from "./assets/discord.svg"; 5 | import Youtube from "./assets/youtube.svg"; 6 | import Tutorials from "./assets/tutorials.svg"; 7 | import Styling from "./assets/styling.png"; 8 | import Context from "./assets/context.png"; 9 | import Assets from "./assets/assets.png"; 10 | import Docs from "./assets/docs.png"; 11 | import Share from "./assets/share.png"; 12 | import FigmaPlugin from "./assets/figma-plugin.png"; 13 | import Testing from "./assets/testing.png"; 14 | import Accessibility from "./assets/accessibility.png"; 15 | import Theming from "./assets/theming.png"; 16 | import AddonLibrary from "./assets/addon-library.png"; 17 | import Init from './foo.tsx' 18 | 19 | export const RightArrow = () => { 20 | return ( 21 | 34 | 35 | 36 | ) 37 | } 38 | 39 | 40 | 41 |
42 |
43 | # SeqFlow JS Components 44 | 45 | SeqFlow JS Components is a library of reusable components that can be used to build SeqFlow applications. This Storybook is a living style guide that documents all the components available in the library. 46 | 47 | You can navigate through the components using the sidebar on the left. Each component has a set of examples that demonstrate how it can be used. 48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /packages/components/stories/assets/accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/accessibility.png -------------------------------------------------------------------------------- /packages/components/stories/assets/accessibility.svg: -------------------------------------------------------------------------------- 1 | 2 | Accessibility 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/components/stories/assets/addon-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/addon-library.png -------------------------------------------------------------------------------- /packages/components/stories/assets/assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/assets.png -------------------------------------------------------------------------------- /packages/components/stories/assets/avif-test-image.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/avif-test-image.avif -------------------------------------------------------------------------------- /packages/components/stories/assets/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/context.png -------------------------------------------------------------------------------- /packages/components/stories/assets/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/docs.png -------------------------------------------------------------------------------- /packages/components/stories/assets/figma-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/figma-plugin.png -------------------------------------------------------------------------------- /packages/components/stories/assets/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/share.png -------------------------------------------------------------------------------- /packages/components/stories/assets/styling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/styling.png -------------------------------------------------------------------------------- /packages/components/stories/assets/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/testing.png -------------------------------------------------------------------------------- /packages/components/stories/assets/theming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/components/stories/assets/theming.png -------------------------------------------------------------------------------- /packages/components/stories/assets/tutorials.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/components/stories/assets/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/components/stories/foo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '../src/index.tsx'; 3 | import * as seqflow from '@seqflow/seqflow' 4 | 5 | async function Main({}, { component, app }: seqflow.Contexts) { 6 | component.renderSync( 7 | component.createDOMElement( 8 | Button, 9 | {}, 10 | 'Hello' 11 | ) 12 | ) 13 | // this.renderSync( 14 | //
15 | //
17 | // ) 18 | } 19 | 20 | // await new Promise((resolve) => setTimeout(resolve, 1000)) 21 | 22 | /* 23 | let r: HTMLElement | null 24 | do { 25 | await new Promise((resolve) => setTimeout(resolve, 300)) 26 | r = document.body.querySelector('.sbdocs-content') 27 | console.log(r) 28 | } while(!r); 29 | 30 | console.log(document, r) 31 | seqflow.start(r, Main, { }, {}) 32 | 33 | */ 34 | 35 | export default function Init() { 36 | const ref = React.createRef(null) 37 | React.useEffect(() => { 38 | if (ref.current) { 39 | const control = seqflow.start(ref.current, Main, { }, { 40 | domains: {}, 41 | }) 42 | return () => { 43 | control.abort(new Error('React unmount')) 44 | } 45 | } 46 | }, [ref.current]) 47 | 48 | return React.createElement('div', { id: 'root', ref }, ['aa']) 49 | } -------------------------------------------------------------------------------- /packages/components/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/**/*.{js,jsx,ts,tsx,css}', 5 | './src/**/*.stories.tsx', 6 | './.storybook/**/*.{js,jsx,ts,tsx,mdx}', 7 | './stories/**/*.{js,jsx,ts,tsx,mdx}', 8 | ], 9 | 10 | theme: { 11 | extend: {}, 12 | }, 13 | daisyui: { 14 | styled: true, 15 | themes: false, 16 | base: true, 17 | utils: true, 18 | logs: true, 19 | rtl: false, 20 | prefix: "" 21 | }, 22 | plugins: [ 23 | require("@tailwindcss/typography"), 24 | require('daisyui') 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /packages/components/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { test } from "vitest"; 2 | 3 | test("not implemented as unit test", async () => { 4 | console.log("Not implemented as unit test"); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/components/tests/test.js: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync } from "node:child_process"; 2 | import waitOn from "wait-on"; 3 | 4 | console.log("STARTING STORYBOOK"); 5 | 6 | const storybookProcess = spawn("pnpm", ["run", "storybook"]); 7 | storybookProcess.stderr.on("data", (data) => { 8 | console.error("stderr", data.toString()); 9 | }); 10 | storybookProcess.stdout.on("data", (data) => { 11 | console.log("stdout", data.toString()); 12 | }); 13 | storybookProcess.on("exit", (code) => { 14 | console.log(`--- Storybook exited with code ${code}`); 15 | }); 16 | 17 | console.log("WAITING FOR STORYBOOK TO START"); 18 | try { 19 | await waitOn({ 20 | resources: ["http://localhost:6006"], 21 | timeout: 10_000, 22 | }); 23 | } catch (error) { 24 | console.error("Storybook did not start in time", error); 25 | process.exit(1); 26 | } 27 | 28 | console.log("STARTING STORYBOOK TESTS"); 29 | try { 30 | spawnSync("pnpm", ["test:storybook"], { stdio: "inherit" }); 31 | } catch (error) { 32 | console.error("Storybook tests failed", error); 33 | process.exit(1); 34 | } 35 | console.log("STORYBOOK TESTS PASSED"); 36 | 37 | console.log("KILLING STORYBOOK"); 38 | storybookProcess.kill(); 39 | 40 | // Force the process to exit 41 | process.exit(0); 42 | -------------------------------------------------------------------------------- /packages/components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["es2022", "dom", "DOM.Iterable"], 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "jsx": "react", 9 | "jsxFactory": "component.createDOMElement", 10 | "jsxFragmentFactory": "component.createDOMFragment", 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "es2022", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "declaration":true, 18 | "types": [], 19 | "skipLibCheck": true 20 | }, 21 | "include": ["./src/**/*", "./src/**/*.stories.tsx", "./tests/**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/components/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, PluginOption } from "vite"; 2 | import { resolve } from 'path' 3 | import * as p from "@seqflow/storybook/vite-plugin"; 4 | 5 | import dts from 'vite-plugin-dts' 6 | 7 | 8 | export default defineConfig(({command}) => { 9 | const plugins: PluginOption[] = [ 10 | dts({ 11 | exclude: ['**/*.stories.tsx', '**/*.stories.ts', '**/*.test.ts', 'stories/**/*'], 12 | }), 13 | ] 14 | // Generate storybook meta data only in serve mode 15 | // This is to avoid generating storybook meta data in build mode 16 | if (command === 'serve') { 17 | // @ts-ignore 18 | plugins.push(p.addStorybookMetaPlugin({})) 19 | } 20 | 21 | return { 22 | root: "src", 23 | build: { 24 | outDir: "../dist", 25 | emptyOutDir: false, 26 | cssCodeSplit: false, 27 | copyPublicDir: false, 28 | lib: { 29 | // Could also be a dictionary or array of multiple entry points 30 | entry: resolve(__dirname, 'src/index.tsx'), 31 | formats: ['es' as const], 32 | name: 'index', 33 | // the proper extensions will be added 34 | fileName: 'index', 35 | }, 36 | rollupOptions: { 37 | output: { 38 | entryFileNames: '[name].js', 39 | }, 40 | // mainEntryPointFilePath: resolve(__dirname, 'src/index.tsx'), 41 | external: /react/, 42 | } 43 | }, 44 | plugins, 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /packages/components/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | }, 7 | }) -------------------------------------------------------------------------------- /packages/create-seqflow/.npmignore: -------------------------------------------------------------------------------- 1 | 2 | .turbo 3 | node_modules 4 | npm-debug.log 5 | coverage 6 | -------------------------------------------------------------------------------- /packages/create-seqflow/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tommaso Allevi 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 | -------------------------------------------------------------------------------- /packages/create-seqflow/README.md: -------------------------------------------------------------------------------- 1 | # Create SeqFlow App 2 | 3 | Create a new SeqFlow app with a single command. 4 | 5 | ## Usage 6 | 7 | ``` 8 | pnpm create seqflow@latest --template empty 9 | ``` 10 | 11 | Run to know more about the options available: 12 | ``` 13 | pnpm create seqflow@latest --help 14 | ``` -------------------------------------------------------------------------------- /packages/create-seqflow/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/create-seqflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-seqflow", 3 | "version": "1.0.0-alpha.3", 4 | "description": "SeqFlow CLI", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "create-seqflow": "./dist/index.js" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "homepage": "https://seqflow.dev", 14 | "repository": "https://github.com/allevo/seqflow-js", 15 | "bugs": "https://github.com/allevo/seqflow-js/issues", 16 | "author": "Tommaso Allevi ", 17 | "engines": { 18 | "node": ">=20.11.1" 19 | }, 20 | "scripts": { 21 | "test": "npm-run-all biome build", 22 | "biome": "biome ci ./src", 23 | "biome:check": "biome check --write src", 24 | "start": "vite -c vite.config.js", 25 | "build": "vite build --emptyOutDir -c vite.config.js" 26 | }, 27 | "keywords": [ 28 | "seqflow", 29 | "cli" 30 | ], 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@biomejs/biome": "1.9.4", 34 | "@types/node": "^20.11.24", 35 | "@types/tar": "^6.1.11", 36 | "npm-run-all2": "^7.0.0", 37 | "tsx": "^4.7.1", 38 | "vite": "^6" 39 | }, 40 | "dependencies": { 41 | "@types/prompts": "^2.4.9", 42 | "prompts": "^2.4.2", 43 | "tar": "^6.2.0" 44 | } 45 | } -------------------------------------------------------------------------------- /packages/create-seqflow/src/index.ts: -------------------------------------------------------------------------------- 1 | import { main } from "./main"; 2 | 3 | const handleSigTerm = () => process.exit(0); 4 | process.on("SIGINT", handleSigTerm); 5 | process.on("SIGTERM", handleSigTerm); 6 | await main(process.argv.slice(2)); 7 | -------------------------------------------------------------------------------- /packages/create-seqflow/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | root: "src", 5 | build: { 6 | minify: false, 7 | target: "node20", 8 | ssr: true , 9 | lib: { 10 | formats: ["es"], 11 | // Could also be a dictionary or array of multiple entry points 12 | entry: 'index.ts', 13 | name: 'create-seqflow', 14 | // the proper extensions will be added 15 | fileName: 'create-seqflow.js', 16 | }, 17 | outDir: "../dist", 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/seqflow/.npmignore: -------------------------------------------------------------------------------- 1 | 2 | .turbo 3 | node_modules 4 | npm-debug.log 5 | coverage 6 | -------------------------------------------------------------------------------- /packages/seqflow/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tommaso Allevi 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 | -------------------------------------------------------------------------------- /packages/seqflow/README.md: -------------------------------------------------------------------------------- 1 | # SeqFlow JS 2 | 3 | This framework is a lightweight, domain-driven front-end framework designed to simplify web application development, reduce complexity, and enhance user experience with an event-driven architecture. 4 | 5 | See the [documentation](https://seqflow.dev) for more information. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | pnpm install @seqflow/seqflow 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```tsx 16 | import { Contexts } from "@seqflow/seqflow"; 17 | 18 | interface Quote { 19 | author: string; 20 | content: string; 21 | } 22 | 23 | async function getRandomQuote(): Promise { 24 | const res = await fetch("https://quotes.seqflow.dev/api/quotes/random") 25 | return await res.json(); 26 | } 27 | 28 | export async function Main({}, { component }: Contexts) { 29 | // Render loading message 30 | component.renderSync( 31 |

Loading...

32 | ); 33 | 34 | // Perform an async operation 35 | const quote = await getRandomQuote(); 36 | 37 | // Replace loading message with quote 38 | component.renderSync( 39 |
40 |
{quote.content}
41 |
{quote.author}
42 |
43 | ); 44 | } 45 | 46 | start(document.getElementById("root"), Main, {}, {}); 47 | ``` 48 | 49 | ## License 50 | 51 | SeqFlow JS is licensed under the MIT License. 52 | -------------------------------------------------------------------------------- /packages/seqflow/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.1/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true, 23 | "correctness": { 24 | "noUnusedImports": "error" 25 | } 26 | } 27 | }, 28 | "javascript": { 29 | "formatter": { 30 | "quoteStyle": "double" 31 | } 32 | }, 33 | "overrides": [ 34 | { 35 | "include": ["tests/**", "types/**"], 36 | "linter": { 37 | "rules": { 38 | "suspicious": { 39 | "noExplicitAny": "off" 40 | }, 41 | "complexity": { 42 | "noUselessFragments": "off", 43 | "noUselessLoneBlockStatements": "off" 44 | }, 45 | "a11y": { 46 | "all": false 47 | }, 48 | "style": { 49 | "useSelfClosingElements": "off", 50 | "noNonNullAssertion": "off" 51 | } 52 | } 53 | } 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /packages/seqflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seqflow/seqflow", 3 | "description": "SeqFlow: A lightweight, domain-driven front-end framework designed to simplify web application development, reduce complexity, and enhance user experience with an event-driven architecture.", 4 | "version": "1.0.0-rc.1", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "scripts": { 9 | "test": "pnpm run unit", 10 | "unit": "vitest --run --coverage && tsc -p types/tsconfig.json", 11 | "biome:check": "biome check --apply src tests types", 12 | "build": "vite build -c vite.config.js" 13 | }, 14 | "keywords": [ 15 | "seqflow", 16 | "framework", 17 | "jsx", 18 | "typescript", 19 | "web-component", 20 | "web-components", 21 | "ui", 22 | "web", 23 | "library", 24 | "router", 25 | "state", 26 | "management", 27 | "reactive", 28 | "modern" 29 | ], 30 | "homepage": "https://seqflow.dev", 31 | "repository": "https://github.com/allevo/seqflow-js", 32 | "bugs": "https://github.com/allevo/seqflow-js/issues", 33 | "author": "Tommaso Allevi ", 34 | "engines": { 35 | "node": ">=20.11.1" 36 | }, 37 | "license": "MIT", 38 | "devDependencies": { 39 | "@biomejs/biome": "1.9.4", 40 | "@testing-library/dom": "^10.4.0", 41 | "@vitest/coverage-v8": "^2.1.8", 42 | "depcheck": "^1.4.7", 43 | "jsdom": "^25.0.1", 44 | "npm-run-all2": "^7.0.0", 45 | "tsd": "^0.31.2", 46 | "tsx": "^4.19.2", 47 | "typescript": "^5.7.2", 48 | "vite": "^6.0.3", 49 | "vite-plugin-checker": "^0.8.0", 50 | "vite-plugin-dts": "^4.3.0", 51 | "vitest": "^2.1.8" 52 | } 53 | } -------------------------------------------------------------------------------- /packages/seqflow/src/debug.ts: -------------------------------------------------------------------------------- 1 | export function debugEventTarget( 2 | et: EventTarget, 3 | log: (_: Event) => void = console.log, 4 | ): EventTarget { 5 | const oldAddEventListener = et.addEventListener; 6 | et.addEventListener = ( 7 | type: string, 8 | listener: EventListenerOrEventListenerObject, 9 | options?: boolean | AddEventListenerOptions, 10 | ) => { 11 | return oldAddEventListener.call(et, type, listener, options); 12 | }; 13 | const oldDispatchEvent = et.dispatchEvent; 14 | et.dispatchEvent = (event: Event) => { 15 | log(event); 16 | return oldDispatchEvent.call(et, event); 17 | }; 18 | 19 | return et; 20 | } 21 | -------------------------------------------------------------------------------- /packages/seqflow/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./component"; 2 | export * from "./types"; 3 | export * from "./app"; 4 | export { createDomainEventClass } from "./domains"; 5 | export { debugEventTarget } from "./debug"; 6 | export { 7 | NavigationEvent, 8 | InMemoryRouter, 9 | BrowserRouter, 10 | type Router, 11 | } from "./router"; 12 | export type { ComponentResult, SeqFlowPlugin } from "./plugin"; 13 | -------------------------------------------------------------------------------- /packages/seqflow/tests/inner/debug.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { debugEventTarget } from "../../src/index"; 3 | 4 | test("debugEventTarget", () => { 5 | const events: Event[] = []; 6 | const et = debugEventTarget(new EventTarget(), (ev) => events.push(ev)); 7 | 8 | const ev1 = new Event("foo"); 9 | et.dispatchEvent(ev1); 10 | 11 | expect(events.length).toBe(1); 12 | expect(events[0]).toBe(ev1); 13 | 14 | const ev2 = new Event("bar"); 15 | et.dispatchEvent(ev2); 16 | 17 | expect(events.length).toBe(2); 18 | expect(events[1]).toBe(ev2); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/seqflow/tests/inner/domains.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest"; 2 | import { createDomainEventClass } from "../../src/domains"; 3 | import { domainEvent } from "../../src/events"; 4 | import { CounterChangedEvent } from "../test-utils"; 5 | 6 | test("createDomainEventClass", async () => { 7 | const MyEvent = createDomainEventClass("counter", "my-event"); 8 | const ev = new MyEvent(undefined); 9 | expect(ev.detail).toBe(undefined); 10 | expect(MyEvent.domainName).toBe("counter"); 11 | expect(MyEvent.t).toBe("my-event"); 12 | expect(ev.type).toBe("my-event"); 13 | }); 14 | 15 | test("domainEvent: wait for events", async () => { 16 | const et = new EventTarget(); 17 | 18 | const abortController = new AbortController(); 19 | 20 | const ev1 = new CounterChangedEvent({ before: 0, current: 5 }); 21 | const ev2 = new CounterChangedEvent({ before: 5, current: 10 }); 22 | setTimeout(() => { 23 | et.dispatchEvent(ev1); 24 | }, 10); 25 | setTimeout(() => { 26 | et.dispatchEvent(ev2); 27 | }, 20); 28 | setTimeout(() => { 29 | abortController.abort(); 30 | }, 30); 31 | 32 | const events: CounterChangedEvent[] = []; 33 | await expect(async () => { 34 | const gen = domainEvent(et, CounterChangedEvent)(abortController); 35 | for await (const ev of gen) { 36 | events.push(ev); 37 | } 38 | }).rejects.toThrow(); 39 | 40 | expect(events.length).toBe(2); 41 | expect(events[0]).equal(ev1); 42 | expect(events[1]).equal(ev2); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/seqflow/tests/inner/lifecycle.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, waitFor } from "@testing-library/dom"; 2 | import { afterEach, beforeEach, expect, test } from "vitest"; 3 | import { 4 | type ComponentProps, 5 | type Contexts, 6 | SeqFlowComponentContext, 7 | } from "../../src/index"; 8 | import { createAppForInnerTest, sleep } from "../test-utils"; 9 | 10 | let component: SeqFlowComponentContext; 11 | let abortController: AbortController; 12 | const logs: any[] = []; 13 | beforeEach(() => { 14 | document.body.innerHTML = ""; 15 | abortController = new AbortController(); 16 | component = new SeqFlowComponentContext( 17 | document.body, 18 | abortController, 19 | createAppForInnerTest(logs), 20 | { local: "root", global: "root" }, 21 | ); 22 | }); 23 | afterEach(() => { 24 | abortController.abort(); 25 | }); 26 | 27 | test("lifecicle", async () => { 28 | let clickCount = 0; 29 | function clicked() { 30 | clickCount++; 31 | } 32 | async function Button(_: ComponentProps, { component }: Contexts) { 33 | component.renderSync( 34 | , 37 | ); 38 | } 39 | async function A(_: ComponentProps, { component }: Contexts) { 40 | component.renderSync(, 32 | ); 33 | 34 | expect(clickCount).toBe(0); 35 | (await screen.findByText(/Button/i)).click(); 36 | await waitFor(() => expect(clickCount).toBe(1)); 37 | (await screen.findByText(/Button/i)).click(); 38 | await waitFor(() => expect(clickCount).toBe(2)); 39 | 40 | // Lifecycle ends 41 | abortController.abort(); 42 | 43 | (await screen.findByText(/Button/i)).click(); 44 | 45 | await sleep(100); 46 | 47 | await waitFor(() => expect(clickCount).toBe(2)); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/seqflow/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2019", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "outDir": "./dist", 18 | }, 19 | "include": [ 20 | "./src/**/*", 21 | "./tests/**/*", 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /packages/seqflow/types/app.tsx: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import type { Contexts, LogFunction, SeqFlowComponent } from "../src/index"; 3 | import type { CounterDomain } from "../tests/test-utils"; 4 | 5 | const _: SeqFlowComponent = async (_, contexts: Contexts) => { 6 | // config 7 | expectType<{ foo: string }>(contexts.app.config); 8 | expectType>(contexts.app.config); 9 | 10 | // logs 11 | expectType(contexts.app.log.debug); 12 | expectType(contexts.app.log.info); 13 | expectType(contexts.app.log.error); 14 | 15 | // domains 16 | expectType(contexts.app.domains.counter); 17 | // @ts-expect-error: Property 'domain1' does not exist on type 'Readonly'.ts(2339) 18 | expectType<{ foo: string }>(contexts.app.domains.domain1); 19 | 20 | // router 21 | expectType(contexts.app.router.segments); 22 | expectType(contexts.app.router.getCurrentPathname()); 23 | 24 | // domainEventTargets 25 | expectType(contexts.app.getDomainEventTarget("counter")); 26 | // @ts-expect-error: Type '"unknown-domain"' is not assignable to type '"counter"'.ts(2322) 27 | expectType(contexts.app.getDomainEventTarget("unknown-domain")); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/seqflow/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2015", "dom"], 5 | "jsx": "react", 6 | "jsxFactory": "component.createDOMElement", 7 | "jsxFragmentFactory": "component.createDOMFragment", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "target": "ES2020", 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "rootDir": "../", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "outDir": "./dist", 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/seqflow/vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import dts from 'vite-plugin-dts' 4 | import checker from 'vite-plugin-checker' 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | dts({ include: ['src'] }), 9 | checker({ typescript: true }), 10 | ], 11 | build: { 12 | emptyOutDir: false, 13 | cssCodeSplit: false, 14 | copyPublicDir: false, 15 | lib: { 16 | // Could also be a dictionary or array of multiple entry points 17 | entry: resolve(__dirname, 'src/index.ts'), 18 | formats: ['es'], 19 | name: 'index', 20 | // the proper extensions will be added 21 | fileName: 'index', 22 | }, 23 | rollupOptions: { 24 | // make sure to externalize deps that shouldn't be bundled 25 | // into your library 26 | external: [], 27 | output: {}, 28 | }, 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /packages/seqflow/vite.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/seqflow/vite.d.ts -------------------------------------------------------------------------------- /packages/seqflow/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | coverage: { 7 | reporter: ['text', 'json', 'html'], 8 | include: ['src'], 9 | // exclude: ['tests', 'types'], 10 | }, 11 | }, 12 | }) -------------------------------------------------------------------------------- /packages/storybook/README.md: -------------------------------------------------------------------------------- 1 | # Storybook for HTML 2 | -------------------------------------------------------------------------------- /packages/storybook/manager.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./dist/manager'); 3 | -------------------------------------------------------------------------------- /packages/storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seqflow/storybook", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Storybook for SeqFlow JS and Vite.", 6 | "keywords": [ 7 | "seqflow", 8 | "storybook" 9 | ], 10 | "license": "MIT", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "node": "./dist/index.js", 15 | "require": "./dist/index.js", 16 | "import": "./dist/index.mjs" 17 | }, 18 | "./preset": "./preset.js", 19 | "./render": "./dist/render.js", 20 | "./package.json": "./package.json", 21 | "./vite-plugin": "./vite-plugin.js" 22 | }, 23 | "main": "dist/index.js", 24 | "module": "dist/index.mjs", 25 | "types": "dist/index.d.ts", 26 | "scripts": { 27 | "biome": "biome ci ./src", 28 | "biome:check": "biome check --apply src tests", 29 | "test": "node --import tsx tests/foo.test.ts", 30 | "build": "npx tsc --outDir ./dist --target es2020 || echo 'ouch'; mv dist/entry-preview.js dist/entry-preview.mjs && mv dist/entry-preview-docs.js dist/entry-preview-docs.mjs", 31 | "depcheck": "depcheck --ignores @vitest/coverage-v8 ." 32 | }, 33 | "dependencies": { 34 | "@storybook/builder-vite": "^8.4.7", 35 | "@storybook/core-server": "^8.4.7", 36 | "@storybook/node-logger": "^8.4.7", 37 | "@storybook/types": "^8.4.7", 38 | "magic-string": "^0.30.14" 39 | }, 40 | "devDependencies": { 41 | "@seqflow/seqflow": "workspace:*", 42 | "@storybook/components": "^8.4.7", 43 | "@storybook/core": "^8.4.7", 44 | "@storybook/docs-tools": "^8.4.7", 45 | "@storybook/global": "^5.0.0", 46 | "@storybook/icons": "^1.3.0", 47 | "@storybook/manager-api": "^8.4.7", 48 | "@storybook/preview-api": "^8.4.7", 49 | "@types/node": "^18.19.67", 50 | "biome": "^0.3.3", 51 | "depcheck": "^1.4.7", 52 | "ts-dedent": "^2.2.0", 53 | "tsx": "^4.19.2", 54 | "typescript": "^5.7.2", 55 | "vite": "^6" 56 | }, 57 | "engines": { 58 | "node": ">=18.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/storybook/preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/preset'); 2 | -------------------------------------------------------------------------------- /packages/storybook/render.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/render'); 2 | -------------------------------------------------------------------------------- /packages/storybook/src/docs/sourceDecorator.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { DecoratorFunction } from '@storybook/types'; 3 | 4 | import type { SeqFlowJSRenderer } from '../types'; 5 | import { Contexts, start } from '@seqflow/seqflow'; 6 | 7 | export const sourceDecorator: DecoratorFunction = async () => { 8 | 9 | }; 10 | 11 | async function App({ component: comp, args }: any, { component }: Contexts) { 12 | const r: Element = component.createDOMElement(comp, args) as Element; 13 | component.renderSync(r as any); 14 | } 15 | 16 | export function buildComponent(component: any, args: any) { 17 | const root = document.createElement('div'); 18 | start(root, App, { component, args }, { 19 | log: console, 20 | domains: {}, 21 | }); 22 | return root; 23 | } -------------------------------------------------------------------------------- /packages/storybook/src/entry-preview-docs.ts: -------------------------------------------------------------------------------- 1 | import type { ArgTypesEnhancer, DecoratorFunction } from '@storybook/types'; 2 | import { enhanceArgTypes } from '@storybook/docs-tools'; 3 | 4 | import { sourceDecorator } from './docs/sourceDecorator.js'; 5 | import type { SeqFlowJSRenderer, Parameters } from './types'; 6 | import { ArgTypesExtractor } from '@storybook/docs-tools'; 7 | 8 | export const decorators: DecoratorFunction[] = [sourceDecorator]; 9 | 10 | const extractArgTypes: ArgTypesExtractor = (component) => { 11 | if (!component.__storybook) { 12 | console.log('Component does not have __storybook prop', component); 13 | return; 14 | } 15 | 16 | return component.__storybook.props 17 | } 18 | 19 | export const parameters: Parameters = { 20 | renderer: '@seqflow/storybook', 21 | docs: { 22 | story: { inline: true }, 23 | extractArgTypes, 24 | source: { 25 | type: 'dynamic', 26 | language: 'jsx', 27 | code: undefined, 28 | excludeDecorators: undefined, 29 | }, 30 | }, 31 | }; 32 | 33 | export const argTypesEnhancers: ArgTypesEnhancer[] = [enhanceArgTypes]; 34 | 35 | export { render } from './render'; 36 | -------------------------------------------------------------------------------- /packages/storybook/src/entry-preview.ts: -------------------------------------------------------------------------------- 1 | import { DocsContext } from '@storybook/preview-api'; 2 | import { renderToCanvas } from './render'; 3 | import type { Parameters } from './types'; 4 | 5 | export const parameters: Parameters = { 6 | renderer: '@seqflow/storybook', 7 | docs: { 8 | story: { inline: true }, 9 | source: { 10 | type: 'dynamic', 11 | language: 'jsx', 12 | code: undefined, 13 | excludeDecorators: undefined, 14 | } 15 | } 16 | }; 17 | 18 | export { render } from './render'; 19 | export { renderToCanvas } from './render'; 20 | -------------------------------------------------------------------------------- /packages/storybook/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /packages/storybook/src/preset.ts: -------------------------------------------------------------------------------- 1 | import type { PresetProperty } from '@storybook/types'; 2 | import { dirname, join } from 'path'; 3 | 4 | function getAbsolutePath(value: I): I { 5 | if (value === 'seqwflow-js-storybook') { 6 | return '/Users/allevo/repos/seqflow-js/packages/storybook' as any; 7 | } 8 | return dirname(require.resolve(join(value, 'package.json'))) as any; 9 | } 10 | 11 | export const core: PresetProperty<'core'> = { 12 | builder: getAbsolutePath('@storybook/builder-vite'), 13 | renderer: getAbsolutePath('seqwflow-js-storybook'), 14 | }; 15 | 16 | export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( 17 | input = [], 18 | options 19 | ) => { 20 | const docsEnabled = Object.keys(await options.presets.apply('docs', {}, options)).length > 0; 21 | const result: string[] = []; 22 | 23 | return Array.from(new Set(result 24 | .concat(input) 25 | .concat([join(__dirname, 'entry-preview.mjs')]) 26 | .concat(docsEnabled ? [join(__dirname, 'entry-preview-docs.mjs')] : []))); 27 | }; 28 | 29 | export { render } from './render'; 30 | -------------------------------------------------------------------------------- /packages/storybook/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig as StorybookConfigBase, WebRenderer, ArgsStoryFn, Args, AnnotatedStoryFn, PlayFunctionContext, StoryContext } from '@storybook/types'; 2 | import type { StorybookConfigVite, BuilderOptions } from '@storybook/builder-vite'; 3 | import { ArgTypesExtractor } from '@storybook/docs-tools'; 4 | import { SeqFlowComponent } from '@seqflow/seqflow'; 5 | 6 | export type FrameworkOptions = { 7 | builder?: BuilderOptions; 8 | }; 9 | 10 | 11 | /** 12 | * The interface for Storybook configuration in `main.ts` files. 13 | */ 14 | export type StorybookConfig = Omit< 15 | StorybookConfigBase, 16 | keyof StorybookConfigVite 17 | > & 18 | StorybookConfigVite; 19 | 20 | 21 | export type StoryFnHtmlReturnType = Promise; 22 | export interface SeqFlowJSRenderer extends WebRenderer { 23 | component: SeqFlowComponent; 24 | storyResult: StoryFnHtmlReturnType; 25 | } 26 | 27 | export interface Parameters { 28 | renderer: '@seqflow/storybook'; 29 | docs?: { 30 | story: { inline: boolean }; 31 | extractArgTypes?: ArgTypesExtractor; 32 | source: { 33 | type: 'dynamic'; 34 | language: 'jsx'; 35 | code: any; 36 | excludeDecorators: any; 37 | }; 38 | }; 39 | } 40 | 41 | export type StoryFn = SeqFlowComponent | { 42 | play: (context: StoryContext) => Promise | void; 43 | component?: SeqFlowComponent, 44 | args?: TArgs; 45 | } 46 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/Button.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface ButtonPropsType { 4 | /** The text */ 5 | label: string; 6 | /** color */ 7 | color?: "neutral" | "primary" | "secondary" | "accent" | "ghost" | "link"; 8 | /** is active? */ 9 | active?: boolean; 10 | state?: "info" | "success" | "warning" | "error"; 11 | outline?: boolean; 12 | size?: "lg" | "normal" | "sm" | "xs"; 13 | wide?: boolean; 14 | glass?: boolean; 15 | disabled?: boolean; 16 | loading?: boolean; 17 | // TODO 18 | // responsive, 19 | // html input type 20 | // square?: boolean, 21 | // circle?: boolean, 22 | } 23 | 24 | export async function Button( 25 | t: ButtonPropsType, 26 | c: Contexts, 27 | ) { 28 | } 29 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/Enum.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface EnumPropsType { 4 | strings?: "neutral" | "primary" | "secondary" | "accent" | "ghost" | "link"; 5 | numbers?: 1 | 2 | 3 | 4; 6 | booleans?: true | false; 7 | } 8 | 9 | export async function Enum( 10 | _: EnumPropsType, 11 | c: Contexts, 12 | ) { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/EnumOutsidePropertyAccessPropsType.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | interface Foo { 4 | strings?: "neutral" | "primary" | "secondary" | "accent" | "ghost" | "link"; 5 | numbers?: 1 | 2 | 3 | 4; 6 | booleans?: true | false; 7 | } 8 | 9 | export interface EnumOutsidePropertyAccessPropsType { 10 | strings?: Foo['strings'], 11 | numbers?: Foo['numbers']; 12 | booleans?: Foo['booleans']; 13 | } 14 | 15 | export async function EnumOutsidePropertyAccess( 16 | _: EnumOutsidePropertyAccessPropsType, 17 | c: Contexts, 18 | ) { } 19 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/ExportDefault.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface ExportDefaultPropsType { 4 | name: string 5 | } 6 | 7 | export default async function ExportDefault( 8 | _: ExportDefaultPropsType, 9 | c: Contexts, 10 | ) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/MultipleExports.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface Export1PropsType { 4 | name: string 5 | } 6 | export async function Export1( 7 | _: Export1PropsType, 8 | c: Contexts, 9 | ) { } 10 | 11 | export interface Export2PropsType { 12 | title: string 13 | } 14 | export async function Export2( 15 | _: Export2PropsType, 16 | c: Contexts, 17 | ) { } 18 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/Normal.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface NormalPropsType { 4 | /** The text */ 5 | label: string; 6 | /** is active? */ 7 | active: boolean; 8 | /** size in pixel */ 9 | size: number; 10 | } 11 | 12 | export async function Normal( 13 | _: NormalPropsType, 14 | c: Contexts, 15 | ) { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/Or2.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface Or2PropsType { 4 | /** the label */ 5 | label: string | JSX.Element; 6 | } 7 | export async function Or2( 8 | _: Or2PropsType, 9 | c: Contexts, 10 | ) { } 11 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/OrSimple.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface OrPropsType { 4 | /** the label */ 5 | label: string | number | boolean; 6 | } 7 | export async function Or( 8 | _: OrPropsType, 9 | c: Contexts, 10 | ) { } 11 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/SimpleEnum.ts: -------------------------------------------------------------------------------- 1 | import { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface SimpleEnumPropsType { 4 | strings?: "neutral" | "primary" | "secondary" | "accent" | "ghost" | "link"; 5 | // numbers?: 1 | 2 | 3 | 4; 6 | // booleans?: true | false; 7 | } 8 | 9 | export async function SimpleEnum( 10 | _: SimpleEnumPropsType, 11 | c: Contexts, 12 | ) { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/WithFunction.ts: -------------------------------------------------------------------------------- 1 | import type { Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface WithFunctionAsPropType { 4 | fn: () => void; 5 | } 6 | 7 | export async function WithFunctionAsProp( 8 | _: WithFunctionAsPropType, 9 | c: Contexts, 10 | ) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /packages/storybook/tests/fixtures/formField.ts: -------------------------------------------------------------------------------- 1 | import { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | 3 | export interface FormFieldPropsType { 4 | label: string | JSX.Element; 5 | errorMessage?: string | JSX.Element; 6 | size: number; 7 | } 8 | 9 | export async function FormField( 10 | {}: ComponentProps, 11 | c: Contexts, 12 | ) { 13 | } -------------------------------------------------------------------------------- /packages/storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | "types": ["node"], 6 | "resolveJsonModule": true, 7 | "baseUrl": ".", 8 | "incremental": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "jsx": "react", 11 | "moduleResolution": "Node", 12 | "module": "ES2020", 13 | "target": "ES2020", 14 | "skipLibCheck": true, 15 | "declaration": true, 16 | "allowSyntheticDefaultImports": true, 17 | "esModuleInterop": true, 18 | "isolatedModules": true, 19 | "lib": ["dom", "dom.iterable", "esnext"], 20 | "strict": true 21 | }, 22 | "include": ["src/**/*"], 23 | "exclude": ["src/**/*.test.*", "src/**/__testfixtures__/**"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/storybook/vite-plugin.js: -------------------------------------------------------------------------------- 1 | export * from './dist/vite-plugin.js' 2 | -------------------------------------------------------------------------------- /packages/website/.gitignore: -------------------------------------------------------------------------------- 1 | printed/ 2 | .vercel -------------------------------------------------------------------------------- /packages/website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | Run 4 | ``` 5 | pnpm install 6 | pnpm start 7 | ``` 8 | 9 | To build and see the output locally, run: 10 | ``` 11 | pnpm build 12 | npx serve dist 13 | ``` 14 | 15 | To deploy: 16 | ``` 17 | vercel build 18 | vercel deploy --prebuilt 19 | ``` -------------------------------------------------------------------------------- /packages/website/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": true, 4 | "rules": { 5 | "style": { 6 | "noNonNullAssertion": "off" 7 | }, 8 | "correctness": { 9 | "useJsxKeyInIterable": "off" 10 | } 11 | } 12 | }, 13 | "files": { 14 | "ignore": ["src/public"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm-run-all biome build", 9 | "biome": "biome ci ./src", 10 | "biome:check": "biome check --write src", 11 | "start": "vite -c vite.config.js", 12 | "build": "NODE_ENV=production vite build --emptyOutDir -c vite.config.js", 13 | "serve:static": "serve dist" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@biomejs/biome": "1.9.4", 20 | "@testing-library/dom": "^10.4.0", 21 | "@types/css-modules": "^1.0.5", 22 | "@types/html-minifier": "^4.0.5", 23 | "@types/jsdom": "^21.1.7", 24 | "@types/node": "^22.10.1", 25 | "@types/prismjs": "^1.26.5", 26 | "chokidar": "^4.0.1", 27 | "jsdom": "^25.0.1", 28 | "markdown-it": "^14.1.0", 29 | "msw": "^2.6.8", 30 | "npm-run-all2": "^7.0.0", 31 | "purgecss": "^7.0.2", 32 | "serve": "^14.2.4", 33 | "tsx": "^4.19.2", 34 | "typescript": "^5.7.2", 35 | "vite": "^6.0.3", 36 | "vite-plugin-checker": "^0.8.0", 37 | "vite-plugin-dts": "^4.3.0", 38 | "vite-plugin-markdown": "^2.2.0", 39 | "vitest": "^2.1.8" 40 | }, 41 | "dependencies": { 42 | "@fortawesome/fontawesome-svg-core": "^6.7.1", 43 | "@fortawesome/free-brands-svg-icons": "^6.7.1", 44 | "@fortawesome/free-regular-svg-icons": "^6.7.1", 45 | "@fortawesome/free-solid-svg-icons": "^6.7.1", 46 | "@oramacloud/client": "^2.1.4", 47 | "@seqflow/components": "workspace:*", 48 | "@seqflow/seqflow": "workspace:*", 49 | "html-minifier": "^4.0.0", 50 | "prismjs": "^1.29.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /packages/website/src/Main.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | @apply flex flex-col min-h-screen; 3 | } 4 | 5 | #main { 6 | @apply flex-grow overflow-y-auto relative; 7 | } 8 | 9 | .header { 10 | position: sticky; 11 | top: 0px; 12 | z-index: 50; 13 | } 14 | -------------------------------------------------------------------------------- /packages/website/src/Main.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "@seqflow/components"; 2 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 3 | import classes from "./Main.module.css"; 4 | import { Header } from "./components/Header"; 5 | import { Example } from "./pages/Example"; 6 | import { GetStarted } from "./pages/GetStarted"; 7 | import { Home } from "./pages/Home"; 8 | import { Why } from "./pages/Why"; 9 | import { Blog } from "./pages/blog/Blog"; 10 | 11 | function getComponent(path: string[]) { 12 | switch (path[0]) { 13 | case "get-started": 14 | return GetStarted; 15 | case "why": 16 | return Why; 17 | case "examples": 18 | return Example; 19 | case "blog": 20 | return Blog; 21 | default: 22 | return Home; 23 | } 24 | } 25 | 26 | export async function Main( 27 | _: ComponentProps, 28 | { component, app }: Contexts, 29 | ) { 30 | component._el.classList.add(classes.wrapper); 31 | const InitalComponent = getComponent(app.router.segments); 32 | component.renderSync( 33 | <> 34 |
35 |
36 | 37 |
38 |
39 | 45 |
46 | , 47 | ); 48 | 49 | const events = component.waitEvents(component.navigationEvent()); 50 | for await (const _ of events) { 51 | const Component = getComponent(app.router.segments); 52 | component.replaceChild("main", () => ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/website/src/components/Arrow.module.css: -------------------------------------------------------------------------------- 1 | .down-arrow { 2 | animation: bounce 1.75s infinite; 3 | 4 | cursor: pointer; 5 | } 6 | 7 | @keyframes bounce { 8 | 0%, 9 | 20%, 10 | 50%, 11 | 80%, 12 | 100% { 13 | transform: translateY(0); 14 | } 15 | 40% { 16 | transform: translateY(30px); 17 | } 18 | 60% { 19 | transform: translateY(15px); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/website/src/components/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import classes from "./Arrow.module.css"; 3 | 4 | export function ArrowSVG(_: ComponentProps, { component }: Contexts) { 5 | const arrowSVG = document.createElementNS( 6 | "http://www.w3.org/2000/svg", 7 | "svg", 8 | ); 9 | arrowSVG.id = "chevron-down"; 10 | const a = 128; 11 | arrowSVG.setAttribute("viewBox", "0 0 24 24"); 12 | arrowSVG.classList.add(classes["down-arrow"]); 13 | 14 | arrowSVG.setAttribute("width", "48"); 15 | arrowSVG.setAttribute("height", "48"); 16 | // arrowSVG.setAttribute("viewBox", "0 0 24 24"); 17 | arrowSVG.setAttribute("fill", "none"); 18 | arrowSVG.setAttribute("stroke", "currentColor"); 19 | arrowSVG.setAttribute("stroke-width", "2"); 20 | arrowSVG.setAttribute("stroke-linecap", "round"); 21 | arrowSVG.setAttribute("stroke-linejoin", "round"); 22 | const polyline = document.createElementNS( 23 | "http://www.w3.org/2000/svg", 24 | "polyline", 25 | ); 26 | polyline.setAttribute("points", "6 9 12 15 18 9"); 27 | arrowSVG.appendChild(polyline); 28 | 29 | component._el.appendChild(arrowSVG); 30 | } 31 | -------------------------------------------------------------------------------- /packages/website/src/components/ContentWithToc.module.css: -------------------------------------------------------------------------------- 1 | .top { 2 | aside { 3 | padding-left: calc(1.5rem * 0.5); 4 | min-width: 300px; 5 | position: relative; 6 | top: 0; 7 | width: 100%; 8 | 9 | .level-1 { 10 | font-weight: bold; 11 | font-size: 1.2rem; 12 | } 13 | .level-3 { 14 | padding-left: 1.5rem; 15 | } 16 | 17 | ul { 18 | list-style: none; 19 | padding: 0; 20 | margin: 0; 21 | 22 | li { 23 | padding: 0px; 24 | 25 | a { 26 | display: inline-block; 27 | width: 100%; 28 | padding: 8px; 29 | text-decoration: none; 30 | } 31 | 32 | a:hover { 33 | color: var(--color-primary); 34 | background-color: var(--bs-secondary-bg); 35 | } 36 | } 37 | } 38 | } 39 | 40 | .divider { 41 | display: flex; 42 | } 43 | } 44 | 45 | .link:hover { 46 | text-decoration: underline; 47 | } 48 | 49 | @media (min-width: 1300px) { 50 | .top { 51 | aside { 52 | position: fixed; 53 | float: left; 54 | top: 77px; 55 | display: block !important; 56 | overflow-y: auto; 57 | width: auto !important; 58 | } 59 | 60 | .divider { 61 | display: none; 62 | } 63 | } 64 | } 65 | 66 | h1, 67 | h2, 68 | h3, 69 | h4 { 70 | scroll-margin-top: 77px; 71 | } 72 | -------------------------------------------------------------------------------- /packages/website/src/components/Header.module.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 992px) { 2 | .github-name { 3 | display: none; 4 | } 5 | } 6 | 7 | .headerAnchor { 8 | @apply flex items-center gap-2; 9 | } 10 | 11 | .links { 12 | @apply flex items-center gap-4; 13 | } 14 | 15 | .submenu ul { 16 | background-color: #3a3d40; 17 | 18 | li { 19 | align-items: flex-start; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/website/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | /* background color */ 7 | --b1: 0.3 0 0; 8 | /* text color */ 9 | --bc: 0.82 0 0; 10 | 11 | /* primary color - button color */ 12 | /* --p: 0.6 0.22 257.22; */ 13 | --p: 0.86 0.01 282.17; 14 | --pc: 0.33 0 0; 15 | /* secondary color - button color */ 16 | --s: 0.64 0.18 146.74; 17 | --sc: 1 0 0; 18 | } 19 | 20 | .toolbar { 21 | right: .6em !important; 22 | } 23 | 24 | pre[class*="language-"] { 25 | border-width: 0px !important; 26 | box-shadow: none; 27 | } 28 | 29 | code[class*="language-"] { 30 | font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "Liberation Mono", 31 | Menlo, Monaco, Consolas, monospace !important; 32 | } 33 | 34 | .token.attr-name { 35 | color: #4aa078; 36 | } 37 | -------------------------------------------------------------------------------- /packages/website/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | SeqFlow JS 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/website/src/index.ts: -------------------------------------------------------------------------------- 1 | import "@seqflow/components/style.css"; 2 | import { start } from "@seqflow/seqflow"; 3 | import { Main } from "./Main"; 4 | import "./index.css"; 5 | 6 | start( 7 | document.getElementById("root")!, 8 | Main, 9 | {}, 10 | { 11 | log: console, 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /packages/website/src/pages/Example.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { ContentWithToc } from "../components/ContentWithToc"; 3 | import { html, toc } from "./Example.md"; 4 | 5 | export async function Example( 6 | _: ComponentProps, 7 | { component }: Contexts, 8 | ) { 9 | component.renderSync( 10 |
11 | 12 |
, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/website/src/pages/GetStarted.md: -------------------------------------------------------------------------------- 1 | 2 | This is the tutorial for the SeqFlow framework. It provides a step-by-step guide for creating a simple application that fetches a random quote from an API and displays it. 3 | 4 | The tutorial covers the following topics: 5 | - [the step 0](/get-started/prerequisites): how to create a new SeqFlow application. 6 | - [the step 1](/get-started/fetch-data): how to fetch data from an API and manage the component state. 7 | - [the step 2](/get-started/split-components): how to create a new component. 8 | - [the step 3](/get-started/refresh-quote): refresh the quote. 9 | - [the step 4](/get-started/configuration): how to configure the application. 10 | - [the step 5](/get-started/test): How to test the application. 11 | - [the step 6](/get-started/domain): Your first domain. 12 | - [the step 7](/get-started/conclusion): Conclusion. 13 | 14 | But let's start from the beginning. 15 | 16 | :::next::: 17 | {"label": "Prerequisites", "next": "/get-started/prerequisites"} 18 | :::end-next::: 19 | -------------------------------------------------------------------------------- /packages/website/src/pages/GetStarted_7conclusion.md: -------------------------------------------------------------------------------- 1 | In this last part of the tutorial, we will cover how to test our SeqFlow application. 2 | 3 | This tutorial taught us how to create a simple application using SeqFlow. We have covered the following topics: 4 | 5 | - How to create a new SeqFlow application. 6 | - How to fetch data from an API and manage the state of the application. 7 | - How to create a new component. 8 | - How to configure the application. 9 | - How to test the application. 10 | - How to create a domain. 11 | 12 | Any comments or suggestions are really appreciated. Feel free to open an issue on the [GitHub repository](https://github.com/allevo/seqflow-js/issues). 13 | 14 | :::next::: 15 | {"label": "See other examples", "next": "/examples"} 16 | :::end-next::: 17 | -------------------------------------------------------------------------------- /packages/website/src/pages/Why.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps, Contexts } from "@seqflow/seqflow"; 2 | import { ContentWithToc } from "../components/ContentWithToc"; 3 | import { html, toc } from "./Why.md"; 4 | 5 | export async function Why(_: ComponentProps, { component }: Contexts) { 6 | component.renderSync( 7 |
8 | 9 |
, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /packages/website/src/pages/blog/2024-11-24-theres-a-new-framework-in-town.md: -------------------------------------------------------------------------------- 1 | 2 | We’re thrilled to announce the release of SeqFlow version 1.0.0-rc.1! After months of development, testing, and fine-tuning, we’re introducing a new JavaScript framework for small and large applications. 3 | 4 | ## What is SeqFlow? 5 | 6 | SeqFlow is another JavaScript framework, but it’s not just another JavaScript framework: it’s a fresh approach to crafting maintainable applications. SeqFlow provides the tools you need without the bloat you don’t. 7 | 8 | SeqFlow is a modern JavaScript framework built with simple reading and predictability in mind. It’s designed to help you build applications without cognitive overload. 9 | 10 | SeqFlow is easy to use: a framework that works with you, not against you. Its API is clean and easy to understand, making it a joy to work with. With SeqFlow, you can focus on building your application, not fighting your framework. 11 | 12 | SeqFlow is a future-ready framework seamlessly integrating with modern paradigms: async/await is enabled by default! 13 | 14 | ## Get Started Today 15 | The journey begins here! Head over to https://seqflow.dev/ to: 16 | 17 | - Try our Quickstart Guide: Get up and running in minutes. 18 | - Explore Examples: See what’s possible with SeqFlow. 19 | - Join the Community: We’d love to hear your feedback, feature requests, and ideas! 20 | 21 | ## We need your help! 22 | 23 | This is just the beginning. We are glad to hear your feedback before moving to the full 1.0 release. We want to know what you love, what you hate, and what you’d like to see in the future. Feel free to open an issue on our GitHub repository! 24 | 25 | Every feedback, every issue opened, and every pull request submitted will help us shape the future of SeqFlow. 26 | 27 | ## A start not an end 28 | 29 | With this release, we’re excited to share our vision for the future of JavaScript frameworks. 30 | We are excited not for what we have built but for what we will build together. 31 | 32 | Welcome to SeqFlow — a new way to flow. -------------------------------------------------------------------------------- /packages/website/src/pages/blog/Blog.module.css: -------------------------------------------------------------------------------- 1 | .blogPost { 2 | max-width: 786px; 3 | } 4 | 5 | @media (max-width: 800px) { 6 | .blogPost { 7 | padding-left: 10px; 8 | padding-right: 10px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/website/src/public/images/Rectangle 5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/website/src/public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/website/src/public/images/favicon.ico -------------------------------------------------------------------------------- /packages/website/src/public/images/github.svg: -------------------------------------------------------------------------------- 1 | GitHub -------------------------------------------------------------------------------- /packages/website/src/public/images/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/website/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: (width: number, height: number) => string; 3 | export default content; 4 | } 5 | declare module "*.md" { 6 | export const html: string; 7 | export const toc: { 8 | title: string; 9 | slug: string; 10 | type: "h2" | "h3"; 11 | level: number; 12 | }[]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/components/*.{js,jsx,ts,tsx,css,md,mdx}', 5 | './src/pages/*.{js,jsx,ts,tsx,css,md,mdx}', 6 | './src/public/*.{js,jsx,ts,tsx,css,md,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | ringColor: "transparent", 11 | }, 12 | }, 13 | plugins: [ 14 | require("@tailwindcss/typography"), 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /packages/website/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "jsx": "react", 5 | "jsxFactory": "component.createDOMElement", 6 | "jsxFragmentFactory": "component.createDOMFragment", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "target": "ES2020", 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "rootDir": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | }, 18 | "include": [ 19 | "./src/**/*", 20 | "./tests/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /packages/website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": true, 3 | "headers": [ 4 | { 5 | "source": "/(.*)", 6 | "headers": [ 7 | { 8 | "key": "Cache-Control", 9 | "value": "public, max-age=0, must-revalidate" 10 | }, 11 | { 12 | "key": "X-Content-Type-Options", 13 | "value": "nosniff" 14 | }, 15 | { 16 | "key": "X-Frame-Options", 17 | "value": "DENY" 18 | }, 19 | { 20 | "key": "X-XSS-Protection", 21 | "value": "1; mode=block" 22 | }, 23 | { 24 | "key": "Content-Security-Policy", 25 | "value": "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; media-src 'none'; " 26 | } 27 | ] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/website/vite.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module '*.md' { 3 | const toc: { slug: string, title: string, type: 'h2' | 'h3', level: number }[]; 4 | const html: string; 5 | 6 | export { toc, html }; 7 | } 8 | 9 | declare module '*.svg' { 10 | const content: (w: number, h: number) => string; 11 | export default content; 12 | } 13 | -------------------------------------------------------------------------------- /packages/website/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | }, 7 | }) -------------------------------------------------------------------------------- /packages/website/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/website/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /packages/website/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allevo/seqflow-js/2ca01051e893c61625f16996dfd9f3c9ca4928f4/packages/website/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' 4 | - '!examples/gen-ai' 5 | - '!examples/orama-search-box' 6 | - '!tools/*' 7 | - 'apps/*' 8 | - '!apps/track-active-component' -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchPackagePatterns": ["*"], 9 | "groupName": "All Dependencies", 10 | "groupSlug": "all-dependencies" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "cache": false, 6 | "dependsOn": ["^build"] 7 | }, 8 | "lint": { 9 | "dependsOn": ["^lint"] 10 | }, 11 | "test": { 12 | "dependsOn": ["build"] 13 | }, 14 | "biome:check": { 15 | "dependsOn": ["^biome:check"] 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------