diff --git a/apps/ui/public/r/p-switch-7.json b/apps/ui/public/r/p-switch-7.json index 6fcbbe63e..7825b05d7 100644 --- a/apps/ui/public/r/p-switch-7.json +++ b/apps/ui/public/r/p-switch-7.json @@ -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 \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 = {\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 {\n if (typeof time === \"string\") {\n onChange(time);\n }\n }}\n value={value}\n >\n }\n >\n \n \n \n
\n }\n />\n
\n No times found.\n \n {(time: string) => (\n \n {time}\n \n )}\n \n
\n \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([]);\n\n return (\n {\n setOpen(nextOpen);\n if (nextOpen) {\n setSelectedDays([]);\n }\n }}\n open={open}\n >\n \n \n }\n />\n }\n >\n \n \n Copy to other days\n \n \n
\n
\n Copy times to\n
\n \n {days\n .filter((target) => target !== day)\n .map((target) => (\n \n ))}\n \n {\n onCopy(selectedDays as Day[]);\n setOpen(false);\n }}\n size=\"sm\"\n >\n Apply\n \n
\n
\n \n );\n}\n\nexport default function Particle() {\n const [availability, setAvailability] =\n useState>(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 \n
\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 \n \n
\n
\n {ranges.length === 0 ? (\n

\n Unavailable\n

\n ) : (\n ranges.map((range) => (\n
\n \n updateStart(day, range.id, start)\n }\n value={range.start}\n />\n \n –\n \n updateEnd(day, range.id, end)}\n value={range.end}\n />\n \n removeRange(day, range.id)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n \n \n Delete range\n \n
\n ))\n )}\n
\n
\n \n addRange(day)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n \n \n Add range\n \n copyTo(day, targets)}\n />\n
\n
\n
\n );\n })}\n \n
\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" } \ No newline at end of file diff --git a/apps/ui/public/r/p-switch-8.json b/apps/ui/public/r/p-switch-8.json new file mode 100644 index 000000000..9464691bd --- /dev/null +++ b/apps/ui/public/r/p-switch-8.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "p-switch-8", + "description": "Weekly availability editor with grouped time range controls", + "dependencies": [ + "lucide-react" + ], + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip" + ], + "files": [ + { + "path": "registry/default/particles/p-switch-8.tsx", + "content": "\"use client\";\n\nimport {\n ArrowRightIcon,\n CopyIcon,\n PlusIcon,\n SearchIcon,\n XIcon,\n} 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 { Group, GroupSeparator, GroupText } from \"@/registry/default/ui/group\";\nimport { Label } from \"@/registry/default/ui/label\";\nimport {\n Popover,\n PopoverPopup,\n PopoverTrigger,\n} from \"@/registry/default/ui/popover\";\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 = {\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 {\n if (typeof time === \"string\") {\n onChange(time);\n }\n }}\n value={value}\n >\n \n }\n >\n \n \n \n
\n }\n />\n
\n No times found.\n \n {(time: string) => (\n \n {time}\n \n )}\n \n
\n \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([]);\n\n return (\n {\n setOpen(nextOpen);\n if (nextOpen) {\n setSelectedDays([]);\n }\n }}\n open={open}\n >\n \n \n }\n />\n }\n >\n \n \n Copy to other days\n \n \n
\n
\n Copy times to\n
\n \n {days\n .filter((target) => target !== day)\n .map((target) => (\n \n ))}\n \n {\n onCopy(selectedDays as Day[]);\n setOpen(false);\n }}\n size=\"sm\"\n >\n Apply\n \n
\n
\n \n );\n}\n\nexport default function Particle() {\n const [availability, setAvailability] =\n useState>(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 \n
\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 \n \n
\n
\n {ranges.length === 0 ? (\n

\n Unavailable\n

\n ) : (\n ranges.map((range) => (\n
\n \n \n updateStart(day, range.id, start)\n }\n value={range.start}\n />\n \n \n \n \n \n updateEnd(day, range.id, end)}\n value={range.end}\n />\n \n \n removeRange(day, range.id)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n \n \n Delete range\n \n
\n ))\n )}\n
\n
\n \n addRange(day)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n \n \n Add range\n \n copyTo(day, targets)}\n />\n
\n
\n
\n );\n })}\n \n
\n );\n}\n", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "categories": [ + "switch", + "combobox", + "group", + "popover", + "time" + ], + "type": "registry:block" +} \ No newline at end of file diff --git a/apps/ui/public/r/p-switch-9.json b/apps/ui/public/r/p-switch-9.json new file mode 100644 index 000000000..bdd7a3f51 --- /dev/null +++ b/apps/ui/public/r/p-switch-9.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "p-switch-9", + "description": "Weekly availability editor with From/To labeled time groups", + "dependencies": [ + "lucide-react" + ], + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip" + ], + "files": [ + { + "path": "registry/default/particles/p-switch-9.tsx", + "content": "\"use client\";\n\nimport { CopyIcon, PlusIcon, SearchIcon, XIcon } from \"lucide-react\";\nimport { useId, 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 { Group, GroupSeparator, GroupText } from \"@/registry/default/ui/group\";\nimport { Label } from \"@/registry/default/ui/label\";\nimport {\n Popover,\n PopoverPopup,\n PopoverTrigger,\n} from \"@/registry/default/ui/popover\";\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 = {\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 id,\n items,\n onChange,\n value,\n}: {\n ariaLabel: string;\n id: string;\n items: string[];\n onChange: (time: string) => void;\n value: string;\n}) {\n return (\n {\n if (typeof time === \"string\") {\n onChange(time);\n }\n }}\n value={value}\n >\n \n }\n >\n \n \n \n
\n }\n />\n
\n No times found.\n \n {(time: string) => (\n \n {time}\n \n )}\n \n
\n \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([]);\n\n return (\n {\n setOpen(nextOpen);\n if (nextOpen) {\n setSelectedDays([]);\n }\n }}\n open={open}\n >\n \n \n }\n />\n }\n >\n \n \n Copy to other days\n \n \n
\n
\n Copy times to\n
\n \n {days\n .filter((target) => target !== day)\n .map((target) => (\n \n ))}\n \n {\n onCopy(selectedDays as Day[]);\n setOpen(false);\n }}\n size=\"sm\"\n >\n Apply\n \n
\n
\n \n );\n}\n\nexport default function Particle() {\n const id = useId();\n const [availability, setAvailability] =\n useState>(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 \n
\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 \n \n
\n
\n {ranges.length === 0 ? (\n

\n Unavailable\n

\n ) : (\n ranges.map((range) => {\n const startId = `${id}-start-${range.id}`;\n const endId = `${id}-end-${range.id}`;\n\n return (\n
\n \n }>\n From\n \n \n \n updateStart(day, range.id, start)\n }\n value={range.start}\n />\n \n }>\n To\n \n \n updateEnd(day, range.id, end)}\n value={range.end}\n />\n \n \n removeRange(day, range.id)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n \n \n Delete range\n \n
\n );\n })\n )}\n
\n
\n \n addRange(day)}\n size=\"icon-sm\"\n variant=\"ghost\"\n />\n }\n >\n \n \n Add range\n \n copyTo(day, targets)}\n />\n
\n
\n
\n );\n })}\n \n
\n );\n}\n", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "categories": [ + "switch", + "combobox", + "group", + "popover", + "time" + ], + "type": "registry:block" +} \ No newline at end of file diff --git a/apps/ui/public/r/registry.json b/apps/ui/public/r/registry.json index 217515665..77b71b589 100644 --- a/apps/ui/public/r/registry.json +++ b/apps/ui/public/r/registry.json @@ -9868,6 +9868,113 @@ ], "type": "registry:block" }, + { + "categories": [ + "switch", + "combobox", + "popover", + "time" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Weekly availability editor with time range combobox pickers", + "files": [ + { + "path": "registry/default/particles/p-switch-7.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "name": "p-switch-7", + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/label", + "@coss/popover", + "@coss/select", + "@coss/switch", + "@coss/tooltip" + ], + "type": "registry:block" + }, + { + "categories": [ + "switch", + "combobox", + "group", + "popover", + "time" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Weekly availability editor with grouped time range controls", + "files": [ + { + "path": "registry/default/particles/p-switch-8.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "name": "p-switch-8", + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip" + ], + "type": "registry:block" + }, + { + "categories": [ + "switch", + "combobox", + "group", + "popover", + "time" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Weekly availability editor with From/To labeled time groups", + "files": [ + { + "path": "registry/default/particles/p-switch-9.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "name": "p-switch-9", + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip" + ], + "type": "registry:block" + }, { "categories": [ "table" diff --git a/apps/ui/registry.json b/apps/ui/registry.json index 217515665..77b71b589 100644 --- a/apps/ui/registry.json +++ b/apps/ui/registry.json @@ -9868,6 +9868,113 @@ ], "type": "registry:block" }, + { + "categories": [ + "switch", + "combobox", + "popover", + "time" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Weekly availability editor with time range combobox pickers", + "files": [ + { + "path": "registry/default/particles/p-switch-7.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "name": "p-switch-7", + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/label", + "@coss/popover", + "@coss/select", + "@coss/switch", + "@coss/tooltip" + ], + "type": "registry:block" + }, + { + "categories": [ + "switch", + "combobox", + "group", + "popover", + "time" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Weekly availability editor with grouped time range controls", + "files": [ + { + "path": "registry/default/particles/p-switch-8.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "name": "p-switch-8", + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip" + ], + "type": "registry:block" + }, + { + "categories": [ + "switch", + "combobox", + "group", + "popover", + "time" + ], + "dependencies": [ + "lucide-react" + ], + "description": "Weekly availability editor with From/To labeled time groups", + "files": [ + { + "path": "registry/default/particles/p-switch-9.tsx", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + "colSpan": 2 + }, + "name": "p-switch-9", + "registryDependencies": [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip" + ], + "type": "registry:block" + }, { "categories": [ "table" diff --git a/apps/ui/registry/__index__.tsx b/apps/ui/registry/__index__.tsx index 1907fce55..da6834fcf 100644 --- a/apps/ui/registry/__index__.tsx +++ b/apps/ui/registry/__index__.tsx @@ -8557,6 +8557,60 @@ export const Index: Record = { categories: ["switch"], meta: undefined, }, + "p-switch-7": { + name: "p-switch-7", + description: "Weekly availability editor with time range combobox pickers", + type: "registry:block", + registryDependencies: ["@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", + type: "registry:block", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/default/particles/p-switch-7.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + categories: ["switch","combobox","popover","time"], + meta: {"className":"**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl","colSpan":2}, + }, + "p-switch-8": { + name: "p-switch-8", + description: "Weekly availability editor with grouped time range controls", + type: "registry:block", + registryDependencies: ["@coss/button","@coss/checkbox","@coss/checkbox-group","@coss/combobox","@coss/group","@coss/label","@coss/popover","@coss/switch","@coss/tooltip"], + files: [{ + path: "registry/default/particles/p-switch-8.tsx", + type: "registry:block", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/default/particles/p-switch-8.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + categories: ["switch","combobox","group","popover","time"], + meta: {"className":"**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl","colSpan":2}, + }, + "p-switch-9": { + name: "p-switch-9", + description: "Weekly availability editor with From/To labeled time groups", + type: "registry:block", + registryDependencies: ["@coss/button","@coss/checkbox","@coss/checkbox-group","@coss/combobox","@coss/group","@coss/label","@coss/popover","@coss/switch","@coss/tooltip"], + files: [{ + path: "registry/default/particles/p-switch-9.tsx", + type: "registry:block", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/default/particles/p-switch-9.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + categories: ["switch","combobox","group","popover","time"], + meta: {"className":"**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl","colSpan":2}, + }, "p-table-1": { name: "p-table-1", description: "Basic table", diff --git a/apps/ui/registry/default/particles/p-switch-7.tsx b/apps/ui/registry/default/particles/p-switch-7.tsx new file mode 100644 index 000000000..9e13362ea --- /dev/null +++ b/apps/ui/registry/default/particles/p-switch-7.tsx @@ -0,0 +1,382 @@ +"use client"; + +import { CopyIcon, PlusIcon, SearchIcon, XIcon } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/registry/default/ui/button"; +import { Checkbox } from "@/registry/default/ui/checkbox"; +import { CheckboxGroup } from "@/registry/default/ui/checkbox-group"; +import { + Combobox, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxPopup, + ComboboxTrigger, + ComboboxValue, +} from "@/registry/default/ui/combobox"; +import { Label } from "@/registry/default/ui/label"; +import { + Popover, + PopoverPopup, + PopoverTrigger, +} from "@/registry/default/ui/popover"; +import { SelectButton } from "@/registry/default/ui/select"; +import { Switch } from "@/registry/default/ui/switch"; +import { + Tooltip, + TooltipPopup, + TooltipProvider, + TooltipTrigger, +} from "@/registry/default/ui/tooltip"; + +const days = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +] as const; + +type Day = (typeof days)[number]; + +type TimeRange = { + id: number; + start: string; + end: string; +}; + +const timeOptions = Array.from({ length: 96 }, (_, i) => { + const hours = Math.floor(i / 4); + const minutes = (i % 4) * 15; + const period = hours < 12 ? "AM" : "PM"; + const displayHours = hours % 12 === 0 ? 12 : hours % 12; + return `${displayHours}:${minutes.toString().padStart(2, "0")} ${period}`; +}); + +const timeIndex = (time: string) => timeOptions.indexOf(time); + +let rangeId = 0; +const createRange = (start: string, end: string): TimeRange => ({ + end, + id: ++rangeId, + start, +}); + +const defaultAvailability: Record = { + Friday: [createRange("9:00 AM", "5:00 PM")], + Monday: [createRange("9:00 AM", "5:00 PM")], + Saturday: [], + Sunday: [], + Thursday: [createRange("9:00 AM", "5:00 PM")], + Tuesday: [ + createRange("9:00 AM", "1:00 PM"), + createRange("3:00 PM", "5:00 PM"), + ], + Wednesday: [createRange("9:00 AM", "5:00 PM")], +}; + +function TimeCombobox({ + ariaLabel, + items, + onChange, + value, +}: { + ariaLabel: string; + items: string[]; + onChange: (time: string) => void; + value: string; +}) { + return ( + { + if (typeof time === "string") { + onChange(time); + } + }} + value={value} + > + } + > + + + +
+ } + /> +
+ No times found. + + {(time: string) => ( + + {time} + + )} + +
+
+ ); +} + +function CopyTimesPopover({ + day, + disabled, + onCopy, +}: { + day: Day; + disabled: boolean; + onCopy: (targets: Day[]) => void; +}) { + const [open, setOpen] = useState(false); + const [selectedDays, setSelectedDays] = useState([]); + + return ( + { + setOpen(nextOpen); + if (nextOpen) { + setSelectedDays([]); + } + }} + open={open} + > + + + } + /> + } + > + + Copy to other days + + +
+
+ Copy times to +
+ + {days + .filter((target) => target !== day) + .map((target) => ( + + ))} + + +
+
+
+ ); +} + +export default function Particle() { + const [availability, setAvailability] = + useState>(defaultAvailability); + + const setDayRanges = (day: Day, ranges: TimeRange[]) => { + setAvailability((prev) => ({ ...prev, [day]: ranges })); + }; + + const toggleDay = (day: Day, enabled: boolean) => { + setDayRanges(day, enabled ? [createRange("9:00 AM", "5:00 PM")] : []); + }; + + const addRange = (day: Day) => { + const ranges = availability[day]; + const lastRange = ranges[ranges.length - 1]; + if (!lastRange) { + setDayRanges(day, [createRange("9:00 AM", "5:00 PM")]); + return; + } + const startIndex = Math.min( + timeIndex(lastRange.end) + 4, + timeOptions.length - 2, + ); + const endIndex = Math.min(startIndex + 4, timeOptions.length - 1); + setDayRanges(day, [ + ...ranges, + createRange(timeOptions[startIndex] ?? "", timeOptions[endIndex] ?? ""), + ]); + }; + + const removeRange = (day: Day, id: number) => { + setDayRanges( + day, + availability[day].filter((range) => range.id !== id), + ); + }; + + const updateStart = (day: Day, id: number, start: string) => { + setDayRanges( + day, + availability[day].map((range) => { + if (range.id !== id) return range; + const end = + timeIndex(start) >= timeIndex(range.end) + ? (timeOptions[ + Math.min(timeIndex(start) + 4, timeOptions.length - 1) + ] ?? range.end) + : range.end; + return { ...range, end, start }; + }), + ); + }; + + const updateEnd = (day: Day, id: number, end: string) => { + setDayRanges( + day, + availability[day].map((range) => + range.id === id ? { ...range, end } : range, + ), + ); + }; + + const copyTo = (source: Day, targets: Day[]) => { + setAvailability((prev) => { + const next = { ...prev }; + for (const target of targets) { + next[target] = prev[source].map((range) => + createRange(range.start, range.end), + ); + } + return next; + }); + }; + + return ( + +
+ {days.map((day) => { + const ranges = availability[day]; + const lastRange = ranges[ranges.length - 1]; + const addDisabled = lastRange + ? timeIndex(lastRange.end) >= timeOptions.length - 2 + : false; + + return ( +
+ +
+
+ {ranges.length === 0 ? ( +

+ Unavailable +

+ ) : ( + ranges.map((range) => ( +
+ + updateStart(day, range.id, start) + } + value={range.start} + /> + + updateEnd(day, range.id, end)} + value={range.end} + /> + + removeRange(day, range.id)} + size="icon-sm" + variant="ghost" + /> + } + > + + Delete range + +
+ )) + )} +
+
+ + addRange(day)} + size="icon-sm" + variant="ghost" + /> + } + > + + Add range + + copyTo(day, targets)} + /> +
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/ui/registry/default/particles/p-switch-8.tsx b/apps/ui/registry/default/particles/p-switch-8.tsx new file mode 100644 index 000000000..90f64aa62 --- /dev/null +++ b/apps/ui/registry/default/particles/p-switch-8.tsx @@ -0,0 +1,397 @@ +"use client"; + +import { + ArrowRightIcon, + CopyIcon, + PlusIcon, + SearchIcon, + XIcon, +} from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/registry/default/ui/button"; +import { Checkbox } from "@/registry/default/ui/checkbox"; +import { CheckboxGroup } from "@/registry/default/ui/checkbox-group"; +import { + Combobox, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxPopup, + ComboboxTrigger, + ComboboxValue, +} from "@/registry/default/ui/combobox"; +import { Group, GroupSeparator, GroupText } from "@/registry/default/ui/group"; +import { Label } from "@/registry/default/ui/label"; +import { + Popover, + PopoverPopup, + PopoverTrigger, +} from "@/registry/default/ui/popover"; +import { Switch } from "@/registry/default/ui/switch"; +import { + Tooltip, + TooltipPopup, + TooltipProvider, + TooltipTrigger, +} from "@/registry/default/ui/tooltip"; + +const days = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +] as const; + +type Day = (typeof days)[number]; + +type TimeRange = { + id: number; + start: string; + end: string; +}; + +const timeOptions = Array.from({ length: 96 }, (_, i) => { + const hours = Math.floor(i / 4); + const minutes = (i % 4) * 15; + const period = hours < 12 ? "AM" : "PM"; + const displayHours = hours % 12 === 0 ? 12 : hours % 12; + return `${displayHours}:${minutes.toString().padStart(2, "0")} ${period}`; +}); + +const timeIndex = (time: string) => timeOptions.indexOf(time); + +let rangeId = 0; +const createRange = (start: string, end: string): TimeRange => ({ + end, + id: ++rangeId, + start, +}); + +const defaultAvailability: Record = { + Friday: [createRange("9:00 AM", "5:00 PM")], + Monday: [createRange("9:00 AM", "5:00 PM")], + Saturday: [], + Sunday: [], + Thursday: [createRange("9:00 AM", "5:00 PM")], + Tuesday: [ + createRange("9:00 AM", "1:00 PM"), + createRange("3:00 PM", "5:00 PM"), + ], + Wednesday: [createRange("9:00 AM", "5:00 PM")], +}; + +function TimeCombobox({ + ariaLabel, + items, + onChange, + value, +}: { + ariaLabel: string; + items: string[]; + onChange: (time: string) => void; + value: string; +}) { + return ( + { + if (typeof time === "string") { + onChange(time); + } + }} + value={value} + > + + } + > + + + +
+ } + /> +
+ No times found. + + {(time: string) => ( + + {time} + + )} + +
+
+ ); +} + +function CopyTimesPopover({ + day, + disabled, + onCopy, +}: { + day: Day; + disabled: boolean; + onCopy: (targets: Day[]) => void; +}) { + const [open, setOpen] = useState(false); + const [selectedDays, setSelectedDays] = useState([]); + + return ( + { + setOpen(nextOpen); + if (nextOpen) { + setSelectedDays([]); + } + }} + open={open} + > + + + } + /> + } + > + + Copy to other days + + +
+
+ Copy times to +
+ + {days + .filter((target) => target !== day) + .map((target) => ( + + ))} + + +
+
+
+ ); +} + +export default function Particle() { + const [availability, setAvailability] = + useState>(defaultAvailability); + + const setDayRanges = (day: Day, ranges: TimeRange[]) => { + setAvailability((prev) => ({ ...prev, [day]: ranges })); + }; + + const toggleDay = (day: Day, enabled: boolean) => { + setDayRanges(day, enabled ? [createRange("9:00 AM", "5:00 PM")] : []); + }; + + const addRange = (day: Day) => { + const ranges = availability[day]; + const lastRange = ranges[ranges.length - 1]; + if (!lastRange) { + setDayRanges(day, [createRange("9:00 AM", "5:00 PM")]); + return; + } + const startIndex = Math.min( + timeIndex(lastRange.end) + 4, + timeOptions.length - 2, + ); + const endIndex = Math.min(startIndex + 4, timeOptions.length - 1); + setDayRanges(day, [ + ...ranges, + createRange(timeOptions[startIndex] ?? "", timeOptions[endIndex] ?? ""), + ]); + }; + + const removeRange = (day: Day, id: number) => { + setDayRanges( + day, + availability[day].filter((range) => range.id !== id), + ); + }; + + const updateStart = (day: Day, id: number, start: string) => { + setDayRanges( + day, + availability[day].map((range) => { + if (range.id !== id) return range; + const end = + timeIndex(start) >= timeIndex(range.end) + ? (timeOptions[ + Math.min(timeIndex(start) + 4, timeOptions.length - 1) + ] ?? range.end) + : range.end; + return { ...range, end, start }; + }), + ); + }; + + const updateEnd = (day: Day, id: number, end: string) => { + setDayRanges( + day, + availability[day].map((range) => + range.id === id ? { ...range, end } : range, + ), + ); + }; + + const copyTo = (source: Day, targets: Day[]) => { + setAvailability((prev) => { + const next = { ...prev }; + for (const target of targets) { + next[target] = prev[source].map((range) => + createRange(range.start, range.end), + ); + } + return next; + }); + }; + + return ( + +
+ {days.map((day) => { + const ranges = availability[day]; + const lastRange = ranges[ranges.length - 1]; + const addDisabled = lastRange + ? timeIndex(lastRange.end) >= timeOptions.length - 2 + : false; + + return ( +
+ +
+
+ {ranges.length === 0 ? ( +

+ Unavailable +

+ ) : ( + ranges.map((range) => ( +
+ + + updateStart(day, range.id, start) + } + value={range.start} + /> + + + + updateEnd(day, range.id, end)} + value={range.end} + /> + + + removeRange(day, range.id)} + size="icon-sm" + variant="ghost" + /> + } + > + + Delete range + +
+ )) + )} +
+
+ + addRange(day)} + size="icon-sm" + variant="ghost" + /> + } + > + + Add range + + copyTo(day, targets)} + /> +
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/ui/registry/default/particles/p-switch-9.tsx b/apps/ui/registry/default/particles/p-switch-9.tsx new file mode 100644 index 000000000..08c8ba745 --- /dev/null +++ b/apps/ui/registry/default/particles/p-switch-9.tsx @@ -0,0 +1,406 @@ +"use client"; + +import { CopyIcon, PlusIcon, SearchIcon, XIcon } from "lucide-react"; +import { useId, useState } from "react"; +import { Button } from "@/registry/default/ui/button"; +import { Checkbox } from "@/registry/default/ui/checkbox"; +import { CheckboxGroup } from "@/registry/default/ui/checkbox-group"; +import { + Combobox, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxPopup, + ComboboxTrigger, + ComboboxValue, +} from "@/registry/default/ui/combobox"; +import { Group, GroupSeparator, GroupText } from "@/registry/default/ui/group"; +import { Label } from "@/registry/default/ui/label"; +import { + Popover, + PopoverPopup, + PopoverTrigger, +} from "@/registry/default/ui/popover"; +import { Switch } from "@/registry/default/ui/switch"; +import { + Tooltip, + TooltipPopup, + TooltipProvider, + TooltipTrigger, +} from "@/registry/default/ui/tooltip"; + +const days = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", +] as const; + +type Day = (typeof days)[number]; + +type TimeRange = { + id: number; + start: string; + end: string; +}; + +const timeOptions = Array.from({ length: 96 }, (_, i) => { + const hours = Math.floor(i / 4); + const minutes = (i % 4) * 15; + const period = hours < 12 ? "AM" : "PM"; + const displayHours = hours % 12 === 0 ? 12 : hours % 12; + return `${displayHours}:${minutes.toString().padStart(2, "0")} ${period}`; +}); + +const timeIndex = (time: string) => timeOptions.indexOf(time); + +let rangeId = 0; +const createRange = (start: string, end: string): TimeRange => ({ + end, + id: ++rangeId, + start, +}); + +const defaultAvailability: Record = { + Friday: [createRange("9:00 AM", "5:00 PM")], + Monday: [createRange("9:00 AM", "5:00 PM")], + Saturday: [], + Sunday: [], + Thursday: [createRange("9:00 AM", "5:00 PM")], + Tuesday: [ + createRange("9:00 AM", "1:00 PM"), + createRange("3:00 PM", "5:00 PM"), + ], + Wednesday: [createRange("9:00 AM", "5:00 PM")], +}; + +function TimeCombobox({ + ariaLabel, + id, + items, + onChange, + value, +}: { + ariaLabel: string; + id: string; + items: string[]; + onChange: (time: string) => void; + value: string; +}) { + return ( + { + if (typeof time === "string") { + onChange(time); + } + }} + value={value} + > + + } + > + + + +
+ } + /> +
+ No times found. + + {(time: string) => ( + + {time} + + )} + +
+
+ ); +} + +function CopyTimesPopover({ + day, + disabled, + onCopy, +}: { + day: Day; + disabled: boolean; + onCopy: (targets: Day[]) => void; +}) { + const [open, setOpen] = useState(false); + const [selectedDays, setSelectedDays] = useState([]); + + return ( + { + setOpen(nextOpen); + if (nextOpen) { + setSelectedDays([]); + } + }} + open={open} + > + + + } + /> + } + > + + Copy to other days + + +
+
+ Copy times to +
+ + {days + .filter((target) => target !== day) + .map((target) => ( + + ))} + + +
+
+
+ ); +} + +export default function Particle() { + const id = useId(); + const [availability, setAvailability] = + useState>(defaultAvailability); + + const setDayRanges = (day: Day, ranges: TimeRange[]) => { + setAvailability((prev) => ({ ...prev, [day]: ranges })); + }; + + const toggleDay = (day: Day, enabled: boolean) => { + setDayRanges(day, enabled ? [createRange("9:00 AM", "5:00 PM")] : []); + }; + + const addRange = (day: Day) => { + const ranges = availability[day]; + const lastRange = ranges[ranges.length - 1]; + if (!lastRange) { + setDayRanges(day, [createRange("9:00 AM", "5:00 PM")]); + return; + } + const startIndex = Math.min( + timeIndex(lastRange.end) + 4, + timeOptions.length - 2, + ); + const endIndex = Math.min(startIndex + 4, timeOptions.length - 1); + setDayRanges(day, [ + ...ranges, + createRange(timeOptions[startIndex] ?? "", timeOptions[endIndex] ?? ""), + ]); + }; + + const removeRange = (day: Day, id: number) => { + setDayRanges( + day, + availability[day].filter((range) => range.id !== id), + ); + }; + + const updateStart = (day: Day, id: number, start: string) => { + setDayRanges( + day, + availability[day].map((range) => { + if (range.id !== id) return range; + const end = + timeIndex(start) >= timeIndex(range.end) + ? (timeOptions[ + Math.min(timeIndex(start) + 4, timeOptions.length - 1) + ] ?? range.end) + : range.end; + return { ...range, end, start }; + }), + ); + }; + + const updateEnd = (day: Day, id: number, end: string) => { + setDayRanges( + day, + availability[day].map((range) => + range.id === id ? { ...range, end } : range, + ), + ); + }; + + const copyTo = (source: Day, targets: Day[]) => { + setAvailability((prev) => { + const next = { ...prev }; + for (const target of targets) { + next[target] = prev[source].map((range) => + createRange(range.start, range.end), + ); + } + return next; + }); + }; + + return ( + +
+ {days.map((day) => { + const ranges = availability[day]; + const lastRange = ranges[ranges.length - 1]; + const addDisabled = lastRange + ? timeIndex(lastRange.end) >= timeOptions.length - 2 + : false; + + return ( +
+ +
+
+ {ranges.length === 0 ? ( +

+ Unavailable +

+ ) : ( + ranges.map((range) => { + const startId = `${id}-start-${range.id}`; + const endId = `${id}-end-${range.id}`; + + return ( +
+ + }> + From + + + + updateStart(day, range.id, start) + } + value={range.start} + /> + + }> + To + + + updateEnd(day, range.id, end)} + value={range.end} + /> + + + removeRange(day, range.id)} + size="icon-sm" + variant="ghost" + /> + } + > + + Delete range + +
+ ); + }) + )} +
+
+ + addRange(day)} + size="icon-sm" + variant="ghost" + /> + } + > + + Add range + + copyTo(day, targets)} + /> +
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/ui/registry/registry-particles.ts b/apps/ui/registry/registry-particles.ts index 5f6afd02e..ae3a44deb 100644 --- a/apps/ui/registry/registry-particles.ts +++ b/apps/ui/registry/registry-particles.ts @@ -4802,6 +4802,78 @@ export const particles: ParticleItem[] = [ registryDependencies: ["@coss/switch"], type: "registry:block", }, + { + categories: categories("switch", "combobox", "popover", "time"), + dependencies: ["lucide-react"], + description: "Weekly availability editor with time range combobox pickers", + files: [{ path: "particles/p-switch-7.tsx", type: "registry:block" }], + meta: { + className: + "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + colSpan: 2, + }, + name: "p-switch-7", + registryDependencies: [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/label", + "@coss/popover", + "@coss/select", + "@coss/switch", + "@coss/tooltip", + ], + type: "registry:block", + }, + { + categories: categories("switch", "combobox", "group", "popover", "time"), + dependencies: ["lucide-react"], + description: "Weekly availability editor with grouped time range controls", + files: [{ path: "particles/p-switch-8.tsx", type: "registry:block" }], + meta: { + className: + "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + colSpan: 2, + }, + name: "p-switch-8", + registryDependencies: [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip", + ], + type: "registry:block", + }, + { + categories: categories("switch", "combobox", "group", "popover", "time"), + dependencies: ["lucide-react"], + description: "Weekly availability editor with From/To labeled time groups", + files: [{ path: "particles/p-switch-9.tsx", type: "registry:block" }], + meta: { + className: + "**:data-[slot=preview]:w-full sm:**:data-[slot=preview]:max-w-4xl", + colSpan: 2, + }, + name: "p-switch-9", + registryDependencies: [ + "@coss/button", + "@coss/checkbox", + "@coss/checkbox-group", + "@coss/combobox", + "@coss/group", + "@coss/label", + "@coss/popover", + "@coss/switch", + "@coss/tooltip", + ], + type: "registry:block", + }, { categories: categories("table"), description: "Basic table",