@fibery/ui-kit 1.40.3 → 1.40.4

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 (82) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +4 -7
  3. package/src/__snapshots__/design-system.test.ts.snap +7265 -0
  4. package/src/a11y-color.test.ts +181 -0
  5. package/src/a11y-color.ts +13 -20
  6. package/src/actions-menu/actions-menu-item.tsx +7 -7
  7. package/src/antd/ant-modal.tsx +10 -5
  8. package/src/antd/styles.ts +6 -6
  9. package/src/app-icon-with-fallback.tsx +2 -2
  10. package/src/app-icon.tsx +2 -2
  11. package/src/button/button.tsx +1 -0
  12. package/src/button/make-button-colors.ts +5 -13
  13. package/src/checkbox.tsx +2 -2
  14. package/src/color-picker/ColorPickerOrLoader.js +2 -2
  15. package/src/color-utils.test.ts +317 -0
  16. package/src/color-utils.ts +180 -0
  17. package/src/comment.tsx +3 -2
  18. package/src/context-menu/index.tsx +12 -7
  19. package/src/create-inline-theme.ts +9 -8
  20. package/src/design-system.colors.ts +760 -0
  21. package/src/design-system.test.ts +287 -7
  22. package/src/design-system.ts +146 -940
  23. package/src/dropdown-menu/index.tsx +21 -16
  24. package/src/emoji-picker/app-icon-picker.tsx +5 -5
  25. package/src/emoji-picker/emoji-picker-content-with-color.tsx +4 -4
  26. package/src/emoji-picker/icon-emoji-picker.tsx +2 -2
  27. package/src/favorites-icon.tsx +1 -1
  28. package/src/file-item/file-icon.tsx +169 -0
  29. package/src/file-item/file-menu-items.tsx +68 -0
  30. package/src/file-item/file-preview-actions.tsx +38 -0
  31. package/src/file-item/file-title.tsx +48 -0
  32. package/src/file-item/types.ts +27 -0
  33. package/src/file-item/use-register-in-image-gallery.tsx +70 -0
  34. package/src/file-item-2.tsx +35 -345
  35. package/src/file-item.tsx +6 -2
  36. package/src/hue-shift.test.ts +91 -0
  37. package/src/icons/ast/FileCounter.ts +8 -0
  38. package/src/icons/ast/FileMultiple.ts +8 -0
  39. package/src/icons/ast/ValueEdit.ts +8 -0
  40. package/src/icons/ast/index.tsx +3 -0
  41. package/src/icons/react/FileCounter.tsx +13 -0
  42. package/src/icons/react/FileMultiple.tsx +13 -0
  43. package/src/icons/react/ValueEdit.tsx +13 -0
  44. package/src/icons/react/index.tsx +3 -0
  45. package/src/icons/svg/file-counter.svg +3 -0
  46. package/src/icons/svg/file-multiple.svg +3 -0
  47. package/src/icons/svg/value-edit.svg +3 -0
  48. package/src/images-gallery/slide-buttons.tsx +4 -11
  49. package/src/lists/actions-menu-row-surface.tsx +5 -5
  50. package/src/mobile-keyboard-aware-popup.tsx +4 -3
  51. package/src/palette-generator.test.ts +566 -0
  52. package/src/palette-generator.ts +166 -0
  53. package/src/palette.ts +71 -55
  54. package/src/platform.ts +0 -3
  55. package/src/popover/index.tsx +13 -15
  56. package/src/progress.tsx +2 -2
  57. package/src/reactions/reaction-button.tsx +12 -6
  58. package/src/root-theme-provider.test.tsx +316 -0
  59. package/src/scale-generator.ts +347 -0
  60. package/src/select/components/menu-list-virtualized.tsx +7 -22
  61. package/src/select/components/menu.tsx +12 -2
  62. package/src/select/select.tsx +2 -1
  63. package/src/select/styles.ts +0 -1
  64. package/src/static-palettes.ts +146 -0
  65. package/src/thematic-color-picker.tsx +266 -0
  66. package/src/thematic-constants.tsx +27 -0
  67. package/src/thematic-controls.tsx +144 -0
  68. package/src/thematic-scales.tsx +122 -0
  69. package/src/thematic-state.ts +333 -0
  70. package/src/thematic.tsx +382 -0
  71. package/src/theme-provider.test.tsx +808 -0
  72. package/src/theme-provider.tsx +132 -69
  73. package/src/theme-settings.ts +1 -1
  74. package/src/theme-styles.ts +12 -5
  75. package/src/toast/toast-action.tsx +1 -1
  76. package/src/toggle-on-off.tsx +2 -2
  77. package/src/toggle.tsx +5 -6
  78. package/src/tooltip.tsx +13 -10
  79. package/src/type-badge.tsx +3 -3
  80. package/src/unit/styles.ts +2 -2
  81. package/src/unit/unit-with-tooltip.tsx +3 -2
  82. package/src/use-long-press.tsx +2 -2
