elbe-ui 0.2.5 → 0.2.14

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.
Files changed (50) hide show
  1. package/dist/index.js +17 -2
  2. package/dist/ui/color_theme.d.ts +5 -0
  3. package/dist/ui/components/badge.d.ts +25 -0
  4. package/dist/ui/components/box.d.ts +1027 -0
  5. package/dist/ui/components/button.d.ts +23 -0
  6. package/dist/ui/components/card.d.ts +14 -0
  7. package/dist/ui/components/dialog.d.ts +8 -0
  8. package/dist/ui/components/flex.d.ts +11 -0
  9. package/dist/ui/components/icon_button.d.ts +19 -0
  10. package/dist/ui/components/input/checkbox.d.ts +6 -0
  11. package/dist/ui/components/input/input_field.d.ts +22 -0
  12. package/dist/ui/components/input/range.d.ts +8 -0
  13. package/dist/ui/components/input/select.d.ts +10 -0
  14. package/dist/ui/components/input/text_area.d.ts +10 -0
  15. package/dist/ui/components/padded.d.ts +25 -0
  16. package/dist/ui/components/text.d.ts +33 -0
  17. package/dist/ui/components/toggle_button.d.ts +12 -0
  18. package/dist/ui/components/util.d.ts +3 -0
  19. package/dist/ui/util/confirm_dialog.d.ts +10 -0
  20. package/dist/ui/util/error_view.d.ts +1 -0
  21. package/dist/ui/util/toast.d.ts +5 -0
  22. package/dist/ui/util/util.d.ts +19 -0
  23. package/elbe.scss +2 -2
  24. package/package.json +5 -2
  25. package/src/index.tsx +24 -0
  26. package/src/ui/color_theme.ts +24 -0
  27. package/src/ui/components/badge.tsx +78 -0
  28. package/src/ui/components/box.tsx +49 -0
  29. package/src/ui/components/button.tsx +61 -0
  30. package/src/ui/components/card.tsx +45 -0
  31. package/src/ui/components/dialog.tsx +51 -0
  32. package/src/ui/components/flex.tsx +64 -0
  33. package/src/ui/components/icon_button.tsx +56 -0
  34. package/src/ui/components/input/checkbox.tsx +32 -0
  35. package/src/ui/components/input/input_field.tsx +57 -0
  36. package/src/ui/components/input/range.tsx +37 -0
  37. package/src/ui/components/input/select.tsx +29 -0
  38. package/src/ui/components/input/text_area.tsx +45 -0
  39. package/src/ui/components/padded.tsx +62 -0
  40. package/src/ui/components/text.tsx +78 -0
  41. package/src/ui/components/toggle_button.tsx +52 -0
  42. package/src/ui/components/util.tsx +3 -0
  43. package/src/ui/util/confirm_dialog.ts +53 -0
  44. package/src/ui/util/error_view.tsx +16 -0
  45. package/src/ui/util/toast.ts +14 -0
  46. package/src/ui/util/util.ts +36 -0
  47. package/style/color_style.scss +148 -0
  48. package/style/components.scss +450 -0
  49. package/style/root.scss +50 -0
  50. package/style/type_style.scss +22 -0
