@sovereignfs/ui 0.33.1 → 0.40.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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/CheckableListRow.module.css +95 -0
  3. package/dist/FileDropzone.module.css +63 -0
  4. package/dist/IconPicker.module.css +63 -0
  5. package/dist/QuantityStepper.module.css +67 -0
  6. package/dist/SuggestionInput.module.css +103 -0
  7. package/dist/index.d.ts +196 -3
  8. package/dist/index.js +1175 -185
  9. package/package.json +1 -1
  10. package/src/components/CheckableListRow/CheckableListRow.module.css +95 -0
  11. package/src/components/CheckableListRow/CheckableListRow.stories.tsx +99 -0
  12. package/src/components/CheckableListRow/CheckableListRow.tsx +89 -0
  13. package/src/components/FileDropzone/FileDropzone.module.css +63 -0
  14. package/src/components/FileDropzone/FileDropzone.tsx +109 -0
  15. package/src/components/FileDropzone/__tests__/FileDropzone.test.tsx +67 -0
  16. package/src/components/Icon/icons/apple.tsx +21 -0
  17. package/src/components/Icon/icons/banana.tsx +21 -0
  18. package/src/components/Icon/icons/beef.tsx +22 -0
  19. package/src/components/Icon/icons/beer.tsx +24 -0
  20. package/src/components/Icon/icons/candy.tsx +24 -0
  21. package/src/components/Icon/icons/carrot.tsx +22 -0
  22. package/src/components/Icon/icons/coffee.tsx +23 -0
  23. package/src/components/Icon/icons/cookie.tsx +25 -0
  24. package/src/components/Icon/icons/croissant.tsx +24 -0
  25. package/src/components/Icon/icons/cup-soda.tsx +23 -0
  26. package/src/components/Icon/icons/drumstick.tsx +21 -0
  27. package/src/components/Icon/icons/egg.tsx +20 -0
  28. package/src/components/Icon/icons/external-link.tsx +22 -0
  29. package/src/components/Icon/icons/file.tsx +21 -0
  30. package/src/components/Icon/icons/fish.tsx +25 -0
  31. package/src/components/Icon/icons/index.ts +93 -1
  32. package/src/components/Icon/icons/milk.tsx +22 -0
  33. package/src/components/Icon/icons/pizza.tsx +24 -0
  34. package/src/components/Icon/icons/salad.tsx +24 -0
  35. package/src/components/Icon/icons/shopping-basket.tsx +26 -0
  36. package/src/components/Icon/icons/snowflake.tsx +31 -0
  37. package/src/components/Icon/icons/spray-can.tsx +29 -0
  38. package/src/components/Icon/icons/upload.tsx +22 -0
  39. package/src/components/Icon/icons/wine.tsx +23 -0
  40. package/src/components/IconPicker/IconPicker.module.css +63 -0
  41. package/src/components/IconPicker/IconPicker.stories.tsx +84 -0
  42. package/src/components/IconPicker/IconPicker.tsx +90 -0
  43. package/src/components/QuantityStepper/QuantityStepper.module.css +67 -0
  44. package/src/components/QuantityStepper/QuantityStepper.stories.tsx +79 -0
  45. package/src/components/QuantityStepper/QuantityStepper.tsx +107 -0
  46. package/src/components/SuggestionInput/SuggestionInput.module.css +103 -0
  47. package/src/components/SuggestionInput/SuggestionInput.stories.tsx +144 -0
  48. package/src/components/SuggestionInput/SuggestionInput.tsx +207 -0
  49. package/src/hooks/index.ts +2 -0
  50. package/src/hooks/useCommitOnEnterOrBlur.ts +39 -0
  51. package/src/index.ts +21 -2
  52. package/src/stories/DesignSystemOverview.stories.tsx +134 -1
  53. package/src/stories/FileDropzone.stories.tsx +69 -0
  54. package/src/stories/InteractionHooks.stories.tsx +55 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sovereignfs/ui",
