@noya-app/noya-designsystem 0.1.44 → 0.1.45

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 (44) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +9 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +320 -78
  5. package/dist/index.d.ts +320 -78
  6. package/dist/index.js +2113 -1374
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +2000 -1274
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +3 -3
  11. package/src/components/AnimatePresence.tsx +10 -1
  12. package/src/components/Avatar.tsx +2 -0
  13. package/src/components/Checkbox.tsx +6 -1
  14. package/src/components/Combobox.tsx +6 -4
  15. package/src/components/ComboboxMenu.tsx +30 -15
  16. package/src/components/CommandPalette.tsx +69 -0
  17. package/src/components/ContextMenu.tsx +33 -134
  18. package/src/components/DropdownMenu.tsx +47 -155
  19. package/src/components/Fade.tsx +62 -0
  20. package/src/components/InputField.tsx +109 -133
  21. package/src/components/Label.tsx +81 -7
  22. package/src/components/LabeledField.tsx +112 -0
  23. package/src/components/ListView.tsx +55 -52
  24. package/src/components/Popover.tsx +12 -1
  25. package/src/components/SearchCompletionMenu.tsx +206 -0
  26. package/src/components/SegmentedControl.tsx +5 -2
  27. package/src/components/SelectMenu.tsx +104 -124
  28. package/src/components/SidebarList.tsx +252 -0
  29. package/src/components/Slider.tsx +18 -26
  30. package/src/components/Text.tsx +1 -0
  31. package/src/components/TextArea.tsx +4 -1
  32. package/src/components/Toolbar.tsx +6 -2
  33. package/src/components/TreeView.tsx +3 -0
  34. package/src/components/WorkspaceLayout.tsx +38 -18
  35. package/src/components/internal/Menu.tsx +58 -10
  36. package/src/components/internal/MenuViewport.tsx +151 -0
  37. package/src/components/internal/SelectItem.tsx +111 -0
  38. package/src/hooks/useIndent.ts +12 -0
  39. package/src/hooks/useLabel.ts +51 -0
  40. package/src/hooks/usePreservePanelSize.tsx +50 -19
  41. package/src/index.tsx +12 -5
  42. package/src/utils/combobox.ts +4 -1
  43. package/src/utils/createSectionedMenu.ts +11 -8
  44. package/src/utils/selection.ts +36 -0
@@ -28,7 +28,7 @@ import { useHover } from "../hooks/useHover";
28
28
  import { cssVars } from "../theme";
29
29
  import { cx } from "../utils/classNames";
30
30
  import { ContextMenu } from "./ContextMenu";
31
- import { InputField, InputFieldInputProps } from "./InputField";
31
+ import { InputField } from "./InputField";
32
32
  import { MenuItem } from "./internal/Menu";
33
33
  import { ScrollArea } from "./ScrollArea";
