optimize elements by reducing the bundle size required

This commit is contained in:
dswbx
2025-01-14 14:10:19 +01:00
parent c7bd0a636b
commit 4ee0f74cdb
30 changed files with 373 additions and 281 deletions

View File

@@ -1,4 +1,7 @@
import fs from "node:fs";
import { $ } from "bun";
import { replace } from "esbuild-plugin-replace";
import * as tsup from "tsup";
const args = process.argv.slice(2);
@@ -80,7 +83,8 @@ await tsup.build({
"src/ui/index.ts",
"src/ui/client/index.ts",
"src/ui/elements/index.ts",
"src/ui/main.css"
"src/ui/main.css",
"src/ui/styles.css"
],
outDir: "dist/ui",
external: [
@@ -108,6 +112,50 @@ await tsup.build({
}
});
/**
* Building UI Elements
* - tailwind-merge is mocked, no exclude
* - ui/client is external, and after built replaced with "bknd/client"
*/
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/ui/elements/index.ts"],
outDir: "dist/ui/elements",
external: [
"ui/client",
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store"
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl"
},
esbuildOptions: (options) => {
options.alias = {
// not important for elements, mock to reduce bundle
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
};
},
onSuccess: async () => {
// manually replace ui/client with bknd/client
const path = "./dist/ui/elements/index.js";
const bundle = await Bun.file(path).text();
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
delayTypes();
}
});
/**
* Building adapters
*/

View File

@@ -17,7 +17,7 @@
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run test && bun run build:all"
"prepublishOnly": "bun run types && bun run test && bun run build:all"
},
"license": "FSL-1.1-MIT",
"dependencies": {
@@ -29,12 +29,12 @@
"dayjs": "^1.11.13",
"fast-xml-parser": "^4.4.0",
"hono": "^4.6.12",
"json-schema-form-react": "^0.0.2",
"kysely": "^0.27.4",
"liquidjs": "^10.15.0",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"swr": "^2.2.5",
"json-schema-form-react": "^0.0.2"
"swr": "^2.2.5"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.613.0",
@@ -62,6 +62,7 @@
"@vitejs/plugin-react": "^4.3.3",
"@xyflow/react": "^12.3.2",
"autoprefixer": "^10.4.20",
"clsx": "^2.1.1",
"esbuild-postcss": "^0.0.4",
"jotai": "^2.10.1",
"open": "^10.1.0",
@@ -168,7 +169,8 @@
"import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs"
},
"./dist/styles.css": "./dist/ui/main.css",
"./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
},
"publishConfig": {

View File

@@ -1,5 +1,5 @@
import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules/ModuleApi";
import type { FileWithPath } from "ui/modules/media/components/dropzone/file-selector";
import type { FileWithPath } from "ui/elements/media/file-selector";
export type MediaApiOptions = BaseModuleApiOptions & {};

View File

@@ -1,7 +1,8 @@
import type { TObject, TString } from "@sinclair/typebox";
import { type Constructor, Registry } from "core";
export { MIME_TYPES } from "./storage/mime-types";
//export { MIME_TYPES } from "./storage/mime-types";
export { guess as guessMimeType } from "./storage/mime-types-tiny";
export {
Storage,
type StorageAdapter,
@@ -19,7 +20,7 @@ import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/Stora
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
export * as StorageEvents from "./storage/events";
export { type FileUploadedEventData } from "./storage/events";
export type { FileUploadedEventData } from "./storage/events";
export * from "./utils";
type ClassThatImplements<T> = Constructor<T> & { prototype: T };

View File

@@ -12,7 +12,6 @@ export type ClientProviderProps = {
};
export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => {
//const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
const winCtx = useBkndWindowContext();
const _ctx_baseUrl = useBaseUrl();
let actualBaseUrl = baseUrl ?? _ctx_baseUrl ?? "";
@@ -31,6 +30,7 @@ export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps)
console.error("error .....", e);
}
console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user });
const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user });
return (

View File

@@ -73,23 +73,3 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
verify
};
};
type AuthStrategyData = Pick<AppAuthSchema, "strategies" | "basepath">;
export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthStrategyData> & {
loading: boolean;
} => {
const [data, setData] = useState<AuthStrategyData>();
const api = useApi(options?.baseUrl);
useEffect(() => {
(async () => {
const res = await api.auth.strategies();
//console.log("res", res);
if (res.res.ok) {
setData(res.body);
}
})();
}, [options?.baseUrl]);
return { strategies: data?.strategies, basepath: data?.basepath, loading: !data };
};

