@noya-app/noya-designsystem 0.1.38 → 0.1.40

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/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +16 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +416 -321
  5. package/dist/index.d.ts +416 -321
  6. package/dist/index.js +9555 -11325
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5057 -6832
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -4
  11. package/src/components/ActivityIndicator.tsx +13 -4
  12. package/src/components/AnimatePresence.tsx +261 -0
  13. package/src/components/Avatar.tsx +202 -80
  14. package/src/components/Button.tsx +10 -6
  15. package/src/components/Checkbox.tsx +10 -9
  16. package/src/components/Chip.tsx +88 -132
  17. package/src/components/{InputFieldWithCompletions.tsx → Combobox.tsx} +10 -3
  18. package/src/components/ContextMenu.tsx +9 -5
  19. package/src/components/Dialog.tsx +20 -23
  20. package/src/components/Divider.tsx +15 -7
  21. package/src/components/DraggableMenuButton.tsx +9 -5
  22. package/src/components/DropdownMenu.tsx +8 -5
  23. package/src/components/FillInputField.tsx +5 -1
  24. package/src/components/FillPreviewBackground.tsx +1 -5
  25. package/src/components/GridView.tsx +71 -46
  26. package/src/components/InputField.tsx +140 -75
  27. package/src/components/InspectorPrimitives.tsx +17 -11
  28. package/src/components/Label.tsx +5 -1
  29. package/src/components/ListView.tsx +28 -15
  30. package/src/components/Progress.tsx +10 -7
  31. package/src/components/SelectMenu.tsx +20 -8
  32. package/src/components/Slider.tsx +70 -8
  33. package/src/components/Sortable.tsx +3 -3
  34. package/src/components/Switch.tsx +19 -2
  35. package/src/components/Text.tsx +5 -1
  36. package/src/components/TextArea.tsx +5 -1
  37. package/src/components/TreeView.tsx +4 -10
  38. package/src/components/UserPointer.tsx +170 -0
  39. package/src/components/WorkspaceLayout.tsx +5 -0
  40. package/src/components/internal/Menu.tsx +14 -5
  41. package/src/components/internal/TextInput.tsx +13 -0
  42. package/src/contexts/DialogContext.tsx +30 -18
  43. package/src/hooks/useTheme.ts +50 -0
  44. package/src/index.css +16 -13
  45. package/src/index.tsx +14 -26
  46. package/src/utils/classNames.ts +5 -0
  47. package/src/utils/colorFromString.ts +44 -0
  48. package/tailwind.config.ts +16 -13
  49. package/src/hooks/useDarkMode.ts +0 -14
  50. package/src/utils/tailwind.ts +0 -9
