@kwiz/fluentui 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/_modules/build.d.ts +2 -0
  4. package/dist/_modules/build.js +3 -0
  5. package/dist/_modules/build.js.map +1 -0
  6. package/dist/_modules/config.d.ts +1 -0
  7. package/dist/_modules/config.js +9 -0
  8. package/dist/_modules/config.js.map +1 -0
  9. package/dist/_modules/constants.d.ts +2 -0
  10. package/dist/_modules/constants.js +3 -0
  11. package/dist/_modules/constants.js.map +1 -0
  12. package/dist/_modules/exports-index.d.ts +1 -0
  13. package/dist/_modules/exports-index.js +2 -0
  14. package/dist/_modules/exports-index.js.map +1 -0
  15. package/dist/controls/button.d.ts +27 -0
  16. package/dist/controls/button.js +100 -0
  17. package/dist/controls/button.js.map +1 -0
  18. package/dist/controls/centered.d.ts +5 -0
  19. package/dist/controls/centered.js +14 -0
  20. package/dist/controls/centered.js.map +1 -0
  21. package/dist/controls/dropdown.d.ts +18 -0
  22. package/dist/controls/dropdown.js +13 -0
  23. package/dist/controls/dropdown.js.map +1 -0
  24. package/dist/controls/error-boundary.d.ts +23 -0
  25. package/dist/controls/error-boundary.js +33 -0
  26. package/dist/controls/error-boundary.js.map +1 -0
  27. package/dist/controls/exports-index.d.ts +17 -0
  28. package/dist/controls/exports-index.js +18 -0
  29. package/dist/controls/exports-index.js.map +1 -0
  30. package/dist/controls/field-editor.d.ts +13 -0
  31. package/dist/controls/field-editor.js +15 -0
  32. package/dist/controls/field-editor.js.map +1 -0
  33. package/dist/controls/file-upload.d.ts +18 -0
  34. package/dist/controls/file-upload.js +41 -0
  35. package/dist/controls/file-upload.js.map +1 -0
  36. package/dist/controls/horizontal.d.ts +8 -0
  37. package/dist/controls/horizontal.js +23 -0
  38. package/dist/controls/horizontal.js.map +1 -0
  39. package/dist/controls/input.d.ts +13 -0
  40. package/dist/controls/input.js +41 -0
  41. package/dist/controls/input.js.map +1 -0
  42. package/dist/controls/list.d.ts +21 -0
  43. package/dist/controls/list.js +72 -0
  44. package/dist/controls/list.js.map +1 -0
  45. package/dist/controls/loading.d.ts +5 -0
  46. package/dist/controls/loading.js +7 -0
  47. package/dist/controls/loading.js.map +1 -0
  48. package/dist/controls/please-wait.d.ts +17 -0
  49. package/dist/controls/please-wait.js +16 -0
  50. package/dist/controls/please-wait.js.map +1 -0
  51. package/dist/controls/prompt.d.ts +16 -0
  52. package/dist/controls/prompt.js +21 -0
  53. package/dist/controls/prompt.js.map +1 -0
  54. package/dist/controls/search.d.ts +13 -0
  55. package/dist/controls/search.js +47 -0
  56. package/dist/controls/search.js.map +1 -0
  57. package/dist/controls/section.d.ts +14 -0
  58. package/dist/controls/section.js +27 -0
  59. package/dist/controls/section.js.map +1 -0
  60. package/dist/controls/svg.d.ts +23 -0
  61. package/dist/controls/svg.js +45 -0
  62. package/dist/controls/svg.js.map +1 -0
  63. package/dist/controls/vertical-content.d.ts +6 -0
  64. package/dist/controls/vertical-content.js +37 -0
  65. package/dist/controls/vertical-content.js.map +1 -0
  66. package/dist/controls/vertical.d.ts +8 -0
  67. package/dist/controls/vertical.js +23 -0
  68. package/dist/controls/vertical.js.map +1 -0
  69. package/dist/exports-index.d.ts +3 -0
  70. package/dist/exports-index.js +4 -0
  71. package/dist/exports-index.js.map +1 -0
  72. package/dist/helpers/hooks.d.ts +22 -0
  73. package/dist/helpers/hooks.js +173 -0
  74. package/dist/helpers/hooks.js.map +1 -0
  75. package/dist/index.d.ts +19 -0
  76. package/dist/index.js +20 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/styles/exports-index.d.ts +2 -0
  79. package/dist/styles/exports-index.js +3 -0
  80. package/dist/styles/exports-index.js.map +1 -0
  81. package/dist/styles/styles.d.ts +19 -0
  82. package/dist/styles/styles.js +79 -0
  83. package/dist/styles/styles.js.map +1 -0
  84. package/dist/styles/theme.d.ts +6 -0
  85. package/dist/styles/theme.js +77 -0
  86. package/dist/styles/theme.js.map +1 -0
  87. package/package.json +71 -0
  88. package/src/_modules/config.ts +9 -0
  89. package/src/_modules/constants.ts +3 -0
  90. package/src/controls/button.tsx +154 -0
  91. package/src/controls/centered.tsx +23 -0
  92. package/src/controls/dropdown.tsx +28 -0
  93. package/src/controls/error-boundary.tsx +42 -0
  94. package/src/controls/field-editor.tsx +42 -0
  95. package/src/controls/file-upload.tsx +68 -0
  96. package/src/controls/horizontal.tsx +35 -0
  97. package/src/controls/input.tsx +59 -0
  98. package/src/controls/list.tsx +118 -0
  99. package/src/controls/loading.tsx +11 -0
  100. package/src/controls/please-wait.tsx +32 -0
  101. package/src/controls/prompt.tsx +59 -0
  102. package/src/controls/search.tsx +66 -0
  103. package/src/controls/section.tsx +52 -0
  104. package/src/controls/svg.tsx +120 -0
  105. package/src/controls/vertical-content.tsx +50 -0
  106. package/src/controls/vertical.tsx +35 -0
  107. package/src/helpers/hooks.ts +189 -0
  108. package/src/index.ts +19 -0
  109. package/src/styles/styles.ts +87 -0
  110. package/src/styles/theme.ts +91 -0
