clan-app: Move changes for 2D view to separate ui-2d folder
This commit is contained in:
39
pkgs/clan-app/process-compose-2d.yaml
Normal file
39
pkgs/clan-app/process-compose-2d.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
version: "0.5"
|
||||
|
||||
processes:
|
||||
# App Dev
|
||||
|
||||
clan-app-ui:
|
||||
namespace: "app"
|
||||
command: |
|
||||
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
|
||||
npm install
|
||||
vite
|
||||
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
|
||||
depends_on:
|
||||
clan-app-ui:
|
||||
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-2d
|
||||
npm run storybook-dev -- --ci
|
||||
ready_log_line: "started"
|
||||
|
||||
luakit:
|
||||
namespace: "storybook"
|
||||
command: "luakit http://localhost:6006"
|
||||
depends_on:
|
||||
storybook:
|
||||
condition: "process_log_ready"
|
||||
1
pkgs/clan-app/ui-2d/.fonts
Symbolic link
1
pkgs/clan-app/ui-2d/.fonts
Symbolic link
@@ -0,0 +1 @@
|
||||
../ui/.fonts
|
||||
5
pkgs/clan-app/ui-2d/.gitignore
vendored
Normal file
5
pkgs/clan-app/ui-2d/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
app/api
|
||||
app/.fonts
|
||||
|
||||
.vite
|
||||
storybook-static
|
||||
34
pkgs/clan-app/ui-2d/.storybook/main.ts
Normal file
34
pkgs/clan-app/ui-2d/.storybook/main.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createRequire } from "module";
|
||||
import { dirname, join } from "path";
|
||||
import { mergeConfig } from "vite";
|
||||
import type { StorybookConfig } from "@kachurun/storybook-solid-vite";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const getAbsolutePath = (pkg: string) =>
|
||||
dirname(require.resolve(join(pkg, "package.json")));
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/components/**/*.mdx", "../src/components/**/*.stories.tsx"],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
getAbsolutePath("@storybook/addon-essentials"),
|
||||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
],
|
||||
framework: {
|
||||
name: "@kachurun/storybook-solid-vite",
|
||||
options: {},
|
||||
},
|
||||
async viteFinal(config) {
|
||||
return mergeConfig(config, {
|
||||
define: { "process.env": {} },
|
||||
});
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
5
pkgs/clan-app/ui-2d/.storybook/preview.css
Normal file
5
pkgs/clan-app/ui-2d/.storybook/preview.css
Normal file
@@ -0,0 +1,5 @@
|
||||
html {
|
||||
/* revert this which only makes sense when rendering inside the clan app webview */
|
||||
overflow-x: revert;
|
||||
overflow-y: revert;
|
||||
}
|
||||
29
pkgs/clan-app/ui-2d/.storybook/preview.ts
Normal file
29
pkgs/clan-app/ui-2d/.storybook/preview.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Preview } from "@kachurun/storybook-solid";
|
||||
|
||||
import "@/src/components/v2/index.css";
|
||||
import "../src/index.css";
|
||||
import "./preview.css";
|
||||
|
||||
export const preview: Preview = {
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: { toc: true },
|
||||
backgrounds: {
|
||||
values: [
|
||||
{ name: "Dark", value: "#333" },
|
||||
{ name: "Light", value: "#ffffff" },
|
||||
],
|
||||
default: "Light",
|
||||
},
|
||||
// automatically create action args for all props that start with "on"
|
||||
actions: { argTypesRegex: "^on.*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
12
pkgs/clan-app/ui-2d/.storybook/test-runner.ts
Normal file
12
pkgs/clan-app/ui-2d/.storybook/test-runner.ts
Normal file
@@ -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;
|
||||
7
pkgs/clan-app/ui-2d/.vscode/settings.json
vendored
Normal file
7
pkgs/clan-app/ui-2d/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]
|
||||
],
|
||||
"editor.wordWrap": "on"
|
||||
}
|
||||
43
pkgs/clan-app/ui-2d/README.md
Normal file
43
pkgs/clan-app/ui-2d/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## Usage
|
||||
|
||||
Those templates dependencies are maintained via [pnpm](https://pnpm.io) via
|
||||
`pnpm up -Lri`.
|
||||
|
||||
This is the reason you see a `pnpm-lock.yaml`. That being said, any package
|
||||
manager will work. This file can be safely be removed once you clone a template.
|
||||
|
||||
```bash
|
||||
$ npm install # or pnpm install or yarn install
|
||||
```
|
||||
|
||||
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm run dev` or `npm start`
|
||||
|
||||
Runs the app in the development mode.<br> Open
|
||||
[http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `dist` folder.<br> It correctly bundles
|
||||
Solid in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br> Your app is
|
||||
ready to be deployed!
|
||||
|
||||
### `npm run storybook`
|
||||
|
||||
Starts an instance of [storybook](https://storybook.js.org/).
|
||||
|
||||
For more info on how to write stories, please [see here](https://storybook.js.org/docs).
|
||||
|
||||
## Deployment
|
||||
|
||||
You can deploy the `dist` folder to any static host provider (netlify, surge,
|
||||
now, etc.)
|
||||
1
pkgs/clan-app/ui-2d/api
Symbolic link
1
pkgs/clan-app/ui-2d/api
Symbolic link
@@ -0,0 +1 @@
|
||||
../ui/api
|
||||
37
pkgs/clan-app/ui-2d/eslint.config.mjs
Normal file
37
pkgs/clan-app/ui-2d/eslint.config.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
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,
|
||||
...pluginQuery.configs["flat/recommended"],
|
||||
...tseslint.configs.strict,
|
||||
...tseslint.configs.stylistic,
|
||||
...tailwind.configs["flat/recommended"],
|
||||
globalIgnores(["src/types/index.d.ts"]),
|
||||
{
|
||||
rules: {
|
||||
"tailwindcss/no-contradicting-classname": [
|
||||
"error",
|
||||
{
|
||||
callees: ["cx"],
|
||||
},
|
||||
],
|
||||
"tailwindcss/no-custom-classname": [
|
||||
"error",
|
||||
{
|
||||
callees: ["cx"],
|
||||
whitelist: ["material-icons"],
|
||||
},
|
||||
],
|
||||
// TODO: make this more strict by removing later
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export default config;
|
||||
97
pkgs/clan-app/ui-2d/gtk.webview.js
Normal file
97
pkgs/clan-app/ui-2d/gtk.webview.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* This script generates a custom index.html file for the webview UI.
|
||||
* It reads the manifest.json file generated by Vite and uses it to generate the HTML file.
|
||||
* It also processes the CSS files to rewrite the URLs in the CSS files to match the new location of the assets.
|
||||
* The script is run after the Vite build is complete.
|
||||
*
|
||||
* This is necessary because the webview UI is loaded from the local file system and the URLs in the CSS files need to be rewritten to match the new location of the assets.
|
||||
* The generated index.html file is then used as the entry point for the webview UI.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import postcss from "postcss";
|
||||
import path from "node:path";
|
||||
import css_url from "postcss-url";
|
||||
|
||||
const distPath = path.resolve("dist");
|
||||
const manifestPath = path.join(distPath, ".vite/manifest.json");
|
||||
const outputPath = path.join(distPath, "index.html");
|
||||
|
||||
fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
|
||||
if (err) {
|
||||
return console.error("Failed to read manifest:", err);
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(data);
|
||||
/** @type {{ file: string; name: string; src: string; isEntry: bool; css: string[]; } []} */
|
||||
const assets = Object.values(manifest);
|
||||
|
||||
console.log(`Generate custom index.html from ${manifestPath} ...`);
|
||||
// Start with a basic HTML structure
|
||||
let htmlContent = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Webview UI</title>`;
|
||||
|
||||
// Add linked stylesheets
|
||||
assets.forEach((asset) => {
|
||||
// console.log(asset);
|
||||
if (asset.src === "index.html") {
|
||||
asset.css.forEach((cssEntry) => {
|
||||
// css to be processed
|
||||
|
||||
const css = fs.readFileSync(`dist/${cssEntry}`, "utf8");
|
||||
|
||||
// process css
|
||||
postcss()
|
||||
.use(
|
||||
css_url({
|
||||
url: (asset, dir) => {
|
||||
const res = path.basename(asset.url);
|
||||
console.log(`Rewriting CSS url(): ${asset.url} to ${res}`);
|
||||
return res;
|
||||
},
|
||||
}),
|
||||
)
|
||||
.process(css, {
|
||||
from: `dist/${cssEntry}`,
|
||||
to: `dist/${cssEntry}`,
|
||||
})
|
||||
.then((result) => {
|
||||
fs.writeFileSync(`dist/${cssEntry}`, result.css, "utf8");
|
||||
});
|
||||
|
||||
// Extend the HTML content with the linked stylesheet
|
||||
console.log(`Relinking html css stylesheet: ${cssEntry}`);
|
||||
htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
htmlContent += `
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
`;
|
||||
// Add scripts
|
||||
assets.forEach((asset) => {
|
||||
if (asset.file.endsWith(".js")) {
|
||||
console.log(`Relinking js script: ${asset.file}`);
|
||||
htmlContent += `\n <script src="${asset.file}"></script>`;
|
||||
}
|
||||
});
|
||||
|
||||
htmlContent += `
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Write the HTML file
|
||||
fs.writeFile(outputPath, htmlContent, (err) => {
|
||||
if (err) {
|
||||
console.error("Failed to write custom index.html:", err);
|
||||
} else {
|
||||
console.log("Custom index.html generated successfully!");
|
||||
}
|
||||
});
|
||||
});
|
||||
1
pkgs/clan-app/ui-2d/icons
Symbolic link
1
pkgs/clan-app/ui-2d/icons
Symbolic link
@@ -0,0 +1 @@
|
||||
../ui/icons
|
||||
14
pkgs/clan-app/ui-2d/index.html
Normal file
14
pkgs/clan-app/ui-2d/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Solid App</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
14253
pkgs/clan-app/ui-2d/package-lock.json
generated
Normal file
14253
pkgs/clan-app/ui-2d/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
pkgs/clan-app/ui-2d/package.json
Normal file
78
pkgs/clan-app/ui-2d/package.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "@clan/ui",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "npm run check && npm run test && vite build && npm run convert-html",
|
||||
"convert-html": "node gtk.webview.js",
|
||||
"serve": "vite preview",
|
||||
"check": "tsc --noEmit --skipLibCheck && eslint ./src",
|
||||
"test": "vitest run --typecheck",
|
||||
"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",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@kachurun/storybook-solid": "^8.6.7",
|
||||
"@kachurun/storybook-solid-vite": "^8.6.7",
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/addon-interactions": "^8.6.14",
|
||||
"@storybook/addon-links": "^8.6.14",
|
||||
"@storybook/addon-viewport": "^8.6.14",
|
||||
"@storybook/builder-vite": "^8.6.14",
|
||||
"@storybook/test-runner": "^0.22.0",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^22.15.19",
|
||||
"@types/three": "^0.176.0",
|
||||
"@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",
|
||||
"prettier": "^3.2.5",
|
||||
"solid-devtools": "^0.34.0",
|
||||
"storybook": "^8.6.14",
|
||||
"storybook-addon-pseudo-states": "^4.0.3",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-solid": "^2.8.2",
|
||||
"vite-plugin-solid-svg": "^0.8.1",
|
||||
"vitest": "^3.1.4",
|
||||
"wait-on": "^8.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.8",
|
||||
"@kobalte/core": "^0.13.10",
|
||||
"@kobalte/tailwindcss": "^0.9.0",
|
||||
"@modular-forms/solid": "^0.25.1",
|
||||
"@solid-primitives/storage": "^4.3.2",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@storybook/test": "^8.6.14",
|
||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||
"@tanstack/solid-query": "^5.76.0",
|
||||
"corvu": "^0.7.1",
|
||||
"material-icons": "^1.13.12",
|
||||
"nanoid": "^5.0.7",
|
||||
"solid-js": "^1.9.7",
|
||||
"solid-markdown": "^2.0.13",
|
||||
"solid-toast": "^0.5.0",
|
||||
"three": "^0.176.0"
|
||||
}
|
||||
}
|
||||
8
pkgs/clan-app/ui-2d/postcss.config.js
Normal file
8
pkgs/clan-app/ui-2d/postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
9
pkgs/clan-app/ui-2d/prettier.config.js
Normal file
9
pkgs/clan-app/ui-2d/prettier.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @see https://prettier.io/docs/en/configuration.html
|
||||
* @type {import("prettier").Config}
|
||||
*/
|
||||
const config = {
|
||||
trailingComma: "all",
|
||||
};
|
||||
|
||||
export default config;
|
||||
127
pkgs/clan-app/ui-2d/src/Form/base/index.tsx
Normal file
127
pkgs/clan-app/ui-2d/src/Form/base/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
|
||||
import type {
|
||||
ComputePositionConfig,
|
||||
ComputePositionReturn,
|
||||
ReferenceElement,
|
||||
} from "@floating-ui/dom";
|
||||
import { computePosition } from "@floating-ui/dom";
|
||||
|
||||
export interface UseFloatingOptions<
|
||||
R extends ReferenceElement,
|
||||
F extends HTMLElement,
|
||||
> extends Partial<ComputePositionConfig> {
|
||||
whileElementsMounted?: (
|
||||
reference: R,
|
||||
floating: F,
|
||||
update: () => void,
|
||||
) => // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
void | (() => void);
|
||||
}
|
||||
|
||||
interface UseFloatingState extends Omit<ComputePositionReturn, "x" | "y"> {
|
||||
x?: number | null;
|
||||
y?: number | null;
|
||||
}
|
||||
|
||||
export interface UseFloatingResult extends UseFloatingState {
|
||||
update(): void;
|
||||
}
|
||||
|
||||
export function useFloating<R extends ReferenceElement, F extends HTMLElement>(
|
||||
reference: () => R | undefined | null,
|
||||
floating: () => F | undefined | null,
|
||||
options?: UseFloatingOptions<R, F>,
|
||||
): UseFloatingResult {
|
||||
const placement = () => options?.placement ?? "bottom";
|
||||
const strategy = () => options?.strategy ?? "absolute";
|
||||
|
||||
const [data, setData] = createSignal<UseFloatingState>({
|
||||
x: null,
|
||||
y: null,
|
||||
placement: placement(),
|
||||
strategy: strategy(),
|
||||
middlewareData: {},
|
||||
});
|
||||
|
||||
const [error, setError] = createSignal<{ value: unknown } | undefined>();
|
||||
|
||||
createEffect(() => {
|
||||
const currentError = error();
|
||||
if (currentError) {
|
||||
throw currentError.value;
|
||||
}
|
||||
});
|
||||
|
||||
const version = createMemo(() => {
|
||||
reference();
|
||||
floating();
|
||||
return {};
|
||||
});
|
||||
|
||||
function update() {
|
||||
const currentReference = reference();
|
||||
const currentFloating = floating();
|
||||
|
||||
if (currentReference && currentFloating) {
|
||||
const capturedVersion = version();
|
||||
computePosition(currentReference, currentFloating, {
|
||||
middleware: options?.middleware,
|
||||
placement: placement(),
|
||||
strategy: strategy(),
|
||||
}).then(
|
||||
(currentData) => {
|
||||
// Check if it's still valid
|
||||
if (capturedVersion === version()) {
|
||||
setData(currentData);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setError(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const currentReference = reference();
|
||||
const currentFloating = floating();
|
||||
|
||||
placement();
|
||||
strategy();
|
||||
|
||||
if (currentReference && currentFloating) {
|
||||
if (options?.whileElementsMounted) {
|
||||
const cleanup = options.whileElementsMounted(
|
||||
currentReference,
|
||||
currentFloating,
|
||||
update,
|
||||
);
|
||||
|
||||
if (cleanup) {
|
||||
onCleanup(cleanup);
|
||||
}
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
get x() {
|
||||
return data().x;
|
||||
},
|
||||
get y() {
|
||||
return data().y;
|
||||
},
|
||||
get placement() {
|
||||
return data().placement;
|
||||
},
|
||||
get strategy() {
|
||||
return data().strategy;
|
||||
},
|
||||
get middlewareData() {
|
||||
return data().middlewareData;
|
||||
},
|
||||
update,
|
||||
};
|
||||
}
|
||||
15
pkgs/clan-app/ui-2d/src/Form/base/label.tsx
Normal file
15
pkgs/clan-app/ui-2d/src/Form/base/label.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { JSX } from "solid-js";
|
||||
interface LabelProps {
|
||||
label: JSX.Element;
|
||||
required?: boolean;
|
||||
}
|
||||
export const Label = (props: LabelProps) => (
|
||||
<span
|
||||
class=" block"
|
||||
classList={{
|
||||
"after:ml-0.5 after:text-primary after:content-['*']": props.required,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
);
|
||||
8
pkgs/clan-app/ui-2d/src/Form/fields/FormSection.tsx
Normal file
8
pkgs/clan-app/ui-2d/src/Form/fields/FormSection.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
interface FormSectionProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
export const FormSection = (props: FormSectionProps) => {
|
||||
return <div class="p-2">{props.children}</div>;
|
||||
};
|
||||
274
pkgs/clan-app/ui-2d/src/Form/fields/Select.tsx
Normal file
274
pkgs/clan-app/ui-2d/src/Form/fields/Select.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import {
|
||||
createUniqueId,
|
||||
createSignal,
|
||||
Show,
|
||||
type JSX,
|
||||
For,
|
||||
createMemo,
|
||||
Accessor,
|
||||
} from "solid-js";
|
||||
import { Portal } from "solid-js/web";
|
||||
import { useFloating } from "../base";
|
||||
import { autoUpdate, flip, hide, offset, shift, size } from "@floating-ui/dom";
|
||||
import { Button } from "../../components/Button/Button";
|
||||
import {
|
||||
InputBase,
|
||||
InputError,
|
||||
InputLabel,
|
||||
InputLabelProps,
|
||||
} from "@/src/components/inputBase";
|
||||
import { FieldLayout } from "./layout";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { useContext } from "corvu/dialog";
|
||||
|
||||
export interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface SelectInputpProps {
|
||||
value: string[] | string;
|
||||
selectProps?: JSX.InputHTMLAttributes<HTMLSelectElement>;
|
||||
options: Option[];
|
||||
label: JSX.Element;
|
||||
labelProps?: InputLabelProps;
|
||||
helperText?: JSX.Element;
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
type?: string;
|
||||
inlineLabel?: JSX.Element;
|
||||
class?: string;
|
||||
adornment?: {
|
||||
position: "start" | "end";
|
||||
content: JSX.Element;
|
||||
};
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
multiple?: boolean;
|
||||
loading?: boolean;
|
||||
portalRef?: Accessor<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export function SelectInput(props: SelectInputpProps) {
|
||||
const dialogContext = (dialogContextId?: string) =>
|
||||
useContext(dialogContextId);
|
||||
|
||||
const _id = createUniqueId();
|
||||
|
||||
const [reference, setReference] = createSignal<HTMLElement>();
|
||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||
|
||||
// `position` is a reactive object.
|
||||
const position = useFloating(reference, floating, {
|
||||
placement: "bottom-start",
|
||||
|
||||
// pass options. Ensure the cleanup function is returned.
|
||||
whileElementsMounted: (reference, floating, update) =>
|
||||
autoUpdate(reference, floating, update, {
|
||||
animationFrame: true,
|
||||
}),
|
||||
middleware: [
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
});
|
||||
},
|
||||
}),
|
||||
offset({ mainAxis: 2 }),
|
||||
shift(),
|
||||
flip(),
|
||||
hide({
|
||||
strategy: "referenceHidden",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Create values list
|
||||
const getValues = createMemo(() => {
|
||||
return Array.isArray(props.value)
|
||||
? (props.value as string[])
|
||||
: typeof props.value === "string"
|
||||
? [props.value]
|
||||
: [];
|
||||
});
|
||||
|
||||
// const getSingleValue = createMemo(() => {
|
||||
// const values = getValues();
|
||||
// return values.length > 0 ? values[0] : "";
|
||||
// });
|
||||
|
||||
const handleClickOption = (opt: Option) => {
|
||||
if (!props.multiple) {
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
props.selectProps.onInput({
|
||||
currentTarget: {
|
||||
value: opt.value,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
let currValues = getValues();
|
||||
|
||||
if (currValues.includes(opt.value)) {
|
||||
currValues = currValues.filter((o) => o !== opt.value);
|
||||
} else {
|
||||
currValues.push(opt.value);
|
||||
}
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
props.selectProps.onInput({
|
||||
currentTarget: {
|
||||
options: currValues.map((value) => ({
|
||||
value,
|
||||
selected: true,
|
||||
disabled: false,
|
||||
})),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldLayout
|
||||
error={props.error && <InputError error={props.error} />}
|
||||
label={
|
||||
<InputLabel
|
||||
description={""}
|
||||
required={props.required}
|
||||
{...props.labelProps}
|
||||
>
|
||||
{props.label}
|
||||
</InputLabel>
|
||||
}
|
||||
field={
|
||||
<InputBase
|
||||
error={!!props.error}
|
||||
disabled={props.disabled}
|
||||
required={props.required}
|
||||
class="!justify-start"
|
||||
divRef={setReference}
|
||||
inputElem={
|
||||
<button
|
||||
// TODO: Keyboard acessibililty
|
||||
// Currently the popover only opens with onClick
|
||||
// Options are not selectable with keyboard
|
||||
tabIndex={-1}
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
const popover = document.getElementById(_id);
|
||||
if (popover) {
|
||||
popover.togglePopover(); // Show or hide the popover
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2"
|
||||
formnovalidate
|
||||
// TODO: Use native popover once Webkit supports it within <form>
|
||||
// popovertarget={_id}
|
||||
// popovertargetaction="toggle"
|
||||
>
|
||||
<Show
|
||||
when={props.adornment && props.adornment.position === "start"}
|
||||
>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
{props.inlineLabel}
|
||||
<div class="flex cursor-default flex-row gap-2">
|
||||
<Show
|
||||
when={
|
||||
getValues() &&
|
||||
getValues.length !== 1 &&
|
||||
getValues()[0] !== ""
|
||||
}
|
||||
fallback={props.placeholder}
|
||||
>
|
||||
<For each={getValues()} fallback={"Select"}>
|
||||
{(item) => (
|
||||
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
|
||||
{item}
|
||||
<Show when={props.multiple}>
|
||||
<button
|
||||
class=""
|
||||
type="button"
|
||||
onClick={(_e) => {
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
props.selectProps.onInput({
|
||||
currentTarget: {
|
||||
options: getValues()
|
||||
.filter((o) => o !== item)
|
||||
.map((value) => ({
|
||||
value,
|
||||
selected: true,
|
||||
disabled: false,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<Show
|
||||
when={props.adornment && props.adornment.position === "end"}
|
||||
>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
<Icon size={12} icon="CaretDown" class="ml-auto mr-2" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Portal
|
||||
mount={
|
||||
props.portalRef ? props.portalRef() || document.body : document.body
|
||||
}
|
||||
>
|
||||
<div
|
||||
id={_id}
|
||||
popover
|
||||
ref={setFloating}
|
||||
style={{
|
||||
margin: 0,
|
||||
position: position.strategy,
|
||||
top: `${position.y ?? 0}px`,
|
||||
left: `${position.x ?? 0}px`,
|
||||
}}
|
||||
class="rounded-md border border-gray-200 bg-white shadow-lg"
|
||||
>
|
||||
<ul class="flex max-h-96 flex-col gap-1 overflow-x-hidden overflow-y-scroll p-1">
|
||||
<Show when={!props.loading} fallback={"Loading ...."}>
|
||||
<For each={props.options}>
|
||||
{(opt) => (
|
||||
<>
|
||||
<li>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!justify-start"
|
||||
onClick={() => handleClickOption(opt)}
|
||||
disabled={opt.disabled}
|
||||
classList={{
|
||||
active:
|
||||
!opt.disabled && getValues().includes(opt.value),
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</Button>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</ul>
|
||||
</div>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
58
pkgs/clan-app/ui-2d/src/Form/fields/TextInput.tsx
Normal file
58
pkgs/clan-app/ui-2d/src/Form/fields/TextInput.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import {
|
||||
InputBase,
|
||||
InputError,
|
||||
InputLabel,
|
||||
InputVariant,
|
||||
} from "@/src/components/inputBase";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import { FieldLayout } from "./layout";
|
||||
|
||||
interface TextInputProps {
|
||||
// Common
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
// Passed to input
|
||||
value: string;
|
||||
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||
placeholder?: string;
|
||||
variant?: InputVariant;
|
||||
// Passed to label
|
||||
label: JSX.Element;
|
||||
help?: string;
|
||||
// Passed to layout
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function TextInput(props: TextInputProps) {
|
||||
const [layoutProps, rest] = splitProps(props, ["class"]);
|
||||
return (
|
||||
<FieldLayout
|
||||
label={
|
||||
<InputLabel
|
||||
class="col-span-2"
|
||||
required={props.required}
|
||||
error={!!props.error}
|
||||
help={props.help}
|
||||
>
|
||||
{props.label}
|
||||
</InputLabel>
|
||||
}
|
||||
field={
|
||||
<InputBase
|
||||
variant={props.variant}
|
||||
error={!!props.error}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
class="col-span-10"
|
||||
{...props.inputProps}
|
||||
value={props.value}
|
||||
/>
|
||||
}
|
||||
error={props.error && <InputError error={props.error} />}
|
||||
{...layoutProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
2
pkgs/clan-app/ui-2d/src/Form/fields/index.ts
Normal file
2
pkgs/clan-app/ui-2d/src/Form/fields/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./FormSection";
|
||||
export * from "./TextInput";
|
||||
26
pkgs/clan-app/ui-2d/src/Form/fields/layout.tsx
Normal file
26
pkgs/clan-app/ui-2d/src/Form/fields/layout.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { JSX, splitProps } from "solid-js";
|
||||
import cx from "classnames";
|
||||
|
||||
interface LayoutProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
field?: JSX.Element;
|
||||
label?: JSX.Element;
|
||||
error?: JSX.Element;
|
||||
}
|
||||
export const FieldLayout = (props: LayoutProps) => {
|
||||
const [intern, divProps] = splitProps(props, [
|
||||
"field",
|
||||
"label",
|
||||
"error",
|
||||
"class",
|
||||
]);
|
||||
return (
|
||||
<div
|
||||
class={cx("grid grid-cols-10 items-center", intern.class)}
|
||||
{...divProps}
|
||||
>
|
||||
<div class="col-span-5 flex items-center">{props.label}</div>
|
||||
<div class="col-span-5">{props.field}</div>
|
||||
{props.error && <span class="col-span-full">{props.error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
32
pkgs/clan-app/ui-2d/src/Form/fieldset/index.tsx
Normal file
32
pkgs/clan-app/ui-2d/src/Form/fieldset/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
|
||||
interface FieldsetProps {
|
||||
legend?: string;
|
||||
children: JSX.Element;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function Fieldset(props: FieldsetProps) {
|
||||
return (
|
||||
<fieldset class="flex flex-col gap-y-2.5">
|
||||
{props.legend && (
|
||||
<div class="px-2">
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
tag="p"
|
||||
size="s"
|
||||
color="primary"
|
||||
weight="medium"
|
||||
>
|
||||
{props.legend}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<div class="flex flex-col gap-y-3 rounded-md border border-secondary-200 bg-secondary-50 p-5">
|
||||
{props.children}
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
928
pkgs/clan-app/ui-2d/src/Form/form/index.tsx
Normal file
928
pkgs/clan-app/ui-2d/src/Form/form/index.tsx
Normal file
@@ -0,0 +1,928 @@
|
||||
import {
|
||||
createForm,
|
||||
Field,
|
||||
FieldArray,
|
||||
FieldValues,
|
||||
FormStore,
|
||||
pattern,
|
||||
ResponseData,
|
||||
setValue,
|
||||
getValues,
|
||||
insert,
|
||||
SubmitHandler,
|
||||
reset,
|
||||
remove,
|
||||
move,
|
||||
} from "@modular-forms/solid";
|
||||
import { JSONSchema7, JSONSchema7Type } from "json-schema";
|
||||
import { TextInput } from "../fields/TextInput";
|
||||
import { createEffect, For, JSX, Match, Show, Switch } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Label } from "../base/label";
|
||||
import { SelectInput } from "../fields/Select";
|
||||
import { Button } from "../../components/Button/Button";
|
||||
import Icon from "@/src/components/icon";
|
||||
|
||||
function generateDefaults(schema: JSONSchema7): unknown {
|
||||
switch (schema.type) {
|
||||
case "string":
|
||||
return ""; // Default value for string
|
||||
|
||||
case "number":
|
||||
case "integer":
|
||||
return 0; // Default value for number/integer
|
||||
|
||||
case "boolean":
|
||||
return false; // Default value for boolean
|
||||
|
||||
case "array":
|
||||
return []; // Default empty array if no items schema or items is true/false
|
||||
|
||||
case "object": {
|
||||
const obj: Record<string, unknown> = {};
|
||||
if (schema.properties) {
|
||||
Object.entries(schema.properties).forEach(([key, propSchema]) => {
|
||||
if (typeof propSchema === "boolean") {
|
||||
obj[key] = false;
|
||||
} else {
|
||||
// if (schema.required schema.required.includes(key))
|
||||
obj[key] = generateDefaults(propSchema);
|
||||
}
|
||||
});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
default:
|
||||
return null; // Default for unknown types or nulls
|
||||
}
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
schema: JSONSchema7;
|
||||
initialValues?: NonNullable<unknown>;
|
||||
handleSubmit?: SubmitHandler<NonNullable<unknown>>;
|
||||
initialPath?: string[];
|
||||
components?: {
|
||||
before?: JSX.Element;
|
||||
after?: JSX.Element;
|
||||
};
|
||||
readonly?: boolean;
|
||||
formProps?: JSX.InputHTMLAttributes<HTMLFormElement>;
|
||||
errorContext?: string;
|
||||
resetOnSubmit?: boolean;
|
||||
}
|
||||
export const DynForm = (props: FormProps) => {
|
||||
const [formStore, { Field, Form: ModuleForm }] = createForm({
|
||||
initialValues: props.initialValues,
|
||||
});
|
||||
|
||||
const handleSubmit: SubmitHandler<NonNullable<unknown>> = async (
|
||||
values,
|
||||
event,
|
||||
) => {
|
||||
console.log("Submitting form values", values, props.errorContext);
|
||||
props.handleSubmit?.(values, event);
|
||||
// setValue(formStore, "root", null);
|
||||
if (props.resetOnSubmit) {
|
||||
console.log("Resetting form", values, props.initialValues);
|
||||
reset(formStore);
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
console.log("FormStore", formStore);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-expect-error: This happened after solidjs upgrade. TOOD: fixme */}
|
||||
<ModuleForm {...props.formProps} onSubmit={handleSubmit}>
|
||||
{props.components?.before}
|
||||
<SchemaFields
|
||||
schema={props.schema}
|
||||
Field={Field}
|
||||
formStore={formStore}
|
||||
path={props.initialPath || []}
|
||||
readonly={!!props.readonly}
|
||||
parent={props.schema}
|
||||
/>
|
||||
{props.components?.after}
|
||||
</ModuleForm>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface UnsupportedProps {
|
||||
schema: JSONSchema7;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Unsupported = (props: UnsupportedProps) => (
|
||||
<div>
|
||||
{props.error && <div class="font-bold text-error-700">{props.error}</div>}
|
||||
<span>
|
||||
Invalid or unsupported schema entry of type:{" "}
|
||||
<b>{JSON.stringify(props.schema.type)}</b>
|
||||
</span>
|
||||
<pre>
|
||||
<code>{JSON.stringify(props.schema, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SchemaFieldsProps<T extends FieldValues, R extends ResponseData> {
|
||||
formStore: FormStore<T, R>;
|
||||
Field: typeof Field<T, R, never>;
|
||||
schema: JSONSchema7;
|
||||
path: string[];
|
||||
readonly: boolean;
|
||||
parent: JSONSchema7;
|
||||
}
|
||||
export function SchemaFields<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
return (
|
||||
<Switch fallback={<Unsupported schema={props.schema} />}>
|
||||
{/* Simple types */}
|
||||
<Match when={props.schema.type === "boolean"}>bool</Match>
|
||||
|
||||
<Match when={props.schema.type === "integer"}>
|
||||
<StringField {...props} schema={props.schema} />
|
||||
</Match>
|
||||
<Match when={props.schema.type === "number"}>
|
||||
<StringField {...props} schema={props.schema} />
|
||||
</Match>
|
||||
<Match when={props.schema.type === "string"}>
|
||||
<StringField {...props} schema={props.schema} />
|
||||
</Match>
|
||||
{/* Composed types */}
|
||||
<Match when={props.schema.type === "array"}>
|
||||
<ArrayFields {...props} schema={props.schema} />
|
||||
</Match>
|
||||
<Match when={props.schema.type === "object"}>
|
||||
<ObjectFields {...props} schema={props.schema} />
|
||||
</Match>
|
||||
{/* Empty / Null */}
|
||||
<Match when={props.schema.type === "null"}>
|
||||
Dont know how to rendner InputType null
|
||||
<Unsupported schema={props.schema} />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
if (
|
||||
props.schema.type !== "string" &&
|
||||
props.schema.type !== "number" &&
|
||||
props.schema.type !== "integer"
|
||||
) {
|
||||
return (
|
||||
<span class="text-error-700">
|
||||
Error cannot render the following as String input.
|
||||
<Unsupported schema={props.schema} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const { Field } = props;
|
||||
|
||||
const validate = props.schema.pattern
|
||||
? pattern(
|
||||
new RegExp(props.schema.pattern),
|
||||
`String should follow pattern ${props.schema.pattern}`,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const commonProps = {
|
||||
label: props.schema.title || props.path.join("."),
|
||||
required:
|
||||
props.parent.required &&
|
||||
props.parent.required.some(
|
||||
(r) => r === props.path[props.path.length - 1],
|
||||
),
|
||||
};
|
||||
const readonly = !!props.readonly;
|
||||
return (
|
||||
<Switch fallback={<Unsupported schema={props.schema} />}>
|
||||
<Match
|
||||
when={props.schema.type === "number" || props.schema.type === "integer"}
|
||||
>
|
||||
{(s) => (
|
||||
<Field
|
||||
// @ts-expect-error: We dont know dynamic names while type checking
|
||||
name={props.path.join(".")}
|
||||
validate={validate}
|
||||
>
|
||||
{(field, fieldProps) => (
|
||||
<>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
...fieldProps,
|
||||
inputmode: "numeric",
|
||||
pattern: "[0-9.]*",
|
||||
readonly,
|
||||
}}
|
||||
{...commonProps}
|
||||
value={(field.value as unknown as string) || ""}
|
||||
error={field.error}
|
||||
// required
|
||||
// altLabel="Leave empty to accept the default"
|
||||
// helperText="Configure how dude connects"
|
||||
// error="Something is wrong now"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={props.schema.enum}>
|
||||
{(_enumSchemas) => (
|
||||
<Field
|
||||
// @ts-expect-error: We dont know dynamic names while type checking
|
||||
name={props.path.join(".")}
|
||||
>
|
||||
{(field, fieldProps) => (
|
||||
<OnlyStringItems itemspec={props.schema}>
|
||||
{(options) => (
|
||||
<SelectInput
|
||||
error={field.error}
|
||||
// altLabel={props.schema.title}
|
||||
label={props.path.join(".")}
|
||||
helperText={props.schema.description}
|
||||
value={field.value || []}
|
||||
options={options.map((o) => ({
|
||||
value: o,
|
||||
label: o,
|
||||
}))}
|
||||
selectProps={fieldProps}
|
||||
required={!!props.schema.minItems}
|
||||
/>
|
||||
)}
|
||||
</OnlyStringItems>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={props.schema.writeOnly && props.schema}>
|
||||
{(s) => (
|
||||
<Field
|
||||
// @ts-expect-error: We dont know dynamic names while type checking
|
||||
name={props.path.join(".")}
|
||||
validate={validate}
|
||||
>
|
||||
{(field, fieldProps) => (
|
||||
<TextInput
|
||||
inputProps={{ ...fieldProps, readonly }}
|
||||
value={field.value as unknown as string}
|
||||
// type="password"
|
||||
error={field.error}
|
||||
{...commonProps}
|
||||
// required
|
||||
// altLabel="Leave empty to accept the default"
|
||||
// helperText="Configure how dude connects"
|
||||
// error="Something is wrong now"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</Match>
|
||||
{/* TODO: when is it a normal string input? */}
|
||||
<Match when={props.schema}>
|
||||
{(s) => (
|
||||
<Field
|
||||
// @ts-expect-error: We dont know dynamic names while type checking
|
||||
name={props.path.join(".")}
|
||||
validate={validate}
|
||||
>
|
||||
{(field, fieldProps) => (
|
||||
<TextInput
|
||||
inputProps={{ ...fieldProps, readonly }}
|
||||
value={field.value as unknown as string}
|
||||
error={field.error}
|
||||
{...commonProps}
|
||||
// placeholder="foobar"
|
||||
// inlineLabel={
|
||||
// <div class="label">
|
||||
// <span class=""></span>
|
||||
// </div>
|
||||
// }
|
||||
// required
|
||||
// altLabel="Leave empty to accept the default"
|
||||
// helperText="Configure how dude connects"
|
||||
// error="Something is wrong now"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
interface OptionSchemaProps {
|
||||
itemSpec: JSONSchema7Type;
|
||||
}
|
||||
export function OptionSchema(props: OptionSchemaProps) {
|
||||
return (
|
||||
<Switch
|
||||
fallback={<option class="text-error-700">Item spec unhandled</option>}
|
||||
>
|
||||
<Match when={typeof props.itemSpec === "string" && props.itemSpec}>
|
||||
{(o) => <option>{o()}</option>}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
interface ValueDisplayProps<T extends FieldValues, R extends ResponseData>
|
||||
extends SchemaFieldsProps<T, R> {
|
||||
children: JSX.Element;
|
||||
listFieldName: string;
|
||||
idx: number;
|
||||
of: number;
|
||||
}
|
||||
export function ListValueDisplay<T extends FieldValues, R extends ResponseData>(
|
||||
props: ValueDisplayProps<T, R>,
|
||||
) {
|
||||
const removeItem = (e: Event) => {
|
||||
e.preventDefault();
|
||||
remove(
|
||||
props.formStore,
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
props.listFieldName,
|
||||
{ at: props.idx },
|
||||
);
|
||||
};
|
||||
const moveItemBy = (dir: number) => (e: Event) => {
|
||||
e.preventDefault();
|
||||
move(
|
||||
props.formStore,
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
props.listFieldName,
|
||||
{ from: props.idx, to: props.idx + dir },
|
||||
);
|
||||
};
|
||||
const topMost = () => props.idx === props.of - 1;
|
||||
const bottomMost = () => props.idx === 0;
|
||||
|
||||
return (
|
||||
<div class="w-full border-b border-secondary-200 px-2 pb-4">
|
||||
<div class="flex w-full items-center gap-2">
|
||||
{props.children}
|
||||
<div class="ml-4 min-w-fit">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="s"
|
||||
type="button"
|
||||
onClick={moveItemBy(1)}
|
||||
disabled={topMost()}
|
||||
startIcon={<Icon icon="ArrowBottom" />}
|
||||
class="h-12"
|
||||
></Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
onClick={moveItemBy(-1)}
|
||||
disabled={bottomMost()}
|
||||
class="h-12"
|
||||
startIcon={<Icon icon="ArrowTop" />}
|
||||
></Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
class="h-12"
|
||||
startIcon={<Icon icon="Trash" />}
|
||||
onClick={removeItem}
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const findDuplicates = (arr: unknown[]) => {
|
||||
const seen = new Set();
|
||||
const duplicates: number[] = [];
|
||||
|
||||
arr.forEach((obj, idx) => {
|
||||
const serializedObj = JSON.stringify(obj);
|
||||
|
||||
if (seen.has(serializedObj)) {
|
||||
duplicates.push(idx);
|
||||
} else {
|
||||
seen.add(serializedObj);
|
||||
}
|
||||
});
|
||||
|
||||
return duplicates;
|
||||
};
|
||||
|
||||
interface OnlyStringItems {
|
||||
children: (items: string[]) => JSX.Element;
|
||||
itemspec: JSONSchema7;
|
||||
}
|
||||
const OnlyStringItems = (props: OnlyStringItems) => {
|
||||
return (
|
||||
<Show
|
||||
when={
|
||||
Array.isArray(props.itemspec.enum) &&
|
||||
typeof props.itemspec.type === "string" &&
|
||||
props.itemspec
|
||||
}
|
||||
fallback={
|
||||
<Unsupported
|
||||
schema={props.itemspec}
|
||||
error="Unsupported array item type"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{props.children(props.itemspec.enum as string[])}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
if (props.schema.type !== "array") {
|
||||
return (
|
||||
<span class="text-error-700">
|
||||
Error cannot render the following as array.
|
||||
<Unsupported schema={props.schema} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const { Field } = props;
|
||||
|
||||
const listFieldName = props.path.join(".");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch fallback={<Unsupported schema={props.schema} />}>
|
||||
<Match
|
||||
when={
|
||||
!Array.isArray(props.schema.items) &&
|
||||
typeof props.schema.items === "object" &&
|
||||
props.schema.items
|
||||
}
|
||||
>
|
||||
{(itemsSchema) => (
|
||||
<>
|
||||
<Switch fallback={<Unsupported schema={props.schema} />}>
|
||||
<Match when={itemsSchema().type === "array"}>
|
||||
<Unsupported
|
||||
schema={props.schema}
|
||||
error="Array of Array is not supported yet."
|
||||
/>
|
||||
</Match>
|
||||
<Match
|
||||
when={itemsSchema().type === "string" && itemsSchema().enum}
|
||||
>
|
||||
<Field
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
name={listFieldName}
|
||||
// @ts-expect-error: type is known due to schema
|
||||
type="string[]"
|
||||
validateOn="touched"
|
||||
revalidateOn="touched"
|
||||
validate={() => {
|
||||
let error = "";
|
||||
const values: unknown[] = getValues(
|
||||
props.formStore,
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
listFieldName,
|
||||
// @ts-expect-error: assumption based on the behavior of selectInput
|
||||
)?.strings?.selection;
|
||||
console.log("vali", { values });
|
||||
if (props.schema.uniqueItems) {
|
||||
const duplicates = findDuplicates(values);
|
||||
if (duplicates.length) {
|
||||
error = `Duplicate entries are not allowed. Please make sure each entry is unique.`;
|
||||
}
|
||||
}
|
||||
if (
|
||||
props.schema.maxItems &&
|
||||
values.length > props.schema.maxItems
|
||||
) {
|
||||
error = `You can only select up to ${props.schema.maxItems} items`;
|
||||
}
|
||||
if (
|
||||
props.schema.minItems &&
|
||||
values.length < props.schema.minItems
|
||||
) {
|
||||
error = `Please select at least ${props.schema.minItems} items.`;
|
||||
}
|
||||
return error;
|
||||
}}
|
||||
>
|
||||
{(field, fieldProps) => (
|
||||
<OnlyStringItems itemspec={itemsSchema()}>
|
||||
{(options) => (
|
||||
<SelectInput
|
||||
multiple
|
||||
error={field.error}
|
||||
// altLabel={props.schema.title}
|
||||
label={listFieldName}
|
||||
helperText={props.schema.description}
|
||||
value={field.value || ""}
|
||||
options={options.map((o) => ({
|
||||
value: o,
|
||||
label: o,
|
||||
}))}
|
||||
selectProps={fieldProps}
|
||||
required={!!props.schema.minItems}
|
||||
/>
|
||||
)}
|
||||
</OnlyStringItems>
|
||||
)}
|
||||
</Field>
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
itemsSchema().type === "string" ||
|
||||
itemsSchema().type === "object"
|
||||
}
|
||||
>
|
||||
{/* !Important: Register the parent field to gain access to array items*/}
|
||||
<FieldArray
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
name={listFieldName}
|
||||
of={props.formStore}
|
||||
validateOn="touched"
|
||||
revalidateOn="touched"
|
||||
validate={() => {
|
||||
let error = "";
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
const values: unknown[] = getValues(
|
||||
props.formStore,
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
listFieldName,
|
||||
);
|
||||
if (props.schema.uniqueItems) {
|
||||
const duplicates = findDuplicates(values);
|
||||
if (duplicates.length) {
|
||||
error = `Duplicate entries are not allowed. Please make sure each entry is unique.`;
|
||||
}
|
||||
}
|
||||
if (
|
||||
props.schema.maxItems &&
|
||||
values.length > props.schema.maxItems
|
||||
) {
|
||||
error = `You can only add up to ${props.schema.maxItems} items`;
|
||||
}
|
||||
if (
|
||||
props.schema.minItems &&
|
||||
values.length < props.schema.minItems
|
||||
) {
|
||||
error = `Please add at least ${props.schema.minItems} items.`;
|
||||
}
|
||||
|
||||
return error;
|
||||
}}
|
||||
>
|
||||
{(fieldArray) => (
|
||||
<>
|
||||
{/* Render existing items */}
|
||||
<For
|
||||
each={fieldArray.items}
|
||||
fallback={
|
||||
// Empty list
|
||||
<span class="text-neutral-500">
|
||||
No {itemsSchema().title || "entries"} yet.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(item, idx) => (
|
||||
<ListValueDisplay
|
||||
{...props}
|
||||
listFieldName={listFieldName}
|
||||
idx={idx()}
|
||||
of={fieldArray.items.length}
|
||||
>
|
||||
<Field
|
||||
// @ts-expect-error: field names are not know ahead of time
|
||||
name={`${listFieldName}.${idx()}`}
|
||||
>
|
||||
{(f, fp) => (
|
||||
<>
|
||||
<DynForm
|
||||
formProps={{
|
||||
class: cx("w-full"),
|
||||
}}
|
||||
resetOnSubmit={true}
|
||||
schema={itemsSchema()}
|
||||
initialValues={
|
||||
itemsSchema().type === "object"
|
||||
? f.value
|
||||
: { "": f.value }
|
||||
}
|
||||
readonly={true}
|
||||
></DynForm>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</ListValueDisplay>
|
||||
)}
|
||||
</For>
|
||||
<Show when={fieldArray.error}>
|
||||
<span class="font-bold text-error-700">
|
||||
{fieldArray.error}
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
{/* Add new item */}
|
||||
<DynForm
|
||||
formProps={{
|
||||
class: cx("px-2 w-full"),
|
||||
}}
|
||||
schema={{
|
||||
...itemsSchema(),
|
||||
title: itemsSchema().title || "thing",
|
||||
}}
|
||||
initialPath={["root"]}
|
||||
// Reset the input field for list items
|
||||
resetOnSubmit={true}
|
||||
initialValues={{
|
||||
root: generateDefaults(itemsSchema()),
|
||||
}}
|
||||
// Button for adding new items
|
||||
components={{
|
||||
before: (
|
||||
<div class="flex w-full justify-end pb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="submit"
|
||||
endIcon={<Icon size={14} icon={"Plus"} />}
|
||||
class="capitalize"
|
||||
>
|
||||
Add {itemsSchema().title}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
// Add the new item to the FieldArray
|
||||
handleSubmit={(values, event) => {
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
const prev: unknown[] = getValues(
|
||||
props.formStore,
|
||||
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
listFieldName,
|
||||
);
|
||||
if (itemsSchema().type === "object") {
|
||||
const newIdx = prev.length;
|
||||
setValue(
|
||||
props.formStore,
|
||||
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
`${listFieldName}.${newIdx}`,
|
||||
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
values.root,
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
insert(props.formStore, listFieldName, {
|
||||
// @ts-expect-error: listFieldName is not known ahead of time
|
||||
value: values.root,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FieldArray>
|
||||
</Match>
|
||||
</Switch>
|
||||
</>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ObjectFieldPropertyLabelProps {
|
||||
schema: JSONSchema7;
|
||||
fallback: JSX.Element;
|
||||
}
|
||||
export function ObjectFieldPropertyLabel(props: ObjectFieldPropertyLabelProps) {
|
||||
return (
|
||||
<Switch fallback={props.fallback}>
|
||||
{/* @ts-expect-error: $exportedModuleInfo should exist since we export it */}
|
||||
<Match when={props.schema?.$exportedModuleInfo?.path}>
|
||||
{(path) => path()[path().length - 1]}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectFields<T extends FieldValues, R extends ResponseData>(
|
||||
props: SchemaFieldsProps<T, R>,
|
||||
) {
|
||||
if (props.schema.type !== "object") {
|
||||
return (
|
||||
<span class="text-error-700">
|
||||
Error cannot render the following as Object
|
||||
<Unsupported schema={props.schema} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const fieldName = props.path.join(".");
|
||||
const { Field } = props;
|
||||
|
||||
return (
|
||||
<Switch
|
||||
fallback={
|
||||
<Unsupported
|
||||
schema={props.schema}
|
||||
error="Dont know how to render objectFields"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Match
|
||||
when={!props.schema.additionalProperties && props.schema.properties}
|
||||
>
|
||||
{(properties) => (
|
||||
<For each={Object.entries(properties())}>
|
||||
{([propName, propSchema]) => (
|
||||
<div
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
class={cx(
|
||||
"w-full grid grid-cols-1 gap-4 justify-items-start",
|
||||
`p-${props.path.length * 2}`,
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
label={propName}
|
||||
required={props.schema.required?.some((r) => r === propName)}
|
||||
/>
|
||||
|
||||
{typeof propSchema === "object" && (
|
||||
<SchemaFields
|
||||
{...props}
|
||||
schema={propSchema}
|
||||
path={[...props.path, propName]}
|
||||
/>
|
||||
)}
|
||||
{typeof propSchema === "boolean" && (
|
||||
<span class="text-error-700">
|
||||
Schema: Object of Boolean not supported
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</Match>
|
||||
{/* Objects where people can define their own keys
|
||||
- Trivial Key-value pairs. Where the value is a string a number or a list of strings (trivial select).
|
||||
- Non-trivial Key-value pairs. Where the value is an object or a list
|
||||
*/}
|
||||
<Match
|
||||
when={
|
||||
typeof props.schema.additionalProperties === "object" &&
|
||||
props.schema.additionalProperties
|
||||
}
|
||||
>
|
||||
{(additionalPropertiesSchema) => (
|
||||
<Switch
|
||||
fallback={
|
||||
<Unsupported
|
||||
schema={additionalPropertiesSchema()}
|
||||
error="type of additionalProperties not supported yet"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* Non-trivival cases */}
|
||||
<Match
|
||||
when={
|
||||
additionalPropertiesSchema().type === "object" &&
|
||||
additionalPropertiesSchema()
|
||||
}
|
||||
>
|
||||
{(itemSchema) => (
|
||||
<Field
|
||||
// Important!: Register the object field to gain access to the dynamic object properties
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
name={fieldName}
|
||||
>
|
||||
{(objectField, fp) => (
|
||||
<>
|
||||
<For
|
||||
fallback={
|
||||
<>
|
||||
<label class="">
|
||||
No{" "}
|
||||
<ObjectFieldPropertyLabel
|
||||
schema={itemSchema()}
|
||||
fallback={"No entries"}
|
||||
/>{" "}
|
||||
yet.
|
||||
</label>
|
||||
</>
|
||||
}
|
||||
each={Object.entries(objectField.value || {})}
|
||||
>
|
||||
{([key, relatedValue]) => (
|
||||
<Field
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
name={`${fieldName}.${key}`}
|
||||
>
|
||||
{(f, fp) => (
|
||||
<div class="w-full border-l-4 border-gray-300 pl-4">
|
||||
<DynForm
|
||||
formProps={{
|
||||
class: cx("w-full"),
|
||||
}}
|
||||
schema={itemSchema()}
|
||||
initialValues={f.value}
|
||||
components={{
|
||||
before: (
|
||||
<div class="flex w-full">
|
||||
<span class="text-xl font-semibold">
|
||||
{key}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="ml-auto"
|
||||
size="s"
|
||||
type="button"
|
||||
onClick={(_e) => {
|
||||
const copy = {
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
...objectField.value,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete copy[key];
|
||||
setValue(
|
||||
props.formStore,
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
`${fieldName}`,
|
||||
copy,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon icon="Trash" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</For>
|
||||
{/* Replace this with a normal input ?*/}
|
||||
<DynForm
|
||||
formProps={{
|
||||
class: cx("w-full"),
|
||||
}}
|
||||
resetOnSubmit={true}
|
||||
initialValues={{ "": "" }}
|
||||
schema={{
|
||||
type: "string",
|
||||
title: `Entry title or key`,
|
||||
}}
|
||||
handleSubmit={(values, event) => {
|
||||
setValue(
|
||||
props.formStore,
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
`${fieldName}`,
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
{ ...objectField.value, [values[""]]: {} },
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
additionalPropertiesSchema().type === "array" &&
|
||||
additionalPropertiesSchema()
|
||||
}
|
||||
>
|
||||
{(itemSchema) => (
|
||||
<Unsupported
|
||||
schema={itemSchema()}
|
||||
error="dynamic arrays are not implemented yet"
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
{/* TODO: Trivial cases */}
|
||||
</Switch>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
189
pkgs/clan-app/ui-2d/src/api/index.tsx
Normal file
189
pkgs/clan-app/ui-2d/src/api/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import schema from "@/api/API.json" with { type: "json" };
|
||||
import { API, Error as ApiError } from "@/api/API";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
import { toast, Toast } from "solid-toast";
|
||||
import {
|
||||
ErrorToastComponent,
|
||||
CancelToastComponent,
|
||||
} from "@/src/components/toast";
|
||||
export type OperationNames = keyof API;
|
||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||
|
||||
export type ApiEnvelope<T> =
|
||||
| {
|
||||
status: "success";
|
||||
data: T;
|
||||
op_key: string;
|
||||
}
|
||||
| ApiError;
|
||||
|
||||
export type Services = NonNullable<Inventory["services"]>;
|
||||
export type ServiceNames = keyof Services;
|
||||
export type ClanService<T extends ServiceNames> = Services[T];
|
||||
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
||||
Services[T]
|
||||
>[string];
|
||||
|
||||
export type SuccessQuery<T extends OperationNames> = Extract<
|
||||
OperationResponse<T>,
|
||||
{ status: "success" }
|
||||
>;
|
||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||
|
||||
export type ErrorQuery<T extends OperationNames> = Extract<
|
||||
OperationResponse<T>,
|
||||
{ status: "error" }
|
||||
>;
|
||||
export type ErrorData<T extends OperationNames> = ErrorQuery<T>["errors"];
|
||||
|
||||
export type ClanOperations = Record<OperationNames, (str: string) => void>;
|
||||
|
||||
export interface GtkResponse<T> {
|
||||
result: T;
|
||||
op_key: string;
|
||||
}
|
||||
const _callApi = <K extends OperationNames>(
|
||||
method: K,
|
||||
args: OperationArgs<K>,
|
||||
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
||||
// if window[method] does not exist, throw an error
|
||||
if (!(method in window)) {
|
||||
console.error(`Method ${method} not found on window object`);
|
||||
// return a rejected promise
|
||||
return {
|
||||
promise: Promise.resolve({
|
||||
status: "error",
|
||||
errors: [
|
||||
{
|
||||
message: `Method ${method} not found on window object`,
|
||||
code: "method_not_found",
|
||||
},
|
||||
],
|
||||
op_key: "noop",
|
||||
}),
|
||||
op_key: "noop",
|
||||
};
|
||||
}
|
||||
|
||||
const promise = (
|
||||
window as unknown as Record<
|
||||
OperationNames,
|
||||
(
|
||||
args: OperationArgs<OperationNames>,
|
||||
) => Promise<OperationResponse<OperationNames>>
|
||||
>
|
||||
)[method](args) as Promise<OperationResponse<K>>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const op_key = (promise as any)._webviewMessageId as string;
|
||||
|
||||
return { promise, op_key };
|
||||
};
|
||||
|
||||
const handleCancel = async <K extends OperationNames>(
|
||||
ops_key: string,
|
||||
orig_task: Promise<OperationResponse<K>>,
|
||||
) => {
|
||||
console.log("Canceling operation: ", ops_key);
|
||||
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key });
|
||||
promise.catch((error) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
<ErrorToastComponent
|
||||
t={t}
|
||||
message={"Unexpected error: " + (error?.message || String(error))}
|
||||
/>
|
||||
),
|
||||
{
|
||||
duration: 5000,
|
||||
},
|
||||
);
|
||||
console.error("Unhandled promise rejection in callApi:", error);
|
||||
});
|
||||
const resp = await promise;
|
||||
|
||||
if (resp.status === "error") {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
<ErrorToastComponent
|
||||
t={t}
|
||||
message={"Failed to cancel operation: " + ops_key}
|
||||
/>
|
||||
),
|
||||
{
|
||||
duration: 5000,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(orig_task as any).cancelled = true;
|
||||
}
|
||||
console.log("Cancel response: ", resp);
|
||||
};
|
||||
|
||||
export const callApi = <K extends OperationNames>(
|
||||
method: K,
|
||||
args: OperationArgs<K>,
|
||||
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
||||
console.log("Calling API", method, args);
|
||||
|
||||
const { promise, op_key } = _callApi(method, args);
|
||||
promise.catch((error) => {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
<ErrorToastComponent
|
||||
t={t}
|
||||
message={"Unexpected error: " + (error?.message || String(error))}
|
||||
/>
|
||||
),
|
||||
{
|
||||
duration: 5000,
|
||||
},
|
||||
);
|
||||
console.error("Unhandled promise rejection in callApi:", error);
|
||||
});
|
||||
|
||||
const toastId = toast.custom(
|
||||
(
|
||||
t, // t is the Toast object, t.id is the id of THIS toast instance
|
||||
) => (
|
||||
<CancelToastComponent
|
||||
t={t}
|
||||
message={"Executing " + method}
|
||||
onCancel={handleCancel.bind(null, op_key, promise)}
|
||||
/>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
},
|
||||
);
|
||||
|
||||
const new_promise = promise.then((response) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const cancelled = (promise as any).cancelled;
|
||||
if (cancelled) {
|
||||
console.log("Not printing toast because operation was cancelled");
|
||||
}
|
||||
|
||||
if (response.status === "error" && !cancelled) {
|
||||
toast.remove(toastId);
|
||||
toast.custom(
|
||||
(t) => (
|
||||
<ErrorToastComponent
|
||||
t={t}
|
||||
message={"Error: " + response.errors[0].message}
|
||||
/>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
return { promise: new_promise, op_key: op_key };
|
||||
};
|
||||
20
pkgs/clan-app/ui-2d/src/api/wifi.ts
Normal file
20
pkgs/clan-app/ui-2d/src/api/wifi.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { callApi } from ".";
|
||||
import { Schema as Inventory } from "@/api/Inventory";
|
||||
|
||||
export const instance_name = (machine_name: string) =>
|
||||
`${machine_name}_wifi_0` as const;
|
||||
|
||||
export async function get_iwd_service(base_path: string, machine_name: string) {
|
||||
const r = await callApi("get_inventory", {
|
||||
flake: { identifier: base_path },
|
||||
}).promise;
|
||||
if (r.status == "error") {
|
||||
return null;
|
||||
}
|
||||
// @FIXME: Clean this up once we implement the feature
|
||||
// @ts-expect-error: This doesn't check currently
|
||||
const inventory: Inventory = r.data;
|
||||
|
||||
const instance_key = instance_name(machine_name);
|
||||
return inventory.services?.iwd?.[instance_key] || null;
|
||||
}
|
||||
188
pkgs/clan-app/ui-2d/src/api_test.tsx
Normal file
188
pkgs/clan-app/ui-2d/src/api_test.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
createForm,
|
||||
FieldValues,
|
||||
getValues,
|
||||
setValue,
|
||||
SubmitHandler,
|
||||
} from "@modular-forms/solid";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
import { Button } from "./components/Button/Button";
|
||||
import { callApi } from "./api";
|
||||
import { API } from "@/api/API";
|
||||
import { createSignal, Match, Switch, For, Show } from "solid-js";
|
||||
import { Typography } from "./components/Typography";
|
||||
import { useQuery } from "@tanstack/solid-query";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import jsonSchema from "@/api/API.json";
|
||||
|
||||
interface APITesterForm extends FieldValues {
|
||||
endpoint: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const ACTUAL_API_ENDPOINT_NAMES: (keyof API)[] = jsonSchema.required.map(
|
||||
(key) => key as keyof API,
|
||||
);
|
||||
|
||||
export const ApiTester = () => {
|
||||
const [persistedTestData, setPersistedTestData] = makePersisted(
|
||||
createSignal<APITesterForm>(),
|
||||
{
|
||||
name: "_test_data",
|
||||
storage: localStorage,
|
||||
},
|
||||
);
|
||||
|
||||
const [formStore, { Form, Field }] = createForm<APITesterForm>({
|
||||
initialValues: persistedTestData() || { endpoint: "", payload: "" },
|
||||
});
|
||||
|
||||
const [endpointSearchTerm, setEndpointSearchTerm] = createSignal(
|
||||
getValues(formStore).endpoint || "",
|
||||
);
|
||||
const [showSuggestions, setShowSuggestions] = createSignal(false);
|
||||
|
||||
const filteredEndpoints = () => {
|
||||
const term = endpointSearchTerm().toLowerCase();
|
||||
if (!term) return ACTUAL_API_ENDPOINT_NAMES;
|
||||
return ACTUAL_API_ENDPOINT_NAMES.filter((ep) =>
|
||||
ep.toLowerCase().includes(term),
|
||||
);
|
||||
};
|
||||
|
||||
const query = useQuery(() => {
|
||||
const currentEndpoint = getValues(formStore).endpoint;
|
||||
const currentPayload = getValues(formStore).payload;
|
||||
const values = getValues(formStore);
|
||||
|
||||
return {
|
||||
queryKey: ["api-tester", currentEndpoint, currentPayload],
|
||||
queryFn: async () => {
|
||||
return await callApi(
|
||||
values.endpoint as keyof API,
|
||||
JSON.parse(values.payload || "{}"),
|
||||
);
|
||||
},
|
||||
staleTime: Infinity,
|
||||
enabled: false,
|
||||
};
|
||||
});
|
||||
|
||||
const handleSubmit: SubmitHandler<APITesterForm> = (values) => {
|
||||
console.log(values);
|
||||
setPersistedTestData(values);
|
||||
setEndpointSearchTerm(values.endpoint);
|
||||
query.refetch();
|
||||
|
||||
const v = getValues(formStore);
|
||||
console.log(v);
|
||||
};
|
||||
return (
|
||||
<div class="p-2">
|
||||
<h1>API Tester</h1>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div class="flex flex-col">
|
||||
<Field name="endpoint">
|
||||
{(field, fieldProps) => (
|
||||
<div class="relative">
|
||||
<TextInput
|
||||
label={"endpoint"}
|
||||
value={field.value || ""}
|
||||
inputProps={{
|
||||
...fieldProps,
|
||||
onInput: (e: Event) => {
|
||||
if (fieldProps.onInput) {
|
||||
(fieldProps.onInput as (ev: Event) => void)(e);
|
||||
}
|
||||
setEndpointSearchTerm(
|
||||
(e.currentTarget as HTMLInputElement).value,
|
||||
);
|
||||
setShowSuggestions(true);
|
||||
},
|
||||
onBlur: (e: FocusEvent) => {
|
||||
if (fieldProps.onBlur) {
|
||||
(fieldProps.onBlur as (ev: FocusEvent) => void)(e);
|
||||
}
|
||||
setTimeout(() => setShowSuggestions(false), 150);
|
||||
},
|
||||
onFocus: (e: FocusEvent) => {
|
||||
setEndpointSearchTerm(field.value || "");
|
||||
setShowSuggestions(true);
|
||||
},
|
||||
onKeyDown: (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setShowSuggestions(false);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Show
|
||||
when={showSuggestions() && filteredEndpoints().length > 0}
|
||||
>
|
||||
<ul class="absolute z-10 mt-1 max-h-60 w-full overflow-y-auto rounded border border-gray-300 bg-white shadow-lg">
|
||||
<For each={filteredEndpoints()}>
|
||||
{(ep) => (
|
||||
<li
|
||||
class="cursor-pointer p-2 hover:bg-gray-100"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
setValue(formStore, "endpoint", ep);
|
||||
setEndpointSearchTerm(ep);
|
||||
setShowSuggestions(false);
|
||||
}}
|
||||
>
|
||||
{ep}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="payload">
|
||||
{(field, fieldProps) => (
|
||||
<div class="my-2 flex flex-col">
|
||||
<label class="mb-1 font-medium" for="payload-textarea">
|
||||
payload
|
||||
</label>
|
||||
<textarea
|
||||
id="payload-textarea"
|
||||
class="min-h-[120px] resize-y rounded border p-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
placeholder={`{\n "key": "value"\n}`}
|
||||
value={field.value || ""}
|
||||
{...fieldProps}
|
||||
onInput={(e) => {
|
||||
fieldProps.onInput?.(e);
|
||||
}}
|
||||
spellcheck={false}
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Button class="m-2" disabled={query.isFetching}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
<div>
|
||||
<Typography hierarchy="title" size="default">
|
||||
Result
|
||||
</Typography>
|
||||
<Switch>
|
||||
<Match when={query.isFetching}>
|
||||
<span>loading ...</span>
|
||||
</Match>
|
||||
<Match when={query.isFetched}>
|
||||
<pre>
|
||||
<code>{JSON.stringify(query.data, null, 2)}</code>
|
||||
</pre>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
16
pkgs/clan-app/ui-2d/src/components/BackButton.tsx
Normal file
16
pkgs/clan-app/ui-2d/src/components/BackButton.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Button } from "./Button/Button";
|
||||
import Icon from "./icon";
|
||||
|
||||
export const BackButton = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="s"
|
||||
class="mr-2"
|
||||
onClick={() => navigate(-1)}
|
||||
startIcon={<Icon icon="CaretLeft" />}
|
||||
></Button>
|
||||
);
|
||||
};
|
||||
55
pkgs/clan-app/ui-2d/src/components/Button/Button-Base.css
Normal file
55
pkgs/clan-app/ui-2d/src/components/Button/Button-Base.css
Normal file
@@ -0,0 +1,55 @@
|
||||
@import "Button-Light.css";
|
||||
@import "Button-Dark.css";
|
||||
@import "Button-Ghost.css";
|
||||
|
||||
.button {
|
||||
@apply inline-flex items-center flex-shrink gap-1 justify-center p-4 font-semibold;
|
||||
letter-spacing: 0.0275rem;
|
||||
}
|
||||
|
||||
/* button SIZES */
|
||||
|
||||
.button--default {
|
||||
padding: theme(padding.2) theme(padding.4);
|
||||
height: theme(height.9);
|
||||
border-radius: theme(borderRadius.DEFAULT);
|
||||
|
||||
&:has(> .button__icon--start):has(> .button__label) {
|
||||
padding-left: theme(padding[2.5]);
|
||||
}
|
||||
|
||||
&:has(> .button__icon--end):has(> .button__label) {
|
||||
padding-right: theme(padding[2.5]);
|
||||
}
|
||||
}
|
||||
|
||||
.button--small {
|
||||
padding: theme(padding[1.5]) theme(padding[3]);
|
||||
height: theme(height.8);
|
||||
border-radius: 3px;
|
||||
|
||||
&:has(> .button__icon--start):has(> .button__label) {
|
||||
padding-left: theme(padding.2);
|
||||
}
|
||||
|
||||
&:has(> .button__label):has(> .button__icon--end) {
|
||||
padding-right: theme(padding.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* button group */
|
||||
|
||||
.button-group .button:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.button-group .button:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.button-group .button:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
31
pkgs/clan-app/ui-2d/src/components/Button/Button-Dark.css
Normal file
31
pkgs/clan-app/ui-2d/src/components/Button/Button-Dark.css
Normal file
@@ -0,0 +1,31 @@
|
||||
/* button DARK and states */
|
||||
|
||||
.button--dark {
|
||||
@apply border border-solid border-secondary-950 bg-primary-800 text-white;
|
||||
|
||||
box-shadow: inset 1px 1px theme(backgroundColor.secondary.700);
|
||||
|
||||
&:disabled {
|
||||
@apply disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300;
|
||||
}
|
||||
|
||||
& .button__icon {
|
||||
color: theme(textColor.secondary.200);
|
||||
}
|
||||
}
|
||||
|
||||
.button--dark-hover:hover {
|
||||
@apply hover:bg-secondary-900;
|
||||
}
|
||||
|
||||
.button--dark-focus:focus {
|
||||
@apply focus:border-secondary-900;
|
||||
}
|
||||
|
||||
.button--dark-active:active {
|
||||
@apply focus:border-secondary-900;
|
||||
}
|
||||
|
||||
.button--dark-active:active {
|
||||
@apply active:border-secondary-900 active:shadow-button-primary-active;
|
||||
}
|
||||
11
pkgs/clan-app/ui-2d/src/components/Button/Button-Ghost.css
Normal file
11
pkgs/clan-app/ui-2d/src/components/Button/Button-Ghost.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.button--ghost-hover:hover {
|
||||
@apply hover:bg-secondary-100 hover:text-secondary-900;
|
||||
}
|
||||
|
||||
.button--ghost-focus:focus {
|
||||
@apply focus:bg-secondary-200 focus:text-secondary-900;
|
||||
}
|
||||
|
||||
.button--ghost-active:active {
|
||||
@apply active:bg-secondary-200 active:text-secondary-900 active:shadow-button-primary-active;
|
||||
}
|
||||
37
pkgs/clan-app/ui-2d/src/components/Button/Button-Light.css
Normal file
37
pkgs/clan-app/ui-2d/src/components/Button/Button-Light.css
Normal file
@@ -0,0 +1,37 @@
|
||||
/* button LIGHT and states */
|
||||
|
||||
.button--light {
|
||||
@apply border border-solid border-secondary-400 bg-secondary-100 text-secondary-950;
|
||||
|
||||
box-shadow: inset 1px 1px theme(backgroundColor.white);
|
||||
|
||||
&:disabled {
|
||||
@apply disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700;
|
||||
}
|
||||
|
||||
& .button__icon {
|
||||
color: theme(textColor.secondary.900);
|
||||
}
|
||||
}
|
||||
|
||||
.button--light-hover:hover {
|
||||
@apply hover:bg-secondary-200;
|
||||
}
|
||||
|
||||
.button--light-focus:focus {
|
||||
@apply focus:bg-secondary-200;
|
||||
|
||||
& .button__label {
|
||||
color: theme(textColor.secondary.900) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.button--light-active:active {
|
||||
@apply active:bg-secondary-200 border-secondary-600 active:text-secondary-900 active:shadow-button-primary-active;
|
||||
|
||||
box-shadow: inset 2px 2px theme(backgroundColor.secondary.300);
|
||||
|
||||
& .button__label {
|
||||
color: theme(textColor.secondary.900) !important;
|
||||
}
|
||||
}
|
||||
97
pkgs/clan-app/ui-2d/src/components/Button/Button.tsx
Normal file
97
pkgs/clan-app/ui-2d/src/components/Button/Button.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "../Typography";
|
||||
|
||||
import "./Button-Base.css";
|
||||
|
||||
type Variants = "dark" | "light" | "ghost";
|
||||
type Size = "default" | "s";
|
||||
|
||||
const variantColors: (
|
||||
disabled: boolean | undefined,
|
||||
) => Record<Variants, string> = (disabled) => ({
|
||||
dark: cx(
|
||||
"button--dark",
|
||||
!disabled && "button--dark-hover", // Hover state
|
||||
!disabled && "button--dark-focus", // Focus state
|
||||
!disabled && "button--dark-active", // Active state
|
||||
// Disabled
|
||||
"disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300",
|
||||
),
|
||||
light: cx(
|
||||
"button--light",
|
||||
|
||||
!disabled && "button--light-hover", // Hover state
|
||||
!disabled && "button--light-focus", // Focus state
|
||||
!disabled && "button--light-active", // Active state
|
||||
),
|
||||
ghost: cx(
|
||||
!disabled && "button--ghost-hover", // Hover state
|
||||
!disabled && "button--ghost-focus", // Focus state
|
||||
!disabled && "button--ghost-active", // Active state
|
||||
),
|
||||
});
|
||||
|
||||
const sizePaddings: Record<Size, string> = {
|
||||
default: cx("button--default"),
|
||||
s: cx("button button--small"), //cx("rounded-sm py-[0.375rem] px-3"),
|
||||
};
|
||||
|
||||
const sizeFont: Record<Size, string> = {
|
||||
default: cx("text-[0.8125rem]"),
|
||||
s: cx("text-[0.75rem]"),
|
||||
};
|
||||
|
||||
export interface ButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variants;
|
||||
size?: Size;
|
||||
children?: JSX.Element;
|
||||
startIcon?: JSX.Element;
|
||||
endIcon?: JSX.Element;
|
||||
class?: string;
|
||||
}
|
||||
export const Button = (props: ButtonProps) => {
|
||||
const [local, other] = splitProps(props, [
|
||||
"children",
|
||||
"variant",
|
||||
"size",
|
||||
"startIcon",
|
||||
"endIcon",
|
||||
"class",
|
||||
]);
|
||||
|
||||
const buttonInvertion = (variant: Variants) => {
|
||||
return !(!variant || variant === "ghost" || variant === "light");
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
class={cx(
|
||||
local.class,
|
||||
"button", // default button class
|
||||
variantColors(props.disabled)[local.variant || "dark"], // button appereance
|
||||
sizePaddings[local.size || "default"], // button size
|
||||
)}
|
||||
{...other}
|
||||
>
|
||||
{local.startIcon && (
|
||||
<span class="button__icon--start">{local.startIcon}</span>
|
||||
)}
|
||||
{local.children && (
|
||||
<Typography
|
||||
class="button__label"
|
||||
hierarchy="label"
|
||||
size={local.size || "default"}
|
||||
color="inherit"
|
||||
inverted={buttonInvertion(local.variant || "dark")}
|
||||
weight="medium"
|
||||
tag="span"
|
||||
>
|
||||
{local.children}
|
||||
</Typography>
|
||||
)}
|
||||
{local.endIcon && <span class="button__icon--end">{local.endIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
100
pkgs/clan-app/ui-2d/src/components/FileInput.tsx
Normal file
100
pkgs/clan-app/ui-2d/src/components/FileInput.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import cx from "classnames";
|
||||
import { createMemo, JSX, Show, splitProps } from "solid-js";
|
||||
|
||||
export interface FileInputProps {
|
||||
ref: (element: HTMLInputElement) => void;
|
||||
name: string;
|
||||
value?: File[] | File;
|
||||
onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
|
||||
onClick: JSX.EventHandler<HTMLInputElement, Event>;
|
||||
onChange: JSX.EventHandler<HTMLInputElement, Event>;
|
||||
onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent>;
|
||||
accept?: string;
|
||||
required?: boolean;
|
||||
multiple?: boolean;
|
||||
class?: string;
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
placeholder?: JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* File input field that users can click or drag files into. Various
|
||||
* decorations can be displayed in or around the field to communicate the entry
|
||||
* requirements.
|
||||
*/
|
||||
export function FileInput(props: FileInputProps) {
|
||||
// Split input element props
|
||||
const [, inputProps] = splitProps(props, [
|
||||
"class",
|
||||
"value",
|
||||
"label",
|
||||
"error",
|
||||
"placeholder",
|
||||
]);
|
||||
|
||||
// Create file list
|
||||
const getFiles = createMemo(() =>
|
||||
props.value
|
||||
? Array.isArray(props.value)
|
||||
? props.value
|
||||
: [props.value]
|
||||
: [],
|
||||
);
|
||||
|
||||
return (
|
||||
<div class={cx(" w-full", props.class)}>
|
||||
<div class="">
|
||||
<span
|
||||
class=" block"
|
||||
classList={{
|
||||
"after:ml-0.5 after:text-primary after:content-['*']":
|
||||
props.required,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={props.helperText}>
|
||||
<span class=" m-1">{props.helperText}</span>
|
||||
</Show>
|
||||
<div
|
||||
class={cx(
|
||||
"relative flex min-h-[96px] w-full items-center justify-center rounded-2xl border-[3px] border-dashed p-8 text-center focus-within:ring-4 md:min-h-[112px] md:text-lg lg:min-h-[128px] lg:p-10 lg:text-xl",
|
||||
!getFiles().length && "text-slate-500",
|
||||
props.error
|
||||
? "border-red-500/25 focus-within:border-red-500/50 focus-within:ring-red-500/10 hover:border-red-500/40 dark:border-red-400/25 dark:focus-within:border-red-400/50 dark:focus-within:ring-red-400/10 dark:hover:border-red-400/40"
|
||||
: "border-slate-200 focus-within:border-sky-500/50 focus-within:ring-sky-500/10 hover:border-slate-300 dark:border-slate-800 dark:focus-within:border-sky-400/50 dark:focus-within:ring-sky-400/10 dark:hover:border-slate-700",
|
||||
)}
|
||||
>
|
||||
<Show
|
||||
when={getFiles().length}
|
||||
fallback={
|
||||
props.placeholder || (
|
||||
<>Click to select file{props.multiple && "s"}</>
|
||||
)
|
||||
}
|
||||
>
|
||||
Selected file{props.multiple && "s"}:{" "}
|
||||
{getFiles()
|
||||
.map(({ name }) => name)
|
||||
.join(", ")}
|
||||
</Show>
|
||||
<input
|
||||
{...inputProps}
|
||||
// Disable drag n drop
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
class="absolute size-full cursor-pointer opacity-0"
|
||||
type="file"
|
||||
id={props.name}
|
||||
aria-invalid={!!props.error}
|
||||
aria-errormessage={`${props.name}-error`}
|
||||
/>
|
||||
{props.error && (
|
||||
<span class=" font-bold text-error-700">{props.error}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
pkgs/clan-app/ui-2d/src/components/Helpers/List.tsx
Normal file
20
pkgs/clan-app/ui-2d/src/components/Helpers/List.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type JSX } from "solid-js";
|
||||
|
||||
type sizes = "small" | "medium" | "large";
|
||||
|
||||
const gapSizes: Record<sizes, string> = {
|
||||
small: "gap-2",
|
||||
medium: "gap-4",
|
||||
large: "gap-6",
|
||||
};
|
||||
|
||||
interface List {
|
||||
children: JSX.Element;
|
||||
gapSize: sizes;
|
||||
}
|
||||
|
||||
export const List = (props: List) => {
|
||||
const { children, gapSize } = props;
|
||||
|
||||
return <ul class={`flex flex-col ${gapSizes[gapSize]}`}> {children}</ul>;
|
||||
};
|
||||
1
pkgs/clan-app/ui-2d/src/components/Helpers/index.tsx
Normal file
1
pkgs/clan-app/ui-2d/src/components/Helpers/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { List } from "./List";
|
||||
84
pkgs/clan-app/ui-2d/src/components/Menu.tsx
Normal file
84
pkgs/clan-app/ui-2d/src/components/Menu.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { children, createSignal, type JSX } from "solid-js";
|
||||
import { useFloating } from "@/src/floating";
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
hide,
|
||||
offset,
|
||||
Placement,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
import cx from "classnames";
|
||||
import { Button } from "./Button/Button";
|
||||
|
||||
interface MenuProps {
|
||||
/**
|
||||
* Used by the html API to associate the popover with the dispatcher button
|
||||
*/
|
||||
popoverid: string;
|
||||
|
||||
label: JSX.Element;
|
||||
|
||||
children?: JSX.Element;
|
||||
buttonProps?: JSX.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
buttonClass?: string;
|
||||
/**
|
||||
* @default "bottom"
|
||||
*/
|
||||
placement?: Placement;
|
||||
}
|
||||
export const Menu = (props: MenuProps) => {
|
||||
const c = children(() => props.children);
|
||||
const [reference, setReference] = createSignal<HTMLElement>();
|
||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||
|
||||
// `position` is a reactive object.
|
||||
const position = useFloating(reference, floating, {
|
||||
placement: "bottom",
|
||||
|
||||
// pass options. Ensure the cleanup function is returned.
|
||||
whileElementsMounted: (reference, floating, update) =>
|
||||
autoUpdate(reference, floating, update, {
|
||||
animationFrame: true,
|
||||
}),
|
||||
middleware: [
|
||||
offset(5),
|
||||
shift(),
|
||||
flip(),
|
||||
|
||||
hide({
|
||||
strategy: "referenceHidden",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="s"
|
||||
popovertarget={props.popoverid}
|
||||
popovertargetaction="toggle"
|
||||
ref={setReference}
|
||||
class={cx("", props.buttonClass)}
|
||||
{...props.buttonProps}
|
||||
>
|
||||
{props.label}
|
||||
</Button>
|
||||
<div
|
||||
popover="auto"
|
||||
id={props.popoverid}
|
||||
ref={setFloating}
|
||||
style={{
|
||||
margin: 0,
|
||||
position: position.strategy,
|
||||
top: `${position.y ?? 0}px`,
|
||||
left: `${position.x ?? 0}px`,
|
||||
}}
|
||||
class="bg-transparent"
|
||||
>
|
||||
{c()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
54
pkgs/clan-app/ui-2d/src/components/SelectInput.tsx
Normal file
54
pkgs/clan-app/ui-2d/src/components/SelectInput.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid";
|
||||
import { Show } from "solid-js";
|
||||
import { type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
|
||||
interface SelectInputProps<T extends FieldValues, R extends ResponseData> {
|
||||
formStore: FormStore<T, R>;
|
||||
value: string;
|
||||
options: JSX.Element;
|
||||
selectProps: JSX.HTMLAttributes<HTMLSelectElement>;
|
||||
label: JSX.Element;
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
topRightLabel?: JSX.Element;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SelectInput<T extends FieldValues, R extends ResponseData>(
|
||||
props: SelectInputProps<T, R>,
|
||||
) {
|
||||
return (
|
||||
<label
|
||||
class={cx(" w-full", props.class)}
|
||||
aria-disabled={props.formStore.submitting}
|
||||
>
|
||||
<div class="">
|
||||
<span
|
||||
class=" block"
|
||||
classList={{
|
||||
"after:ml-0.5 after:text-primary after:content-['*']":
|
||||
props.required,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
<Show when={props.topRightLabel}>
|
||||
<span class="">{props.topRightLabel}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<select
|
||||
{...props.selectProps}
|
||||
required={props.required}
|
||||
class="w-full"
|
||||
value={props.value}
|
||||
>
|
||||
{props.options}
|
||||
</select>
|
||||
|
||||
{props.error && (
|
||||
<span class=" font-bold text-error-700">{props.error}</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { List } from "@/src/components/Helpers";
|
||||
import { SidebarListItem } from "../SidebarListItem";
|
||||
|
||||
export const SidebarFlyout = () => {
|
||||
return (
|
||||
<div class="sidebar__flyout">
|
||||
<div class="sidebar__flyout__inner">
|
||||
<List gapSize="small">
|
||||
<SidebarListItem href="/clans" title="Settings" />
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
71
pkgs/clan-app/ui-2d/src/components/Sidebar/SidebarHeader.tsx
Normal file
71
pkgs/clan-app/ui-2d/src/components/Sidebar/SidebarHeader.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import { SidebarFlyout } from "./SidebarFlyout";
|
||||
import "./css/sidebar.css";
|
||||
import Icon from "../icon";
|
||||
|
||||
interface SidebarProps {
|
||||
clanName: string;
|
||||
showFlyout?: () => boolean;
|
||||
}
|
||||
|
||||
const ClanProfile = (props: SidebarProps) => {
|
||||
return (
|
||||
<div
|
||||
class={`sidebar__profile ${props.showFlyout?.() ? "sidebar__profile--flyout" : ""}`}
|
||||
>
|
||||
<Typography
|
||||
class="sidebar__profile__character"
|
||||
tag="span"
|
||||
hierarchy="title"
|
||||
size="m"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
>
|
||||
{props.clanName.slice(0, 1).toUpperCase()}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ClanTitle = (props: SidebarProps) => {
|
||||
return (
|
||||
<Typography
|
||||
tag="h3"
|
||||
hierarchy="body"
|
||||
size="default"
|
||||
weight="medium"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
>
|
||||
{props.clanName}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarHeader = (props: SidebarProps) => {
|
||||
const [showFlyout, toggleFlyout] = createSignal(false);
|
||||
|
||||
function handleClick() {
|
||||
toggleFlyout(!showFlyout());
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="sidebar__header">
|
||||
<div onClick={handleClick} class="sidebar__header__inner">
|
||||
{/* <ClanProfile clanName={props.clanName} showFlyout={showFlyout} /> */}
|
||||
<div class="w-full pl-1 text-white">
|
||||
<ClanTitle clanName={props.clanName} />
|
||||
</div>
|
||||
<Show
|
||||
when={showFlyout}
|
||||
fallback={<Icon size={12} class="text-white" icon="CaretDown" />}
|
||||
>
|
||||
<Icon size={12} class="text-white" icon="CaretDown" />
|
||||
</Show>
|
||||
</div>
|
||||
{showFlyout() && <SidebarFlyout />}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import "./css/sidebar.css";
|
||||
|
||||
interface SidebarListItem {
|
||||
title: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export const SidebarListItem = (props: SidebarListItem) => {
|
||||
const { title, href } = props;
|
||||
|
||||
return (
|
||||
<li class="">
|
||||
<A class="sidebar__list__link" href={href}>
|
||||
<Typography
|
||||
class="sidebar__list__content"
|
||||
tag="span"
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="normal"
|
||||
color="primary"
|
||||
inverted={true}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</A>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.sidebar__flyout {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: theme(zIndex.30);
|
||||
|
||||
padding: theme(padding[1]);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.sidebar__flyout__inner {
|
||||
position: relative;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
padding: theme(padding.12) theme(padding.3) theme(padding.3);
|
||||
background-color: var(--clr-bg-inv-4);
|
||||
/* / 0.95); */
|
||||
border: 1px solid var(--clr-border-inv-4);
|
||||
border-radius: theme(borderRadius.lg);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.sidebar__header {
|
||||
position: relative;
|
||||
padding: 1px 1px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--clr-bg-inv-3);
|
||||
|
||||
border-bottom: 1px solid var(--clr-border-inv-3);
|
||||
border-top-left-radius: theme(borderRadius.xl);
|
||||
border-top-right-radius: theme(borderRadius.xl);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__header__inner {
|
||||
position: relative;
|
||||
z-index: theme(zIndex.40);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0 theme(gap.3);
|
||||
|
||||
padding: theme(padding.3) theme(padding.3);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.sidebar__list__link {
|
||||
position: relative;
|
||||
cursor: theme(cursor.pointer);
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: theme(zIndex.10);
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: theme(borderRadius.md);
|
||||
transform: scale(0.98);
|
||||
transition: transform 0.24s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
background: var(--clr-bg-inv-acc-2);
|
||||
transform: scale(theme(scale.100));
|
||||
transition: transform 0.32s ease-in-out;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.99);
|
||||
transition: transform 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
&:active:after {
|
||||
background: var(--clr-bg-inv-acc-3);
|
||||
transform: scale(theme(scale.100));
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__list__link {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: block;
|
||||
padding: theme(padding.2) theme(padding.3);
|
||||
}
|
||||
|
||||
.sidebar__list__link.active {
|
||||
&:after {
|
||||
background: var(--clr-bg-inv-acc-3);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__list__content {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.sidebar__profile {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: theme(width.8);
|
||||
height: theme(height.8);
|
||||
|
||||
background: var(--clr-bg-inv-4);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sidebar__profile--flyout {
|
||||
background: var(--clr-bg-def-2);
|
||||
}
|
||||
|
||||
.sidebar__profile--flyout > .sidebar__profile__character {
|
||||
color: var(--clr-fg-def-1) !important;
|
||||
}
|
||||
32
pkgs/clan-app/ui-2d/src/components/Sidebar/css/sidebar.css
Normal file
32
pkgs/clan-app/ui-2d/src/components/Sidebar/css/sidebar.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/* Sidebar Elements */
|
||||
|
||||
@import "./sidebar-header";
|
||||
@import "./sidebar-flyout";
|
||||
@import "./sidebar-list-item";
|
||||
@import "./sidebar-profile";
|
||||
|
||||
/* Sidebar Structure */
|
||||
|
||||
.sidebar {
|
||||
@apply bg-inv-2 h-full border border-solid border-inv-2 min-w-72 rounded-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: theme(padding.2);
|
||||
padding: theme(padding.4) theme(padding.2);
|
||||
}
|
||||
|
||||
.sidebar__section {
|
||||
@apply bg-primary-800/90;
|
||||
|
||||
padding: theme(padding.2);
|
||||
border-radius: theme(borderRadius.md);
|
||||
|
||||
::marker {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
85
pkgs/clan-app/ui-2d/src/components/Sidebar/index.tsx
Normal file
85
pkgs/clan-app/ui-2d/src/components/Sidebar/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { For, type JSX, Show } from "solid-js";
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { AppRoute, routes } from "@/src";
|
||||
import { SidebarHeader } from "./SidebarHeader";
|
||||
import { SidebarListItem } from "./SidebarListItem";
|
||||
import { Typography } from "../Typography";
|
||||
import "./css/sidebar.css";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
import { clanMetaQuery } from "@/src/queries/clan-meta";
|
||||
|
||||
export const SidebarSection = (props: {
|
||||
title: string;
|
||||
icon: IconVariant;
|
||||
children: JSX.Element;
|
||||
}) => {
|
||||
const { title, children } = props;
|
||||
|
||||
return (
|
||||
<details class="sidebar__section accordeon" open>
|
||||
<summary style="display: contents;">
|
||||
<div class="accordeon__header">
|
||||
<Typography
|
||||
class="inline-flex w-full gap-2 uppercase !tracking-wider"
|
||||
tag="p"
|
||||
hierarchy="body"
|
||||
size="xxs"
|
||||
weight="normal"
|
||||
color="tertiary"
|
||||
inverted={true}
|
||||
>
|
||||
<Icon class="opacity-90" icon={props.icon} size={13} />
|
||||
{title}
|
||||
<Icon icon="CaretDown" class="ml-auto" size={10} />
|
||||
</Typography>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="accordeon__body">{children}</div>
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar = (props: RouteSectionProps) => {
|
||||
const query = clanMetaQuery();
|
||||
|
||||
return (
|
||||
<div class="sidebar">
|
||||
<Show
|
||||
when={query.data}
|
||||
fallback={<SidebarHeader clanName={"Untitled"} />}
|
||||
>
|
||||
{(meta) => <SidebarHeader clanName={meta().name} />}
|
||||
</Show>
|
||||
<div class="sidebar__body max-h-[calc(100vh-4rem)] overflow-scroll">
|
||||
<For each={routes.filter((r) => !r.hidden)}>
|
||||
{(route: AppRoute) => (
|
||||
<Show
|
||||
when={route.children}
|
||||
fallback={
|
||||
<SidebarListItem href={route.path} title={route.label} />
|
||||
}
|
||||
>
|
||||
{(children) => (
|
||||
<SidebarSection
|
||||
title={route.label}
|
||||
icon={route.icon || "Paperclip"}
|
||||
>
|
||||
<ul class="flex flex-col gap-y-0.5">
|
||||
<For each={children().filter((r) => !r.hidden)}>
|
||||
{(child) => (
|
||||
<SidebarListItem
|
||||
href={`${route.path}${child.path}`}
|
||||
title={child.label}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</SidebarSection>
|
||||
)}
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
pkgs/clan-app/ui-2d/src/components/TagList/TagList.css
Normal file
7
pkgs/clan-app/ui-2d/src/components/TagList/TagList.css
Normal file
@@ -0,0 +1,7 @@
|
||||
div.tag-list {
|
||||
@apply flex flex-wrap gap-2;
|
||||
|
||||
span.tag {
|
||||
@apply w-fit rounded-full px-3 py-2 bg-inv-4 fg-inv-1;
|
||||
}
|
||||
}
|
||||
21
pkgs/clan-app/ui-2d/src/components/TagList/TagList.tsx
Normal file
21
pkgs/clan-app/ui-2d/src/components/TagList/TagList.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Component, For } from "solid-js";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import "./TagList.css";
|
||||
|
||||
export interface TagListProps {
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export const TagList: Component<TagListProps> = (props) => {
|
||||
return (
|
||||
<div class="tag-list">
|
||||
<For each={props.values}>
|
||||
{(tag) => (
|
||||
<Typography hierarchy="label" size="s" inverted={true} class="tag">
|
||||
{tag}
|
||||
</Typography>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
.fnt-clr-primary {
|
||||
color: var(--clr-fg-def-1);
|
||||
}
|
||||
|
||||
.fnt-clr-secondary {
|
||||
color: var(--clr-fg-def-2);
|
||||
}
|
||||
|
||||
.fnt-clr-tertiary {
|
||||
color: var(--clr-fg-def-3);
|
||||
}
|
||||
|
||||
.fnt-clr-primary.fnt-clr--inverted {
|
||||
color: var(--clr-fg-inv-1);
|
||||
}
|
||||
|
||||
.fnt-clr-secondary.fnt-clr--inverted {
|
||||
color: var(--clr-fg-inv-2);
|
||||
}
|
||||
|
||||
.fnt-clr-tertiary.fnt-clr--inverted {
|
||||
color: var(--clr-fg-inv-3);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@import "./typography-label.css";
|
||||
@import "./typography-body.css";
|
||||
@import "./typography-title.css";
|
||||
@import "./typography-headline.css";
|
||||
@@ -0,0 +1,23 @@
|
||||
.fnt-body-default {
|
||||
font-size: 1rem;
|
||||
line-height: 132%;
|
||||
letter-spacing: 3%;
|
||||
}
|
||||
|
||||
.fnt-body-s {
|
||||
font-size: 0.925rem;
|
||||
line-height: 132%;
|
||||
letter-spacing: 3%;
|
||||
}
|
||||
|
||||
.fnt-body-xs {
|
||||
font-size: 0.875rem;
|
||||
line-height: 132%;
|
||||
letter-spacing: 3%;
|
||||
}
|
||||
|
||||
.fnt-body-xxs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 132%;
|
||||
letter-spacing: 0.00688rem;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.fnt-headline-default {
|
||||
font-size: 1.5rem;
|
||||
line-height: 116%;
|
||||
letter-spacing: 1%;
|
||||
}
|
||||
|
||||
.fnt-headline-m {
|
||||
font-size: 1.75rem;
|
||||
line-height: 116%;
|
||||
letter-spacing: 1%;
|
||||
}
|
||||
|
||||
.fnt-headline-l {
|
||||
font-size: 2rem;
|
||||
line-height: 116%;
|
||||
letter-spacing: 1%;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.fnt-label-default {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.fnt-label-s {
|
||||
font-size: 0.75rem;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.fnt-label-xs {
|
||||
font-size: 0.6875rem;
|
||||
line-height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.fnt-title-default {
|
||||
font-size: 1.125rem;
|
||||
line-height: 124%;
|
||||
letter-spacing: 3%;
|
||||
}
|
||||
|
||||
.fnt-title-m {
|
||||
font-size: 1.25rem;
|
||||
line-height: 124%;
|
||||
letter-spacing: 3%;
|
||||
}
|
||||
|
||||
.fnt-title-l {
|
||||
font-size: 1.375rem;
|
||||
line-height: 124%;
|
||||
letter-spacing: 3%;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@import "./typography-hierarchy/";
|
||||
@import "./typography-color.css";
|
||||
|
||||
.fnt-weight-normal {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.fnt-weight-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fnt-weight-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fnt-weight-normal.fnt-clr--inverted {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.fnt-weight-medium.fnt-clr--inverted {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.fnt-weight-bold.fnt-clr--inverted {
|
||||
font-weight: 700;
|
||||
}
|
||||
109
pkgs/clan-app/ui-2d/src/components/Typography/index.tsx
Normal file
109
pkgs/clan-app/ui-2d/src/components/Typography/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { type JSX } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import cx from "classnames";
|
||||
import "./css/typography.css";
|
||||
|
||||
export type Hierarchy = "body" | "title" | "headline" | "label";
|
||||
type Color = "primary" | "secondary" | "tertiary";
|
||||
type Weight = "normal" | "medium" | "bold";
|
||||
type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
|
||||
|
||||
const colorMap: Record<Color, string> = {
|
||||
primary: cx("fnt-clr-primary"),
|
||||
secondary: cx("fnt-clr-secondary"),
|
||||
tertiary: cx("fnt-clr-tertiary"),
|
||||
};
|
||||
|
||||
// type Size = "default" | "xs" | "s" | "m" | "l";
|
||||
interface SizeForHierarchy {
|
||||
label: {
|
||||
default: string;
|
||||
xs: string;
|
||||
s: string;
|
||||
};
|
||||
body: {
|
||||
default: string;
|
||||
xs: string;
|
||||
xxs: string;
|
||||
s: string;
|
||||
};
|
||||
headline: {
|
||||
default: string;
|
||||
m: string;
|
||||
l: string;
|
||||
};
|
||||
title: {
|
||||
default: string;
|
||||
m: string;
|
||||
l: string;
|
||||
};
|
||||
}
|
||||
|
||||
type AllowedSizes<H extends Hierarchy> = keyof SizeForHierarchy[H];
|
||||
|
||||
const sizeHierarchyMap: SizeForHierarchy = {
|
||||
body: {
|
||||
default: cx("fnt-body-default"),
|
||||
s: cx("fnt-body-s"),
|
||||
xs: cx("fnt-body-xs"),
|
||||
xxs: cx("fnt-body-xxs"),
|
||||
},
|
||||
headline: {
|
||||
default: cx("fnt-headline-default"),
|
||||
// xs: cx("fnt-headline-xs"),
|
||||
// s: cx("fnt-headline-s"),
|
||||
m: cx("fnt-headline-m"),
|
||||
l: cx("fnt-headline-l"),
|
||||
},
|
||||
title: {
|
||||
default: cx("fnt-title-default"),
|
||||
// xs: cx("fnt-title-xs"),
|
||||
// s: cx("fnt-title-s"),
|
||||
m: cx("fnt-title-m"),
|
||||
l: cx("fnt-title-l"),
|
||||
},
|
||||
label: {
|
||||
default: cx("fnt-label-default"),
|
||||
s: cx("fnt-label-s"),
|
||||
xs: cx("fnt-label-xs"),
|
||||
},
|
||||
};
|
||||
|
||||
const weightMap: Record<Weight, string> = {
|
||||
normal: cx("fnt-weight-normal"),
|
||||
medium: cx("fnt-weight-medium"),
|
||||
bold: cx("fnt-weight-bold"),
|
||||
};
|
||||
|
||||
interface _TypographyProps<H extends Hierarchy> {
|
||||
hierarchy: H;
|
||||
size: AllowedSizes<H>;
|
||||
children: JSX.Element;
|
||||
weight?: Weight;
|
||||
color?: Color | "inherit";
|
||||
inverted?: boolean;
|
||||
tag?: Tag;
|
||||
class?: string;
|
||||
classList?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||
return (
|
||||
<Dynamic
|
||||
component={props.tag || "span"}
|
||||
class={cx(
|
||||
props.color === "inherit" && "text-inherit",
|
||||
props.color !== "inherit" && colorMap[props.color || "primary"],
|
||||
props.inverted && "fnt-clr--inverted",
|
||||
sizeHierarchyMap[props.hierarchy][props.size] as string,
|
||||
weightMap[props.weight || "normal"],
|
||||
props.class,
|
||||
)}
|
||||
classList={props.classList}
|
||||
>
|
||||
{props.children}
|
||||
</Dynamic>
|
||||
);
|
||||
};
|
||||
|
||||
export type TypographyProps = _TypographyProps<Hierarchy>;
|
||||
10
pkgs/clan-app/ui-2d/src/components/accordion/accordion.css
Normal file
10
pkgs/clan-app/ui-2d/src/components/accordion/accordion.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.accordion {
|
||||
@apply flex flex-col gap-y-5;
|
||||
}
|
||||
|
||||
.accordion__title {
|
||||
@apply flex h-5 cursor-pointer items-center justify-end gap-x-0.5 px-1 font-medium;
|
||||
}
|
||||
|
||||
.accordion__body {
|
||||
}
|
||||
45
pkgs/clan-app/ui-2d/src/components/accordion/index.tsx
Normal file
45
pkgs/clan-app/ui-2d/src/components/accordion/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createSignal, JSX, Show } from "solid-js";
|
||||
import Icon from "../icon";
|
||||
import { Button } from "../Button/Button";
|
||||
import cx from "classnames";
|
||||
import "./accordion.css";
|
||||
|
||||
interface AccordionProps {
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
class?: string;
|
||||
initiallyOpen?: boolean;
|
||||
}
|
||||
|
||||
export default function Accordion(props: AccordionProps) {
|
||||
const [isOpen, setIsOpen] = createSignal(props.initiallyOpen ?? false);
|
||||
return (
|
||||
<div class={cx(`accordion`, props.class)} tabindex="0">
|
||||
<div onClick={() => setIsOpen(!isOpen())} class="accordion__title">
|
||||
<Show
|
||||
when={isOpen()}
|
||||
fallback={
|
||||
<Button
|
||||
endIcon={<Icon size={12} icon={"CaretDown"} />}
|
||||
variant="ghost"
|
||||
size="s"
|
||||
>
|
||||
{props.title}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
endIcon={<Icon size={12} icon={"CaretUp"} />}
|
||||
variant="ghost"
|
||||
size="s"
|
||||
>
|
||||
{props.title}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={isOpen()}>
|
||||
<div class="accordion__body">{props.children}</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
pkgs/clan-app/ui-2d/src/components/badge/index.tsx
Normal file
39
pkgs/clan-app/ui-2d/src/components/badge/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
import { Typography } from "../Typography";
|
||||
|
||||
interface BadgeProps {
|
||||
color: keyof typeof colorMap;
|
||||
children: JSX.Element;
|
||||
icon?: IconVariant;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
primary: cx("bg-primary-800 text-primary-100"),
|
||||
secondary: cx("bg-secondary-800 text-secondary-100"),
|
||||
blue: "bg-blue-100 text-blue-800",
|
||||
gray: "bg-gray-100 text-gray-800",
|
||||
green: "bg-green-100 text-green-800",
|
||||
orange: "bg-orange-100 text-orange-800",
|
||||
red: "bg-red-100 text-red-800",
|
||||
yellow: "bg-yellow-100 text-yellow-800",
|
||||
};
|
||||
|
||||
export const Badge = (props: BadgeProps) => {
|
||||
return (
|
||||
<div
|
||||
class={cx(
|
||||
"flex px-4 py-2 rounded-sm justify-center items-center gap-1",
|
||||
colorMap[props.color],
|
||||
props.class,
|
||||
)}
|
||||
>
|
||||
{props.icon && <Icon icon={props.icon} class="size-4" />}
|
||||
<Typography hierarchy="label" size="default" color="inherit">
|
||||
{props.children}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
194
pkgs/clan-app/ui-2d/src/components/fileSelect/index.tsx
Normal file
194
pkgs/clan-app/ui-2d/src/components/fileSelect/index.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { FileInput, type FileInputProps } from "@/src/components/FileInput"; // Assuming FileInput can take a ref and has onClick
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import Fieldset from "@/src/Form/fieldset";
|
||||
import Icon from "@/src/components/icon"; // For displaying file icons
|
||||
import { callApi } from "@/src/api";
|
||||
import type { FieldValues } from "@modular-forms/solid";
|
||||
import { Show, For, type Component, type JSX } from "solid-js";
|
||||
|
||||
// Types for the file dialog options passed to callApi
|
||||
interface FileRequestFilter {
|
||||
patterns: string[];
|
||||
mime_types?: string[];
|
||||
}
|
||||
|
||||
export interface FileDialogOptions {
|
||||
title: string;
|
||||
filters?: FileRequestFilter;
|
||||
initial_folder?: string;
|
||||
}
|
||||
|
||||
// Props for the CustomFileField component
|
||||
interface FileSelectorOpts<TFieldName extends string> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Field: any; // The Field component from createForm
|
||||
name: TFieldName; // Name of the form field (e.g., "sshKeys", "profilePicture")
|
||||
label: string; // Legend for Fieldset or main label for the input
|
||||
description?: string | JSX.Element; // Optional description text
|
||||
multiple?: boolean; // True if multiple files can be selected, false for single file
|
||||
fileDialogOptions: FileDialogOptions; // Configuration for the custom file dialog
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
of: any;
|
||||
// Optional props for styling
|
||||
inputClass?: string;
|
||||
fileListClass?: string;
|
||||
// You can add more specific props like `validate` if you want to pass them to Field
|
||||
}
|
||||
|
||||
export const FileSelectorField: Component<FileSelectorOpts<string>> = (
|
||||
props,
|
||||
) => {
|
||||
const {
|
||||
Field,
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
multiple = false,
|
||||
fileDialogOptions,
|
||||
inputClass,
|
||||
fileListClass,
|
||||
} = props;
|
||||
|
||||
// Ref to the underlying HTMLInputElement (assuming FileInput forwards refs or is simple)
|
||||
let actualInputElement: HTMLInputElement | undefined;
|
||||
|
||||
const openAndSetFiles = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (!actualInputElement) {
|
||||
console.error(
|
||||
"CustomFileField: Input element ref is not set. Cannot proceed.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataTransfer = new DataTransfer();
|
||||
const mode = multiple ? "open_multiple_files" : "open_file";
|
||||
|
||||
try {
|
||||
const response = await callApi("open_file", {
|
||||
file_request: {
|
||||
title: fileDialogOptions.title,
|
||||
mode: mode,
|
||||
filters: fileDialogOptions.filters,
|
||||
initial_folder: fileDialogOptions.initial_folder,
|
||||
},
|
||||
}).promise;
|
||||
|
||||
if (
|
||||
response.status === "success" &&
|
||||
response.data &&
|
||||
Array.isArray(response.data)
|
||||
) {
|
||||
(response.data as string[]).forEach((filename) => {
|
||||
// Create File objects. Content is empty as we only have paths.
|
||||
// Type might be generic or derived if possible.
|
||||
dataTransfer.items.add(
|
||||
new File([], filename, { type: "application/octet-stream" }),
|
||||
);
|
||||
});
|
||||
} else if (response.status === "error") {
|
||||
// Consider using a toast or other user notification for API errors
|
||||
console.error("Error from open_file API:", response.errors);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to call open_file API:", error);
|
||||
// Consider using a toast here
|
||||
}
|
||||
|
||||
// Set the FileList on the actual input element
|
||||
Object.defineProperty(actualInputElement, "files", {
|
||||
value: dataTransfer.files,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Dispatch an 'input' event so modular-forms updates its state
|
||||
const inputEvent = new Event("input", { bubbles: true, cancelable: true });
|
||||
actualInputElement.dispatchEvent(inputEvent);
|
||||
|
||||
// Optionally, dispatch 'change' if your forms setup relies more on it
|
||||
// const changeEvent = new Event("change", { bubbles: true, cancelable: true });
|
||||
// actualInputElement.dispatchEvent(changeEvent);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fieldset legend={label}>
|
||||
{description &&
|
||||
(typeof description === "string" ? (
|
||||
<Typography hierarchy="body" size="s" weight="medium" class="mb-2">
|
||||
{description}
|
||||
</Typography>
|
||||
) : (
|
||||
description
|
||||
))}
|
||||
|
||||
<Field name={name} type={multiple ? "File[]" : "File"}>
|
||||
{(
|
||||
field: { value: File | File[]; error?: string },
|
||||
fieldProps: Record<string, unknown>,
|
||||
) => (
|
||||
<>
|
||||
{/*
|
||||
This FileInput component should be clickable.
|
||||
Its 'ref' needs to point to the actual <input type="file"> element.
|
||||
If FileInput is complex, it might need an 'inputRef' prop or similar.
|
||||
*/}
|
||||
<FileInput
|
||||
{...(fieldProps as unknown as FileInputProps)} // Spread modular-forms props
|
||||
ref={(el: HTMLInputElement) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fieldProps as any).ref(el); // Pass ref to modular-forms
|
||||
actualInputElement = el; // Capture for local use
|
||||
}}
|
||||
class={inputClass}
|
||||
multiple={multiple}
|
||||
// The onClick here triggers our custom dialog logic
|
||||
onClick={openAndSetFiles}
|
||||
// The 'value' prop for a file input is not for displaying selected files directly.
|
||||
// We'll display them below. FileInput might show placeholder text.
|
||||
// value={undefined} // Explicitly not setting value from field.value here
|
||||
error={field.error} // Display error from modular-forms
|
||||
/>
|
||||
{field.error && (
|
||||
<Typography hierarchy="body" size="xs" class="mt-1">
|
||||
{field.error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Display the list of selected files */}
|
||||
<Show
|
||||
when={
|
||||
field.value &&
|
||||
(multiple
|
||||
? (field.value as File[]).length > 0
|
||||
: field.value instanceof File)
|
||||
}
|
||||
>
|
||||
<div class={`mt-2 space-y-1 ${fileListClass || ""}`}>
|
||||
<For
|
||||
each={
|
||||
multiple
|
||||
? (field.value as File[])
|
||||
: field.value instanceof File
|
||||
? [field.value as File]
|
||||
: []
|
||||
}
|
||||
>
|
||||
{(file) => (
|
||||
<div class="flex items-center justify-between rounded border p-2 text-sm border-def-1">
|
||||
<span class="truncate" title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
{/* A remove button per file is complex with FileList & modular-forms.
|
||||
For now, clearing all files is simpler (e.g., via FileInput's own clear).
|
||||
Or, the user re-selects files to change the selection. */}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
60
pkgs/clan-app/ui-2d/src/components/group/index.tsx
Normal file
60
pkgs/clan-app/ui-2d/src/components/group/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import cx from "classnames";
|
||||
import { JSX } from "solid-js";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
|
||||
interface GroupProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const Group = (props: GroupProps) => (
|
||||
<div class="flex flex-col gap-8 rounded-md border px-4 py-5 bg-def-2 border-def-2">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export type SectionVariant = "attention" | "danger";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
variant: SectionVariant;
|
||||
headline: JSX.Element;
|
||||
}
|
||||
const variantColorsMap: Record<SectionVariant, string> = {
|
||||
attention: cx("bg-[#9BD8F2] fg-def-1"),
|
||||
danger: cx("bg-semantic-2 fg-semantic-2"),
|
||||
};
|
||||
|
||||
const variantIconColorsMap: Record<SectionVariant, string> = {
|
||||
attention: cx("fg-def-1"),
|
||||
danger: cx("fg-semantic-3"),
|
||||
};
|
||||
|
||||
const variantIconMap: Record<SectionVariant, IconVariant> = {
|
||||
attention: "Attention",
|
||||
danger: "Warning",
|
||||
};
|
||||
|
||||
// SectionHeader component
|
||||
export const SectionHeader = (props: SectionHeaderProps) => (
|
||||
<div
|
||||
class={cx(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2",
|
||||
variantColorsMap[props.variant],
|
||||
)}
|
||||
>
|
||||
{
|
||||
<Icon
|
||||
icon={variantIconMap[props.variant]}
|
||||
class={cx("size-5", variantIconColorsMap[props.variant])}
|
||||
/>
|
||||
}
|
||||
{props.headline}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Section component
|
||||
interface SectionProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
export const Section = (props: SectionProps) => (
|
||||
<div class="flex flex-col gap-3">{props.children}</div>
|
||||
);
|
||||
99
pkgs/clan-app/ui-2d/src/components/icon/index.tsx
Normal file
99
pkgs/clan-app/ui-2d/src/components/icon/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
import ArrowBottom from "@/icons/arrow-bottom.svg";
|
||||
import ArrowLeft from "@/icons/arrow-left.svg";
|
||||
import ArrowRight from "@/icons/arrow-right.svg";
|
||||
import ArrowTop from "@/icons/arrow-top.svg";
|
||||
import Attention from "@/icons/attention.svg";
|
||||
import CaretDown from "@/icons/caret-down.svg";
|
||||
import CaretLeft from "@/icons/caret-left.svg";
|
||||
import CaretRight from "@/icons/caret-right.svg";
|
||||
import CaretUp from "@/icons/caret-up.svg";
|
||||
import Checkmark from "@/icons/checkmark.svg";
|
||||
import ClanIcon from "@/icons/clan-icon.svg";
|
||||
import ClanLogo from "@/icons/clan-logo.svg";
|
||||
import Close from "@/icons/close.svg";
|
||||
import Download from "@/icons/download.svg";
|
||||
import Edit from "@/icons/edit.svg";
|
||||
import Expand from "@/icons/expand.svg";
|
||||
import EyeClose from "@/icons/eye-close.svg";
|
||||
import EyeOpen from "@/icons/eye-open.svg";
|
||||
import Filter from "@/icons/filter.svg";
|
||||
import Flash from "@/icons/flash.svg";
|
||||
import Folder from "@/icons/folder.svg";
|
||||
import Grid from "@/icons/grid.svg";
|
||||
import Info from "@/icons/info.svg";
|
||||
import List from "@/icons/list.svg";
|
||||
import Load from "@/icons/load.svg";
|
||||
import More from "@/icons/more.svg";
|
||||
import Paperclip from "@/icons/paperclip.svg";
|
||||
import Plus from "@/icons/plus.svg";
|
||||
import Reload from "@/icons/reload.svg";
|
||||
import Report from "@/icons/report.svg";
|
||||
import Search from "@/icons/search.svg";
|
||||
import Settings from "@/icons/settings.svg";
|
||||
import Trash from "@/icons/trash.svg";
|
||||
import Update from "@/icons/update.svg";
|
||||
import Warning from "@/icons/warning-filled.svg";
|
||||
|
||||
const icons = {
|
||||
ArrowBottom,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowTop,
|
||||
Attention,
|
||||
CaretDown,
|
||||
CaretLeft,
|
||||
CaretRight,
|
||||
CaretUp,
|
||||
Checkmark,
|
||||
ClanIcon,
|
||||
ClanLogo,
|
||||
Close,
|
||||
Download,
|
||||
Edit,
|
||||
Expand,
|
||||
EyeClose,
|
||||
EyeOpen,
|
||||
Filter,
|
||||
Flash,
|
||||
Folder,
|
||||
Grid,
|
||||
Info,
|
||||
List,
|
||||
Load,
|
||||
More,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Reload,
|
||||
Report,
|
||||
Search,
|
||||
Settings,
|
||||
Trash,
|
||||
Update,
|
||||
Warning,
|
||||
};
|
||||
|
||||
export type IconVariant = keyof typeof icons;
|
||||
|
||||
interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
|
||||
icon: IconVariant;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const Icon: Component<IconProps> = (props) => {
|
||||
const [local, iconProps] = splitProps(props, ["icon"]);
|
||||
|
||||
const IconComponent = icons[local.icon];
|
||||
return IconComponent ? (
|
||||
<IconComponent
|
||||
width={iconProps.size || 16}
|
||||
height={iconProps.size || 16}
|
||||
viewBox="0 0 48 48"
|
||||
// @ts-expect-error: dont know, fix this type nit later
|
||||
ref={iconProps.ref}
|
||||
{...iconProps}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
194
pkgs/clan-app/ui-2d/src/components/inputBase/index.tsx
Normal file
194
pkgs/clan-app/ui-2d/src/components/inputBase/index.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import cx from "classnames";
|
||||
import { JSX, Ref, Show, splitProps } from "solid-js";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
import { Typography, TypographyProps } from "../Typography";
|
||||
|
||||
export type InputVariant = "outlined" | "ghost";
|
||||
interface InputBaseProps {
|
||||
variant?: InputVariant;
|
||||
value?: string;
|
||||
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||
required?: boolean;
|
||||
type?: string;
|
||||
inlineLabel?: JSX.Element;
|
||||
class?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
error?: boolean;
|
||||
icon?: IconVariant;
|
||||
/** Overrides the input element */
|
||||
inputElem?: JSX.Element;
|
||||
divRef?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const variantBorder: Record<InputVariant, string> = {
|
||||
outlined: "border border-inv-3",
|
||||
ghost: "",
|
||||
};
|
||||
|
||||
const fgStateClasses = cx("aria-disabled:fg-def-4 aria-readonly:fg-def-3");
|
||||
|
||||
export const InputBase = (props: InputBaseProps) => {
|
||||
const [internal, inputProps] = splitProps(props, ["class", "divRef"]);
|
||||
return (
|
||||
<div
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
class={cx(
|
||||
// Layout
|
||||
"flex px-2 py-[0.375rem] flex-shrink-0 items-center justify-center gap-2 text-sm leading-6",
|
||||
|
||||
// Background
|
||||
"bg-def-1 hover:bg-def-acc-1",
|
||||
|
||||
// Text
|
||||
"fg-def-1",
|
||||
fgStateClasses,
|
||||
// Border
|
||||
variantBorder[props.variant || "outlined"],
|
||||
"rounded-sm",
|
||||
"hover:border-inv-4",
|
||||
"aria-disabled:border-def-2 aria-disabled:border",
|
||||
// Outline
|
||||
"outline-offset-1 outline-1",
|
||||
"active:outline active:outline-inv-3",
|
||||
"focus-visible:outline-double focus-visible:outline-int-1",
|
||||
|
||||
// Cursor
|
||||
"aria-readonly:cursor-no-drop",
|
||||
props.class,
|
||||
)}
|
||||
classList={{
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
[cx("!border !border-semantic-1 !outline-semantic-1")]: !!props.error,
|
||||
}}
|
||||
aria-invalid={props.error}
|
||||
aria-disabled={props.disabled}
|
||||
aria-readonly={props.readonly}
|
||||
tabIndex={0}
|
||||
role="textbox"
|
||||
ref={internal.divRef}
|
||||
>
|
||||
{props.icon && (
|
||||
<i
|
||||
class={cx("inline-flex fg-def-2", fgStateClasses)}
|
||||
aria-invalid={props.error}
|
||||
aria-disabled={props.disabled}
|
||||
aria-readonly={props.readonly}
|
||||
>
|
||||
<Icon icon={props.icon} font-size="inherit" color="inherit" />
|
||||
</i>
|
||||
)}
|
||||
<Show when={!props.inputElem} fallback={props.inputElem}>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
class="w-full bg-transparent outline-none"
|
||||
value={props.value}
|
||||
type={props.type ? props.type : "text"}
|
||||
readOnly={props.readonly}
|
||||
placeholder={`${props.placeholder || ""}`}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
aria-invalid={props.error}
|
||||
aria-disabled={props.disabled}
|
||||
aria-readonly={props.readonly}
|
||||
{...inputProps}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface InputLabelProps
|
||||
extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
help?: string;
|
||||
labelAction?: JSX.Element;
|
||||
}
|
||||
export const InputLabel = (props: InputLabelProps) => {
|
||||
const [labelProps, forwardProps] = splitProps(props, [
|
||||
"class",
|
||||
"labelAction",
|
||||
]);
|
||||
return (
|
||||
<label
|
||||
class={cx("flex items-center gap-1", labelProps.class)}
|
||||
{...forwardProps}
|
||||
>
|
||||
<span class="flex flex-col justify-center">
|
||||
<span>
|
||||
<Typography
|
||||
hierarchy="label"
|
||||
size="default"
|
||||
weight="bold"
|
||||
class="inline-flex gap-1 align-middle !fg-def-1"
|
||||
classList={{
|
||||
[cx("!fg-semantic-1")]: !!props.error,
|
||||
}}
|
||||
aria-invalid={props.error}
|
||||
>
|
||||
{props.children}
|
||||
</Typography>
|
||||
{props.required && (
|
||||
<Typography
|
||||
class="inline-flex px-1 align-text-top leading-[0.5] fg-def-4"
|
||||
color="inherit"
|
||||
hierarchy="label"
|
||||
weight="bold"
|
||||
size="xs"
|
||||
>
|
||||
<>*</>
|
||||
</Typography>
|
||||
)}
|
||||
{props.help && (
|
||||
<span
|
||||
class=" inline px-2"
|
||||
data-tip={props.help}
|
||||
style={{
|
||||
"--tooltip-color": "#EFFFFF",
|
||||
"--tooltip-text-color": "#0D1416",
|
||||
"--tooltip-tail": "0.8125rem",
|
||||
}}
|
||||
>
|
||||
<Icon class="inline fg-def-3" icon={"Info"} width={"0.8125rem"} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="normal"
|
||||
color="secondary"
|
||||
>
|
||||
{props.description}
|
||||
</Typography>
|
||||
</span>
|
||||
{props.labelAction}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
interface InputErrorProps {
|
||||
error: string;
|
||||
typographyProps?: TypographyProps;
|
||||
}
|
||||
export const InputError = (props: InputErrorProps) => {
|
||||
const [typoClasses, rest] = splitProps(
|
||||
props.typographyProps || { class: "" },
|
||||
["class"],
|
||||
);
|
||||
return (
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
// @ts-expect-error: Dependent type is to complex to check how it is coupled to the override for now
|
||||
size="xxs"
|
||||
weight="medium"
|
||||
class={cx("col-span-full px-1 !fg-semantic-4", typoClasses)}
|
||||
{...rest}
|
||||
>
|
||||
{props.error}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
.machine-item {
|
||||
@apply col-span-1 flex flex-col items-center;
|
||||
|
||||
position: relative;
|
||||
padding: theme(padding.2);
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.machine-item__thumb-wrapper {
|
||||
position: relative;
|
||||
padding: theme(padding.4);
|
||||
|
||||
border-radius: theme(borderRadius.md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.machine-item__thumb {
|
||||
@apply rounded-md bg-secondary-100 border border-secondary-200;
|
||||
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
overflow: hidden;
|
||||
|
||||
transition: transform 0.24s ease-in-out;
|
||||
}
|
||||
|
||||
.machine-item__header {
|
||||
@apply flex flex-col justify-center items-center;
|
||||
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
|
||||
transition: transform 0.18s 0.04s ease-in-out;
|
||||
}
|
||||
|
||||
.machine-item__pseudo {
|
||||
@apply bg-secondary-50;
|
||||
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid theme(borderColor.secondary.100);
|
||||
border-radius: theme(borderRadius.md);
|
||||
|
||||
transition:
|
||||
transform 0.16s ease-in-out,
|
||||
opacity 0.08s ease-in-out;
|
||||
}
|
||||
|
||||
.machine-item:hover {
|
||||
& .machine-item__pseudo {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& .machine-item__thumb {
|
||||
transform: scale(1.02);
|
||||
box-shadow:
|
||||
0 4px 6px rgba(0, 0, 0, 0.1),
|
||||
0 8px 20px rgba(0, 0, 0, 0.15),
|
||||
0 12px 40px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
& .machine-item__header {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.machine-item:not(:hover) .machine-item__pseudo {
|
||||
transform: scale(0.94);
|
||||
opacity: 0;
|
||||
}
|
||||
180
pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx
Normal file
180
pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { createSignal, Setter } from "solid-js";
|
||||
import { callApi, SuccessQuery } from "../../api";
|
||||
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { RndThumbnail } from "../noiseThumbnail";
|
||||
|
||||
import { Filter } from "../../routes/machines";
|
||||
import { Typography } from "../Typography";
|
||||
import "./css/index.css";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
type MachineDetails = SuccessQuery<"list_machines">["data"][string];
|
||||
|
||||
interface MachineListItemProps {
|
||||
name: string;
|
||||
info?: MachineDetails;
|
||||
nixOnly?: boolean;
|
||||
setFilter: Setter<Filter>;
|
||||
}
|
||||
|
||||
export const MachineListItem = (props: MachineListItemProps) => {
|
||||
const { name, info, nixOnly } = props;
|
||||
|
||||
// Bootstrapping
|
||||
const [installing, setInstalling] = createSignal<boolean>(false);
|
||||
|
||||
// Later only updates
|
||||
const [updating, setUpdating] = createSignal<boolean>(false);
|
||||
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (!info?.deploy?.targetHost || installing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const active_clan = activeClanURI();
|
||||
if (!active_clan) {
|
||||
console.error("No active clan selected");
|
||||
return;
|
||||
}
|
||||
if (!info?.deploy?.targetHost) {
|
||||
console.error(
|
||||
"Machine does not have a target host. Specify where the machine should be deployed.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const target_host = await callApi("get_host", {
|
||||
field: "targetHost",
|
||||
flake: { identifier: active_clan },
|
||||
name: name,
|
||||
}).promise;
|
||||
|
||||
if (target_host.status == "error") {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target_host.data === null) {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target_host.data!.data) {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
setInstalling(true);
|
||||
await callApi("install_machine", {
|
||||
opts: {
|
||||
machine: {
|
||||
name: name,
|
||||
flake: {
|
||||
identifier: active_clan,
|
||||
},
|
||||
},
|
||||
no_reboot: true,
|
||||
debug: true,
|
||||
nix_options: [],
|
||||
password: null,
|
||||
},
|
||||
target_host: target_host.data!.data,
|
||||
}).promise.finally(() => setInstalling(false));
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!info?.deploy?.targetHost || installing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const active_clan = activeClanURI();
|
||||
if (!active_clan) {
|
||||
console.error("No active clan selected");
|
||||
return;
|
||||
}
|
||||
if (!info?.deploy.targetHost) {
|
||||
console.error(
|
||||
"Machine does not have a target host. Specify where the machine should be deployed.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setUpdating(true);
|
||||
|
||||
const target_host = await callApi("get_host", {
|
||||
field: "targetHost",
|
||||
flake: { identifier: active_clan },
|
||||
name: name,
|
||||
}).promise;
|
||||
|
||||
if (target_host.status == "error") {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target_host.data === null) {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target_host.data!.data) {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
const build_host = await callApi("get_host", {
|
||||
field: "buildHost",
|
||||
flake: { identifier: active_clan },
|
||||
name: name,
|
||||
}).promise;
|
||||
|
||||
if (build_host.status == "error") {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
if (build_host.data === null) {
|
||||
console.error("No target host found for the machine");
|
||||
return;
|
||||
}
|
||||
|
||||
await callApi("deploy_machine", {
|
||||
machine: {
|
||||
name: name,
|
||||
flake: {
|
||||
identifier: active_clan,
|
||||
},
|
||||
},
|
||||
target_host: target_host.data!.data,
|
||||
build_host: build_host.data?.data || null,
|
||||
}).promise;
|
||||
|
||||
setUpdating(false);
|
||||
};
|
||||
return (
|
||||
<div class="machine-item">
|
||||
<A href={`/machines/${name}`}>
|
||||
<div class="machine-item__thumb-wrapper">
|
||||
<div class="machine-item__thumb">
|
||||
<RndThumbnail name={name} width={100} height={100} />
|
||||
</div>
|
||||
<div class="machine-item__pseudo" />
|
||||
</div>
|
||||
<header class="machine-item__header">
|
||||
<Typography
|
||||
class="text-center"
|
||||
hierarchy="body"
|
||||
size="s"
|
||||
weight="bold"
|
||||
color="primary"
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
</header>
|
||||
</A>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
134
pkgs/clan-app/ui-2d/src/components/modal/index.tsx
Normal file
134
pkgs/clan-app/ui-2d/src/components/modal/index.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import Dialog from "corvu/dialog";
|
||||
import { createSignal, JSX } from "solid-js";
|
||||
import { Button } from "../Button/Button";
|
||||
import Icon from "../icon";
|
||||
import cx from "classnames";
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean | undefined;
|
||||
handleClose: () => void;
|
||||
title: string;
|
||||
children: JSX.Element;
|
||||
class?: string;
|
||||
}
|
||||
export const Modal = (props: ModalProps) => {
|
||||
const [dragging, setDragging] = createSignal(false);
|
||||
const [startOffset, setStartOffset] = createSignal({ x: 0, y: 0 });
|
||||
|
||||
let dialogRef: HTMLDivElement;
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
setDragging(true);
|
||||
const rect = dialogRef.getBoundingClientRect();
|
||||
setStartOffset({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (dragging()) {
|
||||
let newTop = e.clientY - startOffset().y;
|
||||
let newLeft = e.clientX - startOffset().x;
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop = 0;
|
||||
}
|
||||
if (newLeft < 0) {
|
||||
newLeft = 0;
|
||||
}
|
||||
dialogRef.style.top = `${newTop}px`;
|
||||
dialogRef.style.left = `${newLeft}px`;
|
||||
|
||||
// dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`;
|
||||
// dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => setDragging(false);
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} trapFocus={true}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay
|
||||
class="fixed inset-0 z-50 bg-black/50"
|
||||
onMouseMove={handleMouseMove}
|
||||
/>
|
||||
|
||||
<Dialog.Content
|
||||
class={cx(
|
||||
"overflow-hidden absolute left-1/3 top-1/3 z-50 min-w-[560px] rounded-md border border-def-4 focus-visible:outline-none",
|
||||
props.class,
|
||||
)}
|
||||
classList={{
|
||||
"!cursor-grabbing": dragging(),
|
||||
[cx("scale-[101%] transition-transform")]: dragging(),
|
||||
}}
|
||||
ref={(el) => {
|
||||
dialogRef = el;
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseDown={(e: MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent backdrop drag conflict
|
||||
}}
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()} // Prevent backdrop click closing
|
||||
>
|
||||
<Dialog.Label
|
||||
as="div"
|
||||
class="flex w-full justify-center border-b-2 px-4 py-2 align-middle bg-def-3 border-def-4"
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<div
|
||||
class="flex w-full cursor-move flex-col gap-px py-1 "
|
||||
classList={{
|
||||
"!cursor-grabbing": dragging(),
|
||||
}}
|
||||
>
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
</div>
|
||||
<span class="mx-2 select-none whitespace-nowrap">
|
||||
{props.title}
|
||||
</span>
|
||||
<div
|
||||
class="flex w-full cursor-move flex-col gap-px py-1 "
|
||||
classList={{
|
||||
"!cursor-grabbing": dragging(),
|
||||
}}
|
||||
>
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||
</div>
|
||||
|
||||
<div class="absolute right-1 top-2 pl-1 bg-def-3">
|
||||
<Button
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
tabIndex={-1}
|
||||
class="size-4"
|
||||
variant="ghost"
|
||||
onClick={() => props.handleClose()}
|
||||
size="s"
|
||||
startIcon={<Icon icon={"Close"} />}
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Label>
|
||||
<Dialog.Description
|
||||
class="flex max-h-[90vh] flex-col overflow-y-hidden bg-def-1"
|
||||
as="div"
|
||||
>
|
||||
{props.children}
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
119
pkgs/clan-app/ui-2d/src/components/noiseThumbnail/index.tsx
Normal file
119
pkgs/clan-app/ui-2d/src/components/noiseThumbnail/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { For } from "solid-js";
|
||||
|
||||
function mulberry32(seed: number) {
|
||||
return function () {
|
||||
let t = (seed += 0x6d2b79f5);
|
||||
t = Math.imul(t ^ (t >>> 15), 1 | t);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
function calculateCellColor(
|
||||
x: number,
|
||||
y: number,
|
||||
grid: number[][],
|
||||
rand: () => number,
|
||||
) {
|
||||
const rows = grid.length;
|
||||
const cols = grid[0].length;
|
||||
|
||||
// Get the values of neighboring cells
|
||||
const neighbors = [
|
||||
grid[y][(x - 1 + cols) % cols], // Left
|
||||
grid[y][(x + 1) % cols], // Right
|
||||
grid[(y - 1 + rows) % rows][x], // Top
|
||||
grid[(y + 1) % rows][x], // Bottom
|
||||
];
|
||||
|
||||
const makeValue = (threshold: number, wanted: number) =>
|
||||
rand() > threshold ? Math.floor(rand() * 50) : wanted;
|
||||
|
||||
// Calculate the sum of neighbors
|
||||
const neighborSum = neighbors.reduce((sum, val) => sum + val, 0);
|
||||
// Introduce a hard cutoff for fewer intermediate values
|
||||
if (neighborSum < 1) {
|
||||
// Mostly dark squares
|
||||
// return Math.floor(rand() * 50); // Darker square
|
||||
return makeValue(0.9, Math.floor(rand() * 50));
|
||||
} else if (neighborSum >= 3) {
|
||||
// Mostly bright squares
|
||||
// return Math.floor(200 + rand() * 55); // Bright square
|
||||
return makeValue(0.9, Math.floor(200 + rand() * 55));
|
||||
} else {
|
||||
// Rare intermediate values
|
||||
return makeValue(0.4, Math.floor(100 + rand() * 50));
|
||||
}
|
||||
}
|
||||
|
||||
function generatePatternedImage(seed: number, width = 300, height = 150) {
|
||||
const rand = mulberry32(seed);
|
||||
const rowSize = 1 + Math.floor((rand() * width) / 10);
|
||||
const colSize = 1 + Math.floor((rand() * height) / 10);
|
||||
const cols = Math.floor(width / colSize);
|
||||
const rows = Math.floor(height / rowSize);
|
||||
|
||||
// Initialize a 2D grid with random values (0 or 1)
|
||||
const grid = Array.from({ length: rows }, () =>
|
||||
Array.from({ length: cols }, () => (rand() > 0.5 ? 1 : 0)),
|
||||
);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error("Could not get 2d context");
|
||||
}
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const totalCells = rows * cols;
|
||||
for (let i = 0; i < totalCells; i++) {
|
||||
// Calculate polar coordinates
|
||||
const angle = (i / totalCells) * Math.PI * 2 * rand() * 360; // Increase the spiral's density
|
||||
const radius = Math.sqrt(i) * rand() * 4; // Controls how tightly the spiral is packed
|
||||
|
||||
// Convert polar to Cartesian coordinates
|
||||
const x = Math.floor(centerX + radius * Math.cos(angle));
|
||||
const y = Math.floor(centerY + radius * Math.sin(angle));
|
||||
|
||||
// Find grid coordinates
|
||||
const col = Math.floor(x / colSize);
|
||||
const row = Math.floor(y / rowSize);
|
||||
|
||||
// Ensure the cell is within bounds
|
||||
if (col >= 0 && col < cols && row >= 0 && row < rows) {
|
||||
const colorValue = calculateCellColor(col, row, grid, rand);
|
||||
ctx.fillStyle = `rgb(${colorValue}, ${colorValue}, ${colorValue})`;
|
||||
ctx.fillRect(x, y, colSize, rowSize);
|
||||
}
|
||||
}
|
||||
|
||||
return canvas.toDataURL();
|
||||
}
|
||||
|
||||
interface RndThumbnailProps {
|
||||
name: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
export const RndThumbnail = (props: RndThumbnailProps) => {
|
||||
const seed = () =>
|
||||
Array.from(props.name).reduce((acc, char) => acc + char.charCodeAt(0), 0); // Seed from name
|
||||
const imageSrc = () =>
|
||||
generatePatternedImage(seed(), props.width, props.height);
|
||||
|
||||
return <img src={imageSrc()} alt={props.name} />;
|
||||
};
|
||||
|
||||
export const RndThumbnailShow = () => {
|
||||
const names = ["hsjobeki", "mic92", "lassulus", "D", "A", "D", "B", "C"];
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-4">
|
||||
<For each={names}>{(name) => <RndThumbnail name={name} />}</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
301
pkgs/clan-app/ui-2d/src/components/toast/index.tsx
Normal file
301
pkgs/clan-app/ui-2d/src/components/toast/index.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import { toast, Toast } from "solid-toast"; // Make sure to import Toast type
|
||||
import { Component, JSX, createSignal, onCleanup } from "solid-js";
|
||||
|
||||
// --- Icon Components ---
|
||||
|
||||
const ErrorIcon: Component = () => (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ "margin-right": "10px", "flex-shrink": "0" }}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" fill="#FF4D4F" />
|
||||
<path
|
||||
d="M12 7V13"
|
||||
stroke="white"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="16.5" r="1.5" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const InfoIcon: Component = () => (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ "margin-right": "10px", "flex-shrink": "0" }}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" fill="#2196F3" />
|
||||
<path
|
||||
d="M12 11V17"
|
||||
stroke="white"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="8.5" r="1.5" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const WarningIcon: Component = () => (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ "margin-right": "10px", "flex-shrink": "0" }}
|
||||
>
|
||||
<path d="M12 2L22 21H2L12 2Z" fill="#FFC107" />
|
||||
<path
|
||||
d="M12 9V14"
|
||||
stroke="#424242"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="16.5" r="1" fill="#424242" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// --- Base Props and Styles ---
|
||||
|
||||
export interface BaseToastProps {
|
||||
t: Toast;
|
||||
message: string;
|
||||
onCancel?: () => void; // Optional custom function on X click
|
||||
}
|
||||
|
||||
const baseToastStyle: JSX.CSSProperties = {
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "space-between", // To push X to the right
|
||||
gap: "10px", // Space between content and close button
|
||||
background: "#FFFFFF",
|
||||
color: "#333333",
|
||||
padding: "12px 16px",
|
||||
"border-radius": "6px",
|
||||
"box-shadow": "0 2px 8px rgba(0, 0, 0, 0.12)",
|
||||
"font-family":
|
||||
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
"font-size": "14px",
|
||||
"line-height": "1.4",
|
||||
"min-width": "280px",
|
||||
"max-width": "450px",
|
||||
};
|
||||
|
||||
const closeButtonStyle: JSX.CSSProperties = {
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "red", // As per original example's X button
|
||||
"font-size": "1.5em",
|
||||
"font-weight": "bold",
|
||||
cursor: "pointer",
|
||||
padding: "0 0 0 10px", // Space to its left
|
||||
"line-height": "1",
|
||||
"align-self": "center", // Ensure vertical alignment
|
||||
};
|
||||
|
||||
// --- Toast Component Definitions ---
|
||||
|
||||
// Error Toast
|
||||
export const ErrorToastComponent: Component<BaseToastProps> = (props) => {
|
||||
// Local state for click feedback and exit animation
|
||||
let timeoutId: number | undefined;
|
||||
const [clicked, setClicked] = createSignal(false);
|
||||
const [exiting, setExiting] = createSignal(false);
|
||||
|
||||
const handleToastClick = () => {
|
||||
setClicked(true);
|
||||
setTimeout(() => {
|
||||
setExiting(true);
|
||||
timeoutId = window.setTimeout(() => {
|
||||
toast.dismiss(props.t.id);
|
||||
}, 300); // Match exit animation duration
|
||||
}, 100); // Brief color feedback before animating out
|
||||
};
|
||||
|
||||
// Cleanup timeout if unmounted early
|
||||
onCleanup(() => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...baseToastStyle,
|
||||
cursor: "pointer",
|
||||
transition: "background 0.15s, opacity 0.3s, transform 0.3s",
|
||||
background: clicked()
|
||||
? "#ffeaea"
|
||||
: exiting()
|
||||
? "#fff"
|
||||
: baseToastStyle.background,
|
||||
opacity: exiting() ? 0 : 1,
|
||||
transform: exiting() ? "translateY(-20px)" : "none",
|
||||
}}
|
||||
onClick={handleToastClick}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
|
||||
>
|
||||
<ErrorIcon />
|
||||
<span>{props.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// Info Toast
|
||||
export const CancelToastComponent: Component<BaseToastProps> = (props) => {
|
||||
let timeoutId: number | undefined;
|
||||
const [clicked, setClicked] = createSignal(false);
|
||||
const [exiting, setExiting] = createSignal(false);
|
||||
|
||||
// Cleanup timeout if unmounted early
|
||||
onCleanup(() => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
const handleButtonClick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (props.onCancel) props.onCancel();
|
||||
toast.dismiss(props.t.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...baseToastStyle,
|
||||
cursor: "pointer",
|
||||
transition: "background 0.15s, opacity 0.3s, transform 0.3s",
|
||||
background: clicked()
|
||||
? "#eaf4ff"
|
||||
: exiting()
|
||||
? "#fff"
|
||||
: baseToastStyle.background,
|
||||
opacity: exiting() ? 0 : 1,
|
||||
transform: exiting() ? "translateY(-20px)" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
|
||||
>
|
||||
<InfoIcon />
|
||||
<span>{props.message}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
setClicked(true);
|
||||
handleButtonClick(e);
|
||||
}}
|
||||
style={{
|
||||
...closeButtonStyle,
|
||||
color: "#2196F3",
|
||||
"font-size": "1em",
|
||||
"font-weight": "normal",
|
||||
padding: "4px 12px",
|
||||
border: "1px solid #2196F3",
|
||||
"border-radius": "4px",
|
||||
background: clicked() ? "#bbdefb" : "#eaf4ff",
|
||||
cursor: "pointer",
|
||||
transition: "background 0.15s",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
width: "70px",
|
||||
height: "32px",
|
||||
}}
|
||||
aria-label="Cancel"
|
||||
disabled={clicked()}
|
||||
>
|
||||
{clicked() ? (
|
||||
// Simple spinner SVG
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 50 50"
|
||||
style={{ display: "block" }}
|
||||
>
|
||||
<circle
|
||||
cx="25"
|
||||
cy="25"
|
||||
r="20"
|
||||
fill="none"
|
||||
stroke="#2196F3"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="31.415, 31.415"
|
||||
transform="rotate(72 25 25)"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 25 25"
|
||||
to="360 25 25"
|
||||
dur="0.8s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
) : (
|
||||
"Cancel"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Warning Toast
|
||||
export const WarningToastComponent: Component<BaseToastProps> = (props) => {
|
||||
let timeoutId: number | undefined;
|
||||
const [clicked, setClicked] = createSignal(false);
|
||||
const [exiting, setExiting] = createSignal(false);
|
||||
|
||||
const handleToastClick = () => {
|
||||
setClicked(true);
|
||||
setTimeout(() => {
|
||||
setExiting(true);
|
||||
timeoutId = window.setTimeout(() => {
|
||||
toast.dismiss(props.t.id);
|
||||
}, 300);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Cleanup timeout if unmounted early
|
||||
onCleanup(() => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...baseToastStyle,
|
||||
cursor: "pointer",
|
||||
transition: "background 0.15s, opacity 0.3s, transform 0.3s",
|
||||
background: clicked()
|
||||
? "#fff8e1"
|
||||
: exiting()
|
||||
? "#fff"
|
||||
: baseToastStyle.background,
|
||||
opacity: exiting() ? 0 : 1,
|
||||
transform: exiting() ? "translateY(-20px)" : "none",
|
||||
}}
|
||||
onClick={handleToastClick}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
|
||||
>
|
||||
<WarningIcon />
|
||||
<span>{props.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
134
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.css
Normal file
134
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.css
Normal file
@@ -0,0 +1,134 @@
|
||||
.button {
|
||||
@apply flex gap-2 shrink-0 items-center justify-center;
|
||||
@apply px-4 py-2;
|
||||
|
||||
height: theme(height.9);
|
||||
border-radius: 3px;
|
||||
|
||||
/* Add transition for smooth width animation */
|
||||
transition: width 0.5s ease 0.1s;
|
||||
|
||||
&.s {
|
||||
@apply px-3 py-1.5;
|
||||
height: theme(height.7);
|
||||
border-radius: 2px;
|
||||
|
||||
&:has(> .icon-start):has(> .label) {
|
||||
@apply pl-2;
|
||||
}
|
||||
|
||||
&:has(> .icon-end):has(> .label) {
|
||||
@apply pr-2;
|
||||
}
|
||||
}
|
||||
|
||||
&.primary {
|
||||
@apply bg-inv-acc-4 fg-inv-1;
|
||||
@apply border border-solid border-inv-4;
|
||||
@apply shadow-button-primary;
|
||||
|
||||
&.ghost {
|
||||
@apply bg-transparent border-none shadow-none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-inv-acc-3 border-solid shadow-button-primary-hover;
|
||||
border-color: theme(backgroundColor.secondary.700);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-inv-acc-4 border-solid border-inv-3 shadow-button-primary-active;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@apply bg-inv-acc-4 border-solid border-inv-3 shadow-button-primary-focus;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply bg-def-acc-3 border-solid border-def-3 fg-def-3 shadow-none;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
@apply fg-inv-1;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@apply bg-def-acc-2 fg-def-1;
|
||||
@apply border border-solid border-inv-2;
|
||||
@apply shadow-button-secondary;
|
||||
|
||||
&.ghost {
|
||||
@apply bg-transparent border-none shadow-none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-def-acc-3 border-solid shadow-button-secondary-hover;
|
||||
border-color: theme(backgroundColor.secondary.700);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@apply bg-def-acc-3 border-solid border-inv-3 shadow-button-secondary-focus;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-def-acc-3 border-solid border-inv-4 shadow-button-secondary-active;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply bg-def-2 border-solid border-def-2 fg-def-3 shadow-none;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
@apply fg-def-1;
|
||||
|
||||
&.icon-loading {
|
||||
color: #0051ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.icon {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
&:has(> .icon-start):has(> .label) {
|
||||
@apply pl-3.5;
|
||||
}
|
||||
|
||||
&:has(> .icon-end):has(> .label) {
|
||||
@apply pr-3.5;
|
||||
}
|
||||
|
||||
& > div.loader {
|
||||
@apply w-0 opacity-0;
|
||||
@apply top-0 left-0 -mr-2;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
@apply cursor-wait;
|
||||
|
||||
& > div.loader {
|
||||
@apply w-4 opacity-100;
|
||||
margin-right: revert;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* button group */
|
||||
.button-group .button:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.button-group .button:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.button-group .button:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
43
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.mdx
Normal file
43
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { DocsStory, Meta, Canvas } from "@storybook/blocks";
|
||||
|
||||
import * as ButtonStories from "./Button.stories";
|
||||
|
||||
<Meta of={ButtonStories} />
|
||||
|
||||
# Button
|
||||
|
||||
Buttons have a simple hierarchy, `primary` or `secondary`, and two sizes, `default` and `s`.
|
||||
|
||||
A `Button` can also have a label with a `startIcon`, an `endIcon` or both:
|
||||
|
||||
```tsx
|
||||
<Button hierarchy="primary">Label</>
|
||||
<Button hierarchy="secondary" size="s">Label</>
|
||||
<Button hierarchy="primary" startIcon="Flash">Label</>
|
||||
<Button hierarchy="primary" size="s" endIcon="Flash">Label</>
|
||||
<Button hierarchy="primary" startIcon="Flash" endIcon="Flash">Label</>
|
||||
```
|
||||
|
||||
To create a `Button` which is just an icon:
|
||||
|
||||
```tsx
|
||||
<Button icon="Flash"/>
|
||||
<Button icon="User" size="s"/>
|
||||
```
|
||||
|
||||
<DocsStory of={ButtonStories.Primary} />
|
||||
|
||||
<DocsStory of={ButtonStories.Secondary} />
|
||||
|
||||
## Ghost Mode
|
||||
|
||||
Buttons in ghost mode have all the same states as normal buttons, except for their initial state has no background,
|
||||
outline or box shadow.
|
||||
|
||||
```tsx
|
||||
<Button hierarchy="primary" ghost={true}>Label</>
|
||||
```
|
||||
|
||||
<DocsStory of={ButtonStories.GhostPrimary} />
|
||||
|
||||
<DocsStory of={ButtonStories.GhostSecondary} />
|
||||
251
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.stories.tsx
Normal file
251
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.stories.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Button, ButtonProps } from "./Button";
|
||||
import { Component } from "solid-js";
|
||||
import { expect, fn, userEvent, waitFor, within } from "@storybook/test";
|
||||
|
||||
const getCursorStyle = (el: Element) => window.getComputedStyle(el).cursor;
|
||||
|
||||
const ButtonExamples: Component<ButtonProps> = (props) => (
|
||||
<>
|
||||
<div class="grid w-fit grid-cols-4 gap-8">
|
||||
<div>
|
||||
<Button data-testid="default" {...props}>
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button data-testid="small" size="s" {...props}>
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button data-testid="default-disabled" {...props} disabled={true}>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-disabled"
|
||||
{...props}
|
||||
disabled={true}
|
||||
size="s"
|
||||
>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button data-testid="default-start-icon" {...props} startIcon="Flash">
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
size="s"
|
||||
>
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="default-disabled-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
disabled={true}
|
||||
>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-disabled-start-icon"
|
||||
{...props}
|
||||
startIcon="Flash"
|
||||
size="s"
|
||||
disabled={true}
|
||||
>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button data-testid="default-end-icon" {...props} endIcon="Flash">
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-end-icon"
|
||||
{...props}
|
||||
endIcon="Flash"
|
||||
size="s"
|
||||
>
|
||||
Label
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="default-disabled-end-icon"
|
||||
{...props}
|
||||
endIcon="Flash"
|
||||
disabled={true}
|
||||
>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-disabled-end-icon"
|
||||
{...props}
|
||||
endIcon="Flash"
|
||||
size="s"
|
||||
disabled={true}
|
||||
>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button data-testid="default-icon" {...props} icon="Flash" />
|
||||
</div>
|
||||
<div>
|
||||
<Button data-testid="small-icon" {...props} icon="Flash" size="s" />
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="default-disabled-icon"
|
||||
{...props}
|
||||
icon="Flash"
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="small-disabled-icon"
|
||||
{...props}
|
||||
icon="Flash"
|
||||
disabled={true}
|
||||
size="s"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const meta: Meta<ButtonProps> = {
|
||||
title: "Components/Button",
|
||||
component: ButtonExamples,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ButtonProps>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
hierarchy: "primary",
|
||||
onAction: fn(async () => {
|
||||
// wait 500 ms to simulate an action
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
// randomly fail to check that the loading state still returns to normal
|
||||
if (Math.random() > 0.5) {
|
||||
throw new Error("Action failure");
|
||||
}
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
test: {
|
||||
mockTimers: true,
|
||||
},
|
||||
},
|
||||
play: async ({ args, canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const buttons = await canvas.findAllByRole("button");
|
||||
|
||||
for (const button of buttons) {
|
||||
const testID = button.getAttribute("data-testid");
|
||||
|
||||
// skip disabled buttons
|
||||
if (button.hasAttribute("disabled")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await step(`Click on ${testID}`, async () => {
|
||||
// check for the loader
|
||||
const loaders = button.getElementsByClassName("loader");
|
||||
await expect(loaders.length).toEqual(1);
|
||||
|
||||
// assert its width is 0 before we click
|
||||
const [loader] = loaders;
|
||||
await expect(loader.clientWidth).toEqual(0);
|
||||
|
||||
// move the mouse over the button
|
||||
await userEvent.hover(button);
|
||||
|
||||
// the pointer should be normal
|
||||
await expect(getCursorStyle(button)).toEqual("pointer");
|
||||
|
||||
// click the button
|
||||
await userEvent.click(button);
|
||||
|
||||
// check the button has changed
|
||||
await waitFor(async () => {
|
||||
// the action handler should have been called
|
||||
await expect(args.onAction).toHaveBeenCalled();
|
||||
// the button should have a loading class
|
||||
await expect(button).toHaveClass("loading");
|
||||
// the loader should be visible
|
||||
await expect(loader.clientWidth).toBeGreaterThan(0);
|
||||
// the pointer should have changed to wait
|
||||
await expect(getCursorStyle(button)).toEqual("wait");
|
||||
});
|
||||
|
||||
// wait for the action handler to finish
|
||||
await waitFor(async () => {
|
||||
// the loading class should be removed
|
||||
await expect(button).not.toHaveClass("loading");
|
||||
// the loader should be hidden
|
||||
await expect(loader.clientWidth).toEqual(0);
|
||||
// the pointer should be normal
|
||||
await expect(getCursorStyle(button)).toEqual("pointer");
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
...Primary.args,
|
||||
hierarchy: "secondary",
|
||||
},
|
||||
play: Primary.play,
|
||||
};
|
||||
|
||||
export const GhostPrimary: Story = {
|
||||
args: {
|
||||
...Primary.args,
|
||||
hierarchy: "primary",
|
||||
ghost: true,
|
||||
},
|
||||
play: Primary.play,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div class="p-10 bg-def-3">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const GhostSecondary: Story = {
|
||||
args: {
|
||||
...Primary.args,
|
||||
hierarchy: "secondary",
|
||||
ghost: true,
|
||||
},
|
||||
play: Primary.play,
|
||||
};
|
||||
116
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.tsx
Normal file
116
pkgs/clan-app/ui-2d/src/components/v2/Button/Button.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { splitProps, type JSX, createSignal, Show } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "../Typography/Typography";
|
||||
import { Button as KobalteButton } from "@kobalte/core/button";
|
||||
|
||||
import "./Button.css";
|
||||
import Icon, { IconVariant } from "@/src/components/v2/Icon/Icon";
|
||||
import { Loader } from "@/src/components/v2/Loader/Loader";
|
||||
|
||||
export type Size = "default" | "s";
|
||||
export type Hierarchy = "primary" | "secondary";
|
||||
|
||||
export type Action = () => Promise<void>;
|
||||
|
||||
export interface ButtonProps
|
||||
extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
hierarchy?: Hierarchy;
|
||||
size?: Size;
|
||||
ghost?: boolean;
|
||||
children?: JSX.Element;
|
||||
icon?: IconVariant;
|
||||
startIcon?: IconVariant;
|
||||
endIcon?: IconVariant;
|
||||
class?: string;
|
||||
onAction?: Action;
|
||||
}
|
||||
|
||||
const iconSizes: Record<Size, string> = {
|
||||
default: "1rem",
|
||||
s: "0.8125rem",
|
||||
};
|
||||
|
||||
export const Button = (props: ButtonProps) => {
|
||||
const [local, other] = splitProps(props, [
|
||||
"children",
|
||||
"hierarchy",
|
||||
"size",
|
||||
"ghost",
|
||||
"icon",
|
||||
"startIcon",
|
||||
"endIcon",
|
||||
"class",
|
||||
"onAction",
|
||||
]);
|
||||
|
||||
const size = local.size || "default";
|
||||
const hierarchy = local.hierarchy || "primary";
|
||||
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
const onClick = async () => {
|
||||
if (!local.onAction) {
|
||||
console.error("this should not be possible");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await local.onAction();
|
||||
} catch (error) {
|
||||
console.error("Error while executing action", error);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const iconSize = iconSizes[local.size || "default"];
|
||||
|
||||
return (
|
||||
<KobalteButton
|
||||
class={cx(
|
||||
local.class,
|
||||
"button", // default button class
|
||||
size,
|
||||
hierarchy,
|
||||
{
|
||||
icon: local.icon,
|
||||
loading: loading(),
|
||||
ghost: local.ghost,
|
||||
},
|
||||
)}
|
||||
onClick={local.onAction ? onClick : undefined}
|
||||
{...other}
|
||||
>
|
||||
<Loader hierarchy={hierarchy} />
|
||||
|
||||
{local.startIcon && (
|
||||
<Icon icon={local.startIcon} class="icon-start" size={iconSize} />
|
||||
)}
|
||||
|
||||
{local.icon && !local.children && (
|
||||
<Icon icon={local.icon} class="icon" size={iconSize} />
|
||||
)}
|
||||
|
||||
{local.children && !local.icon && (
|
||||
<Typography
|
||||
class="label"
|
||||
hierarchy="label"
|
||||
family="mono"
|
||||
size={local.size || "default"}
|
||||
color="inherit"
|
||||
inverted={local.hierarchy === "secondary"}
|
||||
weight="bold"
|
||||
tag="span"
|
||||
>
|
||||
{local.children}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{local.endIcon && (
|
||||
<Icon icon={local.endIcon} class="icon-end" size={iconSize} />
|
||||
)}
|
||||
</KobalteButton>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
75
pkgs/clan-app/ui-2d/src/components/v2/Icon/Icon.stories.tsx
Normal file
75
pkgs/clan-app/ui-2d/src/components/v2/Icon/Icon.stories.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Component, For } from "solid-js";
|
||||
import Icon, { IconProps, IconVariant } from "./Icon";
|
||||
|
||||
const iconVariants: IconVariant[] = [
|
||||
"ClanIcon",
|
||||
"Checkmark",
|
||||
"Paperclip",
|
||||
"Expand",
|
||||
"Plus",
|
||||
"Trash",
|
||||
"Folder",
|
||||
"CaretRight",
|
||||
"CaretLeft",
|
||||
"CaretUp",
|
||||
"CaretDown",
|
||||
"Close",
|
||||
"Flash",
|
||||
"EyeClose",
|
||||
"EyeOpen",
|
||||
"Settings",
|
||||
"Grid",
|
||||
"List",
|
||||
"Edit",
|
||||
"Load",
|
||||
"ArrowRight",
|
||||
"ArrowLeft",
|
||||
"ArrowTop",
|
||||
"ArrowBottom",
|
||||
"Info",
|
||||
"Update",
|
||||
"Reload",
|
||||
"Search",
|
||||
"Report",
|
||||
"Cursor",
|
||||
"More",
|
||||
"Filter",
|
||||
"Download",
|
||||
"Attention",
|
||||
"User",
|
||||
"WarningFilled",
|
||||
"Modules",
|
||||
"NewMachine",
|
||||
"AI",
|
||||
"Heart",
|
||||
"SearchFilled",
|
||||
"Offline",
|
||||
"Switch",
|
||||
"Tag",
|
||||
"Machine",
|
||||
];
|
||||
|
||||
const IconExamples: Component<IconProps> = (props) => (
|
||||
<div class="inline-flex flex-wrap gap-2">
|
||||
<For each={iconVariants}>{(item) => <Icon {...props} icon={item} />}</For>
|
||||
</div>
|
||||
);
|
||||
|
||||
const meta: Meta<IconProps> = {
|
||||
title: "Components/Icon",
|
||||
component: IconExamples,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<IconProps>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
width: "2rem",
|
||||
height: "2rem",
|
||||
},
|
||||
};
|
||||
126
pkgs/clan-app/ui-2d/src/components/v2/Icon/Icon.tsx
Normal file
126
pkgs/clan-app/ui-2d/src/components/v2/Icon/Icon.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import cx from "classnames";
|
||||
import { Component, JSX, Show, splitProps } from "solid-js";
|
||||
import ArrowBottom from "@/icons/arrow-bottom.svg";
|
||||
import ArrowLeft from "@/icons/arrow-left.svg";
|
||||
import ArrowRight from "@/icons/arrow-right.svg";
|
||||
import ArrowTop from "@/icons/arrow-top.svg";
|
||||
import Attention from "@/icons/attention.svg";
|
||||
import CaretDown from "@/icons/caret-down.svg";
|
||||
import CaretLeft from "@/icons/caret-left.svg";
|
||||
import CaretRight from "@/icons/caret-right.svg";
|
||||
import CaretUp from "@/icons/caret-up.svg";
|
||||
import Checkmark from "@/icons/checkmark.svg";
|
||||
import ClanIcon from "@/icons/clan-icon.svg";
|
||||
import Cursor from "@/icons/cursor.svg";
|
||||
import Close from "@/icons/close.svg";
|
||||
import Download from "@/icons/download.svg";
|
||||
import Edit from "@/icons/edit.svg";
|
||||
import Expand from "@/icons/expand.svg";
|
||||
import EyeClose from "@/icons/eye-close.svg";
|
||||
import EyeOpen from "@/icons/eye-open.svg";
|
||||
import Filter from "@/icons/filter.svg";
|
||||
import Flash from "@/icons/flash.svg";
|
||||
import Folder from "@/icons/folder.svg";
|
||||
import Grid from "@/icons/grid.svg";
|
||||
import Info from "@/icons/info.svg";
|
||||
import List from "@/icons/list.svg";
|
||||
import Load from "@/icons/load.svg";
|
||||
import More from "@/icons/more.svg";
|
||||
import Paperclip from "@/icons/paperclip.svg";
|
||||
import Plus from "@/icons/plus.svg";
|
||||
import Reload from "@/icons/reload.svg";
|
||||
import Report from "@/icons/report.svg";
|
||||
import Search from "@/icons/search.svg";
|
||||
import Settings from "@/icons/settings.svg";
|
||||
import Trash from "@/icons/trash.svg";
|
||||
import Update from "@/icons/update.svg";
|
||||
import WarningFilled from "@/icons/warning-filled.svg";
|
||||
import Modules from "@/icons/modules.svg";
|
||||
import NewMachine from "@/icons/new-machine.svg";
|
||||
import AI from "@/icons/ai.svg";
|
||||
import User from "@/icons/user.svg";
|
||||
import Heart from "@/icons/heart.svg";
|
||||
import SearchFilled from "@/icons/search-filled.svg";
|
||||
import Offline from "@/icons/offline.svg";
|
||||
import Switch from "@/icons/switch.svg";
|
||||
import Tag from "@/icons/tag.svg";
|
||||
import Machine from "@/icons/machine.svg";
|
||||
import Loader from "@/icons/loader.svg";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
|
||||
const icons = {
|
||||
AI,
|
||||
ArrowBottom,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowTop,
|
||||
Attention,
|
||||
CaretDown,
|
||||
CaretLeft,
|
||||
CaretRight,
|
||||
CaretUp,
|
||||
Checkmark,
|
||||
ClanIcon,
|
||||
Close,
|
||||
Cursor,
|
||||
Download,
|
||||
Edit,
|
||||
Expand,
|
||||
EyeClose,
|
||||
EyeOpen,
|
||||
Filter,
|
||||
Flash,
|
||||
Folder,
|
||||
Grid,
|
||||
Heart,
|
||||
Info,
|
||||
List,
|
||||
Load,
|
||||
Machine,
|
||||
Modules,
|
||||
More,
|
||||
NewMachine,
|
||||
Offline,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Reload,
|
||||
Report,
|
||||
Search,
|
||||
SearchFilled,
|
||||
Settings,
|
||||
Switch,
|
||||
Tag,
|
||||
Trash,
|
||||
Update,
|
||||
User,
|
||||
WarningFilled,
|
||||
};
|
||||
|
||||
export type IconVariant = keyof typeof icons;
|
||||
|
||||
export interface IconProps extends JSX.SvgSVGAttributes<SVGElement> {
|
||||
icon: IconVariant;
|
||||
class?: string;
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
const Icon: Component<IconProps> = (props) => {
|
||||
const [local, iconProps] = splitProps(props, ["icon", "class"]);
|
||||
|
||||
const IconComponent = () => icons[local.icon];
|
||||
|
||||
return IconComponent() ? (
|
||||
<Dynamic
|
||||
component={IconComponent()}
|
||||
class={cx("icon", local.class)}
|
||||
width={iconProps.size || "1em"}
|
||||
height={iconProps.size || "1em"}
|
||||
viewBox="0 0 48 48"
|
||||
// @ts-expect-error: dont know, fix this type nit later
|
||||
ref={iconProps.ref}
|
||||
{...iconProps}
|
||||
/>
|
||||
) : undefined;
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
File diff suppressed because it is too large
Load Diff
104
pkgs/clan-app/ui-2d/src/components/v2/Loader/Loader.css
Normal file
104
pkgs/clan-app/ui-2d/src/components/v2/Loader/Loader.css
Normal file
@@ -0,0 +1,104 @@
|
||||
.loader {
|
||||
@apply relative;
|
||||
@apply w-4 h-4;
|
||||
|
||||
&.primary {
|
||||
& > div.wrapper > div.parent,
|
||||
& > div.child {
|
||||
background: #00ff57;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
& > div.wrapper > div.parent,
|
||||
& > div.child {
|
||||
background: #0051ff;
|
||||
}
|
||||
}
|
||||
|
||||
& > div.wrapper {
|
||||
@apply absolute top-0 left-0 w-full h-full;
|
||||
|
||||
transform: translate(0%, 0%) rotate(-45deg);
|
||||
animation: moveLoaderWrapper 1.8s ease-in-out infinite;
|
||||
|
||||
& > div.parent {
|
||||
@apply absolute top-1/2 left-1/2;
|
||||
@apply w-2/3 h-2/3;
|
||||
|
||||
border-radius: 50%;
|
||||
animation: moveLoaderParent 1.8s ease-in-out infinite;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
& > .child {
|
||||
@apply absolute z-10 top-1/2 left-1/2 w-1/2 h-1/2;
|
||||
border-radius: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
animation: moveLoaderChild 1.8s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveLoaderWrapper {
|
||||
0% {
|
||||
transform: translate(0%, 0%) rotate(-45deg);
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: translate(-25%, 0%) rotate(-45deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(0%, 0%) rotate(-45deg);
|
||||
}
|
||||
|
||||
85% {
|
||||
transform: translate(25%, 0%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveLoaderParent {
|
||||
0% {
|
||||
animation-timing-function: ease-in-out;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
35% {
|
||||
animation-timing-function: cubic-bezier(0.7, -0.9, 0.3, 3.2);
|
||||
transform: translateX(-50%) translateY(-50%) skew(20deg, 20deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
animation-timing-function: ease-in-out;
|
||||
transform: translateX(-50%) translateY(-50%) skew(0deg, 0deg);
|
||||
}
|
||||
|
||||
85% {
|
||||
animation-timing-function: cubic-bezier(0.7, -0.9, 0.3, 3.2);
|
||||
transform: translateX(-50%) translateY(-50%) skew(20deg, 20deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveLoaderChild {
|
||||
0% {
|
||||
animation-timing-function: ease-in-out;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
35% {
|
||||
animation-timing-function: cubic-bezier(0.7, -0.9, 0.3, 3.2);
|
||||
transform: translateX(50%) translateY(-50%) scale(0.56);
|
||||
}
|
||||
|
||||
50% {
|
||||
animation-timing-function: ease-in-out;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
85% {
|
||||
animation-timing-function: cubic-bezier(0.7, -0.9, 0.3, 3.2);
|
||||
transform: translateX(-150%) translateY(-50%) scale(0.56);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
import { Loader, LoaderProps } from "@/src/components/v2/Loader/Loader";
|
||||
|
||||
const meta: Meta<LoaderProps> = {
|
||||
title: "Components/Loader",
|
||||
component: Loader,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<LoaderProps>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
hierarchy: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
hierarchy: "secondary",
|
||||
},
|
||||
};
|
||||
19
pkgs/clan-app/ui-2d/src/components/v2/Loader/Loader.tsx
Normal file
19
pkgs/clan-app/ui-2d/src/components/v2/Loader/Loader.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import "./Loader.css";
|
||||
import cx from "classnames";
|
||||
|
||||
export type Hierarchy = "primary" | "secondary";
|
||||
|
||||
export interface LoaderProps {
|
||||
hierarchy?: Hierarchy;
|
||||
}
|
||||
|
||||
export const Loader = (props: LoaderProps) => {
|
||||
return (
|
||||
<div class={cx("loader", props.hierarchy || "primary")}>
|
||||
<div class="wrapper">
|
||||
<div class="parent"></div>
|
||||
</div>
|
||||
<div class="child"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Components/Loader Primary smoke-test 1`] = `
|
||||
<div class="loader primary">
|
||||
<div class="wrapper">
|
||||
<div class="parent">
|
||||
</div>
|
||||
</div>
|
||||
<div class="child">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Components/Loader Secondary smoke-test 1`] = `
|
||||
<div class="loader secondary">
|
||||
<div class="wrapper">
|
||||
<div class="parent">
|
||||
</div>
|
||||
</div>
|
||||
<div class="child">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
7
pkgs/clan-app/ui-2d/src/components/v2/README.md
Normal file
7
pkgs/clan-app/ui-2d/src/components/v2/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Overview
|
||||
|
||||
We will be updating existing components and developing new components in line with the latest designs inside this
|
||||
folder. As they become ready, they can be copied into the root `components` folder, replacing any existing components as
|
||||
necessary.
|
||||
|
||||
This is to avoid merge hell and allow us to rapidly match the latest designs without the burden of integration.
|
||||
154
pkgs/clan-app/ui-2d/src/components/v2/Typography/Typography.css
Normal file
154
pkgs/clan-app/ui-2d/src/components/v2/Typography/Typography.css
Normal file
@@ -0,0 +1,154 @@
|
||||
/* Body */
|
||||
.typography {
|
||||
&.font-weight-normal {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.font-weight-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.font-weight-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.font-body {
|
||||
&.font-family-regular {
|
||||
font-family: "Archivo", sans-serif;
|
||||
}
|
||||
|
||||
&.font-family-condensed {
|
||||
font-family: "Archivo SemiCondensed", sans-serif;
|
||||
}
|
||||
|
||||
&.font-size-default {
|
||||
font-size: 1rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
|
||||
&.font-size-s {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.0175rem;
|
||||
}
|
||||
|
||||
&.font-size-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.0225rem;
|
||||
}
|
||||
|
||||
&.font-size-xxs {
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.00688rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.font-label {
|
||||
&.font-family-condensed {
|
||||
font-family: "Archivo SemiCondensed", sans-serif;
|
||||
|
||||
&.font-size-default {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.0175rem;
|
||||
}
|
||||
|
||||
&.font-size-s {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.0175rem;
|
||||
}
|
||||
|
||||
&.font-size-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.32;
|
||||
letter-spacing: 0.0075rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.font-family-mono {
|
||||
font-family: "Commit Mono", monospace;
|
||||
|
||||
&.font-size-default {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 0;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
&.font-size-s {
|
||||
font-size: 0.75rem;
|
||||
line-height: 0;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
&.font-size-xs {
|
||||
font-size: 0.6875rem;
|
||||
line-height: 0;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.font-title {
|
||||
&.font-family-regular {
|
||||
font-family: "Archivo", sans-serif;
|
||||
}
|
||||
|
||||
&.font-size-default {
|
||||
font-size: 1.125rem;
|
||||
line-height: 124%;
|
||||
letter-spacing: 0.03375rem;
|
||||
}
|
||||
|
||||
&.font-size-m {
|
||||
font-size: 1.25rem;
|
||||
line-height: 124%;
|
||||
letter-spacing: 0.0375rem;
|
||||
}
|
||||
|
||||
&.font-size-l {
|
||||
font-size: 1.375rem;
|
||||
line-height: 124%;
|
||||
letter-spacing: 0.04125rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.font-headline {
|
||||
&.font-family-regular {
|
||||
font-family: "Archivo", sans-serif;
|
||||
}
|
||||
|
||||
&.font-size-default {
|
||||
font-size: 1.5rem;
|
||||
line-height: 116%;
|
||||
letter-spacing: 0.015rem;
|
||||
}
|
||||
|
||||
&.font-size-m {
|
||||
font-size: 1.75rem;
|
||||
line-height: 116%;
|
||||
letter-spacing: 0.0175rem;
|
||||
}
|
||||
|
||||
&.font-size-l {
|
||||
font-size: 2rem;
|
||||
line-height: 116%;
|
||||
letter-spacing: 0.06rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.font-teaser {
|
||||
&.font-family-regular {
|
||||
font-family: "Archivo", sans-serif;
|
||||
}
|
||||
|
||||
&.font-size-default {
|
||||
font-size: 3rem;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.06rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { DocsStory, Meta, Canvas } from "@storybook/blocks";
|
||||
|
||||
import * as TypographyStories from "./Typography.stories";
|
||||
|
||||
<Meta of={TypographyStories} />
|
||||
|
||||
# Typography
|
||||
|
||||
There are two fonts being used within our typography system:
|
||||
|
||||
- [Archivo] in its _Standard_ and _Semi Condensed_ forms
|
||||
- [Fira Code] for mono-spaced content
|
||||
|
||||
## UI Components
|
||||
|
||||
When creating UI components that a user will interact with,
|
||||
you must use the condensed form of `Body`, `Label` and `Label Mono`.
|
||||
|
||||
<DocsStory of={TypographyStories.BodyCondensed} />
|
||||
|
||||
<DocsStory of={TypographyStories.LabelCondensed} />
|
||||
|
||||
<DocsStory of={TypographyStories.LabelMono} />
|
||||
|
||||
## Layout & Prose
|
||||
|
||||
For layout components and general prose, you must use the non-condensed forms of `Body`, `Title`, `Headline` and
|
||||
`Teaser`.
|
||||
|
||||
<DocsStory of={TypographyStories.Body} />
|
||||
<DocsStory of={TypographyStories.Title} />
|
||||
<DocsStory of={TypographyStories.Headline} />
|
||||
<DocsStory of={TypographyStories.Teaser} />
|
||||
|
||||
## Colors
|
||||
|
||||
Text colors come in one of five varieties:
|
||||
|
||||
- `inherit` (which will inherit the text color from the parent)
|
||||
- `primary`
|
||||
- `secondary`
|
||||
- `tertiary`
|
||||
- `quaternary`
|
||||
|
||||
<DocsStory of={TypographyStories.Colors} />
|
||||
|
||||
Each color also has an inverted counterpart.
|
||||
|
||||
<DocsStory of={TypographyStories.ColorsInverted} />
|
||||
|
||||
[Archivo]: https://github.com/Omnibus-Type/Archivo
|
||||
[Fira Code]: https://github.com/tonsky/FiraCode
|
||||
@@ -0,0 +1,166 @@
|
||||
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
|
||||
|
||||
import {
|
||||
AllowedSizes,
|
||||
Color,
|
||||
Family,
|
||||
Hierarchy,
|
||||
Typography,
|
||||
Weight,
|
||||
} from "./Typography";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
|
||||
interface TypographyExamplesProps {
|
||||
weights: Weight[];
|
||||
sizes: ("default" | "s" | "xs" | "xxs" | "m" | "l")[];
|
||||
hierarchy: Hierarchy;
|
||||
colors?: boolean;
|
||||
family?: Family;
|
||||
inverted?: boolean;
|
||||
}
|
||||
|
||||
const colors: (Color | "inherit")[] = [
|
||||
"inherit",
|
||||
"primary",
|
||||
"secondary",
|
||||
"tertiary",
|
||||
"quaternary",
|
||||
];
|
||||
|
||||
const TypographyExamples: Component<TypographyExamplesProps> = (props) => (
|
||||
<table
|
||||
class="w-full min-w-max table-auto text-left"
|
||||
classList={{
|
||||
"text-white bg-inv-1": props.inverted,
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
<For each={props.sizes}>
|
||||
{(size) => (
|
||||
<tr
|
||||
class="border-b border-def-3 even:bg-def-2"
|
||||
classList={{
|
||||
"border-inv-3 even:bg-inv-2": props.inverted,
|
||||
"border-def-3 even:bg-def-2": !props.inverted,
|
||||
}}
|
||||
>
|
||||
<For each={props.weights}>
|
||||
{(weight) => (
|
||||
<td class="px-6 py-2 ">
|
||||
<Show when={!props.colors}>
|
||||
<Typography
|
||||
hierarchy={props.hierarchy}
|
||||
//@ts-expect-error: difficult to generify for the story
|
||||
size={size}
|
||||
weight={weight}
|
||||
family={props.family}
|
||||
>
|
||||
{props.hierarchy} / {size} / {weight}
|
||||
</Typography>
|
||||
</Show>
|
||||
<Show when={props.colors}>
|
||||
<For each={colors}>
|
||||
{(color) => (
|
||||
<>
|
||||
<Typography
|
||||
hierarchy={props.hierarchy}
|
||||
//@ts-expect-error: difficult to generify for the story
|
||||
size={size}
|
||||
weight={weight}
|
||||
color={color}
|
||||
family={props.family}
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.hierarchy} / {size} / {weight} / {color}
|
||||
</Typography>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
const meta: Meta<TypographyExamplesProps> = {
|
||||
title: "Components/Typography",
|
||||
component: TypographyExamples,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<TypographyExamplesProps>;
|
||||
|
||||
export const BodyCondensed: Story = {
|
||||
args: {
|
||||
hierarchy: "body",
|
||||
sizes: ["default", "s", "xs", "xxs"],
|
||||
weights: ["normal", "medium", "bold"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Body: Story = {
|
||||
args: {
|
||||
...BodyCondensed.args,
|
||||
family: "regular",
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelCondensed: Story = {
|
||||
args: {
|
||||
hierarchy: "label",
|
||||
sizes: ["default", "s", "xs"],
|
||||
weights: ["normal", "medium", "bold"],
|
||||
},
|
||||
};
|
||||
|
||||
export const LabelMono: Story = {
|
||||
args: {
|
||||
...LabelCondensed.args,
|
||||
family: "mono",
|
||||
},
|
||||
};
|
||||
|
||||
export const Title: Story = {
|
||||
args: {
|
||||
hierarchy: "title",
|
||||
sizes: ["default", "m", "l"],
|
||||
weights: ["normal", "medium", "bold"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Headline: Story = {
|
||||
args: {
|
||||
hierarchy: "headline",
|
||||
sizes: ["default", "m", "l"],
|
||||
weights: ["normal", "medium", "bold"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Teaser: Story = {
|
||||
args: {
|
||||
hierarchy: "teaser",
|
||||
sizes: ["default"],
|
||||
weights: ["bold"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Colors: Story = {
|
||||
args: {
|
||||
...BodyCondensed.args,
|
||||
colors: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ColorsInverted: Story = {
|
||||
args: {
|
||||
...Colors.args,
|
||||
inverted: true,
|
||||
},
|
||||
};
|
||||
152
pkgs/clan-app/ui-2d/src/components/v2/Typography/Typography.tsx
Normal file
152
pkgs/clan-app/ui-2d/src/components/v2/Typography/Typography.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { type JSX, mergeProps } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import cx from "classnames";
|
||||
import "./Typography.css";
|
||||
|
||||
export type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4" | "div";
|
||||
export type Color = "primary" | "secondary" | "tertiary" | "quaternary";
|
||||
export type Hierarchy = "body" | "title" | "headline" | "label" | "teaser";
|
||||
export type Weight = "normal" | "medium" | "bold";
|
||||
export type Family = "regular" | "condensed" | "mono";
|
||||
|
||||
const colorMap: Record<Color, string> = {
|
||||
primary: cx("fg-def-1"),
|
||||
secondary: cx("fg-def-2"),
|
||||
tertiary: cx("fg-def-3"),
|
||||
quaternary: cx("fg-def-4"),
|
||||
};
|
||||
|
||||
const invertedColorMap: Record<Color, string> = {
|
||||
primary: cx("fg-inv-1"),
|
||||
secondary: cx("fg-inv-2"),
|
||||
tertiary: cx("fg-inv-3"),
|
||||
quaternary: cx("fg-inv-4"),
|
||||
};
|
||||
|
||||
const colorFor = (color: Color | "inherit" = "primary", inverted = false) => {
|
||||
if (color === "inherit") {
|
||||
return "text-inherit";
|
||||
}
|
||||
|
||||
return inverted ? invertedColorMap[color] : colorMap[color];
|
||||
};
|
||||
|
||||
// type Size = "default" | "xs" | "s" | "m" | "l";
|
||||
interface SizeForHierarchy {
|
||||
body: {
|
||||
default: string;
|
||||
s: string;
|
||||
xs: string;
|
||||
xxs: string;
|
||||
};
|
||||
label: {
|
||||
default: string;
|
||||
s: string;
|
||||
xs: string;
|
||||
};
|
||||
headline: {
|
||||
default: string;
|
||||
m: string;
|
||||
l: string;
|
||||
};
|
||||
title: {
|
||||
default: string;
|
||||
m: string;
|
||||
l: string;
|
||||
};
|
||||
teaser: {
|
||||
default: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AllowedSizes<H extends Hierarchy> = keyof SizeForHierarchy[H];
|
||||
|
||||
const sizeHierarchyMap: SizeForHierarchy = {
|
||||
body: {
|
||||
default: cx("font-size-default"),
|
||||
s: cx("font-size-s"),
|
||||
xs: cx("font-size-xs"),
|
||||
xxs: cx("font-size-xxs"),
|
||||
},
|
||||
headline: {
|
||||
default: cx("font-size-default"),
|
||||
m: cx("font-size-m"),
|
||||
l: cx("font-size-l"),
|
||||
},
|
||||
title: {
|
||||
default: cx("font-size-default"),
|
||||
// xs: cx("font-size-xs"),
|
||||
// s: cx("font-size-s"),
|
||||
m: cx("font-size-m"),
|
||||
l: cx("font-size-l"),
|
||||
},
|
||||
label: {
|
||||
default: cx("font-size-default"),
|
||||
s: cx("font-size-s"),
|
||||
xs: cx("font-size-xs"),
|
||||
},
|
||||
teaser: {
|
||||
default: cx("font-size-default"),
|
||||
},
|
||||
};
|
||||
|
||||
const defaultFamilyMap: Record<Hierarchy, Family> = {
|
||||
body: "condensed",
|
||||
label: "condensed",
|
||||
title: "regular",
|
||||
headline: "regular",
|
||||
teaser: "regular",
|
||||
};
|
||||
|
||||
const weightMap: Record<Weight, string> = {
|
||||
normal: cx("font-weight-normal"),
|
||||
medium: cx("font-weight-medium"),
|
||||
bold: cx("font-weight-bold"),
|
||||
};
|
||||
|
||||
interface _TypographyProps<H extends Hierarchy> {
|
||||
hierarchy: H;
|
||||
size: AllowedSizes<H>;
|
||||
color?: Color | "inherit";
|
||||
children: JSX.Element;
|
||||
weight?: Weight;
|
||||
family?: Family;
|
||||
inverted?: boolean;
|
||||
tag?: Tag;
|
||||
class?: string;
|
||||
classList?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||
const family = () =>
|
||||
`font-family-${props.family || defaultFamilyMap[props.hierarchy]}`;
|
||||
|
||||
const color = () => colorFor(props.color, props.inverted);
|
||||
|
||||
const classList = mergeProps(props.classList, {
|
||||
"font-body": props.hierarchy === "body" || !props.hierarchy,
|
||||
"font-label": props.hierarchy === "label",
|
||||
"font-title": props.hierarchy === "title",
|
||||
"font-headline": props.hierarchy === "headline",
|
||||
"font-teaser": props.hierarchy === "teaser",
|
||||
});
|
||||
|
||||
return (
|
||||
<Dynamic
|
||||
class={cx(
|
||||
"typography",
|
||||
color(),
|
||||
family(),
|
||||
weightMap[props.weight || "normal"],
|
||||
sizeHierarchyMap[props.hierarchy][props.size] as string,
|
||||
props.class,
|
||||
)}
|
||||
component={props.tag || "span"}
|
||||
classList={classList}
|
||||
>
|
||||
{props.children}
|
||||
</Dynamic>
|
||||
);
|
||||
};
|
||||
|
||||
export type TypographyProps = _TypographyProps<Hierarchy>;
|
||||
@@ -0,0 +1,955 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Components/Typography Body smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-default font-body">
|
||||
body / default / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-default font-body">
|
||||
body / default / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-default font-body">
|
||||
body / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-s font-body">
|
||||
body / s / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-s font-body">
|
||||
body / s / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-s font-body">
|
||||
body / s / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography BodyCondensed smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography Colors smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-2 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-3 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-def-4 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography ColorsInverted smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left text-white bg-inv-1">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2 border-inv-3 even:bg-inv-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-normal font-size-default font-body">
|
||||
body / default / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-medium font-size-default font-body">
|
||||
body / default / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-bold font-size-default font-body">
|
||||
body / default / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2 border-inv-3 even:bg-inv-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-normal font-size-s font-body">
|
||||
body / s / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-medium font-size-s font-body">
|
||||
body / s / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-bold font-size-s font-body">
|
||||
body / s / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2 border-inv-3 even:bg-inv-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-normal font-size-xs font-body">
|
||||
body / xs / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-medium font-size-xs font-body">
|
||||
body / xs / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-bold font-size-xs font-body">
|
||||
body / xs / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2 border-inv-3 even:bg-inv-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-normal font-size-xxs font-body">
|
||||
body / xxs / normal / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-medium font-size-xxs font-body">
|
||||
body / xxs / medium / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography text-inherit font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / inherit
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-1 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / primary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-2 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / secondary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-3 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / tertiary
|
||||
</span>
|
||||
<br>
|
||||
<span class="typography fg-inv-4 font-family-condensed font-weight-bold font-size-xxs font-body">
|
||||
body / xxs / bold / quaternary
|
||||
</span>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography Headline smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-default font-headline">
|
||||
headline / default / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-default font-headline">
|
||||
headline / default / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-default font-headline">
|
||||
headline / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-m font-headline">
|
||||
headline / m / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-m font-headline">
|
||||
headline / m / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-m font-headline">
|
||||
headline / m / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-l font-headline">
|
||||
headline / l / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-l font-headline">
|
||||
headline / l / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-l font-headline">
|
||||
headline / l / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography LabelCondensed smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-default font-label">
|
||||
label / default / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-default font-label">
|
||||
label / default / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-default font-label">
|
||||
label / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-s font-label">
|
||||
label / s / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-s font-label">
|
||||
label / s / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-s font-label">
|
||||
label / s / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-normal font-size-xs font-label">
|
||||
label / xs / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-medium font-size-xs font-label">
|
||||
label / xs / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-condensed font-weight-bold font-size-xs font-label">
|
||||
label / xs / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography LabelMono smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-normal font-size-default font-label">
|
||||
label / default / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-medium font-size-default font-label">
|
||||
label / default / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-bold font-size-default font-label">
|
||||
label / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-normal font-size-s font-label">
|
||||
label / s / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-medium font-size-s font-label">
|
||||
label / s / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-bold font-size-s font-label">
|
||||
label / s / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-normal font-size-xs font-label">
|
||||
label / xs / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-medium font-size-xs font-label">
|
||||
label / xs / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-mono font-weight-bold font-size-xs font-label">
|
||||
label / xs / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography Teaser smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-default font-teaser">
|
||||
teaser / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
exports[`Components/Typography Title smoke-test 1`] = `
|
||||
<table class="w-full min-w-max table-auto text-left">
|
||||
<tbody>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-default font-title">
|
||||
title / default / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-default font-title">
|
||||
title / default / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-default font-title">
|
||||
title / default / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-m font-title">
|
||||
title / m / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-m font-title">
|
||||
title / m / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-m font-title">
|
||||
title / m / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-def-3 even:bg-def-2">
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-normal font-size-l font-title">
|
||||
title / l / normal
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-medium font-size-l font-title">
|
||||
title / l / medium
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-2 ">
|
||||
<span class="typography fg-def-1 font-family-regular font-weight-bold font-size-l font-title">
|
||||
title / l / bold
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
105
pkgs/clan-app/ui-2d/src/components/v2/index.css
Normal file
105
pkgs/clan-app/ui-2d/src/components/v2/index.css
Normal file
@@ -0,0 +1,105 @@
|
||||
@import "material-icons/iconfont/filled.css";
|
||||
/* List of icons: https://marella.me/material-icons/demo/ */
|
||||
/* @import url(./components/Typography/css/typography.css); */
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
font-weight: 400;
|
||||
src: url(../../../.fonts/Archivo-Regular.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
font-weight: 500;
|
||||
src: url(../../../.fonts/Archivo-Medium.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
font-weight: 600;
|
||||
src: url(../../../.fonts/Archivo-SemiBold.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo SemiCondensed";
|
||||
font-weight: 400;
|
||||
src: url(../../../.fonts/ArchivoSemiCondensed-Regular.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo SemiCondensed";
|
||||
font-weight: 500;
|
||||
src: url(../../../.fonts/ArchivoSemiCondensed-Medium.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo SemiCondensed";
|
||||
font-weight: 600;
|
||||
src: url(../../../.fonts/ArchivoSemiCondensed-SemiBold.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Commit Mono";
|
||||
font-weight: 400;
|
||||
src: url(../../../.fonts/CommitMono-400-Regular.otf) format("otf");
|
||||
}
|
||||
|
||||
:root {
|
||||
--clr-bg-def-1: theme(colors.white);
|
||||
--clr-bg-def-2: theme(colors.primary.50);
|
||||
--clr-bg-def-3: theme(colors.secondary.100);
|
||||
--clr-bg-def-4: theme(colors.secondary.200);
|
||||
|
||||
--clr-border-def-1: theme(colors.secondary.50);
|
||||
--clr-border-def-2: theme(colors.secondary.100);
|
||||
--clr-border-def-3: theme(colors.secondary.200);
|
||||
--clr-border-def-4: theme(colors.secondary.300);
|
||||
|
||||
--clr-border-def-sem-inf-1: theme(colors.info.500);
|
||||
--clr-border-def-sem-inf-2: theme(colors.info.600);
|
||||
--clr-border-def-sem-inf-3: theme(colors.info.700);
|
||||
--clr-border-def-sem-inf-4: theme(colors.info.800);
|
||||
|
||||
--clr-bg-inv-1: theme(colors.primary.600);
|
||||
--clr-bg-inv-2: theme(colors.primary.700);
|
||||
--clr-bg-inv-3: theme(colors.primary.800);
|
||||
--clr-bg-inv-4: theme(colors.primary.900);
|
||||
|
||||
--clr-border-inv-1: theme(colors.secondary.700);
|
||||
--clr-border-inv-2: theme(colors.secondary.800);
|
||||
--clr-border-inv-3: theme(colors.secondary.900);
|
||||
--clr-border-inv-4: theme(colors.secondary.950);
|
||||
|
||||
--clr-bg-inv-acc-1: theme(colors.secondary.500);
|
||||
--clr-bg-inv-acc-2: theme(colors.secondary.600);
|
||||
--clr-bg-inv-acc-3: theme(colors.secondary.700);
|
||||
|
||||
--clr-fg-def-1: theme(colors.secondary.950);
|
||||
--clr-fg-def-2: theme(colors.secondary.900);
|
||||
--clr-fg-def-3: theme(colors.secondary.700);
|
||||
--clr-fg-def-4: theme(colors.secondary.400);
|
||||
|
||||
--clr-fg-inv-1: theme(colors.white);
|
||||
--clr-fg-inv-2: theme(colors.secondary.100);
|
||||
--clr-fg-inv-3: theme(colors.secondary.300);
|
||||
--clr-fg-inv-4: theme(colors.secondary.400);
|
||||
}
|
||||
|
||||
html {
|
||||
@apply font-sans;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
|
||||
-webkit-user-select: none;
|
||||
/* Safari */
|
||||
-moz-user-select: none;
|
||||
/* Firefox */
|
||||
-ms-user-select: none;
|
||||
/* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
/* Standard */
|
||||
}
|
||||
65
pkgs/clan-app/ui-2d/src/contexts/clan.tsx
Normal file
65
pkgs/clan-app/ui-2d/src/contexts/clan.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createContext, createEffect, JSX, useContext } from "solid-js";
|
||||
import { callApi } from "@/src/api";
|
||||
import {
|
||||
activeClanURI,
|
||||
addClanURI,
|
||||
clanURIs,
|
||||
removeClanURI,
|
||||
setActiveClanURI,
|
||||
store,
|
||||
} from "@/src/stores/clan";
|
||||
import { redirect } from "@solidjs/router";
|
||||
|
||||
// Create the context
|
||||
interface ClanContextType {
|
||||
activeClanURI: typeof activeClanURI;
|
||||
setActiveClanURI: typeof setActiveClanURI;
|
||||
clanURIs: typeof clanURIs;
|
||||
addClanURI: typeof addClanURI;
|
||||
removeClanURI: typeof removeClanURI;
|
||||
}
|
||||
|
||||
const ClanContext = createContext<ClanContextType>({
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
});
|
||||
|
||||
interface ClanProviderProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export function ClanProvider(props: ClanProviderProps) {
|
||||
// redirect to welcome if there's no active clan and no clan URIs
|
||||
createEffect(async () => {
|
||||
if (!store.activeClanURI && store.clanURIs.length == 0) {
|
||||
redirect("/welcome");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ClanContext.Provider
|
||||
value={{
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ClanContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Export a hook that provides access to the context
|
||||
export function useClanContext() {
|
||||
const context = useContext(ClanContext);
|
||||
if (!context) {
|
||||
throw new Error("useClanContext must be used within a ClanProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
127
pkgs/clan-app/ui-2d/src/floating/index.tsx
Normal file
127
pkgs/clan-app/ui-2d/src/floating/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
|
||||
import type {
|
||||
ComputePositionConfig,
|
||||
ComputePositionReturn,
|
||||
ReferenceElement,
|
||||
} from "@floating-ui/dom";
|
||||
import { computePosition } from "@floating-ui/dom";
|
||||
|
||||
export interface UseFloatingOptions<
|
||||
R extends ReferenceElement,
|
||||
F extends HTMLElement,
|
||||
> extends Partial<ComputePositionConfig> {
|
||||
whileElementsMounted?: (
|
||||
reference: R,
|
||||
floating: F,
|
||||
update: () => void,
|
||||
) => // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
void | (() => void);
|
||||
}
|
||||
|
||||
interface UseFloatingState extends Omit<ComputePositionReturn, "x" | "y"> {
|
||||
x?: number | null;
|
||||
y?: number | null;
|
||||
}
|
||||
|
||||
export interface UseFloatingResult extends UseFloatingState {
|
||||
update(): void;
|
||||
}
|
||||
|
||||
export function useFloating<R extends ReferenceElement, F extends HTMLElement>(
|
||||
reference: () => R | undefined | null,
|
||||
floating: () => F | undefined | null,
|
||||
options?: UseFloatingOptions<R, F>,
|
||||
): UseFloatingResult {
|
||||
const placement = () => options?.placement ?? "bottom";
|
||||
const strategy = () => options?.strategy ?? "absolute";
|
||||
|
||||
const [data, setData] = createSignal<UseFloatingState>({
|
||||
x: null,
|
||||
y: null,
|
||||
placement: placement(),
|
||||
strategy: strategy(),
|
||||
middlewareData: {},
|
||||
});
|
||||
|
||||
const [error, setError] = createSignal<{ value: unknown } | undefined>();
|
||||
|
||||
createEffect(() => {
|
||||
const currentError = error();
|
||||
if (currentError) {
|
||||
throw currentError.value;
|
||||
}
|
||||
});
|
||||
|
||||
const version = createMemo(() => {
|
||||
reference();
|
||||
floating();
|
||||
return {};
|
||||
});
|
||||
|
||||
function update() {
|
||||
const currentReference = reference();
|
||||
const currentFloating = floating();
|
||||
|
||||
if (currentReference && currentFloating) {
|
||||
const capturedVersion = version();
|
||||
computePosition(currentReference, currentFloating, {
|
||||
middleware: options?.middleware,
|
||||
placement: placement(),
|
||||
strategy: strategy(),
|
||||
}).then(
|
||||
(currentData) => {
|
||||
// Check if it's still valid
|
||||
if (capturedVersion === version()) {
|
||||
setData(currentData);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
setError(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const currentReference = reference();
|
||||
const currentFloating = floating();
|
||||
|
||||
placement();
|
||||
strategy();
|
||||
|
||||
if (currentReference && currentFloating) {
|
||||
if (options?.whileElementsMounted) {
|
||||
const cleanup = options.whileElementsMounted(
|
||||
currentReference,
|
||||
currentFloating,
|
||||
update,
|
||||
);
|
||||
|
||||
if (cleanup) {
|
||||
onCleanup(cleanup);
|
||||
}
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
get x() {
|
||||
return data().x;
|
||||
},
|
||||
get y() {
|
||||
return data().y;
|
||||
},
|
||||
get placement() {
|
||||
return data().placement;
|
||||
},
|
||||
get strategy() {
|
||||
return data().strategy;
|
||||
},
|
||||
get middlewareData() {
|
||||
return data().middlewareData;
|
||||
},
|
||||
update,
|
||||
};
|
||||
}
|
||||
44
pkgs/clan-app/ui-2d/src/hooks/index.ts
Normal file
44
pkgs/clan-app/ui-2d/src/hooks/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { callApi } from "../api";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export const registerClan = async () => {
|
||||
const { setActiveClanURI, addClanURI } = useClanContext();
|
||||
|
||||
try {
|
||||
const loc = await callApi("open_file", {
|
||||
file_request: { mode: "select_folder" },
|
||||
}).promise;
|
||||
if (loc.status === "success" && loc.data) {
|
||||
const data = loc.data[0];
|
||||
addClanURI(data);
|
||||
setActiveClanURI(data);
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the custom file dialog
|
||||
* Returns a native FileList to allow interaction with the native input type="file"
|
||||
*/
|
||||
export const selectSshKeys = async (): Promise<FileList> => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
|
||||
const response = await callApi("open_file", {
|
||||
file_request: {
|
||||
title: "Select SSH Key",
|
||||
mode: "open_file",
|
||||
initial_folder: "~/.ssh",
|
||||
},
|
||||
}).promise;
|
||||
if (response.status === "success" && response.data) {
|
||||
// Add synthetic files to the DataTransfer object
|
||||
// FileList cannot be instantiated directly.
|
||||
response.data.forEach((filename) => {
|
||||
dataTransfer.items.add(new File([], filename));
|
||||
});
|
||||
}
|
||||
return dataTransfer.files;
|
||||
};
|
||||
157
pkgs/clan-app/ui-2d/src/index.css
Normal file
157
pkgs/clan-app/ui-2d/src/index.css
Normal file
@@ -0,0 +1,157 @@
|
||||
@import "material-icons/iconfont/filled.css";
|
||||
/* List of icons: https://marella.me/material-icons/demo/ */
|
||||
/* @import url(./components/Typography/css/typography.css); */
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
font-weight: 400;
|
||||
src: url(../.fonts/ArchivoSemiCondensed-Regular.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
font-weight: 500;
|
||||
src: url(../.fonts/ArchivoSemiCondensed-Medium.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Archivo";
|
||||
font-weight: 600;
|
||||
src: url(../.fonts/ArchivoSemiCondensed-SemiBold.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
to {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
@apply font-sans;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
|
||||
-webkit-user-select: none;
|
||||
/* Safari */
|
||||
-moz-user-select: none;
|
||||
/* Firefox */
|
||||
-ms-user-select: none;
|
||||
/* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
/* Standard */
|
||||
}
|
||||
|
||||
.accordeon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: theme(gap.3);
|
||||
}
|
||||
|
||||
.accordeon__header {
|
||||
padding: theme(padding.2) theme(padding[1.5]) theme(padding.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.accordeon__body {
|
||||
}
|
||||
|
||||
.machine-item-loader {
|
||||
@apply col-span-1 flex flex-col items-center;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: theme(padding.2);
|
||||
border-radius: theme(borderRadius.md);
|
||||
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.machine-item-loader__thumb-wrapper {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
padding: theme(padding.4);
|
||||
|
||||
border-radius: theme(borderRadius.md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.machine-item-loader__thumb {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: theme(backgroundColor.secondary.100);
|
||||
border-radius: theme(borderRadius.md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.machine-item-loader__headline {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
width: 90%;
|
||||
height: 20px;
|
||||
|
||||
background: theme(backgroundColor.secondary.100);
|
||||
border-radius: theme(borderRadius.sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.machine-item-loader__cover {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.machine-item-loader__loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 20%,
|
||||
theme(backgroundColor.secondary.200) 50%,
|
||||
transparent 80%
|
||||
);
|
||||
background-size: 400px 100%;
|
||||
|
||||
animation: loader 4s linear infinite;
|
||||
transition: all 0.56s ease;
|
||||
}
|
||||
|
||||
.machine-item-loader__cover .machine-item-loader__loader {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 20%,
|
||||
theme(backgroundColor.secondary.50) 50%,
|
||||
transparent 80%
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
203
pkgs/clan-app/ui-2d/src/index.tsx
Normal file
203
pkgs/clan-app/ui-2d/src/index.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
/* @refresh reload */
|
||||
import { Portal, render } from "solid-js/web";
|
||||
import { Navigate, RouteDefinition, Router } from "@solidjs/router";
|
||||
|
||||
import "./index.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
import {
|
||||
CreateMachine,
|
||||
MachineDetails,
|
||||
MachineListView,
|
||||
MachineInstall,
|
||||
} from "./routes/machines";
|
||||
import { Layout } from "./layout/layout";
|
||||
import { ClanDetails, ClanList, CreateClan } from "./routes/clans";
|
||||
import { Flash } from "./routes/flash/view";
|
||||
import { HostList } from "./routes/hosts/view";
|
||||
import { Welcome } from "./routes/welcome";
|
||||
import { Toaster } from "solid-toast";
|
||||
import { ModuleList } from "./routes/modules/list";
|
||||
import { ModuleDetails } from "./routes/modules/details";
|
||||
import { ModuleDetails as AddModule } from "./routes/modules/add";
|
||||
import { ApiTester } from "./api_test";
|
||||
import { IconVariant } from "./components/icon";
|
||||
import { Components } from "./routes/components";
|
||||
import { VarsPage } from "./routes/machines/install/vars-step";
|
||||
import { ThreePlayground } from "./three";
|
||||
import { ClanProvider } from "./contexts/clan";
|
||||
|
||||
export const client = new QueryClient();
|
||||
|
||||
const root = document.getElementById("app");
|
||||
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
throw new Error(
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||
);
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Development mode");
|
||||
// Load the debugger in development mode
|
||||
await import("solid-devtools");
|
||||
}
|
||||
|
||||
export type AppRoute = Omit<RouteDefinition, "children"> & {
|
||||
label: string;
|
||||
icon?: IconVariant;
|
||||
children?: AppRoute[];
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export const routes: AppRoute[] = [
|
||||
{
|
||||
path: "/",
|
||||
label: "",
|
||||
hidden: true,
|
||||
component: () => <Navigate href="/machines" />,
|
||||
},
|
||||
{
|
||||
path: "/machines",
|
||||
label: "Machines",
|
||||
icon: "Grid",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
label: "Overview",
|
||||
component: () => <MachineListView />,
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
label: "Create",
|
||||
component: () => <CreateMachine />,
|
||||
},
|
||||
{
|
||||
path: "/:id",
|
||||
label: "Details",
|
||||
hidden: true,
|
||||
component: () => <MachineDetails />,
|
||||
},
|
||||
{
|
||||
path: "/:id/vars",
|
||||
label: "Vars",
|
||||
hidden: true,
|
||||
component: () => <VarsPage />,
|
||||
},
|
||||
{
|
||||
path: "/:id/install",
|
||||
label: "Install",
|
||||
hidden: true,
|
||||
component: () => <MachineInstall />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/clans",
|
||||
label: "Clans",
|
||||
hidden: true,
|
||||
icon: "List",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
label: "Overview",
|
||||
component: () => <ClanList />,
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
label: "Create",
|
||||
component: () => <CreateClan />,
|
||||
},
|
||||
{
|
||||
path: "/:id",
|
||||
label: "Details",
|
||||
hidden: true,
|
||||
component: () => <ClanDetails />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/modules",
|
||||
label: "Modules",
|
||||
icon: "Search",
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
label: "App Store",
|
||||
component: () => <ModuleList />,
|
||||
},
|
||||
{
|
||||
path: "details/:id",
|
||||
label: "Details",
|
||||
hidden: true,
|
||||
component: () => <ModuleDetails />,
|
||||
},
|
||||
{
|
||||
path: "/add/:id",
|
||||
label: "Details",
|
||||
hidden: true,
|
||||
component: () => <AddModule />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/tools",
|
||||
label: "Tools",
|
||||
icon: "Folder",
|
||||
children: [
|
||||
{
|
||||
path: "/flash",
|
||||
label: "Flash Installer",
|
||||
component: () => <Flash />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/welcome",
|
||||
label: "",
|
||||
hidden: true,
|
||||
component: () => <Welcome />,
|
||||
},
|
||||
{
|
||||
path: "/internal-dev",
|
||||
label: "Internal (Only visible in dev mode)",
|
||||
children: [
|
||||
{
|
||||
path: "/hosts",
|
||||
label: "Local Hosts",
|
||||
component: () => <HostList />,
|
||||
},
|
||||
{
|
||||
path: "/3d",
|
||||
label: "3D-Playground",
|
||||
component: () => <ThreePlayground />,
|
||||
},
|
||||
{
|
||||
path: "/api_testing",
|
||||
label: "api_testing",
|
||||
hidden: false,
|
||||
component: () => <ApiTester />,
|
||||
},
|
||||
{
|
||||
path: "/components",
|
||||
label: "Components",
|
||||
hidden: false,
|
||||
component: () => <Components />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
() => (
|
||||
<>
|
||||
<Portal mount={document.body}>
|
||||
<Toaster position="top-right" containerClassName="z-[9999]" />
|
||||
</Portal>
|
||||
<QueryClientProvider client={client}>
|
||||
<ClanProvider>
|
||||
<Router root={Layout}>{routes}</Router>
|
||||
</ClanProvider>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
),
|
||||
root!,
|
||||
);
|
||||
29
pkgs/clan-app/ui-2d/src/layout/header.tsx
Normal file
29
pkgs/clan-app/ui-2d/src/layout/header.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { JSX } from "solid-js";
|
||||
import { Typography } from "../components/Typography";
|
||||
import { BackButton } from "../components/BackButton";
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
toolbar?: JSX.Element;
|
||||
showBack?: boolean;
|
||||
}
|
||||
export const Header = (props: HeaderProps) => {
|
||||
return (
|
||||
<div class="sticky top-0 z-50 flex items-center border-b bg-white/80 px-6 py-4 backdrop-blur-md border-def-3">
|
||||
<div class="flex-none">
|
||||
{props.showBack && <BackButton />}
|
||||
<span class=" lg:hidden" data-tip="Menu">
|
||||
<label class=" " for="toplevel-drawer">
|
||||
<span class="material-icons">menu</span>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<Typography hierarchy="title" size="m" weight="medium" class="">
|
||||
{props.title}
|
||||
</Typography>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-3">{props.toolbar}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user