@a-type/ui 0.3.1 → 0.3.3
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.
- package/dist/cjs/components/button/Button.js +6 -1
- package/dist/cjs/components/button/Button.js.map +1 -1
- package/dist/cjs/components/camera/Camera.d.ts +1 -0
- package/dist/cjs/components/camera/Camera.js +24 -18
- package/dist/cjs/components/camera/Camera.js.map +1 -1
- package/dist/cjs/components/icon/generated/IconSpritesheet.js +1 -1
- package/dist/cjs/components/icon/generated/IconSpritesheet.js.map +1 -1
- package/dist/cjs/components/icon/generated/iconNames.d.ts +1 -1
- package/dist/cjs/components/icon/generated/iconNames.js +10 -0
- package/dist/cjs/components/icon/generated/iconNames.js.map +1 -1
- package/dist/cjs/components/imageUploader/ImageUploader.js +28 -2
- package/dist/cjs/components/imageUploader/ImageUploader.js.map +1 -1
- package/dist/cjs/components/imageUploader/ImageUploader.stories.d.ts +22 -0
- package/dist/cjs/components/imageUploader/ImageUploader.stories.js +28 -0
- package/dist/cjs/components/imageUploader/ImageUploader.stories.js.map +1 -0
- package/dist/esm/components/button/Button.js +6 -1
- package/dist/esm/components/button/Button.js.map +1 -1
- package/dist/esm/components/camera/Camera.d.ts +1 -0
- package/dist/esm/components/camera/Camera.js +24 -18
- package/dist/esm/components/camera/Camera.js.map +1 -1
- package/dist/esm/components/icon/generated/IconSpritesheet.js +1 -1
- package/dist/esm/components/icon/generated/IconSpritesheet.js.map +1 -1
- package/dist/esm/components/icon/generated/iconNames.d.ts +1 -1
- package/dist/esm/components/icon/generated/iconNames.js +10 -0
- package/dist/esm/components/icon/generated/iconNames.js.map +1 -1
- package/dist/esm/components/imageUploader/ImageUploader.js +29 -3
- package/dist/esm/components/imageUploader/ImageUploader.js.map +1 -1
- package/dist/esm/components/imageUploader/ImageUploader.stories.d.ts +22 -0
- package/dist/esm/components/imageUploader/ImageUploader.stories.js +25 -0
- package/dist/esm/components/imageUploader/ImageUploader.stories.js.map +1 -0
- package/package.json +3 -2
- package/src/components/actions/ActionBar.tsx +38 -0
- package/src/components/actions/ActionButton.tsx +59 -0
- package/src/components/actions/index.ts +2 -0
- package/src/components/actions.ts +1 -0
- package/src/components/avatar/Avatar.tsx +62 -0
- package/src/components/avatar/AvatarList.tsx +71 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/avatar.ts +1 -0
- package/src/components/button/Button.stories.tsx +20 -0
- package/src/components/button/Button.tsx +66 -0
- package/src/components/button/ConfirmedButton.tsx +66 -0
- package/src/components/button/classes.tsx +56 -0
- package/src/components/button/index.ts +3 -0
- package/src/components/button.ts +1 -0
- package/src/components/camera/Camera.stories.tsx +40 -0
- package/src/components/camera/Camera.tsx +215 -0
- package/src/components/camera/index.ts +1 -0
- package/src/components/camera.ts +1 -0
- package/src/components/card/Card.stories.tsx +41 -0
- package/src/components/card/Card.tsx +68 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/card.ts +1 -0
- package/src/components/checkbox/Checkbox.tsx +46 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/checkbox.ts +1 -0
- package/src/components/chip/Chip.tsx +29 -0
- package/src/components/chip/index.ts +1 -0
- package/src/components/chip.ts +1 -0
- package/src/components/collapsible/Collapsible.tsx +48 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/collapsible.ts +1 -0
- package/src/components/colorPicker/ColorPicker.tsx +82 -0
- package/src/components/colorPicker/index.ts +1 -0
- package/src/components/colorPicker.ts +1 -0
- package/src/components/contextMenu/contextMenu.tsx +43 -0
- package/src/components/contextMenu.ts +1 -0
- package/src/components/dialog/Dialog.stories.tsx +38 -0
- package/src/components/dialog/Dialog.tsx +267 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dialog.ts +1 -0
- package/src/components/divider/Divider.tsx +26 -0
- package/src/components/divider/index.ts +1 -0
- package/src/components/divider.ts +1 -0
- package/src/components/dropdownMenu/DropdownMenu.stories.tsx +47 -0
- package/src/components/dropdownMenu/DropdownMenu.tsx +89 -0
- package/src/components/dropdownMenu/index.ts +1 -0
- package/src/components/dropdownMenu.ts +1 -0
- package/src/components/errorBoundary/ErrorBoundary.tsx +23 -0
- package/src/components/errorBoundary/index.ts +1 -0
- package/src/components/errorBoundary.ts +1 -0
- package/src/components/forms/Form.tsx +9 -0
- package/src/components/forms/FormikForm.tsx +41 -0
- package/src/components/forms/SubmitButton.tsx +15 -0
- package/src/components/forms/TextField.tsx +112 -0
- package/src/components/forms/index.tsx +4 -0
- package/src/components/forms.ts +1 -0
- package/src/components/icon/Icon.tsx +28 -0
- package/src/components/icon/generated/IconSpritesheet.tsx +442 -0
- package/src/components/icon/generated/iconNames.ts +44 -0
- package/src/components/icon/index.ts +3 -0
- package/src/components/icon.ts +1 -0
- package/src/components/imageUploader/ImageUploader.stories.tsx +39 -0
- package/src/components/imageUploader/ImageUploader.tsx +203 -0
- package/src/components/imageUploader/UploadIcon.tsx +23 -0
- package/src/components/imageUploader/index.ts +1 -0
- package/src/components/imageUploader.ts +1 -0
- package/src/components/infiniteLoadTrigger/InfiniteLoadTrigger.tsx +38 -0
- package/src/components/infiniteLoadTrigger.ts +1 -0
- package/src/components/input/Input.stories.tsx +17 -0
- package/src/components/input/Input.tsx +32 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input.ts +1 -0
- package/src/components/layouts/PageContent.tsx +51 -0
- package/src/components/layouts/PageFixedArea.tsx +17 -0
- package/src/components/layouts/PageNav.tsx +23 -0
- package/src/components/layouts/PageNowPlaying.tsx +24 -0
- package/src/components/layouts/PageRoot.tsx +29 -0
- package/src/components/layouts/PageSection.tsx +23 -0
- package/src/components/layouts/index.tsx +6 -0
- package/src/components/layouts.ts +1 -0
- package/src/components/liveUpdateTextField/LiveUpdateTextField.tsx +132 -0
- package/src/components/liveUpdateTextField/index.ts +1 -0
- package/src/components/liveUpdateTextField.ts +1 -0
- package/src/components/navBar/NavBar.tsx +59 -0
- package/src/components/navBar/index.ts +1 -0
- package/src/components/navBar.ts +1 -0
- package/src/components/note/Note.tsx +21 -0
- package/src/components/note/index.ts +1 -0
- package/src/components/note.ts +1 -0
- package/src/components/numberStepper/NumberStepper.stories.tsx +21 -0
- package/src/components/numberStepper/NumberStepper.tsx +74 -0
- package/src/components/numberStepper/index.ts +1 -0
- package/src/components/numberStepper.ts +1 -0
- package/src/components/particles/ParticleContext.tsx +11 -0
- package/src/components/particles/ParticleLayer.stories.tsx +46 -0
- package/src/components/particles/ParticleLayer.tsx +28 -0
- package/src/components/particles/index.ts +7 -0
- package/src/components/particles/particlesState.ts +502 -0
- package/src/components/particles.ts +1 -0
- package/src/components/peek/Peek.tsx +74 -0
- package/src/components/peek/index.ts +1 -0
- package/src/components/peek.ts +1 -0
- package/src/components/popover/Popover.tsx +84 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover.ts +1 -0
- package/src/components/relativeTime/RelativeTime.tsx +43 -0
- package/src/components/relativeTime/index.ts +1 -0
- package/src/components/relativeTime.ts +1 -0
- package/src/components/richEditor/EditorContent.tsx +4 -0
- package/src/components/richEditor/RichEditor.tsx +38 -0
- package/src/components/richEditor/index.ts +1 -0
- package/src/components/richEditor.ts +1 -0
- package/src/components/select/Select.tsx +247 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select.ts +1 -0
- package/src/components/skeletons/skeletons.tsx +27 -0
- package/src/components/skeletons.ts +1 -0
- package/src/components/spinner/Spinner.tsx +59 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner.ts +1 -0
- package/src/components/switch/Switch.tsx +23 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch.ts +1 -0
- package/src/components/tabs/tabs.tsx +18 -0
- package/src/components/tabs.ts +1 -0
- package/src/components/textArea/TextArea.stories.tsx +21 -0
- package/src/components/textArea/TextArea.tsx +58 -0
- package/src/components/textArea/index.ts +1 -0
- package/src/components/textArea.ts +1 -0
- package/src/components/toggleGroup/toggleGroup.tsx +11 -0
- package/src/components/toggleGroup.ts +1 -0
- package/src/components/tooltip/Tooltip.tsx +56 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip.ts +1 -0
- package/src/components/typography/index.ts +1 -0
- package/src/components/typography/typography.tsx +18 -0
- package/src/components/typography.ts +1 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useMergedRef.ts +14 -0
- package/src/hooks/useOnUnmount.ts +20 -0
- package/src/hooks/useSize.ts +164 -0
- package/src/hooks/useStableCallback.ts +11 -0
- package/src/hooks/useToggle.tsx +9 -0
- package/src/hooks/useVisualViewportOffset.ts +35 -0
- package/src/hooks/withClassName.tsx +21 -0
- package/src/hooks.ts +1 -0
- package/src/uno.preset.ts +767 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Button } from './Button.js';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Button',
|
|
6
|
+
component: Button,
|
|
7
|
+
argTypes: {},
|
|
8
|
+
parameters: {
|
|
9
|
+
controls: { expanded: true },
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
children: 'Button',
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof Button>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof Button>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { forwardRef, ButtonHTMLAttributes } from 'react';
|
|
3
|
+
import { Spinner } from '../spinner.js';
|
|
4
|
+
import { getButtonClassName } from './classes.js';
|
|
5
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
color?:
|
|
9
|
+
| 'primary'
|
|
10
|
+
| 'default'
|
|
11
|
+
| 'ghost'
|
|
12
|
+
| 'destructive'
|
|
13
|
+
| 'ghostDestructive'
|
|
14
|
+
| 'accent'
|
|
15
|
+
| 'contrast';
|
|
16
|
+
size?: 'default' | 'small' | 'icon';
|
|
17
|
+
toggled?: boolean;
|
|
18
|
+
align?: 'start' | 'stretch' | 'end';
|
|
19
|
+
visuallyDisabled?: boolean;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
asChild?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
25
|
+
function Button(
|
|
26
|
+
{
|
|
27
|
+
className,
|
|
28
|
+
color,
|
|
29
|
+
size,
|
|
30
|
+
toggled,
|
|
31
|
+
align,
|
|
32
|
+
visuallyDisabled,
|
|
33
|
+
loading,
|
|
34
|
+
children,
|
|
35
|
+
disabled,
|
|
36
|
+
asChild,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref,
|
|
40
|
+
) {
|
|
41
|
+
const Comp = asChild ? Slot : 'button';
|
|
42
|
+
const buttonProps = {
|
|
43
|
+
ref: ref,
|
|
44
|
+
...props,
|
|
45
|
+
disabled: disabled || loading,
|
|
46
|
+
'data-disabled': visuallyDisabled,
|
|
47
|
+
tabIndex: visuallyDisabled ? -1 : undefined,
|
|
48
|
+
className: classNames(
|
|
49
|
+
getButtonClassName({ color, size, toggled, align }),
|
|
50
|
+
className,
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (asChild) {
|
|
55
|
+
// avoid rendering loading spinner with asChild
|
|
56
|
+
return <Comp {...buttonProps}>{children}</Comp>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Comp {...buttonProps}>
|
|
61
|
+
{loading && <Spinner size={16} className="inline-block w-1em h-1em" />}
|
|
62
|
+
{children}
|
|
63
|
+
</Comp>
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogActions,
|
|
5
|
+
DialogClose,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
DialogTrigger,
|
|
9
|
+
} from '../dialog.js';
|
|
10
|
+
import { P } from '../typography.js';
|
|
11
|
+
import { Button, ButtonProps } from './Button.js';
|
|
12
|
+
|
|
13
|
+
export interface ConfirmedButtonProps extends Omit<ButtonProps, 'onClick'> {
|
|
14
|
+
confirmText: string;
|
|
15
|
+
confirmTitle?: string;
|
|
16
|
+
confirmAction?: string;
|
|
17
|
+
cancelAction?: string;
|
|
18
|
+
onConfirm: () => void | Promise<any>;
|
|
19
|
+
skip?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ConfirmedButton({
|
|
23
|
+
confirmText,
|
|
24
|
+
confirmTitle = 'Are you sure?',
|
|
25
|
+
confirmAction = 'OK',
|
|
26
|
+
cancelAction = 'Nevermind',
|
|
27
|
+
onConfirm,
|
|
28
|
+
skip,
|
|
29
|
+
...rest
|
|
30
|
+
}: ConfirmedButtonProps) {
|
|
31
|
+
const [open, setOpen] = useState(false);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const confirm = useCallback(async () => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
try {
|
|
36
|
+
await onConfirm();
|
|
37
|
+
setOpen(false);
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}, [onConfirm]);
|
|
42
|
+
|
|
43
|
+
if (skip) {
|
|
44
|
+
return <Button {...rest} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
49
|
+
<DialogTrigger asChild>
|
|
50
|
+
<Button {...rest} />
|
|
51
|
+
</DialogTrigger>
|
|
52
|
+
<DialogContent>
|
|
53
|
+
<DialogTitle>{confirmTitle}</DialogTitle>
|
|
54
|
+
<P>{confirmText}</P>
|
|
55
|
+
<DialogActions>
|
|
56
|
+
<DialogClose asChild>
|
|
57
|
+
<Button>{cancelAction}</Button>
|
|
58
|
+
</DialogClose>
|
|
59
|
+
<Button loading={loading} onClick={confirm} color="primary">
|
|
60
|
+
{confirmAction}
|
|
61
|
+
</Button>
|
|
62
|
+
</DialogActions>
|
|
63
|
+
</DialogContent>
|
|
64
|
+
</Dialog>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import type { ButtonProps } from './Button.jsx';
|
|
3
|
+
|
|
4
|
+
export function getButtonClassName({
|
|
5
|
+
color,
|
|
6
|
+
size,
|
|
7
|
+
toggled,
|
|
8
|
+
align,
|
|
9
|
+
}: {
|
|
10
|
+
color?: ButtonProps['color'];
|
|
11
|
+
size?: ButtonProps['size'];
|
|
12
|
+
toggled?: boolean;
|
|
13
|
+
align?: ButtonProps['align'];
|
|
14
|
+
}) {
|
|
15
|
+
return classNames(
|
|
16
|
+
'layer-components:(px-4 py-2 bg-[var(--bg)] [--webkit-tap-highlight-color:transparent] [line-height:1] text-size-md font-sans border border-solid border-transparent rounded-full cursor-pointer font-bold flex flex-row gap-1 items-center relative overflow-visible select-none all:transition duration-200 shadow-none whitespace-nowrap)',
|
|
17
|
+
'layer-components:hover:(bg-[var(--hover)] shadow-[0_0_0_6px_var(--hover)])',
|
|
18
|
+
'layer-components:focus:outline-off',
|
|
19
|
+
'layer-components:focus-visible:(outline-off shadow-[0_0_0_6px_var(--focus,var(--hover))])',
|
|
20
|
+
'layer-components:active:(shadow-[0_0_0_6px_var(--active)] bg-[var(--active)])',
|
|
21
|
+
'important:disabled:(opacity-50 cursor-default bg-[(--var-bg)] shadow-none)',
|
|
22
|
+
'important:[&[data-disabled=true]]:(opacity-50 cursor-default bg-[(--var-bg)] shadow-none)',
|
|
23
|
+
colors[color ?? 'default'],
|
|
24
|
+
`btn-color-${color ?? 'default'}`,
|
|
25
|
+
sizes[size ?? 'default'],
|
|
26
|
+
`size-${size ?? 'default'}`,
|
|
27
|
+
toggled && toggledClass,
|
|
28
|
+
align && aligns[align],
|
|
29
|
+
// compound variants
|
|
30
|
+
color === 'ghost' && toggled && 'layer-variants:bg-primary-wash',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const colors = {
|
|
35
|
+
primary: `layer-variants:[&.btn-color-primary]:([--bg:var(--color-primary-light)] [--hover:var(--color-primary)] [--focus:var(--color-primary)] [--active:var(--color-primary)] color-black border-black focus-visible:([--bg:var(--color-primary)]))`,
|
|
36
|
+
accent: `layer-variants:[&.btn-color-accent]:([--bg:var(--color-accent-wash)] [--hover:var(--color-accent-light)] [--focus:var(--color-accent-light)] [--active:var(--color-accent-light)] color-black border-black focus-visible:([--bg:var(--color-accent-light)]))`,
|
|
37
|
+
default: `layer-variants:[&.btn-color-default]:([--bg:var(--color-white)] [--hover:var(--color-gray-2)] [--focus:var(--color-gray-7)] [--active:var(--color-gray-3)] color-black border-black)`,
|
|
38
|
+
ghost: `layer-variants:[&.btn-color-ghost]:([--bg:transparent] [--hover:var(--color-gray-blend)] [--focus:var(--color-gray-7)] [--active:var(--color-gray-dark-blend)] color-dark-blend hover:([--bg:var(--color-gray-blend)]) focus-visible:([--bg:var(--color-gray-blend)] [--hover:var(--color-gray-7)]))`,
|
|
39
|
+
destructive: `layer-variants:[&.btn-color-destructive]:([--bg:var(--color-attention-light)] [--hover:var(--color-attention-light)] [--focus:var(--color-attention-light)] [--active:var(--color-attention-light)] border-black color-black hover:([--bg:var(--colors-attention)]))`,
|
|
40
|
+
ghostDestructive: `layer-variants:[&.btn-color-ghostDestructive]:([--bg:transparent] [--hover:var(--color-attention-light)] [--focus:var(--color-attention-light)] [--active:var(-color-attention-light)] color-attention-dark)`,
|
|
41
|
+
contrast: `layer-variants:[&.btn-color-contrast]:([--bg:var(--color-black)] [--hover:var(--color-gray-7)] [--focus:var(--color-gray-7)] [--active:var(--color-gray-6)] color-white border-black)`,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const sizes = {
|
|
45
|
+
default: '',
|
|
46
|
+
small: 'layer-variants:[&.size-small]:(px-4 py-1 text-sm rounded-full)',
|
|
47
|
+
icon: 'layer-variants:[&.size-icon]:(p-2 text-sm rounded-full) layer-variants:focus-visible:shadow-[0_0_0_2px_var(--focus,var(--hover))]',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const toggledClass = 'hover:(filter-brightness-[1.1])';
|
|
51
|
+
|
|
52
|
+
const aligns = {
|
|
53
|
+
start: 'self-start',
|
|
54
|
+
stretch: 'self-stretch',
|
|
55
|
+
end: 'self-end',
|
|
56
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './button/index.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import {
|
|
3
|
+
CameraDeviceSelector,
|
|
4
|
+
CameraRoot,
|
|
5
|
+
CameraShutterButton,
|
|
6
|
+
} from './Camera.js';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Camera',
|
|
11
|
+
argTypes: {},
|
|
12
|
+
parameters: {
|
|
13
|
+
controls: { expanded: true },
|
|
14
|
+
},
|
|
15
|
+
args: {},
|
|
16
|
+
} satisfies Meta;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
|
|
20
|
+
type Story = StoryObj;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render() {
|
|
24
|
+
return <CameraDemo />;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function CameraDemo() {
|
|
29
|
+
const [latest, setLatest] = useState<string | undefined>();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div>
|
|
33
|
+
<CameraRoot onCapture={setLatest} className="w-64 h-64">
|
|
34
|
+
<CameraShutterButton />
|
|
35
|
+
<CameraDeviceSelector />
|
|
36
|
+
</CameraRoot>
|
|
37
|
+
{latest && <img src={latest} className="w-full" />}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import {
|
|
3
|
+
MouseEvent,
|
|
4
|
+
ReactNode,
|
|
5
|
+
createContext,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { Button } from '../button.js';
|
|
13
|
+
import { Icon } from '../icon.js';
|
|
14
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select.js';
|
|
15
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
16
|
+
import { withClassName } from '../../hooks.js';
|
|
17
|
+
|
|
18
|
+
const CameraContext = createContext<{
|
|
19
|
+
triggerCapture: () => void;
|
|
20
|
+
selectedDeviceId: string | undefined;
|
|
21
|
+
selectDeviceId: (id: string) => void;
|
|
22
|
+
devices: MediaDeviceInfo[];
|
|
23
|
+
}>({
|
|
24
|
+
triggerCapture: () => {},
|
|
25
|
+
selectedDeviceId: 'default',
|
|
26
|
+
selectDeviceId: () => {},
|
|
27
|
+
devices: [],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export interface CameraRootProps {
|
|
31
|
+
className?: string;
|
|
32
|
+
onCapture?: (data: string) => void;
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
format?: 'image/png' | 'image/jpeg';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const CameraRoot = forwardRef<HTMLDivElement, CameraRootProps>(
|
|
38
|
+
function Camera(
|
|
39
|
+
{ className, onCapture, children, format = 'image/png', ...rest },
|
|
40
|
+
ref,
|
|
41
|
+
) {
|
|
42
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
43
|
+
|
|
44
|
+
const triggerCapture = () => {
|
|
45
|
+
const video = videoRef.current;
|
|
46
|
+
if (video) {
|
|
47
|
+
const canvas = document.createElement('canvas');
|
|
48
|
+
canvas.width = video.videoWidth;
|
|
49
|
+
canvas.height = video.videoHeight;
|
|
50
|
+
canvas.getContext('2d')?.drawImage(video, 0, 0);
|
|
51
|
+
const data = canvas.toDataURL(format);
|
|
52
|
+
onCapture?.(data);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
navigator.mediaDevices.enumerateDevices().then((devices): void => {
|
|
59
|
+
setDevices(devices.filter((device) => device.kind === 'videoinput'));
|
|
60
|
+
});
|
|
61
|
+
}, []);
|
|
62
|
+
const [selectedDeviceId, setSelectedDeviceId] = useState<
|
|
63
|
+
string | undefined
|
|
64
|
+
>();
|
|
65
|
+
const [stream, setStream] = useState<MediaStream | undefined>();
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
navigator.mediaDevices
|
|
68
|
+
.getUserMedia({
|
|
69
|
+
video: {
|
|
70
|
+
deviceId: selectedDeviceId,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
.then((s) => {
|
|
74
|
+
setStream(s);
|
|
75
|
+
});
|
|
76
|
+
}, [selectedDeviceId]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
return () => {
|
|
79
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
80
|
+
};
|
|
81
|
+
}, [stream]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const video = videoRef.current;
|
|
85
|
+
if (video && stream) {
|
|
86
|
+
video.srcObject = stream;
|
|
87
|
+
return () => {
|
|
88
|
+
video.srcObject = null;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}, [stream]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<CameraContext.Provider
|
|
95
|
+
value={{
|
|
96
|
+
devices,
|
|
97
|
+
triggerCapture,
|
|
98
|
+
selectedDeviceId,
|
|
99
|
+
selectDeviceId: setSelectedDeviceId,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<div
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={classNames(
|
|
105
|
+
'layer-components:([font-family:inherit] text-white bg-black rounded-lg overflow-hidden min-w-4 min-h-4 relative)',
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
{...rest}
|
|
109
|
+
>
|
|
110
|
+
<video
|
|
111
|
+
ref={videoRef}
|
|
112
|
+
className="w-full h-full object-cover"
|
|
113
|
+
autoPlay
|
|
114
|
+
muted
|
|
115
|
+
playsInline
|
|
116
|
+
></video>
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
</CameraContext.Provider>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
export interface CameraShutterButtonProps {
|
|
125
|
+
className?: string;
|
|
126
|
+
asChild?: boolean;
|
|
127
|
+
onClick?: (ev: MouseEvent<HTMLButtonElement>) => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const CameraShutterButton = forwardRef<
|
|
131
|
+
HTMLButtonElement,
|
|
132
|
+
CameraShutterButtonProps
|
|
133
|
+
>(function CameraShutterButton({ asChild, onClick, ...rest }, ref) {
|
|
134
|
+
const Comp = asChild ? Slot : StyledShutterButton;
|
|
135
|
+
const { triggerCapture } = useContext(CameraContext);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Comp
|
|
139
|
+
ref={ref}
|
|
140
|
+
aria-label={asChild ? undefined : 'Capture photo'}
|
|
141
|
+
onClick={(ev) => {
|
|
142
|
+
triggerCapture();
|
|
143
|
+
onClick?.(ev);
|
|
144
|
+
}}
|
|
145
|
+
{...rest}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const StyledShutterButton = withClassName(
|
|
151
|
+
'button',
|
|
152
|
+
'absolute bottom-3 left-1/2 -translate-x-1/2 w-16 h-16 bg-white rounded-full cursor-pointer border-2 border-black border-solid ring-2 ring-white opacity-80',
|
|
153
|
+
'hover:bg-gray-1 hover:opacity-100',
|
|
154
|
+
'focus-visible:bg-gray-2',
|
|
155
|
+
'focus:ring-primary focus:outline-none focus:opacity-100',
|
|
156
|
+
'sm:w-8 sm:h-8',
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
export interface CameraDeviceSelectorProps {}
|
|
160
|
+
|
|
161
|
+
export const CameraDeviceSelector = (props: CameraDeviceSelectorProps) => {
|
|
162
|
+
const { devices, selectDeviceId, selectedDeviceId } =
|
|
163
|
+
useContext(CameraContext);
|
|
164
|
+
const swapCamera = () => {
|
|
165
|
+
if (selectedDeviceId) {
|
|
166
|
+
const index = devices.findIndex(
|
|
167
|
+
(device) => device.deviceId === selectedDeviceId,
|
|
168
|
+
);
|
|
169
|
+
if (index === -1) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
selectDeviceId(devices[(index + 1) % devices.length].deviceId);
|
|
173
|
+
} else {
|
|
174
|
+
selectDeviceId(devices[0].deviceId);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (!devices.length || devices.length === 1) return null;
|
|
179
|
+
if (devices.length === 2) {
|
|
180
|
+
return (
|
|
181
|
+
<Button
|
|
182
|
+
size="icon"
|
|
183
|
+
color="ghost"
|
|
184
|
+
className="absolute bottom-2 left-2 text-white"
|
|
185
|
+
onClick={swapCamera}
|
|
186
|
+
>
|
|
187
|
+
<Icon name="refresh" />
|
|
188
|
+
</Button>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<Select
|
|
194
|
+
value={selectedDeviceId || 'default'}
|
|
195
|
+
onValueChange={selectDeviceId}
|
|
196
|
+
>
|
|
197
|
+
<SelectTrigger asChild>
|
|
198
|
+
<Button
|
|
199
|
+
size="icon"
|
|
200
|
+
color="ghost"
|
|
201
|
+
className="absolute bottom-2 left-2 text-white"
|
|
202
|
+
>
|
|
203
|
+
<Icon name="refresh" />
|
|
204
|
+
</Button>
|
|
205
|
+
</SelectTrigger>
|
|
206
|
+
<SelectContent>
|
|
207
|
+
{devices.map((device) => (
|
|
208
|
+
<SelectItem key={device.deviceId} value={device.deviceId}>
|
|
209
|
+
{device.label}
|
|
210
|
+
</SelectItem>
|
|
211
|
+
))}
|
|
212
|
+
</SelectContent>
|
|
213
|
+
</Select>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Camera.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './camera/index.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import {
|
|
3
|
+
CardActions,
|
|
4
|
+
CardFooter,
|
|
5
|
+
CardMain,
|
|
6
|
+
CardRoot,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from './Card.js';
|
|
9
|
+
import { Button } from '../button.js';
|
|
10
|
+
import { Icon } from '../icon.js';
|
|
11
|
+
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'Card',
|
|
14
|
+
component: CardRoot,
|
|
15
|
+
argTypes: {},
|
|
16
|
+
parameters: {
|
|
17
|
+
controls: { expanded: true },
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof CardRoot>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
type Story = StoryObj<typeof CardRoot>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<CardRoot>
|
|
28
|
+
<CardMain>
|
|
29
|
+
<CardTitle>Card Title</CardTitle>
|
|
30
|
+
</CardMain>
|
|
31
|
+
<CardFooter>
|
|
32
|
+
<CardActions>
|
|
33
|
+
<Button size="small">Button</Button>
|
|
34
|
+
<Button size="icon" color="ghost">
|
|
35
|
+
<Icon name="placeholder" />
|
|
36
|
+
</Button>
|
|
37
|
+
</CardActions>
|
|
38
|
+
</CardFooter>
|
|
39
|
+
</CardRoot>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { MouseEvent, ReactNode, forwardRef } from 'react';
|
|
2
|
+
import { withClassName } from '../../hooks.js';
|
|
3
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
|
|
6
|
+
export const CardRoot = withClassName(
|
|
7
|
+
'div',
|
|
8
|
+
'layer-components:(flex flex-col border-light rounded-lg text-lg overflow-hidden h-max-content relative bg-gray-1)',
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const CardMain = forwardRef<
|
|
12
|
+
any,
|
|
13
|
+
{
|
|
14
|
+
asChild?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
onClick?: (ev: MouseEvent) => void;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
}
|
|
20
|
+
>(function CardMain({ asChild, className, compact, ...rest }, ref) {
|
|
21
|
+
const Comp = asChild ? Slot : 'button';
|
|
22
|
+
return (
|
|
23
|
+
<Comp
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={classNames(
|
|
26
|
+
'layer-components:(flex flex-col gap-1 cursor-pointer transition p-4 pb-2 flex-1 relative z-1 bg-transparent border-none text-start)',
|
|
27
|
+
'layer-components:hover:(bg-lightBlend color-black)',
|
|
28
|
+
'layer-components:md:pt-4',
|
|
29
|
+
compact && 'layer-variants:(p-1 bg-white gap-0)',
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
data-compact={compact}
|
|
33
|
+
{...rest}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const CardTitle = withClassName(
|
|
39
|
+
'div',
|
|
40
|
+
'layer-components:(flex flex-col gap-1 mt-auto bg-white p-2 rounded-lg w-auto mr-auto border border-solid border-grayDarkBlend text-md max-h-80px overflow-hidden text-ellipsis max-w-full)',
|
|
41
|
+
'[data-compact=true]>&:(bg-transparent border-none p-2 whitespace-nowrap text-ellipsis overflow-hidden)',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export const CardImage = withClassName(
|
|
45
|
+
'div',
|
|
46
|
+
'layer-components:(absolute z-0 right-0 top-0 bottom-0 w-full h-full)',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export const CardFooter = withClassName(
|
|
50
|
+
'div',
|
|
51
|
+
'layer-components:(flex flex-row p-2 bg-white relative z-1 border-0 border-t border-t-grayDarkBlend border-solid)',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const CardActions = withClassName(
|
|
55
|
+
'div',
|
|
56
|
+
'layer-components:(ml-0 mr-auto flex flex-row gap-1 items-center)',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export const CardMenu = withClassName(
|
|
60
|
+
'div',
|
|
61
|
+
'layer-components:(mr-0 ml-auto flex flex-row gap-1 items-center)',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export const CardGrid = withClassName(
|
|
65
|
+
'div',
|
|
66
|
+
'layer-components:(grid grid-cols-[1fr] [grid-auto-rows:auto] gap-4 p-0 m-0)',
|
|
67
|
+
'layer-components:md:(grid-cols-[repeat(2,1fr)] [grid-auto-rows:1fr] items-end)',
|
|
68
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Card.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './card/index.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
4
|
+
import { ComponentProps, forwardRef } from 'react';
|
|
5
|
+
import { CheckIcon } from '@radix-ui/react-icons';
|
|
6
|
+
import classNames from 'classnames';
|
|
7
|
+
import { withClassName } from '../../hooks/withClassName.js';
|
|
8
|
+
|
|
9
|
+
export const CheckboxRoot = withClassName(
|
|
10
|
+
CheckboxPrimitive.Root,
|
|
11
|
+
classNames(
|
|
12
|
+
'layer-components:(w-28px h-28px flex-shrink-0 relative bg-white border-default transition rounded-full)',
|
|
13
|
+
'layer-components:focus-visible:(outline-off shadow-focus)',
|
|
14
|
+
'layer-components:[&[data-state=checked]]:(bg-primary-light border-primary-dark)',
|
|
15
|
+
'layer-components:[&:hover:not(:disabled)]:shadow-[0_0_0_1px_var(--color-black)]',
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export function CheckboxIndicator({
|
|
20
|
+
children,
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: CheckboxPrimitive.CheckboxIndicatorProps) {
|
|
24
|
+
return (
|
|
25
|
+
<CheckboxPrimitive.Indicator
|
|
26
|
+
className={classNames(
|
|
27
|
+
'absolute center translate-[-50%] color-black',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{children ?? <CheckIcon width={18} height={18} />}
|
|
33
|
+
</CheckboxPrimitive.Indicator>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Checkbox = forwardRef<
|
|
38
|
+
HTMLButtonElement,
|
|
39
|
+
ComponentProps<typeof CheckboxRoot>
|
|
40
|
+
>(function Checkbox(props, ref) {
|
|
41
|
+
return (
|
|
42
|
+
<CheckboxRoot ref={ref} {...props}>
|
|
43
|
+
<CheckboxIndicator />
|
|
44
|
+
</CheckboxRoot>
|
|
45
|
+
);
|
|
46
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Checkbox.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './checkbox/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { HTMLAttributes, forwardRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface ChipProps extends HTMLAttributes<HTMLElement> {
|
|
6
|
+
color?: 'neutral' | 'primary' | 'accent';
|
|
7
|
+
asChild?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Chip = forwardRef<any, ChipProps>(function Chip(
|
|
11
|
+
{ className, color = 'neutral', asChild, ...rest },
|
|
12
|
+
ref,
|
|
13
|
+
) {
|
|
14
|
+
const Component = asChild ? Slot : 'div';
|
|
15
|
+
return (
|
|
16
|
+
<Component
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={classNames(
|
|
19
|
+
'inline-flex flex-row gap-1 items-center whitespace-nowrap border-light border-solid border-1 rounded-full px-2 py-1',
|
|
20
|
+
{
|
|
21
|
+
'bg-primary-wash': color === 'primary',
|
|
22
|
+
'bg-accent-wash': color === 'accent',
|
|
23
|
+
},
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...rest}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Chip.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './chip/index.js';
|