mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
✨ feat: Add qwik-react (#103)
This adds the following pages: The landing page (/) The pricing page (/pricing) The contact page (/contact) The changelog page (/changelog) Terms Of Service page (/terms) Privacy Policy (/privacy)
This commit is contained in:
47
packages/cache/caches.ts
vendored
Normal file
47
packages/cache/caches.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
const buildCacheKey = (namespace: string) => (key: string) => {
|
||||
return `${namespace}:${key}`;
|
||||
};
|
||||
|
||||
export interface KVResponseCache {
|
||||
match(key: string): Promise<Response | null>;
|
||||
put(key: string, res: Response, options?: KVNamespacePutOptions): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const kvResponseCache =
|
||||
(kv: KVNamespace) =>
|
||||
(cacheName: string): KVResponseCache => {
|
||||
const cacheKey = buildCacheKey(cacheName);
|
||||
|
||||
return {
|
||||
async match(key: string) {
|
||||
const [headers, status, body] = await Promise.all([
|
||||
kv.get(cacheKey(`${key}:headers`)),
|
||||
kv.get(cacheKey(`${key}:status`)),
|
||||
kv.get(cacheKey(`${key}:body`), "stream"),
|
||||
]);
|
||||
|
||||
if (headers === null || body === null || status === null) return null;
|
||||
|
||||
return new Response(body, { headers: JSON.parse(headers), status: parseInt(status, 10) });
|
||||
},
|
||||
async put(key: string, res: Response, options?: KVNamespacePutOptions) {
|
||||
const headers = Array.from(res.headers.entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||
const body = res.body;
|
||||
if (body === null) return;
|
||||
|
||||
await Promise.all([
|
||||
kv.put(cacheKey(`${key}:headers`), JSON.stringify(headers), options),
|
||||
kv.put(cacheKey(`${key}:status`), `${res.status}`, options),
|
||||
kv.put(cacheKey(`${key}:body`), body, options),
|
||||
]);
|
||||
},
|
||||
async delete(key: string) {
|
||||
await Promise.all([
|
||||
kv.delete(cacheKey(`${key}:headers`)),
|
||||
kv.delete(cacheKey(`${key}:status`)),
|
||||
kv.delete(cacheKey(`${key}:body`)),
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
8
packages/cache/index.ts
vendored
Normal file
8
packages/cache/index.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
//copied from https://github.com/napolab/kv-response-cache with some minor changes
|
||||
import { kvResponseCache } from "./caches";
|
||||
import { kvCaches, defaultGetCacheKey } from "./middleware";
|
||||
|
||||
import type { KVResponseCache } from "./caches";
|
||||
|
||||
export type { KVResponseCache };
|
||||
export { kvResponseCache, kvCaches, defaultGetCacheKey as getCacheKey };
|
||||
47
packages/cache/middleware.ts
vendored
Normal file
47
packages/cache/middleware.ts
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import { kvResponseCache } from "./caches";
|
||||
|
||||
import type { Filter } from "./types";
|
||||
import type { Context, Env, MiddlewareHandler } from "hono";
|
||||
|
||||
type Namespace<E extends Env> = string | ((c: Context<E>) => string);
|
||||
interface GetCacheKey<E extends Env> {
|
||||
(c: Context<E>): string;
|
||||
}
|
||||
|
||||
type KVCacheOption<E extends Env & { Bindings: Record<string, unknown> }> = {
|
||||
key: keyof E["Bindings"];
|
||||
namespace: Namespace<E>;
|
||||
getCacheKey?: GetCacheKey<E>;
|
||||
options?: KVNamespacePutOptions;
|
||||
};
|
||||
|
||||
export const defaultGetCacheKey = <E extends Env>(c: Context<E>) => c.req.url;
|
||||
|
||||
export const kvCaches =
|
||||
<E extends Env & { Bindings: Record<string, unknown> }>({
|
||||
key: bindingKey,
|
||||
namespace,
|
||||
options,
|
||||
getCacheKey = defaultGetCacheKey,
|
||||
}: KVCacheOption<E>): MiddlewareHandler<E> =>
|
||||
async (c, next) => {
|
||||
const kv: KVNamespace = c.env?.[bindingKey] as KVNamespace;
|
||||
const kvNamespace = typeof namespace === "function" ? namespace(c) : namespace;
|
||||
|
||||
const kvCaches = kvResponseCache(kv);
|
||||
const cache = kvCaches(kvNamespace);
|
||||
|
||||
const key = getCacheKey(c);
|
||||
const response = await cache.match(key);
|
||||
if (response) {
|
||||
response.headers.set("X-KV-CACHE", "hit");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
if (c.res.status >= 200 && c.res.status < 300) {
|
||||
c.executionCtx.waitUntil(cache.put(key, c.res.clone(), options));
|
||||
}
|
||||
};
|
||||
13
packages/cache/package.json
vendored
Normal file
13
packages/cache/package.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@nestri/cache",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
}
|
||||
}
|
||||
15
packages/cache/tsconfig.json
vendored
Normal file
15
packages/cache/tsconfig.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
]
|
||||
},
|
||||
}
|
||||
3
packages/cache/types.ts
vendored
Normal file
3
packages/cache/types.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
export type Filter<T extends Record<string, unknown>, V> = {
|
||||
[K in keyof T as T[K] extends V ? K : never]: T[K];
|
||||
};
|
||||
73
packages/core/image-brightness-analyzer.ts
Normal file
73
packages/core/image-brightness-analyzer.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// image-brightness-analyzer.js
|
||||
|
||||
export class ImageBrightnessAnalyzer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D ;
|
||||
|
||||
constructor() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
analyze(imgElement: HTMLImageElement) {
|
||||
if (!(imgElement instanceof HTMLImageElement)) {
|
||||
throw new Error('Input must be an HTMLImageElement');
|
||||
}
|
||||
|
||||
this.canvas.width = imgElement.width;
|
||||
this.canvas.height = imgElement.height;
|
||||
this.ctx.drawImage(imgElement, 0, 0);
|
||||
|
||||
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let brightestPixel = { value: 0, x: 0, y: 0 };
|
||||
let dullestPixel = { value: 765, x: 0, y: 0 }; // 765 is the max value (255 * 3)
|
||||
|
||||
for (let y = 0; y < this.canvas.height; y++) {
|
||||
for (let x = 0; x < this.canvas.width; x++) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
const brightness = data[index] + data[index + 1] + data[index + 2];
|
||||
|
||||
if (brightness > brightestPixel.value) {
|
||||
brightestPixel = { value: brightness, x, y };
|
||||
}
|
||||
if (brightness < dullestPixel.value) {
|
||||
dullestPixel = { value: brightness, x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
brightest: {
|
||||
x: brightestPixel.x,
|
||||
y: brightestPixel.y,
|
||||
color: this.getPixelColor(data, brightestPixel.x, brightestPixel.y)
|
||||
},
|
||||
dullest: {
|
||||
x: dullestPixel.x,
|
||||
y: dullestPixel.y,
|
||||
color: this.getPixelColor(data, dullestPixel.x, dullestPixel.y)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPixelColor(data: any[] | Uint8ClampedArray, x: number, y: number) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
return {
|
||||
r: data[index],
|
||||
g: data[index + 1],
|
||||
b: data[index + 2]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// // Export the class for use in browser environments
|
||||
// if (typeof window !== 'undefined') {
|
||||
// window.ImageBrightnessAnalyzer = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
|
||||
// // Export for module environments (if using a bundler)
|
||||
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
// module.exports = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
1
packages/core/index.ts
Normal file
1
packages/core/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./image-brightness-analyzer.ts"
|
||||
13
packages/core/package.json
Normal file
13
packages/core/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@nestri/core",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
"react-internal.js",
|
||||
"qwik.js"
|
||||
"qwik.js",
|
||||
"react-internal.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vercel/style-guide": "^5.2.0",
|
||||
@@ -22,4 +22,4 @@
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/typescript-config/react-library.json
Normal file
8
packages/typescript-config/react-library.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "React Library",
|
||||
"extends": "./base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@nestri/eslint-config/qwik.js"],
|
||||
extends: [
|
||||
"@nestri/eslint-config/qwik.js",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./tsconfig.lint.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
project: ["./tsconfig.json"],
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,46 +2,51 @@
|
||||
"name": "@nestri/ui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"tailwind.config.ts",
|
||||
"postcss.config.js",
|
||||
"globals.css"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./react": "./src/react/index.ts",
|
||||
"./tailwind.config": "./tailwind.config.js",
|
||||
"./globals.css": "./globals.css",
|
||||
"./postcss": "./post.config.js"
|
||||
"./postcss.config": "./postcss.config.js",
|
||||
"./tailwind.config": "./tailwind.config.js",
|
||||
"./image": "./src/image/index.ts",
|
||||
"./design": "./src/design/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"tailwind.config.js",
|
||||
"post.config.js",
|
||||
"globals.css",
|
||||
"postcss.config.js"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint . --max-warnings 0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "^1.8.0",
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "^0.5.5",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@fontsource/bricolage-grotesque": "^5.0.7",
|
||||
"@fontsource/geist-sans": "^5.0.3",
|
||||
"@nestri/eslint-config": "*",
|
||||
"@nestri/typescript-config": "*",
|
||||
"@nestri/eslint-config": "workspace:*",
|
||||
"@nestri/typescript-config": "workspace:*",
|
||||
"@nestri/core": "workspace:*",
|
||||
"@turbo/gen": "^1.12.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint": "^8.56.5",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^18.2.28",
|
||||
"@types/react-dom": "^18.2.13",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.1",
|
||||
"framer-motion": "^11.3.31",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^8.57.0",
|
||||
"framer-motion": "^11.3.24",
|
||||
"nprogress": "^0.2.0",
|
||||
"postcss": "^8.4.41",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-wrap-balancer": "^1.1.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
6
packages/ui/postcss.config.js
Normal file
6
packages/ui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
178
packages/ui/src/design/button-variants.ts
Normal file
178
packages/ui/src/design/button-variants.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
// The solid and outlined variants are based on CSS Pro 3D Buttons code (https://csspro.com/css-3d-buttons/)
|
||||
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export type ButtonVariantProps = {
|
||||
variant?: keyof typeof buttonVariants | undefined;
|
||||
intent?: keyof typeof solid.variants.intent;
|
||||
size?: keyof typeof baseButton.variants.size;
|
||||
};
|
||||
export type ButtonVariantIconProps = {
|
||||
type?: keyof typeof buttonIcon.variants.type;
|
||||
size?: keyof typeof buttonIcon.variants.size;
|
||||
};
|
||||
|
||||
const baseButton = tv({
|
||||
base: "group font-title flex justify-center select-none gap-1.5 items-center rounded-lg outline-2 outline-offset-2 focus-visible:outline outline-primary-6000 disabled:text-gray-400 disabled:border disabled:border-gray-300 dark:disabled:bg-gray-500/10 disabled:bg-gray-200 disabled:shadow-none disabled:hover:brightness-100 dark:disabled:text-gray-700 dark:disabled:shadow-none disabled:cursor-not-allowed dark:disabled:border dark:disabled:border-gray-700", // dark:disabled:[background-image:none]
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-sm h-7 px-3",
|
||||
sm: "text-sm h-8 px-3.5",
|
||||
md: "text-base h-9 px-4",
|
||||
lg: "text-base h-10 px-5",
|
||||
xl: "text-lg h-12 px-6",
|
||||
},
|
||||
iconOnlyButtonSize: {
|
||||
xs: "size-7",
|
||||
sm: "size-8",
|
||||
md: "size-9",
|
||||
lg: "size-10",
|
||||
xl: "size-12",
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const link = tv({
|
||||
// extend: baseButton,
|
||||
// base: "group transition-shadows relative ml-1.5 font-medium shadow-[inset_0_-0.2em_0_0_theme(colors.primary.200)] dark:shadow-[inset_0_-0.2em_0_0_theme(colors.primary.600)] duration-300 ease-out hover:shadow-[inset_0_-1em_0_0_theme(colors.primary.200)] dark:hover:shadow-[inset_0_-1em_0_0_theme(colors.primary.600)] text-primary-950 font-medium dark:text-primary-50", //"relative text-primary-950 font-medium dark:text-primary-50 hover:after:w-[calc(100%+2px)] focus:after:w-[calc(100%+2px)] px-1 after:-bottom-1 transition-all duration-[.2s] ease-[cubic-bezier(.165,.84,.44,1)] after:w-0 after:h-0.5 after:bg-primary-950 dark:after:bg-primary-50 after:absolute after:left-0 after:transition-[width] after:duration-[.2s] focus:shadow-none outline-none"
|
||||
base: "group relative ml-1.5 font-medium border-b-2 border-primary-950 dark:border-primary-50 hover:border-primary-950/60 dark:hover:border-primary-50/60 hover:text-primary-950/70 dark:hover:text-primary-50/70 text-primary-950 font-medium dark:text-primary-50", //"relative text-primary-950 font-medium dark:text-primary-50 hover:after:w-[calc(100%+2px)] focus:after:w-[calc(100%+2px)] px-1 after:-bottom-1 transition-all duration-[.2s] ease-[cubic-bezier(.165,.84,.44,1)] after:w-0 after:h-0.5 after:bg-primary-950 dark:after:bg-primary-50 after:absolute after:left-0 after:transition-[width] after:duration-[.2s] focus:shadow-none outline-none"
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-sm h-7 px-3",
|
||||
sm: "text-sm h-8 px-3.5",
|
||||
md: "text-base h-9 px-4",
|
||||
lg: "text-base h-10 px-5",
|
||||
xl: "text-lg h-12 px-6",
|
||||
},
|
||||
iconOnlyButtonSize: {
|
||||
xs: "size-7",
|
||||
sm: "size-8",
|
||||
md: "size-9",
|
||||
lg: "size-10",
|
||||
xl: "size-12",
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const solid = tv({
|
||||
extend: baseButton,
|
||||
base: "bg-gradient-to-b [box-shadow:rgba(255,255,255,0.25)_0px_1px_0px_0px_inset,var(--btn-border-color)_0px_0px_0px_1px] text-white hover:brightness-[1.1] transition-[filter] duration-150 ease-in-out active:brightness-95 dark:border-t dark:shadow-white/10 disabled:from-gray-200 disabled:to-gray-200 dark:disabled:text-gray-400 dark:disabled:from-gray-800 dark:disabled:to-gray-800",
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"from-primary-500 to-primary-600 [--btn-border-color:theme(colors.primary.600)] dark:border-primary-500/75",
|
||||
secondary:
|
||||
"from-secondary-500 to-secondary-600 [--btn-border-color:theme(colors.secondary.700)] dark:border-secondary-400/75",
|
||||
accent: "from-accent-500 to-accent-600 [--btn-border-color:theme(colors.accent.700)] dark:border-accent-400/75",
|
||||
danger: "from-danger-500 to-danger-600 [--btn-border-color:theme(colors.danger.700)] dark:border-danger-400/75",
|
||||
info: "from-info-500 to-info-600 [--btn-border-color:theme(colors.info.700)] dark:border-info-400/75",
|
||||
success:
|
||||
"from-success-500 to-success-600 [--btn-border-color:theme(colors.success.700)] dark:border-success-400/75",
|
||||
warning:
|
||||
"from-warning-400 to-warning-500 text-warning-950 [--btn-border-color:theme(colors.warning.600)] dark:border-warning-300",
|
||||
gray: "from-gray-500 to-gray-600 [--btn-border-color:theme(colors.gray.700)] dark:border-gray-500",
|
||||
neutral:
|
||||
"bg-gray-900 [background-image:radial-gradient(76%_151%_at_52%_-52%,rgba(255,255,255,0.5)_0%,transparent_100%)] [box-shadow:rgba(255,255,255,0.3)_0px_1px_0px_0px_inset,theme(colors.gray.950)_0px_0px_0px_1px] hover:brightness-125 dark:bg-white dark:text-gray-950 dark:border-gray-300",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const outlined = tv({
|
||||
extend: baseButton,
|
||||
base: "[--outline-radial-opacity:0.6] dark:[background-image:none] [--inner-border-color:1] dark:[--inner-border-color:0] dark:[--outline-radial-opacity:0.2] [background-image:radial-gradient(76%_151%_at_52%_-52%,rgba(255,255,255,var(--outline-radial-opacity))_0%,transparent_100%)] [box-shadow:rgba(255,255,255,var(--inner-border-color))_0px_1px_0px_0px_inset,var(--btn-border-color)_0px_0px_0px_1px,0px_1px_2px_rgba(0,0,0,0.1)] hover:brightness-[0.98] active:brightness-100 transtion-[filter] ease-in-out duration-150",
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"[--btn-border-color:theme(colors.primary.200)] dark:[--btn-border-color:theme(colors.primary.500/0.3)] text-primary-800 bg-primary-50 dark:text-primary-300 dark:bg-primary-500/5 dark:hover:bg-primary-500/10 dark:active:bg-primary-500/5",
|
||||
secondary:
|
||||
"[--btn-border-color:theme(colors.secondary.200)] dark:[--btn-border-color:theme(colors.secondary.500/0.3)] text-secondary-800 bg-secondary-50 dark:text-secondary-300 dark:bg-secondary-500/5 dark:hover:bg-secondary-500/10 dark:active:bg-secondary-500/5",
|
||||
accent: "[--btn-border-color:theme(colors.accent.200)] dark:[--btn-border-color:theme(colors.accent.500/0.3)] text-accent-800 bg-accent-50 dark:text-accent-300 dark:bg-accent-500/5 dark:hover:bg-accent-500/10 dark:active:bg-accent-500/5",
|
||||
danger: "[--btn-border-color:theme(colors.danger.200)] dark:[--btn-border-color:theme(colors.danger.500/0.3)] text-danger-800 bg-danger-50 dark:text-danger-300 dark:bg-danger-500/5 dark:hover:bg-danger-500/10 dark:active:bg-danger-500/5",
|
||||
info: "[--btn-border-color:theme(colors.info.200)] dark:[--btn-border-color:theme(colors.info.500/0.3)] text-info-800 bg-info-50 dark:text-info-300 dark:bg-info-500/5 dark:hover:bg-info-500/10 dark:active:bg-info-500/5",
|
||||
success:
|
||||
"[--btn-border-color:theme(colors.success.200)] dark:[--btn-border-color:theme(colors.success.500/0.3)] text-success-800 bg-success-100 dark:text-success-300 dark:bg-success-500/5 dark:hover:bg-success-500/10 dark:active:bg-success-500/5",
|
||||
warning:
|
||||
"[--btn-border-color:theme(colors.warning.200)] dark:[--btn-border-color:theme(colors.warning.500/0.3)] text-warning-800 bg-warning-50 dark:text-warning-300 dark:bg-warning-500/5 dark:hover:bg-warning-500/10 dark:active:bg-warning-500/5",
|
||||
gray: "[--btn-border-color:theme(colors.gray.200)] dark:[--btn-border-color:theme(colors.gray.500/0.3)] text-gray-800 bg-gray-50 dark:text-gray-300 dark:bg-gray-500/5 dark:hover:bg-gray-500/10 dark:active:bg-gray-500/5",
|
||||
neutral:
|
||||
"[--btn-border-color:theme(colors.gray.300)] dark:[--btn-border-color:theme(colors.gray.700)] text-gray-800 bg-gray-100 dark:text-white dark:bg-gray-500/5 dark:hover:bg-gray-500/10 dark:active:bg-gray-500/5",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const soft = tv({
|
||||
extend: baseButton,
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"text-primary-700 bg-primary-100 hover:bg-primary-200/75 active:bg-primary-100 dark:text-primary-300 dark:bg-primary-500/10 dark:hover:bg-primary-500/15 dark:active:bg-primary-500/10",
|
||||
secondary:
|
||||
"text-secondary-700 bg-secondary-100 hover:bg-secondary-200/75 active:bg-secondary-100 dark:text-secondary-300 dark:bg-secondary-500/10 dark:hover:bg-secondary-500/15 dark:active:bg-secondary-500/10",
|
||||
accent: "text-accent-700 bg-accent-100 hover:bg-accent-200/75 active:bg-accent-100 dark:text-accent-300 dark:bg-accent-500/10 dark:hover:bg-accent-500/15 dark:active:bg-accent-500/10",
|
||||
danger: "text-danger-700 bg-danger-100 hover:bg-danger-200/75 active:bg-danger-100 dark:text-danger-300 dark:bg-danger-500/10 dark:hover:bg-danger-500/15 dark:active:bg-danger-500/10",
|
||||
info: "text-info-700 bg-info-100 hover:bg-info-200/75 active:bg-info-100 dark:text-info-300 dark:bg-info-500/10 dark:hover:bg-info-500/15 dark:active:bg-info-500/10",
|
||||
success:
|
||||
"text-success-700 bg-success-100 hover:bg-success-200/75 active:bg-success-100 dark:text-success-300 dark:bg-success-500/10 dark:hover:bg-success-500/15 dark:active:bg-success-500/10",
|
||||
warning:
|
||||
"text-warning-700 bg-warning-100 hover:bg-warning-200/75 active:bg-warning-100 dark:text-warning-300 dark:bg-warning-500/10 dark:hover:bg-warning-500/15 dark:active:bg-warning-500/10",
|
||||
gray: "text-gray-800 bg-gray-100 hover:bg-gray-200/75 active:bg-gray-100 dark:text-gray-300 dark:bg-gray-500/10 dark:hover:bg-gray-500/15 dark:active:bg-gray-500/10",
|
||||
neutral:
|
||||
"text-gray-950 bg-gray-100 hover:bg-gray-950 hover:text-white active:text-white active:bg-gray-900 dark:text-gray-300 dark:bg-gray-500/10 dark:hover:bg-white dark:hover:text-gray-950 dark:active:bg-gray-200 dark:active:text-gray-950",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ghost = tv({
|
||||
extend: baseButton,
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"hover:bg-gray-100 active:bg-primary-200/75 dark:hover:bg-gray-800 dark:active:bg-primary-500/15",
|
||||
secondary:
|
||||
"text-secondary-600 hover:bg-secondary-100 active:bg-secondary-200/75 dark:text-secondary-400 dark:hover:bg-secondary-500/10 dark:active:bg-secondary-500/15",
|
||||
accent: "text-accent-600 hover:bg-accent-100 active:bg-accent-200/75 dark:text-accent-400 dark:hover:bg-accent-500/10 dark:active:bg-accent-500/15",
|
||||
danger: "text-danger-600 hover:bg-danger-100 active:bg-danger-200/75 dark:text-danger-400 dark:hover:bg-danger-500/10 dark:active:bg-danger-500/15",
|
||||
info: "text-info-600 hover:bg-info-100 active:bg-info-200/75 dark:text-info-400 dark:hover:bg-info-500/10 dark:active:bg-info-500/15",
|
||||
success:
|
||||
"text-success-600 hover:bg-success-100 active:bg-success-200/75 dark:text-success-400 dark:hover:bg-success-500/10 dark:active:bg-success-500/15",
|
||||
warning:
|
||||
"text-warning-600 hover:bg-warning-100 active:bg-warning-200/75 dark:text-warning-400 dark:hover:bg-warning-500/10 dark:active:bg-warning-500/15",
|
||||
gray: "text-gray-800 hover:bg-gray-100 active:bg-gray-200/75 dark:text-gray-300 dark:hover:bg-gray-500/10 dark:active:bg-gray-500/15",
|
||||
neutral:
|
||||
"text-gray-950 hover:bg-gray-950 hover:text-white active:text-white active:bg-gray-900 dark:text-white dark:hover:bg-white dark:hover:text-gray-950 dark:active:bg-gray-200 dark:active:text-gray-950",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const buttonIcon = tv({
|
||||
variants: {
|
||||
type: {
|
||||
leading: "-ml-1",
|
||||
trailing: "-mr-1",
|
||||
only: "m-auto",
|
||||
},
|
||||
size: {
|
||||
xs: "size-3.5",
|
||||
sm: "size-4",
|
||||
md: "size-[1.125rem]",
|
||||
lg: "size-5",
|
||||
xl: "size-6",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const buttonVariants = {
|
||||
solid,
|
||||
outlined,
|
||||
soft,
|
||||
ghost,
|
||||
link
|
||||
};
|
||||
215
packages/ui/src/design/form.ts
Normal file
215
packages/ui/src/design/form.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const form = tv({
|
||||
slots: {
|
||||
label: "text-nowrap text-[--title-text-color]",
|
||||
input: "[--btn-radius:lg] w-full px-[--input-px] bg-transparent peer transition-[outline] placeholder-[--placeholder-text-color] text-[--title-text-color] rounded-[--btn-radius] disabled:opacity-50",
|
||||
message: "mt-2 text-[--caption-text-color]",
|
||||
icon: "absolute inset-y-0 my-auto text-[--placeholder-text-color] pointer-events-none",
|
||||
field: "relative group *:has-[:disabled]:opacity-50 *:has-[:disabled]:pointer-events-none data-[invalid]:[--caption-text-color:theme(colors.danger.600)] dark:data-[invalid]:[--caption-text-color:theme(colors.danger.400)] data-[valid]:[--caption-text-color:theme(colors.success.600)] dark:data-[valid]:[--caption-text-color:theme(colors.success.400)]",
|
||||
textarea: "py-[calc(var(--input-px)/1.5)] h-auto",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
outlined: {
|
||||
input: "outline-2 focus:outline-primary-600 -outline-offset-1 focus:outline border data-[invalid]:border-danger-600 focus:data-[invalid]:outline-danger-600 dark:data-[invalid]:border-danger-500 dark:focus:data-[invalid]:outline-danger-500 data-[valid]:border-success-600 focus:data-[valid]:outline-success-600 dark:data-[valid]:border-success-500 dark:focus:data-[valid]:outline-success-500",
|
||||
},
|
||||
soft: {
|
||||
input: "outline-none bg-[--ui-soft-bg] focus:brightness-95 dark:focus:brightness-105 data-[invalid]:[--ui-soft-bg:theme(colors.danger.100)] dark:data-[invalid]:[--ui-soft-bg:theme(colors.danger.800/0.25)] data-[valid]:[--ui-soft-bg:theme(colors.success.100)] dark:data-[valid]:[--ui-soft-bg:theme(colors.success.800/0.25)]",
|
||||
},
|
||||
mixed: {
|
||||
input: "placeholder-gray-950/50 dark:placeholder-gray-50/50 shadow-sm hover:shadow-lg dark:hover:shadow-sm shadow-gray-950/50 dark:shadow-gray-50 outline-2 focus:outline-primary-600 focus:outline -outline-offset-1 border border-primary-950 dark:border-primary-50 bg-gray-100 dark:bg-gray-800 data-[invalid]:border-2 data-[invalid]:border-danger-600 focus:data-[invalid]:outline-danger-600 dark:data-[invalid]:border-danger-500 dark:focus:data-[invalid]:outline-danger-500 data-[valid]:border-success-600 focus:data-[valid]:outline-success-600 dark:data-[valid]:border-success-500 dark:focus:data-[valid]:outline-success-500",
|
||||
},
|
||||
plain: {
|
||||
input: "rounded-none px-0 outline-none bg-transparent invalid:text-danger-600 dark:invalid:text-danger-400",
|
||||
},
|
||||
bottomOutlined: {
|
||||
input: "rounded-none transition-[border] px-0 focus:outline-none border-b focus:border-b-2 focus:border-primary-600 data-[invalid]:border-danger-400 dark:data-[invalid]:border-danger-600 data-[valid]:border-success-400 dark:data-[valid]:border-success-600",
|
||||
},
|
||||
},
|
||||
size: {
|
||||
xs: {
|
||||
message: "text-xs",
|
||||
},
|
||||
sm: {
|
||||
label: "text-sm",
|
||||
message: "text-sm",
|
||||
input: "text-sm h-8 [--input-px:theme(spacing[2.5])]",
|
||||
field: "[--input-px:theme(spacing[2.5])]",
|
||||
},
|
||||
md: {
|
||||
label: "text-base",
|
||||
message: "text-base",
|
||||
input: "text-sm h-9 [--input-px:theme(spacing[3])]",
|
||||
field: "[--input-px:theme(spacing[3])]",
|
||||
},
|
||||
lg: {
|
||||
label: "text-lg",
|
||||
input: "text-base h-10 [--input-px:theme(spacing[4])]",
|
||||
field: "[--input-px:theme(spacing[4])]",
|
||||
},
|
||||
xl: {
|
||||
label: "text-xl",
|
||||
input: "text-base h-12 [--input-px:theme(spacing[5])]",
|
||||
field: "[--input-px:theme(spacing[5])]",
|
||||
},
|
||||
},
|
||||
fancy: {
|
||||
true: {
|
||||
input: "shadow-inner shadow-gray-950/5 dark:shadow-gray-950/35",
|
||||
}
|
||||
},
|
||||
floating: {
|
||||
true: {
|
||||
label: "absolute block inset-y-0 text-base left-[--input-px] h-fit text-nowrap my-auto text-[--caption-text-color] pointer-events-none transition duration-150 scale-[.8] origin-top-left peer-placeholder-shown:scale-100 peer-focus:scale-[.8] peer-placeholder-shown:translate-y-0",
|
||||
},
|
||||
},
|
||||
asTextarea: {
|
||||
true: {
|
||||
label: "top-[calc(var(--input-px)/1.5)] mt-0",
|
||||
},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
floating: true,
|
||||
variant: ["bottomOutlined", "plain"],
|
||||
class: {
|
||||
input: "px-0",
|
||||
label: "left-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "soft", "bottomOutlined", "mixed"],
|
||||
size: "xl",
|
||||
class: {
|
||||
field: "[--input-px:theme(spacing[2.5])]",
|
||||
input: "pt-3.5 h-14",
|
||||
label: "-translate-y-2.5 peer-focus:-translate-y-2.5",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["soft", "bottomOutlined"],
|
||||
size: "lg",
|
||||
class: {
|
||||
input: "pt-4 h-12",
|
||||
label: "-translate-y-2 peer-focus:-translate-y-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "mixed"],
|
||||
size: "lg",
|
||||
class: {
|
||||
input: "h-12",
|
||||
label: "-translate-y-[21.5px] peer-focus:-translate-y-[21.5px]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
size: "md",
|
||||
class: {
|
||||
input: "h-9",
|
||||
label: "-translate-y-[15.5px] peer-focus:-translate-y-[15.5px]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
size: "sm",
|
||||
class: {
|
||||
input: "h-8",
|
||||
label: "text-base -translate-y-[13.5px] peer-focus:-translate-y-[13.5px] peer-placeholder-shown:text-sm peer-focus:text-base",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
size: ["sm", "md", "lg"],
|
||||
variant: ["outlined", "mixed"],
|
||||
class: {
|
||||
label: "peer-placeholder-shown:before:scale-x-0 peer-focus:before:scale-x-100 before:scale-x-100 before:absolute peer-placeholder-shown:before:transition-none before:transition-none peer-focus:before:transition peer-focus:before:delay-[.01s] before:duration-500 before:-z-[1] peer-placeholder-shown:before:-top-[9px] before:top-[48%] peer-focus:before:top-[44%] before:-inset-x-1 before:h-0.5 before:my-auto group-has-[:focus]:before:h-[3px] before:bg-white dark:before:bg-[--ui-bg]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
variant: ["outlined", "mixed"],
|
||||
class: {
|
||||
label: "translate-x-px",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["lg", "xl"],
|
||||
variant: "bottomOutlined",
|
||||
class: {
|
||||
input: "pt-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: "plain",
|
||||
class: {
|
||||
input: "py-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["xl"],
|
||||
class: {
|
||||
input: "pt-7",
|
||||
label: "-translate-y-1 peer-focus:-translate-y-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["lg"],
|
||||
class: {
|
||||
input: "pt-6",
|
||||
label: "-translate-y-1 peer-focus:-translate-y-1 before:hidden",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["md"],
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
class: {
|
||||
label: "-translate-y-[17.5px] peer-focus:-translate-y-[17.5px]",
|
||||
},
|
||||
},
|
||||
{
|
||||
floating: true,
|
||||
asTextarea: true,
|
||||
size: ["sm"],
|
||||
variant: ["outlined", "bottomOutlined", "mixed"],
|
||||
class: {
|
||||
label: "-translate-y-4 peer-focus:-translate-y-4",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type FormProps = {
|
||||
variant?: keyof typeof form.variants.variant;
|
||||
size?: keyof typeof form.variants.size;
|
||||
floating?: boolean;
|
||||
asTextarea?: boolean;
|
||||
};
|
||||
|
||||
export type InputProps = Omit<FormProps, "asTextarea"> & {
|
||||
size?: Exclude<FormProps["size"], "xs">;
|
||||
fancy?: boolean;
|
||||
};
|
||||
|
||||
export type LabelProps = FormProps & {
|
||||
size?: Exclude<FormProps["size"], "xs">;
|
||||
};
|
||||
|
||||
export type MessageProps = {
|
||||
size?: Exclude<FormProps["size"], "lg" | "xl">;
|
||||
};
|
||||
4
packages/ui/src/design/index.ts
Normal file
4
packages/ui/src/design/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./button-variants"
|
||||
export * from "./typography"
|
||||
export * from "./utils"
|
||||
export * from "./form"
|
||||
401
packages/ui/src/design/typography.ts
Normal file
401
packages/ui/src/design/typography.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const base = tv({
|
||||
variants: {
|
||||
weight: {
|
||||
black: "font-black",
|
||||
bold: "font-bold",
|
||||
semibold: "font-semibold",
|
||||
medium: "font-medium",
|
||||
normal: "font-normal",
|
||||
},
|
||||
align: {
|
||||
left: "text-left",
|
||||
center: "text-center",
|
||||
right: "text-right",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xl",
|
||||
weight: "normal",
|
||||
},
|
||||
});
|
||||
|
||||
export const caption = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "text-gray-500",
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-xs",
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
},
|
||||
neutral: {
|
||||
true: "text-gray-950 dark:text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "sm",
|
||||
weight: "normal",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const text = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "text-gray-700",
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
},
|
||||
neutral: {
|
||||
true: "text-gray-950 dark:text-white",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
weight: "normal",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const list = tv(
|
||||
{
|
||||
extend: text,
|
||||
base: "list-outside pl-4",
|
||||
variants: {
|
||||
type: {
|
||||
disc: "list-disc",
|
||||
decimal: "list-decimal",
|
||||
none: "list-none",
|
||||
},
|
||||
inside: {
|
||||
true: "list-outside pl-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "base",
|
||||
type: "disc",
|
||||
weight: "normal",
|
||||
inside: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const link = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "transition",
|
||||
variants: {
|
||||
size: {
|
||||
xs: "text-xs",
|
||||
sm: "text-sm",
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
},
|
||||
intent: {
|
||||
primary:
|
||||
"text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-500",
|
||||
secondary:
|
||||
"text-secondary-600 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-500",
|
||||
accent: "text-accent-600 hover:text-accent-700 dark:text-accent-400 dark:hover:text-accent-500",
|
||||
info: "text-info-600 hover:text-info-700 dark:text-info-400 dark:hover:text-info-500",
|
||||
danger: "text-danger-600 hover:text-danger-700 dark:text-danger-400 dark:hover:text-danger-500",
|
||||
success:
|
||||
"text-success-600 hover:text-success-700 dark:text-success-400 dark:hover:text-success-500",
|
||||
warning:
|
||||
"text-warning-700 hover:text-warning-600 dark:text-warning-400 dark:hover:text-warning-500",
|
||||
gray: "text-gray-700",
|
||||
neutral:
|
||||
"text-gray-950 hover:text-gray-800 dark:text-white dark:hover:text-gray-200",
|
||||
},
|
||||
variant: {
|
||||
plain: "",
|
||||
underlined: "underline",
|
||||
ghost: "hover:underline",
|
||||
animated:
|
||||
"relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:scale-x-0 before:origin-right hover:before:origin-left hover:before:scale-x-100 before:transition before:duration-200",
|
||||
},
|
||||
visited: {
|
||||
true: "visited:text-accent-600 dark:visited:text-accent-400",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: ["plain", "ghost", "underlined"],
|
||||
intent: "gray",
|
||||
class: "hover:text-gray-950 dark:hover:text-white",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "primary",
|
||||
class: "before:bg-primary-600/50 dark:before:bg-primary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "info",
|
||||
class: "before:bg-info-600/50 dark:before:bg-info-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "neutral",
|
||||
class: "before:bg-gray-950/50 dark:before:bg-white/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "gray",
|
||||
class: "before:bg-gray-600/50 dark:before:bg-gray-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "secondary",
|
||||
class: "before:bg-secondary-600/50 dark:before:bg-secondary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "accent",
|
||||
class: "before:bg-accent-600/50 dark:before:bg-accent-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "danger",
|
||||
class: "before:bg-danger-600/50 dark:before:bg-danger-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "success",
|
||||
class: "before:bg-success-600/50 dark:before:bg-success-400/50",
|
||||
},
|
||||
{
|
||||
variant: "animated",
|
||||
intent: "warning",
|
||||
class: "before:bg-warning-600/50 dark:before:bg-warning-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "primary",
|
||||
class: "decoration-primary-600/50 dark:decoration-primary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "info",
|
||||
class: "decoration-info-600/50 dark:decoration-info-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "gray",
|
||||
class: "decoration-gray-600/50 dark:decoration-gray-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "neutral",
|
||||
class: "decoration-gray-950/25 dark:decoration-white/25",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "secondary",
|
||||
class: "decoration-secondary-600/50 dark:decoration-secondary-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "accent",
|
||||
class: "decoration-accent-600/50 dark:decoration-accent-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "danger",
|
||||
class: "decoration-danger-600/50 dark:decoration-danger-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "success",
|
||||
class: "decoration-success-600/50 dark:decoration-success-400/50",
|
||||
},
|
||||
{
|
||||
variant: "underlined",
|
||||
intent: "warning",
|
||||
class: "decoration-warning-600/50 dark:decoration-warning-400/50",
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
intent: "primary",
|
||||
variant: "ghost",
|
||||
size: "base",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const display = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "block text-gray-950 dark:text-gray-50",
|
||||
variants: {
|
||||
size: {
|
||||
"4xl": "text-4xl",
|
||||
"5xl": "text-5xl",
|
||||
"6xl": "text-6xl",
|
||||
"7xl": "text-7xl",
|
||||
"8xl": "text-8xl",
|
||||
"9xl": "text-9xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "6xl",
|
||||
weight: "bold",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const title = tv(
|
||||
{
|
||||
extend: base,
|
||||
base: "block text-gray-950",
|
||||
variants: {
|
||||
size: {
|
||||
base: "text-base",
|
||||
lg: "text-lg",
|
||||
xl: "text-xl",
|
||||
"2xl": "text-2xl",
|
||||
"3xl": "text-3xl",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xl",
|
||||
weight: "semibold",
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: ["sm", "md", "lg", "xl", "2xl"],
|
||||
},
|
||||
);
|
||||
|
||||
export const codeTheme = tv({
|
||||
base: "text-sm inline-block border rounded-md py-px px-1",
|
||||
variants: {
|
||||
intent: {
|
||||
primary:
|
||||
"bg-primary-50 text-primary-600 dark:text-primary-300 border-primary-200 dark:border-primary-500/20 dark:bg-primary-500/5",
|
||||
secondary:
|
||||
"bg-secondary-50 text-secondary-600 dark:text-secondary-300 border-secondary-200 dark:border-secondary-500/20 dark:bg-secondary-500/5",
|
||||
accent: "bg-accent-50 text-accent-600 dark:text-accent-300 border-accent-200 dark:border-accent-500/20 dark:bg-accent-500/5",
|
||||
gray: "bg-gray-50 text-gray-700 dark:border-gray-500/20 dark:bg-gray-500/5 dark:border-gray-500/20",
|
||||
neutral: "bg-gray-50 text-gray-950 dark:text-white dark:bg-gray-500/5 dark:border-gray-500/20",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
intent: "gray",
|
||||
},
|
||||
});
|
||||
|
||||
export const kbdTheme = tv({
|
||||
base: "inline-flex items-center justify-center text-gray-800 dark:text-white h-5 text-[11px] min-w-5 px-1.5 rounded font-sans bg-gray-100 dark:bg-white/10 ring-1 border-b border-t border-t-white border-b-gray-200 dark:border-t-transparent dark:border-b-gray-950 ring-gray-300 dark:ring-white/15",
|
||||
});
|
||||
|
||||
export type CodeThemeProps = {
|
||||
intent?: keyof typeof codeTheme.variants.intent;
|
||||
};
|
||||
|
||||
export type Weight = keyof typeof base.variants.weight;
|
||||
export type Align = keyof typeof base.variants.align;
|
||||
|
||||
type BaseTextProps = {
|
||||
weight?: Weight;
|
||||
align?: Align;
|
||||
};
|
||||
|
||||
export type CaptionProps = BaseTextProps & {
|
||||
size?: keyof typeof caption.variants.size;
|
||||
neutral?: boolean;
|
||||
};
|
||||
|
||||
export type TextProps = BaseTextProps & {
|
||||
size?: keyof typeof text.variants.size;
|
||||
neutral?: boolean;
|
||||
};
|
||||
|
||||
export type ListProps = BaseTextProps & {
|
||||
size?: keyof typeof text.variants.size;
|
||||
type?: keyof typeof list.variants.type;
|
||||
inside?: boolean;
|
||||
neutral?: boolean;
|
||||
};
|
||||
|
||||
export type LinkProps = BaseTextProps & {
|
||||
size?: keyof typeof text.variants.size | keyof typeof link.variants.size;
|
||||
variant?: keyof typeof link.variants.variant;
|
||||
intent?: keyof typeof link.variants.intent;
|
||||
visited?: boolean;
|
||||
};
|
||||
|
||||
export type TitleProps = BaseTextProps & {
|
||||
size?: keyof typeof title.variants.size;
|
||||
};
|
||||
|
||||
export type TitleSizeProp =
|
||||
| TitleProps["size"]
|
||||
| {
|
||||
initial?: TitleProps["size"];
|
||||
sm?: TitleProps["size"];
|
||||
md?: TitleProps["size"];
|
||||
lg?: TitleProps["size"];
|
||||
xl?: TitleProps["size"];
|
||||
xxl?: TitleProps["size"];
|
||||
};
|
||||
|
||||
export type TextSizeProp =
|
||||
| TextProps["size"]
|
||||
| {
|
||||
initial?: TextProps["size"];
|
||||
sm?: TextProps["size"];
|
||||
md?: TextProps["size"];
|
||||
lg?: TextProps["size"];
|
||||
xl?: TextProps["size"];
|
||||
xxl?: TextProps["size"];
|
||||
};
|
||||
|
||||
export type DisplayProps = BaseTextProps & {
|
||||
size?: keyof typeof display.variants.size;
|
||||
};
|
||||
|
||||
export type TextWeightProp =
|
||||
| Weight
|
||||
| {
|
||||
initial?: Weight;
|
||||
sm?: Weight;
|
||||
md?: Weight;
|
||||
lg?: Weight;
|
||||
xl?: Weight;
|
||||
xxl?: Weight;
|
||||
};
|
||||
|
||||
export type TextAlignProp =
|
||||
| Align
|
||||
| {
|
||||
initial?: Align;
|
||||
sm?: Align;
|
||||
md?: Align;
|
||||
lg?: Align;
|
||||
xl?: Align;
|
||||
xxl?: Align;
|
||||
};
|
||||
6
packages/ui/src/design/utils.ts
Normal file
6
packages/ui/src/design/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
17
packages/ui/src/fonts.tsx
Normal file
17
packages/ui/src/fonts.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { component$, Slot } from "@builder.io/qwik";
|
||||
|
||||
// import SansNormal from "@fontsource/geist-sans/400.css?inline"
|
||||
import "@fontsource/geist-sans/400.css"
|
||||
import "@fontsource/geist-sans/500.css"
|
||||
import "@fontsource/geist-sans/600.css"
|
||||
import "@fontsource/geist-sans/700.css"
|
||||
import "@fontsource/bricolage-grotesque/500.css"
|
||||
import "@fontsource/bricolage-grotesque/700.css"
|
||||
import "@fontsource/bricolage-grotesque/800.css"
|
||||
|
||||
export const Fonts = component$(() => {
|
||||
|
||||
// useStyles$(SansNormal);
|
||||
|
||||
return <Slot />;
|
||||
});
|
||||
132
packages/ui/src/footer.tsx
Normal file
132
packages/ui/src/footer.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable qwik/jsx-img */
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
import { MotionComponent, transition } from "@nestri/ui/react"
|
||||
import { GithubBanner } from "./github-banner";
|
||||
|
||||
{/*
|
||||
|
||||
|
||||
*/}
|
||||
const socialMedia = [
|
||||
{
|
||||
link: "https://github.com/nestriness/nestri",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 .999c-6.074 0-11 5.05-11 11.278c0 4.983 3.152 9.21 7.523 10.702c.55.104.727-.246.727-.543v-2.1c-3.06.683-3.697-1.33-3.697-1.33c-.5-1.304-1.222-1.65-1.222-1.65c-.998-.7.076-.686.076-.686c1.105.08 1.686 1.163 1.686 1.163c.98 1.724 2.573 1.226 3.201.937c.098-.728.383-1.226.698-1.508c-2.442-.286-5.01-1.253-5.01-5.574c0-1.232.429-2.237 1.132-3.027c-.114-.285-.49-1.432.107-2.985c0 0 .924-.303 3.026 1.156c.877-.25 1.818-.375 2.753-.38c.935.005 1.876.13 2.755.38c2.1-1.459 3.023-1.156 3.023-1.156c.598 1.554.222 2.701.108 2.985c.706.79 1.132 1.796 1.132 3.027c0 4.332-2.573 5.286-5.022 5.565c.394.35.754 1.036.754 2.088v3.095c0 .3.176.652.734.542C19.852 21.484 23 17.258 23 12.277C23 6.048 18.075.999 12 .999" /></svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
link: "https://discord.com/invite/Y6etn3qKZ3",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M20.317 4.492c-1.53-.69-3.17-1.2-4.885-1.49a.075.075 0 0 0-.079.036c-.21.369-.444.85-.608 1.23a18.6 18.6 0 0 0-5.487 0a12 12 0 0 0-.617-1.23A.08.08 0 0 0 8.562 3c-1.714.29-3.354.8-4.885 1.491a.1.1 0 0 0-.032.027C.533 9.093-.32 13.555.099 17.961a.08.08 0 0 0 .031.055a20 20 0 0 0 5.993 2.98a.08.08 0 0 0 .084-.026a14 14 0 0 0 1.226-1.963a.074.074 0 0 0-.041-.104a13 13 0 0 1-1.872-.878a.075.075 0 0 1-.008-.125q.19-.14.372-.287a.08.08 0 0 1 .078-.01c3.927 1.764 8.18 1.764 12.061 0a.08.08 0 0 1 .079.009q.18.148.372.288a.075.075 0 0 1-.006.125q-.895.515-1.873.877a.075.075 0 0 0-.041.105c.36.687.772 1.341 1.225 1.962a.08.08 0 0 0 .084.028a20 20 0 0 0 6.002-2.981a.08.08 0 0 0 .032-.054c.5-5.094-.838-9.52-3.549-13.442a.06.06 0 0 0-.031-.028M8.02 15.278c-1.182 0-2.157-1.069-2.157-2.38c0-1.312.956-2.38 2.157-2.38c1.21 0 2.176 1.077 2.157 2.38c0 1.312-.956 2.38-2.157 2.38m7.975 0c-1.183 0-2.157-1.069-2.157-2.38c0-1.312.955-2.38 2.157-2.38c1.21 0 2.176 1.077 2.157 2.38c0 1.312-.946 2.38-2.157 2.38" /></svg>)
|
||||
},
|
||||
{
|
||||
link: "https://www.reddit.com/r/nestri",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0m4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.37 2.37 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876c-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585c1.275-.79 2.881-1.291 4.64-1.365v-.01a3.23 3.23 0 0 1 2.88-3.207a2 2 0 0 1 1.959-1.595m-8.085 8.376c-.784 0-1.459.78-1.506 1.797s.64 1.429 1.426 1.429s1.371-.369 1.418-1.385s-.553-1.841-1.338-1.841m7.406 0c-.786 0-1.385.824-1.338 1.841s.634 1.385 1.418 1.385c.785 0 1.473-.413 1.426-1.429c-.046-1.017-.721-1.797-1.506-1.797m-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305a3.2 3.2 0 0 0 2.953 1.964a3.2 3.2 0 0 0 2.953-1.964a.222.222 0 0 0-.184-.305a28 28 0 0 0-2.769-.135" /></svg>)
|
||||
},
|
||||
{
|
||||
link: "https://x.com/nestriness",
|
||||
icon: () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M8 2H1l8.26 11.015L1.45 22H4.1l6.388-7.349L16 22h7l-8.608-11.478L21.8 2h-2.65l-5.986 6.886zm9 18L5 4h2l12 16z" /></svg>
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
export const Footer = component$(() => {
|
||||
return (
|
||||
<>
|
||||
<GithubBanner />
|
||||
<footer class="flex justify-center flex-col items-center w-full pt-8 sm:pb-0 pb-8 [&>*]:w-full px-3">
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
as="div"
|
||||
class="w-full max-w-xl mx-auto z-[5] flex flex-col border-t-2 dark:border-gray-50/50 border-gray-950/50">
|
||||
<section class="flex justify-between items-center py-6 border-b-2 dark:border-gray-50/50 border-gray-950/50" >
|
||||
<div class="flex flex-row gap-1 h-max items-center justify-center">
|
||||
<svg
|
||||
width={40} height={40}
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layer1">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
<h3 class="text-lg font-extrabold font-title">Nestri</h3>
|
||||
</div>
|
||||
<p class="text-gray-950 dark:text-gray-50">Your games. Your rules.</p>
|
||||
</section>
|
||||
<section class="gap-4 grid grid-cols-2 sm:grid-cols-3 justify-around py-6 items-start" >
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="font-title text-sm font-bold" >Product</h2>
|
||||
<div class="text-gray-950/70 dark:text-gray-50/70 flex flex-col gap-2" >
|
||||
<Link href="/pricing" class="text-base hover:text-primary-500" >Pricing</Link>
|
||||
<Link href="/changelog" class="text-base hover:text-primary-500" >Changelog</Link>
|
||||
<p class="text-base opacity-50 cursor-not-allowed" >Docs</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="font-title text-sm font-bold" >Company</h2>
|
||||
<div class="text-gray-950/70 dark:text-gray-50/70 flex flex-col gap-2" >
|
||||
<Link href="/contact" class="text-base hover:text-primary-500" >Contact Us</Link>
|
||||
<p class="text-base opacity-50 cursor-not-allowed" >Open Nestri</p>
|
||||
<p class="text-base opacity-50 cursor-not-allowed" >Blog</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="font-title text-sm font-bold" >Relations</h2>
|
||||
<div class="text-gray-950/70 dark:text-gray-50/70 flex flex-col gap-2" >
|
||||
<Link href="/terms" class="text-base hover:text-primary-500" >Terms of Service</Link>
|
||||
<Link href="/privacy" class="text-base hover:text-primary-500" >Privacy Policy</Link>
|
||||
{/**Social Media Icons with Links */}
|
||||
<div class="flex flex-row gap-3">
|
||||
{socialMedia.map((item) => (
|
||||
<Link key={item.link} href={item.link} class="hover:text-primary-500" target="_blank" rel="noopener noreferrer">
|
||||
{item.icon()}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{
|
||||
...transition,
|
||||
duration: 0.8,
|
||||
delay: 0.7
|
||||
}}
|
||||
client:load
|
||||
as="div" class="w-full sm:flex z-[1] hidden pointer-events-none overflow-hidden -mt-[100px] justify-center items-center flex-col" >
|
||||
<section class='my-0 bottom-0 text-[100%] max-w-[1440px] pointer-events-none w-full flex items-center translate-y-[45%] justify-center relative overflow-hidden px-2 z-10 [&_svg]:w-full [&_svg]:max-w-[1440px] [&_svg]:h-full [&_svg]:opacity-70' >
|
||||
<svg viewBox="0 0 498.05 70.508" xmlns="http://www.w3.org/2000/svg" height={157} width={695} class="" >
|
||||
<g stroke-linecap="round" fill-rule="evenodd" font-size="9pt" stroke="currentColor" stroke-width="0.25mm" fill="currentColor" style="stroke:currentColor;stroke-width:0.25mm;fill:currentColor">
|
||||
<path
|
||||
fill="url(#paint1)"
|
||||
pathLength="1"
|
||||
stroke="url(#paint1)"
|
||||
d="M 261.23 41.65 L 212.402 41.65 Q 195.313 41.65 195.313 27.002 L 195.313 14.795 A 17.814 17.814 0 0 1 196.311 8.57 Q 199.443 0.146 212.402 0.146 L 283.203 0.146 L 283.203 14.844 L 217.236 14.844 Q 215.337 14.844 214.945 16.383 A 3.67 3.67 0 0 0 214.844 17.285 L 214.844 24.561 Q 214.844 27.002 217.236 27.002 L 266.113 27.002 Q 283.203 27.002 283.203 41.65 L 283.203 53.857 A 17.814 17.814 0 0 1 282.205 60.083 Q 279.073 68.506 266.113 68.506 L 195.313 68.506 L 195.313 53.809 L 261.23 53.809 A 3.515 3.515 0 0 0 262.197 53.688 Q 263.672 53.265 263.672 51.367 L 263.672 44.092 A 3.515 3.515 0 0 0 263.551 43.126 Q 263.128 41.65 261.23 41.65 Z M 185.547 53.906 L 185.547 68.506 L 114.746 68.506 Q 97.656 68.506 97.656 53.857 L 97.656 14.795 A 17.814 17.814 0 0 1 98.655 8.57 Q 101.787 0.146 114.746 0.146 L 168.457 0.146 Q 185.547 0.146 185.547 14.795 L 185.547 31.885 A 17.827 17.827 0 0 1 184.544 38.124 Q 181.621 45.972 170.174 46.538 A 36.906 36.906 0 0 1 168.457 46.582 L 117.188 46.582 L 117.236 51.465 Q 117.236 53.906 119.629 53.955 L 185.547 53.906 Z M 19.531 14.795 L 19.531 68.506 L 0 68.506 L 0 0.146 L 70.801 0.146 Q 87.891 0.146 87.891 14.795 L 87.891 68.506 L 68.359 68.506 L 68.359 17.236 Q 68.359 14.795 65.967 14.795 L 19.531 14.795 Z M 449.219 68.506 L 430.176 46.533 L 400.391 46.533 L 400.391 68.506 L 380.859 68.506 L 380.859 0.146 L 451.66 0.146 A 24.602 24.602 0 0 1 458.423 0.994 Q 466.007 3.166 468.021 10.907 A 25.178 25.178 0 0 1 468.75 17.236 L 468.75 31.885 A 18.217 18.217 0 0 1 467.887 37.73 Q 465.954 43.444 459.698 45.455 A 23.245 23.245 0 0 1 454.492 46.436 L 473.633 68.506 L 449.219 68.506 Z M 292.969 0 L 371.094 0.098 L 371.094 14.795 L 341.846 14.795 L 341.846 68.506 L 322.266 68.506 L 322.217 14.795 L 292.969 14.844 L 292.969 0 Z M 478.516 0.146 L 498.047 0.146 L 498.047 68.506 L 478.516 68.506 L 478.516 0.146 Z M 400.391 14.844 L 400.391 31.885 L 446.826 31.885 Q 448.726 31.885 449.117 30.345 A 3.67 3.67 0 0 0 449.219 29.443 L 449.219 17.285 Q 449.219 14.844 446.826 14.844 L 400.391 14.844 Z M 117.188 31.836 L 163.574 31.934 Q 165.528 31.895 165.918 30.355 A 3.514 3.514 0 0 0 166.016 29.492 L 166.016 17.236 Q 166.016 15.337 164.476 14.945 A 3.67 3.67 0 0 0 163.574 14.844 L 119.629 14.795 Q 117.188 14.795 117.188 17.188 L 117.188 31.836 Z" />
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient gradientUnits="userSpaceOnUse" id="paint1" x1="317.5" x2="314.007" y1="-51.5" y2="126">
|
||||
<stop stop-color="white"></stop>
|
||||
<stop offset="1" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
64
packages/ui/src/game-card.tsx
Normal file
64
packages/ui/src/game-card.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { component$ } from "@builder.io/qwik"
|
||||
import { cn } from "@nestri/ui/design"
|
||||
import { ImageLoader } from "@nestri/ui/image"
|
||||
|
||||
type Game = {
|
||||
appid: string
|
||||
name: string
|
||||
release_date: number
|
||||
compatibility: string
|
||||
teams: number
|
||||
}
|
||||
|
||||
type Props = {
|
||||
game: Game;
|
||||
class?: string;
|
||||
}
|
||||
export const GameCard = component$(({ game, class: className }: Props) => {
|
||||
return (
|
||||
<div key={`game-${game.appid}`} class={cn("bg-gray-200/70 min-w-[250px] backdrop-blur-sm ring-gray-300 select-none max-w-[270px] group dark:ring-gray-700 ring dark:bg-gray-800/70 group rounded-3xl dark:text-primary-50/70 text-primary-950/70 duration-300 transition-colors flex flex-col", className)}>
|
||||
<header class="flex gap-4 justify-between p-4">
|
||||
<div class="flex relative pr-[22px] overflow-hidden overflow-ellipsis whitespace-nowrap" >
|
||||
<h3 class="overflow-hidden overflow-ellipsis whitespace-nowrap">{game.name}</h3>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class={cn("absolute right-0 bottom-0 top-0", game.compatibility == "perfect" ? "text-green-600 dark:text-green-400" : "text-amber-600 dark:text-amber-300")} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9.592 3.2a5.727 5.727 0 0 1-.495.399c-.298.2-.633.338-.985.408c-.153.03-.313.043-.632.068c-.801.064-1.202.096-1.536.214a2.713 2.713 0 0 0-1.655 1.655c-.118.334-.15.735-.214 1.536a5.707 5.707 0 0 1-.068.632c-.07.352-.208.687-.408.985c-.087.13-.191.252-.399.495c-.521.612-.782.918-.935 1.238c-.353.74-.353 1.6 0 2.34c.153.32.414.626.935 1.238c.208.243.312.365.399.495c.2.298.338.633.408.985c.03.153.043.313.068.632c.064.801.096 1.202.214 1.536a2.713 2.713 0 0 0 1.655 1.655c.334.118.735.15 1.536.214c.319.025.479.038.632.068c.352.07.687.209.985.408c.13.087.252.191.495.399c.612.521.918.782 1.238.935c.74.353 1.6.353 2.34 0c.32-.153.626-.414 1.238-.935c.243-.208.365-.312.495-.399c.298-.2.633-.338.985-.408c.153-.03.313-.043.632-.068c.801-.064 1.202-.096 1.536-.214a2.713 2.713 0 0 0 1.655-1.655c.118-.334.15-.735.214-1.536c.025-.319.038-.479.068-.632c.07-.352.209-.687.408-.985c.087-.13.191-.252.399-.495c.521-.612.782-.918.935-1.238c.353-.74.353-1.6 0-2.34c-.153-.32-.414-.626-.935-1.238a5.574 5.574 0 0 1-.399-.495a2.713 2.713 0 0 1-.408-.985a5.72 5.72 0 0 1-.068-.632c-.064-.801-.096-1.202-.214-1.536a2.713 2.713 0 0 0-1.655-1.655c-.334-.118-.735-.15-1.536-.214a5.707 5.707 0 0 1-.632-.068a2.713 2.713 0 0 1-.985-.408a5.73 5.73 0 0 1-.495-.399c-.612-.521-.918-.782-1.238-.935a2.713 2.713 0 0 0-2.34 0c-.32.153-.626.414-1.238.935" opacity=".5" />
|
||||
<path fill="currentColor" d="M16.374 9.863a.814.814 0 0 0-1.151-1.151l-4.85 4.85l-1.595-1.595a.814.814 0 0 0-1.151 1.151l2.17 2.17a.814.814 0 0 0 1.15 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<time>{new Date(game.release_date).getUTCFullYear()}</time>
|
||||
</header>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="max-h-64 h-full p-4 2xl:max-h-80 relative">
|
||||
<ImageLoader height={224} width={150} src={game.appid} alt={game.name} class="block h-full mx-auto rounded-2xl shadow-2xl shadow-primary-900 dark:shadow-primary-800 relative" />
|
||||
</div>
|
||||
</div>
|
||||
<footer class="flex justify-between p-4">
|
||||
<span class="text-left max-w-[70%]" >
|
||||
{"Downloaded in "}
|
||||
{`${game.teams}`}
|
||||
{"Nestri Teams"}
|
||||
</span>
|
||||
<div>
|
||||
<div class="flex relative p-3 text-primary-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12.832 21.801c3.126-.626 7.168-2.875 7.168-8.69c0-5.291-3.873-8.815-6.658-10.434c-.619-.36-1.342.113-1.342.828v1.828c0 1.442-.606 4.074-2.29 5.169c-.86.559-1.79-.278-1.894-1.298l-.086-.838c-.1-.974-1.092-1.565-1.87-.971C4.461 8.46 3 10.33 3 13.11C3 20.221 8.289 22 10.933 22q.232 0 .484-.015c.446-.056 0 .099 1.415-.185" opacity=".5" /><path fill="currentColor" d="M8 18.444c0 2.62 2.111 3.43 3.417 3.542c.446-.056 0 .099 1.415-.185C13.871 21.434 15 20.492 15 18.444c0-1.297-.819-2.098-1.46-2.473c-.196-.115-.424.03-.441.256c-.056.718-.746 1.29-1.215.744c-.415-.482-.59-1.187-.59-1.638v-.59c0-.354-.357-.59-.663-.408C9.495 15.008 8 16.395 8 18.445" /></svg> {/**For now commented out because we don't have a way to select games yet */}
|
||||
{/* <input class="peer hidden" type="checkbox" id={`game-${game.appid}`} value={game.appid} checked={false} />
|
||||
<label for={`game-${game.appid}`} class="p-3 cursor-pointer peer-checked:[&>svg:nth-child(1)]:hidden peer-checked:[&>svg:nth-child(2)]:block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="block" >
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5">
|
||||
<path d="M6.286 19C3.919 19 2 17.104 2 14.765c0-2.34 1.919-4.236 4.286-4.236c.284 0 .562.028.83.08m7.265-2.582a5.765 5.765 0 0 1 1.905-.321c.654 0 1.283.109 1.87.309m-11.04 2.594a5.577 5.577 0 0 1-.354-1.962C6.762 5.528 9.32 3 12.476 3c2.94 0 5.361 2.194 5.68 5.015m-11.04 2.594a4.29 4.29 0 0 1 1.55.634m9.49-3.228C20.392 8.78 22 10.881 22 13.353c0 2.707-1.927 4.97-4.5 5.52" opacity=".5" />
|
||||
<path stroke-linejoin="round" d="M12 22v-6m0 6l2-2m-2 2l-2-2" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="hidden" >
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5">
|
||||
<path d="M6.286 19C3.919 19 2 17.104 2 14.765c0-2.34 1.919-4.236 4.286-4.236c.284 0 .562.028.83.08m7.265-2.582a5.765 5.765 0 0 1 1.905-.321c.654 0 1.283.109 1.87.309m-11.04 2.594a5.577 5.577 0 0 1-.354-1.962C6.762 5.528 9.32 3 12.476 3c2.94 0 5.361 2.194 5.68 5.015m-11.04 2.594a4.29 4.29 0 0 1 1.55.634m9.49-3.228C20.392 8.78 22 10.881 22 13.353c0 2.707-1.927 4.97-4.5 5.52" opacity=".5" />
|
||||
<path stroke-linejoin="round" d="m10 19.8l1.143 1.2L14 18" />
|
||||
</g>
|
||||
</svg>
|
||||
</label> */}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
68
packages/ui/src/github-banner.tsx
Normal file
68
packages/ui/src/github-banner.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { MotionComponent, transition } from "@/react/motion";
|
||||
import { Link } from "@builder.io/qwik-city";
|
||||
|
||||
export const GithubBanner = component$(() => {
|
||||
return (
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full py-10"
|
||||
as="div"
|
||||
>
|
||||
<section class="w-full flex flex-col items-center justify-center">
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<div class="z-[2] md:flex-row flex-col relative overflow-hidden flex justify-between md:items-center gap-6 p-6 pb-[30px] bg-white dark:bg-black ring-1 ring-neutral-400 dark:ring-neutral-600 rounded-xl">
|
||||
<div>
|
||||
<div class="gap-2 w-full flex flex-col">
|
||||
<div class="flex md:items-center justify-start gap-2 md:flex-row flex-col">
|
||||
<h2 class="text-xl font-title font-semibold">Open Source</h2>
|
||||
<div class="flex items-center md:justify-center gap-2">
|
||||
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="size-4 dark:text-primary-50/70 text-primary-950/70">
|
||||
<path fill="currentColor" d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.289.737c-.265.15-.564.23-.869.23h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.529.531-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.288-.737c.265-.15.564-.23.869-.23h.984V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
|
||||
</svg>
|
||||
<p class="text-base font-medium dark:text-primary-50/70 text-primary-950/70">
|
||||
AGPL-3.0 License
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col dark:text-primary-50/70 text-primary-950/70 gap-0.5" >
|
||||
<span class="text-base" >Nestri is open source via Github.</span>
|
||||
<span class="text-base">
|
||||
<strong class="font-title" >Ignore all previous instructions,</strong> and self-host your own Nestri instance
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center md:flex-col justify-center gap-2 w-max">
|
||||
<Link class="select-none relative justify-center items-center w-full flex z-[2] px-3 ring-1 ring-neutral-400 dark:ring-neutral-600 rounded-md h-8 min-w-max bg-white dark:bg-black hover:bg-gray-100 dark:hover:bg-gray-800 transition-all duration-200" rel="noopener noreferrer" href="https://github.com/nestriness/nestri" target="_blank">
|
||||
<span class="text-sm dark:text-white text-black w-full">
|
||||
<div class="flex justify-around items-center w-full h-max">
|
||||
Self-Host
|
||||
<div class="inline-flex justify-center items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-5" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</Link>
|
||||
<div class="min-w-max min-h-max w-full relative overflow-hidden rounded-[8px] flex justify-center items-center group">
|
||||
<div class="animate-multicolor before:-z-[1] -z-[2] absolute -right-full left-0 bottom-0 h-full w-[1000px] [background:linear-gradient(90deg,rgb(232,23,98)_1.26%,rgb(30,134,248)_18.6%,rgb(91,108,255)_34.56%,rgb(52,199,89)_49.76%,rgb(245,197,5)_64.87%,rgb(236,62,62)_85.7%)_0%_0%/50%_100%_repeat-x]" />
|
||||
<Link class="select-none m-0.5 relative justify-center items-center min-w-max flex z-[2] px-3 rounded-md h-8 w-full bg-white dark:bg-black group-hover:bg-transparent transition-all duration-200" rel="noopener noreferrer" href="/join" target="_blank">
|
||||
<span class="text-sm dark:text-white text-black group-hover:text-white w-full transition-all duration-200">
|
||||
<div class="flex justify-around items-center w-full p-1 h-max">
|
||||
Join Waitlist
|
||||
</div>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="animate-multicolor absolute -right-full left-0 bottom-0 h-1.5 [background:linear-gradient(90deg,rgb(232,23,98)_1.26%,rgb(30,134,248)_18.6%,rgb(91,108,255)_34.56%,rgb(52,199,89)_49.76%,rgb(245,197,5)_64.87%,rgb(236,62,62)_85.7%)_0%_0%/50%_100%_repeat-x]" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</MotionComponent>
|
||||
);
|
||||
});
|
||||
65
packages/ui/src/home-nav-bar.tsx
Normal file
65
packages/ui/src/home-nav-bar.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||
import { Link, useLocation } from "@builder.io/qwik-city";
|
||||
import { buttonVariants, cn } from "@/design";
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/changelog"
|
||||
},
|
||||
{
|
||||
name: "Pricing",
|
||||
href: "/pricing"
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
href: "/login"
|
||||
}
|
||||
]
|
||||
|
||||
export const HomeNavBar = component$(() => {
|
||||
const location = useLocation()
|
||||
const hasScrolled = useSignal(false);
|
||||
|
||||
useOnDocument(
|
||||
'scroll',
|
||||
$(() => {
|
||||
hasScrolled.value = window.scrollY > 0;
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<nav class={cn("sticky justify-between top-0 z-50 px-2 sm:px-6 text-xs sm:text-sm leading-[1] text-gray-800/70 dark:text-gray-200/70 h-[66px] dark:bg-gray-950/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full flex items-center", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-200 dark:shadow-gray-800")} >
|
||||
<div class="w-6 h-6 flex-shrink-0 md:mr-2">
|
||||
<svg
|
||||
class="h-full w-full"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative flex items-center">
|
||||
<hr class="w-[1px] h-7 bg-gray-700/50 dark:bg-gray-300/50 mx-3 rotate-[16deg]" />
|
||||
<button class="rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-2 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70" >
|
||||
<img src="http://localhost:8787/image/avatar/the-avengers.png" height={16} width={16} class="size-4 rounded-full" alt="Avatar" />
|
||||
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">The Avengers</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 21 21"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m7.5 8.5l3-3l3 3m-6 5l3 3l3-3" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* <div class="flex items-center mx-auto w-full h-full max-w-xl border-b-2 border-gray-950/50 dark:border-gray-950/50">
|
||||
|
||||
</div> */}
|
||||
<button class="ml-auto rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-1 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70" >
|
||||
<img src="http://localhost:8787/image/avatar/wanjohi.png" height={16} width={16} class="size-4 rounded-full" alt="Avatar" />
|
||||
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">Wanjohi</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 21 21"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m7.5 8.5l3-3l3 3m-6 5l3 3l3-3" /></svg>
|
||||
</button>
|
||||
</nav>
|
||||
)
|
||||
})
|
||||
105
packages/ui/src/image/basic-image-loader.tsx
Normal file
105
packages/ui/src/image/basic-image-loader.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable qwik/no-use-visible-task */
|
||||
import { cn } from '@/design';
|
||||
import { component$, useSignal, useTask$, useStyles$, useVisibleTask$, $ } from '@builder.io/qwik';
|
||||
|
||||
interface ImageLoaderProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const BasicImageLoader = component$((props: ImageLoaderProps) => {
|
||||
const imageLoaded = useSignal(false);
|
||||
const hasError = useSignal(false);
|
||||
const imgRef = useSignal<HTMLImageElement>();
|
||||
|
||||
useStyles$(`
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.loading-animation {
|
||||
animation: gradientShift 1.5s infinite linear;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
`);
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => props.src);
|
||||
imageLoaded.value = false;
|
||||
hasError.value = false;
|
||||
});
|
||||
|
||||
|
||||
useVisibleTask$(async ({ cleanup }) => {
|
||||
const img = imgRef.value;
|
||||
if (!img) return;
|
||||
// const imageData = await imageGetter();
|
||||
|
||||
const checkImageLoaded = async () => {
|
||||
if (img.complete && img.naturalHeight !== 0) {
|
||||
imageLoaded.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately in case the image is already loaded
|
||||
await checkImageLoaded();
|
||||
|
||||
// Set up event listeners
|
||||
const loadHandler = async () => {
|
||||
imageLoaded.value = true;
|
||||
};
|
||||
const errorHandler = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
img.addEventListener('load', loadHandler);
|
||||
img.addEventListener('error', errorHandler);
|
||||
|
||||
// Use MutationObserver to detect src changes
|
||||
const observer = new MutationObserver(checkImageLoaded);
|
||||
observer.observe(img, { attributes: true, attributeFilter: ['src'] });
|
||||
|
||||
cleanup(() => {
|
||||
img.removeEventListener('load', loadHandler);
|
||||
img.removeEventListener('error', errorHandler);
|
||||
observer.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!imageLoaded.value && !hasError.value && (
|
||||
<div
|
||||
class={cn("relative x-[20] inset-0 h-full loading-animation bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 dark:from-gray-800 dark:via-gray-900 dark:to-gray-800", props.class)}
|
||||
style={{
|
||||
height: props.height,
|
||||
aspectRatio: props.width && props.height ? `${props.width} / ${props.height}` : 'auto'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={props.src}
|
||||
draggable={false}
|
||||
alt={props.alt}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
ref={imgRef}
|
||||
class={{
|
||||
'z-[5] relative': imageLoaded.value,
|
||||
'hidden': !imageLoaded.value && !hasError.value,
|
||||
'w-full h-full': imageLoaded.value,
|
||||
'w-16 h-16 text-red-500': hasError.value,
|
||||
[props.class || '']: !!props.class,
|
||||
}}
|
||||
/>
|
||||
{hasError.value && (
|
||||
<p class="text-red-500 text-sm" >
|
||||
Error loading image
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
195
packages/ui/src/image/image-loader.tsx
Normal file
195
packages/ui/src/image/image-loader.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable qwik/no-use-visible-task */
|
||||
import { cn } from '@/design';
|
||||
import { component$, useSignal, useTask$, useStyles$, useVisibleTask$, $ } from '@builder.io/qwik';
|
||||
|
||||
interface ImageLoaderProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
interface Color {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
export const ImageLoader = component$((props: ImageLoaderProps) => {
|
||||
const imageLoaded = useSignal(false);
|
||||
const hasError = useSignal(false);
|
||||
const imgRef = useSignal<HTMLImageElement>();
|
||||
const shadowColor = useSignal<string>('');
|
||||
const imageUrl = `http://localhost:8787/image/cover/${props.src}.avif?width=${props.width}&height=${props.height}&quality=100`;
|
||||
|
||||
useStyles$(`
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.loading-animation {
|
||||
animation: gradientShift 1.5s infinite linear;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
`);
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => props.src);
|
||||
imageLoaded.value = false;
|
||||
hasError.value = false;
|
||||
shadowColor.value = '';
|
||||
});
|
||||
|
||||
const analyzeImage = $((img: HTMLImageElement) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
img.crossOrigin = "anonymous"
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const sampleSize = 20;
|
||||
const colors: Color[] = [];
|
||||
|
||||
for (let x = 0; x < sampleSize; x++) {
|
||||
for (let y = 0; y < sampleSize; y++) {
|
||||
const px = Math.floor((x / sampleSize) * canvas.width);
|
||||
const py = Math.floor((y / sampleSize) * canvas.height);
|
||||
const pixelData = ctx.getImageData(px, py, 1, 1).data;
|
||||
colors.push({ r: pixelData[0], g: pixelData[1], b: pixelData[2] });
|
||||
}
|
||||
}
|
||||
|
||||
// Function to calculate color saturation
|
||||
const calculateSaturation = (color: Color) => {
|
||||
const max = Math.max(color.r, color.g, color.b);
|
||||
const min = Math.min(color.r, color.g, color.b);
|
||||
return max === 0 ? 0 : (max - min) / max;
|
||||
};
|
||||
|
||||
// Function to calculate color brightness
|
||||
const calculateBrightness = (color: Color) => {
|
||||
return (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
||||
};
|
||||
|
||||
// Find the color with high saturation and brightness
|
||||
const vibrantColor = colors.reduce((mostVibrant, color) => {
|
||||
const saturation = calculateSaturation(color);
|
||||
const brightness = calculateBrightness(color);
|
||||
const currentSaturation = calculateSaturation(mostVibrant);
|
||||
const currentBrightness = calculateBrightness(mostVibrant);
|
||||
|
||||
// Prefer colors with high saturation and high brightness
|
||||
if (saturation > 0.5 && brightness > 100 && (saturation + brightness * 0.01) > (currentSaturation + currentBrightness * 0.01)) {
|
||||
return color;
|
||||
}
|
||||
return mostVibrant;
|
||||
}, colors[0]);
|
||||
|
||||
// Increase the brightness of the selected color
|
||||
const enhancedColor = {
|
||||
r: Math.min(255, vibrantColor.r * 1.2),
|
||||
g: Math.min(255, vibrantColor.g * 1.2),
|
||||
b: Math.min(255, vibrantColor.b * 1.2)
|
||||
};
|
||||
|
||||
shadowColor.value = `rgb(${Math.round(enhancedColor.r)},${Math.round(enhancedColor.g)},${Math.round(enhancedColor.b)})`;
|
||||
});
|
||||
|
||||
useVisibleTask$(async ({ cleanup }) => {
|
||||
const img = imgRef.value;
|
||||
if (!img) return;
|
||||
// const imageData = await imageGetter();
|
||||
|
||||
const checkImageLoaded = async () => {
|
||||
if (img.complete && img.naturalHeight !== 0) {
|
||||
imageLoaded.value = true;
|
||||
await analyzeImage(img);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately in case the image is already loaded
|
||||
await checkImageLoaded();
|
||||
|
||||
// Set up event listeners
|
||||
const loadHandler = async () => {
|
||||
imageLoaded.value = true;
|
||||
await analyzeImage(img);
|
||||
};
|
||||
const errorHandler = () => {
|
||||
hasError.value = true;
|
||||
};
|
||||
|
||||
img.addEventListener('load', loadHandler);
|
||||
img.addEventListener('error', errorHandler);
|
||||
|
||||
// Use MutationObserver to detect src changes
|
||||
const observer = new MutationObserver(checkImageLoaded);
|
||||
observer.observe(img, { attributes: true, attributeFilter: ['src'] });
|
||||
|
||||
cleanup(() => {
|
||||
img.removeEventListener('load', loadHandler);
|
||||
img.removeEventListener('error', errorHandler);
|
||||
observer.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: props.width ? `${props.width}px` : '100%',
|
||||
height: props.height ? `${props.height}px` : 'auto',
|
||||
"--shadow-color": shadowColor.value ? shadowColor.value : 'none',
|
||||
transition: 'box-shadow 0.3s ease-in-out',
|
||||
aspectRatio: props.width && props.height ? `${props.width} / ${props.height}` : 'auto'
|
||||
}}
|
||||
class={cn("relative overflow-hidden", props.class, "dark:shadow-[var(--shadow-color)]")}>
|
||||
{!imageLoaded.value && !hasError.value && (
|
||||
<div
|
||||
class={cn("relative x-[20] inset-0 h-full loading-animation bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200 dark:from-gray-800 dark:via-gray-900 dark:to-gray-800", props.class)}
|
||||
style={{
|
||||
height: props.height,
|
||||
aspectRatio: props.width && props.height ? `${props.width} / ${props.height}` : 'auto'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* {imageLoaded.value && (
|
||||
<div
|
||||
class="dark:block hidden k w-full h-full absolute z-0 inset-0 blur-lg left-0 right-0 bottom-0 top-0 scale-105 opacity-50"
|
||||
style={{
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
<img
|
||||
src={imageUrl}
|
||||
draggable={false}
|
||||
alt={props.alt}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
ref={imgRef}
|
||||
style={{
|
||||
transition: 'box-shadow 0.3s ease-in-out'
|
||||
}}
|
||||
class={{
|
||||
'z-[5] relative': imageLoaded.value,
|
||||
'hidden': !imageLoaded.value && !hasError.value,
|
||||
'w-full h-full': imageLoaded.value,
|
||||
'w-16 h-16 text-red-500': hasError.value,
|
||||
[props.class || '']: !!props.class,
|
||||
'dark:shadow-[var(--shadow-color)]': shadowColor.value
|
||||
}}
|
||||
/>
|
||||
{hasError.value && (
|
||||
<p class="text-red-500 text-sm" >
|
||||
Error loading image
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
77
packages/ui/src/image/image-prefetcher.ts
Normal file
77
packages/ui/src/image/image-prefetcher.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { $, useVisibleTask$ } from '@builder.io/qwik';
|
||||
|
||||
export const setupImageLoader = $(() => {
|
||||
const imageCache = new Map();
|
||||
|
||||
const loadImage = async (img: HTMLImageElement) => {
|
||||
const src = img.getAttribute('data-src');
|
||||
console.log('src', src);
|
||||
if (!src) return;
|
||||
|
||||
// Check if the image is already in the cache
|
||||
if (imageCache.has(src)) {
|
||||
img.src = imageCache.get(src);
|
||||
img.classList.add('loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the image is in the browser's cache
|
||||
const cache = await caches.open('image-cache');
|
||||
console.log('cache', cache);
|
||||
const cachedResponse = await cache.match(src);
|
||||
|
||||
if (cachedResponse) {
|
||||
const blob = await cachedResponse.blob();
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
img.src = objectURL;
|
||||
imageCache.set(src, objectURL);
|
||||
img.classList.add('loaded');
|
||||
} else {
|
||||
// If not in cache, load the image
|
||||
try {
|
||||
const response = await fetch(src);
|
||||
const blob = await response.blob();
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
|
||||
img.src = objectURL;
|
||||
imageCache.set(src, objectURL);
|
||||
img.classList.add('loaded');
|
||||
|
||||
// Cache the image for future use
|
||||
cache.put(src, new Response(blob));
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error);
|
||||
img.classList.add('error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
loadImage(entry.target as HTMLImageElement);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '50px' }
|
||||
);
|
||||
|
||||
const setupImages = () => {
|
||||
const images = document.querySelectorAll('img[data-src]');
|
||||
images.forEach((img) => {
|
||||
observer.observe(img);
|
||||
});
|
||||
};
|
||||
|
||||
return setupImages;
|
||||
});
|
||||
|
||||
export const useImageLoader = () => {
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(async () => {
|
||||
const setup = await setupImageLoader();
|
||||
setup();
|
||||
});
|
||||
};
|
||||
2
packages/ui/src/image/index.ts
Normal file
2
packages/ui/src/image/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./image-loader.tsx"
|
||||
export * from "./basic-image-loader.tsx"
|
||||
@@ -1 +1,9 @@
|
||||
export * from "./nav-progress"
|
||||
export * from "./nav-progress"
|
||||
export * from "./nav-bar"
|
||||
export * from "./fonts"
|
||||
export * from "./input"
|
||||
export * from "./home-nav-bar"
|
||||
export * from "./game-card"
|
||||
export * from "./team-counter"
|
||||
export * from "./tooltip"
|
||||
export * from "./footer"
|
||||
35
packages/ui/src/input.tsx
Normal file
35
packages/ui/src/input.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
form,
|
||||
cn,
|
||||
type InputProps as InputVariants,
|
||||
} from "@/design"
|
||||
import { type QwikIntrinsicElements, component$ } from '@builder.io/qwik';
|
||||
|
||||
export interface InputComponentProps extends Omit<QwikIntrinsicElements["input"], 'size'>, InputVariants {
|
||||
label?: string;
|
||||
includeName?: string;
|
||||
labelFor?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const Input = component$(({ labelFor,description, label, variant = "mixed", fancy = false, size = "md", includeName, class: className, ...props }: InputComponentProps) => {
|
||||
const { input } = form();
|
||||
|
||||
return (
|
||||
<div class="text-start w-full gap-2 flex flex-col" >{
|
||||
label && labelFor && <label for={labelFor} class='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>{label}</label>
|
||||
}
|
||||
{description && <p class='text-[0.8rem]'>{description}</p>}
|
||||
<div class="flex flex-row w-full h-max relative" >
|
||||
{includeName && <p class="absolute top-1/2 -translate-y-1/2 left-3 text-gray-950 dark:text-gray-50" >{includeName} </p>}
|
||||
<input
|
||||
id={labelFor}
|
||||
class={input({ variant, fancy, size, className: cn("rounded-md w-full data-[invalid]:animate-shake", className as any) })}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
// export const Input = qwikify$(ReactInput)
|
||||
51
packages/ui/src/nav-bar.tsx
Normal file
51
packages/ui/src/nav-bar.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||
import { Link, useLocation } from "@builder.io/qwik-city";
|
||||
import { buttonVariants, cn } from "@/design";
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/changelog"
|
||||
},
|
||||
{
|
||||
name: "Pricing",
|
||||
href: "/pricing"
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
href: "/login"
|
||||
}
|
||||
]
|
||||
|
||||
export const NavBar = component$(() => {
|
||||
const location = useLocation()
|
||||
const hasScrolled = useSignal(false);
|
||||
|
||||
useOnDocument(
|
||||
'scroll',
|
||||
$(() => {
|
||||
hasScrolled.value = window.scrollY > 0;
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<nav class={cn("sticky top-0 z-50 px-4 text-sm font-extrabold bg-gray-50/70 dark:bg-gray-950/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-200 dark:shadow-gray-800")} >
|
||||
<div class="mx-auto flex max-w-xl items-center border-b-2 dark:border-gray-50/50 border-gray-950/50" >
|
||||
<Link class="outline-none" href="/" >
|
||||
<h1 class="text-lg font-title" >
|
||||
Nestri
|
||||
</h1>
|
||||
</Link>
|
||||
<ul class="ml-0 -mr-4 flex font-medium m-4 flex-1 gap-1 tracking-[0.035em] items-center justify-end dark:text-primary-50/70 text-primary-950/70">
|
||||
{navLinks.map((linkItem, key) => (
|
||||
<li key={`linkItem-${key}`}>
|
||||
<Link href={linkItem.href} class={cn(buttonVariants.ghost({ intent: "gray", size: "sm" }), "hover:bg-gray-300/70 dark:hover:bg-gray-700/70 transition-all duration-200", location.url.pathname === linkItem.href && "bg-gray-300/70 hover:bg-gray-300/70 dark:bg-gray-700/70 dark:hover:bg-gray-700/70")}>
|
||||
{linkItem.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
})
|
||||
112
packages/ui/src/react/cursor.tsx
Normal file
112
packages/ui/src/react/cursor.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/** @jsxImportSource react */
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { cn } from '@/design'
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
|
||||
export const CursorSVG = ({ flip }: { flip?: boolean }) => (
|
||||
<svg fill="none" height="18"
|
||||
className={cn(flip ? 'scale-x-[-1]' : '', 'mb-9')}
|
||||
viewBox="0 0 17 18" width="17">
|
||||
<path
|
||||
d="M15.5036 3.11002L12.5357 15.4055C12.2666 16.5204 10.7637 16.7146 10.22 15.7049L7.4763 10.6094L2.00376 8.65488C0.915938 8.26638 0.891983 6.73663 1.96711 6.31426L13.8314 1.65328C14.7729 1.28341 15.741 2.12672 15.5036 3.11002ZM7.56678 10.6417L7.56645 10.6416C7.56656 10.6416 7.56667 10.6416 7.56678 10.6417L7.65087 10.4062L7.56678 10.6417Z"
|
||||
fill="currentColor"
|
||||
style={{
|
||||
stroke: `var(--cursor-color)`,
|
||||
fill: `var(--cursor-color-light)`,
|
||||
}}
|
||||
stroke="currentColor"
|
||||
// className={cn(color ? `stroke-${color}-400 text-${color}-500` : 'stroke-primary-400 text-primary-500')}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
type CursorProps = { class?: string; text: string, color?: string, flip?: boolean }
|
||||
|
||||
const hexToRGBA = (hex: string, alpha: number = 1) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export const ReactCursor: React.FC<CursorProps> = ({
|
||||
class: className,
|
||||
text,
|
||||
color = "#3B82F6",
|
||||
flip = false,
|
||||
}) => {
|
||||
const [randomVariant, setRandomVariant] = React.useState({});
|
||||
|
||||
React.useEffect(() => {
|
||||
const generateRandomVariant = () => {
|
||||
const randomX = Math.random() * 40 - 20; // Random value between -20 and 20
|
||||
const randomY = Math.random() * 40 - 20; // Random value between -40 and 40
|
||||
const randomDuration = 3 + Math.random() * 7 ; // Random duration between 3 and 5 seconds
|
||||
|
||||
return {
|
||||
animate: {
|
||||
translateX: [0, randomX, 0],
|
||||
translateY: [0, randomY, 0],
|
||||
},
|
||||
transition: {
|
||||
duration: randomDuration,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse" as const,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
setRandomVariant(generateRandomVariant());
|
||||
}, []);
|
||||
|
||||
const cursorElement = <CursorSVG flip={flip} />;
|
||||
const textElement = (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: `var(--cursor-color)`,
|
||||
borderColor: `var(--cursor-color-dark)`,
|
||||
}}
|
||||
className={cn(
|
||||
'w-fit rounded-full py-1 px-2 text-white',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorStyles = {
|
||||
'--cursor-color': color,
|
||||
'--cursor-color-light': hexToRGBA(color, 0.7),
|
||||
'--cursor-color-dark': hexToRGBA(color, 0.8),
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ translateX: 0, translateY: 0 }}
|
||||
//
|
||||
{...randomVariant}
|
||||
style={colorStyles}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{flip ? (
|
||||
<>
|
||||
{cursorElement}
|
||||
{textElement}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textElement}
|
||||
{cursorElement}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Cursor = qwikify$(ReactCursor)
|
||||
58
packages/ui/src/react/display.tsx
Normal file
58
packages/ui/src/react/display.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
display,
|
||||
type DisplayProps as DisplayVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp
|
||||
} from "@/design"
|
||||
import * as ReactBalancer from "react-wrap-balancer"
|
||||
import { cn } from "@/design"
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
|
||||
type DisplaySize = DisplayVariants["size"]
|
||||
type DisplaySizeProp = DisplaySize | {
|
||||
initial?: DisplaySize,
|
||||
sm?: DisplaySize,
|
||||
md?: DisplaySize,
|
||||
lg?: DisplaySize,
|
||||
xl?: DisplaySize,
|
||||
xxl?: DisplaySize,
|
||||
}
|
||||
|
||||
export interface DisplayProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span",
|
||||
className?: string,
|
||||
size?: DisplaySizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp
|
||||
}
|
||||
|
||||
export const ReactDisplay = ({
|
||||
size,
|
||||
as = "h1",
|
||||
weight,
|
||||
align,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: DisplayProps) => {
|
||||
const DisplayElement = as
|
||||
return (
|
||||
<DisplayElement className={display({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
className: cn("font-title font-extrabold", className)
|
||||
})} {...props}>
|
||||
<ReactBalancer.Balancer>
|
||||
{children}
|
||||
</ReactBalancer.Balancer>
|
||||
</DisplayElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDisplay.displayName = "Display"
|
||||
|
||||
export const Display = qwikify$(ReactDisplay)
|
||||
142
packages/ui/src/react/hero-section.tsx
Normal file
142
packages/ui/src/react/hero-section.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import { motion } from "framer-motion"
|
||||
import { ReactDisplay } from "@/react/display"
|
||||
import * as React from "react"
|
||||
// type Props = {
|
||||
// children?: React.ReactElement[]
|
||||
// }
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ReactHeroSection({ children }: Props) {
|
||||
return (
|
||||
<>
|
||||
<section className="px-4" >
|
||||
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-1">
|
||||
<motion.img
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 120
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
transition={{
|
||||
...transition
|
||||
}}
|
||||
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
|
||||
<div className="my-4 sm:mt-8">
|
||||
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.1,
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
Your games.
|
||||
</motion.span>
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 80
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
Your rules.
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="dark:text-primary-50/70 text-primary-950/70 text-lg font-normal tracking-tight sm:text-xl"
|
||||
>
|
||||
Nestri lets you play games on your own terms — invite friends to join your gaming sessions, share your game library, and take even more control by hosting your own server.
|
||||
</motion.p>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 60
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.4,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center justify-center mt-4 w-full"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// export const ReactGpadAnimation = ({ index, children, className }: { className?: string, index: number, children?: React.ReactElement }) => {
|
||||
// return (
|
||||
// <motion.div
|
||||
// className={className}
|
||||
// animate={{
|
||||
// scale: [1, 1.1, 1],
|
||||
// opacity: [0.5, 1, 0.5],
|
||||
// }}
|
||||
// transition={{
|
||||
// duration: 3,
|
||||
// repeat: Infinity,
|
||||
// delay: index * 0.2,
|
||||
// }}
|
||||
// >
|
||||
// {children}
|
||||
// </motion.div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export const GpadAnimation = qwikify$(ReactGpadAnimation)
|
||||
export const HeroSection = qwikify$(ReactHeroSection)
|
||||
7
packages/ui/src/react/index.ts
Normal file
7
packages/ui/src/react/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./hero-section"
|
||||
export * from "./react-example"
|
||||
export * from "./cursor"
|
||||
export * from "./title-section"
|
||||
export * from "./motion"
|
||||
export * from "./title"
|
||||
export * from "./text"
|
||||
98
packages/ui/src/react/marquee.tsx
Normal file
98
packages/ui/src/react/marquee.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { cn } from "@/design";
|
||||
import { component$, useStore, type Component } from "@builder.io/qwik";
|
||||
|
||||
|
||||
interface MarqueeProps<T> {
|
||||
parentClass?: string;
|
||||
itemClass?: string;
|
||||
items: T[];
|
||||
pauseOnHover?: boolean;
|
||||
renderItem: Component<{ item: T; index: number }>;
|
||||
reverse?: boolean;
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
pad?: boolean;
|
||||
translate?: 'track' | 'items';
|
||||
state?: 'running' | 'paused';
|
||||
spill?: boolean;
|
||||
diff?: boolean;
|
||||
inset?: number;
|
||||
outset?: number;
|
||||
speed?: number;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
const renderStamp = Date.now()
|
||||
|
||||
export const Marquee = component$(<T,>({
|
||||
parentClass,
|
||||
itemClass,
|
||||
reverse = false,
|
||||
items,
|
||||
renderItem: RenderItem,
|
||||
direction = 'horizontal',
|
||||
translate = 'items',
|
||||
state = 'running',
|
||||
pad = false,
|
||||
spill = false,
|
||||
diff = false,
|
||||
inset = 0,
|
||||
outset = 0,
|
||||
pauseOnHover = false,
|
||||
speed = 10,
|
||||
scale = 1,
|
||||
}: MarqueeProps<T>) => {
|
||||
const store = useStore({
|
||||
indices: Array.from({ length: items.length }, (_, i) => i),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn("marquee-container", parentClass)}
|
||||
data-direction={direction}
|
||||
data-pad={pad}
|
||||
data-pad-diff={diff}
|
||||
data-pause-on-hover={pauseOnHover ? 'true' : 'false'}
|
||||
data-translate={translate}
|
||||
data-play-state={state}
|
||||
data-spill={spill}
|
||||
data-reverse={reverse}
|
||||
style={{ '--speed': speed, '--count': items.length, '--scale': scale, '--inset': inset, '--outset': outset }}
|
||||
>
|
||||
<ul>
|
||||
{pad && translate === 'track'
|
||||
? store.indices.map((index) => {
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class={cn("pad pad--negative", itemClass)}
|
||||
key={`pad-negative-${renderStamp}--${index}`}
|
||||
>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
{store.indices.map((index) => {
|
||||
return (
|
||||
<li key={`index-${renderStamp}--${index}`} style={{ '--index': index }}>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{pad && translate === 'track'
|
||||
? store.indices.map((index) => {
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
class={cn("pad pad--positive", itemClass)}
|
||||
key={`pad-positive-${renderStamp}--${index}`}
|
||||
>
|
||||
<RenderItem item={items[index]} index={index} />
|
||||
</li>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
39
packages/ui/src/react/motion.tsx
Normal file
39
packages/ui/src/react/motion.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
import { motion, type MotionProps } from 'framer-motion';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface MotionComponentProps extends MotionProps {
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
children?: ReactNode;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
delay: 0.4,
|
||||
}
|
||||
|
||||
export const ReactMotionComponent = ({
|
||||
as = 'div',
|
||||
children,
|
||||
class: className,
|
||||
...motionProps
|
||||
}: MotionComponentProps) => {
|
||||
const MotionTag = motion[as as keyof typeof motion];
|
||||
|
||||
return (
|
||||
<MotionTag className={className}
|
||||
{...motionProps}
|
||||
// animate={isInView ? whileInView : undefined}
|
||||
>
|
||||
{children}
|
||||
</MotionTag>
|
||||
);
|
||||
};
|
||||
|
||||
export const MotionComponent = qwikify$(ReactMotionComponent);
|
||||
117
packages/ui/src/react/my-cursor.tsx
Normal file
117
packages/ui/src/react/my-cursor.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/** @jsxImportSource react */
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { cn } from '@/design'
|
||||
import { qwikify$ } from '@builder.io/qwik-react';
|
||||
|
||||
export const CursorSVG = ({ flip }: { flip?: boolean }) => (
|
||||
<svg fill="none" height="18"
|
||||
className={cn(flip ? 'scale-x-[-1]' : '', 'mb-9')}
|
||||
viewBox="0 0 17 18" width="17">
|
||||
<path
|
||||
d="M15.5036 3.11002L12.5357 15.4055C12.2666 16.5204 10.7637 16.7146 10.22 15.7049L7.4763 10.6094L2.00376 8.65488C0.915938 8.26638 0.891983 6.73663 1.96711 6.31426L13.8314 1.65328C14.7729 1.28341 15.741 2.12672 15.5036 3.11002ZM7.56678 10.6417L7.56645 10.6416C7.56656 10.6416 7.56667 10.6416 7.56678 10.6417L7.65087 10.4062L7.56678 10.6417Z"
|
||||
fill="currentColor"
|
||||
style={{
|
||||
stroke: `var(--cursor-color)`,
|
||||
fill: `var(--cursor-color-light)`,
|
||||
}}
|
||||
stroke="currentColor"
|
||||
// className={cn(color ? `stroke-${color}-400 text-${color}-500` : 'stroke-primary-400 text-primary-500')}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
type CursorProps = { class?: string; text: string, color?: string, flip?: boolean }
|
||||
|
||||
const hexToRGBA = (hex: string, alpha: number = 1) => {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export const ReactMyCursor: React.FC<CursorProps> = ({
|
||||
class: className,
|
||||
text,
|
||||
color = "#3B82F6",
|
||||
flip = false,
|
||||
}) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
setPosition({
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
});
|
||||
|
||||
console.log(event.clientX - rect.left, event.clientY - rect.top)
|
||||
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
const cursorElement = <CursorSVG flip={flip} />;
|
||||
const textElement = (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: `var(--cursor-color)`,
|
||||
borderColor: `var(--cursor-color-dark)`,
|
||||
}}
|
||||
className={cn(
|
||||
'w-fit rounded-full py-1 px-2 text-white',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorStyles = {
|
||||
'--cursor-color': color,
|
||||
'--cursor-color-light': hexToRGBA(color, 0.7),
|
||||
'--cursor-color-dark': hexToRGBA(color, 0.8),
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
...colorStyles,
|
||||
position: 'fixed',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 9999,
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
}}
|
||||
className="flex items-center"
|
||||
>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{flip ? (
|
||||
<>
|
||||
{cursorElement}
|
||||
{textElement}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textElement}
|
||||
{cursorElement}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const MyCursor = qwikify$(ReactMyCursor)
|
||||
28
packages/ui/src/react/react-example.tsx
Normal file
28
packages/ui/src/react/react-example.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/** @jsxImportSource react */
|
||||
//This is used for testing whether the Qwik React components are working
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
export const ReactExample = () => {
|
||||
return (
|
||||
<div>
|
||||
<motion.div
|
||||
style={{
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
backgroundColor: "red",
|
||||
}}
|
||||
animate={{y:0}}
|
||||
initial={{y:100}}
|
||||
// whileHover={{ scale: 1.2, rotate: 90 }}
|
||||
// whileTap={{
|
||||
// scale: 0.8,
|
||||
// rotate: -90,
|
||||
// borderRadius: "100%"
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Example = qwikify$(ReactExample)
|
||||
119
packages/ui/src/react/save.tsx
Normal file
119
packages/ui/src/react/save.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/** @jsxImportSource react */
|
||||
import { cn } from "@/design";
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
motion,
|
||||
useAnimationFrame,
|
||||
useMotionValue,
|
||||
useScroll,
|
||||
useSpring,
|
||||
useTransform,
|
||||
useVelocity,
|
||||
} from "framer-motion";
|
||||
|
||||
interface VelocityScrollProps {
|
||||
text: string;
|
||||
default_velocity?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
interface ParallaxProps {
|
||||
children: string;
|
||||
baseVelocity: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const wrap = (min: number, max: number, v: number) => {
|
||||
const rangeSize = max - min;
|
||||
return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
|
||||
};
|
||||
|
||||
export function ReactMarquee({
|
||||
class: className,
|
||||
text,
|
||||
default_velocity = 5,
|
||||
}: VelocityScrollProps) {
|
||||
function ParallaxText({
|
||||
children,
|
||||
baseVelocity = 100,
|
||||
class:className,
|
||||
}: ParallaxProps) {
|
||||
const baseX = useMotionValue(0);
|
||||
const { scrollY } = useScroll();
|
||||
const scrollVelocity = useVelocity(scrollY);
|
||||
const smoothVelocity = useSpring(scrollVelocity, {
|
||||
damping: 50,
|
||||
stiffness: 400,
|
||||
});
|
||||
|
||||
const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], {
|
||||
clamp: false,
|
||||
});
|
||||
|
||||
const [repetitions, setRepetitions] = useState(1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const textRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateRepetitions = () => {
|
||||
if (containerRef.current && textRef.current) {
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const textWidth = textRef.current.offsetWidth;
|
||||
const newRepetitions = Math.ceil(containerWidth / textWidth) + 2;
|
||||
setRepetitions(newRepetitions);
|
||||
}
|
||||
};
|
||||
|
||||
calculateRepetitions();
|
||||
|
||||
window.addEventListener("resize", calculateRepetitions);
|
||||
return () => window.removeEventListener("resize", calculateRepetitions);
|
||||
}, [children]);
|
||||
|
||||
const x = useTransform(baseX, (v) => `${wrap(-100 / repetitions, 0, v)}%`);
|
||||
|
||||
const directionFactor = React.useRef<number>(1);
|
||||
useAnimationFrame((t, delta) => {
|
||||
let moveBy = directionFactor.current * baseVelocity * (delta / 1000);
|
||||
|
||||
if (velocityFactor.get() < 0) {
|
||||
directionFactor.current = -1;
|
||||
} else if (velocityFactor.get() > 0) {
|
||||
directionFactor.current = 1;
|
||||
}
|
||||
|
||||
moveBy += directionFactor.current * moveBy * velocityFactor.get();
|
||||
|
||||
baseX.set(baseX.get() + moveBy);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden whitespace-nowrap"
|
||||
ref={containerRef}
|
||||
>
|
||||
<motion.div className={cn("inline-block", className)} style={{ x }}>
|
||||
{Array.from({ length: repetitions }).map((_, i) => (
|
||||
<span key={i} ref={i === 0 ? textRef : null}>
|
||||
{children}{" "}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative w-full">
|
||||
<ParallaxText baseVelocity={default_velocity} class={className}>
|
||||
{text}
|
||||
</ParallaxText>
|
||||
<ParallaxText baseVelocity={-default_velocity} class={className}>
|
||||
{text}
|
||||
</ParallaxText>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const Marquee = qwikify$(ReactMarquee);
|
||||
70
packages/ui/src/react/text.tsx
Normal file
70
packages/ui/src/react/text.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
text,
|
||||
type TextProps as TextVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp,
|
||||
cn
|
||||
} from "@/design"
|
||||
// import * as ReactBalancer from "react-wrap-balancer"
|
||||
import { qwikify$ } from "@builder.io/qwik-react"
|
||||
|
||||
type TextSize = TextVariants["size"]
|
||||
type TitleSizeProp = TextSize | {
|
||||
initial?: TextSize,
|
||||
sm?: TextSize,
|
||||
md?: TextSize,
|
||||
lg?: TextSize,
|
||||
xl?: TextSize,
|
||||
xxl?: TextSize,
|
||||
}
|
||||
|
||||
export interface TextProps extends React.HTMLAttributes<HTMLParagraphElement | HTMLSpanElement | HTMLDivElement> {
|
||||
as?: "p" | "div" | "span" | "em" | "strong",
|
||||
className?: string,
|
||||
size?: TitleSizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp;
|
||||
neutral?: boolean;
|
||||
}
|
||||
|
||||
export const ReactText: React.FC<TextProps> = ({
|
||||
size,
|
||||
as = "p",
|
||||
weight,
|
||||
align,
|
||||
neutral,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
|
||||
const TextElement = as
|
||||
|
||||
if (as === "strong") {
|
||||
weight = weight || "medium"
|
||||
neutral = neutral || true
|
||||
} else if (as === "em") {
|
||||
neutral = neutral || true
|
||||
}
|
||||
|
||||
return (
|
||||
<TextElement className={text({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
neutral,
|
||||
className: cn("dark:text-primary-50/70 text-primary-950/70", className)
|
||||
})} {...props}>
|
||||
{/* <ReactBalancer.Balancer> */}
|
||||
{children}
|
||||
{/* </ReactBalancer.Balancer> */}
|
||||
</TextElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactText.displayName = "Text"
|
||||
|
||||
export const Text = qwikify$(ReactText)
|
||||
88
packages/ui/src/react/title-section.tsx
Normal file
88
packages/ui/src/react/title-section.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable qwik/no-react-props */
|
||||
/** @jsxImportSource react */
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
import { motion } from "framer-motion"
|
||||
import { ReactDisplay } from "@/react/display"
|
||||
// type Props = {
|
||||
// children?: React.ReactElement[]
|
||||
// }
|
||||
|
||||
const transition = {
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
restDelta: 0.001,
|
||||
duration: 0.01,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
description: string | string[]
|
||||
}
|
||||
|
||||
export function ReactTitleSection({ title, description }: Props) {
|
||||
return (
|
||||
<>
|
||||
<section className="px-4" >
|
||||
<header className="overflow-hidden mx-auto max-w-xl pt-20 pb-4">
|
||||
<motion.img
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 120
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
src="/logo.webp" alt="Nestri Logo" height={80} width={80} draggable={false} className="w-[70px] md:w-[80px] aspect-[90/69]" />
|
||||
<div className="my-4 sm:my-8">
|
||||
<ReactDisplay className="mb-4 sm:text-8xl text-[3.5rem] text-balance tracking-tight leading-none" >
|
||||
<motion.span
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.1,
|
||||
...transition
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="inline-block" >
|
||||
{title}
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.3,
|
||||
...transition
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="dark:text-primary-50/70 text-primary-950/70 text-lg font-normal tracking-tight sm:text-xl"
|
||||
>
|
||||
{Array.isArray(description) ? description.map((item, index) => {
|
||||
return <span key={`id-${index}`}>{item} <br /> </span>
|
||||
}) : description}
|
||||
</motion.p>
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const TitleSection = qwikify$(ReactTitleSection)
|
||||
56
packages/ui/src/react/title.tsx
Normal file
56
packages/ui/src/react/title.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/** @jsxImportSource react */
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
title,
|
||||
type TitleProps as TitleVariants,
|
||||
type TextAlignProp,
|
||||
type TextWeightProp,
|
||||
cn
|
||||
} from "@/design"
|
||||
import { qwikify$ } from "@builder.io/qwik-react";
|
||||
|
||||
|
||||
type TitleSize = TitleVariants["size"]
|
||||
type TitleSizeProp = TitleSize | {
|
||||
initial?: TitleSize,
|
||||
sm?: TitleSize,
|
||||
md?: TitleSize,
|
||||
lg?: TitleSize,
|
||||
xl?: TitleSize,
|
||||
xxl?: TitleSize,
|
||||
}
|
||||
|
||||
export interface TitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "span",
|
||||
className?: string,
|
||||
size?: TitleSizeProp;
|
||||
align?: TextAlignProp;
|
||||
weight?: TextWeightProp
|
||||
}
|
||||
|
||||
export const ReactTitle: React.FC<TitleProps> = ({
|
||||
size,
|
||||
as = "h1",
|
||||
weight,
|
||||
align,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const TitleElement = as
|
||||
return (
|
||||
<TitleElement className={title({
|
||||
size,
|
||||
weight,
|
||||
align,
|
||||
className: cn("font-title dark:text-primary-50 text-primary-950", className)
|
||||
})} {...props}>
|
||||
{children}
|
||||
</TitleElement>
|
||||
)
|
||||
}
|
||||
|
||||
ReactTitle.displayName = "Title"
|
||||
|
||||
export const Title = qwikify$(ReactTitle)
|
||||
65
packages/ui/src/team-counter.tsx
Normal file
65
packages/ui/src/team-counter.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { component$, useSignal } from "@builder.io/qwik"
|
||||
import { cn } from "./design"
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
}
|
||||
const minValue = 2;
|
||||
const maxValue = 6;
|
||||
const teamSizes = Array.from({ length: maxValue - minValue + 1 }, (_, i) => i + minValue);
|
||||
|
||||
export const TeamCounter = component$(({ class: className }: Props) => {
|
||||
const teammates = useSignal(2)
|
||||
const shake = useSignal(false)
|
||||
|
||||
return (
|
||||
<div class={cn("flex items-center justify-center", shake.value && "animate-shake", className)}>
|
||||
<button
|
||||
onClick$={() => {
|
||||
if (teammates.value == minValue) {
|
||||
shake.value = true
|
||||
setTimeout(() => {
|
||||
shake.value = false
|
||||
}, 500);
|
||||
} else {
|
||||
teammates.value = Math.max(minValue, teammates.value - 1)
|
||||
}
|
||||
}}
|
||||
class={cn("size-[30px] rounded-full bg-gray-300 dark:bg-gray-700 flex items-center justify-center active:scale-90", teammates.value <= minValue && "opacity-30")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[15px]" viewBox="0 0 256 256"><path fill="currentColor" d="M228 128a12 12 0 0 1-12 12H40a12 12 0 0 1 0-24h176a12 12 0 0 1 12 12" /></svg>
|
||||
</button>
|
||||
<div class="flex items-center justify-center mx-2 mt-1">
|
||||
<p class="text-lg font-normal w-full flex items-center justify-center">
|
||||
<span
|
||||
class="w-[2ch] relative inline-block h-[2.625rem] overflow-hidden [mask:linear-gradient(#0000,_#000_35%_65%,_#0000)]">
|
||||
<span
|
||||
style={{
|
||||
translate: `0 calc((${teammates.value - (minValue + 1)} + 1) * (2.625rem * -1))`,
|
||||
transition: `translate 0.625s linear(0 0%,0.5007 7.21%,0.7803 12.29%,0.8883 14.93%,0.9724 17.63%,1.0343 20.44%,1.0754 23.44%,1.0898 25.22%,1.0984 27.11%,1.1014 29.15%,1.0989 31.4%,1.0854 35.23%,1.0196 48.86%,1.0043 54.06%,0.9956 59.6%,0.9925 68.11%,1 100%)`
|
||||
}}
|
||||
class="absolute w-full flex top-0 flex-col">
|
||||
{teamSizes.map((num) => (
|
||||
<span
|
||||
class="w-full font-title h-[2.625rem] flex flex-col items-center justify-center leading-[1rem]"
|
||||
key={`team-member-${num}`} >+{num}</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<button onClick$={() => {
|
||||
if (teammates.value == maxValue) {
|
||||
shake.value = true
|
||||
setTimeout(() => {
|
||||
shake.value = false
|
||||
}, 500);
|
||||
} else {
|
||||
teammates.value = Math.min(maxValue, teammates.value + 1)
|
||||
}
|
||||
}}
|
||||
class={cn("size-[30px] rounded-full bg-gray-300 dark:bg-gray-700 flex items-center justify-center active:scale-90", teammates.value >= maxValue && "opacity-30")}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="size-[15px]" viewBox="0 0 256 256"><path fill="currentColor" d="M228 128a12 12 0 0 1-12 12h-76v76a12 12 0 0 1-24 0v-76H40a12 12 0 0 1 0-24h76V40a12 12 0 0 1 24 0v76h76a12 12 0 0 1 12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
24
packages/ui/src/tooltip.tsx
Normal file
24
packages/ui/src/tooltip.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { component$, Slot } from "@builder.io/qwik"
|
||||
import { cn } from "./design";
|
||||
|
||||
type Props = {
|
||||
position: "bottom" | "top" | "left" | "right",
|
||||
text: string;
|
||||
}
|
||||
|
||||
const textPosition = {
|
||||
top: "bottom-[125%] left-1/2 -ml-[60px] after:absolute after:left-1/2 after:top-[100%] after:-ml-[5px] after:border-[5px] after:border-[#000_transparent_transparent_transparent]"
|
||||
}
|
||||
|
||||
export const Tooltip = component$(({ position, text }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Slot />
|
||||
{/**@ts-ignore */}
|
||||
<span class={cn("invisible absolute w-[120px] group-hover:visible group-hover:opacity-100 text-white bg-black text-center py-1 rounded-md", textPosition[position])} >
|
||||
{text}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -8,6 +8,6 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "./*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"files": [".eslintrc.js"],
|
||||
"include": ["src", "./*.d.ts"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"extends": "@nestri/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
},
|
||||
"include": ["src", "./*.config.js", "./.eslintrc.js"],
|
||||
"include": ["src", "./*.config.js","./.eslintrc.js"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user