diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c4c1464 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: FabioGaming +ko_fi: fabiothefox +custom: https://paypal.me/fabiothefox diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..c0cec8c --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,53 @@ +name: Release Publish + +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + - run: npm run format:check + - run: npm run build + - run: npm run test:run + + - name: Pack release zip + id: pack + run: | + PACKAGE_TGZ=$(npm pack --silent) + PACKAGE_ZIP="${PACKAGE_TGZ%.tgz}.zip" + zip -9 "$PACKAGE_ZIP" "$PACKAGE_TGZ" + echo "package_zip=$PACKAGE_ZIP" >> "$GITHUB_OUTPUT" + + - name: Upload release asset + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.pack.outputs.package_zip }} + + - name: Setup GitHub Packages registry + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://npm.pkg.github.com + scope: "@fabiogaming" + + - name: Set GitHub Packages scope + run: npm pkg set name=@fabiogaming/jsx-utils + + - name: Publish to GitHub Packages + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index e0282a1..c344462 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,177 @@ -# . - SolidJS inspired JSX nodes for React ecosystems +# JSX Utils + +A zero dependency React library that adds [SolidJS](https://www.solidjs.com/) JSX-Node based control flow for UI designed to work in any React ecosystem by being platform & framework-agnostic + +--- + +### Table of Contents + +- [Basic Information](#1-basic-information) +- [Documentation](#2-documentation) + - [Getting Started](#21-getting-started) + - [Added JSX-Nodes with Examples](#22-added-jsx-nodes-with-examples) + - [Examples](#23-examples) +- [AI Usage & Transparency](#3-ai-usage--transparency) + +--- + +## 1. Basic Information + +JSX Utils is designed to be a framework-agnostic, zero dependency library you can drop into any React project, be it React through a bundler like Vite or Webpack, NextJS or React Native, these components are pure React logic components and do not contain any platform specific UI nodes + +JSX Utils was made to get rid of the annoying ternary based control flow in the UI, which just makes the JSX layer of React applications unreadable, instead JSX Utils sets on Solid's JSX-Node based UI control flow, as it is much more readable, easier to modify and overall better in terms of developer experience + +--- + +## 2. Documentation + +JSX-Utils features both this, as well as a built in documentation that you can view in any modern editor by hovering over the JSX-Nodes + +### 2.1 Getting Started + +To get started with JSX Utils you can install it both through NPM as well as GitHub's package registry + +NPM Install (recommended): `npm install @fabiothefox/jsx-utils` + +GitHub Install: `npm install @fabiogaming/jsx-utils --registry=https://npm.pkg.github.com` + +**Note** +For GitHub the installation might require some additional steps, such as: + +- Creating an `.npmrc` file with the following content: `@fabiogaming:registry=https://npm.pkg.github.com` at the root of your project +- Logging into the GitHub package registry: `npm login --registry=https://npm.pkg.github.com` + +### 2.2 Added JSX-Nodes With Examples + +#### Show + +Conditionally renders children when `when` is truthy, otherwise renders `fallback` + +```tsx +Loading...

}> +

Welcome back!

+ ... +
+``` + +#### Match + +Renders children only when `when` is truthy. +**Note:** [Match](#match) is typically used inside [Switch](#switch). For standalone conditional rendering, consider [Show](#show). + +```tsx + +

Admin panel