@@ -1,28 +1,17 @@
1
- /* eslint-disable max-lines */
2
1
  import {css, cx} from "@linaria/core";
3
- import {CSSProperties, useCallback, useEffect, useState} from "react";
4
- import {ActionsMenu, ActionsMenuItem, ActionsMenuSeparator} from "./actions-menu";
2
+ import {CSSProperties, useState} from "react";
3
+ import {ActionsMenu} from "./actions-menu";
5
4
  import {ActionsButtonCompact} from "./button/actions-button-compact";
6
- import {border, colors, space, themeVars, fontSize, fontWeight, lineHeight} from "./design-system";
5
+ import {border, fontSize, fontWeight, lineHeight, space, themeVars} from "./design-system";
7
6
  import {makeButtonColors} from "./button/make-button-colors";
8
- import Delete from "./icons/react/Delete";
9
- import FileDownload from "./icons/react/FileDownload";
10
- import FileImage from "./icons/react/FileImage";
11
- import FileVideo from "./icons/react/FileVideo";
12
- import FileTable from "./icons/react/FileTable";
13
- import FileDocument from "./icons/react/FileDocument";
14
- import FileArchive from "./icons/react/FileArchive";
15
- import FilePresentation from "./icons/react/FilePresentation";
16
- import Link from "./icons/react/Link";
17
- import RicheditorOpenLink from "./icons/react/RicheditorOpenLink";
7
+ import {useTheme} from "./theme-provider";
18
8
  import Spinner from "./icons/react/Spinner";
19
- import AppWiki from "./icons/react/AppWiki";
20
- import {copyUrlToClipboard} from "./copy-to-clipboard";
21
- import {useToast} from "./toast/toast-provider";
22
9
  import {Icon} from "./icons/Icon";
23
- import {useImagesGalleryMethods, useImagesGalleryRegistry} from "./images-gallery/images-gallery";
24
- import {isPhoneApp} from "./use-is-phone";
25
- import Pencil from "./icons/react/Pencil";
10
+ import {FileIcon} from "./file-item/file-icon";
11
+ import {FileMenuItems} from "./file-item/file-menu-items";
12
+ import {UploadFileWithContent} from "./file-item/types";
13
+ import {useRegisterInImageGallery} from "./file-item/use-register-in-image-gallery";
14
+ import {FileTitle as FileTitleInner} from "./file-item/file-title";
26
15
 
27
16
  const fileItemStyle = css`
28
17
  display: block;
@@ -79,32 +68,18 @@ const actionsButtonStyle = css`
79
68
  opacity: 0;
