@unitedstatespowersquadrons/components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ name: 'Components Library'
2
+ on:
3
+ push:
4
+ paths:
5
+ - '**.ts'
6
+ - '**.tsx'
7
+ - 'eslint.config.mjs'
8
+ - '**.yml'
9
+ - '**.json'
10
+ branches:
11
+ - 'main'
12
+ - '**'
13
+ workflow_dispatch:
14
+
15
+ jobs:
16
+ eslint:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - name: Checkout Code
20
+ uses: actions/checkout@v4
21
+ - name: Install Packages
22
+ run: yarn
23
+ - name: Run Eslint
24
+ run: yarn run eslint
25
+ tsc:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - name: Checkout Code
29
+ uses: actions/checkout@v4
30
+ - name: Install Packages
31
+ run: yarn
32
+ - name: Run TSC
33
+ run: yarn run tsc
@@ -0,0 +1,126 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+ import { ButtonColorName } from "./Colors";
5
+ import FontAwesomeIcon, { FontAwesomeProps } from "./FontAwesomeIcon";
6
+ import Styles from "./Styles";
7
+ import BaseActionButton from "./BaseActionButton";
8
+ import { HrefProps, MethodProps, OnClickProps } from "./types";
9
+
10
+ const useStyles = createUseStyles({
11
+ big: { ...Styles.bigText },
12
+ blueColored: { ...Styles.colorizeButton("blue") },
13
+ blueGray: { ...Styles.blue },
14
+ button: { ...Styles.button },
15
+ grayColored: { ...Styles.colorizeButton("gray") },
16
+ grayGray: { ...Styles.gray },
17
+ greenColored: { ...Styles.colorizeButton("green") },
18
+ greenGray: { ...Styles.green },
19
+ noMargin: { marginTop: "0" },
20
+ orangeColored: { ...Styles.colorizeButton("orange") },
21
+ orangeGray: { ...Styles.orange },
22
+ purpleColored: { ...Styles.colorizeButton("purple") },
23
+ purpleGray: { ...Styles.purple },
24
+ redColored: { ...Styles.colorizeButton("red") },
25
+ redGray: { ...Styles.red },
26
+ small: { fontSize: "0.8em" },
27
+ subText: {
28
+ fontSize: "0.75em",
29
+ marginLeft: "0.75em",
30
+ },
31
+ });
32
+
33
+ export interface CoreActionButtonProps extends FontAwesomeProps {
34
+ className?: string | undefined;
35
+ color: ButtonColorName;
36
+ grayMode?: boolean | undefined;
37
+ id?: string;
38
+ size?: "big" | "mediumWithMargin" | "medium" | "small" | undefined;
39
+ subtext?: string;
40
+ text: string;
41
+ title?: string | undefined;
42
+ }
43
+
44
+ export interface ActionButtonHrefProps extends CoreActionButtonProps, HrefProps {}
45
+ export interface ActionButtonMethodProps extends CoreActionButtonProps, MethodProps {}
46
+ export interface ActionButtonOnClickProps extends CoreActionButtonProps, OnClickProps {}
47
+
48
+ export type ActionButtonProps =
49
+ | ActionButtonHrefProps
50
+ | ActionButtonMethodProps
51
+ | ActionButtonOnClickProps;
52
+
53
+ const ActionButton = (props: ActionButtonProps) => {
54
+ const { className, color, fa, grayMode, icon, id, mode, size, style, subtext, text, title } = props;
55
+ const classes = useStyles();
56
+
57
+ const colorClassName = classes[`${color}${grayMode ? "Gray" : "Colored"}`];
58
+
59
+ const sizeClassName = () => {
60
+ switch (size) {
61
+ case "big": return classes.big;
62
+ case "mediumWithMargin": return "";
63
+ case "medium": return classes.noMargin;
64
+ case "small": return classNames(classes.small, classes.noMargin);
65
+ default: return classes.big;
66
+ }
67
+ };
68
+
69
+ const buttonContents = () => {
70
+ return(
71
+ <>
72
+ <FontAwesomeIcon icon={icon} fa={fa} mode={mode} style={style} />
73
+ <span>{text}</span>
74
+ {subtext && <small className={classes.subText}>({subtext})</small>}
75
+ </>
76
+ );
77
+ };
78
+
79
+ if ("onClick" in props) {
80
+ const { confirm, onClick } = props;
81
+
82
+ return(
83
+ <BaseActionButton
84
+ className={classNames(classes.button, colorClassName, sizeClassName(), className)}
85
+ id={id}
86
+ onClick={onClick}
87
+ title={title}
88
+ confirm={confirm}
89
+ >
90
+ {buttonContents()}
91
+ </BaseActionButton>
92
+ );
93
+ } else if ("method" in props) {
94
+ const { confirm, href, method } = props;
95
+
96
+ return(
97
+ <BaseActionButton
98
+ className={classNames(classes.button, colorClassName, sizeClassName(), className)}
99
+ href={href}
100
+ id={id}
101
+ method={method}
102
+ title={title}
103
+ confirm={confirm}
104
+ >
105
+ {buttonContents()}
106
+ </BaseActionButton>
107
+ );
108
+ } else if ("href" in props) {
109
+ const { href } = props;
110
+
111
+ return(
112
+ <BaseActionButton
113
+ className={classNames(classes.button, colorClassName, sizeClassName(), className)}
114
+ href={href}
115
+ id={id}
116
+ title={title}
117
+ >
118
+ {buttonContents()}
119
+ </BaseActionButton>
120
+ );
121
+ } else {
122
+ throw new Error("Invalid ActionButton configuration: must specify props: onClick | (href & method?)");
123
+ }
124
+ };
125
+
126
+ export default ActionButton;
@@ -0,0 +1,126 @@
1
+ import React, { useRef } from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+ import { HrefProps, MethodProps, OnClickHandler, OnClickProps } from "./types";
5
+ import { getCsrfTokenHeader } from "./railsFetchJson";
6
+
7
+ const useStyles = createUseStyles({
8
+ button: {
9
+ background: "none",
10
+ border: "none",
11
+ padding: "0",
12
+ },
13
+ });
14
+
15
+ interface CoreBaseActionButtonProps {
16
+ children?: React.ReactNode;
17
+ className?: string | undefined;
18
+ id?: string | undefined;
19
+ title?: string | undefined;
20
+ }
21
+
22
+ export interface BaseActionButtonHrefProps extends CoreBaseActionButtonProps, HrefProps {}
23
+ export interface BaseActionButtonMethodProps extends CoreBaseActionButtonProps, MethodProps {}
24
+ export interface BaseActionButtonOnClickProps extends CoreBaseActionButtonProps, OnClickProps {}
25
+
26
+ export type BaseActionButtonProps =
27
+ | BaseActionButtonHrefProps
28
+ | BaseActionButtonMethodProps
29
+ | BaseActionButtonOnClickProps;
30
+
31
+ const BaseActionButton = (props: BaseActionButtonProps) => {
32
+ const { children, className, id, title } = props;
33
+ const classes = useStyles();
34
+
35
+ const formRef = useRef<HTMLFormElement>(null);
36
+
37
+ if ("onClick" in props) {
38
+ const { onClick } = props;
39
+
40
+ const getConfirmation = () => !props.confirm || confirm(props.confirm);
41
+
42
+ const wrappedOnClick: OnClickHandler = event => {
43
+ event.preventDefault();
44
+
45
+ if (getConfirmation()) onClick(event);
46
+ };
47
+
48
+ return(
49
+ <button
50
+ type="button"
51
+ id={id}
52
+ className={classNames("action-button", classes.button, className)}
53
+ onClick={wrappedOnClick}
54
+ title={title}
55
+ >
56
+ {children}
57
+ </button>
58
+ );
59
+ } else if ("method" in props) {
60
+ const { href, method } = props;
61
+
62
+ const getConfirmation = () => !props.confirm || confirm(props.confirm);
63
+
64
+ const form = () => {
65
+ if (!method) return;
66
+
67
+ return(
68
+ <form action={href} method="POST" ref={formRef}>
69
+ <input type="hidden" name="authenticity_token" value={getCsrfTokenHeader()} />
70
+ <input type="hidden" name="_method" value={method.toUpperCase()} />
71
+ </form>
72
+ );
73
+ };
74
+
75
+ const submitForm: OnClickHandler = event => {
76
+ event.preventDefault();
77
+
78
+ if (!getConfirmation()) return;
79
+
80
+ const methodForm = formRef.current;
81
+
82
+ if (methodForm) methodForm.submit();
83
+ };
84
+
85
+ return(
86
+ <>
87
+ {form()}
88
+ <button
89
+ type="button"
90
+ id={id}
91
+ className={classNames("action-button", classes.button, className)}
92
+ onClick={submitForm}
93
+ title={title}
94
+ >
95
+ {children}
96
+ </button>
97
+ </>
98
+ );
99
+ } else if ("href" in props) {
100
+ const { href, newTab } = props;
101
+
102
+ const onClick = () => {
103
+ if (newTab) {
104
+ open(href, "_blank");
105
+ } else {
106
+ location.href = href;
107
+ }
108
+ };
109
+
110
+ return(
111
+ <button
112
+ type="button"
113
+ onClick={onClick}
114
+ id={id}
115
+ className={classNames("action-button", classes.button, className)}
116
+ title={title}
117
+ >
118
+ {children}
119
+ </button>
120
+ );
121
+ } else {
122
+ throw new Error("Invalid BaseActionButton configuration: must specify props: onClick | (href & method?)");
123
+ }
124
+ };
125
+
126
+ export default BaseActionButton;
package/Colors.tsx ADDED
@@ -0,0 +1,124 @@
1
+ const baseColors = {
2
+ blue: "blue", // #0000FF
3
+ green: "darkgreen", // #006400
4
+ orange: "#CC6600",
5
+ purple: "#6600CC",
6
+ red: "darkred", // #8B0000
7
+ };
8
+
9
+ const hoverColors = {
10
+ blue: "#0000CC",
11
+ green: "#003300",
12
+ orange: "#99CC00",
13
+ purple: "#330099",
14
+ red: "#660000",
15
+ };
16
+
17
+ const shades = {
18
+ _0: "black",
19
+ _1: "#111111",
20
+ _2: "#222222",
21
+ _3: "#333333",
22
+ _4: "#444444",
23
+ _5: "#555555",
24
+ _6: "#666666",
25
+ _7: "#777777",
26
+ _8: "#888888",
27
+ _9: "#999999",
28
+ _a: "#AAAAAA",
29
+ _b: "#BBBBBB",
30
+ _c: "#CCCCCC",
31
+ _d: "#DDDDDD",
32
+ _e: "#EEEEEE",
33
+ _f: "#FFFFFF",
34
+ };
35
+
36
+ type HexChar =
37
+ | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
38
+ | "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f";
39
+ const shade = (value: number | `${HexChar}${HexChar}`) => {
40
+ const validateNumber = () => {
41
+ if (typeof value !== "number") { return; }
42
+ if (value < 0 || value > 255) { throw new Error(`Shade out of bounds: ${value}`); }
43
+ };
44
+
45
+ const validateString = () => {
46
+ if (typeof value !== "string") { return; }
47
+ if (value.length !== 2) { throw new Error(`Shade too long: "${value}"`); }
48
+ if (/[^0-9a-fA-F]/.exec(value)) { throw new Error(`Shade out of bounds: "${value}"`); }
49
+ };
50
+
51
+ validateNumber();
52
+ validateString();
53
+
54
+ const hex = value.toString(16).padStart(2, "0");
55
+
56
+ return `#${hex.repeat(3)}`;
57
+ };
58
+
59
+ /* eslint-disable sort-keys */
60
+ const shadesLegacy = {
61
+ black: shades._0,
62
+ darkGray: shades._3,
63
+ mediumGray: shades._5,
64
+ gray: shades._6,
65
+ lightGray: shades._9,
66
+ veryLightGray: shades._c,
67
+ paleGray: shades._e,
68
+ };
69
+ /* eslint-enable sort-keys */
70
+
71
+ export type ButtonColorName =
72
+ | "blue"
73
+ | "gray"
74
+ | "green"
75
+ | "orange"
76
+ | "purple"
77
+ | "red";
78
+
79
+ const specificColors = {
80
+ answer: {
81
+ highlighted: "#EEFFFF",
82
+ selected: "#99FFFF",
83
+ },
84
+ button: {
85
+ background: {
86
+ hover: {
87
+ blue: "#5588EE",
88
+ gray: "#BBBBBB",
89
+ green: "#55BB55",
90
+ orange: "#EE8833",
91
+ purple: "#8844EE",
92
+ red: "#CC6666",
93
+ },
94
+ normal: {
95
+ blue: "#6699FF",
96
+ gray: shadesLegacy.veryLightGray,
97
+ green: "#66CC66",
98
+ orange: "#FF9944",
99
+ purple: "#9955FF",
100
+ red: "#DD7777",
101
+ },
102
+ },
103
+ border: "gray",
104
+ shadow: "#AAAAAA",
105
+ },
106
+ exam: {
107
+ dragging: "#00CCCC",
108
+ instructions: {
109
+ other: "#FFEEEE",
110
+ selected: "#EEEEFF",
111
+ },
112
+ },
113
+ };
114
+
115
+ const Colors = {
116
+ ...baseColors,
117
+ hover: { ...hoverColors },
118
+ shade,
119
+ shades,
120
+ ...shadesLegacy,
121
+ ...specificColors,
122
+ };
123
+
124
+ export default Colors;
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+
5
+ export interface FontAwesomeProps {
6
+ css?: string | undefined;
7
+ fa?: string | undefined;
8
+ icon: string;
9
+ mode?: "classic" | "duotone" | "sharp" | "sharp-duotone" | undefined;
10
+ style?: "solid" | "regular" | "light" | "thin" | "brands" | "kit" | undefined;
11
+ }
12
+
13
+ const useStyles = createUseStyles({
14
+ icon: {
15
+ padding: "0.25em 0.5em",
16
+ },
17
+ });
18
+
19
+ const FontAwesomeIcon = (props: FontAwesomeProps) => {
20
+ const { css, fa, icon, mode, style } = props;
21
+ const classes = useStyles();
22
+ const faClasses = fa?.split(" ").map(c => `fa-${c}`) || [];
23
+
24
+ return (
25
+ <i
26
+ className={classNames(
27
+ classes.icon, `fa-${style || "solid"}`, `fa-${mode || "classic"}`, `fa-${icon}`, css, ...faClasses
28
+ )}
29
+ />
30
+ );
31
+ };
32
+
33
+ export default FontAwesomeIcon;
package/IconButton.tsx ADDED
@@ -0,0 +1,87 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+ import FontAwesomeIcon, { FontAwesomeProps } from "./FontAwesomeIcon";
5
+ import BaseActionButton from "./BaseActionButton";
6
+ import { HrefProps, MethodProps, OnClickProps } from "./types";
7
+ import Styles from "./Styles";
8
+
9
+ const useStyles = createUseStyles({
10
+ icon: {
11
+ ...Styles.blue,
12
+ cursor: "pointer",
13
+ fontSize: "1.1em",
14
+ },
15
+ });
16
+
17
+ interface CoreIconProps extends FontAwesomeProps {
18
+ className?: string;
19
+ id?: string | undefined;
20
+ title: string;
21
+ }
22
+
23
+ export interface IconButtonHrefProps extends CoreIconProps, HrefProps {}
24
+ export interface IconButtonMethodProps extends CoreIconProps, MethodProps {}
25
+ export interface IconButtonOnClickProps extends CoreIconProps, OnClickProps {}
26
+
27
+ export type IconButtonProps =
28
+ | IconButtonHrefProps
29
+ | IconButtonMethodProps
30
+ | IconButtonOnClickProps;
31
+
32
+ const IconButton = (props: IconButtonProps) => {
33
+ const { className, fa, icon, id, mode, style, title } = props;
34
+ const classes = useStyles();
35
+
36
+ const configuredIcon = <FontAwesomeIcon icon={icon} fa={fa} mode={mode} style={style} css={className} />;
37
+
38
+ const combinedClassName = classNames(classes.icon, className);
39
+
40
+ if ("onClick" in props) {
41
+ const { confirm, onClick } = props;
42
+
43
+ return(
44
+ <BaseActionButton
45
+ className={combinedClassName}
46
+ id={id}
47
+ onClick={onClick}
48
+ title={title}
49
+ confirm={confirm}
50
+ >
51
+ {configuredIcon}
52
+ </BaseActionButton>
53
+ );
54
+ } else if ("method" in props) {
55
+ const { confirm, href, method } = props;
56
+
57
+ return(
58
+ <BaseActionButton
59
+ className={combinedClassName}
60
+ href={href}
61
+ id={id}
62
+ method={method}
63
+ title={title}
64
+ confirm={confirm}
65
+ >
66
+ {configuredIcon}
67
+ </BaseActionButton>
68
+ );
69
+ } else if ("href" in props) {
70
+ const { href } = props;
71
+
72
+ return(
73
+ <BaseActionButton
74
+ className={combinedClassName}
75
+ href={href}
76
+ id={id}
77
+ title={title}
78
+ >
79
+ {configuredIcon}
80
+ </BaseActionButton>
81
+ );
82
+ } else {
83
+ throw new Error("Invalid IconButton configuration: must specify props: onClick | (href & method?)");
84
+ }
85
+ };
86
+
87
+ export default IconButton;
package/Readme.md ADDED
@@ -0,0 +1,25 @@
1
+ # USPS React Components Library
2
+
3
+ [![Testing Status](https://github.com/unitedstatespowersquadrons/components/actions/workflows/main.yml/badge.svg)](https://github.com/unitedstatespowersquadrons/components/actions)
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import * from "@unitedstatespowersquadrons/components";
9
+ ```
10
+
11
+ ## Developer Setup
12
+
13
+ Install dependencies with
14
+
15
+ ```sh
16
+ yarn
17
+ ```
18
+
19
+ ## Toasts
20
+
21
+ More detailed information is available for [Toasts](Toasts/Readme.md) usage.
22
+
23
+ ## Automated Testing
24
+
25
+ GitHub Actions will automatically run `eslint` and `tsc` on branch pushes.
@@ -0,0 +1,35 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+ import IconButton, { IconButtonOnClickProps } from "./IconButton";
5
+
6
+ const useStyles = createUseStyles({
7
+ icon: { cursor: "pointer" },
8
+ });
9
+
10
+ interface Props extends Omit<IconButtonOnClickProps, "icon"> {
11
+ id?: string;
12
+ selected: boolean;
13
+ }
14
+
15
+ const SelectionIconButton = (props: Props) => {
16
+ const { id, onClick, selected } = props;
17
+ const classes = useStyles();
18
+
19
+ const title = selected ? "Remove Selection" : "Add Selection";
20
+ const icon = selected ? "square-minus" : "square-plus";
21
+ const className = selected ? "red" : "green";
22
+
23
+ return(
24
+ <IconButton
25
+ icon={icon}
26
+ id={id}
27
+ onClick={onClick}
28
+ title={title}
29
+ className={classNames(classes.icon, className)}
30
+ mode="duotone"
31
+ />
32
+ );
33
+ };
34
+
35
+ export default SelectionIconButton;
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import TableStyles from "./TableStyles";
4
+
5
+ const useStyles = createUseStyles({
6
+ tbody: TableStyles.tbody,
7
+ });
8
+
9
+ interface Props {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ const Body = (props: Props) => {
14
+ const { children } = props;
15
+ const classes = useStyles();
16
+
17
+ return(
18
+ <tbody className={classes.tbody}>{children}</tbody>
19
+ );
20
+ };
21
+
22
+ export default Body;
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import TableStyles from "./TableStyles";
4
+
5
+ const useStyles = createUseStyles({
6
+ tfoot: TableStyles.tfoot,
7
+ });
8
+
9
+ interface Props {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ const Head = (props: Props) => {
14
+ const { children } = props;
15
+ const classes = useStyles();
16
+
17
+ return(
18
+ <tfoot className={classes.tfoot}>{children}</tfoot>
19
+ );
20
+ };
21
+
22
+ export default Head;
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import TableStyles from "./TableStyles";
4
+
5
+ const useStyles = createUseStyles({
6
+ thead: TableStyles.thead,
7
+ });
8
+
9
+ interface Props {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ const Head = (props: Props) => {
14
+ const { children } = props;
15
+ const classes = useStyles();
16
+
17
+ return(
18
+ <thead className={classes.thead}>{children}</thead>
19
+ );
20
+ };
21
+
22
+ export default Head;
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+ import TableStyles from "./TableStyles";
5
+
6
+ const useStyles = createUseStyles({
7
+ table: TableStyles.table,
8
+ });
9
+
10
+ interface Props {
11
+ children: React.ReactNode;
12
+ className?: string;
13
+ id?: string;
14
+ }
15
+
16
+ const Table = (props: Props) => {
17
+ const { children, className, id } = props;
18
+ const classes = useStyles();
19
+
20
+ return(
21
+ <table id={id} className={classNames(className, classes.table)}>
22
+ {children}
23
+ </table>
24
+ );
25
+ };
26
+
27
+ export default Table;