+ ... +
+``` + +#### Switch + +Renders the children of the first [Match](#match) child whose `when` prop is truthy. + +```tsx + + Loading... + Done! + Something went wrong. + +``` + +#### For + +Renders a list by mapping each item to JSX with a render function. + +```tsx + + {(user, index) => ( +
  • + {index + 1}. {user.name} +
  • + )} +
    +``` + +### 2.3 Examples + +#### User Profile with Status + +Display different UI based on fetch state without nested ternaries: + +```tsx +function UserProfile({ userId }) { + const [state, setState] = useState("loading"); + const [user, setUser] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + fetchUser(userId) + .then((data) => { + setUser(data); + setState("success"); + }) + .catch((err) => { + setError(err); + setState("error"); + }); + }, [userId]); + + return ( +
    + + +

    Loading profile...

    +
    + +

    {user.name}

    +

    {user.bio}

    +
    + +

    Failed to load profile: {error.message}

    +
    +
    +
    + ); +} +``` + +#### Task List with Empty State + +Render a list with a fallback when empty: + +```tsx +function TaskList({ tasks, filter }) { + const filtered = tasks.filter((t) => t.status === filter); + + return ( +
    +

    Tasks ({filtered.length})

    + 0} fallback={

    No tasks yet.

    }> + +
    +
    + ); +} +``` + +--- + +## 3. AI Usage & Transparency + +Copilot has been utilized throughout this project to assist in: + +- Code Reviews on Pull Requests as well as minor adjustments when issues were found in said PRs +- Writing parts of the documentation +- Clearing up confusions about concepts +- Writing the CI +- Slight assistance with mock data for Tests + + All other code as well as test logic has been **hand rolled** diff --git a/package-lock.json b/package-lock.json index 0d84ebc..368604d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "@fabi-dev/jsx-utils", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -16,17 +16,17 @@ "jsdom": "^29.1.1", "prettier": "^3.8.3", "publint": "^0.3.21", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.7" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": ">=20" }, "peerDependencies": { - "react": ">=16.8.0" + "react": ">=17.0.0" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index f787a7f..55eebc7 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,62 @@ { - "name": "@fabi-dev/jsx-utils", + "name": "@fabiothefox/jsx-utils", "version": "1.0.0", - "description": "", - "main": "index.js", + "description": "SolidJS-inspired JSX control flow utilities for React ecosystems", + "license": "MIT", + "author": "FabioTheFox@Fabidev", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/FabioGaming/jsx-utils.git" + }, + "homepage": "https://github.com/FabioGaming/jsx-utils#readme", + "bugs": { + "url": "https://github.com/FabioGaming/jsx-utils/issues" + }, + "keywords": [ + "react", + "react-native", + "nextjs", + "tsx", + "jsx", + "solidjs", + "solid", + "control-flow", + "show", + "switch", + "match", + "for", + "typescript", + "utility" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/FabioGaming" + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, "scripts": { + "build": "tsup", "test": "vitest", "test:run": "vitest run", - "build": "tsup", "format:check": "prettier src/ --check", "format:write": "prettier src/ --write" }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "commonjs", "peerDependencies": { "react": ">=17.0.0" }, @@ -25,13 +68,14 @@ "jsdom": "^29.1.1", "prettier": "^3.8.3", "publint": "^0.3.21", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.0", + "react-dom": "^19.2.0", "tsup": "^8.5.1", "typescript": "^6.0.3", "vitest": "^4.1.7" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - } + "node": "^20.19.0 || >=22.12.0" + }, + "packageManager": "npm@11.6.2" } diff --git a/src/components/For.tsx b/src/components/For.tsx index 9c8ff9c..21a595d 100644 --- a/src/components/For.tsx +++ b/src/components/For.tsx @@ -5,6 +5,20 @@ export type ForProps = { children: (item: T, index: number) => React.ReactNode; }; +/** + * Renders a list by mapping each item to JSX with a render function. + * + * @example + * ```tsx + * + * {(user, index) => ( + *
  • + * {index + 1}. {user.name} + *
  • + * )} + *
    + * ``` + */ export function For(props: ForProps) { return <>{props.each.map((item, i) => props.children(item, i))}; } diff --git a/src/components/Match.tsx b/src/components/Match.tsx index 01d886e..ba8a7a9 100644 --- a/src/components/Match.tsx +++ b/src/components/Match.tsx @@ -5,6 +5,20 @@ export type MatchProps = { children: React.ReactNode; }; +/** + * Renders children only when `when` is truthy. + * + * @remarks + * `Match` is typically used inside `Switch`. + * For standalone conditional rendering, consider `Show`. + * + * @example + * ```tsx + * + *

    Admin panel

    + *
    + * ``` + */ export function Match(props: MatchProps) { return <>{props.when ? props.children : null}; } diff --git a/src/components/Show.tsx b/src/components/Show.tsx index 106a0c2..4d83724 100644 --- a/src/components/Show.tsx +++ b/src/components/Show.tsx @@ -6,6 +6,16 @@ export type ShowProps = { children: React.ReactNode; }; +/** + * Conditionally renders children when `when` is truthy, otherwise renders `fallback`. + * + * @example + * ```tsx + * Loading...

    }> + *

    Welcome back!

    + *
    + * ``` + */ export function Show(props: ShowProps) { return props.when ? <>{props.children} : <>{props.fallback}; } diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 4119c77..b54326f 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -1,6 +1,18 @@ import React from "react"; import { Match, MatchProps } from "./Match"; +/** + * Renders the children of the first `Match` child whose `when` prop is truthy. + * + * @example + * ```tsx + * + * Loading... + * Done! + * Something went wrong. + * + * ``` + */ export function Switch(props: { children: React.ReactNode }) { const children = React.Children.toArray(props.children);