@kanaries/graphic-walker 0.2.13 → 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 (87) 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/dropdownContext/index.d.ts +13 -0
  7. package/dist/components/dropdownSelect/index.d.ts +17 -0
  8. package/dist/components/modal.d.ts +1 -0
  9. package/dist/components/toolbar/toolbar-item.d.ts +1 -0
  10. package/dist/dataSource/dataSelection/config.d.ts +2 -0
  11. package/dist/dataSource/index.d.ts +1 -1
  12. package/dist/fields/datasetFields/dimFields.d.ts +2 -2
  13. package/dist/fields/datasetFields/meaFields.d.ts +2 -2
  14. package/dist/fields/encodeFields/singleEncodeDropDown.d.ts +17 -0
  15. package/dist/fields/encodeFields/singleEncodeEditor.d.ts +11 -0
  16. package/dist/fields/obComponents/obPill.d.ts +3 -3
  17. package/dist/graphic-walker.es.js +20874 -19172
  18. package/dist/graphic-walker.es.js.map +1 -1
  19. package/dist/graphic-walker.umd.js +245 -170
  20. package/dist/graphic-walker.umd.js.map +1 -1
  21. package/dist/insightBoard/index.d.ts +1 -1
  22. package/dist/interfaces.d.ts +1 -0
  23. package/dist/renderer/index.d.ts +3 -1
  24. package/dist/store/visualSpecStore.d.ts +1 -0
  25. package/dist/utils/index.d.ts +2 -0
  26. package/dist/utils/media.d.ts +2 -0
  27. package/dist/vis/react-vega.d.ts +2 -0
  28. package/dist/vis/theme.d.ts +130 -0
  29. package/package.json +2 -1
  30. package/src/App.tsx +8 -5
  31. package/src/components/button/base.ts +1 -0
  32. package/src/components/button/default.tsx +6 -2
  33. package/src/components/button/defaultMini.tsx +17 -0
  34. package/src/components/button/primary.tsx +6 -2
  35. package/src/components/button/primaryMini.tsx +21 -0
  36. package/src/components/callout.tsx +4 -1
  37. package/src/components/clickMenu.tsx +4 -2
  38. package/src/components/container.tsx +9 -0
  39. package/src/components/dataTable/index.tsx +42 -52
  40. package/src/components/dataTable/pagination.tsx +4 -4
  41. package/src/components/dataTypeIcon.tsx +1 -1
  42. package/src/components/dropdownContext/index.tsx +64 -0
  43. package/src/components/dropdownSelect/index.tsx +92 -0
  44. package/src/components/modal.tsx +26 -14
  45. package/src/components/tabs/defaultTab.tsx +4 -4
  46. package/src/components/tabs/editableTab.tsx +5 -5
  47. package/src/components/toolbar/components.tsx +18 -1
  48. package/src/components/toolbar/index.tsx +5 -0
  49. package/src/components/toolbar/toolbar-button.tsx +6 -1
  50. package/src/components/toolbar/toolbar-item.tsx +4 -0
  51. package/src/components/toolbar/toolbar-select-button.tsx +21 -4
  52. package/src/components/toolbar/toolbar-toggle-button.tsx +6 -1
  53. package/src/components/tooltip.tsx +4 -1
  54. package/src/dataSource/dataSelection/config.ts +28 -0
  55. package/src/dataSource/dataSelection/csvData.tsx +77 -32
  56. package/src/dataSource/dataSelection/gwFile.tsx +0 -8
  57. package/src/dataSource/dataSelection/index.tsx +1 -2
  58. package/src/dataSource/dataSelection/publicData.tsx +2 -3
  59. package/src/dataSource/index.tsx +81 -61
  60. package/src/fields/aestheticFields.tsx +3 -1
  61. package/src/fields/components.tsx +21 -2
  62. package/src/fields/datasetFields/dimFields.tsx +43 -35
  63. package/src/fields/datasetFields/index.tsx +2 -2
  64. package/src/fields/datasetFields/meaFields.tsx +73 -47
  65. package/src/fields/encodeFields/singleEncodeDropDown.tsx +92 -0
  66. package/src/fields/encodeFields/singleEncodeEditor.tsx +78 -0
  67. package/src/fields/filterField/filterEditDialog.tsx +2 -1
  68. package/src/fields/filterField/filterPill.tsx +1 -1
  69. package/src/fields/filterField/slider.tsx +1 -1
  70. package/src/fields/filterField/tabs.tsx +11 -21
  71. package/src/fields/obComponents/obPill.tsx +65 -35
  72. package/src/index.css +13 -0
  73. package/src/insightBoard/index.tsx +24 -23
  74. package/src/insightBoard/radioGroupButtons.tsx +7 -0
  75. package/src/interfaces.ts +1 -0
  76. package/src/lib/inferMeta.ts +1 -1
  77. package/src/locales/en-US.json +7 -4
  78. package/src/locales/zh-CN.json +5 -2
  79. package/src/main.tsx +1 -1
  80. package/src/renderer/index.tsx +2 -1
  81. package/src/store/visualSpecStore.ts +16 -0
  82. package/src/utils/index.ts +19 -0
  83. package/src/utils/media.ts +26 -0
  84. package/src/utils/normalization.ts +2 -1
  85. package/src/vis/react-vega.tsx +20 -4
  86. package/src/vis/theme.ts +126 -0
  87. package/src/visualSettings/index.tsx +24 -8