View File

@@ -0,0 +1,29 @@
import { Switch } from "@mantine/core";
import { forwardRef, useEffect, useState } from "react";
export const BooleanInputMantine = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
(props, ref) => {
const [checked, setChecked] = useState(Boolean(props.value));
useEffect(() => {
setChecked(Boolean(props.value));
}, [props.value]);
function handleCheck(e) {
setChecked(e.target.checked);
props.onChange?.(e.target.checked);
}
return (
<div className="flex flex-row">
<Switch
ref={ref}
checked={checked}
onChange={handleCheck}
disabled={props.disabled}
id={props.id}
/>
</div>
);
}
);

View File

@@ -1,11 +1,10 @@
import { Switch } from "@mantine/core";
import { getBrowser } from "core/utils";
import type { Field } from "data";
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb";
import { twMerge } from "tailwind-merge";
import { useEvent } from "../../hooks/use-event";
import { IconButton } from "../buttons/IconButton";
import { IconButton } from "ui/components/buttons/IconButton";
import { useEvent } from "ui/hooks/use-event";
export const Group: React.FC<React.ComponentProps<"div"> & { error?: boolean }> = ({
error,
@@ -131,17 +130,6 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
}
return (
<div className="flex flex-row">
<Switch
ref={ref}
checked={checked}
onChange={handleCheck}
disabled={props.disabled}
id={props.id}
/>
</div>
);
/*return (
<div className="h-11 flex items-center">
<input
{...props}
@@ -153,7 +141,7 @@ export const BooleanInput = forwardRef<HTMLInputElement, React.ComponentProps<"i
disabled={props.disabled}
/>
</div>
);*/
);
}
);

View File

@@ -0,0 +1,17 @@
import { BooleanInputMantine } from "./BooleanInputMantine";
import { DateInput, Input, Textarea } from "./components";
export const formElementFactory = (element: string, props: any) => {
switch (element) {
case "date":
return DateInput;
case "boolean":
return BooleanInputMantine;
case "textarea":
return Textarea;
default:
return Input;
}
};
export * from "./components";

View File

