Skip to content

Commit 9877db5

Browse files
committed
feat(cook-order): add UI component library and update roadmap
- Add base-ui/react based UI components (badge, button, calendar, card, empty, field, input-group, input, label, separator, sheet, sidebar, skeleton, sonner, spinner, table, textarea, toggle, tooltip) - Add use-mobile responsive hook - Add date-fns and react-day-picker dependencies - Update Roadmap with admin/visitor planning items
1 parent cd7f69d commit 9877db5

23 files changed

Lines changed: 2246 additions & 1 deletion

‎apps/cf-worker/cook-order/Roadmap.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,25 @@
44

55
### 管理端
66

7+
- [ ] 登录模块 + 登录页(使用 USERNAME、PASSWORD)
8+
- [ ] 用户管理(含微信绑定)
9+
- [ ] 菜品管理
10+
- [ ] 厨房管理
11+
- [ ] 订单管理
12+
713
### 访客端
814

15+
#### 厨师端
16+
17+
- [ ] 菜品增加 / 删除 / 修改 / 上传菜品图
18+
- [ ] 订单显示指定菜品
19+
- [ ] 开始订单 / 关闭订单 / 分享订单
20+
21+
#### 客户端
22+
23+
- [ ] 订单菜品列表(已选 / 未选,显示多少人已选)
24+
- [ ] 显示已选菜品
25+
926
### 基础能力
1027

1128
- [x] 基于 MD5 hash 的文件去重(同内容只存储一份)

