@a-type/ui 0.3.2 → 0.4.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 (169) hide show
  1. package/dist/cjs/components/datePicker/DatePicker.d.ts +19 -0
  2. package/dist/cjs/components/datePicker/DatePicker.js +97 -0
  3. package/dist/cjs/components/datePicker/DatePicker.js.map +1 -0
  4. package/dist/cjs/components/datePicker/DatePicker.stories.d.ts +16 -0
  5. package/dist/cjs/components/datePicker/DatePicker.stories.js +28 -0
  6. package/dist/cjs/components/datePicker/DatePicker.stories.js.map +1 -0
  7. package/dist/cjs/components/datePicker/index.d.ts +1 -0
  8. package/dist/cjs/components/datePicker/index.js +3 -0
  9. package/dist/cjs/components/datePicker/index.js.map +1 -0
  10. package/dist/esm/components/datePicker/DatePicker.d.ts +19 -0
  11. package/dist/esm/components/datePicker/DatePicker.js +89 -0
  12. package/dist/esm/components/datePicker/DatePicker.js.map +1 -0
  13. package/dist/esm/components/datePicker/DatePicker.stories.d.ts +16 -0
  14. package/dist/esm/components/datePicker/DatePicker.stories.js +25 -0
  15. package/dist/esm/components/datePicker/DatePicker.stories.js.map +1 -0
  16. package/dist/esm/components/datePicker/index.d.ts +1 -0
  17. package/dist/esm/components/datePicker/index.js +2 -0
  18. package/dist/esm/components/datePicker/index.js.map +1 -0
  19. package/package.json +4 -2
  20. package/src/components/actions/ActionBar.tsx +38 -0
  21. package/src/components/actions/ActionButton.tsx +59 -0
  22. package/src/components/actions/index.ts +2 -0
  23. package/src/components/actions.ts +1 -0
  24. package/src/components/avatar/Avatar.tsx +62 -0
  25. package/src/components/avatar/AvatarList.tsx +71 -0
  26. package/src/components/avatar/index.ts +2 -0
  27. package/src/components/avatar.ts +1 -0
  28. package/src/components/button/Button.stories.tsx +20 -0
  29. package/src/components/button/Button.tsx +66 -0
  30. package/src/components/button/ConfirmedButton.tsx +66 -0
  31. package/src/components/button/classes.tsx +56 -0
  32. package/src/components/button/index.ts +3 -0
  33. package/src/components/button.ts +1 -0
  34. package/src/components/camera/Camera.stories.tsx +40 -0
  35. package/src/components/camera/Camera.tsx +215 -0
  36. package/src/components/camera/index.ts +1 -0
  37. package/src/components/camera.ts +1 -0
  38. package/src/components/card/Card.stories.tsx +41 -0
  39. package/src/components/card/Card.tsx +68 -0
  40. package/src/components/card/index.ts +1 -0
  41. package/src/components/card.ts +1 -0
  42. package/src/components/checkbox/Checkbox.tsx +46 -0
  43. package/src/components/checkbox/index.ts +1 -0
  44. package/src/components/checkbox.ts +1 -0
  45. package/src/components/chip/Chip.tsx +29 -0
  46. package/src/components/chip/index.ts +1 -0
  47. package/src/components/chip.ts +1 -0
  48. package/src/components/collapsible/Collapsible.tsx +48 -0
  49. package/src/components/collapsible/index.ts +1 -0
  50. package/src/components/collapsible.ts +1 -0
  51. package/src/components/colorPicker/ColorPicker.tsx +82 -0
  52. package/src/components/colorPicker/index.ts +1 -0
  53. package/src/components/colorPicker.ts +1 -0
  54. package/src/components/contextMenu/contextMenu.tsx +43 -0
  55. package/src/components/contextMenu.ts +1 -0
  56. package/src/components/datePicker/DatePicker.stories.tsx +33 -0
  57. package/src/components/datePicker/DatePicker.tsx +258 -0
  58. package/src/components/datePicker/index.ts +0 -0
  59. package/src/components/dialog/Dialog.stories.tsx +38 -0
  60. package/src/components/dialog/Dialog.tsx +267 -0
  61. package/src/components/dialog/index.ts +1 -0
  62. package/src/components/dialog.ts +1 -0
  63. package/src/components/divider/Divider.tsx +26 -0
  64. package/src/components/divider/index.ts +1 -0
  65. package/src/components/divider.ts +1 -0
  66. package/src/components/dropdownMenu/DropdownMenu.stories.tsx +47 -0
  67. package/src/components/dropdownMenu/DropdownMenu.tsx +89 -0
  68. package/src/components/dropdownMenu/index.ts +1 -0
  69. package/src/components/dropdownMenu.ts +1 -0
  70. package/src/components/errorBoundary/ErrorBoundary.tsx +23 -0
  71. package/src/components/errorBoundary/index.ts +1 -0
  72. package/src/components/errorBoundary.ts +1 -0
  73. package/src/components/forms/Form.tsx +9 -0
  74. package/src/components/forms/FormikForm.tsx +41 -0
  75. package/src/components/forms/SubmitButton.tsx +15 -0
  76. package/src/components/forms/TextField.tsx +112 -0
  77. package/src/components/forms/index.tsx +4 -0
  78. package/src/components/forms.ts +1 -0
  79. package/src/components/icon/Icon.tsx +28 -0
  80. package/src/components/icon/generated/IconSpritesheet.tsx +442 -0
  81. package/src/components/icon/generated/iconNames.ts +44 -0
  82. package/src/components/icon/index.ts +3 -0
  83. package/src/components/icon.ts +1 -0
  84. package/src/components/imageUploader/ImageUploader.stories.tsx +39 -0
  85. package/src/components/imageUploader/ImageUploader.tsx +203 -0
  86. package/src/components/imageUploader/UploadIcon.tsx +23 -0
  87. package/src/components/imageUploader/index.ts +1 -0
  88. package/src/components/imageUploader.ts +1 -0
  89. package/src/components/infiniteLoadTrigger/InfiniteLoadTrigger.tsx +38 -0
  90. package/src/components/infiniteLoadTrigger.ts +1 -0
  91. package/src/components/input/Input.stories.tsx +17 -0
  92. package/src/components/input/Input.tsx +32 -0
  93. package/src/components/input/index.ts +1 -0
  94. package/src/components/input.ts +1 -0
  95. package/src/components/layouts/PageContent.tsx +51 -0
  96. package/src/components/layouts/PageFixedArea.tsx +17 -0
  97. package/src/components/layouts/PageNav.tsx +23 -0
  98. package/src/components/layouts/PageNowPlaying.tsx +24 -0
  99. package/src/components/layouts/PageRoot.tsx +29 -0
  100. package/src/components/layouts/PageSection.tsx +23 -0
  101. package/src/components/layouts/index.tsx +6 -0
  102. package/src/components/layouts.ts +1 -0
  103. package/src/components/liveUpdateTextField/LiveUpdateTextField.tsx +132 -0
  104. package/src/components/liveUpdateTextField/index.ts +1 -0
  105. package/src/components/liveUpdateTextField.ts +1 -0
  106. package/src/components/navBar/NavBar.tsx +59 -0
  107. package/src/components/navBar/index.ts +1 -0
  108. package/src/components/navBar.ts +1 -0
  109. package/src/components/note/Note.tsx +21 -0
  110. package/src/components/note/index.ts +1 -0
  111. package/src/components/note.ts +1 -0
  112. package/src/components/numberStepper/NumberStepper.stories.tsx +21 -0
  113. package/src/components/numberStepper/NumberStepper.tsx +74 -0
  114. package/src/components/numberStepper/index.ts +1 -0
  115. package/src/components/numberStepper.ts +1 -0
  116. package/src/components/particles/ParticleContext.tsx +11 -0
  117. package/src/components/particles/ParticleLayer.stories.tsx +46 -0
  118. package/src/components/particles/ParticleLayer.tsx +28 -0
  119. package/src/components/particles/index.ts +7 -0
  120. package/src/components/particles/particlesState.ts +502 -0
  121. package/src/components/particles.ts +1 -0
  122. package/src/components/peek/Peek.tsx +74 -0
  123. package/src/components/peek/index.ts +1 -0
  124. package/src/components/peek.ts +1 -0
  125. package/src/components/popover/Popover.tsx +84 -0
  126. package/src/components/popover/index.ts +1 -0
  127. package/src/components/popover.ts +1 -0
  128. package/src/components/relativeTime/RelativeTime.tsx +43 -0
  129. package/src/components/relativeTime/index.ts +1 -0
  130. package/src/components/relativeTime.ts +1 -0
  131. package/src/components/richEditor/EditorContent.tsx +4 -0
  132. package/src/components/richEditor/RichEditor.tsx +38 -0
  133. package/src/components/richEditor/index.ts +1 -0
  134. package/src/components/richEditor.ts +1 -0
  135. package/src/components/select/Select.tsx +247 -0
  136. package/src/components/select/index.ts +1 -0
  137. package/src/components/select.ts +1 -0
  138. package/src/components/skeletons/skeletons.tsx +27 -0
  139. package/src/components/skeletons.ts +1 -0
  140. package/src/components/spinner/Spinner.tsx +59 -0
  141. package/src/components/spinner/index.ts +1 -0
  142. package/src/components/spinner.ts +1 -0
  143. package/src/components/switch/Switch.tsx +23 -0
  144. package/src/components/switch/index.ts +1 -0
  145. package/src/components/switch.ts +1 -0
  146. package/src/components/tabs/tabs.tsx +18 -0
  147. package/src/components/tabs.ts +1 -0
  148. package/src/components/textArea/TextArea.stories.tsx +21 -0
  149. package/src/components/textArea/TextArea.tsx +58 -0
  150. package/src/components/textArea/index.ts +1 -0
  151. package/src/components/textArea.ts +1 -0
  152. package/src/components/toggleGroup/toggleGroup.tsx +11 -0
  153. package/src/components/toggleGroup.ts +1 -0
  154. package/src/components/tooltip/Tooltip.tsx +56 -0
  155. package/src/components/tooltip/index.ts +1 -0
  156. package/src/components/tooltip.ts +1 -0
  157. package/src/components/typography/index.ts +1 -0
  158. package/src/components/typography/typography.tsx +18 -0
  159. package/src/components/typography.ts +1 -0
  160. package/src/hooks/index.ts +7 -0
  161. package/src/hooks/useMergedRef.ts +14 -0
  162. package/src/hooks/useOnUnmount.ts +20 -0
  163. package/src/hooks/useSize.ts +164 -0
  164. package/src/hooks/useStableCallback.ts +11 -0
  165. package/src/hooks/useToggle.tsx +9 -0
  166. package/src/hooks/useVisualViewportOffset.ts +35 -0
  167. package/src/hooks/withClassName.tsx +21 -0
  168. package/src/hooks.ts +1 -0
  169. package/src/uno.preset.ts +767 -0
