Compare commits

...

9 Commits

Author SHA1 Message Date
Matthew Ellison
cbd681e63b Update dependencies 2026-08-15 15:37:24 -04:00
Matthew Ellison
7265eaad18 Add password input 2026-03-20 22:12:59 -04:00
Matthew Ellison
53ccfe1d4f Fix typo in error message 2026-03-19 22:57:54 -04:00
Matthew Ellison
a5a2f8324e Update modal config 2026-03-19 22:57:46 -04:00
Matthew Ellison
dc3d1ac60d Add phone input preliminary setup 2026-03-16 23:36:48 -04:00
Matthew Ellison
8fe121951b Update package layout 2026-03-16 23:36:38 -04:00
Matthew Ellison
b345982ab1 Update component css 2026-03-14 15:24:53 -04:00
Matthew Ellison
ca342cc238 Update build config so imports work as expected 2026-03-11 22:52:46 -04:00
Matthew Ellison
0de206016a Updated git info 2026-03-01 14:39:27 -05:00
49 changed files with 3825 additions and 4542 deletions

View File

@@ -1,6 +1,6 @@
import js from "@eslint/js"; import js from "@eslint/js";
import pluginRouter from "@tanstack/eslint-plugin-router"; 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 reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh"; import reactRefresh from "eslint-plugin-react-refresh";
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig, globalIgnores } from "eslint/config";
@@ -9,12 +9,12 @@ import tseslint from "typescript-eslint";
export default defineConfig([ export default defineConfig([
globalIgnores(['dist']), globalIgnores(["dist", "coverage", ".tanstack", ".scannerwork"]),
{ {
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
react.configs.flat.recommended, eslintReact.configs.recommended,
tseslint.configs.recommendedTypeChecked, tseslint.configs.recommendedTypeChecked,
reactHooks.configs.flat["recommended-latest"], reactHooks.configs.flat["recommended-latest"],
reactRefresh.configs.vite, reactRefresh.configs.vite,

View File

@@ -47,6 +47,8 @@ export { default as DragAndDropFileInput } from "./file/DragAndDropFileInput";
export { default as FileInput } from "./file/FileInput"; export { default as FileInput } from "./file/FileInput";
export { default as NumberInput } from "./number/NumberInput"; export { default as NumberInput } from "./number/NumberInput";
export { default as OptionInput } from "./text/OptionInput"; 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 SelectInput } from "./text/SelectInput";
export { default as TextArea } from "./text/TextArea"; export { default as TextArea } from "./text/TextArea";
export { default as TextInput } from "./text/TextInput"; export { default as TextInput } from "./text/TextInput";

View File

@@ -1,20 +1,24 @@
import type { RadioButtonProps } from "$/types/InputTypes"; import type { RadioButtonProps } from "$/types/InputTypes";
import { Radio } from "@headlessui/react"; import { Radio } from "@headlessui/react";
import clsx from "clsx"; import clsx from "clsx";
import { useId } from "react";
export default function RadioButton({ export default function RadioButton({
id = crypto.randomUUID().replaceAll("-", ""), id,
className, className,
labelClassName, labelClassName,
size = "sm", size = "sm",
value, value,
disabled, disabled,
children children
}: Readonly<RadioButtonProps>){ }: Readonly<RadioButtonProps>) {
const reactId = useId();
const effectiveId = id ?? reactId;
return ( return (
<Radio <Radio
id={id} id={effectiveId}
value={value} value={value}
className={clsx( className={clsx(
"group", "group",
@@ -26,7 +30,7 @@ export default function RadioButton({
} }
)} )}
disabled={disabled} disabled={disabled}
aria-labelledby={`${id}Label`} aria-labelledby={`${effectiveId}Label`}
> >
<div <div
className={clsx( className={clsx(
@@ -45,7 +49,7 @@ export default function RadioButton({
{ {
children && children &&
<span <span
id={`${id}Label`} id={`${effectiveId}Label`}
className={labelClassName} className={labelClassName}
> >
{children} {children}

View 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>
);
}

View 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();
}
*/

View File

@@ -16,6 +16,7 @@ export default function TextArea({
placeholder, placeholder,
value, value,
onChange, onChange,
onKeyDown,
disabled disabled
}: Readonly<TextAreaProps>){ }: Readonly<TextAreaProps>){
const componentId = useId(); const componentId = useId();
@@ -44,14 +45,15 @@ export default function TextArea({
rows={rows} rows={rows}
cols={cols} cols={cols}
value={value} value={value}
onChange={onChange} onChange={(e) => onChange?.(e.target.value)}
onKeyDown={onKeyDown}
disabled={disabled} disabled={disabled}
spellCheck={spellCheck} spellCheck={spellCheck}
/> />
<label <label
className={clsx( className={clsx(
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default", "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", "peer-focus:-top-3 peer-focus:left-0 peer-focus:text-sm peer-focus:w-auto peer-focus:h-auto",
"flex items-center", "flex items-center",
labelClassName labelClassName

View File

@@ -14,6 +14,7 @@ export default function TextInput({
placeholder, placeholder,
value, value,
onChange, onChange,
onKeyDown,
disabled disabled
}: Readonly<TextInputProps>){ }: Readonly<TextInputProps>){
const componentId = useId(); const componentId = useId();
@@ -41,14 +42,15 @@ export default function TextInput({
placeholder={placeholder} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
value={value} value={value}
onChange={onChange} onChange={(e) => onChange?.(e.target.value)}
disabled={disabled} disabled={disabled}
spellCheck={spellCheck} spellCheck={spellCheck}
onKeyDown={onKeyDown}
/> />
<label <label
className={clsx( className={clsx(
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default", "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", "peer-focus:-top-3 peer-focus:left-0 peer-focus:text-sm peer-focus:w-auto peer-focus:h-auto",
"flex items-center", "flex items-center",
labelClassName labelClassName

View File

@@ -30,6 +30,8 @@ export default function ModalHeader({
<Button <Button
className="absolute top-1 right-1 cursor-pointer" className="absolute top-1 right-1 cursor-pointer"
onClick={onClose} onClick={onClose}
size="sm"
variant="icon"
> >
<BsXLg <BsXLg
size={20} size={20}

View File

@@ -1,4 +1,4 @@
import { useTheme } from "$/providers/theme/ThemeProvider"; import { useTheme } from "$/provider/theme/ThemeProvider";
import { BsLightbulb, BsLightbulbFill } from "react-icons/bs"; import { BsLightbulb, BsLightbulbFill } from "react-icons/bs";

View File

@@ -20,8 +20,8 @@ export default function MattrixwvTabGroup({
> >
<MattrixwvTabList> <MattrixwvTabList>
{ {
tabs.map((tab, index) => ( tabs.map(tab => (
<MattrixwvTab key={index}> <MattrixwvTab key={tab.id}>
{tab.tab} {tab.tab}
</MattrixwvTab> </MattrixwvTab>
)) ))
@@ -29,8 +29,8 @@ export default function MattrixwvTabGroup({
</MattrixwvTabList> </MattrixwvTabList>
<MattrixwvTabPanels> <MattrixwvTabPanels>
{ {
tabs.map((tab, index) => ( tabs.map(tab => (
<MattrixwvTabPanel key={index}> <MattrixwvTabPanel key={tab.id}>
{tab.content} {tab.content}
</MattrixwvTabPanel> </MattrixwvTabPanel>
)) ))

View File

@@ -6,9 +6,11 @@ import clsx from "clsx";
export default function Toaster({ export default function Toaster({
toast, toast,
className className
}: Readonly<ToasterProps>){ }: Readonly<ToasterProps>) {
//TODO: Rework how your toasters work
return ( return (
<Transition <Transition
// eslint-disable-next-line @eslint-react/purity
show={toast.length > 1 || (toast.length === 1 && toast[0].hideTime > new Date())} show={toast.length > 1 || (toast.length === 1 && toast[0].hideTime > new Date())}
enter="transform transition duration-500" enter="transform transition duration-500"
enterFrom="-translate-y-[25vh]" enterFrom="-translate-y-[25vh]"

View File

@@ -1,3 +1,2 @@
export { default as ToasterProvider, useToaster } from "$/providers/toaster/ToasterProvider";
export { default as Toaster } from "./Toaster"; export { default as Toaster } from "./Toaster";

View File

@@ -1,5 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { @theme {
/* Universal */ /* Universal */
--color-neutral-light: oklch(92.8% 0.006 264.531); /* gray-200 */ --color-neutral-light: oklch(92.8% 0.006 264.531); /* gray-200 */

View File

@@ -7,5 +7,8 @@ export * from "$/component/nav";
export * from "$/component/progress"; export * from "$/component/progress";
export * from "$/component/tab"; export * from "$/component/tab";
export * from "$/component/toaster"; 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
View 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;
}

View 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
View File

@@ -0,0 +1,8 @@
import "axios";
declare module "axios" {
interface InternalAxiosRequestConfig {
_retry?: boolean;
}
}

View File

@@ -0,0 +1 @@
export { default as AxiosProvider, useAxios } from "./AxiosProvider";

View File

@@ -1,5 +1,5 @@
import type { Theme, ThemeProviderProps, ThemeProviderState } from "$/types/ThemeTypes"; 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 = { const themeInitialState: ThemeProviderState = {
@@ -17,7 +17,7 @@ export default function ThemeProvider(props: Readonly<ThemeProviderProps>){
storageKey = "mattrixwv-ui-theme" storageKey = "mattrixwv-ui-theme"
} = props; } = props;
const [ theme, setTheme ] = useState<Theme>((localStorage.getItem(storageKey) as Theme) || defaultTheme); const [ theme, setTheme ] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme);
useEffect(() => { useEffect(() => {
const root = globalThis.document.documentElement; const root = globalThis.document.documentElement;
@@ -43,16 +43,16 @@ export default function ThemeProvider(props: Readonly<ThemeProviderProps>){
}), [storageKey, theme]); }), [storageKey, theme]);
return ( return (
<ThemeProviderContext.Provider value={value}> <ThemeProviderContext value={value}>
{children} {children}
</ThemeProviderContext.Provider> </ThemeProviderContext>
); );
} }
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export function useTheme(){ export function useTheme(){
const context = useContext(ThemeProviderContext); const context = use(ThemeProviderContext);
if(!context){ if(!context){
throw new Error("useTheme must be used within a ThemeProvider"); throw new Error("useTheme must be used within a ThemeProvider");

View File

@@ -0,0 +1 @@
export { default as ThemeProvider, useTheme } from "./ThemeProvider";

View File

@@ -1,7 +1,7 @@
import { DangerMessageBlock, SuccessMessageBlock, WarningMessageBlock } from "$/component/message"; import { DangerMessageBlock, SuccessMessageBlock, WarningMessageBlock } from "$/component/message";
import Toaster from "$/component/toaster/Toaster"; import Toaster from "$/component/toaster/Toaster";
import type { Toast, ToastProviderProps, ToastProviderState } from "$/types/ToasterTypes"; 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 = { const toastInitialState: ToastProviderState = {
@@ -72,20 +72,20 @@ export default function ToasterProvider({
}), [ toast, hideToast, addToast, addSuccess, addWarning, addDanger ]); }), [ toast, hideToast, addToast, addSuccess, addWarning, addDanger ]);
return ( return (
<ToasterProviderContext.Provider value={value}> <ToasterProviderContext value={value}>
<Toaster <Toaster
toast={toast} toast={toast}
className={className} className={className}
/> />
{children} {children}
</ToasterProviderContext.Provider> </ToasterProviderContext>
); );
} }
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export function useToaster(){ export function useToaster(){
const context = useContext(ToasterProviderContext); const context = use(ToasterProviderContext);
if(!context){ if(!context){
throw new Error("useToaster must be used within a ToasterProvider"); throw new Error("useToaster must be used within a ToasterProvider");

View File

@@ -0,0 +1 @@
export { default as ToasterProvider, useToaster } from "./ToasterProvider";

View 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;
}

View 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
});
}

View File

@@ -0,0 +1 @@
export { default as TokenProvider, useToken } from "./TokenProvider";

View File

@@ -1,2 +0,0 @@
export { default as ThemeProvider, useTheme } from "$/providers/theme/ThemeProvider";

37
lib/theme.css Normal file
View 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);
}

View File

@@ -1,5 +1,5 @@
import type React from "react"; import type React from "react";
import type { ChangeEventHandler, ComponentProps } from "react"; import type { ComponentProps, KeyboardEventHandler } from "react";
export interface TextInputProps { export interface TextInputProps {
@@ -11,8 +11,9 @@ export interface TextInputProps {
maxLength?: number; maxLength?: number;
spellCheck?: boolean; spellCheck?: boolean;
placeholder?: string; placeholder?: string;
value?: string; value: string;
onChange?: ChangeEventHandler<HTMLInputElement>; onChange?: (newValue: string) => void;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
disabled?: boolean; disabled?: boolean;
} }
@@ -25,11 +26,12 @@ export interface TextAreaProps {
maxLength?: number; maxLength?: number;
spellCheck?: boolean; spellCheck?: boolean;
placeholder?: string; placeholder?: string;
value?: string; value: string;
disabled?: boolean;
rows?: number; rows?: number;
cols?: number; cols?: number;
onChange?: ChangeEventHandler<HTMLTextAreaElement>; onChange?: (newValue: string) => void;
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
disabled?: boolean;
} }
export interface SelectInputProps { export interface SelectInputProps {

View File

@@ -1,4 +1,5 @@
export interface TabGroupContent { export interface TabGroupContent {
id: string;
tab: React.ReactNode; tab: React.ReactNode;
content: React.ReactNode; content: React.ReactNode;
} }

View File

@@ -6,7 +6,7 @@ export function usePrefersReducedMotion(){
useEffect(() => { useEffect(() => {
const media = globalThis.matchMedia("(prefers-reduced-motion: reduce)"); 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); setReduced(media.matches);
}, [ ]); }, [ ]);

7262
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,56 +20,51 @@
}, },
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",
"react-icons": "^5.5.0" "react-icons": "^5.7.0"
}, },
"peerDependencies": { "peerDependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.10",
"react": "^19.2.3", "axios": "^1.19.0",
"react-dom": "^19.2.3" "react": "^19.2.8",
"react-dom": "^19.2.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint-react/eslint-plugin": "^5.18.6",
"@tailwindcss/vite": "^4.1.18", "@eslint/js": "^10.0.1",
"@tanstack/eslint-plugin-router": "^1.141.0", "@tailwindcss/vite": "^4.3.3",
"@tanstack/react-router": "^1.142.7", "@tanstack/eslint-plugin-router": "^1.162.0",
"@tanstack/react-router-devtools": "^1.142.7", "@tanstack/react-router": "^1.170.29",
"@tanstack/router-plugin": "^1.142.7", "@tanstack/react-router-devtools": "^1.167.1",
"@testing-library/jest-dom": "^6.9.1", "@tanstack/router-plugin": "^1.168.32",
"@testing-library/react": "^16.3.1", "@testing-library/jest-dom": "^7.0.1",
"@testing-library/user-event": "^14.6.1", "@testing-library/react": "^16.3.2",
"@types/node": "^25.0.3", "@testing-library/user-event": "^14.6.4",
"@types/react": "^19.2.7", "@types/node": "^26.2.0",
"@types/react-dom": "^19.2.3", "@types/react": "^19.2.18",
"@vitejs/plugin-react": "^5.1.2", "@types/react-dom": "^19.2.4",
"@vitest/coverage-v8": "^4.0.16", "@vitejs/plugin-react": "^6.0.5",
"eslint": "^9.39.2", "@vitest/coverage-v8": "^4.1.10",
"eslint-plugin-react": "^7.37.5", "eslint": "^10.8.1",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.5.4",
"globals": "^16.5.0", "globals": "^17.11.0",
"jsdom": "^27.3.0", "jsdom": "^30.0.1",
"rollup-plugin-jsx-remove-attributes": "^3.1.2", "rollup-plugin-jsx-remove-attributes": "^3.1.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.3.3",
"typescript": "^5.9.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.50.0", "typescript-eslint": "^8.67.0",
"vite": "^7.3.0", "vite": "^8.2.1",
"vite-plugin-dts": "^4.5.4", "vite-plugin-dts": "^5.0.3",
"vite-plugin-static-copy": "^3.2.0", "vite-plugin-static-copy": "^4.1.1",
"vitest": "^4.0.16" "vitest": "^4.1.10"
},
"overrides": {
"@tailwindcss/vite": {
"vite": "^7.0.0"
}
}, },
"sideEffects": false, "sideEffects": false,
"license": "", "license": "",
"author": "Mattrixwv", "author": "Mattrixwv",
"repository": { "repository": {
"type": "git", "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", "module": "./dist/mattrixwv-components.js",
"types": "./dist/mattrixwv-components.d.ts", "types": "./dist/mattrixwv-components.d.ts",
"exports": { "exports": {
@@ -109,9 +104,21 @@
"types": "./dist/tab.d.ts", "types": "./dist/tab.d.ts",
"import": "./dist/tab.js" "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": { "./toaster": {
"types": "./dist/toaster.d.ts", "types": "./dist/toaster.d.ts",
"import": "./dist/toaster.js" "import": "./dist/toaster.js"
},
"./token": {
"types": "./dist/token.d.ts",
"import": "./dist/token.js"
} }
}, },
"files": [ "files": [

View File

@@ -1,85 +1,16 @@
@import "tailwindcss"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@import "../lib/styles.css"; @import "../lib/components.css";
@import "../lib/theme.css";
@theme { @theme {
--color-neutral-825: oklch(0.253 0 0); --color-neutral-825: oklch(0.253 0 0);
--color-neutral-850: oklch(0.237 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 { input::-webkit-calendar-picker-indicator {
cursor: pointer; 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);
}
}

View File

@@ -1,5 +1,5 @@
import ThemeProvider from "$/providers/theme/ThemeProvider"; import ThemeProvider from "$/provider/theme/ThemeProvider";
import ToasterProvider from "$/providers/toaster/ToasterProvider"; import ToasterProvider from "$/provider/toaster/ToasterProvider";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";

View File

@@ -10,71 +10,71 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index' 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 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({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } 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({ const ButtonsIndexRoute = ButtonsIndexRouteImport.update({
id: '/buttons/', id: '/buttons/',
path: '/buttons/', path: '/buttons/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } 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 { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/buttons': typeof ButtonsIndexRoute '/buttons/': typeof ButtonsIndexRoute
'/input': typeof InputIndexRoute '/input/': typeof InputIndexRoute
'/loading': typeof LoadingIndexRoute '/loading/': typeof LoadingIndexRoute
'/message': typeof MessageIndexRoute '/message/': typeof MessageIndexRoute
'/modal': typeof ModalIndexRoute '/modal/': typeof ModalIndexRoute
'/pill': typeof PillIndexRoute '/pill/': typeof PillIndexRoute
'/progress': typeof ProgressIndexRoute '/progress/': typeof ProgressIndexRoute
'/tab': typeof TabIndexRoute '/tab/': typeof TabIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
@@ -103,14 +103,14 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: fullPaths:
| '/' | '/'
| '/buttons' | '/buttons/'
| '/input' | '/input/'
| '/loading' | '/loading/'
| '/message' | '/message/'
| '/modal' | '/modal/'
| '/pill' | '/pill/'
| '/progress' | '/progress/'
| '/tab' | '/tab/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@@ -156,60 +156,60 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/tab/': { '/buttons/': {
id: '/tab/' id: '/buttons/'
path: '/tab' path: '/buttons'
fullPath: '/tab' fullPath: '/buttons/'
preLoaderRoute: typeof TabIndexRouteImport preLoaderRoute: typeof ButtonsIndexRouteImport
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
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/input/': { '/input/': {
id: '/input/' id: '/input/'
path: '/input' path: '/input'
fullPath: '/input' fullPath: '/input/'
preLoaderRoute: typeof InputIndexRouteImport preLoaderRoute: typeof InputIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/buttons/': { '/loading/': {
id: '/buttons/' id: '/loading/'
path: '/buttons' path: '/loading'
fullPath: '/buttons' fullPath: '/loading/'
preLoaderRoute: typeof ButtonsIndexRouteImport 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 parentRoute: typeof rootRouteImport
} }
} }

View File

@@ -14,21 +14,22 @@ export const Route = createFileRoute("/buttons/")({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function ButtonPage(){ function ButtonPage(){
const [ clickCount, setClickCount ] = useState(0); const [ clickCount, setClickCount ] = useState(0);
const tabs: TabGroupContent[] = [ const tabs: TabGroupContent[] = [
{ tab: "Primary", content: generateTabContent(PrimaryButton, () => setClickCount(clickCount + 1)) }, { id: "primary", tab: "Primary", content: generateTabContent(PrimaryButton, () => setClickCount(clickCount + 1)) },
{ tab: "Secondary", content: generateTabContent(SecondaryButton, () => setClickCount(clickCount + 1)) }, { id: "secondary", tab: "Secondary", content: generateTabContent(SecondaryButton, () => setClickCount(clickCount + 1)) },
{ tab: "Tertiary", content: generateTabContent(TertiaryButton, () => setClickCount(clickCount + 1)) }, { id: "tertiary", tab: "Tertiary", content: generateTabContent(TertiaryButton, () => setClickCount(clickCount + 1)) },
{ tab: "Info", content: generateTabContent(InfoButton, () => setClickCount(clickCount + 1)) }, { id: "info", tab: "Info", content: generateTabContent(InfoButton, () => setClickCount(clickCount + 1)) },
{ tab: "Success", content: generateTabContent(SuccessButton, () => setClickCount(clickCount + 1)) }, { id: "success", tab: "Success", content: generateTabContent(SuccessButton, () => setClickCount(clickCount + 1)) },
{ tab: "Warning", content: generateTabContent(WarningButton, () => setClickCount(clickCount + 1)) }, { id: "warning", tab: "Warning", content: generateTabContent(WarningButton, () => setClickCount(clickCount + 1)) },
{ tab: "Danger", content: generateTabContent(DangerButton, () => setClickCount(clickCount + 1)) }, { id: "danger", tab: "Danger", content: generateTabContent(DangerButton, () => setClickCount(clickCount + 1)) },
{ tab: "Molten", content: generateTabContent(MoltenButton, () => setClickCount(clickCount + 1)) }, { id: "molten", tab: "Molten", content: generateTabContent(MoltenButton, () => setClickCount(clickCount + 1)) },
{ tab: "Dark", content: generateTabContent(DarkButton, () => setClickCount(clickCount + 1)) }, { id: "dark", tab: "Dark", content: generateTabContent(DarkButton, () => setClickCount(clickCount + 1)) },
{ tab: "Light", content: generateTabContent(LightButton, () => 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({ function ButtonDisplay({
title, title,
children children

View File

@@ -6,6 +6,7 @@ export const Route = createFileRoute("/")({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function HomePage(){ function HomePage(){
return ( return (
<div> <div>

View File

@@ -10,6 +10,7 @@ export const Route = createFileRoute('/input/')({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function InputPage(){ function InputPage(){
const [ date, setDate ] = useState<Date>(); const [ date, setDate ] = useState<Date>();
const [ dateTime, setDateTime ] = useState<Date>(); const [ dateTime, setDateTime ] = useState<Date>();
@@ -17,12 +18,12 @@ function InputPage(){
const tabs: TabGroupContent[] = [ const tabs: TabGroupContent[] = [
{ tab: "Checkbox", content: <CheckboxContent/>}, { id: "checkbox", tab: "Checkbox", content: <CheckboxContent/>},
{ tab: "Radio", content: <RadioContent/> }, { id: "radio", tab: "Radio", content: <RadioContent/> },
{ tab: "Date", content: <DateContent date={date} setDate={setDate} dateTime={dateTime} setDateTime={setDateTime} time={time} setTime={setTime}/> }, { id: "date", tab: "Date", content: <DateContent date={date} setDate={setDate} dateTime={dateTime} setDateTime={setDateTime} time={time} setTime={setTime}/> },
{ tab: "File", content: <FileContent/> }, { id: "file", tab: "File", content: <FileContent/> },
{ tab: "Switch", content: <SwitchContent/> }, { id: "switch", tab: "Switch", content: <SwitchContent/> },
{ tab: "Text", content: <TextContent/> } { id: "text", tab: "Text", content: <TextContent/> }
]; ];

View File

@@ -9,14 +9,15 @@ export const Route = createFileRoute("/loading/")({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function LoadingPage(){ function LoadingPage(){
const tabs: TabGroupContent[] = [ const tabs: TabGroupContent[] = [
{ tab: "Spinners", content: generateSpinnersContent() }, { id: "spinners", tab: "Spinners", content: generateSpinnersContent() },
{ tab: "Dots", content: generateDotsContent() }, { id: "dots", tab: "Dots", content: generateDotsContent() },
{ tab: "Bars", content: generateBarsContent() }, { id: "bars", tab: "Bars", content: generateBarsContent() },
{ tab: "Blocks", content: generateBlocksContent() }, { id: "blocks", tab: "Blocks", content: generateBlocksContent() },
{ tab: "Pulses", content: generatePulsesContent() }, { id: "pulses", tab: "Pulses", content: generatePulsesContent() },
{ tab: "Various", content: generateVariousContent() } { id: "various", tab: "Various", content: generateVariousContent() }
]; ];
return ( return (

View File

@@ -12,18 +12,19 @@ export const Route = createFileRoute("/message/")({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function MessagePage(){ function MessagePage(){
const tabs: TabGroupContent[] = [ const tabs: TabGroupContent[] = [
{ tab: "Primary", content: generateTabContent(PrimaryMessageBlock) }, { id: "primary", tab: "Primary", content: generateTabContent(PrimaryMessageBlock) },
{ tab: "Secondary", content: generateTabContent(SecondaryMessageBlock) }, { id: "secondary", tab: "Secondary", content: generateTabContent(SecondaryMessageBlock) },
{ tab: "Tertiary", content: generateTabContent(TertiaryMessageBlock) }, { id: "tertiary", tab: "Tertiary", content: generateTabContent(TertiaryMessageBlock) },
{ tab: "Info", content: generateTabContent(InfoMessageBlock) }, { id: "info", tab: "Info", content: generateTabContent(InfoMessageBlock) },
{ tab: "Success", content: generateTabContent(SuccessMessageBlock) }, { id: "success", tab: "Success", content: generateTabContent(SuccessMessageBlock) },
{ tab: "Warning", content: generateTabContent(WarningMessageBlock) }, { id: "warning", tab: "Warning", content: generateTabContent(WarningMessageBlock) },
{ tab: "Danger", content: generateTabContent(DangerMessageBlock) }, { id: "danger", tab: "Danger", content: generateTabContent(DangerMessageBlock) },
{ tab: "Molten", content: generateTabContent(MoltenMessageBlock) }, { id: "molten", tab: "Molten", content: generateTabContent(MoltenMessageBlock) },
{ tab: "Dark", content: generateTabContent(DarkMessageBlock) }, { id: "dark", tab: "Dark", content: generateTabContent(DarkMessageBlock) },
{ tab: "Light", content: generateTabContent(LightMessageBlock) } { id: "light", tab: "Light", content: generateTabContent(LightMessageBlock) }
]; ];

View File

@@ -1,7 +1,7 @@
import { DangerButton, PrimaryButton, SuccessButton, WarningButton } from "$/component/button"; import { DangerButton, PrimaryButton, SuccessButton, WarningButton } from "$/component/button";
import { PrimaryMessageBlock } from "$/component/message"; import { PrimaryMessageBlock } from "$/component/message";
import { Modal } from "$/component/modal"; import { Modal } from "$/component/modal";
import { useToaster } from "$/providers/toaster/ToasterProvider"; import { useToaster } from "$/provider/toaster/ToasterProvider";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react"; import { useState } from "react";
@@ -11,6 +11,7 @@ export const Route = createFileRoute("/modal/")({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function ModalPage(){ function ModalPage(){
const [ displayCenteredModal, setDisplayCenteredModal ] = useState(false); const [ displayCenteredModal, setDisplayCenteredModal ] = useState(false);
const [ displayTopModal, setDisplayTopModal ] = useState(false); const [ displayTopModal, setDisplayTopModal ] = useState(false);
@@ -74,6 +75,7 @@ function ModalPage(){
); );
} }
// eslint-disable-next-line react-refresh/only-export-components
function DemoCenteredModal({ function DemoCenteredModal({
display, display,
onClose onClose
@@ -94,6 +96,7 @@ function DemoCenteredModal({
); );
} }
// eslint-disable-next-line react-refresh/only-export-components
function DemoTopModal({ function DemoTopModal({
display, display,
onClose onClose

View File

@@ -17,6 +17,7 @@ export const Route = createFileRoute("/pill/")({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function PillPage(){ function PillPage(){
return ( return (
<div className="flex flex-col items-center justify-center gap-y-8"> <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({ function PillLayout({
PillType, PillType,
pillName pillName

View File

@@ -1,15 +1,17 @@
import { DangerButton, PrimaryButton } from '$/component/button'; import { DangerButton, PrimaryButton } from "$/component/button";
import { NumberInput } from '$/component/input'; import { NumberInput } from "$/component/input";
import { DangerProgress, DarkProgress, InfoProgress, LightProgress, MoltenProgress, PrimaryProgress, Progress, SecondaryProgress, SuccessProgress, TertiaryProgress, WarningProgress } from '$/component/progress'; import { DangerProgress, DarkProgress, InfoProgress, LightProgress, MoltenProgress, PrimaryProgress, Progress, SecondaryProgress, SuccessProgress, TertiaryProgress, WarningProgress } from '$/component/progress';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from "@tanstack/react-router";
import { useState } from 'react'; import { useState } from "react";
import { BsDashLg, BsPlusLg } from 'react-icons/bs'; import { BsDashLg, BsPlusLg } from "react-icons/bs";
export const Route = createFileRoute('/progress/')({ export const Route = createFileRoute("/progress/")({
component: ProgressPage, component: ProgressPage
}); });
// eslint-disable-next-line react-refresh/only-export-components
function ProgressPage() { function ProgressPage() {
const [ value, setValue ] = useState(0); const [ value, setValue ] = useState(0);
@@ -125,6 +127,7 @@ function ProgressPage() {
); );
} }
// eslint-disable-next-line react-refresh/only-export-components
function ProgressBlock({ function ProgressBlock({
label, label,
children children

View File

@@ -9,13 +9,14 @@ export const Route = createFileRoute('/tab/')({
}); });
// eslint-disable-next-line react-refresh/only-export-components
function RouteComponent(){ function RouteComponent(){
const tabs: TabGroupContent[] = [ const tabs: TabGroupContent[] = [
{ tab: "Tab 1", content: "Tab 1 Content" }, { id: "1", tab: "Tab 1", content: "Tab 1 Content" },
{ tab: "Tab 2", content: "Tab 2 Content" }, { id: "2", tab: "Tab 2", content: "Tab 2 Content" },
{ tab: "Tab 3", content: "Tab 3 Content" }, { id: "3", tab: "Tab 3", content: "Tab 3 Content" },
{ tab: "Tab 4", content: "Tab 4 Content" }, { id: "4", 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: "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 ( return (

View File

@@ -574,16 +574,18 @@ export function TextContent(){
const [ selected, setSelected ] = useState(selectOptions[0]); const [ selected, setSelected ] = useState(selectOptions[0]);
const [ numberValue, setNumberValue ] = useState(0); const [ numberValue, setNumberValue ] = useState(0);
const [ textValue, setTextValue ] = useState("");
const [ secondTextValue, setSecondTextValue ] = useState("");
return ( return (
<div <div
className="flex flex-col items-center justify-center gap-y-8 mt-8 w-full" className="flex flex-col items-center justify-center gap-y-8 mt-8 w-full"
> >
<GeneralInputDisplay title="Text Input"> <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>
<GeneralInputDisplay title="Text Area"> <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>
<GeneralInputDisplay title="Select"> <GeneralInputDisplay title="Select">
<SelectInput placeholder={selected.label} onChange={(newValue) => setSelected(selectOptions.find((option) => option.value === newValue) || selectOptions[0])}> <SelectInput placeholder={selected.label} onChange={(newValue) => setSelected(selectOptions.find((option) => option.value === newValue) || selectOptions[0])}>

View File

@@ -66,6 +66,7 @@ function LoadingGroup({
{ {
elements.map((spinner, index) => ( elements.map((spinner, index) => (
<div <div
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index} key={index}
className="w-32 h-32" className="w-32 h-32"
> >

View File

@@ -22,7 +22,6 @@
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": { "paths": {
"$/*": [ "./lib/*" ], "$/*": [ "./lib/*" ],
"@/*": [ "./src/*" ], "@/*": [ "./src/*" ],

View File

@@ -22,7 +22,6 @@
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true, "noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": { "paths": {
"$/*": [ "./lib/*" ], "$/*": [ "./lib/*" ],
"#root": [ "." ] "#root": [ "." ]

View File

@@ -7,10 +7,11 @@ import removeTestIdAttribute from "rollup-plugin-jsx-remove-attributes";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import dts from "vite-plugin-dts"; import dts from "vite-plugin-dts";
import { viteStaticCopy } from "vite-plugin-static-copy"; 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/ // https://vite.dev/config/
@@ -25,8 +26,7 @@ export default defineConfig({
dts({ dts({
include: ["lib"], include: ["lib"],
tsconfigPath: "./tsconfig.lib.json", tsconfigPath: "./tsconfig.lib.json",
rollupTypes: true, bundleTypes: true,
insertTypesEntry: true,
compilerOptions: { compilerOptions: {
declarationMap: false declarationMap: false
} }
@@ -35,7 +35,15 @@ export default defineConfig({
viteStaticCopy({ viteStaticCopy({
targets: [ 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: "" dest: ""
} }
] ]
@@ -43,16 +51,17 @@ export default defineConfig({
], ],
resolve: { resolve: {
alias: { alias: {
"@": resolve(__dirname, "src"), "@": resolve(import.meta.dirname, "src"),
"$": resolve(__dirname, "lib"), "$": resolve(import.meta.dirname, "lib"),
"#root": resolve(__dirname) "#root": resolve(import.meta.dirname)
} }
}, },
build: { build: {
lib: { lib: {
entry: { entry: {
index: resolve(__dirname, "lib/index.ts"), "mattrixwv-components": resolve(import.meta.dirname, "lib/index.ts"),
...Object.fromEntries(modules.map(mod => [mod, resolve(__dirname, `lib/component/${mod}/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" ], formats: [ "es" ],
name: "Mattrixwv Component Library" name: "Mattrixwv Component Library"