@aglyn/shared-ui-jsx 1.0.0-beta.246 → 1.0.0-beta.249

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-ui-jsx",
3
- "version": "1.0.0-beta.246",
3
+ "version": "1.0.0-beta.249",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,10 +25,10 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-mdi": "1.0.0-beta.246",
29
- "@aglyn/shared-ui-theme": "1.0.0-beta.246",
30
- "@aglyn/shared-util-tools": "1.0.0-beta.246",
31
- "@aglyn/shared-util-vendor": "1.0.0-beta.246",
28
+ "@aglyn/shared-data-mdi": "1.0.0-beta.249",
29
+ "@aglyn/shared-ui-theme": "1.0.0-beta.249",
30
+ "@aglyn/shared-util-tools": "1.0.0-beta.249",
31
+ "@aglyn/shared-util-vendor": "1.0.0-beta.249",
32
32
  "@mdi/js": "^7.4.47",
33
33
  "@swc/helpers": "0.5.23",
34
34
  "change-case": "^5.4.4",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ReactNode } from 'react';
18
+ import { type WeeklyHours, type WeeklyHoursInterval } from '../utils/weekly-hours';
19
+ export interface WeeklyHoursPickerProps {
20
+ value: WeeklyHours;
21
+ onChange: (hours: WeeklyHours) => void;
22
+ /** Shown above the days, e.g. "Pickup hours". */
23
+ label?: ReactNode;
24
+ helperText?: ReactNode;
25
+ disabled?: boolean;
26
+ /** What a day gets when it is switched on or given another interval. */
27
+ defaultInterval?: WeeklyHoursInterval;
28
+ /** Word for an interval on the add button: "hours" or "window". */
29
+ intervalNoun?: string;
30
+ /**
31
+ * Every interval ends on the day it starts: the To menu offers only times
32
+ * after From. Off by default, where a To before From runs past midnight.
33
+ */
34
+ sameDay?: boolean;
35
+ }
36
+ /**
37
+ * A week of opening hours chosen by clicking: a switch per day, and for an
38
+ * open day one or more from/to times picked from a menu. Nothing is typed.
39
+ */
40
+ export declare function WeeklyHoursPicker(props: WeeklyHoursPickerProps): import("react").JSX.Element;
41
+ export interface OpeningHoursPickerProps extends Omit<WeeklyHoursPickerProps, 'value' | 'onChange'> {
42
+ /** Hours text, one `Mo-Fr 09:00-17:00` line per set of days. */
43
+ value: string | null | undefined;
44
+ onChange: (text: string) => void;
45
+ }
46
+ /**
47
+ * {@link WeeklyHoursPicker} over the stored hours text: reads the lines into a
48
+ * week and writes the week back as lines, so what is saved is unchanged.
49
+ */
50
+ export declare function OpeningHoursPicker(props: OpeningHoursPickerProps): import("react").JSX.Element;
51
+ export default WeeklyHoursPicker;
@@ -0,0 +1,250 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ /**
5
+ * @license
6
+ * Copyright 2026 Aglyn LLC
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */ import { mdiClose, mdiPlus } from "@aglyn/shared-data-mdi";
20
+ import { Box, Button, FormHelperText, IconButton, MenuItem, Stack, Switch, TextField, Tooltip, Typography } from "@mui/material";
21
+ import { useMemo } from "react";
22
+ import { clockLabel, WEEKLY_HOURS_DAYS, weeklyHoursFromText, weeklyHoursToText } from "../utils/weekly-hours.js";
23
+ import { MdiIcon } from "./mdi-icon/index.js";
24
+ const LAST_MINUTE = 23 * 60 + 59;
25
+ /** Every quarter hour, plus 11:59 PM so a day can run to midnight. */ const TIME_OPTIONS = [
26
+ ...Array.from({
27
+ length: 96
28
+ }, (_, step)=>step * 15),
29
+ LAST_MINUTE
30
+ ];
31
+ const DEFAULT_INTERVAL = {
32
+ start: 9 * 60,
33
+ end: 17 * 60
34
+ };
35
+ /**
36
+ * A week of opening hours chosen by clicking: a switch per day, and for an
37
+ * open day one or more from/to times picked from a menu. Nothing is typed.
38
+ */ export function WeeklyHoursPicker(props) {
39
+ const { value, onChange, label, helperText, disabled, defaultInterval = DEFAULT_INTERVAL, intervalNoun = 'hours', sameDay = false } = props;
40
+ const setDay = (index, intervals)=>onChange(value.map((day, i)=>i === index ? intervals : day));
41
+ const optionsFor = (current, after)=>{
42
+ const options = TIME_OPTIONS.includes(current) ? TIME_OPTIONS : [
43
+ ...TIME_OPTIONS,
44
+ current
45
+ ].sort((a, b)=>a - b);
46
+ return after === undefined ? options : options.filter((minutes)=>minutes > after || minutes === current);
47
+ };
48
+ const changed = (interval, edge, minutes)=>{
49
+ const next = _extends({}, interval, {
50
+ [edge]: minutes
51
+ });
52
+ // A From moved past its To drags the To along, keeping the length.
53
+ if (sameDay && next.end <= next.start) {
54
+ next.end = Math.min(next.start + Math.max(interval.end - interval.start, 60), LAST_MINUTE);
55
+ }
56
+ return next;
57
+ };
58
+ return /*#__PURE__*/ _jsxs(Box, {
59
+ role: "group",
60
+ "aria-label": typeof label === 'string' ? label : undefined,
61
+ children: [
62
+ label ? /*#__PURE__*/ _jsx(Typography, {
63
+ variant: "subtitle2",
64
+ sx: {
65
+ mb: 0.5
66
+ },
67
+ children: label
68
+ }) : null,
69
+ /*#__PURE__*/ _jsx(Stack, {
70
+ spacing: 0.75,
71
+ children: WEEKLY_HOURS_DAYS.map(({ index, name })=>{
72
+ var _value_index;
73
+ const intervals = (_value_index = value[index]) != null ? _value_index : [];
74
+ const open = intervals.length > 0;
75
+ const last = intervals[intervals.length - 1];
76
+ return /*#__PURE__*/ _jsxs(Stack, {
77
+ direction: {
78
+ xs: 'column',
79
+ sm: 'row'
80
+ },
81
+ spacing: 1,
82
+ sx: {
83
+ alignItems: {
84
+ xs: 'stretch',
85
+ sm: 'flex-start'
86
+ }
87
+ },
88
+ children: [
89
+ /*#__PURE__*/ _jsxs(Stack, {
90
+ direction: "row",
91
+ sx: {
92
+ alignItems: 'center',
93
+ minWidth: 150
94
+ },
95
+ children: [
96
+ /*#__PURE__*/ _jsx(Switch, {
97
+ size: "small",
98
+ checked: open,
99
+ disabled: disabled,
100
+ onChange: (event)=>setDay(index, event.target.checked ? [
101
+ _extends({}, defaultInterval)
102
+ ] : []),
103
+ slotProps: {
104
+ input: {
105
+ 'aria-label': `Open on ${name}`
106
+ }
107
+ }
108
+ }),
109
+ /*#__PURE__*/ _jsx(Typography, {
110
+ variant: "body2",
111
+ children: name
112
+ })
113
+ ]
114
+ }),
115
+ open ? /*#__PURE__*/ _jsxs(Stack, {
116
+ spacing: 0.75,
117
+ sx: {
118
+ flex: 1
119
+ },
120
+ children: [
121
+ intervals.map((interval, position)=>/*#__PURE__*/ _jsxs(Stack, {
122
+ direction: "row",
123
+ spacing: 1,
124
+ sx: {
125
+ alignItems: 'center'
126
+ },
127
+ children: [
128
+ [
129
+ 'start',
130
+ 'end'
131
+ ].map((edge)=>/*#__PURE__*/ _jsx(TextField, {
132
+ select: true,
133
+ size: "small",
134
+ label: edge === 'start' ? 'From' : 'To',
135
+ value: interval[edge],
136
+ disabled: disabled,
137
+ onChange: (event)=>setDay(index, intervals.map((item, i)=>i === position ? changed(item, edge, Number(event.target.value)) : item)),
138
+ slotProps: {
139
+ htmlInput: {
140
+ 'aria-label': `${name} ${edge === 'start' ? 'from' : 'to'}${intervals.length > 1 ? ` ${position + 1}` : ''}`
141
+ },
142
+ select: {
143
+ MenuProps: {
144
+ slotProps: {
145
+ paper: {
146
+ sx: {
147
+ maxHeight: 320
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ },
154
+ sx: {
155
+ minWidth: 128,
156
+ flex: 1
157
+ },
158
+ children: optionsFor(interval[edge], sameDay && edge === 'end' ? interval.start : undefined).map((minutes)=>/*#__PURE__*/ _jsx(MenuItem, {
159
+ value: minutes,
160
+ children: clockLabel(minutes)
161
+ }, minutes))
162
+ }, edge)),
163
+ /*#__PURE__*/ _jsx(Tooltip, {
164
+ title: `Remove these ${intervalNoun}`,
165
+ children: /*#__PURE__*/ _jsx("span", {
166
+ children: /*#__PURE__*/ _jsx(IconButton, {
167
+ size: "small",
168
+ disabled: disabled,
169
+ "aria-label": `Remove ${name} ${intervalNoun}${intervals.length > 1 ? ` ${position + 1}` : ''}`,
170
+ onClick: ()=>setDay(index, intervals.filter((_, i)=>i !== position)),
171
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
172
+ path: mdiClose.path,
173
+ fontSize: "small"
174
+ })
175
+ })
176
+ })
177
+ })
178
+ ]
179
+ }, position)),
180
+ /*#__PURE__*/ _jsx(Box, {
181
+ children: /*#__PURE__*/ _jsx(Button, {
182
+ size: "small",
183
+ disabled: disabled,
184
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
185
+ path: mdiPlus.path,
186
+ fontSize: "small"
187
+ }),
188
+ "aria-label": `Add ${intervalNoun} on ${name}`,
189
+ onClick: ()=>{
190
+ // A second interval starts an hour after the first ends,
191
+ // the usual shape of a lunch break.
192
+ const start = Math.min(last.end + 60, 22 * 60);
193
+ setDay(index, [
194
+ ...intervals,
195
+ {
196
+ start,
197
+ end: Math.min(start + 4 * 60, LAST_MINUTE)
198
+ }
199
+ ]);
200
+ },
201
+ children: `Add ${intervalNoun}`
202
+ })
203
+ })
204
+ ]
205
+ }) : /*#__PURE__*/ _jsx(Typography, {
206
+ variant: "body2",
207
+ color: "text.secondary",
208
+ sx: {
209
+ pt: {
210
+ sm: 0.75
211
+ }
212
+ },
213
+ children: 'Closed'
214
+ })
215
+ ]
216
+ }, index);
217
+ })
218
+ }),
219
+ helperText ? /*#__PURE__*/ _jsx(FormHelperText, {
220
+ children: helperText
221
+ }) : null
222
+ ]
223
+ });
224
+ }
225
+ /**
226
+ * {@link WeeklyHoursPicker} over the stored hours text: reads the lines into a
227
+ * week and writes the week back as lines, so what is saved is unchanged.
228
+ */ export function OpeningHoursPicker(props) {
229
+ const { value, onChange, helperText } = props, rest = _object_without_properties_loose(props, [
230
+ "value",
231
+ "onChange",
232
+ "helperText"
233
+ ]);
234
+ const { hours, unreadLines } = useMemo(()=>weeklyHoursFromText(value != null ? value : ''), [
235
+ value
236
+ ]);
237
+ return /*#__PURE__*/ _jsx(WeeklyHoursPicker, _extends({}, rest, {
238
+ value: hours,
239
+ onChange: (next)=>onChange(weeklyHoursToText(next)),
240
+ helperText: unreadLines.length ? /*#__PURE__*/ _jsx(Typography, {
241
+ component: "span",
242
+ variant: "caption",
243
+ color: "warning.main",
244
+ children: `Some saved hours could not be read (line ${unreadLines.join(', ')}) and are not shown. ` + 'Set the days again here and save to replace them.'
245
+ }) : helperText
246
+ }));
247
+ }
248
+ export default WeeklyHoursPicker;
249
+
250
+ //# sourceMappingURL=weekly-hours-picker.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/weekly-hours-picker.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { mdiClose, mdiPlus } from '@aglyn/shared-data-mdi'\nimport {\n Box,\n Button,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { type ReactNode, useMemo } from 'react'\n\nimport {\n clockLabel,\n type WeeklyHours,\n type WeeklyHoursInterval,\n WEEKLY_HOURS_DAYS,\n weeklyHoursFromText,\n weeklyHoursToText,\n} from '../utils/weekly-hours'\nimport { MdiIcon } from './mdi-icon'\n\nconst LAST_MINUTE = 23 * 60 + 59\n\n/** Every quarter hour, plus 11:59 PM so a day can run to midnight. */\nconst TIME_OPTIONS: number[] = [...Array.from({ length: 96 }, (_, step) => step * 15), LAST_MINUTE]\n\nconst DEFAULT_INTERVAL: WeeklyHoursInterval = { start: 9 * 60, end: 17 * 60 }\n\nexport interface WeeklyHoursPickerProps {\n value: WeeklyHours\n onChange: (hours: WeeklyHours) => void\n /** Shown above the days, e.g. \"Pickup hours\". */\n label?: ReactNode\n helperText?: ReactNode\n disabled?: boolean\n /** What a day gets when it is switched on or given another interval. */\n defaultInterval?: WeeklyHoursInterval\n /** Word for an interval on the add button: \"hours\" or \"window\". */\n intervalNoun?: string\n /**\n * Every interval ends on the day it starts: the To menu offers only times\n * after From. Off by default, where a To before From runs past midnight.\n */\n sameDay?: boolean\n}\n\n/**\n * A week of opening hours chosen by clicking: a switch per day, and for an\n * open day one or more from/to times picked from a menu. Nothing is typed.\n */\nexport function WeeklyHoursPicker(props: WeeklyHoursPickerProps) {\n const {\n value,\n onChange,\n label,\n helperText,\n disabled,\n defaultInterval = DEFAULT_INTERVAL,\n intervalNoun = 'hours',\n sameDay = false,\n } = props\n\n const setDay = (index: number, intervals: WeeklyHoursInterval[]) =>\n onChange(value.map((day, i) => (i === index ? intervals : day)))\n\n const optionsFor = (current: number, after?: number) => {\n const options = TIME_OPTIONS.includes(current)\n ? TIME_OPTIONS\n : [...TIME_OPTIONS, current].sort((a, b) => a - b)\n return after === undefined\n ? options\n : options.filter((minutes) => minutes > after || minutes === current)\n }\n\n const changed = (\n interval: WeeklyHoursInterval,\n edge: 'start' | 'end',\n minutes: number,\n ): WeeklyHoursInterval => {\n const next = { ...interval, [edge]: minutes }\n // A From moved past its To drags the To along, keeping the length.\n if (sameDay && next.end <= next.start) {\n next.end = Math.min(next.start + Math.max(interval.end - interval.start, 60), LAST_MINUTE)\n }\n return next\n }\n\n return (\n <Box role=\"group\" aria-label={typeof label === 'string' ? label : undefined}>\n {label ? (\n <Typography variant=\"subtitle2\" sx={{ mb: 0.5 }}>\n {label}\n </Typography>\n ) : null}\n <Stack spacing={0.75}>\n {WEEKLY_HOURS_DAYS.map(({ index, name }) => {\n const intervals = value[index] ?? []\n const open = intervals.length > 0\n const last = intervals[intervals.length - 1]\n return (\n <Stack\n key={index}\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ alignItems: { xs: 'stretch', sm: 'flex-start' } }}\n >\n <Stack direction=\"row\" sx={{ alignItems: 'center', minWidth: 150 }}>\n <Switch\n size=\"small\"\n checked={open}\n disabled={disabled}\n onChange={(event) =>\n setDay(index, event.target.checked ? [{ ...defaultInterval }] : [])\n }\n slotProps={{ input: { 'aria-label': `Open on ${name}` } }}\n />\n <Typography variant=\"body2\">{name}</Typography>\n </Stack>\n {open ? (\n <Stack spacing={0.75} sx={{ flex: 1 }}>\n {intervals.map((interval, position) => (\n <Stack key={position} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {(['start', 'end'] as const).map((edge) => (\n <TextField\n key={edge}\n select\n size=\"small\"\n label={edge === 'start' ? 'From' : 'To'}\n value={interval[edge]}\n disabled={disabled}\n onChange={(event) =>\n setDay(\n index,\n intervals.map((item, i) =>\n i === position\n ? changed(item, edge, Number(event.target.value))\n : item,\n ),\n )\n }\n slotProps={{\n htmlInput: {\n 'aria-label': `${name} ${edge === 'start' ? 'from' : 'to'}${\n intervals.length > 1 ? ` ${position + 1}` : ''\n }`,\n },\n select: { MenuProps: { slotProps: { paper: { sx: { maxHeight: 320 } } } } },\n }}\n sx={{ minWidth: 128, flex: 1 }}\n >\n {optionsFor(\n interval[edge],\n sameDay && edge === 'end' ? interval.start : undefined,\n ).map((minutes) => (\n <MenuItem key={minutes} value={minutes}>\n {clockLabel(minutes)}\n </MenuItem>\n ))}\n </TextField>\n ))}\n <Tooltip title={`Remove these ${intervalNoun}`}>\n <span>\n <IconButton\n size=\"small\"\n disabled={disabled}\n aria-label={`Remove ${name} ${intervalNoun}${\n intervals.length > 1 ? ` ${position + 1}` : ''\n }`}\n onClick={() =>\n setDay(\n index,\n intervals.filter((_, i) => i !== position),\n )\n }\n >\n <MdiIcon path={mdiClose.path} fontSize=\"small\" />\n </IconButton>\n </span>\n </Tooltip>\n </Stack>\n ))}\n <Box>\n <Button\n size=\"small\"\n disabled={disabled}\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n aria-label={`Add ${intervalNoun} on ${name}`}\n onClick={() => {\n // A second interval starts an hour after the first ends,\n // the usual shape of a lunch break.\n const start = Math.min(last.end + 60, 22 * 60)\n setDay(index, [\n ...intervals,\n { start, end: Math.min(start + 4 * 60, LAST_MINUTE) },\n ])\n }}\n >\n {`Add ${intervalNoun}`}\n </Button>\n </Box>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ pt: { sm: 0.75 } }}>\n {'Closed'}\n </Typography>\n )}\n </Stack>\n )\n })}\n </Stack>\n {helperText ? <FormHelperText>{helperText}</FormHelperText> : null}\n </Box>\n )\n}\n\nexport interface OpeningHoursPickerProps\n extends Omit<WeeklyHoursPickerProps, 'value' | 'onChange'> {\n /** Hours text, one `Mo-Fr 09:00-17:00` line per set of days. */\n value: string | null | undefined\n onChange: (text: string) => void\n}\n\n/**\n * {@link WeeklyHoursPicker} over the stored hours text: reads the lines into a\n * week and writes the week back as lines, so what is saved is unchanged.\n */\nexport function OpeningHoursPicker(props: OpeningHoursPickerProps) {\n const { value, onChange, helperText, ...rest } = props\n const { hours, unreadLines } = useMemo(() => weeklyHoursFromText(value ?? ''), [value])\n return (\n <WeeklyHoursPicker\n {...rest}\n value={hours}\n onChange={(next) => onChange(weeklyHoursToText(next))}\n helperText={\n unreadLines.length ? (\n <Typography component=\"span\" variant=\"caption\" color=\"warning.main\">\n {`Some saved hours could not be read (line ${unreadLines.join(', ')}) and are not shown. ` +\n 'Set the days again here and save to replace them.'}\n </Typography>\n ) : (\n helperText\n )\n }\n />\n )\n}\n\nexport default WeeklyHoursPicker\n"],"names":["mdiClose","mdiPlus","Box","Button","FormHelperText","IconButton","MenuItem","Stack","Switch","TextField","Tooltip","Typography","useMemo","clockLabel","WEEKLY_HOURS_DAYS","weeklyHoursFromText","weeklyHoursToText","MdiIcon","LAST_MINUTE","TIME_OPTIONS","Array","from","length","_","step","DEFAULT_INTERVAL","start","end","WeeklyHoursPicker","props","value","onChange","label","helperText","disabled","defaultInterval","intervalNoun","sameDay","setDay","index","intervals","map","day","i","optionsFor","current","after","options","includes","sort","a","b","undefined","filter","minutes","changed","interval","edge","next","Math","min","max","role","aria-label","variant","sx","mb","spacing","name","open","last","direction","xs","sm","alignItems","minWidth","size","checked","event","target","slotProps","input","flex","position","select","item","Number","htmlInput","MenuProps","paper","maxHeight","title","span","onClick","path","fontSize","startIcon","color","pt","OpeningHoursPicker","rest","hours","unreadLines","component","join"],"mappings":";;;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,QAAQ,EAAEC,OAAO,QAAQ,yBAAwB;AAC1D,SACEC,GAAG,EACHC,MAAM,EACNC,cAAc,EACdC,UAAU,EACVC,QAAQ,EACRC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,OAAO,EACPC,UAAU,QACL,gBAAe;AACtB,SAAyBC,OAAO,QAAQ,QAAO;AAE/C,SACEC,UAAU,EAGVC,iBAAiB,EACjBC,mBAAmB,EACnBC,iBAAiB,QACZ,2BAAuB;AAC9B,SAASC,OAAO,QAAQ,sBAAY;AAEpC,MAAMC,cAAc,KAAK,KAAK;AAE9B,oEAAoE,GACpE,MAAMC,eAAyB;OAAIC,MAAMC,IAAI,CAAC;QAAEC,QAAQ;IAAG,GAAG,CAACC,GAAGC,OAASA,OAAO;IAAKN;CAAY;AAEnG,MAAMO,mBAAwC;IAAEC,OAAO,IAAI;IAAIC,KAAK,KAAK;AAAG;AAoB5E;;;CAGC,GACD,OAAO,SAASC,kBAAkBC,KAA6B;IAC7D,MAAM,EACJC,KAAK,EACLC,QAAQ,EACRC,KAAK,EACLC,UAAU,EACVC,QAAQ,EACRC,kBAAkBV,gBAAgB,EAClCW,eAAe,OAAO,EACtBC,UAAU,KAAK,EAChB,GAAGR;IAEJ,MAAMS,SAAS,CAACC,OAAeC,YAC7BT,SAASD,MAAMW,GAAG,CAAC,CAACC,KAAKC,IAAOA,MAAMJ,QAAQC,YAAYE;IAE5D,MAAME,aAAa,CAACC,SAAiBC;QACnC,MAAMC,UAAU5B,aAAa6B,QAAQ,CAACH,WAClC1B,eACA;eAAIA;YAAc0B;SAAQ,CAACI,IAAI,CAAC,CAACC,GAAGC,IAAMD,IAAIC;QAClD,OAAOL,UAAUM,YACbL,UACAA,QAAQM,MAAM,CAAC,CAACC,UAAYA,UAAUR,SAASQ,YAAYT;IACjE;IAEA,MAAMU,UAAU,CACdC,UACAC,MACAH;QAEA,MAAMI,OAAO,aAAKF;YAAU,CAACC,KAAK,EAAEH;;QACpC,mEAAmE;QACnE,IAAIjB,WAAWqB,KAAK/B,GAAG,IAAI+B,KAAKhC,KAAK,EAAE;YACrCgC,KAAK/B,GAAG,GAAGgC,KAAKC,GAAG,CAACF,KAAKhC,KAAK,GAAGiC,KAAKE,GAAG,CAACL,SAAS7B,GAAG,GAAG6B,SAAS9B,KAAK,EAAE,KAAKR;QAChF;QACA,OAAOwC;IACT;IAEA,qBACE,MAACxD;QAAI4D,MAAK;QAAQC,cAAY,OAAO/B,UAAU,WAAWA,QAAQoB;;YAC/DpB,sBACC,KAACrB;gBAAWqD,SAAQ;gBAAYC,IAAI;oBAAEC,IAAI;gBAAI;0BAC3ClC;iBAED;0BACJ,KAACzB;gBAAM4D,SAAS;0BACbrD,kBAAkB2B,GAAG,CAAC,CAAC,EAAEF,KAAK,EAAE6B,IAAI,EAAE;wBACnBtC;oBAAlB,MAAMU,aAAYV,eAAAA,KAAK,CAACS,MAAM,YAAZT,eAAgB,EAAE;oBACpC,MAAMuC,OAAO7B,UAAUlB,MAAM,GAAG;oBAChC,MAAMgD,OAAO9B,SAAS,CAACA,UAAUlB,MAAM,GAAG,EAAE;oBAC5C,qBACE,MAACf;wBAECgE,WAAW;4BAAEC,IAAI;4BAAUC,IAAI;wBAAM;wBACrCN,SAAS;wBACTF,IAAI;4BAAES,YAAY;gCAAEF,IAAI;gCAAWC,IAAI;4BAAa;wBAAE;;0CAEtD,MAAClE;gCAAMgE,WAAU;gCAAMN,IAAI;oCAAES,YAAY;oCAAUC,UAAU;gCAAI;;kDAC/D,KAACnE;wCACCoE,MAAK;wCACLC,SAASR;wCACTnC,UAAUA;wCACVH,UAAU,CAAC+C,QACTxC,OAAOC,OAAOuC,MAAMC,MAAM,CAACF,OAAO,GAAG;gDAAC,aAAK1C;6CAAkB,GAAG,EAAE;wCAEpE6C,WAAW;4CAAEC,OAAO;gDAAE,cAAc,CAAC,QAAQ,EAAEb,MAAM;4CAAC;wCAAE;;kDAE1D,KAACzD;wCAAWqD,SAAQ;kDAASI;;;;4BAE9BC,qBACC,MAAC9D;gCAAM4D,SAAS;gCAAMF,IAAI;oCAAEiB,MAAM;gCAAE;;oCACjC1C,UAAUC,GAAG,CAAC,CAACe,UAAU2B,yBACxB,MAAC5E;4CAAqBgE,WAAU;4CAAMJ,SAAS;4CAAGF,IAAI;gDAAES,YAAY;4CAAS;;gDACzE;oDAAC;oDAAS;iDAAM,CAAWjC,GAAG,CAAC,CAACgB,qBAChC,KAAChD;wDAEC2E,MAAM;wDACNR,MAAK;wDACL5C,OAAOyB,SAAS,UAAU,SAAS;wDACnC3B,OAAO0B,QAAQ,CAACC,KAAK;wDACrBvB,UAAUA;wDACVH,UAAU,CAAC+C,QACTxC,OACEC,OACAC,UAAUC,GAAG,CAAC,CAAC4C,MAAM1C,IACnBA,MAAMwC,WACF5B,QAAQ8B,MAAM5B,MAAM6B,OAAOR,MAAMC,MAAM,CAACjD,KAAK,KAC7CuD;wDAIVL,WAAW;4DACTO,WAAW;gEACT,cAAc,GAAGnB,KAAK,CAAC,EAAEX,SAAS,UAAU,SAAS,OACnDjB,UAAUlB,MAAM,GAAG,IAAI,CAAC,CAAC,EAAE6D,WAAW,GAAG,GAAG,IAC5C;4DACJ;4DACAC,QAAQ;gEAAEI,WAAW;oEAAER,WAAW;wEAAES,OAAO;4EAAExB,IAAI;gFAAEyB,WAAW;4EAAI;wEAAE;oEAAE;gEAAE;4DAAE;wDAC5E;wDACAzB,IAAI;4DAAEU,UAAU;4DAAKO,MAAM;wDAAE;kEAE5BtC,WACCY,QAAQ,CAACC,KAAK,EACdpB,WAAWoB,SAAS,QAAQD,SAAS9B,KAAK,GAAG0B,WAC7CX,GAAG,CAAC,CAACa,wBACL,KAAChD;gEAAuBwB,OAAOwB;0EAC5BzC,WAAWyC;+DADCA;uDA9BZG;8DAoCT,KAAC/C;oDAAQiF,OAAO,CAAC,aAAa,EAAEvD,cAAc;8DAC5C,cAAA,KAACwD;kEACC,cAAA,KAACvF;4DACCuE,MAAK;4DACL1C,UAAUA;4DACV6B,cAAY,CAAC,OAAO,EAAEK,KAAK,CAAC,EAAEhC,eAC5BI,UAAUlB,MAAM,GAAG,IAAI,CAAC,CAAC,EAAE6D,WAAW,GAAG,GAAG,IAC5C;4DACFU,SAAS,IACPvD,OACEC,OACAC,UAAUa,MAAM,CAAC,CAAC9B,GAAGoB,IAAMA,MAAMwC;sEAIrC,cAAA,KAAClE;gEAAQ6E,MAAM9F,SAAS8F,IAAI;gEAAEC,UAAS;;;;;;2CAtDnCZ;kDA4Dd,KAACjF;kDACC,cAAA,KAACC;4CACCyE,MAAK;4CACL1C,UAAUA;4CACV8D,yBAAW,KAAC/E;gDAAQ6E,MAAM7F,QAAQ6F,IAAI;gDAAEC,UAAS;;4CACjDhC,cAAY,CAAC,IAAI,EAAE3B,aAAa,IAAI,EAAEgC,MAAM;4CAC5CyB,SAAS;gDACP,yDAAyD;gDACzD,oCAAoC;gDACpC,MAAMnE,QAAQiC,KAAKC,GAAG,CAACU,KAAK3C,GAAG,GAAG,IAAI,KAAK;gDAC3CW,OAAOC,OAAO;uDACTC;oDACH;wDAAEd;wDAAOC,KAAKgC,KAAKC,GAAG,CAAClC,QAAQ,IAAI,IAAIR;oDAAa;iDACrD;4CACH;sDAEC,CAAC,IAAI,EAAEkB,cAAc;;;;+CAK5B,KAACzB;gCAAWqD,SAAQ;gCAAQiC,OAAM;gCAAiBhC,IAAI;oCAAEiC,IAAI;wCAAEzB,IAAI;oCAAK;gCAAE;0CACvE;;;uBAtGAlC;gBA2GX;;YAEDN,2BAAa,KAAC7B;0BAAgB6B;iBAA+B;;;AAGpE;AASA;;;CAGC,GACD,OAAO,SAASkE,mBAAmBtE,KAA8B;IAC/D,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAEE,UAAU,EAAW,GAAGJ,OAATuE,wCAASvE;;;;;IACjD,MAAM,EAAEwE,KAAK,EAAEC,WAAW,EAAE,GAAG1F,QAAQ,IAAMG,oBAAoBe,gBAAAA,QAAS,KAAK;QAACA;KAAM;IACtF,qBACE,KAACF,gCACKwE;QACJtE,OAAOuE;QACPtE,UAAU,CAAC2B,OAAS3B,SAASf,kBAAkB0C;QAC/CzB,YACEqE,YAAYhF,MAAM,iBAChB,KAACX;YAAW4F,WAAU;YAAOvC,SAAQ;YAAUiC,OAAM;sBAClD,CAAC,yCAAyC,EAAEK,YAAYE,IAAI,CAAC,MAAM,qBAAqB,CAAC,GACxF;aAGJvE;;AAKV;AAEA,eAAeL,kBAAiB"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The model behind the weekly hours picker: seven days, each a list of open
19
+ * intervals in minutes since midnight. Index 0 is Sunday, as `Date#getDay`
20
+ * counts, so a booking service's `windows[weekday]` maps onto it as is.
21
+ *
22
+ * Opening hours, pickup hours and delivery windows are STORED as text lines
23
+ * in `schema.org`'s `openingHours` syntax (`Mo-Fr 09:00-17:00`), which the
24
+ * structured-data serializer and checkout read. The picker writes those same
25
+ * lines, so nothing downstream changes; nobody types them.
26
+ */
27
+ export interface WeeklyHoursInterval {
28
+ /** Minutes since midnight. */
29
+ start: number;
30
+ /** Minutes since midnight. Earlier than `start` means the next morning. */
31
+ end: number;
32
+ }
33
+ /** Seven entries, Sunday first; an empty list is a closed day. */
34
+ export type WeeklyHours = WeeklyHoursInterval[][];
35
+ /** Display order and names, Monday first, with each day's index. */
36
+ export declare const WEEKLY_HOURS_DAYS: ReadonlyArray<{
37
+ index: number;
38
+ name: string;
39
+ short: string;
40
+ }>;
41
+ /** An all-closed week. */
42
+ export declare const emptyWeeklyHours: () => WeeklyHours;
43
+ /** `540` → `09:00`. */
44
+ export declare const formatClockMinutes: (minutes: number) => string;
45
+ /** `540` → `9:00 AM`, for a menu. */
46
+ export declare const clockLabel: (minutes: number) => string;
47
+ /**
48
+ * Hours text → the picker's week. Lines that do not read are returned by
49
+ * their 1-based number, so the picker can say what it could not show rather
50
+ * than silently dropping a saved opening time.
51
+ */
52
+ export declare function weeklyHoursFromText(value: unknown): {
53
+ hours: WeeklyHours;
54
+ unreadLines: number[];
55
+ };
56
+ /**
57
+ * The picker's week → hours text, one line per interval per set of days that
58
+ * share it: days with the same hours are written together (`Mo-Fr`), which is
59
+ * how a business reads its own hours back.
60
+ */
61
+ export declare function weeklyHoursToText(hours: WeeklyHours): string;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The model behind the weekly hours picker: seven days, each a list of open
18
+ * intervals in minutes since midnight. Index 0 is Sunday, as `Date#getDay`
19
+ * counts, so a booking service's `windows[weekday]` maps onto it as is.
20
+ *
21
+ * Opening hours, pickup hours and delivery windows are STORED as text lines
22
+ * in `schema.org`'s `openingHours` syntax (`Mo-Fr 09:00-17:00`), which the
23
+ * structured-data serializer and checkout read. The picker writes those same
24
+ * lines, so nothing downstream changes; nobody types them.
25
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
26
+ /** Display order and names, Monday first, with each day's index. */ export const WEEKLY_HOURS_DAYS = [
27
+ {
28
+ index: 1,
29
+ name: 'Monday',
30
+ short: 'Mo'
31
+ },
32
+ {
33
+ index: 2,
34
+ name: 'Tuesday',
35
+ short: 'Tu'
36
+ },
37
+ {
38
+ index: 3,
39
+ name: 'Wednesday',
40
+ short: 'We'
41
+ },
42
+ {
43
+ index: 4,
44
+ name: 'Thursday',
45
+ short: 'Th'
46
+ },
47
+ {
48
+ index: 5,
49
+ name: 'Friday',
50
+ short: 'Fr'
51
+ },
52
+ {
53
+ index: 6,
54
+ name: 'Saturday',
55
+ short: 'Sa'
56
+ },
57
+ {
58
+ index: 0,
59
+ name: 'Sunday',
60
+ short: 'Su'
61
+ }
62
+ ];
63
+ const FULL_NAMES = [
64
+ 'sunday',
65
+ 'monday',
66
+ 'tuesday',
67
+ 'wednesday',
68
+ 'thursday',
69
+ 'friday',
70
+ 'saturday'
71
+ ];
72
+ /** An all-closed week. */ export const emptyWeeklyHours = ()=>Array.from({
73
+ length: 7
74
+ }, ()=>[]);
75
+ /** `540` → `09:00`. */ export const formatClockMinutes = (minutes)=>`${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
76
+ /** `540` → `9:00 AM`, for a menu. */ export const clockLabel = (minutes)=>{
77
+ const hours = Math.floor(minutes / 60);
78
+ const suffix = hours < 12 ? 'AM' : 'PM';
79
+ const twelve = hours % 12 === 0 ? 12 : hours % 12;
80
+ return `${twelve}:${String(minutes % 60).padStart(2, '0')} ${suffix}`;
81
+ };
82
+ /** A day token by its shortest unambiguous prefix (`Mo`, `Mon`, `Monday`). */ const dayIndex = (token)=>{
83
+ const wanted = token.trim().toLowerCase();
84
+ if (wanted.length < 2) return -1;
85
+ return FULL_NAMES.findIndex((day)=>day.startsWith(wanted));
86
+ };
87
+ const clock = (hours, minutes)=>{
88
+ const h = Number(hours);
89
+ const m = Number(minutes);
90
+ return h > 23 || m > 59 ? -1 : h * 60 + m;
91
+ };
92
+ const DASH = /\s*[-–—]\s*/;
93
+ const LINE = /^(.+?)\s+(\d{1,2}):(\d{2})\s*[-–—]\s*(\d{1,2}):(\d{2})$/;
94
+ /** Monday-first position, so a range reads the way a week is written. */ const mondayFirst = (index)=>(index + 6) % 7;
95
+ function parseLine(line) {
96
+ const match = LINE.exec(line.trim());
97
+ if (!match) return null;
98
+ const start = clock(match[2], match[3]);
99
+ const end = clock(match[4], match[5]);
100
+ if (start < 0 || end < 0) return null;
101
+ const days = [];
102
+ for (const part of match[1].split(',')){
103
+ const ends = part.trim().split(DASH);
104
+ if (ends.length > 2) return null;
105
+ const from = dayIndex(ends[0]);
106
+ const to = dayIndex(ends[ends.length - 1]);
107
+ if (from < 0 || to < 0) return null;
108
+ // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.
109
+ const span = (mondayFirst(to) - mondayFirst(from) + 7) % 7;
110
+ for(let step = 0; step <= span; step++){
111
+ const day = (from + step) % 7;
112
+ if (!days.includes(day)) days.push(day);
113
+ }
114
+ }
115
+ return days.length ? {
116
+ days,
117
+ interval: {
118
+ start,
119
+ end
120
+ }
121
+ } : null;
122
+ }
123
+ const sortIntervals = (intervals)=>[
124
+ ...intervals
125
+ ].sort((a, b)=>a.start - b.start || a.end - b.end);
126
+ /**
127
+ * Hours text → the picker's week. Lines that do not read are returned by
128
+ * their 1-based number, so the picker can say what it could not show rather
129
+ * than silently dropping a saved opening time.
130
+ */ export function weeklyHoursFromText(value) {
131
+ const hours = emptyWeeklyHours();
132
+ const unreadLines = [];
133
+ const lines = typeof value === 'string' ? value.split(/\r?\n/) : [];
134
+ lines.forEach((line, index)=>{
135
+ if (!line.trim()) return;
136
+ const row = parseLine(line);
137
+ if (!row) {
138
+ unreadLines.push(index + 1);
139
+ return;
140
+ }
141
+ for (const day of row.days){
142
+ const exists = hours[day].some((interval)=>interval.start === row.interval.start && interval.end === row.interval.end);
143
+ if (!exists) hours[day].push(_extends({}, row.interval));
144
+ }
145
+ });
146
+ return {
147
+ hours: hours.map(sortIntervals),
148
+ unreadLines
149
+ };
150
+ }
151
+ /** Monday-first day indexes → `Mo-Fr`, `Mo,We,Fr`, `Mo-We,Sa`. */ function dayList(days) {
152
+ const positions = days.map(mondayFirst).sort((a, b)=>a - b);
153
+ const runs = [];
154
+ for (const position of positions){
155
+ const last = runs[runs.length - 1];
156
+ if (last && last[last.length - 1] === position - 1) last.push(position);
157
+ else runs.push([
158
+ position
159
+ ]);
160
+ }
161
+ const short = (position)=>WEEKLY_HOURS_DAYS[position].short;
162
+ return runs.flatMap((run)=>run.length >= 3 ? [
163
+ `${short(run[0])}-${short(run[run.length - 1])}`
164
+ ] : run.map(short)).join(',');
165
+ }
166
+ /**
167
+ * The picker's week → hours text, one line per interval per set of days that
168
+ * share it: days with the same hours are written together (`Mo-Fr`), which is
169
+ * how a business reads its own hours back.
170
+ */ export function weeklyHoursToText(hours) {
171
+ const groups = new Map();
172
+ WEEKLY_HOURS_DAYS.forEach(({ index })=>{
173
+ var _hours_index;
174
+ for (const interval of sortIntervals((_hours_index = hours[index]) != null ? _hours_index : [])){
175
+ const key = `${interval.start}-${interval.end}`;
176
+ const group = groups.get(key);
177
+ if (group) group.days.push(index);
178
+ else groups.set(key, {
179
+ days: [
180
+ index
181
+ ],
182
+ interval
183
+ });
184
+ }
185
+ });
186
+ return [
187
+ ...groups.values()
188
+ ].sort((a, b)=>mondayFirst(a.days[0]) - mondayFirst(b.days[0]) || a.interval.start - b.interval.start).map(({ days, interval })=>`${dayList(days)} ${formatClockMinutes(interval.start)}-${formatClockMinutes(interval.end)}`).join('\n');
189
+ }
190
+
191
+ //# sourceMappingURL=weekly-hours.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/utils/weekly-hours.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The model behind the weekly hours picker: seven days, each a list of open\n * intervals in minutes since midnight. Index 0 is Sunday, as `Date#getDay`\n * counts, so a booking service's `windows[weekday]` maps onto it as is.\n *\n * Opening hours, pickup hours and delivery windows are STORED as text lines\n * in `schema.org`'s `openingHours` syntax (`Mo-Fr 09:00-17:00`), which the\n * structured-data serializer and checkout read. The picker writes those same\n * lines, so nothing downstream changes; nobody types them.\n */\n\nexport interface WeeklyHoursInterval {\n /** Minutes since midnight. */\n start: number\n /** Minutes since midnight. Earlier than `start` means the next morning. */\n end: number\n}\n\n/** Seven entries, Sunday first; an empty list is a closed day. */\nexport type WeeklyHours = WeeklyHoursInterval[][]\n\n/** Display order and names, Monday first, with each day's index. */\nexport const WEEKLY_HOURS_DAYS: ReadonlyArray<{ index: number; name: string; short: string }> = [\n { index: 1, name: 'Monday', short: 'Mo' },\n { index: 2, name: 'Tuesday', short: 'Tu' },\n { index: 3, name: 'Wednesday', short: 'We' },\n { index: 4, name: 'Thursday', short: 'Th' },\n { index: 5, name: 'Friday', short: 'Fr' },\n { index: 6, name: 'Saturday', short: 'Sa' },\n { index: 0, name: 'Sunday', short: 'Su' },\n]\n\nconst FULL_NAMES = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']\n\n/** An all-closed week. */\nexport const emptyWeeklyHours = (): WeeklyHours => Array.from({ length: 7 }, () => [])\n\n/** `540` → `09:00`. */\nexport const formatClockMinutes = (minutes: number): string =>\n `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`\n\n/** `540` → `9:00 AM`, for a menu. */\nexport const clockLabel = (minutes: number): string => {\n const hours = Math.floor(minutes / 60)\n const suffix = hours < 12 ? 'AM' : 'PM'\n const twelve = hours % 12 === 0 ? 12 : hours % 12\n return `${twelve}:${String(minutes % 60).padStart(2, '0')} ${suffix}`\n}\n\n/** A day token by its shortest unambiguous prefix (`Mo`, `Mon`, `Monday`). */\nconst dayIndex = (token: string): number => {\n const wanted = token.trim().toLowerCase()\n if (wanted.length < 2) return -1\n return FULL_NAMES.findIndex((day) => day.startsWith(wanted))\n}\n\nconst clock = (hours: string, minutes: string): number => {\n const h = Number(hours)\n const m = Number(minutes)\n return h > 23 || m > 59 ? -1 : h * 60 + m\n}\n\nconst DASH = /\\s*[-–—]\\s*/\nconst LINE = /^(.+?)\\s+(\\d{1,2}):(\\d{2})\\s*[-–—]\\s*(\\d{1,2}):(\\d{2})$/\n\n/** Monday-first position, so a range reads the way a week is written. */\nconst mondayFirst = (index: number): number => (index + 6) % 7\n\nfunction parseLine(line: string): { days: number[]; interval: WeeklyHoursInterval } | null {\n const match = LINE.exec(line.trim())\n if (!match) return null\n const start = clock(match[2], match[3])\n const end = clock(match[4], match[5])\n if (start < 0 || end < 0) return null\n const days: number[] = []\n for (const part of match[1].split(',')) {\n const ends = part.trim().split(DASH)\n if (ends.length > 2) return null\n const from = dayIndex(ends[0])\n const to = dayIndex(ends[ends.length - 1])\n if (from < 0 || to < 0) return null\n // A range may wrap the weekend: `Fr-Mo` is Friday through Monday.\n const span = (mondayFirst(to) - mondayFirst(from) + 7) % 7\n for (let step = 0; step <= span; step++) {\n const day = (from + step) % 7\n if (!days.includes(day)) days.push(day)\n }\n }\n return days.length ? { days, interval: { start, end } } : null\n}\n\nconst sortIntervals = (intervals: WeeklyHoursInterval[]): WeeklyHoursInterval[] =>\n [...intervals].sort((a, b) => a.start - b.start || a.end - b.end)\n\n/**\n * Hours text → the picker's week. Lines that do not read are returned by\n * their 1-based number, so the picker can say what it could not show rather\n * than silently dropping a saved opening time.\n */\nexport function weeklyHoursFromText(value: unknown): { hours: WeeklyHours; unreadLines: number[] } {\n const hours = emptyWeeklyHours()\n const unreadLines: number[] = []\n const lines = typeof value === 'string' ? value.split(/\\r?\\n/) : []\n lines.forEach((line, index) => {\n if (!line.trim()) return\n const row = parseLine(line)\n if (!row) {\n unreadLines.push(index + 1)\n return\n }\n for (const day of row.days) {\n const exists = hours[day].some(\n (interval) => interval.start === row.interval.start && interval.end === row.interval.end,\n )\n if (!exists) hours[day].push({ ...row.interval })\n }\n })\n return { hours: hours.map(sortIntervals), unreadLines }\n}\n\n/** Monday-first day indexes → `Mo-Fr`, `Mo,We,Fr`, `Mo-We,Sa`. */\nfunction dayList(days: number[]): string {\n const positions = days.map(mondayFirst).sort((a, b) => a - b)\n const runs: number[][] = []\n for (const position of positions) {\n const last = runs[runs.length - 1]\n if (last && last[last.length - 1] === position - 1) last.push(position)\n else runs.push([position])\n }\n const short = (position: number) => WEEKLY_HOURS_DAYS[position].short\n return runs\n .flatMap((run) =>\n run.length >= 3 ? [`${short(run[0])}-${short(run[run.length - 1])}`] : run.map(short),\n )\n .join(',')\n}\n\n/**\n * The picker's week → hours text, one line per interval per set of days that\n * share it: days with the same hours are written together (`Mo-Fr`), which is\n * how a business reads its own hours back.\n */\nexport function weeklyHoursToText(hours: WeeklyHours): string {\n const groups = new Map<string, { days: number[]; interval: WeeklyHoursInterval }>()\n WEEKLY_HOURS_DAYS.forEach(({ index }) => {\n for (const interval of sortIntervals(hours[index] ?? [])) {\n const key = `${interval.start}-${interval.end}`\n const group = groups.get(key)\n if (group) group.days.push(index)\n else groups.set(key, { days: [index], interval })\n }\n })\n return [...groups.values()]\n .sort(\n (a, b) =>\n mondayFirst(a.days[0]) - mondayFirst(b.days[0]) || a.interval.start - b.interval.start,\n )\n .map(\n ({ days, interval }) =>\n `${dayList(days)} ${formatClockMinutes(interval.start)}-${formatClockMinutes(interval.end)}`,\n )\n .join('\\n')\n}\n"],"names":["WEEKLY_HOURS_DAYS","index","name","short","FULL_NAMES","emptyWeeklyHours","Array","from","length","formatClockMinutes","minutes","String","Math","floor","padStart","clockLabel","hours","suffix","twelve","dayIndex","token","wanted","trim","toLowerCase","findIndex","day","startsWith","clock","h","Number","m","DASH","LINE","mondayFirst","parseLine","line","match","exec","start","end","days","part","split","ends","to","span","step","includes","push","interval","sortIntervals","intervals","sort","a","b","weeklyHoursFromText","value","unreadLines","lines","forEach","row","exists","some","map","dayList","positions","runs","position","last","flatMap","run","join","weeklyHoursToText","groups","Map","key","group","get","set","values"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC;AAYD,kEAAkE,GAClE,OAAO,MAAMA,oBAAmF;IAC9F;QAAEC,OAAO;QAAGC,MAAM;QAAUC,OAAO;IAAK;IACxC;QAAEF,OAAO;QAAGC,MAAM;QAAWC,OAAO;IAAK;IACzC;QAAEF,OAAO;QAAGC,MAAM;QAAaC,OAAO;IAAK;IAC3C;QAAEF,OAAO;QAAGC,MAAM;QAAYC,OAAO;IAAK;IAC1C;QAAEF,OAAO;QAAGC,MAAM;QAAUC,OAAO;IAAK;IACxC;QAAEF,OAAO;QAAGC,MAAM;QAAYC,OAAO;IAAK;IAC1C;QAAEF,OAAO;QAAGC,MAAM;QAAUC,OAAO;IAAK;CACzC,CAAA;AAED,MAAMC,aAAa;IAAC;IAAU;IAAU;IAAW;IAAa;IAAY;IAAU;CAAW;AAEjG,wBAAwB,GACxB,OAAO,MAAMC,mBAAmB,IAAmBC,MAAMC,IAAI,CAAC;QAAEC,QAAQ;IAAE,GAAG,IAAM,EAAE,EAAC;AAEtF,qBAAqB,GACrB,OAAO,MAAMC,qBAAqB,CAACC,UACjC,GAAGC,OAAOC,KAAKC,KAAK,CAACH,UAAU,KAAKI,QAAQ,CAAC,GAAG,KAAK,CAAC,EAAEH,OAAOD,UAAU,IAAII,QAAQ,CAAC,GAAG,MAAM,CAAA;AAEjG,mCAAmC,GACnC,OAAO,MAAMC,aAAa,CAACL;IACzB,MAAMM,QAAQJ,KAAKC,KAAK,CAACH,UAAU;IACnC,MAAMO,SAASD,QAAQ,KAAK,OAAO;IACnC,MAAME,SAASF,QAAQ,OAAO,IAAI,KAAKA,QAAQ;IAC/C,OAAO,GAAGE,OAAO,CAAC,EAAEP,OAAOD,UAAU,IAAII,QAAQ,CAAC,GAAG,KAAK,CAAC,EAAEG,QAAQ;AACvE,EAAC;AAED,4EAA4E,GAC5E,MAAME,WAAW,CAACC;IAChB,MAAMC,SAASD,MAAME,IAAI,GAAGC,WAAW;IACvC,IAAIF,OAAOb,MAAM,GAAG,GAAG,OAAO,CAAC;IAC/B,OAAOJ,WAAWoB,SAAS,CAAC,CAACC,MAAQA,IAAIC,UAAU,CAACL;AACtD;AAEA,MAAMM,QAAQ,CAACX,OAAeN;IAC5B,MAAMkB,IAAIC,OAAOb;IACjB,MAAMc,IAAID,OAAOnB;IACjB,OAAOkB,IAAI,MAAME,IAAI,KAAK,CAAC,IAAIF,IAAI,KAAKE;AAC1C;AAEA,MAAMC,OAAO;AACb,MAAMC,OAAO;AAEb,uEAAuE,GACvE,MAAMC,cAAc,CAAChC,QAA0B,AAACA,CAAAA,QAAQ,CAAA,IAAK;AAE7D,SAASiC,UAAUC,IAAY;IAC7B,MAAMC,QAAQJ,KAAKK,IAAI,CAACF,KAAKb,IAAI;IACjC,IAAI,CAACc,OAAO,OAAO;IACnB,MAAME,QAAQX,MAAMS,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE;IACtC,MAAMG,MAAMZ,MAAMS,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE;IACpC,IAAIE,QAAQ,KAAKC,MAAM,GAAG,OAAO;IACjC,MAAMC,OAAiB,EAAE;IACzB,KAAK,MAAMC,QAAQL,KAAK,CAAC,EAAE,CAACM,KAAK,CAAC,KAAM;QACtC,MAAMC,OAAOF,KAAKnB,IAAI,GAAGoB,KAAK,CAACX;QAC/B,IAAIY,KAAKnC,MAAM,GAAG,GAAG,OAAO;QAC5B,MAAMD,OAAOY,SAASwB,IAAI,CAAC,EAAE;QAC7B,MAAMC,KAAKzB,SAASwB,IAAI,CAACA,KAAKnC,MAAM,GAAG,EAAE;QACzC,IAAID,OAAO,KAAKqC,KAAK,GAAG,OAAO;QAC/B,kEAAkE;QAClE,MAAMC,OAAO,AAACZ,CAAAA,YAAYW,MAAMX,YAAY1B,QAAQ,CAAA,IAAK;QACzD,IAAK,IAAIuC,OAAO,GAAGA,QAAQD,MAAMC,OAAQ;YACvC,MAAMrB,MAAM,AAAClB,CAAAA,OAAOuC,IAAG,IAAK;YAC5B,IAAI,CAACN,KAAKO,QAAQ,CAACtB,MAAMe,KAAKQ,IAAI,CAACvB;QACrC;IACF;IACA,OAAOe,KAAKhC,MAAM,GAAG;QAAEgC;QAAMS,UAAU;YAAEX;YAAOC;QAAI;IAAE,IAAI;AAC5D;AAEA,MAAMW,gBAAgB,CAACC,YACrB;WAAIA;KAAU,CAACC,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEf,KAAK,GAAGgB,EAAEhB,KAAK,IAAIe,EAAEd,GAAG,GAAGe,EAAEf,GAAG;AAElE;;;;CAIC,GACD,OAAO,SAASgB,oBAAoBC,KAAc;IAChD,MAAMxC,QAAQX;IACd,MAAMoD,cAAwB,EAAE;IAChC,MAAMC,QAAQ,OAAOF,UAAU,WAAWA,MAAMd,KAAK,CAAC,WAAW,EAAE;IACnEgB,MAAMC,OAAO,CAAC,CAACxB,MAAMlC;QACnB,IAAI,CAACkC,KAAKb,IAAI,IAAI;QAClB,MAAMsC,MAAM1B,UAAUC;QACtB,IAAI,CAACyB,KAAK;YACRH,YAAYT,IAAI,CAAC/C,QAAQ;YACzB;QACF;QACA,KAAK,MAAMwB,OAAOmC,IAAIpB,IAAI,CAAE;YAC1B,MAAMqB,SAAS7C,KAAK,CAACS,IAAI,CAACqC,IAAI,CAC5B,CAACb,WAAaA,SAASX,KAAK,KAAKsB,IAAIX,QAAQ,CAACX,KAAK,IAAIW,SAASV,GAAG,KAAKqB,IAAIX,QAAQ,CAACV,GAAG;YAE1F,IAAI,CAACsB,QAAQ7C,KAAK,CAACS,IAAI,CAACuB,IAAI,CAAC,aAAKY,IAAIX,QAAQ;QAChD;IACF;IACA,OAAO;QAAEjC,OAAOA,MAAM+C,GAAG,CAACb;QAAgBO;IAAY;AACxD;AAEA,gEAAgE,GAChE,SAASO,QAAQxB,IAAc;IAC7B,MAAMyB,YAAYzB,KAAKuB,GAAG,CAAC9B,aAAamB,IAAI,CAAC,CAACC,GAAGC,IAAMD,IAAIC;IAC3D,MAAMY,OAAmB,EAAE;IAC3B,KAAK,MAAMC,YAAYF,UAAW;QAChC,MAAMG,OAAOF,IAAI,CAACA,KAAK1D,MAAM,GAAG,EAAE;QAClC,IAAI4D,QAAQA,IAAI,CAACA,KAAK5D,MAAM,GAAG,EAAE,KAAK2D,WAAW,GAAGC,KAAKpB,IAAI,CAACmB;aACzDD,KAAKlB,IAAI,CAAC;YAACmB;SAAS;IAC3B;IACA,MAAMhE,QAAQ,CAACgE,WAAqBnE,iBAAiB,CAACmE,SAAS,CAAChE,KAAK;IACrE,OAAO+D,KACJG,OAAO,CAAC,CAACC,MACRA,IAAI9D,MAAM,IAAI,IAAI;YAAC,GAAGL,MAAMmE,GAAG,CAAC,EAAE,EAAE,CAAC,EAAEnE,MAAMmE,GAAG,CAACA,IAAI9D,MAAM,GAAG,EAAE,GAAG;SAAC,GAAG8D,IAAIP,GAAG,CAAC5D,QAEhFoE,IAAI,CAAC;AACV;AAEA;;;;CAIC,GACD,OAAO,SAASC,kBAAkBxD,KAAkB;IAClD,MAAMyD,SAAS,IAAIC;IACnB1E,kBAAkB2D,OAAO,CAAC,CAAC,EAAE1D,KAAK,EAAE;YACGe;QAArC,KAAK,MAAMiC,YAAYC,eAAclC,eAAAA,KAAK,CAACf,MAAM,YAAZe,eAAgB,EAAE,EAAG;YACxD,MAAM2D,MAAM,GAAG1B,SAASX,KAAK,CAAC,CAAC,EAAEW,SAASV,GAAG,EAAE;YAC/C,MAAMqC,QAAQH,OAAOI,GAAG,CAACF;YACzB,IAAIC,OAAOA,MAAMpC,IAAI,CAACQ,IAAI,CAAC/C;iBACtBwE,OAAOK,GAAG,CAACH,KAAK;gBAAEnC,MAAM;oBAACvC;iBAAM;gBAAEgD;YAAS;QACjD;IACF;IACA,OAAO;WAAIwB,OAAOM,MAAM;KAAG,CACxB3B,IAAI,CACH,CAACC,GAAGC,IACFrB,YAAYoB,EAAEb,IAAI,CAAC,EAAE,IAAIP,YAAYqB,EAAEd,IAAI,CAAC,EAAE,KAAKa,EAAEJ,QAAQ,CAACX,KAAK,GAAGgB,EAAEL,QAAQ,CAACX,KAAK,EAEzFyB,GAAG,CACF,CAAC,EAAEvB,IAAI,EAAES,QAAQ,EAAE,GACjB,GAAGe,QAAQxB,MAAM,CAAC,EAAE/B,mBAAmBwC,SAASX,KAAK,EAAE,CAAC,EAAE7B,mBAAmBwC,SAASV,GAAG,GAAG,EAE/FgC,IAAI,CAAC;AACV"}