80
69
  `;
81
70
 
82
- export type FileItemType = {
83
- uid: string;
84
- name: string;
85
- url: string;
86
- downloadUrl?: string | null;
87
- thumbnailUrl: string | null;
88
- contentType: string;
89
- previewData: null | {
90
- type: "image" | "video" | "iframe";
91
- previewSrc: string | null;
92
- originalSrc: string;
93
- };
94
- status?: string;
95
- };
96
-
97
71
  export type FileItemProps = {
98
- data: FileItemType;
72
+ data: UploadFileWithContent;
99
73
  onDownloadClick?: () => void;
100
- onRemoveClick?: (x: FileItemType) => void;
101
- onRenameClick?: (x: {id: string; name: string}) => void;
74
+ onRemoveClick?: () => void;
75
+ onRenameClick?: (file: {id: string; name: string}) => void;
102
76
  disabled?: boolean;
103
77
  };
104
78
 
105
79
  export const minFileTileWidth = 120;
106
80
 
107
81
  export function FileItem(props: FileItemProps) {
82
+ const theme = useTheme();
108
83
  const {onDownloadClick, onRemoveClick} = props;
109
84
  const {name, thumbnailUrl, status, previewData: preview, contentType} = props.data;
110
85
  const isPending = status === "uploading" || status === "deleting";
@@ -114,59 +89,11 @@ export function FileItem(props: FileItemProps) {
114
89
 
115
90
  const showFileIcon = !isPending && !isBackgroundReady;
116
91
 
117
- const [zoomed, setZoomed] = useState(false);
118
- const imagesRegistry = useImagesGalleryRegistry();
119
- const galleryMethods = useImagesGalleryMethods();
120
-
121
- useEffect(() => {
122
- if (!preview) {
123
- return;
124
- }
125
-
126
- imagesRegistry.set(preview.originalSrc, {
127
- type: preview.type,
128
- src: preview.originalSrc,
129
- label: name,
130
- zoomed: false,
131
- setZoomed,
132
- actions: <FilePreviewActions url={preview.originalSrc} name={name} onDownloadClick={onDownloadClick} />,
133
- });
134
- }, [imagesRegistry, name, preview, onDownloadClick]);
135
-
136
- useEffect(() => {
137
- if (!preview) {
138
- return undefined;
139
- }
140
-
141
- if (zoomed) {
142
- galleryMethods.setActivated(true);
143
- }
144
- const imageData = imagesRegistry.get(preview.originalSrc);
145
- if (imageData) {
146
- imageData.zoomed = zoomed;
147
- } else {
148
- imagesRegistry.set(preview.originalSrc, {
149
- type: preview.type,
150
- src: preview.originalSrc,
151
- label: name,
152
- zoomed,
153
- setZoomed,
154
- actions: <FilePreviewActions url={preview.originalSrc} name={name} onDownloadClick={onDownloadClick} />,
155
- });
156
- }
157
-
158
- return () => {
159
- imagesRegistry.delete(preview.originalSrc);
160
- };
161
- }, [galleryMethods, zoomed, preview, imagesRegistry, name, onDownloadClick]);
162
-
163
- const onContainerClick = useCallback(() => {
164
- if (preview) {
165
- setZoomed(true);
166
- } else {
167
- onDownloadClick?.();
168
- }
169
- }, [preview, onDownloadClick]);
92
+ const {openGallery} = useRegisterInImageGallery({
93
+ preview,
94
+ name,
95
+ onDownloadClick,
96
+ });
170
97
 
171
98
  return (
172
99
  <>
@@ -219,7 +146,7 @@ export function FileItem(props: FileItemProps) {
219
146
  >
220
147
  <div
221
148
  style={isBackgroundReady ? {backgroundImage: `url(${thumbnailUrl})`} : {}}
222
- onClick={onContainerClick}
149
+ onClick={openGallery}
223
150
  className={cx(
224
151
  css`
225
152
  flex-shrink: 0;