@@ -1,13 +1,13 @@
import type { ValueError } from "@sinclair/typebox/value";
import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema";
import clsx from "clsx";
import { type TSchema, Type, Value } from "core/utils";
import { Form, type Validator } from "json-schema-form-react";
import { transform } from "lodash-es";
import type { ComponentPropsWithoutRef } from "react";
import { twMerge } from "tailwind-merge";
import { Button } from "ui/components/buttons/Button";
import { Group, Input, Label } from "ui/components/form/Formy";
import { SocialLink } from "ui/modules/auth/SocialLink";
import { Group, Input, Label } from "ui/components/form/Formy/components";
import { SocialLink } from "./SocialLink";
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit" | "action"> & {
className?: string;
@@ -86,7 +86,7 @@ export function AuthForm({
schema={schema}
validator={validator}
validationMode="change"
className={twMerge("flex flex-col gap-3 w-full", className)}
className={clsx("flex flex-col gap-3 w-full", className)}
>
{({ errors, submitting }) => (
<>

View File

@@ -1,8 +1,6 @@
import type { ReactNode } from "react";
import { useAuthStrategies } from "ui/client/schema/auth/use-auth";
import { Logo } from "ui/components/display/Logo";
import { Link } from "ui/components/wouter/Link";
import { AuthForm } from "ui/modules/auth/AuthForm";
import { useAuthStrategies } from "../hooks/use-auth";
import { AuthForm } from "./AuthForm";
export type AuthScreenProps = {
method?: "POST" | "GET";
@@ -18,13 +16,7 @@ export function AuthScreen({ method = "POST", action = "login", logo, intro }: A
<div className="flex flex-1 flex-col select-none h-dvh w-dvw justify-center items-center bknd-admin">
{!loading && (
<div className="flex flex-col gap-4 items-center w-96 px-6 py-7">
{typeof logo !== "undefined" ? (
logo
) : (
<Link href={"/"} className="link">
<Logo scale={0.25} />
</Link>
)}
{logo ? logo : null}
{typeof intro !== "undefined" ? (
intro
) : (

View File

@@ -0,0 +1,9 @@
import { AuthForm } from "./AuthForm";
import { AuthScreen } from "./AuthScreen";
import { SocialLink } from "./SocialLink";
export const Auth = {
Screen: AuthScreen,
Form: AuthForm,
SocialLink: SocialLink
};

View File

@@ -0,0 +1,23 @@
import type { AppAuthSchema } from "auth/auth-schema";
import { useEffect, useState } from "react";
import { useApi } from "ui/client";
type AuthStrategyData = Pick<AppAuthSchema, "strategies" | "basepath">;
export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthStrategyData> & {
loading: boolean;
} => {
const [data, setData] = useState<AuthStrategyData>();
const api = useApi(options?.baseUrl);
useEffect(() => {
(async () => {
const res = await api.auth.strategies();
//console.log("res", res);
if (res.res.ok) {
setData(res.body);
}
})();
}, [options?.baseUrl]);
return { strategies: data?.strategies, basepath: data?.basepath, loading: !data };
};

View File

@@ -1,2 +1,2 @@
export { Auth } from "ui/modules/auth/index";
export { Auth } from "./auth";
export * from "./media";

View File

@@ -1,15 +0,0 @@
import { PreviewWrapperMemoized } from "ui/modules/media/components/dropzone/Dropzone";
import { DropzoneContainer } from "ui/modules/media/components/dropzone/DropzoneContainer";
export const Media = {
Dropzone: DropzoneContainer,
Preview: PreviewWrapperMemoized
};
export type {
PreviewComponentProps,
FileState,
DropzoneProps,
DropzoneRenderProps
} from "ui/modules/media/components/dropzone/Dropzone";
export type { DropzoneContainerProps } from "ui/modules/media/components/dropzone/DropzoneContainer";

View File

@@ -4,13 +4,8 @@ import type { TAppMediaConfig } from "media/media-schema";
import { useId } from "react";
import { useApi, useBaseUrl, useEntityQuery, useInvalidate } from "ui/client";
import { useEvent } from "ui/hooks/use-event";
import {
Dropzone,
type DropzoneProps,
type DropzoneRenderProps,
type FileState
} from "ui/modules/media/components/dropzone/Dropzone";
import { mediaItemsToFileStates } from "ui/modules/media/helper";
import { Dropzone, type DropzoneProps, type DropzoneRenderProps, type FileState } from "./Dropzone";
import { mediaItemsToFileStates } from "./helper";
export type DropzoneContainerProps = {
children?: (props: DropzoneRenderProps) => JSX.Element;

View File

@@ -4,7 +4,7 @@
* MIT License (2020 Roland Groza)
*/
import { MIME_TYPES } from "media";
import { guess } from "media/storage/mime-types-tiny";
const FILES_TO_IGNORE = [
// Thumbnail cache files for macOS and Windows
@@ -47,10 +47,8 @@ function withMimeType(file: FileWithPath) {
console.log("withMimeType", name, hasExtension);
if (hasExtension && !file.type) {
const ext = name.split(".").pop()!.toLowerCase();
const type = MIME_TYPES.get(ext);
console.log("withMimeType:in", ext, type);
const type = guess(name);
console.log("guessed", type);
if (type) {
Object.defineProperty(file, "type", {

View File

@@ -1,5 +1,5 @@
import type { MediaFieldSchema } from "media/AppMedia";
import type { FileState } from "./components/dropzone/Dropzone";
import type { FileState } from "./Dropzone";
export function mediaItemToFileState(
item: MediaFieldSchema,

View File

@@ -0,0 +1,15 @@
import { PreviewWrapperMemoized } from "./Dropzone";
import { DropzoneContainer } from "./DropzoneContainer";
export const Media = {
Dropzone: DropzoneContainer,
Preview: PreviewWrapperMemoized
};
export type {
PreviewComponentProps,
FileState,
DropzoneProps,
DropzoneRenderProps
} from "./Dropzone";
export type { DropzoneContainerProps } from "./DropzoneContainer";

View File

@@ -0,0 +1,3 @@
export function twMerge(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}

View File

@@ -1,211 +1,77 @@
@import "./components/form/json-schema/styles.css";
@import "@xyflow/react/dist/style.css";
@import "@mantine/core/styles.css";
@import "@mantine/notifications/styles.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
html.fixed,
html.fixed body {
top: 0;
left: 0;
height: 100%;
width: 100%;
position: fixed;
overflow: hidden;
overscroll-behavior-x: contain;
touch-action: none;
}
#bknd-admin,
.bknd-admin {
--color-primary: 9 9 11; /* zinc-950 */
--color-background: 250 250 250; /* zinc-50 */
--color-muted: 228 228 231; /* ? */
--color-darkest: 0 0 0; /* black */
--color-lightest: 255 255 255; /* white */
&.dark {
--color-primary: 250 250 250; /* zinc-50 */
--color-background: 30 31 34;
--color-muted: 47 47 52;
--color-darkest: 255 255 255; /* white */
--color-lightest: 24 24 27; /* black */
}
@mixin light {
--mantine-color-body: rgb(250 250 250);
}
@mixin dark {
--mantine-color-body: rgb(9 9 11);
}
table {
font-size: inherit;
}
}
html,
body {
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none;
}
#bknd-admin {
@apply bg-background text-primary overflow-hidden h-dvh w-dvw;
@apply bg-background text-primary overflow-hidden h-dvh w-dvw;
::selection {
@apply bg-muted;
}
::selection {
@apply bg-muted;
}
input {
&::selection {
@apply bg-primary/15;
}
}
input {
&::selection {
@apply bg-primary/15;
}
}
}
body,
#bknd-admin {
@apply flex flex-1 flex-col h-dvh w-dvw;
@apply flex flex-1 flex-col h-dvh w-dvw;
}
@layer components {
.link {
@apply transition-colors active:translate-y-px;
}
.link {
@apply transition-colors active:translate-y-px;
}
.img-responsive {
@apply max-h-full w-auto;
}
.img-responsive {
@apply max-h-full w-auto;
}
/**
* debug classes
*/
.bordered-red {
@apply border-2 border-red-500;
}
/**
* debug classes
*/
.bordered-red {
@apply border-2 border-red-500;
}
.bordered-green {
@apply border-2 border-green-500;
}
.bordered-green {
@apply border-2 border-green-500;
}
.bordered-blue {
@apply border-2 border-blue-500;
}
.bordered-blue {
@apply border-2 border-blue-500;
}
.bordered-violet {
@apply border-2 border-violet-500;
}
.bordered-violet {
@apply border-2 border-violet-500;
}
.bordered-yellow {
@apply border-2 border-yellow-500;
}
.bordered-yellow {
@apply border-2 border-yellow-500;
}
}
@layer utilities {
}
/* Hide scrollbar for Chrome, Safari and Opera */
.app-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.app-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
div[data-radix-scroll-area-viewport] > div:first-child {
display: block !important;
min-width: 100% !important;
max-width: 100%;
}
/* hide calendar icon on inputs */
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-calendar-picker-indicator {
display: none;
}
/* cm */
.cm-editor {
display: flex;
flex: 1;
}
.animate-fade-in {
animation: fadeInAnimation 200ms ease;
}
@keyframes fadeInAnimation {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
input[readonly]::placeholder,
input[disabled]::placeholder {
opacity: 0.1;
}
.react-flow__pane,
.react-flow__renderer,
.react-flow__node,
.react-flow__edge {
cursor: inherit !important;
.drag-handle {
cursor: grab;
}
}
.react-flow .react-flow__edge path,
.react-flow__connectionline path {
stroke-width: 2;
}
.mantine-TextInput-wrapper input {
font-family: inherit;
line-height: 1;
}
.cm-editor {
background: transparent;
}
.cm-editor.cm-focused {
outline: none;
}
.flex-animate {
transition: flex-grow 0.2s ease, background-color 0.2s ease;
}
.flex-initial {
flex: 0 1 auto;
}
.flex-open {
flex: 1 1 0;
}
#bknd-admin,
.bknd-admin {
/* Chrome, Edge, and Safari */
& *::-webkit-scrollbar {
@apply w-1;
&:horizontal {
@apply h-px;
}
}
/* Chrome, Edge, and Safari */
& *::-webkit-scrollbar {
@apply w-1;
&:horizontal {
@apply h-px;
}
}
& *::-webkit-scrollbar-track {
@apply bg-transparent w-1;
}
& *::-webkit-scrollbar-track {
@apply bg-transparent w-1;
}
& *::-webkit-scrollbar-thumb {
@apply bg-primary/25;
}
& *::-webkit-scrollbar-thumb {
@apply bg-primary/25;
}
}

View File

@@ -2,6 +2,7 @@ import * as React from "react";
import * as ReactDOM from "react-dom/client";
import Admin from "./Admin";
import "./main.css";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>

View File

@@ -1,9 +0,0 @@
import { AuthForm } from "ui/modules/auth/AuthForm";
import { AuthScreen } from "ui/modules/auth/AuthScreen";
import { SocialLink } from "ui/modules/auth/SocialLink";
export const Auth = {
Screen: AuthScreen,
Form: AuthForm,
SocialLink: SocialLink
};

View File

@@ -1,7 +1,18 @@
import { Logo } from "ui/components/display/Logo";
import { Link } from "ui/components/wouter/Link";
import { Auth } from "ui/elements";
import { useBrowserTitle } from "ui/hooks/use-browser-title";
import { AuthScreen } from "ui/modules/auth/AuthScreen";
export function AuthLogin() {
useBrowserTitle(["Login"]);
return <AuthScreen action="login" />;
return (
<Auth.Screen
action="login"
logo={
<Link href={"/"} className="link">
<Logo scale={0.25} />
</Link>
}
/>
);
}

133
app/src/ui/styles.css Normal file
View File

@@ -0,0 +1,133 @@
@import "./components/form/json-schema/styles.css";
@import "@xyflow/react/dist/style.css";
@import "@mantine/core/styles.css";
@import "@mantine/notifications/styles.css";
html.fixed,
html.fixed body {
top: 0;
left: 0;
height: 100%;
width: 100%;
position: fixed;
overflow: hidden;
overscroll-behavior-x: contain;
touch-action: none;
}
#bknd-admin,
.bknd-admin {
--color-primary: 9 9 11; /* zinc-950 */
--color-background: 250 250 250; /* zinc-50 */
--color-muted: 228 228 231; /* ? */
--color-darkest: 0 0 0; /* black */
--color-lightest: 255 255 255; /* white */
&.dark {
--color-primary: 250 250 250; /* zinc-50 */
--color-background: 30 31 34;
--color-muted: 47 47 52;
--color-darkest: 255 255 255; /* white */
--color-lightest: 24 24 27; /* black */
}
@mixin light {
--mantine-color-body: rgb(250 250 250);
}
@mixin dark {
--mantine-color-body: rgb(9 9 11);
}
table {
font-size: inherit;
}
}
html,
body {
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.app-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.app-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
div[data-radix-scroll-area-viewport] > div:first-child {
display: block !important;
min-width: 100% !important;
max-width: 100%;
}
/* hide calendar icon on inputs */
input[type="datetime-local"]::-webkit-calendar-picker-indicator,
input[type="date"]::-webkit-calendar-picker-indicator {
display: none;
}
/* cm */
.cm-editor {
display: flex;
flex: 1;
}
.animate-fade-in {
animation: fadeInAnimation 200ms ease;
}
@keyframes fadeInAnimation {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
input[readonly]::placeholder,
input[disabled]::placeholder {
opacity: 0.1;
}
.react-flow__pane,
.react-flow__renderer,
.react-flow__node,
.react-flow__edge {
cursor: inherit !important;
.drag-handle {
cursor: grab;
}
}
.react-flow .react-flow__edge path,
.react-flow__connectionline path {
stroke-width: 2;
}
.mantine-TextInput-wrapper input {
font-family: inherit;
line-height: 1;
}
.cm-editor {
background: transparent;
}
.cm-editor.cm-focused {
outline: none;
}
.flex-animate {
transition: flex-grow 0.2s ease, background-color 0.2s ease;
}
.flex-initial {
flex: 0 1 auto;
}
.flex-open {
flex: 1 1 0;
}

View File

@@ -19,6 +19,11 @@
"trailingCommas": "none"
}
},
"css": {
"formatter": {
"indentWidth": 3
}
},
"files": {
"ignore": [
"**/node_modules/**",

BIN
bun.lockb

Binary file not shown.