@@ -0,0 +1,170 @@
1
+ import React, { memo, useMemo } from "react";
2
+ import { cx } from "../utils/classNames";
3
+ import { colorFromString } from "../utils/colorFromString";
4
+
5
+ type Point = { x: number; y: number };
6
+
7
+ const UserPointerContainer = memo(function UserPointerContainer({
8
+ point,
9
+ visible,
10
+ children,
11
+ className,
12
+ style,
13
+ }: {
14
+ point: Point;
15
+ visible?: boolean;
16
+ children: React.ReactNode;
17
+ className?: string;
18
+ style?: React.CSSProperties;
19
+ }) {
20
+ return (
21
+ <div
22
+ style={{
23
+ transform: `translate(${Math.round(point.x)}px, ${Math.round(point.y)}px)`,
24
+ transition: "transform 0.075s, opacity 0.2s",
25
+ ...style,
26
+ }}
27
+ className={cx(
28
+ "pointer-events-none",
29
+ visible ? "opacity-100" : "opacity-0",
30
+ className
31
+ )}
32
+ >
33
+ {children}
34
+ </div>
35
+ );
36
+ });
37
+
38
+ const UserNameTag = memo(function UserNameTag({
39
+ backgroundColor,
40
+ className,
41
+ style,
42
+ children,
43
+ }: {
44
+ backgroundColor: string;
45
+ className?: string;
46
+ style?: React.CSSProperties;
47
+ children: React.ReactNode;
48
+ }) {
49
+ return (
50
+ <span
51
+ className={cx(
52
+ "relative inline-block rounded-full text-xs font-medium leading-none text-white shadow-sm px-2 py-1.5 border border-background",
53
+ className
54
+ )}
55
+ style={{
56
+ backgroundColor,
57
+ letterSpacing: "-0.3px",
58
+ ...style,
59
+ }}
60
+ >
61
+ {children}
62
+ </span>
63
+ );
64
+ });
65
+
66
+ const UserPointerIcon = memo(function PointerIcon({
67
+ size,
68
+ color = "black",
69
+ className,
70
+ style,
71
+ }: {
72
+ size: number;
73
+ color?: string;
74
+ className?: string;
75
+ style?: React.CSSProperties;
76
+ }) {
77
+ // Simple triangle shape: left point, top right, bottom right
78
+ const points = "0,5 10,0 10,10";
79
+
80
+ return (
81
+ <svg
82
+ width={size}
83
+ height={size}
84
+ className={className}
85
+ viewBox="0 0 10 10"
86
+ fill="none"
87
+ xmlns="http://www.w3.org/2000/svg"
88
+ style={{
89
+ transform: `translate(0, -${size / 2}px) rotate(45deg)`,
90
+ transformOrigin: "left center",
91
+ ...style,
92
+ }}
93
+ >
94
+ <polygon
95
+ points={points}
96
+ fill={color}
97
+ stroke="var(--background)"
98
+ strokeWidth="0.5"
99
+ />
100
+ </svg>
101
+ );
102
+ });
103
+
104
+ export type UserPointerProps = {
105
+ userId?: string;
106
+ name?: string;
107
+ /** label to display in the tooltip next to the Pointer */
108
+ visible?: boolean;
109
+ point: Point;
110
+ /** defaults to a random color based on the key, but can optionally be overridden */
111
+ backgroundColor?: string;
112
+ style?: React.CSSProperties;
113
+ className?: string;
114
+ };
115
+
116
+ const POINTER_SIZE = 12;
117
+ const POINTER_OVERLAP = 6;
118
+
119
+ export const UserPointer = memo(function UserPointer({
120
+ userId,
121
+ name,
122
+ visible = true,
123
+ point,
124
+ backgroundColor,
125
+ style,
126
+ className,
127
+ }: UserPointerProps) {
128
+ const userBackgroundColor = useMemo(
129
+ () => backgroundColor ?? colorFromString(userId ?? name ?? ""),
130
+ [backgroundColor, userId, name]
131
+ );
132
+
133
+ const pointerOverlapStyle = useMemo(
134
+ () => ({
135
+ marginRight: `-${POINTER_OVERLAP}px`,
136
+ marginBottom: `-${POINTER_OVERLAP}px`,
137
+ }),
138
+ []
139
+ );
140
+
141
+ const nameTagOverlapStyle = useMemo(
142
+ () => ({ marginLeft: `${POINTER_SIZE - POINTER_OVERLAP + 1}px` }),
143
+ []
144
+ );
145
+
146
+ return (
147
+ <UserPointerContainer
148
+ point={point}
149
+ visible={visible}
150
+ className={className}
151
+ style={style}
152
+ >
153
+ {name && (
154
+ <div className="relative">
155
+ <UserPointerIcon
156
+ size={POINTER_SIZE}
157
+ color={userBackgroundColor}
158
+ style={pointerOverlapStyle}
159
+ />
160
+ <UserNameTag
161
+ backgroundColor={userBackgroundColor}
162
+ style={nameTagOverlapStyle}
163
+ >
164
+ {name}
165
+ </UserNameTag>
166
+ </div>
167
+ )}
168
+ </UserPointerContainer>
169
+ );
170
+ });
@@ -33,6 +33,7 @@ interface Props {
33
33
  hasLeftSidebar?: boolean;
34
34
  onChangeLayoutState?: (layoutState: PanelLayoutState) => void;
35
35
  leftSidebarCanResize?: boolean;
36
+ rightSidebarCanResize?: boolean;
36
37
  id?: string;
37
38
  className?: string;
38
39
  style?: React.CSSProperties;
@@ -74,6 +75,7 @@ const WorkspaceLayoutWithTheme = forwardRef(function WorkspaceLayoutWithTheme(
74
75
  leftSidebarMinSize,
75
76
  rightSidebarMinSize,
76
77
  leftSidebarCanResize = false,
78
+ rightSidebarCanResize = false,
77
79
  id,
78
80
  className,
79
81
  style,
@@ -261,6 +263,9 @@ const WorkspaceLayoutWithTheme = forwardRef(function WorkspaceLayoutWithTheme(
261
263
  ref={rightSidebarRef}
262
264
  minSize={rightSidebarMinPercentage ?? rightSidebarPercentage}
263
265
  defaultSize={rightSidebarPercentage}
266
+ {...(!rightSidebarCanResize && {
267
+ maxSize: rightSidebarPercentage,
268
+ })}
264
269
  collapsible
265
270
  style={{
266
271
  display: "flex",
@@ -3,7 +3,6 @@ import React, { memo, ReactNode } from "react";
3
3
  import { useDesignSystemConfiguration } from "../../contexts/DesignSystemConfiguration";
4
4
  import withSeparatorElements from "../../utils/withSeparatorElements";
5
5
  import { IconName } from "../Icons";
6
- import { cn } from "../../utils/tailwind";
7
6
  import { textStyles } from "../Text";
8
7
 
9
8
  export const SEPARATOR_ITEM = "separator";
@@ -108,7 +107,7 @@ export const styles = {
108
107
  shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)]
109
108
  p-1
110
109
  border border-popover-divider
111
- z-50
110
+ z-menu
112
111
  `,
113
112
  };
114
113
 
@@ -135,9 +134,19 @@ export function getKeyboardShortcutsForMenuItems<T extends string>(
135
134
  );
136
135
  }
137
136
 
138
- const ShortcutElement = ({children, fixedWidth}: {children?: React.ReactNode; fixedWidth?: boolean}) => (
139
- <kbd className={cn(textStyles.small, "text-text-disabled", fixedWidth && "w-[0.9rem] text-center")}>{children}</kbd>
140
- )
137
+ const ShortcutElement = ({
138
+ children,
139
+ fixedWidth,
140
+ }: {
141
+ children?: React.ReactNode;
142
+ fixedWidth?: boolean;
143
+ }) => (
144
+ <kbd
145
+ className={`${textStyles.small} text-text-disabled ${fixedWidth && "w-[0.9rem] text-center"}`}
146
+ >
147
+ {children}
148
+ </kbd>
149
+ );
141
150
 
142
151
  export const KeyboardShortcut = memo(function KeyboardShortcut({
143
152
  shortcut,
@@ -60,6 +60,8 @@ const ReadOnlyTextInput = forwardRef(function ReadOnlyTextInput(
60
60
 
61
61
  type ControlledProps = Props & {
62
62
  onChange: (value: string) => void;
63
+ onFocus?: FocusEventHandler;
64
+ onBlur?: FocusEventHandler;
63
65
  };
64
66
 
65
67
  const ControlledTextInput = forwardRef(function ControlledTextInput(
@@ -70,6 +72,7 @@ const ControlledTextInput = forwardRef(function ControlledTextInput(
70
72
  onFocusChange,
71
73
  onBlur,
72
74
  onFocusCapture,
75
+ onFocus,
73
76
  ...rest
74
77
  }: ControlledProps,
75
78
  forwardedRef: ForwardedRef<HTMLInputElement>
@@ -92,6 +95,15 @@ const ControlledTextInput = forwardRef(function ControlledTextInput(
92
95
  [onFocusCapture, onFocusChange]
93
96
  );
94
97
 
98
+ const handleFocus = useCallback(
99
+ (event: React.FocusEvent<HTMLInputElement>) => {
100
+ onFocus?.(event);
101
+
102
+ onFocusChange?.(true);
103
+ },
104
+ [onFocus, onFocusChange]
105
+ );
106
+
95
107
  return (
96
108
  <input
97
109
  ref={forwardedRef}
@@ -105,6 +117,7 @@ const ControlledTextInput = forwardRef(function ControlledTextInput(
105
117
  )}
106
118
  onBlur={handleBlur}
107
119
  onFocusCapture={handleFocusCapture}
120
+ onFocus={handleFocus}
108
121
  />
109
122
  );
110
123
  });
@@ -26,8 +26,14 @@ function createDeferredPromise<T>() {
26
26
 
27
27
  export type DialogContextValue = {
28
28
  openInputDialog(
29
- title: string,
30
- inputValue?: string
29
+ options:
30
+ | string
31
+ | {
32
+ title: string;
33
+ description?: ReactNode;
34
+ placeholder?: string;
35
+ initialValue?: string;
36
+ }
31
37
  ): Promise<string | undefined>;
32
38
 
33
39
  containsElement(element: HTMLElement): boolean;
@@ -43,6 +49,8 @@ export const DialogProvider = function DialogProvider({
43
49
  const [contents, setContents] = useState<
44
50
  | {
45
51
  title: string;
52
+ description?: ReactNode;
53
+ placeholder?: string;
46
54
  inputValue: string;
47
55
  resolve: (value: string | undefined) => void;
48
56
  }
@@ -64,23 +72,25 @@ export const DialogProvider = function DialogProvider({
64
72
  setContents(undefined);
65
73
  }, [contents]);
66
74
 
67
- const open: DialogContextValue["openInputDialog"] = useCallback(
68
- (title, inputValue = "") => {
69
- const { promise, resolve } = createDeferredPromise<string | undefined>();
75
+ const open: DialogContextValue["openInputDialog"] = useCallback((options) => {
76
+ const { promise, resolve } = createDeferredPromise<string | undefined>();
70
77
 
71
- setContents({
72
- title,
73
- inputValue,
74
- resolve: (value) => {
75
- resolve(value);
76
- setContents(undefined);
77
- },
78
- });
78
+ const { title, description, initialValue, placeholder } =
79
+ typeof options === "string" ? { title: options } : options;
79
80
 
80
- return promise;
81
- },
82
- []
83
- );
81
+ setContents({
82
+ title,
83
+ description,
84
+ inputValue: initialValue ?? "",
85
+ placeholder,
86
+ resolve: (value) => {
87
+ resolve(value);
88
+ setContents(undefined);
89
+ },
90
+ });
91
+
92
+ return promise;
93
+ }, []);
84
94
 
85
95
  const handleKeyDown = useCallback(
86
96
  (event: React.KeyboardEvent<HTMLInputElement>) => {
@@ -116,6 +126,7 @@ export const DialogProvider = function DialogProvider({
116
126
  <Dialog
117
127
  ref={dialogRef}
118
128
  title={contents?.title}
129
+ description={contents?.description}
119
130
  open={isOpen}
120
131
  onOpenChange={useCallback(
121
132
  (isOpen: boolean) => {
@@ -138,6 +149,7 @@ export const DialogProvider = function DialogProvider({
138
149
  <InputField.Root>
139
150
  <InputField.Input
140
151
  ref={inputRef}
152
+ placeholder={contents?.placeholder}
141
153
  value={contents?.inputValue ?? ""}
142
154
  onChange={(value) => {
143
155
  setContents((contents) =>
@@ -153,7 +165,7 @@ export const DialogProvider = function DialogProvider({
153
165
  />
154
166
  </InputField.Root>
155
167
  <Spacer.Vertical size={20} />
156
- <div className="flex-1 flex-row items-center">
168
+ <div className="flex flex-1 flex-row items-center">
157
169
  <Spacer.Horizontal />
158
170
  <Button onClick={close}>Cancel</Button>
159
171
  <Spacer.Horizontal size={16} />
@@ -0,0 +1,50 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ /** @default light */
4
+ export type UseThemeReturnType = "dark" | "light";
5
+
6
+ /**
7
+ * Hook to get the current theme of the app via the `data-theme` attribute on the body element.
8
+ * @returns The current theme.
9
+ */
10
+ export function useTheme() {
11
+ const [theme, setTheme] = useState<UseThemeReturnType>("light");
12
+
13
+ const checkTheme = () => {
14
+ const currentTheme = document.body.getAttribute("data-theme");
15
+ switch (currentTheme) {
16
+ case "dark":
17
+ setTheme("dark");
18
+ break;
19
+ case "light":
20
+ setTheme("light");
21
+ break;
22
+ }
23
+ };
24
+
25
+ useEffect(() => {
26
+ checkTheme();
27
+
28
+ // Setup observer for theme changes
29
+ const observer = new MutationObserver((mutations) => {
30
+ mutations.forEach((mutation) => {
31
+ if (
32
+ mutation.type === "attributes" &&
33
+ mutation.attributeName === "data-theme"
34
+ ) {
35
+ checkTheme();
36
+ }
37
+ });
38
+ });
39
+
40
+ // Observe body element for theme changes
41
+ observer.observe(document.body, {
42
+ attributes: true,
43
+ attributeFilter: ["data-theme"],
44
+ });
45
+
46
+ return () => observer.disconnect();
47
+ }, []);
48
+
49
+ return theme;
50
+ }
package/src/index.css CHANGED
@@ -38,7 +38,7 @@
38
38
  --listview-raised-background: rgba(0, 0, 0, 0.03);
39
39
  --listview-editing-background: #fff;
40
40
  --slider-background: white;
41
- --slider-border: #bbb;
41
+ --slider-border: #9698ac;
42
42
  --radio-group-background: white;
43
43
  --mask: rgb(12, 193, 67);
44
44
  --transparent-checker: rgba(255, 255, 255, 0.8);
@@ -63,19 +63,21 @@
63
63
  --icon-selected: rgb(220, 220, 220);
64
64
  --warning: rgb(251, 211, 0);
65
65
  --radio-group-item: rgb(139, 139, 139);
66
- --dot: rgba(0,0,0,0.25);
67
- --row-highlight: #3390FF10;
66
+ --dot: rgba(0, 0, 0, 0.25);
67
+ --row-highlight: #3390ff10;
68
68
  --table-row-background: var(--background);
69
69
  --interactable-z-index: 2;
70
70
  --label-z-index: 3;
71
- --chip-primary-bg: rgba(238,229,255,0.2);
72
- --chip-secondary-bg: rgba(205,238,231,0.2);
73
- --chip-error-bg: rgba(255,219,219,0.2);
74
- --chip-default-bg: rgba(0,0,0,0.1);
75
- --chip-primary-shadow: rgb(238,229,255);
76
- --chip-secondary-shadow: rgb(205,238,231);
77
- --chip-error-shadow: rgb(255,219,219);
78
- --chip-default-shadow: rgb(0,0,0);
71
+ --menu-z-index: 5000;
72
+ --chip-primary-bg: rgba(238, 229, 255, 0.2);
73
+ --chip-secondary-bg: rgba(205, 238, 231, 0.2);
74
+ --chip-error-bg: rgba(255, 219, 219, 0.2);
75
+ --chip-default-bg: rgba(0, 0, 0, 0.1);
76
+ --chip-primary-shadow: rgb(238, 229, 255);
77
+ --chip-secondary-shadow: rgb(205, 238, 231);
78
+ --chip-error-shadow: rgb(255, 219, 219);
79
+ --chip-default-shadow: rgb(0, 0, 0);
80
+ --floating-button: rgb(248, 248, 250);
79
81
  }
80
82
 
81
83
  [data-theme="dark"] {
@@ -113,6 +115,7 @@
113
115
  --active-background: rgba(181, 178, 255, 0.08);
114
116
  --thumbnail-background: #1f1d33;
115
117
  --thumbnail-shadow: #1f1d3366;
116
- --dot: rgba(255,255,255,0.15);
118
+ --dot: rgba(255, 255, 255, 0.15);
117
119
  --table-row-background: var(--sidebar-background);
118
- }
120
+ --floating-button: #333333;
121
+ }
package/src/index.tsx CHANGED
@@ -1,24 +1,11 @@
1
- // These types propagate generics through memo and forwardRef to support generic components
2
- //
3
- // https://stackoverflow.com/questions/60386614/how-to-use-props-with-generics-with-react-memo/60389122#60389122
4
- // https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref/58473012
5
- declare module "react" {
6
- function memo<A, B>(
7
- Component: (props: A) => B
8
- ): (props: A) => React.ReactElement | null;
9
-
10
- function forwardRef<T, P = {}>(
11
- render: (props: P, ref: React.ForwardedRef<T>) => React.ReactElement | null
12
- ): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
13
- }
14
-
15
- // Theme
16
1
  // Components
17
2
  export * from "./components/ActivityIndicator";
3
+ export * from "./components/AnimatePresence";
18
4
  export * from "./components/Avatar";
19
5
  export * from "./components/Button";
20
6
  export * from "./components/Checkbox";
21
7
  export * from "./components/Chip";
8
+ export * from "./components/Combobox";
22
9
  export * from "./components/ContextMenu";
23
10
  export * from "./components/Dialog";
24
11
  export * from "./components/Divider";
@@ -32,8 +19,13 @@ export * from "./components/GridView";
32
19
  export * from "./components/IconButton";
33
20
  export * from "./components/Icons";
34
21
  export * from "./components/InputField";
35
- export * from "./components/InputFieldWithCompletions";
36
22
  export * from "./components/InspectorContainer";
23
+ export { KeyboardShortcut, SEPARATOR_ITEM } from "./components/internal/Menu";
24
+ export type {
25
+ ExtractMenuItemType,
26
+ MenuItem,
27
+ RegularMenuItem,
28
+ } from "./components/internal/Menu";
37
29
  export * from "./components/Label";
38
30
  export * from "./components/LabeledElementView";
39
31
  export * from "./components/ListView";
@@ -52,30 +44,26 @@ export * from "./components/TextArea";
52
44
  export * from "./components/Toast";
53
45
  export * from "./components/Tooltip";
54
46
  export * from "./components/TreeView";
47
+ export * from "./components/UserPointer";
55
48
  export * from "./components/WorkspaceLayout";
56
- export { KeyboardShortcut, SEPARATOR_ITEM } from "./components/internal/Menu";
57
- export type {
58
- ExtractMenuItemType,
59
- MenuItem,
60
- RegularMenuItem,
61
- } from "./components/internal/Menu";
62
49
  // Contexts
63
50
  export * from "./contexts/DesignSystemConfiguration";
64
51
  export * from "./contexts/DialogContext";
65
52
  export * from "./contexts/FloatingWindowContext";
66
- export * from "./contexts/ImageDataContext";
67
53
  export * from "./contexts/GlobalInputBlurContext";
54
+ export * from "./contexts/ImageDataContext";
68
55
  // Hooks
69
56
  export * from "./hooks/useHover";
70
57
  export * from "./hooks/usePlatform";
58
+ export * from "./hooks/usePreservePanelSize";
59
+ export * from "./hooks/useTheme";
71
60
  export * from "./mediaQuery";
72
61
  export * from "./theme";
73
- export { cn } from './utils/tailwind';
62
+ export * from "./utils/classNames";
74
63
  export * from "./utils/createSectionedMenu";
75
64
  export * from "./utils/getGradientBackground";
76
- export * from "./hooks/usePreservePanelSize";
77
- export * from './hooks/useDarkMode';
78
65
  // Utils
66
+ export * from "./utils/colorFromString";
79
67
  export * from "./utils/completions";
80
68
  export * from "./utils/fuzzyScorer";
81
69
  export * from "./utils/sketchColor";
@@ -0,0 +1,5 @@
1
+ export function cx(
2
+ ...args: Array<string | number | boolean | null | undefined>
3
+ ): string {
4
+ return args.filter(Boolean).join(" ").trim();
5
+ }
@@ -0,0 +1,44 @@
1
+ export const colorForStringValues: Record<string, string> = {
2
+ red: "#ef4444", // red-500
3
+ orange: "#f97316", // orange-500
4
+ amber: "#f59e0b", // amber-500
5
+ yellow: "#eab308", // yellow-500
6
+ lime: "#84cc16", // lime-500
7
+ green: "#22c55e", // green-500
8
+ emerald: "#10b981", // emerald-500
9
+ teal: "#14b8a6", // teal-500
10
+ cyan: "#06b6d4", // cyan-500
11
+ sky: "#0ea5e9", // sky-500
12
+ blue: "#3b82f6", // blue-500
13
+ indigo: "#6366f1", // indigo-500
14
+ violet: "#8b5cf6", // violet-500
15
+ purple: "#a855f7", // purple-500
16
+ fuchsia: "#d946ef", // fuchsia-500
17
+ pink: "#ec4899", // pink-500
18
+ rose: "#f43f5e", // rose-500
19
+ };
20
+
21
+ function hashCode(string: string) {
22
+ let hash = 0;
23
+ for (const char of string) {
24
+ hash = char.charCodeAt(0) + ((hash << 5) - hash);
25
+ }
26
+ return hash;
27
+ }
28
+
29
+ function elementFromHash<T>(array: T[], hash: number): T {
30
+ return array[Math.abs(hash) % array.length];
31
+ }
32
+
33
+ /**
34
+ * Create a background color and initials from a name
35
+ * Adapted from (MIT) https://mui.com/material-ui/react-avatar/
36
+ */
37
+ export function colorFromString(str: string) {
38
+ const hash = hashCode(str);
39
+ const backgroundColor = elementFromHash(
40
+ Object.values(colorForStringValues),
41
+ hash
42
+ );
43
+ return backgroundColor;
44
+ }