@@ -321,7 +248,7 @@ export function FileItem(props: FileItemProps) {
321
248
  data={props.data}
322
249
  disabled={props.disabled}
323
250
  actionsButtonStyle={
324
- isBackgroundReady ? makeButtonColors(colors.inversedTextColor, "ghost") : undefined
251
+ isBackgroundReady ? makeButtonColors(themeVars.inversedTextColor, "ghost", theme) : undefined
325
252
  }
326
253
  onDownloadClick={onDownloadClick}
327
254
  onRemoveClick={props.onRemoveClick}
@@ -366,16 +293,14 @@ function FileActionsMenu({
366
293
  onRenameClick,
367
294
  actionsButtonStyle,
368
295
  }: {
369
- data: FileItemType;
296
+ data: UploadFileWithContent;
370
297
  onDownloadClick?: () => void;
371
- onRemoveClick?: (data: FileItemType) => void;
298
+ onRemoveClick?: () => void;
372
299
  onRenameClick?: (data: {id: string; name: string}) => void;
373
300
  disabled?: boolean;
374
301
  setIsActionsOpened: (val: boolean) => void;
375
302
  actionsButtonStyle?: CSSProperties;
376
303
  }) {
377
- const toast = useToast();
378
-
379
304
  return (
380
305
  <ActionsMenu
381
306
  onOpenChange={setIsActionsOpened}
@@ -388,51 +313,20 @@ function FileActionsMenu({
388
313
  />
389
314
  }
390
315
  >
391
- <ActionsMenuItem
392
- Icon={Link}
393
- onSelect={() => {
394
- copyUrlToClipboard({url: `${window.origin}${data.url}`, label: data.name})
395
- .then(() => toast.success({title: "Link copied to clipboard"}))
396
- .catch((error) => toast.error({title: "Unable to copy link to clipboard", subTitle: error.message}));
397
- }}
398
- >
399
- Copy file URL
400
- </ActionsMenuItem>
401
- <a href={data.url} target="_blank" rel="noopener noreferrer">
402
- <ActionsMenuItem Icon={RicheditorOpenLink}>
403
- {isPhoneApp() ? "Open in browser" : "Open in new tab"}
404
- </ActionsMenuItem>
405
- </a>
406
- {isPhoneApp() ? (
407
- <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
408
- Download
409
- </ActionsMenuItem>
410
- ) : (
411
- <a download={data.name} href={`${data.url}?attachment`}>
412
- <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
413
- </a>
414
- )}
415
- {onRenameClick && data.status === "done" ? (
416
- <>
417
- <ActionsMenuSeparator />
418
- <ActionsMenuItem Icon={Pencil} onSelect={() => onRenameClick?.({id: data.uid, name: data.name})}>
419
- Rename
420
- </ActionsMenuItem>
421
- </>
422
- ) : null}
423
- <ActionsMenuSeparator />
424
- <ActionsMenuItem Icon={Delete} onSelect={() => onRemoveClick?.(data)} dangerous disabled={disabled}>
425
- Delete
426
- </ActionsMenuItem>
316
+ {
317
+ <FileMenuItems
318
+ file={data}
319
+ disabled={disabled}
320
+ onDownloadClick={onDownloadClick}
321
+ onRemoveClick={onRemoveClick}
322
+ onRenameClick={onRenameClick}
323
+ />
324
+ }
427
325
  </ActionsMenu>
428
326
  );
429
327
  }
430
328
 
431
329
  function FileTitle({title, className}: {title: string; className?: string}) {
432
- const parts = title.split(".");
433
- const ext = parts.length > 1 ? "." + parts[parts.length - 1] : "";
434
- const name = title.substring(0, title.length - ext.length);
435
-
436
330
  return (
437
331
  <div
438
332
  className={cx(
@@ -458,216 +352,12 @@ function FileTitle({title, className}: {title: string; className?: string}) {
458
352
  className
459
353
  )}
460
354
  >
461
- <div
462
- className={css`
463
- display: flex;
464
- min-width: 0;
465
- align-items: center;
355
+ <FileTitleInner
356
+ title={title}
357
+ containerCss={css`
466
358
  max-width: 180px;
467
359
  `}
468
- >
469
- <span
470
- className={css`
471
- white-space: nowrap;
472
- overflow: hidden;
473
- text-overflow: ellipsis;
474
- /* ellipsis shows ONLY if this span overflows */
475
- min-width: 0;
476
- `}
477
- >
478
- {name}
479
- </span>
480
- <span
481
- className={css`
482
- flex: 0 0 auto;
483
- `}
484
- >
485
- {ext}
486
- </span>
487
- </div>
360
+ />
488
361
  </div>
489
362
  );
490
363
  }
491
-
492
- function FilePreviewActions({url, name, onDownloadClick}: {url: string; name: string; onDownloadClick?: () => void}) {
493
- if (isPhoneApp()) {
494
- return (
495
- <>
496
- <a href={url} target="_blank" rel="noopener noreferrer">
497
- <ActionsMenuItem Icon={RicheditorOpenLink}>Open in browser</ActionsMenuItem>
498
- </a>
499
- <ActionsMenuItem Icon={FileDownload} onSelect={onDownloadClick}>
500
- Download
501
- </ActionsMenuItem>
502
- </>
503
- );
504
- }
505
-
506
- return (
507
- <>
508
- <a href={url} target="_blank" rel="noopener noreferrer">
509
- <ActionsMenuItem Icon={RicheditorOpenLink}>Open in new tab</ActionsMenuItem>
510
- </a>
511
- <a download={name} href={`${url}?attachment`}>
512
- <ActionsMenuItem Icon={FileDownload}>Download</ActionsMenuItem>
513
- </a>
514
- </>
515
- );
516
- }
517
-
518
- function FileIcon({contentType}: {contentType?: string}) {
519
- if (!contentType) {
520
- return <AppWiki />;
521
- }
522
- if (contentType.startsWith("image/")) {
523
- return <FileImage />;
524
- }
525
- if (contentType.startsWith("video/")) {
526
- return <FileVideo />;
527
- }
528
- if (SheetsContentTypes.includes(contentType)) {
529
- return <FileTable />;
530
- }
531
- if (ArchiveContentTypes.includes(contentType)) {
532
- return <FileArchive />;
533
- }
534
- if (PresentationContentTypes.includes(contentType)) {
535
- return <FilePresentation />;
536
- }
537
- if (DocumentContentTypes.includes(contentType) || contentType.startsWith("text/")) {
538
- return <FileDocument />;
539
- }
540
-
541
- return <AppWiki />;
542
- }
543
-
544
- const ArchiveContentTypes = [
545
- "application/zip",
546
- "application/zip-compressed",
547
- "application/x-zip",
548
- "application/x-zip-compressed",
549
- "multipart/x-zip",
550
-
551
- "application/x-rar-compressed",
552
- "application/vnd.rar",
553
-
554
- "application/x-7z-compressed",
555
-
556
- "application/gzip",
557
- "application/x-gzip",
558
-
559
- "application/x-tar",
560
-
561
- "application/x-bzip",
562
- "application/x-bzip2",
563
- "application/x-bzip3",
564
- "application/x-bzip4",
565
-
566
- "application/x-xz",
567
-
568
- "application/zstd",
569
- "application/x-zstd",
570
-
571
- "application/x-lzma",
572
- "application/x-lzip",
573
-
574
- "application/x-lzh",
575
- "application/x-lha",
576
-
577
- "application/x-iso9660-image",
578
- "application/x-cd-image",
579
-
580
- "application/x-apple-diskimage",
581
-
582
- "application/x-arj",
583
-
584
- "application/x-cpio",
585
-
586
- "application/x-rpm",
587
-
588
- "application/x-deb",
589
- "application/vnd.debian.binary-package",
590
-
591
- "application/x-compress",
592
- "application/x-archive",
593
- "application/vnd.android.package-archive",
594
- ];
595
-
596
- const PresentationContentTypes = [
597
- "application/x-iwork-keynote-sffkey",
598
- "application/vnd.ms-powerpoint",
599
- "application/vnd.openxmlformats-officedocument.presentationml.presentation",
600
- "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
601
- "application/vnd.openxmlformats-officedocument.presentationml.template",
602
- "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
603
- "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
604
- "application/vnd.ms-powerpoint.template.macroEnabled.12",
605
- "application/vnd.oasis.opendocument.presentation",
606
- "application/vnd.oasis.opendocument.presentation-template",
607
- "application/vnd.apple.keynote",
608
- "application/vnd.google-apps.presentation",
609
- "application/pdf",
610
- "application/vnd.sun.xml.impress",
611
- "application/vnd.stardivision.impress",
612
- ];
613
-
614
- const SheetsContentTypes = [
615
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
616
- "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
617
- "application/vnd.ms-excel",
618
- "application/vnd.ms-excel.sheet.macroEnabled.12",
619
- "application/vnd.ms-excel.template.macroEnabled.12",
620
- "application/vnd.ms-excel.addin.macroEnabled.12",
621
- "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
622
- "application/vnd.oasis.opendocument.spreadsheet",
623
- "application/vnd.oasis.opendocument.spreadsheet-template",
624
- "application/vnd.apple.numbers",
625
- "application/vnd.google-apps.spreadsheet",
626
- "text/csv",
627
- "text/x-comma-separated-values",
628
- "application/csv",
629
- "text/tab-separated-values",
630
- "application/vnd.lotus-1-2-3",
631
- "application/x-123",
632
- "application/vnd.stardivision.calc",
633
- "application/vnd.sun.xml.calc",
634
- "application/x-dbase",
635
- "application/x-iwork-numbers-sffnumbers",
636
- ];
637
-
638
- const DocumentContentTypes = [
639
- "application/msword",
640
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
641
- "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
642
- "application/vnd.ms-word.document.macroEnabled.12",
643
- "application/vnd.ms-word.template.macroEnabled.12",
644
-
645
- "application/vnd.oasis.opendocument.text",
646
- "application/vnd.oasis.opendocument.text-template",
647
- "application/vnd.oasis.opendocument.text-master",
648
- "application/vnd.oasis.opendocument.text-web",
649
-
650
- "application/vnd.apple.pages",
651
- "application/x-iwork-pages-sffpages",
652
-
653
- "application/rtf",
654
- "text/rtf",
655
-
656
- "text/plain",
657
- "text/markdown",
658
- "text/html",
659
-
660
- "application/vnd.google-apps.document",
661
-
662
- "application/vnd.sun.xml.writer",
663
- "application/vnd.sun.xml.writer.template",
664
- "application/vnd.sun.xml.writer.global",
665
-
666
- "application/vnd.stardivision.writer",
667
- "application/vnd.stardivision.writer-global",
668
-
669
- "application/x-mswrite",
670
- "application/x-abiword",
671
-
672
- "application/pdf",
673
- ];
package/src/file-item.tsx CHANGED
@@ -3,8 +3,9 @@ import {useCallback, useEffect, useState} from "react";
3
3
  import {ActionsMenu, ActionsMenuItem} from "./actions-menu";
4
4
  import {ActionsButtonCompact} from "./button/actions-button-compact";
5
5
  import {Item} from "./item";
6
- import {border, colors, fontWeight, space, textStyles, themeVars} from "./design-system";
6
+ import {border, fontWeight, space, textStyles, themeVars} from "./design-system";
7
7
  import {makeButtonColors} from "./button/make-button-colors";
8
+ import {useTheme} from "./theme-provider";
8
9
  import {useImagesGalleryMethods, useImagesGalleryRegistry} from "./images-gallery/images-gallery";
9
10
  import FileDownload from "./icons/react/FileDownload";
10
11
  import RicheditorOpenLink from "./icons/react/RicheditorOpenLink";
@@ -94,6 +95,7 @@ export type FileItemProps = {
94
95
  };
95
96
 
96
97
  export function FileItem(props: FileItemProps) {
98
+ const theme = useTheme();
97
99
  const {onDownloadClick, onRemoveClick} = props;
98
100
  const {name, url, thumbnailUrl, status, previewData: preview} = props.data;
99
101
  const isActive = status === "uploading" || status === "deleting";
@@ -201,7 +203,9 @@ export function FileItem(props: FileItemProps) {
201
203
  className={css`
202
204
  z-index: 1;
203
205
  `}
204
- style={isBackgroundReady ? makeButtonColors(colors.inversedTextColor, "ghost") : undefined}
206
+ style={
207
+ isBackgroundReady ? makeButtonColors(themeVars.inversedTextColor, "ghost", theme) : undefined
208
+ }
205
209
  />
206
210
  }
207
211
  >
@@ -0,0 +1,91 @@
1
+ import {makeThematicPalette, SCALE_PRESETS} from "./palette-generator";
2
+ import {OKLCH, parse, to} from "./color-utils";
3
+
4
+ function getHue(color: string): number {
5
+ const oklch = to(parse(color), OKLCH);
6
+ return oklch.coords[2] ?? 0;
7
+ }
8
+
9
+ describe("shiftHueTowards", () => {
10
+ it("no shift when baseChroma is 0", () => {
11
+ const palette = makeThematicPalette({
12
+ baseHue: 200,
13
+ baseChroma: 0,
14
+ accentHue: 264,
15
+ accentChroma: 0.16,
16
+ });
17
+
18
+ // Semantic colors should keep original hues (within 1° tolerance)
19
+ expect(Math.abs(getHue(palette.red[9]) - SCALE_PRESETS.red.hue)).toBeLessThan(1);
20
+ expect(Math.abs(getHue(palette.teal[9]) - SCALE_PRESETS.teal.hue)).toBeLessThan(1);
21
+ expect(Math.abs(getHue(palette.yellow[9]) - SCALE_PRESETS.yellow.hue)).toBeLessThan(1);
22
+ });
23
+
24
+ it("shifts hue towards baseHue when baseChroma is high", () => {
25
+ const palette = makeThematicPalette({
26
+ baseHue: 200, // blue-ish
27
+ baseChroma: 0.15, // high chroma → 30% shift
28
+ accentHue: 264,
29
+ accentChroma: 0.16,
30
+ });
31
+
32
+ const redHue = getHue(palette.red[9]);
33
+ const tealHue = getHue(palette.teal[9]);
34
+ const yellowHue = getHue(palette.yellow[9]);
35
+
36
+ // Hues should differ from original presets
37
+ expect(redHue).not.toBeCloseTo(SCALE_PRESETS.red.hue, 0);
38
+ expect(tealHue).not.toBeCloseTo(SCALE_PRESETS.teal.hue, 0);
39
+ expect(yellowHue).not.toBeCloseTo(SCALE_PRESETS.yellow.hue, 0);
40
+
41
+ // Teal (183) should shift towards 200 → increases
42
+ expect(tealHue).toBeGreaterThan(SCALE_PRESETS.teal.hue);
43
+ expect(tealHue).toBeLessThan(200);
44
+
45
+ // Yellow (100) should shift towards 200 → increases
46
+ expect(yellowHue).toBeGreaterThan(SCALE_PRESETS.yellow.hue);
47
+ expect(yellowHue).toBeLessThan(200);
48
+ });
49
+
50
+ it("handles hue wrap-around correctly", () => {
51
+ // Base hue near 0, target hue near 360
52
+ const palette = makeThematicPalette({
53
+ baseHue: 350, // near red
54
+ baseChroma: 0.15,
55
+ accentHue: 264,
56
+ accentChroma: 0.16,
57
+ });
58
+
59
+ // Red (19) is close to 350 via wrap-around (350 → 360 → 19 = 29° distance)
60
+ // Should shift towards 350, so should decrease or wrap
61
+ const redHue = getHue(palette.red[9]);
62
+ // The shift should take the shortest path
63
+ expect(Math.abs(redHue - 350) < 30 || Math.abs(redHue - 350 + 360) < 30).toBe(true);
64
+ });
65
+
66
+ it("caps shift at 30%", () => {
67
+ const lowChroma = makeThematicPalette({
68
+ baseHue: 150, // green
69
+ baseChroma: 0.05, // 10% shift
70
+ accentHue: 264,
71
+ accentChroma: 0.16,
72
+ });
73
+
74
+ const highChroma = makeThematicPalette({
75
+ baseHue: 150,
76
+ baseChroma: 0.25, // would be 50% but capped at 30%
77
+ accentHue: 264,
78
+ accentChroma: 0.16,
79
+ });
80
+
81
+ const lowShiftYellow = getHue(lowChroma.yellow[9]);
82
+ const highShiftYellow = getHue(highChroma.yellow[9]);
83
+
84
+ // Yellow (100) should shift towards 150 → increases
85
+ // Higher chroma = more shift (up to 30% cap)
86
+ expect(lowShiftYellow).toBeGreaterThan(SCALE_PRESETS.yellow.hue);
87
+ expect(highShiftYellow).toBeGreaterThan(lowShiftYellow);
88
+ // But capped - 30% of 50° = 15° max additional shift
89
+ expect(highShiftYellow).toBeLessThan(150);
90
+ });
91
+ });
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const FileCounter: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"viewBox":"0 0 20 20"},"children":[{"type":"element","tagName":"path","properties":{"d":"M11.742 1.5a.75.75 0 0 1 .53.219l4.504 4.497a.752.752 0 0 1-.529 1.281h-3.672a1.583 1.583 0 0 1-1.582-1.584V3H5.745c-.69 0-1.249.56-1.249 1.25v11.5c0 .69.56 1.25 1.25 1.25h3.501a.75.75 0 0 1 0 1.5H5.745a2.749 2.749 0 0 1-2.748-2.75V4.25c0-1.519 1.23-2.75 2.748-2.75h5.997Zm2.173 8c1.668 0 2.795.902 2.795 2.281 0 1.093-.717 1.81-1.647 2.019v.036c1.176.12 1.937.872 1.937 2.06 0 1.565-1.242 2.604-3.074 2.604-1.504 0-2.428-.71-2.8-1.559a1.393 1.393 0 0 1-.126-.567c0-.466.274-.759.727-.759.329 0 .537.144.7.472.28.603.706.962 1.526.962.81 0 1.368-.514 1.368-1.23.006-.837-.558-1.302-1.515-1.302h-.345c-.399 0-.629-.257-.629-.645 0-.376.23-.64.629-.64h.323c.826 0 1.361-.49 1.361-1.194 0-.693-.426-1.152-1.224-1.152-.667 0-1.072.304-1.33.913-.18.406-.371.556-.743.556-.46 0-.695-.293-.695-.729 0-.203.038-.376.12-.58.35-.841 1.247-1.546 2.642-1.546Zm-1.423-3.587a.084.084 0 0 0 .083.084h1.86l-1.943-1.94v1.856Z"},"children":[]}],"metadata":""}]},"name":"file-counter"};
7
+
8
+ export default FileCounter;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const FileMultiple: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"viewBox":"0 0 20 20"},"children":[{"type":"element","tagName":"path","properties":{"d":"M12.742 1a.75.75 0 0 1 .53.219l4.504 4.497c.141.14.22.332.22.531v6.003A2.75 2.75 0 0 1 15.249 15h-.473a2.748 2.748 0 0 1-2.233 2.265l-6.405 1.129a2.749 2.749 0 0 1-3.183-2.23L1.477 7.791a2.75 2.75 0 0 1 2.23-3.186l2.29-.404V3.75A2.75 2.75 0 0 1 8.745 1h3.997ZM3.967 6.084a1.25 1.25 0 0 0-1.013 1.447l1.475 8.371a1.25 1.25 0 0 0 1.448 1.015l6.404-1.13c.45-.08.798-.39.95-.787H8.744a2.75 2.75 0 0 1-2.748-2.75V5.726l-2.03.358ZM8.745 2.5c-.69 0-1.25.56-1.25 1.25v8.5c0 .69.56 1.25 1.25 1.25h6.503c.69 0 1.249-.56 1.249-1.25V6.997h-2.922a1.584 1.584 0 0 1-1.582-1.584V2.5H8.745Zm4.747 2.913c0 .022.009.044.024.06a.085.085 0 0 0 .059.024h1.86l-1.943-1.94v1.856Z"},"children":[]}],"metadata":""}]},"name":"file-multiple"};
7
+
8
+ export default FileMultiple;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const ValueEdit: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20},"children":[{"type":"element","tagName":"path","properties":{"fillRule":"evenodd","clipRule":"evenodd","d":"M3.5 6.25a.75.75 0 0 1 .75-.75h11.5a.75.75 0 0 1 .75.75v2a.75.75 0 0 0 1.5 0v-2A2.25 2.25 0 0 0 15.75 4H4.25A2.25 2.25 0 0 0 2 6.25v5.5A2.25 2.25 0 0 0 4.25 14H12a.75.75 0 0 0 0-1.5H4.25a.75.75 0 0 1-.75-.75v-5.5Zm2.25 2a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 0 0-1.5h-2.5Zm9 2a.75.75 0 0 0 0 1.5h1v4.5h-1a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-1v-4.5h1a.75.75 0 0 0 0-1.5h-3.5Z"},"children":[]}],"metadata":""}]},"name":"value-edit"};
7
+
8
+ export default ValueEdit;
@@ -133,9 +133,11 @@ export { default as Fields } from './Fields';
133
133
  export { default as Figma } from './Figma';
134
134
  export { default as FileArchive } from './FileArchive';
135
135
  export { default as FileBroken } from './FileBroken';
136
+ export { default as FileCounter } from './FileCounter';
136
137
  export { default as FileDocument } from './FileDocument';
137
138
  export { default as FileDownload } from './FileDownload';
138
139
  export { default as FileImage } from './FileImage';
140
+ export { default as FileMultiple } from './FileMultiple';
139
141
  export { default as FilePresentation } from './FilePresentation';
140
142
  export { default as FileTable } from './FileTable';
141
143
  export { default as FileUpload } from './FileUpload';
@@ -404,6 +406,7 @@ export { default as UserGroupRemove } from './UserGroupRemove';
404
406
  export { default as UserGroup } from './UserGroup';
405
407
  export { default as UserRole } from './UserRole';
406
408
  export { default as UserX } from './UserX';
409
+ export { default as ValueEdit } from './ValueEdit';
407
410
  export { default as ViewAi } from './ViewAi';
408
411
  export { default as ViewBoard } from './ViewBoard';
409
412
  export { default as ViewCalendar } from './ViewCalendar';
@@ -0,0 +1,13 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import {forwardRef} from 'react';
4
+ import FileCounterSvg from '../ast/FileCounter';
5
+ import { Icon } from '../Icon';
6
+ import { IconBaseProps } from '../types';
7
+
8
+ const FileCounter = forwardRef<SVGSVGElement, IconBaseProps>(function FileCounter(
9
+ props: IconBaseProps,
10
+ ref: React.Ref<SVGSVGElement>
11
+ ) {return <Icon {...props} className={props.className} ref={ref} icon={FileCounterSvg} />});
12
+
13
+ export default FileCounter;
@@ -0,0 +1,13 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import {forwardRef} from 'react';
4
+ import FileMultipleSvg from '../ast/FileMultiple';
5
+ import { Icon } from '../Icon';
6
+ import { IconBaseProps } from '../types';
7
+
8
+ const FileMultiple = forwardRef<SVGSVGElement, IconBaseProps>(function FileMultiple(
9
+ props: IconBaseProps,
10
+ ref: React.Ref<SVGSVGElement>
11
+ ) {return <Icon {...props} className={props.className} ref={ref} icon={FileMultipleSvg} />});
12
+
13
+ export default FileMultiple;
@@ -0,0 +1,13 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import {forwardRef} from 'react';
4
+ import ValueEditSvg from '../ast/ValueEdit';
5
+ import { Icon } from '../Icon';
6
+ import { IconBaseProps } from '../types';
7
+
8
+ const ValueEdit = forwardRef<SVGSVGElement, IconBaseProps>(function ValueEdit(
9
+ props: IconBaseProps,
10
+ ref: React.Ref<SVGSVGElement>
11
+ ) {return <Icon {...props} className={props.className} ref={ref} icon={ValueEditSvg} />});
12
+
13
+ export default ValueEdit;