@@ -0,0 +1,203 @@
1
+ 'use client';
2
+
3
+ import classNames from 'classnames';
4
+ import { useCallback, useId, useState } from 'react';
5
+ import { Icon } from '../icon.js';
6
+ import { Button } from '../button.js';
7
+ import {
8
+ CameraDeviceSelector,
9
+ CameraRoot,
10
+ CameraShutterButton,
11
+ } from '../camera.js';
12
+
13
+ export interface ImageUploaderProps {
14
+ value: string | null;
15
+ onChange: (value: File | null) => void;
16
+ className?: string;
17
+ maxDimension?: number;
18
+ }
19
+
20
+ /**
21
+ * Renders an image if one is already set, and allows either clicking
22
+ * on the image to select a new one, or dragging a new image onto the
23
+ * component to replace the existing one.
24
+ */
25
+ export function ImageUploader({
26
+ value,
27
+ onChange: handleChange,
28
+ maxDimension,
29
+ ...rest
30
+ }: ImageUploaderProps) {
31
+ const inputId = useId();
32
+ const [dragging, setDragging] = useState(false);
33
+ const [draggingOver, setDraggingOver] = useState(false);
34
+
35
+ const onDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
36
+ e.preventDefault();
37
+ e.stopPropagation();
38
+ setDraggingOver(true);
39
+ }, []);
40
+
41
+ const onDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
42
+ e.preventDefault();
43
+ e.stopPropagation();
44
+ setDraggingOver(false);
45
+ }, []);
46
+
47
+ const onDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
48
+ e.preventDefault();
49
+ e.stopPropagation();
50
+ setDraggingOver(true);
51
+ }, []);
52
+
53
+ const onChange = useCallback(
54
+ async (file: File | null) => {
55
+ if (!file) {
56
+ handleChange(null);
57
+ } else if (maxDimension) {
58
+ const { readAndCompressImage } = await import('browser-image-resizer');
59
+ const resizedImage = await readAndCompressImage(file, {
60
+ maxWidth: maxDimension,
61
+ maxHeight: maxDimension,
62
+ mimeType: file.type,
63
+ });
64
+ handleChange(new File([resizedImage], file.name, { type: file.type }));
65
+ } else {
66
+ handleChange(file);
67
+ }
68
+ },
69
+ [handleChange, maxDimension],
70
+ );
71
+
72
+ const onDrop = useCallback(
73
+ (e: React.DragEvent<HTMLDivElement>) => {
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ setDraggingOver(false);
77
+ if (e.dataTransfer.files.length > 0) {
78
+ onChange(e.dataTransfer.files[0]);
79
+ }
80
+ },
81
+ [onChange],
82
+ );
83
+
84
+ const onDragStart = useCallback((e: React.DragEvent<HTMLDivElement>) => {
85
+ e.preventDefault();
86
+ e.stopPropagation();
87
+ setDragging(true);
88
+ }, []);
89
+
90
+ const onDragEnd = useCallback((e: React.DragEvent<HTMLDivElement>) => {
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ setDragging(false);
94
+ }, []);
95
+
96
+ const onFileChange = useCallback(
97
+ (e: React.ChangeEvent<HTMLInputElement>) => {
98
+ if (e.target.files && e.target.files.length > 0) {
99
+ onChange(e.target.files[0]);
100
+ }
101
+ },
102
+ [onChange],
103
+ );
104
+
105
+ const onFileClick = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
106
+ e.stopPropagation();
107
+ }, []);
108
+
109
+ const [cameraOpen, setCameraOpen] = useState(false);
110
+ const openCamera = () => setCameraOpen(true);
111
+
112
+ return (
113
+ <div
114
+ className={classNames('relative overflow-hidden', rest.className)}
115
+ onDragEnter={onDragEnter}
116
+ onDragLeave={onDragLeave}
117
+ onDragOver={onDragOver}
118
+ onDrop={onDrop}
119
+ onDragStart={onDragStart}
120
+ onDragEnd={onDragEnd}
121
+ >
122
+ {value ? (
123
+ <img src={value} className="w-full h-full object-cover object-center" />
124
+ ) : null}
125
+ {!value && (
126
+ <div
127
+ className={classNames(
128
+ 'absolute inset-0 flex flex-col items-center justify-center gap-3 bg-[rgba(0,0,0,0.05)]',
129
+ {
130
+ '!bg-[rgba(0,0,0,0.1)]': draggingOver,
131
+ },
132
+ )}
133
+ >
134
+ <input
135
+ type="file"
136
+ accept="image/*"
137
+ onChange={onFileChange}
138
+ onClick={onFileClick}
139
+ className="absolute inset--99999 op-0"
140
+ id={inputId}
141
+ />
142
+ <Button color="ghost" asChild>
143
+ <label htmlFor={inputId}>
144
+ <Icon name="upload" />
145
+ <span>{dragging ? 'Drop' : 'Upload'}</span>
146
+ </label>
147
+ </Button>
148
+ <Button color="ghost" onClick={openCamera}>
149
+ <Icon name="camera" />
150
+ <span>Camera</span>
151
+ </Button>
152
+ </div>
153
+ )}
154
+ {!value && cameraOpen && (
155
+ <CameraRoot
156
+ className="absolute w-full h-full z-1"
157
+ format="image/png"
158
+ onCapture={(dataUrl) => {
159
+ onChange(dataURItoBlob(dataUrl));
160
+ setCameraOpen(false);
161
+ }}
162
+ >
163
+ <CameraShutterButton />
164
+ <CameraDeviceSelector />
165
+ <Button
166
+ onClick={() => setCameraOpen(false)}
167
+ color="ghost"
168
+ size="icon"
169
+ className="text-white absolute top-2 right-2"
170
+ >
171
+ <Icon name="x" />
172
+ </Button>
173
+ </CameraRoot>
174
+ )}
175
+ {value && (
176
+ <Button
177
+ color="ghost"
178
+ size="icon"
179
+ className="absolute top-2 right-2 w-32px h-32px border-none p-2 cursor-pointer bg-white color-black rounded-full transition-colors shadow-sm"
180
+ onClick={() => onChange(null)}
181
+ >
182
+ <Icon name="x" />
183
+ </Button>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function dataURItoBlob(dataURI: string) {
190
+ // convert base64/URLEncoded data component to raw binary data held in a string
191
+ var byteString;
192
+ if (dataURI.split(',')[0].indexOf('base64') >= 0)
193
+ byteString = atob(dataURI.split(',')[1]);
194
+ else byteString = unescape(dataURI.split(',')[1]);
195
+ // separate out the mime component
196
+ var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
197
+ // write the bytes of the string to a typed array
198
+ var ia = new Uint8Array(byteString.length);
199
+ for (var i = 0; i < byteString.length; i++) {
200
+ ia[i] = byteString.charCodeAt(i);
201
+ }
202
+ return new File([ia], 'image.png', { type: mimeString });
203
+ }
@@ -0,0 +1,23 @@
1
+ export interface UploadIconProps {
2
+ className?: string;
3
+ }
4
+
5
+ export function UploadIcon(props: UploadIconProps) {
6
+ return (
7
+ <svg
8
+ width="15"
9
+ height="15"
10
+ viewBox="0 0 15 15"
11
+ fill="none"
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ {...props}
14
+ >
15
+ <path
16
+ d="M1.5 10V11.5C1.5 12.6046 2.39543 13.5 3.5 13.5H11.5C12.6046 13.5 13.5 12.6046 13.5 11.5V10M7.5 10.5V1M7.5 1L4 4M7.5 1L11 4"
17
+ stroke="currentColor"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ />
21
+ </svg>
22
+ );
23
+ }
@@ -0,0 +1 @@
1
+ export * from './ImageUploader.js';
@@ -0,0 +1 @@
1
+ export * from './imageUploader/index.js';
@@ -0,0 +1,38 @@
1
+ import { ReactNode, forwardRef, useEffect, useRef } from 'react';
2
+ import classNames from 'classnames';
3
+ import useMergedRef from '../../hooks/useMergedRef.js';
4
+ import { useStableCallback } from '../../hooks.js';
5
+
6
+ export interface InfiniteLoadTriggerProps {
7
+ className?: string;
8
+ children?: ReactNode;
9
+ onVisible?: () => void;
10
+ }
11
+
12
+ export const InfiniteLoadTrigger = forwardRef<
13
+ HTMLDivElement,
14
+ InfiniteLoadTriggerProps
15
+ >(function InfiniteLoadTrigger({ className, onVisible, ...rest }, ref) {
16
+ const innerRef = useRef<HTMLDivElement>(null);
17
+
18
+ const stableOnVisible = useStableCallback(onVisible);
19
+ useEffect(() => {
20
+ const observer = new IntersectionObserver((entries) => {
21
+ if (entries[0].isIntersecting) {
22
+ stableOnVisible();
23
+ }
24
+ });
25
+ observer.observe(innerRef.current!);
26
+ return () => {
27
+ observer.disconnect();
28
+ };
29
+ }, [stableOnVisible]);
30
+
31
+ return (
32
+ <div
33
+ ref={useMergedRef(ref, innerRef)}
34
+ className={classNames('flex flex-col items-center', className)}
35
+ {...rest}
36
+ />
37
+ );
38
+ });
@@ -0,0 +1 @@
1
+ export * from './infiniteLoadTrigger/InfiniteLoadTrigger.js';
@@ -0,0 +1,17 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Input } from './Input.js';
3
+
4
+ const meta = {
5
+ title: 'Input',
6
+ component: Input,
7
+ argTypes: {},
8
+ parameters: {
9
+ controls: { expanded: true },
10
+ },
11
+ } satisfies Meta<typeof Input>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof Input>;
16
+
17
+ export const Default: Story = {};
@@ -0,0 +1,32 @@
1
+ import classNames from 'classnames';
2
+ import { ComponentProps, ComponentPropsWithRef, forwardRef } from 'react';
3
+
4
+ export const inputClassName = classNames(
5
+ 'layer-components:(px-4 py-2 text-md font-sans rounded-lg bg-gray-blend select-auto min-w-60px color-black border-default)',
6
+ 'layer-components:focus:(outline-none bg-gray2)',
7
+ 'layer-components:focus-visible:(outline-none shadow-focus)',
8
+ 'layer-components:md:(min-w-120px)',
9
+ );
10
+
11
+ export const Input = forwardRef<
12
+ HTMLInputElement,
13
+ ComponentProps<'input'> & {
14
+ variant?: 'default' | 'primary';
15
+ }
16
+ >(function Input({ className, variant = 'default', ...props }, ref) {
17
+ return (
18
+ <input
19
+ {...props}
20
+ className={classNames(
21
+ inputClassName,
22
+ {
23
+ 'rounded-full': variant === 'primary',
24
+ },
25
+ className,
26
+ )}
27
+ ref={ref}
28
+ />
29
+ );
30
+ });
31
+
32
+ export type InputProps = ComponentPropsWithRef<'input'>;
@@ -0,0 +1 @@
1
+ export * from './Input.js';
@@ -0,0 +1 @@
1
+ export * from './input/index.js';
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+ import classNames from 'classnames';
3
+ import { HTMLAttributes } from 'react';
4
+ import { useBoundsCssVars } from '../../hooks/useSize.js';
5
+
6
+ export function PageContent({
7
+ children,
8
+ fullHeight,
9
+ noPadding,
10
+ innerProps,
11
+ className,
12
+ ...rest
13
+ }: HTMLAttributes<HTMLDivElement> & {
14
+ fullHeight?: boolean;
15
+ noPadding?: boolean;
16
+ innerProps?: HTMLAttributes<HTMLDivElement>;
17
+ }) {
18
+ const innerRef = useBoundsCssVars<HTMLDivElement>(200, undefined, {
19
+ left: '--content-left',
20
+ top: '--content-top',
21
+ width: '--content-width',
22
+ height: '--content-height',
23
+ ready: '--content-ready',
24
+ });
25
+
26
+ return (
27
+ <div
28
+ className={classNames(
29
+ '[grid-area:content] max-w-full min-w-0 w-full flex flex-col items-start relative flex-1 gap-3',
30
+ className,
31
+ )}
32
+ {...rest}
33
+ >
34
+ <div
35
+ {...innerProps}
36
+ className={classNames(
37
+ 'w-full min-w-0 flex flex-col mb-120px px-4 py-4',
38
+ 'sm:(max-w-700px w-full)',
39
+ {
40
+ 'flex-1': fullHeight,
41
+ 'important:(p-0 sm:p-4)': noPadding,
42
+ },
43
+ innerProps?.className,
44
+ )}
45
+ ref={innerRef}
46
+ >
47
+ {children}
48
+ </div>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,17 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import classNames from 'classnames';
3
+
4
+ export function PageFixedArea({
5
+ className,
6
+ ...props
7
+ }: HTMLAttributes<HTMLDivElement>) {
8
+ return (
9
+ <div
10
+ {...props}
11
+ className={classNames(
12
+ 'layer-components:(sticky top-0 z-nav bg-wash w-full items-stretch gap-2 flex flex-col)',
13
+ className,
14
+ )}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import classNames from 'classnames';
4
+ import { HTMLAttributes } from 'react';
5
+
6
+ export function PageNav({
7
+ className,
8
+ children,
9
+ ...props
10
+ }: HTMLAttributes<HTMLDivElement>) {
11
+ return (
12
+ <div
13
+ {...props}
14
+ className={classNames(
15
+ '[grid-area:nav] relative z-nav',
16
+ 'sm:([grid-area:nav] sticky top-0 h-auto bottom-auto left-auto right-auto)',
17
+ className,
18
+ )}
19
+ >
20
+ {children}
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import classNames from 'classnames';
4
+ import { HTMLAttributes } from 'react';
5
+
6
+ export function PageNowPlaying({
7
+ className,
8
+ unstyled,
9
+ ...props
10
+ }: HTMLAttributes<HTMLDivElement> & { unstyled?: boolean }) {
11
+ return (
12
+ <div
13
+ {...props}
14
+ className={classNames(
15
+ 'fixed bottom-[var(--now-playing-bottom,60px)] left-0 right-0 z-now-playing flex flex-col gap-2 items-end',
16
+ 'sm:(fixed bottom-3 left-[var(--content-left,20%)] transition-opacity top-auto items-end w-[var(--content-width,100%)] max-w-80vw p-0 opacity-[var(--content-ready,0)])',
17
+ unstyled
18
+ ? 'p-2'
19
+ : 'layer-components:(bg-wash p-2px rounded-xl border-light shadow-md min-w-32px items-center justify-center m-2 w-auto)',
20
+ className,
21
+ )}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,29 @@
1
+ import classNames from 'classnames';
2
+ import { forwardRef, ReactNode } from 'react';
3
+
4
+ export const PageRoot = forwardRef<
5
+ HTMLDivElement,
6
+ {
7
+ color?: 'default' | 'lemon';
8
+ children?: ReactNode;
9
+ className?: string;
10
+ }
11
+ >(function PageRoot({ className, children, ...props }, ref) {
12
+ return (
13
+ <div
14
+ ref={ref}
15
+ className={classNames(
16
+ 'flex-grow-1 flex-shrink-1 flex-basis-0 min-h-0 h-full',
17
+ 'grid grid-areas-[content]-[nav] grid-cols-[1fr] grid-rows-[1fr] items-start justify-center',
18
+ 'sm:(grid-areas-[gutter1_nav_content_gutter2] grid-cols-[1fr_auto_min(800px,60vw)_1fr] min-h-auto)',
19
+ {
20
+ 'bg-[var(--palette-yellow-70)]': props.color === 'lemon',
21
+ },
22
+ className,
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+ });
@@ -0,0 +1,23 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import classNames from 'classnames';
3
+ import { withClassName } from '../../hooks/withClassName.js';
4
+
5
+ export function PageSection({
6
+ className,
7
+ ...props
8
+ }: HTMLAttributes<HTMLDivElement>) {
9
+ return (
10
+ <div
11
+ {...props}
12
+ className={classNames(
13
+ 'bg-white rounded-lg border-default p-4 w-full max-w-80vw md:min-w-0',
14
+ className,
15
+ )}
16
+ />
17
+ );
18
+ }
19
+
20
+ export const PageSectionGrid = withClassName(
21
+ 'div',
22
+ 'grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-4 items-start',
23
+ );
@@ -0,0 +1,6 @@
1
+ export * from './PageRoot.js';
2
+ export * from './PageContent.js';
3
+ export * from './PageFixedArea.js';
4
+ export * from './PageNav.js';
5
+ export * from './PageNowPlaying.js';
6
+ export * from './PageSection.js';
@@ -0,0 +1 @@
1
+ export * from './layouts/index.js';
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import {
4
+ ChangeEvent,
5
+ FocusEvent,
6
+ forwardRef,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ import { debounce } from '@a-type/utils';
14
+ import { Input, InputProps } from '../input/Input.js';
15
+ import { TextArea } from '../textArea/TextArea.js';
16
+
17
+ export type LiveUpdateTextFieldProps = {
18
+ value: string;
19
+ debounceMs?: number;
20
+ onChange: (value: string) => void;
21
+ textArea?: boolean;
22
+ className?: string;
23
+ onFocus?: (ev: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
24
+ onBlur?: (ev: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
25
+ autoComplete?: InputProps['autoComplete'];
26
+ autoFocus?: InputProps['autoFocus'];
27
+ required?: boolean;
28
+ placeholder?: string;
29
+ type?: InputProps['type'];
30
+ id?: string;
31
+ };
32
+
33
+ /**
34
+ * An extension of TextField which keeps a local realtime value in state and
35
+ * periodically reports changes to the parent. Use this to connect
36
+ * to the API and update a value from the field directly.
37
+ *
38
+ * This component is optimistic and will not respond to external changes while focused.
39
+ */
40
+ export const LiveUpdateTextField = forwardRef<
41
+ HTMLInputElement | HTMLTextAreaElement,
42
+ LiveUpdateTextFieldProps
43
+ >(function LiveUpdateTextField(
44
+ {
45
+ value,
46
+ onChange,
47
+ debounceMs = 500,
48
+ onFocus,
49
+ onBlur,
50
+ textArea,
51
+ type,
52
+ ...rest
53
+ },
54
+ ref,
55
+ ) {
56
+ const [displayValue, setDisplayValue] = useState(value || '');
57
+ const ignoreUpdates = useRef(false);
58
+ const didChange = useRef(false);
59
+
60
+ const handleFocus = useCallback(
61
+ (ev: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
62
+ onFocus?.(ev);
63
+ ignoreUpdates.current = true;
64
+ },
65
+ [onFocus],
66
+ );
67
+
68
+ const handleBlur = useCallback(
69
+ (ev: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
70
+ onBlur?.(ev);
71
+ ignoreUpdates.current = false;
72
+ // immediately send update if the user typed anything.
73
+ // otherwise pull the latest remote value
74
+ if (didChange.current) {
75
+ onChange?.(displayValue);
76
+ } else {
77
+ setDisplayValue(value || '');
78
+ }
79
+ didChange.current = false;
80
+ },
81
+ [onBlur, displayValue, onChange, value],
82
+ );
83
+
84
+ useEffect(() => {
85
+ if (ignoreUpdates.current) {
86
+ return;
87
+ }
88
+ setDisplayValue(value || '');
89
+ }, [value]);
90
+
91
+ // every once in a while, send an update to parent
92
+ const debouncedOnChange = useMemo(
93
+ () => debounce(onChange || (() => {}), debounceMs),
94
+ [onChange, debounceMs],
95
+ );
96
+
97
+ // update local state instantly and parent eventually
98
+ const handleChange = useCallback(
99
+ (ev: ChangeEvent<any>) => {
100
+ setDisplayValue(ev.target.value);
101
+ debouncedOnChange(ev.target.value);
102
+ didChange.current = true;
103
+ },
104
+ [debouncedOnChange],
105
+ );
106
+
107
+ if (textArea) {
108
+ return (
109
+ <TextArea
110
+ ref={ref as any}
111
+ onFocus={handleFocus}
112
+ onBlur={handleBlur}
113
+ value={displayValue}
114
+ onChange={handleChange}
115
+ autoSize
116
+ {...rest}
117
+ />
118
+ );
119
+ } else {
120
+ return (
121
+ <Input
122
+ ref={ref as any}
123
+ onFocus={handleFocus}
124
+ onBlur={handleBlur}
125
+ value={displayValue}
126
+ onChange={handleChange}
127
+ type={type}
128
+ {...rest}
129
+ />
130
+ );
131
+ }
132
+ });
@@ -0,0 +1 @@
1
+ export * from './LiveUpdateTextField.js';
@@ -0,0 +1 @@
1
+ export * from './liveUpdateTextField/index.js';