panelui-native 0.28.1 → 0.32.0

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 (121) hide show
  1. package/README.md +22 -3
  2. package/lib/module/components/calendar/index.js +450 -132
  3. package/lib/module/components/calendar/index.js.map +1 -1
  4. package/lib/module/components/code-block/highlight.js +363 -0
  5. package/lib/module/components/code-block/highlight.js.map +1 -0
  6. package/lib/module/components/code-block/index.js +313 -0
  7. package/lib/module/components/code-block/index.js.map +1 -0
  8. package/lib/module/components/date-picker/index.js +8 -1
  9. package/lib/module/components/date-picker/index.js.map +1 -1
  10. package/lib/module/components/heatmap-chart/index.js +9 -8
  11. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  12. package/lib/module/components/map/index.js +50 -4
  13. package/lib/module/components/map/index.js.map +1 -1
  14. package/lib/module/components/menu/index.js +714 -0
  15. package/lib/module/components/menu/index.js.map +1 -0
  16. package/lib/module/components/plan/index.js +582 -0
  17. package/lib/module/components/plan/index.js.map +1 -0
  18. package/lib/module/components/popover/index.js +35 -4
  19. package/lib/module/components/popover/index.js.map +1 -1
  20. package/lib/module/components/post/index.js +992 -0
  21. package/lib/module/components/post/index.js.map +1 -0
  22. package/lib/module/components/radio-group/index.js +13 -8
  23. package/lib/module/components/radio-group/index.js.map +1 -1
  24. package/lib/module/components/reasoning/index.js +244 -0
  25. package/lib/module/components/reasoning/index.js.map +1 -0
  26. package/lib/module/components/response/index.js +296 -0
  27. package/lib/module/components/response/index.js.map +1 -0
  28. package/lib/module/components/response/markdown.js +463 -0
  29. package/lib/module/components/response/markdown.js.map +1 -0
  30. package/lib/module/components/select/index.js +97 -15
  31. package/lib/module/components/select/index.js.map +1 -1
  32. package/lib/module/components/sources/index.js +221 -0
  33. package/lib/module/components/sources/index.js.map +1 -0
  34. package/lib/module/components/table/index.js +632 -0
  35. package/lib/module/components/table/index.js.map +1 -0
  36. package/lib/module/components/task/index.js +300 -0
  37. package/lib/module/components/task/index.js.map +1 -0
  38. package/lib/module/components/textarea/index.js +295 -0
  39. package/lib/module/components/textarea/index.js.map +1 -0
  40. package/lib/module/components/tooltip/index.js +179 -23
  41. package/lib/module/components/tooltip/index.js.map +1 -1
  42. package/lib/module/icons/index.js +339 -0
  43. package/lib/module/icons/index.js.map +1 -1
  44. package/lib/module/index.js +12 -1
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/primitives/collapse.js +112 -0
  47. package/lib/module/primitives/collapse.js.map +1 -0
  48. package/lib/module/utils/date.js +129 -22
  49. package/lib/module/utils/date.js.map +1 -1
  50. package/lib/typescript/src/components/calendar/index.d.ts +79 -5
  51. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
  53. package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
  54. package/lib/typescript/src/components/code-block/index.d.ts +113 -0
  55. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/date-picker/index.d.ts +7 -1
  57. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  59. package/lib/typescript/src/components/map/index.d.ts +8 -1
  60. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  61. package/lib/typescript/src/components/menu/index.d.ts +261 -0
  62. package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
  63. package/lib/typescript/src/components/plan/index.d.ts +238 -0
  64. package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
  65. package/lib/typescript/src/components/popover/index.d.ts +35 -1
  66. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  67. package/lib/typescript/src/components/post/index.d.ts +358 -0
  68. package/lib/typescript/src/components/post/index.d.ts.map +1 -0
  69. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
  71. package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
  72. package/lib/typescript/src/components/response/index.d.ts +91 -0
  73. package/lib/typescript/src/components/response/index.d.ts.map +1 -0
  74. package/lib/typescript/src/components/response/markdown.d.ts +88 -0
  75. package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
  76. package/lib/typescript/src/components/select/index.d.ts +27 -2
  77. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/sources/index.d.ts +85 -0
  79. package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
  80. package/lib/typescript/src/components/table/index.d.ts +274 -0
  81. package/lib/typescript/src/components/table/index.d.ts.map +1 -0
  82. package/lib/typescript/src/components/task/index.d.ts +103 -0
  83. package/lib/typescript/src/components/task/index.d.ts.map +1 -0
  84. package/lib/typescript/src/components/textarea/index.d.ts +175 -0
  85. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
  86. package/lib/typescript/src/components/tooltip/index.d.ts +63 -8
  87. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -1
  88. package/lib/typescript/src/icons/index.d.ts +54 -1
  89. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  90. package/lib/typescript/src/index.d.ts +14 -3
  91. package/lib/typescript/src/index.d.ts.map +1 -1
  92. package/lib/typescript/src/primitives/collapse.d.ts +42 -0
  93. package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
  94. package/lib/typescript/src/utils/date.d.ts +49 -5
  95. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  96. package/package.json +1 -1
  97. package/src/components/calendar/index.tsx +519 -139
  98. package/src/components/code-block/highlight.ts +278 -0
  99. package/src/components/code-block/index.tsx +341 -0
  100. package/src/components/date-picker/index.tsx +19 -1
  101. package/src/components/heatmap-chart/index.tsx +9 -9
  102. package/src/components/map/index.tsx +53 -4
  103. package/src/components/menu/index.tsx +812 -0
  104. package/src/components/plan/index.tsx +648 -0
  105. package/src/components/popover/index.tsx +69 -2
  106. package/src/components/post/index.tsx +1116 -0
  107. package/src/components/radio-group/index.tsx +11 -2
  108. package/src/components/reasoning/index.tsx +297 -0
  109. package/src/components/response/index.tsx +390 -0
  110. package/src/components/response/markdown.ts +489 -0
  111. package/src/components/select/index.tsx +132 -12
  112. package/src/components/sources/index.tsx +278 -0
  113. package/src/components/table/index.tsx +747 -0
  114. package/src/components/task/index.tsx +329 -0
  115. package/src/components/textarea/index.tsx +381 -0
  116. package/src/components/tooltip/index.tsx +203 -20
  117. package/src/icons/index.tsx +264 -1
  118. package/src/index.ts +131 -0
  119. package/src/primitives/collapse.tsx +128 -0
  120. package/src/utils/date.ts +146 -22
  121. package/theme.css +112 -0
