@unitedstatespowersquadrons/components 1.0.3 → 1.0.5

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,86 @@
1
+ import React from "react";
2
+
3
+ type ExactJobcodeRank = "CDR" | "LTC" | "LT" | "DC" | "DLTC" | "DLT" | "CC";
4
+ type PatternJobcodeRank = "STFC" | "RC" | "VC";
5
+ type FlagRank =
6
+ | ExactJobcodeRank
7
+ | PatternJobcodeRank
8
+ | "PLTC" | "PC" | "FLT" | "1LT"
9
+ | "PDLTC" | "PDC" | "DAIDE" | "DFLT" | "D1LT"
10
+ | "PNFLT" | "PSTFC" | "PRC" | "PVC" | "PCC" | "NAIDE" | "NFLT";
11
+
12
+ interface Props {
13
+ className?: string;
14
+ flag?: string; // FlagRank | jobcode
15
+ format?: "svg" | "png";
16
+ jobcode?: string;
17
+ insignia?: "flag" | "gold" | "silver";
18
+ rank?: FlagRank;
19
+ size?: number | "thumb";
20
+ }
21
+
22
+ const OfficerFlag = (props: Props) => {
23
+ const { className, flag, format, jobcode, insignia, rank, size } = props;
24
+
25
+ if (!flag && !jobcode && !rank) {
26
+ throw new TypeError("Missing required prop: one of `flag`, `jobcode`, or `rank` must be provided.");
27
+ }
28
+
29
+ const flagBaseUrl = "https://flags.aws.usps.org";
30
+
31
+ const rankFromJobcode = () => {
32
+ if (!jobcode && !flag) return "";
33
+
34
+ const search = (flag || jobcode)!;
35
+
36
+ /* eslint-disable sort-keys */
37
+ const ranks: { [key: string]: ExactJobcodeRank } = {
38
+ 31000: "CDR",
39
+ 32000: "LTC",
40
+ 31010: "LT",
41
+ 21000: "DC",
42
+ 22000: "DLTC",
43
+ 21010: "DLT",
44
+ 11000: "CC",
45
+ };
46
+ const rankPatterns = {
47
+ VC: /1\d000/,
48
+ RC: /1\d{3}0/,
49
+ STFC: /1\d{3}1/,
50
+ };
51
+ /* eslint-enable sort-keys */
52
+
53
+ const matchRankPattern = () => {
54
+ for (const [r, pattern] of Object.entries(rankPatterns)) {
55
+ const regexp = new RegExp(pattern);
56
+
57
+ if (search.match(regexp)) return r;
58
+ }
59
+
60
+ return "";
61
+ };
62
+
63
+ return (search in ranks) ? ranks[search] : matchRankPattern();
64
+ };
65
+
66
+ const displayRank = rank || rankFromJobcode() || flag;
67
+ const formatDir = format ? format.toUpperCase() : "PNG";
68
+ const extension = format ? format.toLowerCase() : "png";
69
+ const sizeExtension = extension === "png" && size ? `.${size}` : "";
70
+
71
+ const subDirectory = () => {
72
+ if (insignia === "flag") {
73
+ return "insignia/";
74
+ } else if (insignia) {
75
+ return `insignia/${insignia}/`;
76
+ } else {
77
+ return "";
78
+ }
79
+ };
80
+
81
+ const combinedPath = `${flagBaseUrl}/${formatDir}/${subDirectory()}${displayRank}${sizeExtension}.${extension}`;
82
+
83
+ return <img className={className} src={combinedPath} alt={displayRank} />;
84
+ };
85
+
86
+ export default OfficerFlag;
@@ -0,0 +1,171 @@
1
+ # Flag Components
2
+
3
+ ## Importing
4
+
5
+ You can import individual components, or the entire namespace:
6
+
7
+ ```tsx
8
+ import { OfficerFlag } from "../Flags";
9
+
10
+ const NewComponent = () => {
11
+ return (
12
+ <OfficerFlag {...flagProps} />
13
+ );
14
+ };
15
+ ```
16
+
17
+ ```tsx
18
+ import Flags from "../Flags";
19
+
20
+ const NewComponent = () => {
21
+ return (
22
+ <Flags.Officer {...flagProps} />
23
+ );
24
+ };
25
+ ```
26
+
27
+ ## Officer Flags
28
+
29
+ All props are individually optional, however one of `flag`, `jobcode`, or `rank` must be provided.
30
+
31
+ - `flag`: generic input for either of the two other options
32
+ - `jobcode`: a jobcode, to be converted into the corresponding rank
33
+ - `rank`: a specific rank
34
+ - exclude all slashes, and `st` for e.g. 1st/Lt
35
+ - See `FlagRank` from [Officer.tsx](Officer.tsx) for the complete list of supported ranks
36
+
37
+ ```tsx
38
+ // All of these are equivalent
39
+ <Flags.Officer flag="CC" />
40
+ <Flags.Officer flag="11000" />
41
+ <Flags.Officer rank="CC" />
42
+ <Flags.Officer jobcode="11000" />
43
+ ```
44
+
45
+ ### Supported Ranks from Jobcodes
46
+
47
+ | Rank | Jobcode | Details |
48
+ |------|---------|-------------------|
49
+ | CDR | 31000 | |
50
+ | LTC | 32000 | |
51
+ | LT | 31010 | Merit Marks Chair |
52
+ | DC | 21000 | |
53
+ | DLTC | 22000 | |
54
+ | DLT | 21010 | Merit Marks Chair |
55
+ | CC | 11000 | |
56
+ | VC | 1*000 | |
57
+ | RC | 1***0 | |
58
+ | STFC | 1***1 | |
59
+
60
+ ### Format
61
+
62
+ Officer flags can be used in either PNG or SVG format.
63
+
64
+ ### Sizes
65
+
66
+ Officer flags support the following sizes for PNG format:
67
+
68
+ - `undefined` -> full size (3072x2048)
69
+ - `1500`
70
+ - `1000`
71
+ - `500`
72
+ - `"thumb"`
73
+
74
+ Sizes refer to the width of the resulting image.
75
+
76
+ ### Insignia Mode
77
+
78
+ Instead of displaying officer flags, the corresponding insignia can be displayed with the prop `insignia`.
79
+
80
+ Insignia support three colors:
81
+
82
+ - `"flag"` -> flag color
83
+ - inverted with background color if background is blue/red
84
+ - standard flag insignia color if background is white
85
+ - `"gold"`
86
+ - `"silver"`
87
+
88
+ Some insignia are narrow enough that they do not support all size options. In such cases, size `"thumb"` is
89
+ usually sufficient.
90
+
91
+ ## Signal Flags
92
+
93
+ You can import either the individual signal flag component or the automatic text group converter component.
94
+
95
+ ### Special Characters
96
+
97
+ Because individual character parsing of text cannot render all possible signal flags, there are four custom
98
+ Unicode codepoints that have special handling when using single-character input (which is always used by
99
+ the automatic text group component):
100
+
101
+ - `\uE000` -> `"code"`
102
+ - `\uE001` -> `"repeat_1"`
103
+ - `\uE002` -> `"repeat_2"`
104
+ - `\uE003` -> `"repeat_3"`
105
+
106
+ ### Individual Signal
107
+
108
+ ```tsx
109
+ import { SignalFlag } from "../Flags";
110
+
111
+ const NewComponent = () => {
112
+ return (
113
+ <SignalFlag flag="alpha" />
114
+ );
115
+ };
116
+ ```
117
+
118
+ ```html
119
+ <img alt="alpha" src="https://flags.aws.usps.org/signals/PNG/letters/alpha.png">
120
+ ```
121
+
122
+ ### Automatic Words
123
+
124
+ ```tsx
125
+ import { SignalFlags } from "../Flags";
126
+
127
+ const NewComponent = () => {
128
+ return (
129
+ <SignalFlags text="United States Power Squadrons" />
130
+ );
131
+ };
132
+ ```
133
+
134
+ ```html
135
+ <div title="United States Power Squadrons">
136
+ <div>
137
+ <img alt="U" src="https://flags.aws.usps.org/signals/PNG/short/u.png">
138
+ <img alt="n" src="https://flags.aws.usps.org/signals/PNG/short/n.png">
139
+ <img alt="i" src="https://flags.aws.usps.org/signals/PNG/short/i.png">
140
+ <img alt="t" src="https://flags.aws.usps.org/signals/PNG/short/t.png">
141
+ <img alt="e" src="https://flags.aws.usps.org/signals/PNG/short/e.png">
142
+ <img alt="d" src="https://flags.aws.usps.org/signals/PNG/short/d.png">
143
+ </div>
144
+ <div>
145
+ <img alt="S" src="https://flags.aws.usps.org/signals/PNG/short/s.png">
146
+ <img alt="t" src="https://flags.aws.usps.org/signals/PNG/short/t.png">
147
+ <img alt="a" src="https://flags.aws.usps.org/signals/PNG/short/a.png">
148
+ <img alt="t" src="https://flags.aws.usps.org/signals/PNG/short/t.png">
149
+ <img alt="e" src="https://flags.aws.usps.org/signals/PNG/short/e.png">
150
+ <img alt="s" src="https://flags.aws.usps.org/signals/PNG/short/s.png">
151
+ </div>
152
+ <div>
153
+ <img alt="P" src="https://flags.aws.usps.org/signals/PNG/short/p.png">
154
+ <img alt="o" src="https://flags.aws.usps.org/signals/PNG/short/o.png">
155
+ <img alt="w" src="https://flags.aws.usps.org/signals/PNG/short/w.png">
156
+ <img alt="e" src="https://flags.aws.usps.org/signals/PNG/short/e.png">
157
+ <img alt="r" src="https://flags.aws.usps.org/signals/PNG/short/r.png">
158
+ </div>
159
+ <div>
160
+ <img alt="S" src="https://flags.aws.usps.org/signals/PNG/short/s.png">
161
+ <img alt="q" src="https://flags.aws.usps.org/signals/PNG/short/q.png">
162
+ <img alt="u" src="https://flags.aws.usps.org/signals/PNG/short/u.png">
163
+ <img alt="a" src="https://flags.aws.usps.org/signals/PNG/short/a.png">
164
+ <img alt="d" src="https://flags.aws.usps.org/signals/PNG/short/d.png">
165
+ <img alt="r" src="https://flags.aws.usps.org/signals/PNG/short/r.png">
166
+ <img alt="o" src="https://flags.aws.usps.org/signals/PNG/short/o.png">
167
+ <img alt="n" src="https://flags.aws.usps.org/signals/PNG/short/n.png">
168
+ <img alt="s" src="https://flags.aws.usps.org/signals/PNG/short/s.png">
169
+ </div>
170
+ </div>
171
+ ```
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+
3
+ interface Props {
4
+ alt?: string;
5
+ className?: string;
6
+ flag: string;
7
+ format?: "svg" | "png" | undefined;
8
+ }
9
+
10
+ const SignalFlag = (props: Props) => {
11
+ const { alt, className, flag, format } = props;
12
+
13
+ const special = ["\uE000", "\uE001", "\uE002", "\uE003"];
14
+ const codes = ["code", "repeat_1", "repeat_2", "repeat_3"];
15
+
16
+ /* eslint-disable stylistic/array-element-newline */
17
+ const letters = [
18
+ "alpha",
19
+ "alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf",
20
+ "hotel", "india", "juliet", "kilo", "lima", "mike", "november",
21
+ "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform",
22
+ "victor", "whiskey", "xray", "yankee", "zulu",
23
+ ];
24
+ const numbers = [
25
+ "one", "two", "three", "four", "five",
26
+ "six", "seven", "eight", "nine", "zero",
27
+ ];
28
+ /* eslint-enable stylistic/array-element-newline */
29
+
30
+ const dir = () => {
31
+ if (special.includes(flag) || codes.includes(flag)) return "codes";
32
+ if (letters.includes(flag)) return "letters";
33
+ if (numbers.includes(flag)) return "numbers";
34
+
35
+ return "short";
36
+ };
37
+
38
+ const flagValue = () => {
39
+ if (special.includes(flag)) {
40
+ const index = special.indexOf(flag);
41
+ return codes[index];
42
+ } else {
43
+ return flag.toLowerCase();
44
+ }
45
+ };
46
+
47
+ const formatDir = format ? format.toUpperCase() : "PNG";
48
+ const extension = format ? format.toLowerCase() : "png";
49
+
50
+ const signalBaseUrl = "https://flags.aws.usps.org/signals";
51
+
52
+ const combinedUrl = `${signalBaseUrl}/${formatDir}/${dir()}/${flagValue()}.${extension}`;
53
+
54
+ return (
55
+ <img className={className} src={combinedUrl} alt={alt || flag} />
56
+ );
57
+ };
58
+
59
+ export default SignalFlag;
@@ -0,0 +1,103 @@
1
+ import React from "react";
2
+ import { createUseStyles } from "react-jss";
3
+ import classNames from "classnames";
4
+ import SignalFlag from "./Signal";
5
+
6
+ type Orientation = "center" | "centerVertical" | "vertical";
7
+
8
+ type Size =
9
+ | "double"
10
+ | "triple"
11
+ | "quadruple"
12
+ | "half"
13
+ | "tall"
14
+ | "vertical"
15
+ | "center";
16
+
17
+ interface Props {
18
+ className?: string;
19
+ format?: "svg" | "png";
20
+ orientation?: Orientation;
21
+ size?: Size;
22
+ text: string;
23
+ title?: string;
24
+ }
25
+
26
+ const useStyles = createUseStyles({
27
+ center: { alignItems: "center" },
28
+ centerVertical: {
29
+ "& div": { flexDirection: "column" },
30
+ alignItems: "center",
31
+ flexDirection: "column",
32
+ },
33
+ double: {
34
+ "& div": { gap: "7px" },
35
+ "& img": { width: "40px" },
36
+ },
37
+ half: {
38
+ "& div": { gap: "3px" },
39
+ "& img": { width: "10px" },
40
+ },
41
+ quadruple: {
42
+ "& div": { gap: "11px" },
43
+ "& img": { width: "80px" },
44
+ },
45
+ signals: {
46
+ "& img": { width: "20px" },
47
+ display: "flex",
48
+ gap: "20px",
49
+ justifyContent: "center",
50
+ width: "100%",
51
+ },
52
+ tall: {
53
+ "& div": { gap: "10px" },
54
+ "& img": {
55
+ height: "50px",
56
+ width: "auto",
57
+ },
58
+ },
59
+ triple: {
60
+ "& div": { gap: "9px" },
61
+ "& img": { width: "60px" },
62
+ },
63
+ vertical: {
64
+ "& div": { flexDirection: "column" },
65
+ flexDirection: "column",
66
+ },
67
+ word: {
68
+ display: "flex",
69
+ gap: "5px",
70
+ },
71
+ });
72
+
73
+ const SignalFlags = (props: Props) => {
74
+ const { className, format, orientation, size, text, title } = props;
75
+ const classes = useStyles();
76
+
77
+ const regexp = /[a-z0-9\s\uE000\uE001\uE002\uE003]/ig;
78
+ const matches = text.match(regexp);
79
+
80
+ if (!matches) return "";
81
+
82
+ const words: string[][] = [[]];
83
+
84
+ const specialClassNames = () => {
85
+ const selectedOrientation = orientation ? classes[orientation] : "";
86
+ const selectedSize = size ? classes[size] : "";
87
+ return [selectedOrientation, selectedSize];
88
+ };
89
+
90
+ // Build words array by splitting on spaces
91
+ matches.forEach(match => match === " " ? words.push([]) : words.at(-1)!.push(match));
92
+
93
+ return (
94
+ <div className={classNames(className, classes.signals, ...specialClassNames())} title={title || text}>
95
+ {words.map((word, wordIndex) => (
96
+ <div className={classes.word} key={wordIndex}>{word.map((flag, flagIndex) =>
97
+ <SignalFlag flag={flag} format={format} key={flagIndex} />
98
+ )}</div>))}
99
+ </div>
100
+ );
101
+ };
102
+
103
+ export default SignalFlags;
package/Flags/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import Officer from "./Officer";
2
+ import Signal from "./Signal";
3
+ import Signals from "./Signals";
4
+
5
+ export { default as OfficerFlag } from "./Officer";
6
+ export { default as SignalFlag } from "./Signal";
7
+ export { default as SignalFlags } from "./Signals";
8
+
9
+ export default { Officer, Signal, Signals };
package/index.tsx CHANGED
@@ -1,30 +1,24 @@
1
1
  export { default as FontAwesomeIcon } from "./FontAwesomeIcon";
