@kanaries/graphic-walker 0.2.12 → 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 (99) hide show
  1. package/dist/App.d.ts +4 -2
  2. package/dist/assets/explainer.worker-8428eb12.js.map +1 -1
  3. package/dist/components/button/base.d.ts +1 -0
  4. package/dist/components/button/defaultMini.d.ts +4 -0
  5. package/dist/components/button/primaryMini.d.ts +4 -0
  6. package/dist/components/dataTable/index.d.ts +10 -0
  7. package/dist/components/dataTable/pagination.d.ts +10 -0
  8. package/dist/components/dropdownContext/index.d.ts +13 -0
  9. package/dist/components/dropdownSelect/index.d.ts +17 -0
  10. package/dist/components/modal.d.ts +1 -0
  11. package/dist/components/tabs/defaultTab.d.ts +14 -0
  12. package/dist/components/tabs/{pureTab.d.ts → editableTab.d.ts} +2 -2
  13. package/dist/components/toolbar/toolbar-item.d.ts +1 -0
  14. package/dist/dataSource/dataSelection/config.d.ts +2 -0
  15. package/dist/dataSource/datasetConfig/index.d.ts +3 -0
  16. package/dist/dataSource/index.d.ts +1 -1
  17. package/dist/fields/datasetFields/dimFields.d.ts +2 -2
  18. package/dist/fields/datasetFields/meaFields.d.ts +2 -2
  19. package/dist/fields/encodeFields/singleEncodeDropDown.d.ts +17 -0
  20. package/dist/fields/encodeFields/singleEncodeEditor.d.ts +11 -0
  21. package/dist/fields/obComponents/obPill.d.ts +3 -3
  22. package/dist/graphic-walker.es.js +21038 -19133
  23. package/dist/graphic-walker.es.js.map +1 -1
  24. package/dist/graphic-walker.umd.js +247 -170
  25. package/dist/graphic-walker.umd.js.map +1 -1
  26. package/dist/insightBoard/index.d.ts +1 -1
  27. package/dist/interfaces.d.ts +5 -0
  28. package/dist/renderer/index.d.ts +3 -1
  29. package/dist/segments/segmentNav.d.ts +3 -0
  30. package/dist/store/commonStore.d.ts +7 -1
  31. package/dist/store/visualSpecStore.d.ts +1 -0
  32. package/dist/utils/index.d.ts +2 -0
  33. package/dist/utils/media.d.ts +2 -0
  34. package/dist/vis/react-vega.d.ts +2 -0
  35. package/dist/vis/theme.d.ts +130 -0
  36. package/package.json +2 -1
  37. package/src/App.tsx +74 -46
  38. package/src/components/button/base.ts +1 -0
  39. package/src/components/button/default.tsx +6 -2
  40. package/src/components/button/defaultMini.tsx +17 -0
  41. package/src/components/button/primary.tsx +6 -2
  42. package/src/components/button/primaryMini.tsx +21 -0
  43. package/src/components/callout.tsx +4 -1
  44. package/src/components/clickMenu.tsx +4 -2
  45. package/src/components/container.tsx +9 -0
  46. package/src/components/dataTable/index.tsx +177 -0
  47. package/src/components/dataTable/pagination.tsx +44 -0
  48. package/src/components/dataTypeIcon.tsx +1 -1
  49. package/src/components/dropdownContext/index.tsx +64 -0
  50. package/src/components/dropdownSelect/index.tsx +92 -0
  51. package/src/components/modal.tsx +26 -14
  52. package/src/components/tabs/defaultTab.tsx +43 -0
  53. package/src/components/tabs/{pureTab.tsx → editableTab.tsx} +7 -7
  54. package/src/components/toolbar/components.tsx +18 -1
  55. package/src/components/toolbar/index.tsx +5 -0
  56. package/src/components/toolbar/toolbar-button.tsx +6 -1
  57. package/src/components/toolbar/toolbar-item.tsx +4 -0
  58. package/src/components/toolbar/toolbar-select-button.tsx +21 -4
  59. package/src/components/toolbar/toolbar-toggle-button.tsx +6 -1
  60. package/src/components/tooltip.tsx +4 -1
  61. package/src/dataSource/dataSelection/config.ts +28 -0
  62. package/src/dataSource/dataSelection/csvData.tsx +77 -32
  63. package/src/dataSource/dataSelection/gwFile.tsx +0 -8
  64. package/src/dataSource/dataSelection/index.tsx +1 -2
  65. package/src/dataSource/dataSelection/publicData.tsx +2 -3
  66. package/src/dataSource/datasetConfig/index.tsx +21 -0
  67. package/src/dataSource/index.tsx +81 -61
  68. package/src/dataSource/table.tsx +11 -155
  69. package/src/fields/aestheticFields.tsx +3 -1
  70. package/src/fields/components.tsx +21 -2
  71. package/src/fields/datasetFields/dimFields.tsx +43 -35
  72. package/src/fields/datasetFields/index.tsx +2 -2
  73. package/src/fields/datasetFields/meaFields.tsx +73 -47
  74. package/src/fields/encodeFields/singleEncodeDropDown.tsx +92 -0
  75. package/src/fields/encodeFields/singleEncodeEditor.tsx +78 -0
  76. package/src/fields/filterField/filterEditDialog.tsx +2 -1
  77. package/src/fields/filterField/filterPill.tsx +1 -1
  78. package/src/fields/filterField/slider.tsx +1 -1
  79. package/src/fields/filterField/tabs.tsx +11 -21
  80. package/src/fields/obComponents/obPill.tsx +65 -35
  81. package/src/index.css +13 -0
  82. package/src/insightBoard/index.tsx +24 -23
  83. package/src/insightBoard/radioGroupButtons.tsx +7 -0
  84. package/src/interfaces.ts +6 -0
  85. package/src/lib/inferMeta.ts +1 -1
  86. package/src/locales/en-US.json +14 -3
  87. package/src/locales/zh-CN.json +12 -1
  88. package/src/main.tsx +1 -1
  89. package/src/renderer/index.tsx +3 -1
  90. package/src/segments/segmentNav.tsx +58 -0
  91. package/src/segments/visNav.tsx +2 -2
  92. package/src/store/commonStore.ts +28 -2
  93. package/src/store/visualSpecStore.ts +16 -0
  94. package/src/utils/index.ts +19 -0
  95. package/src/utils/media.ts +26 -0
  96. package/src/utils/normalization.ts +2 -1
  97. package/src/vis/react-vega.tsx +20 -4
  98. package/src/vis/theme.ts +126 -0
  99. package/src/visualSettings/index.tsx +24 -8