@@ -0,0 +1,154 @@
1
+ import { Button, ButtonProps, CompoundButton, compoundButtonClassNames, CompoundButtonProps, makeStyles, mergeClasses, tokens, Tooltip } from '@fluentui/react-components';
2
+ import { capitalizeFirstLetter, isFunction, isNullOrEmptyString, isNullOrUndefined, isString, PushNoDuplicate } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { useCommonStyles, widthMedium } from '../styles/styles';
5
+
6
+ interface IProps {
7
+ title: string;//required
8
+ showTitleWithIcon?: boolean;
9
+ dontStretch?: boolean;
10
+ hideOnPrint?: boolean;
11
+ dontCenterText?: boolean;
12
+ hoverIcon?: JSX.Element;
13
+ hoverTitle?: string;
14
+ }
15
+ interface IPropsCompound extends IProps {
16
+ width?: string | number;
17
+ }
18
+
19
+ export type ButtonEXProps = IProps & ButtonProps;
20
+ export type CompoundButtonEXProps = IPropsCompound & CompoundButtonProps;
21
+
22
+ const useStyles = makeStyles({
23
+ buttonNoCenter: {
24
+ justifyContent: 'flex-start',
25
+ '& *': {
26
+ /* a button with no center that has content of a vertical, or multiple labels */
27
+ alignItems: 'flex-start'
28
+ }
29
+ },
30
+ danger: {
31
+ backgroundColor: tokens.colorStatusDangerBackground1,
32
+ color: tokens.colorStatusWarningForeground2,
33
+
34
+ [`& .${compoundButtonClassNames.secondaryContent}`]: {
35
+ color: tokens.colorStatusWarningForeground1
36
+ }
37
+ },
38
+ success: {
39
+ color: tokens.colorStatusSuccessForeground1,
40
+
41
+ [`& .${compoundButtonClassNames.secondaryContent}`]: {
42
+ color: tokens.colorStatusSuccessForeground1,
43
+ }
44
+ },
45
+ primarySubtle: {
46
+ color: tokens.colorBrandForeground1,
47
+
48
+ [`& .${compoundButtonClassNames.secondaryContent}`]: {
49
+ color: tokens.colorBrandForeground1,
50
+ }
51
+ },
52
+ })
53
+
54
+
55
+ export const ButtonEX = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
56
+ const [hover, setHover] = React.useState(false);
57
+ const trackHover = !isNullOrEmptyString(props.hoverTitle) || !isNullOrUndefined(props.hoverIcon);
58
+
59
+ const title = hover && !isNullOrEmptyString(props.hoverTitle) ? props.hoverTitle
60
+ : props.title || props['aria-label'];
61
+ const icon = hover && !isNullOrUndefined(props.hoverIcon) ? props.hoverIcon : props.icon;
62
+ let hasIcon = !isNullOrUndefined(icon);
63
+ let hasText = props.children || !hasIcon || (hasIcon && props.showTitleWithIcon === true);
64
+
65
+ const commonCssNames = useCommonStyles();
66
+ const cssNames = useStyles();
67
+ let css: string[] = [];
68
+
69
+ if (props.hideOnPrint) PushNoDuplicate(css, commonCssNames.printHide);
70
+ if (props.dontCenterText) PushNoDuplicate(css, cssNames.buttonNoCenter);
71
+
72
+ if (!isNullOrEmptyString(props.className)) css.push(...props.className.split(' '));
73
+
74
+ let btn = <Button ref={ref} appearance='subtle' {...props} className={mergeClasses(...css)}
75
+ aria-label={title} title={undefined} icon={icon}
76
+ onMouseEnter={trackHover ? (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
77
+ setHover(true);
78
+ if (isFunction(props.onMouseEnter))
79
+ props.onMouseEnter(e as any);
80
+ } : props.onMouseEnter as any}
81
+ onMouseLeave={trackHover ? (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
82
+ setHover(false);
83
+ if (isFunction(props.onMouseLeave))
84
+ props.onMouseLeave(e as any);
85
+ } : props.onMouseLeave as any}
86
+ >{props.children ||
87
+ //no icon? will show the title by default
88
+ (hasText && capitalizeFirstLetter(title))}</Button>;
89
+ if (!hasText || props.children)//icon only or when content is different than props.title
90
+ btn = <Tooltip showDelay={1000} relationship='label' withArrow appearance='inverted' content={title}>
91
+ {btn}
92
+ </Tooltip>;
93
+
94
+ return (
95
+ props.dontStretch ? <div>{btn}</div> : btn
96
+
97
+ );
98
+ });
99
+ export const ButtonEXSecondary = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
100
+ return (
101
+ <ButtonEX ref={ref} appearance='secondary' shape='circular' {...props}></ButtonEX>
102
+ );
103
+ });
104
+ export const ButtonEXPrimary = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
105
+ return (
106
+ <ButtonEXSecondary ref={ref} appearance='primary' {...props}>{props.children}</ButtonEXSecondary>
107
+ );
108
+ });
109
+ export const ButtonEXDanger = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
110
+ const cssNames = useStyles();
111
+ return (
112
+ <ButtonEXSecondary ref={ref} className={props.disabled ? undefined : cssNames.danger} {...props}>{props.children}</ButtonEXSecondary>
113
+ );
114
+ });
115
+ export const ButtonEXSuccess = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
116
+ const cssNames = useStyles();
117
+ return (
118
+ <ButtonEX ref={ref} className={cssNames.success} {...props}>{props.children}</ButtonEX>
119
+ );
120
+ });
121
+ export const ButtonEXPrimarySubtle = React.forwardRef<HTMLButtonElement, (ButtonEXProps)>((props, ref) => {
122
+ const cssNames = useStyles();
123
+ return (
124
+ <ButtonEX ref={ref} className={props.disabled ? undefined : cssNames.primarySubtle} {...props}>{props.children}</ButtonEX>
125
+ );
126
+ });
127
+
128
+ export const CompoundButtonEX = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
129
+ let title = props.title || props['aria-label'];
130
+ let tooltip = isString(props.secondaryContent) ? props.secondaryContent : title;
131
+ let max = typeof (props.width) === "undefined" ? widthMedium : props.width;
132
+ return (
133
+ <Tooltip showDelay={1000} relationship='label' withArrow appearance='inverted' content={tooltip}>
134
+ <CompoundButton ref={ref} appearance='subtle' style={{ justifyContent: "flex-start", maxWidth: max }} {...props} aria-label={tooltip} title={undefined}>
135
+ {props.children || capitalizeFirstLetter(title)}</CompoundButton>
136
+ </Tooltip>
137
+ );
138
+ });
139
+ export const CompoundButtonEXSecondary = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
140
+ return (
141
+ <CompoundButtonEX ref={ref} appearance='secondary' shape='circular' {...props}></CompoundButtonEX>
142
+ );
143
+ });
144
+ export const CompoundButtonEXPrimary = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
145
+ return (
146
+ <CompoundButtonEXSecondary ref={ref} appearance='primary' {...props}>{props.children}</CompoundButtonEXSecondary>
147
+ );
148
+ });
149
+ export const CompoundButtonEXDanger = React.forwardRef<HTMLButtonElement, (CompoundButtonEXProps)>((props, ref) => {
150
+ const cssNames = useStyles();
151
+ return (
152
+ <CompoundButtonEXSecondary ref={ref} className={cssNames.danger} {...props}>{props.children}</CompoundButtonEXSecondary>
153
+ );
154
+ });
@@ -0,0 +1,23 @@
1
+ import { makeStyles } from '@fluentui/react-components';
2
+ import React from 'react';
3
+ import { Horizontal } from './horizontal';
4
+ import { Vertical } from './vertical';
5
+
6
+ const useStyles = makeStyles({
7
+ center: {
8
+ justifyContent: 'center'
9
+ },
10
+ })
11
+
12
+ interface IProps {
13
+ }
14
+ export const Centered: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
15
+ const cssNames = useStyles();
16
+ return (
17
+ <Vertical main css={[cssNames.center]}>
18
+ <Horizontal css={[cssNames.center]}>
19
+ {props.children}
20
+ </Horizontal>
21
+ </Vertical>
22
+ );
23
+ }
@@ -0,0 +1,28 @@
1
+ import { Dropdown, Option } from '@fluentui/react-components';
2
+ import { filterEmptyEntries, firstOrNull } from '@kwiz/common';
3
+ import React from 'react';
4
+
5
+ interface IProps<dataType, keyType extends string = string> {
6
+ selected: keyType[];
7
+ items: { key: keyType, value: string, data?: dataType, option?: JSX.Element; }[];
8
+ onSelect: (item: { key: keyType, value: string, data?: dataType }) => void;
9
+ placeholder?: string;
10
+ }
11
+
12
+ function $DropdownEX<keyType extends string = string, dataType = never>(props: IProps<dataType, keyType>, ref: React.ForwardedRef<HTMLButtonElement>) {
13
+ let text = filterEmptyEntries(props.selected.map(s => {
14
+ let v = firstOrNull(props.items, i => i.key === s);
15
+ return v ? v.value : ''
16
+ })).join(', ');
17
+ return (
18
+ <Dropdown placeholder={props.placeholder} ref={ref} appearance='underline' selectedOptions={props.selected} value={text} >
19
+ {props.items.map(i => <Option key={i.key} value={i.key}
20
+ onClick={() => { props.onSelect(i); }}
21
+ text={i.value}
22
+ >{i.option ? i.option : i.value}</Option>)}
23
+ </Dropdown>
24
+
25
+ );
26
+ }
27
+
28
+ export const DropdownEX = React.forwardRef($DropdownEX);
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+ import { GetLogger } from "../_modules/config";
3
+
4
+ const logger = GetLogger("ErrorBoundary");
5
+
6
+ interface iProps {
7
+ errorComponent?: JSX.Element,
8
+ /** If changeMarker changes, it will check the error again */
9
+ changeMarker: string | number
10
+ }
11
+ interface iState { hasError: boolean; marker: string | number; }
12
+ export class ErrorBoundary extends React.Component<React.PropsWithChildren<iProps>, iState> {
13
+ constructor(props: iProps) {
14
+ super(props);
15
+ this.state = { hasError: false, marker: props.changeMarker };
16
+ }
17
+
18
+ static getDerivedStateFromError(error) {
19
+ // Update state so the next render will show the fallback UI.
20
+ return { hasError: true };
21
+ }
22
+ static getDerivedStateFromProps(props: iProps, state: iState) {
23
+ if (props.changeMarker !== state.marker)
24
+ return { hasError: false, marker: props.changeMarker };
25
+ else return null;
26
+ }
27
+
28
+ componentDidCatch(error, errorInfo) {
29
+ // You can also log the error to an error reporting service
30
+ logger.error(error);
31
+ logger.error(errorInfo);
32
+ }
33
+
34
+ render() {
35
+ if (this.state.hasError) {
36
+ // You can render any custom fallback UI
37
+ return this.props.errorComponent || <h1>Something went wrong.</h1>;
38
+ }
39
+
40
+ return this.props.children;
41
+ }
42
+ }
@@ -0,0 +1,42 @@
1
+ import { Field, mergeClasses, Textarea } from '@fluentui/react-components';
2
+ import { isNullOrUndefined } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { GetLogger } from '../_modules/config';
5
+ import { InputEx } from './input';
6
+
7
+ const logger = GetLogger('FieldEditor');
8
+
9
+ interface IProps {
10
+ required?: boolean;
11
+ error?: string;
12
+ value: string;
13
+ onChange: (newValue: string) => void;
14
+ css: string[];
15
+ label: string;
16
+ description?: string;
17
+ type?: "text" | "multiline"
18
+ }
19
+ export const FieldEditor: React.FunctionComponent<IProps> = (props) => {
20
+ if (isNullOrUndefined(props.value)) {
21
+ logger.error(`${props.label}: value should not be null`);
22
+ }
23
+ return (
24
+ <Field required={props.required}
25
+ validationMessage={props.error || props.description}
26
+ validationState={props.error ? "error" : "none"}>
27
+ {props.type === "multiline"
28
+ ? <Textarea className={props.css && mergeClasses(...props.css)}
29
+ required={props.required}
30
+ placeholder={props.label}
31
+ value={props.value || ""}
32
+ onChange={(e, data) => props.onChange(data.value)}
33
+ />
34
+ : <InputEx className={props.css && mergeClasses(...props.css)}
35
+ required={props.required}
36
+ appearance='underline'
37
+ placeholder={props.label}
38
+ value={props.value || ""}
39
+ onChange={(e, data) => props.onChange(data.value)} />}
40
+ </Field>
41
+ );
42
+ }
@@ -0,0 +1,68 @@
1
+ import { ButtonProps } from "@fluentui/react-components";
2
+ import { isFunction, isNotEmptyArray, isNullOrEmptyString } from '@kwiz/common';
3
+ import * as React from "react";
4
+ import { ButtonEX, CompoundButtonEXSecondary } from "./button";
5
+
6
+ interface iProps {
7
+ showTitleWithIcon?: boolean;
8
+ title?: string;
9
+ /** Passing this will turn the button into a compound button */
10
+ secondaryContent?: string;
11
+ limitFileTypes?: string[];
12
+ allowMultiple?: boolean;
13
+ icon?: JSX.Element;
14
+ onChange?: (newFile: File | FileList) => void;
15
+ /** only works for single file, reads it as base64 */
16
+ asBase64?: (base64: string) => void;
17
+ buttonProps?: ButtonProps;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ export const FileUpload = React.forwardRef<HTMLButtonElement, (iProps)>((props, ref) => {
22
+ const hiddenFileInput = React.useRef(null);
23
+ const isMulti = props.allowMultiple === true;
24
+ return <>
25
+ {isNullOrEmptyString(props.secondaryContent)
26
+ ? <ButtonEX ref={ref} {...(props.buttonProps || {})} icon={props.icon} showTitleWithIcon={props.showTitleWithIcon} onClick={() => {
27
+ hiddenFileInput.current.value = "";
28
+ hiddenFileInput.current.click();
29
+ }} title={props.title}
30
+ disabled={props.disabled}
31
+ />
32
+ : <CompoundButtonEXSecondary ref={ref} {...(props.buttonProps || {})} icon={props.icon}
33
+ secondaryContent={props.secondaryContent}
34
+ onClick={() => {
35
+ hiddenFileInput.current.value = "";
36
+ hiddenFileInput.current.click();
37
+ }} title={props.title}
38
+ disabled={props.disabled}
39
+ />}
40
+ <input type="file" ref={hiddenFileInput} style={{ display: "none" }} multiple={isMulti}
41
+ accept={isNotEmptyArray(props.limitFileTypes) ? props.limitFileTypes.map(ft => `.${ft}`).join() : undefined}
42
+ onChange={(e) => {
43
+ if (e.target.files && e.target.files.length > 0) {
44
+ if (isMulti) {
45
+ if (isFunction(props.onChange)) {
46
+ props.onChange(e.target.files);
47
+ }
48
+ }
49
+ else {
50
+ const fileUploaded = e.target.files && e.target.files[0];
51
+ if (isFunction(props.onChange)) {
52
+ props.onChange(fileUploaded);
53
+ }
54
+ if (isFunction(props.asBase64) && fileUploaded) {
55
+ const reader = new FileReader();
56
+ reader.onloadend = () => {
57
+ console.log(reader.result);
58
+ if (!isNullOrEmptyString(reader.result))
59
+ props.asBase64(reader.result as string);
60
+ };
61
+ reader.readAsDataURL(fileUploaded);
62
+ }
63
+ }
64
+ }
65
+ }}
66
+ />
67
+ </>;
68
+ });
@@ -0,0 +1,35 @@
1
+ import { makeStyles } from '@fluentui/react-components';
2
+ import { isNotEmptyArray } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { KnownClassNames, mixins } from '../styles/styles';
5
+ import { ISectionProps, Section } from './section';
6
+
7
+ const useStyles = makeStyles({
8
+ horizontal: {
9
+ ...mixins.flex,
10
+ flexDirection: 'row'
11
+ },
12
+ wrap: mixins.wrap,
13
+ nogap: mixins.nogap
14
+ })
15
+
16
+ interface IProps extends ISectionProps {
17
+ wrap?: boolean;
18
+ nogap?: boolean;
19
+ }
20
+ export const Horizontal: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
21
+ const cssNames = useStyles();
22
+ let css: string[] = [KnownClassNames.horizontal];
23
+
24
+ css.push(cssNames.horizontal);
25
+ if (props.wrap)
26
+ css.push(cssNames.wrap);
27
+ if (props.nogap)
28
+ css.push(cssNames.nogap);
29
+
30
+ if (isNotEmptyArray(props.css)) css.push(...props.css);
31
+
32
+ return (
33
+ <Section {...props} css={css} />
34
+ );
35
+ }
@@ -0,0 +1,59 @@
1
+ import { GriffelStyle, Input, InputProps, makeStyles, mergeClasses, Textarea, TextareaProps } from '@fluentui/react-components';
2
+ import { isFunction } from '@kwiz/common';
3
+ import React from 'react';
4
+
5
+
6
+ interface IProps extends InputProps {
7
+ onOK?: () => void;
8
+ onCancel?: () => void;
9
+ }
10
+ export const InputEx: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
11
+ return (
12
+ <Input appearance='underline' {...props}
13
+ onKeyDown={isFunction(props.onOK) || isFunction(props.onCancel)
14
+ ? e => {
15
+ if (isFunction(props.onOK) && e.key === "Enter") props.onOK();
16
+ else if (isFunction(props.onCancel) && e.key === "Escape") props.onCancel();
17
+ }
18
+ : undefined
19
+ }
20
+ />
21
+ );
22
+ }
23
+
24
+ const fullSize: GriffelStyle = {
25
+ width: '100% !important',
26
+ maxHeight: '100% !important'
27
+ };
28
+ const useStyles = makeStyles({
29
+ fullSizeTextArea: {
30
+ ...fullSize,
31
+ ['& > textarea']: fullSize
32
+ },
33
+ })
34
+
35
+ interface IPropsTextArea extends TextareaProps {
36
+ fullSize?: boolean;
37
+ growNoShrink?: boolean;
38
+ }
39
+ export const TextAreaEx: React.FunctionComponent<React.PropsWithChildren<IPropsTextArea>> = (props) => {
40
+ const cssNames = useStyles();
41
+ let css: string[] = [];
42
+
43
+ if (props.fullSize) css.push(cssNames.fullSizeTextArea);
44
+ const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
45
+ const recalcHeight = React.useCallback(() => {
46
+ if (textAreaRef.current && props.growNoShrink) {
47
+ if (textAreaRef.current.scrollHeight > textAreaRef.current.clientHeight)
48
+ textAreaRef.current.style.minHeight = textAreaRef.current.scrollHeight + 'px';
49
+ }
50
+ }, [textAreaRef]);
51
+
52
+ let style: React.CSSProperties = { height: '100%', ...props.style };
53
+ return (
54
+ <Textarea ref={textAreaRef} className={mergeClasses(...css)} {...props} style={style} onChange={(e, d) => {
55
+ if (props.onChange) props.onChange(e, d);
56
+ recalcHeight();
57
+ }} />
58
+ );
59
+ }
@@ -0,0 +1,118 @@
1
+ import { makeStyles, tokens } from '@fluentui/react-components';
2
+ import { LOGO_BLUE_SQUARE, LOGO_WHITE_SQUARE, isNullOrUndefined, isString } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { KnownClassNames, mixins } from '../styles/styles';
5
+ import { Horizontal } from './horizontal';
6
+ import { Section } from './section';
7
+ import { Vertical } from './vertical';
8
+
9
+ const useStyles = makeStyles({
10
+ list: {
11
+ rowGap: 0
12
+ },
13
+ listItem: {
14
+ padding: tokens.spacingVerticalS,
15
+ ':hover': {
16
+ backgroundColor: tokens.colorNeutralBackground1Hover
17
+ }
18
+ },
19
+ listItemSelected: {
20
+ backgroundColor: tokens.colorNeutralBackground1Selected
21
+ },
22
+ media: {
23
+ width: '32px',
24
+ fontSize: tokens.fontSizeBase600,
25
+ display: 'flex',
26
+ flexDirection: 'column',
27
+ justifyContent: 'center'
28
+ },
29
+ image: {
30
+ width: tokens.lineHeightBase600,
31
+ height: tokens.lineHeightBase600,
32
+ backgroundPosition: 'center center',
33
+ backgroundSize: 'cover',
34
+ borderRadius: tokens.borderRadiusCircular,
35
+ border: `1px solid ${tokens.colorNeutralStroke1}`
36
+ },
37
+ listItemBody: {
38
+ rowGap: 0,
39
+ width: 'calc(100% - 44px)'
40
+ },
41
+ listItemHeader: mixins.ellipsis,
42
+ listItemContent: {
43
+ ...mixins.ellipsis,
44
+ fontSize: tokens.fontSizeBase200
45
+ },
46
+ listItemMedia: {
47
+ ...mixins.ellipsis,
48
+ maxWidth: '20%',
49
+ '& svg': {
50
+ height: tokens.fontSizeBase300
51
+ },
52
+ '& button': {
53
+ padding: 0,
54
+ minWidth: 0,
55
+ minHeight: 0,
56
+ height: '14px'
57
+ }
58
+ },
59
+ listItemMediaNoTrim: {
60
+ overflow: 'visible',
61
+ maxWidth: 'fit-content'
62
+ },
63
+ listItemMultilineContent: {
64
+ whiteSpace: 'pre-line'
65
+ }
66
+ });
67
+
68
+ export interface iListItem {
69
+ key: string | number;
70
+ media?: JSX.Element | string;
71
+ header: string;
72
+ headerMedia?: JSX.Element | string;
73
+ content?: string | JSX.Element | (string | JSX.Element)[];
74
+ onClickOnMedia?: boolean;
75
+ onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
76
+ selected?: boolean;
77
+ }
78
+ interface IProps {
79
+ selectable?: boolean;
80
+ items: iListItem[];
81
+ showAllHeaderMedia?: boolean;
82
+ /** allow multiline content */
83
+ multiline?: boolean;
84
+ dark?: boolean;
85
+ }
86
+
87
+ export const ListEx = (props: IProps) => {
88
+
89
+ const cssNames = useStyles();
90
+
91
+ const listItemElm = (item: iListItem) => <Horizontal key={item.key} css={[cssNames.listItem, item.selected && cssNames.listItemSelected]} onClick={item.onClick}>
92
+ {item.media && <Section css={[cssNames.media]} onClick={(e) => {
93
+ if (!item.onClickOnMedia)
94
+ e.stopPropagation();//media may have its on onclick
95
+ }}>{
96
+ isString(item.media)
97
+ ? <div className={cssNames.image} style={{ backgroundImage: `url('${encodeURI(item.media)}'), url('${props.dark ? LOGO_WHITE_SQUARE : LOGO_BLUE_SQUARE}')` }}></div>
98
+ : item.media
99
+ }</Section>}
100
+ <Vertical main css={[cssNames.listItemBody]}>
101
+ <Horizontal main>
102
+ <Section main css={[cssNames.listItemHeader]}>{item.header}</Section>
103
+ {item.headerMedia && <Section onClick={(e) => {
104
+ e.stopPropagation();//media may have its on onclick
105
+ }} css={[cssNames.listItemMedia, props.showAllHeaderMedia && cssNames.listItemMediaNoTrim]}>{item.headerMedia}</Section>}
106
+ </Horizontal>
107
+ {!isNullOrUndefined(item.content)
108
+ ? (Array.isArray(item.content) ? item.content : [item.content]).map((c, idx) => isNullOrUndefined(c) ? undefined : <Section key={idx} css={[cssNames.listItemContent, props.multiline ? cssNames.listItemMultilineContent : undefined]}>{c}</Section>)
109
+ : undefined}
110
+ </Vertical>
111
+ </Horizontal>;
112
+
113
+ return (
114
+ <Vertical css={[cssNames.list, KnownClassNames.list]}>
115
+ {props.items.map(item => listItemElm(item))}
116
+ </Vertical>
117
+ );
118
+ }
@@ -0,0 +1,11 @@
1
+ import { LOGO_ANIM_SMALL } from '@kwiz/common';
2
+ import { Centered } from './centered';
3
+ import React from 'react';
4
+
5
+ interface IProps {
6
+ }
7
+ export const Loading: React.FunctionComponent<IProps> = (props) => {
8
+ return (
9
+ <Centered><img src={LOGO_ANIM_SMALL} alt="loading" style={{ width: '15vw' }} /></Centered>
10
+ );
11
+ }
@@ -0,0 +1,32 @@
1
+ import { Field, ProgressBar } from '@fluentui/react-components';
2
+ import { isFunction } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { IPrompterProps, Prompter } from './prompt';
5
+
6
+ interface IProps {
7
+ step?: number; max?: number;
8
+ /** do not wrap in a dialog */
9
+ contentOnly?: boolean;
10
+ cancelText?: string;
11
+ onCancel?: () => void;
12
+ }
13
+ export const PleaseWait: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
14
+ const field = <Field validationMessage="please wait..." validationState="none">
15
+ <ProgressBar value={props.step} max={props.max} />
16
+ </Field>;
17
+ return (props.contentOnly
18
+ ? field
19
+ : <Prompter hideOk
20
+ hideCancel={!isFunction(props.onCancel)}
21
+ cancelButtonText={props.cancelText || 'cancel'}
22
+ onCancel={props.onCancel}>{field}</Prompter>
23
+ );
24
+ }
25
+
26
+ export const PleaseWaitPrompt = (props: { message: string; step?: number; max?: number; }): IPrompterProps => ({
27
+ //title: 'please wait...',
28
+ hideOk: true, hideCancel: true,
29
+ children: <Field validationMessage={props.message} validationState="none">
30
+ <ProgressBar value={props.step} max={props.max} />
31
+ </Field>
32
+ });
@@ -0,0 +1,59 @@
1
+ import { Dialog, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger } from '@fluentui/react-components';
2
+ import { isNullOrEmptyString } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { ButtonEXProps, ButtonEXSecondary } from './button';
5
+
6
+ export interface IPrompterProps {
7
+ hideOk?: boolean;
8
+ hideCancel?: boolean;
9
+ /** return false to prevent closing the dialog. */
10
+ onOK?: () => Promise<void> | void;
11
+ onCancel?: () => void;
12
+ okButtonText?: string;
13
+ cancelButtonText?: string;
14
+ title?: string;
15
+ okButtonProps?: Partial<ButtonEXProps>;
16
+ cancelButtonProps?: Partial<ButtonEXProps>;
17
+ children?: JSX.Element;
18
+ }
19
+ export const Prompter: React.FunctionComponent<React.PropsWithChildren<IPrompterProps>> = (props) => {
20
+ let okProps: ButtonEXProps = {
21
+ ...(props.okButtonProps as any || {}),
22
+ onClick: () => props.onOK(),
23
+ title: props.okButtonText || 'yes'
24
+ };
25
+ let cancelProps: ButtonEXProps = {
26
+ ...(props.cancelButtonProps as any || {}),
27
+ onClick: () => props.onCancel(),
28
+ title: props.cancelButtonText || 'no'
29
+ };
30
+ React.useEffect(() => {
31
+ let handler = (e: KeyboardEvent) => {
32
+ if (e.key === "Enter") props.onOK();
33
+ else if (e.key === "Escape") props.onCancel();
34
+
35
+ };
36
+ document.addEventListener("keydown", handler);
37
+ return () => document.removeEventListener("keydown", handler);
38
+ });
39
+ return (
40
+ <Dialog open>
41
+ <DialogSurface>
42
+ {!isNullOrEmptyString(props.title) && <DialogTitle>{props.title}</DialogTitle>}
43
+ <DialogBody>
44
+ <DialogContent>
45
+ {props.children}
46
+ </DialogContent>
47
+ <DialogActions>
48
+ {props.hideOk ? undefined : <DialogTrigger disableButtonEnhancement>
49
+ <ButtonEXSecondary {...okProps} />
50
+ </DialogTrigger>}
51
+ {props.hideCancel ? undefined : <DialogTrigger disableButtonEnhancement>
52
+ <ButtonEXSecondary {...cancelProps} />
53
+ </DialogTrigger>}
54
+ </DialogActions>
55
+ </DialogBody>
56
+ </DialogSurface>
57
+ </Dialog>
58
+ );
59
+ }