diff --git a/demos/iconset/package.json b/demos/iconset/package.json
index e1183da82..988bf4def 100644
--- a/demos/iconset/package.json
+++ b/demos/iconset/package.json
@@ -31,23 +31,23 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/experimental-utils": "^5.62.0",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.9",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/jsx/package.json b/demos/jsx/package.json
index d7e0e0e92..50ca78035 100644
--- a/demos/jsx/package.json
+++ b/demos/jsx/package.json
@@ -31,23 +31,23 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/experimental-utils": "^5.62.0",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.9",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/leaflet-map/package.json b/demos/leaflet-map/package.json
index b27d942ee..d47d8c00f 100644
--- a/demos/leaflet-map/package.json
+++ b/demos/leaflet-map/package.json
@@ -32,22 +32,22 @@
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/core": "workspace:^",
diff --git a/demos/lottie/package.json b/demos/lottie/package.json
index 50e224d04..49201d63c 100644
--- a/demos/lottie/package.json
+++ b/demos/lottie/package.json
@@ -30,23 +30,23 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/experimental-utils": "^5.62.0",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.9",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/core": "workspace:^",
diff --git a/demos/luxon/package.json b/demos/luxon/package.json
index 5e8344538..3abc7fce9 100644
--- a/demos/luxon/package.json
+++ b/demos/luxon/package.json
@@ -33,22 +33,22 @@
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/moment/package.json b/demos/moment/package.json
index aaf4bf4cc..f70db4876 100644
--- a/demos/moment/package.json
+++ b/demos/moment/package.json
@@ -33,22 +33,22 @@
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/core": "workspace:^",
diff --git a/demos/monaco-editor/package.json b/demos/monaco-editor/package.json
index 36b314d59..134739bc0 100644
--- a/demos/monaco-editor/package.json
+++ b/demos/monaco-editor/package.json
@@ -32,17 +32,17 @@
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/core": "workspace:^",
diff --git a/demos/monaco-editor/src/index.html b/demos/monaco-editor/src/index.html
index 32c335f8c..75cd8a935 100644
--- a/demos/monaco-editor/src/index.html
+++ b/demos/monaco-editor/src/index.html
@@ -2,7 +2,7 @@
diff --git a/demos/podcast-fixed-player/package.json b/demos/podcast-fixed-player/package.json
index ee7655ba9..e2eb8f79f 100644
--- a/demos/podcast-fixed-player/package.json
+++ b/demos/podcast-fixed-player/package.json
@@ -30,23 +30,23 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/experimental-utils": "^5.62.0",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.9",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/podcast/package.json b/demos/podcast/package.json
index 68d1a53df..e4691b86e 100644
--- a/demos/podcast/package.json
+++ b/demos/podcast/package.json
@@ -30,23 +30,23 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/experimental-utils": "^5.62.0",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.9",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/router-slide-transition/package.json b/demos/router-slide-transition/package.json
index 00e0f7c3f..08622c81c 100644
--- a/demos/router-slide-transition/package.json
+++ b/demos/router-slide-transition/package.json
@@ -34,19 +34,19 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/core": "workspace:^",
diff --git a/demos/router-view/package.json b/demos/router-view/package.json
index 9bd14e636..811df567e 100644
--- a/demos/router-view/package.json
+++ b/demos/router-view/package.json
@@ -32,22 +32,22 @@
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/rv-video/package.json b/demos/rv-video/package.json
index aa490ab08..9a350f500 100644
--- a/demos/rv-video/package.json
+++ b/demos/rv-video/package.json
@@ -32,22 +32,22 @@
"@ribajs/types": "workspace:^",
"@ribajs/vite-config": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
"postcss-modules": "^6.0.1",
- "postcss-preset-env": "^11.2.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "postcss-preset-env": "^11.2.1",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"serve": "^14.2.6",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
},
"dependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/demos/tw-accordion/package.json b/demos/tw-accordion/package.json
new file mode 100644
index 000000000..b696a19d8
--- /dev/null
+++ b/demos/tw-accordion/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-accordion",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-accordion/src/css/main.css b/demos/tw-accordion/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-accordion/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-accordion/src/index.html b/demos/tw-accordion/src/index.html
new file mode 100644
index 000000000..9388f8261
--- /dev/null
+++ b/demos/tw-accordion/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
TW Accordion Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-accordion/src/ts/accordion.module.ts b/demos/tw-accordion/src/ts/accordion.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-accordion/src/ts/accordion.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-accordion/src/ts/components/demo-accordion/demo-accordion.component.html b/demos/tw-accordion/src/ts/components/demo-accordion/demo-accordion.component.html
new file mode 100644
index 000000000..8f04898b3
--- /dev/null
+++ b/demos/tw-accordion/src/ts/components/demo-accordion/demo-accordion.component.html
@@ -0,0 +1,60 @@
+
TW Accordion & Collapse Demo
+
+
+
+ Accordion
+ An accordion with three items. Only one item can be open at a time.
+
+
+ Riba.js is a TypeScript declarative data-binding and Web Components framework. It evolved from Rivets.js and tinybind, offering a modern approach to building reactive user interfaces with custom elements and template binding.
+
+
+ Tailwind CSS is a utility-first CSS framework that provides low-level utility classes to build custom designs. Instead of pre-designed components, it gives you building blocks to create any design directly in your markup.
+
+
+ The @ribajs/tw module brings Tailwind-styled UI components into the Riba.js ecosystem. It provides ready-to-use Web Components like accordions, dropdowns, forms, and more — all styled with Tailwind CSS utility classes and powered by Riba.js data binding.
+
+
+
+
+
+
+ Accordion (Multiple Open)
+ An accordion that allows multiple items to be open simultaneously.
+
+
+ This section is open by default. You can open other sections without closing this one.
+
+
+ Open this alongside the first section to see multiple panels expanded at once.
+
+
+ All three sections can be open at the same time when show-only-one is set to false.
+
+
+
+
+
+
+ Standalone Collapse
+ A standalone collapsible section, independent of any accordion group.
+
+
+
+
+
+
+
+
+ Collapse with Rich Content
+ A collapse component can contain any HTML content.
+
+
+ Smooth animated transitions
+ Keyboard accessible
+ ARIA attributes for screen readers
+ Works standalone or inside an accordion
+ Customizable via Tailwind classes
+
+
+
diff --git a/demos/tw-accordion/src/ts/components/demo-accordion/demo-accordion.component.ts b/demos/tw-accordion/src/ts/components/demo-accordion/demo-accordion.component.ts
new file mode 100644
index 000000000..f3916b342
--- /dev/null
+++ b/demos/tw-accordion/src/ts/components/demo-accordion/demo-accordion.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoAccordionComponent extends Component {
+ public static tagName = "demo-accordion";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoAccordionComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-accordion.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-accordion/src/ts/components/index.ts b/demos/tw-accordion/src/ts/components/index.ts
new file mode 100644
index 000000000..86df93226
--- /dev/null
+++ b/demos/tw-accordion/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoAccordionComponent } from "./demo-accordion/demo-accordion.component.js";
diff --git a/demos/tw-accordion/src/ts/main.ts b/demos/tw-accordion/src/ts/main.ts
new file mode 100644
index 000000000..a953941c5
--- /dev/null
+++ b/demos/tw-accordion/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./accordion.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-accordion/tsconfig.json b/demos/tw-accordion/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-accordion/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-accordion/vite.config.js b/demos/tw-accordion/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-accordion/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-basics/package.json b/demos/tw-basics/package.json
new file mode 100644
index 000000000..dd5c01975
--- /dev/null
+++ b/demos/tw-basics/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-basics",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-basics/src/css/main.css b/demos/tw-basics/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-basics/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-basics/src/index.html b/demos/tw-basics/src/index.html
new file mode 100644
index 000000000..d57d5ca64
--- /dev/null
+++ b/demos/tw-basics/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
Tailwind Basics Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-basics/src/ts/basics.module.ts b/demos/tw-basics/src/ts/basics.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-basics/src/ts/basics.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-basics/src/ts/components/demo-basics/demo-basics.component.html b/demos/tw-basics/src/ts/components/demo-basics/demo-basics.component.html
new file mode 100644
index 000000000..4fc26b545
--- /dev/null
+++ b/demos/tw-basics/src/ts/components/demo-basics/demo-basics.component.html
@@ -0,0 +1,328 @@
+
Tailwind Basics Demo
+
+
+
+
+
+
+ Badges
+
+ Default
+ Info
+ Success
+ Warning
+ Error
+
+ Outline Badges
+
+ Default
+ Info
+ Success
+ Warning
+ Error
+
+ Badge Sizes
+
+ Small
+ Medium
+ Large
+
+
+
+
+
+ Avatars
+ Sizes
+
+
+
+
+
+
+
+ With Status
+
+
+
+
+
+
+ With Image
+
+
+
+
+
+
+
+
+
+ Cards
+
+
+ This is a basic card with an image and some body text content.
+
+
+ A compact card without an image. Useful for dense layouts.
+
+
+ Cards can contain any content, including other components.
+
+
+
+
+
+
+ Keyboard Keys
+
+
+ Press Ctrl + C to copy.
+
+
+ Press Ctrl + Shift + I to open DevTools.
+
+
+ Press Enter to submit the form.
+
+
+
+
+
+
+ Skeleton Loading
+
+
+
Text Lines
+
+
+
+
Circle
+
+
+
+
Rectangle
+
+
+
+
+
Card Skeleton
+
+
+
+
+
+
+ Progress Bars
+
+
+
+
+
+
Sizes
+
+
+
+
+
+
+
+
+
+ Rating
+
+
+
Interactive (click to rate)
+
+
+
+
Read-only (4.5 out of 5)
+
+
+
+
+
Extra large rating (out of 10)
+
+
+
+
+
+
+
+ Breadcrumbs
+
+
+
Default separator
+
+
+
+
+
+
+
+
+
Custom separator
+
+
+
+
+
+
+
+
+
+
+
+ Animated Buttons
+ Click a button to see the animation.
+
+
+
+ Pulse on Click
+
+
+
+
+ Bounce on Click
+
+
+
+
+ Spin on Click
+
+
+
+
+
+
+
+ Tooltips
+
+
+ Top
+
+
+ Bottom
+
+
+ Left
+
+
+ Right
+
+
+
+
+
+
+ Swap Animations
+ Click to toggle between two states with different animations.
+
+
+
+
+
+ Pagination
+
+
+
10 pages, max 5 visible
+
+
+
+
20 pages, starting on page 8
+
+
+
+
+
+
+
+ Color Picker
+
+
+
+
+
+
+
+
+
+ Collapse
+ Standalone collapsible sections (independent from Accordion).
+
+
+
+ Click to expand details
+
+
+
+ This content is collapsible. It can contain any HTML including other components, images, or forms.
+
+
+
+
+ Another collapsible section
+
+
+
+ Each collapse section operates independently. Opening one does not close the others (unlike an accordion).
+
+
+
+
diff --git a/demos/tw-basics/src/ts/components/demo-basics/demo-basics.component.ts b/demos/tw-basics/src/ts/components/demo-basics/demo-basics.component.ts
new file mode 100644
index 000000000..d7d4f0eed
--- /dev/null
+++ b/demos/tw-basics/src/ts/components/demo-basics/demo-basics.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoBasicsComponent extends Component {
+ public static tagName = "demo-basics";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoBasicsComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-basics.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-basics/src/ts/components/index.ts b/demos/tw-basics/src/ts/components/index.ts
new file mode 100644
index 000000000..12d9b2cef
--- /dev/null
+++ b/demos/tw-basics/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoBasicsComponent } from "./demo-basics/demo-basics.component.js";
diff --git a/demos/tw-basics/src/ts/main.ts b/demos/tw-basics/src/ts/main.ts
new file mode 100644
index 000000000..30eb87dc3
--- /dev/null
+++ b/demos/tw-basics/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./basics.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-basics/tsconfig.json b/demos/tw-basics/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-basics/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-basics/vite.config.js b/demos/tw-basics/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-basics/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-dropdown/package.json b/demos/tw-dropdown/package.json
new file mode 100644
index 000000000..97e2a2c2b
--- /dev/null
+++ b/demos/tw-dropdown/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-dropdown",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-dropdown/src/css/main.css b/demos/tw-dropdown/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-dropdown/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-dropdown/src/index.html b/demos/tw-dropdown/src/index.html
new file mode 100644
index 000000000..8eb5d45e1
--- /dev/null
+++ b/demos/tw-dropdown/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
TW Dropdown Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-dropdown/src/ts/components/demo-dropdown/demo-dropdown.component.html b/demos/tw-dropdown/src/ts/components/demo-dropdown/demo-dropdown.component.html
new file mode 100644
index 000000000..e3fc2b5b7
--- /dev/null
+++ b/demos/tw-dropdown/src/ts/components/demo-dropdown/demo-dropdown.component.html
@@ -0,0 +1,201 @@
+
TW Dropdown & Navbar Demo
+
+
+
+ Navbar
+ A responsive navbar with collapsible mobile menu.
+
+
+
+
+
+
+
+
+
+
+ Basic Dropdown
+ A simple dropdown menu with default bottom-start placement.
+
+
+ Options
+
+
+
+
+
+
+
+
+
+
+ Dropdown Placements
+ Dropdowns can be positioned in different directions using the placement attribute.
+
+
+
+ Bottom Start
+
+
+
+
+
+
+ Bottom End
+
+
+
+
+
+
+ Top Start
+
+
+
+
+
+
+ Right Start
+
+
+
+
+
+
+
+
+ Dropdown with Keyboard Navigation
+ This dropdown supports keyboard navigation. Click the button then use Arrow Down / Arrow Up to navigate, Enter to select, and Escape to close.
+
+
+ Account Menu
+
+
+
+
+
+
+
+
+
+
+ Styled Dropdown with Icons
+ A dropdown with icon-decorated menu items.
+
+
+ Actions
+
+
+
+
+
+
+
diff --git a/demos/tw-dropdown/src/ts/components/demo-dropdown/demo-dropdown.component.ts b/demos/tw-dropdown/src/ts/components/demo-dropdown/demo-dropdown.component.ts
new file mode 100644
index 000000000..1318618c9
--- /dev/null
+++ b/demos/tw-dropdown/src/ts/components/demo-dropdown/demo-dropdown.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoDropdownComponent extends Component {
+ public static tagName = "demo-dropdown";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoDropdownComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-dropdown.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-dropdown/src/ts/components/index.ts b/demos/tw-dropdown/src/ts/components/index.ts
new file mode 100644
index 000000000..d1606563a
--- /dev/null
+++ b/demos/tw-dropdown/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoDropdownComponent } from "./demo-dropdown/demo-dropdown.component.js";
diff --git a/demos/tw-dropdown/src/ts/dropdown.module.ts b/demos/tw-dropdown/src/ts/dropdown.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-dropdown/src/ts/dropdown.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-dropdown/src/ts/main.ts b/demos/tw-dropdown/src/ts/main.ts
new file mode 100644
index 000000000..ac2ba9b00
--- /dev/null
+++ b/demos/tw-dropdown/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./dropdown.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-dropdown/tsconfig.json b/demos/tw-dropdown/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-dropdown/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-dropdown/vite.config.js b/demos/tw-dropdown/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-dropdown/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-form/package.json b/demos/tw-form/package.json
new file mode 100644
index 000000000..5e83beb39
--- /dev/null
+++ b/demos/tw-form/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-form",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-form/src/css/main.css b/demos/tw-form/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-form/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-form/src/index.html b/demos/tw-form/src/index.html
new file mode 100644
index 000000000..3fb1747af
--- /dev/null
+++ b/demos/tw-form/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
TW Form Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-form/src/ts/components/demo-form/demo-form.component.html b/demos/tw-form/src/ts/components/demo-form/demo-form.component.html
new file mode 100644
index 000000000..b8716f0a9
--- /dev/null
+++ b/demos/tw-form/src/ts/components/demo-form/demo-form.component.html
@@ -0,0 +1,349 @@
+
TW Form Demo
+
+
+
+ Registration Form
+ A form with various input types and HTML5 validation. Fields marked with * are required.
+
+
+
+
+
+ Contact Form
+ A simpler contact form demonstrating required fields and email validation.
+
+
+
+
+
+ Your Name *
+
+
+
+
+
+ Email *
+
+
+
+
+
+ Subject
+
+
+ General Inquiry
+ Support
+ Feedback
+ Bug Report
+
+
+
+
+ Message *
+
+
+
+
+
+
+ Send Message
+
+
+ Reset
+
+
+
+
+
+
+
+
+
+ Validation States
+ Examples of form fields showing different validation requirements. Try submitting the form to see validation in action.
+
+
+
+
+
+ Required Field *
+
+
+
+
+
+ Email Format *
+
+
+
+
+
+ Min Length (5 characters) *
+
+
+
Enter at least 5 characters.
+
+
+
+ Pattern (letters only)
+
+
+
Only alphabetic characters (A-Z, a-z).
+
+
+
+
+ Validate & Submit
+
+
+ Reset
+
+
+
+
+
+
diff --git a/demos/tw-form/src/ts/components/demo-form/demo-form.component.ts b/demos/tw-form/src/ts/components/demo-form/demo-form.component.ts
new file mode 100644
index 000000000..a34b0568a
--- /dev/null
+++ b/demos/tw-form/src/ts/components/demo-form/demo-form.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoFormComponent extends Component {
+ public static tagName = "demo-form";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoFormComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-form.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-form/src/ts/components/index.ts b/demos/tw-form/src/ts/components/index.ts
new file mode 100644
index 000000000..10f448d21
--- /dev/null
+++ b/demos/tw-form/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoFormComponent } from "./demo-form/demo-form.component.js";
diff --git a/demos/tw-form/src/ts/form.module.ts b/demos/tw-form/src/ts/form.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-form/src/ts/form.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-form/src/ts/main.ts b/demos/tw-form/src/ts/main.ts
new file mode 100644
index 000000000..871124d6f
--- /dev/null
+++ b/demos/tw-form/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./form.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-form/tsconfig.json b/demos/tw-form/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-form/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-form/vite.config.js b/demos/tw-form/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-form/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-interactive/package.json b/demos/tw-interactive/package.json
new file mode 100644
index 000000000..f43eca7d7
--- /dev/null
+++ b/demos/tw-interactive/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-interactive",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "Interactive components demo for @ribajs/tw",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-interactive/src/css/main.css b/demos/tw-interactive/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-interactive/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-interactive/src/index.html b/demos/tw-interactive/src/index.html
new file mode 100644
index 000000000..6cafe6f96
--- /dev/null
+++ b/demos/tw-interactive/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
Tailwind Interactive Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-interactive/src/ts/components/demo-interactive/demo-interactive.component.html b/demos/tw-interactive/src/ts/components/demo-interactive/demo-interactive.component.html
new file mode 100644
index 000000000..92c562073
--- /dev/null
+++ b/demos/tw-interactive/src/ts/components/demo-interactive/demo-interactive.component.html
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+ Tailwind Interactive Demo
+
+
+
+ Tagged Image
+
+ Click on the numbered pins to reveal details about different areas of the image.
+
+
+
+ A historic clock tower rising above the surrounding buildings, dating back to the 19th century.
+ The bustling main street with shops, cafes, and pedestrians. A hub of daily activity.
+ Golden hour lighting creating a warm atmospheric glow across the cityscape.
+ Local vendors selling fresh produce, flowers, and artisanal goods every weekend.
+
+
+
+
+
+
+ Share Button
+
+ Uses the native Web Share API on supported devices, with a dropdown fallback for desktop browsers.
+ Try clicking the button to see available sharing options.
+
+
+
+
Share this page
+
Spread the word about Riba.js and help other developers discover it.
+
+
+
+
+
+
+
+ Content Slider
+
+ A multi-column content slider with drag support and navigation controls. Drag to scroll or use the arrow buttons.
+
+
+
+
+
+
+
+
+ Collapse / FAQ
+
+ Standalone collapsible sections. Each operates independently unlike an accordion.
+
+
+
+
+ What is Riba.js?
+
+
+
+ Riba.js is a TypeScript declarative data-binding and Web Components framework. It evolved from Rivets.js and tinybind into a modern, ESM-only library with full TypeScript support.
+
+
+
+
+ How does data binding work?
+
+
+
+ Riba uses rv-* attributes on HTML elements to bind data from your model to the DOM. The Observer pattern with Object.defineProperty ensures changes propagate automatically in both directions.
+
+
+
+
+ Can I use it with Tailwind CSS?
+
+
+
+ Absolutely! The @ribajs/tw package provides a complete set of Tailwind-styled components including alerts, cards, tabs, sidebars, slideshows, and many more.
+
+
+
+
+
+
+
+ Usage Example
+
+ Getting started with Riba.js is straightforward. Install the core package and any modules you need,
+ then bind your model to the DOM.
+
+ import { Riba, coreModule } from "@ribajs/core";
+import { twModule } from "@ribajs/tw";
+
+const riba = new Riba();
+riba.module.register(coreModule.init());
+riba.module.register(twModule.init());
+riba.bind(document.body, { greeting: "Hello!" });
+
+
+
diff --git a/demos/tw-interactive/src/ts/components/demo-interactive/demo-interactive.component.ts b/demos/tw-interactive/src/ts/components/demo-interactive/demo-interactive.component.ts
new file mode 100644
index 000000000..0493b8311
--- /dev/null
+++ b/demos/tw-interactive/src/ts/components/demo-interactive/demo-interactive.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoInteractiveComponent extends Component {
+ public static tagName = "demo-interactive";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoInteractiveComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-interactive.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-interactive/src/ts/components/index.ts b/demos/tw-interactive/src/ts/components/index.ts
new file mode 100644
index 000000000..b98ff0c6c
--- /dev/null
+++ b/demos/tw-interactive/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoInteractiveComponent } from "./demo-interactive/demo-interactive.component.js";
diff --git a/demos/tw-interactive/src/ts/interactive.module.ts b/demos/tw-interactive/src/ts/interactive.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-interactive/src/ts/interactive.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-interactive/src/ts/main.ts b/demos/tw-interactive/src/ts/main.ts
new file mode 100644
index 000000000..f3ba299fe
--- /dev/null
+++ b/demos/tw-interactive/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./interactive.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-interactive/tsconfig.json b/demos/tw-interactive/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-interactive/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-interactive/vite.config.js b/demos/tw-interactive/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-interactive/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-notifications/package.json b/demos/tw-notifications/package.json
new file mode 100644
index 000000000..4f9d91901
--- /dev/null
+++ b/demos/tw-notifications/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@ribajs/demo-tw-notifications",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/events": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-notifications/src/css/main.css b/demos/tw-notifications/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-notifications/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-notifications/src/index.html b/demos/tw-notifications/src/index.html
new file mode 100644
index 000000000..b46bc84e4
--- /dev/null
+++ b/demos/tw-notifications/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
TW Notifications Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-notifications/src/ts/components/demo-notifications/demo-notifications.component.html b/demos/tw-notifications/src/ts/components/demo-notifications/demo-notifications.component.html
new file mode 100644
index 000000000..1c6175bc4
--- /dev/null
+++ b/demos/tw-notifications/src/ts/components/demo-notifications/demo-notifications.component.html
@@ -0,0 +1,66 @@
+
TW Notifications Demo
+
+
+
+
+
+
+ Toast Notifications
+
+ Click the buttons below to trigger toast notifications. They appear in the bottom-right corner and auto-dismiss after a timeout.
+
+
+
+ Show Info Toast
+
+
+ Show Success Toast
+
+
+ Show Warning Toast
+
+
+ Show Error Toast (no auto-dismiss)
+
+
+
+
+
+
+ Modal Notification
+
+ Click the button below to open a modal dialog notification.
+
+
+ Show Modal
+
+
+
+
+
+ Inline Alerts
+
+ Static alert components embedded directly in the page.
+
+
+
+
+
+
+
+
diff --git a/demos/tw-notifications/src/ts/components/demo-notifications/demo-notifications.component.ts b/demos/tw-notifications/src/ts/components/demo-notifications/demo-notifications.component.ts
new file mode 100644
index 000000000..8221482ff
--- /dev/null
+++ b/demos/tw-notifications/src/ts/components/demo-notifications/demo-notifications.component.ts
@@ -0,0 +1,95 @@
+import { Component } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoNotificationsComponent extends Component {
+ public static tagName = "demo-notifications";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ protected toastChannel = new EventDispatcher("toast");
+
+ public scope = {
+ showInfoToast: this.showInfoToast.bind(this),
+ showSuccessToast: this.showSuccessToast.bind(this),
+ showWarningToast: this.showWarningToast.bind(this),
+ showErrorToast: this.showErrorToast.bind(this),
+ showModal: this.showModal.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoNotificationsComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ public showInfoToast() {
+ this.toastChannel.trigger("show-notification", {
+ kind: "toast",
+ type: "info",
+ title: "Info",
+ message: "This is an informational toast notification.",
+ timeout: 5000,
+ });
+ }
+
+ public showSuccessToast() {
+ this.toastChannel.trigger("show-notification", {
+ kind: "toast",
+ type: "success",
+ title: "Success",
+ message: "The operation completed successfully!",
+ timeout: 5000,
+ });
+ }
+
+ public showWarningToast() {
+ this.toastChannel.trigger("show-notification", {
+ kind: "toast",
+ type: "warning",
+ title: "Warning",
+ message: "Please review your input before proceeding.",
+ timeout: 7000,
+ });
+ }
+
+ public showErrorToast() {
+ this.toastChannel.trigger("show-notification", {
+ kind: "toast",
+ type: "error",
+ title: "Error",
+ message: "Something went wrong. Please try again later.",
+ timeout: 0,
+ });
+ }
+
+ public showModal() {
+ this.toastChannel.trigger("show-notification", {
+ kind: "modal",
+ title: "Confirm Action",
+ message:
+ "Are you sure you want to proceed? This action cannot be undone.",
+ closable: true,
+ });
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-notifications.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-notifications/src/ts/components/index.ts b/demos/tw-notifications/src/ts/components/index.ts
new file mode 100644
index 000000000..2a85c0268
--- /dev/null
+++ b/demos/tw-notifications/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoNotificationsComponent } from "./demo-notifications/demo-notifications.component.js";
diff --git a/demos/tw-notifications/src/ts/main.ts b/demos/tw-notifications/src/ts/main.ts
new file mode 100644
index 000000000..40f30594b
--- /dev/null
+++ b/demos/tw-notifications/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./notifications.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-notifications/src/ts/notifications.module.ts b/demos/tw-notifications/src/ts/notifications.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-notifications/src/ts/notifications.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-notifications/tsconfig.json b/demos/tw-notifications/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-notifications/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-notifications/vite.config.js b/demos/tw-notifications/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-notifications/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-sidebar/package.json b/demos/tw-sidebar/package.json
new file mode 100644
index 000000000..f86d13768
--- /dev/null
+++ b/demos/tw-sidebar/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-sidebar",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-sidebar/src/css/main.css b/demos/tw-sidebar/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-sidebar/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-sidebar/src/index.html b/demos/tw-sidebar/src/index.html
new file mode 100644
index 000000000..a742c9c84
--- /dev/null
+++ b/demos/tw-sidebar/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
TW Sidebar Demo
+
+
+
+
+
+
+
diff --git a/demos/tw-sidebar/src/ts/components/demo-sidebar/demo-sidebar.component.html b/demos/tw-sidebar/src/ts/components/demo-sidebar/demo-sidebar.component.html
new file mode 100644
index 000000000..0608b6219
--- /dev/null
+++ b/demos/tw-sidebar/src/ts/components/demo-sidebar/demo-sidebar.component.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
TW Sidebar Demo
+
+
+
+ Overlap Mode
+
+ The sidebar floats over the content with a dark backdrop. Click the backdrop or the close button to dismiss.
+
+
+
+
+
+
+
+ Left Overlay
+
+
+
+
+
+ Right Overlay
+
+
+
+
+
+
+
+
+
+
+ Side Mode
+
+ The sidebar pushes the main content aside. No backdrop is shown and the content reflows.
+
+
+
+
+
+
+
+ Toggle Side Mode
+
+
+
+
+
+
+
+ Main Content
+
+
+ Welcome to the dashboard. Use the sidebar to navigate between different sections.
+
+
+ View and manage your active projects. Click on a project to see its details in the right sidebar.
+
+
+ Track your performance metrics. Open the details panel for more information.
+
+
+
+
+
diff --git a/demos/tw-sidebar/src/ts/components/demo-sidebar/demo-sidebar.component.ts b/demos/tw-sidebar/src/ts/components/demo-sidebar/demo-sidebar.component.ts
new file mode 100644
index 000000000..eef051808
--- /dev/null
+++ b/demos/tw-sidebar/src/ts/components/demo-sidebar/demo-sidebar.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoSidebarComponent extends Component {
+ public static tagName = "demo-sidebar";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoSidebarComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-sidebar.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-sidebar/src/ts/components/index.ts b/demos/tw-sidebar/src/ts/components/index.ts
new file mode 100644
index 000000000..93d1d080a
--- /dev/null
+++ b/demos/tw-sidebar/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoSidebarComponent } from "./demo-sidebar/demo-sidebar.component.js";
diff --git a/demos/tw-sidebar/src/ts/main.ts b/demos/tw-sidebar/src/ts/main.ts
new file mode 100644
index 000000000..4002dd9f0
--- /dev/null
+++ b/demos/tw-sidebar/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./sidebar.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-sidebar/src/ts/sidebar.module.ts b/demos/tw-sidebar/src/ts/sidebar.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-sidebar/src/ts/sidebar.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-sidebar/tsconfig.json b/demos/tw-sidebar/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-sidebar/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-sidebar/vite.config.js b/demos/tw-sidebar/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-sidebar/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-slideshow/package.json b/demos/tw-slideshow/package.json
new file mode 100644
index 000000000..e9d54a130
--- /dev/null
+++ b/demos/tw-slideshow/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@ribajs/demo-tw-slideshow",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "main": "./src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "start": "vite",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "tailwindcss": "^4.2.2"
+ },
+ "devDependencies": {
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ }
+}
diff --git a/demos/tw-slideshow/src/css/main.css b/demos/tw-slideshow/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-slideshow/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-slideshow/src/index.html b/demos/tw-slideshow/src/index.html
new file mode 100644
index 000000000..4833e2a1a
--- /dev/null
+++ b/demos/tw-slideshow/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
Tailwind Slideshow Demo
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-slideshow/src/ts/components/demo-slideshow/demo-slideshow.component.html b/demos/tw-slideshow/src/ts/components/demo-slideshow/demo-slideshow.component.html
new file mode 100644
index 000000000..f1a8c65fd
--- /dev/null
+++ b/demos/tw-slideshow/src/ts/components/demo-slideshow/demo-slideshow.component.html
@@ -0,0 +1,116 @@
+
+
Tailwind Slideshow Demo
+
+
+
+ Basic Slideshow
+ A slideshow with 4 images, prev/next controls, indicator dots, and desktop drag-scroll enabled.
+
+
+
+
+
+
+
+ Carousel with Autoplay
+ A multi-column carousel that auto-advances every 3 seconds.
+
+
+
+
+
+
+
+ Slideshow with Video Slide
+ A slideshow containing a video that auto-plays when its slide is active.
+
+
+
+
+
+
+
+ Content Slider
+ A multi-column slider for cards or content blocks. Supports drag-scroll and keyboard navigation.
+
+
+
+
+
diff --git a/demos/tw-slideshow/src/ts/components/demo-slideshow/demo-slideshow.component.ts b/demos/tw-slideshow/src/ts/components/demo-slideshow/demo-slideshow.component.ts
new file mode 100644
index 000000000..cebe47279
--- /dev/null
+++ b/demos/tw-slideshow/src/ts/components/demo-slideshow/demo-slideshow.component.ts
@@ -0,0 +1,37 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoSlideshowComponent extends Component {
+ public static tagName = "rv-demo-slideshow";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(DemoSlideshowComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-slideshow.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-slideshow/src/ts/components/index.ts b/demos/tw-slideshow/src/ts/components/index.ts
new file mode 100644
index 000000000..1c333530a
--- /dev/null
+++ b/demos/tw-slideshow/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoSlideshowComponent } from "./demo-slideshow/demo-slideshow.component.js";
diff --git a/demos/tw-slideshow/src/ts/main.ts b/demos/tw-slideshow/src/ts/main.ts
new file mode 100644
index 000000000..cba784a97
--- /dev/null
+++ b/demos/tw-slideshow/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { SlideshowDemoModule } from "./slideshow.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(SlideshowDemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-slideshow/src/ts/slideshow.module.ts b/demos/tw-slideshow/src/ts/slideshow.module.ts
new file mode 100644
index 000000000..08aec5d2d
--- /dev/null
+++ b/demos/tw-slideshow/src/ts/slideshow.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const SlideshowDemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-slideshow/tsconfig.json b/demos/tw-slideshow/tsconfig.json
new file mode 100644
index 000000000..7e47a757d
--- /dev/null
+++ b/demos/tw-slideshow/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "exclude": ["node_modules", "dist/**/*.d.ts"]
+}
diff --git a/demos/tw-slideshow/vite.config.js b/demos/tw-slideshow/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-slideshow/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-tabs/package.json b/demos/tw-tabs/package.json
new file mode 100644
index 000000000..9107f4fc0
--- /dev/null
+++ b/demos/tw-tabs/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@ribajs/demo-tw-tabs",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-tabs/src/css/main.css b/demos/tw-tabs/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-tabs/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-tabs/src/index.html b/demos/tw-tabs/src/index.html
new file mode 100644
index 000000000..1ef6466d4
--- /dev/null
+++ b/demos/tw-tabs/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
TW Tabs Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-tabs/src/ts/components/demo-tabs/demo-tabs.component.html b/demos/tw-tabs/src/ts/components/demo-tabs/demo-tabs.component.html
new file mode 100644
index 000000000..41ba30ab7
--- /dev/null
+++ b/demos/tw-tabs/src/ts/components/demo-tabs/demo-tabs.component.html
@@ -0,0 +1,109 @@
+
TW Tabs + Steps Demo
+
+
+
+ Tabs
+
+ Click on the tab headers to switch between content panels. Tabs are defined using <template> children.
+
+
+
+
+
Overview
+
+ Riba.js is a TypeScript declarative data-binding and Web Components framework.
+ It evolved from Rivets.js and tinybind into a modern, ESM-only library.
+
+
+ Declarative data binding with rv-* attributes
+ Web Components v1 support
+ TypeScript-first design
+ ESM-only, modern build pipeline
+
+
+
+
+
+
Features
+
+
+
Binders
+
+ Bind DOM attributes and properties to your model using rv-text, rv-html, rv-value, rv-on-*, rv-each-*, and many more.
+
+
+
+
Formatters
+
+ Transform values using pipe syntax: string manipulation, comparison, math, type conversion, and more.
+
+
+
+
Components
+
+ Custom elements with lifecycle hooks, template loading, and automatic data binding through the scope object.
+
+
+
+
Router
+
+ PJAX-based SPA navigation with transition animations, prefetching, and history management.
+
+
+
+
+
+
+
+
Getting Started
+
+
+ Install the packages
+ yarn add @ribajs/core @ribajs/tw
+
+
+ Create your Riba instance
+ import { Riba, coreModule } from "@ribajs/core";
+import { twModule } from "@ribajs/tw";
+
+const riba = new Riba();
+riba.module.register(coreModule.init());
+riba.module.register(twModule.init());
+
+
+ Bind to the DOM
+ riba.bind(document.body, { message: "Hello!" });
+
+
+
+
+
+
+
+
+
+ Steps Wizard
+
+ Click on any step circle to navigate to that step. Completed steps show a checkmark.
+
+
+ Horizontal Steps
+
+
+
+
+
+
+
+
+
+ Vertical Steps
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-tabs/src/ts/components/demo-tabs/demo-tabs.component.ts b/demos/tw-tabs/src/ts/components/demo-tabs/demo-tabs.component.ts
new file mode 100644
index 000000000..6ffbc8730
--- /dev/null
+++ b/demos/tw-tabs/src/ts/components/demo-tabs/demo-tabs.component.ts
@@ -0,0 +1,36 @@
+import { Component } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoTabsComponent extends Component {
+ public static tagName = "demo-tabs";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ public scope = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoTabsComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-tabs.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-tabs/src/ts/components/index.ts b/demos/tw-tabs/src/ts/components/index.ts
new file mode 100644
index 000000000..d6c345d77
--- /dev/null
+++ b/demos/tw-tabs/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoTabsComponent } from "./demo-tabs/demo-tabs.component.js";
diff --git a/demos/tw-tabs/src/ts/main.ts b/demos/tw-tabs/src/ts/main.ts
new file mode 100644
index 000000000..4575ffd1d
--- /dev/null
+++ b/demos/tw-tabs/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./tabs.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-tabs/src/ts/tabs.module.ts b/demos/tw-tabs/src/ts/tabs.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-tabs/src/ts/tabs.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-tabs/tsconfig.json b/demos/tw-tabs/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-tabs/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-tabs/vite.config.js b/demos/tw-tabs/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-tabs/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/demos/tw-theme/package.json b/demos/tw-theme/package.json
new file mode 100644
index 000000000..35a82829c
--- /dev/null
+++ b/demos/tw-theme/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@ribajs/demo-tw-theme",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "description": "",
+ "main": "./src/ts/main.ts",
+ "module": "src/ts/main.ts",
+ "source": "src/ts/main.ts",
+ "private": true,
+ "scripts": {
+ "build": "vite build --mode production",
+ "build:dev": "vite build --mode development",
+ "start": "vite",
+ "start:dev": "vite",
+ "serve": "serve dist",
+ "clear": "rm -rf ./dist",
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "src"
+ ],
+ "author": "",
+ "license": "MIT",
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/vite-config": "workspace:^",
+ "@tailwindcss/vite": "^4.2.2",
+ "eslint": "^10.2.0",
+ "serve": "^14.2.6",
+ "typescript": "6.0.2",
+ "vite": "^8.0.8"
+ },
+ "dependencies": {
+ "@ribajs/core": "workspace:^",
+ "@ribajs/events": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/tw": "workspace:^",
+ "@ribajs/utils": "workspace:^",
+ "tailwindcss": "^4.2.2"
+ }
+}
diff --git a/demos/tw-theme/src/css/main.css b/demos/tw-theme/src/css/main.css
new file mode 100644
index 000000000..eec2b775c
--- /dev/null
+++ b/demos/tw-theme/src/css/main.css
@@ -0,0 +1,4 @@
+@import "tailwindcss";
+@import "@ribajs/tw/src/css/utilities.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
diff --git a/demos/tw-theme/src/index.html b/demos/tw-theme/src/index.html
new file mode 100644
index 000000000..ab2a9f247
--- /dev/null
+++ b/demos/tw-theme/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
TW Theme Demo
+
+
+
+
+
+
+
+
+
diff --git a/demos/tw-theme/src/ts/components/demo-theme/demo-theme.component.html b/demos/tw-theme/src/ts/components/demo-theme/demo-theme.component.html
new file mode 100644
index 000000000..9a436eecf
--- /dev/null
+++ b/demos/tw-theme/src/ts/components/demo-theme/demo-theme.component.html
@@ -0,0 +1,308 @@
+
+
+
+
+
+
+
+
+
TW Theme Demo
+
+
+
+ Theme Switcher
+
+ Use the controls below to switch between light, dark, and system themes.
+ The entire page will update to reflect the selected theme.
+
+
+
+
+
Dropdown Mode
+
+
+
+
+
Icon Mode
+
+
+
+
+
Large Icon
+
+
+
+
+
+
+
+ Theme-Aware Cards
+
+ These cards use Tailwind's dark: variant classes to adapt their appearance based on the active theme.
+
+
+
+
+
+
Light Theme
+
+ A clean, bright appearance with white backgrounds and dark text. Perfect for well-lit environments.
+
+
+
+
+
+
Dark Theme
+
+ Easy on the eyes with dark backgrounds and light text. Ideal for low-light conditions and reducing eye strain.
+
+
+
+
+
+
System Theme
+
+ Automatically follows your operating system's theme preference, switching seamlessly between light and dark.
+
+
+
+
+
+
+
+ Typography
+
+ Text elements that adapt to the current theme using Tailwind dark mode classes.
+
+
+
+
Heading Text
+
+ This is body text that adjusts its color based on the active theme. In light mode, it appears as dark gray on a light background. In dark mode, it becomes light gray on a dark background.
+
+
+ This is secondary text with a more subtle appearance, useful for descriptions, captions, and less important information.
+
+
+ This is a themed link
+
+
+
+
+
+
+ Color Palette
+
+ See how color swatches adapt between light and dark mode backgrounds.
+
+
+
+
+
+
+
Success
+
green-500
+
+
+
+
Warning
+
yellow-500
+
+
+
+
+
+
+
+ Form Elements
+
+ Form controls that respond to theme changes.
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Message
+
+
+
+ Submit
+
+
+
+
+
+
+ Sidebar (Backdrop Test)
+
+ Opens a sidebar in overlap mode. The dark backdrop should be noticeably darker in dark mode
+ (bg-black/70) than in light mode
+ (bg-black/50).
+
+
+
+
+
+
+
+ Open Left Sidebar
+
+
+
+
+ Open Right Sidebar
+
+
+
+
+
+
+
+
+
+
+ Collapse & Accordion
+
+ Collapse button uses bg-blue-600 dark:bg-blue-700;
+ the panel below adapts its border and background to the theme.
+
+
+
+
+
+
+
+ This accordion group shows one item at a time. The chevron rotates when expanded and the panel background switches between bg-white and dark:bg-gray-800.
+
+
+ Borders also adapt: border-gray-200 in light mode, dark:border-gray-700 in dark mode.
+
+
+ Inline code like bg-gray-100 is styled for both themes as well.
+
+
+
+
+
+
+
+ Dropdown
+
+ Floating menu surface — tests dark-mode background, hover state and divider color.
+
+
+
+ Options
+
+
+
+
+
+
+
+
+
+
+ Alerts
+
+ All four severity levels in their light and dark variants.
+
+
+
+
+
+
+
+
+
+
+
+ Notifications (Second Backdrop Test)
+
+ The modal renders an additional dark backdrop. Toasts stack in the corner with type-colored surfaces.
+
+
+
+ Open Modal
+
+
+ Info Toast
+
+
+ Success Toast
+
+
+ Warning Toast
+
+
+ Error Toast
+
+
+
diff --git a/demos/tw-theme/src/ts/components/demo-theme/demo-theme.component.ts b/demos/tw-theme/src/ts/components/demo-theme/demo-theme.component.ts
new file mode 100644
index 000000000..97053dd35
--- /dev/null
+++ b/demos/tw-theme/src/ts/components/demo-theme/demo-theme.component.ts
@@ -0,0 +1,95 @@
+import { Component } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export class DemoThemeComponent extends Component {
+ public static tagName = "demo-theme";
+
+ protected autobind = true;
+ static get observedAttributes(): string[] {
+ return [];
+ }
+
+ protected notifications = new EventDispatcher("theme-demo");
+
+ public scope = {
+ showModal: this.showModal.bind(this),
+ showInfoToast: this.showInfoToast.bind(this),
+ showSuccessToast: this.showSuccessToast.bind(this),
+ showWarningToast: this.showWarningToast.bind(this),
+ showErrorToast: this.showErrorToast.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ super.init(DemoThemeComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ public showModal() {
+ this.notifications.trigger("show-notification", {
+ kind: "modal",
+ title: "Theme Backdrop Test",
+ message:
+ "This modal renders a second backdrop. Toggle the theme to compare the light and dark overlay.",
+ closable: true,
+ });
+ }
+
+ public showInfoToast() {
+ this.notifications.trigger("show-notification", {
+ kind: "toast",
+ type: "info",
+ title: "Info",
+ message: "Informational toast in the active theme.",
+ timeout: 5000,
+ });
+ }
+
+ public showSuccessToast() {
+ this.notifications.trigger("show-notification", {
+ kind: "toast",
+ type: "success",
+ title: "Success",
+ message: "Success toast in the active theme.",
+ timeout: 5000,
+ });
+ }
+
+ public showWarningToast() {
+ this.notifications.trigger("show-notification", {
+ kind: "toast",
+ type: "warning",
+ title: "Warning",
+ message: "Warning toast in the active theme.",
+ timeout: 7000,
+ });
+ }
+
+ public showErrorToast() {
+ this.notifications.trigger("show-notification", {
+ kind: "toast",
+ type: "error",
+ title: "Error",
+ message: "Error toast in the active theme.",
+ timeout: 0,
+ });
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: template } =
+ await import("./demo-theme.component.html?raw");
+ return template;
+ }
+ }
+}
diff --git a/demos/tw-theme/src/ts/components/index.ts b/demos/tw-theme/src/ts/components/index.ts
new file mode 100644
index 000000000..6c85d2a0e
--- /dev/null
+++ b/demos/tw-theme/src/ts/components/index.ts
@@ -0,0 +1 @@
+export { DemoThemeComponent } from "./demo-theme/demo-theme.component.js";
diff --git a/demos/tw-theme/src/ts/main.ts b/demos/tw-theme/src/ts/main.ts
new file mode 100644
index 000000000..3ee9c3c21
--- /dev/null
+++ b/demos/tw-theme/src/ts/main.ts
@@ -0,0 +1,14 @@
+import { coreModule, Riba } from "@ribajs/core";
+import { extrasModule } from "@ribajs/extras";
+import { twModule } from "@ribajs/tw";
+import { DemoModule } from "./theme.module.js";
+
+const riba = new Riba();
+const model = {};
+
+riba.module.register(coreModule.init());
+riba.module.register(extrasModule.init());
+riba.module.register(twModule.init());
+riba.module.register(DemoModule.init());
+
+riba.bind(document.body, model);
diff --git a/demos/tw-theme/src/ts/theme.module.ts b/demos/tw-theme/src/ts/theme.module.ts
new file mode 100644
index 000000000..064033ae0
--- /dev/null
+++ b/demos/tw-theme/src/ts/theme.module.ts
@@ -0,0 +1,12 @@
+import { RibaModule } from "@ribajs/core";
+import * as components from "./components/index.js";
+
+export const DemoModule: RibaModule = {
+ binders: {},
+ components,
+ formatters: {},
+ services: {},
+ init() {
+ return this;
+ },
+};
diff --git a/demos/tw-theme/tsconfig.json b/demos/tw-theme/tsconfig.json
new file mode 100644
index 000000000..df31a4647
--- /dev/null
+++ b/demos/tw-theme/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": []
+ },
+ "exclude": [
+ "node_modules",
+ "dist/**/*.d.ts"
+ ]
+}
diff --git a/demos/tw-theme/vite.config.js b/demos/tw-theme/vite.config.js
new file mode 100644
index 000000000..136a53c10
--- /dev/null
+++ b/demos/tw-theme/vite.config.js
@@ -0,0 +1,6 @@
+import tailwindcss from "@tailwindcss/vite";
+import { ribaViteConfig } from "@ribajs/vite-config";
+
+const config = ribaViteConfig();
+config.plugins = [...(config.plugins || []), tailwindcss()];
+export default config;
diff --git a/infra/doc/package.json b/infra/doc/package.json
index d57279e48..0001362c8 100644
--- a/infra/doc/package.json
+++ b/infra/doc/package.json
@@ -43,15 +43,15 @@
"devDependencies": {
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
- "@types/node": "^24.12.0",
+ "@types/node": "^24.12.2",
"@types/prismjs": "^1.26.6",
"concurrently": "^9.2.1",
"js-yaml": "^4.1.1",
- "marked": "^15.0.0",
- "pug": "^3.0.3",
- "sass": "^1.98.0",
+ "marked": "^18.0.0",
+ "pug": "^3.0.4",
+ "sass": "^1.99.0",
"typescript": "^6.0.2",
- "vite": "^8.0.3",
- "vite-plugin-static-copy": "^3.2.0"
+ "vite": "^8.0.8",
+ "vite-plugin-static-copy": "^4.0.1"
}
}
diff --git a/infra/eslint-config/package.json b/infra/eslint-config/package.json
index 762a01060..e8a4f1952 100644
--- a/infra/eslint-config/package.json
+++ b/infra/eslint-config/package.json
@@ -37,14 +37,14 @@
"prettier"
],
"dependencies": {
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.5",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"typescript": "6.0.2"
},
"bugs": {
diff --git a/infra/postcss-config/package.json b/infra/postcss-config/package.json
index 25fd5867b..c7861970b 100644
--- a/infra/postcss-config/package.json
+++ b/infra/postcss-config/package.json
@@ -42,8 +42,8 @@
"dependencies": {
"@fullhuman/postcss-purgecss": "^8.0.0",
"@ribajs/npm-package": "workspace:^",
- "postcss": "^8.5.8",
- "postcss-preset-env": "^11.2.0"
+ "postcss": "^8.5.9",
+ "postcss-preset-env": "^11.2.1"
},
"optionalDependencies": {
"@ribajs/bs5": "workspace:^",
diff --git a/infra/vite-config/package.json b/infra/vite-config/package.json
index 466487f10..192c2d563 100644
--- a/infra/vite-config/package.json
+++ b/infra/vite-config/package.json
@@ -16,6 +16,6 @@
"dependencies": {
"@ribajs/iconset": "workspace:^",
"rollup-plugin-pug": "^1.1.1",
- "vite": "^8.0.3"
+ "vite": "^8.0.8"
}
}
diff --git a/package.json b/package.json
index 23ce530c9..6b9e98b62 100644
--- a/package.json
+++ b/package.json
@@ -55,24 +55,24 @@
"test:e2e:headed": "playwright test --headed"
},
"devDependencies": {
- "@playwright/test": "^1.59.0",
+ "@playwright/test": "^1.59.1",
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@ribajs/types": "workspace:^",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.5",
- "jsdom": "^26.1.0",
- "prettier": "^3.8.1",
- "sass": "^1.98.0",
+ "jsdom": "^29.0.2",
+ "prettier": "^3.8.2",
+ "sass": "^1.99.0",
"typescript": "^6.0.2",
- "vite": "^8.0.3",
- "vitest": "^3.2.4"
+ "vite": "^8.0.8",
+ "vitest": "^4.1.4"
},
"workspaces": [
"packages/*",
@@ -118,7 +118,8 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/nest-theme": "workspace:^",
"@ribajs/npm-package": "workspace:^",
- "@ribajs/strapi": "workspace:^"
+ "@ribajs/strapi": "workspace:^",
+ "@ribajs/tw": "workspace:^"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
diff --git a/packages/accessibility/package.json b/packages/accessibility/package.json
index 86f41b09e..6c36868f5 100644
--- a/packages/accessibility/package.json
+++ b/packages/accessibility/package.json
@@ -23,8 +23,8 @@
"@ribajs/gamecontroller.js": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/eslint": "^9.6.1",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/artcodestudio/package.json b/packages/artcodestudio/package.json
index 27f349fd7..35d4772ff 100644
--- a/packages/artcodestudio/package.json
+++ b/packages/artcodestudio/package.json
@@ -25,17 +25,17 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/types": "workspace:^",
"@types/jest": "^30.0.0",
- "@types/node": "^24.12.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@types/node": "^24.12.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
}
}
diff --git a/packages/bs5-photoswipe/package.json b/packages/bs5-photoswipe/package.json
index 6de7a8cc4..374493a48 100644
--- a/packages/bs5-photoswipe/package.json
+++ b/packages/bs5-photoswipe/package.json
@@ -39,15 +39,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/bs5/package.json b/packages/bs5/package.json
index a4af6b40d..caf95f39d 100644
--- a/packages/bs5/package.json
+++ b/packages/bs5/package.json
@@ -54,16 +54,16 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/types": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"@yarnpkg/pnpify": "^4.1.6",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/bs5/src/components/bs5-slideshow/bs5-slideshow-slides.component.html b/packages/bs5/src/components/bs5-slideshow/bs5-slideshow-slides.component.html
index f838b1119..23a686d75 100644
--- a/packages/bs5/src/components/bs5-slideshow/bs5-slideshow-slides.component.html
+++ b/packages/bs5/src/components/bs5-slideshow/bs5-slideshow-slides.component.html
@@ -1,3 +1,3 @@
diff --git a/packages/cache/package.json b/packages/cache/package.json
index 3cf87ebc8..9e753deef 100644
--- a/packages/cache/package.json
+++ b/packages/cache/package.json
@@ -55,12 +55,12 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/content-slider/package.json b/packages/content-slider/package.json
index c99d2b5a6..df7a7f6d5 100644
--- a/packages/content-slider/package.json
+++ b/packages/content-slider/package.json
@@ -39,15 +39,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/core/package.json b/packages/core/package.json
index fc0b7a4b1..f834bdded 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -52,16 +52,16 @@
"@ribajs/tsconfig": "workspace:^",
"@ribajs/types": "workspace:^",
"@types/jest": "^30.0.0",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-config": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"source-map-support": "^0.5.21",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"bugs": {
diff --git a/packages/empty-template/package.json b/packages/empty-template/package.json
index 53525a626..34b3b93b3 100644
--- a/packages/empty-template/package.json
+++ b/packages/empty-template/package.json
@@ -23,8 +23,8 @@
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
"@types/eslint": "^9.6.1",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/events/package.json b/packages/events/package.json
index e61313b7b..805239799 100644
--- a/packages/events/package.json
+++ b/packages/events/package.json
@@ -24,12 +24,12 @@
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"jest": "^30.3.0",
"jest-config": "^30.3.0",
"jest-extended": "^7.0.0",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
}
}
diff --git a/packages/extras/package.json b/packages/extras/package.json
index f0a6b841a..f3510dbbf 100644
--- a/packages/extras/package.json
+++ b/packages/extras/package.json
@@ -39,15 +39,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/extras/src/binders/index.ts b/packages/extras/src/binders/index.ts
index 41cfbe084..6348bc21c 100644
--- a/packages/extras/src/binders/index.ts
+++ b/packages/extras/src/binders/index.ts
@@ -5,3 +5,8 @@ export * from "./scroll-position-angle.binder.js";
export * from "./scrollbar-autoscroll.binder.js";
export * from "./scrollbar-draggable.binder.js";
export * from "./sync-element-property.binder.js";
+export * from "./scroll-to-on-event.binder.js";
+export * from "./toggle-attribute.binder.js";
+export * from "./toggle-class.binder.js";
+export * from "./scrollspy-class.binder.js";
+export * from "./show-toast-on.binder.js";
diff --git a/packages/extras/src/binders/scroll-to-on-event.binder.ts b/packages/extras/src/binders/scroll-to-on-event.binder.ts
new file mode 100644
index 000000000..4ddd521d6
--- /dev/null
+++ b/packages/extras/src/binders/scroll-to-on-event.binder.ts
@@ -0,0 +1,37 @@
+import { Binder } from "@ribajs/core";
+import { scrollTo } from "@ribajs/utils/src/dom.js";
+
+export class ScrollToOnEventBinder extends Binder
{
+ static key = "scroll-to-on-*";
+
+ private target?: HTMLElement;
+
+ private _onEvent(event: Event) {
+ const offset = Number(this.el.dataset.offset || 0);
+ const scrollElement = this.el.dataset.scrollElement
+ ? document.querySelector(this.el.dataset.scrollElement)
+ : window;
+ if (this.target) {
+ scrollTo(this.target, offset, scrollElement);
+ event.preventDefault();
+ }
+ }
+
+ private onEvent = this._onEvent.bind(this);
+
+ bind(el: HTMLUnknownElement) {
+ this.onEvent = this.onEvent.bind(this);
+ const eventName = this.args[0] as string;
+ el.addEventListener(eventName, this.onEvent);
+ }
+
+ routine(el: HTMLUnknownElement, targetSelector: string) {
+ this.target =
+ document.querySelector(targetSelector) || undefined;
+ }
+
+ unbind(el: HTMLElement) {
+ const eventName = this.args[0] as string;
+ el.removeEventListener(eventName, this.onEvent);
+ }
+}
diff --git a/packages/extras/src/binders/scrollspy-class.binder.ts b/packages/extras/src/binders/scrollspy-class.binder.ts
new file mode 100644
index 000000000..f91fc6e90
--- /dev/null
+++ b/packages/extras/src/binders/scrollspy-class.binder.ts
@@ -0,0 +1,69 @@
+import { Binder } from "@ribajs/core";
+import { debounce } from "@ribajs/utils/src/control.js";
+import { isInViewport } from "@ribajs/utils/src/dom.js";
+
+/**
+ * Adds or removes a CSS class based on whether a target element is in the viewport.
+ *
+ * Usage: `rv-scrollspy-active="'#section-id'"`
+ * This will add/remove the class "active" on the bound element when #section-id
+ * is in the viewport.
+ */
+export class ScrollspyClassBinder extends Binder {
+ static key = "scrollspy-*";
+
+ private target?: HTMLElement;
+ private className?: string;
+
+ private _onScroll() {
+ if (!this.target) {
+ throw new Error("No target element found!");
+ }
+
+ if (!this.className) {
+ throw new Error("className not set!");
+ }
+
+ if (this.isInViewport(this.target)) {
+ this.el.classList.add(this.className);
+ if (this.el.type === "radio") {
+ this.el.checked = true;
+ }
+ } else {
+ this.el.classList.remove(this.className);
+ if (this.el.type === "radio") {
+ this.el.checked = false;
+ }
+ }
+ }
+
+ private onScroll = debounce(this._onScroll.bind(this));
+
+ private _isInViewport(elem: Element) {
+ if (!elem) {
+ return false;
+ }
+ const offsetTop = Number(this.el.dataset.offset || 0);
+ const offsetBottom = Number(this.el.dataset.offsetBottom || 0);
+ return isInViewport(elem, { top: offsetTop, bottom: offsetBottom });
+ }
+
+ private isInViewport = this._isInViewport.bind(this);
+
+ bind() {
+ window.addEventListener("scroll", this.onScroll, {
+ passive: true,
+ });
+ this.onScroll();
+ }
+
+ routine(el: HTMLElement, targetSelector: string) {
+ const nativeIDTargetSelector = targetSelector.replace("#", "");
+ this.target = document.getElementById(nativeIDTargetSelector) || undefined;
+ this.className = this.args[0] as string;
+ }
+
+ unbind() {
+ window.removeEventListener("scroll", this.onScroll);
+ }
+}
diff --git a/packages/extras/src/binders/show-toast-on.binder.ts b/packages/extras/src/binders/show-toast-on.binder.ts
new file mode 100644
index 000000000..4afacc4be
--- /dev/null
+++ b/packages/extras/src/binders/show-toast-on.binder.ts
@@ -0,0 +1,45 @@
+import { Binder } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import type { ToastNotification } from "../types/notification.js";
+
+/**
+ * Shows a toast notification when a specified event fires on the element.
+ *
+ * Usage: `rv-show-toast-on-click="toastData"`
+ */
+export class ShowToastOnEventBinder extends Binder<
+ ToastNotification,
+ HTMLElement
+> {
+ static key = "show-toast-on-*";
+
+ private toastData?: ToastNotification;
+
+ private _onEvent() {
+ if (!this.toastData) {
+ throw new Error("Toast data not set!");
+ }
+ const toastData: ToastNotification = { ...this.toastData };
+ const notificationDispatcher = new EventDispatcher("toast");
+ notificationDispatcher.trigger("show-notification", toastData);
+ }
+
+ private onEvent = this._onEvent.bind(this);
+
+ bind(el: HTMLElement) {
+ const eventName = this.args[0] as string;
+ el.addEventListener(eventName as any, this.onEvent);
+ }
+
+ routine(_el: HTMLElement, toastData: ToastNotification) {
+ if (this.args === null) {
+ throw new Error("args is null");
+ }
+ this.toastData = toastData;
+ }
+
+ unbind(el: HTMLElement) {
+ const eventName = this.args[0] as string;
+ el.removeEventListener(eventName as any, this.onEvent);
+ }
+}
diff --git a/packages/extras/src/binders/toggle-attribute.binder.ts b/packages/extras/src/binders/toggle-attribute.binder.ts
new file mode 100644
index 000000000..15c14c888
--- /dev/null
+++ b/packages/extras/src/binders/toggle-attribute.binder.ts
@@ -0,0 +1,97 @@
+import { Binder } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { TOGGLE_BUTTON, TOGGLE_ATTRIBUTE } from "../constants/index.js";
+
+/**
+ * Framework-agnostic toggle attribute binder.
+ * Adds / removes an attribute via toggle-button EventDispatcher.
+ */
+export class ToggleAttributeBinder extends Binder {
+ static key = "toggle-attribute-*";
+
+ private toggleButtonEvents?: EventDispatcher;
+ private state = "off";
+
+ private _triggerState() {
+ this.toggleButtonEvents?.trigger(
+ TOGGLE_BUTTON.eventNames.state,
+ this.state,
+ );
+ }
+
+ private triggerState = this._triggerState.bind(this);
+
+ private _onToggle() {
+ this.toggle.bind(this)(this.el);
+ }
+
+ private onToggle = this._onToggle.bind(this);
+
+ private toggle(el: HTMLElement) {
+ if (this.state === "removed") {
+ this.add.bind(this)(el);
+ } else {
+ this.remove.bind(this)(el);
+ }
+ }
+
+ private remove(el: HTMLElement) {
+ const attributeName = this.args[0] as string;
+ el.removeAttribute(attributeName);
+ this.state = "removed";
+ el.dispatchEvent(
+ new CustomEvent(TOGGLE_ATTRIBUTE.elEventNames.removed, {
+ detail: { attributeName },
+ }),
+ );
+ this.triggerState();
+ }
+
+ private add(el: HTMLElement) {
+ const attributeName = this.args[0] as string;
+ el.setAttribute(attributeName, attributeName);
+ this.state = "added";
+ el.dispatchEvent(
+ new CustomEvent(TOGGLE_ATTRIBUTE.elEventNames.added, {
+ detail: { attributeName },
+ }),
+ );
+ this.triggerState();
+ }
+
+ bind(el: HTMLElement) {
+ const attributeName = this.args[0] as string;
+ this.state = el.hasAttribute(attributeName) ? "added" : "removed";
+ }
+
+ unbind() {
+ this.toggleButtonEvents?.off(
+ TOGGLE_BUTTON.eventNames.toggle,
+ this.onToggle,
+ this,
+ );
+ this.toggleButtonEvents?.off(
+ TOGGLE_BUTTON.eventNames.init,
+ this.triggerState,
+ this,
+ );
+ }
+
+ routine(el: HTMLElement, newId: string) {
+ const oldId = this._getValue(el);
+ let toggleButton = this.toggleButtonEvents;
+ if (oldId && toggleButton) {
+ toggleButton.off(TOGGLE_BUTTON.eventNames.toggle, this.onToggle, this);
+ toggleButton.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+
+ if (!this.toggleButtonEvents) {
+ this.toggleButtonEvents = new EventDispatcher(
+ TOGGLE_BUTTON.nsPrefix + newId,
+ );
+ toggleButton = this.toggleButtonEvents as EventDispatcher;
+ toggleButton.on(TOGGLE_BUTTON.eventNames.toggle, this.onToggle, this);
+ toggleButton.on(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+ }
+}
diff --git a/packages/extras/src/binders/toggle-class.binder.ts b/packages/extras/src/binders/toggle-class.binder.ts
new file mode 100644
index 000000000..6768edd73
--- /dev/null
+++ b/packages/extras/src/binders/toggle-class.binder.ts
@@ -0,0 +1,97 @@
+import { Binder } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { TOGGLE_BUTTON, TOGGLE_CLASS } from "../constants/index.js";
+
+/**
+ * Framework-agnostic toggle class binder.
+ * Adds / removes a CSS class via toggle-button EventDispatcher.
+ */
+export class ToggleClassBinder extends Binder {
+ static key = "toggle-class-*";
+
+ private toggleButtonEvents?: EventDispatcher;
+ private state = "off";
+
+ private _triggerState() {
+ this.toggleButtonEvents?.trigger(
+ TOGGLE_BUTTON.eventNames.state,
+ this.state,
+ );
+ }
+
+ private triggerState = this._triggerState.bind(this);
+
+ private _onToggle() {
+ this.toggle.bind(this)(this.el);
+ }
+
+ private onToggle = this._onToggle.bind(this);
+
+ private toggle(el: HTMLButtonElement) {
+ if (this.state === "removed") {
+ this.add.bind(this)(el);
+ } else {
+ this.remove.bind(this)(el);
+ }
+ }
+
+ private remove(el: HTMLButtonElement) {
+ const className = this.args[0] as string;
+ el.classList.remove(className);
+ this.state = "removed";
+ el.dispatchEvent(
+ new CustomEvent(TOGGLE_CLASS.elEventNames.removed, {
+ detail: { className },
+ }),
+ );
+ this.triggerState();
+ }
+
+ private add(el: HTMLButtonElement) {
+ const className = this.args[0] as string;
+ el.classList.add(className);
+ this.state = "added";
+ el.dispatchEvent(
+ new CustomEvent(TOGGLE_CLASS.elEventNames.added, {
+ detail: { className },
+ }),
+ );
+ this.triggerState();
+ }
+
+ bind(el: HTMLButtonElement) {
+ const className = this.args[0] as string;
+ this.state = el.classList.contains(className) ? "added" : "removed";
+ }
+
+ unbind() {
+ this.toggleButtonEvents?.off(
+ TOGGLE_BUTTON.eventNames.toggle,
+ this.onToggle,
+ this,
+ );
+ this.toggleButtonEvents?.off(
+ TOGGLE_BUTTON.eventNames.init,
+ this.triggerState,
+ this,
+ );
+ }
+
+ routine(el: HTMLButtonElement, newId: string) {
+ const oldId = this._getValue(el);
+ let toggleButton = this.toggleButtonEvents;
+ if (oldId && toggleButton) {
+ toggleButton.off(TOGGLE_BUTTON.eventNames.toggle, this.onToggle, this);
+ toggleButton.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+
+ if (!this.toggleButtonEvents) {
+ this.toggleButtonEvents = new EventDispatcher(
+ TOGGLE_BUTTON.nsPrefix + newId,
+ );
+ toggleButton = this.toggleButtonEvents as EventDispatcher;
+ toggleButton.on(TOGGLE_BUTTON.eventNames.toggle, this.onToggle, this);
+ toggleButton.on(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+ }
+}
diff --git a/packages/extras/src/constants/index.ts b/packages/extras/src/constants/index.ts
new file mode 100644
index 000000000..ed8345696
--- /dev/null
+++ b/packages/extras/src/constants/index.ts
@@ -0,0 +1,23 @@
+export const TOGGLE_BUTTON = {
+ nsPrefix: "toggle-button-",
+ eventNames: {
+ toggle: "toggle",
+ toggled: "toggled",
+ init: "init",
+ state: "state",
+ },
+};
+
+export const TOGGLE_ATTRIBUTE = {
+ elEventNames: {
+ removed: "attribute-removed",
+ added: "attribute-added",
+ },
+};
+
+export const TOGGLE_CLASS = {
+ elEventNames: {
+ removed: "class-removed",
+ added: "class-added",
+ },
+};
diff --git a/packages/extras/src/index.ts b/packages/extras/src/index.ts
index 6f1d749c5..abb841782 100644
--- a/packages/extras/src/index.ts
+++ b/packages/extras/src/index.ts
@@ -1,4 +1,5 @@
export * from "./binders/index.js";
+export * from "./constants/index.js";
export * from "./components/index.js";
export * from "./helper/index.js";
export * from "./services/index.js";
diff --git a/packages/extras/src/services/dragscroll.service.ts b/packages/extras/src/services/dragscroll.service.ts
index a880d7a75..9d315ac4f 100644
--- a/packages/extras/src/services/dragscroll.service.ts
+++ b/packages/extras/src/services/dragscroll.service.ts
@@ -24,8 +24,14 @@ export class Dragscroll {
this.el = el;
this.options = options;
+ console.debug("[Dragscroll] constructor", {
+ el: el.tagName + "." + el.className.split(" ").slice(0, 3).join("."),
+ touchCapable: this.touchCapable,
+ detectGlobalMove: options.detectGlobalMove,
+ });
+
if (this.touchCapable) {
- // Do noting on touch devices
+ console.debug("[Dragscroll] Skipping — touch device detected");
return this;
}
@@ -84,12 +90,19 @@ export class Dragscroll {
public checkDraggable = throttle(this._checkDraggable.bind(this));
protected onMouseDown(e: MouseEvent) {
+ console.debug("[Dragscroll] mousedown", {
+ clientX: e.clientX,
+ clientY: e.clientY,
+ });
this.pushed = true;
this.lastClientX = e.clientX;
this.lastClientY = e.clientY;
}
protected onMouseUp() {
+ if (this.pushed) {
+ console.debug("[Dragscroll] mouseup");
+ }
this.pushed = false;
}
diff --git a/packages/extras/src/services/index.ts b/packages/extras/src/services/index.ts
index 034cb17f7..b3dfa0e42 100644
--- a/packages/extras/src/services/index.ts
+++ b/packages/extras/src/services/index.ts
@@ -5,3 +5,4 @@ export * from "./fullscreen.service.js";
export * from "./gameloop.service.js";
export * from "./touch-events/scroll-events.service.js";
export * from "./touch-events/touch-events.service.js";
+export * from "./modal.service.js";
diff --git a/packages/extras/src/services/modal.service.spec.ts b/packages/extras/src/services/modal.service.spec.ts
new file mode 100644
index 000000000..7ea14878e
--- /dev/null
+++ b/packages/extras/src/services/modal.service.spec.ts
@@ -0,0 +1,202 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { ModalService } from "./modal.service.js";
+
+describe("ModalService", () => {
+ let dialog: HTMLDialogElement;
+
+ beforeEach(() => {
+ dialog = document.createElement("dialog") as HTMLDialogElement;
+ // jsdom does not implement showModal()/close() on
+ dialog.showModal = vi.fn();
+ dialog.close = vi.fn();
+ document.body.appendChild(dialog);
+ });
+
+ afterEach(() => {
+ dialog.remove();
+ document.body.style.overflow = "";
+ });
+
+ describe("constructor", () => {
+ it("creates service without throwing", () => {
+ expect(() => new ModalService(dialog)).not.toThrow();
+ });
+ });
+
+ describe("isShown", () => {
+ it("defaults to false", () => {
+ const modal = new ModalService(dialog);
+ expect(modal.isShown).toBe(false);
+ });
+ });
+
+ describe("show()", () => {
+ it("calls showModal() on the dialog element", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ expect(dialog.showModal).toHaveBeenCalledTimes(1);
+ });
+
+ it("sets body overflow to hidden", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ expect(document.body.style.overflow).toBe("hidden");
+ });
+
+ it("sets isShown to true", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ expect(modal.isShown).toBe(true);
+ });
+
+ it("dispatches modal.show event", () => {
+ const modal = new ModalService(dialog);
+ const handler = vi.fn();
+ dialog.addEventListener("modal.show", handler);
+ modal.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("dispatches modal.shown event", () => {
+ const modal = new ModalService(dialog);
+ const handler = vi.fn();
+ dialog.addEventListener("modal.shown", handler);
+ modal.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already shown", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ (dialog.showModal as ReturnType).mockClear();
+ modal.show();
+ expect(dialog.showModal).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("hide()", () => {
+ it("calls close() on the dialog element", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ modal.hide();
+ expect(dialog.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("restores body overflow", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ expect(document.body.style.overflow).toBe("hidden");
+ modal.hide();
+ expect(document.body.style.overflow).toBe("");
+ });
+
+ it("sets isShown to false", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ modal.hide();
+ expect(modal.isShown).toBe(false);
+ });
+
+ it("dispatches modal.hide event", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ const handler = vi.fn();
+ dialog.addEventListener("modal.hide", handler);
+ modal.hide();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("dispatches modal.hidden event", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ const handler = vi.fn();
+ dialog.addEventListener("modal.hidden", handler);
+ modal.hide();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already hidden", () => {
+ const modal = new ModalService(dialog);
+ const handler = vi.fn();
+ dialog.addEventListener("modal.hide", handler);
+ modal.hide();
+ expect(handler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("toggle()", () => {
+ it("shows when currently hidden", () => {
+ const modal = new ModalService(dialog);
+ modal.toggle();
+ expect(modal.isShown).toBe(true);
+ });
+
+ it("hides when currently shown", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ modal.toggle();
+ expect(modal.isShown).toBe(false);
+ });
+
+ it("round-trips correctly: hidden -> shown -> hidden", () => {
+ const modal = new ModalService(dialog);
+ modal.toggle();
+ expect(modal.isShown).toBe(true);
+ modal.toggle();
+ expect(modal.isShown).toBe(false);
+ });
+ });
+
+ describe("Escape key", () => {
+ it("triggers hide when Escape is pressed", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ const event = new KeyboardEvent("keydown", {
+ key: "Escape",
+ bubbles: true,
+ });
+ dialog.dispatchEvent(event);
+ expect(modal.isShown).toBe(false);
+ });
+
+ it("does not hide when a different key is pressed", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ const event = new KeyboardEvent("keydown", {
+ key: "Enter",
+ bubbles: true,
+ });
+ dialog.dispatchEvent(event);
+ expect(modal.isShown).toBe(true);
+ });
+ });
+
+ describe("dispose()", () => {
+ it("hides the modal if shown", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ modal.dispose();
+ expect(modal.isShown).toBe(false);
+ expect(dialog.close).toHaveBeenCalled();
+ });
+
+ it("removes event listeners (Escape no longer triggers hide)", () => {
+ const modal = new ModalService(dialog);
+ modal.show();
+ modal.dispose();
+ const handler = vi.fn();
+ dialog.addEventListener("modal.hide", handler);
+ const event = new KeyboardEvent("keydown", {
+ key: "Escape",
+ bubbles: true,
+ });
+ dialog.dispatchEvent(event);
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it("does not throw when called on an already-hidden modal", () => {
+ const modal = new ModalService(dialog);
+ expect(() => modal.dispose()).not.toThrow();
+ });
+ });
+});
diff --git a/packages/extras/src/services/modal.service.ts b/packages/extras/src/services/modal.service.ts
new file mode 100644
index 000000000..63d41664a
--- /dev/null
+++ b/packages/extras/src/services/modal.service.ts
@@ -0,0 +1,88 @@
+/**
+ * Framework-agnostic modal service.
+ *
+ * Uses the native `` element with scroll lock + optional backdrop-click
+ * to close. Dispatches `modal.show`, `modal.shown`, `modal.hide`, `modal.hidden`
+ * events on the element.
+ */
+export class ModalService {
+ protected el: HTMLDialogElement;
+ protected backdrop: HTMLElement | null = null;
+ protected _isShown = false;
+ protected abortController = new AbortController();
+
+ constructor(
+ el: HTMLDialogElement,
+ options: { backdrop?: boolean; keyboard?: boolean } = {},
+ ) {
+ this.el = el;
+ const signal = this.abortController.signal;
+
+ this.el.addEventListener("keydown", this._onKeydown, { signal });
+
+ if (options.backdrop !== false) {
+ this.el.addEventListener("click", this._onBackdropClick, { signal });
+ }
+ }
+
+ get isShown() {
+ return this._isShown;
+ }
+
+ show() {
+ if (this._isShown) return;
+
+ this.el.dispatchEvent(new CustomEvent("modal.show"));
+
+ document.body.style.overflow = "hidden";
+
+ this.el.showModal();
+ this._isShown = true;
+
+ this.el.dispatchEvent(new CustomEvent("modal.shown"));
+ }
+
+ hide() {
+ if (!this._isShown) return;
+
+ this.el.dispatchEvent(new CustomEvent("modal.hide"));
+
+ this.el.close();
+ document.body.style.overflow = "";
+ this._isShown = false;
+
+ this.el.dispatchEvent(new CustomEvent("modal.hidden"));
+ }
+
+ toggle() {
+ if (this._isShown) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ private _onKeydown = (event: KeyboardEvent) => {
+ if (event.key === "Escape" && this._isShown) {
+ event.preventDefault();
+ this.hide();
+ }
+ };
+
+ /**
+ * Close when clicking the dialog backdrop (the ::backdrop pseudo-element).
+ * A click on the dialog itself (not its children) means the backdrop was clicked.
+ */
+ private _onBackdropClick = (event: MouseEvent) => {
+ if (event.target === this.el) {
+ this.hide();
+ }
+ };
+
+ dispose() {
+ this.abortController.abort();
+ if (this._isShown) {
+ this.hide();
+ }
+ }
+}
diff --git a/packages/extras/src/types/index.ts b/packages/extras/src/types/index.ts
index 01d536b4a..0dfefa4ba 100644
--- a/packages/extras/src/types/index.ts
+++ b/packages/extras/src/types/index.ts
@@ -16,3 +16,4 @@ export * from "./touch-settings.js";
export * from "./touch-swipe-data.js";
export * from "./touch-type.js";
export * from "./video-component-scope.js";
+export * from "./notification.js";
diff --git a/packages/extras/src/types/notification.ts b/packages/extras/src/types/notification.ts
new file mode 100644
index 000000000..c253adf74
--- /dev/null
+++ b/packages/extras/src/types/notification.ts
@@ -0,0 +1,34 @@
+export interface Notification {
+ title?: string;
+ message: string;
+ /**
+ * Notification kind: "toast" for toast notifications, "modal" for modal dialogs.
+ */
+ kind: "toast" | "modal";
+ /**
+ * Visual style type for the notification.
+ */
+ type?: "info" | "success" | "warning" | "error";
+ /**
+ * Auto-dismiss timeout in milliseconds. 0 means no auto-dismiss.
+ */
+ timeout?: number;
+}
+
+export interface ModalNotification extends Notification {
+ kind: "modal";
+ /** Show close button */
+ closable?: boolean;
+}
+
+export interface ToastNotification extends Notification {
+ kind: "toast";
+ /** Position of the toast */
+ position?:
+ | "top-right"
+ | "top-left"
+ | "bottom-right"
+ | "bottom-left"
+ | "top-center"
+ | "bottom-center";
+}
diff --git a/packages/extras/tsconfig.json b/packages/extras/tsconfig.json
index 87cbfa3d5..f21735372 100644
--- a/packages/extras/tsconfig.json
+++ b/packages/extras/tsconfig.json
@@ -8,6 +8,7 @@
"include": ["src/**/*"],
"exclude": [
"node_modules",
- "dist/**/*.d.ts"
+ "dist/**/*.d.ts",
+ "src/**/*.spec.ts"
]
}
diff --git a/packages/fuse/package.json b/packages/fuse/package.json
index a464d86df..c7070f367 100644
--- a/packages/fuse/package.json
+++ b/packages/fuse/package.json
@@ -16,7 +16,7 @@
"@ribajs/core": "workspace:^",
"@ribajs/jsx": "workspace:^",
"@ribajs/utils": "workspace:^",
- "fuse.js": "^7.1.0"
+ "fuse.js": "^7.3.0"
},
"main": "src/index.ts",
"module": "src/index.ts",
@@ -24,8 +24,8 @@
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
"@types/eslint": "^9.6.1",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/fuse/src/components/search/search.component.tsx b/packages/fuse/src/components/search/search.component.tsx
index 1e80e4654..bb3282df1 100644
--- a/packages/fuse/src/components/search/search.component.tsx
+++ b/packages/fuse/src/components/search/search.component.tsx
@@ -36,7 +36,7 @@ export class FuseSearchComponent extends Component {
console.warn("fuse is not ready! Did you forget to call initFuse?");
return [];
}
- this.scope.results = this.fuse?.search(this.scope.searchPattern) || [];
+ this.scope.results = this.fuse?.search(this.scope.searchPattern) || [];
console.debug("on search", this.scope.searchPattern, this.scope.results);
}
diff --git a/packages/history/package.json b/packages/history/package.json
index b1ae581b4..4a2a09a6a 100644
--- a/packages/history/package.json
+++ b/packages/history/package.json
@@ -54,7 +54,7 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index bb169840e..55e20a1cb 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -38,12 +38,12 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/jquery/package.json b/packages/jquery/package.json
index 051ccc64a..e4b95c7fa 100644
--- a/packages/jquery/package.json
+++ b/packages/jquery/package.json
@@ -39,16 +39,16 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-config": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/jsx/package.json b/packages/jsx/package.json
index c0605f09d..4b68dc40e 100644
--- a/packages/jsx/package.json
+++ b/packages/jsx/package.json
@@ -21,8 +21,8 @@
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
"@types/eslint": "^9.6.1",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/jsx/src/jsx.ts b/packages/jsx/src/jsx.ts
index ef81a102c..c65ce03e5 100644
--- a/packages/jsx/src/jsx.ts
+++ b/packages/jsx/src/jsx.ts
@@ -76,6 +76,8 @@ export function renderElement(element: JsxElement | null | undefined): string {
for (const [key, val] of Object.entries(props ?? {})) {
if (val == null) continue;
+ // Skip React/esbuild dev-only props that should never be rendered to DOM
+ if (key === "__self" || key === "__source") continue;
if (typeof val == "boolean" && !isCustomElement(tag)) {
if (val) {
diff --git a/packages/leaflet-map/package.json b/packages/leaflet-map/package.json
index 29dbce6f9..5ab9ccc3c 100644
--- a/packages/leaflet-map/package.json
+++ b/packages/leaflet-map/package.json
@@ -39,15 +39,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/lottie/package.json b/packages/lottie/package.json
index f013ee042..b46c7bb8a 100644
--- a/packages/lottie/package.json
+++ b/packages/lottie/package.json
@@ -26,9 +26,9 @@
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
"@types/eslint": "^9.6.1",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
- "sass": "^1.98.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
+ "sass": "^1.99.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/luxon/package.json b/packages/luxon/package.json
index aa25f488e..333795ca7 100644
--- a/packages/luxon/package.json
+++ b/packages/luxon/package.json
@@ -48,15 +48,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/masonry/package.json b/packages/masonry/package.json
index 154b22cf0..17edde729 100644
--- a/packages/masonry/package.json
+++ b/packages/masonry/package.json
@@ -25,15 +25,15 @@
"@types/imagesloaded": "^4.1.7",
"@types/jest": "^30.0.0",
"@types/masonry-layout": "^4.2.8",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
}
}
diff --git a/packages/moment/package.json b/packages/moment/package.json
index 2d4f6cc3c..b5ca2aa89 100644
--- a/packages/moment/package.json
+++ b/packages/moment/package.json
@@ -51,16 +51,16 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
"jest-ts-webcompat-resolver": "^1.0.1",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/monaco-editor/package.json b/packages/monaco-editor/package.json
index c0e4c6e84..06cfc859f 100644
--- a/packages/monaco-editor/package.json
+++ b/packages/monaco-editor/package.json
@@ -49,15 +49,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/octobercms/package.json b/packages/octobercms/package.json
index 1d390d8cb..0bdad40ab 100644
--- a/packages/octobercms/package.json
+++ b/packages/octobercms/package.json
@@ -38,16 +38,16 @@
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
"@types/jquery": "^4.0.0",
- "@types/node": "^24.12.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/podcast/package.json b/packages/podcast/package.json
index 2b183f061..e6097513b 100644
--- a/packages/podcast/package.json
+++ b/packages/podcast/package.json
@@ -29,9 +29,9 @@
},
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
- "@types/node": "^24.12.0",
+ "@types/node": "^24.12.2",
"concurrently": "^9.2.1",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/router/package.json b/packages/router/package.json
index 1812604e2..bbf607f97 100644
--- a/packages/router/package.json
+++ b/packages/router/package.json
@@ -50,15 +50,15 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
"@typescript-eslint/experimental-utils": "^5.62.0",
- "@typescript-eslint/parser": "^8.57.2",
- "@typescript-eslint/typescript-estree": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/parser": "^8.58.1",
+ "@typescript-eslint/typescript-estree": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-visitor-keys": "^5.0.1",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"regexpp": "^3.2.0",
"tsutils": "^3.21.0",
"typescript": "6.0.2"
diff --git a/packages/router/src/binders/dispatch-on-route.binder.spec.ts b/packages/router/src/binders/dispatch-on-route.binder.spec.ts
new file mode 100644
index 000000000..4b3e58c58
--- /dev/null
+++ b/packages/router/src/binders/dispatch-on-route.binder.spec.ts
@@ -0,0 +1,99 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { EventDispatcher } from "@ribajs/events";
+import { DispatchOnRouteBinder } from "./dispatch-on-route.binder.js";
+
+/**
+ * Minimal Binder harness: instantiate directly, call bind/routine/unbind.
+ * We bypass the full view pipeline and drive the lifecycle hooks the same way
+ * the view does.
+ */
+function makeBinder(el: HTMLElement, modifier: "match" | "unmatch") {
+ const binder = new DispatchOnRouteBinder();
+ binder.el = el;
+ binder.args = [modifier];
+ return binder;
+}
+
+describe("DispatchOnRouteBinder", () => {
+ let el: HTMLElement;
+ let originalPathname: string;
+
+ beforeEach(() => {
+ el = document.createElement("div");
+ document.body.appendChild(el);
+ originalPathname = window.location.pathname;
+ });
+
+ afterEach(() => {
+ el.remove();
+ // Reset history state to avoid bleed between tests
+ window.history.replaceState(null, "", originalPathname);
+ vi.restoreAllMocks();
+ });
+
+ it("fires `route-matched` when URL matches on initial routine", () => {
+ window.history.replaceState(null, "", "/docs");
+ const binder = makeBinder(el, "match");
+ const handler = vi.fn();
+ el.addEventListener("route-matched", handler);
+
+ binder.routine(el, "/docs");
+
+ expect(handler).toHaveBeenCalledTimes(1);
+ expect((handler.mock.calls[0][0] as CustomEvent).detail).toEqual({
+ url: "/docs",
+ });
+ });
+
+ it("does not fire `route-matched` when URL does not match", () => {
+ window.history.replaceState(null, "", "/other");
+ const binder = makeBinder(el, "match");
+ const handler = vi.fn();
+ el.addEventListener("route-matched", handler);
+
+ binder.routine(el, "/docs");
+
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it("fires `route-unmatched` in unmatch mode when URL does not match", () => {
+ window.history.replaceState(null, "", "/other");
+ const binder = makeBinder(el, "unmatch");
+ const handler = vi.fn();
+ el.addEventListener("route-unmatched", handler);
+
+ binder.routine(el, "/docs");
+
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("re-evaluates on `newPageReady` dispatcher event", () => {
+ window.history.replaceState(null, "", "/other");
+ const binder = makeBinder(el, "match");
+ const handler = vi.fn();
+ el.addEventListener("route-matched", handler);
+
+ binder.routine(el, "/docs");
+ expect(handler).not.toHaveBeenCalled();
+
+ window.history.replaceState(null, "", "/docs");
+ EventDispatcher.getInstance("main").trigger("newPageReady");
+
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("removes dispatcher listener on unbind", () => {
+ window.history.replaceState(null, "", "/other");
+ const binder = makeBinder(el, "match");
+ const handler = vi.fn();
+ el.addEventListener("route-matched", handler);
+
+ binder.routine(el, "/docs");
+ binder.unbind();
+
+ window.history.replaceState(null, "", "/docs");
+ EventDispatcher.getInstance("main").trigger("newPageReady");
+
+ expect(handler).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/router/src/binders/dispatch-on-route.binder.ts b/packages/router/src/binders/dispatch-on-route.binder.ts
new file mode 100644
index 000000000..ea1013db6
--- /dev/null
+++ b/packages/router/src/binders/dispatch-on-route.binder.ts
@@ -0,0 +1,54 @@
+import { Binder } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { onRoute } from "@ribajs/utils/src/url.js";
+
+/**
+ * Dispatches a CustomEvent on the bound element when the current URL matches
+ * (or explicitly does not match) the bound route value.
+ *
+ * Usage:
+ * rv-dispatch-on-route-match="'/docs'"
+ * → fires `route-matched` when the URL matches `/docs`.
+ * rv-dispatch-on-route-unmatch="'/docs'"
+ * → fires `route-unmatched` when the URL does not match.
+ *
+ * Combine with standard event binders to react without coupling to any service:
+ *
+ */
+export class DispatchOnRouteBinder extends Binder {
+ static key = "dispatch-on-route-*";
+
+ private dispatcher = EventDispatcher.getInstance("main");
+ private url?: string;
+ private mode: "match" | "unmatch" = "match";
+
+ private _onNewPage() {
+ if (!this.url) return;
+ const matches = onRoute(this.url);
+ const shouldFire = this.mode === "match" ? matches : !matches;
+ if (shouldFire) {
+ const eventName =
+ this.mode === "match" ? "route-matched" : "route-unmatched";
+ this.el.dispatchEvent(
+ new CustomEvent(eventName, { detail: { url: this.url } }),
+ );
+ }
+ }
+
+ private onNewPage = this._onNewPage.bind(this);
+
+ unbind() {
+ this.dispatcher.off("newPageReady", this.onNewPage);
+ }
+
+ routine(el: HTMLElement, url: string) {
+ if (this.args === null) {
+ throw new Error("args is null");
+ }
+ this.url = url;
+ this.mode = (this.args[0] as "match" | "unmatch") ?? "match";
+ this.dispatcher.off("newPageReady", this.onNewPage);
+ this.dispatcher.on("newPageReady", this.onNewPage);
+ this.onNewPage();
+ }
+}
diff --git a/packages/router/src/binders/index.ts b/packages/router/src/binders/index.ts
index cd0ddc8ed..c39d7270a 100644
--- a/packages/router/src/binders/index.ts
+++ b/packages/router/src/binders/index.ts
@@ -4,3 +4,4 @@ export * from "./route-class-star.binder.js";
export * from "./route-preload.binder.js";
export * from "./parent-route-class-star.binder.js";
export * from "./route-back-on-star.binder.js";
+export * from "./dispatch-on-route.binder.js";
diff --git a/packages/shopify-easdk/package.json b/packages/shopify-easdk/package.json
index c5b7352de..8412bcbd2 100644
--- a/packages/shopify-easdk/package.json
+++ b/packages/shopify-easdk/package.json
@@ -38,12 +38,12 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/shopify-nest/package.json b/packages/shopify-nest/package.json
index 06faa4e14..edc504542 100644
--- a/packages/shopify-nest/package.json
+++ b/packages/shopify-nest/package.json
@@ -32,8 +32,8 @@
"source": "src/index.ts",
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/packages/shopify-tda/package.json b/packages/shopify-tda/package.json
index 525eb64f5..3e663dedf 100644
--- a/packages/shopify-tda/package.json
+++ b/packages/shopify-tda/package.json
@@ -38,13 +38,13 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@types/node": "^24.12.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"typescript": "6.0.2"
},
"dependencies": {
diff --git a/packages/shopify-tda/src/components/instagram/instagram.component.html b/packages/shopify-tda/src/components/instagram/instagram.component.html
index 9110124bc..88ee6dcdf 100644
--- a/packages/shopify-tda/src/components/instagram/instagram.component.html
+++ b/packages/shopify-tda/src/components/instagram/instagram.component.html
@@ -1,13 +1,13 @@
{% comment %} IMAGE {% endcomment %}
-
+
{% comment %} CAROUSEL_ALBUM {% endcomment %}
-
-
+
+
{% comment %} VIDEO {% endcomment %}
-
+
Your browser does not support the video tag.
diff --git a/packages/shopify/package.json b/packages/shopify/package.json
index 4df83e0d0..7e45daac5 100644
--- a/packages/shopify/package.json
+++ b/packages/shopify/package.json
@@ -52,20 +52,20 @@
"@types/debug": "^4.1.13",
"@types/jest": "^30.0.0",
"@types/lodash": "^4.17.24",
- "@types/node": "^24.12.0",
+ "@types/node": "^24.12.2",
"@types/prettier": "^3.0.0",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
"debug": "^4.4.3",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"graceful-fs": "^4.2.11",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.2",
"terser": "^5.46.1",
- "ts-jest": "^29.4.6",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
},
"optionalDependencies": {
diff --git a/packages/ssr/package.json b/packages/ssr/package.json
index 2d42c3240..0045f1a9a 100644
--- a/packages/ssr/package.json
+++ b/packages/ssr/package.json
@@ -26,15 +26,15 @@
"@types/imagesloaded": "^4.1.7",
"@types/jest": "^30.0.0",
"@types/masonry-layout": "^4.2.8",
- "@typescript-eslint/eslint-plugin": "^8.57.2",
- "@typescript-eslint/parser": "^8.57.2",
- "eslint": "^10.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"jest": "^30.3.0",
"jest-extended": "^7.0.0",
- "prettier": "^3.8.1",
- "ts-jest": "^29.4.6",
+ "prettier": "^3.8.2",
+ "ts-jest": "^29.4.9",
"typescript": "6.0.2"
}
}
diff --git a/packages/strapi/package.json b/packages/strapi/package.json
index 8241f8fd9..ed52f9611 100644
--- a/packages/strapi/package.json
+++ b/packages/strapi/package.json
@@ -17,6 +17,6 @@
"source": "src/index.ts",
"devDependencies": {
"@ribajs/tsconfig": "workspace:^",
- "@types/node": "^24.12.0"
+ "@types/node": "^24.12.2"
}
}
diff --git a/packages/tw/package.json b/packages/tw/package.json
new file mode 100644
index 000000000..5bf976449
--- /dev/null
+++ b/packages/tw/package.json
@@ -0,0 +1,74 @@
+{
+ "name": "@ribajs/tw",
+ "description": "Tailwind CSS module for Riba.js",
+ "version": "2.0.0-rc.23",
+ "type": "module",
+ "engines": {
+ "node": ">=24.0.0"
+ },
+ "author": "Pascal Garber ",
+ "contributors": [],
+ "url": "https://github.com/ribajs/riba/tree/master/packages/tw",
+ "homepage": "https://ribajs.com/",
+ "main": "src/index.ts",
+ "module": "src/index.ts",
+ "source": "src/index.ts",
+ "license": "MIT",
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/ribajs/riba/blob/master/LICENSE"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ribajs/riba.git"
+ },
+ "keywords": [
+ "Tailwind",
+ "Tailwind CSS",
+ "Art+Code Studio",
+ "Riba",
+ "Rivets",
+ "tinybind",
+ "SPA",
+ "TypeScript",
+ "Browser"
+ ],
+ "scripts": {
+ "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx,.cts,.mts --fix && tsc --noEmit",
+ "watch": "yarn run build:module --watch",
+ "build:module": "tsc",
+ "clean": "rm -rf ./dist ./lib",
+ "check": "tsc --noEmit"
+ },
+ "files": [
+ "/src",
+ "/dist"
+ ],
+ "devDependencies": {
+ "@ribajs/eslint-config": "workspace:^",
+ "@ribajs/tsconfig": "workspace:^",
+ "@ribajs/types": "workspace:^",
+ "@types/jest": "^30.0.0",
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
+ "@typescript-eslint/parser": "^8.58.1",
+ "@yarnpkg/pnpify": "^4.1.6",
+ "eslint": "^10.2.0",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.5",
+ "prettier": "^3.8.2",
+ "typescript": "6.0.2"
+ },
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6",
+ "@ribajs/core": "workspace:^",
+ "@ribajs/events": "workspace:^",
+ "@ribajs/extras": "workspace:^",
+ "@ribajs/jsx": "workspace:^",
+ "@ribajs/utils": "workspace:^"
+ },
+ "bugs": {
+ "url": "https://github.com/ribajs/riba/issues"
+ }
+}
diff --git a/packages/tw/src/binders/index.ts b/packages/tw/src/binders/index.ts
new file mode 100644
index 000000000..5aedb3fd5
--- /dev/null
+++ b/packages/tw/src/binders/index.ts
@@ -0,0 +1,10 @@
+// Abstract binder not exported — cannot be used as a concrete binder
+// export { TwAbstractBreakpointBinder } from "./tw-abstract-breakpoint.binder.js";
+export { TwAttributeBreakpointBinder } from "./tw-attribute-breakpoint.binder.js";
+export { TwComponentAttributeBreakpointBinder } from "./tw-co-attribute-breakpoint.binder.js";
+export { DropdownBinder } from "./tw-dropdown.binder.js";
+export { PopoverBinder } from "./tw-popover.binder.js";
+export { TooltipBinder } from "./tw-tooltip.binder.js";
+export { ToggleCollapseOnEventBinder } from "./tw-toggle-collapse-on-event.binder.js";
+export { ToggleClassBinder } from "./tw-toggle-class.binder.js";
+export { ToggleAttributeBinder } from "./tw-toggle-attribute.binder.js";
diff --git a/packages/tw/src/binders/tw-abstract-breakpoint.binder.ts b/packages/tw/src/binders/tw-abstract-breakpoint.binder.ts
new file mode 100644
index 000000000..cf9bde5ba
--- /dev/null
+++ b/packages/tw/src/binders/tw-abstract-breakpoint.binder.ts
@@ -0,0 +1,146 @@
+import { BasicComponent, Binder, View } from "@ribajs/core";
+import { TwService } from "../services/tw.service.js";
+import {
+ parseJsonString,
+ jsonStringify,
+ camelCase,
+} from "@ribajs/utils/src/type.js";
+
+export abstract class TwAbstractBreakpointBinder<
+ E extends HTMLElement = HTMLElement,
+> extends Binder {
+ protected abstract defaultAttributeBinder: Binder<
+ any,
+ HTMLElement | BasicComponent
+ >;
+ protected tw: TwService;
+ protected breakpoint: string;
+ protected attributeName: string;
+ protected val?: any;
+
+ constructor(
+ view: View,
+ el: E,
+ type: string | null,
+ name: string,
+ keypath: string | undefined,
+ formatters: string[] | null,
+ identifier: string | null,
+ ) {
+ super(view, el, type, name, keypath, formatters, identifier);
+ this.tw = TwService.getSingleton();
+ if (this.args.length !== 2) {
+ throw new Error(
+ "The TwAbstractBreakpointBinder was not initialized correctly!",
+ );
+ }
+ const breakpoint = this.args[0].toString();
+ const attributeName = this.args[1].toString();
+ if (!this.tw.breakpointNames.includes(breakpoint)) {
+ throw new Error(
+ `Unknown breakpoint "${breakpoint}"! You can define breakpoints at the initialization of the twModule.`,
+ );
+ }
+ this.breakpoint = breakpoint;
+ this.attributeName = attributeName;
+ }
+
+ protected onBreakpointChanges() {
+ this.setAttributeOnMatch();
+ }
+
+ protected setAttributeOnMatch() {
+ if (!this.tw.activeBreakpoint) return;
+ if (this.isBreakpointMatch(this.breakpoint)) {
+ return this.defaultAttributeBinder.routine(this.el, this.val);
+ }
+ if (this.breakpointUnhandled(this.tw.activeBreakpoint.name)) {
+ this.defaultAttributeBinder.routine(this.el, undefined);
+ }
+ }
+
+ protected isBreakpointMatch(breakpoint = this.breakpoint) {
+ if (!this.tw.activeBreakpoint) {
+ return false;
+ }
+ if (this.tw.activeBreakpoint.name === breakpoint) {
+ return true;
+ }
+ const myBreakpoints = this.myBreakpoints(breakpoint);
+ if (myBreakpoints.includes(this.tw.activeBreakpoint.name)) {
+ return true;
+ }
+ return false;
+ }
+
+ protected myBreakpoints(breakpoint = this.breakpoint) {
+ const myBreakpoints: string[] = [breakpoint];
+ let nextBreakpoint: string | null = breakpoint;
+ while (nextBreakpoint) {
+ nextBreakpoint = this.tw.getNextBreakpointByName(nextBreakpoint);
+ if (!nextBreakpoint || this.breakpointHandledByAnother(nextBreakpoint)) {
+ break;
+ }
+ myBreakpoints.push(nextBreakpoint);
+ }
+ return myBreakpoints;
+ }
+
+ protected breakpointHandledByAnother(name: string) {
+ const handledBreakpoints = this.getHandledBreakpoints();
+ return handledBreakpoints.includes(name);
+ }
+
+ protected breakpointUnhandled(breakpoint: string) {
+ let prevBreakpoint: string | null = breakpoint;
+ let unhandled = true;
+ while (prevBreakpoint && unhandled) {
+ if (this.breakpointHandledByAnother(prevBreakpoint)) {
+ unhandled = false;
+ break;
+ }
+ prevBreakpoint = this.tw.getPrevBreakpointByName(prevBreakpoint);
+ if (!prevBreakpoint) {
+ break;
+ }
+ }
+ return unhandled;
+ }
+
+ protected addToHandledBreakpoints() {
+ const handledBreakpoints = this.getHandledBreakpoints();
+ handledBreakpoints.push(this.breakpoint);
+ this.el.dataset[camelCase(this.attributeName)] =
+ jsonStringify(handledBreakpoints);
+ }
+
+ protected getHandledBreakpoints() {
+ const handledBreakpoints: string[] = parseJsonString(
+ this.el.dataset[camelCase(this.attributeName)] || "[]",
+ );
+ if (!Array.isArray(handledBreakpoints)) {
+ throw new Error("breakpoints dataset has unsupported values!");
+ }
+ return handledBreakpoints;
+ }
+
+ bind(el: HTMLElement) {
+ this.tw.on("breakpoint:changed", this.onBreakpointChanges, this);
+ if (typeof this.defaultAttributeBinder.bind === "function") {
+ this.defaultAttributeBinder.bind(el);
+ }
+ }
+
+ routine(el: HTMLElement, newValue: any) {
+ this.addToHandledBreakpoints();
+ this.val = newValue;
+ this.setAttributeOnMatch();
+ }
+
+ unbind(el: HTMLElement) {
+ this.tw?.off("breakpoint:changed", this.onBreakpointChanges, this);
+ if (typeof this.defaultAttributeBinder.unbind === "function") {
+ this.defaultAttributeBinder.unbind(el);
+ }
+ }
+}
diff --git a/packages/tw/src/binders/tw-abstract-toggle.binder.ts b/packages/tw/src/binders/tw-abstract-toggle.binder.ts
new file mode 100644
index 000000000..405644174
--- /dev/null
+++ b/packages/tw/src/binders/tw-abstract-toggle.binder.ts
@@ -0,0 +1,118 @@
+import { Binder } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { TOGGLE_BUTTON } from "../constants/index.js";
+
+export interface ToggleEventNames {
+ readonly added: string;
+ readonly removed: string;
+}
+
+type ToggleState = "added" | "removed" | "off";
+
+/**
+ * Shared base for `tw-toggle-class-*` and `tw-toggle-attribute-*`.
+ *
+ * Subclasses provide the DOM-mutation primitives; this base handles the state
+ * machine + the wiring to the `tw-toggle-button` EventDispatcher namespace.
+ */
+export abstract class AbstractToggleBinder<
+ E extends HTMLElement = HTMLElement,
+> extends Binder {
+ protected toggleButtonEvents?: EventDispatcher;
+ protected state: ToggleState = "off";
+
+ /** The event names dispatched on the bound element for this toggle kind. */
+ protected abstract readonly eventNames: ToggleEventNames;
+
+ /** Apply the "added" side effect on the element. */
+ protected abstract applyAdd(el: E, value: string): void;
+
+ /** Apply the "removed" side effect on the element. */
+ protected abstract applyRemove(el: E, value: string): void;
+
+ /** Detect whether the element currently has the toggle applied. */
+ protected abstract detectState(el: E, value: string): boolean;
+
+ private _triggerState() {
+ this.toggleButtonEvents?.trigger(
+ TOGGLE_BUTTON.eventNames.state,
+ this.state,
+ );
+ }
+
+ protected triggerState = this._triggerState.bind(this);
+
+ private _onToggle() {
+ this.toggle(this.el);
+ }
+
+ protected onToggle = this._onToggle.bind(this);
+
+ protected toggle(el: E) {
+ if (this.state === "removed") {
+ this.add(el);
+ } else {
+ this.remove(el);
+ }
+ }
+
+ protected remove(el: E) {
+ const value = this.args[0] as string;
+ this.applyRemove(el, value);
+ this.state = "removed";
+ el.dispatchEvent(
+ new CustomEvent(this.eventNames.removed, {
+ detail: { value },
+ }),
+ );
+ this.triggerState();
+ }
+
+ protected add(el: E) {
+ const value = this.args[0] as string;
+ this.applyAdd(el, value);
+ this.state = "added";
+ el.dispatchEvent(
+ new CustomEvent(this.eventNames.added, {
+ detail: { value },
+ }),
+ );
+ this.triggerState();
+ }
+
+ bind(el: E) {
+ const value = this.args[0] as string;
+ this.state = this.detectState(el, value) ? "added" : "removed";
+ }
+
+ unbind() {
+ this.toggleButtonEvents?.off(
+ TOGGLE_BUTTON.eventNames.toggle,
+ this.onToggle,
+ this,
+ );
+ this.toggleButtonEvents?.off(
+ TOGGLE_BUTTON.eventNames.init,
+ this.triggerState,
+ this,
+ );
+ }
+
+ routine(el: E, newId: string) {
+ const oldId = this._getValue(el);
+ let toggleButton = this.toggleButtonEvents;
+ if (oldId && toggleButton) {
+ toggleButton.off(TOGGLE_BUTTON.eventNames.toggle, this.onToggle, this);
+ toggleButton.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+
+ if (!this.toggleButtonEvents) {
+ this.toggleButtonEvents = new EventDispatcher(
+ TOGGLE_BUTTON.nsPrefix + newId,
+ );
+ toggleButton = this.toggleButtonEvents;
+ toggleButton.on(TOGGLE_BUTTON.eventNames.toggle, this.onToggle, this);
+ toggleButton.on(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+ }
+}
diff --git a/packages/tw/src/binders/tw-attribute-breakpoint.binder.ts b/packages/tw/src/binders/tw-attribute-breakpoint.binder.ts
new file mode 100644
index 000000000..4852df958
--- /dev/null
+++ b/packages/tw/src/binders/tw-attribute-breakpoint.binder.ts
@@ -0,0 +1,43 @@
+import { AttributeBinder, View } from "@ribajs/core";
+import { TwAbstractBreakpointBinder } from "./tw-abstract-breakpoint.binder.js";
+
+/**
+ * Responsive attribute breakpoint binder.
+ *
+ * Sets HTML attributes only at certain breakpoints (viewport widths).
+ *
+ * @example
+ * ```html
+ *
+ * ```
+ */
+export class TwAttributeBreakpointBinder extends TwAbstractBreakpointBinder {
+ static key = "tw-attr-*-*";
+
+ defaultAttributeBinder: AttributeBinder;
+
+ constructor(
+ view: View,
+ el: HTMLElement,
+ type: string | null,
+ name: string,
+ keypath: string | undefined,
+ formatters: string[] | null,
+ identifier: string | null,
+ ) {
+ super(view, el, type, name, keypath, formatters, identifier);
+ this.defaultAttributeBinder = new AttributeBinder(
+ view,
+ el,
+ this.attributeName,
+ name,
+ keypath,
+ formatters,
+ "*",
+ );
+ }
+}
diff --git a/packages/tw/src/binders/tw-co-attribute-breakpoint.binder.ts b/packages/tw/src/binders/tw-co-attribute-breakpoint.binder.ts
new file mode 100644
index 000000000..6134d6587
--- /dev/null
+++ b/packages/tw/src/binders/tw-co-attribute-breakpoint.binder.ts
@@ -0,0 +1,42 @@
+import { BasicComponent, ComponentAttributeBinder, View } from "@ribajs/core";
+import { TwAbstractBreakpointBinder } from "./tw-abstract-breakpoint.binder.js";
+
+/**
+ * Responsive component attribute breakpoint binder.
+ *
+ * Passes attributes to Riba Components that should only be set at certain breakpoints.
+ *
+ * @example
+ * ```html
+ *
+ * ```
+ */
+export class TwComponentAttributeBreakpointBinder extends TwAbstractBreakpointBinder {
+ static key = "tw-co-*-*";
+
+ defaultAttributeBinder: ComponentAttributeBinder;
+
+ constructor(
+ view: View,
+ el: BasicComponent,
+ type: string | null,
+ name: string,
+ keypath: string | undefined,
+ formatters: string[] | null,
+ identifier: string | null,
+ ) {
+ super(view, el, type, name, keypath, formatters, identifier);
+ this.defaultAttributeBinder = new ComponentAttributeBinder(
+ view,
+ el,
+ `co-${this.attributeName}`,
+ name,
+ keypath,
+ formatters,
+ ComponentAttributeBinder.key,
+ );
+ }
+}
diff --git a/packages/tw/src/binders/tw-dropdown.binder.ts b/packages/tw/src/binders/tw-dropdown.binder.ts
new file mode 100644
index 000000000..55ef2f9d9
--- /dev/null
+++ b/packages/tw/src/binders/tw-dropdown.binder.ts
@@ -0,0 +1,46 @@
+import { Binder } from "@ribajs/core";
+import { DropdownService } from "../services/dropdown.service.js";
+import type { Placement } from "@floating-ui/dom";
+
+/**
+ * Initializes a dropdown on an element using Floating UI for positioning.
+ */
+export class DropdownBinder extends Binder<
+ Partial<{ placement: Placement }>,
+ HTMLElement
+> {
+ static key = "tw-dropdown";
+
+ private toggler?: HTMLElement;
+ private menu?: HTMLElement;
+ private dropdownService?: DropdownService;
+
+ bind(el: HTMLElement) {
+ this.toggler = ((el.classList.contains("dropdown-toggle")
+ ? el
+ : el.querySelector(".dropdown-toggle")) || el) as HTMLElement;
+
+ this.menu = (el.querySelector("[role='menu']") ||
+ el.querySelector(".dropdown-menu")) as HTMLElement | undefined;
+ }
+
+ routine(el: HTMLElement, options: Partial<{ placement: Placement }> = {}) {
+ if (!this.toggler) {
+ throw new Error("No dropdown toggle element found!");
+ }
+ if (!this.menu) {
+ throw new Error(
+ 'No dropdown menu element found! Expected [role="menu"] or .dropdown-menu',
+ );
+ }
+
+ if (this.dropdownService) {
+ this.dropdownService.dispose();
+ }
+
+ this.dropdownService = new DropdownService(this.toggler, this.menu, {
+ placement: options.placement,
+ });
+ this.dropdownService.hide();
+ }
+}
diff --git a/packages/tw/src/binders/tw-popover.binder.ts b/packages/tw/src/binders/tw-popover.binder.ts
new file mode 100644
index 000000000..0326a9339
--- /dev/null
+++ b/packages/tw/src/binders/tw-popover.binder.ts
@@ -0,0 +1,110 @@
+import { Binder } from "@ribajs/core";
+import { PopoverService } from "../services/popover.service.js";
+import type { Placement } from "@floating-ui/dom";
+
+export interface TwPopoverOptions {
+ placement?: Placement;
+ arrow?: boolean;
+ popoverSelector?: string;
+}
+
+/**
+ * Initializes a popover on an element using Floating UI for positioning.
+ *
+ * Methods "show", "hide", "toggle", "dispose" can be called by dispatching
+ * a `trigger-${methodName}` event on the bound element.
+ */
+export class PopoverBinder extends Binder<
+ string | TwPopoverOptions,
+ HTMLElement
+> {
+ static key = "tw-popover";
+
+ private popoverService?: PopoverService;
+ private listeners: { [key: string]: EventListener } = {};
+
+ routine(el: HTMLElement, optionsOrSelector: string | TwPopoverOptions) {
+ let options: TwPopoverOptions = {};
+
+ if (typeof optionsOrSelector === "string") {
+ options.popoverSelector = optionsOrSelector;
+ } else if (typeof optionsOrSelector === "object") {
+ options = { ...optionsOrSelector };
+ }
+
+ const popoverSelector = options.popoverSelector || "[data-tw-popover]";
+ const popoverEl = el.querySelector(popoverSelector);
+
+ if (!popoverEl) {
+ console.warn(
+ `[tw-popover] No popover element found with selector "${popoverSelector}"`,
+ );
+ return;
+ }
+
+ // Destroy previous popover if it already exists
+ if (this.popoverService) {
+ this.popoverService.dispose();
+ }
+
+ const popoverService = new PopoverService(el, popoverEl, {
+ placement: options.placement,
+ arrow: options.arrow,
+ });
+
+ this.popoverService = popoverService;
+
+ /*
+ * Methods "show", "hide", etc. can be called by dispatching an
+ * event `trigger-${methodName}` on the bound element.
+ */
+ const methodNames: (keyof PopoverService)[] = [
+ "show",
+ "hide",
+ "toggle",
+ "dispose",
+ ];
+
+ // Remove listeners of previous popover if there already was one
+ if (this.listeners) {
+ for (const [trigger, listener] of Object.entries(this.listeners)) {
+ this.el.removeEventListener(trigger, listener as EventListener);
+ }
+ }
+
+ this.listeners = Object.create(null);
+ for (const methodName of methodNames) {
+ if (
+ popoverService[methodName] &&
+ typeof popoverService[methodName] === "function"
+ ) {
+ const trigger = `trigger-${String(methodName)}`;
+ const listener = (popoverService[methodName] as any).bind(
+ popoverService,
+ );
+ this.el.addEventListener(trigger, listener);
+ this.listeners[trigger] = listener;
+ }
+ }
+ }
+
+ bind(el: HTMLElement) {
+ // Inform ancestors that this popover was bound
+ el.dispatchEvent(
+ new CustomEvent("bound.tw.popover", { bubbles: true, cancelable: true }),
+ );
+ }
+
+ unbind() {
+ // Destroy popover if it already exists
+ if (this.popoverService) {
+ this.popoverService.dispose();
+ }
+ // Remove listeners if there are any
+ if (this.listeners) {
+ for (const [trigger, listener] of Object.entries(this.listeners)) {
+ this.el.removeEventListener(trigger, listener as EventListener);
+ }
+ }
+ }
+}
diff --git a/packages/tw/src/binders/tw-toggle-attribute.binder.ts b/packages/tw/src/binders/tw-toggle-attribute.binder.ts
new file mode 100644
index 000000000..210b8ae18
--- /dev/null
+++ b/packages/tw/src/binders/tw-toggle-attribute.binder.ts
@@ -0,0 +1,32 @@
+import { TOGGLE_ATTRIBUTE } from "../constants/index.js";
+import {
+ AbstractToggleBinder,
+ ToggleEventNames,
+} from "./tw-abstract-toggle.binder.js";
+
+/**
+ * Adds / removes the attribute on click on the tw-toggle-button with the same id.
+ * E.g. with this binder you can toggle a hidden attribute to show / hide the element.
+ *
+ * Events:
+ * - `attribute-removed`
+ * - `attribute-added`
+ */
+export class ToggleAttributeBinder extends AbstractToggleBinder {
+ static key = "tw-toggle-attribute-*";
+
+ protected readonly eventNames: ToggleEventNames =
+ TOGGLE_ATTRIBUTE.elEventNames;
+
+ protected applyAdd(el: HTMLElement, attributeName: string) {
+ el.setAttribute(attributeName, attributeName);
+ }
+
+ protected applyRemove(el: HTMLElement, attributeName: string) {
+ el.removeAttribute(attributeName);
+ }
+
+ protected detectState(el: HTMLElement, attributeName: string): boolean {
+ return el.hasAttribute(attributeName);
+ }
+}
diff --git a/packages/tw/src/binders/tw-toggle-class.binder.ts b/packages/tw/src/binders/tw-toggle-class.binder.ts
new file mode 100644
index 000000000..b6754e3f9
--- /dev/null
+++ b/packages/tw/src/binders/tw-toggle-class.binder.ts
@@ -0,0 +1,30 @@
+import { TOGGLE_CLASS } from "../constants/index.js";
+import {
+ AbstractToggleBinder,
+ ToggleEventNames,
+} from "./tw-abstract-toggle.binder.js";
+
+/**
+ * Adds / removes the class on click on the tw-toggle-button with the same id.
+ *
+ * Events:
+ * - `class-removed`
+ * - `class-added`
+ */
+export class ToggleClassBinder extends AbstractToggleBinder {
+ static key = "tw-toggle-class-*";
+
+ protected readonly eventNames: ToggleEventNames = TOGGLE_CLASS.elEventNames;
+
+ protected applyAdd(el: HTMLButtonElement, className: string) {
+ el.classList.add(className);
+ }
+
+ protected applyRemove(el: HTMLButtonElement, className: string) {
+ el.classList.remove(className);
+ }
+
+ protected detectState(el: HTMLButtonElement, className: string): boolean {
+ return el.classList.contains(className);
+ }
+}
diff --git a/packages/tw/src/binders/tw-toggle-collapse-on-event.binder.ts b/packages/tw/src/binders/tw-toggle-collapse-on-event.binder.ts
new file mode 100644
index 000000000..8ef1f8a43
--- /dev/null
+++ b/packages/tw/src/binders/tw-toggle-collapse-on-event.binder.ts
@@ -0,0 +1,60 @@
+import { Binder } from "@ribajs/core";
+import { CollapseService } from "../services/collapse.service.js";
+
+/**
+ * Toggles collapse on target elements when a specified event fires.
+ *
+ * Usage: `rv-tw-toggle-collapse-on-click="'.my-collapsible'"`
+ */
+export class ToggleCollapseOnEventBinder extends Binder {
+ static key = "tw-toggle-collapse-on-*";
+
+ private targets = new Map();
+
+ private _onEvent(event: Event) {
+ event.preventDefault();
+ for (const collapseService of this.targets.values()) {
+ collapseService.toggle();
+ }
+ }
+
+ private onEvent = this._onEvent.bind(this);
+
+ bind(el: HTMLElement) {
+ if (this.args === null) {
+ throw new Error("args is null");
+ }
+ const eventName = this.args[0] as string;
+ el.addEventListener(eventName, this.onEvent);
+ }
+
+ unbind() {
+ const eventName = this.args[0] as string;
+ this.el.removeEventListener(eventName, this.onEvent);
+ }
+
+ routine(el: HTMLElement, targetSelector: string) {
+ const newTargets = Array.from(
+ document.querySelectorAll(targetSelector),
+ );
+
+ if (newTargets.length <= 0) {
+ console.warn(
+ `[toggleCollapseOnEventBinder] No element with selector "${targetSelector}" found.`,
+ );
+ }
+
+ for (const target of this.targets.keys()) {
+ if (!newTargets.find((x) => x === target)) {
+ this.targets.get(target)?.dispose();
+ this.targets.delete(target);
+ }
+ }
+
+ for (const target of newTargets) {
+ if (!this.targets.has(target)) {
+ this.targets.set(target, new CollapseService(target, { show: false }));
+ }
+ }
+ }
+}
diff --git a/packages/tw/src/binders/tw-tooltip.binder.ts b/packages/tw/src/binders/tw-tooltip.binder.ts
new file mode 100644
index 000000000..0edb18864
--- /dev/null
+++ b/packages/tw/src/binders/tw-tooltip.binder.ts
@@ -0,0 +1,61 @@
+import { Binder } from "@ribajs/core";
+import { TooltipService } from "../services/tooltip.service.js";
+import type { Placement } from "@floating-ui/dom";
+
+export interface TwTooltipOptions {
+ content?: string;
+ placement?: Placement;
+ showDelay?: number;
+ hideDelay?: number;
+}
+
+/**
+ * Initializes a tooltip on an element using Floating UI for positioning.
+ *
+ * Bind a string to set the tooltip content, or an options object for
+ * more control (placement, delays, etc.).
+ */
+export class TooltipBinder extends Binder<
+ string | TwTooltipOptions,
+ HTMLElement
+> {
+ static key = "tw-tooltip";
+
+ private tooltipService?: TooltipService;
+
+ bind(el: HTMLElement) {
+ // Service will be created in routine when content is available
+ }
+
+ routine(el: HTMLElement, optionsOrContent: string | TwTooltipOptions) {
+ let options: TwTooltipOptions = {};
+
+ if (typeof optionsOrContent === "string") {
+ options.content = optionsOrContent;
+ } else if (typeof optionsOrContent === "object") {
+ options = { ...optionsOrContent };
+ }
+
+ if (this.tooltipService) {
+ // Update content if tooltip already exists
+ if (options.content !== undefined) {
+ this.tooltipService.content = options.content;
+ }
+ return;
+ }
+
+ this.tooltipService = new TooltipService(el, {
+ content: options.content,
+ placement: options.placement || "top",
+ showDelay: options.showDelay,
+ hideDelay: options.hideDelay,
+ });
+ }
+
+ unbind() {
+ if (this.tooltipService) {
+ this.tooltipService.dispose();
+ this.tooltipService = undefined;
+ }
+ }
+}
diff --git a/packages/tw/src/components/index.ts b/packages/tw/src/components/index.ts
new file mode 100644
index 000000000..2c61f31d6
--- /dev/null
+++ b/packages/tw/src/components/index.ts
@@ -0,0 +1,41 @@
+// Phase 2: Core components
+export { TwIconComponent } from "./tw-icon/tw-icon.component.js";
+export { TwButtonComponent } from "./tw-button/tw-button.component.js";
+export { TwToggleButtonComponent } from "./tw-toggle-button/tw-toggle-button.component.js";
+export { TwCollapseComponent } from "./tw-collapse/tw-collapse.component.js";
+export { TwAccordionComponent } from "./tw-accordion/tw-accordion.component.js";
+export { TwToastItemComponent } from "./tw-toast-item/tw-toast-item.component.js";
+export { TwModalItemComponent } from "./tw-modal-item/tw-modal-item.component.js";
+export { TwNotificationContainerComponent } from "./tw-notification-container/tw-notification-container.component.js";
+
+// Phase 3: Interactive components
+export { TwDropdownComponent } from "./tw-dropdown/tw-dropdown.component.js";
+export { TwNavbarComponent } from "./tw-navbar/tw-navbar.component.js";
+export { TwTabsComponent } from "./tw-tabs/tw-tabs.component.js";
+export { TwSidebarComponent } from "./tw-sidebar/tw-sidebar.component.js";
+export { TwSlideshowComponent } from "./tw-slideshow/tw-slideshow.component.js";
+export { TwCarouselComponent } from "./tw-carousel/tw-carousel.component.js";
+export { TwSlideVideoComponent } from "./tw-slide-video/tw-slide-video.component.js";
+export { TwSliderComponent } from "./tw-slider/tw-slider.component.js";
+export { TwScrollspyComponent } from "./tw-scrollspy/tw-scrollspy.component.js";
+export { TwFormComponent } from "./tw-form/tw-form.component.js";
+export { TwColorpickerComponent } from "./tw-colorpicker/tw-colorpicker.component.js";
+export { TwShareComponent } from "./tw-share/tw-share.component.js";
+export { TwTaggedImageComponent } from "./tw-tagged-image/tw-tagged-image.component.js";
+export { TwContentsComponent } from "./tw-contents/tw-contents.component.js";
+export { TwThemeButtonComponent } from "./tw-theme-button/tw-theme-button.component.js";
+
+// Phase 4: New TW-native components
+export { TwAlertComponent } from "./tw-alert/tw-alert.component.js";
+export { TwBadgeComponent } from "./tw-badge/tw-badge.component.js";
+export { TwAvatarComponent } from "./tw-avatar/tw-avatar.component.js";
+export { TwCardComponent } from "./tw-card/tw-card.component.js";
+export { TwSkeletonComponent } from "./tw-skeleton/tw-skeleton.component.js";
+export { TwBreadcrumbComponent } from "./tw-breadcrumb/tw-breadcrumb.component.js";
+export { TwKbdComponent } from "./tw-kbd/tw-kbd.component.js";
+export { TwPaginationComponent } from "./tw-pagination/tw-pagination.component.js";
+export { TwStepsComponent } from "./tw-steps/tw-steps.component.js";
+export { TwProgressComponent } from "./tw-progress/tw-progress.component.js";
+export { TwRatingComponent } from "./tw-rating/tw-rating.component.js";
+export { TwSwapComponent } from "./tw-swap/tw-swap.component.js";
+export { TwTooltipComponent } from "./tw-tooltip/tw-tooltip.component.js";
diff --git a/packages/tw/src/components/tw-accordion/tw-accordion.component.html b/packages/tw/src/components/tw-accordion/tw-accordion.component.html
new file mode 100644
index 000000000..6ce922922
--- /dev/null
+++ b/packages/tw/src/components/tw-accordion/tw-accordion.component.html
@@ -0,0 +1,27 @@
+
diff --git a/packages/tw/src/components/tw-accordion/tw-accordion.component.spec.ts b/packages/tw/src/components/tw-accordion/tw-accordion.component.spec.ts
new file mode 100644
index 000000000..726bcf09d
--- /dev/null
+++ b/packages/tw/src/components/tw-accordion/tw-accordion.component.spec.ts
@@ -0,0 +1,37 @@
+import { describe, it, expect } from "vitest";
+import template from "./tw-accordion.component.html?raw";
+
+describe("TwAccordionComponent template", () => {
+ it("contains the accordion structure classes", () => {
+ expect(template).toContain("divide-y");
+ expect(template).toContain("rounded-lg");
+ expect(template).toContain("border");
+ expect(template).toContain("tw-accordion-item");
+ expect(template).toContain("tw-accordion-header");
+ });
+
+ it("has a toggle button with rv-on-click binding", () => {
+ expect(template).toContain("rv-on-click");
+ });
+
+ it("has aria-expanded binding for accessibility", () => {
+ expect(template).toContain("rv-aria-expanded");
+ });
+
+ it("does not contain oversized SVG (has h-4 w-4 classes)", () => {
+ expect(template).toContain("h-4 w-4");
+ });
+
+ it("uses transition for the chevron icon rotation", () => {
+ expect(template).toContain("transition-transform");
+ expect(template).toContain("rv-class-rotate-180");
+ });
+
+ it("renders item title with rv-html binding", () => {
+ expect(template).toContain('rv-html="item.title"');
+ });
+
+ it("renders item content with rv-template binding", () => {
+ expect(template).toContain('rv-template="item.content"');
+ });
+});
diff --git a/packages/tw/src/components/tw-accordion/tw-accordion.component.ts b/packages/tw/src/components/tw-accordion/tw-accordion.component.ts
new file mode 100644
index 000000000..b5b26eba1
--- /dev/null
+++ b/packages/tw/src/components/tw-accordion/tw-accordion.component.ts
@@ -0,0 +1,184 @@
+import { TemplatesComponent } from "@ribajs/core";
+import { CollapseService } from "../../services/collapse.service.js";
+import template from "./tw-accordion.component.html?raw";
+
+interface AccordionItem {
+ title: string;
+ content: string;
+ show?: boolean;
+ iconDirection?: string;
+ handle?: string;
+}
+
+export class TwAccordionComponent extends TemplatesComponent {
+ public static tagName = "tw-accordion";
+
+ static get observedAttributes(): string[] {
+ return [
+ "items",
+ "collapse-icon-src",
+ "collapse-icon-size",
+ "show-only-one",
+ ];
+ }
+
+ protected autobind = true;
+
+ protected templateAttributes = [
+ { name: "title", required: true },
+ { name: "show", required: false, type: "boolean" as const },
+ { name: "icon-direction", required: false },
+ ];
+
+ protected collapseServices = new Map();
+
+ public scope = {
+ items: [] as AccordionItem[],
+ toggle: this.toggle.bind(this),
+ show: this.showItem.bind(this),
+ hide: this.hideItem.bind(this),
+ collapseIconSrc: undefined as string | undefined,
+ collapseIconSize: 16,
+ showOnlyOne: true,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwAccordionComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ // Initialize CollapseService for each item so the initial state is correct
+ this.scope.items.forEach((item, index) => {
+ const el = this.getCollapseEl(index);
+ if (el) {
+ el.style.transition = "max-height 0.3s ease";
+ const service = new CollapseService(el, { show: !!item.show });
+ this.collapseServices.set(index, service);
+ }
+ });
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected transformTemplateAttributes(
+ attributes: Record,
+ index: number,
+ ): AccordionItem {
+ const result = super.transformTemplateAttributes(attributes, index);
+ result.handle =
+ result.handle || (result.title || "").toLowerCase().replace(/\s+/g, "-");
+ result.show = !!result.show;
+ result.iconDirection =
+ result.iconDirection || (result.show ? "up" : "down");
+ return result;
+ }
+
+ public toggle(item: AccordionItem, index: number) {
+ const el = this.getCollapseEl(index);
+ if (!el) return;
+
+ const service = this.getOrCreateService(el, index, !item.show);
+
+ if (item.show) {
+ this.onHide(item, index);
+ service.hide();
+ } else {
+ if (this.scope.showOnlyOne) {
+ this.hideAll(index);
+ }
+ this.onShow(item, index);
+ service.show();
+ }
+ }
+
+ public showItem(item: AccordionItem, index: number) {
+ const el = this.getCollapseEl(index);
+ if (!el) return;
+
+ if (this.scope.showOnlyOne) {
+ this.hideAll(index);
+ }
+
+ const service = this.getOrCreateService(el, index, false);
+ service.show();
+ this.onShow(item, index);
+ }
+
+ public hideItem(item: AccordionItem, index: number) {
+ const el = this.getCollapseEl(index);
+ if (!el) return;
+
+ const service = this.getOrCreateService(el, index, true);
+ service.hide();
+ this.onHide(item, index);
+ }
+
+ protected hideAll(exceptIndex?: number) {
+ this.scope.items.forEach((item, i) => {
+ if (i !== exceptIndex && item.show) {
+ this.hideItem(item, i);
+ }
+ });
+ }
+
+ protected getCollapseEl(index: number): HTMLElement | null {
+ return this.querySelector(`[data-index="${index}"]`);
+ }
+
+ protected getOrCreateService(
+ el: HTMLElement,
+ index: number,
+ startHidden: boolean,
+ ): CollapseService {
+ let service = this.collapseServices.get(index);
+ if (!service) {
+ el.style.transition = "max-height 0.3s ease";
+ service = new CollapseService(el, { show: !startHidden });
+ this.collapseServices.set(index, service);
+ }
+ return service;
+ }
+
+ protected onShow(item: AccordionItem, _index: number) {
+ item.show = true;
+ item.iconDirection = "up";
+ this.dispatchEvent(
+ new CustomEvent("visibility-changed", {
+ detail: { item, show: true },
+ }),
+ );
+ }
+
+ protected onHide(item: AccordionItem, _index: number) {
+ item.show = false;
+ item.iconDirection = "down";
+ this.dispatchEvent(
+ new CustomEvent("visibility-changed", {
+ detail: { item, show: false },
+ }),
+ );
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.collapseServices.forEach((service) => service.dispose());
+ this.collapseServices.clear();
+ }
+
+ protected template() {
+ if (this.hasChildNodes()) {
+ const children = Array.from(this.children);
+ const hasNonTemplate = children.some((c) => c.tagName !== "TEMPLATE");
+ if (hasNonTemplate) return null;
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-alert/tw-alert.component.html b/packages/tw/src/components/tw-alert/tw-alert.component.html
new file mode 100644
index 000000000..ead04c7aa
--- /dev/null
+++ b/packages/tw/src/components/tw-alert/tw-alert.component.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dismiss
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-alert/tw-alert.component.spec.ts b/packages/tw/src/components/tw-alert/tw-alert.component.spec.ts
new file mode 100644
index 000000000..8afb62b7f
--- /dev/null
+++ b/packages/tw/src/components/tw-alert/tw-alert.component.spec.ts
@@ -0,0 +1,45 @@
+import { describe, it, expect } from "vitest";
+import template from "./tw-alert.component.html?raw";
+
+describe("TwAlertComponent template", () => {
+ it("contains blue color classes for info type", () => {
+ expect(template).toContain("bg-blue-50");
+ expect(template).toContain("text-blue-800");
+ expect(template).toContain("border-blue-300");
+ });
+
+ it("contains green color classes for success type", () => {
+ expect(template).toContain("bg-green-50");
+ expect(template).toContain("text-green-800");
+ expect(template).toContain("border-green-300");
+ });
+
+ it("contains yellow color classes for warning type", () => {
+ expect(template).toContain("bg-yellow-50");
+ expect(template).toContain("text-yellow-800");
+ expect(template).toContain("border-yellow-300");
+ });
+
+ it("contains red color classes for error type", () => {
+ expect(template).toContain("bg-red-50");
+ expect(template).toContain("text-red-800");
+ expect(template).toContain("border-red-300");
+ });
+
+ it("has a dismiss button with rv-on-click=\"dismiss\"", () => {
+ expect(template).toContain('rv-on-click="dismiss"');
+ });
+
+ it("has the dismiss button conditionally shown via rv-if=\"dismissible\"", () => {
+ expect(template).toContain('rv-if="dismissible"');
+ });
+
+ it("has role=\"alert\" for accessibility", () => {
+ expect(template).toContain('role="alert"');
+ });
+
+ it("displays title and message via rv-text bindings", () => {
+ expect(template).toContain('rv-text="title"');
+ expect(template).toContain('rv-text="message"');
+ });
+});
diff --git a/packages/tw/src/components/tw-alert/tw-alert.component.ts b/packages/tw/src/components/tw-alert/tw-alert.component.ts
new file mode 100644
index 000000000..0e1f78c9d
--- /dev/null
+++ b/packages/tw/src/components/tw-alert/tw-alert.component.ts
@@ -0,0 +1,60 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-alert.component.html?raw";
+
+type AlertType = "info" | "success" | "warning" | "error";
+
+interface Scope extends ScopeBase {
+ type: AlertType;
+ title: string;
+ message: string;
+ dismissible: boolean;
+ visible: boolean;
+ icon: string;
+ dismiss: TwAlertComponent["dismiss"];
+}
+
+export class TwAlertComponent extends Component {
+ public static tagName = "tw-alert";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["type", "title", "message", "dismissible", "icon"];
+ }
+
+ public scope: Scope = {
+ type: "info",
+ title: "",
+ message: "",
+ dismissible: false,
+ visible: true,
+ icon: "",
+ dismiss: this.dismiss.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwAlertComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ public dismiss() {
+ this.scope.visible = false;
+ this.dispatchEvent(
+ new CustomEvent("dismissed", {
+ detail: { type: this.scope.type },
+ }),
+ );
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-avatar/tw-avatar.component.html b/packages/tw/src/components/tw-avatar/tw-avatar.component.html
new file mode 100644
index 000000000..2290399da
--- /dev/null
+++ b/packages/tw/src/components/tw-avatar/tw-avatar.component.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-avatar/tw-avatar.component.ts b/packages/tw/src/components/tw-avatar/tw-avatar.component.ts
new file mode 100644
index 000000000..1374caa93
--- /dev/null
+++ b/packages/tw/src/components/tw-avatar/tw-avatar.component.ts
@@ -0,0 +1,135 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-avatar.component.html?raw";
+
+type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
+type AvatarStatus = "" | "online" | "offline" | "busy" | "away";
+
+interface Scope extends ScopeBase {
+ src: string;
+ alt: string;
+ size: AvatarSize;
+ status: AvatarStatus;
+ placeholder: string;
+ initials: string;
+ imgClass: string;
+ placeholderClass: string;
+ dotClass: string;
+}
+
+const SIZE_CLASSES: Record = {
+ xs: "h-6 w-6",
+ sm: "h-8 w-8",
+ md: "h-10 w-10",
+ lg: "h-14 w-14",
+ xl: "h-20 w-20",
+};
+
+const STATUS_DOT_SIZE_CLASSES: Record = {
+ xs: "h-1.5 w-1.5",
+ sm: "h-2 w-2",
+ md: "h-2.5 w-2.5",
+ lg: "h-3.5 w-3.5",
+ xl: "h-4 w-4",
+};
+
+const STATUS_COLOR_CLASSES: Record = {
+ online: "bg-green-400",
+ offline: "bg-gray-400",
+ busy: "bg-red-400",
+ away: "bg-yellow-400",
+};
+
+const INITIALS_TEXT_SIZE: Record = {
+ xs: "text-xs",
+ sm: "text-xs",
+ md: "text-sm",
+ lg: "text-lg",
+ xl: "text-xl",
+};
+
+export class TwAvatarComponent extends Component {
+ public static tagName = "tw-avatar";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["src", "alt", "size", "status", "placeholder"];
+ }
+
+ public scope: Scope = {
+ src: "",
+ alt: "",
+ size: "md",
+ status: "",
+ placeholder: "",
+ initials: "",
+ imgClass: SIZE_CLASSES.md,
+ placeholderClass: `${SIZE_CLASSES.md} ${INITIALS_TEXT_SIZE.md}`,
+ dotClass: STATUS_DOT_SIZE_CLASSES.md,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwAvatarComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ this.updateComputedScope();
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.updateComputedScope();
+ }
+
+ protected updateComputedScope() {
+ const size = this.scope.size || "md";
+ const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.md;
+ const textClass = INITIALS_TEXT_SIZE[size] || INITIALS_TEXT_SIZE.md;
+ const dotSize = STATUS_DOT_SIZE_CLASSES[size] || STATUS_DOT_SIZE_CLASSES.md;
+ const dotColor = this.scope.status
+ ? STATUS_COLOR_CLASSES[this.scope.status] || ""
+ : "";
+
+ this.scope.imgClass = sizeClass;
+ this.scope.placeholderClass = `${sizeClass} ${textClass}`;
+ this.scope.dotClass = `${dotSize} ${dotColor}`.trim();
+
+ // Derive initials from placeholder text
+ if (this.scope.placeholder) {
+ const text = this.scope.placeholder.trim();
+ const parts = text.split(/\s+/);
+ if (parts.length === 1 && text.length <= 3) {
+ this.scope.initials = text.toUpperCase();
+ } else {
+ this.scope.initials = parts
+ .slice(0, 2)
+ .map((p) => p.charAt(0).toUpperCase())
+ .join("");
+ }
+ }
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-badge/tw-badge.component.ts b/packages/tw/src/components/tw-badge/tw-badge.component.ts
new file mode 100644
index 000000000..6bddfc763
--- /dev/null
+++ b/packages/tw/src/components/tw-badge/tw-badge.component.ts
@@ -0,0 +1,122 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+
+type BadgeType = "default" | "info" | "success" | "warning" | "error";
+type BadgeSize = "sm" | "md" | "lg";
+
+interface Scope extends ScopeBase {
+ type: BadgeType;
+ size: BadgeSize;
+ outline: boolean;
+}
+
+const TYPE_CLASSES: Record = {
+ default: {
+ solid: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
+ outline:
+ "bg-transparent border border-gray-500 text-gray-500 dark:border-gray-400 dark:text-gray-400",
+ },
+ info: {
+ solid: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
+ outline:
+ "bg-transparent border border-blue-400 text-blue-400 dark:border-blue-500 dark:text-blue-500",
+ },
+ success: {
+ solid: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
+ outline:
+ "bg-transparent border border-green-400 text-green-400 dark:border-green-500 dark:text-green-500",
+ },
+ warning: {
+ solid:
+ "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
+ outline:
+ "bg-transparent border border-yellow-400 text-yellow-400 dark:border-yellow-500 dark:text-yellow-500",
+ },
+ error: {
+ solid: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
+ outline:
+ "bg-transparent border border-red-400 text-red-400 dark:border-red-500 dark:text-red-500",
+ },
+};
+
+const SIZE_CLASSES: Record = {
+ sm: "text-xs px-1.5 py-0.5",
+ md: "text-xs px-2.5 py-0.5",
+ lg: "text-sm px-3 py-1",
+};
+
+export class TwBadgeComponent extends Component {
+ public static tagName = "tw-badge";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["type", "size", "outline"];
+ }
+
+ public scope: Scope = {
+ type: "default",
+ size: "md",
+ outline: false,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwBadgeComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.applyClasses();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (["type", "size", "outline"].includes(attributeName)) {
+ this.applyClasses();
+ }
+ }
+
+ protected applyClasses() {
+ const baseClasses = "inline-flex items-center font-medium rounded-full";
+ const typeConfig = TYPE_CLASSES[this.scope.type] || TYPE_CLASSES.default;
+ const colorClasses = this.scope.outline
+ ? typeConfig.outline
+ : typeConfig.solid;
+ const sizeClasses = SIZE_CLASSES[this.scope.size] || SIZE_CLASSES.md;
+
+ // Remove previously applied badge classes
+ this.className = this.className
+ .replace(
+ /(^|\s)(inline-flex|items-center|font-medium|rounded-full|text-xs|text-sm|px-\S+|py-\S+|bg-\S+|text-\S+|border-\S+|border|dark:\S+)\b/g,
+ "",
+ )
+ .trim();
+
+ const allClasses = `${baseClasses} ${sizeClasses} ${colorClasses}`;
+ allClasses.split(/\s+/).forEach((cls) => {
+ if (cls) this.classList.add(cls);
+ });
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-breadcrumb/tw-breadcrumb.component.html b/packages/tw/src/components/tw-breadcrumb/tw-breadcrumb.component.html
new file mode 100644
index 000000000..9c36a2a32
--- /dev/null
+++ b/packages/tw/src/components/tw-breadcrumb/tw-breadcrumb.component.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-breadcrumb/tw-breadcrumb.component.ts b/packages/tw/src/components/tw-breadcrumb/tw-breadcrumb.component.ts
new file mode 100644
index 000000000..abea5ca97
--- /dev/null
+++ b/packages/tw/src/components/tw-breadcrumb/tw-breadcrumb.component.ts
@@ -0,0 +1,104 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-breadcrumb.component.html?raw";
+
+interface BreadcrumbItem {
+ label: string;
+ href?: string;
+ active?: boolean;
+ /** Computed display mode: "link" | "active" | "plain" */
+ mode?: "link" | "active" | "plain";
+}
+
+interface Scope extends ScopeBase {
+ items: BreadcrumbItem[];
+ separator: string;
+}
+
+export class TwBreadcrumbComponent extends Component {
+ public static tagName = "tw-breadcrumb";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["items", "separator"];
+ }
+
+ public scope: Scope = {
+ items: [],
+ separator: "/",
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ // Parse templates BEFORE init() triggers template loading which replaces children
+ if (this.scope.items.length === 0) {
+ this.parseChildTemplates();
+ }
+ this.init(TwBreadcrumbComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected parseChildTemplates() {
+ const templates = this.querySelectorAll("template");
+ if (templates.length === 0) return;
+
+ const items: BreadcrumbItem[] = [];
+ templates.forEach((tmpl) => {
+ const label = tmpl.getAttribute("label") || "";
+ const href = tmpl.getAttribute("href") || undefined;
+ const active = tmpl.hasAttribute("active");
+ if (label) {
+ items.push({ label, href, active });
+ }
+ });
+ if (items.length > 0) {
+ this.scope.items = items;
+ }
+ this.computeItemModes();
+ }
+
+ protected computeItemModes() {
+ for (const item of this.scope.items) {
+ if (item.active) {
+ item.mode = "active";
+ } else if (item.href) {
+ item.mode = "link";
+ } else {
+ item.mode = "plain";
+ }
+ }
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "items" && typeof newValue === "string") {
+ try {
+ this.scope.items = JSON.parse(newValue);
+ this.computeItemModes();
+ } catch {
+ // Ignore parse errors
+ }
+ }
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-button/tw-button.component.ts b/packages/tw/src/components/tw-button/tw-button.component.ts
new file mode 100644
index 000000000..56358eb35
--- /dev/null
+++ b/packages/tw/src/components/tw-button/tw-button.component.ts
@@ -0,0 +1,105 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+
+interface Scope extends ScopeBase {
+ animationClass: string;
+ onClick: TwButtonComponent["onClick"];
+}
+
+export class TwButtonComponent extends Component {
+ public static tagName = "tw-button";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["animation-class"];
+ }
+
+ public scope: Scope = {
+ animationClass: "animate-pulse",
+ onClick: this.onClick.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ public onClick() {
+ this.startAnimation();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwButtonComponent.observedAttributes);
+ }
+
+ protected startAnimation() {
+ this.classList.add(this.scope.animationClass);
+ }
+
+ protected onStartAnimation() {
+ //
+ }
+
+ protected onEndAnimation() {
+ setTimeout(() => {
+ this.classList.remove(this.scope.animationClass);
+ });
+ }
+
+ protected async init(observedAttributes: string[]) {
+ return super.init(observedAttributes).then((view) => {
+ this.onStartAnimation = this.onStartAnimation.bind(this);
+ this.addEventListener(
+ "webkitAnimationStart" as "animationstart",
+ this.onStartAnimation,
+ );
+ this.addEventListener("animationstart", this.onStartAnimation);
+ this.onEndAnimation = this.onEndAnimation.bind(this);
+ this.addEventListener(
+ "webkitAnimationEnd" as "animationend",
+ this.onEndAnimation,
+ );
+ this.addEventListener("animationend", this.onEndAnimation);
+ this.addEventListener("click", this.scope.onClick);
+ return view;
+ });
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ }
+
+ // deconstruction
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener(
+ "webkitAnimationStart" as "animationstart",
+ this.onStartAnimation,
+ );
+ this.removeEventListener("animationstart", this.onStartAnimation);
+ this.removeEventListener(
+ "webkitAnimationEnd" as "animationend",
+ this.onEndAnimation,
+ );
+ this.removeEventListener("animationend", this.onEndAnimation);
+ this.removeEventListener("click", this.scope.onClick);
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-card/tw-card.component.html b/packages/tw/src/components/tw-card/tw-card.component.html
new file mode 100644
index 000000000..24e320c20
--- /dev/null
+++ b/packages/tw/src/components/tw-card/tw-card.component.html
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/packages/tw/src/components/tw-card/tw-card.component.ts b/packages/tw/src/components/tw-card/tw-card.component.ts
new file mode 100644
index 000000000..d4eac4e31
--- /dev/null
+++ b/packages/tw/src/components/tw-card/tw-card.component.ts
@@ -0,0 +1,78 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import template from "./tw-card.component.html?raw";
+
+interface Scope extends ScopeBase {
+ imageSrc: string;
+ imageAlt: string;
+ title: string;
+ compact: boolean;
+ paddingClass: string;
+ titleSizeClass: string;
+}
+
+export class TwCardComponent extends Component {
+ public static tagName = "tw-card";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["image-src", "image-alt", "title", "compact"];
+ }
+
+ public scope: Scope = {
+ imageSrc: "",
+ imageAlt: "",
+ title: "",
+ compact: false,
+ paddingClass: "p-5",
+ titleSizeClass: "text-xl",
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwCardComponent.observedAttributes);
+ }
+
+ protected updateComputedClasses() {
+ this.scope.paddingClass = this.scope.compact ? "p-3" : "p-5";
+ this.scope.titleSizeClass = this.scope.compact ? "text-lg" : "text-xl";
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.updateComputedClasses();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "compact") {
+ this.updateComputedClasses();
+ }
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected template(): ReturnType {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-carousel/tw-carousel.component.ts b/packages/tw/src/components/tw-carousel/tw-carousel.component.ts
new file mode 100644
index 000000000..30931a5b4
--- /dev/null
+++ b/packages/tw/src/components/tw-carousel/tw-carousel.component.ts
@@ -0,0 +1,153 @@
+import { TemplateFunction } from "@ribajs/core";
+import {
+ TwSlideshowComponent,
+ TwSlideshowComponentScope,
+} from "../tw-slideshow/tw-slideshow.component.js";
+
+/**
+ * Carousel component — a thin alias for TwSlideshowComponent with
+ * different defaults suited for auto-playing, infinite-looping carousels.
+ */
+export class TwCarouselComponent extends TwSlideshowComponent {
+ public static override tagName = "tw-carousel";
+
+ static override get observedAttributes(): string[] {
+ return [
+ ...TwSlideshowComponent.observedAttributes,
+ "autoplay",
+ "autoplay-interval",
+ ];
+ }
+
+ protected override defaultScope: TwSlideshowComponentScope = {
+ // Options — carousel-specific defaults
+ slidesToScroll: 1,
+ controls: true,
+ controlsPosition: "inside-middle",
+ sticky: false,
+ indicators: true,
+ indicatorsPosition: "inside-bottom",
+ drag: true,
+ touchScroll: true,
+ angle: "horizontal",
+ infinite: true,
+ columns: 0,
+
+ // States
+ items: [],
+ nextIndex: -1,
+ prevIndex: -1,
+ enableNextControl: false,
+ enablePrevControl: false,
+ showControls: false,
+ showIndicators: false,
+ activeSlides: [],
+ isScrolling: false,
+ slideItemStyle: {},
+
+ // Template methods
+ next: this.next.bind(this),
+ prev: this.prev.bind(this),
+ goTo: this.goTo.bind(this),
+ enableTouchScroll: this.enableTouchScroll.bind(this),
+ disableTouchScroll: this.disableTouchScroll.bind(this),
+
+ // Classes
+ controlsPositionClass: "",
+ indicatorsPositionClass: "",
+ };
+
+ protected autoplayEnabled = false;
+ protected autoplayInterval = 5000;
+ protected autoplayTimerIndex: number | null = null;
+
+ public override scope: TwSlideshowComponentScope = {
+ ...this.defaultScope,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected override parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+
+ if (attributeName === "autoplay") {
+ this.autoplayEnabled = !!newValue;
+ if (this.autoplayEnabled) {
+ this.startAutoplay();
+ } else {
+ this.stopAutoplay();
+ }
+ }
+
+ if (attributeName === "autoplayInterval") {
+ this.autoplayInterval =
+ typeof newValue === "number"
+ ? newValue
+ : parseInt(newValue, 10) || 5000;
+ if (this.autoplayEnabled) {
+ this.startAutoplay();
+ }
+ }
+ }
+
+ protected override async afterBind() {
+ await super.afterBind();
+ if (this.autoplayEnabled) {
+ this.startAutoplay();
+ }
+ this.addEventListener("mouseenter", this.onMouseEnter);
+ this.addEventListener("mouseleave", this.onMouseLeave);
+ }
+
+ protected onMouseEnter = () => {
+ if (this.autoplayEnabled) {
+ this.stopAutoplay();
+ }
+ };
+
+ protected onMouseLeave = () => {
+ if (this.autoplayEnabled) {
+ this.startAutoplay();
+ }
+ };
+
+ protected startAutoplay() {
+ this.stopAutoplay();
+ if (this.autoplayInterval > 0) {
+ this.autoplayTimerIndex = window.setInterval(() => {
+ this.next();
+ }, this.autoplayInterval);
+ }
+ }
+
+ protected stopAutoplay() {
+ if (this.autoplayTimerIndex !== null) {
+ window.clearInterval(this.autoplayTimerIndex);
+ this.autoplayTimerIndex = null;
+ }
+ }
+
+ protected override disconnectedCallback() {
+ this.stopAutoplay();
+ this.removeEventListener("mouseenter", this.onMouseEnter);
+ this.removeEventListener("mouseleave", this.onMouseLeave);
+ super.disconnectedCallback();
+ }
+
+ protected override template(): ReturnType {
+ // Re-use the slideshow template
+ return super.template();
+ }
+}
diff --git a/packages/tw/src/components/tw-collapse/tw-collapse.component.html b/packages/tw/src/components/tw-collapse/tw-collapse.component.html
new file mode 100644
index 000000000..fe6f1e38b
--- /dev/null
+++ b/packages/tw/src/components/tw-collapse/tw-collapse.component.html
@@ -0,0 +1,23 @@
+
diff --git a/packages/tw/src/components/tw-collapse/tw-collapse.component.ts b/packages/tw/src/components/tw-collapse/tw-collapse.component.ts
new file mode 100644
index 000000000..98a68f0d4
--- /dev/null
+++ b/packages/tw/src/components/tw-collapse/tw-collapse.component.ts
@@ -0,0 +1,100 @@
+import { Component } from "@ribajs/core";
+import { CollapseService } from "../../services/collapse.service.js";
+import template from "./tw-collapse.component.html?raw";
+
+export class TwCollapseComponent extends Component {
+ public static tagName = "tw-collapse";
+
+ static get observedAttributes(): string[] {
+ return ["title", "content", "collapsed"];
+ }
+
+ protected autobind = true;
+
+ protected collapseService?: CollapseService;
+
+ public scope = {
+ title: "",
+ content: "",
+ collapsed: true,
+ toggle: this.toggle.bind(this),
+ show: this.show.bind(this),
+ hide: this.hide.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwCollapseComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["title"];
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ if (this.hasChildNodes() && !this.scope.content) {
+ this.scope.content = this.innerHTML;
+ }
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ const collapseEl = this.querySelector(".tw-collapse-content");
+ if (collapseEl) {
+ collapseEl.style.transition = "max-height 0.3s ease";
+ this.collapseService = new CollapseService(collapseEl, {
+ show: !this.scope.collapsed,
+ });
+ }
+ }
+
+ public toggle() {
+ this.collapseService?.toggle();
+ this.scope.collapsed = !this.collapseService?.isShown;
+ this.dispatchVisibilityChanged();
+ }
+
+ public show() {
+ this.collapseService?.show();
+ this.scope.collapsed = false;
+ this.dispatchVisibilityChanged();
+ }
+
+ public hide() {
+ this.collapseService?.hide();
+ this.scope.collapsed = true;
+ this.dispatchVisibilityChanged();
+ }
+
+ protected dispatchVisibilityChanged() {
+ this.dispatchEvent(
+ new CustomEvent("visibility-changed", {
+ detail: { collapsed: this.scope.collapsed },
+ }),
+ );
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.collapseService?.dispose();
+ }
+
+ protected template() {
+ if (this.hasChildNodes()) {
+ const nonTemplateChildren = Array.from(this.childNodes).filter(
+ (n) =>
+ n.nodeType !== Node.COMMENT_NODE &&
+ (n as Element).tagName !== "TEMPLATE",
+ );
+ if (nonTemplateChildren.length > 0) {
+ return null;
+ }
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-colorpicker/tw-colorpicker.component.html b/packages/tw/src/components/tw-colorpicker/tw-colorpicker.component.html
new file mode 100644
index 000000000..12f5a3ee4
--- /dev/null
+++ b/packages/tw/src/components/tw-colorpicker/tw-colorpicker.component.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-colorpicker/tw-colorpicker.component.ts b/packages/tw/src/components/tw-colorpicker/tw-colorpicker.component.ts
new file mode 100644
index 000000000..cfa5a440b
--- /dev/null
+++ b/packages/tw/src/components/tw-colorpicker/tw-colorpicker.component.ts
@@ -0,0 +1,117 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import template from "./tw-colorpicker.component.html?raw";
+
+interface Scope extends ScopeBase {
+ /** The current color value in hex format */
+ value: string;
+ /** Optional label displayed above the picker */
+ label: string;
+ /** Callback when the color changes via the native input */
+ onInput: TwColorpickerComponent["onInput"];
+ /** Callback when the user confirms a color (change event) */
+ onChange: TwColorpickerComponent["onChange"];
+ /** Copy the current hex value to the clipboard */
+ copyHex: TwColorpickerComponent["copyHex"];
+ /** Whether the hex value was recently copied */
+ copied: boolean;
+}
+
+/**
+ * A simple color picker component using the native ` `
+ * enhanced with a visual preview swatch and a hex code display.
+ */
+export class TwColorpickerComponent extends Component {
+ public static tagName = "tw-colorpicker";
+
+ protected autobind = true;
+ public _debug = false;
+
+ static get observedAttributes(): string[] {
+ return ["value", "label"];
+ }
+
+ public scope: Scope = {
+ value: "#0088ff",
+ label: "",
+ onInput: this.onInput.bind(this),
+ onChange: this.onChange.bind(this),
+ copyHex: this.copyHex.bind(this),
+ copied: false,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwColorpickerComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ /**
+ * Fired continuously while the user drags the picker.
+ */
+ public onInput(event: Event) {
+ const input = event.target as HTMLInputElement;
+ this.scope.value = input.value;
+ this.dispatchEvent(
+ new CustomEvent("color-input", {
+ detail: { value: this.scope.value },
+ }),
+ );
+ }
+
+ /**
+ * Fired when the user commits a color (closes the native picker).
+ */
+ public onChange(event: Event) {
+ const input = event.target as HTMLInputElement;
+ this.scope.value = input.value;
+ this.dispatchEvent(
+ new CustomEvent("color-change", {
+ detail: { value: this.scope.value },
+ }),
+ );
+ }
+
+ /**
+ * Copy the current hex color to the clipboard.
+ */
+ public async copyHex() {
+ try {
+ await navigator.clipboard.writeText(this.scope.value);
+ this.scope.copied = true;
+ setTimeout(() => {
+ this.scope.copied = false;
+ }, 2000);
+ } catch (err) {
+ console.warn("Failed to copy color to clipboard", err);
+ }
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ }
+
+ protected template(): ReturnType {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-contents/tw-contents.component.html b/packages/tw/src/components/tw-contents/tw-contents.component.html
new file mode 100644
index 000000000..4eb9e1410
--- /dev/null
+++ b/packages/tw/src/components/tw-contents/tw-contents.component.html
@@ -0,0 +1,66 @@
+
+
+
+
Table of Contents
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-contents/tw-contents.component.ts b/packages/tw/src/components/tw-contents/tw-contents.component.ts
new file mode 100644
index 000000000..7c682d093
--- /dev/null
+++ b/packages/tw/src/components/tw-contents/tw-contents.component.ts
@@ -0,0 +1,202 @@
+import { Component, ScopeBase } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export interface Anchor {
+ element: HTMLHeadingElement;
+ href: string;
+ title: string;
+ childs: Anchor[];
+}
+
+export interface Scope extends ScopeBase {
+ /**
+ * Heading level to start scanning from (default 2 = h2).
+ */
+ headersStart: number;
+ /**
+ * How many heading levels deep to scan (e.g. 2 means h2 and h3 when start=2).
+ */
+ headersDepth: number;
+ /**
+ * How many parent levels to search for an id attribute on each header element.
+ */
+ findHeaderIdDepth: number;
+ /**
+ * CSS selector for the container to scan for headings.
+ */
+ headerParentSelector?: string;
+ /**
+ * Pixel offset when scrolling to a heading.
+ */
+ scrollOffset: number;
+ /**
+ * The element to scroll (CSS selector). Defaults to the window.
+ */
+ scrollElement?: string;
+ /**
+ * Whether to show a toggle button to collapse/expand the TOC.
+ */
+ showToggle: boolean;
+ /**
+ * Whether the TOC is currently collapsed.
+ */
+ collapsed: boolean;
+ /**
+ * Array of found headers / anchors.
+ */
+ anchors: Anchor[];
+ /**
+ * Toggle the TOC visibility.
+ */
+ toggle: TwContentsComponent["toggle"];
+}
+
+export class TwContentsComponent extends Component {
+ public static tagName = "tw-contents";
+
+ protected autobind = true;
+ public _debug = false;
+
+ protected wrapperElement?: Element;
+
+ static get observedAttributes(): string[] {
+ return [
+ "headers-start",
+ "headers-depth",
+ "find-header-id-depth",
+ "header-parent-selector",
+ "scroll-offset",
+ "scroll-element",
+ "show-toggle",
+ ];
+ }
+
+ public scope: Scope = {
+ headersDepth: 1,
+ headersStart: 2,
+ findHeaderIdDepth: 1,
+ headerParentSelector: undefined,
+ scrollOffset: 0,
+ scrollElement: undefined,
+ showToggle: false,
+ collapsed: false,
+ anchors: [],
+ toggle: this.toggle.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwContentsComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["headersStart", "headersDepth", "headerParentSelector"];
+ }
+
+ protected getIdFromElementOrParent(
+ element: HTMLElement,
+ depth = 1,
+ ): string | null {
+ if (element.id) {
+ return element.id;
+ }
+ if (depth <= this.scope.findHeaderIdDepth) {
+ if (element.parentElement) {
+ return this.getIdFromElementOrParent(element.parentElement, ++depth);
+ }
+ }
+ return null;
+ }
+
+ protected pushHeaders(
+ wrapperElement: Element,
+ headersStart: number,
+ headersDepth: number,
+ pushTo: Anchor[],
+ ) {
+ const headerElements = wrapperElement.querySelectorAll(
+ "h" + headersStart,
+ ) as NodeListOf;
+ headerElements.forEach((headerElement) => {
+ const id = this.getIdFromElementOrParent(headerElement);
+ if (!id) {
+ // Generate an id if the heading doesn't have one
+ const generatedId =
+ "tw-toc-" +
+ headerElement.textContent
+ ?.trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-|-$/g, "");
+ if (generatedId && generatedId !== "tw-toc-") {
+ headerElement.id = generatedId;
+ } else {
+ return;
+ }
+ }
+ const anchor: Anchor = {
+ element: headerElement,
+ href: "#" + (headerElement.id || id),
+ title: headerElement.textContent?.trim() || headerElement.innerHTML,
+ childs: [],
+ };
+ pushTo.push(anchor);
+ if (headerElement.parentElement && headersDepth >= headersStart + 1) {
+ this.pushHeaders(
+ headerElement.parentElement,
+ headersStart + 1,
+ headersDepth,
+ anchor.childs,
+ );
+ }
+ });
+ }
+
+ protected async afterBind() {
+ if (
+ this.scope.headerParentSelector &&
+ this.scope.headersStart &&
+ this.scope.headersDepth
+ ) {
+ this.wrapperElement =
+ document.querySelector(this.scope.headerParentSelector) || undefined;
+ this.scope.anchors = [];
+ if (!this.wrapperElement) {
+ console.error(
+ "tw-contents: No element found for selector",
+ this.scope.headerParentSelector,
+ );
+ return;
+ }
+ this.pushHeaders(
+ this.wrapperElement,
+ this.scope.headersStart,
+ this.scope.headersDepth,
+ this.scope.anchors,
+ );
+ }
+ await super.afterBind();
+ }
+
+ public toggle() {
+ this.scope.collapsed = !this.scope.collapsed;
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.scope.anchors = [];
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ } else {
+ const { default: tpl } = await import("./tw-contents.component.html?raw");
+ return tpl;
+ }
+ }
+}
diff --git a/packages/tw/src/components/tw-dropdown/tw-dropdown.component.html b/packages/tw/src/components/tw-dropdown/tw-dropdown.component.html
new file mode 100644
index 000000000..4a08cb4be
--- /dev/null
+++ b/packages/tw/src/components/tw-dropdown/tw-dropdown.component.html
@@ -0,0 +1,27 @@
+
diff --git a/packages/tw/src/components/tw-dropdown/tw-dropdown.component.ts b/packages/tw/src/components/tw-dropdown/tw-dropdown.component.ts
new file mode 100644
index 000000000..c05268216
--- /dev/null
+++ b/packages/tw/src/components/tw-dropdown/tw-dropdown.component.ts
@@ -0,0 +1,127 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { DropdownService } from "../../services/dropdown.service.js";
+
+import type { Placement } from "@floating-ui/dom";
+
+interface Scope extends ScopeBase {
+ toggle: TwDropdownComponent["toggle"];
+ show: TwDropdownComponent["show"];
+ hide: TwDropdownComponent["hide"];
+ isShown: boolean;
+ placement: Placement;
+}
+
+export class TwDropdownComponent extends Component {
+ public static tagName = "tw-dropdown";
+
+ protected autobind = true;
+
+ protected dropdown?: DropdownService;
+ protected triggerEl: HTMLElement | null = null;
+ protected menuEl: HTMLElement | null = null;
+
+ static get observedAttributes(): string[] {
+ return ["placement"];
+ }
+
+ public scope: Scope = {
+ toggle: this.toggle.bind(this),
+ show: this.show.bind(this),
+ hide: this.hide.bind(this),
+ isShown: false,
+ placement: "bottom-start",
+ };
+
+ constructor() {
+ super();
+ }
+
+ public toggle() {
+ if (!this.dropdown) {
+ throw new Error("Dropdown not ready!");
+ }
+ this.dropdown.toggle();
+ }
+
+ public show() {
+ if (!this.dropdown) {
+ throw new Error("Dropdown not ready!");
+ }
+ this.dropdown.show();
+ }
+
+ public hide() {
+ if (!this.dropdown) {
+ throw new Error("Dropdown not ready!");
+ }
+ this.dropdown.hide();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwDropdownComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.initDropdown();
+ this.addDropdownListeners();
+ await super.afterBind();
+ }
+
+ protected initDropdown() {
+ // Find the trigger element: a button or element with [data-dropdown-trigger]
+ this.triggerEl =
+ this.querySelector("[data-dropdown-trigger]") ||
+ this.querySelector("button") ||
+ this;
+
+ // Find the menu element: an element with [data-dropdown-menu] or role="menu"
+ this.menuEl =
+ this.querySelector("[data-dropdown-menu]") ||
+ this.querySelector('[role="menu"]') ||
+ this.querySelector("ul") ||
+ this.querySelector("div:last-child");
+
+ if (!this.menuEl) {
+ console.warn("[tw-dropdown] No menu element found.");
+ return;
+ }
+
+ // Apply Tailwind positioning classes
+ this.menuEl.classList.add("absolute", "z-50");
+
+ this.dropdown = new DropdownService(this.triggerEl, this.menuEl, {
+ placement: this.scope.placement,
+ });
+
+ this.scope.isShown = this.dropdown.isShown;
+ }
+
+ protected _onShown = () => {
+ this.scope.isShown = true;
+ };
+
+ protected _onHidden = () => {
+ this.scope.isShown = false;
+ };
+
+ protected addDropdownListeners() {
+ this.triggerEl?.addEventListener("tw.dropdown.shown", this._onShown);
+ this.triggerEl?.addEventListener("tw.dropdown.hidden", this._onHidden);
+ }
+
+ protected removeDropdownListeners() {
+ this.triggerEl?.removeEventListener("tw.dropdown.shown", this._onShown);
+ this.triggerEl?.removeEventListener("tw.dropdown.hidden", this._onHidden);
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeDropdownListeners();
+ this.dropdown?.dispose();
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-form/tw-form.component.html b/packages/tw/src/components/tw-form/tw-form.component.html
new file mode 100644
index 000000000..d3211770d
--- /dev/null
+++ b/packages/tw/src/components/tw-form/tw-form.component.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+ Reset
+
+
+
diff --git a/packages/tw/src/components/tw-form/tw-form.component.ts b/packages/tw/src/components/tw-form/tw-form.component.ts
new file mode 100644
index 000000000..d0bf3568d
--- /dev/null
+++ b/packages/tw/src/components/tw-form/tw-form.component.ts
@@ -0,0 +1,438 @@
+import {
+ Component,
+ HttpService,
+ HttpMethod,
+ HttpDataType,
+ ScopeBase,
+} from "@ribajs/core";
+import { stripHtml } from "@ribajs/utils/src/type.js";
+import {
+ getUID,
+ hasChildNodesTrim,
+ scrollTo,
+ getViewportDimensions,
+} from "@ribajs/utils/src/dom.js";
+
+export interface ValidationObject {
+ fields:
+ | {
+ [name: string]: string | boolean | string[];
+ }
+ | FormData;
+ valid: boolean;
+ error?: string;
+}
+
+export interface SubmitSettings {
+ action: string;
+ method: HttpMethod;
+ target?: string;
+ type: HttpDataType;
+}
+
+export interface Scope extends ScopeBase {
+ id: string;
+ form: ValidationObject;
+ onSubmit: TwFormComponent["onSubmit"];
+ onReset: TwFormComponent["onReset"];
+ enableSubmit: TwFormComponent["enableSubmit"];
+
+ disableSubmitUntilChange: boolean;
+ submitDisabled: boolean;
+ /**
+ * Set this to `true` to submit the form using ajax.
+ * Set this to `false` to use the default submit request with a page reload.
+ */
+ useAjax: boolean;
+ /**
+ * Used for the ajax submit request. Default is "form" but can also be "script" | "json" | "xml" | "text" | "html" | "form".
+ */
+ ajaxRequestType: HttpDataType;
+ /**
+ * If `true`, form data is collected automatically from form elements.
+ * If `false`, you should bind values manually via `rv-value`.
+ */
+ autoSetFormData: boolean;
+ stripHtml: boolean;
+ scrollToInvalidElement: boolean;
+ animateInvalidElement: boolean;
+}
+
+export class TwFormComponent extends Component {
+ public static tagName = "tw-form";
+ public _debug = false;
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return [
+ "id",
+ "disable-submit-until-change",
+ "use-ajax",
+ "ajax-request-type",
+ "auto-set-form-data",
+ "strip-html",
+ "scroll-to-invalid-element",
+ "animate-invalid-element",
+ ];
+ }
+
+ protected formEl: HTMLFormElement | null = null;
+
+ protected getDefaultScope(): Scope {
+ const scope: Scope = {
+ id: getUID("form"),
+
+ form: {
+ fields: {},
+ valid: false,
+ error: undefined,
+ },
+
+ disableSubmitUntilChange: false,
+ submitDisabled: false,
+ onSubmit: this.onSubmit.bind(this),
+ onReset: this.onReset.bind(this),
+ enableSubmit: this.enableSubmit.bind(this),
+
+ useAjax: true,
+ ajaxRequestType: "form",
+ autoSetFormData: true,
+ stripHtml: true,
+ scrollToInvalidElement: true,
+ animateInvalidElement: true,
+ };
+ return scope;
+ }
+
+ public scope: Scope = this.getDefaultScope();
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwFormComponent.observedAttributes);
+ }
+
+ public enableSubmit() {
+ if (this.scope.disableSubmitUntilChange) {
+ this.scope.submitDisabled = false;
+ }
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.id = this.scope.id;
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ }
+
+ protected stripHtmlFromFields() {
+ for (const key in this.scope.form.fields) {
+ if (
+ (this.scope.form.fields as any)[key] &&
+ typeof (this.scope.form.fields as any)[key] === "string"
+ ) {
+ (this.scope.form.fields as any)[key] = stripHtml(
+ (this.scope.form.fields as any)[key] as string,
+ );
+ }
+ }
+ }
+
+ public onSubmit(event: Event, el: HTMLButtonElement) {
+ if (!this.formEl) {
+ console.warn("No form found");
+ return false;
+ }
+
+ if (this.scope.autoSetFormData) {
+ this.getFormValues();
+ }
+
+ if (this.scope.stripHtml) {
+ this.stripHtmlFromFields();
+ }
+
+ this.validate(this.formEl, this.scope.form);
+
+ if (!this.scope.form.valid) {
+ this.onInvalidForm(event);
+ return;
+ }
+
+ const submitSettings = this.getSubmitSettings(event);
+ if (submitSettings?.target === "_blank") {
+ return true;
+ }
+
+ if (this.scope.useAjax) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.ajaxSubmit(event, el);
+ }
+ }
+
+ public onReset(event: Event) {
+ if (!this.formEl) {
+ console.warn("No form found");
+ return;
+ }
+ this.formEl.reset();
+ this.scope.form.valid = false;
+ this.scope.form.error = undefined;
+ this.scope.form.fields = {};
+
+ // Remove validation styling
+ this.formEl.classList.remove("tw-was-validated");
+ this.clearValidationErrors();
+
+ this.dispatchEvent(new CustomEvent("form-reset"));
+ }
+
+ protected async ajaxSubmit(event?: Event, el?: HTMLButtonElement) {
+ const submitSettings = this.getSubmitSettings(event);
+ if (!submitSettings) {
+ console.warn("Can't get submit settings");
+ return;
+ }
+
+ if (this.scope.autoSetFormData) {
+ this.getFormValues();
+ }
+
+ try {
+ const res = await HttpService.fetch(
+ submitSettings.action,
+ submitSettings.method,
+ this.scope.form.fields,
+ submitSettings.type,
+ );
+
+ if (!res || !res.body) {
+ return this.onErrorSubmit("500", "Error", "Empty body!");
+ }
+
+ const message = res.body && res.body.message ? res.body.message : "";
+ if (Number(res.status) >= 400) {
+ this.onErrorSubmit(res.status.toString(), message, res.body);
+ }
+ return this.onSuccessSubmit(res.status.toString(), message, res.body);
+ } catch (err: any) {
+ if (err.status && err.body) {
+ this.onErrorSubmit(err.status, err.body.message, err.body);
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ protected getSubmitSettings(event?: Event) {
+ if (!this.formEl) {
+ console.warn("No form found");
+ return null;
+ }
+
+ let action = this.formEl.action;
+ let method = this.formEl.method;
+ let target = this.formEl.target;
+
+ // Overwrite action by formaction attribute on the submitter button
+ if ((event as SubmitEvent)?.submitter) {
+ const submitter = (event as SubmitEvent).submitter as HTMLButtonElement;
+ action = submitter.formAction || action;
+ method = submitter.formMethod || method;
+ target = submitter.formTarget || target;
+ }
+
+ const settings: SubmitSettings = {
+ action,
+ method: method.toUpperCase() as HttpMethod,
+ target,
+ type: this.scope.ajaxRequestType,
+ };
+
+ return settings;
+ }
+
+ protected onInvalidForm(event: Event): void {
+ event.preventDefault();
+ event.stopPropagation();
+ if (!this.formEl) {
+ console.warn("No form found");
+ return;
+ }
+ const invalidElements =
+ this.formEl.querySelectorAll(":invalid");
+
+ // Show validation messages on all invalid elements
+ this.showValidationErrors(invalidElements);
+
+ if (invalidElements && invalidElements.length) {
+ const invalidElement = invalidElements[0];
+ if (this.scope.scrollToInvalidElement) {
+ this.scrollToElement(invalidElement);
+ }
+ if (this.scope.animateInvalidElement) {
+ this.animateInvalidElement(invalidElement);
+ }
+ }
+
+ this.dispatchEvent(
+ new CustomEvent("invalid", {
+ detail: { elements: invalidElements },
+ }),
+ );
+ }
+
+ protected showValidationErrors(invalidElements: NodeListOf) {
+ // Clear previous error messages
+ this.clearValidationErrors();
+
+ invalidElements.forEach((el) => {
+ const input = el as
+ | HTMLInputElement
+ | HTMLSelectElement
+ | HTMLTextAreaElement;
+ if (!input.validationMessage) return;
+
+ // Add red border
+ input.classList.add(
+ "!border-red-500",
+ "!ring-red-500",
+ "!focus:border-red-500",
+ "!focus:ring-red-500",
+ );
+
+ // Create error message element
+ const errorEl = document.createElement("p");
+ errorEl.className =
+ "mt-1 text-sm text-red-600 dark:text-red-400 tw-validation-error";
+ errorEl.textContent = input.validationMessage;
+
+ // Insert after the input (or after its parent if wrapped in a label)
+ const insertAfter = input.closest("label") || input;
+ insertAfter.parentNode?.insertBefore(errorEl, insertAfter.nextSibling);
+
+ // Clear error on input change
+ const clearOnInput = () => {
+ input.classList.remove(
+ "!border-red-500",
+ "!ring-red-500",
+ "!focus:border-red-500",
+ "!focus:ring-red-500",
+ );
+ errorEl.remove();
+ input.removeEventListener("input", clearOnInput);
+ input.removeEventListener("change", clearOnInput);
+ };
+ input.addEventListener("input", clearOnInput);
+ input.addEventListener("change", clearOnInput);
+ });
+ }
+
+ protected clearValidationErrors() {
+ if (!this.formEl) return;
+ // Remove all previous error messages
+ this.formEl
+ .querySelectorAll(".tw-validation-error")
+ .forEach((el) => el.remove());
+ // Remove red border classes from all inputs
+ this.formEl.querySelectorAll(".\\!border-red-500").forEach((el) => {
+ el.classList.remove(
+ "!border-red-500",
+ "!ring-red-500",
+ "!focus:border-red-500",
+ "!focus:ring-red-500",
+ );
+ });
+ }
+
+ protected scrollToElement(element: HTMLElement) {
+ const vp = getViewportDimensions();
+ const offset = vp.h / 2;
+ scrollTo(element, offset, window);
+ }
+
+ protected animateInvalidElement(element: HTMLElement, endsOn = 3000) {
+ element.classList.add("tw-invalid-flash");
+ setTimeout(() => {
+ element.classList.remove("tw-invalid-flash");
+ }, endsOn);
+ }
+
+ protected onErrorSubmit(status: string, message: string, response: any) {
+ this.dispatchEvent(
+ new CustomEvent("submit-error", {
+ detail: { status, message, response },
+ }),
+ );
+ }
+
+ protected onSuccessSubmit(status: string, message: string, response: any) {
+ if (this.scope.disableSubmitUntilChange) {
+ this.scope.submitDisabled = true;
+ }
+
+ this.dispatchEvent(
+ new CustomEvent("submit-success", {
+ detail: { status, message, response },
+ }),
+ );
+ }
+
+ protected validate(
+ form: HTMLFormElement,
+ validationScope: ValidationObject,
+ errorEventName = "validation-error",
+ ) {
+ validationScope.valid = form.checkValidity();
+ validationScope.error = form.validationMessage;
+ if (!validationScope.valid) {
+ this.dispatchEvent(new CustomEvent(errorEventName));
+ form.classList.add("tw-was-validated");
+ }
+ }
+
+ protected getFormValues() {
+ if (!this.formEl) {
+ console.warn("No form found");
+ return null;
+ }
+ this.scope.form.fields = new FormData(this.formEl);
+ return this.scope.form.fields;
+ }
+
+ protected initForm() {
+ const formEl = this.querySelector("form");
+ if (formEl) {
+ this.formEl = formEl;
+ this.formEl.setAttribute("novalidate", "");
+ this.formEl.addEventListener("submit", (event: Event) => {
+ this.onSubmit(event, event.target as HTMLButtonElement);
+ });
+ this.formEl.addEventListener("reset", (event: Event) => {
+ this.onReset(event);
+ });
+ } else {
+ console.warn("tw-form without a child found");
+ }
+ }
+
+ protected async template() {
+ if (hasChildNodesTrim(this)) {
+ this.initForm();
+ return null;
+ } else {
+ const { default: tpl } = await import("./tw-form.component.html?raw");
+ return tpl;
+ }
+ }
+}
diff --git a/packages/tw/src/components/tw-icon/tw-icon.component.ts b/packages/tw/src/components/tw-icon/tw-icon.component.ts
new file mode 100644
index 000000000..b4b3c8c79
--- /dev/null
+++ b/packages/tw/src/components/tw-icon/tw-icon.component.ts
@@ -0,0 +1,209 @@
+import { BasicComponent, TemplateFunction } from "@ribajs/core";
+
+const iconCache = new Map();
+
+export class TwIconComponent extends BasicComponent {
+ public static tagName = "tw-icon";
+
+ static get observedAttributes(): string[] {
+ return ["size", "width", "height", "src", "color", "direction", "alt"];
+ }
+
+ protected autobind = false;
+ public scope: any = {};
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.setAttribute("aria-hidden", "true");
+ this.setAttribute("role", "icon");
+ this.setAttribute("alt", "icon");
+ this.classList.add("iconset");
+ this.init(TwIconComponent.observedAttributes);
+ // set default values
+ if (!this.scope.direction) {
+ this.scope.direction = "up";
+ this.attributeChangedCallback(
+ "direction",
+ null,
+ this.scope.direction,
+ null,
+ );
+ }
+ }
+
+ protected async attributeChangedCallback(
+ name: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ // injects the changed attributes to scope
+ super.attributeChangedCallback(name, oldValue, newValue, namespace);
+
+ if (name === "src") {
+ this.onSrcChanged();
+ }
+
+ if (name === "color") {
+ this.setColor(newValue);
+ }
+
+ if (name === "size") {
+ this.setSize(newValue);
+ }
+
+ if (name === "width") {
+ this.setWidth(newValue);
+ }
+
+ if (name === "height") {
+ this.setHeight(newValue);
+ }
+
+ if (name === "direction") {
+ this.setDirection(newValue);
+ }
+ }
+
+ protected getSvg() {
+ return this.querySelector("svg");
+ }
+
+ protected async loadIcon(src: string) {
+ try {
+ let svgText = iconCache.get(src);
+ if (!svgText) {
+ const response = await fetch(src);
+ svgText = await response.text();
+ iconCache.set(src, svgText);
+ }
+ // Note: innerHTML is used intentionally here to inject SVG markup from
+ // trusted icon sources, consistent with the bs5-icon implementation.
+ this.innerHTML = svgText;
+ } catch (error) {
+ console.error(`[tw-icon] Failed to load icon: ${src}`, error);
+ }
+ }
+
+ protected async onSrcChanged() {
+ if (!this.scope.src) {
+ this.innerHTML = "";
+ return;
+ }
+
+ const currentSvg = this.getSvg();
+ const oldSrc = currentSvg ? currentSvg.getAttribute("src") : "";
+
+ // Icon already set (maybe on SSR)
+ if (oldSrc === this.scope.src) {
+ return;
+ }
+
+ await this.loadIcon(this.scope.src);
+
+ const newSvg = this.getSvg();
+ if (newSvg) {
+ newSvg.setAttribute("src", this.scope.src);
+ }
+ }
+
+ protected removeColor() {
+ this.className = this.className.replace(/(^|\s)text-\S+/g, "");
+ this.style.color = "";
+ }
+
+ protected setColor(color?: string) {
+ if (!color) {
+ return this.removeColor();
+ }
+ if (color.includes(",")) {
+ const colorArr = color.split(",");
+ if (colorArr.length > 0) {
+ this.className = this.className.replace(/(^|\s)text-\S+/g, "");
+ for (let i = 0; i < colorArr.length; i++) {
+ const newColor: string = colorArr[i];
+ if (newColor.startsWith("#") || newColor.startsWith("rgb")) {
+ this.style.color = newColor;
+ }
+ this.classList.add(`text-${newColor}`);
+ }
+ }
+ } else {
+ this.style.color = color;
+ this.className = this.className.replace(/(^|\s)text-\S+/g, "");
+ this.classList.add(`text-${color}`);
+ }
+ }
+
+ protected setSize(size: number) {
+ this.style.height = size + "px";
+ this.style.width = size + "px";
+ this.className = this.className.replace(/(^|\s)size-\S+/g, "");
+ this.classList.add(`size-${size}`);
+ }
+
+ protected setWidth(width: number) {
+ this.style.width = width + "px";
+ this.className = this.className.replace(/(^|\s)w-\S+/g, "");
+ this.classList.add(`w-${width}`);
+ }
+
+ protected setHeight(height: number) {
+ this.style.height = height + "px";
+ this.className = this.className.replace(/(^|\s)h-\S+/g, "");
+ this.classList.add(`h-${height}`);
+ }
+
+ protected setDirection(direction: string) {
+ let classString = `direction-${direction}`;
+ if (direction === "left") {
+ classString += " rotate-270";
+ } else if (
+ direction === "left-top" ||
+ direction === "left-up" ||
+ direction === "top-left" ||
+ direction === "up-left"
+ ) {
+ classString += " rotate-315";
+ } else if (direction === "top" || direction === "up") {
+ classString += " rotate-0";
+ } else if (
+ direction === "top-right" ||
+ direction === "up-right" ||
+ direction === "right-top" ||
+ direction === "right-up"
+ ) {
+ classString += " rotate-45";
+ } else if (direction === "right") {
+ classString += " rotate-90";
+ } else if (
+ direction === "right-bottom" ||
+ direction === "right-down" ||
+ direction === "bottom-right" ||
+ direction === "down-right"
+ ) {
+ classString += " rotate-135";
+ } else if (direction === "bottom" || direction === "down") {
+ classString += " rotate-180";
+ } else if (
+ direction === "left-bottom" ||
+ direction === "left-down" ||
+ direction === "bottom-left" ||
+ direction === "down-left"
+ ) {
+ classString += " rotate-225";
+ }
+
+ this.className = this.className.replace(/(^|\s)direction-\S+/g, "");
+ this.className = this.className.replace(/(^|\s)rotate-\S+/g, "");
+ this.className += " " + classString;
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-kbd/tw-kbd.component.ts b/packages/tw/src/components/tw-kbd/tw-kbd.component.ts
new file mode 100644
index 000000000..b97edc864
--- /dev/null
+++ b/packages/tw/src/components/tw-kbd/tw-kbd.component.ts
@@ -0,0 +1,85 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+
+type KbdSize = "sm" | "md" | "lg";
+
+interface Scope extends ScopeBase {
+ size: KbdSize;
+}
+
+const SIZE_CLASSES: Record = {
+ sm: "px-1.5 py-0.5 text-xs",
+ md: "px-2 py-1 text-sm",
+ lg: "px-2.5 py-1.5 text-base",
+};
+
+export class TwKbdComponent extends Component {
+ public static tagName = "tw-kbd";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["size"];
+ }
+
+ public scope: Scope = {
+ size: "md",
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwKbdComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.applyClasses();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "size") {
+ this.applyClasses();
+ }
+ }
+
+ protected applyClasses() {
+ const baseClasses =
+ "inline-flex items-center rounded-md border border-gray-300 bg-gray-100 font-mono font-semibold text-gray-800 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm";
+ const sizeClasses = SIZE_CLASSES[this.scope.size] || SIZE_CLASSES.md;
+
+ // Remove previously applied kbd classes
+ this.className = this.className
+ .replace(
+ /(^|\s)(inline-flex|items-center|rounded-md|border-\S+|border|bg-\S+|font-mono|font-semibold|text-\S+|px-\S+|py-\S+|shadow-sm|dark:\S+)\b/g,
+ "",
+ )
+ .trim();
+
+ const allClasses = `${baseClasses} ${sizeClasses}`;
+ allClasses.split(/\s+/).forEach((cls) => {
+ if (cls) this.classList.add(cls);
+ });
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-modal-item/tw-modal-item.component.html b/packages/tw/src/components/tw-modal-item/tw-modal-item.component.html
new file mode 100644
index 000000000..10a805756
--- /dev/null
+++ b/packages/tw/src/components/tw-modal-item/tw-modal-item.component.html
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/packages/tw/src/components/tw-modal-item/tw-modal-item.component.ts b/packages/tw/src/components/tw-modal-item/tw-modal-item.component.ts
new file mode 100644
index 000000000..6ec69e4ce
--- /dev/null
+++ b/packages/tw/src/components/tw-modal-item/tw-modal-item.component.ts
@@ -0,0 +1,99 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { ModalService } from "@ribajs/extras";
+import type { ModalNotification } from "../../types/index.js";
+import template from "./tw-modal-item.component.html?raw";
+
+interface Scope extends ScopeBase {
+ iconUrl?: string;
+ modal?: ModalNotification;
+ onHidden: TwModalItemComponent["onHidden"];
+ dismiss: TwModalItemComponent["dismiss"];
+ index: number;
+ $parent?: any;
+ $event?: CustomEvent;
+}
+
+/**
+ * Use this component to show a modal inside a tw-notification-container
+ */
+export class TwModalItemComponent extends Component {
+ public static tagName = "tw-modal-item";
+
+ protected autobind = true;
+
+ protected modalService?: ModalService;
+
+ static get observedAttributes(): string[] {
+ return ["modal", "index"];
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["modal"];
+ }
+
+ public scope: Scope = {
+ iconUrl: undefined,
+ modal: undefined,
+ onHidden: this.onHidden.bind(this),
+ dismiss: this.dismiss.bind(this),
+ index: -1,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwModalItemComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.initModal();
+ }
+
+ protected initModal() {
+ const modal = this.scope.modal;
+ const dialogEl = this.querySelector("dialog");
+ if (modal && dialogEl) {
+ this.modalService = new ModalService(dialogEl, {
+ backdrop: true,
+ keyboard: true,
+ });
+
+ dialogEl.addEventListener("modal.hidden", this.scope.onHidden, {
+ once: true,
+ });
+
+ this.modalService.show();
+ }
+ }
+
+ /** Can be called to dismiss the modal */
+ public dismiss() {
+ this.modalService?.hide();
+ }
+
+ /** Remove modal from DOM once hidden */
+ public onHidden() {
+ const parentScope = this.scope.$parent?.$parent;
+ if (typeof parentScope?.onItemHide === "function" && this.scope.modal) {
+ parentScope.onItemHide(
+ this.scope.$event,
+ this,
+ this.scope.index,
+ this.scope.modal,
+ );
+ }
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.modalService?.dispose();
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-navbar/tw-navbar.component.html b/packages/tw/src/components/tw-navbar/tw-navbar.component.html
new file mode 100644
index 000000000..bd3fd7108
--- /dev/null
+++ b/packages/tw/src/components/tw-navbar/tw-navbar.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-navbar/tw-navbar.component.ts b/packages/tw/src/components/tw-navbar/tw-navbar.component.ts
new file mode 100644
index 000000000..b9e86a4c9
--- /dev/null
+++ b/packages/tw/src/components/tw-navbar/tw-navbar.component.ts
@@ -0,0 +1,170 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { CollapseService } from "../../services/collapse.service.js";
+
+interface Scope extends ScopeBase {
+ toggle: TwNavbarComponent["toggle"];
+ show: TwNavbarComponent["show"];
+ hide: TwNavbarComponent["hide"];
+ isCollapsed: boolean;
+ collapseSelector: string;
+}
+
+export class TwNavbarComponent extends Component {
+ public static tagName = "tw-navbar";
+
+ protected autobind = true;
+
+ protected collapseServices = new Map();
+ protected routerEvents?: EventDispatcher;
+
+ static get observedAttributes(): string[] {
+ return ["collapse-selector"];
+ }
+
+ public scope: Scope = {
+ toggle: this.toggle.bind(this),
+ show: this.show.bind(this),
+ hide: this.hide.bind(this),
+ isCollapsed: true,
+ collapseSelector: "[data-navbar-collapse]",
+ };
+
+ constructor() {
+ super();
+ this.onStateChange = this.onStateChange.bind(this);
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.routerEvents = new EventDispatcher("main");
+ this.routerEvents.on("newPageReady", this.onNewPageReady, this);
+ this.setCollapseElements();
+ this.onStateChange();
+ this.init(TwNavbarComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.hide();
+ await super.afterBind();
+ }
+
+ public toggle(event?: Event) {
+ for (const service of this.collapseServices.values()) {
+ service.toggle();
+ }
+ this.onStateChange();
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ public show(event?: Event) {
+ for (const service of this.collapseServices.values()) {
+ service.show();
+ }
+ this.onStateChange();
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ public hide(event?: Event) {
+ for (const service of this.collapseServices.values()) {
+ service.hide();
+ }
+ this.onStateChange();
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ protected setCollapseElements() {
+ const collapseElements = Array.from(
+ this.querySelectorAll(this.scope.collapseSelector) || [],
+ );
+
+ // Remove stale entries
+ for (const el of this.collapseServices.keys()) {
+ if (!collapseElements.includes(el)) {
+ this.disposeCollapseElement(el);
+ }
+ }
+
+ // Add new entries
+ for (const el of collapseElements) {
+ if (!this.collapseServices.has(el)) {
+ el.style.transition = "max-height 0.3s ease";
+ const service = new CollapseService(el, { show: false });
+ this.collapseServices.set(el, service);
+ el.addEventListener("tw.collapse.shown", this.onStateChange);
+ el.addEventListener("tw.collapse.hidden", this.onStateChange);
+ }
+ }
+
+ this.hide();
+ }
+
+ protected disposeCollapseElement(el: HTMLElement) {
+ const service = this.collapseServices.get(el);
+ if (service) {
+ service.dispose();
+ }
+ this.collapseServices.delete(el);
+ el.removeEventListener("tw.collapse.shown", this.onStateChange);
+ el.removeEventListener("tw.collapse.hidden", this.onStateChange);
+ }
+
+ protected disposeAllCollapseElements() {
+ for (const el of this.collapseServices.keys()) {
+ this.disposeCollapseElement(el);
+ }
+ }
+
+ protected onStateChange() {
+ const firstService = this.collapseServices.values().next().value;
+ this.scope.isCollapsed = firstService ? firstService.isCollapsed : true;
+
+ if (this.scope.isCollapsed) {
+ this.setAttribute("aria-expanded", "false");
+ } else {
+ this.setAttribute("aria-expanded", "true");
+ }
+ }
+
+ protected onNewPageReady() {
+ this.hide();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "collapseSelector") {
+ this.setCollapseElements();
+ }
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.disposeAllCollapseElements();
+ if (this.routerEvents) {
+ this.routerEvents.off("newPageReady", this.onNewPageReady, this);
+ }
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-notification-container/tw-notification-container.component.html b/packages/tw/src/components/tw-notification-container/tw-notification-container.component.html
new file mode 100644
index 000000000..6278db8ba
--- /dev/null
+++ b/packages/tw/src/components/tw-notification-container/tw-notification-container.component.html
@@ -0,0 +1,12 @@
+
+
diff --git a/packages/tw/src/components/tw-notification-container/tw-notification-container.component.ts b/packages/tw/src/components/tw-notification-container/tw-notification-container.component.ts
new file mode 100644
index 000000000..72edb9620
--- /dev/null
+++ b/packages/tw/src/components/tw-notification-container/tw-notification-container.component.ts
@@ -0,0 +1,81 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import type { Notification } from "../../types/index.js";
+import template from "./tw-notification-container.component.html?raw";
+
+export interface Scope extends ScopeBase {
+ iconUrl?: string;
+ positionClass: string;
+ notifications: Notification[];
+ channelName: string;
+ onItemHide: TwNotificationContainerComponent["onItemHide"];
+}
+
+/**
+ * Container for toast and modal notifications.
+ * Listens on an EventDispatcher channel for "show-notification" events
+ * and renders the appropriate tw-toast-item or tw-modal-item components.
+ */
+export class TwNotificationContainerComponent extends Component {
+ public static tagName = "tw-notification-container";
+
+ protected autobind = true;
+
+ protected channelEvents?: EventDispatcher;
+
+ static get observedAttributes(): string[] {
+ return ["icon-url", "position-class", "channel-name"];
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ public scope: Scope = {
+ iconUrl: undefined,
+ positionClass: "fixed bottom-4 right-4",
+ notifications: [],
+ channelName: "toast",
+ onItemHide: this.onItemHide.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwNotificationContainerComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.channelEvents = new EventDispatcher(this.scope.channelName);
+ this.channelEvents.on("show-notification", this.onShowNotification, this);
+ }
+
+ protected onShowNotification(notification: Notification) {
+ this.scope.notifications.push(notification);
+ }
+
+ public onItemHide(
+ _event: CustomEvent | Event | undefined,
+ _el: HTMLElement,
+ index: number,
+ _notification: Notification,
+ ) {
+ if (index >= 0 && index < this.scope.notifications.length) {
+ this.scope.notifications.splice(index, 1);
+ }
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.channelEvents?.off("show-notification", this.onShowNotification, this);
+ }
+
+ protected template(): ReturnType {
+ if (this.hasChildNodes()) return null;
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-pagination/tw-pagination.component.html b/packages/tw/src/components/tw-pagination/tw-pagination.component.html
new file mode 100644
index 000000000..b20505b4f
--- /dev/null
+++ b/packages/tw/src/components/tw-pagination/tw-pagination.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+ …
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-pagination/tw-pagination.component.ts b/packages/tw/src/components/tw-pagination/tw-pagination.component.ts
new file mode 100644
index 000000000..d25859f8a
--- /dev/null
+++ b/packages/tw/src/components/tw-pagination/tw-pagination.component.ts
@@ -0,0 +1,204 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-pagination.component.html?raw";
+
+export interface PaginationPage {
+ number: number;
+ isEllipsis: boolean;
+ isCurrent: boolean;
+ pageClass: string;
+}
+
+const ACTIVE_PAGE_CLASS = "bg-blue-600 text-white";
+const INACTIVE_PAGE_CLASS =
+ "bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700";
+const ELLIPSIS_PAGE_CLASS =
+ "bg-white text-gray-700 cursor-default dark:bg-gray-800 dark:text-gray-300";
+
+function pageClassFor(page: {
+ isEllipsis: boolean;
+ isCurrent: boolean;
+}): string {
+ if (page.isEllipsis) return ELLIPSIS_PAGE_CLASS;
+ return page.isCurrent ? ACTIVE_PAGE_CLASS : INACTIVE_PAGE_CLASS;
+}
+
+export interface Scope extends ScopeBase {
+ totalPages: number;
+ currentPage: number;
+ maxVisible: number;
+ prevLabel: string;
+ nextLabel: string;
+ pages: PaginationPage[];
+ goToPage: TwPaginationComponent["goToPage"];
+ prevPage: TwPaginationComponent["prevPage"];
+ nextPage: TwPaginationComponent["nextPage"];
+ hasPrev: boolean;
+ hasNext: boolean;
+}
+
+export class TwPaginationComponent extends Component {
+ public static tagName = "tw-pagination";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return [
+ "total-pages",
+ "current-page",
+ "max-visible",
+ "prev-label",
+ "next-label",
+ ];
+ }
+
+ public scope: Scope = {
+ totalPages: 1,
+ currentPage: 1,
+ maxVisible: 5,
+ prevLabel: "Previous",
+ nextLabel: "Next",
+ pages: [],
+ goToPage: this.goToPage.bind(this),
+ prevPage: this.prevPage.bind(this),
+ nextPage: this.nextPage.bind(this),
+ hasPrev: false,
+ hasNext: false,
+ };
+
+ constructor() {
+ super();
+ }
+
+ public goToPage(page: PaginationPage) {
+ if (page.isEllipsis) {
+ return;
+ }
+ this.scope.currentPage = page.number;
+ this.updatePages();
+ this.dispatchEvent(
+ new CustomEvent("page-changed", {
+ detail: { page: this.scope.currentPage },
+ bubbles: true,
+ }),
+ );
+ }
+
+ public prevPage() {
+ if (this.scope.hasPrev) {
+ this.scope.currentPage--;
+ this.updatePages();
+ this.dispatchEvent(
+ new CustomEvent("page-changed", {
+ detail: { page: this.scope.currentPage },
+ bubbles: true,
+ }),
+ );
+ }
+ }
+
+ public nextPage() {
+ if (this.scope.hasNext) {
+ this.scope.currentPage++;
+ this.updatePages();
+ this.dispatchEvent(
+ new CustomEvent("page-changed", {
+ detail: { page: this.scope.currentPage },
+ bubbles: true,
+ }),
+ );
+ }
+ }
+
+ protected computePages(): PaginationPage[] {
+ const total = this.scope.totalPages;
+ const current = this.scope.currentPage;
+ const maxVisible = this.scope.maxVisible;
+ const raw: Array> = [];
+
+ if (total <= maxVisible) {
+ for (let i = 1; i <= total; i++) {
+ raw.push({ number: i, isEllipsis: false, isCurrent: i === current });
+ }
+ } else {
+ // Always show first page
+ raw.push({ number: 1, isEllipsis: false, isCurrent: current === 1 });
+
+ const halfVisible = Math.floor((maxVisible - 2) / 2);
+ let start = Math.max(2, current - halfVisible);
+ let end = Math.min(total - 1, current + halfVisible);
+
+ // Adjust range to fill maxVisible - 2 slots (excluding first and last)
+ const slotsAvailable = maxVisible - 2;
+ if (end - start + 1 < slotsAvailable) {
+ if (start === 2) {
+ end = Math.min(total - 1, start + slotsAvailable - 1);
+ } else {
+ start = Math.max(2, end - slotsAvailable + 1);
+ }
+ }
+
+ if (start > 2) {
+ raw.push({ number: -1, isEllipsis: true, isCurrent: false });
+ }
+
+ for (let i = start; i <= end; i++) {
+ raw.push({ number: i, isEllipsis: false, isCurrent: i === current });
+ }
+
+ if (end < total - 1) {
+ raw.push({ number: -1, isEllipsis: true, isCurrent: false });
+ }
+
+ // Always show last page
+ raw.push({
+ number: total,
+ isEllipsis: false,
+ isCurrent: current === total,
+ });
+ }
+
+ return raw.map((p) => ({ ...p, pageClass: pageClassFor(p) }));
+ }
+
+ protected updatePages() {
+ // Replace the array wholesale so rv-each re-renders reactively.
+ // In-place property mutations on items inside rv-each don't always
+ // propagate to child views — see CLAUDE.md note on rv-each.
+ this.scope.pages = this.computePages();
+ this.scope.hasPrev = this.scope.currentPage > 1;
+ this.scope.hasNext = this.scope.currentPage < this.scope.totalPages;
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwPaginationComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.updatePages();
+ await super.afterBind();
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["total-pages"];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ this.updatePages();
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-progress/tw-progress.component.html b/packages/tw/src/components/tw-progress/tw-progress.component.html
new file mode 100644
index 000000000..71c0efa6e
--- /dev/null
+++ b/packages/tw/src/components/tw-progress/tw-progress.component.html
@@ -0,0 +1,48 @@
+
+
+
+ Progress
+ {percentage}%
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-progress/tw-progress.component.ts b/packages/tw/src/components/tw-progress/tw-progress.component.ts
new file mode 100644
index 000000000..fd8ea8907
--- /dev/null
+++ b/packages/tw/src/components/tw-progress/tw-progress.component.ts
@@ -0,0 +1,124 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-progress.component.html?raw";
+
+export type ProgressType = "default" | "info" | "success" | "warning" | "error";
+export type ProgressSize = "xs" | "sm" | "md" | "lg";
+
+export interface Scope extends ScopeBase {
+ value: number;
+ max: number;
+ type: ProgressType;
+ showLabel: boolean;
+ size: ProgressSize;
+ percentage: number;
+ striped: boolean;
+ animated: boolean;
+ barColorClass: string;
+ barHeightClass: string;
+ barClass: string;
+}
+
+export class TwProgressComponent extends Component {
+ public static tagName = "tw-progress";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return [
+ "value",
+ "max",
+ "type",
+ "show-label",
+ "size",
+ "striped",
+ "animated",
+ ];
+ }
+
+ public scope: Scope = {
+ value: 0,
+ max: 100,
+ type: "default",
+ showLabel: false,
+ size: "md",
+ percentage: 0,
+ striped: false,
+ animated: false,
+ barColorClass: "bg-blue-600 dark:bg-blue-500",
+ barHeightClass: "h-4",
+ barClass: "bg-blue-600 dark:bg-blue-500 h-4",
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected computePercentage(): number {
+ if (this.scope.max <= 0) return 0;
+ return Math.round((this.scope.value / this.scope.max) * 100);
+ }
+
+ protected getColorClass(): string {
+ switch (this.scope.type) {
+ case "info":
+ return "bg-blue-500 dark:bg-blue-400";
+ case "success":
+ return "bg-green-500 dark:bg-green-400";
+ case "warning":
+ return "bg-yellow-500 dark:bg-yellow-400";
+ case "error":
+ return "bg-red-500 dark:bg-red-400";
+ default:
+ return "bg-blue-600 dark:bg-blue-500";
+ }
+ }
+
+ protected getHeightClass(): string {
+ switch (this.scope.size) {
+ case "xs":
+ return "h-1";
+ case "sm":
+ return "h-2";
+ case "lg":
+ return "h-6";
+ default:
+ return "h-4";
+ }
+ }
+
+ protected updateComputed() {
+ this.scope.percentage = this.computePercentage();
+ this.scope.barColorClass = this.getColorClass();
+ this.scope.barHeightClass = this.getHeightClass();
+ this.scope.barClass = `${this.scope.barColorClass} ${this.scope.barHeightClass}`;
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwProgressComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.updateComputed();
+ await super.afterBind();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ this.updateComputed();
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-rating/tw-rating.component.html b/packages/tw/src/components/tw-rating/tw-rating.component.html
new file mode 100644
index 000000000..db69e59aa
--- /dev/null
+++ b/packages/tw/src/components/tw-rating/tw-rating.component.html
@@ -0,0 +1,31 @@
+
diff --git a/packages/tw/src/components/tw-rating/tw-rating.component.ts b/packages/tw/src/components/tw-rating/tw-rating.component.ts
new file mode 100644
index 000000000..182d214c6
--- /dev/null
+++ b/packages/tw/src/components/tw-rating/tw-rating.component.ts
@@ -0,0 +1,138 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-rating.component.html?raw";
+
+export interface Star {
+ index: number;
+ filled: boolean;
+ half: boolean;
+}
+
+export interface Scope extends ScopeBase {
+ value: number;
+ max: number;
+ readonly: boolean;
+ size: string;
+ stars: Star[];
+ setRating: TwRatingComponent["setRating"];
+ onHover: TwRatingComponent["onHover"];
+ onLeave: TwRatingComponent["onLeave"];
+ hoverValue: number;
+ sizeClass: string;
+}
+
+export class TwRatingComponent extends Component {
+ public static tagName = "tw-rating";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["value", "max", "readonly", "size"];
+ }
+
+ public scope: Scope = {
+ value: 0,
+ max: 5,
+ readonly: false,
+ size: "md",
+ stars: [],
+ setRating: this.setRating.bind(this),
+ onHover: this.onHover.bind(this),
+ onLeave: this.onLeave.bind(this),
+ hoverValue: 0,
+ sizeClass: "h-6 w-6",
+ };
+
+ constructor() {
+ super();
+ }
+
+ public setRating(star: Star) {
+ if (this.scope.readonly) return;
+ this.scope.value = star.index + 1;
+ this.scope.hoverValue = 0;
+ this.updateStars();
+ this.dispatchEvent(
+ new CustomEvent("rating-changed", {
+ detail: { value: this.scope.value },
+ bubbles: true,
+ }),
+ );
+ }
+
+ public onHover(star: Star) {
+ if (this.scope.readonly) return;
+ this.scope.hoverValue = star.index + 1;
+ this.updateStars();
+ }
+
+ public onLeave() {
+ if (this.scope.readonly) return;
+ this.scope.hoverValue = 0;
+ this.updateStars();
+ }
+
+ protected computeStars(): Star[] {
+ const displayValue = this.scope.hoverValue || this.scope.value;
+ const stars: Star[] = [];
+
+ for (let i = 0; i < this.scope.max; i++) {
+ const filled = i < Math.floor(displayValue);
+ const half = !filled && i < displayValue && displayValue - i >= 0.5;
+ stars.push({ index: i, filled, half });
+ }
+
+ return stars;
+ }
+
+ protected getSizeClass(): string {
+ switch (this.scope.size) {
+ case "xs":
+ return "h-3 w-3";
+ case "sm":
+ return "h-4 w-4";
+ case "lg":
+ return "h-8 w-8";
+ case "xl":
+ return "h-10 w-10";
+ default:
+ return "h-6 w-6";
+ }
+ }
+
+ protected updateStars() {
+ // Replace the array wholesale so rv-each re-renders reactively.
+ // In-place property mutations on items inside rv-each don't always
+ // propagate to child views — see CLAUDE.md note on rv-each.
+ this.scope.stars = this.computeStars();
+ this.scope.sizeClass = this.getSizeClass();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwRatingComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.updateStars();
+ await super.afterBind();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ this.updateStars();
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-scrollspy/tw-scrollspy.component.html b/packages/tw/src/components/tw-scrollspy/tw-scrollspy.component.html
new file mode 100644
index 000000000..185af27f4
--- /dev/null
+++ b/packages/tw/src/components/tw-scrollspy/tw-scrollspy.component.html
@@ -0,0 +1,47 @@
+
+
+
diff --git a/packages/tw/src/components/tw-scrollspy/tw-scrollspy.component.ts b/packages/tw/src/components/tw-scrollspy/tw-scrollspy.component.ts
new file mode 100644
index 000000000..c4e1bb4af
--- /dev/null
+++ b/packages/tw/src/components/tw-scrollspy/tw-scrollspy.component.ts
@@ -0,0 +1,352 @@
+import { Component, TemplateFunction } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import { debounce } from "@ribajs/utils/src/control";
+import template from "./tw-scrollspy.component.html?raw";
+
+export interface ScrollspyAnchor {
+ element: HTMLHeadingElement;
+ href: string;
+ title: string;
+ active: boolean;
+ childs: ScrollspyAnchor[];
+}
+
+export interface TwScrollspyComponentScope {
+ /** Starting header level (e.g. 2 for h2) */
+ headersStart: number;
+ /** Number of header levels to include (e.g. 2 means h2 and h3) */
+ headersDepth: number;
+ /** Depth to search parent elements for an id */
+ findHeaderIdDepth: number;
+ /** CSS selector for the container element that holds the headers */
+ headerParentSelector?: string;
+ /** Selector for a custom target element to observe scrolling on */
+ target?: string;
+ /** Pixel offset from top when calculating scroll position */
+ offset: number;
+ /** Root margin for IntersectionObserver (e.g. "0px 0px -50% 0px") */
+ rootMargin: string;
+ /** Pixel offset when scrolling to an anchor */
+ scrollOffset: number;
+ /** Array of found anchors */
+ anchors: ScrollspyAnchor[];
+ /** Template method: scroll to a given anchor */
+ scrollToAnchor: TwScrollspyComponent["scrollToAnchor"];
+}
+
+/**
+ * Watches scroll position and highlights the active navigation link
+ * based on which section is currently in the viewport.
+ * Uses IntersectionObserver for efficient scroll tracking.
+ */
+export class TwScrollspyComponent extends Component {
+ public static tagName = "tw-scrollspy";
+
+ protected autobind = true;
+
+ protected observer?: IntersectionObserver;
+
+ protected wrapperElement?: Element;
+
+ protected scrollTarget?: Element | Window;
+
+ static get observedAttributes(): string[] {
+ return [
+ "headers-start",
+ "headers-depth",
+ "find-header-id-depth",
+ "header-parent-selector",
+ "target",
+ "offset",
+ "root-margin",
+ "scroll-offset",
+ ];
+ }
+
+ public scope: TwScrollspyComponentScope = {
+ headersStart: 2,
+ headersDepth: 1,
+ findHeaderIdDepth: 1,
+ headerParentSelector: undefined,
+ target: undefined,
+ offset: 0,
+ rootMargin: "0px 0px -60% 0px",
+ scrollOffset: 0,
+ anchors: [],
+ scrollToAnchor: this.scrollToAnchor.bind(this),
+ };
+
+ constructor() {
+ super();
+ this._onScroll = this._onScroll.bind(this);
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwScrollspyComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["headerParentSelector"];
+ }
+
+ /**
+ * Scroll to the element matching the anchor's href.
+ */
+ public scrollToAnchor(
+ event: Event,
+ scope: { href?: string } & Record,
+ ) {
+ event.preventDefault();
+ const href =
+ scope.href ||
+ (event.currentTarget as HTMLAnchorElement)?.getAttribute("href");
+ if (!href) return;
+
+ const targetEl = document.querySelector(href);
+ if (targetEl) {
+ const top =
+ targetEl.getBoundingClientRect().top +
+ window.scrollY -
+ this.scope.scrollOffset;
+ window.scrollTo({ top, behavior: "smooth" });
+ }
+ }
+
+ protected getIdFromElementOrParent(
+ element: HTMLElement,
+ depth = 1,
+ ): string | null {
+ if (element.id) {
+ return element.id;
+ }
+ if (depth <= this.scope.findHeaderIdDepth) {
+ if (element.parentElement) {
+ return this.getIdFromElementOrParent(element.parentElement, ++depth);
+ }
+ }
+ return null;
+ }
+
+ protected pushHeaders(
+ wrapperElement: Element,
+ headersStart: number,
+ headersDepth: number,
+ pushTo: ScrollspyAnchor[],
+ ) {
+ const headerElements = wrapperElement.querySelectorAll(
+ "h" + headersStart,
+ ) as NodeListOf;
+ headerElements.forEach((headerElement) => {
+ const id = this.getIdFromElementOrParent(headerElement);
+ if (!id) {
+ return;
+ }
+ pushTo.push({
+ element: headerElement,
+ href: "#" + id,
+ title: headerElement.textContent || headerElement.innerHTML,
+ active: false,
+ childs: [],
+ });
+ if (headerElement.parentElement && headersDepth >= headersStart + 1) {
+ this.pushHeaders(
+ headerElement.parentElement,
+ headersStart + 1,
+ headersDepth,
+ pushTo[pushTo.length - 1].childs,
+ );
+ }
+ });
+ }
+
+ protected initIntersectionObserver() {
+ this.destroyObserver();
+
+ const rootElement = this.scope.target
+ ? document.querySelector(this.scope.target)
+ : null;
+
+ this.observer = new IntersectionObserver(
+ (entries) => {
+ for (const entry of entries) {
+ const id = entry.target.id || entry.target.parentElement?.id;
+ if (!id) continue;
+ const href = "#" + id;
+ this.setAnchorActive(this.scope.anchors, href, entry.isIntersecting);
+ }
+ },
+ {
+ root: rootElement,
+ rootMargin: this.scope.rootMargin,
+ threshold: 0,
+ },
+ );
+
+ this.observeAnchors(this.scope.anchors);
+ }
+
+ protected observeAnchors(anchors: ScrollspyAnchor[]) {
+ for (const anchor of anchors) {
+ if (anchor.element) {
+ // Observe the section element (parent with id) or the header itself
+ const targetId = anchor.href.substring(1);
+ const targetEl = document.getElementById(targetId);
+ if (targetEl) {
+ this.observer?.observe(targetEl);
+ }
+ }
+ if (anchor.childs.length) {
+ this.observeAnchors(anchor.childs);
+ }
+ }
+ }
+
+ protected setAnchorActive(
+ anchors: ScrollspyAnchor[],
+ href: string,
+ active: boolean,
+ ): boolean {
+ for (const anchor of anchors) {
+ if (anchor.href === href) {
+ anchor.active = active;
+ return true;
+ }
+ if (anchor.childs.length) {
+ const found = this.setAnchorActive(anchor.childs, href, active);
+ if (found) return true;
+ }
+ }
+ return false;
+ }
+
+ protected _onScroll() {
+ this.updateActiveByScroll();
+ }
+
+ protected onScroll = debounce(this._onScroll.bind(this), 100);
+
+ protected updateActiveByScroll() {
+ if (!this.scope.anchors.length) return;
+
+ const offset = this.scope.offset;
+ let lastActiveHref: string | null = null;
+
+ // Flatten all anchors
+ const allAnchors = this.flattenAnchors(this.scope.anchors);
+
+ for (const anchor of allAnchors) {
+ const targetId = anchor.href.substring(1);
+ const targetEl = document.getElementById(targetId);
+ if (!targetEl) continue;
+
+ const rect = targetEl.getBoundingClientRect();
+ if (rect.top - offset <= 0) {
+ lastActiveHref = anchor.href;
+ }
+ }
+
+ // Deactivate all, then activate the last one above the fold
+ for (const anchor of allAnchors) {
+ anchor.active = anchor.href === lastActiveHref;
+ }
+ }
+
+ protected flattenAnchors(anchors: ScrollspyAnchor[]): ScrollspyAnchor[] {
+ const result: ScrollspyAnchor[] = [];
+ for (const anchor of anchors) {
+ result.push(anchor);
+ if (anchor.childs.length) {
+ result.push(...this.flattenAnchors(anchor.childs));
+ }
+ }
+ return result;
+ }
+
+ protected addEventListeners() {
+ this.scrollTarget = this.scope.target
+ ? document.querySelector(this.scope.target) || window
+ : window;
+ (this.scrollTarget as EventTarget).addEventListener(
+ "scroll",
+ this.onScroll,
+ { passive: true },
+ );
+ }
+
+ protected removeEventListeners() {
+ if (this.scrollTarget) {
+ (this.scrollTarget as EventTarget).removeEventListener(
+ "scroll",
+ this.onScroll,
+ );
+ }
+ }
+
+ protected destroyObserver() {
+ if (this.observer) {
+ this.observer.disconnect();
+ this.observer = undefined;
+ }
+ }
+
+ protected async afterBind() {
+ if (
+ this.scope.headerParentSelector &&
+ this.scope.headersStart &&
+ this.scope.headersDepth
+ ) {
+ this.wrapperElement =
+ document.querySelector(this.scope.headerParentSelector) || undefined;
+ this.scope.anchors = [];
+ if (!this.wrapperElement) {
+ console.error(
+ `[tw-scrollspy] No wrapper element found for selector: ${this.scope.headerParentSelector}`,
+ );
+ return;
+ }
+ this.pushHeaders(
+ this.wrapperElement,
+ this.scope.headersStart,
+ this.scope.headersStart + this.scope.headersDepth - 1,
+ this.scope.anchors,
+ );
+ }
+
+ this.initIntersectionObserver();
+ this.addEventListeners();
+
+ // Set initial active state
+ this.updateActiveByScroll();
+
+ await super.afterBind();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ }
+
+ protected disconnectedCallback() {
+ this.removeEventListeners();
+ this.destroyObserver();
+ this.scope.anchors = [];
+ super.disconnectedCallback();
+ }
+
+ protected template(): ReturnType {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-share/tw-share.component.html b/packages/tw/src/components/tw-share/tw-share.component.html
new file mode 100644
index 000000000..dbf1dd54d
--- /dev/null
+++ b/packages/tw/src/components/tw-share/tw-share.component.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Share
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-share/tw-share.component.ts b/packages/tw/src/components/tw-share/tw-share.component.ts
new file mode 100644
index 000000000..0cc74cfa1
--- /dev/null
+++ b/packages/tw/src/components/tw-share/tw-share.component.ts
@@ -0,0 +1,262 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import { stripHtml } from "@ribajs/utils/src/type.js";
+import { getUrl } from "@ribajs/utils/src/url.js";
+import template from "./tw-share.component.html?raw";
+
+export interface ShareItem {
+ id: string;
+ label: string;
+ urlTemplate: string;
+ mediaUrlTemplate?: string;
+ type: "popup" | "href" | "download" | "clipboard";
+ url: string;
+ available?: boolean;
+ availableFor: string[];
+ filename?: string;
+}
+
+export type ShareUrlType = "page" | "image" | "video";
+
+export interface Scope extends ScopeBase {
+ type: ShareUrlType;
+ title: string;
+ text: string;
+ /** Page URL to share */
+ url?: string;
+ /** Comma-separated platform list or "all" */
+ platforms: string;
+
+ /** true if the browser supports native share */
+ isNative: boolean;
+ /** Whether the dropdown menu is currently open */
+ menuOpen: boolean;
+
+ /** Share items for fallback dropdown */
+ shareItems: ShareItem[];
+
+ // Methods
+ share: TwShareComponent["share"];
+ shareOnService: TwShareComponent["shareOnService"];
+ toggleMenu: TwShareComponent["toggleMenu"];
+}
+
+export class TwShareComponent extends Component {
+ public static tagName = "tw-share";
+
+ protected autobind = true;
+ public _debug = false;
+
+ static get observedAttributes(): string[] {
+ return ["type", "title", "text", "url", "platforms"];
+ }
+
+ public scope: Scope = {
+ type: "page",
+ title: document.title,
+ text: "",
+ url: undefined,
+ platforms: "all",
+
+ isNative: typeof navigator.share === "function",
+ menuOpen: false,
+
+ shareItems: [],
+
+ share: this.share.bind(this),
+ shareOnService: this.shareOnService.bind(this),
+ toggleMenu: this.toggleMenu.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwShareComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.scope.shareItems = this.buildShareItems();
+ }
+
+ protected buildShareItems(): ShareItem[] {
+ const newLine = "%0A";
+ const all: ShareItem[] = [
+ {
+ id: "facebook",
+ label: "Facebook",
+ urlTemplate: "https://www.facebook.com/sharer/sharer.php?u={{url}}",
+ type: "popup",
+ url: "",
+ availableFor: ["page", "image", "video"],
+ },
+ {
+ id: "twitter",
+ label: "Twitter / X",
+ urlTemplate:
+ "https://twitter.com/intent/tweet?text={{text}}&url={{url}}",
+ type: "popup",
+ url: "",
+ availableFor: ["page", "image", "video"],
+ },
+ {
+ id: "whatsapp",
+ label: "WhatsApp",
+ urlTemplate: `https://api.whatsapp.com/send?text={{text}}${newLine}${newLine}{{url}}`,
+ type: "popup",
+ url: "",
+ availableFor: ["page", "image", "video"],
+ },
+ {
+ id: "telegram",
+ label: "Telegram",
+ urlTemplate: "https://telegram.me/share/url?url={{url}}&text={{text}}",
+ type: "popup",
+ url: "",
+ availableFor: ["page", "image", "video"],
+ },
+ {
+ id: "email",
+ label: "Email",
+ urlTemplate: `mailto:?subject={{title}}&body={{text}}${newLine}${newLine}{{url}}`,
+ type: "href",
+ url: "",
+ availableFor: ["page", "image", "video"],
+ },
+ {
+ id: "clipboard",
+ label: "Copy link",
+ urlTemplate: "{{url}}",
+ type: "clipboard",
+ url: "",
+ availableFor: ["page", "image", "video"],
+ },
+ ];
+
+ const platforms = this.scope.platforms.toLowerCase().trim();
+ if (platforms === "all") {
+ return all;
+ }
+ const allowed = platforms.split(",").map((p) => p.trim());
+ return all.filter((item) => allowed.includes(item.id));
+ }
+
+ protected getURLForShare(): string {
+ if (this.scope.url) {
+ return getUrl(this.scope.url);
+ }
+ return window.location.href;
+ }
+
+ protected getTextForShare(): string {
+ return stripHtml(this.scope.text);
+ }
+
+ protected getTitleForShare(): string {
+ return stripHtml(this.scope.title);
+ }
+
+ protected updateShareURLs() {
+ const url = this.getURLForShare();
+ const text = this.getTextForShare();
+ const title = this.getTitleForShare();
+
+ for (const item of this.scope.shareItems) {
+ const encode = item.type !== "clipboard";
+ item.available = item.availableFor.includes(this.scope.type);
+ item.url = item.urlTemplate
+ .replace("{{url}}", encode ? encodeURIComponent(url) : url)
+ .replace("{{text}}", encode ? encodeURIComponent(text) : text)
+ .replace("{{title}}", encode ? encodeURIComponent(title) : title);
+ }
+ }
+
+ /**
+ * Trigger the native Web Share API or open the fallback dropdown.
+ */
+ public async share(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.scope.isNative) {
+ try {
+ await navigator.share({
+ title: this.scope.title,
+ text: this.scope.text,
+ url: this.scope.url || window.location.href,
+ });
+ return;
+ } catch (error: any) {
+ if (error.name === "AbortError") {
+ return;
+ }
+ // Fallback to menu if native share fails
+ }
+ }
+
+ this.updateShareURLs();
+ this.toggleMenu();
+ }
+
+ /**
+ * Share via a specific service from the dropdown.
+ */
+ public async shareOnService(item: ShareItem, event: Event) {
+ this.scope.menuOpen = false;
+
+ if (item.type === "clipboard") {
+ event.preventDefault();
+ event.stopPropagation();
+ try {
+ await navigator.clipboard.writeText(item.url);
+ } catch (err) {
+ console.warn("Failed to copy to clipboard", err);
+ }
+ return false;
+ }
+
+ if (item.type === "download") {
+ return true;
+ }
+
+ if (item.type === "href") {
+ // Let the default anchor behavior handle mailto: etc.
+ return true;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ window.open(
+ item.url,
+ "Share",
+ "scrollbars=yes,resizable=yes,toolbar=no," +
+ "location=yes,width=550,height=420,top=100,left=" +
+ (window.screen ? Math.round(screen.width / 2 - 275) : 100),
+ );
+
+ return false;
+ }
+
+ public toggleMenu() {
+ this.scope.menuOpen = !this.scope.menuOpen;
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ }
+
+ protected template(): ReturnType {
+ if (hasChildNodesTrim(this)) {
+ return null;
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-sidebar/tw-sidebar.component.ts b/packages/tw/src/components/tw-sidebar/tw-sidebar.component.ts
new file mode 100644
index 000000000..61c775410
--- /dev/null
+++ b/packages/tw/src/components/tw-sidebar/tw-sidebar.component.ts
@@ -0,0 +1,465 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import { debounce } from "@ribajs/utils/src/control.js";
+import { TOGGLE_BUTTON } from "../../constants/index.js";
+import { TwService } from "../../services/tw.service.js";
+
+export type SidebarState =
+ | "hidden"
+ | "side-left"
+ | "side-right"
+ | "overlap-left"
+ | "overlap-right";
+
+export type SidebarPosition = "left" | "right";
+export type SidebarMode = "side" | "overlap";
+
+interface Scope extends ScopeBase {
+ containerSelector?: string;
+ state: SidebarState;
+ oldState: SidebarState;
+ id?: string;
+ width: string;
+ position: SidebarPosition;
+ mode: SidebarMode;
+ autoShow: boolean;
+ autoHide: boolean;
+ watchNewPageReadyEvent: boolean;
+ forceHideOnLocationPathnames: string[];
+ forceShowOnLocationPathnames: string[];
+ preventScrollingOnOverlap: boolean;
+ hide: TwSidebarComponent["hide"];
+ show: TwSidebarComponent["show"];
+ toggle: TwSidebarComponent["toggle"];
+}
+
+export class TwSidebarComponent extends Component {
+ public static tagName = "tw-sidebar";
+
+ public static states: SidebarState[] = [
+ "hidden",
+ "side-left",
+ "side-right",
+ "overlap-left",
+ "overlap-right",
+ ];
+
+ protected autobind = true;
+
+ protected tw: TwService;
+
+ static get observedAttributes(): string[] {
+ return [
+ "id",
+ "container-selector",
+ "position",
+ "mode",
+ "width",
+ "auto-show",
+ "auto-hide",
+ "force-hide-on-location-pathnames",
+ "force-show-on-location-pathnames",
+ "watch-new-page-ready-event",
+ "prevent-scrolling-on-overlap",
+ ];
+ }
+
+ public events?: EventDispatcher;
+ protected routerEvents = new EventDispatcher("main");
+ protected backdropEl?: HTMLElement;
+
+ protected defaults: Scope = {
+ containerSelector: undefined,
+ state: "hidden",
+ oldState: "hidden",
+ id: undefined,
+ width: "16rem",
+ position: "left",
+ mode: "overlap",
+ autoShow: false,
+ autoHide: false,
+ watchNewPageReadyEvent: true,
+ forceHideOnLocationPathnames: [],
+ forceShowOnLocationPathnames: [],
+ preventScrollingOnOverlap: true,
+ hide: this.hide.bind(this),
+ show: this.show.bind(this),
+ toggle: this.toggle.bind(this),
+ };
+
+ public scope: Scope = { ...this.defaults };
+
+ constructor() {
+ super();
+ this.tw = TwService.getSingleton();
+ this.onEnvironmentChanges = this.onEnvironmentChanges.bind(this);
+ }
+
+ public setState(state: SidebarState) {
+ this.scope.oldState = this.scope.state;
+ this.scope.state = state;
+ this.onStateChange();
+ }
+
+ public getState() {
+ return this.scope.state;
+ }
+
+ public getShowMode(): SidebarState {
+ return `${this.scope.mode}-${this.scope.position}` as SidebarState;
+ }
+
+ public hide() {
+ this.setState("hidden");
+ }
+
+ public show() {
+ this.setState(this.getShowMode());
+ }
+
+ public toggle() {
+ if (this.scope.state === "hidden") {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+
+ protected preventScrolling(scrollEl = document.body) {
+ scrollEl.style.overflow = "hidden";
+ }
+
+ protected allowScrolling(scrollEl = document.body) {
+ if (scrollEl.style.overflow === "hidden") {
+ scrollEl.style.overflow = "";
+ }
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwSidebarComponent.observedAttributes);
+ this.addEventListeners();
+ this.onEnvironmentChanges();
+ }
+
+ protected onBreakpoint() {
+ this.onEnvironmentChanges();
+ }
+
+ protected addEventListeners() {
+ this.tw.events.on("breakpoint:changed", this.onBreakpoint, this);
+ }
+
+ protected removeEventListeners() {
+ this.events?.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ this.events?.off(TOGGLE_BUTTON.eventNames.toggle, this.toggle, this);
+ this.routerEvents.off("newPageReady", this.onEnvironmentChanges, this);
+ this.tw.events.off("breakpoint:changed", this.onBreakpoint, this);
+ }
+
+ protected initToggleButtonEventDispatcher() {
+ if (this.events) {
+ this.events.off(TOGGLE_BUTTON.eventNames.toggle, this.toggle, this);
+ this.events.off(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+ const namespace = TOGGLE_BUTTON.nsPrefix + this.scope.id;
+ this.events = new EventDispatcher(namespace);
+ this.events.on(TOGGLE_BUTTON.eventNames.toggle, this.toggle, this);
+ this.events.on(TOGGLE_BUTTON.eventNames.init, this.triggerState, this);
+ }
+
+ protected initRouterEventDispatcher() {
+ if (this.scope.watchNewPageReadyEvent) {
+ this.routerEvents.on("newPageReady", this.onEnvironmentChanges, this);
+ }
+ }
+
+ protected showBackdrop() {
+ if (this.backdropEl) return;
+ const el = document.createElement("div");
+ el.className =
+ "fixed inset-0 z-40 bg-black/50 dark:bg-black/70 transition-opacity duration-200 ease-in-out";
+ el.style.opacity = "0";
+ el.addEventListener("click", () => this.hide());
+ document.body.appendChild(el);
+ this.backdropEl = el;
+ // Double RAF ensures the initial opacity:0 is committed before we animate to 1
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ el.style.opacity = "1";
+ });
+ });
+ }
+
+ protected hideBackdrop() {
+ if (!this.backdropEl) return;
+ const el = this.backdropEl;
+ this.backdropEl = undefined;
+ el.style.opacity = "0";
+ const onEnd = () => {
+ el.removeEventListener("transitionend", onEnd);
+ el.remove();
+ };
+ el.addEventListener("transitionend", onEnd);
+ // Fallback in case transitionend doesn't fire (e.g. element was never visible)
+ setTimeout(() => {
+ if (el.parentNode) {
+ el.removeEventListener("transitionend", onEnd);
+ el.remove();
+ }
+ }, 300);
+ }
+
+ protected initCloseButton() {
+ if (this.querySelector("[data-sidebar-close]")) return;
+ const closeBtn = document.createElement("button");
+ closeBtn.setAttribute("data-sidebar-close", "");
+ closeBtn.setAttribute("type", "button");
+ closeBtn.setAttribute("aria-label", "Close sidebar");
+ closeBtn.className =
+ "absolute top-3 right-3 rounded-md p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200";
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("class", "h-5 w-5");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ svg.setAttribute("stroke-width", "1.5");
+ svg.setAttribute("stroke", "currentColor");
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ path.setAttribute("stroke-linecap", "round");
+ path.setAttribute("stroke-linejoin", "round");
+ path.setAttribute("d", "M6 18 18 6M6 6l12 12");
+ svg.appendChild(path);
+ closeBtn.appendChild(svg);
+ closeBtn.addEventListener("click", () => this.hide());
+ this.insertBefore(closeBtn, this.firstChild);
+ }
+
+ protected onHidden() {
+ const translateX = this.scope.position === "left" ? "-100%" : "100%";
+ this.style.transform = `translateX(${translateX})`;
+ this.style.width = this.scope.width;
+ this.setContainersStyle(this.scope.state);
+ this.hideBackdrop();
+ if (this.scope.preventScrollingOnOverlap) {
+ this.allowScrolling();
+ }
+ }
+
+ protected onSide(state: SidebarState) {
+ this.style.transform = "translateX(0)";
+ this.style.width = this.scope.width;
+ this.setContainersStyle(state);
+ this.hideBackdrop();
+ if (this.scope.preventScrollingOnOverlap) {
+ this.allowScrolling();
+ }
+ }
+
+ protected onOverlap(state: SidebarState) {
+ this.style.transform = "translateX(0)";
+ this.style.width = this.scope.width;
+ this.setContainersStyle(state);
+ this.showBackdrop();
+ if (this.scope.preventScrollingOnOverlap) {
+ this.preventScrolling();
+ }
+ }
+
+ protected triggerState() {
+ this.events?.trigger("state", this.scope.state);
+ }
+
+ protected onStateChange() {
+ switch (this.scope.state) {
+ case "side-left":
+ case "side-right":
+ this.onSide(this.scope.state);
+ break;
+ case "overlap-left":
+ case "overlap-right":
+ this.onOverlap(this.scope.state);
+ break;
+ default:
+ this.onHidden();
+ break;
+ }
+
+ // Remove all state classes, then add the current one
+ this.classList.remove(...TwSidebarComponent.states);
+ this.classList.add(this.scope.state);
+
+ if (this.events) {
+ this.events.trigger(TOGGLE_BUTTON.eventNames.toggled, this.scope.state);
+ }
+ this.dispatchEvent(
+ new CustomEvent(TOGGLE_BUTTON.eventNames.toggled, {
+ detail: this.scope.state,
+ }),
+ );
+ }
+
+ protected setStateByEnvironment() {
+ if (
+ this.scope.forceHideOnLocationPathnames.includes(window.location.pathname)
+ ) {
+ return this.hide();
+ }
+ if (
+ this.scope.forceShowOnLocationPathnames.includes(window.location.pathname)
+ ) {
+ return this.show();
+ }
+ if (this.scope.autoHide) {
+ return this.hide();
+ }
+ if (this.scope.autoShow) {
+ return this.show();
+ }
+ }
+
+ protected _onEnvironmentChanges() {
+ this.setStateByEnvironment();
+ }
+
+ protected onEnvironmentChanges = debounce(
+ this._onEnvironmentChanges.bind(this),
+ );
+
+ protected getContainers() {
+ return this.scope.containerSelector
+ ? document.querySelectorAll(this.scope.containerSelector)
+ : undefined;
+ }
+
+ protected setContainersStyle(state: SidebarState) {
+ const containers = this.getContainers() || [];
+ for (let i = 0; i < containers.length; i++) {
+ this.setContainerStyle(containers[i], state);
+ }
+ }
+
+ protected setContainerStyle(container: HTMLElement, state: SidebarState) {
+ const currStyle = container.style;
+ const width = this.scope.width;
+
+ switch (state) {
+ case "side-left":
+ currStyle.marginLeft = width;
+ currStyle.marginRight = "";
+ break;
+ case "side-right":
+ currStyle.marginRight = width;
+ currStyle.marginLeft = "";
+ break;
+ case "hidden":
+ currStyle.marginLeft = "";
+ currStyle.marginRight = "";
+ break;
+ default:
+ // Overlap modes do not shift the container
+ break;
+ }
+
+ currStyle.transition = "margin 0.3s ease";
+ }
+
+ protected initHostStyles() {
+ // The component uses template() => null, so we must style the host element
+ this.style.position = "fixed";
+ this.style.top = "0";
+ this.style.height = "100%";
+ this.style.zIndex = "50";
+ this.style.display = "flex";
+ this.style.flexDirection = "column";
+ this.style.overflowY = "auto";
+ this.style.padding = "1rem";
+ this.style.paddingTop = "2.5rem";
+ this.style.transition = "transform 0.3s ease-in-out";
+ this.style.boxShadow =
+ "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)";
+ // Theme-aware surface via Tailwind classes (dark variant triggers from .dark ancestor)
+ this.classList.add("bg-white", "dark:bg-gray-800");
+ if (this.scope.position === "left") {
+ this.style.left = "0";
+ this.style.right = "";
+ } else {
+ this.style.right = "0";
+ this.style.left = "";
+ }
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.initHostStyles();
+ this.initCloseButton();
+ this.scope.oldState = this.getShowMode();
+ this.initRouterEventDispatcher();
+ return this.onEnvironmentChanges();
+ }
+
+ protected async afterBind() {
+ this.onEnvironmentChanges();
+ await super.afterBind();
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["id"];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ switch (attributeName) {
+ case "containerSelector":
+ this.setContainersStyle(this.scope.state);
+ break;
+ case "id":
+ this.initToggleButtonEventDispatcher();
+ break;
+ case "width":
+ this.style.width = newValue;
+ this.onStateChange();
+ this.setContainersStyle(this.scope.state);
+ break;
+ case "position":
+ this.initHostStyles();
+ this.onStateChange();
+ break;
+ case "mode":
+ this.onStateChange();
+ this.setContainersStyle(this.scope.state);
+ break;
+ case "autoHide":
+ case "autoShow":
+ this.setStateByEnvironment();
+ break;
+ default:
+ break;
+ }
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListeners();
+ this.hideBackdrop();
+ }
+
+ protected template(): ReturnType {
+ if (!hasChildNodesTrim(this)) {
+ console.warn(
+ "[tw-sidebar] No child elements found. Provide sidebar content as children.",
+ );
+ }
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-skeleton/tw-skeleton.component.html b/packages/tw/src/components/tw-skeleton/tw-skeleton.component.html
new file mode 100644
index 000000000..d7df82fa1
--- /dev/null
+++ b/packages/tw/src/components/tw-skeleton/tw-skeleton.component.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading...
+
diff --git a/packages/tw/src/components/tw-skeleton/tw-skeleton.component.ts b/packages/tw/src/components/tw-skeleton/tw-skeleton.component.ts
new file mode 100644
index 000000000..b7ef25ec0
--- /dev/null
+++ b/packages/tw/src/components/tw-skeleton/tw-skeleton.component.ts
@@ -0,0 +1,74 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-skeleton.component.html?raw";
+
+type SkeletonType = "text" | "circle" | "rect" | "card";
+
+interface Scope extends ScopeBase {
+ type: SkeletonType;
+ width: string;
+ height: string;
+ lines: number;
+ lineArray: number[];
+}
+
+export class TwSkeletonComponent extends Component {
+ public static tagName = "tw-skeleton";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["type", "width", "height", "lines"];
+ }
+
+ public scope: Scope = {
+ type: "text",
+ width: "",
+ height: "",
+ lines: 3,
+ lineArray: [0, 1, 2],
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwSkeletonComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "lines") {
+ this.updateLineArray();
+ }
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.updateLineArray();
+ }
+
+ protected updateLineArray() {
+ const count = Math.max(1, Number(this.scope.lines) || 3);
+ this.scope.lineArray = Array.from({ length: count }, (_, i) => i);
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-slide-video/tw-slide-video.component.html b/packages/tw/src/components/tw-slide-video/tw-slide-video.component.html
new file mode 100644
index 000000000..001be8601
--- /dev/null
+++ b/packages/tw/src/components/tw-slide-video/tw-slide-video.component.html
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-slide-video/tw-slide-video.component.ts b/packages/tw/src/components/tw-slide-video/tw-slide-video.component.ts
new file mode 100644
index 000000000..df0a3868b
--- /dev/null
+++ b/packages/tw/src/components/tw-slide-video/tw-slide-video.component.ts
@@ -0,0 +1,183 @@
+import { Component, TemplateFunction } from "@ribajs/core";
+import { TwSlideshowComponent } from "../tw-slideshow/tw-slideshow.component.js";
+import { TwSliderComponent } from "../tw-slider/tw-slider.component.js";
+import template from "./tw-slide-video.component.html?raw";
+
+interface Scope {
+ /** Video source URL */
+ src: string;
+ /** Video MIME type (e.g. "video/mp4") */
+ type: string;
+ /** Set to true to play the video automatically when the slide is active */
+ autoplayOnActive: boolean;
+ /** Whether the video should be muted */
+ muted: boolean;
+ /** Whether the video should loop */
+ loop: boolean;
+ /** Whether to show native video controls */
+ showControls: boolean;
+}
+
+/**
+ * A video element that auto-plays when its parent slide becomes active.
+ * Place inside a tw-slideshow or tw-slider slide.
+ */
+export class TwSlideVideoComponent extends Component {
+ public static tagName = "tw-slide-video";
+
+ public scope: Scope = {
+ src: "",
+ type: "video/mp4",
+ autoplayOnActive: true,
+ muted: true,
+ loop: true,
+ showControls: false,
+ };
+
+ protected slider: TwSliderComponent | TwSlideshowComponent | null = null;
+ protected videoEl: HTMLVideoElement | null = null;
+ protected slideEl: HTMLElement | null = null;
+
+ static get observedAttributes(): string[] {
+ return ["src", "type", "autoplay-on-active", "muted", "loop", "controls"];
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwSlideVideoComponent.observedAttributes);
+ this.onSlideEnd = this.onSlideEnd.bind(this);
+ }
+
+ protected onSlideEnd() {
+ this.debug("[TwSlideVideoComponent] onSlideEnd", this.slideEl?.classList);
+
+ if (this.scope.autoplayOnActive) {
+ this.playIfActive();
+ }
+ }
+
+ protected playIfActive() {
+ if (this.slideEl?.classList.contains("active")) {
+ if (this.videoEl) {
+ this.videoEl.play().catch((err) => {
+ this.debug("Autoplay prevented:", err);
+ });
+ }
+ } else {
+ if (this.videoEl) {
+ this.videoEl.pause();
+ }
+ }
+ }
+
+ protected async waitForUserInteraction(): Promise {
+ return new Promise((resolve) => {
+ const removeEventListeners = () => {
+ document.removeEventListener("click", clickHandler);
+ document.removeEventListener("scroll", scrollHandler);
+ document.removeEventListener("mousemove", mouseMoveHandler);
+ };
+
+ const clickHandler = () => {
+ removeEventListeners();
+ resolve("click");
+ };
+
+ const scrollHandler = () => {
+ removeEventListeners();
+ resolve("scroll");
+ };
+
+ const mouseMoveHandler = () => {
+ removeEventListeners();
+ resolve("mousemove");
+ };
+
+ document.addEventListener("click", clickHandler);
+ document.addEventListener("scroll", scrollHandler);
+ document.addEventListener("mousemove", mouseMoveHandler);
+ });
+ }
+
+ protected addEventListeners() {
+ this.slider?.addEventListener(
+ "scrollended",
+ this.onSlideEnd as EventListener,
+ );
+ }
+
+ protected removeEventListeners() {
+ this.slider?.removeEventListener(
+ "scrollended",
+ this.onSlideEnd as EventListener,
+ );
+ }
+
+ protected async beforeBind(): Promise {
+ await super.beforeBind();
+ this.addEventListeners();
+ }
+
+ protected async afterBind(): Promise {
+ this.videoEl = this.querySelector("video");
+
+ if (this.scope.autoplayOnActive) {
+ if (this.slideEl?.classList.contains("active")) {
+ await this.waitForUserInteraction();
+ if (this.slideEl?.classList.contains("active")) {
+ this.playIfActive();
+ }
+ }
+ }
+ await super.afterBind();
+ }
+
+ protected async beforeTemplate(): Promise {
+ this.slider =
+ this.closest(TwSlideshowComponent.tagName) ||
+ this.closest(TwSliderComponent.tagName);
+ this.slideEl = this.closest(".slide");
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["src"];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+
+ if (attributeName === "controls") {
+ this.scope.showControls = !!newValue;
+ }
+ }
+
+ protected disconnectedCallback() {
+ this.removeEventListeners();
+ super.disconnectedCallback();
+ }
+
+ protected template(): ReturnType {
+ // If the component already has children (e.g. a custom video element), skip the template
+ if (this.hasChildNodes()) {
+ const nonTemplateChildren = Array.from(this.childNodes).filter(
+ (n) =>
+ n.nodeType !== Node.COMMENT_NODE &&
+ (n as Element).tagName !== "TEMPLATE",
+ );
+ if (nonTemplateChildren.length > 0) {
+ return null;
+ }
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-slider/tw-slider.component.html b/packages/tw/src/components/tw-slider/tw-slider.component.html
new file mode 100644
index 000000000..2da3c99b9
--- /dev/null
+++ b/packages/tw/src/components/tw-slider/tw-slider.component.html
@@ -0,0 +1,45 @@
+
diff --git a/packages/tw/src/components/tw-slider/tw-slider.component.ts b/packages/tw/src/components/tw-slider/tw-slider.component.ts
new file mode 100644
index 000000000..dd0bb03e6
--- /dev/null
+++ b/packages/tw/src/components/tw-slider/tw-slider.component.ts
@@ -0,0 +1,1027 @@
+import { Component, TemplateFunction } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { scrollTo } from "@ribajs/utils/src/dom.js";
+import { debounce } from "@ribajs/utils/src/control";
+import {
+ Dragscroll,
+ DragscrollOptions,
+ ScrollPosition,
+ ScrollEventsService,
+ getScrollPosition,
+} from "@ribajs/extras";
+import template from "./tw-slider.component.html?raw";
+
+export interface TwSliderSlide {
+ active: boolean;
+ index: number;
+ el?: HTMLElement;
+}
+
+export interface TwSliderComponentScope {
+ // Options
+ slidesToScroll: number;
+ controls: boolean;
+ controlsPosition: string;
+ sticky: boolean;
+ indicators: boolean;
+ indicatorsPosition: string;
+ drag: boolean;
+ touchScroll: boolean;
+ angle: "horizontal" | "vertical";
+ infinite: boolean;
+ columns: number;
+
+ // States
+ items: TwSliderSlide[];
+ nextIndex: number;
+ prevIndex: number;
+ enableNextControl: boolean;
+ enablePrevControl: boolean;
+ showControls: boolean;
+ showIndicators: boolean;
+ activeSlides: number[];
+ isScrolling: boolean;
+ slideItemStyle: Record;
+ slideTemplate?: string;
+
+ // Template methods
+ next: TwSliderComponent["next"];
+ prev: TwSliderComponent["prev"];
+ goTo: TwSliderComponent["goTo"];
+ enableTouchScroll: TwSliderComponent["enableTouchScroll"];
+ disableTouchScroll: TwSliderComponent["disableTouchScroll"];
+
+ // Classes
+ controlsPositionClass: string;
+ indicatorsPositionClass: string;
+}
+
+const SLIDER_INNER_SELECTOR = ".slider-row";
+const SLIDES_SELECTOR = `${SLIDER_INNER_SELECTOR} .slide`;
+
+/**
+ * A horizontal content slider with drag/scroll support and optional
+ * indicators/controls. Simpler than the slideshow — intended for
+ * content items rather than full-bleed images.
+ *
+ * @event scrolling - Fires when the slider is scrolling
+ * @event scrollended - Fires when the slider has scrolled
+ */
+export class TwSliderComponent extends Component {
+ protected resizeObserver?: ResizeObserver;
+
+ protected get sliderInner() {
+ return this.querySelector(SLIDER_INNER_SELECTOR);
+ }
+
+ protected get slideElements() {
+ return Array.from(this.querySelectorAll(SLIDES_SELECTOR));
+ }
+
+ protected get controlsElements() {
+ return this.querySelectorAll(".slider-control-prev, .slider-control-next");
+ }
+
+ protected get indicatorsElement() {
+ return this.querySelector(".slider-indicators");
+ }
+
+ public static EVENTS = {
+ scrolling: "scrolling",
+ scrollended: "scrollended",
+ };
+
+ static get observedAttributes(): string[] {
+ return [
+ "items",
+ "slides-to-scroll",
+ "controls",
+ "controls-position",
+ "drag",
+ "sticky",
+ "indicators",
+ "indicators-position",
+ "infinite",
+ "columns",
+ ];
+ }
+
+ protected defaultScope: TwSliderComponentScope = {
+ // Options — slider defaults differ from slideshow
+ slidesToScroll: 1,
+ controls: true,
+ controlsPosition: "inside-middle",
+ sticky: false,
+ indicators: true,
+ indicatorsPosition: "outside-bottom",
+ drag: false,
+ touchScroll: true,
+ angle: "horizontal",
+ infinite: false,
+ columns: 0,
+
+ // States
+ items: [],
+ nextIndex: -1,
+ prevIndex: -1,
+ enableNextControl: false,
+ enablePrevControl: false,
+ showControls: false,
+ showIndicators: false,
+ activeSlides: [],
+ isScrolling: false,
+ slideItemStyle: {},
+
+ // Template methods
+ next: this.next.bind(this),
+ prev: this.prev.bind(this),
+ goTo: this.goTo.bind(this),
+ enableTouchScroll: this.enableTouchScroll.bind(this),
+ disableTouchScroll: this.disableTouchScroll.bind(this),
+
+ // Classes
+ controlsPositionClass: "",
+ indicatorsPositionClass: "",
+ };
+
+ public static tagName = "tw-slider";
+
+ protected autobind = true;
+
+ protected dragscrollService?: Dragscroll;
+
+ protected scrollEventsService?: ScrollEventsService;
+
+ protected routerEvents = new EventDispatcher("main");
+
+ public scope: TwSliderComponentScope = {
+ ...this.defaultScope,
+ };
+
+ constructor() {
+ super();
+ this.onViewChanges = this.onViewChanges.bind(this);
+ this.onVisibilityChanged = this.onVisibilityChanged.bind(this);
+ this.onScroll = this.onScroll.bind(this);
+ this.onScrollEnd = this.onScrollEnd.bind(this);
+ }
+
+ /**
+ * Go to next slide
+ */
+ public next() {
+ this.scrollToNextSlide();
+ }
+
+ /**
+ * Go to prev slide
+ */
+ public prev() {
+ this.scrollToPrevSlide();
+ }
+
+ /**
+ * Go to slide by index
+ */
+ public goTo(index: number, fromRight = false) {
+ if (index === -1 && !this.scope.infinite) {
+ console.warn(`End of slider reached!`);
+ return -1;
+ }
+
+ if (index !== -1 && fromRight && this.scope.activeSlides.length > 1) {
+ index = index - this.scope.activeSlides.length + 1;
+ if (index < 0) {
+ index = 0;
+ }
+ }
+
+ const item = this.scope.items[index];
+
+ if (!item || !this.sliderInner || !item.el) {
+ console.warn(`Slide element with index "${index}" not found!`);
+ return -1;
+ }
+
+ scrollTo(item.el, 0, this.sliderInner, this.scope.angle);
+ return index;
+ }
+
+ protected getNextIndex(currentActive: number) {
+ let nextIndex = currentActive + this.scope.slidesToScroll;
+
+ if (nextIndex > this.scope.items.length - 1) {
+ if (this.scope.infinite) {
+ nextIndex = nextIndex - this.scope.items.length;
+ } else {
+ return -1;
+ }
+ }
+ return nextIndex;
+ }
+
+ protected getPrevIndex(currentActive: number) {
+ let prevIndex = currentActive - this.scope.slidesToScroll;
+
+ if (prevIndex < 0) {
+ if (this.scope.infinite) {
+ prevIndex = this.scope.items.length - 1 + (prevIndex + 1);
+ } else {
+ return -1;
+ }
+ }
+ return prevIndex;
+ }
+
+ protected scrollToNextSlide() {
+ if (this.scope.isScrolling) {
+ this.scope.nextIndex = this.getNextIndex(this.scope.nextIndex);
+ this.updateControls();
+ }
+ return this.goTo(this.scope.nextIndex, true);
+ }
+
+ protected scrollToPrevSlide() {
+ if (this.scope.isScrolling) {
+ this.scope.prevIndex = this.getPrevIndex(this.scope.prevIndex);
+ this.updateControls();
+ }
+ return this.goTo(this.scope.prevIndex, false);
+ }
+
+ protected initOptions() {
+ this.setOptions();
+ }
+
+ protected setOptions() {
+ if (this.scope.drag) {
+ this.enableDesktopDragscroll();
+ } else {
+ this.disableDesktopDragscroll();
+ }
+ if (this.scope.touchScroll) {
+ this.enableTouchScroll();
+ } else {
+ this.disableTouchScroll();
+ }
+ this.updateColumns();
+ this.setControlsOptions();
+ this.setIndicatorsOptions();
+ }
+
+ protected updateColumns() {
+ this.scope.slideItemStyle ||= {};
+
+ if (this.scope.columns > 0) {
+ this.scope.slideItemStyle.flex = `0 0 ${100 / this.scope.columns}%`;
+ } else {
+ this.scope.slideItemStyle.flex = "";
+ }
+ }
+
+ protected setControlsOptions() {
+ const position = this.scope.controlsPosition?.split("-");
+ if (this.scope.controls && position.length === 2) {
+ this.scope.controlsPositionClass = `control-${position[0]} control-${position[1]}`;
+ } else {
+ this.scope.controlsPositionClass = "";
+ }
+ }
+
+ protected setIndicatorsOptions() {
+ const positions = this.scope.indicatorsPosition?.split("-");
+ if (this.scope.indicators && positions.length === 2) {
+ this.scope.indicatorsPositionClass = `indicators-${positions[0]} indicators-${positions[1]}`;
+ } else {
+ this.scope.indicatorsPositionClass = "";
+ }
+ }
+
+ protected _onViewChanges() {
+ this.debug("onViewChanges");
+ if (!this.scope.items?.length || !this.slideElements?.length) {
+ return;
+ }
+ try {
+ this.updateSlides();
+ } catch (error: any) {
+ this.throw(error);
+ }
+ }
+
+ protected onViewChanges = debounce(this._onViewChanges.bind(this));
+
+ protected onVisibilityChanged(event: CustomEvent) {
+ if (event.detail.visible) {
+ this.dragscrollService?.checkDraggable();
+ }
+ }
+
+ protected onScroll(
+ event: CustomEvent<{
+ startPosition: ScrollPosition | null;
+ currentPosition: ScrollPosition;
+ direction: string;
+ }>,
+ ) {
+ this.scope.isScrolling = true;
+ this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
+ }
+
+ protected onScrollEnd(
+ event: CustomEvent<{
+ startPosition: ScrollPosition | null;
+ endPosition: ScrollPosition | null;
+ direction: string;
+ }>,
+ ) {
+ this.scope.isScrolling = false;
+ if (!this.scope.items?.length) {
+ return;
+ }
+
+ if (event.detail.direction === "none") {
+ return;
+ }
+
+ this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
+
+ try {
+ this.updateSlides();
+ } catch (error: any) {
+ this.throw(error);
+ }
+ }
+
+ protected connectedCallback() {
+ if (this.scope.items.length || this.scope.slideTemplate) {
+ this.updateItems();
+ } else {
+ this.initItemsByChildren();
+ }
+
+ super.connectedCallback();
+ this.init(TwSliderComponent.observedAttributes);
+ this.addEventListeners();
+ }
+
+ protected onKeyDown = (event: KeyboardEvent) => {
+ const isHorizontal = this.scope.angle === "horizontal";
+ const prevKey = isHorizontal ? "ArrowLeft" : "ArrowUp";
+ const nextKey = isHorizontal ? "ArrowRight" : "ArrowDown";
+ if (event.key === prevKey) {
+ event.preventDefault();
+ this.prev();
+ } else if (event.key === nextKey) {
+ event.preventDefault();
+ this.next();
+ }
+ };
+
+ protected addEventListeners() {
+ this.routerEvents.on("newPageReady", this.onViewChanges);
+
+ if (window.ResizeObserver) {
+ this.resizeObserver = new window.ResizeObserver(this.onViewChanges);
+ this.resizeObserver?.observe(this);
+ }
+
+ window.addEventListener("resize", this.onViewChanges, { passive: true });
+
+ this.addEventListener(
+ "visibility-changed" as any,
+ this.onVisibilityChanged,
+ );
+
+ this.sliderInner?.addEventListener(
+ TwSliderComponent.EVENTS.scrolling,
+ this.onScroll as EventListener,
+ { passive: true },
+ );
+ this.sliderInner?.addEventListener(
+ TwSliderComponent.EVENTS.scrollended,
+ this.onScrollEnd as EventListener,
+ { passive: true },
+ );
+
+ // Keyboard navigation — make the inner focusable and listen for arrow keys
+ if (this.sliderInner) {
+ if (!this.sliderInner.hasAttribute("tabindex")) {
+ this.sliderInner.setAttribute("tabindex", "0");
+ }
+ this.sliderInner.addEventListener("keydown", this.onKeyDown);
+ }
+ }
+
+ protected removeEventListeners() {
+ this.routerEvents.off("newPageReady", this.onViewChanges, this);
+
+ window.removeEventListener("resize", this.onViewChanges);
+
+ this.resizeObserver?.unobserve(this);
+
+ this.removeEventListener(
+ "visibility-changed" as any,
+ this.onVisibilityChanged,
+ );
+
+ this.sliderInner?.removeEventListener(
+ TwSliderComponent.EVENTS.scrolling,
+ this.onScroll as EventListener,
+ );
+ this.sliderInner?.removeEventListener(
+ TwSliderComponent.EVENTS.scrollended,
+ this.onScrollEnd as EventListener,
+ );
+ this.sliderInner?.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ protected initAll() {
+ this.initSliderInner();
+ this.initOptions();
+ this.addEventListeners();
+ this.updateSlides();
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.validateItems();
+ }
+
+ protected async afterBind() {
+ if (this._usePassthrough) {
+ this.disableDesktopDragscroll();
+ this.wrapInSliderInner();
+ this.injectControls();
+ this.injectIndicators();
+ }
+ this.initAll();
+ this.updateItems();
+ if (this.scope.drag) {
+ this.enableDesktopDragscroll();
+ }
+ this.classList.add(`${TwSliderComponent.tagName}-ready`);
+ await super.afterBind();
+ }
+
+ /** Whether we're using pass-through mode (consumer provided slide children) */
+ protected _usePassthrough = false;
+
+ /**
+ * In pass-through mode, wrap existing content in a .slider-inner container
+ * so controls/indicators can be positioned correctly.
+ */
+ protected wrapInSliderInner() {
+ if (this.querySelector(".slider-inner")) return;
+ const wrapper = document.createElement("div");
+ wrapper.className = "slider-inner relative overflow-hidden w-full";
+ while (this.firstChild) {
+ wrapper.appendChild(this.firstChild);
+ }
+ this.appendChild(wrapper);
+ }
+
+ protected createChevronSvg(direction: "left" | "right"): SVGElement {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("class", "w-4 h-4");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("stroke", "currentColor");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ path.setAttribute("stroke-linecap", "round");
+ path.setAttribute("stroke-linejoin", "round");
+ path.setAttribute("stroke-width", "2");
+ path.setAttribute(
+ "d",
+ direction === "left" ? "M15 19l-7-7 7-7" : "M9 5l7 7-7 7",
+ );
+ svg.appendChild(path);
+ return svg;
+ }
+
+ protected _passthroughPrevBtn?: HTMLButtonElement;
+ protected _passthroughNextBtn?: HTMLButtonElement;
+
+ protected injectControls() {
+ if (!this.scope.controls) return;
+ const inner = this.querySelector(".slider-inner");
+ if (!inner || inner.querySelector(".slider-controls")) return;
+
+ const controlsDiv = document.createElement("div");
+ controlsDiv.className =
+ "slider-controls absolute inset-0 flex items-center justify-between pointer-events-none z-10";
+
+ const prevBtn = document.createElement("button");
+ prevBtn.className =
+ "slider-control-prev pointer-events-auto flex items-center justify-center w-8 h-8 rounded-full bg-gray-800/60 text-white hover:bg-gray-800/80 transition-colors disabled:opacity-30 disabled:cursor-not-allowed ml-1";
+ prevBtn.setAttribute("aria-label", "Previous");
+ prevBtn.appendChild(this.createChevronSvg("left"));
+ prevBtn.addEventListener("click", () => this.prev());
+
+ const nextBtn = document.createElement("button");
+ nextBtn.className =
+ "slider-control-next pointer-events-auto flex items-center justify-center w-8 h-8 rounded-full bg-gray-800/60 text-white hover:bg-gray-800/80 transition-colors disabled:opacity-30 disabled:cursor-not-allowed mr-1";
+ nextBtn.setAttribute("aria-label", "Next");
+ nextBtn.appendChild(this.createChevronSvg("right"));
+ nextBtn.addEventListener("click", () => this.next());
+
+ controlsDiv.appendChild(prevBtn);
+ controlsDiv.appendChild(nextBtn);
+ inner.appendChild(controlsDiv);
+
+ this._passthroughPrevBtn = prevBtn;
+ this._passthroughNextBtn = nextBtn;
+
+ // Apply initial enabled state (subsequent updates happen via updateControls)
+ this.syncPassthroughControls();
+ }
+
+ protected injectIndicators() {
+ if (!this.scope.indicators) return;
+ const inner = this.querySelector(".slider-inner");
+ if (!inner || inner.querySelector(".slider-indicators")) return;
+
+ const indicatorsDiv = document.createElement("div");
+ indicatorsDiv.className =
+ "slider-indicators flex justify-center gap-1.5 mt-3";
+
+ inner.appendChild(indicatorsDiv);
+
+ const origUpdateIndicators = this.updateIndicators.bind(this);
+ this.updateIndicators = () => {
+ origUpdateIndicators();
+ this.renderPassthroughIndicators(indicatorsDiv);
+ };
+ }
+
+ protected renderPassthroughIndicators(container: HTMLElement) {
+ if (!this.scope.showIndicators) {
+ container.style.display = "none";
+ return;
+ }
+ container.style.display = "";
+
+ while (container.children.length > this.scope.items.length) {
+ container.lastChild?.remove();
+ }
+ while (container.children.length < this.scope.items.length) {
+ const dot = document.createElement("button");
+ dot.className = "w-2 h-2 rounded-full transition-colors";
+ dot.setAttribute("aria-label", "Go to item");
+ const idx = container.children.length;
+ dot.addEventListener("click", () => this.goTo(idx));
+ container.appendChild(dot);
+ }
+
+ Array.from(container.children).forEach((dot, i) => {
+ const isActive = this.scope.activeSlides.includes(i);
+ dot.classList.toggle("bg-gray-800", isActive);
+ dot.classList.toggle("bg-gray-300", !isActive);
+ });
+ }
+
+ protected async afterAllBind() {
+ this.updateItems();
+ await super.afterAllBind();
+ }
+
+ protected initSliderInner() {
+ if (!this.sliderInner) {
+ this.throw(new Error("Can't init slider inner!"));
+ return;
+ }
+
+ this.scrollEventsService = new ScrollEventsService(this.sliderInner);
+ }
+
+ protected preventDragstart = (e: Event) => e.preventDefault();
+
+ protected onDragMouseDown = () => {
+ // Disable scroll-snap and smooth scrolling during drag so that
+ // rapid programmatic scrollLeft changes apply immediately.
+ if (this.sliderInner) {
+ this.sliderInner.style.scrollSnapType = "none";
+ this.sliderInner.style.scrollBehavior = "auto";
+ }
+ };
+
+ protected onDragMouseUp = () => {
+ if (this.sliderInner) {
+ this.sliderInner.style.scrollSnapType = "";
+ this.sliderInner.style.scrollBehavior = "";
+ }
+ };
+
+ protected enableDesktopDragscroll() {
+ if (!this.dragscrollService) {
+ if (!this.sliderInner) {
+ return;
+ }
+ const dragscrollOptions: DragscrollOptions = { detectGlobalMove: true };
+ this.dragscrollService = new Dragscroll(
+ this.sliderInner,
+ dragscrollOptions,
+ );
+ }
+
+ this.sliderInner?.addEventListener("mousedown", this.onDragMouseDown);
+ window.addEventListener("mouseup", this.onDragMouseUp);
+
+ // Prevent ghost images when dragging
+ this.addEventListener("dragstart", this.preventDragstart);
+ this.classList.add("drag-none");
+ const draggables = this.querySelectorAll("img, video, svg, a");
+ draggables.forEach((el) => {
+ el.setAttribute("draggable", "false");
+ el.classList.add("drag-none");
+ });
+ }
+
+ protected disableDesktopDragscroll() {
+ if (this.dragscrollService) {
+ this.dragscrollService.destroy();
+ this.dragscrollService = undefined;
+ }
+ this.sliderInner?.removeEventListener("mousedown", this.onDragMouseDown);
+ window.removeEventListener("mouseup", this.onDragMouseUp);
+ this.removeEventListener("dragstart", this.preventDragstart);
+ this.classList.remove("drag-none");
+ const draggables = this.querySelectorAll("img, video, svg, a");
+ draggables.forEach((el) => {
+ el.removeAttribute("draggable");
+ el.classList.remove("drag-none");
+ });
+ }
+
+ public enableTouchScroll() {
+ this.classList.remove("touchscroll-disabled");
+ }
+
+ public disableTouchScroll() {
+ this.classList.add("touchscroll-disabled");
+ }
+
+ protected validateItems() {
+ if (!this.scope.items) {
+ this.throw(new Error("No items to validate!"));
+ return;
+ }
+ for (let i = 0; i < this.scope.items.length; i++) {
+ const item = this.scope.items[i];
+ item.index = item.index || i;
+ item.active = item.active || false;
+ }
+ }
+
+ public updateItems() {
+ let hasChange = false;
+ const slideEls = this.slideElements;
+
+ if (!this.scope.slideTemplate) {
+ slideEls.forEach((slideEl, index) => {
+ const exists = this.scope.items.find((item) => item.el === slideEl);
+ if (!exists) {
+ this.addItemByElement(slideEl, index);
+ hasChange = true;
+ }
+ });
+
+ for (const item of this.scope.items) {
+ const exists = slideEls.find((slideEl) => slideEl === item.el);
+ if (!exists) {
+ this.removeItem(item.index, false);
+ hasChange = true;
+ }
+ }
+ }
+
+ if (hasChange) {
+ this.updateItemIndexes();
+ this.updateSlides();
+ }
+
+ return hasChange;
+ }
+
+ protected removeItem(index: number, updateIndex = true) {
+ const item = this.scope.items[index];
+ if (!item) {
+ return;
+ }
+ item.el?.remove();
+ this.scope.items.splice(index, 1);
+ if (updateIndex) this.updateItemIndexes();
+ }
+
+ protected updateItemIndexes() {
+ for (let i = 0; i < this.scope.items.length; i++) {
+ this.scope.items[i].index = i;
+ }
+ }
+
+ protected addItemByElement(slideElement: HTMLElement, index: number) {
+ slideElement.setAttribute("data-index", index.toString());
+ const attributes: TwSliderSlide = {
+ active: false,
+ index,
+ el: slideElement,
+ };
+ this.scope.items.push(attributes);
+ }
+
+ protected initItemsByChildren() {
+ if (!this.slideElements) {
+ this.throw(
+ new Error(
+ "Can't add items by children because no slide children are found!",
+ ),
+ );
+ }
+ this.scope.items = [];
+ this.slideElements.forEach(this.addItemByElement.bind(this));
+ }
+
+ protected getScrollPosition(): ScrollPosition | null {
+ if (!this.sliderInner) {
+ return null;
+ }
+ return getScrollPosition(this.sliderInner);
+ }
+
+ protected setAllSlidesInactive(excludeIndex = -1) {
+ for (const item of this.scope.items) {
+ if (item.index !== excludeIndex) {
+ item.active = false;
+ item.el?.classList.remove("active");
+ }
+ }
+ }
+
+ protected setSlideActive(index: number) {
+ if (index === -1 || !this.scope.items?.length) {
+ console.warn(new Error("Most centered slide not found!"));
+ index = 0;
+ }
+ if (!this.scope.items?.[index]) {
+ index = 0;
+ }
+ if (!this.scope.items?.[index]) {
+ this.throw(new Error("Slide item to set active, not found!"));
+ return 0;
+ }
+
+ const item = this.scope.items[index];
+ item.active = true;
+ item.el?.classList.add("active");
+ }
+
+ protected setSlidesActive(slides: number[]) {
+ this.setAllSlidesInactive();
+ for (const slideIndex of slides) {
+ this.setSlideActive(slideIndex);
+ }
+ }
+
+ protected isScrollable() {
+ if (!this.sliderInner) {
+ return false;
+ }
+
+ return this.scope.angle === "horizontal"
+ ? this.sliderInner.scrollWidth > this.sliderInner.clientWidth
+ : this.sliderInner.scrollHeight > this.sliderInner.clientHeight;
+ }
+
+ protected getSlideElementByIndex(index: number) {
+ if (!this.sliderInner) {
+ return undefined;
+ }
+ return this.sliderInner.querySelector(
+ `[data-index="${index}"]`,
+ ) as HTMLElement;
+ }
+
+ protected isSlideVisible(item: TwSliderSlide, _offset: number) {
+ if (!this.sliderInner) {
+ return false;
+ }
+ const containerRect = this.sliderInner.getBoundingClientRect();
+ item.el ||= this.getSlideElementByIndex(item.index);
+ const slideEl = item.el;
+ if (!slideEl) {
+ console.warn("Slide element not found!");
+ return false;
+ }
+ const slideRect = slideEl.getBoundingClientRect();
+
+ // Count as "visible" if more than 50% of the slide overlaps the container.
+ // This is tolerant to gaps, padding and rounding errors.
+ if (this.scope.angle === "horizontal") {
+ const visibleLeft = Math.max(slideRect.left, containerRect.left);
+ const visibleRight = Math.min(slideRect.right, containerRect.right);
+ const visibleWidth = Math.max(0, visibleRight - visibleLeft);
+ return visibleWidth / slideRect.width > 0.5;
+ } else {
+ const visibleTop = Math.max(slideRect.top, containerRect.top);
+ const visibleBottom = Math.min(slideRect.bottom, containerRect.bottom);
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ return visibleHeight / slideRect.height > 0.5;
+ }
+ }
+
+ protected getVisibleSlides(offset: number) {
+ const activeSlides: number[] = [];
+
+ if (!this.scope.items?.length) {
+ return activeSlides;
+ }
+
+ for (const item of this.scope.items) {
+ if (this.isSlideVisible(item, offset)) {
+ activeSlides.push(item.index);
+ }
+ }
+
+ return activeSlides.sort((a, b) => a - b);
+ }
+
+ protected setVisibleSlidesActive(offset: number) {
+ this.setAllSlidesInactive();
+ const activeSlides = this.getVisibleSlides(offset);
+ this.setSlidesActive(activeSlides);
+ return activeSlides;
+ }
+
+ updateActiveSlides(offset = 8) {
+ const activeSlides = this.setVisibleSlidesActive(offset);
+ const firstIndex = activeSlides[0] || 0;
+ const lastIndex = activeSlides[activeSlides.length - 1] || 0;
+ const prevIndex = this.getPrevIndex(firstIndex);
+ const nextIndex = this.getNextIndex(lastIndex);
+ return { firstIndex, lastIndex, activeSlides, prevIndex, nextIndex };
+ }
+
+ protected updateSlides(offset = 8, isRetry = false): number[] {
+ if (!this.scope.items.length) {
+ return [];
+ }
+ const { activeSlides, firstIndex, prevIndex, nextIndex } =
+ this.updateActiveSlides(offset);
+
+ if (!activeSlides.length && !isRetry) {
+ let fallbackOffset = offset * 2;
+ if (this.scope.angle === "horizontal") {
+ const slideWidth = this.scope.items[0]?.el?.clientWidth || 0;
+ if (slideWidth) {
+ fallbackOffset = Math.round(slideWidth / 2 - 0.5);
+ }
+ } else {
+ const slideHeight = this.scope.items[0]?.el?.clientHeight || 0;
+ if (slideHeight) {
+ fallbackOffset = Math.round(slideHeight / 2 - 0.5);
+ }
+ }
+ return this.updateSlides(fallbackOffset, true);
+ }
+
+ this.scope.activeSlides = activeSlides;
+ this.scope.prevIndex = prevIndex;
+ this.scope.nextIndex = nextIndex;
+
+ this.updateControls();
+ this.updateIndicators();
+
+ if (this.scope.sticky) {
+ this.goTo(firstIndex);
+ }
+
+ return activeSlides;
+ }
+
+ protected updateControls() {
+ const isScrollable = this.isScrollable();
+ this.scope.showControls =
+ this.scope.controls && isScrollable && this.scope.items.length > 1;
+
+ if (this.scope.infinite) {
+ this.scope.enableNextControl = true;
+ this.scope.enablePrevControl = true;
+ } else {
+ this.scope.enableNextControl =
+ isScrollable &&
+ this.scope.nextIndex !== -1 &&
+ this.scope.nextIndex <= this.scope.items.length - 1;
+ this.scope.enablePrevControl =
+ isScrollable &&
+ this.scope.prevIndex !== -1 &&
+ this.scope.prevIndex >= 0;
+ }
+
+ this.syncPassthroughControls();
+ }
+
+ /**
+ * Hook for pass-through mode: keeps the injected DOM buttons in sync with
+ * the current enable*Control state. No-op unless `injectControls()` has run.
+ */
+ protected syncPassthroughControls() {
+ if (this._passthroughPrevBtn) {
+ this._passthroughPrevBtn.disabled = !this.scope.enablePrevControl;
+ }
+ if (this._passthroughNextBtn) {
+ this._passthroughNextBtn.disabled = !this.scope.enableNextControl;
+ }
+ }
+
+ protected updateIndicators() {
+ const isScrollable = this.isScrollable();
+ this.scope.showIndicators =
+ this.scope.indicators && isScrollable && this.scope.items.length > 1;
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+
+ if (attributeName === "items") {
+ this.validateItems();
+ }
+
+ if (attributeName === "drag") {
+ if (this.scope.drag) {
+ this.enableDesktopDragscroll();
+ } else {
+ this.disableDesktopDragscroll();
+ }
+ }
+
+ if (attributeName === "touchScroll") {
+ if (this.scope.touchScroll) {
+ this.enableTouchScroll();
+ } else {
+ this.disableTouchScroll();
+ }
+ }
+
+ if (attributeName === "controls" || attributeName === "controlsPosition") {
+ this.setControlsOptions();
+ }
+
+ if (
+ attributeName === "indicators" ||
+ attributeName === "indicatorsPosition"
+ ) {
+ this.setIndicatorsOptions();
+ }
+
+ if (attributeName === "columns") {
+ this.updateColumns();
+ }
+ }
+
+ protected disconnectedCallback() {
+ this.removeEventListeners();
+ }
+
+ protected async beforeTemplate(): Promise {
+ const templates = Array.from(this.querySelectorAll("template"));
+ for (const tpl of templates) {
+ const type = tpl.getAttribute("type");
+ switch (type) {
+ case "slide-item":
+ this.scope.slideTemplate =
+ tpl.content.children.item(0)?.outerHTML || undefined;
+ this.debug("Slide template found!", this.scope.slideTemplate);
+ break;
+ default:
+ console.warn(`Unknown template type: ${type}`, tpl);
+ break;
+ }
+ }
+ }
+
+ protected template(): ReturnType {
+ // Pass-through mode: consumer provided their own .slider-row / .slide markup
+ if (
+ this.querySelector(SLIDER_INNER_SELECTOR) ||
+ this.querySelector(SLIDES_SELECTOR)
+ ) {
+ this._usePassthrough = true;
+ return null;
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-slideshow/tw-slideshow.component.html b/packages/tw/src/components/tw-slideshow/tw-slideshow.component.html
new file mode 100644
index 000000000..202b18ea6
--- /dev/null
+++ b/packages/tw/src/components/tw-slideshow/tw-slideshow.component.html
@@ -0,0 +1,46 @@
+
diff --git a/packages/tw/src/components/tw-slideshow/tw-slideshow.component.ts b/packages/tw/src/components/tw-slideshow/tw-slideshow.component.ts
new file mode 100644
index 000000000..4ccc65d1a
--- /dev/null
+++ b/packages/tw/src/components/tw-slideshow/tw-slideshow.component.ts
@@ -0,0 +1,1087 @@
+import { Component, TemplateFunction } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { scrollTo } from "@ribajs/utils/src/dom.js";
+import { debounce } from "@ribajs/utils/src/control";
+import {
+ Dragscroll,
+ DragscrollOptions,
+ ScrollPosition,
+ ScrollEventsService,
+ getScrollPosition,
+} from "@ribajs/extras";
+import template from "./tw-slideshow.component.html?raw";
+
+export interface TwSlideshowSlide {
+ active: boolean;
+ index: number;
+ el?: HTMLElement;
+}
+
+export interface TwSlideshowComponentScope {
+ // Options
+ slidesToScroll: number;
+ controls: boolean;
+ controlsPosition: string;
+ sticky: boolean;
+ indicators: boolean;
+ indicatorsPosition: string;
+ drag: boolean;
+ touchScroll: boolean;
+ angle: "horizontal" | "vertical";
+ infinite: boolean;
+ columns: number;
+
+ // States
+ items: TwSlideshowSlide[];
+ nextIndex: number;
+ prevIndex: number;
+ enableNextControl: boolean;
+ enablePrevControl: boolean;
+ showControls: boolean;
+ showIndicators: boolean;
+ activeSlides: number[];
+ isScrolling: boolean;
+ slideItemStyle: Record;
+ slideTemplate?: string;
+
+ // Template methods
+ next: TwSlideshowComponent["next"];
+ prev: TwSlideshowComponent["prev"];
+ goTo: TwSlideshowComponent["goTo"];
+ enableTouchScroll: TwSlideshowComponent["enableTouchScroll"];
+ disableTouchScroll: TwSlideshowComponent["disableTouchScroll"];
+
+ // Classes
+ controlsPositionClass: string;
+ indicatorsPositionClass: string;
+}
+
+const SLIDESHOW_INNER_SELECTOR = ".slideshow-row";
+const SLIDES_SELECTOR = `${SLIDESHOW_INNER_SELECTOR} .slide`;
+
+/**
+ * A full-featured slideshow/carousel component with drag, touch, auto-scroll,
+ * indicators, and controls. Supports `` children.
+ *
+ * @event scrolling - Fires when the slideshow is scrolling
+ * @event scrollended - Fires when the slideshow has scrolled
+ */
+export class TwSlideshowComponent extends Component {
+ protected resizeObserver?: ResizeObserver;
+
+ protected get slideshowInner() {
+ return this.querySelector(SLIDESHOW_INNER_SELECTOR);
+ }
+
+ protected get slideElements() {
+ return Array.from(this.querySelectorAll(SLIDES_SELECTOR));
+ }
+
+ protected get controlsElements() {
+ return this.querySelectorAll(
+ ".slideshow-control-prev, .slideshow-control-next",
+ );
+ }
+
+ protected get indicatorsElement() {
+ return this.querySelector(".slideshow-indicators");
+ }
+
+ public static EVENTS = {
+ scrolling: "scrolling",
+ scrollended: "scrollended",
+ };
+
+ static get observedAttributes(): string[] {
+ return [
+ "items",
+ "slides-to-scroll",
+ "controls",
+ "controls-position",
+ "drag",
+ "angle",
+ "sticky",
+ "indicators",
+ "indicators-position",
+ "infinite",
+ "columns",
+ ];
+ }
+
+ protected defaultScope: TwSlideshowComponentScope = {
+ // Options
+ slidesToScroll: 1,
+ controls: true,
+ controlsPosition: "inside-middle",
+ sticky: false,
+ indicators: true,
+ indicatorsPosition: "inside-bottom",
+ drag: true,
+ touchScroll: true,
+ angle: "horizontal",
+ infinite: false,
+ columns: 0,
+
+ // States
+ items: [],
+ nextIndex: -1,
+ prevIndex: -1,
+ enableNextControl: false,
+ enablePrevControl: false,
+ showControls: false,
+ showIndicators: false,
+ activeSlides: [],
+ isScrolling: false,
+ slideItemStyle: {},
+
+ // Template methods
+ next: this.next.bind(this),
+ prev: this.prev.bind(this),
+ goTo: this.goTo.bind(this),
+ enableTouchScroll: this.enableTouchScroll.bind(this),
+ disableTouchScroll: this.disableTouchScroll.bind(this),
+
+ // Classes
+ controlsPositionClass: "",
+ indicatorsPositionClass: "",
+ };
+
+ public static tagName = "tw-slideshow";
+
+ protected autobind = true;
+
+ protected dragscrollService?: Dragscroll;
+
+ protected scrollEventsService?: ScrollEventsService;
+
+ protected routerEvents = new EventDispatcher("main");
+
+ public scope: TwSlideshowComponentScope = {
+ ...this.defaultScope,
+ };
+
+ constructor() {
+ super();
+ this.onViewChanges = this.onViewChanges.bind(this);
+ this.onVisibilityChanged = this.onVisibilityChanged.bind(this);
+ this.onScroll = this.onScroll.bind(this);
+ this.onScrollEnd = this.onScrollEnd.bind(this);
+ }
+
+ /**
+ * Go to next slide
+ */
+ public next() {
+ this.scrollToNextSlide();
+ }
+
+ /**
+ * Go to prev slide
+ */
+ public prev() {
+ this.scrollToPrevSlide();
+ }
+
+ /**
+ * Go to slide by index
+ * @param index The index of the slide you want to go to
+ * @param fromRight If true, the index is calculated from the right side of the slideshow
+ * @returns The index of the slide you went to or -1 if the end is reached
+ */
+ public goTo(index: number, fromRight = false) {
+ if (index === -1 && !this.scope.infinite) {
+ console.warn(`End of slideshow reached!`);
+ return -1;
+ }
+
+ if (index !== -1 && fromRight && this.scope.activeSlides.length > 1) {
+ index = index - this.scope.activeSlides.length + 1;
+ if (index < 0) {
+ index = 0;
+ }
+ }
+
+ const item = this.scope.items[index];
+
+ if (!item || !this.slideshowInner || !item.el) {
+ console.warn(`Slide element with index "${index}" not found!`);
+ return -1;
+ }
+
+ scrollTo(item.el, 0, this.slideshowInner, this.scope.angle);
+ return index;
+ }
+
+ /**
+ * Calculate the next index to scroll to
+ */
+ protected getNextIndex(currentActive: number) {
+ let nextIndex = currentActive + this.scope.slidesToScroll;
+
+ if (nextIndex > this.scope.items.length - 1) {
+ if (this.scope.infinite) {
+ nextIndex = nextIndex - this.scope.items.length;
+ } else {
+ return -1;
+ }
+ }
+ return nextIndex;
+ }
+
+ /**
+ * Calculate the previous index to scroll to
+ */
+ protected getPrevIndex(currentActive: number) {
+ let prevIndex = currentActive - this.scope.slidesToScroll;
+
+ if (prevIndex < 0) {
+ if (this.scope.infinite) {
+ prevIndex = this.scope.items.length - 1 + (prevIndex + 1);
+ } else {
+ return -1;
+ }
+ }
+ return prevIndex;
+ }
+
+ protected scrollToNextSlide() {
+ if (this.scope.isScrolling) {
+ this.scope.nextIndex = this.getNextIndex(this.scope.nextIndex);
+ this.updateControls();
+ }
+ return this.goTo(this.scope.nextIndex, true);
+ }
+
+ protected scrollToPrevSlide() {
+ if (this.scope.isScrolling) {
+ this.scope.prevIndex = this.getPrevIndex(this.scope.prevIndex);
+ this.updateControls();
+ }
+ return this.goTo(this.scope.prevIndex, false);
+ }
+
+ protected initOptions() {
+ this.setOptions();
+ }
+
+ protected setOptions() {
+ if (this.scope.drag) {
+ this.enableDesktopDragscroll();
+ } else {
+ this.disableDesktopDragscroll();
+ }
+ if (this.scope.touchScroll) {
+ this.enableTouchScroll();
+ } else {
+ this.disableTouchScroll();
+ }
+ this.updateColumns();
+ this.setControlsOptions();
+ this.setIndicatorsOptions();
+ }
+
+ protected updateColumns() {
+ this.scope.slideItemStyle ||= {};
+
+ if (this.scope.columns > 0) {
+ this.scope.slideItemStyle.flex = `0 0 ${100 / this.scope.columns}%`;
+ } else {
+ this.scope.slideItemStyle.flex = "";
+ }
+ }
+
+ protected setControlsOptions() {
+ const position = this.scope.controlsPosition?.split("-");
+ if (this.scope.controls && position.length === 2) {
+ this.scope.controlsPositionClass = `control-${position[0]} control-${position[1]}`;
+ } else {
+ this.scope.controlsPositionClass = "";
+ }
+ }
+
+ protected setIndicatorsOptions() {
+ const positions = this.scope.indicatorsPosition?.split("-");
+ if (this.scope.indicators && positions.length === 2) {
+ this.scope.indicatorsPositionClass = `indicators-${positions[0]} indicators-${positions[1]}`;
+ } else {
+ this.scope.indicatorsPositionClass = "";
+ }
+ }
+
+ protected _onViewChanges() {
+ this.debug("onViewChanges");
+ if (!this.scope.items?.length || !this.slideElements?.length) {
+ return;
+ }
+ try {
+ this.updateSlides();
+ } catch (error: any) {
+ this.throw(error);
+ }
+ }
+
+ protected onViewChanges = debounce(this._onViewChanges.bind(this));
+
+ protected onVisibilityChanged(event: CustomEvent) {
+ if (event.detail.visible) {
+ this.dragscrollService?.checkDraggable();
+ }
+ }
+
+ protected onScroll(
+ event: CustomEvent<{
+ startPosition: ScrollPosition | null;
+ currentPosition: ScrollPosition;
+ direction: string;
+ }>,
+ ) {
+ this.scope.isScrolling = true;
+ this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
+ }
+
+ protected onScrollEnd(
+ event: CustomEvent<{
+ startPosition: ScrollPosition | null;
+ endPosition: ScrollPosition | null;
+ direction: string;
+ }>,
+ ) {
+ this.scope.isScrolling = false;
+ if (!this.scope.items?.length) {
+ return;
+ }
+
+ if (event.detail.direction === "none") {
+ return;
+ }
+
+ try {
+ // Update active slides BEFORE dispatching the event,
+ // so listeners (e.g. tw-slide-video) see the correct active state
+ this.updateSlides();
+ } catch (error: any) {
+ this.throw(error);
+ }
+
+ this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
+ }
+
+ protected connectedCallback() {
+ if (this.scope.items.length || this.scope.slideTemplate) {
+ this.updateItems();
+ } else {
+ this.initItemsByChildren();
+ }
+
+ super.connectedCallback();
+ this.init(TwSlideshowComponent.observedAttributes);
+ this.addEventListeners();
+ }
+
+ protected onKeyDown = (event: KeyboardEvent) => {
+ const isHorizontal = this.scope.angle === "horizontal";
+ const prevKey = isHorizontal ? "ArrowLeft" : "ArrowUp";
+ const nextKey = isHorizontal ? "ArrowRight" : "ArrowDown";
+ if (event.key === prevKey) {
+ event.preventDefault();
+ this.prev();
+ } else if (event.key === nextKey) {
+ event.preventDefault();
+ this.next();
+ }
+ };
+
+ protected addEventListeners() {
+ this.routerEvents.on("newPageReady", this.onViewChanges);
+
+ if (window.ResizeObserver) {
+ this.resizeObserver = new window.ResizeObserver(this.onViewChanges);
+ this.resizeObserver?.observe(this);
+ }
+
+ window.addEventListener("resize", this.onViewChanges, { passive: true });
+
+ this.addEventListener(
+ "visibility-changed" as any,
+ this.onVisibilityChanged,
+ );
+
+ this.slideshowInner?.addEventListener(
+ "scrolling",
+ this.onScroll as EventListener,
+ { passive: true },
+ );
+ this.slideshowInner?.addEventListener(
+ "scrollended",
+ this.onScrollEnd as EventListener,
+ { passive: true },
+ );
+
+ // Keyboard navigation — make the inner focusable and listen for arrow keys
+ if (this.slideshowInner) {
+ if (!this.slideshowInner.hasAttribute("tabindex")) {
+ this.slideshowInner.setAttribute("tabindex", "0");
+ }
+ this.slideshowInner.addEventListener("keydown", this.onKeyDown);
+ }
+ }
+
+ protected removeEventListeners() {
+ this.routerEvents.off("newPageReady", this.onViewChanges, this);
+
+ window.removeEventListener("resize", this.onViewChanges);
+
+ this.resizeObserver?.unobserve(this);
+
+ this.removeEventListener(
+ "visibility-changed" as any,
+ this.onVisibilityChanged,
+ );
+
+ this.slideshowInner?.removeEventListener(
+ "scrolling",
+ this.onScroll as EventListener,
+ );
+ this.slideshowInner?.removeEventListener(
+ "scrollended",
+ this.onScrollEnd as EventListener,
+ );
+ this.slideshowInner?.removeEventListener("keydown", this.onKeyDown);
+ }
+
+ protected initAll() {
+ this.initSlideshowInner();
+ this.initOptions();
+ this.addEventListeners();
+ this.updateSlides();
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ this.validateItems();
+ }
+
+ protected async afterBind() {
+ if (this._usePassthrough) {
+ // Destroy any dragscroll created during attribute parsing (before DOM was restructured)
+ this.disableDesktopDragscroll();
+ this.wrapInSlideshowInner();
+ this.injectControls();
+ this.injectIndicators();
+ }
+ this.initAll();
+ this.updateItems();
+ // Re-apply drag classes after items are fully initialized
+ if (this.scope.drag) {
+ this.enableDesktopDragscroll();
+ }
+ this.classList.add(`${TwSlideshowComponent.tagName}-ready`);
+ await super.afterBind();
+ }
+
+ protected async afterAllBind() {
+ this.updateItems();
+ await super.afterAllBind();
+ }
+
+ protected initSlideshowInner() {
+ if (!this.slideshowInner) {
+ this.throw(new Error("Can't init slideshow inner!"));
+ return;
+ }
+
+ this.scrollEventsService = new ScrollEventsService(this.slideshowInner);
+ }
+
+ protected preventDragstart = (e: Event) => e.preventDefault();
+
+ protected onDragMouseDown = () => {
+ // Disable scroll-snap and smooth scrolling during drag so that
+ // rapid programmatic scrollLeft changes in onMouseMove apply immediately
+ // instead of each triggering a competing smooth animation.
+ if (this.slideshowInner) {
+ this.slideshowInner.style.scrollSnapType = "none";
+ this.slideshowInner.style.scrollBehavior = "auto";
+ }
+ };
+
+ protected onDragMouseUp = () => {
+ // Re-enable scroll-snap and smooth scrolling
+ if (this.slideshowInner) {
+ this.slideshowInner.style.scrollSnapType = "";
+ this.slideshowInner.style.scrollBehavior = "";
+ }
+ };
+
+ protected enableDesktopDragscroll() {
+ console.debug("[tw-slideshow] enableDesktopDragscroll", {
+ hasDragscrollService: !!this.dragscrollService,
+ hasSlideshowInner: !!this.slideshowInner,
+ scopeDrag: this.scope.drag,
+ passthrough: this._usePassthrough,
+ });
+
+ if (!this.dragscrollService) {
+ if (!this.slideshowInner) {
+ console.warn(
+ "[tw-slideshow] No slideshowInner found, cannot enable dragscroll",
+ );
+ return;
+ }
+ const dragscrollOptions: DragscrollOptions = { detectGlobalMove: true };
+ this.dragscrollService = new Dragscroll(
+ this.slideshowInner,
+ dragscrollOptions,
+ );
+ console.debug("[tw-slideshow] Dragscroll service created");
+ }
+
+ // Temporarily disable scroll-snap while dragging
+ this.slideshowInner?.addEventListener("mousedown", this.onDragMouseDown);
+ window.addEventListener("mouseup", this.onDragMouseUp);
+
+ // Prevent ghost images: suppress the native dragstart event and
+ // set draggable=false on all draggable content (images, links, etc.)
+ this.addEventListener("dragstart", this.preventDragstart);
+ this.classList.add("drag-none");
+ const draggables = this.querySelectorAll("img, video, svg, a");
+ draggables.forEach((el) => {
+ el.setAttribute("draggable", "false");
+ el.classList.add("drag-none");
+ });
+ }
+
+ protected disableDesktopDragscroll() {
+ if (this.dragscrollService) {
+ this.dragscrollService.destroy();
+ this.dragscrollService = undefined;
+ }
+ this.slideshowInner?.removeEventListener("mousedown", this.onDragMouseDown);
+ window.removeEventListener("mouseup", this.onDragMouseUp);
+ this.removeEventListener("dragstart", this.preventDragstart);
+ this.classList.remove("drag-none");
+ const draggables = this.querySelectorAll("img, video, svg, a");
+ draggables.forEach((el) => {
+ el.removeAttribute("draggable");
+ el.classList.remove("drag-none");
+ });
+ }
+
+ public enableTouchScroll() {
+ this.classList.remove("touchscroll-disabled");
+ }
+
+ public disableTouchScroll() {
+ this.classList.add("touchscroll-disabled");
+ }
+
+ protected validateItems() {
+ if (!this.scope.items) {
+ this.throw(new Error("No items to validate!"));
+ return;
+ }
+ for (let i = 0; i < this.scope.items.length; i++) {
+ const item = this.scope.items[i];
+ item.index = item.index || i;
+ item.active = item.active || false;
+ }
+ }
+
+ public updateItems() {
+ let hasChange = false;
+ const slideEls = this.slideElements;
+
+ if (!this.scope.slideTemplate) {
+ slideEls.forEach((slideEl, index) => {
+ const exists = this.scope.items.find((item) => item.el === slideEl);
+ if (!exists) {
+ this.addItemByElement(slideEl, index);
+ hasChange = true;
+ }
+ });
+
+ for (const item of this.scope.items) {
+ const exists = slideEls.find((slideEl) => slideEl === item.el);
+ if (!exists) {
+ this.removeItem(item.index, false);
+ hasChange = true;
+ }
+ }
+ }
+
+ if (hasChange) {
+ this.updateItemIndexes();
+ this.updateSlides();
+ }
+
+ return hasChange;
+ }
+
+ protected removeItem(index: number, updateIndex = true) {
+ const item = this.scope.items[index];
+ if (!item) {
+ return;
+ }
+ item.el?.remove();
+ this.scope.items.splice(index, 1);
+ if (updateIndex) this.updateItemIndexes();
+ }
+
+ protected updateItemIndexes() {
+ for (let i = 0; i < this.scope.items.length; i++) {
+ const item = this.scope.items[i];
+ item.index = i;
+ }
+ }
+
+ protected addItemByElement(slideElement: HTMLElement, index: number) {
+ slideElement.setAttribute("data-index", index.toString());
+ const attributes: TwSlideshowSlide = {
+ active: false,
+ index,
+ el: slideElement,
+ };
+ this.scope.items.push(attributes);
+ }
+
+ protected initItemsByChildren() {
+ if (!this.slideElements) {
+ this.throw(
+ new Error(
+ "Can't add items by children because no slide children are found!",
+ ),
+ );
+ }
+ this.scope.items = [];
+ this.slideElements.forEach(this.addItemByElement.bind(this));
+ }
+
+ protected getScrollPosition(): ScrollPosition | null {
+ if (!this.slideshowInner) {
+ return null;
+ }
+ return getScrollPosition(this.slideshowInner);
+ }
+
+ protected setAllSlidesInactive(excludeIndex = -1) {
+ for (const item of this.scope.items) {
+ if (item.index !== excludeIndex) {
+ item.active = false;
+ item.el?.classList.remove("active");
+ }
+ }
+ }
+
+ protected setSlideActive(index: number) {
+ if (index === -1 || !this.scope.items?.length) {
+ console.warn(new Error("Most centered slide not found!"));
+ index = 0;
+ }
+ if (!this.scope.items?.[index]) {
+ index = 0;
+ }
+ if (!this.scope.items?.[index]) {
+ this.throw(new Error("Slide item to set active, not found!"));
+ return 0;
+ }
+
+ const item = this.scope.items[index];
+ item.active = true;
+ item.el?.classList.add("active");
+ }
+
+ protected setSlidesActive(slides: number[]) {
+ this.setAllSlidesInactive();
+ for (const slideIndex of slides) {
+ this.setSlideActive(slideIndex);
+ }
+ }
+
+ protected isScrollable() {
+ if (!this.slideshowInner) {
+ return false;
+ }
+
+ const hasScrollableContent =
+ this.scope.angle === "horizontal"
+ ? this.slideshowInner.scrollWidth > this.slideshowInner.clientWidth
+ : this.slideshowInner.scrollHeight > this.slideshowInner.clientHeight;
+
+ return hasScrollableContent;
+ }
+
+ protected getSlideElementByIndex(index: number) {
+ if (!this.slideshowInner) {
+ return undefined;
+ }
+ return this.slideshowInner.querySelector(
+ `[data-index="${index}"]`,
+ ) as HTMLElement;
+ }
+
+ protected isSlideVisible(item: TwSlideshowSlide, _offset: number) {
+ if (!this.slideshowInner) {
+ return false;
+ }
+ const containerRect = this.slideshowInner.getBoundingClientRect();
+ item.el ||= this.getSlideElementByIndex(item.index);
+ const slideEl = item.el;
+ if (!slideEl) {
+ console.warn("Slide element not found!");
+ return false;
+ }
+ const slideRect = slideEl.getBoundingClientRect();
+
+ // Count as "visible" if more than 50% of the slide overlaps the container.
+ // This is tolerant to gaps, padding and rounding errors.
+ if (this.scope.angle === "horizontal") {
+ const visibleLeft = Math.max(slideRect.left, containerRect.left);
+ const visibleRight = Math.min(slideRect.right, containerRect.right);
+ const visibleWidth = Math.max(0, visibleRight - visibleLeft);
+ return visibleWidth / slideRect.width > 0.5;
+ } else {
+ const visibleTop = Math.max(slideRect.top, containerRect.top);
+ const visibleBottom = Math.min(slideRect.bottom, containerRect.bottom);
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ return visibleHeight / slideRect.height > 0.5;
+ }
+ }
+
+ protected getVisibleSlides(offset: number) {
+ const activeSlides: number[] = [];
+
+ if (!this.scope.items?.length) {
+ return activeSlides;
+ }
+
+ for (const item of this.scope.items) {
+ if (this.isSlideVisible(item, offset)) {
+ activeSlides.push(item.index);
+ }
+ }
+
+ return activeSlides.sort((a, b) => a - b);
+ }
+
+ protected setVisibleSlidesActive(offset: number) {
+ this.setAllSlidesInactive();
+ const activeSlides = this.getVisibleSlides(offset);
+ this.setSlidesActive(activeSlides);
+ return activeSlides;
+ }
+
+ updateActiveSlides(offset = 8) {
+ const activeSlides = this.setVisibleSlidesActive(offset);
+ const firstIndex = activeSlides[0] || 0;
+ const lastIndex = activeSlides[activeSlides.length - 1] || 0;
+ const prevIndex = this.getPrevIndex(firstIndex);
+ const nextIndex = this.getNextIndex(lastIndex);
+ return {
+ firstIndex,
+ lastIndex,
+ activeSlides,
+ prevIndex,
+ nextIndex,
+ };
+ }
+
+ protected updateSlides(offset = 8, isRetry = false): number[] {
+ if (!this.scope.items.length) {
+ return [];
+ }
+ const { activeSlides, firstIndex, prevIndex, nextIndex } =
+ this.updateActiveSlides(offset);
+
+ // Try again with a bigger offset if no slides are found
+ if (!activeSlides.length && !isRetry) {
+ let fallbackOffset = offset * 2;
+ if (this.scope.angle === "horizontal") {
+ const slideWidth = this.scope.items[0]?.el?.clientWidth || 0;
+ if (slideWidth) {
+ fallbackOffset = Math.round(slideWidth / 2 - 0.5);
+ }
+ } else {
+ const slideHeight = this.scope.items[0]?.el?.clientHeight || 0;
+ if (slideHeight) {
+ fallbackOffset = Math.round(slideHeight / 2 - 0.5);
+ }
+ }
+ return this.updateSlides(fallbackOffset, true);
+ }
+
+ this.scope.activeSlides = activeSlides;
+ this.scope.prevIndex = prevIndex;
+ this.scope.nextIndex = nextIndex;
+
+ this.updateControls();
+ this.updateIndicators();
+
+ if (this.scope.sticky) {
+ this.goTo(firstIndex);
+ }
+
+ return activeSlides;
+ }
+
+ protected updateControls() {
+ const isScrollable = this.isScrollable();
+ this.scope.showControls =
+ this.scope.controls && isScrollable && this.scope.items.length > 1;
+
+ if (this.scope.infinite) {
+ this.scope.enableNextControl = true;
+ this.scope.enablePrevControl = true;
+ } else {
+ this.scope.enableNextControl =
+ isScrollable &&
+ this.scope.nextIndex !== -1 &&
+ this.scope.nextIndex <= this.scope.items.length - 1;
+ this.scope.enablePrevControl =
+ isScrollable &&
+ this.scope.prevIndex !== -1 &&
+ this.scope.prevIndex >= 0;
+ }
+
+ this.syncPassthroughControls();
+ }
+
+ /**
+ * Hook for pass-through mode: keeps the injected DOM buttons in sync with
+ * the current enable*Control state. No-op unless `injectControls()` has run.
+ */
+ protected syncPassthroughControls() {
+ if (this._passthroughPrevBtn) {
+ this._passthroughPrevBtn.disabled = !this.scope.enablePrevControl;
+ }
+ if (this._passthroughNextBtn) {
+ this._passthroughNextBtn.disabled = !this.scope.enableNextControl;
+ }
+ }
+
+ protected updateIndicators() {
+ const isScrollable = this.isScrollable();
+ this.scope.showIndicators =
+ this.scope.indicators && isScrollable && this.scope.items.length > 1;
+ }
+
+ protected requiredAttributes(): string[] {
+ return [];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+
+ if (attributeName === "items") {
+ this.validateItems();
+ }
+
+ if (attributeName === "drag") {
+ if (this.scope.drag) {
+ this.enableDesktopDragscroll();
+ } else {
+ this.disableDesktopDragscroll();
+ }
+ }
+
+ if (attributeName === "touchScroll") {
+ if (this.scope.touchScroll) {
+ this.enableTouchScroll();
+ } else {
+ this.disableTouchScroll();
+ }
+ }
+
+ if (attributeName === "controls" || attributeName === "controlsPosition") {
+ this.setControlsOptions();
+ }
+
+ if (
+ attributeName === "indicators" ||
+ attributeName === "indicatorsPosition"
+ ) {
+ this.setIndicatorsOptions();
+ }
+
+ if (attributeName === "columns") {
+ this.updateColumns();
+ }
+ }
+
+ protected disconnectedCallback() {
+ this.removeEventListeners();
+ }
+
+ protected async beforeTemplate(): Promise {
+ const templates = Array.from(this.querySelectorAll("template"));
+ for (const tpl of templates) {
+ const type = tpl.getAttribute("type");
+ switch (type) {
+ case "slide-item":
+ this.scope.slideTemplate =
+ tpl.content.children.item(0)?.outerHTML || undefined;
+ this.debug("Slide template found!", this.scope.slideTemplate);
+ break;
+ default:
+ console.warn(`Unknown template type: ${type}`, tpl);
+ break;
+ }
+ }
+ }
+
+ /** Whether we're using pass-through mode (consumer provided slide children) */
+ protected _usePassthrough = false;
+
+ protected template(): ReturnType {
+ // If the consumer already provides .slideshow-row or .slide children,
+ // keep them and enhance with controls/indicators dynamically.
+ if (
+ this.querySelector(SLIDESHOW_INNER_SELECTOR) ||
+ this.querySelector(SLIDES_SELECTOR)
+ ) {
+ this._usePassthrough = true;
+ return null;
+ }
+ return template;
+ }
+
+ /**
+ * In pass-through mode, wrap existing content in a .slideshow-inner container
+ * so controls/indicators can be positioned correctly.
+ */
+ protected wrapInSlideshowInner() {
+ if (this.querySelector(".slideshow-inner")) return;
+
+ const wrapper = document.createElement("div");
+ wrapper.className = "slideshow-inner relative overflow-hidden w-full";
+
+ while (this.firstChild) {
+ wrapper.appendChild(this.firstChild);
+ }
+ this.appendChild(wrapper);
+ }
+
+ /**
+ * Create an SVG chevron element for control buttons.
+ */
+ protected createChevronSvg(direction: "left" | "right"): SVGElement {
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("class", "w-5 h-5");
+ svg.setAttribute("fill", "none");
+ svg.setAttribute("stroke", "currentColor");
+ svg.setAttribute("viewBox", "0 0 24 24");
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
+ path.setAttribute("stroke-linecap", "round");
+ path.setAttribute("stroke-linejoin", "round");
+ path.setAttribute("stroke-width", "2");
+ path.setAttribute(
+ "d",
+ direction === "left" ? "M15 19l-7-7 7-7" : "M9 5l7 7-7 7",
+ );
+ svg.appendChild(path);
+ return svg;
+ }
+
+ /**
+ * Dynamically inject prev/next control buttons for pass-through mode.
+ */
+ protected _passthroughPrevBtn?: HTMLButtonElement;
+ protected _passthroughNextBtn?: HTMLButtonElement;
+
+ protected injectControls() {
+ if (!this.scope.controls) return;
+ const inner = this.querySelector(".slideshow-inner");
+ if (!inner || inner.querySelector(".slideshow-controls")) return;
+
+ const controlsDiv = document.createElement("div");
+ controlsDiv.className =
+ "slideshow-controls absolute inset-0 flex items-center justify-between pointer-events-none z-10";
+
+ const prevBtn = document.createElement("button");
+ prevBtn.className =
+ "slideshow-control-prev pointer-events-auto flex items-center justify-center w-10 h-10 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors disabled:opacity-30 disabled:cursor-not-allowed ml-2";
+ prevBtn.setAttribute("aria-label", "Previous slide");
+ prevBtn.appendChild(this.createChevronSvg("left"));
+ prevBtn.addEventListener("click", () => this.prev());
+
+ const nextBtn = document.createElement("button");
+ nextBtn.className =
+ "slideshow-control-next pointer-events-auto flex items-center justify-center w-10 h-10 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors disabled:opacity-30 disabled:cursor-not-allowed mr-2";
+ nextBtn.setAttribute("aria-label", "Next slide");
+ nextBtn.appendChild(this.createChevronSvg("right"));
+ nextBtn.addEventListener("click", () => this.next());
+
+ controlsDiv.appendChild(prevBtn);
+ controlsDiv.appendChild(nextBtn);
+ inner.appendChild(controlsDiv);
+
+ this._passthroughPrevBtn = prevBtn;
+ this._passthroughNextBtn = nextBtn;
+
+ // Apply initial enabled state (subsequent updates happen via updateControls)
+ this.syncPassthroughControls();
+ }
+
+ /**
+ * Dynamically inject indicator dots for pass-through mode.
+ */
+ protected injectIndicators() {
+ if (!this.scope.indicators) return;
+ const inner = this.querySelector(".slideshow-inner");
+ if (!inner || inner.querySelector(".slideshow-indicators")) return;
+
+ const indicatorsDiv = document.createElement("div");
+ indicatorsDiv.className =
+ "slideshow-indicators absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 z-10";
+
+ inner.appendChild(indicatorsDiv);
+
+ // Override updateIndicators to also render pass-through dots
+ const origUpdateIndicators = this.updateIndicators.bind(this);
+ this.updateIndicators = () => {
+ origUpdateIndicators();
+ this.renderPassthroughIndicators(indicatorsDiv);
+ };
+ }
+
+ /**
+ * Render indicator dots based on current items for pass-through mode.
+ */
+ protected renderPassthroughIndicators(container: HTMLElement) {
+ if (!this.scope.showIndicators) {
+ container.style.display = "none";
+ return;
+ }
+ container.style.display = "";
+
+ // Sync dot count with items
+ while (container.children.length > this.scope.items.length) {
+ container.lastChild?.remove();
+ }
+ while (container.children.length < this.scope.items.length) {
+ const dot = document.createElement("button");
+ dot.className =
+ "w-2.5 h-2.5 rounded-full transition-colors border border-white/60";
+ dot.setAttribute("aria-label", "Go to slide");
+ const idx = container.children.length;
+ dot.addEventListener("click", () => this.goTo(idx));
+ container.appendChild(dot);
+ }
+
+ // Update active state
+ Array.from(container.children).forEach((dot, i) => {
+ const isActive = this.scope.activeSlides.includes(i);
+ dot.classList.toggle("bg-white", isActive);
+ dot.classList.toggle("bg-white/40", !isActive);
+ });
+ }
+}
diff --git a/packages/tw/src/components/tw-steps/tw-steps.component.html b/packages/tw/src/components/tw-steps/tw-steps.component.html
new file mode 100644
index 000000000..c2abd329c
--- /dev/null
+++ b/packages/tw/src/components/tw-steps/tw-steps.component.html
@@ -0,0 +1,121 @@
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-steps/tw-steps.component.spec.ts b/packages/tw/src/components/tw-steps/tw-steps.component.spec.ts
new file mode 100644
index 000000000..5d728dbe0
--- /dev/null
+++ b/packages/tw/src/components/tw-steps/tw-steps.component.spec.ts
@@ -0,0 +1,12 @@
+import { describe, it, expect } from "vitest";
+import template from "./tw-steps.component.html?raw";
+
+describe("TwStepsComponent template", () => {
+ it("should not contain the typo class text-white2", () => {
+ expect(template).not.toContain("text-white2");
+ });
+
+ it("should contain the correct class text-white", () => {
+ expect(template).toContain("text-white");
+ });
+});
diff --git a/packages/tw/src/components/tw-steps/tw-steps.component.ts b/packages/tw/src/components/tw-steps/tw-steps.component.ts
new file mode 100644
index 000000000..974129cb9
--- /dev/null
+++ b/packages/tw/src/components/tw-steps/tw-steps.component.ts
@@ -0,0 +1,142 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import template from "./tw-steps.component.html?raw";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+
+export interface StepItem {
+ label: string;
+ description?: string;
+ state: "completed" | "current" | "upcoming";
+ index: number;
+}
+
+export interface Scope extends ScopeBase {
+ items: StepItem[];
+ currentStep: number;
+ orientation: "horizontal" | "vertical";
+ lastIndex: number;
+ goToStep: TwStepsComponent["goToStep"];
+}
+
+export class TwStepsComponent extends Component {
+ public static tagName = "tw-steps";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["items", "current-step", "orientation"];
+ }
+
+ public scope: Scope = {
+ items: [],
+ currentStep: 0,
+ orientation: "horizontal",
+ lastIndex: 0,
+ goToStep: this.goToStep.bind(this),
+ };
+
+ constructor() {
+ super();
+ }
+
+ public goToStep(step: StepItem) {
+ this.scope.currentStep = step.index;
+ this.updateStepStates();
+ this.dispatchEvent(
+ new CustomEvent("step-changed", {
+ detail: { step: this.scope.currentStep },
+ bubbles: true,
+ }),
+ );
+ }
+
+ protected updateStepStates() {
+ for (const item of this.scope.items) {
+ if (item.index < this.scope.currentStep) {
+ item.state = "completed";
+ } else if (item.index === this.scope.currentStep) {
+ item.state = "current";
+ } else {
+ item.state = "upcoming";
+ }
+ }
+ }
+
+ protected initItemsFromAttribute() {
+ if (this.scope.items && this.scope.items.length > 0) {
+ // Items may have been passed as a JSON attribute; normalize them
+ this.scope.items = this.scope.items.map((item, index) => ({
+ label: item.label,
+ description: item.description || undefined,
+ state: "upcoming" as const,
+ index,
+ }));
+ this.scope.lastIndex = this.scope.items.length - 1;
+ this.updateStepStates();
+ }
+ }
+
+ protected initItemsFromTemplates() {
+ const templates = Array.from(this.querySelectorAll("template"));
+ if (templates.length > 0) {
+ this.scope.items = templates.map((tpl, index) => ({
+ label:
+ tpl.getAttribute("title") ||
+ tpl.getAttribute("label") ||
+ `Step ${index + 1}`,
+ description: tpl.getAttribute("description") || undefined,
+ state: "upcoming" as const,
+ index,
+ }));
+ this.scope.lastIndex = this.scope.items.length - 1;
+ this.updateStepStates();
+ }
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ if (!this.scope.items.length) {
+ this.initItemsFromTemplates();
+ }
+ this.init(TwStepsComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.initItemsFromAttribute();
+ await super.afterBind();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "currentStep" || attributeName === "items") {
+ this.initItemsFromAttribute();
+ }
+ }
+
+ protected template(): ReturnType {
+ if (!hasChildNodesTrim(this)) {
+ return template;
+ }
+ // If the element only contains children, use our template
+ const children = Array.from(this.childNodes);
+ const hasOnlyTemplates = children.every(
+ (child) =>
+ child.nodeType === Node.COMMENT_NODE ||
+ (child.nodeType === Node.TEXT_NODE && !child.textContent?.trim()) ||
+ child instanceof HTMLTemplateElement,
+ );
+ if (hasOnlyTemplates) {
+ return template;
+ }
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-swap/tw-swap.component.ts b/packages/tw/src/components/tw-swap/tw-swap.component.ts
new file mode 100644
index 000000000..52637f6e4
--- /dev/null
+++ b/packages/tw/src/components/tw-swap/tw-swap.component.ts
@@ -0,0 +1,147 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+
+export type SwapAnimation = "rotate" | "flip" | "fade";
+
+export interface Scope extends ScopeBase {
+ active: boolean;
+ animation: SwapAnimation;
+ toggle: TwSwapComponent["toggle"];
+}
+
+export class TwSwapComponent extends Component {
+ public static tagName = "tw-swap";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["active", "animation"];
+ }
+
+ public scope: Scope = {
+ active: false,
+ animation: "rotate",
+ toggle: this.toggle.bind(this),
+ };
+
+ protected onEl: HTMLElement | null = null;
+ protected offEl: HTMLElement | null = null;
+
+ constructor() {
+ super();
+ }
+
+ public toggle() {
+ this.scope.active = !this.scope.active;
+ this.updateSwapState();
+ this.dispatchEvent(
+ new CustomEvent("swap-changed", {
+ detail: { active: this.scope.active },
+ bubbles: true,
+ }),
+ );
+ }
+
+ protected findSwapChildren() {
+ this.onEl = this.querySelector("[data-swap-on]");
+ this.offEl = this.querySelector("[data-swap-off]");
+ }
+
+ protected updateSwapState() {
+ if (!this.onEl || !this.offEl) {
+ this.findSwapChildren();
+ }
+ if (!this.onEl || !this.offEl) {
+ return;
+ }
+
+ const animation = this.scope.animation;
+ this.setAttribute("data-animation", animation);
+
+ const showEl = this.scope.active ? this.onEl : this.offEl;
+ const hideEl = this.scope.active ? this.offEl : this.onEl;
+
+ // Position both absolutely so they overlap
+ showEl.style.position = "relative";
+ showEl.style.opacity = "1";
+ showEl.style.pointerEvents = "";
+ hideEl.style.position = "absolute";
+ hideEl.style.opacity = "0";
+ hideEl.style.pointerEvents = "none";
+
+ // Apply animation transforms
+ switch (animation) {
+ case "rotate":
+ showEl.style.transform = "rotate(0deg)";
+ hideEl.style.transform = "rotate(180deg)";
+ break;
+ case "flip":
+ showEl.style.transform = "scaleY(1)";
+ hideEl.style.transform = "scaleY(0)";
+ break;
+ case "fade":
+ default:
+ showEl.style.transform = "";
+ hideEl.style.transform = "";
+ break;
+ }
+ }
+
+ protected setupStyles() {
+ this.style.display = "inline-flex";
+ this.style.alignItems = "center";
+ this.style.justifyContent = "center";
+ this.style.position = "relative";
+ this.style.cursor = "pointer";
+
+ const transition = "transform 0.3s ease, opacity 0.3s ease";
+ if (this.onEl) {
+ this.onEl.style.transition = transition;
+ this.onEl.style.top = "0";
+ this.onEl.style.left = "0";
+ }
+ if (this.offEl) {
+ this.offEl.style.transition = transition;
+ this.offEl.style.top = "0";
+ this.offEl.style.left = "0";
+ }
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener("click", this.scope.toggle);
+ this.init(TwSwapComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.findSwapChildren();
+ this.setupStyles();
+ this.updateSwapState();
+ await super.afterBind();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "active") {
+ this.updateSwapState();
+ }
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener("click", this.scope.toggle);
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-tabs/tw-tabs.component.html b/packages/tw/src/components/tw-tabs/tw-tabs.component.html
new file mode 100644
index 000000000..50bc09702
--- /dev/null
+++ b/packages/tw/src/components/tw-tabs/tw-tabs.component.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-tabs/tw-tabs.component.ts b/packages/tw/src/components/tw-tabs/tw-tabs.component.ts
new file mode 100644
index 000000000..c77bcd9dd
--- /dev/null
+++ b/packages/tw/src/components/tw-tabs/tw-tabs.component.ts
@@ -0,0 +1,294 @@
+import {
+ handleizeFormatter,
+ FormatterFn,
+ TemplateFunction,
+ TemplatesComponent,
+ ScopeBase,
+} from "@ribajs/core";
+import templateHorizontal from "./tw-tabs.component.html?raw";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import { throttle } from "@ribajs/utils/src/control.js";
+
+const handleize = handleizeFormatter.read as FormatterFn;
+
+export interface Tab {
+ title: string;
+ content: string;
+ handle: string;
+ active: boolean;
+ type?: string;
+ index: number;
+}
+
+export interface Scope extends ScopeBase {
+ items: Tab[];
+ activate: TwTabsComponent["activate"];
+ deactivate: TwTabsComponent["deactivate"];
+ deactivateAll: TwTabsComponent["deactivateAll"];
+ optionTabsAutoHeight: boolean;
+}
+
+export class TwTabsComponent extends TemplatesComponent {
+ public static tagName = "tw-tabs";
+
+ protected autobind = true;
+
+ protected templateAttributes = [
+ { name: "title", required: true },
+ { name: "handle", required: false },
+ { name: "type", required: false },
+ { name: "active", required: false },
+ { name: "index", required: false },
+ ];
+
+ public scope: Scope = {
+ items: new Array(),
+ activate: this.activate.bind(this),
+ deactivate: this.deactivate.bind(this),
+ deactivateAll: this.deactivateAll.bind(this),
+ optionTabsAutoHeight: false,
+ };
+
+ protected tabs?: NodeListOf;
+ protected tabPanes?: NodeListOf;
+
+ static get observedAttributes(): string[] {
+ return [
+ "option-tabs-auto-height",
+ "tab-0-title",
+ "tab-0-content",
+ "tab-0-handle",
+ "tab-1-title",
+ "tab-1-content",
+ "tab-1-handle",
+ "tab-2-title",
+ "tab-2-content",
+ "tab-2-handle",
+ "tab-3-title",
+ "tab-3-content",
+ "tab-3-handle",
+ "tab-4-title",
+ "tab-4-content",
+ "tab-4-handle",
+ "tab-5-title",
+ "tab-5-content",
+ "tab-5-handle",
+ "tab-6-title",
+ "tab-6-content",
+ "tab-6-handle",
+ "tab-7-title",
+ "tab-7-content",
+ "tab-7-handle",
+ "tab-8-title",
+ "tab-8-content",
+ "tab-8-handle",
+ "tab-9-title",
+ "tab-9-content",
+ "tab-9-handle",
+ ];
+ }
+
+ constructor() {
+ super();
+ }
+
+ protected _onResize() {
+ this.setHeight();
+ }
+
+ protected onResize = throttle(this._onResize.bind(this));
+
+ /**
+ * Make all tab panes the same height as the tallest one.
+ */
+ public setHeight() {
+ if (this.scope.optionTabsAutoHeight) {
+ return;
+ }
+ this.setElements();
+
+ let highest = 0;
+ if (!this.tabPanes) return;
+
+ this.tabPanes.forEach((pane) => {
+ const el = pane as HTMLElement;
+ if (!el.style) return;
+ el.style.height = "auto";
+ el.style.display = "block";
+ const height = el.offsetHeight || 0;
+ if (height > highest) {
+ highest = height;
+ }
+ });
+
+ this.tabPanes.forEach((pane) => {
+ const el = pane as HTMLElement;
+ if (!el.style) return;
+ el.style.display = "";
+ if (highest > 0) {
+ el.style.height = highest + "px";
+ }
+ });
+ }
+
+ public deactivateAll() {
+ for (const tab of this.scope.items) {
+ this.deactivate(tab);
+ }
+ }
+
+ public deactivate(tab: Tab) {
+ tab.active = false;
+ const child = this.getTabContentChildByIndex(tab.index);
+ if (child) {
+ this.triggerVisibilityChangedForElement(child, false);
+ }
+ }
+
+ public activate(tab: Tab) {
+ this.deactivateAll();
+ tab.active = true;
+ const child = this.getTabContentChildByIndex(tab.index);
+ if (child) {
+ this.triggerVisibilityChangedForElement(child, true);
+ }
+ }
+
+ protected activateFirstTab() {
+ if (this.scope.items.length > 0) {
+ this.activate(this.scope.items[0]);
+ }
+ }
+
+ protected getTabContentChildByIndex(index: number) {
+ return (
+ this.querySelector(
+ `.tw-tab-content .tw-tab-pane:nth-child(${index + 1}) > *`,
+ ) || undefined
+ );
+ }
+
+ protected triggerVisibilityChangedForElement(
+ element: Element,
+ visible: boolean,
+ ) {
+ setTimeout(() => {
+ element.dispatchEvent(
+ new CustomEvent("visibility-changed", { detail: { visible } }),
+ );
+ }, 200);
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.initTabs();
+ this.activateFirstTab();
+ this.init(TwTabsComponent.observedAttributes);
+ }
+
+ protected disconnectedCallback() {
+ window.removeEventListener("resize", this.onResize);
+ }
+
+ protected setElements() {
+ this.tabs = this.querySelectorAll('[role="tab"]');
+ this.tabPanes = this.querySelectorAll('[role="tabpanel"]');
+ }
+
+ protected resizeTabsArray(newSize: number) {
+ while (newSize > this.scope.items.length) {
+ this.scope.items.push({
+ handle: "",
+ title: "",
+ content: "",
+ active: false,
+ index: this.scope.items.length,
+ });
+ }
+ }
+
+ protected initTabs() {
+ this.setElements();
+
+ if (this.scope.optionTabsAutoHeight) {
+ window.removeEventListener("resize", this.onResize);
+ window.addEventListener("resize", this.onResize, { passive: true });
+ this.setHeight();
+ }
+ }
+
+ protected addTabByAttribute(attributeName: string, newValue: string) {
+ const index = Number(attributeName.replace(/[^0-9]/g, ""));
+ if (index >= this.scope.items.length) {
+ this.resizeTabsArray(index + 1);
+ }
+ this.scope.items[index].index = index;
+ if (attributeName.endsWith("Content")) {
+ this.scope.items[index].content = newValue;
+ }
+ if (attributeName.endsWith("Title")) {
+ this.scope.items[index].title = newValue;
+ this.scope.items[index].handle =
+ this.scope.items[index].handle ||
+ handleize(this.scope.items[index].title);
+ }
+ if (attributeName.endsWith("Handle")) {
+ this.scope.items[index].handle = newValue;
+ }
+
+ // Activate first tab once it has content and title
+ if (
+ this.scope.items.length > 0 &&
+ this.scope.items[0] &&
+ this.scope.items[0].content.length > 0 &&
+ this.scope.items[0].title.length > 0 &&
+ this.scope.items[0].handle.length > 0
+ ) {
+ this.activateFirstTab();
+ }
+ }
+
+ protected transformTemplateAttributes(attributes: any, index: number) {
+ attributes = super.transformTemplateAttributes(attributes, index);
+ if (!attributes.handle && attributes.title) {
+ attributes.handle = handleize(attributes.title);
+ }
+ attributes.active = attributes.active || false;
+ return attributes;
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName.startsWith("tab")) {
+ this.addTabByAttribute(attributeName, newValue);
+ this.initTabs();
+ }
+ }
+
+ protected async afterBind(): Promise {
+ setTimeout(() => {
+ if (this.scope.optionTabsAutoHeight) {
+ this.setHeight();
+ }
+ }, 500);
+ await super.afterBind();
+ }
+
+ protected template(): ReturnType {
+ if (!hasChildNodesTrim(this) || this.hasOnlyTemplateChilds()) {
+ return templateHorizontal;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/packages/tw/src/components/tw-tagged-image/tw-tagged-image.component.html b/packages/tw/src/components/tw-tagged-image/tw-tagged-image.component.html
new file mode 100644
index 000000000..0aaf7b803
--- /dev/null
+++ b/packages/tw/src/components/tw-tagged-image/tw-tagged-image.component.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-tagged-image/tw-tagged-image.component.ts b/packages/tw/src/components/tw-tagged-image/tw-tagged-image.component.ts
new file mode 100644
index 000000000..041ad952a
--- /dev/null
+++ b/packages/tw/src/components/tw-tagged-image/tw-tagged-image.component.ts
@@ -0,0 +1,195 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import template from "./tw-tagged-image.component.html?raw";
+
+export interface ImageTag {
+ /** Horizontal position as a fraction 0..1 */
+ x: number;
+ /** Vertical position as a fraction 0..1 */
+ y: number;
+ /** Short label shown on the pin */
+ label: string;
+ /** Optional rich content shown in the popover */
+ content?: string;
+ /** Computed CSS left value */
+ left?: string;
+ /** Computed CSS top value */
+ top?: string;
+ /** Internal index */
+ index?: number;
+ /** Whether the popover is currently visible */
+ open?: boolean;
+}
+
+interface Scope extends ScopeBase {
+ /** Image source URL */
+ src: string;
+ /** Image alt text */
+ alt: string;
+ /** Tags array: each tag has x, y, label, and optional content */
+ tags: ImageTag[];
+ /** Toggle a tag popover */
+ toggleTag: TwTaggedImageComponent["toggleTag"];
+ /** Close a tag popover */
+ closeTag: TwTaggedImageComponent["closeTag"];
+ /** Update tag positions (called on image load / resize) */
+ updatePositions: TwTaggedImageComponent["updatePositions"];
+}
+
+export class TwTaggedImageComponent extends Component {
+ public static tagName = "tw-tagged-image";
+
+ protected autobind = true;
+ public _debug = false;
+
+ protected imageEl: HTMLImageElement | null = null;
+
+ static get observedAttributes(): string[] {
+ return ["src", "alt", "tags"];
+ }
+
+ public scope: Scope = {
+ src: "",
+ alt: "",
+ tags: [],
+ toggleTag: this.toggleTag.bind(this),
+ closeTag: this.closeTag.bind(this),
+ updatePositions: this.updatePositions.bind(this),
+ };
+
+ private resizeHandler = this.updatePositions.bind(this);
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ // Parse children BEFORE init() triggers template loading which replaces them
+ this.parseChildTags();
+ this.initTags();
+ this.init(TwTaggedImageComponent.observedAttributes);
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["src"];
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "tags") {
+ this.initTags();
+ this.updatePositions();
+ }
+ }
+
+ protected async beforeBind() {
+ await super.beforeBind();
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.imageEl = this.querySelector("img");
+ if (this.imageEl) {
+ this.imageEl.addEventListener("load", this.resizeHandler);
+ }
+ window.addEventListener("resize", this.resizeHandler);
+ this.updatePositions();
+ }
+
+ /**
+ * Parse `` child elements the same way bs5-tagged-image does.
+ */
+ protected parseChildTags() {
+ const tagEls = this.querySelectorAll("tag");
+ for (const tagEl of Array.from(tagEls)) {
+ const label =
+ tagEl.getAttribute("title") || tagEl.getAttribute("label") || "";
+ const content = tagEl.innerHTML || undefined;
+ const x = parseFloat(tagEl.getAttribute("x") || "0");
+ const y = parseFloat(tagEl.getAttribute("y") || "0");
+ this.scope.tags.push({ x, y, label, content });
+ }
+ }
+
+ protected initTags() {
+ for (let i = 0; i < this.scope.tags.length; i++) {
+ const tag = this.scope.tags[i];
+ tag.index = i;
+ tag.open = tag.open ?? false;
+ tag.left = tag.x * 100 + "%";
+ tag.top = tag.y * 100 + "%";
+ }
+ }
+
+ /**
+ * Recalculate tag positions based on image dimensions and object-fit.
+ */
+ public updatePositions() {
+ const img = this.imageEl;
+ if (!img) return;
+
+ const { width, height, naturalWidth, naturalHeight } = img;
+ if (!naturalWidth || !naturalHeight) return;
+
+ const wRatio = naturalWidth / width;
+ const hRatio = naturalHeight / height;
+ const fit = window.getComputedStyle(img).getPropertyValue("object-fit");
+
+ for (const tag of this.scope.tags) {
+ if (
+ (fit === "cover" && wRatio > hRatio) ||
+ (fit === "contain" && hRatio > wRatio)
+ ) {
+ tag.top = tag.y * 100 + "%";
+ tag.left = ((wRatio / hRatio) * (tag.x - 0.5) + 0.5) * 100 + "%";
+ } else if (fit === "cover" || fit === "contain") {
+ tag.left = tag.x * 100 + "%";
+ tag.top = ((hRatio / wRatio) * (tag.y - 0.5) + 0.5) * 100 + "%";
+ } else {
+ tag.left = tag.x * 100 + "%";
+ tag.top = tag.y * 100 + "%";
+ }
+ }
+ }
+
+ public toggleTag(tag: ImageTag) {
+ // Close other tags first
+ for (const t of this.scope.tags) {
+ if (t !== tag) {
+ t.open = false;
+ }
+ }
+ tag.open = !tag.open;
+ }
+
+ public closeTag(tag: ImageTag) {
+ tag.open = false;
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ if (this.imageEl) {
+ this.imageEl.removeEventListener("load", this.resizeHandler);
+ }
+ window.removeEventListener("resize", this.resizeHandler);
+ }
+
+ protected template(): ReturnType {
+ if (hasChildNodesTrim(this)) {
+ // If user provided child elements (including elements), still use the template
+ // because parseChildTags already extracted the data.
+ }
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-theme-button/tw-theme-button.component.html b/packages/tw/src/components/tw-theme-button/tw-theme-button.component.html
new file mode 100644
index 000000000..86652f4d4
--- /dev/null
+++ b/packages/tw/src/components/tw-theme-button/tw-theme-button.component.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tw/src/components/tw-theme-button/tw-theme-button.component.ts b/packages/tw/src/components/tw-theme-button/tw-theme-button.component.ts
new file mode 100644
index 000000000..884ba0991
--- /dev/null
+++ b/packages/tw/src/components/tw-theme-button/tw-theme-button.component.ts
@@ -0,0 +1,136 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { ThemeService } from "../../services/theme.service.js";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import template from "./tw-theme-button.component.html?raw";
+
+import type { ThemeChoice, ThemeChangedData } from "../../types/index.js";
+
+const THEME_CHOICES: ThemeChoice[] = ["os", "light", "dark"];
+
+interface Scope extends ScopeBase {
+ mode: "dropdown" | "icon";
+ labels: Record;
+ iconSize: number;
+ setTheme: TwThemeButtonComponent["setTheme"];
+ selectTheme: TwThemeButtonComponent["selectTheme"];
+ toggleTheme: TwThemeButtonComponent["toggleTheme"];
+ selected: ThemeChoice | undefined;
+ choices: ThemeChoice[];
+}
+
+export class TwThemeButtonComponent extends Component {
+ public static tagName = "tw-theme-button";
+
+ protected autobind = true;
+
+ protected theme: ThemeService;
+
+ static get observedAttributes(): string[] {
+ return ["mode", "labels", "icon-size"];
+ }
+
+ public scope: Scope = {
+ mode: "icon",
+ labels: {
+ os: "System",
+ light: "Light",
+ dark: "Dark",
+ },
+ iconSize: 24,
+ setTheme: this.setTheme.bind(this),
+ selectTheme: this.selectTheme.bind(this),
+ toggleTheme: this.toggleTheme.bind(this),
+ selected: undefined,
+ choices: THEME_CHOICES,
+ };
+
+ constructor() {
+ super();
+ this.theme = ThemeService.getSingleton();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwThemeButtonComponent.observedAttributes);
+ this.addThemeListeners();
+ this.initTheme();
+ }
+
+ protected addThemeListeners() {
+ this.theme.onChange((data) => {
+ this.onThemeChange(data);
+ });
+ }
+
+ protected onThemeChange(data: ThemeChangedData) {
+ this.scope.selected = data.current.choice;
+ }
+
+ protected async beforeBind() {
+ this.initTheme();
+ }
+
+ initTheme() {
+ const data = this.theme.getThemeData();
+ this.scope.selected = data.choice || undefined;
+ }
+
+ public setTheme(theme: ThemeChoice) {
+ this.theme.set(theme);
+ }
+
+ /**
+ * Used in `dropdown` mode (select element).
+ */
+ public selectTheme() {
+ if (this.scope.selected) {
+ this.theme.set(this.scope.selected);
+ }
+ }
+
+ /**
+ * Used in `icon` mode (button element). Cycles through light -> dark -> os.
+ */
+ public toggleTheme() {
+ const data = this.theme.getThemeData();
+ const resolved = data.resolved;
+ const choice = data.choice;
+
+ if (choice === "os") {
+ // OS mode: switch to opposite of current resolved
+ if (resolved === "light") {
+ this.theme.set("dark");
+ } else {
+ this.theme.set("light");
+ }
+ } else if (choice === "dark") {
+ // From dark, go to light (or os if system prefers dark)
+ const systemPrefersDark = window.matchMedia(
+ "(prefers-color-scheme: dark)",
+ ).matches;
+ if (systemPrefersDark) {
+ this.theme.set("os");
+ } else {
+ this.theme.set("light");
+ }
+ } else {
+ // From light, go to dark (or os if system prefers light)
+ const systemPrefersLight = window.matchMedia(
+ "(prefers-color-scheme: light)",
+ ).matches;
+ if (systemPrefersLight) {
+ this.theme.set("os");
+ } else {
+ this.theme.set("dark");
+ }
+ }
+ }
+
+ protected template(): ReturnType {
+ if (!hasChildNodesTrim(this)) {
+ return template;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/packages/tw/src/components/tw-toast-item/tw-toast-item.component.html b/packages/tw/src/components/tw-toast-item/tw-toast-item.component.html
new file mode 100644
index 000000000..7368cc41c
--- /dev/null
+++ b/packages/tw/src/components/tw-toast-item/tw-toast-item.component.html
@@ -0,0 +1,30 @@
+
diff --git a/packages/tw/src/components/tw-toast-item/tw-toast-item.component.ts b/packages/tw/src/components/tw-toast-item/tw-toast-item.component.ts
new file mode 100644
index 000000000..a57f28c62
--- /dev/null
+++ b/packages/tw/src/components/tw-toast-item/tw-toast-item.component.ts
@@ -0,0 +1,184 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { ToastService } from "../../services/toast.service.js";
+import type { ToastNotification } from "../../types/index.js";
+import template from "./tw-toast-item.component.html?raw";
+
+type ToastType = "info" | "success" | "warning" | "error";
+
+const TYPE_STYLES: Record<
+ ToastType,
+ {
+ container: string;
+ title: string;
+ message: string;
+ closeButton: string;
+ icon: string;
+ }
+> = {
+ info: {
+ container:
+ "bg-blue-50 ring-blue-200 dark:bg-blue-900/30 dark:ring-blue-700",
+ title: "text-blue-800 dark:text-blue-200",
+ message: "text-blue-700 dark:text-blue-300",
+ closeButton:
+ "bg-blue-50 text-blue-500 hover:text-blue-600 focus:ring-blue-500 dark:bg-blue-900/30 dark:text-blue-400 dark:hover:text-blue-300",
+ icon: ' ',
+ },
+ success: {
+ container:
+ "bg-green-50 ring-green-200 dark:bg-green-900/30 dark:ring-green-700",
+ title: "text-green-800 dark:text-green-200",
+ message: "text-green-700 dark:text-green-300",
+ closeButton:
+ "bg-green-50 text-green-500 hover:text-green-600 focus:ring-green-500 dark:bg-green-900/30 dark:text-green-400 dark:hover:text-green-300",
+ icon: ' ',
+ },
+ warning: {
+ container:
+ "bg-yellow-50 ring-yellow-200 dark:bg-yellow-900/30 dark:ring-yellow-700",
+ title: "text-yellow-800 dark:text-yellow-200",
+ message: "text-yellow-700 dark:text-yellow-300",
+ closeButton:
+ "bg-yellow-50 text-yellow-500 hover:text-yellow-600 focus:ring-yellow-500 dark:bg-yellow-900/30 dark:text-yellow-400 dark:hover:text-yellow-300",
+ icon: ' ',
+ },
+ error: {
+ container: "bg-red-50 ring-red-200 dark:bg-red-900/30 dark:ring-red-700",
+ title: "text-red-800 dark:text-red-200",
+ message: "text-red-700 dark:text-red-300",
+ closeButton:
+ "bg-red-50 text-red-500 hover:text-red-600 focus:ring-red-500 dark:bg-red-900/30 dark:text-red-400 dark:hover:text-red-300",
+ icon: ' ',
+ },
+};
+
+interface Scope extends ScopeBase {
+ toast?: ToastNotification;
+ containerClass: string;
+ titleClass: string;
+ messageClass: string;
+ closeButtonClass: string;
+ iconSvg: string;
+ dismiss: TwToastItemComponent["dismiss"];
+ onHidden: TwToastItemComponent["onHidden"];
+ index: number;
+ $parent?: any;
+ $event?: CustomEvent;
+}
+
+/**
+ * Use this component to show a toast inside a tw-notification-container
+ */
+export class TwToastItemComponent extends Component {
+ public static tagName = "tw-toast-item";
+
+ protected autobind = true;
+
+ protected toastService?: ToastService;
+
+ static get observedAttributes(): string[] {
+ return ["toast", "index"];
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["toast"];
+ }
+
+ public scope: Scope = {
+ toast: undefined,
+ containerClass: TYPE_STYLES.info.container,
+ titleClass: TYPE_STYLES.info.title,
+ messageClass: TYPE_STYLES.info.message,
+ closeButtonClass: TYPE_STYLES.info.closeButton,
+ iconSvg: TYPE_STYLES.info.icon,
+ dismiss: this.dismiss.bind(this),
+ onHidden: this.onHidden.bind(this),
+ index: -1,
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwToastItemComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ this.updateTypeStyles();
+ this.initToast();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "toast") {
+ this.updateTypeStyles();
+ }
+ }
+
+ protected updateTypeStyles() {
+ const toastType = (this.scope.toast?.type || "info") as ToastType;
+ const styles = TYPE_STYLES[toastType] || TYPE_STYLES.info;
+ this.scope.containerClass = styles.container;
+ this.scope.titleClass = styles.title;
+ this.scope.messageClass = styles.message;
+ this.scope.closeButtonClass = styles.closeButton;
+ this.scope.iconSvg = styles.icon;
+ }
+
+ protected initToast() {
+ const toast = this.scope.toast;
+ const toastEl = this.firstElementChild as HTMLElement | null;
+ if (toast && toastEl) {
+ this.toastService = new ToastService(toastEl, {
+ autoDismiss: toast.timeout ?? 5000,
+ show: true,
+ });
+
+ // Call onHidden on hidden event once
+ toastEl.addEventListener("tw.toast.hidden", this.scope.onHidden, {
+ once: true,
+ });
+ }
+ }
+
+ /** Can be called if toast should be dismissed */
+ public dismiss() {
+ this.toastService?.hide();
+ }
+
+ /** Remove toast from DOM once hidden */
+ public onHidden() {
+ // Navigate up the parent scope chain to the notification container
+ const parentScope = this.scope.$parent?.$parent;
+ if (typeof parentScope?.onItemHide === "function" && this.scope.toast) {
+ parentScope.onItemHide(
+ this.scope.$event,
+ this,
+ this.scope.index,
+ this.scope.toast,
+ );
+ }
+ }
+
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ this.toastService?.dispose();
+ }
+
+ protected template(): ReturnType {
+ return template;
+ }
+}
diff --git a/packages/tw/src/components/tw-toggle-button/tw-toggle-button.component.ts b/packages/tw/src/components/tw-toggle-button/tw-toggle-button.component.ts
new file mode 100644
index 000000000..59929ee23
--- /dev/null
+++ b/packages/tw/src/components/tw-toggle-button/tw-toggle-button.component.ts
@@ -0,0 +1,173 @@
+/**
+ * This component is used to trigger a toggle event used in other components or
+ * parts of your project. It uses the EventDispatcher to communicate with target
+ * components via a shared namespace.
+ *
+ * @attribute "target-id" (Required) The id with which the toggle event is triggered
+ * @method toggle Triggers the toggle event
+ * @property state Can be 'hidden', 'added', 'removed', or a positional state
+ * @property isActive Is true if the state is not 'hidden', 'removed', or 'undefined'
+ * @property targetId Passed attribute value, see `target-id` attribute
+ */
+
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+import { EventDispatcher } from "@ribajs/events";
+import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
+import { TOGGLE_BUTTON } from "../../constants/index.js";
+
+type State =
+ | "undefined"
+ | "overlay-left"
+ | "overlay-right"
+ | "side-left"
+ | "side-right"
+ | "hidden"
+ | "added"
+ | "removed";
+
+interface Scope extends ScopeBase {
+ targetId?: string;
+ toggle: TwToggleButtonComponent["toggle"];
+ state: State;
+ isActive: boolean;
+}
+
+export class TwToggleButtonComponent extends Component {
+ static get observedAttributes(): string[] {
+ return ["target-id"];
+ }
+
+ protected requiredAttributes(): string[] {
+ return ["target-id"];
+ }
+
+ public static tagName = "tw-toggle-button";
+
+ protected autobind = true;
+
+ protected eventDispatcher?: EventDispatcher;
+
+ protected lifecycleEvents = EventDispatcher.getInstance("lifecycle");
+
+ public scope: Scope = {
+ targetId: undefined,
+ toggle: this.toggle.bind(this),
+ state: "undefined",
+ isActive: true,
+ };
+
+ constructor() {
+ super();
+ this.lifecycleEvents.once(
+ "ComponentLifecycle:allBound",
+ this.onAllComponentsReady,
+ this,
+ );
+ }
+
+ public toggle() {
+ if (this.eventDispatcher) {
+ this.eventDispatcher.trigger(
+ TOGGLE_BUTTON.eventNames.toggle,
+ this.scope.targetId,
+ );
+ }
+ }
+
+ protected onAllComponentsReady() {
+ // Trigger init to request current state from all connected components
+ this.eventDispatcher?.trigger(
+ TOGGLE_BUTTON.eventNames.init,
+ this.scope.targetId,
+ );
+ }
+
+ protected async afterBind() {
+ await super.afterBind();
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.init(TwToggleButtonComponent.observedAttributes);
+ }
+
+ protected onToggledEvent(state: State) {
+ this.scope.state = state;
+ this.scope.isActive = state !== "hidden" && state !== "removed";
+ }
+
+ protected initEventDispatcher(id: string) {
+ if (this.eventDispatcher) {
+ this.eventDispatcher.off(
+ TOGGLE_BUTTON.eventNames.toggled,
+ this.onToggledEvent,
+ this,
+ );
+ }
+ const namespace = TOGGLE_BUTTON.nsPrefix + id;
+ this.eventDispatcher = new EventDispatcher(namespace);
+ this.eventDispatcher.on(
+ TOGGLE_BUTTON.eventNames.toggled,
+ this.onToggledEvent,
+ this,
+ );
+ // Triggered state triggered by `..trigger('init', ...`
+ this.eventDispatcher.on(
+ TOGGLE_BUTTON.eventNames.state,
+ this.onToggledEvent,
+ this,
+ );
+ }
+
+ protected async attributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.attributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ if (attributeName === "targetId" && newValue) {
+ this.initEventDispatcher(newValue);
+ }
+ }
+
+ // deconstruction
+ protected disconnectedCallback() {
+ super.disconnectedCallback();
+ if (this.eventDispatcher) {
+ this.eventDispatcher.off(
+ TOGGLE_BUTTON.eventNames.toggled,
+ this.onToggledEvent,
+ this,
+ );
+ }
+ }
+
+ protected template(): ReturnType {
+ if (!hasChildNodesTrim(this)) {
+ console.warn(
+ "[tw-toggle-button] No child elements found. Provide button content as children.",
+ );
+ }
+ return null;
+ }
+}
diff --git a/packages/tw/src/components/tw-tooltip/tw-tooltip.component.ts b/packages/tw/src/components/tw-tooltip/tw-tooltip.component.ts
new file mode 100644
index 000000000..9eaf563cc
--- /dev/null
+++ b/packages/tw/src/components/tw-tooltip/tw-tooltip.component.ts
@@ -0,0 +1,221 @@
+import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
+
+export type TooltipPosition = "top" | "bottom" | "left" | "right";
+
+export interface Scope extends ScopeBase {
+ content: string;
+ position: TooltipPosition;
+}
+
+export class TwTooltipComponent extends Component {
+ public static tagName = "tw-tooltip";
+
+ protected autobind = true;
+
+ static get observedAttributes(): string[] {
+ return ["content", "position"];
+ }
+
+ public scope: Scope = {
+ content: "",
+ position: "top",
+ };
+
+ constructor() {
+ super();
+ }
+
+ protected getPositionClasses(): string[] {
+ switch (this.scope.position) {
+ case "bottom":
+ return ["tw-tooltip", "tw-tooltip-bottom"];
+ case "left":
+ return ["tw-tooltip", "tw-tooltip-left"];
+ case "right":
+ return ["tw-tooltip", "tw-tooltip-right"];
+ default:
+ return ["tw-tooltip", "tw-tooltip-top"];
+ }
+ }
+
+ protected updateTooltip() {
+ // Set the tooltip content via data attribute
+ this.setAttribute("data-tooltip", this.scope.content);
+
+ // Remove old position classes
+ this.classList.remove(
+ "tw-tooltip",
+ "tw-tooltip-top",
+ "tw-tooltip-bottom",
+ "tw-tooltip-left",
+ "tw-tooltip-right",
+ );
+
+ // Add new position classes
+ const classes = this.getPositionClasses();
+ for (const cls of classes) {
+ this.classList.add(cls);
+ }
+ }
+
+ protected injectStyles() {
+ const styleId = "tw-tooltip-styles";
+ if (document.getElementById(styleId)) {
+ return;
+ }
+
+ const style = document.createElement("style");
+ style.id = styleId;
+ style.textContent = `
+ .tw-tooltip {
+ position: relative;
+ display: inline-block;
+ }
+
+ .tw-tooltip::before {
+ content: attr(data-tooltip);
+ position: absolute;
+ padding: 0.375rem 0.75rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ font-weight: 500;
+ color: #fff;
+ background-color: #1f2937;
+ border-radius: 0.375rem;
+ white-space: nowrap;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.15s ease-in-out, visibility 0.15s ease-in-out;
+ pointer-events: none;
+ z-index: 50;
+ }
+
+ .tw-tooltip::after {
+ content: '';
+ position: absolute;
+ border: 5px solid transparent;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.15s ease-in-out, visibility 0.15s ease-in-out;
+ pointer-events: none;
+ z-index: 50;
+ }
+
+ .tw-tooltip:hover::before,
+ .tw-tooltip:hover::after {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ /* Top (default) */
+ .tw-tooltip-top::before {
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-bottom: 5px;
+ }
+ .tw-tooltip-top::after {
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-top-color: #1f2937;
+ }
+
+ /* Bottom */
+ .tw-tooltip-bottom::before {
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ margin-top: 5px;
+ }
+ .tw-tooltip-bottom::after {
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-bottom-color: #1f2937;
+ }
+
+ /* Left */
+ .tw-tooltip-left::before {
+ right: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ margin-right: 5px;
+ }
+ .tw-tooltip-left::after {
+ right: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ border-left-color: #1f2937;
+ }
+
+ /* Right */
+ .tw-tooltip-right::before {
+ left: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ margin-left: 5px;
+ }
+ .tw-tooltip-right::after {
+ left: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ border-right-color: #1f2937;
+ }
+
+ /* Dark mode (class-based via ThemeService) */
+ .dark .tw-tooltip::before,
+ .dark.tw-tooltip::before {
+ background-color: #e5e7eb;
+ color: #111827;
+ }
+ .dark .tw-tooltip-top::after,
+ .dark.tw-tooltip-top::after {
+ border-top-color: #e5e7eb;
+ }
+ .dark .tw-tooltip-bottom::after,
+ .dark.tw-tooltip-bottom::after {
+ border-bottom-color: #e5e7eb;
+ }
+ .dark .tw-tooltip-left::after,
+ .dark.tw-tooltip-left::after {
+ border-left-color: #e5e7eb;
+ }
+ .dark .tw-tooltip-right::after,
+ .dark.tw-tooltip-right::after {
+ border-right-color: #e5e7eb;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ protected connectedCallback() {
+ super.connectedCallback();
+ this.injectStyles();
+ this.init(TwTooltipComponent.observedAttributes);
+ }
+
+ protected async afterBind() {
+ this.updateTooltip();
+ await super.afterBind();
+ }
+
+ protected parsedAttributeChangedCallback(
+ attributeName: string,
+ oldValue: any,
+ newValue: any,
+ namespace: string | null,
+ ) {
+ super.parsedAttributeChangedCallback(
+ attributeName,
+ oldValue,
+ newValue,
+ namespace,
+ );
+ this.updateTooltip();
+ }
+
+ protected template(): ReturnType {
+ return null;
+ }
+}
diff --git a/packages/tw/src/constants/index.spec.ts b/packages/tw/src/constants/index.spec.ts
new file mode 100644
index 000000000..b1edbc7d6
--- /dev/null
+++ b/packages/tw/src/constants/index.spec.ts
@@ -0,0 +1,80 @@
+import { describe, it, expect } from "vitest";
+import {
+ DEFAULT_BP_SM,
+ DEFAULT_BP_MD,
+ DEFAULT_BP_LG,
+ DEFAULT_BP_XL,
+ DEFAULT_BP_2XL,
+ DEFAULT_MODULE_OPTIONS,
+ TOGGLE_BUTTON,
+ TOGGLE_ATTRIBUTE,
+ TOGGLE_CLASS,
+} from "./index.js";
+
+describe("tw constants", () => {
+ describe("DEFAULT_MODULE_OPTIONS", () => {
+ it("contains the five default Tailwind breakpoints", () => {
+ expect(DEFAULT_MODULE_OPTIONS.breakpoints).toHaveLength(5);
+ });
+
+ it("defines sm at 640px", () => {
+ expect(DEFAULT_BP_SM).toEqual({ dimension: 640, name: "sm" });
+ });
+
+ it("defines md at 768px", () => {
+ expect(DEFAULT_BP_MD).toEqual({ dimension: 768, name: "md" });
+ });
+
+ it("defines lg at 1024px", () => {
+ expect(DEFAULT_BP_LG).toEqual({ dimension: 1024, name: "lg" });
+ });
+
+ it("defines xl at 1280px", () => {
+ expect(DEFAULT_BP_XL).toEqual({ dimension: 1280, name: "xl" });
+ });
+
+ it("defines 2xl at 1536px", () => {
+ expect(DEFAULT_BP_2XL).toEqual({ dimension: 1536, name: "2xl" });
+ });
+
+ it("has breakpoints in ascending order of dimension", () => {
+ const dims = DEFAULT_MODULE_OPTIONS.breakpoints.map(
+ (bp) => bp.dimension,
+ );
+ for (let i = 1; i < dims.length; i++) {
+ expect(dims[i]).toBeGreaterThan(dims[i - 1]);
+ }
+ });
+
+ it("enables allowStoreDataInBrowser by default", () => {
+ expect(DEFAULT_MODULE_OPTIONS.allowStoreDataInBrowser).toBe(true);
+ });
+ });
+
+ describe("TOGGLE_BUTTON", () => {
+ it("has a namespace prefix", () => {
+ expect(TOGGLE_BUTTON.nsPrefix).toBe("tw-toggle-button-");
+ });
+
+ it("has toggle, toggled, init, and state event names", () => {
+ expect(TOGGLE_BUTTON.eventNames.toggle).toBe("toggle");
+ expect(TOGGLE_BUTTON.eventNames.toggled).toBe("toggled");
+ expect(TOGGLE_BUTTON.eventNames.init).toBe("init");
+ expect(TOGGLE_BUTTON.eventNames.state).toBe("state");
+ });
+ });
+
+ describe("TOGGLE_ATTRIBUTE", () => {
+ it("has removed and added element event names", () => {
+ expect(TOGGLE_ATTRIBUTE.elEventNames.removed).toBe("attribute-removed");
+ expect(TOGGLE_ATTRIBUTE.elEventNames.added).toBe("attribute-added");
+ });
+ });
+
+ describe("TOGGLE_CLASS", () => {
+ it("has removed and added element event names", () => {
+ expect(TOGGLE_CLASS.elEventNames.removed).toBe("class-removed");
+ expect(TOGGLE_CLASS.elEventNames.added).toBe("class-added");
+ });
+ });
+});
diff --git a/packages/tw/src/constants/index.ts b/packages/tw/src/constants/index.ts
new file mode 100644
index 000000000..345718adc
--- /dev/null
+++ b/packages/tw/src/constants/index.ts
@@ -0,0 +1,42 @@
+import type { Breakpoint, TwModuleOptions } from "../types/index.js";
+
+export const DEFAULT_BP_SM: Breakpoint = { dimension: 640, name: "sm" };
+export const DEFAULT_BP_MD: Breakpoint = { dimension: 768, name: "md" };
+export const DEFAULT_BP_LG: Breakpoint = { dimension: 1024, name: "lg" };
+export const DEFAULT_BP_XL: Breakpoint = { dimension: 1280, name: "xl" };
+export const DEFAULT_BP_2XL: Breakpoint = { dimension: 1536, name: "2xl" };
+
+export const DEFAULT_MODULE_OPTIONS: TwModuleOptions = {
+ breakpoints: [
+ DEFAULT_BP_SM,
+ DEFAULT_BP_MD,
+ DEFAULT_BP_LG,
+ DEFAULT_BP_XL,
+ DEFAULT_BP_2XL,
+ ],
+ allowStoreDataInBrowser: true,
+};
+
+export const TOGGLE_BUTTON = {
+ nsPrefix: "tw-toggle-button-",
+ eventNames: {
+ toggle: "toggle",
+ toggled: "toggled",
+ init: "init",
+ state: "state",
+ },
+};
+
+export const TOGGLE_ATTRIBUTE = {
+ elEventNames: {
+ removed: "attribute-removed",
+ added: "attribute-added",
+ },
+};
+
+export const TOGGLE_CLASS = {
+ elEventNames: {
+ removed: "class-removed",
+ added: "class-added",
+ },
+};
diff --git a/packages/tw/src/css/index.css b/packages/tw/src/css/index.css
new file mode 100644
index 000000000..d7cbdedbd
--- /dev/null
+++ b/packages/tw/src/css/index.css
@@ -0,0 +1,26 @@
+@import "tailwindcss";
+
+/*
+ * Consumers of @ribajs/tw must add @source directives to scan the component
+ * templates so that Tailwind generates the required utility classes.
+ * They must also @import the utilities file for custom @utility definitions.
+ *
+ * Example in your project's CSS:
+ *
+ * @import "tailwindcss";
+ * @import "@ribajs/tw/src/css/utilities.css";
+ * @source "../../node_modules/@ribajs/tw/src/**\/*.html";
+ * @source "../../node_modules/@ribajs/tw/src/**\/*.ts";
+ * @custom-variant dark (&:where(.dark, .dark *));
+ *
+ * In a Yarn PnP monorepo, use relative paths to the workspace package:
+ * @import "../../../../packages/tw/src/css/utilities.css";
+ * @source "../../../../packages/tw/src/**\/*.html";
+ * @source "../../../../packages/tw/src/**\/*.ts";
+ */
+
+@import "./utilities.css";
+
+/* Use class-based dark mode so ThemeService can toggle it via the `dark` class on */
+@custom-variant dark (&:where(.dark, .dark *));
+
diff --git a/packages/tw/src/css/utilities.css b/packages/tw/src/css/utilities.css
new file mode 100644
index 000000000..6ce3edb9b
--- /dev/null
+++ b/packages/tw/src/css/utilities.css
@@ -0,0 +1,74 @@
+/* ── Custom styles for @ribajs/tw components ── */
+
+/* Scan all tw component templates and TS files so consumers of this
+ stylesheet only need to @import it, without adding their own @source
+ directives with brittle relative paths. */
+@source "../**/*.html";
+@source "../**/*.ts";
+
+/* Dynamic utility classes used in component TS files via rv-class bindings. */
+/* @source inline ensures Tailwind generates these even if the file scanner misses them. */
+@source inline("h-1 h-2 h-3 h-4 h-6 h-8 h-10 h-14 h-20 w-3 w-4 w-6 w-8 w-10 w-14 w-20 h-1.5 w-1.5 h-2.5 w-2.5 h-3.5 w-3.5 text-xs text-sm text-lg text-xl bg-blue-500 bg-blue-600 bg-green-400 bg-green-500 bg-yellow-400 bg-yellow-500 bg-red-400 bg-red-500 bg-gray-400 dark:bg-blue-400 dark:bg-green-400 dark:bg-yellow-400 dark:bg-red-400");
+
+/* All tw-* custom elements should be block-level so layout utilities
+ (space-y, gap, grid, flex) work correctly on them. */
+tw-accordion,
+tw-alert,
+tw-breadcrumb,
+tw-card,
+tw-carousel,
+tw-collapse,
+tw-contents,
+tw-dropdown,
+tw-form,
+tw-modal-item,
+tw-navbar,
+tw-notification-container,
+tw-pagination,
+tw-progress,
+tw-scrollspy,
+tw-share,
+tw-sidebar,
+tw-skeleton,
+tw-slide-video,
+tw-slider,
+tw-slideshow,
+tw-steps,
+tw-swap,
+tw-tabs,
+tw-tagged-image,
+tw-toast-item {
+ display: block;
+}
+
+/* Inline-level custom elements that should participate in text flow. */
+tw-avatar,
+tw-badge,
+tw-colorpicker,
+tw-kbd,
+tw-rating,
+tw-toggle-button,
+tw-tooltip {
+ display: inline-block;
+}
+
+/* Clickable elements bound with rv-on-click should show a pointer cursor */
+[rv-on-click] {
+ cursor: pointer;
+}
+
+/* Hide scrollbar while keeping scroll functionality */
+.scrollbar-none {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+.scrollbar-none::-webkit-scrollbar {
+ display: none;
+}
+
+/* Prevent drag ghost images (used by slideshow/slider drag mode) */
+.drag-none {
+ -webkit-user-drag: none;
+ user-drag: none;
+ user-select: none;
+}
diff --git a/packages/tw/src/formatters/index.ts b/packages/tw/src/formatters/index.ts
new file mode 100644
index 000000000..cb0ff5c3b
--- /dev/null
+++ b/packages/tw/src/formatters/index.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/tw/src/index.ts b/packages/tw/src/index.ts
new file mode 100644
index 000000000..2def95548
--- /dev/null
+++ b/packages/tw/src/index.ts
@@ -0,0 +1,7 @@
+export * from "./services/index.js";
+export * from "./binders/index.js";
+export * from "./components/index.js";
+export * from "./formatters/index.js";
+export * from "./types/index.js";
+export * from "./constants/index.js";
+export { twModule } from "./tw.module.js";
diff --git a/packages/tw/src/services/collapse.service.spec.ts b/packages/tw/src/services/collapse.service.spec.ts
new file mode 100644
index 000000000..25621712a
--- /dev/null
+++ b/packages/tw/src/services/collapse.service.spec.ts
@@ -0,0 +1,198 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { CollapseService } from "./collapse.service.js";
+
+describe("CollapseService", () => {
+ let el: HTMLElement;
+
+ beforeEach(() => {
+ el = document.createElement("div");
+ // jsdom returns "" for getComputedStyle().transitionDuration, so
+ // the constructor will fall back to the default 300ms.
+ document.body.appendChild(el);
+ });
+
+ afterEach(() => {
+ el.remove();
+ });
+
+ describe("constructor", () => {
+ it("initializes as shown when element has no hidden class and no show option", () => {
+ const collapse = new CollapseService(el);
+ expect(collapse.isShown).toBe(true);
+ expect(collapse.isCollapsed).toBe(false);
+ });
+
+ it("initializes as hidden when element has the hidden class", () => {
+ el.classList.add("hidden");
+ const collapse = new CollapseService(el);
+ expect(collapse.isShown).toBe(false);
+ expect(collapse.isCollapsed).toBe(true);
+ });
+
+ it("initializes as shown when show option is true", () => {
+ el.classList.add("hidden");
+ const collapse = new CollapseService(el, { show: true });
+ expect(collapse.isShown).toBe(true);
+ });
+
+ it("initializes as hidden when show option is false", () => {
+ const collapse = new CollapseService(el, { show: false });
+ expect(collapse.isShown).toBe(false);
+ expect(el.classList.contains("hidden")).toBe(true);
+ expect(el.style.maxHeight).toBe("0px");
+ expect(el.style.overflow).toBe("hidden");
+ });
+ });
+
+ describe("show()", () => {
+ it("removes the hidden class", () => {
+ el.classList.add("hidden");
+ const collapse = new CollapseService(el, { show: false });
+ collapse.show();
+ expect(el.classList.contains("hidden")).toBe(false);
+ });
+
+ it("sets isShown to true", () => {
+ const collapse = new CollapseService(el, { show: false });
+ collapse.show();
+ expect(collapse.isShown).toBe(true);
+ expect(collapse.isCollapsed).toBe(false);
+ });
+
+ it("dispatches tw.collapse.show event", () => {
+ const collapse = new CollapseService(el, { show: false });
+ const handler = vi.fn();
+ el.addEventListener("tw.collapse.show", handler);
+ collapse.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already shown", () => {
+ const collapse = new CollapseService(el, { show: true });
+ const handler = vi.fn();
+ el.addEventListener("tw.collapse.show", handler);
+ collapse.show();
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it("dispatches tw.collapse.shown event after transition", () => {
+ vi.useFakeTimers();
+ const collapse = new CollapseService(el, { show: false });
+ const handler = vi.fn();
+ el.addEventListener("tw.collapse.shown", handler);
+ collapse.show();
+
+ // Not fired yet
+ expect(handler).not.toHaveBeenCalled();
+
+ // After transition duration (default 300ms)
+ vi.advanceTimersByTime(300);
+ expect(handler).toHaveBeenCalledTimes(1);
+ vi.useRealTimers();
+ });
+ });
+
+ describe("hide()", () => {
+ it("sets maxHeight to 0", () => {
+ const collapse = new CollapseService(el, { show: true });
+ collapse.hide();
+ expect(el.style.maxHeight).toBe("0px");
+ });
+
+ it("sets isShown to false", () => {
+ const collapse = new CollapseService(el, { show: true });
+ collapse.hide();
+ expect(collapse.isShown).toBe(false);
+ expect(collapse.isCollapsed).toBe(true);
+ });
+
+ it("dispatches tw.collapse.hide event", () => {
+ const collapse = new CollapseService(el, { show: true });
+ const handler = vi.fn();
+ el.addEventListener("tw.collapse.hide", handler);
+ collapse.hide();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already hidden", () => {
+ const collapse = new CollapseService(el, { show: false });
+ const handler = vi.fn();
+ el.addEventListener("tw.collapse.hide", handler);
+ collapse.hide();
+ expect(handler).not.toHaveBeenCalled();
+ });
+
+ it("adds hidden class after transition duration", () => {
+ vi.useFakeTimers();
+ const collapse = new CollapseService(el, { show: true });
+ collapse.hide();
+
+ // hidden class not added immediately
+ expect(el.classList.contains("hidden")).toBe(false);
+
+ vi.advanceTimersByTime(300);
+ expect(el.classList.contains("hidden")).toBe(true);
+ vi.useRealTimers();
+ });
+
+ it("dispatches tw.collapse.hidden event after transition", () => {
+ vi.useFakeTimers();
+ const collapse = new CollapseService(el, { show: true });
+ const handler = vi.fn();
+ el.addEventListener("tw.collapse.hidden", handler);
+ collapse.hide();
+
+ expect(handler).not.toHaveBeenCalled();
+ vi.advanceTimersByTime(300);
+ expect(handler).toHaveBeenCalledTimes(1);
+ vi.useRealTimers();
+ });
+ });
+
+ describe("toggle()", () => {
+ it("hides when currently shown", () => {
+ const collapse = new CollapseService(el, { show: true });
+ collapse.toggle();
+ expect(collapse.isShown).toBe(false);
+ });
+
+ it("shows when currently hidden", () => {
+ const collapse = new CollapseService(el, { show: false });
+ collapse.toggle();
+ expect(collapse.isShown).toBe(true);
+ });
+
+ it("round-trips correctly: shown -> hidden -> shown", () => {
+ const collapse = new CollapseService(el, { show: true });
+ collapse.toggle();
+ expect(collapse.isCollapsed).toBe(true);
+ collapse.toggle();
+ expect(collapse.isShown).toBe(true);
+ });
+ });
+
+ describe("isShown / isCollapsed", () => {
+ it("isShown and isCollapsed are always complementary", () => {
+ const collapse = new CollapseService(el, { show: true });
+ expect(collapse.isShown).toBe(true);
+ expect(collapse.isCollapsed).toBe(false);
+
+ collapse.hide();
+ expect(collapse.isShown).toBe(false);
+ expect(collapse.isCollapsed).toBe(true);
+ });
+ });
+
+ describe("dispose()", () => {
+ it("clears maxHeight and overflow styles", () => {
+ const collapse = new CollapseService(el, { show: false });
+ // After constructor, styles are set
+ expect(el.style.maxHeight).toBe("0px");
+ expect(el.style.overflow).toBe("hidden");
+
+ collapse.dispose();
+ expect(el.style.maxHeight).toBe("");
+ expect(el.style.overflow).toBe("");
+ });
+ });
+});
diff --git a/packages/tw/src/services/collapse.service.ts b/packages/tw/src/services/collapse.service.ts
new file mode 100644
index 000000000..ef8f04a04
--- /dev/null
+++ b/packages/tw/src/services/collapse.service.ts
@@ -0,0 +1,93 @@
+/**
+ * Pure JS collapse service — no Bootstrap dependency.
+ *
+ * Uses CSS `max-height` + `overflow: hidden` transitions.
+ * Dispatches `tw.collapse.show`, `tw.collapse.shown`,
+ * `tw.collapse.hide`, `tw.collapse.hidden` events on the element.
+ */
+export class CollapseService {
+ protected el: HTMLElement;
+ protected _isShown: boolean;
+ protected transitionDuration: number;
+
+ constructor(el: HTMLElement, options: { show?: boolean } = {}) {
+ this.el = el;
+ this.transitionDuration =
+ parseFloat(getComputedStyle(el).transitionDuration) * 1000 || 300;
+ this._isShown = options.show ?? !el.classList.contains("hidden");
+
+ // Initialize state
+ if (!this._isShown) {
+ el.style.maxHeight = "0";
+ el.style.overflow = "hidden";
+ el.classList.add("hidden");
+ }
+ }
+
+ get isShown() {
+ return this._isShown;
+ }
+
+ get isCollapsed() {
+ return !this._isShown;
+ }
+
+ show() {
+ if (this._isShown) return;
+
+ this.el.dispatchEvent(new CustomEvent("tw.collapse.show"));
+ this.el.classList.remove("hidden");
+ this.el.style.overflow = "hidden";
+
+ // Measure full height
+ this.el.style.maxHeight = "none";
+ const fullHeight = this.el.scrollHeight;
+ this.el.style.maxHeight = "0";
+
+ // Force reflow
+ void this.el.offsetHeight;
+
+ this.el.style.maxHeight = `${fullHeight}px`;
+ this._isShown = true;
+
+ setTimeout(() => {
+ this.el.style.maxHeight = "none";
+ this.el.style.overflow = "";
+ this.el.dispatchEvent(new CustomEvent("tw.collapse.shown"));
+ }, this.transitionDuration);
+ }
+
+ hide() {
+ if (!this._isShown) return;
+
+ this.el.dispatchEvent(new CustomEvent("tw.collapse.hide"));
+
+ // Set explicit height so transition works
+ this.el.style.maxHeight = `${this.el.scrollHeight}px`;
+ this.el.style.overflow = "hidden";
+
+ // Force reflow
+ void this.el.offsetHeight;
+
+ this.el.style.maxHeight = "0";
+ this._isShown = false;
+
+ setTimeout(() => {
+ this.el.classList.add("hidden");
+ this.el.dispatchEvent(new CustomEvent("tw.collapse.hidden"));
+ }, this.transitionDuration);
+ }
+
+ toggle() {
+ if (this._isShown) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ dispose() {
+ this.el.style.maxHeight = "";
+ this.el.style.overflow = "";
+ }
+}
diff --git a/packages/tw/src/services/dropdown.service.spec.ts b/packages/tw/src/services/dropdown.service.spec.ts
new file mode 100644
index 000000000..38a7dd516
--- /dev/null
+++ b/packages/tw/src/services/dropdown.service.spec.ts
@@ -0,0 +1,156 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+
+vi.mock("@floating-ui/dom", () => ({
+ computePosition: vi
+ .fn()
+ .mockResolvedValue({ x: 0, y: 0, middlewareData: {}, placement: "bottom-start" }),
+ flip: vi.fn(() => ({})),
+ shift: vi.fn(() => ({})),
+ offset: vi.fn(() => ({})),
+ autoUpdate: vi.fn(() => () => {}),
+}));
+
+import { DropdownService } from "./dropdown.service.js";
+
+describe("DropdownService", () => {
+ let trigger: HTMLElement;
+ let menu: HTMLElement;
+
+ beforeEach(() => {
+ trigger = document.createElement("button");
+ menu = document.createElement("div");
+ document.body.appendChild(trigger);
+ document.body.appendChild(menu);
+ });
+
+ afterEach(() => {
+ trigger.remove();
+ menu.remove();
+ });
+
+ describe("constructor", () => {
+ it("creates service without throwing", () => {
+ expect(() => new DropdownService(trigger, menu)).not.toThrow();
+ });
+
+ it("hides the menu initially", () => {
+ new DropdownService(trigger, menu);
+ expect(menu.style.display).toBe("none");
+ });
+ });
+
+ describe("isShown", () => {
+ it("defaults to false", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ expect(dropdown.isShown).toBe(false);
+ });
+ });
+
+ describe("show()", () => {
+ it("makes menu visible and sets isShown to true", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ expect(menu.style.display).not.toBe("none");
+ expect(dropdown.isShown).toBe(true);
+ });
+
+ it("dispatches tw.dropdown.show event", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ const handler = vi.fn();
+ trigger.addEventListener("tw.dropdown.show", handler);
+ dropdown.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("dispatches tw.dropdown.shown event", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ const handler = vi.fn();
+ trigger.addEventListener("tw.dropdown.shown", handler);
+ dropdown.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already shown", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ const handler = vi.fn();
+ trigger.addEventListener("tw.dropdown.show", handler);
+ dropdown.show();
+ expect(handler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("hide()", () => {
+ it("hides menu and sets isShown to false", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ dropdown.hide();
+ expect(menu.style.display).toBe("none");
+ expect(dropdown.isShown).toBe(false);
+ });
+
+ it("dispatches tw.dropdown.hide event", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ const handler = vi.fn();
+ trigger.addEventListener("tw.dropdown.hide", handler);
+ dropdown.hide();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("dispatches tw.dropdown.hidden event", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ const handler = vi.fn();
+ trigger.addEventListener("tw.dropdown.hidden", handler);
+ dropdown.hide();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already hidden", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ const handler = vi.fn();
+ trigger.addEventListener("tw.dropdown.hide", handler);
+ dropdown.hide();
+ expect(handler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("toggle()", () => {
+ it("shows when currently hidden", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.toggle();
+ expect(dropdown.isShown).toBe(true);
+ });
+
+ it("hides when currently shown", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ dropdown.toggle();
+ expect(dropdown.isShown).toBe(false);
+ });
+
+ it("round-trips correctly: hidden -> shown -> hidden", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.toggle();
+ expect(dropdown.isShown).toBe(true);
+ dropdown.toggle();
+ expect(dropdown.isShown).toBe(false);
+ });
+ });
+
+ describe("dispose()", () => {
+ it("hides the menu if shown", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ dropdown.show();
+ dropdown.dispose();
+ expect(dropdown.isShown).toBe(false);
+ expect(menu.style.display).toBe("none");
+ });
+
+ it("does not throw when called on an already-hidden dropdown", () => {
+ const dropdown = new DropdownService(trigger, menu);
+ expect(() => dropdown.dispose()).not.toThrow();
+ });
+ });
+});
diff --git a/packages/tw/src/services/dropdown.service.ts b/packages/tw/src/services/dropdown.service.ts
new file mode 100644
index 000000000..5852645bf
--- /dev/null
+++ b/packages/tw/src/services/dropdown.service.ts
@@ -0,0 +1,140 @@
+import {
+ computePosition,
+ flip,
+ shift,
+ offset,
+ autoUpdate,
+ type Placement,
+} from "@floating-ui/dom";
+
+/**
+ * Pure JS dropdown service — uses Floating UI for positioning.
+ *
+ * Dispatches `tw.dropdown.show`, `tw.dropdown.shown`,
+ * `tw.dropdown.hide`, `tw.dropdown.hidden` events on the trigger element.
+ */
+export class DropdownService {
+ protected trigger: HTMLElement;
+ protected menu: HTMLElement;
+ protected _isShown = false;
+ protected placement: Placement;
+ protected cleanup?: () => void;
+ protected abortController = new AbortController();
+ protected onDocumentClick = this._onDocumentClick.bind(this);
+ protected onKeydown = this._onKeydown.bind(this);
+
+ constructor(
+ trigger: HTMLElement,
+ menu: HTMLElement,
+ options: { placement?: Placement } = {},
+ ) {
+ this.trigger = trigger;
+ this.menu = menu;
+ this.placement = options.placement || "bottom-start";
+
+ // Initially hidden
+ this.menu.style.display = "none";
+
+ this.trigger.addEventListener("click", () => this.toggle(), {
+ signal: this.abortController.signal,
+ });
+ }
+
+ get isShown() {
+ return this._isShown;
+ }
+
+ show() {
+ if (this._isShown) return;
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.dropdown.show"));
+ this.menu.classList.remove("hidden");
+ this.menu.style.display = "";
+
+ // Position with Floating UI
+ this.cleanup = autoUpdate(this.trigger, this.menu, () => {
+ computePosition(this.trigger, this.menu, {
+ placement: this.placement,
+ middleware: [offset(4), flip(), shift({ padding: 8 })],
+ }).then(({ x, y }) => {
+ Object.assign(this.menu.style, {
+ left: `${x}px`,
+ top: `${y}px`,
+ });
+ });
+ });
+
+ this._isShown = true;
+
+ // Close on outside click / keyboard
+ document.addEventListener("click", this.onDocumentClick, true);
+ document.addEventListener("keydown", this.onKeydown);
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.dropdown.shown"));
+ }
+
+ hide() {
+ if (!this._isShown) return;
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.dropdown.hide"));
+
+ this.menu.classList.add("hidden");
+ this.menu.style.display = "none";
+ this.cleanup?.();
+ this.cleanup = undefined;
+ this._isShown = false;
+
+ document.removeEventListener("click", this.onDocumentClick, true);
+ document.removeEventListener("keydown", this.onKeydown);
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.dropdown.hidden"));
+ }
+
+ toggle() {
+ if (this._isShown) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ protected _onDocumentClick(event: MouseEvent) {
+ const target = event.target as Node;
+ if (!this.trigger.contains(target) && !this.menu.contains(target)) {
+ this.hide();
+ }
+ }
+
+ protected _onKeydown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ this.hide();
+ this.trigger.focus();
+ }
+
+ // Arrow key navigation within menu items
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
+ event.preventDefault();
+ const items = Array.from(
+ this.menu.querySelectorAll(
+ "[role='menuitem']:not([disabled]), a:not([disabled]), button:not([disabled])",
+ ),
+ );
+ if (items.length === 0) return;
+
+ const currentIndex = items.indexOf(document.activeElement as HTMLElement);
+ let nextIndex: number;
+ if (event.key === "ArrowDown") {
+ nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
+ } else {
+ nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
+ }
+ items[nextIndex].focus();
+ }
+ }
+
+ dispose() {
+ this.hide();
+ this.cleanup?.();
+ this.abortController.abort();
+ }
+}
diff --git a/packages/tw/src/services/index.ts b/packages/tw/src/services/index.ts
new file mode 100644
index 000000000..e30ac1769
--- /dev/null
+++ b/packages/tw/src/services/index.ts
@@ -0,0 +1,9 @@
+export { TwService } from "./tw.service.js";
+export { ThemeService } from "./theme.service.js";
+export { CollapseService } from "./collapse.service.js";
+// Re-exported from @ribajs/extras (moved for framework independence)
+export { ModalService } from "@ribajs/extras";
+export { DropdownService } from "./dropdown.service.js";
+export { ToastService } from "./toast.service.js";
+export { TooltipService } from "./tooltip.service.js";
+export { PopoverService } from "./popover.service.js";
diff --git a/packages/tw/src/services/popover.service.ts b/packages/tw/src/services/popover.service.ts
new file mode 100644
index 000000000..68918c4f5
--- /dev/null
+++ b/packages/tw/src/services/popover.service.ts
@@ -0,0 +1,151 @@
+import {
+ computePosition,
+ flip,
+ shift,
+ offset,
+ arrow as arrowMiddleware,
+ autoUpdate,
+ type Placement,
+} from "@floating-ui/dom";
+
+/**
+ * Pure JS popover service — uses Floating UI for positioning.
+ *
+ * Dispatches `tw.popover.show`, `tw.popover.shown`,
+ * `tw.popover.hide`, `tw.popover.hidden` events on the trigger element.
+ */
+export class PopoverService {
+ protected trigger: HTMLElement;
+ protected popoverEl: HTMLElement;
+ protected arrowEl: HTMLElement | null = null;
+ protected placement: Placement;
+ protected _isShown = false;
+ protected cleanup?: () => void;
+ protected abortController = new AbortController();
+ protected onDocumentClick = this._onDocumentClick.bind(this);
+
+ constructor(
+ trigger: HTMLElement,
+ popoverEl: HTMLElement,
+ options: {
+ placement?: Placement;
+ arrow?: boolean;
+ } = {},
+ ) {
+ this.trigger = trigger;
+ this.popoverEl = popoverEl;
+ this.placement = options.placement || "top";
+
+ if (options.arrow !== false) {
+ this.arrowEl = this.popoverEl.querySelector("[data-tw-arrow]");
+ }
+
+ this.popoverEl.style.display = "none";
+ this.popoverEl.classList.add(
+ "absolute",
+ "z-50",
+ "opacity-0",
+ "transition-opacity",
+ "duration-150",
+ );
+
+ this.trigger.addEventListener("click", () => this.toggle(), {
+ signal: this.abortController.signal,
+ });
+ }
+
+ get isShown() {
+ return this._isShown;
+ }
+
+ show() {
+ if (this._isShown) return;
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.popover.show"));
+ this.popoverEl.style.display = "";
+
+ const middleware = [offset(8), flip(), shift({ padding: 8 })];
+ if (this.arrowEl) {
+ middleware.push(arrowMiddleware({ element: this.arrowEl }));
+ }
+
+ this.cleanup = autoUpdate(this.trigger, this.popoverEl, () => {
+ computePosition(this.trigger, this.popoverEl, {
+ placement: this.placement,
+ middleware,
+ }).then(({ x, y, middlewareData, placement }) => {
+ Object.assign(this.popoverEl.style, {
+ left: `${x}px`,
+ top: `${y}px`,
+ });
+
+ if (this.arrowEl && middlewareData.arrow) {
+ const { x: ax, y: ay } = middlewareData.arrow;
+ const side = placement.split("-")[0];
+ const staticSide = {
+ top: "bottom",
+ right: "left",
+ bottom: "top",
+ left: "right",
+ }[side] as string;
+
+ Object.assign(this.arrowEl.style, {
+ left: ax != null ? `${ax}px` : "",
+ top: ay != null ? `${ay}px` : "",
+ [staticSide]: "-4px",
+ });
+ }
+ });
+ });
+
+ this._isShown = true;
+
+ requestAnimationFrame(() => {
+ this.popoverEl.classList.remove("opacity-0");
+ this.popoverEl.classList.add("opacity-100");
+ });
+
+ document.addEventListener("click", this.onDocumentClick, true);
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.popover.shown"));
+ }
+
+ hide() {
+ if (!this._isShown) return;
+
+ this.trigger.dispatchEvent(new CustomEvent("tw.popover.hide"));
+
+ this.popoverEl.classList.remove("opacity-100");
+ this.popoverEl.classList.add("opacity-0");
+
+ setTimeout(() => {
+ this.popoverEl.style.display = "none";
+ this.cleanup?.();
+ this.cleanup = undefined;
+ this._isShown = false;
+ document.removeEventListener("click", this.onDocumentClick, true);
+ this.trigger.dispatchEvent(new CustomEvent("tw.popover.hidden"));
+ }, 150);
+ }
+
+ toggle() {
+ if (this._isShown) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ protected _onDocumentClick(event: MouseEvent) {
+ const target = event.target as Node;
+ if (!this.trigger.contains(target) && !this.popoverEl.contains(target)) {
+ this.hide();
+ }
+ }
+
+ dispose() {
+ this.hide();
+ this.cleanup?.();
+ this.abortController.abort();
+ }
+}
diff --git a/packages/tw/src/services/theme.service.spec.ts b/packages/tw/src/services/theme.service.spec.ts
new file mode 100644
index 000000000..b8407e668
--- /dev/null
+++ b/packages/tw/src/services/theme.service.spec.ts
@@ -0,0 +1,204 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { TwService } from "./tw.service.js";
+import { ThemeService } from "./theme.service.js";
+import { DEFAULT_MODULE_OPTIONS } from "../constants/index.js";
+
+describe("ThemeService", () => {
+ let matchMediaDark: boolean;
+
+ beforeEach(() => {
+ // Reset singletons
+ (TwService as any).instance = undefined;
+ (ThemeService as any).instance = undefined;
+
+ // Clean up DOM and storage
+ document.documentElement.classList.remove("dark");
+ localStorage.clear();
+
+ // Default to light mode for matchMedia
+ matchMediaDark = false;
+
+ // Mock window.matchMedia
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ configurable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches:
+ query === "(prefers-color-scheme: dark)" ? matchMediaDark : false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+
+ // Initialize TwService first (ThemeService depends on it)
+ TwService.setSingleton({ ...DEFAULT_MODULE_OPTIONS });
+ });
+
+ function createService() {
+ return ThemeService.getSingleton();
+ }
+
+ describe("singleton", () => {
+ it("creates a singleton via getSingleton()", () => {
+ const service = createService();
+ expect(service).toBeInstanceOf(ThemeService);
+ });
+
+ it("returns the same instance on subsequent calls", () => {
+ const a = createService();
+ const b = ThemeService.getSingleton();
+ expect(a).toBe(b);
+ });
+
+ it("throws when setSingleton() is called twice", () => {
+ ThemeService.setSingleton();
+ expect(() => ThemeService.setSingleton()).toThrow(/already defined/);
+ });
+ });
+
+ describe("set()", () => {
+ it('adds "dark" class to when set to "dark"', () => {
+ const service = createService();
+ service.set("dark");
+ expect(document.documentElement.classList.contains("dark")).toBe(true);
+ });
+
+ it('removes "dark" class from when set to "light"', () => {
+ const service = createService();
+ // First set dark
+ document.documentElement.classList.add("dark");
+ service.set("light");
+ expect(document.documentElement.classList.contains("dark")).toBe(false);
+ });
+
+ it('resolves "os" to light when system preference is light', () => {
+ matchMediaDark = false;
+ const service = createService();
+ service.set("os");
+ expect(document.documentElement.classList.contains("dark")).toBe(false);
+ });
+
+ it('resolves "os" to dark when system preference is dark', () => {
+ matchMediaDark = true;
+ // Recreate singletons so matchMedia mock takes effect in init
+ (ThemeService as any).instance = undefined;
+ const service = createService();
+ service.set("os");
+ expect(document.documentElement.classList.contains("dark")).toBe(true);
+ });
+
+ it("updates the current property", () => {
+ const service = createService();
+ service.set("dark");
+ expect(service.current).toBe("dark");
+ service.set("light");
+ expect(service.current).toBe("light");
+ service.set("os");
+ expect(service.current).toBe("os");
+ });
+
+ it("returns ThemeData with choice and resolved fields", () => {
+ const service = createService();
+ const data = service.set("dark");
+ expect(data.choice).toBe("dark");
+ expect(data.resolved).toBe("dark");
+ });
+
+ it('falls back to "os" for an unsupported theme choice', () => {
+ const service = createService();
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+ const data = service.set("invalid" as any);
+ expect(data.choice).toBe("os");
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining("Unsupported theme"),
+ );
+ warnSpy.mockRestore();
+ });
+ });
+
+ describe("getThemeData()", () => {
+ it("returns the correct structure for dark", () => {
+ const service = createService();
+ service.set("dark");
+ const data = service.getThemeData();
+ expect(data).toEqual({ choice: "dark", resolved: "dark" });
+ });
+
+ it("returns the correct structure for light", () => {
+ const service = createService();
+ service.set("light");
+ const data = service.getThemeData();
+ expect(data).toEqual({ choice: "light", resolved: "light" });
+ });
+
+ it("returns the correct structure for os (light system)", () => {
+ matchMediaDark = false;
+ const service = createService();
+ service.set("os");
+ const data = service.getThemeData();
+ expect(data).toEqual({ choice: "os", resolved: "light" });
+ });
+
+ it("accepts an explicit choice parameter", () => {
+ const service = createService();
+ const data = service.getThemeData("dark");
+ expect(data).toEqual({ choice: "dark", resolved: "dark" });
+ });
+ });
+
+ describe("localStorage persistence", () => {
+ it("persists the theme choice to localStorage", () => {
+ const service = createService();
+ service.set("dark");
+ expect(localStorage.getItem("tw-theme")).toBe("dark");
+ });
+
+ it("restores the theme choice from localStorage on init", () => {
+ localStorage.setItem("tw-theme", "dark");
+ const service = createService();
+ expect(service.current).toBe("dark");
+ expect(document.documentElement.classList.contains("dark")).toBe(true);
+ });
+
+ it("defaults to os when no value is stored", () => {
+ const service = createService();
+ // The initial set in init() uses the stored value or "os"
+ expect(service.current).toBe("os");
+ });
+
+ it("does not persist when allowStoreDataInBrowser is false", () => {
+ // Reset and recreate TwService with storage disabled
+ (TwService as any).instance = undefined;
+ TwService.setSingleton({
+ ...DEFAULT_MODULE_OPTIONS,
+ allowStoreDataInBrowser: false,
+ });
+ const service = createService();
+ service.set("dark");
+ expect(localStorage.getItem("tw-theme")).toBeNull();
+ });
+ });
+
+ describe("onChange()", () => {
+ it("fires a callback when the theme changes", () => {
+ const service = createService();
+ const cb = vi.fn();
+ service.onChange(cb);
+ service.set("dark");
+ expect(cb).toHaveBeenCalledWith(
+ expect.objectContaining({
+ previous: expect.any(Object),
+ current: expect.objectContaining({
+ choice: "dark",
+ resolved: "dark",
+ }),
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/tw/src/services/theme.service.ts b/packages/tw/src/services/theme.service.ts
new file mode 100644
index 000000000..b72bf8643
--- /dev/null
+++ b/packages/tw/src/services/theme.service.ts
@@ -0,0 +1,124 @@
+import { EventDispatcher } from "@ribajs/events";
+import { TwService } from "./tw.service.js";
+
+import type {
+ ThemeChoice,
+ ThemeData,
+ ThemeChangedData,
+} from "../types/index.js";
+
+const STORAGE_KEY = "tw-theme";
+const THEME_CHOICES: ThemeChoice[] = ["os", "light", "dark"];
+
+/**
+ * Theme service for Tailwind dark mode.
+ *
+ * Uses class-based dark mode by toggling the `dark` class on ``.
+ * Persists the user's choice in localStorage.
+ */
+export class ThemeService {
+ protected eventDispatcher = EventDispatcher.getInstance();
+
+ protected static instance?: ThemeService;
+ protected tw: TwService;
+ public current: ThemeChoice = "os";
+
+ protected constructor() {
+ this.tw = TwService.getSingleton();
+ this.addEventListeners();
+ this.init();
+ }
+
+ public static getSingleton() {
+ if (ThemeService.instance) {
+ return ThemeService.instance;
+ }
+ return this.setSingleton();
+ }
+
+ public static setSingleton() {
+ if (ThemeService.instance) {
+ throw new Error(`Singleton of ThemeService already defined!`);
+ }
+ ThemeService.instance = new ThemeService();
+ return ThemeService.instance;
+ }
+
+ protected addEventListeners() {
+ window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addEventListener("change", () => {
+ this.set(this.current);
+ });
+ }
+
+ public init() {
+ let savedTheme: ThemeChoice = "os";
+ if (this.tw.options.allowStoreDataInBrowser) {
+ savedTheme = (localStorage.getItem(STORAGE_KEY) || "os") as ThemeChoice;
+ }
+ return this.set(savedTheme);
+ }
+
+ public set(newChoice: ThemeChoice): ThemeData {
+ const oldData = this.getThemeData();
+
+ if (!THEME_CHOICES.includes(newChoice)) {
+ console.warn(`Unsupported theme "${newChoice}", falling back to "os".`);
+ newChoice = "os";
+ }
+
+ if (this.tw.options.allowStoreDataInBrowser) {
+ localStorage.setItem(STORAGE_KEY, newChoice);
+ }
+
+ const resolved = this.resolveTheme(newChoice);
+
+ // Toggle the `dark` class on for Tailwind's dark: variant
+ if (resolved === "dark") {
+ document.documentElement.classList.add("dark");
+ } else {
+ document.documentElement.classList.remove("dark");
+ }
+
+ this.current = newChoice;
+ const newData = this.getThemeData(newChoice);
+ this.triggerChange(oldData, newData);
+ return newData;
+ }
+
+ public getThemeData(choice: ThemeChoice = this.current): ThemeData {
+ const resolved = this.resolveTheme(choice);
+ return { choice, resolved };
+ }
+
+ /**
+ * Resolves a theme choice to "light" or "dark".
+ * "os" → system preference, otherwise the explicit choice.
+ */
+ protected resolveTheme(choice: ThemeChoice): "light" | "dark" {
+ if (choice === "os") {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ }
+ return choice;
+ }
+
+ protected triggerChange(previous: ThemeData, current: ThemeData) {
+ const data: ThemeChangedData = { previous, current };
+ this.eventDispatcher.trigger("theme-change", data);
+ }
+
+ onChange(cb: (data: ThemeChangedData) => void, thisContext?: any): void {
+ this.eventDispatcher.on("theme-change", cb, thisContext);
+ }
+
+ onceChange(cb: (data: ThemeChangedData) => void, thisContext?: any): void {
+ this.eventDispatcher.once("theme-change", cb, thisContext);
+ }
+
+ offChange(cb?: (data: ThemeChangedData) => void, thisContext?: any): void {
+ this.eventDispatcher.off("theme-change", cb, thisContext);
+ }
+}
diff --git a/packages/tw/src/services/toast.service.spec.ts b/packages/tw/src/services/toast.service.spec.ts
new file mode 100644
index 000000000..4027e8ef3
--- /dev/null
+++ b/packages/tw/src/services/toast.service.spec.ts
@@ -0,0 +1,192 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { ToastService } from "./toast.service.js";
+
+describe("ToastService", () => {
+ let el: HTMLElement;
+
+ beforeEach(() => {
+ el = document.createElement("div");
+ document.body.appendChild(el);
+ });
+
+ afterEach(() => {
+ el.remove();
+ vi.useRealTimers();
+ });
+
+ describe("constructor", () => {
+ it("creates service without throwing", () => {
+ expect(() => new ToastService(el)).not.toThrow();
+ });
+ });
+
+ describe("isShown", () => {
+ it("defaults to false", () => {
+ const toast = new ToastService(el);
+ expect(toast.isShown).toBe(false);
+ });
+
+ it("is true when constructed with show: true", () => {
+ const toast = new ToastService(el, { show: true });
+ expect(toast.isShown).toBe(true);
+ });
+ });
+
+ describe("show()", () => {
+ it("removes the hidden class", () => {
+ el.classList.add("hidden");
+ const toast = new ToastService(el);
+ toast.show();
+ expect(el.classList.contains("hidden")).toBe(false);
+ });
+
+ it("adds the animate-fade-in class", () => {
+ const toast = new ToastService(el);
+ toast.show();
+ expect(el.classList.contains("animate-fade-in")).toBe(true);
+ });
+
+ it("sets isShown to true", () => {
+ const toast = new ToastService(el);
+ toast.show();
+ expect(toast.isShown).toBe(true);
+ });
+
+ it("dispatches tw.toast.show event", () => {
+ const toast = new ToastService(el);
+ const handler = vi.fn();
+ el.addEventListener("tw.toast.show", handler);
+ toast.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("dispatches tw.toast.shown event", () => {
+ const toast = new ToastService(el);
+ const handler = vi.fn();
+ el.addEventListener("tw.toast.shown", handler);
+ toast.show();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already shown", () => {
+ const toast = new ToastService(el);
+ toast.show();
+ const handler = vi.fn();
+ el.addEventListener("tw.toast.show", handler);
+ toast.show();
+ expect(handler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("hide()", () => {
+ it("adds animate-fade-out class", () => {
+ const toast = new ToastService(el);
+ toast.show();
+ toast.hide();
+ expect(el.classList.contains("animate-fade-out")).toBe(true);
+ });
+
+ it("eventually adds hidden class and sets isShown to false (via fallback timeout)", () => {
+ vi.useFakeTimers();
+ const toast = new ToastService(el, { autoDismiss: 0 });
+ toast.show();
+ toast.hide();
+
+ // Not yet hidden (waiting for animationend or fallback)
+ // Advance past the 500ms fallback
+ vi.advanceTimersByTime(500);
+
+ expect(el.classList.contains("hidden")).toBe(true);
+ expect(toast.isShown).toBe(false);
+ });
+
+ it("dispatches tw.toast.hide event", () => {
+ const toast = new ToastService(el, { autoDismiss: 0 });
+ toast.show();
+ const handler = vi.fn();
+ el.addEventListener("tw.toast.hide", handler);
+ toast.hide();
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("dispatches tw.toast.hidden event after animation completes", () => {
+ vi.useFakeTimers();
+ const toast = new ToastService(el, { autoDismiss: 0 });
+ toast.show();
+ const handler = vi.fn();
+ el.addEventListener("tw.toast.hidden", handler);
+ toast.hide();
+
+ // Not fired yet
+ expect(handler).not.toHaveBeenCalled();
+
+ // After fallback timeout
+ vi.advanceTimersByTime(500);
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it("does nothing when already hidden", () => {
+ const toast = new ToastService(el);
+ const handler = vi.fn();
+ el.addEventListener("tw.toast.hide", handler);
+ toast.hide();
+ expect(handler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("auto-dismiss", () => {
+ it("triggers hide after the specified delay", () => {
+ vi.useFakeTimers();
+ const toast = new ToastService(el, { autoDismiss: 2000 });
+ toast.show();
+ expect(toast.isShown).toBe(true);
+
+ // Advance to just before the auto-dismiss fires
+ vi.advanceTimersByTime(1999);
+ expect(toast.isShown).toBe(true);
+
+ // Advance past the auto-dismiss delay
+ vi.advanceTimersByTime(1);
+ // hide() was called, which adds animate-fade-out; then the 500ms fallback fires
+ vi.advanceTimersByTime(500);
+
+ expect(toast.isShown).toBe(false);
+ });
+
+ it("does not auto-dismiss when autoDismiss is 0", () => {
+ vi.useFakeTimers();
+ const toast = new ToastService(el, { autoDismiss: 0 });
+ toast.show();
+
+ vi.advanceTimersByTime(10000);
+ expect(toast.isShown).toBe(true);
+ });
+ });
+
+ describe("dispose()", () => {
+ it("adds hidden class and sets isShown to false if shown", () => {
+ const toast = new ToastService(el, { autoDismiss: 0 });
+ toast.show();
+ toast.dispose();
+ expect(el.classList.contains("hidden")).toBe(true);
+ expect(toast.isShown).toBe(false);
+ });
+
+ it("does not throw when called on an already-hidden toast", () => {
+ const toast = new ToastService(el);
+ expect(() => toast.dispose()).not.toThrow();
+ });
+
+ it("clears auto-dismiss timeout", () => {
+ vi.useFakeTimers();
+ const toast = new ToastService(el, { autoDismiss: 1000 });
+ toast.show();
+ toast.dispose();
+
+ // Advance past the auto-dismiss delay — nothing should happen
+ vi.advanceTimersByTime(2000);
+ // dispose already set isShown to false; no further side effects
+ expect(toast.isShown).toBe(false);
+ });
+ });
+});
diff --git a/packages/tw/src/services/toast.service.ts b/packages/tw/src/services/toast.service.ts
new file mode 100644
index 000000000..ac27430bf
--- /dev/null
+++ b/packages/tw/src/services/toast.service.ts
@@ -0,0 +1,86 @@
+/**
+ * Pure JS toast service — auto-dismiss with CSS transitions.
+ *
+ * Dispatches `tw.toast.show`, `tw.toast.shown`,
+ * `tw.toast.hide`, `tw.toast.hidden` events on the element.
+ */
+export class ToastService {
+ protected el: HTMLElement;
+ protected _isShown = false;
+ protected autoDismissTimeout?: ReturnType;
+ protected autoDismissDelay: number;
+
+ constructor(
+ el: HTMLElement,
+ options: { autoDismiss?: number; show?: boolean } = {},
+ ) {
+ this.el = el;
+ this.autoDismissDelay = options.autoDismiss ?? 5000;
+
+ if (options.show) {
+ this.show();
+ }
+ }
+
+ get isShown() {
+ return this._isShown;
+ }
+
+ show() {
+ if (this._isShown) return;
+
+ this.el.dispatchEvent(new CustomEvent("tw.toast.show"));
+ this.el.classList.remove("hidden");
+ this.el.classList.add("animate-fade-in");
+ this._isShown = true;
+
+ this.el.dispatchEvent(new CustomEvent("tw.toast.shown"));
+
+ if (this.autoDismissDelay > 0) {
+ this.autoDismissTimeout = setTimeout(() => {
+ this.hide();
+ }, this.autoDismissDelay);
+ }
+ }
+
+ hide() {
+ if (!this._isShown) return;
+
+ this.el.dispatchEvent(new CustomEvent("tw.toast.hide"));
+ this.clearAutoDismiss();
+
+ this.el.classList.add("animate-fade-out");
+
+ const onEnd = () => {
+ this.el.classList.add("hidden");
+ this.el.classList.remove("animate-fade-in", "animate-fade-out");
+ this._isShown = false;
+ this.el.dispatchEvent(new CustomEvent("tw.toast.hidden"));
+ this.el.removeEventListener("animationend", onEnd);
+ };
+
+ this.el.addEventListener("animationend", onEnd, { once: true });
+
+ // Fallback if no animation is defined
+ setTimeout(() => {
+ if (this._isShown) {
+ onEnd();
+ }
+ }, 500);
+ }
+
+ protected clearAutoDismiss() {
+ if (this.autoDismissTimeout) {
+ clearTimeout(this.autoDismissTimeout);
+ this.autoDismissTimeout = undefined;
+ }
+ }
+
+ dispose() {
+ this.clearAutoDismiss();
+ if (this._isShown) {
+ this.el.classList.add("hidden");
+ this._isShown = false;
+ }
+ }
+}
diff --git a/packages/tw/src/services/tooltip.service.ts b/packages/tw/src/services/tooltip.service.ts
new file mode 100644
index 000000000..88111d1a2
--- /dev/null
+++ b/packages/tw/src/services/tooltip.service.ts
@@ -0,0 +1,170 @@
+import {
+ computePosition,
+ flip,
+ shift,
+ offset,
+ arrow as arrowMiddleware,
+ type Placement,
+} from "@floating-ui/dom";
+
+/**
+ * Pure JS tooltip service — uses Floating UI for positioning.
+ */
+export class TooltipService {
+ protected trigger: HTMLElement;
+ protected tooltipEl: HTMLElement;
+ protected arrowEl: HTMLElement | null = null;
+ protected placement: Placement;
+ protected _isShown = false;
+ protected showDelay: number;
+ protected hideDelay: number;
+ protected showTimeout?: ReturnType;
+ protected hideTimeout?: ReturnType;
+ protected abortController = new AbortController();
+
+ constructor(
+ trigger: HTMLElement,
+ options: {
+ content?: string;
+ placement?: Placement;
+ showDelay?: number;
+ hideDelay?: number;
+ } = {},
+ ) {
+ this.trigger = trigger;
+ this.placement = options.placement || "top";
+ this.showDelay = options.showDelay ?? 0;
+ this.hideDelay = options.hideDelay ?? 0;
+
+ // Create tooltip element
+ this.tooltipEl = document.createElement("div");
+ this.tooltipEl.role = "tooltip";
+ this.tooltipEl.className =
+ "absolute z-50 rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-lg dark:bg-gray-700 pointer-events-none opacity-0 transition-opacity duration-150";
+ this.tooltipEl.textContent = options.content || "";
+
+ // Create arrow
+ this.arrowEl = document.createElement("div");
+ this.arrowEl.className =
+ "absolute h-2 w-2 rotate-45 bg-gray-900 dark:bg-gray-700";
+ this.tooltipEl.appendChild(this.arrowEl);
+
+ this.tooltipEl.style.display = "none";
+ document.body.appendChild(this.tooltipEl);
+
+ const signal = this.abortController.signal;
+ this.trigger.addEventListener("mouseenter", () => this.scheduleShow(), {
+ signal,
+ });
+ this.trigger.addEventListener("mouseleave", () => this.scheduleHide(), {
+ signal,
+ });
+ this.trigger.addEventListener("focus", () => this.scheduleShow(), {
+ signal,
+ });
+ this.trigger.addEventListener("blur", () => this.scheduleHide(), {
+ signal,
+ });
+ }
+
+ get isShown() {
+ return this._isShown;
+ }
+
+ set content(value: string) {
+ // Preserve arrow element
+ const textNode = this.tooltipEl.firstChild;
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
+ textNode.textContent = value;
+ } else {
+ this.tooltipEl.insertBefore(
+ document.createTextNode(value),
+ this.tooltipEl.firstChild,
+ );
+ }
+ }
+
+ protected scheduleShow() {
+ this.clearTimeouts();
+ this.showTimeout = setTimeout(() => this.show(), this.showDelay);
+ }
+
+ protected scheduleHide() {
+ this.clearTimeouts();
+ this.hideTimeout = setTimeout(() => this.hide(), this.hideDelay);
+ }
+
+ protected clearTimeouts() {
+ if (this.showTimeout) {
+ clearTimeout(this.showTimeout);
+ this.showTimeout = undefined;
+ }
+ if (this.hideTimeout) {
+ clearTimeout(this.hideTimeout);
+ this.hideTimeout = undefined;
+ }
+ }
+
+ show() {
+ if (this._isShown) return;
+
+ this.tooltipEl.style.display = "";
+ this._isShown = true;
+
+ const middleware = [offset(8), flip(), shift({ padding: 8 })];
+ if (this.arrowEl) {
+ middleware.push(arrowMiddleware({ element: this.arrowEl }));
+ }
+
+ computePosition(this.trigger, this.tooltipEl, {
+ placement: this.placement,
+ middleware,
+ }).then(({ x, y, middlewareData, placement }) => {
+ Object.assign(this.tooltipEl.style, {
+ left: `${x}px`,
+ top: `${y}px`,
+ });
+
+ if (this.arrowEl && middlewareData.arrow) {
+ const { x: ax, y: ay } = middlewareData.arrow;
+ const side = placement.split("-")[0];
+ const staticSide = {
+ top: "bottom",
+ right: "left",
+ bottom: "top",
+ left: "right",
+ }[side] as string;
+
+ Object.assign(this.arrowEl.style, {
+ left: ax != null ? `${ax}px` : "",
+ top: ay != null ? `${ay}px` : "",
+ [staticSide]: "-4px",
+ });
+ }
+ });
+
+ // Fade in
+ requestAnimationFrame(() => {
+ this.tooltipEl.classList.remove("opacity-0");
+ this.tooltipEl.classList.add("opacity-100");
+ });
+ }
+
+ hide() {
+ if (!this._isShown) return;
+
+ this.tooltipEl.classList.remove("opacity-100");
+ this.tooltipEl.classList.add("opacity-0");
+
+ setTimeout(() => {
+ this.tooltipEl.style.display = "none";
+ this._isShown = false;
+ }, 150);
+ }
+
+ dispose() {
+ this.clearTimeouts();
+ this.abortController.abort();
+ this.tooltipEl.remove();
+ }
+}
diff --git a/packages/tw/src/services/tw.service.spec.ts b/packages/tw/src/services/tw.service.spec.ts
new file mode 100644
index 000000000..fc6369402
--- /dev/null
+++ b/packages/tw/src/services/tw.service.spec.ts
@@ -0,0 +1,233 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { TwService } from "./tw.service.js";
+import { DEFAULT_MODULE_OPTIONS } from "../constants/index.js";
+
+describe("TwService", () => {
+ beforeEach(() => {
+ // Reset the singleton between tests
+ (TwService as any).instance = undefined;
+ });
+
+ function createService() {
+ return TwService.setSingleton({ ...DEFAULT_MODULE_OPTIONS });
+ }
+
+ describe("singleton", () => {
+ it("creates a singleton with setSingleton()", () => {
+ const service = createService();
+ expect(service).toBeInstanceOf(TwService);
+ });
+
+ it("returns the same instance from getSingleton()", () => {
+ const service = createService();
+ expect(TwService.getSingleton()).toBe(service);
+ });
+
+ it("throws when getSingleton() is called before setSingleton()", () => {
+ expect(() => TwService.getSingleton()).toThrow();
+ });
+
+ it("throws when setSingleton() is called twice", () => {
+ createService();
+ expect(() => TwService.setSingleton()).toThrow(
+ /already defined/,
+ );
+ });
+ });
+
+ describe("breakpointNames", () => {
+ it("returns the names of all breakpoints in order", () => {
+ const service = createService();
+ expect(service.breakpointNames).toEqual([
+ "sm",
+ "md",
+ "lg",
+ "xl",
+ "2xl",
+ ]);
+ });
+ });
+
+ describe("getBreakpointByDimension()", () => {
+ it("returns null for a width smaller than the smallest breakpoint", () => {
+ const service = createService();
+ // 500 < 640 (sm), so no breakpoint matched
+ expect(service.getBreakpointByDimension(500)).toBeNull();
+ });
+
+ it("returns sm for width 640 (exactly at sm)", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(640);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("sm");
+ });
+
+ it("returns sm for width 700 (between sm and md)", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(700);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("sm");
+ });
+
+ it("returns md for width 800 (between md and lg)", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(800);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("md");
+ });
+
+ it("returns lg for width 1100 (between lg and xl)", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(1100);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("lg");
+ });
+
+ it("returns xl for width 1300 (between xl and 2xl)", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(1300);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("xl");
+ });
+
+ it("returns 2xl for width 1600 (above 2xl)", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(1600);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("2xl");
+ });
+
+ it("returns 2xl for width exactly at 1536", () => {
+ const service = createService();
+ const bp = service.getBreakpointByDimension(1536);
+ expect(bp).not.toBeNull();
+ expect(bp!.name).toBe("2xl");
+ });
+ });
+
+ describe("getBreakpointByName()", () => {
+ it("returns the breakpoint object for a valid name", () => {
+ const service = createService();
+ const bp = service.getBreakpointByName("md");
+ expect(bp).not.toBeNull();
+ expect(bp!.dimension).toBe(768);
+ });
+
+ it("returns null for an unknown name", () => {
+ const service = createService();
+ expect(service.getBreakpointByName("xxl")).toBeNull();
+ });
+
+ it("returns the correct object for each known breakpoint name", () => {
+ const service = createService();
+ const expected: Record = {
+ sm: 640,
+ md: 768,
+ lg: 1024,
+ xl: 1280,
+ "2xl": 1536,
+ };
+ for (const [name, dim] of Object.entries(expected)) {
+ const bp = service.getBreakpointByName(name);
+ expect(bp).not.toBeNull();
+ expect(bp!.dimension).toBe(dim);
+ }
+ });
+ });
+
+ describe("getNextBreakpointByName()", () => {
+ it("returns the next breakpoint name", () => {
+ const service = createService();
+ expect(service.getNextBreakpointByName("sm")).toBe("md");
+ expect(service.getNextBreakpointByName("md")).toBe("lg");
+ expect(service.getNextBreakpointByName("lg")).toBe("xl");
+ expect(service.getNextBreakpointByName("xl")).toBe("2xl");
+ });
+
+ it("returns null for the last breakpoint", () => {
+ const service = createService();
+ expect(service.getNextBreakpointByName("2xl")).toBeNull();
+ });
+
+ it("throws for an unknown breakpoint name", () => {
+ const service = createService();
+ expect(() => service.getNextBreakpointByName("xxl")).toThrow(
+ /does not exist/,
+ );
+ });
+ });
+
+ describe("getPrevBreakpointByName()", () => {
+ it("returns the previous breakpoint name", () => {
+ const service = createService();
+ expect(service.getPrevBreakpointByName("2xl")).toBe("xl");
+ expect(service.getPrevBreakpointByName("xl")).toBe("lg");
+ expect(service.getPrevBreakpointByName("lg")).toBe("md");
+ expect(service.getPrevBreakpointByName("md")).toBe("sm");
+ });
+
+ it("returns null for the first breakpoint", () => {
+ const service = createService();
+ expect(service.getPrevBreakpointByName("sm")).toBeNull();
+ });
+
+ it("throws for an unknown breakpoint name", () => {
+ const service = createService();
+ expect(() => service.getPrevBreakpointByName("xxl")).toThrow(
+ /does not exist/,
+ );
+ });
+ });
+
+ describe("isBreakpointGreaterThan()", () => {
+ it("returns true when the first breakpoint is larger", () => {
+ const service = createService();
+ expect(service.isBreakpointGreaterThan("lg", "sm")).toBe(true);
+ });
+
+ it("returns false when the first breakpoint is smaller", () => {
+ const service = createService();
+ expect(service.isBreakpointGreaterThan("sm", "lg")).toBe(false);
+ });
+
+ it("returns false when comparing the same breakpoint", () => {
+ const service = createService();
+ expect(service.isBreakpointGreaterThan("md", "md")).toBe(false);
+ });
+
+ it("returns null when one of the breakpoints does not exist", () => {
+ const service = createService();
+ expect(service.isBreakpointGreaterThan("xxl", "sm")).toBeNull();
+ });
+ });
+
+ describe("isBreakpointSmallerThan()", () => {
+ it("returns true when the first breakpoint is smaller", () => {
+ const service = createService();
+ expect(service.isBreakpointSmallerThan("sm", "lg")).toBe(true);
+ });
+
+ it("returns false when the first breakpoint is larger", () => {
+ const service = createService();
+ expect(service.isBreakpointSmallerThan("lg", "sm")).toBe(false);
+ });
+
+ it("returns false when comparing the same breakpoint", () => {
+ const service = createService();
+ expect(service.isBreakpointSmallerThan("md", "md")).toBe(false);
+ });
+
+ it("returns null when one of the breakpoints does not exist", () => {
+ const service = createService();
+ expect(service.isBreakpointSmallerThan("sm", "xxl")).toBeNull();
+ });
+ });
+
+ describe("options", () => {
+ it("exposes the options passed at construction", () => {
+ const service = createService();
+ expect(service.options.breakpoints).toHaveLength(5);
+ expect(service.options.allowStoreDataInBrowser).toBe(true);
+ });
+ });
+});
diff --git a/packages/tw/src/services/tw.service.ts b/packages/tw/src/services/tw.service.ts
new file mode 100644
index 000000000..bc8d1b927
--- /dev/null
+++ b/packages/tw/src/services/tw.service.ts
@@ -0,0 +1,222 @@
+import { Breakpoint, TwModuleOptions } from "../types/index.js";
+import { DEFAULT_MODULE_OPTIONS } from "../constants/index.js";
+import { debounce } from "@ribajs/utils/src/control.js";
+import { getViewportDimensions } from "@ribajs/utils/src/dom.js";
+import { EventDispatcher, EventCallback } from "@ribajs/events";
+
+/**
+ * Tailwind Service — breakpoint management singleton.
+ *
+ * Events:
+ * - `breakpoint:changed` — fired when the active breakpoint changes
+ */
+export class TwService {
+ protected _options: TwModuleOptions = DEFAULT_MODULE_OPTIONS;
+ protected _events = EventDispatcher.getInstance("tw");
+ protected _activeBreakpoint: Breakpoint | null = null;
+
+ public static instance?: TwService;
+
+ protected constructor(options: TwModuleOptions) {
+ this._options = options;
+ this.sortBreakpoints(this._options.breakpoints);
+ this._onViewChanges();
+ this.addEventListeners();
+ }
+
+ public static getSingleton() {
+ if (TwService.instance) {
+ return TwService.instance;
+ }
+
+ throw new Error(
+ "Singleton of TwService not defined, please call `TwService.setSingleton` or `twModule.init` first!",
+ );
+ }
+
+ public static setSingleton(
+ options: TwModuleOptions = DEFAULT_MODULE_OPTIONS,
+ ) {
+ if (TwService.instance) {
+ throw new Error(`Singleton of TwService already defined!`);
+ }
+ TwService.instance = new TwService(options);
+ return TwService.instance;
+ }
+
+ protected onBreakpointChanges() {
+ this._events.trigger("breakpoint:changed", this.activeBreakpoint);
+ }
+
+ protected setActiveBreakpoint(breakpoint: Breakpoint) {
+ if (breakpoint && breakpoint.name !== this.activeBreakpoint?.name) {
+ this._activeBreakpoint = breakpoint;
+ this.onBreakpointChanges();
+ }
+ }
+
+ protected addEventListeners() {
+ window.addEventListener("resize", this.onViewChanges, { passive: true });
+ }
+
+ protected removeEventListeners() {
+ window.removeEventListener("resize", this.onViewChanges);
+ }
+
+ protected _onViewChanges() {
+ const vp = getViewportDimensions();
+ const newBreakpoint = this.getBreakpointByDimension(vp.w);
+ if (newBreakpoint) {
+ this.setActiveBreakpoint(newBreakpoint);
+ }
+ }
+
+ protected onViewChanges = debounce(this._onViewChanges.bind(this));
+
+ public sortBreakpoints(breakpoints: Breakpoint[]) {
+ breakpoints.sort((a, b) => a.dimension - b.dimension);
+ }
+
+ public get options() {
+ return this._options;
+ }
+
+ public get activeBreakpoint() {
+ return this._activeBreakpoint;
+ }
+
+ public get breakpointNames() {
+ return this.options.breakpoints.map((bp) => bp.name);
+ }
+
+ public get events() {
+ return this._events;
+ }
+
+ public on(
+ eventName: "breakpoint:changed",
+ cb: (activeBreakpoint: Breakpoint) => void,
+ thisContext?: any,
+ ): void;
+ public on(eventName: string, cb: EventCallback, thisContext?: any) {
+ return this.events.on(eventName, cb, thisContext);
+ }
+
+ public once(
+ eventName: "breakpoint:changed",
+ cb: (activeBreakpoint: Breakpoint) => void,
+ thisContext?: any,
+ ): void;
+ public once(eventName: string, cb: EventCallback, thisContext?: any) {
+ return this.events.once(eventName, cb, thisContext);
+ }
+
+ public off(
+ eventName: "breakpoint:changed",
+ cb: (activeBreakpoint: Breakpoint) => void,
+ thisContext?: any,
+ ): void;
+ public off(eventName: string, cb: EventCallback, thisContext?: any) {
+ return this.events.off(eventName, cb, thisContext);
+ }
+
+ /**
+ * Get breakpoint for width.
+ * Uses mobile-first logic: returns the largest breakpoint whose dimension <= the given width.
+ */
+ public getBreakpointByDimension(
+ dimension: number,
+ breakpoints?: Breakpoint[],
+ ): Breakpoint | null {
+ breakpoints = breakpoints || this.options.breakpoints;
+
+ let matched: Breakpoint | null = null;
+ for (const bp of breakpoints) {
+ if (dimension >= bp.dimension) {
+ matched = bp;
+ } else {
+ break;
+ }
+ }
+ return matched;
+ }
+
+ /**
+ * Get breakpoint by name.
+ */
+ public getBreakpointByName(
+ name: string,
+ breakpoints?: Breakpoint[],
+ ): Breakpoint | null {
+ breakpoints = breakpoints || this.options.breakpoints;
+ return breakpoints.find((bp) => bp.name === name) || null;
+ }
+
+ public getNextBreakpointByName(name: string): string | null {
+ const names = this.breakpointNames;
+ const index = names.indexOf(name);
+ if (index < 0) {
+ throw new Error(`The breakpoint "${name}" does not exist!`);
+ }
+ if (index === names.length - 1) {
+ return null;
+ }
+ return names[index + 1];
+ }
+
+ public getPrevBreakpointByName(name: string): string | null {
+ const names = this.breakpointNames;
+ const index = names.indexOf(name);
+ if (index < 0) {
+ throw new Error(`The breakpoint "${name}" does not exist!`);
+ }
+ if (index === 0) {
+ return null;
+ }
+ return names[index - 1];
+ }
+
+ public isBreakpointGreaterThan(
+ isName: string,
+ compareName: string,
+ ): boolean | null {
+ const a = this.getBreakpointByName(isName);
+ const b = this.getBreakpointByName(compareName);
+ if (a && b) {
+ return a.dimension > b.dimension;
+ }
+ return null;
+ }
+
+ public isBreakpointSmallerThan(
+ isName: string,
+ compareName: string,
+ ): boolean | null {
+ const a = this.getBreakpointByName(isName);
+ const b = this.getBreakpointByName(compareName);
+ if (a && b) {
+ return a.dimension < b.dimension;
+ }
+ return null;
+ }
+
+ public isActiveBreakpointGreaterThan(compareName: string): boolean | null {
+ if (!this.activeBreakpoint) {
+ return null;
+ }
+ return this.isBreakpointGreaterThan(
+ this.activeBreakpoint.name,
+ compareName,
+ );
+ }
+
+ public isActiveBreakpointSmallerThan(compareName: string): boolean | null {
+ if (!this.activeBreakpoint) {
+ return null;
+ }
+ return this.isBreakpointSmallerThan(
+ this.activeBreakpoint.name,
+ compareName,
+ );
+ }
+}
diff --git a/packages/tw/src/tw.module.ts b/packages/tw/src/tw.module.ts
new file mode 100644
index 000000000..1bd412270
--- /dev/null
+++ b/packages/tw/src/tw.module.ts
@@ -0,0 +1,26 @@
+import { RibaModule } from "@ribajs/core";
+import { extend } from "@ribajs/utils/src/type.js";
+import { TwModuleOptions } from "./types/index.js";
+
+import * as binders from "./binders/index.js";
+import * as components from "./components/index.js";
+import * as formatters from "./formatters/index.js";
+import * as services from "./services/index.js";
+import * as constants from "./constants/index.js";
+
+export const twModule: RibaModule> = {
+ binders,
+ services,
+ formatters,
+ components,
+ constants,
+ init(partialOptions = {}) {
+ const options = extend(
+ { deep: true, keepValues: true },
+ partialOptions,
+ constants.DEFAULT_MODULE_OPTIONS,
+ ) as TwModuleOptions;
+ services.TwService.setSingleton(options);
+ return this;
+ },
+};
diff --git a/packages/tw/src/types/breakpoint.ts b/packages/tw/src/types/breakpoint.ts
new file mode 100644
index 000000000..c9dad07a3
--- /dev/null
+++ b/packages/tw/src/types/breakpoint.ts
@@ -0,0 +1,4 @@
+export interface Breakpoint {
+ name: string;
+ dimension: number;
+}
diff --git a/packages/tw/src/types/index.ts b/packages/tw/src/types/index.ts
new file mode 100644
index 000000000..465bd2485
--- /dev/null
+++ b/packages/tw/src/types/index.ts
@@ -0,0 +1,4 @@
+export * from "./breakpoint.js";
+export * from "./module-options.js";
+export * from "./theme.js";
+export * from "./notification.js";
diff --git a/packages/tw/src/types/module-options.ts b/packages/tw/src/types/module-options.ts
new file mode 100644
index 000000000..13dd2b1c8
--- /dev/null
+++ b/packages/tw/src/types/module-options.ts
@@ -0,0 +1,6 @@
+import type { Breakpoint } from "./breakpoint.js";
+
+export interface TwModuleOptions {
+ breakpoints: Breakpoint[];
+ allowStoreDataInBrowser: boolean;
+}
diff --git a/packages/tw/src/types/notification.ts b/packages/tw/src/types/notification.ts
new file mode 100644
index 000000000..b9733a276
--- /dev/null
+++ b/packages/tw/src/types/notification.ts
@@ -0,0 +1,5 @@
+export type {
+ Notification,
+ ModalNotification,
+ ToastNotification,
+} from "@ribajs/extras/src/types/notification.js";
diff --git a/packages/tw/src/types/theme.ts b/packages/tw/src/types/theme.ts
new file mode 100644
index 000000000..5653c3221
--- /dev/null
+++ b/packages/tw/src/types/theme.ts
@@ -0,0 +1,11 @@
+export type ThemeChoice = "os" | "light" | "dark";
+
+export interface ThemeData {
+ choice: ThemeChoice;
+ resolved: "light" | "dark";
+}
+
+export interface ThemeChangedData {
+ previous: ThemeData;
+ current: ThemeData;
+}
diff --git a/packages/tw/tsconfig.json b/packages/tw/tsconfig.json
new file mode 100644
index 000000000..54e6f06b8
--- /dev/null
+++ b/packages/tw/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "@ribajs/tsconfig/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "types": ["jest"]
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist/**/*.d.ts", "src/**/*.spec.ts"]
+}
diff --git a/packages/utils/package.json b/packages/utils/package.json
index fdc2e47d3..ee906925b 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -55,8 +55,8 @@
"@ribajs/eslint-config": "workspace:^",
"@ribajs/tsconfig": "workspace:^",
"@types/jest": "^30.0.0",
- "@types/node": "^24.12.0",
- "eslint": "^10.1.0",
+ "@types/node": "^24.12.2",
+ "eslint": "^10.2.0",
"typescript": "6.0.2"
}
}
diff --git a/yarn.lock b/yarn.lock
index 649c4715e..db59f5f2d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -27,6 +27,37 @@ __metadata:
languageName: node
linkType: hard
+"@asamuzakjp/css-color@npm:^5.1.5":
+ version: 5.1.10
+ resolution: "@asamuzakjp/css-color@npm:5.1.10"
+ dependencies:
+ "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-parser-algorithms": "npm:^4.0.0"
+ "@csstools/css-tokenizer": "npm:^4.0.0"
+ checksum: 10/d4b482362d7246009cbfe804cd44c2d218d1ffa61b03348bb4f68e9e1263ac058003fc55174eee422f2dea4101e6cea3809c1a26d6b1ddaa975a824e647c5b5d
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/dom-selector@npm:^7.0.6":
+ version: 7.0.9
+ resolution: "@asamuzakjp/dom-selector@npm:7.0.9"
+ dependencies:
+ "@asamuzakjp/nwsapi": "npm:^2.3.9"
+ bidi-js: "npm:^1.0.3"
+ css-tree: "npm:^3.2.1"
+ is-potential-custom-element-name: "npm:^1.0.1"
+ checksum: 10/1c024c5d75999887adf0649443c9fd322840d500488144b2d0485b31031c535e609342ad8951bbe9b70c1b1e24058e5c1e3c6711dc4d9bbc5c306d3270813e27
+ languageName: node
+ linkType: hard
+
+"@asamuzakjp/nwsapi@npm:^2.3.9":
+ version: 2.3.9
+ resolution: "@asamuzakjp/nwsapi@npm:2.3.9"
+ checksum: 10/95a6d1c102e1117fe818da087fcc5b914d23e0699855991bae50b891435dd1945ad7d384198f8bcf616207fd85b7ec32e3db6b96e9309d84c6903b8dc4151e34
+ languageName: node
+ linkType: hard
+
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0":
version: 7.29.0
resolution: "@babel/code-frame@npm:7.29.0"
@@ -403,6 +434,17 @@ __metadata:
languageName: node
linkType: hard
+"@bramus/specificity@npm:^2.4.2":
+ version: 2.4.2
+ resolution: "@bramus/specificity@npm:2.4.2"
+ dependencies:
+ css-tree: "npm:^3.0.0"
+ bin:
+ specificity: bin/cli.js
+ checksum: 10/4255ed6ff12f7db9ec3c21acfd0da2327d30ec29deb199345810cdcad992618f40039c5483eefeb665913bffbc80b690e9f1b954fbbbfa93480c6a22f9c3a69c
+ languageName: node
+ linkType: hard
+
"@csstools/cascade-layer-name-parser@npm:^3.0.0":
version: 3.0.0
resolution: "@csstools/cascade-layer-name-parser@npm:3.0.0"
@@ -447,6 +489,16 @@ __metadata:
languageName: node
linkType: hard
+"@csstools/css-calc@npm:^3.2.0":
+ version: 3.2.0
+ resolution: "@csstools/css-calc@npm:3.2.0"
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^4.0.0
+ "@csstools/css-tokenizer": ^4.0.0
+ checksum: 10/7eec51a21945a74aa6a407d1e6290d0f4c5d01829a42c01a56ce2055216398540cc3120837b15a0db38601bcb40cf97f1d991fefb3ee9d00d9cec03d67beba4c
+ languageName: node
+ linkType: hard
+
"@csstools/css-color-parser@npm:^3.0.9":
version: 3.1.0
resolution: "@csstools/css-color-parser@npm:3.1.0"
@@ -473,6 +525,19 @@ __metadata:
languageName: node
linkType: hard
+"@csstools/css-color-parser@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "@csstools/css-color-parser@npm:4.1.0"
+ dependencies:
+ "@csstools/color-helpers": "npm:^6.0.2"
+ "@csstools/css-calc": "npm:^3.2.0"
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^4.0.0
+ "@csstools/css-tokenizer": ^4.0.0
+ checksum: 10/794508011a95ebac3856e67e0333ca4174604d2dfddc101d991f2ebfd52b3c99cd36a08462675c2a07d057ca3787187fcd7eac98bced2eefdd9040b37853426d
+ languageName: node
+ linkType: hard
+
"@csstools/css-parser-algorithms@npm:^3.0.4":
version: 3.0.5
resolution: "@csstools/css-parser-algorithms@npm:3.0.5"
@@ -491,6 +556,18 @@ __metadata:
languageName: node
linkType: hard
+"@csstools/css-syntax-patches-for-csstree@npm:^1.1.1":
+ version: 1.1.3
+ resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.3"
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+ checksum: 10/1c91dc03b64ca913eed5064ca0e434da1c0be8def6ce20f932d1db10f9b478ac3830c99a033b0edf75954cf9164c7c267b220ed9faffbc3342bf320870c3bb4b
+ languageName: node
+ linkType: hard
+
"@csstools/css-tokenizer@npm:^3.0.3":
version: 3.0.4
resolution: "@csstools/css-tokenizer@npm:3.0.4"
@@ -515,18 +592,18 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-alpha-function@npm:^2.0.3":
- version: 2.0.3
- resolution: "@csstools/postcss-alpha-function@npm:2.0.3"
+"@csstools/postcss-alpha-function@npm:^2.0.4":
+ version: 2.0.4
+ resolution: "@csstools/postcss-alpha-function@npm:2.0.4"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/2f1c31ddec0001f197798f9ec5b1a2cd12ae5822c761e43f77300ca5011a089b0469327a5c01d76735f356cbc1fa40cd1da404d5f2d56e456140c591be3b639d
+ checksum: 10/6aaef65589dfc9608c4e32b97b4d25d6d473c78bd9af5fc5caece521dd7543612bb15dc5fa089969177ce9323c469113501d72e260ffca890bacd11de53e2df6
languageName: node
linkType: hard
@@ -542,63 +619,63 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-color-function-display-p3-linear@npm:^2.0.2":
- version: 2.0.2
- resolution: "@csstools/postcss-color-function-display-p3-linear@npm:2.0.2"
+"@csstools/postcss-color-function-display-p3-linear@npm:^2.0.3":
+ version: 2.0.3
+ resolution: "@csstools/postcss-color-function-display-p3-linear@npm:2.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/8a1ece9067546c135a552f822b8b255ba09224cc0b01fe04f411caec13916a905d49e6c18a36e0f449eccffecd41ea280d5ef93479887c1a7cfdd0698ed10691
+ checksum: 10/76ddb89455ef869f39c52c66ec6427796e555af566fbddc5655d9c870786184723c8ed04645111e6e657944e006f49b95fe05eac1449134d174970811bce2286
languageName: node
linkType: hard
-"@csstools/postcss-color-function@npm:^5.0.2":
- version: 5.0.2
- resolution: "@csstools/postcss-color-function@npm:5.0.2"
+"@csstools/postcss-color-function@npm:^5.0.3":
+ version: 5.0.3
+ resolution: "@csstools/postcss-color-function@npm:5.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/7fb789dcbb5290a63d20a412bbe603706b474f4b0d222a0ab2dfba2bfe957a04c303d3b47c61bb1bd815fca5ed97f4494cdd2bb488b9f69b7df0adc4227d68a8
+ checksum: 10/c773500ed98f33b850d2f7c0d4cfa0eeb7ba43d2f9c9bea2e9951b5af563b86396d8de3c7f5120d2f34eb03a7d250747ac14ec1494fb1ed7b4d67a99928c07f6
languageName: node
linkType: hard
-"@csstools/postcss-color-mix-function@npm:^4.0.2":
- version: 4.0.2
- resolution: "@csstools/postcss-color-mix-function@npm:4.0.2"
+"@csstools/postcss-color-mix-function@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "@csstools/postcss-color-mix-function@npm:4.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/8ca3690e994ce37478dbdf3121c109f5c990ab3e11b8e9625fbd228d2a0efa64d63f55b3e4fea5b64343cb1d299d829ef4d00c2cfb9ea8407da7f83ce11ad2e4
+ checksum: 10/90ef3df3fcb0fe2f882bac792b4a1fd5100fb1b8e904495959cd9ce41cddf691ccf1e1687e907e213100add92e9ccf81aba447a7023c52ae4e6bdca02ece5ad1
languageName: node
linkType: hard
-"@csstools/postcss-color-mix-variadic-function-arguments@npm:^2.0.2":
- version: 2.0.2
- resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:2.0.2"
+"@csstools/postcss-color-mix-variadic-function-arguments@npm:^2.0.3":
+ version: 2.0.3
+ resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:2.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/52aab5cc5896e35e95eaae8be8738317aa76727f3cf4ea867e48d2b00b31ba16501267b3dacb5ec9268c3316f99c1df236d07a09fd11e177daa7765d817123a5
+ checksum: 10/f5122cdda1c5937770b1895b38054bde24afba669c7c4bbda97d5b9ea89e7d188bb5066ed093614529d28ec8da3123db1cb5d9732905de8d1d4da60fd17252f5
languageName: node
linkType: hard
@@ -616,31 +693,31 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-contrast-color-function@npm:^3.0.2":
- version: 3.0.2
- resolution: "@csstools/postcss-contrast-color-function@npm:3.0.2"
+"@csstools/postcss-contrast-color-function@npm:^3.0.3":
+ version: 3.0.3
+ resolution: "@csstools/postcss-contrast-color-function@npm:3.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/5013b6bd61da4eae881ffb52e0f1e98b63b7e8ead2e5d81b097cc9c7798404853d39cf9b879a72838ba6df695e3cfe09db753c4295a44bd4a27f9c805719ecef
+ checksum: 10/466e4a2fa3b072295e04bcc75e217da4429458d967ef34c8211a51b498c2feb5702a2a9d1134686e68e852ea0e32818dbbe00d8971a9f02edc1471c7318d0669
languageName: node
linkType: hard
-"@csstools/postcss-exponential-functions@npm:^3.0.1":
- version: 3.0.1
- resolution: "@csstools/postcss-exponential-functions@npm:3.0.1"
+"@csstools/postcss-exponential-functions@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "@csstools/postcss-exponential-functions@npm:3.0.2"
dependencies:
- "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/ddbfedbddf9b99304f4c5a303648e28f59987018075f9401b7b9f2a84b2f4516ffbfd0705f7e2b83f118a778df4925408e1a03cedb16d1ce1307f76996c75166
+ checksum: 10/daecaa2bafd2f6e6031c40325cb112fc00450c47264a63a51196d2dfafe11b1837df6fe4b1a2cd7727aceeb8b2b1848ba97221b6df0f503cadfefab71a59610d
languageName: node
linkType: hard
@@ -667,46 +744,46 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-gamut-mapping@npm:^3.0.2":
- version: 3.0.2
- resolution: "@csstools/postcss-gamut-mapping@npm:3.0.2"
+"@csstools/postcss-gamut-mapping@npm:^3.0.3":
+ version: 3.0.3
+ resolution: "@csstools/postcss-gamut-mapping@npm:3.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/4cd515c22f3dcf91b2739bd22fc73bb62d512e7bd91dfa72143b4608fb48babca0d8cd3ebc6633ccf3b3f80a2b41a1209b7862daf46c27e2edcf0b41055e1e36
+ checksum: 10/eadb88106c66fcbd7a1012607f957a5f0bbde4b334dc49305a0758a3171c0fe973ec7a81a5cac77094c191b3e50a189ecf5c36972e02385caafbe15d2583eeaa
languageName: node
linkType: hard
-"@csstools/postcss-gradients-interpolation-method@npm:^6.0.2":
- version: 6.0.2
- resolution: "@csstools/postcss-gradients-interpolation-method@npm:6.0.2"
+"@csstools/postcss-gradients-interpolation-method@npm:^6.0.3":
+ version: 6.0.3
+ resolution: "@csstools/postcss-gradients-interpolation-method@npm:6.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/5873f85a77725247baa65363d98f3267c2ed1bc2f3facd73ae6484870dbe7c42cb8018142ffb0557d4cbbc696f0f593bf56916b81fd09e31ef3458f278a5c6f2
+ checksum: 10/98d2b62321ede5c19825e7f3d91440b24772a199b00d58f13ac02f707b4ddde0edc86d5a63ec13bf86d6b6a84331f7010dbe1a6f0e80805a52061ba93f50c912
languageName: node
linkType: hard
-"@csstools/postcss-hwb-function@npm:^5.0.2":
- version: 5.0.2
- resolution: "@csstools/postcss-hwb-function@npm:5.0.2"
+"@csstools/postcss-hwb-function@npm:^5.0.3":
+ version: 5.0.3
+ resolution: "@csstools/postcss-hwb-function@npm:5.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/1ce6e08c39c9a2a6830becdfdf01c9b3632725777c0f900d320fea39723498080dcf754d74ed7d7b690e441298760041ec79b9a2b946d1fcde1dd965ee348513
+ checksum: 10/373cbcba68227a2c8fdf23447b99a91313c8aebabd6479939514a1fedc55cea0f4712dee5351036eae382eb20ae16efd4c114ccd630024cf85f78d04f67faa05
languageName: node
linkType: hard
@@ -808,17 +885,17 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-media-minmax@npm:^3.0.1":
- version: 3.0.1
- resolution: "@csstools/postcss-media-minmax@npm:3.0.1"
+"@csstools/postcss-media-minmax@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "@csstools/postcss-media-minmax@npm:3.0.2"
dependencies:
- "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/media-query-list-parser": "npm:^5.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/aa7300ec800ef3343642d8abcc03dc9e279280ec3e00de126e3b3c524b3e67123528f703cda5676fd2fa2af33e4766af92acf991eba6df378de7ce567ae3a159
+ checksum: 10/95c448bc303b2e08e816fb248ca0bf337694eee87ac9aa84dbe51ae59b897c111ee3ca235944b5e96070a94aa33920797907674f9c1dc9e986f668c3816e3703
languageName: node
linkType: hard
@@ -870,18 +947,18 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-oklab-function@npm:^5.0.2":
- version: 5.0.2
- resolution: "@csstools/postcss-oklab-function@npm:5.0.2"
+"@csstools/postcss-oklab-function@npm:^5.0.3":
+ version: 5.0.3
+ resolution: "@csstools/postcss-oklab-function@npm:5.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/2783f325ab98e9547de316f3cb52f3dd05748bf995ca36d4da7511ba347d3e80ca12abd4f1b4f5d85129e9c03f5b7159a9f673e4d37f31cafd39939687edb8af
+ checksum: 10/c564a299a42d4fc8a3edc009a98bc2aaa4b8f4b1c5aca36fb3c6d03716e86e111045797cd4fef79ce8c9d54a308d009050704cbac43055dbfc0d79e1a701ea28
languageName: node
linkType: hard
@@ -917,31 +994,31 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-random-function@npm:^3.0.1":
- version: 3.0.1
- resolution: "@csstools/postcss-random-function@npm:3.0.1"
+"@csstools/postcss-random-function@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "@csstools/postcss-random-function@npm:3.0.2"
dependencies:
- "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/b3e8c4451bb134b55ea0b1516917d870edefb964fb8220f365c2b7c586a43287ab4cb6519a725cc7bfc1d3df677fdb5d9694f75084776caee81f282d1686c223
+ checksum: 10/64500ad627f9ed5ac47a7c3231dede5c763ec20fb4ccd3c594cf64b24edd7df932d9fd770101b10fd55c36186509ba287290fdafb457f2e1c7e5e8cab5e332b4
languageName: node
linkType: hard
-"@csstools/postcss-relative-color-syntax@npm:^4.0.2":
- version: 4.0.2
- resolution: "@csstools/postcss-relative-color-syntax@npm:4.0.2"
+"@csstools/postcss-relative-color-syntax@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "@csstools/postcss-relative-color-syntax@npm:4.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/328e2824fd1afd141bf284172b2e75dbeb734c6613c14a49bf40dd409248efc91947d1316dda14cbed204fb9b454c340faf2590b347f13debd21d6f3ead6c553
+ checksum: 10/04c6031f14c1c55cb1dadf517d4a5332984fc0b29eca067987b97cb911da3c1d5da7c80cf1904ee04d5c3014fc18ec2eb666b553a7a87460d97249c517fac101
languageName: node
linkType: hard
@@ -956,29 +1033,29 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-sign-functions@npm:^2.0.1":
- version: 2.0.1
- resolution: "@csstools/postcss-sign-functions@npm:2.0.1"
+"@csstools/postcss-sign-functions@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "@csstools/postcss-sign-functions@npm:2.0.2"
dependencies:
- "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/4c50616715e8cd645c85f3ce3c5244d9ab8575a40b45c738083d9401858a330b79b8734755014306c17467116615cbc9690132b656c980cb2aa5bd99a4f3493f
+ checksum: 10/c9a5f5ee273c5d81863dab433869f481feae89da076e7c3928c1f4de93d020cacfc3cef5409f4d073a08bbd1aac69fd0291cb4dbdfe209cec3e58ec194d3b88f
languageName: node
linkType: hard
-"@csstools/postcss-stepped-value-functions@npm:^5.0.1":
- version: 5.0.1
- resolution: "@csstools/postcss-stepped-value-functions@npm:5.0.1"
+"@csstools/postcss-stepped-value-functions@npm:^5.0.2":
+ version: 5.0.2
+ resolution: "@csstools/postcss-stepped-value-functions@npm:5.0.2"
dependencies:
- "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/e0c3f3e335e399ab4a87cfdeba50103a254a408807d574487aee11ac1aa83402058a3725f167d2c162ee9e333d90412844b63f094b3043d6b9415afe5f9808db
+ checksum: 10/eb71f1071a685d63f308b7fcd0079b4cf41a3021ed0f4d4ee5ae09631aa6caec6d2af813d49e7e25beec26379cfeede28c71af6eaef8c0f1f4db7598babf3fff
languageName: node
linkType: hard
@@ -1017,16 +1094,16 @@ __metadata:
languageName: node
linkType: hard
-"@csstools/postcss-trigonometric-functions@npm:^5.0.1":
- version: 5.0.1
- resolution: "@csstools/postcss-trigonometric-functions@npm:5.0.1"
+"@csstools/postcss-trigonometric-functions@npm:^5.0.2":
+ version: 5.0.2
+ resolution: "@csstools/postcss-trigonometric-functions@npm:5.0.2"
dependencies:
- "@csstools/css-calc": "npm:^3.1.1"
+ "@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/c34d2995efacd1ab6d4376bf3441de2240acee26d3792d5d3f36cb75002c3305df975d40d7112900721a91bae2337ab3ede8c207ebde5963d45aa83a301778a4
+ checksum: 10/58aabb5c7fd8f97f8b992a490124788e371f3b5b0f647fdd88cc66a3c8d41169cd87906bdef0416506ff3e45d51783b336d5a699f7570e57b124dce8e2ae67ca
languageName: node
linkType: hard
@@ -1066,7 +1143,17 @@ __metadata:
languageName: node
linkType: hard
-"@emnapi/core@npm:^1.4.3":
+"@emnapi/core@npm:1.9.2":
+ version: 1.9.2
+ resolution: "@emnapi/core@npm:1.9.2"
+ dependencies:
+ "@emnapi/wasi-threads": "npm:1.2.1"
+ tslib: "npm:^2.4.0"
+ checksum: 10/32084861f306b405f10f3ae13d1a49fa75650bdaaa40704892c397856815fe5d3781670d2662806d39c2d8a19bb62826dd7b870a79858f7be77500d9d0d3d91a
+ languageName: node
+ linkType: hard
+
+"@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.8.1":
version: 1.9.1
resolution: "@emnapi/core@npm:1.9.1"
dependencies:
@@ -1076,7 +1163,16 @@ __metadata:
languageName: node
linkType: hard
-"@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.7.0":
+"@emnapi/runtime@npm:1.9.2":
+ version: 1.9.2
+ resolution: "@emnapi/runtime@npm:1.9.2"
+ dependencies:
+ tslib: "npm:^2.4.0"
+ checksum: 10/de123d6b7acdbe34bf997523be761e5ae6d8f9b3967b72e8e50ff7dd1791a2a0d2b9fb0d7d92230b0738502980ea6f947189b7c1f47814ff666515a55c6fff48
+ languageName: node
+ linkType: hard
+
+"@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.7.0, @emnapi/runtime@npm:^1.8.1":
version: 1.9.1
resolution: "@emnapi/runtime@npm:1.9.1"
dependencies:
@@ -1085,7 +1181,7 @@ __metadata:
languageName: node
linkType: hard
-"@emnapi/wasi-threads@npm:1.2.0":
+"@emnapi/wasi-threads@npm:1.2.0, @emnapi/wasi-threads@npm:^1.1.0":
version: 1.2.0
resolution: "@emnapi/wasi-threads@npm:1.2.0"
dependencies:
@@ -1094,185 +1190,12 @@ __metadata:
languageName: node
linkType: hard
-"@esbuild/aix-ppc64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/aix-ppc64@npm:0.27.4"
- conditions: os=aix & cpu=ppc64
- languageName: node
- linkType: hard
-
-"@esbuild/android-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/android-arm64@npm:0.27.4"
- conditions: os=android & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/android-arm@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/android-arm@npm:0.27.4"
- conditions: os=android & cpu=arm
- languageName: node
- linkType: hard
-
-"@esbuild/android-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/android-x64@npm:0.27.4"
- conditions: os=android & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/darwin-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/darwin-arm64@npm:0.27.4"
- conditions: os=darwin & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/darwin-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/darwin-x64@npm:0.27.4"
- conditions: os=darwin & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/freebsd-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/freebsd-arm64@npm:0.27.4"
- conditions: os=freebsd & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/freebsd-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/freebsd-x64@npm:0.27.4"
- conditions: os=freebsd & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/linux-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-arm64@npm:0.27.4"
- conditions: os=linux & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/linux-arm@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-arm@npm:0.27.4"
- conditions: os=linux & cpu=arm
- languageName: node
- linkType: hard
-
-"@esbuild/linux-ia32@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-ia32@npm:0.27.4"
- conditions: os=linux & cpu=ia32
- languageName: node
- linkType: hard
-
-"@esbuild/linux-loong64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-loong64@npm:0.27.4"
- conditions: os=linux & cpu=loong64
- languageName: node
- linkType: hard
-
-"@esbuild/linux-mips64el@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-mips64el@npm:0.27.4"
- conditions: os=linux & cpu=mips64el
- languageName: node
- linkType: hard
-
-"@esbuild/linux-ppc64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-ppc64@npm:0.27.4"
- conditions: os=linux & cpu=ppc64
- languageName: node
- linkType: hard
-
-"@esbuild/linux-riscv64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-riscv64@npm:0.27.4"
- conditions: os=linux & cpu=riscv64
- languageName: node
- linkType: hard
-
-"@esbuild/linux-s390x@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-s390x@npm:0.27.4"
- conditions: os=linux & cpu=s390x
- languageName: node
- linkType: hard
-
-"@esbuild/linux-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/linux-x64@npm:0.27.4"
- conditions: os=linux & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/netbsd-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/netbsd-arm64@npm:0.27.4"
- conditions: os=netbsd & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/netbsd-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/netbsd-x64@npm:0.27.4"
- conditions: os=netbsd & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/openbsd-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/openbsd-arm64@npm:0.27.4"
- conditions: os=openbsd & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/openbsd-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/openbsd-x64@npm:0.27.4"
- conditions: os=openbsd & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/openharmony-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/openharmony-arm64@npm:0.27.4"
- conditions: os=openharmony & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/sunos-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/sunos-x64@npm:0.27.4"
- conditions: os=sunos & cpu=x64
- languageName: node
- linkType: hard
-
-"@esbuild/win32-arm64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/win32-arm64@npm:0.27.4"
- conditions: os=win32 & cpu=arm64
- languageName: node
- linkType: hard
-
-"@esbuild/win32-ia32@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/win32-ia32@npm:0.27.4"
- conditions: os=win32 & cpu=ia32
- languageName: node
- linkType: hard
-
-"@esbuild/win32-x64@npm:0.27.4":
- version: 0.27.4
- resolution: "@esbuild/win32-x64@npm:0.27.4"
- conditions: os=win32 & cpu=x64
+"@emnapi/wasi-threads@npm:1.2.1":
+ version: 1.2.1
+ resolution: "@emnapi/wasi-threads@npm:1.2.1"
+ dependencies:
+ tslib: "npm:^2.4.0"
+ checksum: 10/57cd4292be81c05d26aa886d68a9e4c449ff666e8503fed6463dfc6b64a4e4213f03c152d53296b7cda32840271e38cd33347332070658f01befeb9bf4e59f36
languageName: node
linkType: hard
@@ -1294,49 +1217,87 @@ __metadata:
languageName: node
linkType: hard
-"@eslint/config-array@npm:^0.23.3":
- version: 0.23.3
- resolution: "@eslint/config-array@npm:0.23.3"
+"@eslint/config-array@npm:^0.23.4":
+ version: 0.23.5
+ resolution: "@eslint/config-array@npm:0.23.5"
dependencies:
- "@eslint/object-schema": "npm:^3.0.3"
+ "@eslint/object-schema": "npm:^3.0.5"
debug: "npm:^4.3.1"
minimatch: "npm:^10.2.4"
- checksum: 10/5014b11b73056ded9d52fb306aa5e711a5b9ca92777bcb6d646f79d43327b0ac247fd7bd3dc15cedfe70cfddcef1ef49ecd874b6608cec617d592cd1b05c4a23
+ checksum: 10/0e05be2b5c8b9f9fb8094948fd2d35591a32091b9d39205181f2ed9bec0e2c8b2969f019f40a0388755a025408b98929e2d0beccb4fbd6609c1c0d6c9e9a14f0
languageName: node
linkType: hard
-"@eslint/config-helpers@npm:^0.5.3":
- version: 0.5.3
- resolution: "@eslint/config-helpers@npm:0.5.3"
+"@eslint/config-helpers@npm:^0.5.4":
+ version: 0.5.5
+ resolution: "@eslint/config-helpers@npm:0.5.5"
dependencies:
- "@eslint/core": "npm:^1.1.1"
- checksum: 10/3b84df3d13bd9118807602136ee6cfbf98540d5959d1515fc46d96c605859eda978bed69289fb93d290fc1f4b5e339c9e1e2cba3a29d7ac8f1f93adb32a35d1a
+ "@eslint/core": "npm:^1.2.1"
+ checksum: 10/19072449502b928a716df87b2d9b13c7befb21974b0f93fdbea705ddba098792142808105170ef2183c28ce13ac9fa1713ef0599749f8469434ac2b914fc8f4d
languageName: node
linkType: hard
-"@eslint/core@npm:^1.1.1":
- version: 1.1.1
- resolution: "@eslint/core@npm:1.1.1"
+"@eslint/core@npm:^1.2.0, @eslint/core@npm:^1.2.1":
+ version: 1.2.1
+ resolution: "@eslint/core@npm:1.2.1"
dependencies:
"@types/json-schema": "npm:^7.0.15"
- checksum: 10/e847dd70b4398ba9e732ff50cc14a47114531d6e746c345278998881e6714ca665a1af0056694a18e48d87adec77c5b595b5badde1e55f6671ed5afe731701f7
+ checksum: 10/e1f9f5534f495b74a4c13c372e8f2feaf0c67f5dd666111c849c97c221d4ba730c98333a2ca94dd28cd7c24e3b1016bd868ca03c42e070732c047053f854cb13
languageName: node
linkType: hard
-"@eslint/object-schema@npm:^3.0.3":
- version: 3.0.3
- resolution: "@eslint/object-schema@npm:3.0.3"
- checksum: 10/24425256313eb41f315aa5f483a193986d488798c9e51b75a9e82c360e57663cbf6a7d64460a572719e2103f3c386308ad5eb4f9c79d4f9ec51aa00a4ce4e2ab
+"@eslint/object-schema@npm:^3.0.5":
+ version: 3.0.5
+ resolution: "@eslint/object-schema@npm:3.0.5"
+ checksum: 10/42e9ec2551d7cafe1825f20494576c9a867dfd26e728b66620f55d954cd5c4c9c4987755d147893985b8d39b49dace31117e59e7bc9564eb411b397e579a50e7
languageName: node
linkType: hard
-"@eslint/plugin-kit@npm:^0.6.1":
- version: 0.6.1
- resolution: "@eslint/plugin-kit@npm:0.6.1"
+"@eslint/plugin-kit@npm:^0.7.0":
+ version: 0.7.1
+ resolution: "@eslint/plugin-kit@npm:0.7.1"
dependencies:
- "@eslint/core": "npm:^1.1.1"
+ "@eslint/core": "npm:^1.2.1"
levn: "npm:^0.4.1"
- checksum: 10/8af22d94720b2474a992a80c5be7584baf75821386d25c34966b359fbc1e3a319989df721404c48c08b20e115683d6d0b344793dc354f5916c2d867c6c0aa04e
+ checksum: 10/8f923f4cdadadd215e0c2028e6a53101bb148a7780cdb4dc8cd69b0c77fc88496742e87e0605b12905ff715e2c7ad6cbd2d92c5653cdbf91cca1e229b5022c1f
+ languageName: node
+ linkType: hard
+
+"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.0, @exodus/bytes@npm:^1.6.0":
+ version: 1.15.0
+ resolution: "@exodus/bytes@npm:1.15.0"
+ peerDependencies:
+ "@noble/hashes": ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ "@noble/hashes":
+ optional: true
+ checksum: 10/d18519341c354356b65b9ac64b8166880972d122feff4038a92c0e2d2c8579794429117a2bc636bca584e7bf2fdad6d27f0874b2647d4a866c125843497ef193
+ languageName: node
+ linkType: hard
+
+"@floating-ui/core@npm:^1.7.5":
+ version: 1.7.5
+ resolution: "@floating-ui/core@npm:1.7.5"
+ dependencies:
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10/fecdc9b3ce93f02bf78a6114b93730a4cb9fa8234c62f9a949016186297a039c9f9cd3c5c81ff74b93ebddf0b32048c4af7a528afe7904b75423ed2e7491b888
+ languageName: node
+ linkType: hard
+
+"@floating-ui/dom@npm:^1.7.6":
+ version: 1.7.6
+ resolution: "@floating-ui/dom@npm:1.7.6"
+ dependencies:
+ "@floating-ui/core": "npm:^1.7.5"
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10/84dff2ffdf85c8b92d7edafc543c55869abbeaeb3007fa983159467e050153b507a0f5fe8e84f88c3f28c35a82de9df9c20a6eef5560cbba3afae19141444ff2
+ languageName: node
+ linkType: hard
+
+"@floating-ui/utils@npm:^0.2.11":
+ version: 0.2.11
+ resolution: "@floating-ui/utils@npm:0.2.11"
+ checksum: 10/72150138ba1c274d757a1da85233202fa9fdfd2272ec1fb0883eb0ffdf138863af81573049ed2c20b98adb4b7ae2236065541ce14037fe328955089831a678d5
languageName: node
linkType: hard
@@ -2030,6 +1991,18 @@ __metadata:
languageName: node
linkType: hard
+"@napi-rs/wasm-runtime@npm:^1.1.3":
+ version: 1.1.3
+ resolution: "@napi-rs/wasm-runtime@npm:1.1.3"
+ dependencies:
+ "@tybys/wasm-util": "npm:^0.10.1"
+ peerDependencies:
+ "@emnapi/core": ^1.7.1
+ "@emnapi/runtime": ^1.7.1
+ checksum: 10/a09f53dea7f5d11cbf4b4e3f10f726dd488b4a715f14f197dd619920d733e657261bb87d399628689dbe2b23b4353ddc122303d0583c4ef6cc4a5245367dfb2a
+ languageName: node
+ linkType: hard
+
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2086,10 +2059,10 @@ __metadata:
languageName: node
linkType: hard
-"@oxc-project/types@npm:=0.122.0":
- version: 0.122.0
- resolution: "@oxc-project/types@npm:0.122.0"
- checksum: 10/2b33895c7701a595d10b9c7b0927222954becc4c6cbde7a7b582e9524828937368baacba1cbb6e3c33bc9a18e0a35435ffff6c53f511762ae872d55d3e993a8c
+"@oxc-project/types@npm:=0.124.0":
+ version: 0.124.0
+ resolution: "@oxc-project/types@npm:0.124.0"
+ checksum: 10/d40ca0769b19b327b89fca30c9c224aace1b696e8b4f5adc2c67ee711e17401d532fdcfe49b8903916e8749ea67b572d3c470045279e27d26ac39af7bf1fc611
languageName: node
linkType: hard
@@ -2251,14 +2224,14 @@ __metadata:
languageName: node
linkType: hard
-"@playwright/test@npm:^1.59.0":
- version: 1.59.0
- resolution: "@playwright/test@npm:1.59.0"
+"@playwright/test@npm:^1.59.1":
+ version: 1.59.1
+ resolution: "@playwright/test@npm:1.59.1"
dependencies:
- playwright: "npm:1.59.0"
+ playwright: "npm:1.59.1"
bin:
playwright: cli.js
- checksum: 10/4a756c257fa9c0edec4ba202edf10e2fab8cf69cd7b5255229977c616390d7cadb03fedf586281693f9886b0e56b197f3810526aca0acbeccb58c1dc1d767dde
+ checksum: 10/27a894c4d4216b51cddc96e18fd0638a9e2e0a3f0b7ee32a56121fb61df395ec43529f5dcdca32578af8a34a04722ee3767f99f0ae4d39fa8edceda89a96014c
languageName: node
linkType: hard
@@ -2301,8 +2274,8 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/eslint": "npm:^9.6.1"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2318,17 +2291,17 @@ __metadata:
"@ribajs/types": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@types/node": "npm:^24.12.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@types/node": "npm:^24.12.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2346,16 +2319,16 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
photoswipe: "npm:^5.4.4"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2377,17 +2350,17 @@ __metadata:
"@sphinxxxx/color-conversion": "npm:^2.2.2"
"@types/bootstrap": "npm:^5.2.10"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2399,14 +2372,14 @@ __metadata:
"@ribajs/eslint-config": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
idb-keyval: "npm:^6.2.2"
keshi: "npm:^3.0.2"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2423,16 +2396,16 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2451,16 +2424,16 @@ __metadata:
"@ribajs/types": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-config: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
source-map-support: "npm:^0.5.21"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -2476,23 +2449,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2507,23 +2480,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2539,24 +2512,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2573,24 +2546,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2607,24 +2580,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2643,23 +2616,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2679,24 +2652,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2713,24 +2686,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2748,23 +2721,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2781,23 +2754,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2815,25 +2788,25 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@types/node": "npm:^24.12.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@types/node": "npm:^24.12.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2850,24 +2823,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2884,24 +2857,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2920,23 +2893,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2950,23 +2923,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -2980,23 +2953,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3011,23 +2984,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3042,19 +3015,19 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3072,23 +3045,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3104,19 +3077,19 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
+ postcss-preset-env: "npm:^11.2.1"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3131,23 +3104,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3164,24 +3137,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3196,23 +3169,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3229,24 +3202,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3264,24 +3237,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3298,24 +3271,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
leaflet: "npm:^1.9.4"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3331,23 +3304,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3364,23 +3337,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3396,23 +3369,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3426,21 +3399,21 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/types": "workspace:^"
"@ribajs/vite-config": "workspace:^"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
animejs: "npm:^4.3.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3457,23 +3430,23 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3491,18 +3464,18 @@ __metadata:
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
bootstrap: "npm:5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
monaco-editor: "npm:0.55.1"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -3520,60 +3493,259 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
-"@ribajs/doc@workspace:infra/doc":
+"@ribajs/demo-tw-accordion@workspace:demos/tw-accordion":
version: 0.0.0-use.local
- resolution: "@ribajs/doc@workspace:infra/doc"
+ resolution: "@ribajs/demo-tw-accordion@workspace:demos/tw-accordion"
dependencies:
- "@popperjs/core": "npm:^2.11.8"
- "@ribajs/bs5": "workspace:^"
"@ribajs/core": "workspace:^"
- "@ribajs/demo-core-each-item": "workspace:^"
- "@ribajs/demo-extras-scroll-events": "workspace:^"
- "@ribajs/demo-extras-touch-events": "workspace:^"
- "@ribajs/demo-i18n-static": "workspace:^"
- "@ribajs/demo-router-slide-transition": "workspace:^"
"@ribajs/eslint-config": "workspace:^"
- "@ribajs/events": "workspace:^"
"@ribajs/extras": "workspace:^"
- "@ribajs/history": "workspace:^"
- "@ribajs/i18n": "workspace:^"
- "@ribajs/iconset": "workspace:^"
- "@ribajs/router": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
"@ribajs/utils": "workspace:^"
- "@types/node": "npm:^24.12.0"
- "@types/prismjs": "npm:^1.26.6"
- bootstrap: "npm:^5.3.8"
- concurrently: "npm:^9.2.1"
- js-yaml: "npm:^4.1.1"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-basics@workspace:demos/tw-basics":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-basics@workspace:demos/tw-basics"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-dropdown@workspace:demos/tw-dropdown":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-dropdown@workspace:demos/tw-dropdown"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-form@workspace:demos/tw-form":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-form@workspace:demos/tw-form"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-interactive@workspace:demos/tw-interactive":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-interactive@workspace:demos/tw-interactive"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-notifications@workspace:demos/tw-notifications":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-notifications@workspace:demos/tw-notifications"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/events": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-sidebar@workspace:demos/tw-sidebar":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-sidebar@workspace:demos/tw-sidebar"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-slideshow@workspace:demos/tw-slideshow":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-slideshow@workspace:demos/tw-slideshow"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-tabs@workspace:demos/tw-tabs":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-tabs@workspace:demos/tw-tabs"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/demo-tw-theme@workspace:demos/tw-theme":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/demo-tw-theme@workspace:demos/tw-theme"
+ dependencies:
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/events": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/tw": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@ribajs/vite-config": "workspace:^"
+ "@tailwindcss/vite": "npm:^4.2.2"
+ eslint: "npm:^10.2.0"
+ serve: "npm:^14.2.6"
+ tailwindcss: "npm:^4.2.2"
+ typescript: "npm:6.0.2"
+ vite: "npm:^8.0.8"
+ languageName: unknown
+ linkType: soft
+
+"@ribajs/doc@workspace:infra/doc":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/doc@workspace:infra/doc"
+ dependencies:
+ "@popperjs/core": "npm:^2.11.8"
+ "@ribajs/bs5": "workspace:^"
+ "@ribajs/core": "workspace:^"
+ "@ribajs/demo-core-each-item": "workspace:^"
+ "@ribajs/demo-extras-scroll-events": "workspace:^"
+ "@ribajs/demo-extras-touch-events": "workspace:^"
+ "@ribajs/demo-i18n-static": "workspace:^"
+ "@ribajs/demo-router-slide-transition": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/events": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/history": "workspace:^"
+ "@ribajs/i18n": "workspace:^"
+ "@ribajs/iconset": "workspace:^"
+ "@ribajs/router": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@types/node": "npm:^24.12.2"
+ "@types/prismjs": "npm:^1.26.6"
+ bootstrap: "npm:^5.3.8"
+ concurrently: "npm:^9.2.1"
+ js-yaml: "npm:^4.1.1"
lorem-ipsum: "npm:^2.0.8"
- marked: "npm:^15.0.0"
+ marked: "npm:^18.0.0"
prismjs: "npm:^1.30.0"
- pug: "npm:^3.0.3"
- sass: "npm:^1.98.0"
+ pug: "npm:^3.0.4"
+ sass: "npm:^1.99.0"
typescript: "npm:^6.0.2"
- vite: "npm:^8.0.3"
- vite-plugin-static-copy: "npm:^3.2.0"
+ vite: "npm:^8.0.8"
+ vite-plugin-static-copy: "npm:^4.0.1"
languageName: unknown
linkType: soft
@@ -3586,8 +3758,8 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/eslint": "npm:^9.6.1"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3596,14 +3768,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "@ribajs/eslint-config@workspace:infra/eslint-config"
dependencies:
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-import-resolver-typescript: "npm:^4.4.4"
eslint-plugin-import: "npm:^2.32.0"
eslint-plugin-prettier: "npm:^5.5.5"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3614,12 +3786,12 @@ __metadata:
dependencies:
"@ribajs/tsconfig": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
jest: "npm:^30.3.0"
jest-config: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3634,16 +3806,16 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
ev-emitter: "npm:^2.1.2"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3657,9 +3829,9 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/eslint": "npm:^9.6.1"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
- fuse.js: "npm:^7.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
+ fuse.js: "npm:^7.3.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3682,7 +3854,7 @@ __metadata:
"@ribajs/eslint-config": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@types/jest": "npm:^30.0.0"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3698,12 +3870,12 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3730,17 +3902,17 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
"@types/jquery": "npm:^4.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-config: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
jquery: "npm:^4.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3753,8 +3925,8 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/eslint": "npm:^9.6.1"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3771,17 +3943,17 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
"@types/leaflet": "npm:^1.9.21"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
leaflet: "npm:^1.9.4"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3795,10 +3967,10 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/eslint": "npm:^9.6.1"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
lottie-web: "npm:^5.13.0"
- sass: "npm:^1.98.0"
+ sass: "npm:^1.99.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3812,16 +3984,16 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@types/jest": "npm:^30.0.0"
"@types/luxon": "npm:^3.7.1"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
luxon: "npm:^3.7.2"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3838,16 +4010,16 @@ __metadata:
"@types/imagesloaded": "npm:^4.1.7"
"@types/jest": "npm:^30.0.0"
"@types/masonry-layout": "npm:^4.2.8"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
masonry-layout: "npm:^4.2.2"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3861,17 +4033,17 @@ __metadata:
"@ribajs/eslint-config": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
jest-ts-webcompat-resolver: "npm:^1.0.1"
moment: "npm:^2.30.1"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3886,16 +4058,16 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
monaco-editor: "npm:0.55.1"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3962,17 +4134,17 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
"@types/jquery": "npm:^4.0.0"
- "@types/node": "npm:^24.12.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@types/node": "npm:^24.12.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -3989,24 +4161,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -4022,24 +4194,24 @@ __metadata:
"@ribajs/utils": "workspace:^"
"@ribajs/vite-config": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
bootstrap: "npm:^5.3.8"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- postcss: "npm:^8.5.8"
+ postcss: "npm:^8.5.9"
postcss-modules: "npm:^6.0.1"
- postcss-preset-env: "npm:^11.2.0"
- prettier: "npm:^3.8.1"
- sass: "npm:^1.98.0"
+ postcss-preset-env: "npm:^11.2.1"
+ prettier: "npm:^3.8.2"
+ sass: "npm:^1.99.0"
serve: "npm:^14.2.6"
typescript: "npm:6.0.2"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
@@ -4053,9 +4225,9 @@ __metadata:
"@ribajs/events": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
- "@types/node": "npm:^24.12.0"
+ "@types/node": "npm:^24.12.2"
concurrently: "npm:^9.2.1"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -4078,8 +4250,8 @@ __metadata:
"@ribajs/shopify-easdk": "workspace:^"
"@ribajs/shopify-tda": "workspace:^"
leaflet: "npm:^1.9.4"
- postcss: "npm:^8.5.8"
- postcss-preset-env: "npm:^11.2.0"
+ postcss: "npm:^8.5.9"
+ postcss-preset-env: "npm:^11.2.1"
dependenciesMeta:
"@ribajs/bs5":
optional: true
@@ -4121,15 +4293,15 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
"@typescript-eslint/experimental-utils": "npm:^5.62.0"
- "@typescript-eslint/parser": "npm:^8.57.2"
- "@typescript-eslint/typescript-estree": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ "@typescript-eslint/typescript-estree": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
eslint-visitor-keys: "npm:^5.0.1"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
regexpp: "npm:^3.2.0"
tsutils: "npm:^3.21.0"
typescript: "npm:6.0.2"
@@ -4148,12 +4320,12 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -4171,9 +4343,9 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/debug": "npm:^4.1.13"
- "@types/node": "npm:^24.12.0"
+ "@types/node": "npm:^24.12.2"
debug: "npm:^4.4.3"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
monaco-editor: "npm:^0.55.1"
typescript: "npm:6.0.2"
languageName: unknown
@@ -4193,13 +4365,13 @@ __metadata:
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@types/node": "npm:^24.12.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
socket.io-client: "npm:4.8.3"
typescript: "npm:6.0.2"
languageName: unknown
@@ -4222,20 +4394,20 @@ __metadata:
"@types/debug": "npm:^4.1.13"
"@types/jest": "npm:^30.0.0"
"@types/lodash": "npm:^4.17.24"
- "@types/node": "npm:^24.12.0"
+ "@types/node": "npm:^24.12.2"
"@types/prettier": "npm:^3.0.0"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
debug: "npm:^4.4.3"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
graceful-fs: "npm:^4.2.11"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
+ prettier: "npm:^3.8.2"
terser: "npm:^5.46.1"
- ts-jest: "npm:^29.4.6"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
dependenciesMeta:
"@ribajs/shopify-tda":
@@ -4257,15 +4429,15 @@ __metadata:
"@types/imagesloaded": "npm:^4.1.7"
"@types/jest": "npm:^30.0.0"
"@types/masonry-layout": "npm:^4.2.8"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
- eslint: "npm:^10.1.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-plugin-prettier: "npm:^5.5.5"
jest: "npm:^30.3.0"
jest-extended: "npm:^7.0.0"
- prettier: "npm:^3.8.1"
- ts-jest: "npm:^29.4.6"
+ prettier: "npm:^3.8.2"
+ ts-jest: "npm:^29.4.9"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -4277,7 +4449,7 @@ __metadata:
"@ribajs/core": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@ribajs/utils": "workspace:^"
- "@types/node": "npm:^24.12.0"
+ "@types/node": "npm:^24.12.2"
languageName: unknown
linkType: soft
@@ -4287,6 +4459,31 @@ __metadata:
languageName: unknown
linkType: soft
+"@ribajs/tw@workspace:^, @ribajs/tw@workspace:packages/tw":
+ version: 0.0.0-use.local
+ resolution: "@ribajs/tw@workspace:packages/tw"
+ dependencies:
+ "@floating-ui/dom": "npm:^1.7.6"
+ "@ribajs/core": "workspace:^"
+ "@ribajs/eslint-config": "workspace:^"
+ "@ribajs/events": "workspace:^"
+ "@ribajs/extras": "workspace:^"
+ "@ribajs/jsx": "workspace:^"
+ "@ribajs/tsconfig": "workspace:^"
+ "@ribajs/types": "workspace:^"
+ "@ribajs/utils": "workspace:^"
+ "@types/jest": "npm:^30.0.0"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
+ "@yarnpkg/pnpify": "npm:^4.1.6"
+ eslint: "npm:^10.2.0"
+ eslint-config-prettier: "npm:^10.1.8"
+ eslint-plugin-prettier: "npm:^5.5.5"
+ prettier: "npm:^3.8.2"
+ typescript: "npm:6.0.2"
+ languageName: unknown
+ linkType: soft
+
"@ribajs/types@workspace:^, @ribajs/types@workspace:infra/types":
version: 0.0.0-use.local
resolution: "@ribajs/types@workspace:infra/types"
@@ -4302,8 +4499,8 @@ __metadata:
"@ribajs/eslint-config": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@types/jest": "npm:^30.0.0"
- "@types/node": "npm:^24.12.0"
- eslint: "npm:^10.1.0"
+ "@types/node": "npm:^24.12.2"
+ eslint: "npm:^10.2.0"
typescript: "npm:6.0.2"
languageName: unknown
linkType: soft
@@ -4314,372 +4511,363 @@ __metadata:
dependencies:
"@ribajs/iconset": "workspace:^"
rollup-plugin-pug: "npm:^1.1.1"
- vite: "npm:^8.0.3"
+ vite: "npm:^8.0.8"
languageName: unknown
linkType: soft
-"@rolldown/binding-android-arm64@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.12"
+"@rolldown/binding-android-arm64@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.15"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
-"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.12"
+"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.15"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
-"@rolldown/binding-darwin-x64@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.12"
+"@rolldown/binding-darwin-x64@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.15"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
-"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.12"
+"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.15"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
-"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.15"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
-"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.15"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
-"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.15"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
-"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.15"
conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node
linkType: hard
-"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.15"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
-"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.15"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.12"
+"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.15"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.12"
+"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.15"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
-"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.12"
+"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.15"
dependencies:
- "@napi-rs/wasm-runtime": "npm:^1.1.1"
+ "@emnapi/core": "npm:1.9.2"
+ "@emnapi/runtime": "npm:1.9.2"
+ "@napi-rs/wasm-runtime": "npm:^1.1.3"
conditions: cpu=wasm32
languageName: node
linkType: hard
-"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.12"
+"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.15"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.12"
+"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.15"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
-"@rolldown/pluginutils@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "@rolldown/pluginutils@npm:1.0.0-rc.12"
- checksum: 10/6ce1601849b3095a2b6e57074c1f8a661eba67ebf65cf9afdf894d903302318247ddb69ab6cbc621e7f582408af301ea0523ed59ddb9a4ef3ea97f3d7002683e
+"@rolldown/pluginutils@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "@rolldown/pluginutils@npm:1.0.0-rc.15"
+ checksum: 10/528e6c4ebe43cc64daa1b068b23aac3df5de1aa152842f842c00d343dc4505603133f1f4e95c761551bca42fcf8506063d955c27d3b7ca748b6426d11d1e9fb5
languageName: node
linkType: hard
-"@rollup/rollup-android-arm-eabi@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-android-arm-eabi@npm:4.60.1"
- conditions: os=android & cpu=arm
+"@rtsao/scc@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@rtsao/scc@npm:1.1.0"
+ checksum: 10/17d04adf404e04c1e61391ed97bca5117d4c2767a76ae3e879390d6dec7b317fcae68afbf9e98badee075d0b64fa60f287729c4942021b4d19cd01db77385c01
languageName: node
linkType: hard
-"@rollup/rollup-android-arm64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-android-arm64@npm:4.60.1"
- conditions: os=android & cpu=arm64
+"@sinclair/typebox@npm:^0.34.0":
+ version: 0.34.49
+ resolution: "@sinclair/typebox@npm:0.34.49"
+ checksum: 10/5eb77de66c9deff83d43aa1f667832e2468f4dbd0ba91b80684f741a2e1e4120ffedb779be1578ae5b848250c3fbeffc032dc726947c5e42f3393903c1358cb9
languageName: node
linkType: hard
-"@rollup/rollup-darwin-arm64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-darwin-arm64@npm:4.60.1"
- conditions: os=darwin & cpu=arm64
+"@sindresorhus/is@npm:^4.0.0":
+ version: 4.6.0
+ resolution: "@sindresorhus/is@npm:4.6.0"
+ checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0
languageName: node
linkType: hard
-"@rollup/rollup-darwin-x64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-darwin-x64@npm:4.60.1"
- conditions: os=darwin & cpu=x64
+"@sinonjs/commons@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "@sinonjs/commons@npm:3.0.1"
+ dependencies:
+ type-detect: "npm:4.0.8"
+ checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117
languageName: node
linkType: hard
-"@rollup/rollup-freebsd-arm64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-freebsd-arm64@npm:4.60.1"
- conditions: os=freebsd & cpu=arm64
+"@sinonjs/fake-timers@npm:^15.0.0":
+ version: 15.1.1
+ resolution: "@sinonjs/fake-timers@npm:15.1.1"
+ dependencies:
+ "@sinonjs/commons": "npm:^3.0.1"
+ checksum: 10/f262d613ea7f7cdb1b5d90c0cae01b7c6b797d6d0f1ca0fe30b7b69012e3076bb8a0f69d735bc69d2824b9bb1efb8554ca9765b4a6bb22defdec9ce79e7cd8a4
languageName: node
linkType: hard
-"@rollup/rollup-freebsd-x64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-freebsd-x64@npm:4.60.1"
- conditions: os=freebsd & cpu=x64
+"@socket.io/component-emitter@npm:~3.1.0":
+ version: 3.1.2
+ resolution: "@socket.io/component-emitter@npm:3.1.2"
+ checksum: 10/89888f00699eb34e3070624eb7b8161fa29f064aeb1389a48f02195d55dd7c52a504e52160016859f6d6dffddd54324623cdd47fd34b3d46f9ed96c18c456edc
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm-gnueabihf@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.60.1"
- conditions: os=linux & cpu=arm & libc=glibc
+"@sphinxxxx/color-conversion@npm:^2.2.2":
+ version: 2.2.2
+ resolution: "@sphinxxxx/color-conversion@npm:2.2.2"
+ checksum: 10/60be08eb37d873a7e8e7987d348d09363237934e2827ded6df6322a113aa30b19de36edb6ce74749aad3039fdf3ccc386cdd6b30b74d2f0ba8c772d5cfa5b3fc
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm-musleabihf@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.60.1"
- conditions: os=linux & cpu=arm & libc=musl
+"@standard-schema/spec@npm:^1.0.0, @standard-schema/spec@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@standard-schema/spec@npm:1.1.0"
+ checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm64-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.60.1"
- conditions: os=linux & cpu=arm64 & libc=glibc
+"@standard-schema/utils@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "@standard-schema/utils@npm:0.3.0"
+ checksum: 10/7084f875d322792f2e0a5904009434c8374b9345b09ba89828b68fd56fa3c2b366d35bf340d9e8c72736ef01793c2f70d350c372ed79845dc3566c58d34b4b51
languageName: node
linkType: hard
-"@rollup/rollup-linux-arm64-musl@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-arm64-musl@npm:4.60.1"
- conditions: os=linux & cpu=arm64 & libc=musl
+"@szmarczak/http-timer@npm:^4.0.5":
+ version: 4.0.6
+ resolution: "@szmarczak/http-timer@npm:4.0.6"
+ dependencies:
+ defer-to-connect: "npm:^2.0.0"
+ checksum: 10/c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95
languageName: node
linkType: hard
-"@rollup/rollup-linux-loong64-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.60.1"
- conditions: os=linux & cpu=loong64 & libc=glibc
+"@tailwindcss/node@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/node@npm:4.2.2"
+ dependencies:
+ "@jridgewell/remapping": "npm:^2.3.5"
+ enhanced-resolve: "npm:^5.19.0"
+ jiti: "npm:^2.6.1"
+ lightningcss: "npm:1.32.0"
+ magic-string: "npm:^0.30.21"
+ source-map-js: "npm:^1.2.1"
+ tailwindcss: "npm:4.2.2"
+ checksum: 10/7c3eaa66e644ad6d369a869f6755b9880032e22103fd90ff0bbc538c2177f149a400c90064ca0cfadb82ff1c30651f19931d3af80ec3532e06be0139cdbf8620
languageName: node
linkType: hard
-"@rollup/rollup-linux-loong64-musl@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-loong64-musl@npm:4.60.1"
- conditions: os=linux & cpu=loong64 & libc=musl
+"@tailwindcss/oxide-android-arm64@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-android-arm64@npm:4.2.2"
+ conditions: os=android & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-linux-ppc64-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.60.1"
- conditions: os=linux & cpu=ppc64 & libc=glibc
+"@tailwindcss/oxide-darwin-arm64@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-darwin-arm64@npm:4.2.2"
+ conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-linux-ppc64-musl@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.60.1"
- conditions: os=linux & cpu=ppc64 & libc=musl
+"@tailwindcss/oxide-darwin-x64@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-darwin-x64@npm:4.2.2"
+ conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
-"@rollup/rollup-linux-riscv64-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.60.1"
- conditions: os=linux & cpu=riscv64 & libc=glibc
+"@tailwindcss/oxide-freebsd-x64@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-freebsd-x64@npm:4.2.2"
+ conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
-"@rollup/rollup-linux-riscv64-musl@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.60.1"
- conditions: os=linux & cpu=riscv64 & libc=musl
+"@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.2.2"
+ conditions: os=linux & cpu=arm
languageName: node
linkType: hard
-"@rollup/rollup-linux-s390x-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.60.1"
- conditions: os=linux & cpu=s390x & libc=glibc
+"@tailwindcss/oxide-linux-arm64-gnu@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-linux-arm64-gnu@npm:4.2.2"
+ conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-linux-x64-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-x64-gnu@npm:4.60.1"
- conditions: os=linux & cpu=x64 & libc=glibc
+"@tailwindcss/oxide-linux-arm64-musl@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-linux-arm64-musl@npm:4.2.2"
+ conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
-"@rollup/rollup-linux-x64-musl@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-linux-x64-musl@npm:4.60.1"
- conditions: os=linux & cpu=x64 & libc=musl
+"@tailwindcss/oxide-linux-x64-gnu@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-linux-x64-gnu@npm:4.2.2"
+ conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@rollup/rollup-openbsd-x64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-openbsd-x64@npm:4.60.1"
- conditions: os=openbsd & cpu=x64
+"@tailwindcss/oxide-linux-x64-musl@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-linux-x64-musl@npm:4.2.2"
+ conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@rollup/rollup-openharmony-arm64@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-openharmony-arm64@npm:4.60.1"
- conditions: os=openharmony & cpu=arm64
+"@tailwindcss/oxide-wasm32-wasi@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-wasm32-wasi@npm:4.2.2"
+ dependencies:
+ "@emnapi/core": "npm:^1.8.1"
+ "@emnapi/runtime": "npm:^1.8.1"
+ "@emnapi/wasi-threads": "npm:^1.1.0"
+ "@napi-rs/wasm-runtime": "npm:^1.1.1"
+ "@tybys/wasm-util": "npm:^0.10.1"
+ tslib: "npm:^2.8.1"
+ conditions: cpu=wasm32
languageName: node
linkType: hard
-"@rollup/rollup-win32-arm64-msvc@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.60.1"
+"@tailwindcss/oxide-win32-arm64-msvc@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-win32-arm64-msvc@npm:4.2.2"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@rollup/rollup-win32-ia32-msvc@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.60.1"
- conditions: os=win32 & cpu=ia32
- languageName: node
- linkType: hard
-
-"@rollup/rollup-win32-x64-gnu@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-win32-x64-gnu@npm:4.60.1"
- conditions: os=win32 & cpu=x64
- languageName: node
- linkType: hard
-
-"@rollup/rollup-win32-x64-msvc@npm:4.60.1":
- version: 4.60.1
- resolution: "@rollup/rollup-win32-x64-msvc@npm:4.60.1"
+"@tailwindcss/oxide-win32-x64-msvc@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide-win32-x64-msvc@npm:4.2.2"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
-"@rtsao/scc@npm:^1.1.0":
- version: 1.1.0
- resolution: "@rtsao/scc@npm:1.1.0"
- checksum: 10/17d04adf404e04c1e61391ed97bca5117d4c2767a76ae3e879390d6dec7b317fcae68afbf9e98badee075d0b64fa60f287729c4942021b4d19cd01db77385c01
- languageName: node
- linkType: hard
-
-"@sinclair/typebox@npm:^0.34.0":
- version: 0.34.49
- resolution: "@sinclair/typebox@npm:0.34.49"
- checksum: 10/5eb77de66c9deff83d43aa1f667832e2468f4dbd0ba91b80684f741a2e1e4120ffedb779be1578ae5b848250c3fbeffc032dc726947c5e42f3393903c1358cb9
- languageName: node
- linkType: hard
-
-"@sindresorhus/is@npm:^4.0.0":
- version: 4.6.0
- resolution: "@sindresorhus/is@npm:4.6.0"
- checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0
- languageName: node
- linkType: hard
-
-"@sinonjs/commons@npm:^3.0.1":
- version: 3.0.1
- resolution: "@sinonjs/commons@npm:3.0.1"
- dependencies:
- type-detect: "npm:4.0.8"
- checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117
- languageName: node
- linkType: hard
-
-"@sinonjs/fake-timers@npm:^15.0.0":
- version: 15.1.1
- resolution: "@sinonjs/fake-timers@npm:15.1.1"
- dependencies:
- "@sinonjs/commons": "npm:^3.0.1"
- checksum: 10/f262d613ea7f7cdb1b5d90c0cae01b7c6b797d6d0f1ca0fe30b7b69012e3076bb8a0f69d735bc69d2824b9bb1efb8554ca9765b4a6bb22defdec9ce79e7cd8a4
- languageName: node
- linkType: hard
-
-"@socket.io/component-emitter@npm:~3.1.0":
- version: 3.1.2
- resolution: "@socket.io/component-emitter@npm:3.1.2"
- checksum: 10/89888f00699eb34e3070624eb7b8161fa29f064aeb1389a48f02195d55dd7c52a504e52160016859f6d6dffddd54324623cdd47fd34b3d46f9ed96c18c456edc
- languageName: node
- linkType: hard
-
-"@sphinxxxx/color-conversion@npm:^2.2.2":
- version: 2.2.2
- resolution: "@sphinxxxx/color-conversion@npm:2.2.2"
- checksum: 10/60be08eb37d873a7e8e7987d348d09363237934e2827ded6df6322a113aa30b19de36edb6ce74749aad3039fdf3ccc386cdd6b30b74d2f0ba8c772d5cfa5b3fc
- languageName: node
- linkType: hard
-
-"@standard-schema/spec@npm:^1.0.0":
- version: 1.1.0
- resolution: "@standard-schema/spec@npm:1.1.0"
- checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763
- languageName: node
- linkType: hard
-
-"@standard-schema/utils@npm:^0.3.0":
- version: 0.3.0
- resolution: "@standard-schema/utils@npm:0.3.0"
- checksum: 10/7084f875d322792f2e0a5904009434c8374b9345b09ba89828b68fd56fa3c2b366d35bf340d9e8c72736ef01793c2f70d350c372ed79845dc3566c58d34b4b51
+"@tailwindcss/oxide@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/oxide@npm:4.2.2"
+ dependencies:
+ "@tailwindcss/oxide-android-arm64": "npm:4.2.2"
+ "@tailwindcss/oxide-darwin-arm64": "npm:4.2.2"
+ "@tailwindcss/oxide-darwin-x64": "npm:4.2.2"
+ "@tailwindcss/oxide-freebsd-x64": "npm:4.2.2"
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "npm:4.2.2"
+ "@tailwindcss/oxide-linux-arm64-gnu": "npm:4.2.2"
+ "@tailwindcss/oxide-linux-arm64-musl": "npm:4.2.2"
+ "@tailwindcss/oxide-linux-x64-gnu": "npm:4.2.2"
+ "@tailwindcss/oxide-linux-x64-musl": "npm:4.2.2"
+ "@tailwindcss/oxide-wasm32-wasi": "npm:4.2.2"
+ "@tailwindcss/oxide-win32-arm64-msvc": "npm:4.2.2"
+ "@tailwindcss/oxide-win32-x64-msvc": "npm:4.2.2"
+ dependenciesMeta:
+ "@tailwindcss/oxide-android-arm64":
+ optional: true
+ "@tailwindcss/oxide-darwin-arm64":
+ optional: true
+ "@tailwindcss/oxide-darwin-x64":
+ optional: true
+ "@tailwindcss/oxide-freebsd-x64":
+ optional: true
+ "@tailwindcss/oxide-linux-arm-gnueabihf":
+ optional: true
+ "@tailwindcss/oxide-linux-arm64-gnu":
+ optional: true
+ "@tailwindcss/oxide-linux-arm64-musl":
+ optional: true
+ "@tailwindcss/oxide-linux-x64-gnu":
+ optional: true
+ "@tailwindcss/oxide-linux-x64-musl":
+ optional: true
+ "@tailwindcss/oxide-wasm32-wasi":
+ optional: true
+ "@tailwindcss/oxide-win32-arm64-msvc":
+ optional: true
+ "@tailwindcss/oxide-win32-x64-msvc":
+ optional: true
+ checksum: 10/acc2399cfc7e9b60457ab0a7eaaea17d5496c9cc02f49a5b04d51d7c54062c644872ac1168062e780b2fab8f3a9027394989bbe786416ac828ace4938a8e8f37
languageName: node
linkType: hard
-"@szmarczak/http-timer@npm:^4.0.5":
- version: 4.0.6
- resolution: "@szmarczak/http-timer@npm:4.0.6"
+"@tailwindcss/vite@npm:^4.2.2":
+ version: 4.2.2
+ resolution: "@tailwindcss/vite@npm:4.2.2"
dependencies:
- defer-to-connect: "npm:^2.0.0"
- checksum: 10/c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95
+ "@tailwindcss/node": "npm:4.2.2"
+ "@tailwindcss/oxide": "npm:4.2.2"
+ tailwindcss: "npm:4.2.2"
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+ checksum: 10/b0881d1101a6e75d1ceb33a595073bc553b2c53d37c453e3d27b8b3a5113113d860f92ad108dd1d5f3727e9a68144346389714a940a46dbb4140d393a1b6d39f
languageName: node
linkType: hard
@@ -4839,7 +5027,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8":
+"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8":
version: 1.0.8
resolution: "@types/estree@npm:1.0.8"
checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099
@@ -5032,12 +5220,12 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:^24.12.0":
- version: 24.12.0
- resolution: "@types/node@npm:24.12.0"
+"@types/node@npm:^24.12.2":
+ version: 24.12.2
+ resolution: "@types/node@npm:24.12.2"
dependencies:
undici-types: "npm:~7.16.0"
- checksum: 10/e9dcf8a378af5a636353b6d88a6fae018504bab776410ac6b5411e29afbe601ba9d7957356556fc27268a62814ca4085974f785613482c18f739686efcd49655
+ checksum: 10/99b9f15e67a4b3c39b39ad83ee0febad7f0b4709c004863104d7acfa4146dd7e58c12a08a9a7ff2be8c2eefd0063bf991fade0879d7c4a370a0ee7fd4c799e8a
languageName: node
linkType: hard
@@ -5164,23 +5352,23 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/eslint-plugin@npm:^8.57.2":
- version: 8.58.0
- resolution: "@typescript-eslint/eslint-plugin@npm:8.58.0"
+"@typescript-eslint/eslint-plugin@npm:^8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/eslint-plugin@npm:8.58.1"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
- "@typescript-eslint/scope-manager": "npm:8.58.0"
- "@typescript-eslint/type-utils": "npm:8.58.0"
- "@typescript-eslint/utils": "npm:8.58.0"
- "@typescript-eslint/visitor-keys": "npm:8.58.0"
+ "@typescript-eslint/scope-manager": "npm:8.58.1"
+ "@typescript-eslint/type-utils": "npm:8.58.1"
+ "@typescript-eslint/utils": "npm:8.58.1"
+ "@typescript-eslint/visitor-keys": "npm:8.58.1"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
- "@typescript-eslint/parser": ^8.58.0
+ "@typescript-eslint/parser": ^8.58.1
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/0b1f4d4e62279fc5925c0a89d94816db0e236648eb4aec522cf22071877c6ecc63146b6830a1075049f402b842f45d7332ce6ae67883639754dcbb1881d07c34
+ checksum: 10/0fcbe6faadb77313aa91c895c977a24fc72a79eed62f46f7b2d5804db52a9af99351b33b9c4d73fdabb0f69772d5d4a9acdef249a0d1526a44d3817fb51419b5
languageName: node
linkType: hard
@@ -5195,32 +5383,32 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/parser@npm:^8.57.2":
- version: 8.58.0
- resolution: "@typescript-eslint/parser@npm:8.58.0"
+"@typescript-eslint/parser@npm:^8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/parser@npm:8.58.1"
dependencies:
- "@typescript-eslint/scope-manager": "npm:8.58.0"
- "@typescript-eslint/types": "npm:8.58.0"
- "@typescript-eslint/typescript-estree": "npm:8.58.0"
- "@typescript-eslint/visitor-keys": "npm:8.58.0"
+ "@typescript-eslint/scope-manager": "npm:8.58.1"
+ "@typescript-eslint/types": "npm:8.58.1"
+ "@typescript-eslint/typescript-estree": "npm:8.58.1"
+ "@typescript-eslint/visitor-keys": "npm:8.58.1"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/0498e593b14841023b7495544637acaca84807de1ac17174fa9331af61ba35791da919e16680c6897002b4a843d5f0fe812c460a28a65fca7e3ae8e5971baf7a
+ checksum: 10/062584d26609e82169459ebf0c59f4925ba6596f4ea1637a320c34a25c34117585c458b9c6c268f5eeaee1988f4c7257d34d4bd05a214a88de12110e71b48493
languageName: node
linkType: hard
-"@typescript-eslint/project-service@npm:8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/project-service@npm:8.58.0"
+"@typescript-eslint/project-service@npm:8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/project-service@npm:8.58.1"
dependencies:
- "@typescript-eslint/tsconfig-utils": "npm:^8.58.0"
- "@typescript-eslint/types": "npm:^8.58.0"
+ "@typescript-eslint/tsconfig-utils": "npm:^8.58.1"
+ "@typescript-eslint/types": "npm:^8.58.1"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/fab2601f76b2df61b09e3b7ff364d0e17e6d80e65e84e8a8d11f6a0813748bed3912da098659d00f46b1f277d462bd7529157182b72b5e2e0b41ee6176a0edd7
+ checksum: 10/2f3136268fc262e77e8c8c14291e60c54e0228b63ccb022826b6def6d80b83ce9c3a92fef11c888889fb204343c845556868c49495c3aa0a115e9a861dd5fe99
languageName: node
linkType: hard
@@ -5234,38 +5422,38 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/scope-manager@npm:8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/scope-manager@npm:8.58.0"
+"@typescript-eslint/scope-manager@npm:8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/scope-manager@npm:8.58.1"
dependencies:
- "@typescript-eslint/types": "npm:8.58.0"
- "@typescript-eslint/visitor-keys": "npm:8.58.0"
- checksum: 10/97293f1215faa785a3c1ee8d630591db9dcd5fb6bdcdd0b2e818c80478d41e59a05003fb33000530780dc466fb8cf662352932080ee7406c4aaac72af4000541
+ "@typescript-eslint/types": "npm:8.58.1"
+ "@typescript-eslint/visitor-keys": "npm:8.58.1"
+ checksum: 10/dc070fd73847807e32cb7dfc37512abd0b1a485b0037d8cfb6c593555a5b673d3ee9d19c61504ea71d067ad610c66f64d70d56f3a5db51895c0a25e45621cd08
languageName: node
linkType: hard
-"@typescript-eslint/tsconfig-utils@npm:8.58.0, @typescript-eslint/tsconfig-utils@npm:^8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.0"
+"@typescript-eslint/tsconfig-utils@npm:8.58.1, @typescript-eslint/tsconfig-utils@npm:^8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.1"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/4f47212c0e26e6b06e97044ec5e483007d5145ef6b205393a0b43cbc0b385c75c14ba5749d01cf7d1ff100332c2cf1d336f060f7d2191bb67fb892bb4446afaa
+ checksum: 10/4a5cf9a5eb834d05f2d37f7d80319575cf4a75aa52807b96edc0db24349ba417b41cb6f5257ffb07b8b9b4c59c7438637e8c75ed7c2b513bcb07e259b49e058e
languageName: node
linkType: hard
-"@typescript-eslint/type-utils@npm:8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/type-utils@npm:8.58.0"
+"@typescript-eslint/type-utils@npm:8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/type-utils@npm:8.58.1"
dependencies:
- "@typescript-eslint/types": "npm:8.58.0"
- "@typescript-eslint/typescript-estree": "npm:8.58.0"
- "@typescript-eslint/utils": "npm:8.58.0"
+ "@typescript-eslint/types": "npm:8.58.1"
+ "@typescript-eslint/typescript-estree": "npm:8.58.1"
+ "@typescript-eslint/utils": "npm:8.58.1"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/da09868cd0b2cebb8cc4494e73aeed3997a7a4ff899fef496c54731610af1f92f8e3169f9b7f23060105db892f3bc880aacc0bdc7c1734ea5829252230c13aea
+ checksum: 10/39d62d6711590e817cf9a36257c19ea18e201ceca42b900350e121ea8986c167fbdd9da385ced29c61e38a1b5c76b6c320d59e21d4dd7f32767520e31aef4654
languageName: node
linkType: hard
@@ -5276,10 +5464,10 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/types@npm:8.58.0"
- checksum: 10/c68eac0bc25812fdbb2ed4a121e42bfca9f24f3c6be95f6a9c4e7b9af767f1bcfacd6d496e358166143e0a1801dc7d042ce1b5e69946ac2768d9114ff6b8d375
+"@typescript-eslint/types@npm:8.58.1, @typescript-eslint/types@npm:^8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/types@npm:8.58.1"
+ checksum: 10/447e1351af8a47297096f063b327c69b1c986af89e39cb39e142bb35d7bec2ce8f34f31edcf62d1beb2e09a38e2029b12b50b335dae4e7c9ff49bd82f9127523
languageName: node
linkType: hard
@@ -5301,14 +5489,14 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/typescript-estree@npm:8.58.0, @typescript-eslint/typescript-estree@npm:^8.57.2":
- version: 8.58.0
- resolution: "@typescript-eslint/typescript-estree@npm:8.58.0"
+"@typescript-eslint/typescript-estree@npm:8.58.1, @typescript-eslint/typescript-estree@npm:^8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/typescript-estree@npm:8.58.1"
dependencies:
- "@typescript-eslint/project-service": "npm:8.58.0"
- "@typescript-eslint/tsconfig-utils": "npm:8.58.0"
- "@typescript-eslint/types": "npm:8.58.0"
- "@typescript-eslint/visitor-keys": "npm:8.58.0"
+ "@typescript-eslint/project-service": "npm:8.58.1"
+ "@typescript-eslint/tsconfig-utils": "npm:8.58.1"
+ "@typescript-eslint/types": "npm:8.58.1"
+ "@typescript-eslint/visitor-keys": "npm:8.58.1"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
semver: "npm:^7.7.3"
@@ -5316,7 +5504,7 @@ __metadata:
ts-api-utils: "npm:^2.5.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/4d6c4175e8a4d5c097393d161016836cc322f090c3f69fd751f5bbc25afce64df9ea0c97cee8b36ac060e06dc2cca2a4de7a0c7e04e19727cc4bd98ab3291fed
+ checksum: 10/107510b484148a8a9a5874f5451b9a6649609607ee5e67de36cded786157987a5262b145398b1bd1935afab66134532369a4d6abb53c6f5b7744e3ace0b13f07
languageName: node
linkType: hard
@@ -5338,18 +5526,18 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/utils@npm:8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/utils@npm:8.58.0"
+"@typescript-eslint/utils@npm:8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/utils@npm:8.58.1"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
- "@typescript-eslint/scope-manager": "npm:8.58.0"
- "@typescript-eslint/types": "npm:8.58.0"
- "@typescript-eslint/typescript-estree": "npm:8.58.0"
+ "@typescript-eslint/scope-manager": "npm:8.58.1"
+ "@typescript-eslint/types": "npm:8.58.1"
+ "@typescript-eslint/typescript-estree": "npm:8.58.1"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
- checksum: 10/936433b761a990147612d78bb4afc79244239541b4a4061fbbc2de1810b40ec7f78eb4e9181e5d9c5ab7acbd9bf49fc6195dbb1d823370f717f07ad492ad6c7e
+ checksum: 10/c51a5e116d1a09d0eb701c5884d5b9b8c22f79c427cb4c46357e4bcb7dfdfd9beba92e5d518572f42111b7335541a4ccefe3c05595fc3d666c1b62ddd1522e54
languageName: node
linkType: hard
@@ -5363,13 +5551,13 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/visitor-keys@npm:8.58.0":
- version: 8.58.0
- resolution: "@typescript-eslint/visitor-keys@npm:8.58.0"
+"@typescript-eslint/visitor-keys@npm:8.58.1":
+ version: 8.58.1
+ resolution: "@typescript-eslint/visitor-keys@npm:8.58.1"
dependencies:
- "@typescript-eslint/types": "npm:8.58.0"
+ "@typescript-eslint/types": "npm:8.58.1"
eslint-visitor-keys: "npm:^5.0.0"
- checksum: 10/50b0779e19079dedf3723323a4dfa398c639b3da48f2fcf071c22ca69342e03592f1726d68ea59b9b5a51f14ab112eabc5c93fd2579c84b02a3320042ae20066
+ checksum: 10/e9f34741da6fc0cb8e9eb67828ea4427ac2004a33ce8d1e1e9ba038471f9ed68405eca871651bb2efa793a467bc5233a4310c5571ad1497cb2a84a600e1733a8
languageName: node
linkType: hard
@@ -5515,86 +5703,85 @@ __metadata:
languageName: node
linkType: hard
-"@vitest/expect@npm:3.2.4":
- version: 3.2.4
- resolution: "@vitest/expect@npm:3.2.4"
+"@vitest/expect@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/expect@npm:4.1.4"
dependencies:
+ "@standard-schema/spec": "npm:^1.1.0"
"@types/chai": "npm:^5.2.2"
- "@vitest/spy": "npm:3.2.4"
- "@vitest/utils": "npm:3.2.4"
- chai: "npm:^5.2.0"
- tinyrainbow: "npm:^2.0.0"
- checksum: 10/dc69ce886c13714dfbbff78f2d2cb7eb536017e82301a73c42d573a9e9d2bf91005ac7abd9b977adf0a3bd431209f45a8ac2418029b68b0a377e092607c843ce
+ "@vitest/spy": "npm:4.1.4"
+ "@vitest/utils": "npm:4.1.4"
+ chai: "npm:^6.2.2"
+ tinyrainbow: "npm:^3.1.0"
+ checksum: 10/3317bc42e4ee39cfa2102a9f08f0c7975817a74d9503a14e0b1715e5b8c4ab31c5646c07ef8d2d3f71bdf6f1b3053949b175df9c8457e0c0bb3f38b9e031f259
languageName: node
linkType: hard
-"@vitest/mocker@npm:3.2.4":
- version: 3.2.4
- resolution: "@vitest/mocker@npm:3.2.4"
+"@vitest/mocker@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/mocker@npm:4.1.4"
dependencies:
- "@vitest/spy": "npm:3.2.4"
+ "@vitest/spy": "npm:4.1.4"
estree-walker: "npm:^3.0.3"
- magic-string: "npm:^0.30.17"
+ magic-string: "npm:^0.30.21"
peerDependencies:
msw: ^2.4.9
- vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
- checksum: 10/5e92431b6ed9fc1679060e4caef3e4623f4750542a5d7cd944774f8217c4d231e273202e8aea00bab33260a5a9222ecb7005d80da0348c3c829bd37d123071a8
+ checksum: 10/f07f8877635eb03f63981d0d3348bb82fabe7607bbb6b259045bf0b64fae79150b1f399aa7ce42926e4769dc8cde9b7d79d1f665eae2d17b22ecc9ec54663698
languageName: node
linkType: hard
-"@vitest/pretty-format@npm:3.2.4, @vitest/pretty-format@npm:^3.2.4":
- version: 3.2.4
- resolution: "@vitest/pretty-format@npm:3.2.4"
+"@vitest/pretty-format@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/pretty-format@npm:4.1.4"
dependencies:
- tinyrainbow: "npm:^2.0.0"
- checksum: 10/8dd30cbf956e01fbab042fe651fb5175d9f0cd00b7b569a46cd98df89c4fec47dab12916201ad6e09a4f25f2a2ec8927a4bfdc61118593097f759c90b18a51d4
+ tinyrainbow: "npm:^3.1.0"
+ checksum: 10/e06d63ce4f797ad578ee19aeec996f72835a7274ee2eb75dce12d7b45debcda72d054f58b6f4e5dac4424681dc13dbad7ac023c6017fc60406cabea5a352e4c3
languageName: node
linkType: hard
-"@vitest/runner@npm:3.2.4":
- version: 3.2.4
- resolution: "@vitest/runner@npm:3.2.4"
+"@vitest/runner@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/runner@npm:4.1.4"
dependencies:
- "@vitest/utils": "npm:3.2.4"
+ "@vitest/utils": "npm:4.1.4"
pathe: "npm:^2.0.3"
- strip-literal: "npm:^3.0.0"
- checksum: 10/197bd55def519ef202f990b7c1618c212380831827c116240871033e4973decb780503c705ba9245a12bd8121f3ac4086ffcb3e302148b62d9bd77fd18dd1deb
+ checksum: 10/a852477adc6254e1d304bcba9b137f98f09a7001a557e8e4f4404518e3ade58a16ab459e83cf223e38cc37dc4b04d1248a14df56b056a0ae68fc54b19a1226fb
languageName: node
linkType: hard
-"@vitest/snapshot@npm:3.2.4":
- version: 3.2.4
- resolution: "@vitest/snapshot@npm:3.2.4"
+"@vitest/snapshot@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/snapshot@npm:4.1.4"
dependencies:
- "@vitest/pretty-format": "npm:3.2.4"
- magic-string: "npm:^0.30.17"
+ "@vitest/pretty-format": "npm:4.1.4"
+ "@vitest/utils": "npm:4.1.4"
+ magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
- checksum: 10/acfb682491b9ca9345bf9fed02c2779dec43e0455a380c1966b0aad8dd81c79960902cf34621ab48fe80a0eaf8c61cc42dec186a1321dc3c9897ef2ebd5f1bc4
+ checksum: 10/e957cc95274a9663cd59e5b34c99b6e4e5cd989f04dadf9e3cec6c7bc64b4d167229644f31fd44c19c7acbbcb7cbbbb50e8084dbf1e0322ee411a697d80d490a
languageName: node
linkType: hard
-"@vitest/spy@npm:3.2.4":
- version: 3.2.4
- resolution: "@vitest/spy@npm:3.2.4"
- dependencies:
- tinyspy: "npm:^4.0.3"
- checksum: 10/7d38c299f42a8c7e5e41652b203af98ca54e63df69c3b072d0e401d5a57fbbba3e39d8538ac1b3022c26718a6388d0bcc222bc2f07faab75942543b9247c007d
+"@vitest/spy@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/spy@npm:4.1.4"
+ checksum: 10/516e465413fc6a22e0c7e99871f3b9703277c309e94e7247bbdb83a8e807e2da968cf7a30c61503afd6b565787e822786b8aad443210eba5488192a36730f3ab
languageName: node
linkType: hard
-"@vitest/utils@npm:3.2.4":
- version: 3.2.4
- resolution: "@vitest/utils@npm:3.2.4"
+"@vitest/utils@npm:4.1.4":
+ version: 4.1.4
+ resolution: "@vitest/utils@npm:4.1.4"
dependencies:
- "@vitest/pretty-format": "npm:3.2.4"
- loupe: "npm:^3.1.4"
- tinyrainbow: "npm:^2.0.0"
- checksum: 10/7f12ef63bd8ee13957744d1f336b0405f164ade4358bf9dfa531f75bbb58ffac02bf61aba65724311ddbc50b12ba54853a169e59c6b837c16086173b9a480710
+ "@vitest/pretty-format": "npm:4.1.4"
+ convert-source-map: "npm:^2.0.0"
+ tinyrainbow: "npm:^3.1.0"
+ checksum: 10/f599ae744f0ff45edda90d0c52eea9809b7367adca39fc985f85880322236d089dfdf6625f04913f03a25a160eccbbc0b16dd3201ccc0ae48087992b1ea755d5
languageName: node
linkType: hard
@@ -6217,6 +6404,15 @@ __metadata:
languageName: node
linkType: hard
+"bidi-js@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "bidi-js@npm:1.0.3"
+ dependencies:
+ require-from-string: "npm:^2.0.2"
+ checksum: 10/c4341c7a98797efe3d186cd99d6f97e9030a4f959794ca200ef2ec0a678483a916335bba6c2c0608a21d04a221288a31c9fd0faa0cd9b3903b93594b42466a6a
+ languageName: node
+ linkType: hard
+
"binary-extensions@npm:^2.0.0":
version: 2.3.0
resolution: "binary-extensions@npm:2.3.0"
@@ -6347,13 +6543,6 @@ __metadata:
languageName: node
linkType: hard
-"cac@npm:^6.7.14":
- version: 6.7.14
- resolution: "cac@npm:6.7.14"
- checksum: 10/002769a0fbfc51c062acd2a59df465a2a947916b02ac50b56c69ec6c018ee99ac3e7f4dd7366334ea847f1ecacf4defaa61bcd2ac283db50156ce1f1d8c8ad42
- languageName: node
- linkType: hard
-
"cacache@npm:^20.0.1":
version: 20.0.4
resolution: "cacache@npm:20.0.4"
@@ -6478,16 +6667,10 @@ __metadata:
languageName: node
linkType: hard
-"chai@npm:^5.2.0":
- version: 5.3.3
- resolution: "chai@npm:5.3.3"
- dependencies:
- assertion-error: "npm:^2.0.1"
- check-error: "npm:^2.1.1"
- deep-eql: "npm:^5.0.1"
- loupe: "npm:^3.1.0"
- pathval: "npm:^2.0.0"
- checksum: 10/0d0ef63106083b05c7ba510697cd9991a02b8df5984a7d010ab4af10205c7a1f27d1c06bfa4679540894295ac4dcc22aa2a281e2e4cfe5133c1db379626689a2
+"chai@npm:^6.2.2":
+ version: 6.2.2
+ resolution: "chai@npm:6.2.2"
+ checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f
languageName: node
linkType: hard
@@ -6540,13 +6723,6 @@ __metadata:
languageName: node
linkType: hard
-"check-error@npm:^2.1.1":
- version: 2.1.3
- resolution: "check-error@npm:2.1.3"
- checksum: 10/f1868d3db60f5a7da92e140ccf33e9152bf6124161fa9b7a4ae8eafdb05e66e1f13570401e56f314f037b0f1b71eaf38ad0c7256310d82c6105e9d85ded0f202
- languageName: node
- linkType: hard
-
"chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
@@ -6871,7 +7047,7 @@ __metadata:
languageName: node
linkType: hard
-"css-tree@npm:^3.0.1":
+"css-tree@npm:^3.0.0, css-tree@npm:^3.0.1, css-tree@npm:^3.2.1":
version: 3.2.1
resolution: "css-tree@npm:3.2.1"
dependencies:
@@ -6943,6 +7119,16 @@ __metadata:
languageName: node
linkType: hard
+"data-urls@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "data-urls@npm:7.0.0"
+ dependencies:
+ whatwg-mimetype: "npm:^5.0.0"
+ whatwg-url: "npm:^16.0.0"
+ checksum: 10/60f88ded4306aea5d6251c4db100ca272fc026014004d68aad4db495397a73bb39d17a6bd29ed9ab348c88a28f6e97266a1759985df4e12dc8c02bb8544c7731
+ languageName: node
+ linkType: hard
+
"data-view-buffer@npm:^1.0.2":
version: 1.0.2
resolution: "data-view-buffer@npm:1.0.2"
@@ -7013,7 +7199,7 @@ __metadata:
languageName: node
linkType: hard
-"decimal.js@npm:^10.5.0":
+"decimal.js@npm:^10.5.0, decimal.js@npm:^10.6.0":
version: 10.6.0
resolution: "decimal.js@npm:10.6.0"
checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69
@@ -7041,13 +7227,6 @@ __metadata:
languageName: node
linkType: hard
-"deep-eql@npm:^5.0.1":
- version: 5.0.2
- resolution: "deep-eql@npm:5.0.2"
- checksum: 10/a529b81e2ef8821621d20a36959a0328873a3e49d393ad11f8efe8559f31239494c2eb889b80342808674c475802ba95b9d6c4c27641b9a029405104c1b59fcf
- languageName: node
- linkType: hard
-
"deep-extend@npm:^0.6.0":
version: 0.6.0
resolution: "deep-extend@npm:0.6.0"
@@ -7283,6 +7462,16 @@ __metadata:
languageName: node
linkType: hard
+"enhanced-resolve@npm:^5.19.0":
+ version: 5.20.1
+ resolution: "enhanced-resolve@npm:5.20.1"
+ dependencies:
+ graceful-fs: "npm:^4.2.4"
+ tapable: "npm:^2.3.0"
+ checksum: 10/588afc56de97334e5742faebcf8177a504da08ea817d399f9901f35d8e9e5e6fa86b4c2ce95a99081f947764e09c9991cc0fc0ba5751bae455c329643a709187
+ languageName: node
+ linkType: hard
+
"entities@npm:^4.2.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
@@ -7389,10 +7578,10 @@ __metadata:
languageName: node
linkType: hard
-"es-module-lexer@npm:^1.7.0":
- version: 1.7.0
- resolution: "es-module-lexer@npm:1.7.0"
- checksum: 10/b6f3e576a3fed4d82b0d0ad4bbf6b3a5ad694d2e7ce8c4a069560da3db6399381eaba703616a182b16dde50ce998af64e07dcf49f2ae48153b9e07be3f107087
+"es-module-lexer@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "es-module-lexer@npm:2.0.0"
+ checksum: 10/b075855289b5f40ee496f3d7525c5c501d029c3da15c22298a0030d625bf36d1da0768b26278f7f4bada2a602459b505888e20b77c414fba5da5619b0e84dbd1
languageName: node
linkType: hard
@@ -7424,117 +7613,28 @@ __metadata:
hasown: "npm:^2.0.2"
checksum: 10/c351f586c30bbabc62355be49564b2435468b52c3532b8a1663672e3d10dc300197e69c247869dd173e56d86423ab95fc0c10b0939cdae597094e0fdca078cba
languageName: node
- linkType: hard
-
-"es-to-primitive@npm:^1.3.0":
- version: 1.3.0
- resolution: "es-to-primitive@npm:1.3.0"
- dependencies:
- is-callable: "npm:^1.2.7"
- is-date-object: "npm:^1.0.5"
- is-symbol: "npm:^1.0.4"
- checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b
- languageName: node
- linkType: hard
-
-"es-toolkit@npm:^1.39.7":
- version: 1.45.1
- resolution: "es-toolkit@npm:1.45.1"
- dependenciesMeta:
- "@trivago/prettier-plugin-sort-imports@4.3.0":
- unplugged: true
- prettier-plugin-sort-re-exports@0.0.1:
- unplugged: true
- checksum: 10/e092803bd0ba473db04798311e39bfc1e27e7bb958309f2e49c1be59931c7d88d5d84fc12483b32e0e73c2dec42739b73b4dfe6322a37c2a158b552456b24134
- languageName: node
- linkType: hard
-
-"esbuild@npm:^0.27.0":
- version: 0.27.4
- resolution: "esbuild@npm:0.27.4"
- dependencies:
- "@esbuild/aix-ppc64": "npm:0.27.4"
- "@esbuild/android-arm": "npm:0.27.4"
- "@esbuild/android-arm64": "npm:0.27.4"
- "@esbuild/android-x64": "npm:0.27.4"
- "@esbuild/darwin-arm64": "npm:0.27.4"
- "@esbuild/darwin-x64": "npm:0.27.4"
- "@esbuild/freebsd-arm64": "npm:0.27.4"
- "@esbuild/freebsd-x64": "npm:0.27.4"
- "@esbuild/linux-arm": "npm:0.27.4"
- "@esbuild/linux-arm64": "npm:0.27.4"
- "@esbuild/linux-ia32": "npm:0.27.4"
- "@esbuild/linux-loong64": "npm:0.27.4"
- "@esbuild/linux-mips64el": "npm:0.27.4"
- "@esbuild/linux-ppc64": "npm:0.27.4"
- "@esbuild/linux-riscv64": "npm:0.27.4"
- "@esbuild/linux-s390x": "npm:0.27.4"
- "@esbuild/linux-x64": "npm:0.27.4"
- "@esbuild/netbsd-arm64": "npm:0.27.4"
- "@esbuild/netbsd-x64": "npm:0.27.4"
- "@esbuild/openbsd-arm64": "npm:0.27.4"
- "@esbuild/openbsd-x64": "npm:0.27.4"
- "@esbuild/openharmony-arm64": "npm:0.27.4"
- "@esbuild/sunos-x64": "npm:0.27.4"
- "@esbuild/win32-arm64": "npm:0.27.4"
- "@esbuild/win32-ia32": "npm:0.27.4"
- "@esbuild/win32-x64": "npm:0.27.4"
- dependenciesMeta:
- "@esbuild/aix-ppc64":
- optional: true
- "@esbuild/android-arm":
- optional: true
- "@esbuild/android-arm64":
- optional: true
- "@esbuild/android-x64":
- optional: true
- "@esbuild/darwin-arm64":
- optional: true
- "@esbuild/darwin-x64":
- optional: true
- "@esbuild/freebsd-arm64":
- optional: true
- "@esbuild/freebsd-x64":
- optional: true
- "@esbuild/linux-arm":
- optional: true
- "@esbuild/linux-arm64":
- optional: true
- "@esbuild/linux-ia32":
- optional: true
- "@esbuild/linux-loong64":
- optional: true
- "@esbuild/linux-mips64el":
- optional: true
- "@esbuild/linux-ppc64":
- optional: true
- "@esbuild/linux-riscv64":
- optional: true
- "@esbuild/linux-s390x":
- optional: true
- "@esbuild/linux-x64":
- optional: true
- "@esbuild/netbsd-arm64":
- optional: true
- "@esbuild/netbsd-x64":
- optional: true
- "@esbuild/openbsd-arm64":
- optional: true
- "@esbuild/openbsd-x64":
- optional: true
- "@esbuild/openharmony-arm64":
- optional: true
- "@esbuild/sunos-x64":
- optional: true
- "@esbuild/win32-arm64":
- optional: true
- "@esbuild/win32-ia32":
- optional: true
- "@esbuild/win32-x64":
- optional: true
- bin:
- esbuild: bin/esbuild
- checksum: 10/32b46ec22ef78bae6cc141145022a4c0209852c07151f037fbefccc2033ca54e7f33705f8fca198eb7026f400142f64c2dbc9f0d0ce9c0a638ebc472a04abc4a
+ linkType: hard
+
+"es-to-primitive@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "es-to-primitive@npm:1.3.0"
+ dependencies:
+ is-callable: "npm:^1.2.7"
+ is-date-object: "npm:^1.0.5"
+ is-symbol: "npm:^1.0.4"
+ checksum: 10/17faf35c221aad59a16286cbf58ef6f080bf3c485dff202c490d074d8e74da07884e29b852c245d894eac84f73c58330ec956dfd6d02c0b449d75eb1012a3f9b
+ languageName: node
+ linkType: hard
+
+"es-toolkit@npm:^1.39.7":
+ version: 1.45.1
+ resolution: "es-toolkit@npm:1.45.1"
+ dependenciesMeta:
+ "@trivago/prettier-plugin-sort-imports@4.3.0":
+ unplugged: true
+ prettier-plugin-sort-re-exports@0.0.1:
+ unplugged: true
+ checksum: 10/e092803bd0ba473db04798311e39bfc1e27e7bb958309f2e49c1be59931c7d88d5d84fc12483b32e0e73c2dec42739b73b4dfe6322a37c2a158b552456b24134
languageName: node
linkType: hard
@@ -7717,16 +7817,16 @@ __metadata:
languageName: node
linkType: hard
-"eslint@npm:^10.1.0":
- version: 10.1.0
- resolution: "eslint@npm:10.1.0"
+"eslint@npm:^10.2.0":
+ version: 10.2.0
+ resolution: "eslint@npm:10.2.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.2"
- "@eslint/config-array": "npm:^0.23.3"
- "@eslint/config-helpers": "npm:^0.5.3"
- "@eslint/core": "npm:^1.1.1"
- "@eslint/plugin-kit": "npm:^0.6.1"
+ "@eslint/config-array": "npm:^0.23.4"
+ "@eslint/config-helpers": "npm:^0.5.4"
+ "@eslint/core": "npm:^1.2.0"
+ "@eslint/plugin-kit": "npm:^0.7.0"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.2"
@@ -7758,7 +7858,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
- checksum: 10/fb0c65660f6a98d6411d47c4a9cd14783e97f50481046de61903be59005f48a2b9a7c120e83280ec21298232b2369d3848b44e99017852b23bd794ca6ea44b66
+ checksum: 10/583589ed96922aad9507c94339e843c8929c297d505ae7d70579cef56b435a10d8a48d24616eb4fb53fbe75d8655adb8e44add5d5b2bca100148d31d890ab3a4
languageName: node
linkType: hard
@@ -7876,7 +7976,7 @@ __metadata:
languageName: node
linkType: hard
-"expect-type@npm:^1.2.1":
+"expect-type@npm:^1.3.0":
version: 1.3.0
resolution: "expect-type@npm:1.3.0"
checksum: 10/a5fada3d0c621649261f886e7d93e6bf80ce26d8a86e5d517e38301b8baec8450ab2cb94ba6e7a0a6bf2fc9ee55f54e1b06938ef1efa52ddcfeffbfa01acbbcc
@@ -8154,10 +8254,10 @@ __metadata:
languageName: node
linkType: hard
-"fuse.js@npm:^7.1.0":
- version: 7.1.0
- resolution: "fuse.js@npm:7.1.0"
- checksum: 10/9f9105e54372897a46cb3e04074f0db5bd0a428320d4618276a57e6142d7502235a556f05cf87aa3c5d6d9c6fdfa06b901b78379c48aa0951672ccbc4a1bfe70
+"fuse.js@npm:^7.3.0":
+ version: 7.3.0
+ resolution: "fuse.js@npm:7.3.0"
+ checksum: 10/b2cdc39e46acb9524fe900356af74c987ecb1dbc67412df651a5291fa072d212e6d74457f0b5d1c39baf79539481f62b613ac792afd8995dd1fc3d20b5354914
languageName: node
linkType: hard
@@ -8390,7 +8490,7 @@ __metadata:
languageName: node
linkType: hard
-"graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.6":
+"graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2
@@ -8404,7 +8504,7 @@ __metadata:
languageName: node
linkType: hard
-"handlebars@npm:^4.7.8":
+"handlebars@npm:^4.7.9":
version: 4.7.9
resolution: "handlebars@npm:4.7.9"
dependencies:
@@ -8495,6 +8595,15 @@ __metadata:
languageName: node
linkType: hard
+"html-encoding-sniffer@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "html-encoding-sniffer@npm:6.0.0"
+ dependencies:
+ "@exodus/bytes": "npm:^1.6.0"
+ checksum: 10/97392e45d8aff57f180f62a1b12e62201c8451af68424b8bc3196f78e273891f2df285e5be43a3f28c7ba4badf9524ef305db65c4e4935a9e796afc86d9654b8
+ languageName: node
+ linkType: hard
+
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
@@ -9579,6 +9688,15 @@ __metadata:
languageName: node
linkType: hard
+"jiti@npm:^2.6.1":
+ version: 2.6.1
+ resolution: "jiti@npm:2.6.1"
+ bin:
+ jiti: lib/jiti-cli.mjs
+ checksum: 10/8cd72c5fd03a0502564c3f46c49761090f6dadead21fa191b73535724f095ad86c2fa89ee6fe4bc3515337e8d406cc8fb2d37b73fa0c99a34584bac35cd4a4de
+ languageName: node
+ linkType: hard
+
"jquery@npm:^4.0.0":
version: 4.0.0
resolution: "jquery@npm:4.0.0"
@@ -9600,13 +9718,6 @@ __metadata:
languageName: node
linkType: hard
-"js-tokens@npm:^9.0.1":
- version: 9.0.1
- resolution: "js-tokens@npm:9.0.1"
- checksum: 10/3288ba73bb2023adf59501979fb4890feb6669cc167b13771b226814fde96a1583de3989249880e3f4d674040d1815685db9a9880db9153307480d39dc760365
- languageName: node
- linkType: hard
-
"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1":
version: 3.14.2
resolution: "js-yaml@npm:3.14.2"
@@ -9663,6 +9774,40 @@ __metadata:
languageName: node
linkType: hard
+"jsdom@npm:^29.0.2":
+ version: 29.0.2
+ resolution: "jsdom@npm:29.0.2"
+ dependencies:
+ "@asamuzakjp/css-color": "npm:^5.1.5"
+ "@asamuzakjp/dom-selector": "npm:^7.0.6"
+ "@bramus/specificity": "npm:^2.4.2"
+ "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1"
+ "@exodus/bytes": "npm:^1.15.0"
+ css-tree: "npm:^3.2.1"
+ data-urls: "npm:^7.0.0"
+ decimal.js: "npm:^10.6.0"
+ html-encoding-sniffer: "npm:^6.0.0"
+ is-potential-custom-element-name: "npm:^1.0.1"
+ lru-cache: "npm:^11.2.7"
+ parse5: "npm:^8.0.0"
+ saxes: "npm:^6.0.0"
+ symbol-tree: "npm:^3.2.4"
+ tough-cookie: "npm:^6.0.1"
+ undici: "npm:^7.24.5"
+ w3c-xmlserializer: "npm:^5.0.0"
+ webidl-conversions: "npm:^8.0.1"
+ whatwg-mimetype: "npm:^5.0.0"
+ whatwg-url: "npm:^16.0.1"
+ xml-name-validator: "npm:^5.0.0"
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+ checksum: 10/3ad1d9a5b6aba067427bc43be98e1c51fab489bf689a6530e596278c6326fe053c94fc47a9c133f126fbe914f421283ae723fb92214dfe4959ca6cf2ee1666f6
+ languageName: node
+ linkType: hard
+
"jsesc@npm:^3.0.2":
version: 3.1.0
resolution: "jsesc@npm:3.1.0"
@@ -9872,7 +10017,7 @@ __metadata:
languageName: node
linkType: hard
-"lightningcss@npm:^1.32.0":
+"lightningcss@npm:1.32.0, lightningcss@npm:^1.32.0":
version: 1.32.0
resolution: "lightningcss@npm:1.32.0"
dependencies:
@@ -9993,13 +10138,6 @@ __metadata:
languageName: node
linkType: hard
-"loupe@npm:^3.1.0, loupe@npm:^3.1.4":
- version: 3.2.1
- resolution: "loupe@npm:3.2.1"
- checksum: 10/a4d78ec758aaa04e0e35d5cd1c15e970beb9cdbfd3d0f34f98b9bcda489f896a7190b3b6cc40b7a6dcb8e97e82e96eafaae10096aaa469804acdba6f7c2bde5f
- languageName: node
- linkType: hard
-
"lowercase-keys@npm:^2.0.0":
version: 2.0.0
resolution: "lowercase-keys@npm:2.0.0"
@@ -10021,6 +10159,13 @@ __metadata:
languageName: node
linkType: hard
+"lru-cache@npm:^11.2.7":
+ version: 11.3.5
+ resolution: "lru-cache@npm:11.3.5"
+ checksum: 10/3701b77e87765a3aea453402a7850bdbf7e02445210f35bd5ba1561f601f605f488bf9932be4a3851a6664073924f671a1ec99c4a1a98c457e0d126872a3e04f
+ languageName: node
+ linkType: hard
+
"lru-cache@npm:^5.1.1":
version: 5.1.1
resolution: "lru-cache@npm:5.1.1"
@@ -10037,7 +10182,7 @@ __metadata:
languageName: node
linkType: hard
-"magic-string@npm:^0.30.17":
+"magic-string@npm:^0.30.21":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
dependencies:
@@ -10100,12 +10245,12 @@ __metadata:
languageName: node
linkType: hard
-"marked@npm:^15.0.0":
- version: 15.0.12
- resolution: "marked@npm:15.0.12"
+"marked@npm:^18.0.0":
+ version: 18.0.0
+ resolution: "marked@npm:18.0.0"
bin:
marked: bin/marked.js
- checksum: 10/deeb619405c0c46af00c99b18b3365450abeb309104b24e3658f46142344f6b7c4117608c3b5834084d8738e92f81240c19f596e6ee369260f96e52b3457eaee
+ checksum: 10/eb746a1f6e9b570ccc174cbf339504a432681bb76a1419d8b8b036c487235a55cbf94f4fa2c1e3c347a1d3f5f3666d4395bb74ea4ea4960c89b66d3a948c96d0
languageName: node
linkType: hard
@@ -10558,6 +10703,13 @@ __metadata:
languageName: node
linkType: hard
+"obug@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "obug@npm:2.1.1"
+ checksum: 10/bdcf9213361786688019345f3452b95a1dc73710e4b403c82a1994b98bad6abc31b26cb72a482128c5fd53ea9daf6fbb7d0e0e7b2b7e9c8be6d779deeccee07f
+ languageName: node
+ linkType: hard
+
"on-headers@npm:~1.1.0":
version: 1.1.0
resolution: "on-headers@npm:1.1.0"
@@ -10704,6 +10856,15 @@ __metadata:
languageName: node
linkType: hard
+"parse5@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "parse5@npm:8.0.0"
+ dependencies:
+ entities: "npm:^6.0.0"
+ checksum: 10/1973850932bb1cbd52ab64502761489fbe1bb43a52dee7ce41aac0b6c33a51a92aaee04661590b0912b739ae9ee316bce4c78c8ea34af42a7e522c983c3c6cf5
+ languageName: node
+ linkType: hard
+
"path-exists@npm:^4.0.0":
version: 4.0.0
resolution: "path-exists@npm:4.0.0"
@@ -10780,13 +10941,6 @@ __metadata:
languageName: node
linkType: hard
-"pathval@npm:^2.0.0":
- version: 2.0.1
- resolution: "pathval@npm:2.0.1"
- checksum: 10/f5e8b82f6b988a5bba197970af050268fd800780d0f9ee026e6f0b544ac4b17ab52bebeabccb790d63a794530a1641ae399ad07ecfc67ad337504c85dc9e5693
- languageName: node
- linkType: hard
-
"photoswipe@npm:^5.4.4":
version: 5.4.4
resolution: "photoswipe@npm:5.4.4"
@@ -10808,7 +10962,7 @@ __metadata:
languageName: node
linkType: hard
-"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3, picomatch@npm:^4.0.4":
+"picomatch@npm:^4.0.3, picomatch@npm:^4.0.4":
version: 4.0.4
resolution: "picomatch@npm:4.0.4"
checksum: 10/f6ef80a3590827ce20378ae110ac78209cc4f74d39236370f1780f957b7ee41c12acde0e4651b90f39983506fd2f5e449994716f516db2e9752924aff8de93ce
@@ -10831,27 +10985,27 @@ __metadata:
languageName: node
linkType: hard
-"playwright-core@npm:1.59.0":
- version: 1.59.0
- resolution: "playwright-core@npm:1.59.0"
+"playwright-core@npm:1.59.1":
+ version: 1.59.1
+ resolution: "playwright-core@npm:1.59.1"
bin:
playwright-core: cli.js
- checksum: 10/6df95346fc9512915e75fba3752c67e5acb18832801b9496d6cfea32500d3a6dab1056c75789e9a26cabfbefceb21241ca45351af5ad3f02b45afb2af91698d7
+ checksum: 10/d27857a6701587c2a9bfa26fed9a5d8c617a392299b99b187f2ddc198d012a1e296449806bc907220debea938152677e8b4d91d304ed00645f762f778de3abec
languageName: node
linkType: hard
-"playwright@npm:1.59.0":
- version: 1.59.0
- resolution: "playwright@npm:1.59.0"
+"playwright@npm:1.59.1":
+ version: 1.59.1
+ resolution: "playwright@npm:1.59.1"
dependencies:
fsevents: "npm:2.3.2"
- playwright-core: "npm:1.59.0"
+ playwright-core: "npm:1.59.1"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
- checksum: 10/2415646868de2f1e20be5c52d17738f4347c3d9fda4b166034d790897dde42374eb715fdd94e60536c6b19bd17f6361c213093e70158d602fd496eb81cd01888
+ checksum: 10/17b2df42effa362adc6aa3192b625bd80f26b91a0c253a2375ac89ace68407b746dd87b4081629c50c58c3cb031c5b837a32fef43a3c98c60ea504e0b001e5fa
languageName: node
linkType: hard
@@ -10884,18 +11038,18 @@ __metadata:
languageName: node
linkType: hard
-"postcss-color-functional-notation@npm:^8.0.2":
- version: 8.0.2
- resolution: "postcss-color-functional-notation@npm:8.0.2"
+"postcss-color-functional-notation@npm:^8.0.3":
+ version: 8.0.3
+ resolution: "postcss-color-functional-notation@npm:8.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/0b0a02ea40ba6832b83b50a57bce8b7517f1cb330197ad44f8a0cb86be14a45b8dc15e6cf9da59d0e16ec68c77fcd212a9f013a10f8628cf8245609eb53c9fd6
+ checksum: 10/46204ea94bc550a96a30c44b9099ee3621fde13ebd74a78ca6a75873bc5c64278d7a2f77401302574599f6e2f680fc18b82d9d9da737b2ef8fa51239358e63c3
languageName: node
linkType: hard
@@ -11042,18 +11196,18 @@ __metadata:
languageName: node
linkType: hard
-"postcss-lab-function@npm:^8.0.2":
- version: 8.0.2
- resolution: "postcss-lab-function@npm:8.0.2"
+"postcss-lab-function@npm:^8.0.3":
+ version: 8.0.3
+ resolution: "postcss-lab-function@npm:8.0.3"
dependencies:
- "@csstools/css-color-parser": "npm:^4.0.2"
+ "@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/utilities": "npm:^3.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/1fc6f3fb11d8bed5468ba8edd8aa0aa764e32b6c52f966e464adf75cae227fb84cd82de9811154f16b13c2ddf422d57b217f391bef70fbc6c541be85e792a688
+ checksum: 10/eb4fda96cd551dd10c7e5891ce80f1458e6f9e10e6260bf42ba370a7d9825be7dd897939175b89bfeea4b3bf7618e400e0ba0fd0ef05583ef451b9a679872038
languageName: node
linkType: hard
@@ -11183,24 +11337,24 @@ __metadata:
languageName: node
linkType: hard
-"postcss-preset-env@npm:^11.2.0":
- version: 11.2.0
- resolution: "postcss-preset-env@npm:11.2.0"
+"postcss-preset-env@npm:^11.2.1":
+ version: 11.2.1
+ resolution: "postcss-preset-env@npm:11.2.1"
dependencies:
- "@csstools/postcss-alpha-function": "npm:^2.0.3"
+ "@csstools/postcss-alpha-function": "npm:^2.0.4"
"@csstools/postcss-cascade-layers": "npm:^6.0.0"
- "@csstools/postcss-color-function": "npm:^5.0.2"
- "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.2"
- "@csstools/postcss-color-mix-function": "npm:^4.0.2"
- "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^2.0.2"
+ "@csstools/postcss-color-function": "npm:^5.0.3"
+ "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.3"
+ "@csstools/postcss-color-mix-function": "npm:^4.0.3"
+ "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^2.0.3"
"@csstools/postcss-content-alt-text": "npm:^3.0.0"
- "@csstools/postcss-contrast-color-function": "npm:^3.0.2"
- "@csstools/postcss-exponential-functions": "npm:^3.0.1"
+ "@csstools/postcss-contrast-color-function": "npm:^3.0.3"
+ "@csstools/postcss-exponential-functions": "npm:^3.0.2"
"@csstools/postcss-font-format-keywords": "npm:^5.0.0"
"@csstools/postcss-font-width-property": "npm:^1.0.0"
- "@csstools/postcss-gamut-mapping": "npm:^3.0.2"
- "@csstools/postcss-gradients-interpolation-method": "npm:^6.0.2"
- "@csstools/postcss-hwb-function": "npm:^5.0.2"
+ "@csstools/postcss-gamut-mapping": "npm:^3.0.3"
+ "@csstools/postcss-gradients-interpolation-method": "npm:^6.0.3"
+ "@csstools/postcss-hwb-function": "npm:^5.0.3"
"@csstools/postcss-ic-unit": "npm:^5.0.0"
"@csstools/postcss-initial": "npm:^3.0.0"
"@csstools/postcss-is-pseudo-class": "npm:^6.0.0"
@@ -11210,24 +11364,24 @@ __metadata:
"@csstools/postcss-logical-overscroll-behavior": "npm:^3.0.0"
"@csstools/postcss-logical-resize": "npm:^4.0.0"
"@csstools/postcss-logical-viewport-units": "npm:^4.0.0"
- "@csstools/postcss-media-minmax": "npm:^3.0.1"
+ "@csstools/postcss-media-minmax": "npm:^3.0.2"
"@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^4.0.0"
"@csstools/postcss-mixins": "npm:^1.0.0"
"@csstools/postcss-nested-calc": "npm:^5.0.0"
"@csstools/postcss-normalize-display-values": "npm:^5.0.1"
- "@csstools/postcss-oklab-function": "npm:^5.0.2"
+ "@csstools/postcss-oklab-function": "npm:^5.0.3"
"@csstools/postcss-position-area-property": "npm:^2.0.0"
"@csstools/postcss-progressive-custom-properties": "npm:^5.0.0"
"@csstools/postcss-property-rule-prelude-list": "npm:^2.0.0"
- "@csstools/postcss-random-function": "npm:^3.0.1"
- "@csstools/postcss-relative-color-syntax": "npm:^4.0.2"
+ "@csstools/postcss-random-function": "npm:^3.0.2"
+ "@csstools/postcss-relative-color-syntax": "npm:^4.0.3"
"@csstools/postcss-scope-pseudo-class": "npm:^5.0.0"
- "@csstools/postcss-sign-functions": "npm:^2.0.1"
- "@csstools/postcss-stepped-value-functions": "npm:^5.0.1"
+ "@csstools/postcss-sign-functions": "npm:^2.0.2"
+ "@csstools/postcss-stepped-value-functions": "npm:^5.0.2"
"@csstools/postcss-syntax-descriptor-syntax-production": "npm:^2.0.0"
"@csstools/postcss-system-ui-font-family": "npm:^2.0.0"
"@csstools/postcss-text-decoration-shorthand": "npm:^5.0.3"
- "@csstools/postcss-trigonometric-functions": "npm:^5.0.1"
+ "@csstools/postcss-trigonometric-functions": "npm:^5.0.2"
"@csstools/postcss-unset-value": "npm:^5.0.0"
autoprefixer: "npm:^10.4.24"
browserslist: "npm:^4.28.1"
@@ -11237,7 +11391,7 @@ __metadata:
cssdb: "npm:^8.8.0"
postcss-attribute-case-insensitive: "npm:^8.0.0"
postcss-clamp: "npm:^4.1.0"
- postcss-color-functional-notation: "npm:^8.0.2"
+ postcss-color-functional-notation: "npm:^8.0.3"
postcss-color-hex-alpha: "npm:^11.0.0"
postcss-color-rebeccapurple: "npm:^11.0.0"
postcss-custom-media: "npm:^12.0.1"
@@ -11250,7 +11404,7 @@ __metadata:
postcss-font-variant: "npm:^5.0.0"
postcss-gap-properties: "npm:^7.0.0"
postcss-image-set-function: "npm:^8.0.0"
- postcss-lab-function: "npm:^8.0.2"
+ postcss-lab-function: "npm:^8.0.3"
postcss-logical: "npm:^9.0.0"
postcss-nesting: "npm:^14.0.0"
postcss-opacity-percentage: "npm:^3.0.0"
@@ -11262,7 +11416,7 @@ __metadata:
postcss-selector-not: "npm:^9.0.0"
peerDependencies:
postcss: ^8.4
- checksum: 10/0aa9d512cd20eee15242765b28b26f63115c9b31b83946b0f15d7e1a79062ddeff5bbf1edbd6d4fcc3ea260552112443041cfe7b4733e9e560ec39a53d1b812d
+ checksum: 10/40b10b4b0d6f07298a73aaa69db6ffc4e66fc04bd4f68941eda40ea4842d8cff7ca50605ebf7d609cc4ff11f05385c819c28d4b384138d5dccf2c80ae7783c84
languageName: node
linkType: hard
@@ -11314,7 +11468,7 @@ __metadata:
languageName: node
linkType: hard
-"postcss@npm:^8.4.47, postcss@npm:^8.5.6, postcss@npm:^8.5.8":
+"postcss@npm:^8.4.47, postcss@npm:^8.5.8":
version: 8.5.8
resolution: "postcss@npm:8.5.8"
dependencies:
@@ -11325,6 +11479,17 @@ __metadata:
languageName: node
linkType: hard
+"postcss@npm:^8.5.9":
+ version: 8.5.9
+ resolution: "postcss@npm:8.5.9"
+ dependencies:
+ nanoid: "npm:^3.3.11"
+ picocolors: "npm:^1.1.1"
+ source-map-js: "npm:^1.2.1"
+ checksum: 10/b34661c6efca87f7bf747c6c943435e4bfef7617a238c3719103e607c605d16720682fb121b7ebd4f7f748228dfdf8711b82f7ffed80a5c4a9249c070ed5fd87
+ languageName: node
+ linkType: hard
+
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -11341,7 +11506,7 @@ __metadata:
languageName: node
linkType: hard
-"prettier@npm:*, prettier@npm:^3.8.1":
+"prettier@npm:*":
version: 3.8.1
resolution: "prettier@npm:3.8.1"
bin:
@@ -11350,6 +11515,15 @@ __metadata:
languageName: node
linkType: hard
+"prettier@npm:^3.8.2":
+ version: 3.8.2
+ resolution: "prettier@npm:3.8.2"
+ bin:
+ prettier: bin/prettier.cjs
+ checksum: 10/fd784175bc600c07eb2c44d7ec4ee7133f95f26492adad61b6a15c06f438b858181faf096ab74163d7f49500ad80cff4479c6abb084e161a2e85a9df5974ecd1
+ languageName: node
+ linkType: hard
+
"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.0":
version: 30.3.0
resolution: "pretty-format@npm:30.3.0"
@@ -11624,7 +11798,7 @@ __metadata:
languageName: node
linkType: hard
-"pug@npm:^3.0.3, pug@npm:^3.0.4":
+"pug@npm:^3.0.4":
version: 3.0.4
resolution: "pug@npm:3.0.4"
dependencies:
@@ -11919,27 +12093,27 @@ __metadata:
version: 0.0.0-use.local
resolution: "ribajs@workspace:."
dependencies:
- "@playwright/test": "npm:^1.59.0"
+ "@playwright/test": "npm:^1.59.1"
"@popperjs/core": "npm:^2.11.8"
"@ribajs/eslint-config": "workspace:^"
"@ribajs/tsconfig": "workspace:^"
"@ribajs/types": "workspace:^"
- "@typescript-eslint/eslint-plugin": "npm:^8.57.2"
- "@typescript-eslint/parser": "npm:^8.57.2"
+ "@typescript-eslint/eslint-plugin": "npm:^8.58.1"
+ "@typescript-eslint/parser": "npm:^8.58.1"
"@yarnpkg/pnpify": "npm:^4.1.6"
- eslint: "npm:^10.1.0"
+ eslint: "npm:^10.2.0"
eslint-config-prettier: "npm:^10.1.8"
eslint-import-resolver-typescript: "npm:^4.4.4"
eslint-plugin-import: "npm:^2.32.0"
eslint-plugin-prettier: "npm:^5.5.5"
glob: "npm:^13.0.6"
- jsdom: "npm:^26.1.0"
- prettier: "npm:^3.8.1"
+ jsdom: "npm:^29.0.2"
+ prettier: "npm:^3.8.2"
pug: "npm:^3.0.4"
- sass: "npm:^1.98.0"
+ sass: "npm:^1.99.0"
typescript: "npm:^6.0.2"
- vite: "npm:^8.0.3"
- vitest: "npm:^3.2.4"
+ vite: "npm:^8.0.8"
+ vitest: "npm:^4.1.4"
dependenciesMeta:
prettier:
unplugged: true
@@ -11955,27 +12129,27 @@ __metadata:
languageName: node
linkType: hard
-"rolldown@npm:1.0.0-rc.12":
- version: 1.0.0-rc.12
- resolution: "rolldown@npm:1.0.0-rc.12"
- dependencies:
- "@oxc-project/types": "npm:=0.122.0"
- "@rolldown/binding-android-arm64": "npm:1.0.0-rc.12"
- "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.12"
- "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.12"
- "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.12"
- "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.12"
- "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.12"
- "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.12"
- "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.12"
- "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.12"
- "@rolldown/pluginutils": "npm:1.0.0-rc.12"
+"rolldown@npm:1.0.0-rc.15":
+ version: 1.0.0-rc.15
+ resolution: "rolldown@npm:1.0.0-rc.15"
+ dependencies:
+ "@oxc-project/types": "npm:=0.124.0"
+ "@rolldown/binding-android-arm64": "npm:1.0.0-rc.15"
+ "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.15"
+ "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.15"
+ "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.15"
+ "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.15"
+ "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.15"
+ "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.15"
+ "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.15"
+ "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.15"
+ "@rolldown/pluginutils": "npm:1.0.0-rc.15"
dependenciesMeta:
"@rolldown/binding-android-arm64":
optional: true
@@ -12009,7 +12183,7 @@ __metadata:
optional: true
bin:
rolldown: bin/cli.mjs
- checksum: 10/b8cc0d9df80b495a57b63d69a16a5566c600162046edd407f335a6d27e5b6618a2d88d63e82c4e77a1447d18edcc6900696e041c33236ef38ab51d33cf5da2fe
+ checksum: 10/cc07a103297573690bad1469e96e282230f9eb1acc4e22bf3318294bf5b5221d475d1c0822be6fe4958c5618983cac70fb0155afe510ab51516a053564c9304a
languageName: node
linkType: hard
@@ -12036,96 +12210,6 @@ __metadata:
languageName: node
linkType: hard
-"rollup@npm:^4.43.0":
- version: 4.60.1
- resolution: "rollup@npm:4.60.1"
- dependencies:
- "@rollup/rollup-android-arm-eabi": "npm:4.60.1"
- "@rollup/rollup-android-arm64": "npm:4.60.1"
- "@rollup/rollup-darwin-arm64": "npm:4.60.1"
- "@rollup/rollup-darwin-x64": "npm:4.60.1"
- "@rollup/rollup-freebsd-arm64": "npm:4.60.1"
- "@rollup/rollup-freebsd-x64": "npm:4.60.1"
- "@rollup/rollup-linux-arm-gnueabihf": "npm:4.60.1"
- "@rollup/rollup-linux-arm-musleabihf": "npm:4.60.1"
- "@rollup/rollup-linux-arm64-gnu": "npm:4.60.1"
- "@rollup/rollup-linux-arm64-musl": "npm:4.60.1"
- "@rollup/rollup-linux-loong64-gnu": "npm:4.60.1"
- "@rollup/rollup-linux-loong64-musl": "npm:4.60.1"
- "@rollup/rollup-linux-ppc64-gnu": "npm:4.60.1"
- "@rollup/rollup-linux-ppc64-musl": "npm:4.60.1"
- "@rollup/rollup-linux-riscv64-gnu": "npm:4.60.1"
- "@rollup/rollup-linux-riscv64-musl": "npm:4.60.1"
- "@rollup/rollup-linux-s390x-gnu": "npm:4.60.1"
- "@rollup/rollup-linux-x64-gnu": "npm:4.60.1"
- "@rollup/rollup-linux-x64-musl": "npm:4.60.1"
- "@rollup/rollup-openbsd-x64": "npm:4.60.1"
- "@rollup/rollup-openharmony-arm64": "npm:4.60.1"
- "@rollup/rollup-win32-arm64-msvc": "npm:4.60.1"
- "@rollup/rollup-win32-ia32-msvc": "npm:4.60.1"
- "@rollup/rollup-win32-x64-gnu": "npm:4.60.1"
- "@rollup/rollup-win32-x64-msvc": "npm:4.60.1"
- "@types/estree": "npm:1.0.8"
- fsevents: "npm:~2.3.2"
- dependenciesMeta:
- "@rollup/rollup-android-arm-eabi":
- optional: true
- "@rollup/rollup-android-arm64":
- optional: true
- "@rollup/rollup-darwin-arm64":
- optional: true
- "@rollup/rollup-darwin-x64":
- optional: true
- "@rollup/rollup-freebsd-arm64":
- optional: true
- "@rollup/rollup-freebsd-x64":
- optional: true
- "@rollup/rollup-linux-arm-gnueabihf":
- optional: true
- "@rollup/rollup-linux-arm-musleabihf":
- optional: true
- "@rollup/rollup-linux-arm64-gnu":
- optional: true
- "@rollup/rollup-linux-arm64-musl":
- optional: true
- "@rollup/rollup-linux-loong64-gnu":
- optional: true
- "@rollup/rollup-linux-loong64-musl":
- optional: true
- "@rollup/rollup-linux-ppc64-gnu":
- optional: true
- "@rollup/rollup-linux-ppc64-musl":
- optional: true
- "@rollup/rollup-linux-riscv64-gnu":
- optional: true
- "@rollup/rollup-linux-riscv64-musl":
- optional: true
- "@rollup/rollup-linux-s390x-gnu":
- optional: true
- "@rollup/rollup-linux-x64-gnu":
- optional: true
- "@rollup/rollup-linux-x64-musl":
- optional: true
- "@rollup/rollup-openbsd-x64":
- optional: true
- "@rollup/rollup-openharmony-arm64":
- optional: true
- "@rollup/rollup-win32-arm64-msvc":
- optional: true
- "@rollup/rollup-win32-ia32-msvc":
- optional: true
- "@rollup/rollup-win32-x64-gnu":
- optional: true
- "@rollup/rollup-win32-x64-msvc":
- optional: true
- fsevents:
- optional: true
- bin:
- rollup: dist/bin/rollup
- checksum: 10/6866a35efc999990e191fc954a859ba802d13be63ca13b04746459455982f6b8784d92e5eea8db3ef8acf8baba8c43e8e6cb741f3233ba4c46adf148d3708a9c
- languageName: node
- linkType: hard
-
"rrweb-cssom@npm:^0.8.0":
version: 0.8.0
resolution: "rrweb-cssom@npm:0.8.0"
@@ -12199,9 +12283,9 @@ __metadata:
languageName: node
linkType: hard
-"sass@npm:^1.98.0":
- version: 1.98.0
- resolution: "sass@npm:1.98.0"
+"sass@npm:^1.99.0":
+ version: 1.99.0
+ resolution: "sass@npm:1.99.0"
dependencies:
"@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0"
@@ -12212,7 +12296,7 @@ __metadata:
optional: true
bin:
sass: sass.js
- checksum: 10/37d134d07639dc8fc8557495c3b98801bb736f0d1fc5fbfb2e0dba3c21200f447cf3e6166cd285603c922171366696c1d2e766af2c4cf82d882a3dc4d4e39f00
+ checksum: 10/93f9d5c3b3e4659fb68a8e90d9637818581b0152dfb543ac159d57bb1e384f6b2009b7ac35ef88883f441cddfabd248fdcbba3fe814f4f23e3bfe58c917787a6
languageName: node
linkType: hard
@@ -12241,7 +12325,7 @@ __metadata:
languageName: node
linkType: hard
-"semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3":
+"semver@npm:^7.1.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4":
version: 7.7.4
resolution: "semver@npm:7.7.4"
bin:
@@ -12650,10 +12734,10 @@ __metadata:
languageName: node
linkType: hard
-"std-env@npm:^3.9.0":
- version: 3.10.0
- resolution: "std-env@npm:3.10.0"
- checksum: 10/19c9cda4f370b1ffae2b8b08c72167d8c3e5cfa972aaf5c6873f85d0ed2faa729407f5abb194dc33380708c00315002febb6f1e1b484736bfcf9361ad366013a
+"std-env@npm:^4.0.0-rc.1":
+ version: 4.0.0
+ resolution: "std-env@npm:4.0.0"
+ checksum: 10/19ef21cd85da52dc1178288d1b69e242b6579c0a76ddba2374f859aa58678797ec4a34c4f1fe6b9007a032e04d6fd3fca4e27349c88809265a9cbd90d924203f
languageName: node
linkType: hard
@@ -12797,15 +12881,6 @@ __metadata:
languageName: node
linkType: hard
-"strip-literal@npm:^3.0.0":
- version: 3.1.0
- resolution: "strip-literal@npm:3.1.0"
- dependencies:
- js-tokens: "npm:^9.0.1"
- checksum: 10/6eb00906a1c343a1050579d1d6023e067a2d72152edb92e64cad49535115beb2e77905ace24aa459f29b66e75edba75ef9d8eca90575b0322640d64a5d37e131
- languageName: node
- linkType: hard
-
"supports-color@npm:*":
version: 10.2.2
resolution: "supports-color@npm:10.2.2"
@@ -12871,6 +12946,20 @@ __metadata:
languageName: node
linkType: hard
+"tailwindcss@npm:4.2.2, tailwindcss@npm:^4.2.2":
+ version: 4.2.2
+ resolution: "tailwindcss@npm:4.2.2"
+ checksum: 10/f468b441d32b7278e7e2639b0f95f59fb8a52678e8c3a027f636ec51006ed32f68aeb2e66eaf81b958b234745d2ccae4757e4ba5dda23c5e158065060f90aa78
+ languageName: node
+ linkType: hard
+
+"tapable@npm:^2.3.0":
+ version: 2.3.2
+ resolution: "tapable@npm:2.3.2"
+ checksum: 10/fd3affe2e34efb3970883f934b1828f10b48dffb1eb71a52b7f955bfdd88bf80e94ec388704d95334f72ddf77e34d813b19e1f4bf56897d20252fa025d44bede
+ languageName: node
+ linkType: hard
+
"tar@npm:^7.5.3, tar@npm:^7.5.4":
version: 7.5.13
resolution: "tar@npm:7.5.13"
@@ -12916,10 +13005,10 @@ __metadata:
languageName: node
linkType: hard
-"tinyexec@npm:^0.3.2":
- version: 0.3.2
- resolution: "tinyexec@npm:0.3.2"
- checksum: 10/b9d5fed3166fb1acd1e7f9a89afcd97ccbe18b9c1af0278e429455f6976d69271ba2d21797e7c36d57d6b05025e525d2882d88c2ab435b60d1ddf2fea361de57
+"tinyexec@npm:^1.0.2":
+ version: 1.1.1
+ resolution: "tinyexec@npm:1.1.1"
+ checksum: 10/480bbd7b0cdd73652e1a03ed82cec29cbde7d75e68094b65d289eb31578d467954d81af41f3a6de0bc805f6c22c89a36d24986a6c4c0349fa230dfb3924530a7
languageName: node
linkType: hard
@@ -12940,24 +13029,10 @@ __metadata:
languageName: node
linkType: hard
-"tinypool@npm:^1.1.1":
- version: 1.1.1
- resolution: "tinypool@npm:1.1.1"
- checksum: 10/0d54139e9dbc6ef33349768fa78890a4d708d16a7ab68e4e4ef3bb740609ddf0f9fd13292c2f413fbba756166c97051a657181c8f7ae92ade690604f183cc01d
- languageName: node
- linkType: hard
-
-"tinyrainbow@npm:^2.0.0":
- version: 2.0.0
- resolution: "tinyrainbow@npm:2.0.0"
- checksum: 10/94d4e16246972614a5601eeb169ba94f1d49752426312d3cf8cc4f2cc663a2e354ffc653aa4de4eebccbf9eeebdd0caef52d1150271fdfde65d7ae7f3dcb9eb5
- languageName: node
- linkType: hard
-
-"tinyspy@npm:^4.0.3":
- version: 4.0.4
- resolution: "tinyspy@npm:4.0.4"
- checksum: 10/858a99e3ded2fba8fe7c243099d9e58e926d6525af03d19cdf86c1a9a30398161fb830b4f77890d266bcc1c69df08fa6f4baf29d089385e4cdaa98d7b6296e7c
+"tinyrainbow@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "tinyrainbow@npm:3.1.0"
+ checksum: 10/4c2c01dde1e5bb9a74973daaae141d4d733d246280b2f9a7f6a9e7dd8e940d48b2580a6086125278777897bc44635d6ccec5f9f563c2179dd2129f4542d0ec05
languageName: node
linkType: hard
@@ -12968,6 +13043,13 @@ __metadata:
languageName: node
linkType: hard
+"tldts-core@npm:^7.0.28":
+ version: 7.0.28
+ resolution: "tldts-core@npm:7.0.28"
+ checksum: 10/f4189f80c1d5205689e4776a7759bc9862ae01b242a6e986958b9ddcd2327c76e9ca11122ed3251312d4b8c464026f30aeda597ab5571c7d82c500b64729914b
+ languageName: node
+ linkType: hard
+
"tldts@npm:^6.1.32":
version: 6.1.86
resolution: "tldts@npm:6.1.86"
@@ -12979,6 +13061,17 @@ __metadata:
languageName: node
linkType: hard
+"tldts@npm:^7.0.5":
+ version: 7.0.28
+ resolution: "tldts@npm:7.0.28"
+ dependencies:
+ tldts-core: "npm:^7.0.28"
+ bin:
+ tldts: bin/cli.js
+ checksum: 10/e8f87e73b52785d3177084fb631e3a513976f1f71359a372803bc4b86d26bac890fab41375b07047a64569e6c64d068cbc5bef56ab3464c0586bc3fbf202935b
+ languageName: node
+ linkType: hard
+
"tmpl@npm:1.0.5":
version: 1.0.5
resolution: "tmpl@npm:1.0.5"
@@ -13025,6 +13118,15 @@ __metadata:
languageName: node
linkType: hard
+"tough-cookie@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "tough-cookie@npm:6.0.1"
+ dependencies:
+ tldts: "npm:^7.0.5"
+ checksum: 10/915b1167e0630598eb0644e8bc089ddc28a23bf05f3c329a4a0d879c6b9801a2603be65acb06b5d2dd0f589cabb06bb638837f8222dd82a7023655f07269451a
+ languageName: node
+ linkType: hard
+
"tr46@npm:^5.1.0":
version: 5.1.1
resolution: "tr46@npm:5.1.1"
@@ -13034,6 +13136,15 @@ __metadata:
languageName: node
linkType: hard
+"tr46@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "tr46@npm:6.0.0"
+ dependencies:
+ punycode: "npm:^2.3.1"
+ checksum: 10/e6d402eb2b780a40042f327f77b4ae316da1d2b18a29c16e48c239f5267c6005bbf780f854179cfae62b02dfaa70b0e9aad8f0078ccc4225f5b3b3b131928e8f
+ languageName: node
+ linkType: hard
+
"tree-kill@npm:1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
@@ -13059,17 +13170,17 @@ __metadata:
languageName: node
linkType: hard
-"ts-jest@npm:^29.4.6":
- version: 29.4.6
- resolution: "ts-jest@npm:29.4.6"
+"ts-jest@npm:^29.4.9":
+ version: 29.4.9
+ resolution: "ts-jest@npm:29.4.9"
dependencies:
bs-logger: "npm:^0.2.6"
fast-json-stable-stringify: "npm:^2.1.0"
- handlebars: "npm:^4.7.8"
+ handlebars: "npm:^4.7.9"
json5: "npm:^2.2.3"
lodash.memoize: "npm:^4.1.2"
make-error: "npm:^1.3.6"
- semver: "npm:^7.7.3"
+ semver: "npm:^7.7.4"
type-fest: "npm:^4.41.0"
yargs-parser: "npm:^21.1.1"
peerDependencies:
@@ -13079,7 +13190,7 @@ __metadata:
babel-jest: ^29.0.0 || ^30.0.0
jest: ^29.0.0 || ^30.0.0
jest-util: ^29.0.0 || ^30.0.0
- typescript: ">=4.3 <6"
+ typescript: ">=4.3 <7"
peerDependenciesMeta:
"@babel/core":
optional: true
@@ -13095,7 +13206,7 @@ __metadata:
optional: true
bin:
ts-jest: cli.js
- checksum: 10/e0ff9e13f684166d5331808b288043b8054f49a1c2970480a92ba3caec8d0ff20edd092f2a4e7a3ad8fcb9ba4d674bee10ec7ee75046d8066bbe43a7d16cf72e
+ checksum: 10/f5e81b1e13fff08da5b92d5a72f984f3393f40df73a1ae54473a780436b95dddb1452c78256e6d70a701c09ea427449657a5fbb3d142dc7e7a82eb192e80c3db
languageName: node
linkType: hard
@@ -13118,7 +13229,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:^2.1.0, tslib@npm:^2.4.0":
+"tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7
@@ -13318,6 +13429,13 @@ __metadata:
languageName: node
linkType: hard
+"undici@npm:^7.24.5":
+ version: 7.25.0
+ resolution: "undici@npm:7.25.0"
+ checksum: 10/038d3568c72bb976e3cc389284f7f1cc64cd70d578300e4676a449fbcb624a35fe99ac127b5f3729f18b8246d6c090444ab61b1b67736bb88f52a3e913d76bf8
+ languageName: node
+ linkType: hard
+
"unrs-resolver@npm:^1.7.11":
version: 1.11.1
resolution: "unrs-resolver@npm:1.11.1"
@@ -13443,104 +13561,34 @@ __metadata:
languageName: node
linkType: hard
-"vite-node@npm:3.2.4":
- version: 3.2.4
- resolution: "vite-node@npm:3.2.4"
- dependencies:
- cac: "npm:^6.7.14"
- debug: "npm:^4.4.1"
- es-module-lexer: "npm:^1.7.0"
- pathe: "npm:^2.0.3"
- vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0"
- bin:
- vite-node: vite-node.mjs
- checksum: 10/343244ecabbab3b6e1a3065dabaeefa269965a7a7c54652d4b7a7207ee82185e887af97268c61755dcb2dd6a6ce5d9e114400cbd694229f38523e935703cc62f
- languageName: node
- linkType: hard
-
-"vite-plugin-static-copy@npm:^3.2.0":
- version: 3.4.0
- resolution: "vite-plugin-static-copy@npm:3.4.0"
+"vite-plugin-static-copy@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "vite-plugin-static-copy@npm:4.0.1"
dependencies:
chokidar: "npm:^3.6.0"
p-map: "npm:^7.0.4"
picocolors: "npm:^1.1.1"
tinyglobby: "npm:^0.2.15"
peerDependencies:
- vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
- checksum: 10/282d35274203b24c693f8677e7c683399c6bcc3e7327795eb990aa69f66ee7bf9977795be0230df8a11e0416ea2bbf49d98624595a49655353919016953a0587
- languageName: node
- linkType: hard
-
-"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0":
- version: 7.3.1
- resolution: "vite@npm:7.3.1"
- dependencies:
- esbuild: "npm:^0.27.0"
- fdir: "npm:^6.5.0"
- fsevents: "npm:~2.3.3"
- picomatch: "npm:^4.0.3"
- postcss: "npm:^8.5.6"
- rollup: "npm:^4.43.0"
- tinyglobby: "npm:^0.2.15"
- peerDependencies:
- "@types/node": ^20.19.0 || >=22.12.0
- jiti: ">=1.21.0"
- less: ^4.0.0
- lightningcss: ^1.21.0
- sass: ^1.70.0
- sass-embedded: ^1.70.0
- stylus: ">=0.54.8"
- sugarss: ^5.0.0
- terser: ^5.16.0
- tsx: ^4.8.1
- yaml: ^2.4.2
- dependenciesMeta:
- fsevents:
- optional: true
- peerDependenciesMeta:
- "@types/node":
- optional: true
- jiti:
- optional: true
- less:
- optional: true
- lightningcss:
- optional: true
- sass:
- optional: true
- sass-embedded:
- optional: true
- stylus:
- optional: true
- sugarss:
- optional: true
- terser:
- optional: true
- tsx:
- optional: true
- yaml:
- optional: true
- bin:
- vite: bin/vite.js
- checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ checksum: 10/fb3fc7942034ee71f90fb874efb63b50a3103ab47e06bc9f9df47eb2dbc6440f005c1ea6045dbf95e4575fcbead0c134cb1007d7383e791b9db30176e126e7f5
languageName: node
linkType: hard
-"vite@npm:^8.0.3":
- version: 8.0.3
- resolution: "vite@npm:8.0.3"
+"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.8":
+ version: 8.0.8
+ resolution: "vite@npm:8.0.8"
dependencies:
fsevents: "npm:~2.3.3"
lightningcss: "npm:^1.32.0"
picomatch: "npm:^4.0.4"
postcss: "npm:^8.5.8"
- rolldown: "npm:1.0.0-rc.12"
+ rolldown: "npm:1.0.0-rc.15"
tinyglobby: "npm:^0.2.15"
peerDependencies:
"@types/node": ^20.19.0 || >=22.12.0
"@vitejs/devtools": ^0.1.0
- esbuild: ^0.27.0
+ esbuild: ^0.27.0 || ^0.28.0
jiti: ">=1.21.0"
less: ^4.0.0
sass: ^1.70.0
@@ -13580,53 +13628,63 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
- checksum: 10/745b791cb71297ac3877af061da44751d93f198413426bbb76a1f8384d76d4162a6ad739b2bcdf5fb966cd1295db59412614aee60738e40e1c99cee561e682f0
+ checksum: 10/07a74ec338a9f309c4a2d003a17289d7054086260ed3fe3e2ff9bf377e5bf4701919f44b7312917aa2a8fcc8189fd4a545799a1945d68fcdc8a1e61f20c19bf8
languageName: node
linkType: hard
-"vitest@npm:^3.2.4":
- version: 3.2.4
- resolution: "vitest@npm:3.2.4"
+"vitest@npm:^4.1.4":
+ version: 4.1.4
+ resolution: "vitest@npm:4.1.4"
dependencies:
- "@types/chai": "npm:^5.2.2"
- "@vitest/expect": "npm:3.2.4"
- "@vitest/mocker": "npm:3.2.4"
- "@vitest/pretty-format": "npm:^3.2.4"
- "@vitest/runner": "npm:3.2.4"
- "@vitest/snapshot": "npm:3.2.4"
- "@vitest/spy": "npm:3.2.4"
- "@vitest/utils": "npm:3.2.4"
- chai: "npm:^5.2.0"
- debug: "npm:^4.4.1"
- expect-type: "npm:^1.2.1"
- magic-string: "npm:^0.30.17"
+ "@vitest/expect": "npm:4.1.4"
+ "@vitest/mocker": "npm:4.1.4"
+ "@vitest/pretty-format": "npm:4.1.4"
+ "@vitest/runner": "npm:4.1.4"
+ "@vitest/snapshot": "npm:4.1.4"
+ "@vitest/spy": "npm:4.1.4"
+ "@vitest/utils": "npm:4.1.4"
+ es-module-lexer: "npm:^2.0.0"
+ expect-type: "npm:^1.3.0"
+ magic-string: "npm:^0.30.21"
+ obug: "npm:^2.1.1"
pathe: "npm:^2.0.3"
- picomatch: "npm:^4.0.2"
- std-env: "npm:^3.9.0"
+ picomatch: "npm:^4.0.3"
+ std-env: "npm:^4.0.0-rc.1"
tinybench: "npm:^2.9.0"
- tinyexec: "npm:^0.3.2"
- tinyglobby: "npm:^0.2.14"
- tinypool: "npm:^1.1.1"
- tinyrainbow: "npm:^2.0.0"
- vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0"
- vite-node: "npm:3.2.4"
+ tinyexec: "npm:^1.0.2"
+ tinyglobby: "npm:^0.2.15"
+ tinyrainbow: "npm:^3.1.0"
+ vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0"
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
- "@types/debug": ^4.1.12
- "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0
- "@vitest/browser": 3.2.4
- "@vitest/ui": 3.2.4
+ "@opentelemetry/api": ^1.9.0
+ "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
+ "@vitest/browser-playwright": 4.1.4
+ "@vitest/browser-preview": 4.1.4
+ "@vitest/browser-webdriverio": 4.1.4
+ "@vitest/coverage-istanbul": 4.1.4
+ "@vitest/coverage-v8": 4.1.4
+ "@vitest/ui": 4.1.4
happy-dom: "*"
jsdom: "*"
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
"@edge-runtime/vm":
optional: true
- "@types/debug":
+ "@opentelemetry/api":
optional: true
"@types/node":
optional: true
- "@vitest/browser":
+ "@vitest/browser-playwright":
+ optional: true
+ "@vitest/browser-preview":
+ optional: true
+ "@vitest/browser-webdriverio":
+ optional: true
+ "@vitest/coverage-istanbul":
+ optional: true
+ "@vitest/coverage-v8":
optional: true
"@vitest/ui":
optional: true
@@ -13634,9 +13692,11 @@ __metadata:
optional: true
jsdom:
optional: true
+ vite:
+ optional: false
bin:
vitest: vitest.mjs
- checksum: 10/f10bbce093ecab310ecbe484536ef4496fb9151510b2be0c5907c65f6d31482d9c851f3182531d1d27d558054aa78e8efd9d4702ba6c82058657e8b6a52507ee
+ checksum: 10/c5608c506ae9ab3d0baa7445290c240941ad54a93eb853a005b2fe518efb1b28282945e0565ca16a624cca5b23af0c33ee34fbc2c38e6664ea54b08b9a22a653
languageName: node
linkType: hard
@@ -13679,6 +13739,13 @@ __metadata:
languageName: node
linkType: hard
+"webidl-conversions@npm:^8.0.1":
+ version: 8.0.1
+ resolution: "webidl-conversions@npm:8.0.1"
+ checksum: 10/0f7007311f1fc257a8e406dd236f13b61fb57cf0fddb476aec33457d2d0add2d012d6df0eeb00934399238e3f3b9dad30f59dc6ac83024ae0ebd5a518bf365e8
+ languageName: node
+ linkType: hard
+
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
@@ -13695,6 +13762,13 @@ __metadata:
languageName: node
linkType: hard
+"whatwg-mimetype@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "whatwg-mimetype@npm:5.0.0"
+ checksum: 10/a2d5da445f671ed34010b45283ffb9ba3c68c695b8ec91f7400cfc9149c35eb2bc47bd2c39bbe8e026786b955ace03402ba2e5cfde4955434a3ec3c511a85d0a
+ languageName: node
+ linkType: hard
+
"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.1":
version: 14.2.0
resolution: "whatwg-url@npm:14.2.0"
@@ -13705,6 +13779,17 @@ __metadata:
languageName: node
linkType: hard
+"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1":
+ version: 16.0.1
+ resolution: "whatwg-url@npm:16.0.1"
+ dependencies:
+ "@exodus/bytes": "npm:^1.11.0"
+ tr46: "npm:^6.0.0"
+ webidl-conversions: "npm:^8.0.1"
+ checksum: 10/221cc15ef89288dc1fafdb409352c62ab12ba9ff7f0753e925d8799c87b20371f3bc762dc0a8a5b9c23cddc4b1860537fc6c1bcc9d816ace9b3d3c47212cd163
+ languageName: node
+ linkType: hard
+
"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
version: 1.1.1
resolution: "which-boxed-primitive@npm:1.1.1"