Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions apps/ui/public/r/p-switch-7.json
Comment thread
pasqualevitiello marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "p-switch-7",
"type": "registry:block",
"description": "Custom size switch",
"description": "Weekly availability editor with time range combobox pickers",
"dependencies": [
"lucide-react"
],
"registryDependencies": [
"@coss/switch"
"@coss/button",
"@coss/checkbox",
"@coss/checkbox-group",
"@coss/combobox",
"@coss/label",
"@coss/popover",
"@coss/select",
"@coss/switch",
"@coss/tooltip"
],
"files": [
{
"path": "registry/default/particles/p-switch-7.tsx",
"content": "import { Switch } from \"@/registry/default/ui/switch\";\n\nexport default function Particle() {\n return (\n <Switch className=\"[--thumb-size:--spacing(4)] sm:[--thumb-size:--spacing(3)]\" />\n );\n}\n",
"content": "\"use client\";\n\nimport { CopyIcon, PlusIcon, SearchIcon, XIcon } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { Checkbox } from \"@/registry/default/ui/checkbox\";\nimport { CheckboxGroup } from \"@/registry/default/ui/checkbox-group\";\nimport {\n Combobox,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n ComboboxList,\n ComboboxPopup,\n ComboboxTrigger,\n ComboboxValue,\n} from \"@/registry/default/ui/combobox\";\nimport { Label } from \"@/registry/default/ui/label\";\nimport {\n Popover,\n PopoverPopup,\n PopoverTrigger,\n} from \"@/registry/default/ui/popover\";\nimport { SelectButton } from \"@/registry/default/ui/select\";\nimport { Switch } from \"@/registry/default/ui/switch\";\nimport {\n Tooltip,\n TooltipPopup,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\n\nconst days = [\n \"Monday\",\n \"Tuesday\",\n \"Wednesday\",\n \"Thursday\",\n \"Friday\",\n \"Saturday\",\n \"Sunday\",\n] as const;\n\ntype Day = (typeof days)[number];\n\ntype TimeRange = {\n id: number;\n start: string;\n end: string;\n};\n\nconst timeOptions = Array.from({ length: 96 }, (_, i) => {\n const hours = Math.floor(i / 4);\n const minutes = (i % 4) * 15;\n const period = hours < 12 ? \"AM\" : \"PM\";\n const displayHours = hours % 12 === 0 ? 12 : hours % 12;\n return `${displayHours}:${minutes.toString().padStart(2, \"0\")} ${period}`;\n});\n\nconst timeIndex = (time: string) => timeOptions.indexOf(time);\n\nlet rangeId = 0;\nconst createRange = (start: string, end: string): TimeRange => ({\n end,\n id: ++rangeId,\n start,\n});\n\nconst defaultAvailability: Record<Day, TimeRange[]> = {\n Friday: [createRange(\"9:00 AM\", \"5:00 PM\")],\n Monday: [createRange(\"9:00 AM\", \"5:00 PM\")],\n Saturday: [],\n Sunday: [],\n Thursday: [createRange(\"9:00 AM\", \"5:00 PM\")],\n Tuesday: [\n createRange(\"9:00 AM\", \"1:00 PM\"),\n createRange(\"3:00 PM\", \"5:00 PM\"),\n ],\n Wednesday: [createRange(\"9:00 AM\", \"5:00 PM\")],\n};\n\nfunction TimeCombobox({\n ariaLabel,\n items,\n onChange,\n value,\n}: {\n ariaLabel: string;\n items: string[];\n onChange: (time: string) => void;\n value: string;\n}) {\n return (\n <Combobox\n autoHighlight\n items={items}\n onValueChange={(time) => {\n if (typeof time === \"string\") {\n onChange(time);\n }\n }}\n value={value}\n >\n <ComboboxTrigger\n aria-label={ariaLabel}\n render={<SelectButton className=\"w-27 tabular-nums\" size=\"sm\" />}\n >\n <ComboboxValue />\n </ComboboxTrigger>\n <ComboboxPopup aria-label={ariaLabel} className=\"min-w-44\">\n <div className=\"border-b p-2\">\n <ComboboxInput\n className=\"rounded-md before:rounded-[calc(var(--radius-md)-1px)]\"\n placeholder=\"Search time\"\n showTrigger={false}\n size=\"sm\"\n startAddon={<SearchIcon />}\n />\n </div>\n <ComboboxEmpty>No times found.</ComboboxEmpty>\n <ComboboxList>\n {(time: string) => (\n <ComboboxItem key={time} value={time}>\n <span className=\"tabular-nums\">{time}</span>\n </ComboboxItem>\n )}\n </ComboboxList>\n </ComboboxPopup>\n </Combobox>\n );\n}\n\nfunction CopyTimesPopover({\n day,\n disabled,\n onCopy,\n}: {\n day: Day;\n disabled: boolean;\n onCopy: (targets: Day[]) => void;\n}) {\n const [open, setOpen] = useState(false);\n const [selectedDays, setSelectedDays] = useState<string[]>([]);\n\n return (\n <Popover\n onOpenChange={(nextOpen) => {\n setOpen(nextOpen);\n if (nextOpen) {\n setSelectedDays([]);\n }\n }}\n open={open}\n >\n <Tooltip disableHoverablePopup>\n <PopoverTrigger\n render={\n <TooltipTrigger\n render={\n <Button\n aria-label={`Copy ${day} times to other days`}\n disabled={disabled}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n />\n }\n >\n <CopyIcon aria-hidden=\"true\" />\n </PopoverTrigger>\n <TooltipPopup>Copy to other days</TooltipPopup>\n </Tooltip>\n <PopoverPopup align=\"end\" className=\"w-44\">\n <div className=\"flex flex-col gap-3\">\n <div className=\"font-medium text-foreground text-sm\">\n Copy times to\n </div>\n <CheckboxGroup\n aria-label={`Copy ${day} times to`}\n onValueChange={setSelectedDays}\n value={selectedDays}\n >\n {days\n .filter((target) => target !== day)\n .map((target) => (\n <Label key={target}>\n <Checkbox value={target} />\n {target}\n </Label>\n ))}\n </CheckboxGroup>\n <Button\n disabled={selectedDays.length === 0}\n onClick={() => {\n onCopy(selectedDays as Day[]);\n setOpen(false);\n }}\n size=\"sm\"\n >\n Apply\n </Button>\n </div>\n </PopoverPopup>\n </Popover>\n );\n}\n\nexport default function Particle() {\n const [availability, setAvailability] =\n useState<Record<Day, TimeRange[]>>(defaultAvailability);\n\n const setDayRanges = (day: Day, ranges: TimeRange[]) => {\n setAvailability((prev) => ({ ...prev, [day]: ranges }));\n };\n\n const toggleDay = (day: Day, enabled: boolean) => {\n setDayRanges(day, enabled ? [createRange(\"9:00 AM\", \"5:00 PM\")] : []);\n };\n\n const addRange = (day: Day) => {\n const ranges = availability[day];\n const lastRange = ranges[ranges.length - 1];\n if (!lastRange) {\n setDayRanges(day, [createRange(\"9:00 AM\", \"5:00 PM\")]);\n return;\n }\n const startIndex = Math.min(\n timeIndex(lastRange.end) + 4,\n timeOptions.length - 2,\n );\n const endIndex = Math.min(startIndex + 4, timeOptions.length - 1);\n setDayRanges(day, [\n ...ranges,\n createRange(timeOptions[startIndex] ?? \"\", timeOptions[endIndex] ?? \"\"),\n ]);\n };\n\n const removeRange = (day: Day, id: number) => {\n setDayRanges(\n day,\n availability[day].filter((range) => range.id !== id),\n );\n };\n\n const updateStart = (day: Day, id: number, start: string) => {\n setDayRanges(\n day,\n availability[day].map((range) => {\n if (range.id !== id) return range;\n const end =\n timeIndex(start) >= timeIndex(range.end)\n ? (timeOptions[\n Math.min(timeIndex(start) + 4, timeOptions.length - 1)\n ] ?? range.end)\n : range.end;\n return { ...range, end, start };\n }),\n );\n };\n\n const updateEnd = (day: Day, id: number, end: string) => {\n setDayRanges(\n day,\n availability[day].map((range) =>\n range.id === id ? { ...range, end } : range,\n ),\n );\n };\n\n const copyTo = (source: Day, targets: Day[]) => {\n setAvailability((prev) => {\n const next = { ...prev };\n for (const target of targets) {\n next[target] = prev[source].map((range) =>\n createRange(range.start, range.end),\n );\n }\n return next;\n });\n };\n\n return (\n <TooltipProvider delay={0}>\n <div className=\"divide-y\">\n {days.map((day) => {\n const ranges = availability[day];\n const lastRange = ranges[ranges.length - 1];\n const addDisabled = lastRange\n ? timeIndex(lastRange.end) >= timeOptions.length - 2\n : false;\n\n return (\n <div\n className=\"flex flex-col gap-4 py-3 first:pt-0 last:pb-0 md:flex-row md:flex-wrap md:items-start\"\n key={day}\n >\n <Label className=\"flex h-8 w-30 shrink-0 items-center gap-2.5 sm:h-7\">\n <Switch\n checked={ranges.length > 0}\n onCheckedChange={(checked) => toggleDay(day, checked)}\n />\n {day}\n </Label>\n <div className=\"flex w-full min-w-0 items-start gap-4 md:flex-1\">\n <div className=\"flex min-w-0 flex-col gap-2\">\n {ranges.length === 0 ? (\n <p className=\"flex h-8 items-center text-muted-foreground sm:h-7 sm:text-sm\">\n Unavailable\n </p>\n ) : (\n ranges.map((range) => (\n <div className=\"flex items-center gap-2\" key={range.id}>\n <TimeCombobox\n ariaLabel={`${day} start time`}\n items={timeOptions}\n onChange={(start) =>\n updateStart(day, range.id, start)\n }\n value={range.start}\n />\n <span\n aria-hidden=\"true\"\n className=\"text-muted-foreground\"\n >\n –\n </span>\n <TimeCombobox\n ariaLabel={`${day} end time`}\n items={timeOptions.slice(timeIndex(range.start) + 1)}\n onChange={(end) => updateEnd(day, range.id, end)}\n value={range.end}\n />\n <Tooltip disableHoverablePopup>\n <TooltipTrigger\n render={\n <Button\n aria-label={`Delete ${range.start} to ${range.end} on ${day}`}\n onClick={() => removeRange(day, range.id)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n <XIcon aria-hidden=\"true\" />\n </TooltipTrigger>\n <TooltipPopup>Delete range</TooltipPopup>\n </Tooltip>\n </div>\n ))\n )}\n </div>\n <div className=\"ml-auto flex shrink-0 gap-1\">\n <Tooltip disableHoverablePopup>\n <TooltipTrigger\n render={\n <Button\n aria-label={`Add time range to ${day}`}\n disabled={addDisabled}\n onClick={() => addRange(day)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n <PlusIcon aria-hidden=\"true\" />\n </TooltipTrigger>\n <TooltipPopup>Add range</TooltipPopup>\n </Tooltip>\n <CopyTimesPopover\n day={day}\n disabled={ranges.length === 0}\n onCopy={(targets) => copyTo(day, targets)}\n />\n </div>\n </div>\n </div>\n );\n })}\n </div>\n </TooltipProvider>\n );\n}\n",
"type": "registry:block"
}
],
"meta": {
"className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl",
"colSpan": 2
},
"categories": [
"switch"
]
"switch",
"combobox",
"popover",
"time"
],
"type": "registry:block"
}
Loading
Loading