From 68cea5be9a19c1dd9044d0943e14b046a958e45f Mon Sep 17 00:00:00 2001 From: Arno V Date: Fri, 3 Nov 2023 12:42:53 -0400 Subject: [PATCH 1/3] feat: first release --- package.json | 5 +- packages/ui-components/package.json | 33 +- .../ui-components/src/common/constants.ts | 9 - packages/ui-components/src/common/hooks.ts | 26 -- .../ui-components/src/common/jsonUtilities.ts | 15 - packages/ui-components/src/common/strings.ts | 7 - packages/ui-components/src/common/types.d.ts | 63 ---- .../ui-components/src/common/utilities.ts | 68 ---- .../src/components/Button/ButtonLink.tsx | 57 ++++ .../src/components/Button/ButtonTypes.d.ts | 6 + .../Button/__tests__/ButtonLink.test.tsx | 95 ++++++ .../src/components/Button/button.css | 4 - .../src/components/Button/utilities.ts | 14 +- .../src/components/Footer/Footer.tsx | 24 +- .../src/components/Footer/FooterTypes.d.ts | 5 + .../Footer/__tests__/Footer.test.tsx | 50 +++ .../src/components/Footer/footer.css | 8 - .../ui-components/src/components/index.ts | 3 +- packages/ui-components/vite.config.ts | 30 +- yarn.lock | 305 +++++++++--------- 20 files changed, 428 insertions(+), 399 deletions(-) delete mode 100644 packages/ui-components/src/common/constants.ts delete mode 100644 packages/ui-components/src/common/hooks.ts delete mode 100644 packages/ui-components/src/common/jsonUtilities.ts delete mode 100644 packages/ui-components/src/common/strings.ts delete mode 100644 packages/ui-components/src/common/types.d.ts create mode 100644 packages/ui-components/src/components/Button/ButtonLink.tsx create mode 100644 packages/ui-components/src/components/Button/__tests__/ButtonLink.test.tsx create mode 100644 packages/ui-components/src/components/Footer/FooterTypes.d.ts create mode 100644 packages/ui-components/src/components/Footer/__tests__/Footer.test.tsx diff --git a/package.json b/package.json index d7de2090..3b3c57aa 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,6 @@ "test": "yarn lerna run test" }, "devDependencies": { - "@versini/dev-dependencies-client": "1.0.2", - "glob": "10.3.10", - "vite-plugin-dts": "3.6.3", - "vite-plugin-lib-inject-css": "1.3.0" + "@versini/dev-dependencies-client": "1.0.3" } } diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index 5b630a39..22c51185 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -1,9 +1,14 @@ { "name": "@versini/ui-components", - "version": "1.0.0", + "version": "0.0.1", "license": "MIT", "author": "Arno Versini", "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "clean": "rimraf dist", "dev": "vite --host", @@ -15,15 +20,25 @@ "test:coverage:ui": "vitest --coverage --ui", "test:watch": "vitest" }, - "dependencies": { - "@floating-ui/react": "0.26.0", + "peerDependencies": { + "@floating-ui/react": "^0.26.0", + "@tailwindcss/typography": "0.5.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "3.3.3" + }, + "devDependencies": { + "@floating-ui/react": "^0.26.0", "@tailwindcss/typography": "0.5.10", - "autoprefixer": "10.4.16", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "3.3.3" + }, + "dependencies": { "clsx": "2.0.0", - "postcss": "8.4.31", - "react": "18.2.0", - "react-dom": "18.2.0", - "tailwindcss": "3.3.3", "uuid": "9.0.1" - } + }, + "sideEffects": [ + "**/*.css" + ] } diff --git a/packages/ui-components/src/common/constants.ts b/packages/ui-components/src/common/constants.ts deleted file mode 100644 index 1fc4098e..00000000 --- a/packages/ui-components/src/common/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const ACTION_GET_DATA = "action-get-data"; -export const ACTION_SET_DATA = "action-set-data"; -export const ACTION_SET_STATUS = "action-set-status"; -export const ACTION_STATUS_ERROR = "error"; -export const ACTION_STATUS_STALE = "stale"; -export const ACTION_STATUS_SUCCESS = "success"; - -export const LOCAL_STORAGE_PREFIX = "my-shortcuts-"; -export const LOCAL_STORAGE_BASIC_AUTH = "basic-auth"; diff --git a/packages/ui-components/src/common/hooks.ts b/packages/ui-components/src/common/hooks.ts deleted file mode 100644 index f5c9d337..00000000 --- a/packages/ui-components/src/common/hooks.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { LOCAL_STORAGE_BASIC_AUTH, LOCAL_STORAGE_PREFIX } from "./constants"; -import { obfuscate, unObfuscate } from "./utilities"; - -type LocalStorageKey = typeof LOCAL_STORAGE_BASIC_AUTH; - -export const useLocalStorage = () => { - return { - get: (key: LocalStorageKey): string | boolean | null => { - const data = unObfuscate( - localStorage.getItem(LOCAL_STORAGE_PREFIX + key) || "", - ); - if (data === "true" || data === "false") { - return data === "true"; - } - return data; - }, - set: (key: LocalStorageKey, value: string | boolean) => { - const data = typeof value === "boolean" ? value.toString() : value; - const obfuscatedValue = obfuscate(data.trim()) || ""; - localStorage.setItem(LOCAL_STORAGE_PREFIX + key, obfuscatedValue); - }, - remove: (key: LocalStorageKey) => { - localStorage.removeItem(LOCAL_STORAGE_PREFIX + key); - }, - }; -}; diff --git a/packages/ui-components/src/common/jsonUtilities.ts b/packages/ui-components/src/common/jsonUtilities.ts deleted file mode 100644 index eb910184..00000000 --- a/packages/ui-components/src/common/jsonUtilities.ts +++ /dev/null @@ -1,15 +0,0 @@ -import JSON5 from "json5"; -import { v4 as uuidv4 } from "uuid"; - -import type { ShortcutDataProps } from "./types"; - -export const jsonParse = (json: string) => { - return JSON5.parse(json); -}; - -export const addUniqueId = (data: ShortcutDataProps[]) => { - return data.map((item) => { - item["id"] = uuidv4(); - return item; - }); -}; diff --git a/packages/ui-components/src/common/strings.ts b/packages/ui-components/src/common/strings.ts deleted file mode 100644 index b3775a49..00000000 --- a/packages/ui-components/src/common/strings.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const APP_NAME = "My Shortcuts"; -export const APP_OWNER = "gizmette.com"; -export const FAKE_USER_EMAIL = "fake-user@fake.com"; -export const FAKE_USER_NAME = "fake-user-name"; -export const LOG_IN = "Log in"; -export const LOG_OUT = "Log out"; -export const PASSWORD_PLACEHOLDER = "Enter password"; diff --git a/packages/ui-components/src/common/types.d.ts b/packages/ui-components/src/common/types.d.ts deleted file mode 100644 index cf1921ac..00000000 --- a/packages/ui-components/src/common/types.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - ACTION_GET_DATA, - ACTION_SET_DATA, - ACTION_SET_STATUS, - ACTION_STATUS_ERROR, - ACTION_STATUS_STALE, - ACTION_STATUS_SUCCESS, -} from "./constants"; - -export type ShortcutDataProps = { - id: string; - label: string; - url: string; -}; - -export type ShortcutProps = { - position: number; - title: string; - data: ShortcutDataProps[]; -}; - -export type StateProps = { - status: - | string - | typeof ACTION_STATUS_STALE - | typeof ACTION_STATUS_ERROR - | typeof ACTION_STATUS_SUCCESS; - shortcuts: ShortcutProps[]; -}; - -export type ActionProps = - | undefined - | { - type: typeof ACTION_SET_STATUS; - payload: { - status: - | typeof ACTION_STATUS_STALE - | typeof ACTION_STATUS_ERROR - | typeof ACTION_STATUS_SUCCESS; - }; - } - | { - type: typeof ACTION_GET_DATA; - payload: { - status: - | string - | typeof ACTION_STATUS_ERROR - | typeof ACTION_STATUS_SUCCESS; - shortcuts: ShortcutProps[]; - }; - } - | { - type: typeof ACTION_SET_DATA; - payload: { - status: typeof ACTION_STATUS_STALE; - shortcut: ShortcutProps; - }; - }; - -export type AppContextProps = { - state?: StateProps; - dispatch: React.Dispatch; -}; diff --git a/packages/ui-components/src/common/utilities.ts b/packages/ui-components/src/common/utilities.ts index a1eb91a2..eacb03ee 100644 --- a/packages/ui-components/src/common/utilities.ts +++ b/packages/ui-components/src/common/utilities.ts @@ -14,71 +14,3 @@ export const truncate = (fullString: string, maxLength: number) => { fullString, }; }; - -/* c8 ignore start */ -export const serviceCall = async ({ - name, - data, - method = "POST", - headers = {}, -}: { - name: string; - data: any; - method?: string; - headers?: any; -}) => { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/${name}`, - { - method, - headers: { - ...headers, - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }, - ); - return response; -}; -/* c8 ignore stop */ - -/* c8 ignore start */ -export const getViewportWidth = () => { - return Math.max( - document.documentElement.clientWidth || 0, - window.innerWidth || 0, - ); -}; -/* c8 ignore stop */ - -export const obfuscate = (str: string) => { - /** - * First we use encodeURIComponent to get percent-encoded - * UTF-8, then we convert the percent encodings into raw - * bytes which can be fed into btoa. - */ - return window.btoa( - encodeURIComponent(str).replace( - /%([0-9A-F]{2})/g, - function toSolidBytes(_match, p1) { - return String.fromCharCode(Number(`0x${p1}`)); - }, - ), - ); -}; - -export const unObfuscate = (str: string) => { - /** - * Going backwards: from bytestream, to percent-encoding, - * to original string. - */ - return decodeURIComponent( - window - .atob(str) - .split("") - .map(function (c) { - return `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`; - }) - .join(""), - ); -}; diff --git a/packages/ui-components/src/components/Button/ButtonLink.tsx b/packages/ui-components/src/components/Button/ButtonLink.tsx new file mode 100644 index 00000000..dc88eb27 --- /dev/null +++ b/packages/ui-components/src/components/Button/ButtonLink.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +import { truncate } from "../../common/utilities"; +import type { ButtonLinkProps } from "./ButtonTypes"; +import { getButtonClasses, TYPE_LINK } from "./utilities"; + +export const ButtonLink = React.forwardRef( + ( + { + children, + kind = "dark", + fullWidth = false, + className, + slim = false, + raw = false, + "aria-label": ariaLabel, + link, + target, + maxLabelLength, + }, + ref, + ) => { + const buttonClass = getButtonClasses({ + type: TYPE_LINK, + kind, + fullWidth, + disabled: false, + raw, + className, + slim, + }); + + const formattedLabel = + maxLabelLength && typeof children === "string" + ? truncate(children, maxLabelLength) + : null; + + const extraProps = { + target, + rel: target === "_blank" ? "noopener noreferrer" : undefined, + }; + + return ( + <> + + {formattedLabel?.truncatedString || children} + + + ); + }, +); diff --git a/packages/ui-components/src/components/Button/ButtonTypes.d.ts b/packages/ui-components/src/components/Button/ButtonTypes.d.ts index 91396dfe..8debddcb 100644 --- a/packages/ui-components/src/components/Button/ButtonTypes.d.ts +++ b/packages/ui-components/src/components/Button/ButtonTypes.d.ts @@ -13,3 +13,9 @@ export type ButtonProps = { export type ButtonIconProps = { label?: string; } & ButtonProps; + +export type ButtonLinkProps = { + link?: string; + target?: string; + maxLabelLength?: number; +} & ButtonProps; diff --git a/packages/ui-components/src/components/Button/__tests__/ButtonLink.test.tsx b/packages/ui-components/src/components/Button/__tests__/ButtonLink.test.tsx new file mode 100644 index 00000000..c37199e2 --- /dev/null +++ b/packages/ui-components/src/components/Button/__tests__/ButtonLink.test.tsx @@ -0,0 +1,95 @@ +import { render, screen } from "@testing-library/react"; + +import { ButtonLink } from "../.."; + +describe("ButtonLink (exceptions)", () => { + it("should be able to require/import from root", () => { + expect(ButtonLink).toBeDefined(); + }); +}); + +describe("ButtonLink modifiers", () => { + it("should render a default anchor", async () => { + render(hello); + const button = await screen.findByRole("link"); + expect(button.className).toContain("py-2"); + }); + + it("should render a slim anchor", async () => { + render( + + hello + , + ); + const button = await screen.findByRole("link"); + expect(button.className).toContain("py-1"); + }); + + it("should render a dark link", async () => { + render( + + hello + , + ); + const button = await screen.findByRole("link"); + const buttonClass = button.className; + expect(buttonClass).toContain("text-slate-200"); + expect(buttonClass).toContain("bg-slate-900"); + }); + + it("should render a light anchor", async () => { + render( + + hello + , + ); + const button = await screen.findByRole("link"); + const buttonClass = button.className; + expect(buttonClass).toContain("text-slate-200"); + expect(buttonClass).toContain("bg-slate-500"); + }); + + it("should render a fullWidth link", async () => { + render( + + hello + , + ); + const button = await screen.findByRole("link"); + expect(button.className).toContain("w-full"); + }); + + it("should render an anchor with truncated text", async () => { + render( + + hello world + , + ); + const button = await screen.findByRole("link"); + expect(button.className).toContain("py-2"); + const label = await screen.findByText("hello..."); + expect(label).toBeDefined(); + }); + + it("should render an anchor with full text", async () => { + render( + + hello world + , + ); + const button = await screen.findByRole("link"); + expect(button.className).toContain("py-2"); + const label = await screen.findByText("hello world"); + expect(label).toBeDefined(); + }); + + it("should render an anchor element with a special rel value", async () => { + render( + + Hello World + , + ); + const button = await screen.findByRole("link"); + expect(button).toHaveAttribute("rel", "noopener noreferrer"); + }); +}); diff --git a/packages/ui-components/src/components/Button/button.css b/packages/ui-components/src/components/Button/button.css index 03c774e0..b5c61c95 100644 --- a/packages/ui-components/src/components/Button/button.css +++ b/packages/ui-components/src/components/Button/button.css @@ -1,7 +1,3 @@ @tailwind base; @tailwind components; @tailwind utilities; - -h1.heading { - font-family: "Open Sans"; -} diff --git a/packages/ui-components/src/components/Button/utilities.ts b/packages/ui-components/src/components/Button/utilities.ts index 0e1e77c1..53ff5306 100644 --- a/packages/ui-components/src/components/Button/utilities.ts +++ b/packages/ui-components/src/components/Button/utilities.ts @@ -2,9 +2,10 @@ import clsx from "clsx"; export const TYPE_ICON = "icon"; export const TYPE_BUTTON = "button"; +export const TYPE_LINK = "link"; type getButtonClassesProps = { - type: typeof TYPE_BUTTON | typeof TYPE_ICON; + type: typeof TYPE_BUTTON | typeof TYPE_ICON | typeof TYPE_LINK; className?: string; raw: boolean; kind: string; @@ -24,8 +25,10 @@ export const getButtonClasses = ({ }: getButtonClassesProps) => { return clsx( className, - "text-sm font-medium focus:outline-none focus:ring-2 focus:ring-slate-300 focus:ring-offset-0", + "focus:outline-none focus:ring-2 focus:ring-slate-300 focus:ring-offset-0", { + "text-sm font-medium sm:text-base": type === TYPE_BUTTON, + "text-center text-sm": type === TYPE_LINK, "p-2": type === TYPE_ICON, "rounded-full ": !raw, "rounded-sm ": raw, @@ -36,9 +39,12 @@ export const getButtonClasses = ({ kind === "light" && !disabled && !raw, "bg-slate-500 text-slate-200": kind === "light" && disabled && !raw, "w-full": fullWidth, - "px-4 py-1": slim && !raw && type === TYPE_BUTTON, - "px-4 py-2": !slim && !raw && type === TYPE_BUTTON, + "px-0 py-1 sm:px-4": + slim && !raw && (type === TYPE_BUTTON || type === TYPE_LINK), + "px-4 py-2": + !slim && !raw && (type === TYPE_BUTTON || type === TYPE_LINK), "disabled:cursor-not-allowed disabled:opacity-50": disabled, + "max-h-8": type === TYPE_LINK, }, ); }; diff --git a/packages/ui-components/src/components/Footer/Footer.tsx b/packages/ui-components/src/components/Footer/Footer.tsx index 5bf89917..8a84820d 100644 --- a/packages/ui-components/src/components/Footer/Footer.tsx +++ b/packages/ui-components/src/components/Footer/Footer.tsx @@ -1,19 +1,19 @@ import "./footer.css"; -import { APP_NAME, APP_OWNER } from "../../common/strings"; -import { isDev } from "../../common/utilities"; +import clsx from "clsx"; -export const Footer = () => { - const buildClass = isDev ? "text-slate-900" : "text-slate-300"; - return ( -