@@ -0,0 +1,177 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import styled from "styled-components";
3
+ import { IMutField, IRow } from "../../interfaces";
4
+ import { useTranslation } from "react-i18next";
5
+ import Pagination from "./pagination";
6
+ import { ChevronUpDownIcon } from "@heroicons/react/24/outline";
7
+ import DropdownContext from "../dropdownContext";
8
+
9
+ interface DataTableProps {
10
+ size?: number;
11
+ metas: IMutField[];
12
+ data: IRow[];
13
+ onMetaChange: (fid: string, fIndex: number, meta: Partial<IMutField>) => void;
14
+ }
15
+ const Container = styled.div`
16
+ overflow-x: auto;
17
+ max-height: 660px;
18
+ overflow-y: auto;
19
+ table {
20
+ box-sizing: content-box;
21
+ border-collapse: collapse;
22
+ font-size: 12px;
23
+ tbody {
24
+ td {
25
+ }
26
+ td.number {
27
+ text-align: right;
28
+ }
29
+ td.text {
30
+ text-align: left;
31
+ }
32
+ }
33
+ }
34
+ `;
35
+ const ANALYTIC_TYPE_LIST = ["dimension", "measure"];
36
+ const SEMANTIC_TYPE_LIST = ["nominal", "ordinal", "quantitative", "temporal"];
37
+ // function getCellType(field: IMutField): 'number' | 'text' {
38
+ // return field.dataType === 'number' || field.dataType === 'integer' ? 'number' : 'text';
39
+ // }
40
+ function getHeaderType(field: IMutField): "number" | "text" {
41
+ return field.analyticType === "dimension" ? "text" : "number";
42
+ }
43
+
44
+ function getHeaderClassNames(field: IMutField) {
45
+ return field.analyticType === "dimension" ? "border-t-4 border-blue-400" : "border-t-4 border-purple-400";
46
+ }
47
+
48
+ function getSemanticColors(field: IMutField): string {
49
+ switch (field.semanticType) {
50
+ case "nominal":
51
+ return "bg-sky-100 text-sky-800";
52
+ case "ordinal":
53
+ return "bg-indigo-100 text-indigo-800";
54
+ case "quantitative":
55
+ return "bg-purple-100 text-purple-800";
56
+ case "temporal":
57
+ return "bg-yellow-100 text-yellow-800";
58
+ default:
59
+ return "bg-gray-400";
60
+ }
61
+ }
62
+
63
+ const DataTable: React.FC<DataTableProps> = (props) => {
64
+ const { size = 10, data, metas, onMetaChange } = props;
65
+ const [pageIndex, setPageIndex] = useState(0);
66
+ const { t } = useTranslation();
67
+
68
+ const analyticTypeList = useMemo<{ value: string; label: string }[]>(() => {
69
+ return ANALYTIC_TYPE_LIST.map((at) => ({
70
+ value: at,
71
+ label: t(`constant.analytic_type.${at}`),
72
+ }));
73
+ }, []);
74
+
75
+ const semanticTypeList = useMemo<{ value: string; label: string }[]>(() => {
76
+ return SEMANTIC_TYPE_LIST.map((st) => ({
77
+ value: st,
78
+ label: t(`constant.semantic_type.${st}`),
79
+ }));
80
+ }, []);
81
+
82
+ const from = pageIndex * size;
83
+ const to = Math.min((pageIndex + 1) * size, data.length - 1);
84
+
85
+ return (
86
+ <Container className="rounded border-gray-200 dark:border-gray-700 border">
87
+ <Pagination
88
+ total={data.length}
89
+ from={from + 1}
90
+ to={to + 1}
91
+ onNext={() => {
92
+ setPageIndex(Math.min(Math.ceil(data.length / size) - 1, pageIndex + 1));
93
+ }}
94
+ onPrev={() => {
95
+ setPageIndex(Math.max(0, pageIndex - 1));
96
+ }}
97
+ />
98
+ <table className="min-w-full divide-y">
99
+ <thead className="bg-gray-50 dark:bg-gray-900">
100
+ <tr className="divide-x divide-gray-200 dark:divide-gray-600">
101
+ {metas.map((field, fIndex) => (
102
+ <th key={field.fid} className={""}>
103
+ <div
104
+ className={
105
+ getHeaderClassNames(field) +
106
+ " whitespace-nowrap py-3.5 px-6 text-left text-xs font-semibold text-gray-900 dark:text-gray-50 sm:pl-6"
107
+ }
108
+ >
109
+ <b>{field.name || field.fid}</b>
110
+ <div>
111
+ <DropdownContext
112
+ options={analyticTypeList}
113
+ onSelect={(value) => {
114
+ onMetaChange(field.fid, fIndex, {
115
+ analyticType: value as IMutField["analyticType"],
116
+ });
117
+ }}
118
+ >
119
+ <span
120
+ className={
121
+ "cursor-pointer inline-flex px-2.5 py-0.5 text-xs font-medium mt-1 rounded-full text-xs text-white " +
122
+ (field.analyticType === "dimension" ? "bg-blue-500" : "bg-purple-500")
123
+ }
124
+ >
125
+ {field.analyticType}
126
+ <ChevronUpDownIcon className="ml-2 w-3" />
127
+ </span>
128
+ </DropdownContext>
129
+ </div>
130
+ <div>
131
+ <DropdownContext
132
+ options={semanticTypeList}
133
+ onSelect={(value) => {
134
+ onMetaChange(field.fid, fIndex, {
135
+ semanticType: value as IMutField["semanticType"],
136
+ });
137
+ }}
138
+ >
139
+ <span
140
+ className={
141
+ "cursor-pointer inline-flex px-2.5 py-0.5 text-xs font-medium mt-1 rounded-full text-xs " +
142
+ getSemanticColors(field)
143
+ }
144
+ >
145
+ {field.semanticType}
146
+ <ChevronUpDownIcon className="ml-2 w-3" />
147
+ </span>
148
+ </DropdownContext>
149
+ </div>
150
+ </div>
151
+ </th>
152
+ ))}
153
+ </tr>
154
+ </thead>
155
+ <tbody className="divide-y divide-gray-100 dark:divide-gray-600 bg-white dark:bg-zinc-900">
156
+ {data.slice(from, to).map((row, index) => (
157
+ <tr className={"divide-x divide-gray-200 dark:divide-gray-600 " + (index % 2 ? "bg-gray-50 dark:bg-gray-800" : "")} key={index}>
158
+ {metas.map((field) => (
159
+ <td
160
+ key={field.fid + index}
161
+ className={
162
+ getHeaderType(field) +
163
+ " whitespace-nowrap py-2 pl-4 pr-3 text-xs text-gray-500 dark:text-gray-300 sm:pl-6"
164
+ }
165
+ >
166
+ {row[field.fid]}
167
+ </td>
168
+ ))}
169
+ </tr>
170
+ ))}
171
+ </tbody>
172
+ </table>
173
+ </Container>
174
+ );
175
+ };
176
+
177
+ export default DataTable;
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ interface PaginationProps {
4
+ from: number;
5
+ to: number;
6
+ total: number;
7
+ onPrev: () => void;
8
+ onNext: () => void;
9
+ }
10
+ export default function Pagination(props: PaginationProps) {
11
+ const { from , to, total, onNext, onPrev } = props;
12
+ const { t } = useTranslation();
13
+ return (
14
+ <nav
15
+ className="flex items-center justify-between border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-zinc-900 px-4 py-3 sm:px-6"
16
+ aria-label="Pagination"
17
+ >
18
+ <div className="hidden sm:block">
19
+ <p className="text-sm text-gray-800 dark:text-gray-100">
20
+ Showing <span className="font-medium">{from}</span> to <span className="font-medium">{to}</span> of{" "}
21
+ <span className="font-medium">{total}</span> results
22
+ </p>
23
+ </div>
24
+ <div className="flex flex-1 justify-between sm:justify-end">
25
+ <button
26
+ onClick={() => {
27
+ onPrev();
28
+ }}
29
+ className="relative inline-flex items-center rounded-md border border-gray-300 bg-white dark:bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800"
30
+ >
31
+ {t('actions.prev')}
32
+ </button>
33
+ <button
34
+ onClick={() => {
35
+ onNext()
36
+ }}
37
+ className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white dark:bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800"
38
+ >
39
+ {t('actions.next')}
40
+ </button>
41
+ </div>
42
+ </nav>
43
+ );
44
+ }
@@ -6,7 +6,7 @@ const DataTypeIcon: React.FC<{ dataType: IMutField["semanticType"]; analyticType
6
6
  props
7
7
  ) => {
8
8
  const { dataType, analyticType } = props;
9
- const color = analyticType === "dimension" ? "text-blue-500" : "text-green-500";
9
+ const color = analyticType === "dimension" ? "text-blue-500" : "text-purple-500";
10
10
  const iconClassName = `w-3 inline-block mr-0.5 ${color}`;
11
11
  switch (dataType) {
12
12
  case "quantitative":
@@ -0,0 +1,64 @@
1
+ import React, { Fragment } from "react";
2
+ import { Menu, Transition } from "@headlessui/react";
3
+
4
+ function classNames(...classes: string[]) {
5
+ return classes.filter(Boolean).join(" ");
6
+ }
7
+
8
+ export interface IDropdownContextOption {
9
+ label: string;
10
+ value: string;
11
+ disabled?: boolean;
12
+ }
13
+ interface IDropdownContextProps {
14
+ options?: IDropdownContextOption[];
15
+ disable?: boolean;
16
+ onSelect?: (value: string, index: number) => void;
17
+ }
18
+ const DropdownContext: React.FC<IDropdownContextProps> = (props) => {
19
+ const { options = [], disable } = props;
20
+
21
+ if (disable) {
22
+ return <Fragment>{props.children}</Fragment>;
23
+ }
24
+
25
+ return (
26
+ <Menu as="span" className="relative block text-left">
27
+ <Menu.Button className="block w-full text-left">{props.children}</Menu.Button>
28
+
29
+ <Transition
30
+ as={Fragment}
31
+ enter="transition ease-out duration-100"
32
+ enterFrom="transform opacity-0 scale-95"
33
+ enterTo="transform opacity-100 scale-100"
34
+ leave="transition ease-in duration-75"
35
+ leaveFrom="transform opacity-100 scale-100"
36
+ leaveTo="transform opacity-0 scale-95"
37
+ >
38
+ <Menu.Items className="absolute left-0 z-50 mt-2 w-56 origin-top-right rounded-md bg-white dark:bg-zinc-900 shadow-lg border border-gray-50 dark:border-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none">
39
+ <div className="py-1">
40
+ {options.map((option, index) => (
41
+ <Menu.Item key={option.value}>
42
+ {(p) => (
43
+ <span
44
+ className={classNames(
45
+ p.active ? "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-50" : "text-gray-700 dark:text-gray-200",
46
+ "block px-4 py-2 text-sm"
47
+ )}
48
+ onClick={() => {
49
+ props.onSelect && !props.disable && props.onSelect(option.value, index);
50
+ }}
51
+ >
52
+ {option.label}
53
+ </span>
54
+ )}
55
+ </Menu.Item>
56
+ ))}
57
+ </div>
58
+ </Menu.Items>
59
+ </Transition>
60
+ </Menu>
61
+ );
62
+ };
63
+
64
+ export default DropdownContext;
@@ -0,0 +1,92 @@
1
+ import React, { Fragment } from "react";
2
+ import { Listbox, Menu, Transition } from "@headlessui/react";
3
+ import { CheckIcon, ChevronDownIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
4
+
5
+ function classNames(...classes: string[]) {
6
+ return classes.filter(Boolean).join(" ");
7
+ }
8
+
9
+ export interface IDropdownSelectOption {
10
+ label: string;
11
+ value: string;
12
+ disabled?: boolean;
13
+ }
14
+ interface IDropdownSelectProps {
15
+ options?: IDropdownSelectOption[];
16
+ disable?: boolean;
17
+ selectedKey: string;
18
+ onSelect?: (value: string) => void;
19
+ placeholder?: string;
20
+ className?: string;
21
+ buttonClassName?: string;
22
+ }
23
+ const DropdownSelect: React.FC<IDropdownSelectProps> = (props) => {
24
+ const { options = [], disable, selectedKey, onSelect, placeholder = "Select an option", className, buttonClassName } = props;
25
+
26
+ const selectedItem = options.find((op) => op.value === selectedKey);
27
+
28
+ if (disable) {
29
+ return <Fragment>{props.children}</Fragment>;
30
+ }
31
+ let rootClassName = "relative";
32
+ let btnComputedClassName = "relative cursor-default text-xs rounded-lg bg-white dark:bg-zinc-900 px-2.5 py-1.5 pr-10 text-left border border-gray-200 dark:border-gray-700 focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 truncate"
33
+ if (buttonClassName) {
34
+ btnComputedClassName = btnComputedClassName + " " + buttonClassName;
35
+ }
36
+ if (className) {
37
+ rootClassName = rootClassName + " " + className;
38
+ }
39
+ return (
40
+ <Listbox
41
+ value={selectedKey}
42
+ onChange={(newKey) => {
43
+ onSelect && onSelect(newKey);
44
+ }}
45
+ >
46
+ <div className={rootClassName}>
47
+ <Listbox.Button className={btnComputedClassName}>
48
+ <span className="block truncate dark:text-white">{selectedItem?.label || ""}</span>
49
+ { selectedItem === undefined && <span className="block truncate text-gray-400">{placeholder}</span>}
50
+ <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
51
+ <ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
52
+ </span>
53
+ </Listbox.Button>
54
+ <Transition
55
+ as={Fragment}
56
+ leave="transition ease-in duration-100"
57
+ leaveFrom="opacity-100"
58
+ leaveTo="opacity-0"
59
+ >
60
+ <Listbox.Options className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
61
+ {options.map((op, opIndex) => (
62
+ <Listbox.Option
63
+ key={op.value}
64
+ className={({ active }) =>
65
+ `relative cursor-default select-none py-2 pl-10 pr-4 ${
66
+ active ? "bg-amber-100 text-amber-900 dark:bg-amber-800 dark:text-amber-50" : "text-gray-900 dark:text-gray-50"
67
+ }`
68
+ }
69
+ value={op.value}
70
+ >
71
+ {({ selected }) => (
72
+ <>
73
+ <span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>
74
+ {op.label}
75
+ </span>
76
+ {selected && (
77
+ <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600 dark:text-amber-400">
78
+ <CheckIcon className="h-5 w-5" aria-hidden="true" />
79
+ </span>
80
+ )}
81
+ </>
82
+ )}
83
+ </Listbox.Option>
84
+ ))}
85
+ </Listbox.Options>
86
+ </Transition>
87
+ </div>
88
+ </Listbox>
89
+ );
90
+ };
91
+
92
+ export default DropdownSelect;
@@ -1,6 +1,9 @@
1
1
  import React, { useRef } from "react";
2
2
  import styled from "styled-components";
3
3
  import { XCircleIcon } from "@heroicons/react/24/outline";
4
+ import { Fragment, useState } from "react";
5
+ import { Dialog, Transition } from "@headlessui/react";
6
+ import { ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/24/outline";
4
7
 
5
8
  const Background = styled.div({
6
9
  position: "fixed",
@@ -28,27 +31,33 @@ const Container = styled.div`
28
31
  padding: 12px;
29
32
  font-size: 14px;
30
33
  align-items: center;
34
+ @media (prefers-color-scheme: dark) {
35
+ background-color: #000;
36
+ }
31
37
  }
32
38
  > div.container {
33
- padding: 1em;
39
+ padding: 0.5em 1em 1em 1em;
34
40
  }
35
41
  position: fixed;
36
42
  left: 50%;
37
43
  top: 50%;
38
44
  transform: translate(-50%, -50%);
39
45
  background-color: #fff;
46
+ @media (prefers-color-scheme: dark) {
47
+ background-color: #000;
48
+ }
40
49
  /* box-shadow: 0px 0px 12px 3px rgba(0, 0, 0, 0.19); */
41
50
  border-radius: 4px;
42
51
  z-index: 999;
43
52
  `;
44
53
  interface ModalProps {
45
54
  onClose?: () => void;
55
+ show?: boolean;
46
56
  title?: string;
47
57
  }
48
58
  const Modal: React.FC<ModalProps> = (props) => {
49
- const { onClose, title } = props;
59
+ const { onClose, title, show } = props;
50
60
  const prevMouseDownTimeRef = useRef(0);
51
-
52
61
  return (
53
62
  <Background
54
63
  // This is a safer replacement of onClick handler.
@@ -56,7 +65,7 @@ const Modal: React.FC<ModalProps> = (props) => {
56
65
  // at a different element and then released when the mouse is moved on the target element.
57
66
  // This case is required to be prevented, especially disturbing when interacting
58
67
  // with a Slider component.
59
- className="border border-gray-300"
68
+ className={"border border-gray-300 dark:border-gray-600 " + (show ? "block" : "hidden")}
60
69
  onMouseDown={() => (prevMouseDownTimeRef.current = Date.now())}
61
70
  onMouseOut={() => (prevMouseDownTimeRef.current = 0)}
62
71
  onMouseUp={() => {
@@ -65,17 +74,20 @@ const Modal: React.FC<ModalProps> = (props) => {
65
74
  }
66
75
  }}
67
76
  >
68
- <Container role="dialog" className="shadow-lg" onMouseDown={(e) => e.stopPropagation()}>
69
- <div className="header relative h-9">
70
- <header className="font-bold">{title}</header>
71
- <XCircleIcon
72
- className="text-red-600 absolute right-2 w-6 cursor-pointer"
73
- role="button"
74
- tabIndex={0}
75
- aria-label="close dialog"
76
- onClick={onClose}
77
- />
77
+ <Container role="dialog" className="shadow-lg rounded-md border border-gray-100 dark:border-gray-800" onMouseDown={(e) => e.stopPropagation()}>
78
+ <div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
79
+ <button
80
+ type="button"
81
+ className="rounded-md bg-white dark:bg-zinc-900 text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
82
+ onClick={() => {
83
+ onClose?.();
84
+ }}
85
+ >
86
+ <span className="sr-only">Close</span>
87
+ <XMarkIcon className="h-6 w-6" aria-hidden="true" />
88
+ </button>
78
89
  </div>
90
+ <div className="px-6 pt-4 text-base font-semibold leading-6 text-gray-900 dark:text-gray-50">{title}</div>
79
91
  <div className="container">{props.children}</div>
80
92
  </Container>
81
93
  </Background>
@@ -0,0 +1,43 @@
1
+ import React, { ReactElement } from "react";
2
+
3
+ function classNames(...classes: string[]) {
4
+ return classes.filter(Boolean).join(' ')
5
+ }
6
+
7
+ export interface ITabOption {
8
+ label: string | ReactElement;
9
+ key: string;
10
+ }
11
+ interface DefaultProps {
12
+ tabs: ITabOption[];
13
+ selectedKey: string;
14
+ onSelected: (selectedKey: string, index: number) => void;
15
+ allowEdit?: boolean;
16
+ onEditLabel?: (label: string, index: number) => void;
17
+ }
18
+ export default function Default(props: DefaultProps) {
19
+ const { tabs, selectedKey, onSelected } = props;
20
+
21
+ return (
22
+ <div className="border-b border-gray-200 dark:border-gray-700 mb-2" >
23
+ <nav className="-mb-px flex space-x-8" role="tablist" aria-label="Tabs">
24
+ {tabs.map((tab, tabIndex) => (
25
+ <span
26
+ role="tab"
27
+ tabIndex={0}
28
+ onClick={() => {
29
+ onSelected(tab.key, tabIndex)
30
+ }}
31
+ key={tab.key}
32
+ className={classNames(
33
+ tab.key === selectedKey
34
+ ? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-300'
35
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300 dark:text-gray-400',
36
+ 'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm cursor-pointer'
37
+ )}
38
+ >{tab.label}</span>
39
+ ))}
40
+ </nav>
41
+ </div>
42
+ );
43
+ }
@@ -11,14 +11,14 @@ export interface ITabOption {
11
11
  key: string;
12
12
  options?: Record<string, any>;
13
13
  }
14
- interface PureTabsProps {
14
+ interface EditableTabsProps {
15
15
  tabs: ITabOption[];
16
16
  selectedKey: string;
17
17
  onSelected: (selectedKey: string, index: number) => void;
18
18
  allowEdit?: boolean;
19
19
  onEditLabel?: (label: string, index: number) => void;
20
20
  }
21
- export default function PureTabs(props: PureTabsProps) {
21
+ export default function EditableTabs(props: EditableTabsProps) {
22
22
  const { tabs, selectedKey, onSelected, allowEdit, onEditLabel } = props;
23
23
  const [editList, setEditList] = useState<boolean[]>([]);
24
24
  const { t } = useTranslation();
@@ -32,8 +32,8 @@ export default function PureTabs(props: PureTabsProps) {
32
32
  }, [clearEditStatus]);
33
33
 
34
34
  return (
35
- <div className="border-b border-gray-200 overflow-x-auto overflow-y-hidden" onMouseLeave={clearEditStatus}>
36
- <nav className="-mb-px flex h-8 border-gray-200 border-l" role="tablist" aria-label="Tabs">
35
+ <div className="border-b border-gray-200 dark:border-gray-800 overflow-x-auto overflow-y-hidden" onMouseLeave={clearEditStatus}>
36
+ <nav className="-mb-px flex h-8 border-gray-200 dark:border-gray-800" role="tablist" aria-label="Tabs">
37
37
  {tabs.map((tab, tabIndex) => (
38
38
  <span
39
39
  role="tab"
@@ -64,9 +64,9 @@ export default function PureTabs(props: PureTabsProps) {
64
64
  key={tab.key}
65
65
  className={classNames(
66
66
  tab.key === selectedKey
67
- ? "text-black bg-gray-100"
68
- : "text-gray-500 hover:text-gray-700",
69
- "whitespace-nowrap border-gray-200 py-1 px-2 border-t border-r border-b pr-6 text-sm cursor-pointer"
67
+ ? "border rounded-t"
68
+ : "text-gray-500 dark:text-gray-400 hover:text-gray-700 hover:bg-gray-50 dark:hover:text-gray-200 dark:hover:bg-gray-800",
69
+ "whitespace-nowrap border-gray-200 dark:border-gray-700 py-1 px-2 pr-6 text-sm cursor-pointer dark:text-white"
70
70
  )}
71
71
  />
72
72
  ))}
@@ -46,6 +46,14 @@ export const ToolbarContainer = styled.div`
46
46
  height: var(--height);
47
47
  background-color: var(--background-color);
48
48
  color: var(--color);
49
+ border: 1px solid;
50
+ border-color: #e5e7eb;
51
+ // dark mode
52
+ @media (prefers-color-scheme: dark) {
53
+ background-color: var(--background-color-dark);
54
+ color: var(--color-dark);
55
+ border-color: #4b5563;
56
+ }
49
57
  /* box-shadow: 0px 1px 3px 1px rgba(136, 136, 136, 0.1); */
50
58
  border-radius: 2px;
51
59
  overflow: hidden;
@@ -94,7 +102,6 @@ export const ToolbarItemContainerElement = styled.div<{ split: boolean }>`
94
102
  &[aria-disabled=false] {
95
103
  cursor: pointer;
96
104
  :hover, :focus, &.open {
97
- background-image: linear-gradient(#FFFFFFCC, #FEFEFECC);
98
105
  --background-color: #FEFEFE;
99
106
  color: var(--color-hover);
100
107
  &.split * svg {
@@ -104,6 +111,16 @@ export const ToolbarItemContainerElement = styled.div<{ split: boolean }>`
104
111
  & svg {
105
112
  text-shadow: 0 0 1.5px var(--shadow-color);
106
113
  }
114
+ background-color: var(--background-color);
115
+ }
116
+ }
117
+ @media (prefers-color-scheme: dark) {
118
+ color: var(--dark-mode-color);
119
+ &[aria-disabled=false] {
120
+ :hover, :focus, &.open {
121
+ --background-color: #202020;
122
+ color: var(--dark-mode-color-hover);
123
+ }
107
124
  }
108
125
  }
109
126
  transition: color 100ms, background-image 100ms;
@@ -11,6 +11,11 @@ const Root = styled.div`
11
11
  --color-hover: #555;
12
12
  --blue: #282958;
13
13
  --blue-dark: #1d1e38;
14
+ --dark-mode-background-color: #1f1f1f;
15
+ --dark-mode-color: #aaa;
16
+ --dark-mode-color-hover: #ccc;
17
+ --dark-mode-blue: #282958;
18
+ --dark-mode-blue-dark: #1d1e38;
14
19
  `;
15
20
 
16
21
  export interface ToolbarProps {
@@ -12,13 +12,18 @@ const ToolbarButton = memo<IToolbarProps<ToolbarButtonItem>>(function ToolbarBut
12
12
  const { icon: Icon, label, disabled, onClick } = item;
13
13
  const handlers = useHandlers(() => onClick?.(), disabled ?? false);
14
14
 
15
+ const mergedIconStyles = {
16
+ ...styles?.icon,
17
+ ...item.styles?.icon,
18
+ };
19
+
15
20
  return (
16
21
  <>
17
22
  <ToolbarItemContainer
18
23
  props={props}
19
24
  handlers={onClick ? handlers : null}
20
25
  >
21
- <Icon style={styles?.icon} />
26
+ <Icon style={mergedIconStyles} />
22
27
  </ToolbarItemContainer>
23
28
  </>
24
29
  );
@@ -35,6 +35,9 @@ const FormContainer = styled(ToolbarContainer)`
35
35
  width: max-content;
36
36
  height: max-content;
37
37
  background-color: #fff;
38
+ @media (prefers-color-scheme: dark) {
39
+ background-color: #000;
40
+ }
38
41
  `;
39
42
 
40
43
  export interface IToolbarItem {
@@ -48,6 +51,7 @@ export interface IToolbarItem {
48
51
  disabled?: boolean;
49
52
  menu?: ToolbarProps;
50
53
  form?: JSX.Element;
54
+ styles?: Partial<Pick<NonNullable<ToolbarProps['styles']>, 'item' | 'icon' | 'splitIcon'>>;
51
55
  }
52
56
 
53
57
  export const ToolbarItemSplitter = '-';