‎apps/cf-worker/cook-order/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"classcat": "^5.0.5",
2929
"clsx": "^2.1.1",
3030
"crypto-js": "^4.2.0",
31+
"date-fns": "^2.30.0",
3132
"dayjs": "^1.11.20",
3233
"drizzle-orm": "^0.45.2",
3334
"hls.js": "^1.6.15",
@@ -36,6 +37,7 @@
3637
"mime-types": "^2.1.35",
3738
"next-themes": "^0.4.6",
3839
"react": "19.2.1",
40+
"react-day-picker": "^9.14.0",
3941
"react-dom": "19.2.1",
4042
"react-helmet": "^6.1.0",
4143
"react-router": "^7.13.2",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { mergeProps } from "@base-ui/react/merge-props"
2+
import { useRender } from "@base-ui/react/use-render"
3+
import { cva, type VariantProps } from "class-variance-authority"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const badgeVariants = cva(
8+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9+
{
10+
variants: {
11+
variant: {
12+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13+
secondary:
14+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15+
destructive:
16+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17+
outline:
18+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19+
ghost:
20+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21+
link: "text-primary underline-offset-4 hover:underline",
22+
},
23+
},
24+
defaultVariants: {
25+
variant: "default",
26+
},
27+
}
28+
)
29+
30+
function Badge({
31+
className,
32+
variant = "default",
33+
render,
34+
...props
35+
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36+
return useRender({
37+
defaultTagName: "span",
38+
props: mergeProps<"span">(
39+
{
40+
className: cn(badgeVariants({ variant }), className),
41+
},
42+
props
43+
),
44+
render,
45+
state: {
46+
slot: "badge",
47+
variant,
48+
},
49+
})
50+
}
51+
52+
export { Badge, badgeVariants }
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Button as ButtonPrimitive } from "@base-ui/react/button"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const buttonVariants = cva(
7+
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8+
{
9+
variants: {
10+
variant: {
11+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
12+
outline:
13+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
14+
secondary:
15+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
16+
ghost:
17+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
18+
destructive:
19+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
20+
link: "text-primary underline-offset-4 hover:underline",
21+
},
22+
size: {
23+
default:
24+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
25+
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
26+
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
27+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
28+
icon: "size-8",
29+
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
30+
"icon-sm": "size-7 rounded-none",
31+
"icon-lg": "size-9",
32+
},
33+
},
34+
defaultVariants: {
35+
variant: "default",
36+
size: "default",
37+
},
38+
}
39+
)
40+
41+
function Button({
42+
className,
43+
variant = "default",
44+
size = "default",
45+
...props
46+
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
47+
return (
48+
<ButtonPrimitive
49+
data-slot="button"
50+
className={cn(buttonVariants({ variant, size, className }))}
51+
{...props}
52+
/>
53+
)
54+
}
55+
56+
export { Button, buttonVariants }
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import * as React from "react"
2+
import {
3+
DayPicker,
4+
getDefaultClassNames,
5+
type DayButton,
6+
type Locale,
7+
} from "react-day-picker"
8+
9+
import { cn } from "@/lib/utils"
10+
import { Button, buttonVariants } from "@/components/ui/button"
11+
import { CaretLeftIcon, CaretRightIcon, CaretDownIcon } from "@phosphor-icons/react"
12+
13+
function Calendar({
14+
className,
15+
classNames,
16+
showOutsideDays = true,
17+
captionLayout = "label",
18+
buttonVariant = "ghost",
19+
locale,
20+
formatters,
21+
components,
22+
...props
23+
}: React.ComponentProps<typeof DayPicker> & {
24+
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
25+
}) {
26+
const defaultClassNames = getDefaultClassNames()
27+
28+
return (
29+
<DayPicker
30+
showOutsideDays={showOutsideDays}
31+
className={cn(
32+
"group/calendar bg-background p-2 [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
33+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
34+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35+
className
36+
)}
37+
captionLayout={captionLayout}
38+
locale={locale}
39+
formatters={{
40+
formatMonthDropdown: (date) =>
41+
date.toLocaleString(locale?.code, { month: "short" }),
42+
...formatters,
43+
}}
44+
classNames={{
45+
root: cn("w-fit", defaultClassNames.root),
46+
months: cn(
47+
"relative flex flex-col gap-4 md:flex-row",
48+
defaultClassNames.months
49+
),
50+
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
51+
nav: cn(
52+
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
53+
defaultClassNames.nav
54+
),
55+
button_previous: cn(
56+
buttonVariants({ variant: buttonVariant }),
57+
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
58+
defaultClassNames.button_previous
59+
),
60+
button_next: cn(
61+
buttonVariants({ variant: buttonVariant }),
62+
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
63+
defaultClassNames.button_next
64+
),
65+
month_caption: cn(
66+
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
67+
defaultClassNames.month_caption
68+
),
69+
dropdowns: cn(
70+
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
71+
defaultClassNames.dropdowns
72+
),
73+
dropdown_root: cn(
74+
"relative rounded-(--cell-radius)",
75+
defaultClassNames.dropdown_root
76+
),
77+
dropdown: cn(
78+
"absolute inset-0 bg-popover opacity-0",
79+
defaultClassNames.dropdown
80+
),
81+
caption_label: cn(
82+
"font-medium select-none",
83+
captionLayout === "label"
84+
? "text-sm"
85+
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
86+
defaultClassNames.caption_label
87+
),
88+
table: "w-full border-collapse",
89+
weekdays: cn("flex", defaultClassNames.weekdays),
90+
weekday: cn(
91+
"flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
92+
defaultClassNames.weekday
93+
),
94+
week: cn("mt-2 flex w-full", defaultClassNames.week),
95+
week_number_header: cn(
96+
"w-(--cell-size) select-none",
97+
defaultClassNames.week_number_header
98+
),
99+
week_number: cn(
100+
"text-[0.8rem] text-muted-foreground select-none",
101+
defaultClassNames.week_number
102+
),
103+
day: cn(
104+
"group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
105+
props.showWeekNumber
106+
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
107+
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
108+
defaultClassNames.day
109+
),
110+
range_start: cn(
111+
"relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
112+
defaultClassNames.range_start
113+
),
114+
range_middle: cn("rounded-none", defaultClassNames.range_middle),
115+
range_end: cn(
116+
"relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
117+
defaultClassNames.range_end
118+
),
119+
today: cn(
120+
"rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
121+
defaultClassNames.today
122+
),
123+
outside: cn(
124+
"text-muted-foreground aria-selected:text-muted-foreground",
125+
defaultClassNames.outside
126+
),
127+
disabled: cn(
128+
"text-muted-foreground opacity-50",
129+
defaultClassNames.disabled
130+
),
131+
hidden: cn("invisible", defaultClassNames.hidden),
132+
...classNames,
133+
}}
134+
components={{
135+
Root: ({ className, rootRef, ...props }) => {
136+
return (
137+
<div
138+
data-slot="calendar"
139+
ref={rootRef}
140+
className={cn(className)}
141+
{...props}
142+
/>
143+
)
144+
},
145+
Chevron: ({ className, orientation, ...props }) => {
146+
if (orientation === "left") {
147+
return (
148+
<CaretLeftIcon className={cn("size-4", className)} {...props} />
149+
)
150+
}
151+
152+
if (orientation === "right") {
153+
return (
154+
<CaretRightIcon className={cn("size-4", className)} {...props} />
155+
)
156+
}
157+
158+
return (
159+
<CaretDownIcon className={cn("size-4", className)} {...props} />
160+
)
161+
},
162+
DayButton: ({ ...props }) => (
163+
<CalendarDayButton locale={locale} {...props} />
164+
),
165+
WeekNumber: ({ children, ...props }) => {
166+
return (
167+
<td {...props}>
168+
<div className="flex size-(--cell-size) items-center justify-center text-center">
169+
{children}
170+
</div>
171+
</td>
172+
)
173+
},
174+
...components,
175+
}}
176+
{...props}
177+
/>
178+
)
179+
}
180+
181+
function CalendarDayButton({
182+
className,
183+
day,
184+
modifiers,
185+
locale,
186+
...props
187+
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
188+
const defaultClassNames = getDefaultClassNames()
189+
190+
const ref = React.useRef<HTMLButtonElement>(null)
191+
React.useEffect(() => {
192+
if (modifiers.focused) ref.current?.focus()
193+
}, [modifiers.focused])
194+
195+
return (
196+
<Button
197+
variant="ghost"
198+
size="icon"
199+
data-day={day.date.toLocaleDateString(locale?.code)}
200+
data-selected-single={
201+
modifiers.selected &&
202+
!modifiers.range_start &&
203+
!modifiers.range_end &&
204+
!modifiers.range_middle
205+
}
206+
data-range-start={modifiers.range_start}
207+
data-range-end={modifiers.range_end}
208+
data-range-middle={modifiers.range_middle}
209+
className={cn(
210+
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
211+
defaultClassNames.day,
212+
className
213+
)}
214+
{...props}
215+
/>
216+
)
217+
}
218+
219+
export { Calendar, CalendarDayButton }

0 commit comments

Comments
 (0)