@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.
- package/dist/App.d.ts +4 -2
- package/dist/assets/explainer.worker-8428eb12.js.map +1 -1
- package/dist/components/button/base.d.ts +1 -0
- package/dist/components/button/defaultMini.d.ts +4 -0
- package/dist/components/button/primaryMini.d.ts +4 -0
- package/dist/components/dataTable/index.d.ts +10 -0
- package/dist/components/dataTable/pagination.d.ts +10 -0
- package/dist/components/dropdownContext/index.d.ts +13 -0
- package/dist/components/dropdownSelect/index.d.ts +17 -0
- package/dist/components/modal.d.ts +1 -0
- package/dist/components/tabs/defaultTab.d.ts +14 -0
- package/dist/components/tabs/{pureTab.d.ts → editableTab.d.ts} +2 -2
- package/dist/components/toolbar/toolbar-item.d.ts +1 -0
- package/dist/dataSource/dataSelection/config.d.ts +2 -0
- package/dist/dataSource/datasetConfig/index.d.ts +3 -0
- package/dist/dataSource/index.d.ts +1 -1
- package/dist/fields/datasetFields/dimFields.d.ts +2 -2
- package/dist/fields/datasetFields/meaFields.d.ts +2 -2
- package/dist/fields/encodeFields/singleEncodeDropDown.d.ts +17 -0
- package/dist/fields/encodeFields/singleEncodeEditor.d.ts +11 -0
- package/dist/fields/obComponents/obPill.d.ts +3 -3
- package/dist/graphic-walker.es.js +21038 -19133
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +247 -170
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/insightBoard/index.d.ts +1 -1
- package/dist/interfaces.d.ts +5 -0
- package/dist/renderer/index.d.ts +3 -1
- package/dist/segments/segmentNav.d.ts +3 -0
- package/dist/store/commonStore.d.ts +7 -1
- package/dist/store/visualSpecStore.d.ts +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/media.d.ts +2 -0
- package/dist/vis/react-vega.d.ts +2 -0
- package/dist/vis/theme.d.ts +130 -0
- package/package.json +2 -1
- package/src/App.tsx +74 -46
- package/src/components/button/base.ts +1 -0
- package/src/components/button/default.tsx +6 -2
- package/src/components/button/defaultMini.tsx +17 -0
- package/src/components/button/primary.tsx +6 -2
- package/src/components/button/primaryMini.tsx +21 -0
- package/src/components/callout.tsx +4 -1
- package/src/components/clickMenu.tsx +4 -2
- package/src/components/container.tsx +9 -0
- package/src/components/dataTable/index.tsx +177 -0
- package/src/components/dataTable/pagination.tsx +44 -0
- package/src/components/dataTypeIcon.tsx +1 -1
- package/src/components/dropdownContext/index.tsx +64 -0
- package/src/components/dropdownSelect/index.tsx +92 -0
- package/src/components/modal.tsx +26 -14
- package/src/components/tabs/defaultTab.tsx +43 -0
- package/src/components/tabs/{pureTab.tsx → editableTab.tsx} +7 -7
- package/src/components/toolbar/components.tsx +18 -1
- package/src/components/toolbar/index.tsx +5 -0
- package/src/components/toolbar/toolbar-button.tsx +6 -1
- package/src/components/toolbar/toolbar-item.tsx +4 -0
- package/src/components/toolbar/toolbar-select-button.tsx +21 -4
- package/src/components/toolbar/toolbar-toggle-button.tsx +6 -1
- package/src/components/tooltip.tsx +4 -1
- package/src/dataSource/dataSelection/config.ts +28 -0
- package/src/dataSource/dataSelection/csvData.tsx +77 -32
- package/src/dataSource/dataSelection/gwFile.tsx +0 -8
- package/src/dataSource/dataSelection/index.tsx +1 -2
- package/src/dataSource/dataSelection/publicData.tsx +2 -3
- package/src/dataSource/datasetConfig/index.tsx +21 -0
- package/src/dataSource/index.tsx +81 -61
- package/src/dataSource/table.tsx +11 -155
- package/src/fields/aestheticFields.tsx +3 -1
- package/src/fields/components.tsx +21 -2
- package/src/fields/datasetFields/dimFields.tsx +43 -35
- package/src/fields/datasetFields/index.tsx +2 -2
- package/src/fields/datasetFields/meaFields.tsx +73 -47
- package/src/fields/encodeFields/singleEncodeDropDown.tsx +92 -0
- package/src/fields/encodeFields/singleEncodeEditor.tsx +78 -0
- package/src/fields/filterField/filterEditDialog.tsx +2 -1
- package/src/fields/filterField/filterPill.tsx +1 -1
- package/src/fields/filterField/slider.tsx +1 -1
- package/src/fields/filterField/tabs.tsx +11 -21
- package/src/fields/obComponents/obPill.tsx +65 -35
- package/src/index.css +13 -0
- package/src/insightBoard/index.tsx +24 -23
- package/src/insightBoard/radioGroupButtons.tsx +7 -0
- package/src/interfaces.ts +6 -0
- package/src/lib/inferMeta.ts +1 -1
- package/src/locales/en-US.json +14 -3
- package/src/locales/zh-CN.json +12 -1
- package/src/main.tsx +1 -1
- package/src/renderer/index.tsx +3 -1
- package/src/segments/segmentNav.tsx +58 -0
- package/src/segments/visNav.tsx +2 -2
- package/src/store/commonStore.ts +28 -2
- package/src/store/visualSpecStore.ts +16 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/media.ts +26 -0
- package/src/utils/normalization.ts +2 -1
- package/src/vis/react-vega.tsx +20 -4
- package/src/vis/theme.ts +126 -0
- 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-
|
|
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;
|
package/src/components/modal.tsx
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
? "
|
|
68
|
-
: "text-gray-500 hover:text-gray-700",
|
|
69
|
-
"whitespace-nowrap border-gray-200 py-1 px-2
|
|
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={
|
|
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 = '-';
|