@@ -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>
@@ -19,7 +19,7 @@ export default function Default(props: DefaultProps) {
19
19
  const { tabs, selectedKey, onSelected } = props;
20
20
 
21
21
  return (
22
- <div className="border-b border-gray-200 mb-2" >
22
+ <div className="border-b border-gray-200 dark:border-gray-700 mb-2" >
23
23
  <nav className="-mb-px flex space-x-8" role="tablist" aria-label="Tabs">
24
24
  {tabs.map((tab, tabIndex) => (
25
25
  <span
@@ -31,9 +31,9 @@ export default function Default(props: DefaultProps) {
31
31
  key={tab.key}
32
32
  className={classNames(
33
33
  tab.key === selectedKey
34
- ? 'border-indigo-500 text-indigo-600'
35
- : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
36
- 'whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm'
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
37
  )}
38
38
  >{tab.label}</span>
39
39
  ))}
@@ -32,8 +32,8 @@ export default function EditableTabs(props: EditableTabsProps) {
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 EditableTabs(props: EditableTabsProps) {
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-r border-t 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 = '-';
@@ -16,7 +16,21 @@ const OptionGroup = styled(ToolbarContainer)`
16
16
  --color: #777;
17
17
  --color-hover: #555;
18
18
  --blue: #282958;
19
+ --dark-mode-background-color: #1f1f1f;
20
+ --dark-mode-background-color-hover: #2f2f2f;
21
+ --dark-mode-color: #aaa;
22
+ --dark-mode-color-hover: #ccc;
23
+ --dark-mode-blue: #282958;
19
24
  background-color: var(--background-color);
25
+ // dark mode
26
+ @media (prefers-color-scheme: dark) {
27
+ /* --dark-mode-background-color: #1f1f1f;
28
+ --dark-mode-background-color-hover: #2f2f2f;
29
+ --dark-mode-color: #aaa;
30
+ --dark-mode-color-hover: #ccc;
31
+ --dark-mode-blue: #282958; */
32
+ background-color: var(--dark-mode-background-color);
33
+ }
20
34
  `;
21
35
 
22
36
  const Option = styled(ToolbarItemContainerElement)`
@@ -115,6 +129,11 @@ const ToolbarSelectButton = memo<IToolbarProps<ToolbarSelectButtonItem>>(functio
115
129
  const currentOption = options.find(opt => opt.key === value);
116
130
  const CurrentIcon = currentOption?.icon;
117
131
 
132
+ const mergedIconStyles = {
133
+ ...styles?.icon,
134
+ ...item.styles?.icon,
135
+ };
136
+
118
137
  return (
119
138
  <>
120
139
  <ToolbarItemContainer
@@ -126,20 +145,18 @@ const ToolbarSelectButton = memo<IToolbarProps<ToolbarSelectButtonItem>>(functio
126
145
  handlers={handlers}
127
146
  aria-haspopup="listbox"
128
147
  >
129
- <Icon style={styles?.icon} />
148
+ <Icon style={mergedIconStyles} />
130
149
  {CurrentIcon && (
131
150
  <CurrentIcon
132
151
  style={{
133
- ...styles?.icon,
134
152
  position: 'absolute',
135
153
  left: 'calc(var(--height) - var(--icon-size) * 1.2)',
136
154
  bottom: 'calc((var(--height) - var(--icon-size)) * 0.1)',
137
155
  width: 'calc(var(--icon-size) * 0.6)',
138
156
  height: 'calc(var(--icon-size) * 0.6)',
139
157
  margin: 'calc((var(--height) - var(--icon-size)) * 0.2)',
140
- filter: 'drop-shadow(0 0 0.5px var(--background-color)) '.repeat(4),
141
158
  pointerEvents: 'none',
142
- color: '#1d1e38',
159
+ ...mergedIconStyles,
143
160
  }}
144
161
  />
145
162
  )}
@@ -50,6 +50,11 @@ const ToolbarToggleButton = memo<IToolbarProps<ToolbarToggleButtonItem>>(functio
50
50
  const { icon: Icon, label, disabled, checked, onChange } = item;
51
51
  const handlers = useHandlers(() => onChange(!checked), disabled ?? false);
52
52
 
53
+ const mergedIconStyles = {
54
+ ...styles?.icon,
55
+ ...item.styles?.icon,
56
+ };
57
+
53
58
  return (
54
59
  <>
55
60
  <ToolbarItemContainer
@@ -59,7 +64,7 @@ const ToolbarToggleButton = memo<IToolbarProps<ToolbarToggleButtonItem>>(functio
59
64
  aria-checked={checked}
60
65
  >
61
66
  <ToggleContainer checked={checked}>
62
- <Icon style={styles?.icon} />
67
+ <Icon style={mergedIconStyles} />
63
68
  </ToggleContainer>
64
69
  </ToolbarItemContainer>
65
70
  </>
@@ -32,6 +32,9 @@ const Bubble = styled.div`
32
32
  height: 8px;
33
33
  transform: translate(-50%, 50%) rotate(45deg);
34
34
  background-color: #fff;
35
+ @media (prefers-color-scheme: dark) {
36
+ background-color: #000;
37
+ }
35
38
  border-radius: 1px;
36
39
  }
37
40
  `;
@@ -125,7 +128,7 @@ const Tooltip = memo<TooltipProps>(function Tooltip({
125
128
  root &&
126
129
  createPortal(
127
130
  <Bubble
128
- className="fixed text-xs p-1 px-3 text-gray-500 bg-white z-50"
131
+ className="fixed text-xs p-1 px-3 text-gray-500 bg-white dark:bg-zinc-900 z-50"
129
132
  onMouseOver={() => setHover(true)}
130
133
  onMouseOut={() => setHover(false)}
131
134
  style={{ left: pos[0], top: pos[1] - 4 }}
@@ -0,0 +1,28 @@
1
+ import { IDropdownSelectOption } from "../../components/dropdownSelect";
2
+
3
+ export const charsetOptions: IDropdownSelectOption[] = [
4
+ {
5
+ label: 'UTF-8',
6
+ value: 'utf-8',
7
+ },
8
+ {
9
+ label: 'GB2312',
10
+ value: 'gb2312',
11
+ },
12
+ {
13
+ label: 'US-ASCII',
14
+ value: 'us-ascii',
15
+ },
16
+ {
17
+ label: 'Big5',
18
+ value: 'big5',
19
+ },
20
+ {
21
+ label: 'Big5-HKSCS',
22
+ value: 'Big5-HKSCS',
23
+ },
24
+ {
25
+ label: 'GB18030',
26
+ value: 'GB18030',
27
+ },
28
+ ]
@@ -1,4 +1,4 @@
1
- import React, { useRef, useCallback } from "react";
1
+ import React, { useRef, useCallback, useState } from "react";
2
2
  import { FileReader } from "@kanaries/web-data-loader";
3
3
  import { IRow } from "../../interfaces";
4
4
  import Table from "../table";
@@ -8,25 +8,51 @@ import { observer } from "mobx-react-lite";
8
8
  import { useTranslation } from "react-i18next";
9
9
  import DefaultButton from "../../components/button/default";
10
10
  import PrimaryButton from "../../components/button/primary";
11
+ import DropdownSelect from "../../components/dropdownSelect";
12
+ import { charsetOptions } from "./config";
11
13
 
12
14
  const Container = styled.div`
13
15
  overflow-x: auto;
16
+ min-height: 300px;
14
17
  `;
15
18
 
16
19
  interface ICSVData {}
17
20
  const CSVData: React.FC<ICSVData> = (props) => {
18
21
  const fileRef = useRef<HTMLInputElement>(null);
19
22
  const { commonStore } = useGlobalStore();
20
- const { tmpDSName, tmpDataSource } = commonStore;
23
+ const { tmpDSName, tmpDataSource, tmpDSRawFields } = commonStore;
24
+ const [encoding, setEncoding] = useState<string>("utf-8");
21
25
 
22
26
  const onSubmitData = useCallback(() => {
23
27
  commonStore.commitTempDS();
24
28
  }, []);
25
29
 
26
30
  const { t } = useTranslation("translation", { keyPrefix: "DataSource.dialog.file" });
31
+ const fileLoaded = tmpDataSource.length > 0 && tmpDSRawFields.length > 0;
27
32
 
28
33
  return (
29
34
  <Container>
35
+ {!fileLoaded && (
36
+ <div className="text-center">
37
+ <svg
38
+ className="mx-auto h-12 w-12 text-gray-400"
39
+ fill="none"
40
+ viewBox="0 0 24 24"
41
+ stroke="currentColor"
42
+ aria-hidden="true"
43
+ >
44
+ <path
45
+ vectorEffect="non-scaling-stroke"
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ strokeWidth={2}
49
+ d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
50
+ />
51
+ </svg>
52
+ <h3 className="mt-2 text-sm font-semibold text-gray-900">{t('choose_file')}</h3>
53
+ <p className="mt-1 text-sm text-gray-500">{t('get_start_desc')}</p>
54
+ </div>
55
+ )}
30
56
  <input
31
57
  style={{ display: "none" }}
32
58
  type="file"
@@ -39,42 +65,61 @@ const CSVData: React.FC<ICSVData> = (props) => {
39
65
  file,
40
66
  config: { type: "reservoirSampling", size: Infinity },
41
67
  onLoading: () => {},
68
+ encoding,
42
69
  }).then((data) => {
43
70
  commonStore.updateTempDS(data as IRow[]);
44
71
  });
45
72
  }
46
73
  }}
47
74
  />
48
- <div className="mt-1 mb-1">
49
- <DefaultButton
50
- onClick={() => {
51
- if (fileRef.current) {
52
- fileRef.current.click();
53
- }
54
- }}
55
- text={t("open")}
56
- />
57
- <PrimaryButton
58
- text={t("submit")}
59
- disabled={tmpDataSource.length === 0}
60
- onClick={() => {
61
- onSubmitData();
62
- }}
63
- />
64
- </div>
65
- <div className="my-2">
66
- <label className="block text-xs text-gray-800 mb-1 font-bold">{t("dataset_name")}</label>
67
- <input
68
- type="text"
69
- placeholder={t("dataset_name")}
70
- value={tmpDSName}
71
- onChange={(e) => {
72
- commonStore.updateTempName(e.target.value);
73
- }}
74
- className="text-xs p-2 rounded border border-gray-200 outline-none focus:outline-none focus:border-blue-500 placeholder:italic placeholder:text-slate-400"
75
- />
76
- </div>
77
- <Table />
75
+ {!fileLoaded && (
76
+ <div className="my-1 flex justify-center">
77
+ <DefaultButton
78
+ className="mr-2"
79
+ onClick={() => {
80
+ if (fileRef.current) {
81
+ fileRef.current.click();
82
+ }
83
+ }}
84
+ text={t("open")}
85
+ />
86
+ <div className="inline-block relative">
87
+ <DropdownSelect
88
+ buttonClassName="w-36"
89
+ options={charsetOptions}
90
+ selectedKey={encoding}
91
+ onSelect={(k) => {
92
+ setEncoding(k);
93
+ }}
94
+ />
95
+ </div>
96
+ </div>
97
+ )}
98
+ {fileLoaded && (
99
+ <div className="mb-2 mt-6">
100
+ <label className="block text-xs text-gray-800 dark:text-gray-200 mb-1 font-bold">
101
+ {t("dataset_name")}
102
+ </label>
103
+ <input
104
+ type="text"
105
+ placeholder={t("dataset_name")}
106
+ value={tmpDSName}
107
+ onChange={(e) => {
108
+ commonStore.updateTempName(e.target.value);
109
+ }}
110
+ className="text-xs mr-2 p-2 rounded border border-gray-200 dark:border-gray-800 outline-none focus:outline-none focus:border-blue-500 placeholder:italic placeholder:text-slate-400 dark:bg-stone-900"
111
+ />
112
+ <PrimaryButton
113
+ className="mr-2"
114
+ text={t("submit")}
115
+ disabled={tmpDataSource.length === 0}
116
+ onClick={() => {
117
+ onSubmitData();
118
+ }}
119
+ />
120
+ </div>
121
+ )}
122
+ {fileLoaded && <Table />}
78
123
  </Container>
79
124
  );
80
125
  };