tempest-react-sdk 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +35 -5
  2. package/dist/audio/audio-bus.cjs +2 -0
  3. package/dist/audio/audio-bus.cjs.map +1 -0
  4. package/dist/audio/audio-bus.js +116 -0
  5. package/dist/audio/audio-bus.js.map +1 -0
  6. package/dist/audio/use-audio-bus.cjs +2 -0
  7. package/dist/audio/use-audio-bus.cjs.map +1 -0
  8. package/dist/audio/use-audio-bus.js +11 -0
  9. package/dist/audio/use-audio-bus.js.map +1 -0
  10. package/dist/auth/refresh-queue.cjs +1 -1
  11. package/dist/auth/refresh-queue.cjs.map +1 -1
  12. package/dist/auth/refresh-queue.js +6 -6
  13. package/dist/auth/refresh-queue.js.map +1 -1
  14. package/dist/components/RangeSlider/RangeSlider.cjs +1 -1
  15. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  16. package/dist/components/RangeSlider/RangeSlider.js +23 -23
  17. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  18. package/dist/components/Scheduler/Scheduler.cjs +1 -1
  19. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  20. package/dist/components/Scheduler/Scheduler.js +1 -1
  21. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  22. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -1
  23. package/dist/components/Scheduler/Scheduler.module.js.map +1 -1
  24. package/dist/components/Slider/Slider.cjs +1 -1
  25. package/dist/components/Slider/Slider.cjs.map +1 -1
  26. package/dist/components/Slider/Slider.js +9 -9
  27. package/dist/components/Slider/Slider.js.map +1 -1
  28. package/dist/forms/FormField.cjs +1 -1
  29. package/dist/forms/FormField.cjs.map +1 -1
  30. package/dist/forms/FormField.js +23 -21
  31. package/dist/forms/FormField.js.map +1 -1
  32. package/dist/http/describe-api-error.cjs +1 -1
  33. package/dist/http/describe-api-error.cjs.map +1 -1
  34. package/dist/http/describe-api-error.js +4 -2
  35. package/dist/http/describe-api-error.js.map +1 -1
  36. package/dist/http/use-describe-api-error.cjs +1 -1
  37. package/dist/http/use-describe-api-error.cjs.map +1 -1
  38. package/dist/http/use-describe-api-error.js +3 -2
  39. package/dist/http/use-describe-api-error.js.map +1 -1
  40. package/dist/offline/create-offline-database.cjs +2 -0
  41. package/dist/offline/create-offline-database.cjs.map +1 -0
  42. package/dist/offline/create-offline-database.js +29 -0
  43. package/dist/offline/create-offline-database.js.map +1 -0
  44. package/dist/offline/create-offline-store.cjs +1 -1
  45. package/dist/offline/create-offline-store.cjs.map +1 -1
  46. package/dist/offline/create-offline-store.js +29 -25
  47. package/dist/offline/create-offline-store.js.map +1 -1
  48. package/dist/query/QueryProvider.cjs +1 -1
  49. package/dist/query/QueryProvider.cjs.map +1 -1
  50. package/dist/query/QueryProvider.js +13 -11
  51. package/dist/query/QueryProvider.js.map +1 -1
  52. package/dist/query/foreign-client-warning.cjs +2 -0
  53. package/dist/query/foreign-client-warning.cjs.map +1 -0
  54. package/dist/query/foreign-client-warning.js +12 -0
  55. package/dist/query/foreign-client-warning.js.map +1 -0
  56. package/dist/tempest-react-sdk.cjs +1 -1
  57. package/dist/tempest-react-sdk.d.ts +695 -41
  58. package/dist/tempest-react-sdk.js +52 -46
  59. package/dist/utils/duplicate-instance.cjs +2 -0
  60. package/dist/utils/duplicate-instance.cjs.map +1 -0
  61. package/dist/utils/duplicate-instance.js +6 -0
  62. package/dist/utils/duplicate-instance.js.map +1 -0
  63. package/dist/utils/format.cjs +1 -1
  64. package/dist/utils/format.cjs.map +1 -1
  65. package/dist/utils/format.js +8 -3
  66. package/dist/utils/format.js.map +1 -1
  67. package/dist/webrtc/opus-sdp.cjs +3 -0
  68. package/dist/webrtc/opus-sdp.cjs.map +1 -0
  69. package/dist/webrtc/opus-sdp.js +94 -0
  70. package/dist/webrtc/opus-sdp.js.map +1 -0
  71. package/dist/webrtc/sender-bitrate.cjs +2 -0
  72. package/dist/webrtc/sender-bitrate.cjs.map +1 -0
  73. package/dist/webrtc/sender-bitrate.js +15 -0
  74. package/dist/webrtc/sender-bitrate.js.map +1 -0
  75. package/dist/ws/create-web-socket.cjs +1 -1
  76. package/dist/ws/create-web-socket.cjs.map +1 -1
  77. package/dist/ws/create-web-socket.js +123 -47
  78. package/dist/ws/create-web-socket.js.map +1 -1
  79. package/dist/ws/resilience.cjs +2 -0
  80. package/dist/ws/resilience.cjs.map +1 -0
  81. package/dist/ws/resilience.js +21 -0
  82. package/dist/ws/resilience.js.map +1 -0
  83. package/dist/ws/use-web-socket.cjs +1 -1
  84. package/dist/ws/use-web-socket.cjs.map +1 -1
  85. package/dist/ws/use-web-socket.js +37 -23
  86. package/dist/ws/use-web-socket.js.map +1 -1
  87. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./scheduler-layout.cjs"),n=require("./Scheduler.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=6e4;function o({events:o,anchor:s,days:c=7,startHour:l=8,endHour:u=20,snapMinutes:d=30,onEventClick:f,onSlotClick:p,renderEvent:m,locale:h=`pt-BR`,showCurrentTime:g=!0,now:_,className:v,...y}){let[b,x]=(0,r.useState)(()=>_??new Date);(0,r.useEffect)(()=>{if(_||!g)return;let e=setInterval(()=>x(new Date),a);return()=>clearInterval(e)},[_,g]);let S=_??b,C=(0,r.useMemo)(()=>({startMinute:l*60,endMinute:u*60}),[l,u]),w=(s??S).toDateString(),T=(0,r.useMemo)(()=>t.dayRange(new Date(w),c),[w,c]),E=(0,r.useMemo)(()=>t.layoutEvents({events:o,days:T,window:C}),[o,T,C]),D=(0,r.useMemo)(()=>t.layoutAllDay({events:o,days:T}),[o,T]),O=(0,r.useMemo)(()=>t.hourMarks(C),[C]),k=new Intl.DateTimeFormat(h,{weekday:`short`,day:`numeric`}),A=new Intl.DateTimeFormat(h,{hour:`2-digit`,minute:`2-digit`}),j=(e,t)=>{let n=t.currentTarget.getBoundingClientRect(),r=n.height>0?(t.clientY-n.top)/n.height:0,i=C.startMinute+r*(C.endMinute-C.startMinute),a=Math.round(i/d)*d,o=Math.max(C.startMinute,Math.min(C.endMinute,a)),s=new Date(e);return s.setHours(0,o,0,0),s},M=g?t.fractionOfWindow(S,C):null,N=T.findIndex(e=>t.isSameDay(e,S));return(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,v),...y,children:[(0,i.jsxs)(`div`,{className:n.default.head,style:{"--tempest-scheduler-days":c},children:[(0,i.jsx)(`span`,{className:n.default.gutterHead}),T.map((t,r)=>(0,i.jsx)(`span`,{className:e.cn(n.default.dayHead,r===N&&n.default.today),children:k.format(t)},t.toISOString()))]}),D.length>0&&(0,i.jsxs)(`div`,{className:n.default.allDayLane,style:{"--tempest-scheduler-days":c},children:[(0,i.jsx)(`span`,{className:n.default.gutterHead,children:`Dia inteiro`}),(0,i.jsx)(`div`,{className:n.default.allDayTrack,children:D.map(({event:e,dayIndex:t,span:r})=>(0,i.jsx)(`button`,{type:`button`,className:n.default.allDayEvent,style:{gridColumn:`${t+1} / span ${r}`},onClick:()=>f?.(e),disabled:!f,children:e.title},e.id))})]}),(0,i.jsxs)(`div`,{className:n.default.body,style:{"--tempest-scheduler-days":c},tabIndex:0,role:`group`,"aria-label":`Grade de horários`,children:[(0,i.jsx)(`div`,{className:n.default.gutter,children:O.map(e=>(0,i.jsx)(`span`,{className:n.default.hourLabel,style:{top:`${(e-C.startMinute)/(C.endMinute-C.startMinute)*100}%`},children:A.format(new Date(2026,0,1,Math.floor(e/60),e%60))},e))}),(0,i.jsxs)(`div`,{className:n.default.grid,"aria-label":`Agenda`,children:[T.map((t,r)=>(0,i.jsx)(`div`,{className:e.cn(n.default.dayColumn,r===N&&n.default.todayColumn),role:`group`,"aria-label":k.format(t),onClick:p?e=>{e.target===e.currentTarget&&p(j(t,e))}:void 0,children:O.map(e=>(0,i.jsx)(`span`,{className:n.default.hourLine,style:{top:`${(e-C.startMinute)/(C.endMinute-C.startMinute)*100}%`}},e))},t.toISOString())),E.map(e=>(0,i.jsx)(`button`,{type:`button`,className:n.default.event,style:{gridColumn:e.dayIndex+1,top:`${e.top*100}%`,height:`${e.height*100}%`,left:`${e.column/e.columns*100}%`,width:`${1/e.columns*100}%`},onClick:()=>f?.(e.event),disabled:!f,title:`${e.event.title} — ${A.format(e.event.start)}`,children:m?m(e.event):(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(`span`,{className:n.default.eventTime,children:A.format(e.event.start)}),(0,i.jsx)(`span`,{className:n.default.eventTitle,children:e.event.title})]})},`${e.event.id}-${e.dayIndex}`)),M!==null&&N>=0&&(0,i.jsx)(`span`,{className:n.default.nowLine,style:{top:`${M*100}%`},"aria-hidden":!0,"data-testid":`scheduler-now`})]})]})]})}exports.Scheduler=o;