@@ -0,0 +1,64 @@
1
+ import { applyProps, type ElbeProps } from "./box";
2
+
3
+ export type FlexProps = {
4
+ children: any;
5
+ gap?: number;
6
+ // shorthand for cross="stretch"
7
+ stretch?: boolean;
8
+ main?:
9
+ | "start"
10
+ | "center"
11
+ | "end"
12
+ | "stretch"
13
+ | "space-between"
14
+ | "space-around"
15
+ | "space-evenly";
16
+ cross?:
17
+ | "start"
18
+ | "center"
19
+ | "end"
20
+ | "stretch"
21
+ | "space-between"
22
+ | "space-around"
23
+ | "space-evenly";
24
+ } & ElbeProps;
25
+
26
+ export function FlexSpace({}) {
27
+ return <div style="flex:1"></div>;
28
+ }
29
+
30
+ export function Column({
31
+ gap = 1,
32
+ main = "start",
33
+ cross,
34
+ stretch = false,
35
+ children,
36
+ ...p
37
+ }: FlexProps) {
38
+ return _Flex(false, { gap, main, cross, stretch, children }, p);
39
+ }
40
+
41
+ export function Row({
42
+ gap = 1,
43
+ main = "start",
44
+ cross,
45
+ stretch = false,
46
+ children,
47
+ ...p
48
+ }: FlexProps) {
49
+ return _Flex(true, { gap, main, cross, stretch, children }, p);
50
+ }
51
+
52
+ function _Flex(row: boolean, p: FlexProps, elbe: ElbeProps) {
53
+ return (
54
+ <div
55
+ {...applyProps(elbe, row ? "row" : "column", {
56
+ justifyContent: p.main,
57
+ alignItems: p.cross || (p.stretch ? "stretch" : "center"),
58
+ gap: `${p.gap}rem`,
59
+ })}
60
+ >
61
+ {p.children}
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,56 @@
1
+ import React from "preact/compat";
2
+ import type { ElbeColorManners, ElbeColorStyles } from "../color_theme";
3
+ import type { ElbeChild } from "../util/util";
4
+ import { applyProps, type ElbeProps } from "./box";
5
+
6
+ export type IconChild = ElbeChild | ((_: any) => ElbeChild);
7
+
8
+ export type IconButtonProps = {
9
+ icon?: IconChild;
10
+ colorStyle?: ElbeColorStyles;
11
+
12
+ onTap?: () => void;
13
+ } & ElbeProps;
14
+
15
+ export class IconButton extends React.Component<
16
+ IconButtonProps & { colorManner?: ElbeColorManners }
17
+ > {
18
+ static major = (p: IconButtonProps) => _btn(p, "major");
19
+ static minor = (p: IconButtonProps) => _btn(p, "minor");
20
+ static action = (p: IconButtonProps) => _btn(p, "action");
21
+ static integrated = (p: IconButtonProps) => _btn(p, "integrated");
22
+
23
+ render() {
24
+ return _btn(this.props, this.props.colorManner);
25
+ }
26
+ }
27
+
28
+ function _btn(
29
+ { icon, onTap, ...elbe }: IconButtonProps,
30
+ colorManner: ElbeColorManners = "major"
31
+ ) {
32
+ return (
33
+ <button
34
+ {...applyProps(
35
+ elbe,
36
+ [
37
+ "row",
38
+ "main-center",
39
+ "gap-half",
40
+ elbe.colorStyle,
41
+ colorManner,
42
+ !onTap && "disabled",
43
+ ],
44
+ {
45
+ border: "none",
46
+ borderRadius: "3rem",
47
+ height: "3rem",
48
+ width: "3rem",
49
+ }
50
+ )}
51
+ onClick={() => onTap && onTap()}
52
+ >
53
+ {typeof icon === "function" ? icon({}) : icon}
54
+ </button>
55
+ );
56
+ }
@@ -0,0 +1,32 @@
1
+ import { applyProps, type ElbeProps } from "../box";
2
+
3
+ export function Checkbox({
4
+ value,
5
+ label,
6
+ onChange,
7
+ ...elbe
8
+ }: {
9
+ value: boolean;
10
+ label?: string;
11
+ onChange?: ((checked: boolean) => void) | null;
12
+ } & ElbeProps) {
13
+ return (
14
+ <div
15
+ class={`row ${onChange ? "" : "disabled"}`}
16
+ style={{
17
+ gap: ".75rem",
18
+ filter: onChange ? "" : "grayscale(1)",
19
+ opacity: onChange ? "" : "0.5",
20
+ }}
21
+ >
22
+ <input
23
+ type="checkbox"
24
+ {...applyProps(elbe)}
25
+ disabled={!onChange}
26
+ checked={value}
27
+ onChange={(e) => onChange?.(e.currentTarget.checked)}
28
+ />
29
+ {label && <div style="margin-top: -.25rem">{label}</div>}
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,57 @@
1
+ import React from "preact/compat";
2
+ import { applyProps, type ElbeProps } from "../box";
3
+ import { _TextArea } from "./text_area";
4
+
5
+ export type InputFieldProps = {
6
+ label?: string;
7
+ hint: string;
8
+ readonly?: boolean;
9
+ value: string | number;
10
+ onInput?: (value: string) => void;
11
+ } & ElbeProps;
12
+
13
+ export class Field extends React.Component<
14
+ InputFieldProps & {
15
+ type?: "text" | "number" | "password" | "date" | "time" | "email";
16
+ }
17
+ > {
18
+ static text = (p: InputFieldProps) => <Field {...p} type="text" />;
19
+ static number = (p: InputFieldProps) => <Field {...p} type="number" />;
20
+ static password = (p: InputFieldProps) => <Field {...p} type="password" />;
21
+ static date = (p: InputFieldProps) => <Field {...p} type="date" />;
22
+ static time = (p: InputFieldProps) => <Field {...p} type="time" />;
23
+ static email = (p: InputFieldProps) => <Field {...p} type="email" />;
24
+
25
+ static multiLine = _TextArea;
26
+
27
+ render() {
28
+ const { label, hint, readonly, type, value, onInput, ...elbe } = this.props;
29
+
30
+ return (
31
+ <div
32
+ style={{
33
+ width: "12rem !important",
34
+ display: "flex",
35
+ flexDirection: "column",
36
+ alignItems: "stretch",
37
+ }}
38
+ data-tooltip={this.props?.tooltip}
39
+ >
40
+ <input
41
+ type={this.props.type}
42
+ {...applyProps(this.props, null, {
43
+ width: "100%",
44
+ })}
45
+ size={5}
46
+ label={this.props.label}
47
+ placeholder={this.props.hint}
48
+ value={this.props.value}
49
+ onInput={(e) =>
50
+ this.props.onInput && this.props.onInput(e.currentTarget.value)
51
+ }
52
+ readonly={this.props.readonly}
53
+ />
54
+ </div>
55
+ );
56
+ }
57
+ }
@@ -0,0 +1,37 @@
1
+ import { _ElbeErr } from "../../util/error_view";
2
+ import { applyProps, type ElbeProps } from "../box";
3
+
4
+ export function Range({
5
+ value,
6
+ onChange,
7
+ min = 0,
8
+ max = 100,
9
+ step = 1,
10
+ ...elbe
11
+ }: {
12
+ value: number;
13
+ min?: number;
14
+ step?: number;
15
+ max?: number;
16
+ onChange?: ((value: number) => void) | null;
17
+ } & ElbeProps) {
18
+ return min > max ? (
19
+ _ElbeErr("Range: max is smaller than min")
20
+ ) : (
21
+ <input
22
+ type="range"
23
+ {...applyProps(elbe, null, {
24
+ filter: onChange ? "" : "grayscale(1)",
25
+ opacity: onChange ? "" : "0.5",
26
+ cursor: onChange ? "pointer" : "not-allowed",
27
+ width: "100%",
28
+ })}
29
+ min={min}
30
+ max={max}
31
+ step={step}
32
+ disabled={!onChange}
33
+ value={value}
34
+ onInput={(e) => onChange?.(Number(e.currentTarget.value))}
35
+ />
36
+ );
37
+ }
@@ -0,0 +1,29 @@
1
+ import { applyProps, type ElbeProps } from "../box";
2
+
3
+ export function Select({
4
+ options,
5
+ value,
6
+ label,
7
+ onChange,
8
+ ...elbe
9
+ }: {
10
+ options: { key: string; label: string }[];
11
+ value?: string;
12
+ label?: string;
13
+ onChange: (value: string) => any;
14
+ } & ElbeProps) {
15
+ return (
16
+ <select
17
+ {...applyProps(elbe)}
18
+ value={value}
19
+ label={label}
20
+ onChange={(e) => onChange(e.currentTarget.value)}
21
+ >
22
+ {options.map(({ key, label }) => (
23
+ <option key={key} value={key}>
24
+ {label}
25
+ </option>
26
+ ))}
27
+ </select>
28
+ );
29
+ }
@@ -0,0 +1,45 @@
1
+ import { applyProps, type ElbeProps } from "../box";
2
+
3
+ export function _TextArea({
4
+ label,
5
+ hint,
6
+ readonly,
7
+ rows = 4,
8
+ maxLength,
9
+ value,
10
+ onInput,
11
+ ...elbe
12
+ }: {
13
+ label?: string;
14
+ hint: string;
15
+ rows?: number;
16
+ maxLength?: number;
17
+ readonly?: boolean;
18
+ value: string | number;
19
+ onInput?: (value: string) => void;
20
+ } & ElbeProps) {
21
+ return (
22
+ <div
23
+ style={{
24
+ width: "12rem !important",
25
+ display: "flex",
26
+ flexDirection: "column",
27
+ alignItems: "stretch",
28
+ }}
29
+ data-tooltip={elbe.tooltip}
30
+ >
31
+ <textarea
32
+ {...applyProps(elbe, null, { width: "100%" })}
33
+ label={label}
34
+ size={5}
35
+ cols={5}
36
+ placeholder={hint}
37
+ rows={rows}
38
+ maxLength={maxLength}
39
+ value={value}
40
+ onInput={(e) => onInput && onInput(e.currentTarget.value)}
41
+ readonly={readonly}
42
+ />
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,62 @@
1
+ import React from "preact/compat";
2
+
3
+ export type PaddedProps = {
4
+ children: any;
5
+ };
6
+
7
+ export class Padded extends React.Component<
8
+ PaddedProps & {
9
+ top: number;
10
+ right: number;
11
+ bottom: number;
12
+ left: number;
13
+ }
14
+ > {
15
+ constructor(
16
+ props: PaddedProps & {
17
+ top: number;
18
+ right: number;
19
+ bottom: number;
20
+ left: number;
21
+ }
22
+ ) {
23
+ super(props);
24
+ }
25
+
26
+ static all = ({ amount = 1, ...p }: PaddedProps & { amount: number }) => (
27
+ <Padded
28
+ {...{ ...p, top: amount, right: amount, bottom: amount, left: amount }}
29
+ />
30
+ );
31
+
32
+ static symmetric = ({
33
+ vertical = 0,
34
+ horizontal = 0,
35
+ ...p
36
+ }: PaddedProps & { vertical: number; horizontal: number }) => (
37
+ <Padded
38
+ {...{
39
+ ...p,
40
+ top: vertical,
41
+ bottom: vertical,
42
+ left: horizontal,
43
+ right: horizontal,
44
+ }}
45
+ />
46
+ );
47
+
48
+ render() {
49
+ return (
50
+ <div
51
+ style={{
52
+ paddingTop: `${this.props.top}rem`,
53
+ paddingRight: `${this.props.right}rem`,
54
+ paddingBottom: `${this.props.bottom}rem`,
55
+ paddingLeft: `${this.props.left}rem`,
56
+ }}
57
+ >
58
+ {this.props.children}
59
+ </div>
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,78 @@
1
+ import React from "preact/compat";
2
+ import type { ElbeTypeStyles } from "../color_theme";
3
+ import type { ElbeChildren } from "../util/util";
4
+ import { applyProps, type ElbeProps } from "./box";
5
+
6
+ export type TextProps = {
7
+ align?: "start" | "center" | "end";
8
+ bold?: boolean;
9
+ italic?: boolean;
10
+ underline?: boolean;
11
+ striked?: boolean;
12
+ color?: string;
13
+ size?: number;
14
+ children?: ElbeChildren;
15
+ v?: string;
16
+ } & ElbeProps;
17
+
18
+ export class Text extends React.Component<
19
+ TextProps & { typeStyle?: ElbeTypeStyles }
20
+ > {
21
+ static h1 = (p: TextProps) => <Text {...p} typeStyle="header-1" />;
22
+ static h2 = (p: TextProps) => <Text {...p} typeStyle="header-2" />;
23
+ static h3 = (p: TextProps) => <Text {...p} typeStyle="header-3" />;
24
+ static h4 = (p: TextProps) => <Text {...p} typeStyle="header-4" />;
25
+ static h5 = (p: TextProps) => <Text {...p} typeStyle="header-5" />;
26
+ static h6 = (p: TextProps) => <Text {...p} typeStyle="header-6" />;
27
+ static s = (p: TextProps) => <Text {...p} typeStyle="text-s" />;
28
+ static m = (p: TextProps) => <Text {...p} typeStyle="text-m" />;
29
+ static l = (p: TextProps) => <Text {...p} typeStyle="text-l" />;
30
+ static code = (p: TextProps) => <Text {...p} typeStyle="code" />;
31
+
32
+ constructor({
33
+ typeStyle = "text-m",
34
+ ...props
35
+ }: TextProps & { typeStyle?: ElbeTypeStyles }) {
36
+ super({ ...props, typeStyle });
37
+ }
38
+
39
+ render() {
40
+ const {
41
+ align,
42
+ bold,
43
+ italic,
44
+ underline,
45
+ striked,
46
+ color,
47
+ size,
48
+ children,
49
+ typeStyle,
50
+ v,
51
+ ...elbe
52
+ } = this.props;
53
+ return (
54
+ <span
55
+ {...applyProps(
56
+ elbe,
57
+ [
58
+ "text",
59
+ align,
60
+ bold && "b",
61
+ italic && "i",
62
+ underline && "underline",
63
+ striked && "striked",
64
+ color,
65
+ typeStyle,
66
+ ],
67
+ {
68
+ fontSize: size ? `${size}rem` : "",
69
+ color: color || "",
70
+ }
71
+ )}
72
+ >
73
+ {v}
74
+ {children}
75
+ </span>
76
+ );
77
+ }
78
+ }
@@ -0,0 +1,52 @@
1
+ import { Icons, applyProps } from "../..";
2
+ import { _ElbeErr } from "../util/error_view";
3
+ import type { ElbeChild } from "../util/util";
4
+ import type { ElbeProps } from "./box";
5
+ import { Card } from "./card";
6
+ import { Column, FlexSpace } from "./flex";
7
+
8
+ export type ToggleButtonItem<T> = {
9
+ icon?: (_: any) => ElbeChild;
10
+ label: string;
11
+ key: T;
12
+ };
13
+
14
+ export function ToggleButton<T>({
15
+ items,
16
+ onSelect,
17
+ value,
18
+ ...elbe
19
+ }: {
20
+ items: ToggleButtonItem<T>[];
21
+
22
+ onSelect: ((value: T) => void) | null;
23
+ value: T;
24
+ } & ElbeProps) {
25
+ return items.length < 1 ? (
26
+ _ElbeErr("ToggleButton requires at least one item")
27
+ ) : (
28
+ <Card
29
+ mode="light"
30
+ colorScheme="primary"
31
+ padding={0}
32
+ {...applyProps(elbe, null, { overflow: "hidden", width: "100%" })}
33
+ >
34
+ <Column stretch gap={0}>
35
+ {items.map((item) => (
36
+ <Card
37
+ onTap={onSelect ? () => onSelect(item.key) : undefined}
38
+ class={`row b ${onSelect ? "pointer" : "disabled"}`}
39
+ style={{ border: "none", borderRadius: 0, gap: ".75rem" }}
40
+ colorStyle="accent"
41
+ colorManner={value === item.key ? "minor" : "action"}
42
+ >
43
+ {item.icon?.({})}
44
+ {item.label}
45
+ <FlexSpace />
46
+ {value === item.key && <Icons.Check />}
47
+ </Card>
48
+ ))}
49
+ </Column>
50
+ </Card>
51
+ );
52
+ }
@@ -0,0 +1,3 @@
1
+ export function Spaced({ amount = 1 }) {
2
+ return <div style={{ width: amount + "rem", height: amount + "rem" }}></div>;
3
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * show a simple confirm dialog
3
+ * @param param0 the title and message of the dialog
4
+ * @returns a promise that resolves to true if the user clicks "yes" or "okay" and false if the user clicks "no"
5
+ */
6
+ export function showConfirmDialog({
7
+ title,
8
+ message,
9
+ okay = false,
10
+ }: {
11
+ message: string;
12
+ title: string;
13
+ okay?: boolean;
14
+ }): Promise<boolean> {
15
+ return new Promise((resolve, reject) => {
16
+ const dialog = document.createElement("div");
17
+ dialog.classList.add("dialog");
18
+ dialog.innerHTML = `<dialog open>
19
+ <div
20
+ class=" card plain-opaque"
21
+ style="max-width: 30rem; min-width: 10rem"
22
+ >
23
+ <div class="row cross-start">
24
+ <div class="flex-1 b" style="margin-top: 0.47rem; font-size: 1.2rem">
25
+ ${title}
26
+ </div>
27
+ <button class="integrated" style="width: 3rem" onclick="conf_resolve(false)">
28
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x "><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
29
+ </button>
30
+ </div>
31
+ <div style="margin-top: 1rem; margin-bottom: 1rem">
32
+ ${message}
33
+ </div>
34
+ <div class="row main-end gap">
35
+
36
+ ${
37
+ okay
38
+ ? '<button class="loud" style="padding-left:1rem; padding-right:1rem" onclick="conf_resolve(true)">okay</button>'
39
+ : '<button class="loud minor" style="padding-left:1rem; padding-right:1rem" onclick="conf_resolve(false)">no</button>' +
40
+ '<button class="loud" style="padding-left:1rem; padding-right:1rem" onclick="conf_resolve(true)">yes</button>'
41
+ }
42
+ </div>
43
+
44
+ </div>
45
+ </dialog>
46
+ `;
47
+ document.body.appendChild(dialog);
48
+ (window as any)["conf_resolve"] = (v: any) => {
49
+ document.body.removeChild(dialog);
50
+ resolve(v);
51
+ };
52
+ });
53
+ }
@@ -0,0 +1,16 @@
1
+ import { Icons } from "../..";
2
+
3
+ export function _ElbeErr(msg: string) {
4
+ return (
5
+ <div
6
+ class="row text-s gap-half"
7
+ style="background: #ee0044; color: white; border-radius: 4px; text-align: left; padding: .5rem"
8
+ >
9
+ <Icons.CircleX />
10
+ <div class="column gap-none cross-stretch">
11
+ <b class="text-s">elbe error</b>
12
+ <span style="margin-top: -.125rem">{msg}</span>
13
+ </div>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * show a toast message at the bottom of the screen
3
+ * @param message the message to show
4
+ */
5
+
6
+ export function showToast(message: string) {
7
+ const toast = document.createElement("div");
8
+ toast.classList.add("toast");
9
+ toast.innerText = message;
10
+ document.body.appendChild(toast);
11
+ setTimeout(() => {
12
+ toast.remove();
13
+ }, 3000);
14
+ }
@@ -0,0 +1,36 @@
1
+ import type React from "preact/compat";
2
+ import { showToast } from "./toast";
3
+
4
+ export type ElbeChild = React.ReactNode;
5
+ export type ElbeChildren = ElbeChild[] | ElbeChild;
6
+
7
+ /**
8
+ * use the web share api if available, otherwise copy the data to the clipboard
9
+ * @param data the data you want to share
10
+ * @param toastMsg the message to show in the toast if the share api is not available
11
+ */
12
+ export function share(
13
+ data: { title: string; text?: string; url: string },
14
+ toastMsg = "copied to clipboard. Share it with others."
15
+ ) {
16
+ const msg = `${data.title}\n${data.text ?? ""}\n\n${data.url}`;
17
+
18
+ if (navigator.share) {
19
+ navigator.share(data).catch(() => copyToClipboard(msg, toastMsg));
20
+ } else {
21
+ copyToClipboard(msg, toastMsg);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * copy the text to the clipboard
27
+ * @param text the text to copy to the clipboard
28
+ * @param toastMsg the message to show in the toast
29
+ */
30
+ export function copyToClipboard(
31
+ text: string,
32
+ toastMsg = "copied to clipboard"
33
+ ) {
34
+ navigator.clipboard.writeText(text);
35
+ if (toastMsg) showToast(toastMsg);
36
+ }