Upload docs
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"vue": {
|
||||
"src": "../../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "5e2bcecf",
|
||||
"fileHash": "505179d7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
@@ -19,13 +19,13 @@
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "f08e5a15",
|
||||
"fileHash": "3e2c9d39",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@theme/index": {
|
||||
"src": "../../../../../node_modules/vitepress/dist/client/theme-default/index.js",
|
||||
"file": "@theme_index.js",
|
||||
"fileHash": "442c9e5b",
|
||||
"fileHash": "127b4204",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,7 +8,11 @@ export default defineConfig({
|
||||
outDir: '../../docs',
|
||||
base: isDev ? '/absproxy/5174/sigpro/' : '/sigpro/',
|
||||
|
||||
// CONFIGURACIÓN DE VITE (Motor interno)
|
||||
// AÑADIDO: Head para estilos
|
||||
head: [
|
||||
['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/daisyui@5/dist/full.css' }]
|
||||
],
|
||||
|
||||
vite: {
|
||||
outDir: '../../docs',
|
||||
base: isDev ? '/absproxy/5174/sigpro/' : '/sigpro/',
|
||||
@@ -24,6 +28,8 @@ export default defineConfig({
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Guide', link: '/guide/getting-started' },
|
||||
{ text: 'Api', link: '/api/quick' },
|
||||
// AÑADIDO: UI en nav
|
||||
{ text: 'UI', link: '/ui/introduction' },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
@@ -40,6 +46,7 @@ export default defineConfig({
|
||||
{ text: 'Quick Start', link: '/api/quick' },
|
||||
{ text: '$', link: '/api/$' },
|
||||
{ text: '$.html', link: '/api/html' },
|
||||
{ text: '$.router', link: '/api/router' },
|
||||
{ text: '$.mount', link: '/api/mount' },
|
||||
{ text: 'Tags', link: '/api/tags' },
|
||||
]
|
||||
@@ -48,11 +55,8 @@ export default defineConfig({
|
||||
text: 'Plugins',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/plugins/quick' },
|
||||
{ text: '@core Router Plugin', link: '/plugins/core.router' },
|
||||
{ text: '@core UI Plugin', link: '/plugins/core.ui' },
|
||||
{ text: '@core UI Fetch', link: '/plugins/core.fetch' },
|
||||
{ text: '@core UI Storage', link: '/plugins/core.storage' },
|
||||
{ text: '@core UI Debug', link: '/plugins/core.debug' },
|
||||
{ text: '@core Debug', link: '/plugins/core.debug' },
|
||||
{ text: 'Custom', link: '/plugins/custom' },
|
||||
]
|
||||
},
|
||||
@@ -61,6 +65,19 @@ export default defineConfig({
|
||||
items: [
|
||||
{ text: 'Vite Plugin', link: '/vite/plugin' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'UI Components',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/ui/introduction' },
|
||||
{ text: 'Installation', link: '/ui/installation' },
|
||||
{ text: 'Button', link: '/ui/button' },
|
||||
{ text: 'Input', link: '/ui/input' },
|
||||
{ text: 'Form Components', link: '/ui/form' },
|
||||
{ text: 'Modal & Drawer', link: '/ui/modal' },
|
||||
{ text: 'Navigation', link: '/ui/navigation' },
|
||||
{ text: 'Layout', link: '/ui/layout' },
|
||||
]
|
||||
}
|
||||
],
|
||||
socialLinks: [
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
# The Reactive Core: `$( )`
|
||||
|
||||
The `$` function is the heart of **SigPro**. It is a **Unified Reactive Constructor** that handles state, derivations, and side effects through a single, consistent interface.
|
||||
The `$` function is the heart of **SigPro**. It is a **Unified Reactive Constructor** that handles state, derivations, and automatic persistence through a single, consistent interface.
|
||||
|
||||
## 1. The Constructor: `$( input )`
|
||||
## 1. The Constructor: `$( input, [key] )`
|
||||
|
||||
Depending on what you pass into `$( )`, SigPro creates a different type of reactive primitive:
|
||||
Depending on the arguments you pass, SigPro creates different reactive primitives:
|
||||
|
||||
| Input Type | Result | Internal Behavior |
|
||||
| :--- | :--- | :--- |
|
||||
| **Value** (String, Number, Object...) | **Signal** | Creates a piece of mutable state. |
|
||||
| **Function** | **Computed / Effect** | Creates a derived value that tracks dependencies. |
|
||||
| Argument | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **input** | `Value` / `Function` | **Yes** | Initial state or reactive logic. |
|
||||
| **key** | `string` | No | If provided, the signal **persists** in `localStorage`. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Signal (State)
|
||||
## 2. Signal (State & Persistence)
|
||||
|
||||
A **Signal** is a "box" that holds a value. It provides a getter/setter function to interact with that value.
|
||||
A **Signal** is a reactive "box" for data. SigPro now supports **Native Persistence**: if you provide a second argument (the `key`), the signal will automatically sync with `localStorage`.
|
||||
|
||||
* **When to use:** For data that changes over time (counters, user input, toggle states, API data).
|
||||
* **Syntax:** `const $state = $(initialValue);`
|
||||
* **Standard:** `const $count = $(0);`
|
||||
* **Persistent:** `const $theme = $("light", "app-theme");` (Restores value on page reload).
|
||||
|
||||
### Example:
|
||||
```javascript
|
||||
const $name = $("Alice");
|
||||
const $user = $("Guest", "session-user"); // Automatically saved/loaded
|
||||
|
||||
// Read the value (Getter)
|
||||
console.log($name()); // "Alice"
|
||||
// Read (Getter)
|
||||
console.log($user());
|
||||
|
||||
// Update the value (Setter)
|
||||
$name("Bob");
|
||||
// Update (Setter + Auto-save to Disk)
|
||||
$user("Alice");
|
||||
|
||||
// Update based on previous value
|
||||
$name(current => current + " Smith");
|
||||
// Functional Update
|
||||
$user(prev => prev.toUpperCase());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Computed (Derived State)
|
||||
|
||||
When you pass a **function** to `$( )` that **returns a value**, SigPro creates a **Computed Signal**. It automatically tracks which signals are used inside it and re-runs only when they change.
|
||||
When you pass a **function** that **returns a value**, SigPro creates a **Computed Signal**. It tracks dependencies and recalculates only when necessary.
|
||||
|
||||
* **When to use:** For values that depend on other signals (totals, filtered lists, formatted strings).
|
||||
* **Syntax:** `const $derived = $(() => logic);`
|
||||
|
||||
### Example:
|
||||
@@ -48,54 +47,56 @@ When you pass a **function** to `$( )` that **returns a value**, SigPro creates
|
||||
const $price = $(100);
|
||||
const $qty = $(2);
|
||||
|
||||
// Automatically tracks $price and $qty
|
||||
// Auto-tracks $price and $qty
|
||||
const $total = $(() => $price() * $qty());
|
||||
|
||||
console.log($total()); // 200
|
||||
|
||||
$qty(3); // $total updates to 300 automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Effects (Side Effects)
|
||||
## 4. Effects (Reactive Actions)
|
||||
|
||||
An **Effect** is a function passed to `$( )` that **does not return a value** (or returns `undefined`). SigPro treats this as a subscription that performs an action whenever its dependencies change.
|
||||
An **Effect** is a function that **does not return a value**. It performs an action (side effect) whenever the signals it "touches" change.
|
||||
|
||||
* **When to use:** For DOM manipulations, logging, or syncing with external APIs (LocalStorage, Fetch).
|
||||
* **When to use:** Logging, manual DOM tweaks, or syncing with external APIs.
|
||||
* **Syntax:** `$(() => { action });`
|
||||
|
||||
### Example:
|
||||
```javascript
|
||||
const $theme = $("light");
|
||||
const $status = $("online");
|
||||
|
||||
// This effect runs every time $theme changes
|
||||
// Runs every time $status changes
|
||||
$(() => {
|
||||
document.body.className = $theme();
|
||||
console.log("Theme updated to:", $theme());
|
||||
console.log("System status is now:", $status());
|
||||
});
|
||||
|
||||
$theme("dark"); // Logs: Theme updated to: dark
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Summary Table: Usage Guide
|
||||
|
||||
| Primitive | Logic Type | Returns Value? | Typical Use Case |
|
||||
| Primitive | Logic Type | Persistence? | Typical Use Case |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Signal** | Static | Yes (Mutable) | `const $user = $("Guest")` |
|
||||
| **Computed** | Read-only | Yes (Automatic) | `const $isLoggedIn = $(() => $user() !== "Guest")` |
|
||||
| **Effect** | Imperative | No | `$(() => localStorage.setItem('user', $user()))` |
|
||||
|
||||
|
||||
| **Signal** | Mutable State | **Yes** (Optional) | `$(0, 'counter')` |
|
||||
| **Computed** | Derived / Read-only | No | `$(() => $a() + $b())` |
|
||||
| **Effect** | Imperative Action | No | `$(() => alert($msg()))` |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pro Tip: Naming Convention
|
||||
In SigPro, we use the **`$` prefix** (e.g., `$count`) for variables that hold a reactive function. This makes it easy to distinguish between a standard variable and a reactive one at a glance:
|
||||
## 💡 Pro Tip: The Power of Native Persistence
|
||||
|
||||
In SigPro, you don't need external plugins for basic storage. By using the `key` parameter in a Signal, you gain:
|
||||
1. **Zero Boilerplate:** No more `JSON.parse(localStorage.getItem(...))`.
|
||||
2. **Instant Hydration:** The value is restored **before** the UI renders, preventing "flicker".
|
||||
3. **Atomic Safety:** Data is saved to disk exactly when the signal changes, ensuring your app state is always safe.
|
||||
|
||||
---
|
||||
|
||||
### Naming Convention
|
||||
We use the **`$` prefix** (e.g., `$count`) for reactive functions to distinguish them from static variables at a glance:
|
||||
|
||||
```javascript
|
||||
let count = 0; // Static
|
||||
const $count = $(0); // Reactive (Function)
|
||||
let count = 0; // Static
|
||||
const $count = $(0); // Reactive Signal
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Application Mounter: `$.mount`
|
||||
# Application Mounter: `$.router.mount` (Core)
|
||||
|
||||
The `$.mount` function is the entry point of your reactive world. It takes a **SigPro component** (or a plain DOM node) and injects it into the real document.
|
||||
The `$.mount` function is the entry point of your reactive world. It takes a **SigPro component** (or a plain DOM node) and injects it into the real document, bridging the gap between your logic and the browser.
|
||||
|
||||
## 1. Syntax: `$.mount(node, [target])`
|
||||
|
||||
@@ -14,18 +14,19 @@ The `$.mount` function is the entry point of your reactive world. It takes a **S
|
||||
## 2. Usage Scenarios
|
||||
|
||||
### A. The "Clean Slate" (Main Entry)
|
||||
In a modern app (like our `main.js` example), you usually want to control the entire page. By default, `$.mount` clears the target's existing HTML before mounting.
|
||||
In a modern app, you usually want to control the entire page. By default, `$.mount` clears the target's existing HTML before mounting your application.
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import { $ } from 'SigPro';
|
||||
import { $ } from 'sigpro';
|
||||
import App from './App.js';
|
||||
|
||||
$.mount(App); // Mounts to <body> by default
|
||||
// SigPro: No .then() needed, global tags are ready immediately
|
||||
$.mount(App);
|
||||
```
|
||||
|
||||
### B. Targeting a Specific Container
|
||||
If you have an existing HTML structure and only want **SigPro** to manage a specific part (like a `#root` div), pass a CSS selector or a reference.
|
||||
If you have an existing HTML structure and want **SigPro** to manage only a specific section (like a `#root` div), pass a CSS selector or a reference.
|
||||
|
||||
```html
|
||||
<div id="sidebar"></div>
|
||||
@@ -33,7 +34,7 @@ If you have an existing HTML structure and only want **SigPro** to manage a spec
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Local mount to a specific ID
|
||||
// Mount to a specific ID
|
||||
$.mount(MyComponent, '#app-root');
|
||||
|
||||
// Or using a direct DOM reference
|
||||
@@ -43,11 +44,11 @@ $.mount(SidebarComponent, sidebar);
|
||||
|
||||
---
|
||||
|
||||
## 3. Mounting with Pure HTML
|
||||
One of SigPro's strengths is that it works perfectly alongside "Old School" HTML. You can create a reactive "island" inside a static page.
|
||||
## 3. Creating "Reactive Islands"
|
||||
One of SigPro's strengths is its ability to work alongside "Old School" static HTML. You can inject a reactive widget into any part of a legacy page.
|
||||
|
||||
```javascript
|
||||
// A small reactive widget in a static .js file
|
||||
// A small reactive widget
|
||||
const CounterWidget = () => {
|
||||
const $c = $(0);
|
||||
return button({ onclick: () => $c(v => v + 1) }, [
|
||||
@@ -55,41 +56,40 @@ const CounterWidget = () => {
|
||||
]);
|
||||
};
|
||||
|
||||
// Mount it into an existing div in your HTML
|
||||
// Mount it into an existing div in your static HTML
|
||||
$.mount(CounterWidget, '#counter-container');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. How it Works (The "Wipe" Logic)
|
||||
When `$.mount` is called, it performs two critical steps:
|
||||
|
||||
1. **Clearance:** It sets `target.innerHTML = ''`. This ensures no "zombie" HTML from previous renders or static placeholders interferes with your app.
|
||||
2. **Injection:** It appends your component. If you passed a **Function**, it executes it first to get the DOM node.
|
||||
|
||||
## 4. How it Works (Lifecycle)
|
||||
When `$.mount` is called, it performs three critical steps:
|
||||
|
||||
1. **Resolution:** If you passed a **Function**, it executes it once to generate the initial DOM node.
|
||||
2. **Clearance:** It sets `target.innerHTML = ''`. This prevents "zombie" HTML or static placeholders from interfering with your app.
|
||||
3. **Injection:** It appends the resulting node to the target.
|
||||
|
||||
---
|
||||
|
||||
## 5. Global vs. Local Scope
|
||||
|
||||
### Global (The "Framework" Way)
|
||||
In a standard Vite/ESM project, you initialize SigPro globally in `main.js`. This makes the `$` and the tag helpers (`div`, `button`, etc.) available everywhere in your project.
|
||||
In a standard Vite project, you initialize SigPro in your entry file. This makes `$` and the tag helpers (`div`, `button`, etc.) available globally for a clean, declarative developer experience.
|
||||
|
||||
```javascript
|
||||
// main.js - Global Initialization
|
||||
import 'SigPro';
|
||||
// src/main.js
|
||||
import { $ } from 'sigpro';
|
||||
|
||||
// Now any other file can just use:
|
||||
// Any component in any file can now use:
|
||||
$.mount(() => h1("Global App"));
|
||||
```
|
||||
|
||||
### Local (The "Library" Way)
|
||||
If you are worried about polluting the global `window` object, you can import and use SigPro locally within a specific module.
|
||||
If you prefer to avoid polluting the `window` object, you can import and use SigPro locally within specific modules.
|
||||
|
||||
```javascript
|
||||
// widget.js - Local usage
|
||||
import { $ } from 'SigPro';
|
||||
// widget.js
|
||||
import { $ } from 'sigpro';
|
||||
|
||||
const myNode = $.html('div', 'Local Widget');
|
||||
$.mount(myNode, '#widget-target');
|
||||
@@ -97,12 +97,11 @@ $.mount(myNode, '#widget-target');
|
||||
|
||||
---
|
||||
|
||||
### Summary Cheat Sheet
|
||||
## 6. Summary Cheat Sheet
|
||||
|
||||
| Goal | Code |
|
||||
| :--- | :--- |
|
||||
| **Mount to body** | `$.mount(App)` |
|
||||
| **Mount to ID** | `$.mount(App, '#id')` |
|
||||
| **Mount to Element** | `$.mount(App, myElement)` |
|
||||
| **Reactive Widget** | `$.mount(() => div("Hi"), '#widget')` |
|
||||
|
||||
| **Direct Function** | `$.mount(() => div("Hi"), '#widget')` |
|
||||
|
||||
106
src/docs/api/router.md
Normal file
106
src/docs/api/router.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Routing Engine: `$.router`
|
||||
|
||||
The `$.router` is SigPro's high-performance, hash-based navigation system. It connects the browser's URL directly to your reactive signals, enabling seamless page transitions without full reloads.
|
||||
|
||||
## 1. Core Features
|
||||
|
||||
* **Hash-based:** Works everywhere without special server configuration (using `#/path`).
|
||||
* **Lazy Loading:** Pages are only downloaded when the user visits the route, keeping the initial bundle under 2KB.
|
||||
* **Reactive:** The view updates automatically and surgically when the hash changes.
|
||||
* **Dynamic Routes:** Built-in support for parameters like `/user/:id`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Syntax: `$.router(routes)`
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **routes** | `Array<Object>` | **Yes** | An array of route definitions `{ path, component }`. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Setting Up Routes
|
||||
|
||||
In your `App.js` (or a dedicated routes file), define your navigation map and inject it into your layout.
|
||||
|
||||
```javascript
|
||||
const routes = [
|
||||
{ path: '/', component: () => h1("Home Page") },
|
||||
{
|
||||
path: '/admin',
|
||||
// Lazy Loading: This file is only fetched when needed
|
||||
component: () => import('./pages/Admin.js')
|
||||
},
|
||||
{ path: '/user/:id', component: (params) => h2(`User ID: ${params.id}`) },
|
||||
{ path: '*', component: () => div("404 - Page Not Found") }
|
||||
];
|
||||
|
||||
export default () => div([
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
button({ onclick: () => $.router.go('/') }, "Home"),
|
||||
button({ onclick: () => $.router.go('/admin') }, "Admin")
|
||||
])
|
||||
]),
|
||||
// The router returns a reactive div that swaps content
|
||||
main($.router(routes))
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Navigation (`$.router.go`)
|
||||
|
||||
To move between pages programmatically (e.g., inside an `onclick` event or after a successful fetch), use the `$.router.go` helper.
|
||||
|
||||
```javascript
|
||||
button({
|
||||
onclick: () => $.router.go('/admin')
|
||||
}, "Go to Admin")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. How it Works (Under the Hood)
|
||||
|
||||
The router tracks the `window.location.hash` and uses a reactive signal to trigger a re-render of the specific area where `$.router(routes)` is placed.
|
||||
|
||||
1. **Match:** It filters your route array to find the best fit, handling dynamic segments (`:id`) and fallbacks (`*`).
|
||||
2. **Resolve:** * If it's a standard function, it executes it immediately.
|
||||
* If it's a **Promise** (via `import()`), it renders a temporary `Loading...` state and swaps the content once the module arrives.
|
||||
3. **Inject:** It replaces the previous DOM node with the new page content surgically using `replaceWith()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration with UI Components
|
||||
|
||||
Since the router is reactive, you can easily create "active" states in your navigation menus by checking the current hash.
|
||||
|
||||
```javascript
|
||||
// Example of a reactive navigation link
|
||||
const NavLink = (path, label) => {
|
||||
const $active = $(() => window.location.hash === `#${path}`);
|
||||
|
||||
return button({
|
||||
$class: () => $active() ? 'nav-active' : 'nav-link',
|
||||
onclick: () => $.router.go(path)
|
||||
}, label);
|
||||
};
|
||||
|
||||
nav([
|
||||
NavLink('/', 'Home'),
|
||||
NavLink('/settings', 'Settings')
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary: Route Component Types
|
||||
|
||||
| Component Type | Behavior |
|
||||
| :--- | :--- |
|
||||
| **HTMLElement** | Rendered immediately. |
|
||||
| **Function `(params) => ...`** | Executed with URL parameters and rendered. |
|
||||
| **Promise / `import()`** | Triggers **Lazy Loading** with a loading state. |
|
||||
| **String / Number** | Rendered as simple text inside a span. |
|
||||
@@ -8,18 +8,18 @@ You can install SigPro via your favorite package manager:
|
||||
|
||||
::: code-group
|
||||
```bash [npm]
|
||||
npm install SigPro
|
||||
npm install sigpro
|
||||
````
|
||||
|
||||
```bash [pnpm]
|
||||
pnpm add SigPro
|
||||
pnpm add sigpro
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add SigPro
|
||||
yarn add sigpro
|
||||
```
|
||||
```bash [bun]
|
||||
bun add SigPro
|
||||
bun add sigpro
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
# Extending SigPro: `$.plugin`
|
||||
|
||||
The plugin system is the engine's way of growing. It allows you to inject new functionality directly into the `$` object or load external resources.
|
||||
The plugin system is the engine's modular backbone. It allows you to inject new functionality directly into the `$` object, register custom global tags, or load external libraries seamlessly.
|
||||
|
||||
## 1. How Plugins Work
|
||||
|
||||
A plugin in **SigPro** is simply a function that receives the core instance. When you run `$.plugin(MyPlugin)`, the engine hands over the `$` object so the plugin can attach new methods or register global tags (like `div()`, `span()`, etc.).
|
||||
A plugin in **SigPro** is a function that receives the core instance. When you call `$.plugin(MyPlugin)`, the engine hands over the `$` object so the plugin can attach new methods or extend the reactive system.
|
||||
|
||||
### Functional Plugin Example
|
||||
```javascript
|
||||
// A plugin that adds a simple logger to any signal
|
||||
// A plugin that adds a simple watcher to any signal
|
||||
const Logger = ($) => {
|
||||
$.watch = (target, label = "Log") => {
|
||||
$(() => console.log(`[${label}]:`, target()));
|
||||
@@ -23,79 +23,72 @@ $.watch($count, "Counter"); // Now available globally via $
|
||||
|
||||
---
|
||||
|
||||
## 2. Initialization Patterns
|
||||
## 2. Initialization Patterns (SigPro)
|
||||
|
||||
Since plugins often set up global variables (like the HTML tags), the order of initialization is critical. Here are the two ways to start your app:
|
||||
Thanks to the **Synchronous Tag Engine**, you no longer need complex `import()` nesting. Global tags like `div()`, `span()`, and `button()` are ready the moment you import the Core.
|
||||
|
||||
### Option A: The "Safe" Async Start (Recommended)
|
||||
This is the most robust way. It ensures all global tags (`div`, `button`, etc.) are created **before** your App code is even read by the browser.
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
import { $ } from 'sigpro';
|
||||
import { UI, Router } from 'sigpro/plugins';
|
||||
|
||||
// 1. Load plugins first
|
||||
$.plugin([UI, Router]).then(() => {
|
||||
|
||||
// 2. Import your app only after the environment is ready
|
||||
import('./App.js').then(appFile => {
|
||||
const MyApp = appFile.default;
|
||||
$.mount(MyApp, '#app');
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
### Option B: Static Start (No Global Tags)
|
||||
Use this only if you prefer **not** to use global tags and want to use `$.html` directly in your components. This allows for standard static imports.
|
||||
### The "Natural" Start (Recommended)
|
||||
This is the standard way to build apps. It's clean, readable, and supports standard ESM imports.
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
import { $ } from 'sigpro';
|
||||
import { UI } from 'sigpro/plugins';
|
||||
import MyApp from './App.js'; // Static import works here
|
||||
import App from './App.js'; // Static import works perfectly!
|
||||
|
||||
// 1. Register plugins
|
||||
$.plugin(UI);
|
||||
$.mount(MyApp, '#app');
|
||||
|
||||
// 2. Mount your app directly
|
||||
$.mount(App, '#app');
|
||||
```
|
||||
> **Warning:** In this mode, if `App.js` uses `div()` instead of `$.html('div')`, it will throw a `ReferenceError`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Resource Plugins (External Scripts)
|
||||
|
||||
You can pass a **URL** or an **Array of URLs**. SigPro will inject them as `<script>` tags and return a Promise that resolves when the scripts are fully loaded and executed.
|
||||
You can pass a **URL** or an **Array of URLs**. SigPro will inject them as `<script>` tags and return a **Promise** that resolves when the scripts are fully loaded. This is perfect for integrating heavy third-party libraries only when needed.
|
||||
|
||||
```javascript
|
||||
// Loading external libraries as plugins
|
||||
await $.plugin([
|
||||
$.plugin([
|
||||
'https://cdn.jsdelivr.net/npm/chart.js',
|
||||
'https://cdn.example.com/custom-ui-lib.js'
|
||||
]);
|
||||
|
||||
console.log("External resources are ready to use!");
|
||||
]).then(() => {
|
||||
console.log("External resources are ready to use!");
|
||||
$.mount(DashboardApp);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Polymorphic Loading Reference
|
||||
|
||||
The `$.plugin` method adapts to whatever you throw at it:
|
||||
The `$.plugin` method is smart; it adapts its behavior based on the input type:
|
||||
|
||||
| Input Type | Action | Behavior |
|
||||
| :--- | :--- | :--- |
|
||||
| **Function** | Executes `fn($)` | Synchronous / Immediate |
|
||||
| **String (URL)** | Injects `<script src="...">` | Asynchronous (Returns Promise) |
|
||||
| **Array** | Processes each item in the list | Returns Promise if any item is Async |
|
||||
| **Function** | Executes `fn($)` | **Synchronous**: Immediate availability. |
|
||||
| **String (URL)** | Injects `<script src="...">` | **Asynchronous**: Returns a Promise. |
|
||||
| **Array** | Processes each item in the list | Returns a Promise if any item is a URL. |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pro Tip: Why the `.then()`?
|
||||
## 💡 Pro Tip: When to use `.then()`?
|
||||
|
||||
Using `$.plugin([...]).then(...)` is like giving your app a "Pre-flight Check". It guarantees that:
|
||||
1. All reactive methods are attached.
|
||||
2. Global HTML tags are defined.
|
||||
3. External libraries (like Chart.js) are loaded.
|
||||
4. **The result:** Your components are cleaner, smaller, and error-free.
|
||||
In **SigPro**, you only need `.then()` in two specific cases:
|
||||
1. **External Assets:** When loading a plugin via a URL (CDN).
|
||||
2. **Strict Dependency:** If your `App.js` requires a variable that is strictly defined inside an asynchronous external script (like `window.Chart`).
|
||||
|
||||
For everything else (UI components, Router, Local State), just call `$.plugin()` and continue with your code. It's that simple.
|
||||
|
||||
---
|
||||
|
||||
### Summary Cheat Sheet
|
||||
|
||||
| Goal | Code |
|
||||
| :--- | :--- |
|
||||
| **Local Plugin** | `$.plugin(myPlugin)` |
|
||||
| **Multiple Plugins** | `$.plugin([UI, Router])` |
|
||||
| **External Library** | `$.plugin('https://...').then(...)` |
|
||||
| **Hybrid Load** | `$.plugin([UI, 'https://...']).then(...)` |
|
||||
|
||||
223
src/docs/public/sigpro.js
Normal file
223
src/docs/public/sigpro.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* SigPro - Atomic Unified Reactive Engine
|
||||
* A lightweight, fine-grained reactivity system with built-in routing and plugin support.
|
||||
* @author Gemini & User
|
||||
*/
|
||||
(() => {
|
||||
/** @type {Function|null} Internal tracker for the currently executing reactive effect. */
|
||||
let activeEffect = null;
|
||||
|
||||
/**
|
||||
* @typedef {Object} SigPro
|
||||
* @property {function(any|function, string=): Function} $ - Creates a Signal or Computed. Optional key for localStorage.
|
||||
* @property {function(string, Object=, any=): HTMLElement} html - Creates a reactive HTML element.
|
||||
* @property {function((HTMLElement|function), (HTMLElement|string)=): void} mount - Mounts a component to the DOM.
|
||||
* @property {function(Array<Object>): HTMLElement} router - Initializes a hash-based router.
|
||||
* @property {function(string): void} router.go - Programmatic navigation to a hash path.
|
||||
* @property {function((function|string|Array<string>)): (Promise<SigPro>|SigPro)} plugin - Extends SigPro or loads external scripts.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a Signal (state) or a Computed/Effect (reaction).
|
||||
* Supports optional persistence in localStorage.
|
||||
* * @param {any|function} initial - Initial value or a function for computed logic.
|
||||
* @param {string} [key] - Optional localStorage key for automatic state persistence.
|
||||
* @returns {Function} A reactive accessor/mutator function.
|
||||
*/
|
||||
const $ = (initial, key) => {
|
||||
const subs = new Set();
|
||||
|
||||
if (typeof initial === 'function') {
|
||||
let cached;
|
||||
const runner = () => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = runner;
|
||||
try {
|
||||
const next = initial();
|
||||
if (!Object.is(cached, next)) {
|
||||
cached = next;
|
||||
subs.forEach(s => s());
|
||||
}
|
||||
} finally { activeEffect = prev; }
|
||||
};
|
||||
runner();
|
||||
return () => {
|
||||
if (activeEffect) subs.add(activeEffect);
|
||||
return cached;
|
||||
};
|
||||
}
|
||||
|
||||
if (key) {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved !== null) {
|
||||
try { initial = JSON.parse(saved); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
|
||||
if (!Object.is(initial, next)) {
|
||||
initial = next;
|
||||
if (key) localStorage.setItem(key, JSON.stringify(initial));
|
||||
subs.forEach(s => s());
|
||||
}
|
||||
}
|
||||
if (activeEffect) subs.add(activeEffect);
|
||||
return initial;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hyperscript engine to render reactive HTML nodes.
|
||||
* @param {string} tag - The HTML tag name (e.g., 'div', 'button').
|
||||
* @param {Object} [props] - Attributes, events (onclick), or reactive props ($value, $class).
|
||||
* @param {any} [content] - String, Node, Array of nodes, or reactive function.
|
||||
* @returns {HTMLElement} A live DOM element linked to SigPro signals.
|
||||
*/
|
||||
$.html = (tag, props = {}, content = []) => {
|
||||
const el = document.createElement(tag);
|
||||
if (typeof props !== 'object' || props instanceof Node || Array.isArray(props) || typeof props === 'function') {
|
||||
content = props;
|
||||
props = {};
|
||||
}
|
||||
|
||||
for (let [key, val] of Object.entries(props)) {
|
||||
if (key.startsWith('on')) {
|
||||
el.addEventListener(key.toLowerCase().slice(2), val);
|
||||
} else if (key.startsWith('$')) {
|
||||
const attr = key.slice(1);
|
||||
// Two-way binding for inputs
|
||||
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
|
||||
const ev = attr === 'checked' ? 'change' : 'input';
|
||||
el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value));
|
||||
}
|
||||
// Reactive attribute update
|
||||
$(() => {
|
||||
const v = typeof val === 'function' ? val() : val;
|
||||
if (attr === 'value' || attr === 'checked') el[attr] = v;
|
||||
else if (typeof v === 'boolean') el.toggleAttribute(attr, v);
|
||||
else el.setAttribute(attr, v ?? '');
|
||||
});
|
||||
} else el.setAttribute(key, val);
|
||||
}
|
||||
|
||||
const append = (c) => {
|
||||
if (Array.isArray(c)) return c.forEach(append);
|
||||
if (typeof c === 'function') {
|
||||
const node = document.createTextNode('');
|
||||
$(() => {
|
||||
const res = c();
|
||||
if (res instanceof Node) {
|
||||
if (node.parentNode) node.replaceWith(res);
|
||||
} else {
|
||||
node.textContent = res ?? '';
|
||||
}
|
||||
});
|
||||
return el.appendChild(node);
|
||||
}
|
||||
el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
|
||||
};
|
||||
append(content);
|
||||
return el;
|
||||
};
|
||||
|
||||
const tags = ['div', 'span', 'p', 'button', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'label', 'section', 'nav', 'main', 'header', 'footer', 'input', 'form', 'img', 'select', 'option', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'canvas', 'video', 'audio'];
|
||||
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
|
||||
|
||||
/**
|
||||
* Application mounter.
|
||||
* @param {HTMLElement|function} node - Root component or element to mount.
|
||||
* @param {HTMLElement|string} [target=document.body] - Target element or CSS selector.
|
||||
*/
|
||||
$.mount = (node, target = document.body) => {
|
||||
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
||||
if (el) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(typeof node === 'function' ? node() : node);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a reactive hash-based router.
|
||||
* Maps URL hash changes to component rendering and supports Vite's dynamic imports.
|
||||
* * @param {Array<{path: string, component: Function|Promise|HTMLElement}>} routes - Array of route objects.
|
||||
* @returns {HTMLElement} A reactive div container that swaps content based on the current hash.
|
||||
*/
|
||||
$.router = (routes) => {
|
||||
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||
|
||||
return $.html('div', [
|
||||
() => {
|
||||
const current = sPath();
|
||||
const cP = current.split('/').filter(Boolean);
|
||||
|
||||
const route = routes.find(r => {
|
||||
const rP = r.path.split('/').filter(Boolean);
|
||||
if (rP.length !== cP.length) return false;
|
||||
return rP.every((part, i) => part.startsWith(':') || part === cP[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (!route) return $.html('h1', "404 - Not Found");
|
||||
|
||||
const rP = route.path.split('/').filter(Boolean);
|
||||
const params = {};
|
||||
rP.forEach((part, i) => {
|
||||
if (part.startsWith(':')) params[part.slice(1)] = cP[i];
|
||||
});
|
||||
|
||||
const result = typeof route.component === 'function' ? route.component(params) : route.component;
|
||||
|
||||
if (result instanceof Promise) {
|
||||
const $lazyNode = $($.html('span', "Loading..."));
|
||||
result.then(m => {
|
||||
const content = m.default || m;
|
||||
const finalView = typeof content === 'function' ? content(params) : content;
|
||||
$lazyNode(finalView);
|
||||
});
|
||||
return () => $lazyNode();
|
||||
}
|
||||
|
||||
return result instanceof Node ? result : $.html('span', String(result));
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Programmatically navigates to a specific path using the hash.
|
||||
* * @param {string} path - The destination path (e.g., '/home' or 'settings').
|
||||
* @example
|
||||
* $.router.go('/profile/42');
|
||||
*/
|
||||
$.router.go = (path) => {
|
||||
window.location.hash = path.startsWith('/') ? path : `/${path}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Polymorphic Plugin System.
|
||||
* Registers internal functions or loads external .js files as plugins.
|
||||
* @param {function|string|Array<string>} source - Plugin function or URL(s).
|
||||
* @returns {Promise<SigPro>|SigPro} Resolves with the $ instance after loading or registering.
|
||||
*/
|
||||
$.plugin = (source) => {
|
||||
if (typeof source === 'function') {
|
||||
source($);
|
||||
return $;
|
||||
}
|
||||
const urls = Array.isArray(source) ? source : [source];
|
||||
return Promise.all(urls.map(url => new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
console.log(`%c[SigPro] Plugin Loaded: ${url}`, "color: #51cf66; font-weight: bold;");
|
||||
resolve();
|
||||
};
|
||||
script.onerror = () => reject(new Error(`[SigPro] Failed to load: ${url}`));
|
||||
document.head.appendChild(script);
|
||||
}))).then(() => $);
|
||||
};
|
||||
|
||||
window.$ = $;
|
||||
})();
|
||||
114
src/docs/ui/button.md
Normal file
114
src/docs/ui/button.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Button Component
|
||||
|
||||
The `_button` component creates reactive buttons with built-in support for loading states, icons, badges, and disabled states.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
<div id="basic-button-demo"></div>
|
||||
|
||||
```javascript
|
||||
_button({ onclick: () => alert('Clicked!') }, 'Click Me')
|
||||
```
|
||||
|
||||
## Loading State
|
||||
|
||||
The `$loading` signal automatically shows a spinner and disables the button.
|
||||
|
||||
<div id="loading-button-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $loading = $(false)
|
||||
|
||||
_button({
|
||||
$loading: $loading,
|
||||
onclick: async () => {
|
||||
$loading(true)
|
||||
await saveData()
|
||||
$loading(false)
|
||||
}
|
||||
}, 'Save')
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
Add icons to buttons using the `icon` prop.
|
||||
|
||||
<div id="icon-button-demo"></div>
|
||||
|
||||
```javascript
|
||||
_button({ icon: '⭐' }, 'Favorite')
|
||||
_button({ icon: '💾' }, 'Save')
|
||||
_button({ icon: '🗑️', class: 'btn-error' }, 'Delete')
|
||||
```
|
||||
|
||||
## Badges
|
||||
|
||||
Add badges to buttons for notifications or status indicators.
|
||||
|
||||
<div id="badge-button-demo"></div>
|
||||
|
||||
```javascript
|
||||
_button({ badge: '3' }, 'Notifications')
|
||||
_button({ badge: 'New', badgeClass: 'badge-secondary' }, 'Update Available')
|
||||
```
|
||||
|
||||
## Button Variants
|
||||
|
||||
Use daisyUI classes to style your buttons.
|
||||
|
||||
<div id="variant-button-demo"></div>
|
||||
|
||||
```javascript
|
||||
_button({ class: 'btn-primary' }, 'Primary')
|
||||
_button({ class: 'btn-secondary' }, 'Secondary')
|
||||
_button({ class: 'btn-outline' }, 'Outline')
|
||||
_button({ class: 'btn-sm' }, 'Small')
|
||||
```
|
||||
|
||||
## Counter Example
|
||||
|
||||
<div id="counter-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $count = $(0)
|
||||
|
||||
_button({
|
||||
onclick: () => $count($count() + 1),
|
||||
icon: '🔢'
|
||||
}, () => `Count: ${$count()}`)
|
||||
```
|
||||
|
||||
## Async Action Example
|
||||
|
||||
<div id="async-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $saving = $(false)
|
||||
const $success = $(false)
|
||||
|
||||
_button({
|
||||
$loading: $saving,
|
||||
icon: '💾',
|
||||
onclick: async () => {
|
||||
$saving(true)
|
||||
await saveToDatabase()
|
||||
$saving(false)
|
||||
$success(true)
|
||||
setTimeout(() => $success(false), 2000)
|
||||
}
|
||||
}, 'Save')
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `$loading` | `Signal<boolean>` | Shows spinner and disables button |
|
||||
| `$disabled` | `Signal<boolean>` | Disables the button |
|
||||
| `icon` | `string \| Node` | Icon to display before text |
|
||||
| `badge` | `string` | Badge text to display |
|
||||
| `badgeClass` | `string` | Additional CSS classes for badge |
|
||||
| `class` | `string \| function` | Additional CSS classes |
|
||||
| `onclick` | `function` | Click event handler |
|
||||
| `type` | `string` | Button type ('button', 'submit', etc.) |
|
||||
|
||||
112
src/docs/ui/form.md
Normal file
112
src/docs/ui/form.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Form Components
|
||||
|
||||
SigPro UI provides a complete set of reactive form components including select dropdowns, checkboxes, radio buttons, and range sliders.
|
||||
|
||||
## Select Dropdown (`_select`)
|
||||
|
||||
Creates a reactive dropdown select with options.
|
||||
|
||||
<div id="select-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $role = $('user')
|
||||
|
||||
_select({
|
||||
label: 'User Role',
|
||||
options: [
|
||||
{ value: 'admin', label: 'Administrator' },
|
||||
{ value: 'user', label: 'Standard User' },
|
||||
{ value: 'guest', label: 'Guest' }
|
||||
],
|
||||
$value: $role
|
||||
})
|
||||
```
|
||||
|
||||
## Checkbox (`_checkbox`)
|
||||
|
||||
Reactive checkbox with label.
|
||||
|
||||
<div id="checkbox-demo"></div>
|
||||
|
||||
|
||||
```javascript
|
||||
const $agreed = $(false)
|
||||
|
||||
_checkbox({
|
||||
label: 'I agree to the terms and conditions',
|
||||
$value: $agreed
|
||||
})
|
||||
```
|
||||
|
||||
## Radio Button (`_radio`)
|
||||
|
||||
Radio buttons for selecting one option from a group.
|
||||
|
||||
<div id="radio-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $payment = $('credit')
|
||||
|
||||
_radio({ name: 'payment', label: 'Credit Card', value: 'credit', $value: $payment })
|
||||
_radio({ name: 'payment', label: 'PayPal', value: 'paypal', $value: $payment })
|
||||
_radio({ name: 'payment', label: 'Crypto', value: 'crypto', $value: $payment })
|
||||
```
|
||||
|
||||
## Range Slider (`_range`)
|
||||
|
||||
Reactive range slider for numeric values.
|
||||
|
||||
<div id="range-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $volume = $(50)
|
||||
|
||||
_range({
|
||||
label: 'Volume',
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
$value: $volume
|
||||
})
|
||||
```
|
||||
|
||||
## Complete Form Example
|
||||
|
||||
<div id="complete-form-demo"></div>
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### `_select`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `label` | `string` | Field label |
|
||||
| `options` | `Array<{value: any, label: string}>` | Select options |
|
||||
| `$value` | `Signal<any>` | Selected value signal |
|
||||
|
||||
### `_checkbox`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `label` | `string` | Checkbox label |
|
||||
| `$value` | `Signal<boolean>` | Checked state signal |
|
||||
|
||||
### `_radio`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `name` | `string` | Radio group name |
|
||||
| `label` | `string` | Radio option label |
|
||||
| `value` | `any` | Value for this option |
|
||||
| `$value` | `Signal<any>` | Group selected value signal |
|
||||
|
||||
### `_range`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `label` | `string` | Slider label |
|
||||
| `min` | `number` | Minimum value |
|
||||
| `max` | `number` | Maximum value |
|
||||
| `step` | `number` | Step increment |
|
||||
| `$value` | `Signal<number>` | Current value signal |
|
||||
137
src/docs/ui/input.md
Normal file
137
src/docs/ui/input.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Input Component
|
||||
|
||||
The `_input` component creates reactive form inputs with built-in support for labels, tooltips, error messages, and two-way binding.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
<div id="basic-input-demo"></div>
|
||||
|
||||
|
||||
```javascript
|
||||
const $name = $('')
|
||||
|
||||
_input({
|
||||
label: 'Name',
|
||||
placeholder: 'Enter your name',
|
||||
$value: $name
|
||||
})
|
||||
```
|
||||
|
||||
## With Tooltip
|
||||
|
||||
The `tip` prop adds an info badge with a tooltip.
|
||||
|
||||
<div id="tooltip-input-demo"></div>
|
||||
|
||||
|
||||
```javascript
|
||||
_input({
|
||||
label: 'Username',
|
||||
tip: 'Choose a unique username (min. 3 characters)',
|
||||
placeholder: 'johndoe123',
|
||||
$value: $username
|
||||
})
|
||||
```
|
||||
|
||||
## With Error Handling
|
||||
|
||||
The `$error` signal displays an error message and styles the input accordingly.
|
||||
|
||||
<div id="error-input-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $email = $('')
|
||||
const $error = $(null)
|
||||
|
||||
const validate = (value) => {
|
||||
if (value && !value.includes('@')) {
|
||||
$error('Please enter a valid email address')
|
||||
} else {
|
||||
$error(null)
|
||||
}
|
||||
}
|
||||
|
||||
_input({
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
placeholder: 'user@example.com',
|
||||
$value: $email,
|
||||
$error: $error,
|
||||
oninput: (e) => validate(e.target.value)
|
||||
})
|
||||
```
|
||||
|
||||
## Input Types
|
||||
|
||||
The component supports all standard HTML input types.
|
||||
|
||||
<div id="types-input-demo"></div>
|
||||
|
||||
```javascript
|
||||
_input({ label: 'Text', placeholder: 'Text input', $value: $text })
|
||||
_input({ label: 'Password', type: 'password', placeholder: '••••••••', $value: $password })
|
||||
_input({ label: 'Number', type: 'number', placeholder: '0', $value: $number })
|
||||
```
|
||||
|
||||
## Two-Way Binding
|
||||
|
||||
The `$value` prop creates two-way binding between the input and the signal.
|
||||
|
||||
<div id="binding-input-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $message = $('Hello World')
|
||||
|
||||
_input({
|
||||
label: 'Message',
|
||||
$value: $message
|
||||
})
|
||||
|
||||
// The input updates when signal changes, and vice versa
|
||||
_button({ onclick: () => $message('Reset!') }, 'Reset Signal')
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `label` | `string` | Field label text |
|
||||
| `tip` | `string` | Tooltip text shown on hover |
|
||||
| `$value` | `Signal<any>` | Two-way bound value signal |
|
||||
| `$error` | `Signal<string\|null>` | Error message signal |
|
||||
| `type` | `string` | Input type (text, email, password, number, etc.) |
|
||||
| `placeholder` | `string` | Placeholder text |
|
||||
| `class` | `string \| function` | Additional CSS classes |
|
||||
| `oninput` | `function` | Input event handler |
|
||||
| `onchange` | `function` | Change event handler |
|
||||
| `disabled` | `boolean` | Disabled state |
|
||||
|
||||
## Examples
|
||||
|
||||
### Registration Form Field
|
||||
|
||||
<div id="register-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $username = $('')
|
||||
const $usernameError = $(null)
|
||||
const $email = $('')
|
||||
const $emailError = $(null)
|
||||
|
||||
_input({
|
||||
label: 'Username',
|
||||
placeholder: 'johndoe',
|
||||
$value: $username,
|
||||
$error: $usernameError,
|
||||
oninput: (e) => validateUsername(e.target.value)
|
||||
})
|
||||
|
||||
_input({
|
||||
label: 'Email',
|
||||
type: 'email',
|
||||
placeholder: 'john@example.com',
|
||||
$value: $email,
|
||||
$error: $emailError,
|
||||
oninput: (e) => validateEmail(e.target.value)
|
||||
})
|
||||
```
|
||||
53
src/docs/ui/installation.md
Normal file
53
src/docs/ui/installation.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Installation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18 or higher
|
||||
- A project with SigPro already installed
|
||||
|
||||
## Step 1: Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install -D tailwindcss @tailwindcss/vite daisyui@next
|
||||
```
|
||||
|
||||
## Step 2: Configure Tailwind CSS v4
|
||||
|
||||
Create a CSS file (e.g., `src/app.css`):
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
```
|
||||
|
||||
## Step 3: Import CSS in Your Entry Point
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
import './app.css';
|
||||
import { $ } from 'sigpro';
|
||||
import { UI } from 'sigpro/plugins';
|
||||
|
||||
$.plugin(UI).then(() => {
|
||||
console.log('✅ UI Components ready');
|
||||
import('./App.js').then(app => $.mount(app.default));
|
||||
});
|
||||
```
|
||||
|
||||
## Step 4: Verify Installation
|
||||
|
||||
<div id="test-install"></div>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Styles not applying?
|
||||
- Make sure `app.css` is imported before any other code
|
||||
- Check that Tailwind is properly configured in your build tool
|
||||
|
||||
### Components not found?
|
||||
- Ensure `$.plugin(UI)` has completed before using components
|
||||
- Check browser console for any loading errors
|
||||
|
||||
### Reactive updates not working?
|
||||
- Make sure you're passing signals, not primitive values
|
||||
- Use `$value` prop for two-way binding
|
||||
33
src/docs/ui/introduction.md
Normal file
33
src/docs/ui/introduction.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# UI Components
|
||||
|
||||
The **SigPro UI** plugin is a high-level component library built on top of the reactive core. It leverages **Tailwind CSS v4** for utility styling and **daisyUI v5** for semantic, themeable components.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Fully Reactive**: Every component automatically updates with signals
|
||||
- 🎨 **Themeable**: Supports all daisyUI themes out of the box
|
||||
- 📱 **Responsive**: Designed to work on all devices
|
||||
- 🔧 **Zero Dependencies**: Pure SigPro with no framework overhead
|
||||
|
||||
## Quick Demo
|
||||
|
||||
<div id="quick-demo"></div>
|
||||
|
||||
## What's Included
|
||||
|
||||
The UI plugin provides a comprehensive set of reactive components:
|
||||
|
||||
| Category | Components |
|
||||
|----------|------------|
|
||||
| **Actions** | `_button` |
|
||||
| **Forms** | `_input`, `_select`, `_checkbox`, `_radio`, `_range` |
|
||||
| **Layout** | `_fieldset`, `_accordion`, `_drawer` |
|
||||
| **Navigation** | `_navbar`, `_menu`, `_tabs` |
|
||||
| **Overlays** | `_modal`, `_dropdown` |
|
||||
| **Feedback** | `_badge`, `_tooltip` |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Installation Guide](/ui/installation) - Set up Tailwind and daisyUI
|
||||
- [Button Component](/ui/button) - Explore the button component
|
||||
- [Form Components](/ui/form) - Build reactive forms with validation
|
||||
117
src/docs/ui/layout.md
Normal file
117
src/docs/ui/layout.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Layout Components
|
||||
|
||||
Layout components for structuring your application with containers, sections, and collapsible panels.
|
||||
|
||||
## Fieldset (`_fieldset`)
|
||||
|
||||
Groups related form fields with a legend.
|
||||
|
||||
<div id="fieldset-demo"></div>
|
||||
|
||||
|
||||
```javascript
|
||||
_fieldset({ legend: 'Personal Information' }, [
|
||||
_input({ label: 'Full Name', $value: $name }),
|
||||
_input({ label: 'Email Address', type: 'email', $value: $email }),
|
||||
_select({ label: 'Role', options: [...], $value: $role })
|
||||
])
|
||||
```
|
||||
|
||||
## Accordion (`_accordion`)
|
||||
|
||||
Collapsible content panels. Can be used as standalone or grouped.
|
||||
|
||||
### Single Accordion
|
||||
|
||||
<div id="single-accordion-demo"></div>
|
||||
|
||||
```javascript
|
||||
_accordion({ title: 'What is SigPro UI?' }, [
|
||||
$.html('p', {}, 'SigPro UI is a reactive component library...')
|
||||
])
|
||||
```
|
||||
|
||||
### Grouped Accordions (Radio Behavior)
|
||||
|
||||
When multiple accordions share the same `name`, only one can be open at a time.
|
||||
|
||||
<div id="grouped-accordion-demo"></div>
|
||||
|
||||
```javascript
|
||||
// Grouped accordions - only one open at a time
|
||||
_accordion({ title: 'Getting Started', name: 'faq' }, content1)
|
||||
_accordion({ title: 'Installation', name: 'faq' }, content2)
|
||||
_accordion({ title: 'Customization', name: 'faq' }, content3)
|
||||
```
|
||||
|
||||
### Accordion with Open State
|
||||
|
||||
Control the initial open state with the `open` prop.
|
||||
|
||||
<div id="open-accordion-demo"></div>
|
||||
|
||||
```javascript
|
||||
_accordion({ title: 'Open by Default', open: true }, [
|
||||
$.html('p', {}, 'This accordion starts open.')
|
||||
])
|
||||
```
|
||||
|
||||
## Complete Layout Example
|
||||
|
||||
<div id="complete-layout-demo"></div>
|
||||
|
||||
## API Reference
|
||||
|
||||
### `_fieldset`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `legend` | `string` | Fieldset title/legend text |
|
||||
| `class` | `string \| function` | Additional CSS classes |
|
||||
|
||||
### `_accordion`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `title` | `string` | Accordion header text |
|
||||
| `name` | `string` | Group name for radio behavior (optional) |
|
||||
| `open` | `boolean` | Initially open state (default: false) |
|
||||
|
||||
## Styling Tips
|
||||
|
||||
### Custom Fieldset Styling
|
||||
|
||||
```javascript
|
||||
_fieldset({
|
||||
legend: 'Custom Styled',
|
||||
class: 'bg-primary/10 border-primary'
|
||||
}, [
|
||||
// content
|
||||
])
|
||||
```
|
||||
|
||||
### Custom Accordion Styling
|
||||
|
||||
```javascript
|
||||
_accordion({
|
||||
title: 'Styled Accordion',
|
||||
class: 'bg-base-200'
|
||||
}, [
|
||||
// content
|
||||
])
|
||||
```
|
||||
|
||||
### Nested Layouts
|
||||
|
||||
Layout components can be nested to create complex structures:
|
||||
|
||||
```javascript
|
||||
_fieldset({ legend: 'Main Section' }, [
|
||||
_accordion({ title: 'Subsection 1' }, [
|
||||
_input({ label: 'Field 1', $value: $field1 })
|
||||
]),
|
||||
_accordion({ title: 'Subsection 2' }, [
|
||||
_input({ label: 'Field 2', $value: $field2 })
|
||||
])
|
||||
])
|
||||
```
|
||||
90
src/docs/ui/modal.md
Normal file
90
src/docs/ui/modal.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Modal & Drawer Components
|
||||
|
||||
Overlay components for dialogs, side panels, and popups with reactive control.
|
||||
|
||||
## Modal (`_modal`)
|
||||
|
||||
A dialog component that appears on top of the page. The modal is completely removed from the DOM when closed, optimizing performance.
|
||||
|
||||
### Basic Modal
|
||||
|
||||
<div id="basic-modal-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $open = $(false)
|
||||
|
||||
_button({ onclick: () => $open(true) }, 'Open Modal')
|
||||
|
||||
_modal({ $open: $open, title: 'Welcome' }, [
|
||||
$.html('p', {}, 'This is a simple modal dialog.'),
|
||||
_button({ onclick: () => $open(false) }, 'Close')
|
||||
])
|
||||
```
|
||||
|
||||
### Modal with Actions
|
||||
|
||||
<div id="action-modal-demo"></div>
|
||||
|
||||
|
||||
```javascript
|
||||
const $open = $(false)
|
||||
const $result = $(null)
|
||||
|
||||
_modal({ $open: $open, title: 'Confirm Delete' }, [
|
||||
$.html('p', {}, 'Are you sure you want to delete this item?'),
|
||||
_button({ class: 'btn-error', onclick: () => {
|
||||
$result('Item deleted')
|
||||
$open(false)
|
||||
} }, 'Delete')
|
||||
])
|
||||
```
|
||||
|
||||
### Modal with Form
|
||||
|
||||
<div id="form-modal-demo"></div>
|
||||
|
||||
## Drawer (`_drawer`)
|
||||
|
||||
A sidebar panel that slides in from the side.
|
||||
|
||||
### Basic Drawer
|
||||
|
||||
<div id="basic-drawer-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $open = $(false)
|
||||
|
||||
_drawer({
|
||||
id: 'my-drawer',
|
||||
$open: $open,
|
||||
content: $.html('div', {}, 'Main content'),
|
||||
side: $.html('div', { class: 'p-4' }, [
|
||||
$.html('h3', {}, 'Menu'),
|
||||
$.html('ul', { class: 'menu' }, [
|
||||
$.html('li', {}, [$.html('a', { onclick: () => $open(false) }, 'Close')])
|
||||
])
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
### Drawer with Navigation Menu
|
||||
|
||||
<div id="nav-drawer-demo"></div>
|
||||
|
||||
## API Reference
|
||||
|
||||
### `_modal`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `$open` | `Signal<boolean>` | Controls modal visibility |
|
||||
| `title` | `string` | Modal title text |
|
||||
|
||||
### `_drawer`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `id` | `string` | Unique identifier for the drawer |
|
||||
| `$open` | `Signal<boolean>` | Controls drawer visibility |
|
||||
| `content` | `HTMLElement` | Main content area |
|
||||
| `side` | `HTMLElement` | Sidebar content |
|
||||
124
src/docs/ui/navigation.md
Normal file
124
src/docs/ui/navigation.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Navigation Components
|
||||
|
||||
Navigation components for building menus, navbars, and tabs with reactive active states.
|
||||
|
||||
## Navbar (`_navbar`)
|
||||
|
||||
A responsive navigation bar with built-in styling.
|
||||
|
||||
<div id="navbar-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $active = $('Home')
|
||||
|
||||
_navbar({ class: 'shadow-md' }, [
|
||||
div({ class: 'flex-1' }, [
|
||||
a({ class: 'text-xl font-bold' }, 'MyApp')
|
||||
]),
|
||||
div({ class: 'flex-none gap-2' }, [
|
||||
_button({
|
||||
class: () => $active() === 'Home' ? 'btn-primary btn-sm' : 'btn-ghost btn-sm',
|
||||
onclick: () => $active('Home')
|
||||
}, 'Home'),
|
||||
_button({
|
||||
class: () => $active() === 'About' ? 'btn-primary btn-sm' : 'btn-ghost btn-sm',
|
||||
onclick: () => $active('About')
|
||||
}, 'About')
|
||||
])
|
||||
])
|
||||
```
|
||||
|
||||
## Menu (`_menu`)
|
||||
|
||||
Vertical navigation menu with active state highlighting.
|
||||
|
||||
<div id="menu-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $selected = $('dashboard')
|
||||
|
||||
_menu({ items: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: '📊',
|
||||
active: () => $selected() === 'dashboard',
|
||||
onclick: () => $selected('dashboard')
|
||||
},
|
||||
{
|
||||
label: 'Analytics',
|
||||
icon: '📈',
|
||||
active: () => $selected() === 'analytics',
|
||||
onclick: () => $selected('analytics')
|
||||
}
|
||||
]})
|
||||
```
|
||||
|
||||
## Tabs (`_tabs`)
|
||||
|
||||
Horizontal tabs with lifted styling and active state.
|
||||
|
||||
<div id="tabs-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $activeTab = $('profile')
|
||||
|
||||
_tabs({ items: [
|
||||
{
|
||||
label: 'Profile',
|
||||
active: () => $activeTab() === 'profile',
|
||||
onclick: () => $activeTab('profile')
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
active: () => $activeTab() === 'settings',
|
||||
onclick: () => $activeTab('settings')
|
||||
}
|
||||
]})
|
||||
```
|
||||
|
||||
## Dropdown (`_dropdown`)
|
||||
|
||||
A dropdown menu that appears on click.
|
||||
|
||||
<div id="dropdown-demo"></div>
|
||||
|
||||
```javascript
|
||||
const $selected = $(null)
|
||||
|
||||
_dropdown({ label: 'Options' }, [
|
||||
li([a({ onclick: () => $selected('Edit') }, '✏️ Edit')]),
|
||||
li([a({ onclick: () => $selected('Duplicate') }, '📋 Duplicate')]),
|
||||
li([a({ onclick: () => $selected('Delete') }, '🗑️ Delete')])
|
||||
])
|
||||
```
|
||||
|
||||
## Complete Navigation Example
|
||||
|
||||
<div id="complete-nav-demo"></div>
|
||||
|
||||
## API Reference
|
||||
|
||||
### `_navbar`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `class` | `string \| function` | Additional CSS classes |
|
||||
|
||||
### `_menu`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `items` | `Array<{label: string, icon?: any, active?: boolean\|function, onclick: function}>` | Menu items |
|
||||
|
||||
### `_tabs`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `items` | `Array<{label: string, active: boolean\|function, onclick: function}>` | Tab items |
|
||||
|
||||
### `_dropdown`
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `label` | `string` | Dropdown trigger text |
|
||||
| `class` | `string \| function` | Additional CSS classes |
|
||||
@@ -1,10 +1,10 @@
|
||||
# Vite Plugin: File-based Routing
|
||||
|
||||
The `sigproRouter` plugin for Vite automates route generation by scanning your pages directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
|
||||
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
|
||||
|
||||
## 1. Project Structure
|
||||
|
||||
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure.
|
||||
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
|
||||
|
||||
```text
|
||||
my-sigpro-app/
|
||||
@@ -17,7 +17,7 @@ my-sigpro-app/
|
||||
│ │ └── blog/
|
||||
│ │ ├── index.js → #/blog
|
||||
│ │ └── [slug].js → #/blog/:slug
|
||||
│ ├── App.js (Optional App Shell)
|
||||
│ ├── App.js (Main Layout)
|
||||
│ └── main.js (Entry Point)
|
||||
├── vite.config.js
|
||||
└── package.json
|
||||
@@ -27,7 +27,7 @@ my-sigpro-app/
|
||||
|
||||
## 2. Setup & Configuration
|
||||
|
||||
Add the plugin to your `vite.config.js`.
|
||||
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
@@ -43,67 +43,80 @@ export default defineConfig({
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
You can implement the router either directly in your entry point or inside an App component to support persistent layouts (like a navbar that doesn't re-render).
|
||||
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
|
||||
|
||||
### Option A: Direct in `main.js`
|
||||
Best for simple apps where the router occupies the entire viewport.
|
||||
Ideal for single-page applications where the router controls the entire viewport.
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import { $ } from 'sigpro';
|
||||
import { Router } from 'sigpro/plugins';
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
$.plugin(Router).then(() => {
|
||||
$.mount(_router(routes), '#app');
|
||||
});
|
||||
// The Core already has $.router ready
|
||||
$.mount($.router(routes), '#app');
|
||||
```
|
||||
|
||||
### Option B: Inside `App.js` (With Layout)
|
||||
Recommended for apps with a fixed Sidebar or Navbar.
|
||||
### Option B: Inside `App.js` (Persistent Layout)
|
||||
Recommended for professional apps with a fixed Sidebar or Navbar that should not re-render when changing pages.
|
||||
|
||||
```javascript
|
||||
// src/main.js
|
||||
import { $ } from 'sigpro';
|
||||
import { Router } from 'sigpro/plugins';
|
||||
import App from './App.js';
|
||||
|
||||
$.plugin(Router).then(() => {
|
||||
import('./App.js').then(app => $.mount(app.default, '#app'));
|
||||
});
|
||||
$.mount(App, '#app');
|
||||
|
||||
// src/App.js
|
||||
import { routes } from 'virtual:sigpro-routes';
|
||||
|
||||
export default () => {
|
||||
return div({ class: 'layout' }, [
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
a({ href: '#/' }, "Home"),
|
||||
a({ href: '#/blog' }, "Blog")
|
||||
])
|
||||
]),
|
||||
// The router only swaps the content inside this <main> tag
|
||||
main(_router(routes))
|
||||
]);
|
||||
};
|
||||
export default () => div({ class: 'layout' }, [
|
||||
header([
|
||||
h1("SigPro App"),
|
||||
nav([
|
||||
button({ onclick: () => $.router.go('/') }, "Home"),
|
||||
button({ onclick: () => $.router.go('/blog') }, "Blog")
|
||||
])
|
||||
]),
|
||||
// Only the content inside <main> will be swapped reactively
|
||||
main($.router(routes))
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Route Mapping Reference
|
||||
|
||||
| File Path | Generated Route | Logic |
|
||||
The plugin follows a simple convention to transform your file system into a routing map.
|
||||
|
||||
| File Path | Generated Path | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `index.js` | `/` | Home page |
|
||||
| `about.js` | `/about` | Static path |
|
||||
| `[id].js` | `/:id` | Dynamic parameter |
|
||||
| `blog/index.js` | `/blog` | Folder index |
|
||||
| `_utils.js` | *Ignored* | Files starting with `_` are skipped |
|
||||
| `index.js` | `/` | The application root. |
|
||||
| `about.js` | `/about` | A static page. |
|
||||
| `[id].js` | `/:id` | Dynamic parameter (passed to the component). |
|
||||
| `blog/index.js` | `/blog` | Folder index page. |
|
||||
| `_utils.js` | *Ignored* | Files starting with `_` are excluded from routing. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Installation
|
||||
## 5. How it Works (Vite Virtual Module)
|
||||
|
||||
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `$.router()`:
|
||||
|
||||
```javascript
|
||||
// Internal representation generated by the plugin
|
||||
export const routes = [
|
||||
{ path: '/', component: () => import('/src/pages/index.js') },
|
||||
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.
|
||||
|
||||
---
|
||||
|
||||
## 6. Installation
|
||||
|
||||
::: code-group
|
||||
```bash [NPM]
|
||||
|
||||
Reference in New Issue
Block a user