Compare commits
9 Commits
v0.0.9
...
cbd681e63b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbd681e63b
|
||
|
|
7265eaad18
|
||
|
|
53ccfe1d4f
|
||
|
|
a5a2f8324e
|
||
|
|
dc3d1ac60d
|
||
|
|
8fe121951b
|
||
|
|
b345982ab1
|
||
|
|
ca342cc238
|
||
|
|
0de206016a
|
@@ -1,6 +1,6 @@
|
||||
import js from "@eslint/js";
|
||||
import pluginRouter from "@tanstack/eslint-plugin-router";
|
||||
import react from "eslint-plugin-react";
|
||||
import eslintReact from "@eslint-react/eslint-plugin";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
@@ -9,12 +9,12 @@ import tseslint from "typescript-eslint";
|
||||
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
globalIgnores(["dist", "coverage", ".tanstack", ".scannerwork"]),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
react.configs.flat.recommended,
|
||||
eslintReact.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
reactHooks.configs.flat["recommended-latest"],
|
||||
reactRefresh.configs.vite,
|
||||
|
||||
@@ -47,6 +47,8 @@ export { default as DragAndDropFileInput } from "./file/DragAndDropFileInput";
|
||||
export { default as FileInput } from "./file/FileInput";
|
||||
export { default as NumberInput } from "./number/NumberInput";
|
||||
export { default as OptionInput } from "./text/OptionInput";
|
||||
export { default as PasswordInput } from "./text/PasswordInput";
|
||||
export { default as PhoneInput } from "./text/PhoneInput";
|
||||
export { default as SelectInput } from "./text/SelectInput";
|
||||
export { default as TextArea } from "./text/TextArea";
|
||||
export { default as TextInput } from "./text/TextInput";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { RadioButtonProps } from "$/types/InputTypes";
|
||||
import { Radio } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
import { useId } from "react";
|
||||
|
||||
|
||||
export default function RadioButton({
|
||||
id = crypto.randomUUID().replaceAll("-", ""),
|
||||
id,
|
||||
className,
|
||||
labelClassName,
|
||||
size = "sm",
|
||||
@@ -12,9 +13,12 @@ export default function RadioButton({
|
||||
disabled,
|
||||
children
|
||||
}: Readonly<RadioButtonProps>) {
|
||||
const reactId = useId();
|
||||
const effectiveId = id ?? reactId;
|
||||
|
||||
return (
|
||||
<Radio
|
||||
id={id}
|
||||
id={effectiveId}
|
||||
value={value}
|
||||
className={clsx(
|
||||
"group",
|
||||
@@ -26,7 +30,7 @@ export default function RadioButton({
|
||||
}
|
||||
)}
|
||||
disabled={disabled}
|
||||
aria-labelledby={`${id}Label`}
|
||||
aria-labelledby={`${effectiveId}Label`}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -45,7 +49,7 @@ export default function RadioButton({
|
||||
{
|
||||
children &&
|
||||
<span
|
||||
id={`${id}Label`}
|
||||
id={`${effectiveId}Label`}
|
||||
className={labelClassName}
|
||||
>
|
||||
{children}
|
||||
|
||||
66
lib/component/input/text/PasswordInput.tsx
Normal file
66
lib/component/input/text/PasswordInput.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { TextInputProps } from "$/types/InputTypes";
|
||||
import clsx from "clsx";
|
||||
import { useId } from "react";
|
||||
|
||||
|
||||
export default function PasswordInput({
|
||||
id,
|
||||
className,
|
||||
inputClassName,
|
||||
labelClassName,
|
||||
name,
|
||||
maxLength,
|
||||
spellCheck,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
disabled
|
||||
}: Readonly<TextInputProps>){
|
||||
const componentId = useId();
|
||||
const activeId = id ?? componentId;
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex flex-row items-center justify-center rounded-lg border-2 w-full",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="relative flex flex-row items-center justify-center px-2 py-1 w-full"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id={activeId}
|
||||
className={clsx(
|
||||
"peer bg-transparent outline-none placeholder-transparent w-full",
|
||||
inputClassName
|
||||
)}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
disabled={disabled}
|
||||
spellCheck={spellCheck}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<label
|
||||
className={clsx(
|
||||
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default",
|
||||
"peer-placeholder-shown:top-0 peer-placeholder-shown:-left-1 peer-placeholder-shown:text-inherit peer-placeholder-shown:text-base peer-placeholder-shown:h-full peer-placeholder-shown:cursor-text peer-placeholder-shown:w-[98%]",
|
||||
"peer-focus:-top-3 peer-focus:left-0 peer-focus:text-sm peer-focus:w-auto peer-focus:h-auto",
|
||||
"flex items-center",
|
||||
labelClassName
|
||||
)}
|
||||
style={{ transitionProperty: "top, left, font-size, line-height", transitionTimingFunction: "cubic-bezier(0.4 0, 0.2, 1)", transitionDuration: "250ms" }}
|
||||
htmlFor={activeId}
|
||||
>
|
||||
{placeholder}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
87
lib/component/input/text/PhoneInput.tsx
Normal file
87
lib/component/input/text/PhoneInput.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { TextInputProps } from "$/types/InputTypes";
|
||||
import clsx from "clsx";
|
||||
import { useId } from "react";
|
||||
|
||||
|
||||
export default function PhoneInput({
|
||||
id,
|
||||
className,
|
||||
inputClassName,
|
||||
labelClassName,
|
||||
name,
|
||||
maxLength,
|
||||
spellCheck,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
disabled
|
||||
}: Readonly<TextInputProps>){
|
||||
const componentId = useId();
|
||||
const activeId = id ?? componentId;
|
||||
|
||||
//TODO: Figure out how to setup phone number
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex flex-row items-center justify-center rounded-lg border-2 w-full",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="relative flex flex-row items-center justify-center px-2 py-1 w-full"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id={activeId}
|
||||
className={clsx(
|
||||
"peer bg-transparent outline-none placeholder-transparent w-full",
|
||||
inputClassName
|
||||
)}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={disabled}
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
<label
|
||||
className={clsx(
|
||||
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default",
|
||||
"peer-placeholder-shown:top-0 peer-placeholder-shown:-left-1 peer-placeholder-shown:text-inherit peer-placeholder-shown:text-base peer-placeholder-shown:h-full peer-placeholder-shown:cursor-text peer-placeholder-shown:w-[99%]",
|
||||
"peer-focus:-top-3 peer-focus:left-0 peer-focus:text-sm peer-focus:w-auto peer-focus:h-auto",
|
||||
"flex items-center",
|
||||
labelClassName
|
||||
)}
|
||||
style={{ transitionProperty: "top, left, font-size, line-height", transitionTimingFunction: "cubic-bezier(0.4 0, 0.2, 1)", transitionDuration: "250ms" }}
|
||||
htmlFor={activeId}
|
||||
>
|
||||
{placeholder}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
function formatPhoneNumber(phoneNumber: string): string {
|
||||
const chars: string[] = [];
|
||||
|
||||
// Separate the string into individual characters
|
||||
for(let cnt = 0;cnt < phoneNumber.length;++cnt){
|
||||
chars.push(phoneNumber.charAt(cnt));
|
||||
}
|
||||
|
||||
// Add _ for any chars that don't exist
|
||||
for(let cnt = chars.length;cnt < 10;++cnt){
|
||||
chars.push("_");
|
||||
}
|
||||
|
||||
// Put the values into the correct format
|
||||
return "(" + chars.slice(0, 3).join() + ") " + chars.slice(3, 6).join() + "-" + chars.slice(6).join();
|
||||
}
|
||||
*/
|
||||
@@ -16,6 +16,7 @@ export default function TextArea({
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
disabled
|
||||
}: Readonly<TextAreaProps>){
|
||||
const componentId = useId();
|
||||
@@ -44,14 +45,15 @@ export default function TextArea({
|
||||
rows={rows}
|
||||
cols={cols}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={disabled}
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
<label
|
||||
className={clsx(
|
||||
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default",
|
||||
"peer-placeholder-shown:top-0 peer-placeholder-shown:-left-1 peer-placeholder-shown:text-inherit peer-placeholder-shown:text-base peer-placeholder-shown:bg-transparent peer-placeholder-shown:cursor-text peer-placeholder-shown:w-[99%]",
|
||||
"peer-placeholder-shown:top-0 peer-placeholder-shown:-left-1 peer-placeholder-shown:text-inherit peer-placeholder-shown:text-base peer-placeholder-shown:cursor-text peer-placeholder-shown:w-[98%]",
|
||||
"peer-focus:-top-3 peer-focus:left-0 peer-focus:text-sm peer-focus:w-auto peer-focus:h-auto",
|
||||
"flex items-center",
|
||||
labelClassName
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function TextInput({
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
disabled
|
||||
}: Readonly<TextInputProps>){
|
||||
const componentId = useId();
|
||||
@@ -41,14 +42,15 @@ export default function TextInput({
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
disabled={disabled}
|
||||
spellCheck={spellCheck}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<label
|
||||
className={clsx(
|
||||
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default",
|
||||
"peer-placeholder-shown:top-0 peer-placeholder-shown:-left-1 peer-placeholder-shown:text-inherit peer-placeholder-shown:text-base peer-placeholder-shown:bg-transparent peer-placeholder-shown:h-full peer-placeholder-shown:cursor-text peer-placeholder-shown:w-[99%]",
|
||||
"peer-placeholder-shown:top-0 peer-placeholder-shown:-left-1 peer-placeholder-shown:text-inherit peer-placeholder-shown:text-base peer-placeholder-shown:h-full peer-placeholder-shown:cursor-text peer-placeholder-shown:w-[98%]",
|
||||
"peer-focus:-top-3 peer-focus:left-0 peer-focus:text-sm peer-focus:w-auto peer-focus:h-auto",
|
||||
"flex items-center",
|
||||
labelClassName
|
||||
|
||||
@@ -30,6 +30,8 @@ export default function ModalHeader({
|
||||
<Button
|
||||
className="absolute top-1 right-1 cursor-pointer"
|
||||
onClick={onClose}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
>
|
||||
<BsXLg
|
||||
size={20}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTheme } from "$/providers/theme/ThemeProvider";
|
||||
import { useTheme } from "$/provider/theme/ThemeProvider";
|
||||
import { BsLightbulb, BsLightbulbFill } from "react-icons/bs";
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ export default function MattrixwvTabGroup({
|
||||
>
|
||||
<MattrixwvTabList>
|
||||
{
|
||||
tabs.map((tab, index) => (
|
||||
<MattrixwvTab key={index}>
|
||||
tabs.map(tab => (
|
||||
<MattrixwvTab key={tab.id}>
|
||||
{tab.tab}
|
||||
</MattrixwvTab>
|
||||
))
|
||||
@@ -29,8 +29,8 @@ export default function MattrixwvTabGroup({
|
||||
</MattrixwvTabList>
|
||||
<MattrixwvTabPanels>
|
||||
{
|
||||
tabs.map((tab, index) => (
|
||||
<MattrixwvTabPanel key={index}>
|
||||
tabs.map(tab => (
|
||||
<MattrixwvTabPanel key={tab.id}>
|
||||
{tab.content}
|
||||
</MattrixwvTabPanel>
|
||||
))
|
||||
|
||||
@@ -7,8 +7,10 @@ export default function Toaster({
|
||||
toast,
|
||||
className
|
||||
}: Readonly<ToasterProps>) {
|
||||
//TODO: Rework how your toasters work
|
||||
return (
|
||||
<Transition
|
||||
// eslint-disable-next-line @eslint-react/purity
|
||||
show={toast.length > 1 || (toast.length === 1 && toast[0].hideTime > new Date())}
|
||||
enter="transform transition duration-500"
|
||||
enterFrom="-translate-y-[25vh]"
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as ToasterProvider, useToaster } from "$/providers/toaster/ToasterProvider";
|
||||
export { default as Toaster } from "./Toaster";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
@theme {
|
||||
/* Universal */
|
||||
--color-neutral-light: oklch(92.8% 0.006 264.531); /* gray-200 */
|
||||
@@ -7,5 +7,8 @@ export * from "$/component/nav";
|
||||
export * from "$/component/progress";
|
||||
export * from "$/component/tab";
|
||||
export * from "$/component/toaster";
|
||||
export * from "$/providers/theme/theme";
|
||||
export * from "$/provider/axios";
|
||||
export * from "$/provider/theme";
|
||||
export * from "$/provider/toaster";
|
||||
export * from "$/provider/token";
|
||||
|
||||
|
||||
70
lib/layout.css
Normal file
70
lib/layout.css
Normal file
@@ -0,0 +1,70 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
body {
|
||||
margin-inline: auto;
|
||||
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#root {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
a.active {
|
||||
color: oklch(70.7% 0.165 254.624); /* blue-400 */
|
||||
}
|
||||
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
margin-inline: auto;
|
||||
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
margin-inline: auto;
|
||||
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
97
lib/provider/axios/AxiosProvider.tsx
Normal file
97
lib/provider/axios/AxiosProvider.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { AxiosError, AxiosInstance } from "axios";
|
||||
import axios from "axios";
|
||||
import { createContext, use, useMemo } from "react";
|
||||
import { useToken } from "../token";
|
||||
|
||||
|
||||
export interface AxiosState {
|
||||
publicApi: AxiosInstance;
|
||||
authorizedApi: AxiosInstance;
|
||||
}
|
||||
|
||||
const initialState: AxiosState = {
|
||||
publicApi: {} as AxiosInstance,
|
||||
authorizedApi: {} as AxiosInstance
|
||||
}
|
||||
|
||||
const AxiosContext = createContext<AxiosState>(initialState);
|
||||
|
||||
|
||||
export default function AxiosProvider({
|
||||
apiUrl,
|
||||
children
|
||||
}: Readonly<{
|
||||
apiUrl: string;
|
||||
children: React.ReactNode;
|
||||
}>){
|
||||
const { getToken } = useToken();
|
||||
|
||||
const publicApi = useMemo(() => {
|
||||
const api = axios.create({
|
||||
baseURL: apiUrl,
|
||||
withCredentials: true
|
||||
});
|
||||
return api;
|
||||
}, [apiUrl]);
|
||||
const authorizedApi = useMemo(() => {
|
||||
const api = axios.create({
|
||||
baseURL: apiUrl,
|
||||
withCredentials: true
|
||||
});
|
||||
api.interceptors.request.use(async (config) => {
|
||||
try{
|
||||
const token = await getToken();
|
||||
if(token){
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
catch (e) {
|
||||
const error = e as Error;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
api.interceptors.response.use(r => r, async (error: AxiosError) => {
|
||||
const original = error.config;
|
||||
if(!original){
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if(error.response?.status === 401 && !original._retry){
|
||||
original._retry = true;
|
||||
try{
|
||||
const newToken = await getToken();
|
||||
original.headers.Authorization = `Bearer ${newToken}`;
|
||||
return api(original);
|
||||
}
|
||||
catch (e) {
|
||||
const refreshError = e as Error;
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
});
|
||||
return api;
|
||||
}, [apiUrl, getToken]);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
publicApi,
|
||||
authorizedApi
|
||||
}), [authorizedApi, publicApi]);
|
||||
|
||||
return (
|
||||
<AxiosContext value={value}>
|
||||
{children}
|
||||
</AxiosContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useAxios(){
|
||||
const context = use(AxiosContext);
|
||||
|
||||
if(!context){
|
||||
throw new Error("useAxios must be called inside an AxiosProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
8
lib/provider/axios/axios.d.ts
vendored
Normal file
8
lib/provider/axios/axios.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import "axios";
|
||||
|
||||
|
||||
declare module "axios" {
|
||||
interface InternalAxiosRequestConfig {
|
||||
_retry?: boolean;
|
||||
}
|
||||
}
|
||||
1
lib/provider/axios/index.ts
Normal file
1
lib/provider/axios/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AxiosProvider, useAxios } from "./AxiosProvider";
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Theme, ThemeProviderProps, ThemeProviderState } from "$/types/ThemeTypes";
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { createContext, use, useEffect, useMemo, useState } from "react";
|
||||
|
||||
|
||||
const themeInitialState: ThemeProviderState = {
|
||||
@@ -17,7 +17,7 @@ export default function ThemeProvider(props: Readonly<ThemeProviderProps>){
|
||||
storageKey = "mattrixwv-ui-theme"
|
||||
} = props;
|
||||
|
||||
const [ theme, setTheme ] = useState<Theme>((localStorage.getItem(storageKey) as Theme) || defaultTheme);
|
||||
const [ theme, setTheme ] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme);
|
||||
|
||||
useEffect(() => {
|
||||
const root = globalThis.document.documentElement;
|
||||
@@ -43,16 +43,16 @@ export default function ThemeProvider(props: Readonly<ThemeProviderProps>){
|
||||
}), [storageKey, theme]);
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider value={value}>
|
||||
<ThemeProviderContext value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
</ThemeProviderContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useTheme(){
|
||||
const context = useContext(ThemeProviderContext);
|
||||
const context = use(ThemeProviderContext);
|
||||
|
||||
if(!context){
|
||||
throw new Error("useTheme must be used within a ThemeProvider");
|
||||
1
lib/provider/theme/index.ts
Normal file
1
lib/provider/theme/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ThemeProvider, useTheme } from "./ThemeProvider";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DangerMessageBlock, SuccessMessageBlock, WarningMessageBlock } from "$/component/message";
|
||||
import Toaster from "$/component/toaster/Toaster";
|
||||
import type { Toast, ToastProviderProps, ToastProviderState } from "$/types/ToasterTypes";
|
||||
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||
import { createContext, use, useCallback, useMemo, useState } from "react";
|
||||
|
||||
|
||||
const toastInitialState: ToastProviderState = {
|
||||
@@ -72,20 +72,20 @@ export default function ToasterProvider({
|
||||
}), [ toast, hideToast, addToast, addSuccess, addWarning, addDanger ]);
|
||||
|
||||
return (
|
||||
<ToasterProviderContext.Provider value={value}>
|
||||
<ToasterProviderContext value={value}>
|
||||
<Toaster
|
||||
toast={toast}
|
||||
className={className}
|
||||
/>
|
||||
{children}
|
||||
</ToasterProviderContext.Provider>
|
||||
</ToasterProviderContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useToaster(){
|
||||
const context = useContext(ToasterProviderContext);
|
||||
const context = use(ToasterProviderContext);
|
||||
|
||||
if(!context){
|
||||
throw new Error("useToaster must be used within a ToasterProvider");
|
||||
1
lib/provider/toaster/index.ts
Normal file
1
lib/provider/toaster/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ToasterProvider, useToaster } from "./ToasterProvider";
|
||||
77
lib/provider/token/TokenProvider.tsx
Normal file
77
lib/provider/token/TokenProvider.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createContext, use, useCallback, useMemo, useRef } from "react";
|
||||
import { defaultTokenData, fetchToken, parseToken, type TokenData } from "./TokenUtils";
|
||||
|
||||
export interface TokenState {
|
||||
getToken: () => Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
const initialState: TokenState = {
|
||||
getToken: () => new Promise(() => {})
|
||||
}
|
||||
|
||||
const TokenContext = createContext<TokenState>(initialState);
|
||||
|
||||
|
||||
export default function TokenProvider({
|
||||
apiUrl,
|
||||
children
|
||||
}: Readonly<{
|
||||
apiUrl: string;
|
||||
children: React.ReactNode;
|
||||
}>){
|
||||
const tokenRef = useRef<TokenData>(defaultTokenData);
|
||||
const refreshPromiseRef = useRef<Promise<string | null | undefined>>(null);
|
||||
|
||||
const getToken = useCallback(async () => {
|
||||
if(refreshPromiseRef.current){
|
||||
return refreshPromiseRef.current;
|
||||
}
|
||||
|
||||
const { accessToken, expires } = tokenRef.current;
|
||||
|
||||
const isExpired = Date.now() > (expires - 5000); //Give a 5 second buffer
|
||||
|
||||
if(!accessToken || isExpired){
|
||||
refreshPromiseRef.current = (async () => {
|
||||
try {
|
||||
const rawToken = (await fetchToken(apiUrl)).token;
|
||||
const parsedToken = parseToken(rawToken);
|
||||
tokenRef.current = parsedToken;
|
||||
return rawToken;
|
||||
}
|
||||
catch(error){
|
||||
tokenRef.current = defaultTokenData;
|
||||
throw error;
|
||||
}
|
||||
finally {
|
||||
refreshPromiseRef.current = null;
|
||||
}
|
||||
})();
|
||||
return refreshPromiseRef.current;
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}, [apiUrl]);
|
||||
|
||||
const value: TokenState = useMemo(() => ({
|
||||
getToken
|
||||
}), [getToken]);
|
||||
|
||||
return (
|
||||
<TokenContext value={value}>
|
||||
{children}
|
||||
</TokenContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export function useToken(){
|
||||
const context = use(TokenContext);
|
||||
|
||||
if(!context){
|
||||
throw new Error("useToken must be called inside a TokenProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
52
lib/provider/token/TokenUtils.ts
Normal file
52
lib/provider/token/TokenUtils.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
interface TokenResponse {
|
||||
token: string | null | undefined;
|
||||
}
|
||||
|
||||
interface TokenBody {
|
||||
token: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
export interface LoginBody {
|
||||
login: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface TokenData {
|
||||
accessToken: string | null | undefined;
|
||||
issued: number;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
export const defaultTokenData: TokenData = {
|
||||
accessToken: "",
|
||||
issued: 0,
|
||||
expires: 0
|
||||
}
|
||||
|
||||
export async function fetchToken(apiUrl: string){
|
||||
const res = await fetch(`${apiUrl}/auth/refresh`, { method: "POST", credentials: "include" });
|
||||
return await res.json() as TokenResponse;
|
||||
}
|
||||
|
||||
export function parseToken(rawToken: string | null | undefined): TokenData {
|
||||
if(!rawToken){
|
||||
return defaultTokenData;
|
||||
}
|
||||
|
||||
const payloads = rawToken.split(".");
|
||||
if(payloads.length !== 3){
|
||||
return defaultTokenData;
|
||||
}
|
||||
|
||||
const payload = payloads[1];
|
||||
|
||||
const tokenBody = JSON.parse(atob(payload)) as TokenBody;
|
||||
|
||||
return ({
|
||||
accessToken: rawToken,
|
||||
issued: tokenBody.iat * 1000,
|
||||
expires: tokenBody.exp * 1000
|
||||
});
|
||||
}
|
||||
1
lib/provider/token/index.ts
Normal file
1
lib/provider/token/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as TokenProvider, useToken } from "./TokenProvider";
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as ThemeProvider, useTheme } from "$/providers/theme/ThemeProvider";
|
||||
|
||||
37
lib/theme.css
Normal file
37
lib/theme.css
Normal file
@@ -0,0 +1,37 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
|
||||
@theme {
|
||||
--light-text-color: #213547;
|
||||
--light-bg-color: #FFFFFF;
|
||||
|
||||
--dark-text-color: #FFFFFFDE;
|
||||
--dark-bg-color: #242424;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
:root.light {
|
||||
--text-color: var(--light-text-color);
|
||||
--bg-color: var(--light-bg-color);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--text-color: var(--dark-text-color);
|
||||
--bg-color: var(--dark-bg-color);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from "react";
|
||||
import type { ChangeEventHandler, ComponentProps } from "react";
|
||||
import type { ComponentProps, KeyboardEventHandler } from "react";
|
||||
|
||||
|
||||
export interface TextInputProps {
|
||||
@@ -11,8 +11,9 @@ export interface TextInputProps {
|
||||
maxLength?: number;
|
||||
spellCheck?: boolean;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
value: string;
|
||||
onChange?: (newValue: string) => void;
|
||||
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -25,11 +26,12 @@ export interface TextAreaProps {
|
||||
maxLength?: number;
|
||||
spellCheck?: boolean;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
value: string;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
||||
onChange?: (newValue: string) => void;
|
||||
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectInputProps {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface TabGroupContent {
|
||||
id: string;
|
||||
tab: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function usePrefersReducedMotion(){
|
||||
|
||||
useEffect(() => {
|
||||
const media = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect,@eslint-react/set-state-in-effect
|
||||
setReduced(media.matches);
|
||||
}, [ ]);
|
||||
|
||||
|
||||
7270
package-lock.json
generated
7270
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
83
package.json
@@ -20,56 +20,51 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"react-icons": "^5.5.0"
|
||||
"react-icons": "^5.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3"
|
||||
"@headlessui/react": "^2.2.10",
|
||||
"axios": "^1.19.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/eslint-plugin-router": "^1.141.0",
|
||||
"@tanstack/react-router": "^1.142.7",
|
||||
"@tanstack/react-router-devtools": "^1.142.7",
|
||||
"@tanstack/router-plugin": "^1.142.7",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"globals": "^16.5.0",
|
||||
"jsdom": "^27.3.0",
|
||||
"@eslint-react/eslint-plugin": "^5.18.6",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@tanstack/eslint-plugin-router": "^1.162.0",
|
||||
"@tanstack/react-router": "^1.170.29",
|
||||
"@tanstack/react-router-devtools": "^1.167.1",
|
||||
"@tanstack/router-plugin": "^1.168.32",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.4",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.4",
|
||||
"globals": "^17.11.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"rollup-plugin-jsx-remove-attributes": "^3.1.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-plugin-static-copy": "^3.2.0",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"overrides": {
|
||||
"@tailwindcss/vite": {
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.67.0",
|
||||
"vite": "^8.2.1",
|
||||
"vite-plugin-dts": "^5.0.3",
|
||||
"vite-plugin-static-copy": "^4.1.1",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"license": "",
|
||||
"author": "Mattrixwv",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://bitbucket.org/Mattrixwv/mattrixwvReactComponents.git"
|
||||
"url": "git+https://git.mattrixwv.com/BaseLibraries/MattrixwvReactComponents.git"
|
||||
},
|
||||
"main": "./dist/mattrixwv-components.cjs",
|
||||
"module": "./dist/mattrixwv-components.js",
|
||||
"types": "./dist/mattrixwv-components.d.ts",
|
||||
"exports": {
|
||||
@@ -109,9 +104,21 @@
|
||||
"types": "./dist/tab.d.ts",
|
||||
"import": "./dist/tab.js"
|
||||
},
|
||||
"./axios": {
|
||||
"types": "./dist/axios.d.ts",
|
||||
"import": "./dist/axios.js"
|
||||
},
|
||||
"./theme": {
|
||||
"types": "./dist/theme.d.ts",
|
||||
"import": "./dist/theme.js"
|
||||
},
|
||||
"./toaster": {
|
||||
"types": "./dist/toaster.d.ts",
|
||||
"import": "./dist/toaster.js"
|
||||
},
|
||||
"./token": {
|
||||
"types": "./dist/token.d.ts",
|
||||
"import": "./dist/token.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,85 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@import "../lib/styles.css";
|
||||
@import "../lib/components.css";
|
||||
@import "../lib/theme.css";
|
||||
|
||||
|
||||
@theme {
|
||||
--color-neutral-825: oklch(0.253 0 0);
|
||||
--color-neutral-850: oklch(0.237 0 0);
|
||||
|
||||
|
||||
--light-text-color: #213547;
|
||||
--light-bg-color: #FFFFFF;
|
||||
|
||||
--dark-text-color: #FFFFFFDE;
|
||||
--dark-bg-color: #242424;
|
||||
}
|
||||
|
||||
|
||||
:root.light {
|
||||
--text-color: var(--light-text-color);
|
||||
--bg-color: var(--light-bg-color);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
--text-color: var(--dark-text-color);
|
||||
--bg-color: var(--dark-bg-color);
|
||||
}
|
||||
|
||||
input::-webkit-calendar-picker-indicator {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
#root {
|
||||
padding-top: 4rem;
|
||||
padding-inline: 1rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-inline: auto;
|
||||
|
||||
min-width: 320px;
|
||||
height: 100vh;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a.active {
|
||||
color: var(--color-blue-400);
|
||||
}
|
||||
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-btween;
|
||||
align-items: center;
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spinnerAnimate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ThemeProvider from "$/providers/theme/ThemeProvider";
|
||||
import ToasterProvider from "$/providers/toaster/ToasterProvider";
|
||||
import ThemeProvider from "$/provider/theme/ThemeProvider";
|
||||
import ToasterProvider from "$/provider/toaster/ToasterProvider";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
@@ -10,71 +10,71 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as TabIndexRouteImport } from './routes/tab/index'
|
||||
import { Route as ProgressIndexRouteImport } from './routes/progress/index'
|
||||
import { Route as PillIndexRouteImport } from './routes/pill/index'
|
||||
import { Route as ModalIndexRouteImport } from './routes/modal/index'
|
||||
import { Route as MessageIndexRouteImport } from './routes/message/index'
|
||||
import { Route as LoadingIndexRouteImport } from './routes/loading/index'
|
||||
import { Route as InputIndexRouteImport } from './routes/input/index'
|
||||
import { Route as ButtonsIndexRouteImport } from './routes/buttons/index'
|
||||
import { Route as InputIndexRouteImport } from './routes/input/index'
|
||||
import { Route as LoadingIndexRouteImport } from './routes/loading/index'
|
||||
import { Route as MessageIndexRouteImport } from './routes/message/index'
|
||||
import { Route as ModalIndexRouteImport } from './routes/modal/index'
|
||||
import { Route as PillIndexRouteImport } from './routes/pill/index'
|
||||
import { Route as ProgressIndexRouteImport } from './routes/progress/index'
|
||||
import { Route as TabIndexRouteImport } from './routes/tab/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const TabIndexRoute = TabIndexRouteImport.update({
|
||||
id: '/tab/',
|
||||
path: '/tab/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ProgressIndexRoute = ProgressIndexRouteImport.update({
|
||||
id: '/progress/',
|
||||
path: '/progress/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PillIndexRoute = PillIndexRouteImport.update({
|
||||
id: '/pill/',
|
||||
path: '/pill/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ModalIndexRoute = ModalIndexRouteImport.update({
|
||||
id: '/modal/',
|
||||
path: '/modal/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const MessageIndexRoute = MessageIndexRouteImport.update({
|
||||
id: '/message/',
|
||||
path: '/message/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LoadingIndexRoute = LoadingIndexRouteImport.update({
|
||||
id: '/loading/',
|
||||
path: '/loading/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const InputIndexRoute = InputIndexRouteImport.update({
|
||||
id: '/input/',
|
||||
path: '/input/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ButtonsIndexRoute = ButtonsIndexRouteImport.update({
|
||||
id: '/buttons/',
|
||||
path: '/buttons/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const InputIndexRoute = InputIndexRouteImport.update({
|
||||
id: '/input/',
|
||||
path: '/input/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const LoadingIndexRoute = LoadingIndexRouteImport.update({
|
||||
id: '/loading/',
|
||||
path: '/loading/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const MessageIndexRoute = MessageIndexRouteImport.update({
|
||||
id: '/message/',
|
||||
path: '/message/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ModalIndexRoute = ModalIndexRouteImport.update({
|
||||
id: '/modal/',
|
||||
path: '/modal/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PillIndexRoute = PillIndexRouteImport.update({
|
||||
id: '/pill/',
|
||||
path: '/pill/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ProgressIndexRoute = ProgressIndexRouteImport.update({
|
||||
id: '/progress/',
|
||||
path: '/progress/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const TabIndexRoute = TabIndexRouteImport.update({
|
||||
id: '/tab/',
|
||||
path: '/tab/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/buttons': typeof ButtonsIndexRoute
|
||||
'/input': typeof InputIndexRoute
|
||||
'/loading': typeof LoadingIndexRoute
|
||||
'/message': typeof MessageIndexRoute
|
||||
'/modal': typeof ModalIndexRoute
|
||||
'/pill': typeof PillIndexRoute
|
||||
'/progress': typeof ProgressIndexRoute
|
||||
'/tab': typeof TabIndexRoute
|
||||
'/buttons/': typeof ButtonsIndexRoute
|
||||
'/input/': typeof InputIndexRoute
|
||||
'/loading/': typeof LoadingIndexRoute
|
||||
'/message/': typeof MessageIndexRoute
|
||||
'/modal/': typeof ModalIndexRoute
|
||||
'/pill/': typeof PillIndexRoute
|
||||
'/progress/': typeof ProgressIndexRoute
|
||||
'/tab/': typeof TabIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@@ -103,14 +103,14 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/buttons'
|
||||
| '/input'
|
||||
| '/loading'
|
||||
| '/message'
|
||||
| '/modal'
|
||||
| '/pill'
|
||||
| '/progress'
|
||||
| '/tab'
|
||||
| '/buttons/'
|
||||
| '/input/'
|
||||
| '/loading/'
|
||||
| '/message/'
|
||||
| '/modal/'
|
||||
| '/pill/'
|
||||
| '/progress/'
|
||||
| '/tab/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -156,60 +156,60 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/tab/': {
|
||||
id: '/tab/'
|
||||
path: '/tab'
|
||||
fullPath: '/tab'
|
||||
preLoaderRoute: typeof TabIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/progress/': {
|
||||
id: '/progress/'
|
||||
path: '/progress'
|
||||
fullPath: '/progress'
|
||||
preLoaderRoute: typeof ProgressIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/pill/': {
|
||||
id: '/pill/'
|
||||
path: '/pill'
|
||||
fullPath: '/pill'
|
||||
preLoaderRoute: typeof PillIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/modal/': {
|
||||
id: '/modal/'
|
||||
path: '/modal'
|
||||
fullPath: '/modal'
|
||||
preLoaderRoute: typeof ModalIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/message/': {
|
||||
id: '/message/'
|
||||
path: '/message'
|
||||
fullPath: '/message'
|
||||
preLoaderRoute: typeof MessageIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/loading/': {
|
||||
id: '/loading/'
|
||||
path: '/loading'
|
||||
fullPath: '/loading'
|
||||
preLoaderRoute: typeof LoadingIndexRouteImport
|
||||
'/buttons/': {
|
||||
id: '/buttons/'
|
||||
path: '/buttons'
|
||||
fullPath: '/buttons/'
|
||||
preLoaderRoute: typeof ButtonsIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/input/': {
|
||||
id: '/input/'
|
||||
path: '/input'
|
||||
fullPath: '/input'
|
||||
fullPath: '/input/'
|
||||
preLoaderRoute: typeof InputIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/buttons/': {
|
||||
id: '/buttons/'
|
||||
path: '/buttons'
|
||||
fullPath: '/buttons'
|
||||
preLoaderRoute: typeof ButtonsIndexRouteImport
|
||||
'/loading/': {
|
||||
id: '/loading/'
|
||||
path: '/loading'
|
||||
fullPath: '/loading/'
|
||||
preLoaderRoute: typeof LoadingIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/message/': {
|
||||
id: '/message/'
|
||||
path: '/message'
|
||||
fullPath: '/message/'
|
||||
preLoaderRoute: typeof MessageIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/modal/': {
|
||||
id: '/modal/'
|
||||
path: '/modal'
|
||||
fullPath: '/modal/'
|
||||
preLoaderRoute: typeof ModalIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/pill/': {
|
||||
id: '/pill/'
|
||||
path: '/pill'
|
||||
fullPath: '/pill/'
|
||||
preLoaderRoute: typeof PillIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/progress/': {
|
||||
id: '/progress/'
|
||||
path: '/progress'
|
||||
fullPath: '/progress/'
|
||||
preLoaderRoute: typeof ProgressIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/tab/': {
|
||||
id: '/tab/'
|
||||
path: '/tab'
|
||||
fullPath: '/tab/'
|
||||
preLoaderRoute: typeof TabIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,21 +14,22 @@ export const Route = createFileRoute("/buttons/")({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ButtonPage(){
|
||||
const [ clickCount, setClickCount ] = useState(0);
|
||||
|
||||
|
||||
const tabs: TabGroupContent[] = [
|
||||
{ tab: "Primary", content: generateTabContent(PrimaryButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Secondary", content: generateTabContent(SecondaryButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Tertiary", content: generateTabContent(TertiaryButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Info", content: generateTabContent(InfoButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Success", content: generateTabContent(SuccessButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Warning", content: generateTabContent(WarningButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Danger", content: generateTabContent(DangerButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Molten", content: generateTabContent(MoltenButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Dark", content: generateTabContent(DarkButton, () => setClickCount(clickCount + 1)) },
|
||||
{ tab: "Light", content: generateTabContent(LightButton, () => setClickCount(clickCount + 1)) }
|
||||
{ id: "primary", tab: "Primary", content: generateTabContent(PrimaryButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "secondary", tab: "Secondary", content: generateTabContent(SecondaryButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "tertiary", tab: "Tertiary", content: generateTabContent(TertiaryButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "info", tab: "Info", content: generateTabContent(InfoButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "success", tab: "Success", content: generateTabContent(SuccessButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "warning", tab: "Warning", content: generateTabContent(WarningButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "danger", tab: "Danger", content: generateTabContent(DangerButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "molten", tab: "Molten", content: generateTabContent(MoltenButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "dark", tab: "Dark", content: generateTabContent(DarkButton, () => setClickCount(clickCount + 1)) },
|
||||
{ id: "light", tab: "Light", content: generateTabContent(LightButton, () => setClickCount(clickCount + 1)) }
|
||||
];
|
||||
|
||||
|
||||
@@ -117,6 +118,7 @@ function generateTabContent(Fn: (props: ButtonProps) => JSX.Element, onClick: ()
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ButtonDisplay({
|
||||
title,
|
||||
children
|
||||
|
||||
@@ -6,6 +6,7 @@ export const Route = createFileRoute("/")({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function HomePage(){
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export const Route = createFileRoute('/input/')({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function InputPage(){
|
||||
const [ date, setDate ] = useState<Date>();
|
||||
const [ dateTime, setDateTime ] = useState<Date>();
|
||||
@@ -17,12 +18,12 @@ function InputPage(){
|
||||
|
||||
|
||||
const tabs: TabGroupContent[] = [
|
||||
{ tab: "Checkbox", content: <CheckboxContent/>},
|
||||
{ tab: "Radio", content: <RadioContent/> },
|
||||
{ tab: "Date", content: <DateContent date={date} setDate={setDate} dateTime={dateTime} setDateTime={setDateTime} time={time} setTime={setTime}/> },
|
||||
{ tab: "File", content: <FileContent/> },
|
||||
{ tab: "Switch", content: <SwitchContent/> },
|
||||
{ tab: "Text", content: <TextContent/> }
|
||||
{ id: "checkbox", tab: "Checkbox", content: <CheckboxContent/>},
|
||||
{ id: "radio", tab: "Radio", content: <RadioContent/> },
|
||||
{ id: "date", tab: "Date", content: <DateContent date={date} setDate={setDate} dateTime={dateTime} setDateTime={setDateTime} time={time} setTime={setTime}/> },
|
||||
{ id: "file", tab: "File", content: <FileContent/> },
|
||||
{ id: "switch", tab: "Switch", content: <SwitchContent/> },
|
||||
{ id: "text", tab: "Text", content: <TextContent/> }
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -9,14 +9,15 @@ export const Route = createFileRoute("/loading/")({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function LoadingPage(){
|
||||
const tabs: TabGroupContent[] = [
|
||||
{ tab: "Spinners", content: generateSpinnersContent() },
|
||||
{ tab: "Dots", content: generateDotsContent() },
|
||||
{ tab: "Bars", content: generateBarsContent() },
|
||||
{ tab: "Blocks", content: generateBlocksContent() },
|
||||
{ tab: "Pulses", content: generatePulsesContent() },
|
||||
{ tab: "Various", content: generateVariousContent() }
|
||||
{ id: "spinners", tab: "Spinners", content: generateSpinnersContent() },
|
||||
{ id: "dots", tab: "Dots", content: generateDotsContent() },
|
||||
{ id: "bars", tab: "Bars", content: generateBarsContent() },
|
||||
{ id: "blocks", tab: "Blocks", content: generateBlocksContent() },
|
||||
{ id: "pulses", tab: "Pulses", content: generatePulsesContent() },
|
||||
{ id: "various", tab: "Various", content: generateVariousContent() }
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,18 +12,19 @@ export const Route = createFileRoute("/message/")({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function MessagePage(){
|
||||
const tabs: TabGroupContent[] = [
|
||||
{ tab: "Primary", content: generateTabContent(PrimaryMessageBlock) },
|
||||
{ tab: "Secondary", content: generateTabContent(SecondaryMessageBlock) },
|
||||
{ tab: "Tertiary", content: generateTabContent(TertiaryMessageBlock) },
|
||||
{ tab: "Info", content: generateTabContent(InfoMessageBlock) },
|
||||
{ tab: "Success", content: generateTabContent(SuccessMessageBlock) },
|
||||
{ tab: "Warning", content: generateTabContent(WarningMessageBlock) },
|
||||
{ tab: "Danger", content: generateTabContent(DangerMessageBlock) },
|
||||
{ tab: "Molten", content: generateTabContent(MoltenMessageBlock) },
|
||||
{ tab: "Dark", content: generateTabContent(DarkMessageBlock) },
|
||||
{ tab: "Light", content: generateTabContent(LightMessageBlock) }
|
||||
{ id: "primary", tab: "Primary", content: generateTabContent(PrimaryMessageBlock) },
|
||||
{ id: "secondary", tab: "Secondary", content: generateTabContent(SecondaryMessageBlock) },
|
||||
{ id: "tertiary", tab: "Tertiary", content: generateTabContent(TertiaryMessageBlock) },
|
||||
{ id: "info", tab: "Info", content: generateTabContent(InfoMessageBlock) },
|
||||
{ id: "success", tab: "Success", content: generateTabContent(SuccessMessageBlock) },
|
||||
{ id: "warning", tab: "Warning", content: generateTabContent(WarningMessageBlock) },
|
||||
{ id: "danger", tab: "Danger", content: generateTabContent(DangerMessageBlock) },
|
||||
{ id: "molten", tab: "Molten", content: generateTabContent(MoltenMessageBlock) },
|
||||
{ id: "dark", tab: "Dark", content: generateTabContent(DarkMessageBlock) },
|
||||
{ id: "light", tab: "Light", content: generateTabContent(LightMessageBlock) }
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DangerButton, PrimaryButton, SuccessButton, WarningButton } from "$/component/button";
|
||||
import { PrimaryMessageBlock } from "$/component/message";
|
||||
import { Modal } from "$/component/modal";
|
||||
import { useToaster } from "$/providers/toaster/ToasterProvider";
|
||||
import { useToaster } from "$/provider/toaster/ToasterProvider";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -11,6 +11,7 @@ export const Route = createFileRoute("/modal/")({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ModalPage(){
|
||||
const [ displayCenteredModal, setDisplayCenteredModal ] = useState(false);
|
||||
const [ displayTopModal, setDisplayTopModal ] = useState(false);
|
||||
@@ -74,6 +75,7 @@ function ModalPage(){
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function DemoCenteredModal({
|
||||
display,
|
||||
onClose
|
||||
@@ -94,6 +96,7 @@ function DemoCenteredModal({
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function DemoTopModal({
|
||||
display,
|
||||
onClose
|
||||
|
||||
@@ -17,6 +17,7 @@ export const Route = createFileRoute("/pill/")({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function PillPage(){
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-y-8">
|
||||
@@ -34,6 +35,7 @@ function PillPage(){
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function PillLayout({
|
||||
PillType,
|
||||
pillName
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { DangerButton, PrimaryButton } from '$/component/button';
|
||||
import { NumberInput } from '$/component/input';
|
||||
import { DangerButton, PrimaryButton } from "$/component/button";
|
||||
import { NumberInput } from "$/component/input";
|
||||
import { DangerProgress, DarkProgress, InfoProgress, LightProgress, MoltenProgress, PrimaryProgress, Progress, SecondaryProgress, SuccessProgress, TertiaryProgress, WarningProgress } from '$/component/progress';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { BsDashLg, BsPlusLg } from 'react-icons/bs';
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { BsDashLg, BsPlusLg } from "react-icons/bs";
|
||||
|
||||
|
||||
export const Route = createFileRoute('/progress/')({
|
||||
component: ProgressPage,
|
||||
export const Route = createFileRoute("/progress/")({
|
||||
component: ProgressPage
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ProgressPage() {
|
||||
const [ value, setValue ] = useState(0);
|
||||
|
||||
@@ -125,6 +127,7 @@ function ProgressPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function ProgressBlock({
|
||||
label,
|
||||
children
|
||||
|
||||
@@ -9,13 +9,14 @@ export const Route = createFileRoute('/tab/')({
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function RouteComponent(){
|
||||
const tabs: TabGroupContent[] = [
|
||||
{ tab: "Tab 1", content: "Tab 1 Content" },
|
||||
{ tab: "Tab 2", content: "Tab 2 Content" },
|
||||
{ tab: "Tab 3", content: "Tab 3 Content" },
|
||||
{ tab: "Tab 4", content: "Tab 4 Content" },
|
||||
{ tab: <div className="flex flex-row items-center justify-center gap-x-2">Tab <BsXLg/></div>, content: <div className="flex flex-row items-center justify-center gap-x-2">Tab <BsXLg/> Content</div> }
|
||||
{ id: "1", tab: "Tab 1", content: "Tab 1 Content" },
|
||||
{ id: "2", tab: "Tab 2", content: "Tab 2 Content" },
|
||||
{ id: "3", tab: "Tab 3", content: "Tab 3 Content" },
|
||||
{ id: "4", tab: "Tab 4", content: "Tab 4 Content" },
|
||||
{ id: "5", tab: <div className="flex flex-row items-center justify-center gap-x-2">Tab <BsXLg/></div>, content: <div className="flex flex-row items-center justify-center gap-x-2">Tab <BsXLg/> Content</div> }
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -574,16 +574,18 @@ export function TextContent(){
|
||||
|
||||
const [ selected, setSelected ] = useState(selectOptions[0]);
|
||||
const [ numberValue, setNumberValue ] = useState(0);
|
||||
const [ textValue, setTextValue ] = useState("");
|
||||
const [ secondTextValue, setSecondTextValue ] = useState("");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center gap-y-8 mt-8 w-full"
|
||||
>
|
||||
<GeneralInputDisplay title="Text Input">
|
||||
<TextInput placeholder="Text Input" labelClassName="bg-(--bg-color) peer-focus:bg-(--bg-color)"/>
|
||||
<TextInput placeholder="Text Input" labelClassName="bg-(--bg-color) peer-focus:bg-(--bg-color)" value={textValue} onChange={setTextValue}/>
|
||||
</GeneralInputDisplay>
|
||||
<GeneralInputDisplay title="Text Area">
|
||||
<TextArea placeholder="Textarea" className="resize" labelClassName="bg-(--bg-color) peer-focus:bg-(--bg-color)"/>
|
||||
<TextArea placeholder="Textarea" className="resize" labelClassName="bg-(--bg-color) peer-focus:bg-(--bg-color)" value={secondTextValue} onChange={setSecondTextValue}/>
|
||||
</GeneralInputDisplay>
|
||||
<GeneralInputDisplay title="Select">
|
||||
<SelectInput placeholder={selected.label} onChange={(newValue) => setSelected(selectOptions.find((option) => option.value === newValue) || selectOptions[0])}>
|
||||
|
||||
@@ -66,6 +66,7 @@ function LoadingGroup({
|
||||
{
|
||||
elements.map((spinner, index) => (
|
||||
<div
|
||||
// eslint-disable-next-line @eslint-react/no-array-index-key
|
||||
key={index}
|
||||
className="w-32 h-32"
|
||||
>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$/*": [ "./lib/*" ],
|
||||
"@/*": [ "./src/*" ],
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$/*": [ "./lib/*" ],
|
||||
"#root": [ "." ]
|
||||
|
||||
@@ -7,10 +7,11 @@ import removeTestIdAttribute from "rollup-plugin-jsx-remove-attributes";
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import { peerDependencies } from "./package.json";
|
||||
import { peerDependencies } from "./package.json" with { type: "json" };
|
||||
|
||||
|
||||
const modules = [ "button", "input", "loading", "message", "modal", "nav", "progress", "tab", "toaster" ];
|
||||
const components = [ "button", "input", "loading", "message", "modal", "nav", "progress", "tab", "toaster" ];
|
||||
const providers = [ "axios", "theme", "toaster", "token" ];
|
||||
|
||||
|
||||
// https://vite.dev/config/
|
||||
@@ -25,8 +26,7 @@ export default defineConfig({
|
||||
dts({
|
||||
include: ["lib"],
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
rollupTypes: true,
|
||||
insertTypesEntry: true,
|
||||
bundleTypes: true,
|
||||
compilerOptions: {
|
||||
declarationMap: false
|
||||
}
|
||||
@@ -35,7 +35,15 @@ export default defineConfig({
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: resolve(__dirname, "lib/styles.css"),
|
||||
src: resolve(import.meta.dirname, "lib/components.css"),
|
||||
dest: ""
|
||||
},
|
||||
{
|
||||
src: resolve(import.meta.dirname, "lib/layout.css"),
|
||||
dest: ""
|
||||
},
|
||||
{
|
||||
src: resolve(import.meta.dirname, "lib/theme.css"),
|
||||
dest: ""
|
||||
}
|
||||
]
|
||||
@@ -43,16 +51,17 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
"$": resolve(__dirname, "lib"),
|
||||
"#root": resolve(__dirname)
|
||||
"@": resolve(import.meta.dirname, "src"),
|
||||
"$": resolve(import.meta.dirname, "lib"),
|
||||
"#root": resolve(import.meta.dirname)
|
||||
}
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: {
|
||||
index: resolve(__dirname, "lib/index.ts"),
|
||||
...Object.fromEntries(modules.map(mod => [mod, resolve(__dirname, `lib/component/${mod}/index.ts`)]))
|
||||
"mattrixwv-components": resolve(import.meta.dirname, "lib/index.ts"),
|
||||
...Object.fromEntries(components.map(mod => [mod, resolve(import.meta.dirname, `lib/component/${mod}/index.ts`)])),
|
||||
...Object.fromEntries(providers.map(mod => [mod, resolve(import.meta.dirname, `lib/provider/${mod}/index.ts`)]))
|
||||
},
|
||||
formats: [ "es" ],
|
||||
name: "Mattrixwv Component Library"
|
||||
|
||||
Reference in New Issue
Block a user