2
- export { default as railsFetchJson, getCsrfTokenHeader } from "./railsFetchJson";
3
- export { default as Striped, StripedTable, StripedHead, StripedBody } from "./StripedTable";
2
+
3
+ export { default as railsFetchJson } from "./railsFetchJson";
4
+ export * from "./railsFetchJson";
5
+
6
+ export { default as Striped } from "./StripedTable";
7
+ export * from "./StripedTable";
8
+
9
+ export { default as Flags } from "./Flags";
10
+ export * from "./Flags";
11
+
4
12
  export { default as ActionButton } from "./ActionButton";
5
13
  export { default as IconButton } from "./IconButton";
6
14
  export { default as SelectionIconButton } from "./SelectionIconButton";
7
- export { Toasts, createDismissToast, Toast_Status } from "./Toasts";
15
+ export * from "./Toasts";
8
16
  export { default as Colors } from "./Colors";
9
17
  export { default as Styles } from "./Styles";
10
18
 
11
- export type {
12
- AppStateWithToasts,
13
- DispatchFunc,
14
- DispatchHandler,
15
- OnClickHandler,
16
- OnClickOrKeyboardHandler,
17
- OnInputChangeHandler,
18
- RailsResponse,
19
- } from "./types";
20
- export type { ButtonColorName } from "./Colors";
21
- export type { FontAwesomeProps } from "./FontAwesomeIcon";
22
- export type {
23
- ActionButtonProps,
24
- ActionButtonHrefProps,
25
- ActionButtonMethodProps,
26
- ActionButtonOnClickProps,
27
- CoreActionButtonProps,
28
- } from "./ActionButton";
29
- export type { IconButtonProps, IconButtonHrefProps, IconButtonOnClickProps } from "./IconButton";
30
- export type { Toast, ToastAppAction, ToastProps } from "./Toasts";
19
+ export type * from "./types";
20
+ export type * from "./Colors";
21
+ export type * from "./FontAwesomeIcon";
22
+ export type * from "./ActionButton";
23
+ export type * from "./IconButton";
24
+ export type * from "./Toasts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unitedstatespowersquadrons/components",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "USPS shared React components library",
5
5
  "main": "index.tsx",
6
6
  "scripts": {
package/tsconfig.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "noUnusedParameters": true,
19
19
  "strict": true,
20
20
  "skipLibCheck": true,
21
- "target": "ES2019",
21
+ "target": "ES2022",
22
22
  },
23
23
  "exclude": [
24
24
  "app/javascript/mount.tsx",