3
- "version": "0.33.1",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "Sovereign Design System — design tokens and React components.",
6
6
  "keywords": [
@@ -0,0 +1,95 @@
1
+ .row {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-3);
5
+ width: 100%;
6
+ }
7
+
8
+ .tapTarget {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: var(--sv-space-2);
12
+ flex: 1;
13
+ min-width: 0;
14
+ min-height: var(--sv-touch-target-min);
15
+ cursor: pointer;
16
+ }
17
+
18
+ .tapTarget:focus-visible {
19
+ outline: 2px solid var(--sv-color-focus-ring);
20
+ outline-offset: 2px;
21
+ border-radius: var(--sv-radius-sm);
22
+ }
23
+
24
+ /* Visual box — matches Checkbox.module.css's .box/.checked/.tick. */
25
+ .box {
26
+ --checkbox-box-size: 18px;
27
+ flex-shrink: 0;
28
+ width: var(--checkbox-box-size);
29
+ height: var(--checkbox-box-size);
30
+ border-radius: var(--sv-radius-sm);
31
+ border: 1.5px solid var(--sv-color-border-strong);
32
+ background: var(--sv-color-surface);
33
+ transition:
34
+ background-color 0.15s ease,
35
+ border-color 0.15s ease;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ }
40
+
41
+ .checked {
42
+ background-color: var(--sv-color-accent);
43
+ border-color: var(--sv-color-accent);
44
+ color: var(--sv-white);
45
+ }
46
+
47
+ .tick {
48
+ width: 10px;
49
+ height: 8px;
50
+ pointer-events: none;
51
+ }
52
+
53
+ .icon {
54
+ display: flex;
55
+ align-items: center;
56
+ flex-shrink: 0;
57
+ color: var(--sv-color-text-muted);
58
+ }
59
+
60
+ .label {
61
+ flex: 1;
62
+ min-width: 0;
63
+ font-size: var(--sv-font-size-sm);
64
+ color: var(--sv-color-text-primary);
65
+ overflow: hidden;
66
+ text-overflow: ellipsis;
67
+ white-space: nowrap;
68
+ transition: color 0.15s ease;
69
+ position: relative;
70
+ }
71
+
72
+ .label::after {
73
+ content: '';
74
+ position: absolute;
75
+ left: 0;
76
+ top: 50%;
77
+ width: 100%;
78
+ height: 1.5px;
79
+ background: var(--sv-color-text-muted);
80
+ transform: scaleX(0);
81
+ transform-origin: left center;
82
+ transition: transform 0.2s ease;
83
+ }
84
+
85
+ .struck {
86
+ color: var(--sv-color-text-muted);
87
+ }
88
+
89
+ .struck::after {
90
+ transform: scaleX(1);
91
+ }
92
+
93
+ .trailing {
94
+ flex-shrink: 0;
95
+ }
@@ -0,0 +1,99 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { CheckableListRow } from './CheckableListRow';
4
+ import { Icon } from '../Icon/Icon';
5
+
6
+ const meta = {
7
+ title: 'Components/CheckableListRow',
8
+ component: CheckableListRow,
9
+ parameters: {
10
+ layout: 'padded',
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'A whole-row tap target that toggles a checked state, with strike-through on the label when checked — for "tap the row to mark it done" lists.',
15
+ },
16
+ },
17
+ },
18
+ args: {
19
+ checked: false,
20
+ onCheckedChange: () => {},
21
+ label: 'Bananas',
22
+ },
23
+ } satisfies Meta<typeof CheckableListRow>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ function ControlledRow({
29
+ initial = false,
30
+ icon,
31
+ trailing,
32
+ disabled,
33
+ }: {
34
+ initial?: boolean;
35
+ icon?: boolean;
36
+ trailing?: boolean;
37
+ disabled?: boolean;
38
+ }) {
39
+ const [checked, setChecked] = useState(initial);
40
+ return (
41
+ <div style={{ width: 320 }}>
42
+ <CheckableListRow
43
+ checked={checked}
44
+ onCheckedChange={setChecked}
45
+ label="Bananas"
46
+ icon={icon ? <Icon name="banana" size="md" aria-hidden /> : undefined}
47
+ trailing={trailing ? <span style={{ fontSize: 13, color: '#888' }}>6 pcs</span> : undefined}
48
+ disabled={disabled}
49
+ />
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export const Default: Story = {
55
+ render: () => <ControlledRow />,
56
+ };
57
+
58
+ export const Checked: Story = {
59
+ render: () => <ControlledRow initial />,
60
+ };
61
+
62
+ export const WithIconAndTrailing: Story = {
63
+ render: () => <ControlledRow icon trailing />,
64
+ };
65
+
66
+ export const CheckedWithIconAndTrailing: Story = {
67
+ render: () => <ControlledRow initial icon trailing />,
68
+ };
69
+
70
+ export const Disabled: Story = {
71
+ render: () => <ControlledRow icon trailing disabled />,
72
+ };
73
+
74
+ export const List: Story = {
75
+ name: 'Multiple rows (list context)',
76
+ render: () => {
77
+ const items = ['Bananas', 'Milk', 'Eggs', 'Coffee'];
78
+ const [checkedSet, setCheckedSet] = useState<Set<string>>(new Set(['Milk']));
79
+ return (
80
+ <div style={{ width: 320, display: 'flex', flexDirection: 'column', gap: 4 }}>
81
+ {items.map((item) => (
82
+ <CheckableListRow
83
+ key={item}
84
+ checked={checkedSet.has(item)}
85
+ onCheckedChange={(checked) =>
86
+ setCheckedSet((prev) => {
87
+ const next = new Set(prev);
88
+ if (checked) next.add(item);
89
+ else next.delete(item);
90
+ return next;
91
+ })
92
+ }
93
+ label={item}
94
+ />
95
+ ))}
96
+ </div>
97
+ );
98
+ },
99
+ };
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import type { KeyboardEvent, ReactNode } from 'react';
4
+ import styles from './CheckableListRow.module.css';
5
+
6
+ export interface CheckableListRowProps {
7
+ checked: boolean;
8
+ onCheckedChange: (checked: boolean) => void;
9
+ label: string;
10
+ /** Leading icon, e.g. a per-item `<Icon>`. */
11
+ icon?: ReactNode;
12
+ /** Trailing content, e.g. a quantity, a `QuantityStepper`, or a badge. */
13
+ trailing?: ReactNode;
14
+ disabled?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * CheckableListRow — a whole-row tap target that toggles a checked state,
20
+ * with strike-through on the label when checked. Built for "tap the row to
21
+ * mark it done" lists (Sovereign Shopper's tap-to-buy, SHP-07) where the
22
+ * checkbox itself is too small a target on mobile and the row already has
23
+ * nothing else competing for the tap.
24
+ *
25
+ * Renders as `role="checkbox"` on the row itself rather than composing
26
+ * `Checkbox`'s native `<input>` — nesting a real checkbox input inside the
27
+ * row's own click handler would mean two overlapping interactive elements
28
+ * (invalid HTML, and the input's own click would double-fire the row's
29
+ * toggle). The visual box matches `Checkbox`'s styling for consistency.
30
+ *
31
+ * `trailing` content (e.g. a `QuantityStepper`) is rendered outside the
32
+ * `role="checkbox"` element so its own interactive controls (+/- buttons)
33
+ * don't sit inside another interactive element either — clicking those
34
+ * doesn't toggle the row.
35
+ */
36
+ export function CheckableListRow({
37
+ checked,
38
+ onCheckedChange,
39
+ label,
40
+ icon,
41
+ trailing,
42
+ disabled = false,
43
+ className,
44
+ }: CheckableListRowProps) {
45
+ function toggle() {
46
+ if (!disabled) onCheckedChange(!checked);
47
+ }
48
+
49
+ function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
50
+ if (e.key === ' ' || e.key === 'Enter') {
51
+ e.preventDefault();
52
+ toggle();
53
+ }
54
+ }
55
+
56
+ return (
57
+ <div className={[styles.row, className].filter(Boolean).join(' ')}>
58
+ <div
59
+ role="checkbox"
60
+ aria-checked={checked}
61
+ aria-label={label}
62
+ aria-disabled={disabled || undefined}
63
+ tabIndex={disabled ? -1 : 0}
64
+ className={styles.tapTarget}
65
+ onClick={toggle}
66
+ onKeyDown={handleKeyDown}
67
+ >
68
+ <span className={[styles.box, checked ? styles.checked : ''].filter(Boolean).join(' ')}>
69
+ {checked && (
70
+ <svg className={styles.tick} viewBox="0 0 10 8" fill="none" aria-hidden="true">
71
+ <path
72
+ d="M1 4l3 3 5-6"
73
+ stroke="currentColor"
74
+ strokeWidth="1.5"
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ />
78
+ </svg>
79
+ )}
80
+ </span>
81
+ {icon && <span className={styles.icon}>{icon}</span>}
82
+ <span className={[styles.label, checked ? styles.struck : ''].filter(Boolean).join(' ')}>
83
+ {label}
84
+ </span>
85
+ </div>
86
+ {trailing && <span className={styles.trailing}>{trailing}</span>}
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,63 @@
1
+ .dropzone {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--sv-space-4);
5
+ width: 100%;
6
+ box-sizing: border-box;
7
+ padding: var(--sv-space-4) var(--sv-space-5);
8
+ border: 1.5px dashed var(--sv-color-border);
9
+ border-radius: var(--sv-radius-md);
10
+ background-color: var(--sv-color-surface-raised);
11
+ cursor: pointer;
12
+ transition:
13
+ background-color 0.12s ease,
14
+ border-color 0.12s ease;
15
+ }
16
+
17
+ @media (hover: hover) {
18
+ .dropzone:hover {
19
+ background-color: var(--sv-color-surface-sunken);
20
+ border-color: var(--sv-color-border-strong);
21
+ }
22
+ }
23
+
24
+ .dropzoneActive {
25
+ background-color: var(--sv-color-surface-sunken);
26
+ border-color: var(--sv-color-border-strong);
27
+ }
28
+
29
+ .dropzoneDisabled {
30
+ opacity: 0.5;
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ .icon {
35
+ display: inline-flex;
36
+ flex-shrink: 0;
37
+ color: var(--sv-color-text-muted);
38
+ }
39
+
40
+ .text {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 2px;
44
+ min-width: 0;
45
+ }
46
+
47
+ .title {
48
+ font-size: var(--sv-font-size-sm);
49
+ font-weight: var(--sv-font-weight-medium);
50
+ color: var(--sv-color-text-primary);
51
+ overflow: hidden;
52
+ text-overflow: ellipsis;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .hint {
57
+ font-size: var(--sv-font-size-xs);
58
+ color: var(--sv-color-text-muted);
59
+ }
60
+
61
+ .hiddenInput {
62
+ display: none;
63
+ }
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import type { DragEvent, ReactNode } from 'react';
4
+ import { useRef, useState } from 'react';
5
+ import { Icon } from '../Icon/Icon';
6
+ import styles from './FileDropzone.module.css';
7
+
8
+ export interface FileDropzoneProps {
9
+ /** Native `<input type="file">` `name` attribute — set this to submit the
10
+ * file as part of a native `<form action={...}>` (Server Action) request. */
11
+ name?: string;
12
+ /** Native `<input type="file">` `accept` attribute, e.g. `".zip,application/zip"`. */
13
+ accept?: string;
14
+ /** Icon shown beside the label — a file-type glyph. Defaults to a generic file icon. */
15
+ icon?: ReactNode;
16
+ /** Primary label — e.g. "Choose a ZIP file", or the selected file's name. */
17
+ label: string;
18
+ /** Secondary hint — e.g. "or drag and drop here", or the selected file's size. */
19
+ hint?: string;
20
+ /** Called with the picked or dropped file, or `null` if the input is cleared. */
21
+ onFileSelect: (file: File | null) => void;
22
+ /** Accessible name for the control — describe what will be uploaded. */
23
+ ariaLabel: string;
24
+ disabled?: boolean;
25
+ className?: string;
26
+ }
27
+
28
+ /**
29
+ * A styled drag-and-drop file picker: a dashed-border dropzone wrapping a
30
+ * visually-hidden native `<input type="file">`, so the native picker still
31
+ * opens on click and the field participates in normal form semantics. A
32
+ * dropped file is written into the input's own `FileList` (via `DataTransfer`)
33
+ * so `name` + native `<form action>` submission sees it exactly like a
34
+ * click-picked file — not just the `onFileSelect` callback.
35
+ *
36
+ * Caller owns the selected-file state and passes `label`/`hint` accordingly
37
+ * (e.g. swap to the picked file's name/size) — this component is otherwise
38
+ * uncontrolled and stateless beyond its own drag-hover visual.
39
+ *
40
+ * Do not pass `required` through to a wrapping form's validation via this
41
+ * field — `display: none` on the hidden input does not exempt it from HTML5
42
+ * constraint validation, so a `required` hidden input can block submission
43
+ * with no visible field for the user to focus. Validate `onFileSelect`'s
44
+ * result in application code instead.
45
+ */
46
+ export function FileDropzone({
47
+ name,
48
+ accept,
49
+ icon,
50
+ label,
51
+ hint,
52
+ onFileSelect,
53
+ ariaLabel,
54
+ disabled,
55
+ className,
56
+ }: FileDropzoneProps) {
57
+ const [dragging, setDragging] = useState(false);
58
+ const inputRef = useRef<HTMLInputElement>(null);
59
+
60
+ return (
61
+ <label
62
+ className={[
63
+ styles.dropzone,
64
+ dragging ? styles.dropzoneActive : '',
65
+ disabled ? styles.dropzoneDisabled : '',
66
+ className,
67
+ ]
68
+ .filter(Boolean)
69
+ .join(' ')}
70
+ onDragOver={(event: DragEvent) => {
71
+ if (disabled) return;
72
+ event.preventDefault();
73
+ setDragging(true);
74
+ }}
75
+ onDragLeave={() => setDragging(false)}
76
+ onDrop={(event: DragEvent) => {
77
+ if (disabled) return;
78
+ event.preventDefault();
79
+ setDragging(false);
80
+ const dropped = event.dataTransfer.files[0];
81
+ if (!dropped) return;
82
+ if (inputRef.current && typeof DataTransfer !== 'undefined') {
83
+ const transfer = new DataTransfer();
84
+ transfer.items.add(dropped);
85
+ inputRef.current.files = transfer.files;
86
+ }
87
+ onFileSelect(dropped);
88
+ }}
89
+ >
90
+ <span className={styles.icon} aria-hidden="true">
91
+ {icon ?? <Icon name="file" size="lg" aria-hidden />}
92
+ </span>
93
+ <span className={styles.text}>
94
+ <span className={styles.title}>{label}</span>
95
+ {hint && <span className={styles.hint}>{hint}</span>}
96
+ </span>
97
+ <input
98
+ ref={inputRef}
99
+ type="file"
100
+ name={name}
101
+ aria-label={ariaLabel}
102
+ accept={accept}
103
+ disabled={disabled}
104
+ onChange={(event) => onFileSelect(event.target.files?.[0] ?? null)}
105
+ className={styles.hiddenInput}
106
+ />
107
+ </label>
108
+ );
109
+ }
@@ -0,0 +1,67 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
4
+ import { FileDropzone } from '../FileDropzone';
5
+
6
+ afterEach(cleanup);
7
+
8
+ describe('FileDropzone', () => {
9
+ it('renders the label and hint', () => {
10
+ render(
11
+ <FileDropzone
12
+ label="Choose a ZIP file"
13
+ hint="or drag and drop here"
14
+ ariaLabel="Upload ZIP file"
15
+ onFileSelect={vi.fn()}
16
+ />,
17
+ );
18
+ expect(screen.getByText('Choose a ZIP file')).toBeDefined();
19
+ expect(screen.getByText('or drag and drop here')).toBeDefined();
20
+ });
21
+
22
+ it('exposes an accessible name via aria-label', () => {
23
+ render(
24
+ <FileDropzone label="Choose a file" ariaLabel="Upload font file" onFileSelect={vi.fn()} />,
25
+ );
26
+ expect(screen.getByLabelText('Upload font file')).toBeDefined();
27
+ });
28
+
29
+ it('forwards accept to the native file input', () => {
30
+ render(
31
+ <FileDropzone
32
+ label="Choose a file"
33
+ ariaLabel="Upload"
34
+ accept=".zip"
35
+ onFileSelect={vi.fn()}
36
+ />,
37
+ );
38
+ const input = screen.getByLabelText('Upload') as HTMLInputElement;
39
+ expect(input.accept).toBe('.zip');
40
+ });
41
+
42
+ it('calls onFileSelect with the picked file', () => {
43
+ const onFileSelect = vi.fn();
44
+ render(<FileDropzone label="Choose a file" ariaLabel="Upload" onFileSelect={onFileSelect} />);
45
+ const input = screen.getByLabelText('Upload') as HTMLInputElement;
46
+ const file = new File(['content'], 'font.woff2');
47
+ fireEvent.change(input, { target: { files: [file] } });
48
+ expect(onFileSelect).toHaveBeenCalledWith(file);
49
+ });
50
+
51
+ it('calls onFileSelect with the dropped file', () => {
52
+ const onFileSelect = vi.fn();
53
+ render(<FileDropzone label="Choose a file" ariaLabel="Upload" onFileSelect={onFileSelect} />);
54
+ const dropzone = screen.getByLabelText('Upload').closest('label');
55
+ if (!dropzone) throw new Error('dropzone label not found');
56
+ const file = new File(['content'], 'font.woff2');
57
+ fireEvent.drop(dropzone, { dataTransfer: { files: [file] } });
58
+ expect(onFileSelect).toHaveBeenCalledWith(file);
59
+ });
60
+
61
+ it('is disabled when the disabled prop is set', () => {
62
+ render(
63
+ <FileDropzone label="Choose a file" ariaLabel="Upload" disabled onFileSelect={vi.fn()} />,
64
+ );
65
+ expect((screen.getByLabelText('Upload') as HTMLInputElement).disabled).toBe(true);
66
+ });
67
+ });
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function AppleIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M12 6.528V3a1 1 0 0 1 1-1h0" />
18
+ <path d="M18.237 21A15 15 0 0 0 22 11a6 6 0 0 0-10-4.472A6 6 0 0 0 2 11a15.1 15.1 0 0 0 3.763 10 3 3 0 0 0 3.648.648 5.5 5.5 0 0 1 5.178 0A3 3 0 0 0 18.237 21" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function BananaIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M4 13c3.5-2 8-2 10 2a5.5 5.5 0 0 1 8 5" />
18
+ <path d="M5.15 17.89c5.52-1.52 8.65-6.89 7-12C11.55 4 11.5 2 13 2c3.22 0 5 5.5 5 8 0 6.5-4.2 12-10.49 12C5.11 22 2 22 2 20c0-1.5 1.14-1.55 3.15-2.11Z" />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,22 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function BeefIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M16.4 13.7A6.5 6.5 0 1 0 6.28 6.6c-1.1 3.13-.78 3.9-3.18 6.08A3 3 0 0 0 5 18c4 0 8.4-1.8 11.4-4.3" />
18
+ <path d="m18.5 6 2.19 4.5a6.48 6.48 0 0 1-2.29 7.2C15.4 20.2 11 22 7 22a3 3 0 0 1-2.68-1.66L2.4 16.5" />
19
+ <circle cx="12.5" cy="8.5" r="2.5" />
20
+ </svg>
21
+ );
22
+ }
@@ -0,0 +1,24 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function BeerIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M17 11h1a3 3 0 0 1 0 6h-1" />
18
+ <path d="M9 12v6" />
19
+ <path d="M13 12v6" />
20
+ <path d="M14 7.5c-1 0-1.44.5-3 .5s-2-.5-3-.5-1.72.5-2.5.5a2.5 2.5 0 0 1 0-5c.78 0 1.57.5 2.5.5S9.44 2 11 2s2 1.5 3 1.5 1.72-.5 2.5-.5a2.5 2.5 0 0 1 0 5c-.78 0-1.5-.5-2.5-.5Z" />
21
+ <path d="M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8" />
22
+ </svg>
23
+ );
24
+ }
@@ -0,0 +1,24 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function CandyIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M10 7v10.9" />
18
+ <path d="M14 6.1V17" />
19
+ <path d="M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4" />
20
+ <path d="M16.536 7.465a5 5 0 0 0-7.072 0l-2 2a5 5 0 0 0 0 7.07 5 5 0 0 0 7.072 0l2-2a5 5 0 0 0 0-7.07" />
21
+ <path d="M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4" />
22
+ </svg>
23
+ );
24
+ }
@@ -0,0 +1,22 @@
1
+ // Generated by scripts/generate-icons.ts — do not edit.
2
+ // Source: Lucide (https://lucide.dev) — ISC license, see packages/ui/NOTICE.
3
+ import type { SVGProps } from 'react';
4
+
5
+ export function CarrotIcon(props: SVGProps<SVGSVGElement>) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ {...props}
16
+ >
17
+ <path d="M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46" />
18
+ <path d="M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z" />
19
+ <path d="M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z" />
20
+ </svg>
21
+ );
22
+ }