@@ -3,9 +3,15 @@
3
3
  *
4
4
  * A popover is a panel you open and deal with; a tooltip is a whisper. It
5
5
  * carries a word or two about what a control does, appears without taking the
6
- * screen, and goes away on its own. That is why it is inverted rather than
7
- * surface-coloured, why it is not dismissible with a scrim, and why it closes
8
- * after a beat instead of waiting to be told.
6
+ * screen, and goes away on its own. That is why it is inverted by default, why
7
+ * it is not dismissible with a scrim, and why it closes after a beat instead of
8
+ * waiting to be told.
9
+ *
10
+ * The inversion is a default rather than a rule, because the whisper has a
11
+ * larger sibling: a tooltip that carries a heading and a sentence stops reading
12
+ * as a different layer and starts reading as a panel with the wrong colours.
13
+ * `variant="surface"` makes it one, and the sizing props — `width`,
14
+ * `minWidth`, `maxHeight`, `scrollable` — are what let it hold that much.
9
15
  *
10
16
  * On touch there is no hover to open it, so the gesture is a long press by
11
17
  * default — the platform's own "tell me more" gesture — with `openOn="press"`
@@ -44,6 +50,7 @@ import {
44
50
  } from 'react';
45
51
  import {
46
52
  Pressable,
53
+ ScrollView,
47
54
  useWindowDimensions,
48
55
  View,
49
56
  type LayoutChangeEvent,
@@ -58,6 +65,7 @@ import Animated, {
58
65
  withTiming,
59
66
  } from 'react-native-reanimated';
60
67
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
68
+ import { tv } from 'tailwind-variants';
61
69
  import { Portal } from '../../primitives/portal';
62
70
  import { Text, type TextProps } from '../../primitives/text';
63
71
  import { cn } from '../../utils/cn';
@@ -73,6 +81,57 @@ const DEFAULT_DURATION = 1500;
73
81
 
74
82
  export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
75
83
  export type TooltipAlign = 'start' | 'center' | 'end';
84
+ export type TooltipVariant = 'inverted' | 'surface';
85
+
86
+ /**
87
+ * Every colour the tooltip has, in one place.
88
+ *
89
+ * They were three literals at three call sites — the panel, the arrow and the
90
+ * default text — which meant retheming a tooltip took three `className`
91
+ * overrides that each had to be kept in step with the others. As a variant it
92
+ * is one prop, and the arrow and the text read it off the context rather than
93
+ * being told again.
94
+ */
95
+ const tooltipVariants = tv({
96
+ slots: {
97
+ content: 'rounded-lg px-2.5 py-1.5 shadow-md',
98
+ text: 'text-sm font-medium',
99
+ title: 'text-sm font-semibold',
100
+ description: 'text-sm',
101
+ arrow: 'rounded-[1px]',
102
+ },
103
+ variants: {
104
+ variant: {
105
+ /*
106
+ * The default, and deliberately not a surface colour: a whisper over the
107
+ * page should read as a different layer rather than as another panel of
108
+ * it. It is the treatment a one-line label wants.
109
+ */
110
+ inverted: {
111
+ content: 'bg-foreground',
112
+ text: 'text-background',
113
+ title: 'text-background',
114
+ description: 'text-background/70',
115
+ arrow: 'bg-foreground',
116
+ },
117
+ /*
118
+ * For a tooltip carrying more than a label — a heading, a sentence, a row
119
+ * of controls. At that size the inversion stops reading as a whisper and
120
+ * starts reading as a panel with the wrong colours, so it becomes one.
121
+ */
122
+ surface: {
123
+ content: 'border border-border bg-popover',
124
+ text: 'text-popover-foreground',
125
+ title: 'text-popover-foreground',
126
+ description: 'text-muted-foreground',
127
+ arrow: 'border-border bg-popover',
128
+ },
129
+ },
130
+ },
131
+ defaultVariants: {
132
+ variant: 'inverted',
133
+ },
134
+ });
76
135
 
77
136
  interface TriggerRect {
78
137
  x: number;
@@ -97,6 +156,9 @@ interface TooltipContextValue {
97
156
  openOn: TooltipOpenOn;
98
157
  /** Accessibility label carried onto the trigger. */
99
158
  label?: string;
159
+ /** Which set of colours the panel, arrow and text draw from. */
160
+ variant: TooltipVariant;
161
+ setVariant: (variant: TooltipVariant) => void;
100
162
  }
101
163
 
102
164
  export type TooltipOpenOn = 'longPress' | 'press';
@@ -149,6 +211,12 @@ function TooltipRoot({
149
211
  const [trigger, setTrigger] = useState<TriggerRect | null>(null);
150
212
  const [placement, setPlacement] = useState<TooltipPlacement>('top');
151
213
  const [arrowOffset, setArrowOffset] = useState(0);
214
+ /*
215
+ * Published by Content rather than passed to it, because the arrow and the
216
+ * text sit beside the panel in the tree and would otherwise each have to be
217
+ * told which colours to use.
218
+ */
219
+ const [variant, setVariant] = useState<TooltipVariant>('inverted');
152
220
 
153
221
  const isControlled = open !== undefined;
154
222
  const resolvedOpen = isControlled ? open : internalOpen;
@@ -186,8 +254,10 @@ function TooltipRoot({
186
254
  setArrowOffset,
187
255
  openOn,
188
256
  label,
257
+ variant,
258
+ setVariant,
189
259
  }),
190
- [resolvedOpen, show, hide, trigger, placement, arrowOffset, openOn, label]
260
+ [resolvedOpen, show, hide, trigger, placement, arrowOffset, openOn, label, variant]
191
261
  );
192
262
 
193
263
  return <TooltipContext.Provider value={context}>{children}</TooltipContext.Provider>;
@@ -282,6 +352,34 @@ export interface TooltipContentProps extends ViewProps {
282
352
  offset?: number;
283
353
  /** Nudge along the alignment axis, in pixels. */
284
354
  alignOffset?: number;
355
+ /**
356
+ * Which set of colours the panel, its arrow and its text draw from.
357
+ *
358
+ * `inverted` is the default and right for a label: a whisper over the page
359
+ * should read as a different layer rather than as another panel of it.
360
+ * `surface` matches the popover — reach for it once the tooltip carries a
361
+ * heading and a sentence, where the inversion stops reading as a whisper.
362
+ */
363
+ variant?: TooltipVariant;
364
+ /**
365
+ * `content-fit` sizes to the content, `trigger` matches the trigger's width,
366
+ * `full` spans the safe area, and a number is that many pixels. Worth setting
367
+ * for anything longer than a label, which would otherwise run to whatever
368
+ * width the sentence happens to want.
369
+ */
370
+ width?: number | 'trigger' | 'full' | 'content-fit';
371
+ /** Floor for the panel's width, in pixels. */
372
+ minWidth?: number;
373
+ /**
374
+ * Ceiling for the panel's height, in pixels. Always clamped to the room
375
+ * inside the safe area, which is also the default.
376
+ */
377
+ maxHeight?: number;
378
+ /**
379
+ * Scroll the body when it is taller than `maxHeight`. Off by default — a
380
+ * label has nothing to scroll, and a scroller around one only adds a bounce.
381
+ */
382
+ scrollable?: boolean;
285
383
  children?: ReactNode;
286
384
  }
287
385
 
@@ -291,13 +389,25 @@ function TooltipContent({
291
389
  align = 'center',
292
390
  offset = DEFAULT_OFFSET,
293
391
  alignOffset = 0,
392
+ variant = 'inverted',
393
+ width = 'content-fit',
394
+ minWidth,
395
+ maxHeight,
396
+ scrollable = false,
294
397
  children,
295
398
  onLayout: onLayoutProp,
296
399
  style,
297
400
  ...props
298
401
  }: TooltipContentProps) {
299
402
  const context = useTooltip('Tooltip.Content');
300
- const { open, hide, trigger, setPlacement, setArrowOffset } = context;
403
+ const { open, hide, trigger, setPlacement, setArrowOffset, setVariant } = context;
404
+ const slots = tooltipVariants({ variant });
405
+
406
+ // The arrow and the text sit beside this panel in the tree, so the chosen
407
+ // colours are published rather than passed down.
408
+ useEffect(() => {
409
+ setVariant(variant);
410
+ }, [variant, setVariant]);
301
411
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
302
412
  const insets = useSafeAreaInsets();
303
413
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);
@@ -332,6 +442,28 @@ function TooltipContent({
332
442
  bottom: screenHeight - insets.bottom - SCREEN_MARGIN,
333
443
  };
334
444
 
445
+ const available = bounds.right - bounds.left;
446
+ const requestedWidth =
447
+ width === 'content-fit'
448
+ ? undefined
449
+ : width === 'trigger'
450
+ ? trigger?.width
451
+ : width === 'full'
452
+ ? available
453
+ : width;
454
+
455
+ // The floor never wins past the space there actually is — a panel wider than
456
+ // the screen is worse than a cramped one.
457
+ const resolvedWidth =
458
+ requestedWidth === undefined
459
+ ? minWidth === undefined
460
+ ? undefined
461
+ : Math.min(minWidth, available)
462
+ : Math.min(Math.max(requestedWidth, minWidth ?? 0), available);
463
+
464
+ const room = bounds.bottom - bounds.top;
465
+ const resolvedMaxHeight = Math.min(maxHeight ?? room, room);
466
+
335
467
  const position =
336
468
  trigger && size
337
469
  ? place({ trigger, size, placement, align, offset, alignOffset, bounds })
@@ -418,14 +550,20 @@ function TooltipContent({
418
550
  // so it is laid out off-screen rather than at the origin.
419
551
  top: position?.top ?? -9999,
420
552
  left: position?.left ?? -9999,
421
- maxWidth: bounds.right - bounds.left,
553
+ maxWidth: available,
554
+ maxHeight: resolvedMaxHeight,
555
+ width: resolvedWidth,
422
556
  }}
423
557
  >
424
558
  <Animated.View
425
- accessibilityRole="text"
559
+ // A panel with a heading and a paragraph is a group, not a run of
560
+ // text — the flat role would have a screen reader read it as one
561
+ // string with no structure.
562
+ accessibilityRole={scrollable || width !== 'content-fit' ? undefined : 'text'}
426
563
  style={[labelStyle, style]}
427
564
  className={cn(
428
- 'rounded-lg bg-foreground px-2.5 py-1.5 shadow-md',
565
+ slots.content(),
566
+ scrollable ? 'overflow-hidden' : undefined,
429
567
  className
430
568
  )}
431
569
  {...props}
@@ -438,12 +576,12 @@ function TooltipContent({
438
576
  view bare. Only the text nodes need the treatment; an element
439
577
  is already responsible for itself.
440
578
  */}
441
- {Children.map(children, (child) =>
442
- typeof child === 'string' || typeof child === 'number' ? (
443
- <TooltipText>{child}</TooltipText>
444
- ) : (
445
- child
446
- )
579
+ {scrollable ? (
580
+ <ScrollView bounces={false} showsVerticalScrollIndicator={false}>
581
+ {wrapTooltipText(children)}
582
+ </ScrollView>
583
+ ) : (
584
+ wrapTooltipText(children)
447
585
  )}
448
586
  </Animated.View>
449
587
  </Animated.View>
@@ -453,11 +591,53 @@ function TooltipContent({
453
591
  );
454
592
  }
455
593
 
456
- /** The label's default text: small and inverted against the dark surface. */
457
- const TooltipText = ({ className, ...props }: TextProps) => (
458
- <Text size="sm" weight="medium" className={cn('text-background', className)} {...props} />
459
- );
594
+ /** Bare strings become the label's default text; elements speak for themselves. */
595
+ function wrapTooltipText(children: ReactNode) {
596
+ return Children.map(children, (child) =>
597
+ typeof child === 'string' || typeof child === 'number' ? (
598
+ <TooltipText>{child}</TooltipText>
599
+ ) : (
600
+ child
601
+ )
602
+ );
603
+ }
604
+
605
+ /** The label's default text, coloured to whatever the panel is made of. */
606
+ function TooltipText({ className, ...props }: TextProps) {
607
+ const { variant } = useTooltip('Tooltip.Text');
608
+ const { text } = tooltipVariants({ variant });
609
+
610
+ return <Text className={cn(text(), className)} {...props} />;
611
+ }
460
612
  TooltipText.displayName = 'Tooltip.Text';
613
+ TooltipTitle.displayName = 'Tooltip.Title';
614
+ TooltipDescription.displayName = 'Tooltip.Description';
615
+
616
+ export interface TooltipTitleProps extends TextProps {
617
+ className?: string;
618
+ }
619
+
620
+ /** A heading, for a tooltip carrying more than a label. */
621
+ function TooltipTitle({ className, ...props }: TooltipTitleProps) {
622
+ const { variant } = useTooltip('Tooltip.Title');
623
+ const { title } = tooltipVariants({ variant });
624
+
625
+ return (
626
+ <Text accessibilityRole="header" className={cn(title(), className)} {...props} />
627
+ );
628
+ }
629
+
630
+ export interface TooltipDescriptionProps extends TextProps {
631
+ className?: string;
632
+ }
633
+
634
+ /** The sentence under a `Tooltip.Title`, in the panel's secondary colour. */
635
+ function TooltipDescription({ className, ...props }: TooltipDescriptionProps) {
636
+ const { variant } = useTooltip('Tooltip.Description');
637
+ const { description } = tooltipVariants({ variant });
638
+
639
+ return <Text className={cn(description(), className)} {...props} />;
640
+ }
461
641
 
462
642
  export interface TooltipArrowProps extends ViewProps {
463
643
  className?: string;
@@ -471,7 +651,8 @@ export interface TooltipArrowProps extends ViewProps {
471
651
  * the trigger rather than over the label's middle.
472
652
  */
473
653
  function TooltipArrow({ className, style, ...props }: TooltipArrowProps) {
474
- const { trigger, placement, arrowOffset } = useTooltip('Tooltip.Arrow');
654
+ const { trigger, placement, arrowOffset, variant } = useTooltip('Tooltip.Arrow');
655
+ const { arrow } = tooltipVariants({ variant });
475
656
  if (!trigger) return null;
476
657
 
477
658
  const vertical = placement === 'top' || placement === 'bottom';
@@ -499,7 +680,7 @@ function TooltipArrow({ className, style, ...props }: TooltipArrowProps) {
499
680
  },
500
681
  style,
501
682
  ]}
502
- className={cn('rounded-[1px] bg-foreground', className)}
683
+ className={cn(arrow(), className)}
503
684
  {...props}
504
685
  />
505
686
  );
@@ -606,5 +787,7 @@ export const Tooltip = Object.assign(TooltipRoot, {
606
787
  Trigger: TooltipTrigger,
607
788
  Content: TooltipContent,
608
789
  Arrow: TooltipArrow,
790
+ Title: TooltipTitle,
791
+ Description: TooltipDescription,
609
792
  Text: TooltipText,
610
793
  });
@@ -133,12 +133,18 @@ export function CalendarIcon({ size = 16, color, ...props }: IconProps) {
133
133
  );
134
134
  }
135
135
 
136
- export function BookmarkIcon({ size = 16, color, ...props }: IconProps) {
136
+ export function BookmarkIcon({
137
+ size = 16,
138
+ color,
139
+ filled,
140
+ ...props
141
+ }: IconProps & { filled?: boolean }) {
137
142
  const resolved = useResolvedColor(color, '#737373');
138
143
  return (
139
144
  <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
140
145
  <Path
141
146
  d="m19 21-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"
147
+ fill={filled ? resolved : 'none'}
142
148
  stroke={resolved}
143
149
  strokeWidth={2}
144
150
  strokeLinecap="round"
@@ -872,3 +878,260 @@ export function UnlockIcon({ size = 16, color, ...props }: IconProps) {
872
878
  </Svg>
873
879
  );
874
880
  }
881
+
882
+ /**
883
+ * Four-pointed sparkles. The mark for a model doing something on your behalf —
884
+ * a reasoning step, a generated plan — where a gear or a brain reads as
885
+ * machinery rather than as assistance.
886
+ */
887
+ export function SparklesIcon({ size = 16, color, ...props }: IconProps) {
888
+ const resolved = useResolvedColor(color, '#737373');
889
+ return (
890
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
891
+ <Path
892
+ d="M11 3 12.6 8.4 18 10l-5.4 1.6L11 17l-1.6-5.4L4 10l5.4-1.6z"
893
+ stroke={resolved}
894
+ strokeWidth={2}
895
+ strokeLinejoin="round"
896
+ />
897
+ <Path
898
+ d="M18 15.5 18.7 18l2.3.7-2.3.7-.7 2.3-.7-2.3-2.3-.7 2.3-.7z"
899
+ stroke={resolved}
900
+ strokeWidth={1.6}
901
+ strokeLinejoin="round"
902
+ />
903
+ </Svg>
904
+ );
905
+ }
906
+
907
+ /** Two stacked sheets — the copy action, paired with CheckIcon once it has run. */
908
+ export function CopyIcon({ size = 16, color, ...props }: IconProps) {
909
+ const resolved = useResolvedColor(color, '#737373');
910
+ return (
911
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
912
+ <Path
913
+ d="M9 9h10v12H9z"
914
+ stroke={resolved}
915
+ strokeWidth={2}
916
+ strokeLinejoin="round"
917
+ />
918
+ <Path
919
+ d="M15 5H5v12h2"
920
+ stroke={resolved}
921
+ strokeWidth={2}
922
+ strokeLinecap="round"
923
+ strokeLinejoin="round"
924
+ />
925
+ </Svg>
926
+ );
927
+ }
928
+
929
+ /** Two links of a chain, for a row that opens somewhere outside the app. */
930
+ export function LinkIcon({ size = 16, color, ...props }: IconProps) {
931
+ const resolved = useResolvedColor(color, '#737373');
932
+ return (
933
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
934
+ <Path
935
+ d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"
936
+ stroke={resolved}
937
+ strokeWidth={2}
938
+ strokeLinecap="round"
939
+ strokeLinejoin="round"
940
+ />
941
+ <Path
942
+ d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"
943
+ stroke={resolved}
944
+ strokeWidth={2}
945
+ strokeLinecap="round"
946
+ strokeLinejoin="round"
947
+ />
948
+ </Svg>
949
+ );
950
+ }
951
+
952
+ /** A list with its items ticked off — a plan, or a run of steps. */
953
+ export function ListChecksIcon({ size = 16, color, ...props }: IconProps) {
954
+ const resolved = useResolvedColor(color, '#737373');
955
+ return (
956
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
957
+ <Path
958
+ d="m3 6 1.8 1.8L8 4.5M3 16l1.8 1.8L8 14.5"
959
+ stroke={resolved}
960
+ strokeWidth={2}
961
+ strokeLinecap="round"
962
+ strokeLinejoin="round"
963
+ />
964
+ <Path
965
+ d="M12 6.5h9M12 17.5h9"
966
+ stroke={resolved}
967
+ strokeWidth={2}
968
+ strokeLinecap="round"
969
+ />
970
+ </Svg>
971
+ );
972
+ }
973
+
974
+ /** A hollow ring, for a step that has not started. */
975
+ export function CircleIcon({ size = 16, color, ...props }: IconProps) {
976
+ const resolved = useResolvedColor(color, '#737373');
977
+ return (
978
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
979
+ <Circle cx={12} cy={12} r={8} stroke={resolved} strokeWidth={2} />
980
+ </Svg>
981
+ );
982
+ }
983
+
984
+ /* -------------------------------------------------------------------------- */
985
+ /* Social */
986
+ /* -------------------------------------------------------------------------- */
987
+
988
+ /**
989
+ * The icons a post's action row is made of.
990
+ *
991
+ * These are the only ones in the set that take `filled`, and it is not a
992
+ * stylistic choice: a like, a save and a vote are toggles, and the outline and
993
+ * the solid are the two states of one control. Drawn as two different icons
994
+ * they would swap shape under the finger; drawn as one that fills, the shape
995
+ * stays put and only the inside changes.
996
+ */
997
+ export interface ToggleIconProps extends IconProps {
998
+ /** Solid rather than outlined — the on state of a like, a save, a vote. */
999
+ filled?: boolean;
1000
+ }
1001
+
1002
+ /** Upvote. Filled once the vote is cast. */
1003
+ export function ArrowUpIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
1004
+ const resolved = useResolvedColor(color, '#737373');
1005
+ return (
1006
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1007
+ <Path
1008
+ d="M9 18v-6H5l7-7 7 7h-4v6z"
1009
+ fill={filled ? resolved : 'none'}
1010
+ stroke={resolved}
1011
+ strokeWidth={2}
1012
+ strokeLinecap="round"
1013
+ strokeLinejoin="round"
1014
+ />
1015
+ </Svg>
1016
+ );
1017
+ }
1018
+
1019
+ /** Downvote. The same arrow, turned over. */
1020
+ export function ArrowDownIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
1021
+ const resolved = useResolvedColor(color, '#737373');
1022
+ return (
1023
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1024
+ <Path
1025
+ d="M9 6v6H5l7 7 7-7h-4V6z"
1026
+ fill={filled ? resolved : 'none'}
1027
+ stroke={resolved}
1028
+ strokeWidth={2}
1029
+ strokeLinecap="round"
1030
+ strokeLinejoin="round"
1031
+ />
1032
+ </Svg>
1033
+ );
1034
+ }
1035
+
1036
+ /** A like. */
1037
+ export function HeartIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
1038
+ const resolved = useResolvedColor(color, '#737373');
1039
+ return (
1040
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1041
+ <Path
1042
+ d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
1043
+ fill={filled ? resolved : 'none'}
1044
+ stroke={resolved}
1045
+ strokeWidth={2}
1046
+ strokeLinecap="round"
1047
+ strokeLinejoin="round"
1048
+ />
1049
+ </Svg>
1050
+ );
1051
+ }
1052
+
1053
+ /** A reply, or the count of them. */
1054
+ export function MessageCircleIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
1055
+ const resolved = useResolvedColor(color, '#737373');
1056
+ return (
1057
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1058
+ <Path
1059
+ d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"
1060
+ fill={filled ? resolved : 'none'}
1061
+ stroke={resolved}
1062
+ strokeWidth={2}
1063
+ strokeLinecap="round"
1064
+ strokeLinejoin="round"
1065
+ />
1066
+ </Svg>
1067
+ );
1068
+ }
1069
+
1070
+ /** Views — how many times a thing has been seen rather than acted on. */
1071
+ export function EyeIcon({ size = 16, color, ...props }: IconProps) {
1072
+ const resolved = useResolvedColor(color, '#737373');
1073
+ return (
1074
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1075
+ <Path
1076
+ d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0"
1077
+ stroke={resolved}
1078
+ strokeWidth={2}
1079
+ strokeLinecap="round"
1080
+ strokeLinejoin="round"
1081
+ />
1082
+ <Circle cx={12} cy={12} r={3} stroke={resolved} strokeWidth={2} />
1083
+ </Svg>
1084
+ );
1085
+ }
1086
+
1087
+ /** A repost — the same thing sent round again. */
1088
+ export function RepeatIcon({ size = 16, color, ...props }: IconProps) {
1089
+ const resolved = useResolvedColor(color, '#737373');
1090
+ return (
1091
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1092
+ <Path
1093
+ d="m2 9 3-3 3 3M5 6v10a2 2 0 0 0 2 2h6M22 15l-3 3-3-3M19 18V8a2 2 0 0 0-2-2h-6"
1094
+ stroke={resolved}
1095
+ strokeWidth={2}
1096
+ strokeLinecap="round"
1097
+ strokeLinejoin="round"
1098
+ />
1099
+ </Svg>
1100
+ );
1101
+ }
1102
+
1103
+ export interface BadgeCheckIconProps extends IconProps {
1104
+ /** The tick's colour when the rosette is solid. Defaults to white. */
1105
+ checkColor?: string;
1106
+ }
1107
+
1108
+ /**
1109
+ * The verified rosette.
1110
+ *
1111
+ * Solid by default, unlike everything else here. A verification mark is a
1112
+ * claim about the account rather than a control, and an outlined one next to
1113
+ * an outlined like button reads as another thing you could press.
1114
+ */
1115
+ export function BadgeCheckIcon({
1116
+ size = 16,
1117
+ color,
1118
+ checkColor = '#ffffff',
1119
+ ...props
1120
+ }: BadgeCheckIconProps) {
1121
+ const resolved = useResolvedColor(color, '#737373');
1122
+ return (
1123
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1124
+ <Path
1125
+ d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"
1126
+ fill={resolved}
1127
+ />
1128
+ <Path
1129
+ d="m9 12 2 2 4-4"
1130
+ stroke={checkColor}
1131
+ strokeWidth={2.5}
1132
+ strokeLinecap="round"
1133
+ strokeLinejoin="round"
1134
+ />
1135
+ </Svg>
1136
+ );
1137
+ }