1
+ const e=require("../../utils/cn.cjs"),t=require("./scheduler-layout.cjs"),n=require("./Scheduler.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=6e4;function o({events:o,anchor:s,days:c=7,startHour:l=8,endHour:u=20,snapMinutes:d=30,onEventClick:f,onSlotClick:p,renderEvent:m,locale:h=`pt-BR`,showCurrentTime:g=!0,now:_,className:v,...y}){let[b,x]=(0,r.useState)(()=>_??new Date);(0,r.useEffect)(()=>{if(_||!g)return;let e=setInterval(()=>x(new Date),a);return()=>clearInterval(e)},[_,g]);let S=_??b,C=(0,r.useMemo)(()=>({startMinute:l*60,endMinute:u*60}),[l,u]),w=(s??S).toDateString(),T=(0,r.useMemo)(()=>t.dayRange(new Date(w),c),[w,c]),E=(0,r.useMemo)(()=>t.layoutEvents({events:o,days:T,window:C}),[o,T,C]),D=(0,r.useMemo)(()=>t.layoutAllDay({events:o,days:T}),[o,T]),O=(0,r.useMemo)(()=>t.hourMarks(C),[C]),k=new Intl.DateTimeFormat(h,{weekday:`short`,day:`numeric`}),A=new Intl.DateTimeFormat(h,{hour:`2-digit`,minute:`2-digit`}),j=(e,t)=>{let n=t.currentTarget.getBoundingClientRect(),r=n.height>0?(t.clientY-n.top)/n.height:0,i=C.startMinute+r*(C.endMinute-C.startMinute),a=Math.round(i/d)*d,o=Math.max(C.startMinute,Math.min(C.endMinute,a)),s=new Date(e);return s.setHours(0,o,0,0),s},M=g?t.fractionOfWindow(S,C):null,N=T.findIndex(e=>t.isSameDay(e,S));return(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,v),...y,children:[(0,i.jsxs)(`div`,{className:n.default.head,style:{"--tempest-scheduler-days":c},children:[(0,i.jsx)(`span`,{className:n.default.gutterHead}),T.map((t,r)=>(0,i.jsx)(`span`,{className:e.cn(n.default.dayHead,r===N&&n.default.today),children:k.format(t)},t.toISOString()))]}),D.length>0&&(0,i.jsxs)(`div`,{className:n.default.allDayLane,style:{"--tempest-scheduler-days":c},children:[(0,i.jsx)(`span`,{className:n.default.gutterHead,children:`Dia inteiro`}),(0,i.jsx)(`div`,{className:n.default.allDayTrack,children:D.map(({event:e,dayIndex:t,span:r})=>(0,i.jsx)(`button`,{type:`button`,className:n.default.allDayEvent,style:{gridColumn:`${t+1} / span ${r}`},onClick:()=>f?.(e),disabled:!f,children:e.title},e.id))})]}),(0,i.jsxs)(`div`,{className:n.default.body,style:{"--tempest-scheduler-days":c},tabIndex:0,role:`group`,"aria-label":`Grade de horários`,children:[(0,i.jsx)(`div`,{className:n.default.gutter,children:O.map(e=>(0,i.jsx)(`span`,{className:n.default.hourLabel,style:{top:`${(e-C.startMinute)/(C.endMinute-C.startMinute)*100}%`},children:A.format(new Date(2026,0,1,Math.floor(e/60),e%60))},e))}),(0,i.jsxs)(`div`,{className:n.default.grid,"aria-label":`Agenda`,children:[T.map((t,r)=>(0,i.jsx)(`div`,{className:e.cn(n.default.dayColumn,r===N&&n.default.todayColumn),role:`group`,"aria-label":k.format(t),onClick:p?e=>{e.target===e.currentTarget&&p(j(t,e))}:void 0,children:O.map(e=>(0,i.jsx)(`span`,{className:n.default.hourLine,style:{top:`${(e-C.startMinute)/(C.endMinute-C.startMinute)*100}%`}},e))},t.toISOString())),E.map(e=>(0,i.jsx)(`button`,{type:`button`,className:n.default.event,style:{gridColumn:`${e.dayIndex+1} / span 1`,top:`${e.top*100}%`,height:`${e.height*100}%`,left:`${e.column/e.columns*100}%`,width:`${1/e.columns*100}%`},onClick:()=>f?.(e.event),disabled:!f,title:`${e.event.title} — ${A.format(e.event.start)}`,children:m?m(e.event):(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(`span`,{className:n.default.eventTime,children:A.format(e.event.start)}),(0,i.jsx)(`span`,{className:n.default.eventTitle,children:e.event.title})]})},`${e.event.id}-${e.dayIndex}`)),M!==null&&N>=0&&(0,i.jsx)(`span`,{className:n.default.nowLine,style:{top:`${M*100}%`},"aria-hidden":!0,"data-testid":`scheduler-now`})]})]})]})}exports.Scheduler=o;
2
2
  //# sourceMappingURL=Scheduler.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Scheduler.cjs","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the grid is defined by\n * anchor, days, startHour, endHour and snapMinutes; the content by events,\n * renderEvent, onEventClick and onSlotClick; the reading by locale, showCurrentTime\n * and now. The body lays out overlapping events into columns, which needs the whole\n * day's events at once.\n */\nimport { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: item.dayIndex + 1,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
1
+ {"version":3,"file":"Scheduler.cjs","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the grid is defined by\n * anchor, days, startHour, endHour and snapMinutes; the content by events,\n * renderEvent, onEventClick and onSlotClick; the reading by locale, showCurrentTime\n * and now. The body lays out overlapping events into columns, which needs the whole\n * day's events at once.\n */\nimport { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: `${item.dayIndex + 1} / span 1`,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
@@ -99,7 +99,7 @@ function h({ events: h, anchor: g, days: _ = 7, startHour: v = 8, endHour: y = 2
99
99
  type: "button",
100
100
  className: s.event,
101
101
  style: {
102
- gridColumn: e.dayIndex + 1,
102
+ gridColumn: `${e.dayIndex + 1} / span 1`,
103
103
  top: `${e.top * 100}%`,
104
104
  height: `${e.height * 100}%`,
105
105
  left: `${e.column / e.columns * 100}%`,
@@ -1 +1 @@
1
- {"version":3,"file":"Scheduler.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the grid is defined by\n * anchor, days, startHour, endHour and snapMinutes; the content by events,\n * renderEvent, onEventClick and onSlotClick; the reading by locale, showCurrentTime\n * and now. The body lays out overlapping events into columns, which needs the whole\n * day's events at once.\n */\nimport { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: item.dayIndex + 1,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
1
+ {"version":3,"file":"Scheduler.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the grid is defined by\n * anchor, days, startHour, endHour and snapMinutes; the content by events,\n * renderEvent, onEventClick and onSlotClick; the reading by locale, showCurrentTime\n * and now. The body lays out overlapping events into columns, which needs the whole\n * day's events at once.\n */\nimport { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: `${item.dayIndex + 1} / span 1`,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Scheduler.module.cjs","names":[],"sources":["../../../src/components/Scheduler/Scheduler.module.css"],"sourcesContent":[".wrapper {\n /*\n * Column widths are declared once here and reused by the header, the all-day\n * lane and the grid — three separate grids that have to line up exactly, and\n * would drift the moment any of them computed its own tracks.\n */\n --tempest-scheduler-gutter: 3.5rem;\n --tempest-scheduler-height: 32rem;\n\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.head,\n.allDayLane {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) repeat(\n var(--tempest-scheduler-days),\n minmax(0, 1fr)\n );\n border-bottom: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.dayHead {\n padding: var(--tempest-space-2);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: var(--tempest-tracking-wide);\n text-transform: capitalize;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayHead.today {\n color: var(--tempest-primary);\n}\n\n.gutterHead {\n padding: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n text-align: right;\n}\n\n.allDayTrack {\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n grid-column: 2 / -1;\n gap: 2px;\n padding: 2px;\n}\n\n.allDayEvent {\n overflow: hidden;\n padding: 0 var(--tempest-space-2);\n border: none;\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.5rem;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.allDayEvent:disabled {\n cursor: default;\n}\n\n.body {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) 1fr;\n height: var(--tempest-scheduler-height);\n overflow-y: auto;\n}\n\n.gutter {\n position: relative;\n border-right: 1px solid var(--tempest-border);\n}\n\n.hourLabel {\n position: absolute;\n right: var(--tempest-space-1);\n /*\n * Nudged up by half a line so the label reads as sitting *on* its hour line\n * rather than hanging below it.\n */\n transform: translateY(-50%);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.grid {\n position: relative;\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n}\n\n.dayColumn {\n position: relative;\n grid-row: 1;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayColumn:first-child {\n border-left: none;\n}\n\n.todayColumn {\n background-color: var(--tempest-surface);\n}\n\n.hourLine {\n position: absolute;\n left: 0;\n right: 0;\n border-top: 1px solid var(--tempest-border);\n pointer-events: none;\n}\n\n/*\n * Events live in the same grid as the day columns, one row deep, positioned inside\n * their column by percentage. Being siblings of the columns rather than children is\n * what lets an event overlay the hour lines without clipping.\n */\n.event {\n position: absolute;\n grid-row: 1;\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow: hidden;\n padding: 2px var(--tempest-space-1);\n border: 1px solid var(--tempest-bg);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.event:disabled {\n cursor: default;\n}\n\n.event:focus-visible {\n outline: 2px solid var(--tempest-text);\n outline-offset: -2px;\n}\n\n/*\n * No `opacity` here, deliberately. Dimming the time to signal it as secondary drops\n * it below the AA contrast threshold against the event fill — the browser `axe` sweep\n * caught exactly that (jsdom cannot, since it does not paint). The hierarchy comes\n * from size and weight instead, which cost nothing in contrast.\n */\n.eventTime {\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.eventTitle {\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: var(--tempest-leading-snug);\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.nowLine {\n position: absolute;\n left: 0;\n right: 0;\n grid-row: 1;\n grid-column: 1 / -1;\n height: 0;\n border-top: 2px solid var(--tempest-danger, #dc2626);\n pointer-events: none;\n z-index: 1;\n}\n\n@media (max-width: 640px) {\n .wrapper {\n --tempest-scheduler-gutter: 2.75rem;\n }\n\n .dayHead {\n font-size: 0.625rem;\n padding: var(--tempest-space-1);\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"Scheduler.module.cjs","names":[],"sources":["../../../src/components/Scheduler/Scheduler.module.css"],"sourcesContent":[".wrapper {\n /*\n * Column widths are declared once here and reused by the header, the all-day\n * lane and the grid — three separate grids that have to line up exactly, and\n * would drift the moment any of them computed its own tracks.\n */\n --tempest-scheduler-gutter: 3.5rem;\n --tempest-scheduler-height: 32rem;\n\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.head,\n.allDayLane {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) repeat(\n var(--tempest-scheduler-days),\n minmax(0, 1fr)\n );\n border-bottom: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.dayHead {\n padding: var(--tempest-space-2);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: var(--tempest-tracking-wide);\n text-transform: capitalize;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayHead.today {\n color: var(--tempest-primary);\n}\n\n.gutterHead {\n padding: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n text-align: right;\n}\n\n.allDayTrack {\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n grid-column: 2 / -1;\n gap: 2px;\n padding: 2px;\n}\n\n.allDayEvent {\n overflow: hidden;\n padding: 0 var(--tempest-space-2);\n border: none;\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.5rem;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.allDayEvent:disabled {\n cursor: default;\n}\n\n.body {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) 1fr;\n height: var(--tempest-scheduler-height);\n overflow-y: auto;\n}\n\n.gutter {\n position: relative;\n border-right: 1px solid var(--tempest-border);\n}\n\n.hourLabel {\n position: absolute;\n right: var(--tempest-space-1);\n /*\n * Nudged up by half a line so the label reads as sitting *on* its hour line\n * rather than hanging below it.\n */\n transform: translateY(-50%);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.grid {\n position: relative;\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n}\n\n.dayColumn {\n position: relative;\n grid-row: 1;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayColumn:first-child {\n border-left: none;\n}\n\n.todayColumn {\n background-color: var(--tempest-surface);\n}\n\n.hourLine {\n position: absolute;\n left: 0;\n right: 0;\n border-top: 1px solid var(--tempest-border);\n pointer-events: none;\n}\n\n/*\n * Events live in the same grid as the day columns, one row deep, positioned inside\n * their column by percentage. Being siblings of the columns rather than children is\n * what lets an event overlay the hour lines without clipping.\n */\n/*\n * Absolutely positioned, so the inline `grid-column` it is placed with must name\n * BOTH lines. For an abspos child of a grid container an `auto` side does not\n * mean \"span 1\" — CSS Grid §9.2 resolves it to the container's padding edge, so\n * a bare `grid-column: 3` runs from column 3 to the end of the week and every\n * event covers the whole view. A one-day scheduler hides it: there, \"column 1 to\n * the edge\" happens to be one column. Hence `N / span 1` in Scheduler.tsx.\n */\n.event {\n position: absolute;\n grid-row: 1;\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow: hidden;\n padding: 2px var(--tempest-space-1);\n border: 1px solid var(--tempest-bg);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.event:disabled {\n cursor: default;\n}\n\n.event:focus-visible {\n outline: 2px solid var(--tempest-text);\n outline-offset: -2px;\n}\n\n/*\n * No `opacity` here, deliberately. Dimming the time to signal it as secondary drops\n * it below the AA contrast threshold against the event fill — the browser `axe` sweep\n * caught exactly that (jsdom cannot, since it does not paint). The hierarchy comes\n * from size and weight instead, which cost nothing in contrast.\n */\n.eventTime {\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.eventTitle {\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: var(--tempest-leading-snug);\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.nowLine {\n position: absolute;\n left: 0;\n right: 0;\n grid-row: 1;\n grid-column: 1 / -1;\n height: 0;\n border-top: 2px solid var(--tempest-danger, #dc2626);\n pointer-events: none;\n z-index: 1;\n}\n\n@media (max-width: 640px) {\n .wrapper {\n --tempest-scheduler-gutter: 2.75rem;\n }\n\n .dayHead {\n font-size: 0.625rem;\n padding: var(--tempest-space-1);\n }\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"Scheduler.module.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.module.css"],"sourcesContent":[".wrapper {\n /*\n * Column widths are declared once here and reused by the header, the all-day\n * lane and the grid — three separate grids that have to line up exactly, and\n * would drift the moment any of them computed its own tracks.\n */\n --tempest-scheduler-gutter: 3.5rem;\n --tempest-scheduler-height: 32rem;\n\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.head,\n.allDayLane {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) repeat(\n var(--tempest-scheduler-days),\n minmax(0, 1fr)\n );\n border-bottom: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.dayHead {\n padding: var(--tempest-space-2);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: var(--tempest-tracking-wide);\n text-transform: capitalize;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayHead.today {\n color: var(--tempest-primary);\n}\n\n.gutterHead {\n padding: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n text-align: right;\n}\n\n.allDayTrack {\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n grid-column: 2 / -1;\n gap: 2px;\n padding: 2px;\n}\n\n.allDayEvent {\n overflow: hidden;\n padding: 0 var(--tempest-space-2);\n border: none;\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.5rem;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.allDayEvent:disabled {\n cursor: default;\n}\n\n.body {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) 1fr;\n height: var(--tempest-scheduler-height);\n overflow-y: auto;\n}\n\n.gutter {\n position: relative;\n border-right: 1px solid var(--tempest-border);\n}\n\n.hourLabel {\n position: absolute;\n right: var(--tempest-space-1);\n /*\n * Nudged up by half a line so the label reads as sitting *on* its hour line\n * rather than hanging below it.\n */\n transform: translateY(-50%);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.grid {\n position: relative;\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n}\n\n.dayColumn {\n position: relative;\n grid-row: 1;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayColumn:first-child {\n border-left: none;\n}\n\n.todayColumn {\n background-color: var(--tempest-surface);\n}\n\n.hourLine {\n position: absolute;\n left: 0;\n right: 0;\n border-top: 1px solid var(--tempest-border);\n pointer-events: none;\n}\n\n/*\n * Events live in the same grid as the day columns, one row deep, positioned inside\n * their column by percentage. Being siblings of the columns rather than children is\n * what lets an event overlay the hour lines without clipping.\n */\n.event {\n position: absolute;\n grid-row: 1;\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow: hidden;\n padding: 2px var(--tempest-space-1);\n border: 1px solid var(--tempest-bg);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.event:disabled {\n cursor: default;\n}\n\n.event:focus-visible {\n outline: 2px solid var(--tempest-text);\n outline-offset: -2px;\n}\n\n/*\n * No `opacity` here, deliberately. Dimming the time to signal it as secondary drops\n * it below the AA contrast threshold against the event fill — the browser `axe` sweep\n * caught exactly that (jsdom cannot, since it does not paint). The hierarchy comes\n * from size and weight instead, which cost nothing in contrast.\n */\n.eventTime {\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.eventTitle {\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: var(--tempest-leading-snug);\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.nowLine {\n position: absolute;\n left: 0;\n right: 0;\n grid-row: 1;\n grid-column: 1 / -1;\n height: 0;\n border-top: 2px solid var(--tempest-danger, #dc2626);\n pointer-events: none;\n z-index: 1;\n}\n\n@media (max-width: 640px) {\n .wrapper {\n --tempest-scheduler-gutter: 2.75rem;\n }\n\n .dayHead {\n font-size: 0.625rem;\n padding: var(--tempest-space-1);\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"Scheduler.module.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.module.css"],"sourcesContent":[".wrapper {\n /*\n * Column widths are declared once here and reused by the header, the all-day\n * lane and the grid — three separate grids that have to line up exactly, and\n * would drift the moment any of them computed its own tracks.\n */\n --tempest-scheduler-gutter: 3.5rem;\n --tempest-scheduler-height: 32rem;\n\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.head,\n.allDayLane {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) repeat(\n var(--tempest-scheduler-days),\n minmax(0, 1fr)\n );\n border-bottom: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.dayHead {\n padding: var(--tempest-space-2);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: var(--tempest-tracking-wide);\n text-transform: capitalize;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayHead.today {\n color: var(--tempest-primary);\n}\n\n.gutterHead {\n padding: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n text-align: right;\n}\n\n.allDayTrack {\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n grid-column: 2 / -1;\n gap: 2px;\n padding: 2px;\n}\n\n.allDayEvent {\n overflow: hidden;\n padding: 0 var(--tempest-space-2);\n border: none;\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.5rem;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.allDayEvent:disabled {\n cursor: default;\n}\n\n.body {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) 1fr;\n height: var(--tempest-scheduler-height);\n overflow-y: auto;\n}\n\n.gutter {\n position: relative;\n border-right: 1px solid var(--tempest-border);\n}\n\n.hourLabel {\n position: absolute;\n right: var(--tempest-space-1);\n /*\n * Nudged up by half a line so the label reads as sitting *on* its hour line\n * rather than hanging below it.\n */\n transform: translateY(-50%);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.grid {\n position: relative;\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n}\n\n.dayColumn {\n position: relative;\n grid-row: 1;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayColumn:first-child {\n border-left: none;\n}\n\n.todayColumn {\n background-color: var(--tempest-surface);\n}\n\n.hourLine {\n position: absolute;\n left: 0;\n right: 0;\n border-top: 1px solid var(--tempest-border);\n pointer-events: none;\n}\n\n/*\n * Events live in the same grid as the day columns, one row deep, positioned inside\n * their column by percentage. Being siblings of the columns rather than children is\n * what lets an event overlay the hour lines without clipping.\n */\n/*\n * Absolutely positioned, so the inline `grid-column` it is placed with must name\n * BOTH lines. For an abspos child of a grid container an `auto` side does not\n * mean \"span 1\" — CSS Grid §9.2 resolves it to the container's padding edge, so\n * a bare `grid-column: 3` runs from column 3 to the end of the week and every\n * event covers the whole view. A one-day scheduler hides it: there, \"column 1 to\n * the edge\" happens to be one column. Hence `N / span 1` in Scheduler.tsx.\n */\n.event {\n position: absolute;\n grid-row: 1;\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow: hidden;\n padding: 2px var(--tempest-space-1);\n border: 1px solid var(--tempest-bg);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.event:disabled {\n cursor: default;\n}\n\n.event:focus-visible {\n outline: 2px solid var(--tempest-text);\n outline-offset: -2px;\n}\n\n/*\n * No `opacity` here, deliberately. Dimming the time to signal it as secondary drops\n * it below the AA contrast threshold against the event fill — the browser `axe` sweep\n * caught exactly that (jsdom cannot, since it does not paint). The hierarchy comes\n * from size and weight instead, which cost nothing in contrast.\n */\n.eventTime {\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.eventTitle {\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: var(--tempest-leading-snug);\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.nowLine {\n position: absolute;\n left: 0;\n right: 0;\n grid-row: 1;\n grid-column: 1 / -1;\n height: 0;\n border-top: 2px solid var(--tempest-danger, #dc2626);\n pointer-events: none;\n z-index: 1;\n}\n\n@media (max-width: 640px) {\n .wrapper {\n --tempest-scheduler-gutter: 2.75rem;\n }\n\n .dayHead {\n font-size: 0.625rem;\n padding: var(--tempest-space-1);\n }\n}\n"],"mappings":""}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("./Slider.module.cjs");let n=require("react"),r=require("react/jsx-runtime");function i({value:i,onChange:a,min:o=0,max:s=100,step:c=1,label:l,helperText:u,disabled:d=!1,formatValue:f,className:p}){let m=(0,n.useMemo)(()=>{let e=s-o;return e<=0?0:Math.min(100,Math.max(0,(i-o)/e*100))},[i,o,s]),h=f?.(i)??String(i),g=(0,n.useCallback)(e=>{a(Number(e.target.value))},[a]);return(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,p),children:[l&&(0,r.jsxs)(`div`,{className:t.default.label,children:[(0,r.jsx)(`span`,{children:l}),(0,r.jsx)(`span`,{className:t.default.value,children:h})]}),(0,r.jsxs)(`div`,{className:t.default.field,children:[(0,r.jsx)(`div`,{className:t.default.track}),(0,r.jsx)(`div`,{className:t.default.fill,style:{width:`${m}%`}}),(0,r.jsx)(`input`,{type:`range`,className:t.default.input,min:o,max:s,step:c,value:i,onChange:g,disabled:d,"aria-label":l??`Slider`,"aria-valuetext":h})]}),u&&(0,r.jsx)(`span`,{className:t.default.helper,children:u})]})}exports.Slider=i;
1
+ const e=require("../../utils/cn.cjs"),t=require("./Slider.module.cjs");let n=require("react"),r=require("react/jsx-runtime");function i({value:i,onChange:a,min:o=0,max:s=100,step:c=1,label:l,helperText:u,disabled:d=!1,formatValue:f,"aria-label":p,className:m}){let h=(0,n.useMemo)(()=>{let e=s-o;return e<=0?0:Math.min(100,Math.max(0,(i-o)/e*100))},[i,o,s]),g=f?.(i)??String(i),_=(0,n.useCallback)(e=>{a(Number(e.target.value))},[a]);return(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,m),children:[l&&(0,r.jsxs)(`div`,{className:t.default.label,children:[(0,r.jsx)(`span`,{children:l}),(0,r.jsx)(`span`,{className:t.default.value,children:g})]}),(0,r.jsxs)(`div`,{className:t.default.field,children:[(0,r.jsx)(`div`,{className:t.default.track}),(0,r.jsx)(`div`,{className:t.default.fill,style:{width:`${h}%`}}),(0,r.jsx)(`input`,{type:`range`,className:t.default.input,min:o,max:s,step:c,value:i,onChange:_,disabled:d,"aria-label":p??l??`Slider`,"aria-valuetext":g})]}),u&&(0,r.jsx)(`span`,{className:t.default.helper,children:u})]})}exports.Slider=i;
2
2
  //# sourceMappingURL=Slider.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Slider.cjs","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — min, max, step, value, onChange, formatValue plus\n * the field chrome (label, helperText, disabled, className). Every one is used by\n * the range variant too — the pair is deliberately the same API.\n */\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Slider.module.css\";\n\nexport interface SliderProps {\n /** Current value. */\n value: number;\n /** Called with the new value on every change. */\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n /** Formatter for the value badge next to the label. Defaults to the raw number. */\n formatValue?: (value: number) => string;\n className?: string;\n}\n\n/**\n * Single-thumb slider built on a native `<input type=\"range\">`, so it stays\n * accessible (keyboard + screen reader) with no positioning libs. The active\n * fill is a percentage-width bar. For a two-thumb range, use `RangeSlider`.\n */\nexport function Slider({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n label,\n helperText,\n disabled = false,\n formatValue,\n className,\n}: SliderProps) {\n const pct = useMemo(() => {\n const range = max - min;\n if (range <= 0) return 0;\n return Math.min(100, Math.max(0, ((value - min) / range) * 100));\n }, [value, min, max]);\n\n const valueText = formatValue?.(value) ?? String(value);\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(Number(event.target.value));\n },\n [onChange],\n );\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {label && (\n <div className={styles.label}>\n <span>{label}</span>\n <span className={styles.value}>{valueText}</span>\n </div>\n )}\n <div className={styles.field}>\n <div className={styles.track} />\n <div className={styles.fill} style={{ width: `${pct}%` }} />\n <input\n type=\"range\"\n className={styles.input}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleChange}\n disabled={disabled}\n aria-label={label ?? \"Slider\"}\n aria-valuetext={valueText}\n />\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"6HA8BA,SAAgB,EAAO,CACnB,QACA,WACA,MAAM,EACN,MAAM,IACN,OAAO,EACP,QACA,aACA,WAAW,GACX,cACA,aACY,CACZ,IAAM,GAAA,EAAM,EAAA,QAAA,KAAc,CACtB,IAAM,EAAQ,EAAM,EAEpB,OADI,GAAS,EAAU,EAChB,KAAK,IAAI,IAAK,KAAK,IAAI,GAAK,EAAQ,GAAO,EAAS,GAAG,CAAC,CACnE,EAAG,CAAC,EAAO,EAAK,CAAG,CAAC,EAEd,EAAY,IAAc,CAAK,GAAK,OAAO,CAAK,EAEhD,GAAA,EAAe,EAAA,YAAA,CAChB,GAAqD,CAClD,EAAS,OAAO,EAAM,OAAO,KAAK,CAAC,CACvC,EACA,CAAC,CAAQ,CACb,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,EAA5C,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,CAAY,CAAA,GACnB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAgB,CAAA,CAC/C,KAET,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAQ,CAAA,GAC/B,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,MAAO,GAAG,EAAI,EAAG,CAAI,CAAA,GAC3D,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,QACL,UAAW,EAAA,QAAO,MACb,MACA,MACC,OACC,QACP,SAAU,EACA,WACV,aAAY,GAAS,SACrB,iBAAgB,CACnB,CAAA,CACA,IACJ,IAAc,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,CAChE,GAEb"}
1
+ {"version":3,"file":"Slider.cjs","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — min, max, step, value, onChange, formatValue plus\n * the field chrome (label, helperText, disabled, className). Every one is used by\n * the range variant too — the pair is deliberately the same API.\n */\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Slider.module.css\";\n\nexport interface SliderProps {\n /** Current value. */\n value: number;\n /** Called with the new value on every change. */\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n /** Formatter for the value badge next to the label. Defaults to the raw number. */\n formatValue?: (value: number) => string;\n /**\n * Accessible name, for when the visible `label` block does not fit.\n *\n * A slider in a one-line footer, a table cell or a toolbar has no room for the\n * label row above the track, and without this every such control announces\n * itself as \"Slider\" — several on the same screen become indistinguishable to a\n * screen reader. Takes precedence over `label`; wrapping the field in an outer\n * `<label>` does not work, because an explicit `aria-label` on the input wins\n * the accessible-name precedence order.\n */\n \"aria-label\"?: string;\n className?: string;\n}\n\n/**\n * Single-thumb slider built on a native `<input type=\"range\">`, so it stays\n * accessible (keyboard + screen reader) with no positioning libs. The active\n * fill is a percentage-width bar. For a two-thumb range, use `RangeSlider`.\n */\nexport function Slider({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n label,\n helperText,\n disabled = false,\n formatValue,\n \"aria-label\": ariaLabel,\n className,\n}: SliderProps) {\n const pct = useMemo(() => {\n const range = max - min;\n if (range <= 0) return 0;\n return Math.min(100, Math.max(0, ((value - min) / range) * 100));\n }, [value, min, max]);\n\n const valueText = formatValue?.(value) ?? String(value);\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(Number(event.target.value));\n },\n [onChange],\n );\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {label && (\n <div className={styles.label}>\n <span>{label}</span>\n <span className={styles.value}>{valueText}</span>\n </div>\n )}\n <div className={styles.field}>\n <div className={styles.track} />\n <div className={styles.fill} style={{ width: `${pct}%` }} />\n <input\n type=\"range\"\n className={styles.input}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleChange}\n disabled={disabled}\n aria-label={ariaLabel ?? label ?? \"Slider\"}\n aria-valuetext={valueText}\n />\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":"6HAyCA,SAAgB,EAAO,CACnB,QACA,WACA,MAAM,EACN,MAAM,IACN,OAAO,EACP,QACA,aACA,WAAW,GACX,cACA,aAAc,EACd,aACY,CACZ,IAAM,GAAA,EAAM,EAAA,QAAA,KAAc,CACtB,IAAM,EAAQ,EAAM,EAEpB,OADI,GAAS,EAAU,EAChB,KAAK,IAAI,IAAK,KAAK,IAAI,GAAK,EAAQ,GAAO,EAAS,GAAG,CAAC,CACnE,EAAG,CAAC,EAAO,EAAK,CAAG,CAAC,EAEd,EAAY,IAAc,CAAK,GAAK,OAAO,CAAK,EAEhD,GAAA,EAAe,EAAA,YAAA,CAChB,GAAqD,CAClD,EAAS,OAAO,EAAM,OAAO,KAAK,CAAC,CACvC,EACA,CAAC,CAAQ,CACb,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,CAAS,EAA5C,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,CAAY,CAAA,GACnB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAgB,CAAA,CAC/C,KAET,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAQ,CAAA,GAC/B,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAM,MAAO,CAAE,MAAO,GAAG,EAAI,EAAG,CAAI,CAAA,GAC3D,EAAA,EAAA,IAAA,CAAC,QAAD,CACI,KAAK,QACL,UAAW,EAAA,QAAO,MACb,MACA,MACC,OACC,QACP,SAAU,EACA,WACV,aAAY,GAAa,GAAS,SAClC,iBAAgB,CACnB,CAAA,CACA,IACJ,IAAc,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,CAChE,GAEb"}
@@ -3,25 +3,25 @@ import t from "./Slider.module.js";
3
3
  import { useCallback as n, useMemo as r } from "react";
4
4
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
5
5
  //#region src/components/Slider/Slider.tsx
6
- function o({ value: o, onChange: s, min: c = 0, max: l = 100, step: u = 1, label: d, helperText: f, disabled: p = !1, formatValue: m, className: h }) {
7
- let g = r(() => {
6
+ function o({ value: o, onChange: s, min: c = 0, max: l = 100, step: u = 1, label: d, helperText: f, disabled: p = !1, formatValue: m, "aria-label": h, className: g }) {
7
+ let _ = r(() => {
8
8
  let e = l - c;
9
9
  return e <= 0 ? 0 : Math.min(100, Math.max(0, (o - c) / e * 100));
10
10
  }, [
11
11
  o,
12
12
  c,
13
13
  l
14
- ]), _ = m?.(o) ?? String(o), v = n((e) => {
14
+ ]), v = m?.(o) ?? String(o), y = n((e) => {
15
15
  s(Number(e.target.value));
16
16
  }, [s]);
17
17
  return /* @__PURE__ */ a("div", {
18
- className: e(t.wrapper, h),
18
+ className: e(t.wrapper, g),
19
19
  children: [
20
20
  d && /* @__PURE__ */ a("div", {
21
21
  className: t.label,
22
22
  children: [/* @__PURE__ */ i("span", { children: d }), /* @__PURE__ */ i("span", {
23
23
  className: t.value,
24
- children: _
24
+ children: v
25
25
  })]
26
26
  }),
27
27
  /* @__PURE__ */ a("div", {
@@ -30,7 +30,7 @@ function o({ value: o, onChange: s, min: c = 0, max: l = 100, step: u = 1, label
30
30
  /* @__PURE__ */ i("div", { className: t.track }),
31
31
  /* @__PURE__ */ i("div", {
32
32
  className: t.fill,
33
- style: { width: `${g}%` }
33
+ style: { width: `${_}%` }
34
34
  }),
35
35
  /* @__PURE__ */ i("input", {
36
36
  type: "range",
@@ -39,10 +39,10 @@ function o({ value: o, onChange: s, min: c = 0, max: l = 100, step: u = 1, label
39
39
  max: l,
40
40
  step: u,
41
41
  value: o,
42
- onChange: v,
42
+ onChange: y,
43
43
  disabled: p,
44
- "aria-label": d ?? "Slider",
45
- "aria-valuetext": _
44
+ "aria-label": h ?? d ?? "Slider",
45
+ "aria-valuetext": v
46
46
  })
47
47
  ]
48
48
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — min, max, step, value, onChange, formatValue plus\n * the field chrome (label, helperText, disabled, className). Every one is used by\n * the range variant too — the pair is deliberately the same API.\n */\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Slider.module.css\";\n\nexport interface SliderProps {\n /** Current value. */\n value: number;\n /** Called with the new value on every change. */\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n /** Formatter for the value badge next to the label. Defaults to the raw number. */\n formatValue?: (value: number) => string;\n className?: string;\n}\n\n/**\n * Single-thumb slider built on a native `<input type=\"range\">`, so it stays\n * accessible (keyboard + screen reader) with no positioning libs. The active\n * fill is a percentage-width bar. For a two-thumb range, use `RangeSlider`.\n */\nexport function Slider({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n label,\n helperText,\n disabled = false,\n formatValue,\n className,\n}: SliderProps) {\n const pct = useMemo(() => {\n const range = max - min;\n if (range <= 0) return 0;\n return Math.min(100, Math.max(0, ((value - min) / range) * 100));\n }, [value, min, max]);\n\n const valueText = formatValue?.(value) ?? String(value);\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(Number(event.target.value));\n },\n [onChange],\n );\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {label && (\n <div className={styles.label}>\n <span>{label}</span>\n <span className={styles.value}>{valueText}</span>\n </div>\n )}\n <div className={styles.field}>\n <div className={styles.track} />\n <div className={styles.fill} style={{ width: `${pct}%` }} />\n <input\n type=\"range\"\n className={styles.input}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleChange}\n disabled={disabled}\n aria-label={label ?? \"Slider\"}\n aria-valuetext={valueText}\n />\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":";;;;;AA8BA,SAAgB,EAAO,EACnB,UACA,aACA,SAAM,GACN,SAAM,KACN,UAAO,GACP,UACA,eACA,cAAW,IACX,gBACA,gBACY;CACZ,IAAM,IAAM,QAAc;EACtB,IAAM,IAAQ,IAAM;EAEpB,OADI,KAAS,IAAU,IAChB,KAAK,IAAI,KAAK,KAAK,IAAI,IAAK,IAAQ,KAAO,IAAS,GAAG,CAAC;CACnE,GAAG;EAAC;EAAO;EAAK;CAAG,CAAC,GAEd,IAAY,IAAc,CAAK,KAAK,OAAO,CAAK,GAEhD,IAAe,GAChB,MAAqD;EAClD,EAAS,OAAO,EAAM,OAAO,KAAK,CAAC;CACvC,GACA,CAAC,CAAQ,CACb;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAA5C,UAAA;GACK,KACG,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACI,kBAAC,QAAD,EAAA,UAAO,EAAY,CAAA,GACnB,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAQ,UAAA;IAAgB,CAAA,CAC/C;;GAET,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACI,kBAAC,OAAD,EAAK,WAAW,EAAO,MAAQ,CAAA;KAC/B,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAM,OAAO,EAAE,OAAO,GAAG,EAAI,GAAG;KAAI,CAAA;KAC3D,kBAAC,SAAD;MACI,MAAK;MACL,WAAW,EAAO;MACb;MACA;MACC;MACC;MACP,UAAU;MACA;MACV,cAAY,KAAS;MACrB,kBAAgB;KACnB,CAAA;IACA;;GACJ,KAAc,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAS,UAAA;GAAiB,CAAA;EAChE;;AAEb"}
1
+ {"version":3,"file":"Slider.js","names":[],"sources":["../../../src/components/Slider/Slider.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — min, max, step, value, onChange, formatValue plus\n * the field chrome (label, helperText, disabled, className). Every one is used by\n * the range variant too — the pair is deliberately the same API.\n */\nimport { useCallback, useMemo } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Slider.module.css\";\n\nexport interface SliderProps {\n /** Current value. */\n value: number;\n /** Called with the new value on every change. */\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n label?: string;\n helperText?: string;\n disabled?: boolean;\n /** Formatter for the value badge next to the label. Defaults to the raw number. */\n formatValue?: (value: number) => string;\n /**\n * Accessible name, for when the visible `label` block does not fit.\n *\n * A slider in a one-line footer, a table cell or a toolbar has no room for the\n * label row above the track, and without this every such control announces\n * itself as \"Slider\" — several on the same screen become indistinguishable to a\n * screen reader. Takes precedence over `label`; wrapping the field in an outer\n * `<label>` does not work, because an explicit `aria-label` on the input wins\n * the accessible-name precedence order.\n */\n \"aria-label\"?: string;\n className?: string;\n}\n\n/**\n * Single-thumb slider built on a native `<input type=\"range\">`, so it stays\n * accessible (keyboard + screen reader) with no positioning libs. The active\n * fill is a percentage-width bar. For a two-thumb range, use `RangeSlider`.\n */\nexport function Slider({\n value,\n onChange,\n min = 0,\n max = 100,\n step = 1,\n label,\n helperText,\n disabled = false,\n formatValue,\n \"aria-label\": ariaLabel,\n className,\n}: SliderProps) {\n const pct = useMemo(() => {\n const range = max - min;\n if (range <= 0) return 0;\n return Math.min(100, Math.max(0, ((value - min) / range) * 100));\n }, [value, min, max]);\n\n const valueText = formatValue?.(value) ?? String(value);\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>): void => {\n onChange(Number(event.target.value));\n },\n [onChange],\n );\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {label && (\n <div className={styles.label}>\n <span>{label}</span>\n <span className={styles.value}>{valueText}</span>\n </div>\n )}\n <div className={styles.field}>\n <div className={styles.track} />\n <div className={styles.fill} style={{ width: `${pct}%` }} />\n <input\n type=\"range\"\n className={styles.input}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={handleChange}\n disabled={disabled}\n aria-label={ariaLabel ?? label ?? \"Slider\"}\n aria-valuetext={valueText}\n />\n </div>\n {helperText && <span className={styles.helper}>{helperText}</span>}\n </div>\n );\n}\n"],"mappings":";;;;;AAyCA,SAAgB,EAAO,EACnB,UACA,aACA,SAAM,GACN,SAAM,KACN,UAAO,GACP,UACA,eACA,cAAW,IACX,gBACA,cAAc,GACd,gBACY;CACZ,IAAM,IAAM,QAAc;EACtB,IAAM,IAAQ,IAAM;EAEpB,OADI,KAAS,IAAU,IAChB,KAAK,IAAI,KAAK,KAAK,IAAI,IAAK,IAAQ,KAAO,IAAS,GAAG,CAAC;CACnE,GAAG;EAAC;EAAO;EAAK;CAAG,CAAC,GAEd,IAAY,IAAc,CAAK,KAAK,OAAO,CAAK,GAEhD,IAAe,GAChB,MAAqD;EAClD,EAAS,OAAO,EAAM,OAAO,KAAK,CAAC;CACvC,GACA,CAAC,CAAQ,CACb;CAEA,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAA5C,UAAA;GACK,KACG,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACI,kBAAC,QAAD,EAAA,UAAO,EAAY,CAAA,GACnB,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAQ,UAAA;IAAgB,CAAA,CAC/C;;GAET,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA;KACI,kBAAC,OAAD,EAAK,WAAW,EAAO,MAAQ,CAAA;KAC/B,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAM,OAAO,EAAE,OAAO,GAAG,EAAI,GAAG;KAAI,CAAA;KAC3D,kBAAC,SAAD;MACI,MAAK;MACL,WAAW,EAAO;MACb;MACA;MACC;MACC;MACP,UAAU;MACA;MACV,cAAY,KAAa,KAAS;MAClC,kBAAgB;KACnB,CAAA;IACA;;GACJ,KAAc,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAS,UAAA;GAAiB,CAAA;EAChE;;AAEb"}
@@ -1,2 +1,2 @@
1
- let e=require("react"),t=require("react/jsx-runtime"),n=require("react-hook-form");function r({name:r,label:i,helperText:a,required:o,control:s,children:c}){let l=(0,n.useFormContext)(),u=s??l?.control;if(!u)throw Error("FormField requires either a `control` prop or a <FormProvider> in the tree.");return(0,t.jsx)(n.Controller,{name:r,control:u,render:({field:t,fieldState:n})=>{if(!(0,e.isValidElement)(c))return c;let r=n.error?.message;return(0,e.cloneElement)(c,{name:t.name,value:t.value,onChange:t.onChange,onBlur:t.onBlur,ref:t.ref,label:i,helperText:r?void 0:a,error:r,required:o,"aria-invalid":!!r})}})}exports.FormField=r;
1
+ const e=require("../utils/duplicate-instance.cjs");let t=require("react"),n=require("react/jsx-runtime"),r=require("react-hook-form");var i="FormField found no form to bind to. Either pass a `control` prop, or mount a <FormProvider> above it. If a <FormProvider> IS mounted above it, the app and the SDK are resolving two copies of react-hook-form: the provider publishes on one copy's context and this field reads the other's. "+e.DUPLICATE_COPY_REMEDY;function a({name:e,label:a,helperText:o,required:s,control:c,children:l}){let u=(0,r.useFormContext)(),d=c??u?.control;if(!d)throw Error(i);return(0,n.jsx)(r.Controller,{name:e,control:d,render:({field:e,fieldState:n})=>{if(!(0,t.isValidElement)(l))return l;let r=n.error?.message;return(0,t.cloneElement)(l,{name:e.name,value:e.value,onChange:e.onChange,onBlur:e.onBlur,ref:e.ref,label:a,helperText:r?void 0:o,error:r,required:s,"aria-invalid":!!r})}})}exports.FormField=a;
2
2
  //# sourceMappingURL=FormField.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"FormField.cjs","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — FormFieldChildProps is not a component's props — it\n * is the contract handed to whatever control the field wraps, and it is exactly what\n * react-hook-form's register returns (name, value, onChange, onBlur, ref) plus what\n * the field adds around it (error, label, helperText, required, id). Trimming it\n * breaks the wiring it exists to describe.\n */\nimport { cloneElement, isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport {\n Controller,\n useFormContext,\n type Control,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\n\nexport interface FormFieldChildProps {\n name: string;\n value: unknown;\n onChange: (...args: unknown[]) => void;\n onBlur: () => void;\n ref: ControllerRenderProps[\"ref\"];\n error?: string;\n \"aria-invalid\"?: boolean;\n \"aria-describedby\"?: string;\n label?: ReactNode;\n helperText?: ReactNode;\n required?: boolean;\n id?: string;\n}\n\nexport interface FormFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n> {\n /** Field name (dot path supported, e.g. `\"address.city\"`). */\n name: TName;\n /** Field label rendered by the wrapped control. */\n label?: ReactNode;\n /** Helper text rendered by the wrapped control when there is no error. */\n helperText?: ReactNode;\n /** When `true`, marks the control as required (visual hint + native attribute). */\n required?: boolean;\n /**\n * Explicit `control` from `useForm()` / `useZodForm()`. Optional when a\n * `FormProvider` is in the tree.\n */\n control?: Control<TValues>;\n /**\n * The control to render. Receives `{ value, onChange, onBlur, ref, error, ... }`\n * via `cloneElement`. Pass `<Input />`, `<Select />`, masked inputs, etc.\n */\n children: ReactElement;\n}\n\n/**\n * Glue between `react-hook-form` `Controller` and the SDK's controlled\n * components. Wraps any control that accepts `{ value, onChange, label,\n * error }` and routes RHF state into it — eliminating the per-field\n * `<Controller render={...} />` boilerplate.\n *\n * @example\n * const form = useZodForm(schema);\n * <FormProvider {...form}>\n * <Form>\n * <FormField name=\"email\" label=\"Email\" required>\n * <Input type=\"email\" />\n * </FormField>\n * <FormField name=\"cep\" label=\"CEP\">\n * <CEPInput />\n * </FormField>\n * </Form>\n * </FormProvider>;\n */\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, helperText, required, control, children }: FormFieldProps<TValues, TName>) {\n const context = useFormContext<TValues>();\n const resolvedControl = control ?? context?.control;\n if (!resolvedControl) {\n throw new Error(\n \"FormField requires either a `control` prop or a <FormProvider> in the tree.\",\n );\n }\n\n return (\n <Controller\n name={name}\n control={resolvedControl}\n render={({ field, fieldState }) => {\n if (!isValidElement(children)) return children;\n const errorMessage = fieldState.error?.message;\n return cloneElement(children as ReactElement<FormFieldChildProps>, {\n name: field.name,\n value: field.value,\n onChange: field.onChange,\n onBlur: field.onBlur,\n ref: field.ref,\n label,\n helperText: errorMessage ? undefined : helperText,\n error: errorMessage,\n required,\n \"aria-invalid\": !!errorMessage,\n });\n }}\n />\n );\n}\n"],"mappings":"mFA4EA,SAAgB,EAGd,CAAE,OAAM,QAAO,aAAY,WAAU,UAAS,YAA4C,CACxF,IAAM,GAAA,EAAU,EAAA,eAAA,CAAwB,EAClC,EAAkB,GAAW,GAAS,QAC5C,GAAI,CAAC,EACD,MAAU,MACN,6EACJ,EAGJ,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACU,OACN,QAAS,EACT,QAAS,CAAE,QAAO,gBAAiB,CAC/B,GAAI,EAAA,EAAC,EAAA,eAAA,CAAe,CAAQ,EAAG,OAAO,EACtC,IAAM,EAAe,EAAW,OAAO,QACvC,OAAA,EAAO,EAAA,aAAA,CAAa,EAA+C,CAC/D,KAAM,EAAM,KACZ,MAAO,EAAM,MACb,SAAU,EAAM,SAChB,OAAQ,EAAM,OACd,IAAK,EAAM,IACX,QACA,WAAY,EAAe,IAAA,GAAY,EACvC,MAAO,EACP,WACA,eAAgB,CAAC,CAAC,CACtB,CAAC,CACL,CACH,CAAA,CAET"}
1
+ {"version":3,"file":"FormField.cjs","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — FormFieldChildProps is not a component's props — it\n * is the contract handed to whatever control the field wraps, and it is exactly what\n * react-hook-form's register returns (name, value, onChange, onBlur, ref) plus what\n * the field adds around it (error, label, helperText, required, id). Trimming it\n * breaks the wiring it exists to describe.\n */\nimport { cloneElement, isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport {\n Controller,\n useFormContext,\n type Control,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\n\nimport { DUPLICATE_COPY_REMEDY } from \"../utils/duplicate-instance\";\n\nexport interface FormFieldChildProps {\n name: string;\n value: unknown;\n onChange: (...args: unknown[]) => void;\n onBlur: () => void;\n ref: ControllerRenderProps[\"ref\"];\n error?: string;\n \"aria-invalid\"?: boolean;\n \"aria-describedby\"?: string;\n label?: ReactNode;\n helperText?: ReactNode;\n required?: boolean;\n id?: string;\n}\n\nexport interface FormFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n> {\n /** Field name (dot path supported, e.g. `\"address.city\"`). */\n name: TName;\n /** Field label rendered by the wrapped control. */\n label?: ReactNode;\n /** Helper text rendered by the wrapped control when there is no error. */\n helperText?: ReactNode;\n /** When `true`, marks the control as required (visual hint + native attribute). */\n required?: boolean;\n /**\n * Explicit `control` from `useForm()` / `useZodForm()`. Optional when a\n * `FormProvider` is in the tree.\n */\n control?: Control<TValues>;\n /**\n * The control to render. Receives `{ value, onChange, onBlur, ref, error, ... }`\n * via `cloneElement`. Pass `<Input />`, `<Select />`, masked inputs, etc.\n */\n children: ReactElement;\n}\n\n/**\n * Glue between `react-hook-form` `Controller` and the SDK's controlled\n * components. Wraps any control that accepts `{ value, onChange, label,\n * error }` and routes RHF state into it — eliminating the per-field\n * `<Controller render={...} />` boilerplate.\n *\n * @example\n * const form = useZodForm(schema);\n * <FormProvider {...form}>\n * <Form>\n * <FormField name=\"email\" label=\"Email\" required>\n * <Input type=\"email\" />\n * </FormField>\n * <FormField name=\"cep\" label=\"CEP\">\n * <CEPInput />\n * </FormField>\n * </Form>\n * </FormProvider>;\n */\n/**\n * What is thrown when no `control` can be resolved.\n *\n * It names **two** causes, because the throw cannot tell them apart and the\n * second one is the expensive one. The obvious reading — no provider, no prop —\n * is usually right. But the identical throw happens with a `<FormProvider>`\n * mounted directly above, when the app and the SDK resolve two different copies\n * of `react-hook-form`: the provider publishes on the app copy's context and\n * `useFormContext` here reads the SDK copy's, which is empty. Someone looking at\n * a provider they can see, being told it is missing, does not go looking at\n * `node_modules` — so the message has to send them there.\n */\nconst NO_CONTROL_MESSAGE =\n \"FormField found no form to bind to. Either pass a `control` prop, or mount a \" +\n \"<FormProvider> above it. If a <FormProvider> IS mounted above it, the app and the \" +\n \"SDK are resolving two copies of react-hook-form: the provider publishes on one \" +\n \"copy's context and this field reads the other's. \" +\n DUPLICATE_COPY_REMEDY;\n\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, helperText, required, control, children }: FormFieldProps<TValues, TName>) {\n const context = useFormContext<TValues>();\n const resolvedControl = control ?? context?.control;\n if (!resolvedControl) {\n throw new Error(NO_CONTROL_MESSAGE);\n }\n\n return (\n <Controller\n name={name}\n control={resolvedControl}\n render={({ field, fieldState }) => {\n if (!isValidElement(children)) return children;\n const errorMessage = fieldState.error?.message;\n return cloneElement(children as ReactElement<FormFieldChildProps>, {\n name: field.name,\n value: field.value,\n onChange: field.onChange,\n onBlur: field.onBlur,\n ref: field.ref,\n label,\n helperText: errorMessage ? undefined : helperText,\n error: errorMessage,\n required,\n \"aria-invalid\": !!errorMessage,\n });\n }}\n />\n );\n}\n"],"mappings":"sIA0FA,IAAM,EACF,kSAIA,EAAA,sBAEJ,SAAgB,EAGd,CAAE,OAAM,QAAO,aAAY,WAAU,UAAS,YAA4C,CACxF,IAAM,GAAA,EAAU,EAAA,eAAA,CAAwB,EAClC,EAAkB,GAAW,GAAS,QAC5C,GAAI,CAAC,EACD,MAAU,MAAM,CAAkB,EAGtC,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACU,OACN,QAAS,EACT,QAAS,CAAE,QAAO,gBAAiB,CAC/B,GAAI,EAAA,EAAC,EAAA,eAAA,CAAe,CAAQ,EAAG,OAAO,EACtC,IAAM,EAAe,EAAW,OAAO,QACvC,OAAA,EAAO,EAAA,aAAA,CAAa,EAA+C,CAC/D,KAAM,EAAM,KACZ,MAAO,EAAM,MACb,SAAU,EAAM,SAChB,OAAQ,EAAM,OACd,IAAK,EAAM,IACX,QACA,WAAY,EAAe,IAAA,GAAY,EACvC,MAAO,EACP,WACA,eAAgB,CAAC,CAAC,CACtB,CAAC,CACL,CACH,CAAA,CAET"}
@@ -1,32 +1,34 @@
1
- import { cloneElement as e, isValidElement as t } from "react";
2
- import { jsx as n } from "react/jsx-runtime";
3
- import { Controller as r, useFormContext as i } from "react-hook-form";
1
+ import { DUPLICATE_COPY_REMEDY as e } from "../utils/duplicate-instance.js";
2
+ import { cloneElement as t, isValidElement as n } from "react";
3
+ import { jsx as r } from "react/jsx-runtime";
4
+ import { Controller as i, useFormContext as a } from "react-hook-form";
4
5
  //#region src/forms/FormField.tsx
5
- function a({ name: a, label: o, helperText: s, required: c, control: l, children: u }) {
6
- let d = i(), f = l ?? d?.control;
7
- if (!f) throw Error("FormField requires either a `control` prop or a <FormProvider> in the tree.");
8
- return /* @__PURE__ */ n(r, {
9
- name: a,
10
- control: f,
11
- render: ({ field: n, fieldState: r }) => {
12
- if (!t(u)) return u;
6
+ var o = "FormField found no form to bind to. Either pass a `control` prop, or mount a <FormProvider> above it. If a <FormProvider> IS mounted above it, the app and the SDK are resolving two copies of react-hook-form: the provider publishes on one copy's context and this field reads the other's. " + e;
7
+ function s({ name: e, label: s, helperText: c, required: l, control: u, children: d }) {
8
+ let f = a(), p = u ?? f?.control;
9
+ if (!p) throw Error(o);
10
+ return /* @__PURE__ */ r(i, {
11
+ name: e,
12
+ control: p,
13
+ render: ({ field: e, fieldState: r }) => {
14
+ if (!n(d)) return d;
13
15
  let i = r.error?.message;
14
- return e(u, {
15
- name: n.name,
16
- value: n.value,
17
- onChange: n.onChange,
18
- onBlur: n.onBlur,
19
- ref: n.ref,
20
- label: o,
21
- helperText: i ? void 0 : s,
16
+ return t(d, {
17
+ name: e.name,
18
+ value: e.value,
19
+ onChange: e.onChange,
20
+ onBlur: e.onBlur,
21
+ ref: e.ref,
22
+ label: s,
23
+ helperText: i ? void 0 : c,
22
24
  error: i,
23
- required: c,
25
+ required: l,
24
26
  "aria-invalid": !!i
25
27
  });
26
28
  }
27
29
  });
28
30
  }
29
31
  //#endregion
30
- export { a as FormField };
32
+ export { s as FormField };
31
33
 
32
34
  //# sourceMappingURL=FormField.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FormField.js","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — FormFieldChildProps is not a component's props — it\n * is the contract handed to whatever control the field wraps, and it is exactly what\n * react-hook-form's register returns (name, value, onChange, onBlur, ref) plus what\n * the field adds around it (error, label, helperText, required, id). Trimming it\n * breaks the wiring it exists to describe.\n */\nimport { cloneElement, isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport {\n Controller,\n useFormContext,\n type Control,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\n\nexport interface FormFieldChildProps {\n name: string;\n value: unknown;\n onChange: (...args: unknown[]) => void;\n onBlur: () => void;\n ref: ControllerRenderProps[\"ref\"];\n error?: string;\n \"aria-invalid\"?: boolean;\n \"aria-describedby\"?: string;\n label?: ReactNode;\n helperText?: ReactNode;\n required?: boolean;\n id?: string;\n}\n\nexport interface FormFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n> {\n /** Field name (dot path supported, e.g. `\"address.city\"`). */\n name: TName;\n /** Field label rendered by the wrapped control. */\n label?: ReactNode;\n /** Helper text rendered by the wrapped control when there is no error. */\n helperText?: ReactNode;\n /** When `true`, marks the control as required (visual hint + native attribute). */\n required?: boolean;\n /**\n * Explicit `control` from `useForm()` / `useZodForm()`. Optional when a\n * `FormProvider` is in the tree.\n */\n control?: Control<TValues>;\n /**\n * The control to render. Receives `{ value, onChange, onBlur, ref, error, ... }`\n * via `cloneElement`. Pass `<Input />`, `<Select />`, masked inputs, etc.\n */\n children: ReactElement;\n}\n\n/**\n * Glue between `react-hook-form` `Controller` and the SDK's controlled\n * components. Wraps any control that accepts `{ value, onChange, label,\n * error }` and routes RHF state into it — eliminating the per-field\n * `<Controller render={...} />` boilerplate.\n *\n * @example\n * const form = useZodForm(schema);\n * <FormProvider {...form}>\n * <Form>\n * <FormField name=\"email\" label=\"Email\" required>\n * <Input type=\"email\" />\n * </FormField>\n * <FormField name=\"cep\" label=\"CEP\">\n * <CEPInput />\n * </FormField>\n * </Form>\n * </FormProvider>;\n */\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, helperText, required, control, children }: FormFieldProps<TValues, TName>) {\n const context = useFormContext<TValues>();\n const resolvedControl = control ?? context?.control;\n if (!resolvedControl) {\n throw new Error(\n \"FormField requires either a `control` prop or a <FormProvider> in the tree.\",\n );\n }\n\n return (\n <Controller\n name={name}\n control={resolvedControl}\n render={({ field, fieldState }) => {\n if (!isValidElement(children)) return children;\n const errorMessage = fieldState.error?.message;\n return cloneElement(children as ReactElement<FormFieldChildProps>, {\n name: field.name,\n value: field.value,\n onChange: field.onChange,\n onBlur: field.onBlur,\n ref: field.ref,\n label,\n helperText: errorMessage ? undefined : helperText,\n error: errorMessage,\n required,\n \"aria-invalid\": !!errorMessage,\n });\n }}\n />\n );\n}\n"],"mappings":";;;;AA4EA,SAAgB,EAGd,EAAE,SAAM,UAAO,eAAY,aAAU,YAAS,eAA4C;CACxF,IAAM,IAAU,EAAwB,GAClC,IAAkB,KAAW,GAAS;CAC5C,IAAI,CAAC,GACD,MAAU,MACN,6EACJ;CAGJ,OACI,kBAAC,GAAD;EACU;EACN,SAAS;EACT,SAAS,EAAE,UAAO,oBAAiB;GAC/B,IAAI,CAAC,EAAe,CAAQ,GAAG,OAAO;GACtC,IAAM,IAAe,EAAW,OAAO;GACvC,OAAO,EAAa,GAA+C;IAC/D,MAAM,EAAM;IACZ,OAAO,EAAM;IACb,UAAU,EAAM;IAChB,QAAQ,EAAM;IACd,KAAK,EAAM;IACX;IACA,YAAY,IAAe,KAAA,IAAY;IACvC,OAAO;IACP;IACA,gBAAgB,CAAC,CAAC;GACtB,CAAC;EACL;CACH,CAAA;AAET"}
1
+ {"version":3,"file":"FormField.js","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — FormFieldChildProps is not a component's props — it\n * is the contract handed to whatever control the field wraps, and it is exactly what\n * react-hook-form's register returns (name, value, onChange, onBlur, ref) plus what\n * the field adds around it (error, label, helperText, required, id). Trimming it\n * breaks the wiring it exists to describe.\n */\nimport { cloneElement, isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport {\n Controller,\n useFormContext,\n type Control,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\n\nimport { DUPLICATE_COPY_REMEDY } from \"../utils/duplicate-instance\";\n\nexport interface FormFieldChildProps {\n name: string;\n value: unknown;\n onChange: (...args: unknown[]) => void;\n onBlur: () => void;\n ref: ControllerRenderProps[\"ref\"];\n error?: string;\n \"aria-invalid\"?: boolean;\n \"aria-describedby\"?: string;\n label?: ReactNode;\n helperText?: ReactNode;\n required?: boolean;\n id?: string;\n}\n\nexport interface FormFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n> {\n /** Field name (dot path supported, e.g. `\"address.city\"`). */\n name: TName;\n /** Field label rendered by the wrapped control. */\n label?: ReactNode;\n /** Helper text rendered by the wrapped control when there is no error. */\n helperText?: ReactNode;\n /** When `true`, marks the control as required (visual hint + native attribute). */\n required?: boolean;\n /**\n * Explicit `control` from `useForm()` / `useZodForm()`. Optional when a\n * `FormProvider` is in the tree.\n */\n control?: Control<TValues>;\n /**\n * The control to render. Receives `{ value, onChange, onBlur, ref, error, ... }`\n * via `cloneElement`. Pass `<Input />`, `<Select />`, masked inputs, etc.\n */\n children: ReactElement;\n}\n\n/**\n * Glue between `react-hook-form` `Controller` and the SDK's controlled\n * components. Wraps any control that accepts `{ value, onChange, label,\n * error }` and routes RHF state into it — eliminating the per-field\n * `<Controller render={...} />` boilerplate.\n *\n * @example\n * const form = useZodForm(schema);\n * <FormProvider {...form}>\n * <Form>\n * <FormField name=\"email\" label=\"Email\" required>\n * <Input type=\"email\" />\n * </FormField>\n * <FormField name=\"cep\" label=\"CEP\">\n * <CEPInput />\n * </FormField>\n * </Form>\n * </FormProvider>;\n */\n/**\n * What is thrown when no `control` can be resolved.\n *\n * It names **two** causes, because the throw cannot tell them apart and the\n * second one is the expensive one. The obvious reading — no provider, no prop —\n * is usually right. But the identical throw happens with a `<FormProvider>`\n * mounted directly above, when the app and the SDK resolve two different copies\n * of `react-hook-form`: the provider publishes on the app copy's context and\n * `useFormContext` here reads the SDK copy's, which is empty. Someone looking at\n * a provider they can see, being told it is missing, does not go looking at\n * `node_modules` — so the message has to send them there.\n */\nconst NO_CONTROL_MESSAGE =\n \"FormField found no form to bind to. Either pass a `control` prop, or mount a \" +\n \"<FormProvider> above it. If a <FormProvider> IS mounted above it, the app and the \" +\n \"SDK are resolving two copies of react-hook-form: the provider publishes on one \" +\n \"copy's context and this field reads the other's. \" +\n DUPLICATE_COPY_REMEDY;\n\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, helperText, required, control, children }: FormFieldProps<TValues, TName>) {\n const context = useFormContext<TValues>();\n const resolvedControl = control ?? context?.control;\n if (!resolvedControl) {\n throw new Error(NO_CONTROL_MESSAGE);\n }\n\n return (\n <Controller\n name={name}\n control={resolvedControl}\n render={({ field, fieldState }) => {\n if (!isValidElement(children)) return children;\n const errorMessage = fieldState.error?.message;\n return cloneElement(children as ReactElement<FormFieldChildProps>, {\n name: field.name,\n value: field.value,\n onChange: field.onChange,\n onBlur: field.onBlur,\n ref: field.ref,\n label,\n helperText: errorMessage ? undefined : helperText,\n error: errorMessage,\n required,\n \"aria-invalid\": !!errorMessage,\n });\n }}\n />\n );\n}\n"],"mappings":";;;;;AA0FA,IAAM,IACF,oSAIA;AAEJ,SAAgB,EAGd,EAAE,SAAM,UAAO,eAAY,aAAU,YAAS,eAA4C;CACxF,IAAM,IAAU,EAAwB,GAClC,IAAkB,KAAW,GAAS;CAC5C,IAAI,CAAC,GACD,MAAU,MAAM,CAAkB;CAGtC,OACI,kBAAC,GAAD;EACU;EACN,SAAS;EACT,SAAS,EAAE,UAAO,oBAAiB;GAC/B,IAAI,CAAC,EAAe,CAAQ,GAAG,OAAO;GACtC,IAAM,IAAe,EAAW,OAAO;GACvC,OAAO,EAAa,GAA+C;IAC/D,MAAM,EAAM;IACZ,OAAO,EAAM;IACb,UAAU,EAAM;IAChB,QAAQ,EAAM;IACd,KAAK,EAAM;IACX;IACA,YAAY,IAAe,KAAA,IAAY;IACvC,OAAO;IACP;IACA,gBAAgB,CAAC,CAAC;GACtB,CAAC;EACL;CACH,CAAA;AAET"}
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs");var t={offline:`Sem conexão com o servidor. Verifique sua internet e tente de novo.`,validation:`Confira os campos destacados e tente de novo.`},n=`tempest.error.offline`,r=`tempest.error.validation`;function i(){return typeof navigator<`u`&&navigator.onLine===!1}function a(n,r,a){let o=a?.offline??t.offline;if(e.isApiError(n)){if(n.status===0)return o;if(n.fields&&Object.keys(n.fields).length>0)return a?.validation??t.validation;let i=n.detail.trim();return i!==``&&i!==e.syntheticDetail(n.status)?i:`${r} (HTTP ${n.status})`}return i()?o:r}exports.API_ERROR_OFFLINE_KEY=n,exports.API_ERROR_VALIDATION_KEY=r,exports.DEFAULT_API_ERROR_STRINGS=t,exports.describeApiError=a;
1
+ const e=require("./errors.cjs");var t={offline:`Sem conexão com o servidor. Verifique sua internet e tente de novo.`,validation:`Confira os campos destacados e tente de novo.`},n=`tempest.error.offline`,r=`tempest.error.validation`;function i(){return typeof navigator<`u`&&navigator.onLine===!1}function a(n,r,a){let o=a?.offline??t.offline;if(e.isApiError(n)){let i=n.code===void 0?void 0:a?.codes?.[n.code];if(i!==void 0)return i;if(n.status===0)return o;if(n.fields&&Object.keys(n.fields).length>0)return a?.validation??t.validation;let s=n.detail.trim();return a?.useDetail!==!1&&s!==``&&s!==e.syntheticDetail(n.status)?s:`${r} (HTTP ${n.status})`}return i()?o:r}exports.API_ERROR_OFFLINE_KEY=n,exports.API_ERROR_VALIDATION_KEY=r,exports.DEFAULT_API_ERROR_STRINGS=t,exports.describeApiError=a;
2
2
  //# sourceMappingURL=describe-api-error.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param strings - Overrides for the fixed sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n strings?: Partial<ApiErrorStrings>,\n): string {\n const offline = strings?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return strings?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (detail !== \"\" && detail !== syntheticDetail(error.status)) return detail;\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":"gCA2BA,IAAa,EAA6C,CACtD,QAAS,sEACT,WAAY,+CAChB,EAUa,EAAwB,wBAMxB,EAA2B,2BAYxC,SAAS,GAA4B,CACjC,OAAO,OAAO,UAAc,KAAe,UAAU,SAAW,EACpE,CAqCA,SAAgB,EACZ,EACA,EACA,EACM,CACN,IAAM,EAAU,GAAS,SAAW,EAA0B,QAE9D,GAAI,EAAA,WAAW,CAAK,EAAG,CACnB,GAAI,EAAM,SAAW,EAAG,OAAO,EAC/B,GAAI,EAAM,QAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,OAAS,EACnD,OAAO,GAAS,YAAc,EAA0B,WAE5D,IAAM,EAAS,EAAM,OAAO,KAAK,EAEjC,OADI,IAAW,IAAM,IAAW,EAAA,gBAAgB,EAAM,MAAM,EAAU,EAC/D,GAAG,EAAS,SAAS,EAAM,OAAO,EAC7C,CAIA,OAFI,EAAiB,EAAU,EAExB,CACX"}
1
+ {"version":3,"file":"describe-api-error.cjs","names":[],"sources":["../../src/http/describe-api-error.ts"],"sourcesContent":["// The last mile of error handling: a typed error is what code reads, a sentence\n// is what a person reads, and every app was writing the funnel between the two.\n// The case everyone forgets is the request that never reached the server, which\n// without special handling renders as the nonsense \"erro 0\".\n\nimport { isApiError, syntheticDetail } from \"./errors\";\n\n/** The fixed sentences {@link describeApiError} may need. */\nexport interface ApiErrorStrings {\n /** Shown when the request never reached the server. */\n offline: string;\n /**\n * Shown when the backend rejected the payload field by field.\n *\n * The per-field messages are on `error.fields`, to be attached to the inputs\n * themselves; this sentence is what the toast says.\n */\n validation: string;\n}\n\n/**\n * PT-BR defaults, used when no strings are supplied and no catalog answers.\n *\n * The SDK's copy is pt-BR everywhere else (`FilterBar`, `DataTable`, `Chat`), so\n * the default here matches rather than introducing an English string that only\n * shows up on a network failure.\n */\nexport const DEFAULT_API_ERROR_STRINGS: ApiErrorStrings = {\n offline: \"Sem conexão com o servidor. Verifique sua internet e tente de novo.\",\n validation: \"Confira os campos destacados e tente de novo.\",\n};\n\n/**\n * Translation key the {@link useDescribeApiError} hook looks up.\n *\n * A catalog that does not define it falls back to\n * {@link DEFAULT_API_ERROR_STRINGS}, because `t` returns the key itself when the\n * lookup misses and printing `tempest.error.offline` at the user would be worse\n * than printing pt-BR at them.\n */\nexport const API_ERROR_OFFLINE_KEY = \"tempest.error.offline\";\n\n/**\n * Translation key for the validation sentence, looked up the same way as\n * {@link API_ERROR_OFFLINE_KEY}.\n */\nexport const API_ERROR_VALIDATION_KEY = \"tempest.error.validation\";\n\n/**\n * Everything {@link describeApiError} accepts beyond the error and the fallback.\n *\n * Extends the fixed sentences rather than sitting beside them, so a caller that\n * already passed `{ offline, validation }` keeps compiling untouched.\n */\nexport interface DescribeApiErrorOptions extends Partial<ApiErrorStrings> {\n /**\n * Maps the backend's programmatic `code` to a sentence in your language.\n *\n * The client already surfaces `code` on `ApiError`, but without this every\n * app writes the same `switch` over it. A hit here wins over every other\n * step: it is the only sentence written for that exact case, by someone who\n * knew both the backend contract and the screen it lands on.\n */\n codes?: Readonly<Record<string, string>>;\n /**\n * Whether the backend's `detail` may be shown when no `code` matched.\n * Default `true`.\n *\n * Set it to `false` when `detail` is written for developers rather than\n * users, or when it could echo internals — the result is then always either\n * a sentence you wrote or the fallback.\n */\n useDetail?: boolean;\n}\n\n/**\n * Whether the browser currently reports itself as offline.\n *\n * `fetch` rejects a network failure with a plain `TypeError` whose message\n * differs per browser (\"Failed to fetch\", \"NetworkError when attempting to fetch\n * resource.\", \"Load failed\"), so sniffing the message is not portable. The online\n * flag is, and it is the signal that matters for the sentence being chosen.\n *\n * @returns `true` only when the environment positively says it is offline.\n */\nfunction browserIsOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/**\n * Turn any caught value into a sentence worth showing.\n *\n * The funnel, in order:\n *\n * 0. `codes[error.code]` — the sentence you wrote for that exact backend case.\n * Checked first because nothing the funnel derives can beat it, and because a\n * request that never landed carries no `code` for it to shadow.\n * 1. A request that never reached the server — `status === 0`, or a non-API\n * error thrown while the browser reports itself offline — produces the\n * offline sentence. This is the step apps skip, and skipping it renders\n * \"erro 0\" or a raw `TypeError` at the user.\n * 2. A validation rejection — `error.fields` is set — produces the validation\n * sentence, **not** `detail`. On a `422` the `detail` line is assembled from\n * the backend's field paths and the validator's own wording\n * (`\"items.0.price: Input should be greater than 0\"`), which is right for a\n * log and wrong for a person: it is half English in a pt-BR screen and it\n * names internals. The per-field messages stay on `fields`, where a form can\n * attach them to the inputs that failed.\n * 3. The backend's own `detail`, which is the most specific thing available and\n * is already written for a person — unless `useDetail: false` says that text\n * is for developers.\n * 4. `fallback`, with `(HTTP <status>)` appended when a status is known, so the\n * screenshot in the support ticket carries the one fact a developer needs.\n *\n * Pure on purpose: it works in an interceptor, in a logger and anywhere outside\n * the React tree. {@link useDescribeApiError} is the same funnel with the\n * sentences resolved through `I18nProvider`.\n *\n * @example\n * catch (error) {\n * toast(describeApiError(error, \"Não foi possível salvar o pedido\"));\n * }\n *\n * @example\n * catch (error) {\n * toast(\n * describeApiError(error, \"Não foi possível se candidatar\", {\n * codes: {\n * SERVICE_FULL: \"Este serviço atingiu o limite de vagas.\",\n * CANDIDATE_ALREADY_EXISTS: \"Você já se candidatou a este serviço.\",\n * },\n * useDetail: false,\n * }),\n * );\n * }\n *\n * @param error - The caught value, of any shape.\n * @param fallback - What to say when the error carries nothing better.\n * @param options - A `codes` catalog, `useDetail`, and overrides for the fixed\n * sentences.\n * @returns A sentence to show the user.\n */\nexport function describeApiError(\n error: unknown,\n fallback: string,\n options?: DescribeApiErrorOptions,\n): string {\n const offline = options?.offline ?? DEFAULT_API_ERROR_STRINGS.offline;\n\n if (isApiError(error)) {\n const mapped = error.code === undefined ? undefined : options?.codes?.[error.code];\n if (mapped !== undefined) return mapped;\n if (error.status === 0) return offline;\n if (error.fields && Object.keys(error.fields).length > 0) {\n return options?.validation ?? DEFAULT_API_ERROR_STRINGS.validation;\n }\n const detail = error.detail.trim();\n if (\n options?.useDetail !== false &&\n detail !== \"\" &&\n detail !== syntheticDetail(error.status)\n ) {\n return detail;\n }\n return `${fallback} (HTTP ${error.status})`;\n }\n\n if (browserIsOffline()) return offline;\n\n return fallback;\n}\n"],"mappings":"gCA2BA,IAAa,EAA6C,CACtD,QAAS,sEACT,WAAY,+CAChB,EAUa,EAAwB,wBAMxB,EAA2B,2BAuCxC,SAAS,GAA4B,CACjC,OAAO,OAAO,UAAc,KAAe,UAAU,SAAW,EACpE,CAuDA,SAAgB,EACZ,EACA,EACA,EACM,CACN,IAAM,EAAU,GAAS,SAAW,EAA0B,QAE9D,GAAI,EAAA,WAAW,CAAK,EAAG,CACnB,IAAM,EAAS,EAAM,OAAS,IAAA,GAAY,IAAA,GAAY,GAAS,QAAQ,EAAM,MAC7E,GAAI,IAAW,IAAA,GAAW,OAAO,EACjC,GAAI,EAAM,SAAW,EAAG,OAAO,EAC/B,GAAI,EAAM,QAAU,OAAO,KAAK,EAAM,MAAM,CAAC,CAAC,OAAS,EACnD,OAAO,GAAS,YAAc,EAA0B,WAE5D,IAAM,EAAS,EAAM,OAAO,KAAK,EAQjC,OANI,GAAS,YAAc,IACvB,IAAW,IACX,IAAW,EAAA,gBAAgB,EAAM,MAAM,EAEhC,EAEJ,GAAG,EAAS,SAAS,EAAM,OAAO,EAC7C,CAIA,OAFI,EAAiB,EAAU,EAExB,CACX"}
@@ -10,10 +10,12 @@ function a() {
10
10
  function o(r, i, o) {
11
11
  let s = o?.offline ?? n.offline;
12
12
  if (e(r)) {
13
+ let e = r.code === void 0 ? void 0 : o?.codes?.[r.code];
14
+ if (e !== void 0) return e;
13
15
  if (r.status === 0) return s;
14
16
  if (r.fields && Object.keys(r.fields).length > 0) return o?.validation ?? n.validation;
15
- let e = r.detail.trim();
16
- return e !== "" && e !== t(r.status) ? e : `${i} (HTTP ${r.status})`;
17
+ let a = r.detail.trim();
18
+ return o?.useDetail !== !1 && a !== "" && a !== t(r.status) ? a : `${i} (HTTP ${r.status})`;
17
19
  }
18
20
  return a() ? s : i;
19
21
  }