diff --git a/src/pswui/components/Dialog.tsx b/src/pswui/components/Dialog/Component.tsx similarity index 92% rename from src/pswui/components/Dialog.tsx rename to src/pswui/components/Dialog/Component.tsx index 704db6f..f4bdc36 100644 --- a/src/pswui/components/Dialog.tsx +++ b/src/pswui/components/Dialog/Component.tsx @@ -1,32 +1,13 @@ -import React, { Dispatch, SetStateAction, useState } from "react"; +import React, { useState } from "react"; import { Slot, VariantProps, vcn } from "@pswui-lib"; import ReactDOM from "react-dom"; -/** - * ========================= - * DialogContext - * ========================= - */ - -interface DialogContext { - opened: boolean; -} - -const initialDialogContext: DialogContext = { opened: false }; -const DialogContext = React.createContext< - [DialogContext, Dispatch>] ->([ +import { + DialogContext, initialDialogContext, - () => { - if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { - console.warn( - "It seems like you're using DialogContext outside of a provider.", - ); - } - }, -]); - -const useDialogContext = () => React.useContext(DialogContext); + useDialogContext, + IDialogContext, +} from "./Context"; /** * ========================= @@ -39,7 +20,7 @@ interface DialogRootProps { } const DialogRoot = ({ children }: DialogRootProps) => { - const state = useState(initialDialogContext); + const state = useState(initialDialogContext); return ( {children} ); @@ -411,7 +392,6 @@ const DialogFooter = React.forwardRef( ); export { - useDialogContext, DialogRoot, DialogTrigger, DialogOverlay, diff --git a/src/pswui/components/Dialog/Context.ts b/src/pswui/components/Dialog/Context.ts new file mode 100644 index 0000000..3b59c7b --- /dev/null +++ b/src/pswui/components/Dialog/Context.ts @@ -0,0 +1,27 @@ +import { Dispatch, SetStateAction, useContext, createContext } from "react"; + +/** + * ========================= + * DialogContext + * ========================= + */ + +export interface IDialogContext { + opened: boolean; +} + +export const initialDialogContext: IDialogContext = { opened: false }; +export const DialogContext = createContext< + [IDialogContext, Dispatch>] +>([ + initialDialogContext, + () => { + if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { + console.warn( + "It seems like you're using DialogContext outside of a provider.", + ); + } + }, +]); + +export const useDialogContext = () => useContext(DialogContext); diff --git a/src/pswui/components/Dialog/index.ts b/src/pswui/components/Dialog/index.ts new file mode 100644 index 0000000..9202698 --- /dev/null +++ b/src/pswui/components/Dialog/index.ts @@ -0,0 +1,2 @@ +export * from "./Component"; +export { useDialogContext } from "./Context"; diff --git a/src/pswui/components/Tabs.tsx b/src/pswui/components/Tabs/Component.tsx similarity index 58% rename from src/pswui/components/Tabs.tsx rename to src/pswui/components/Tabs/Component.tsx index 7355f23..bc90902 100644 --- a/src/pswui/components/Tabs.tsx +++ b/src/pswui/components/Tabs/Component.tsx @@ -1,30 +1,7 @@ import { AsChild, Slot, VariantProps, vcn } from "@pswui-lib"; import React from "react"; -interface Tab { - name: string; -} - -interface TabContextBody { - tabs: Tab[]; - active: [number, string] /* index, name */; -} - -const TabContext = React.createContext< - [TabContextBody, React.Dispatch>] ->([ - { - tabs: [], - active: [0, ""], - }, - () => { - if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { - console.warn( - "It seems like you're using TabContext outside of provider.", - ); - } - }, -]); +import { TabContextBody, TabContext, Tab } from "./Context"; interface TabProviderProps { defaultName: string; @@ -40,77 +17,6 @@ const TabProvider = ({ defaultName, children }: TabProviderProps) => { return {children}; }; -/** - * Provides current state for tab, using context. - * Also provides functions to control state. - */ -const useTabState = () => { - const [state, setState] = React.useContext(TabContext); - - function getActiveTab() { - return state.active; - } - - function setActiveTab(name: string): void; - function setActiveTab(index: number): void; - function setActiveTab(param: string | number) { - if (typeof param === "number") { - if (param < 0 || param >= state.tabs.length) { - if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { - console.error( - `Invalid index passed to setActiveTab: ${param}, valid indices are 0 to ${ - state.tabs.length - 1 - }`, - ); - } - return; - } - - setState((prev) => { - return { - ...prev, - active: [param, prev.tabs[param].name], - }; - }); - } else if (typeof param === "string") { - const index = state.tabs.findIndex((tab) => tab.name === param); - if (index === -1) { - if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { - console.error( - `Invalid name passed to setActiveTab: ${param}, valid names are ${state.tabs - .map((tab) => tab.name) - .join(", ")}`, - ); - } - return; - } - - setActiveTab(index); - } - } - - function setPreviousActive() { - if (state.active[0] === 0) { - return; - } - setActiveTab(state.active[0] - 1); - } - - function setNextActive() { - if (state.active[0] === state.tabs.length - 1) { - return; - } - setActiveTab(state.active[0] + 1); - } - - return { - getActiveTab, - setActiveTab, - setPreviousActive, - setNextActive, - }; -}; - const [TabListVariant, resolveTabListVariantProps] = vcn({ base: "flex flex-row bg-gray-100 dark:bg-neutral-800 rounded-lg p-1.5 gap-1", variants: {}, @@ -169,6 +75,7 @@ const TabTrigger = (props: TabTriggerProps) => { }; }); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [name]); const Comp = props.asChild ? Slot : "button"; @@ -226,4 +133,4 @@ const TabContent = (props: TabContentProps) => { } }; -export { TabProvider, useTabState, TabList, TabTrigger, TabContent }; +export { TabProvider, TabList, TabTrigger, TabContent }; diff --git a/src/pswui/components/Tabs/Context.ts b/src/pswui/components/Tabs/Context.ts new file mode 100644 index 0000000..4f00846 --- /dev/null +++ b/src/pswui/components/Tabs/Context.ts @@ -0,0 +1,26 @@ +import React from "react"; + +export interface Tab { + name: string; +} + +export interface TabContextBody { + tabs: Tab[]; + active: [number, string] /* index, name */; +} + +export const TabContext = React.createContext< + [TabContextBody, React.Dispatch>] +>([ + { + tabs: [], + active: [0, ""], + }, + () => { + if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { + console.warn( + "It seems like you're using TabContext outside of provider.", + ); + } + }, +]); diff --git a/src/pswui/components/Tabs/Hook.ts b/src/pswui/components/Tabs/Hook.ts new file mode 100644 index 0000000..26e2aa3 --- /dev/null +++ b/src/pswui/components/Tabs/Hook.ts @@ -0,0 +1,74 @@ +import React from "react"; + +import { TabContext } from "./Context"; + +/** + * Provides current state for tab, using context. + * Also provides functions to control state. + */ +export const useTabState = () => { + const [state, setState] = React.useContext(TabContext); + + function getActiveTab() { + return state.active; + } + + function setActiveTab(name: string): void; + function setActiveTab(index: number): void; + function setActiveTab(param: string | number) { + if (typeof param === "number") { + if (param < 0 || param >= state.tabs.length) { + if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { + console.error( + `Invalid index passed to setActiveTab: ${param}, valid indices are 0 to ${ + state.tabs.length - 1 + }`, + ); + } + return; + } + + setState((prev) => { + return { + ...prev, + active: [param, prev.tabs[param].name], + }; + }); + } else if (typeof param === "string") { + const index = state.tabs.findIndex((tab) => tab.name === param); + if (index === -1) { + if (process.env.NODE_ENV && process.env.NODE_ENV === "development") { + console.error( + `Invalid name passed to setActiveTab: ${param}, valid names are ${state.tabs + .map((tab) => tab.name) + .join(", ")}`, + ); + } + return; + } + + setActiveTab(index); + } + } + + function setPreviousActive() { + if (state.active[0] === 0) { + return; + } + setActiveTab(state.active[0] - 1); + } + + function setNextActive() { + if (state.active[0] === state.tabs.length - 1) { + return; + } + setActiveTab(state.active[0] + 1); + } + + return { + getActiveTab, + setActiveTab, + setPreviousActive, + setNextActive, + }; +}; diff --git a/src/pswui/components/Tabs/index.ts b/src/pswui/components/Tabs/index.ts new file mode 100644 index 0000000..c16790c --- /dev/null +++ b/src/pswui/components/Tabs/index.ts @@ -0,0 +1,2 @@ +export * from "./Component"; +export * from "./Hook"; diff --git a/src/pswui/components/Toast.tsx b/src/pswui/components/Toast/Component.tsx similarity index 60% rename from src/pswui/components/Toast.tsx rename to src/pswui/components/Toast/Component.tsx index 92e292a..9200465 100644 --- a/src/pswui/components/Toast.tsx +++ b/src/pswui/components/Toast/Component.tsx @@ -2,148 +2,19 @@ import React, { useEffect, useId, useRef } from "react"; import ReactDOM from "react-dom"; import { VariantProps, vcn } from "@pswui-lib"; -interface ToastOption { - closeButton: boolean; - closeTimeout: number | null; -} - -const defaultToastOption: ToastOption = { - closeButton: true, - closeTimeout: 3000, -}; - -const toastColors = { - background: "bg-white dark:bg-black", - borders: { - default: "border-black/10 dark:border-white/20", - error: "border-red-500/80", - success: "border-green-500/80", - warning: "border-yellow-500/80", - loading: "border-black/50 dark:border-white/50 animate-pulse", - }, -}; - -const [toastVariant] = vcn({ - base: `flex flex-col gap-2 border p-4 rounded-lg pr-8 pointer-events-auto ${toastColors.background} relative transition-all duration-150`, - variants: { - status: { - default: toastColors.borders.default, - error: toastColors.borders.error, - success: toastColors.borders.success, - warning: toastColors.borders.warning, - loading: toastColors.borders.loading, - }, - life: { - born: "-translate-y-full md:translate-y-full scale-90 ease-[cubic-bezier(0,.6,.7,1)]", - normal: "translate-y-0 scale-100 ease-[cubic-bezier(0,.6,.7,1)]", - dead: "-translate-y-full md:translate-y-full scale-90 ease-[cubic-bezier(.6,0,1,.7)]", - }, - }, - defaults: { - status: "default", - life: "born", - }, -}); - -interface ToastBody extends Omit, "preset"> { - title: string; - description: string; -} - -let index = 0; -let toasts: Record< - `${number}`, - ToastBody & Partial & { subscribers: (() => void)[] } -> = {}; -let subscribers: (() => void)[] = []; - -/** - * ==== - * Controls - * ==== - */ - -function subscribe(callback: () => void) { - subscribers.push(callback); - return () => { - subscribers = subscribers.filter((subscriber) => subscriber !== callback); - }; -} - -function getSnapshot() { - return { ...toasts }; -} - -function subscribeSingle(id: `${number}`) { - return (callback: () => void) => { - toasts[id].subscribers.push(callback); - return () => { - toasts[id].subscribers = toasts[id].subscribers.filter( - (subscriber) => subscriber !== callback, - ); - }; - }; -} - -function getSingleSnapshot(id: `${number}`) { - return () => { - return { - ...toasts[id], - }; - }; -} - -function notify() { - subscribers.forEach((subscriber) => subscriber()); -} - -function notifySingle(id: `${number}`) { - toasts[id].subscribers.forEach((subscriber) => subscriber()); -} - -function close(id: `${number}`) { - toasts[id] = { - ...toasts[id], - life: "dead", - }; - notifySingle(id); -} - -function update( - id: `${number}`, - toast: Partial & Partial>, -) { - toasts[id] = { - ...toasts[id], - ...toast, - }; - notifySingle(id); -} - -function addToast(toast: Omit & Partial) { - const id: `${number}` = `${index}`; - toasts[id] = { - ...toast, - subscribers: [], - life: "born", - }; - index += 1; - notify(); - - return { - update: (toast: Partial & Partial>) => - update(id, toast), - close: () => close(id), - }; -} - -function useToast() { - return { - toast: addToast, - update, - close, - }; -} +import { toastVariant } from "./Variant"; +import { + ToastOption, + toasts, + subscribeSingle, + getSingleSnapshot, + notifySingle, + close, + notify, + defaultToastOption, + subscribe, + getSnapshot, +} from "./Store"; const ToastTemplate = ({ id, @@ -161,7 +32,7 @@ const ToastTemplate = ({ subscribeSingle(id)(() => { setToast(getSingleSnapshot(id)()); }); - }, []); + }, [id]); const toastData = { ...globalOption, @@ -225,7 +96,7 @@ const ToastTemplate = ({ }, calculatedTransitionDuration); return () => clearTimeout(timeout); } - }, [toastData.life, toastData.closeTimeout, toastData.closeButton]); + }, [id, toastData.life, toastData.closeTimeout, toastData.closeButton]); return (
((props, ref) => { const internalRef = useRef(null); useEffect(() => { - const unsubscribe = subscribe(() => { + return subscribe(() => { setToastList(getSnapshot()); }); - return unsubscribe; }, []); const option = React.useMemo(() => { @@ -308,7 +178,7 @@ const Toaster = React.forwardRef((props, ref) => { {ReactDOM.createPortal(
{ internalRef.current = el; @@ -334,4 +204,4 @@ const Toaster = React.forwardRef((props, ref) => { ); }); -export { Toaster, useToast }; +export { Toaster }; diff --git a/src/pswui/components/Toast/Hook.ts b/src/pswui/components/Toast/Hook.ts new file mode 100644 index 0000000..f6d74a2 --- /dev/null +++ b/src/pswui/components/Toast/Hook.ts @@ -0,0 +1,9 @@ +import { addToast, update, close } from "./Store"; + +export function useToast() { + return { + toast: addToast, + update, + close, + }; +} diff --git a/src/pswui/components/Toast/Store.ts b/src/pswui/components/Toast/Store.ts new file mode 100644 index 0000000..04fb4f2 --- /dev/null +++ b/src/pswui/components/Toast/Store.ts @@ -0,0 +1,100 @@ +import { ToastBody } from "./Variant"; + +export interface ToastOption { + closeButton: boolean; + closeTimeout: number | null; +} + +export const defaultToastOption: ToastOption = { + closeButton: true, + closeTimeout: 3000, +}; + +let index = 0; +export const toasts: Record< + `${number}`, + ToastBody & Partial & { subscribers: (() => void)[] } +> = {}; +let subscribers: (() => void)[] = []; + +/** + * ==== + * Controls + * ==== + */ + +export function subscribe(callback: () => void) { + subscribers.push(callback); + return () => { + subscribers = subscribers.filter((subscriber) => subscriber !== callback); + }; +} + +export function getSnapshot() { + return { ...toasts }; +} + +export function subscribeSingle(id: `${number}`) { + return (callback: () => void) => { + toasts[id].subscribers.push(callback); + return () => { + toasts[id].subscribers = toasts[id].subscribers.filter( + (subscriber) => subscriber !== callback, + ); + }; + }; +} + +export function getSingleSnapshot(id: `${number}`) { + return () => { + return { + ...toasts[id], + }; + }; +} + +export function notify() { + subscribers.forEach((subscriber) => subscriber()); +} + +export function notifySingle(id: `${number}`) { + toasts[id].subscribers.forEach((subscriber) => subscriber()); +} + +export function close(id: `${number}`) { + toasts[id] = { + ...toasts[id], + life: "dead", + }; + notifySingle(id); +} + +export function update( + id: `${number}`, + toast: Partial & Partial>, +) { + toasts[id] = { + ...toasts[id], + ...toast, + }; + notifySingle(id); +} + +export function addToast( + toast: Omit & Partial, +) { + const id: `${number}` = `${index}`; + toasts[id] = { + ...toast, + subscribers: [], + life: "born", + }; + index += 1; + notify(); + + return { + update: (toast: Partial & Partial>) => + update(id, toast), + close: () => close(id), + }; +} diff --git a/src/pswui/components/Toast/Variant.ts b/src/pswui/components/Toast/Variant.ts new file mode 100644 index 0000000..2a0285c --- /dev/null +++ b/src/pswui/components/Toast/Variant.ts @@ -0,0 +1,40 @@ +import { VariantProps, vcn } from "@pswui-lib"; + +const toastColors = { + background: "bg-white dark:bg-black", + borders: { + default: "border-black/10 dark:border-white/20", + error: "border-red-500/80", + success: "border-green-500/80", + warning: "border-yellow-500/80", + loading: "border-black/50 dark:border-white/50 animate-pulse", + }, +}; + +export const [toastVariant, resolveToastVariantProps] = vcn({ + base: `flex flex-col gap-2 border p-4 rounded-lg pr-8 pointer-events-auto ${toastColors.background} relative transition-all duration-150`, + variants: { + status: { + default: toastColors.borders.default, + error: toastColors.borders.error, + success: toastColors.borders.success, + warning: toastColors.borders.warning, + loading: toastColors.borders.loading, + }, + life: { + born: "-translate-y-full md:translate-y-full scale-90 ease-[cubic-bezier(0,.6,.7,1)]", + normal: "translate-y-0 scale-100 ease-[cubic-bezier(0,.6,.7,1)]", + dead: "-translate-y-full md:translate-y-full scale-90 ease-[cubic-bezier(.6,0,1,.7)]", + }, + }, + defaults: { + status: "default", + life: "born", + }, +}); + +export interface ToastBody + extends Omit, "preset"> { + title: string; + description: string; +} diff --git a/src/pswui/components/Toast/index.ts b/src/pswui/components/Toast/index.ts new file mode 100644 index 0000000..e69de29