34
34
  import {
@@ -38,7 +38,6 @@ import {
38
38
  Sortable,
39
39
  } from "./Sortable";
40
40
  import { Spacer } from "./Spacer";
41
- import { labelTextStyles } from "./SelectMenu";
42
41
 
43
42
  export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
44
43
  export type ListRowPosition = "only" | "first" | "middle" | "last";
@@ -122,24 +121,12 @@ const ListViewRowTitle = ({
122
121
  * EditableRowTitle
123
122
  * ------------------------------------------------------------------------- */
124
123
 
125
- const ListViewEditableRowTitleElement = forwardRef(
126
- (
127
- { className, ...props }: InputFieldInputProps,
128
- forwardedRef: ForwardedRef<HTMLInputElement>
129
- ) => (
130
- <InputField.Input
131
- ref={forwardedRef}
132
- className={cx(`bg-listview-editing-background `, className)}
133
- {...props}
134
- />
135
- )
136
- );
137
-
138
124
  export interface EditableRowProps {
139
125
  value: string;
140
126
  onSubmitEditing: (value: string) => void;
141
127
  autoFocus: boolean;
142
128
  placeholder?: string;
129
+ className?: string;
143
130
  }
144
131
 
145
132
  function ListViewEditableRowTitle({
@@ -147,6 +134,7 @@ function ListViewEditableRowTitle({
147
134
  onSubmitEditing,
148
135
  autoFocus,
149
136
  placeholder,
137
+ className,
150
138
  }: EditableRowProps) {
151
139
  const inputRef = useRef<HTMLInputElement | null>(null);
152
140
 
@@ -165,24 +153,17 @@ function ListViewEditableRowTitle({
165
153
  }, [autoFocus]);
166
154
 
167
155
  return (
168
- <ListViewEditableRowTitleElement
156
+ <InputField.Input
169
157
  ref={inputRef}
170
- variant="bare"
171
158
  value={value}
172
159
  placeholder={placeholder}
173
160
  onSubmit={onSubmitEditing}
174
161
  allowSubmittingWithSameValue
162
+ className={cx("-mx-1.5 -my-1 bg-listview-editing-background", className)}
175
163
  />
176
164
  );
177
165
  }
178
166
 
179
- function getPositionMargin(marginType: ListRowMarginType) {
180
- return {
181
- top: marginType === "top" || marginType === "vertical" ? 8 : 0,
182
- bottom: marginType === "bottom" || marginType === "vertical" ? 8 : 0,
183
- };
184
- }
185
-
186
167
  /* ----------------------------------------------------------------------------
187
168
  * Row
188
169
  * ------------------------------------------------------------------------- */
@@ -225,18 +206,24 @@ const RowContainer = forwardRef<
225
206
  style,
226
207
  $clickable,
227
208
  "aria-describedby": _, // Causes hydration warning
209
+ role,
210
+ "aria-label": ariaLabel,
211
+ "aria-roledescription": ariaRoleDescription,
212
+ "aria-selected": ariaSelected,
228
213
  ...props
229
214
  },
230
215
  ref
231
216
  ) => {
232
- const margin = getPositionMargin($marginType);
233
-
234
217
  return (
235
218
  <div
236
219
  ref={ref}
220
+ role={role}
221
+ aria-label={ariaLabel}
222
+ aria-roledescription={ariaRoleDescription}
223
+ aria-selected={ariaSelected ?? $selected}
237
224
  className={cx(
238
225
  $isSectionHeader && $sectionHeaderVariant === "label"
239
- ? labelTextStyles
226
+ ? "font-sans text-label uppercase leading-3 font-bold text-text-muted"
240
227
  : "font-sans text-heading5 font-normal",
241
228
  !$isSectionHeader &&
242
229
  $selected &&
@@ -254,22 +241,11 @@ const RowContainer = forwardRef<
254
241
  paddingRight: $selected ? "8px" : "12px",
255
242
  paddingBottom: $selected ? "4px" : "6px",
256
243
  paddingLeft: $selected ? "8px" : "12px",
257
- borderRadius:
258
- $variant === "padded" ? "2px" : $selected ? "4px" : "",
259
- marginLeft: $variant === "padded" ? "8px" : $selected ? "4px" : "",
260
- marginRight: $variant === "padded" ? "8px" : $selected ? "4px" : "",
261
- marginTop:
262
- $variant === "padded"
263
- ? `${margin.top}px`
264
- : $selected
265
- ? "2px"
266
- : "",
267
- marginBottom:
268
- $variant === "padded"
269
- ? `${margin.bottom}px`
270
- : $selected
271
- ? "2px"
272
- : "",
244
+ borderRadius: $selected ? "4px" : "",
245
+ marginLeft: $selected ? "4px" : "",
246
+ marginRight: $selected ? "4px" : "",
247
+ marginTop: $selected ? "2px" : "",
248
+ marginBottom: $selected ? "2px" : "",
273
249
  }),
274
250
  ...($selected &&
275
251
  !$isSectionHeader && {
@@ -351,6 +327,7 @@ const ListViewDragIndicatorElement = forwardRef<
351
327
  zIndex: 1000,
352
328
  position: "absolute",
353
329
  borderRadius: "3px",
330
+ pointerEvents: "none",
354
331
  ...($relativeDropPosition === "inside"
355
332
  ? {
356
333
  inset: 2,
@@ -396,6 +373,7 @@ interface ListViewClickInfo {
396
373
  interface ListViewRowProps<MenuItemType extends string = string> {
397
374
  id?: string;
398
375
  tabIndex?: number;
376
+ role?: string;
399
377
  selected?: boolean;
400
378
  /** @default 0 */
401
379
  depth?: number;
@@ -432,6 +410,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
432
410
  {
433
411
  id,
434
412
  tabIndex = 0,
413
+ role,
435
414
  gap,
436
415
  backgroundColor,
437
416
  selected = false,
@@ -526,6 +505,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
526
505
  onContextMenu={onContextMenu}
527
506
  $isSectionHeader={isSectionHeader}
528
507
  id={id}
508
+ role={role}
529
509
  $gap={gap ?? 0}
530
510
  $backgroundColor={backgroundColor}
531
511
  {...hoverProps}
@@ -764,14 +744,18 @@ type RenderProps<T> = {
764
744
  virtualized?: Size;
765
745
  };
766
746
 
767
- type ListViewVariant = "normal" | "padded" | "bare";
747
+ type ListViewVariant = "normal" | "bare";
768
748
 
769
749
  type ListViewSectionHeaderVariant = "normal" | "label";
770
750
 
771
- type ListViewRootProps = {
751
+ export type ListViewRootProps = {
772
752
  id?: string;
773
753
  className?: string;
774
754
  style?: CSSProperties;
755
+ role?: string;
756
+ "aria-labelledby"?: string;
757
+ "aria-orientation"?: "vertical" | "horizontal";
758
+ "aria-multiselectable"?: boolean;
775
759
  onPress?: () => void;
776
760
  scrollable?: boolean;
777
761
  expandable?: boolean;
@@ -788,6 +772,11 @@ type ListViewRootProps = {
788
772
  divider?: boolean;
789
773
  gap?: number;
790
774
  colorScheme?: ListColorScheme;
775
+ containerRef?: React.RefObject<HTMLDivElement>;
776
+ onDragOver?: React.DragEventHandler<HTMLDivElement>;
777
+ onDragEnter?: React.DragEventHandler<HTMLDivElement>;
778
+ onDragLeave?: React.DragEventHandler<HTMLDivElement>;
779
+ onDrop?: React.DragEventHandler<HTMLDivElement>;
791
780
  };
792
781
 
793
782
  const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
@@ -795,6 +784,10 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
795
784
  id,
796
785
  className,
797
786
  style,
787
+ role,
788
+ "aria-labelledby": ariaLabelledby,
789
+ "aria-orientation": ariaOrientation,
790
+ "aria-multiselectable": ariaMultiselectable,
798
791
  onPress,
799
792
  scrollable = false,
800
793
  expandable = true,
@@ -812,6 +805,11 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
812
805
  pressEventName = "onClick",
813
806
  colorScheme = "primary",
814
807
  gap = 0,
808
+ containerRef,
809
+ onDragOver,
810
+ onDragEnter,
811
+ onDragLeave,
812
+ onDrop,
815
813
  }: RenderProps<T> & ListViewRootProps,
816
814
  forwardedRef: ForwardedRef<IVirtualizedList>
817
815
  ) {
@@ -1002,17 +1000,13 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
1002
1000
  const getItemHeight = useCallback(
1003
1001
  (index: number) => {
1004
1002
  const child = getItemContextValue(index);
1005
- const margin = child?.marginType
1006
- ? getPositionMargin(child.marginType)
1007
- : { top: 0, bottom: 0 };
1008
1003
  const height =
1009
- (child?.isSectionHeader && child.sectionHeaderVariant === "label"
1004
+ child?.isSectionHeader && child.sectionHeaderVariant === "label"
1010
1005
  ? SECTION_HEADER_LABEL_HEIGHT
1011
- : ROW_HEIGHT) +
1012
- (variant === "padded" ? margin.top + margin.bottom : 0);
1006
+ : ROW_HEIGHT;
1013
1007
  return height;
1014
1008
  },
1015
- [getItemContextValue, variant]
1009
+ [getItemContextValue]
1016
1010
  );
1017
1011
 
1018
1012
  const getKey = useCallback(
@@ -1027,6 +1021,10 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
1027
1021
  <ListViewDraggingContext.Provider value={draggingContextValue}>
1028
1022
  <div
1029
1023
  id={id}
1024
+ role={role}
1025
+ aria-labelledby={ariaLabelledby}
1026
+ aria-orientation={ariaOrientation}
1027
+ aria-multiselectable={ariaMultiselectable}
1030
1028
  className={cx(
1031
1029
  `flex flex-col text-text-muted ${scrollable ? "flex-1" : "flex-none"} `,
1032
1030
  className
@@ -1035,6 +1033,11 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
1035
1033
  {...{
1036
1034
  [pressEventName]: handleClick,
1037
1035
  }}
1036
+ ref={containerRef}
1037
+ onDragOver={onDragOver}
1038
+ onDragEnter={onDragEnter}
1039
+ onDragLeave={onDragLeave}
1040
+ onDrop={onDrop}
1038
1041
  >
1039
1042
  {withScrollable((scrollElementRef: HTMLDivElement | null) =>
1040
1043
  withSortable(
@@ -1,5 +1,6 @@
1
1
  import * as PopoverPrimitive from "@radix-ui/react-popover";
2
2
  import React, { ComponentProps } from "react";
3
+ import { cx } from "../utils/classNames";
3
4
  import { IconButton } from "./IconButton";
4
5
 
5
6
  const closeStyles: React.CSSProperties = {
@@ -28,6 +29,7 @@ interface Props
28
29
  sideOffset?: number;
29
30
  onClickClose?: () => void;
30
31
  showArrow?: boolean;
32
+ className?: string;
31
33
  }
32
34
 
33
35
  export function Popover({
@@ -46,13 +48,22 @@ export function Popover({
46
48
  onInteractOutside,
47
49
  onFocusOutside,
48
50
  onClickClose,
51
+ className,
49
52
  }: Props) {
50
53
  return (
51
54
  <PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
52
55
  <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
53
56
  <PopoverPrimitive.Portal>
54
57
  <PopoverPrimitive.Content
55
- className={`rounded font-[14px] bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] max-h-[600px] overflow-y-auto text-text-muted z-[1000] ${variant === "large" ? "w-[680px]" : variant === "normal" ? "w-[240px]" : ""}`}
58
+ className={cx(
59
+ "rounded font-[14px] bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
60
+ variant === "large"
61
+ ? "w-[680px]"
62
+ : variant === "normal"
63
+ ? "w-[240px]"
64
+ : "",
65
+ className
66
+ )}
56
67
  side={side}
57
68
  align="center"
58
69
  sideOffset={sideOffset}
@@ -0,0 +1,206 @@
1
+ import {
2
+ ComboboxMenu,
3
+ ComboboxOption,
4
+ Divider,
5
+ IScoredItem,
6
+ InputField,
7
+ ListView,
8
+ Small,
9
+ fuzzyFilter,
10
+ getNextIndex,
11
+ } from "@noya-app/noya-designsystem";
12
+ import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
13
+ import React, {
14
+ forwardRef,
15
+ useCallback,
16
+ useEffect,
17
+ useImperativeHandle,
18
+ useMemo,
19
+ useRef,
20
+ useState,
21
+ } from "react";
22
+
23
+ export interface ISearchCompletionMenu {
24
+ focus: () => void;
25
+ }
26
+
27
+ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
28
+ {
29
+ onSelect,
30
+ onHover,
31
+ onClose,
32
+ items,
33
+ width = 200,
34
+ }: {
35
+ onSelect: (item: ComboboxOption) => void;
36
+ onHover?: (item: ComboboxOption) => void;
37
+ onClose: () => void;
38
+ items: ComboboxOption[];
39
+ width?: number;
40
+ },
41
+ forwardedRef: React.ForwardedRef<ISearchCompletionMenu>
42
+ ) {
43
+ const [state, setState] = useState({
44
+ search: "",
45
+ index: 0,
46
+ });
47
+
48
+ const { index, search } = state;
49
+
50
+ const listRef = React.useRef<ListView.VirtualizedList>(null);
51
+
52
+ useEffect(() => {
53
+ listRef.current?.scrollToIndex(index);
54
+ }, [index]);
55
+
56
+ const results = useMemo(
57
+ () =>
58
+ fuzzyFilter({
59
+ items: items.map((item) => item.name),
60
+ query: search,
61
+ }).map((item) => ({
62
+ ...item,
63
+ ...items[item.index],
64
+ })) as (ComboboxOption & IScoredItem)[],
65
+ [items, search]
66
+ );
67
+
68
+ const setSearch = useCallback((search: string) => {
69
+ setState((state) => ({ ...state, search, index: 0 }));
70
+ }, []);
71
+
72
+ const setIndex = useCallback((index: number) => {
73
+ setState((state) => ({ ...state, index }));
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ onHover?.(results[index]!);
78
+ }, [index, onHover, results]);
79
+
80
+ const searchHeight = 31;
81
+
82
+ const listSize =
83
+ results.length > 0
84
+ ? {
85
+ width,
86
+ height: Math.min(results.length * 31, 31 * 6.5),
87
+ }
88
+ : {
89
+ width,
90
+ height: 60,
91
+ };
92
+
93
+ const elementSize = {
94
+ width: listSize.width,
95
+ height: searchHeight + listSize.height,
96
+ };
97
+
98
+ const selectAndClose = useCallback(
99
+ (item: ComboboxOption) => {
100
+ onSelect(item);
101
+ onClose();
102
+ },
103
+ [onSelect, onClose]
104
+ );
105
+
106
+ const handleKeyDown = useCallback(
107
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
108
+ handleKeyboardEvent(event.nativeEvent, getCurrentPlatform(navigator), {
109
+ ArrowUp: () => {
110
+ const nextIndex = getNextIndex(
111
+ results,
112
+ index,
113
+ "previous",
114
+ (item) => item.disabled ?? false
115
+ );
116
+ setIndex(nextIndex);
117
+ },
118
+ ArrowDown: () => {
119
+ const nextIndex = getNextIndex(
120
+ results,
121
+ index,
122
+ "next",
123
+ (item) => item.disabled ?? false
124
+ );
125
+ setIndex(nextIndex);
126
+ },
127
+ Tab: () => selectAndClose(results[index]!),
128
+ Enter: () => selectAndClose(results[index]!),
129
+ Escape: () => onClose(),
130
+ });
131
+ },
132
+ [index, onClose, selectAndClose, results, setIndex]
133
+ );
134
+
135
+ const inputRef = useRef<HTMLInputElement>(null);
136
+
137
+ useImperativeHandle(forwardedRef, () => ({
138
+ focus: () => {
139
+ inputRef.current?.focus();
140
+ },
141
+ }));
142
+
143
+ return (
144
+ <div {...elementSize} className="flex flex-col overflow-hidden">
145
+ <InputField.Root className="flex flex-0">
146
+ <InputField.Input
147
+ ref={inputRef}
148
+ value={search}
149
+ placeholder="Filter..."
150
+ style={{
151
+ height: searchHeight,
152
+ borderRadius: 0,
153
+ outline: "none",
154
+ boxShadow: "none",
155
+ backgroundColor: "white",
156
+ padding: "4px 12px",
157
+ }}
158
+ onChange={setSearch}
159
+ onKeyDown={handleKeyDown}
160
+ // onBlur={(event) => {
161
+ // const isWithinPopover =
162
+ // event.relatedTarget &&
163
+ // event.relatedTarget instanceof HTMLElement &&
164
+ // event.relatedTarget.role === 'dialog';
165
+
166
+ // if (isWithinPopover) {
167
+ // inputRef.current?.focus();
168
+
169
+ // event.stopPropagation();
170
+ // event.preventDefault();
171
+ // }
172
+ // }}
173
+ // onPointerDown={(event) => {
174
+ // event.stopPropagation();
175
+ // event.preventDefault();
176
+ // }}
177
+ // onFocusCapture={(event) => {
178
+ // event.stopPropagation();
179
+ // event.preventDefault();
180
+ // }}
181
+ />
182
+ </InputField.Root>
183
+ <Divider />
184
+ {results.length > 0 ? (
185
+ <ComboboxMenu
186
+ ref={listRef}
187
+ listSize={listSize}
188
+ items={results}
189
+ selectedIndex={index}
190
+ onSelectItem={(item) => {
191
+ if (item.type === "sectionHeader") return;
192
+ selectAndClose(item);
193
+ }}
194
+ onHoverIndex={setIndex}
195
+ />
196
+ ) : (
197
+ <div
198
+ {...listSize}
199
+ className="flex flex-col p-20 items-center justify-center"
200
+ >
201
+ <Small color="textDisabled">No results</Small>
202
+ </div>
203
+ )}
204
+ </div>
205
+ );
206
+ });
@@ -8,6 +8,7 @@ import React, {
8
8
  useContext,
9
9
  useMemo,
10
10
  } from "react";
11
+ import { useLabel } from "../hooks/useLabel";
11
12
  import { cx } from "../utils/classNames";
12
13
  import { renderIcon } from "./Icons";
13
14
  import { RegularMenuItem } from "./internal/Menu";
@@ -77,6 +78,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
77
78
  : "aria-checked:bg-background aria-checked:text-text",
78
79
  "focus:z-interactable",
79
80
  "aria-checked:shadow-[0_1px_1px_rgba(0,0,0,0.1)]",
81
+ "whitespace-pre",
80
82
  className
81
83
  )}
82
84
  {...props}
@@ -105,7 +107,7 @@ const Separator = ({ transparent }: { transparent: boolean }) => (
105
107
  export const SegmentedControl = memo(function SegmentedControl<
106
108
  T extends string,
107
109
  >({
108
- id,
110
+ id: idProp,
109
111
  value,
110
112
  onValueChange,
111
113
  colorScheme,
@@ -116,6 +118,7 @@ export const SegmentedControl = memo(function SegmentedControl<
116
118
  style,
117
119
  }: SegmentedControlProps<T>) {
118
120
  const contextValue = useMemo(() => ({ colorScheme }), [colorScheme]);
121
+ const { fieldId: id } = useLabel({ fieldId: idProp });
119
122
 
120
123
  const handleValueChange = useCallback(
121
124
  (value: T) => {
@@ -135,7 +138,7 @@ export const SegmentedControl = memo(function SegmentedControl<
135
138
  value={value}
136
139
  onValueChange={handleValueChange}
137
140
  className={cx(
138
- `flex items-stretch w-full appearance-none relative outline-none min-h-[27px] rounded bg-input-background py-[2px] px-[2px]`,
141
+ `flex items-stretch flex-auto appearance-none relative outline-none min-h-[27px] rounded bg-input-background py-[2px] px-[2px]`,
139
142
  className
140
143
  )}
141
144
  style={style}