diff --git a/pkgs/clan-app/README.md b/pkgs/clan-app/README.md
index 8f1b81dd9..14cbcc41b 100644
--- a/pkgs/clan-app/README.md
+++ b/pkgs/clan-app/README.md
@@ -26,7 +26,7 @@ direnv: export +AR +AS +CC +CLAN_CORE_PATH +CONFIG_SHELL +CXX +DETERMINISTIC_BUI
Once that has loaded, you can run the local dev environment by running:
```
-$ process-compose --use-uds --keep-project
+$ process-compose --use-uds --keep-project -n app
```
This will start a [process-compose] instance containing two processes:
@@ -50,6 +50,24 @@ From there you can start `clan-app` again with `F7`.
Follow the instructions below to set up your development environment and start the application:
+## Storybook
+
+We use [Storybook] to develop UI components.
+It can be started by running the following:
+
+```console
+$ process-compose --use-uds --keep-project -n storybook
+```
+
+This will start a [process-compose] instance containing two processes:
+
+* `storybook` which is the main [storybook] process.
+* `luakit` which is a [webkit]-based browser for viewing the stories with. This is the same underlying engine used when
+rendering the app.
+
+You can run storybook tests with `npm run test-storybook`.
+If you change how a component(s) renders,
+you will need to update the snapshots with `npm run test-storybook-update-snapshots`.
## Start clan-app without process-compose
@@ -139,3 +157,5 @@ Here are some important documentation links related to the Clan App:
[process-compose]: https://f1bonacc1.github.io/process-compose/
[vite]: https://vite.dev/
[webview]: https://github.com/webview/webview
+[Storybook]: https://storybook.js.org/
+[webkit]: https://webkit.org/
\ No newline at end of file
diff --git a/pkgs/clan-app/flake-module.nix b/pkgs/clan-app/flake-module.nix
index a23698a87..cbcbd7f1f 100644
--- a/pkgs/clan-app/flake-module.nix
+++ b/pkgs/clan-app/flake-module.nix
@@ -29,6 +29,8 @@
fonts = config.packages.fonts;
};
+ packages.clan-app-ui-storybook = self'.packages.clan-app-ui.storybook;
+
checks = config.packages.clan-app.tests;
};
}
diff --git a/pkgs/clan-app/process-compose.yaml b/pkgs/clan-app/process-compose.yaml
index 4a230f7fe..93f883022 100644
--- a/pkgs/clan-app/process-compose.yaml
+++ b/pkgs/clan-app/process-compose.yaml
@@ -1,7 +1,10 @@
version: "0.5"
processes:
+ # App Dev
+
clan-app-ui:
+ namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui
npm install
@@ -9,6 +12,7 @@ processes:
ready_log_line: "VITE"
clan-app:
+ namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app
./bin/clan-app --debug --content-uri http://localhost:3000
@@ -17,3 +21,19 @@ processes:
condition: "process_log_ready"
is_foreground: true
ready_log_line: "Debug mode enabled"
+
+ # Storybook Dev
+
+ storybook:
+ namespace: "storybook"
+ command: |
+ cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui
+ npm run storybook-dev -- --ci
+ ready_log_line: "started"
+
+ luakit:
+ namespace: "storybook"
+ command: "luakit http://localhost:6006"
+ depends_on:
+ storybook:
+ condition: "process_log_ready"
diff --git a/pkgs/clan-app/shell.nix b/pkgs/clan-app/shell.nix
index 0eaa84fdc..2825829fb 100644
--- a/pkgs/clan-app/shell.nix
+++ b/pkgs/clan-app/shell.nix
@@ -5,8 +5,11 @@
webview-lib,
clan-app-ui,
clan-ts-api,
+ ps,
process-compose,
json2ts,
+ playwright-driver,
+ luakit,
self',
}:
let
@@ -25,11 +28,14 @@ mkShell {
packages = [
# required for reload-python-api.sh script
json2ts
+ # for viewing the storybook in a webkit-based browser to match webview
+ luakit
];
inherit (clan-app) propagatedBuildInputs;
nativeBuildInputs = clan-app.nativeBuildInputs ++ [
+ ps
process-compose
];
@@ -56,7 +62,6 @@ mkShell {
# Add current package to PYTHONPATH
export PYTHONPATH="$(pwd)''${PYTHONPATH:+:$PYTHONPATH:}"
-
popd
# Add clan-cli to the python path so that we can import it without building it in nix first
@@ -77,6 +82,18 @@ mkShell {
chmod -R +w api
popd
+ # configure playwright for storybook snapshot testing
+ export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
+ export PLAYWRIGHT_BROWSERS_PATH=${
+ playwright-driver.browsers.override {
+ withFfmpeg = false;
+ withFirefox = false;
+ withChromium = false;
+ withChromiumHeadlessShell = true;
+ }
+ }
+ export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE="ubuntu-24.04"
+
# configure process-compose
if test -f "$GIT_ROOT/pkgs/clan-app/.local.env"; then
source "$GIT_ROOT/pkgs/clan-app/.local.env"
diff --git a/pkgs/clan-app/ui.nix b/pkgs/clan-app/ui.nix
index 22f8bb641..b264e7492 100644
--- a/pkgs/clan-app/ui.nix
+++ b/pkgs/clan-app/ui.nix
@@ -2,11 +2,12 @@
buildNpmPackage,
nodejs_22,
importNpmLock,
-
clan-ts-api,
+ playwright-driver,
+ ps,
fonts,
}:
-buildNpmPackage {
+buildNpmPackage (finalAttrs: {
pname = "clan-app-ui";
version = "0.0.1";
nodejs = nodejs_22;
@@ -15,6 +16,7 @@ buildNpmPackage {
npmDeps = importNpmLock {
npmRoot = ./ui;
};
+
npmConfigHook = importNpmLock.npmConfigHook;
preBuild = ''
@@ -22,4 +24,36 @@ buildNpmPackage {
cp -r ${clan-ts-api}/* api
cp -r ${fonts} ".fonts"
'';
-}
+
+ passthru = rec {
+ storybook = buildNpmPackage {
+ pname = "${finalAttrs.pname}-storybook";
+ inherit (finalAttrs)
+ version
+ nodejs
+ src
+ npmDeps
+ npmConfigHook
+ preBuild
+ ;
+
+ nativeBuildInputs = finalAttrs.nativeBuildInputs ++ [
+ ps
+ ];
+
+ npmBuildScript = "test-storybook-static";
+
+ env = finalAttrs.env // {
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1;
+ PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers.override {
+ withChromiumHeadlessShell = true;
+ }}";
+ PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "ubuntu-24.04";
+ };
+
+ postBuild = ''
+ mv storybook-static $out
+ '';
+ };
+ };
+})
diff --git a/pkgs/clan-app/ui/.gitignore b/pkgs/clan-app/ui/.gitignore
index 5eeb7ff46..eed390273 100644
--- a/pkgs/clan-app/ui/.gitignore
+++ b/pkgs/clan-app/ui/.gitignore
@@ -1,4 +1,5 @@
app/api
app/.fonts
-.vite
\ No newline at end of file
+.vite
+storybook-static
\ No newline at end of file
diff --git a/pkgs/clan-app/ui/.storybook/main.ts b/pkgs/clan-app/ui/.storybook/main.ts
index ac193c520..518caac24 100644
--- a/pkgs/clan-app/ui/.storybook/main.ts
+++ b/pkgs/clan-app/ui/.storybook/main.ts
@@ -12,7 +12,6 @@ const config: StorybookConfig = {
addons: [
getAbsolutePath("@storybook/addon-links"),
getAbsolutePath("@storybook/addon-essentials"),
- getAbsolutePath("@chromatic-com/storybook"),
getAbsolutePath("@storybook/addon-interactions"),
],
framework: {
@@ -27,6 +26,9 @@ const config: StorybookConfig = {
docs: {
autodocs: "tag",
},
+ core: {
+ disableTelemetry: true,
+ },
};
export default config;
diff --git a/pkgs/clan-app/ui/.storybook/test-runner.ts b/pkgs/clan-app/ui/.storybook/test-runner.ts
new file mode 100644
index 000000000..f9344480a
--- /dev/null
+++ b/pkgs/clan-app/ui/.storybook/test-runner.ts
@@ -0,0 +1,12 @@
+import type { TestRunnerConfig } from "@storybook/test-runner";
+
+const config: TestRunnerConfig = {
+ async postVisit(page, context) {
+ // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root
+ const elementHandler = await page.$("#storybook-root");
+ const innerHTML = await elementHandler.innerHTML();
+ expect(innerHTML).toMatchSnapshot();
+ },
+};
+
+export default config;
diff --git a/pkgs/clan-app/ui/eslint.config.mjs b/pkgs/clan-app/ui/eslint.config.mjs
index c837e0825..44b2bae56 100644
--- a/pkgs/clan-app/ui/eslint.config.mjs
+++ b/pkgs/clan-app/ui/eslint.config.mjs
@@ -2,6 +2,7 @@ import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import tailwind from "eslint-plugin-tailwindcss";
import pluginQuery from "@tanstack/eslint-plugin-query";
+import { globalIgnores } from "eslint/config";
const config = tseslint.config(
eslint.configs.recommended,
@@ -9,6 +10,7 @@ const config = tseslint.config(
...tseslint.configs.strict,
...tseslint.configs.stylistic,
...tailwind.configs["flat/recommended"],
+ globalIgnores(["src/types/index.d.ts"]),
{
rules: {
"tailwindcss/no-contradicting-classname": [
diff --git a/pkgs/clan-app/ui/package-lock.json b/pkgs/clan-app/ui/package-lock.json
index 4e8e38024..c38ba7863 100644
--- a/pkgs/clan-app/ui/package-lock.json
+++ b/pkgs/clan-app/ui/package-lock.json
@@ -25,7 +25,6 @@
},
"devDependencies": {
"@babel/plugin-syntax-import-attributes": "^7.27.1",
- "@chromatic-com/storybook": "^3.2.6",
"@eslint/js": "^9.3.0",
"@kachurun/storybook-solid": "^8.6.7",
"@kachurun/storybook-solid-vite": "^8.6.7",
@@ -42,8 +41,10 @@
"@typescript-eslint/parser": "^8.32.1",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
+ "concurrently": "^9.1.2",
"eslint": "^9.27.0",
"eslint-plugin-tailwindcss": "^3.17.0",
+ "http-server": "^14.1.1",
"jsdom": "^26.1.0",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
@@ -56,7 +57,8 @@
"vite": "^6.3.5",
"vite-plugin-solid": "^2.8.2",
"vite-plugin-solid-svg": "^0.8.1",
- "vitest": "^3.1.4"
+ "vitest": "^3.1.4",
+ "wait-on": "^8.0.3"
}
},
"node_modules/@adobe/css-tools": {
@@ -653,56 +655,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@chromatic-com/storybook": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-3.2.6.tgz",
- "integrity": "sha512-FDmn5Ry2DzQdik+eq2sp/kJMMT36Ewe7ONXUXM2Izd97c7r6R/QyGli8eyh/F0iyqVvbLveNYFyF0dBOJNwLqw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chromatic": "^11.15.0",
- "filesize": "^10.0.12",
- "jsonfile": "^6.1.0",
- "react-confetti": "^6.1.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=16.0.0",
- "yarn": ">=1.22.18"
- },
- "peerDependencies": {
- "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0"
- }
- },
- "node_modules/@chromatic-com/storybook/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/@chromatic-com/storybook/node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
"node_modules/@corvu/accordion": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@corvu/accordion/-/accordion-0.2.5.tgz",
@@ -4719,6 +4671,13 @@
"node": ">=4"
}
},
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
@@ -4988,6 +4947,26 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/better-opn": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz",
@@ -5345,30 +5324,6 @@
"node": ">= 6"
}
},
- "node_modules/chromatic": {
- "version": "11.29.0",
- "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.29.0.tgz",
- "integrity": "sha512-yisBlntp9hHVj19lIQdpTlcYIXuU9H/DbFuu6tyWHmj6hWT2EtukCCcxYXL78XdQt1vm2GfIrtgtKpj/Rzmo4A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "chroma": "dist/bin.js",
- "chromatic": "dist/bin.js",
- "chromatic-cli": "dist/bin.js"
- },
- "peerDependencies": {
- "@chromatic-com/cypress": "^0.*.* || ^1.0.0",
- "@chromatic-com/playwright": "^0.*.* || ^1.0.0"
- },
- "peerDependenciesMeta": {
- "@chromatic-com/cypress": {
- "optional": true
- },
- "@chromatic-com/playwright": {
- "optional": true
- }
- }
- },
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -5546,6 +5501,48 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
+ "node_modules/concurrently": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
+ "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.8.1",
+ "shell-quote": "^1.8.1",
+ "supports-color": "^8.1.1",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/concurrently/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -5553,6 +5550,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/corser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
+ "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/corvu": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/corvu/-/corvu-0.7.2.tgz",
@@ -6496,6 +6503,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -6673,16 +6687,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/filesize": {
- "version": "10.1.6",
- "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
- "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 10.4.0"
- }
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -7260,6 +7264,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -7384,6 +7398,21 @@
"dev": true,
"license": "BSD-2-Clause"
},
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -7398,6 +7427,73 @@
"node": ">= 14"
}
},
+ "node_modules/http-server": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^4.1.2",
+ "corser": "^2.0.1",
+ "he": "^1.2.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy": "^1.18.1",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.6",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.28",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^4.0.1"
+ },
+ "bin": {
+ "http-server": "bin/http-server"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-server/node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-server/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/http-server/node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -8608,6 +8704,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jest-process-manager/node_modules/wait-on": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
+ "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.1",
+ "joi": "^17.11.0",
+ "lodash": "^4.17.21",
+ "minimist": "^1.2.8",
+ "rxjs": "^7.8.1"
+ },
+ "bin": {
+ "wait-on": "bin/wait-on"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/jest-regex-util": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
@@ -9201,19 +9317,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -10544,6 +10647,19 @@
"node": ">= 6"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -10588,6 +10704,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)",
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -10982,6 +11108,20 @@
"node": ">=10"
}
},
+ "node_modules/portfinder": {
+ "version": "1.0.37",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz",
+ "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.6",
+ "debug": "^4.3.6"
+ },
+ "engines": {
+ "node": ">= 10.12"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -11351,6 +11491,22 @@
],
"license": "MIT"
},
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11381,22 +11537,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/react-confetti": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz",
- "integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tween-functions": "^1.2.0"
- },
- "engines": {
- "node": ">=16"
- },
- "peerDependencies": {
- "react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@@ -11549,6 +11689,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -11799,6 +11946,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/secure-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+ "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -11878,6 +12032,95 @@
"node": ">=8"
}
},
+ "node_modules/shell-quote": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
+ "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -12896,13 +13139,6 @@
"dev": true,
"license": "0BSD"
},
- "node_modules/tween-functions": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
- "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==",
- "dev": true,
- "license": "BSD"
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -13010,6 +13246,18 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/union": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
+ "dev": true,
+ "dependencies": {
+ "qs": "^6.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/unist-util-is": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz",
@@ -13096,16 +13344,6 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
- "node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
"node_modules/unplugin": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
@@ -13160,6 +13398,13 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -13528,17 +13773,17 @@
}
},
"node_modules/wait-on": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
- "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.3.tgz",
+ "integrity": "sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "axios": "^1.6.1",
- "joi": "^17.11.0",
+ "axios": "^1.8.2",
+ "joi": "^17.13.3",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
- "rxjs": "^7.8.1"
+ "rxjs": "^7.8.2"
},
"bin": {
"wait-on": "bin/wait-on"
diff --git a/pkgs/clan-app/ui/package.json b/pkgs/clan-app/ui/package.json
index b70c61330..490d433be 100644
--- a/pkgs/clan-app/ui/package.json
+++ b/pkgs/clan-app/ui/package.json
@@ -11,12 +11,16 @@
"serve": "vite preview",
"check": "tsc --noEmit --skipLibCheck && eslint ./src",
"test": "vitest run --typecheck",
- "storybook": "storybook dev -p 6006"
+ "storybook": "storybook",
+ "storybook-build": "storybook build",
+ "storybook-dev": "storybook dev -p 6006",
+ "test-storybook": "test-storybook --browsers chromium --ci",
+ "test-storybook-update-snapshots": "npm run test-storybook -- --updateSnapshot",
+ "test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'http-server storybook-static --port 6006 --silent' 'wait-on tcp:127.0.0.1:6006 && npm run test-storybook'"
},
"license": "MIT",
"devDependencies": {
"@babel/plugin-syntax-import-attributes": "^7.27.1",
- "@chromatic-com/storybook": "^3.2.6",
"@eslint/js": "^9.3.0",
"@kachurun/storybook-solid": "^8.6.7",
"@kachurun/storybook-solid-vite": "^8.6.7",
@@ -33,8 +37,10 @@
"@typescript-eslint/parser": "^8.32.1",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
+ "concurrently": "^9.1.2",
"eslint": "^9.27.0",
"eslint-plugin-tailwindcss": "^3.17.0",
+ "http-server": "^14.1.1",
"jsdom": "^26.1.0",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
@@ -47,7 +53,8 @@
"vite": "^6.3.5",
"vite-plugin-solid": "^2.8.2",
"vite-plugin-solid-svg": "^0.8.1",
- "vitest": "^3.1.4"
+ "vitest": "^3.1.4",
+ "wait-on": "^8.0.3"
},
"dependencies": {
"@floating-ui/dom": "^1.6.8",
diff --git a/pkgs/clan-app/ui/src/components/Button/__snapshots__/Button.stories.tsx.snap b/pkgs/clan-app/ui/src/components/Button/__snapshots__/Button.stories.tsx.snap
new file mode 100644
index 000000000..38ba3e641
--- /dev/null
+++ b/pkgs/clan-app/ui/src/components/Button/__snapshots__/Button.stories.tsx.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Components/Button Default smoke-test 1`] = `
+
+`;
+
+exports[`Components/Button Ghost smoke-test 1`] = `
+
+`;
+
+exports[`Components/Button Light smoke-test 1`] = `
+
+`;
+
+exports[`Components/Button Small smoke-test 1`] = `
+
+`;
diff --git a/pkgs/clan-app/ui/src/types/index.d.ts b/pkgs/clan-app/ui/src/types/index.d.ts
new file mode 100644
index 000000000..25ed621a0
--- /dev/null
+++ b/pkgs/clan-app/ui/src/types/index.d.ts
@@ -0,0 +1,90 @@
+// @ts-nocheck
+declare module "@kachurun/storybook-solid" {
+ import type { SolidRenderer } from "types";
+ import type {
+ AnnotatedStoryFn,
+ Args,
+ ArgsFromMeta,
+ ArgsStoryFn,
+ ComponentAnnotations,
+ DecoratorFunction,
+ LoaderFunction,
+ ProjectAnnotations,
+ StoryAnnotations,
+ StoryContext as GenericStoryContext,
+ StrictArgs,
+ } from "@storybook/types";
+ import type { Component as ComponentType, ComponentProps } from "solid-js";
+ import type { SetOptional, Simplify } from "type-fest";
+ export type {
+ ArgTypes,
+ Args,
+ Parameters,
+ StrictArgs,
+ } from "@storybook/types";
+ export type { SolidRenderer };
+ /**
+ * Metadata to configure the stories for a component.
+ *
+ * @see [Default export](https://storybook.js.org/docs/formats/component-story-format/#default-export)
+ */
+ export type Meta =
+ TCmpOrArgs extends ComponentType
+ ? ComponentAnnotations>
+ : ComponentAnnotations;
+ /**
+ * Story function that represents a CSFv2 component example.
+ *
+ * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports)
+ */
+ export type StoryFn =
+ TCmpOrArgs extends ComponentType
+ ? AnnotatedStoryFn>
+ : AnnotatedStoryFn;
+ /**
+ * Story function that represents a CSFv3 component example.
+ *
+ * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports)
+ */
+ export type StoryObj = TMetaOrCmpOrArgs extends {
+ render?: ArgsStoryFn;
+ component?: infer Component;
+ args?: infer DefaultArgs;
+ }
+ ? Simplify<
+ (Component extends ComponentType
+ ? ComponentProps
+ : unknown) &
+ ArgsFromMeta
+ > extends infer TArgs
+ ? StoryAnnotations<
+ SolidRenderer,
+ TArgs,
+ SetOptional<
+ TArgs,
+ keyof TArgs & keyof (DefaultArgs & ActionArgs)
+ >
+ >
+ : never
+ : TMetaOrCmpOrArgs extends ComponentType
+ ? StoryAnnotations>
+ : StoryAnnotations;
+ type ActionArgs = {
+ [P in keyof TArgs as TArgs[P] extends (...args: any[]) => any
+ ? ((...args: any[]) => void) extends TArgs[P]
+ ? P
+ : never
+ : never]: TArgs[P];
+ };
+ export type Decorator = DecoratorFunction<
+ SolidRenderer,
+ TArgs
+ >;
+ export type Loader = LoaderFunction;
+ export type StoryContext = GenericStoryContext<
+ SolidRenderer,
+ TArgs
+ >;
+ export type Preview = ProjectAnnotations;
+}
+//# sourceMappingURL=index.d.ts.map