tempest-react-sdk 0.50.0 → 0.52.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.
- package/dist/audio/sfx-pool.cjs +1 -1
- package/dist/audio/sfx-pool.cjs.map +1 -1
- package/dist/audio/sfx-pool.js +36 -36
- package/dist/audio/sfx-pool.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +6 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/components/BarList/BarList.cjs +1 -1
- package/dist/components/BarList/BarList.cjs.map +1 -1
- package/dist/components/BarList/BarList.js +6 -1
- package/dist/components/BarList/BarList.js.map +1 -1
- package/dist/components/BarList/bar-list-model.cjs +1 -1
- package/dist/components/BarList/bar-list-model.cjs.map +1 -1
- package/dist/components/BarList/bar-list-model.js +8 -8
- package/dist/components/BarList/bar-list-model.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +5 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +66 -63
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.module.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js +25 -45
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
- package/dist/components/Sidebar/SidebarEntry.cjs +2 -0
- package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -0
- package/dist/components/Sidebar/SidebarEntry.js +40 -0
- package/dist/components/Sidebar/SidebarEntry.js.map +1 -0
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +84 -71
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/describe-api-error.cjs +1 -1
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js +4 -2
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/timeout.cjs +2 -0
- package/dist/http/timeout.cjs.map +1 -0
- package/dist/http/timeout.js +22 -0
- package/dist/http/timeout.js.map +1 -0
- package/dist/http/use-describe-api-error.cjs +1 -1
- package/dist/http/use-describe-api-error.cjs.map +1 -1
- package/dist/http/use-describe-api-error.js +3 -2
- package/dist/http/use-describe-api-error.js.map +1 -1
- package/dist/icons/material-symbols.cjs.map +1 -1
- package/dist/icons/material-symbols.js.map +1 -1
- package/dist/offline/create-offline-database.cjs +2 -0
- package/dist/offline/create-offline-database.cjs.map +1 -0
- package/dist/offline/create-offline-database.js +29 -0
- package/dist/offline/create-offline-database.js.map +1 -0
- package/dist/offline/create-offline-store.cjs +1 -1
- package/dist/offline/create-offline-store.cjs.map +1 -1
- package/dist/offline/create-offline-store.js +29 -25
- package/dist/offline/create-offline-store.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +286 -13
- package/dist/tempest-react-sdk.js +43 -42
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +8 -3
- package/dist/utils/format.js.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +49 -41
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/package.json +1 -1
|
@@ -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":";;;;;;AAmDA,IAAM,IAAU;AA6BhB,SAAgB,EAAU,EACtB,WACA,WACA,MAAM,IAAW,GACjB,eAAY,GACZ,aAAU,IACV,iBAAc,IACd,iBACA,gBACA,gBACA,YAAS,SACT,qBAAkB,IAClB,QACA,cACA,GAAG,KACY;CACf,IAAM,CAAC,GAAO,KAAY,QAAqB,qBAAO,IAAI,KAAK,CAAC;CAQhE,QAAgB;EACZ,IAAI,KAAO,CAAC,GAAiB;EAC7B,IAAM,IAAK,kBAAkB,kBAAS,IAAI,KAAK,CAAC,GAAG,CAAO;EAC1D,aAAa,cAAc,CAAE;CACjC,GAAG,CAAC,GAAK,CAAe,CAAC;CAEzB,IAAM,IAAY,KAAO,GAEnB,IAAS,SACJ;EAAE,aAAa,IAAY;EAAI,WAAW,IAAU;CAAG,IAC9D,CAAC,GAAW,CAAO,CACvB,GASM,KAAa,KAAU,EAAA,CAAW,aAAa,GAC/C,IAAU,QAAc,EAAS,IAAI,KAAK,CAAS,GAAG,CAAQ,GAAG,CAAC,GAAW,CAAQ,CAAC,GAEtF,IAAS,QACL,EAAa;EAAE;EAAQ,MAAM;EAAS;CAAO,CAAC,GACpD;EAAC;EAAQ;EAAS;CAAM,CAC5B,GACM,IAAS,QAAc,EAAa;EAAE;EAAQ,MAAM;CAAQ,CAAC,GAAG,CAAC,GAAQ,CAAO,CAAC,GACjF,IAAQ,QAAc,EAAU,CAAM,GAAG,CAAC,CAAM,CAAC,GAEjD,IAAW,IAAI,KAAK,eAAe,GAAQ;EAAE,SAAS;EAAS,KAAK;CAAU,CAAC,GAC/E,IAAY,IAAI,KAAK,eAAe,GAAQ;EAAE,MAAM;EAAW,QAAQ;CAAU,CAAC,GAGlF,KAAiB,GAAW,MAAkD;EAChF,IAAM,IAAO,EAAM,cAAc,sBAAsB,GACjD,IAAW,EAAK,SAAS,KAAK,EAAM,UAAU,EAAK,OAAO,EAAK,SAAS,GACxE,IAAM,EAAO,cAAc,KAAY,EAAO,YAAY,EAAO,cACjE,IAAU,KAAK,MAAM,IAAM,CAAW,IAAI,GAC1C,IAAU,KAAK,IAAI,EAAO,aAAa,KAAK,IAAI,EAAO,WAAW,CAAO,CAAC,GAC1E,IAAS,IAAI,KAAK,CAAG;EAE3B,OADA,EAAO,SAAS,GAAG,GAAS,GAAG,CAAC,GACzB;CACX,GAEM,IAAkB,IAAkB,EAAiB,GAAW,CAAM,IAAI,MAC1E,IAAa,EAAQ,WAAW,MAAQ,EAAU,GAAK,CAAS,CAAC;CAEvE,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,CAAS;EAAG,GAAI;EAAnD,UAAA;GACI,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAG,4BAAuC,EAAS;IAF9D,UAAA,CAII,kBAAC,QAAD,EAAM,WAAW,EAAO,WAAa,CAAA,GACpC,EAAQ,KAAK,GAAK,MACf,kBAAC,QAAD;KAEI,WAAW,EAAG,EAAO,SAAS,MAAU,KAAc,EAAO,KAAK;KAEjE,UAAA,EAAS,OAAO,CAAG;IAClB,GAJG,EAAI,YAAY,CAInB,CACT,CACA;;GAEJ,EAAO,SAAS,KACb,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAG,4BAAuC,EAAS;IAF9D,UAAA,CAII,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAY,UAAA;IAAiB,CAAA,GACrD,kBAAC,OAAD;KAAK,WAAW,EAAO;KAClB,UAAA,EAAO,KAAK,EAAE,UAAO,aAAU,cAC5B,kBAAC,UAAD;MAEI,MAAK;MACL,WAAW,EAAO;MAClB,OAAO,EAAE,YAAY,GAAG,IAAW,EAAE,UAAU,IAAO;MACtD,eAAe,IAAe,CAAK;MACnC,UAAU,CAAC;MAEV,UAAA,EAAM;KACH,GARC,EAAM,EAQP,CACX;IACA,CAAA,CACJ;;GAWT,kBAAC,OAAD;IACI,WAAW,EAAO;IAClB,OAAO,EAAG,4BAAuC,EAAS;IAC1D,UAAU;IACV,MAAK;IACL,cAAW;IALf,UAAA,CAOI,kBAAC,OAAD;KAAK,WAAW,EAAO;KAClB,UAAA,EAAM,KAAK,MACR,kBAAC,QAAD;MAEI,WAAW,EAAO;MAClB,OAAO,EACH,KAAK,IAAK,IAAS,EAAO,gBAAgB,EAAO,YAAY,EAAO,eAAgB,IAAI,GAC5F;MAEC,UAAA,EAAU,OACP,IAAI,KAAK,MAAM,GAAG,GAAG,KAAK,MAAM,IAAS,EAAE,GAAG,IAAS,EAAE,CAC7D;KACE,GATG,CASH,CACT;IACA,CAAA,GASL,kBAAC,OAAD;KAAK,WAAW,EAAO;KAAM,cAAW;KAAxC,UAAA;MACK,EAAQ,KAAK,GAAK,MACf,kBAAC,OAAD;OAEI,WAAW,EACP,EAAO,WACP,MAAU,KAAc,EAAO,WACnC;OACA,MAAK;OACL,cAAY,EAAS,OAAO,CAAG;OAC/B,SACI,KACO,MAAU;QAGH,EAAM,WAAW,EAAM,iBAC3B,EAAY,EAAc,GAAK,CAAK,CAAC;OACzC,IACA,KAAA;OAGT,UAAA,EAAM,KAAK,MACR,kBAAC,QAAD;QAEI,WAAW,EAAO;QAClB,OAAO,EACH,KAAK,IAAK,IAAS,EAAO,gBAAgB,EAAO,YAAY,EAAO,eAAgB,IAAI,GAC5F;OACH,GALQ,CAKR,CACJ;MACA,GA3BI,EAAI,YAAY,CA2BpB,CACR;MAEA,EAAO,KAAK,MACT,kBAAC,UAAD;OAEI,MAAK;OACL,WAAW,EAAO;OAClB,OAAO;QACH,YAAY,EAAK,WAAW;QAC5B,KAAK,GAAG,EAAK,MAAM,IAAI;QACvB,QAAQ,GAAG,EAAK,SAAS,IAAI;QAC7B,MAAM,GAAI,EAAK,SAAS,EAAK,UAAW,IAAI;QAC5C,OAAO,GAAI,IAAI,EAAK,UAAW,IAAI;OACvC;OACA,eAAe,IAAe,EAAK,KAAK;OACxC,UAAU,CAAC;OACX,OAAO,GAAG,EAAK,MAAM,MAAM,KAAK,EAAU,OAAO,EAAK,MAAM,KAAK;OAEhE,UAAA,IACG,EAAY,EAAK,KAAK,IAEtB,kBAAA,GAAA,EAAA,UAAA,CACI,kBAAC,QAAD;QAAM,WAAW,EAAO;QACnB,UAAA,EAAU,OAAO,EAAK,MAAM,KAAK;OAChC,CAAA,GACN,kBAAC,QAAD;QAAM,WAAW,EAAO;QAAa,UAAA,EAAK,MAAM;OAAY,CAAA,CAC9D,EAAA,CAAA;MAEF,GAxBC,GAAG,EAAK,MAAM,GAAG,GAAG,EAAK,UAwB1B,CACX;MAEA,MAAoB,QAAQ,KAAc,KACvC,kBAAC,QAAD;OACI,WAAW,EAAO;OAClB,OAAO,EAAE,KAAK,GAAG,IAAkB,IAAI,GAAG;OAC1C,eAAA;OACA,eAAY;MACf,CAAA;KAEJ;IACJ,CAAA,CAAA;;EACJ;;AAEb"}
|
|
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("./Sidebar.module.cjs");let
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./Sidebar.module.cjs"),n=require("./SidebarEntry.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(e){let t=[],n;for(let r of e){if(r.type===`separator`){t.push({kind:`separator`,key:r.key}),n=void 0;continue}if(r.type===`section`){n={kind:`items`,key:r.key,section:r,items:[]},t.push(n);continue}n||(n={kind:`items`,key:`loose-${r.key}`,items:[]},t.push(n)),n.items.push(r)}return t}function o({header:o,items:s,value:c,onChange:l,footer:u,collapsed:d=!1,width:f=240,collapsedWidth:p=64,className:m,style:h,...g}){let _=(0,r.useId)(),v=a(s),y=typeof(d?p:f)==`number`?`${d?p:f}px`:d?p:f;return(0,i.jsxs)(`aside`,{className:e.cn(t.default.sidebar,d&&t.default.collapsed,m),style:{width:y,...h},...g,children:[o&&(0,i.jsx)(`div`,{className:t.default.header,children:o}),(0,i.jsx)(`nav`,{className:t.default.nav,"aria-label":`Navegação lateral`,children:v.map(r=>{if(r.kind===`separator`)return(0,i.jsx)(`hr`,{className:t.default.separator},r.key);if(!r.section)return(0,i.jsx)(`div`,{className:t.default.group,children:r.items.map(e=>(0,i.jsx)(n.SidebarEntry,{item:e,active:e.key===c,collapsed:d,onSelect:l},e.key))},r.key);let a=`${_}-${r.key}`;return(0,i.jsxs)(`div`,{role:`group`,"aria-labelledby":a,className:t.default.group,children:[(0,i.jsx)(`div`,{id:a,role:`presentation`,className:e.cn(t.default.section,d&&t.default.sectionCollapsed),children:r.section.label}),r.items.map(e=>(0,i.jsx)(n.SidebarEntry,{item:e,active:e.key===c,collapsed:d,onSelect:l},e.key))]},r.key)})}),u&&(0,i.jsx)(`div`,{className:t.default.footer,children:u})]})}exports.Sidebar=o;
|
|
2
2
|
//# sourceMappingURL=Sidebar.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.cjs","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — header, items, footer are the three regions,\n * value/onChange the selection, and collapsed/width/collapsedWidth the two layout\n * modes it switches between.\n */\nimport { useId, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Sidebar.module.css\";\n\nexport interface SidebarItem {\n key: string;\n label: ReactNode;\n icon?: ReactNode;\n badge?: ReactNode;\n disabled?: boolean;\n /**\n * Renders the entry as a link to this URL instead of a button.\n *\n * `onChange` still fires on click, so an app that tracks the active key keeps\n * working — the difference is that the entry now behaves like navigation:\n * middle-click, ctrl-click and \"copy link address\" do what the user expects,\n * and a screen reader announces a link rather than a button.\n *\n * A `disabled` entry ignores this and stays a `<button disabled>`: there is no\n * disabled state for an anchor, and dropping the `href` to fake one leaves a\n * link that announces itself as actionable and is not.\n */\n href?: string;\n}\n\n/**\n * A section heading. Every item after it belongs to the section, until the next\n * section or separator.\n */\nexport interface SidebarSection {\n type: \"section\";\n key: string;\n label: ReactNode;\n}\n\n/** A plain divider, for splitting the list without naming the parts. */\nexport interface SidebarSeparator {\n type: \"separator\";\n key: string;\n}\n\n/**\n * One entry of the navigation list.\n *\n * An entry with no `type` is an item, which is what keeps a plain\n * `SidebarItem[]` a valid `SidebarEntry[]`: adding sections to this component\n * needed no change at any existing call site.\n */\nexport type SidebarEntry = ({ type?: \"item\" } & SidebarItem) | SidebarSection | SidebarSeparator;\n\nexport interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n /** Top slot — typically the logo + brand. */\n header?: ReactNode;\n /** Navigation entries. An entry with no `type` is an item. */\n items: SidebarEntry[];\n /** Active item key. */\n value?: string;\n /** Fires when an item is clicked. Receives the item's `key`. */\n onChange?: (key: string) => void;\n /** Bottom slot — typically settings/profile/logout. */\n footer?: ReactNode;\n /** Collapsed mode — only icons visible. Default `false`. */\n collapsed?: boolean;\n /** Width when expanded, in pixels or any CSS length. Default `240px`. */\n width?: number | string;\n /** Width when collapsed, in pixels or any CSS length. Default `64px`. */\n collapsedWidth?: number | string;\n}\n\n/** A run of items, optionally under a section heading. */\ninterface ItemBlock {\n kind: \"items\";\n key: string;\n section?: SidebarSection;\n items: SidebarItem[];\n}\n\n/** A standalone divider between blocks. */\ninterface SeparatorBlock {\n kind: \"separator\";\n key: string;\n}\n\ntype Block = ItemBlock | SeparatorBlock;\n\n/**\n * Fold the flat entry list into the blocks that get rendered.\n *\n * A section opens a block that swallows every following item; a separator closes\n * whatever is open, so items after it are loose again. Items before the first\n * section are loose too — which is the whole existing behaviour, and why a list\n * with no sections comes out of here as one unnamed block.\n *\n * @param entries - The `items` prop, as given.\n * @returns Blocks in render order.\n */\nfunction toBlocks(entries: readonly SidebarEntry[]): Block[] {\n const blocks: Block[] = [];\n let open: ItemBlock | undefined;\n\n for (const entry of entries) {\n if (entry.type === \"separator\") {\n blocks.push({ kind: \"separator\", key: entry.key });\n open = undefined;\n continue;\n }\n if (entry.type === \"section\") {\n open = { kind: \"items\", key: entry.key, section: entry, items: [] };\n blocks.push(open);\n continue;\n }\n if (!open) {\n open = { kind: \"items\", key: `loose-${entry.key}`, items: [] };\n blocks.push(open);\n }\n open.items.push(entry);\n }\n return blocks;\n}\n\n/**\n * Desktop sidebar navigation. Pair with `<Show above=\"md\">` and a `Drawer`\n * for mobile.\n *\n * Sixteen screens in one flat list is a wall, so `items` also takes section\n * headings and separators. A section renders as a labelled `role=\"group\"`, which\n * is what tells a screen reader \"Monitoring, group, 3 items\" — the reason it is\n * not a styled `disabled` item, which would announce an unavailable button and\n * stay in the navigation order.\n *\n * @example\n * const [tab, setTab] = useState(\"home\");\n * <Show above=\"md\">\n * <Sidebar\n * header={<Brand />}\n * items={[{ key: \"home\", label: \"Home\", icon: <Home /> }]}\n * value={tab}\n * onChange={setTab}\n * />\n * </Show>\n *\n * @example\n * <Sidebar\n * items={[\n * { type: \"section\", key: \"monitoring\", label: \"Monitoring\" },\n * { key: \"overview\", label: \"Overview\", href: \"/overview\" },\n * { key: \"activity\", label: \"Activity\", href: \"/activity\" },\n * { type: \"section\", key: \"admin\", label: \"Administration\" },\n * { key: \"settings\", label: \"Settings\", href: \"/settings\" },\n * ]}\n * value={tab}\n * onChange={setTab}\n * />\n */\nexport function Sidebar({\n header,\n items,\n value,\n onChange,\n footer,\n collapsed = false,\n width = 240,\n collapsedWidth = 64,\n className,\n style,\n ...props\n}: SidebarProps) {\n const baseId = useId();\n const blocks = toBlocks(items);\n const finalWidth =\n typeof (collapsed ? collapsedWidth : width) === \"number\"\n ? `${collapsed ? collapsedWidth : width}px`\n : collapsed\n ? collapsedWidth\n : width;\n\n /**\n * Render one navigation entry.\n *\n * @param item - The entry to render.\n * @returns The button, or the anchor when the entry carries an `href`.\n */\n const renderItem = (item: SidebarItem): ReactNode => {\n const active = item.key === value;\n const title = collapsed && typeof item.label === \"string\" ? item.label : undefined;\n const body = (\n <>\n {item.icon && <span className={styles.icon}>{item.icon}</span>}\n {!collapsed && <span className={styles.label}>{item.label}</span>}\n {!collapsed && item.badge !== undefined && (\n <span className={styles.badge}>{item.badge}</span>\n )}\n </>\n );\n\n if (item.href && !item.disabled) {\n return (\n <a\n key={item.key}\n href={item.href}\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n onClick={() => onChange?.(item.key)}\n title={title}\n >\n {body}\n </a>\n );\n }\n\n return (\n <button\n key={item.key}\n type=\"button\"\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n disabled={item.disabled}\n onClick={() => onChange?.(item.key)}\n title={title}\n >\n {body}\n </button>\n );\n };\n\n return (\n <aside\n className={cn(styles.sidebar, collapsed && styles.collapsed, className)}\n style={{ width: finalWidth, ...style }}\n {...props}\n >\n {header && <div className={styles.header}>{header}</div>}\n <nav className={styles.nav} aria-label=\"Navegação lateral\">\n {blocks.map((block) => {\n if (block.kind === \"separator\") {\n return <hr key={block.key} className={styles.separator} />;\n }\n if (!block.section) {\n return (\n <div key={block.key} className={styles.group}>\n {block.items.map(renderItem)}\n </div>\n );\n }\n const labelId = `${baseId}-${block.key}`;\n return (\n <div\n key={block.key}\n role=\"group\"\n aria-labelledby={labelId}\n className={styles.group}\n >\n <div\n id={labelId}\n role=\"presentation\"\n className={cn(styles.section, collapsed && styles.sectionCollapsed)}\n >\n {block.section.label}\n </div>\n {block.items.map(renderItem)}\n </div>\n );\n })}\n </nav>\n {footer && <div className={styles.footer}>{footer}</div>}\n </aside>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Sidebar.cjs","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — header, items, footer are the three regions,\n * value/onChange the selection, and collapsed/width/collapsedWidth the two layout\n * modes it switches between.\n */\nimport { useId, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Sidebar.module.css\";\nimport { SidebarEntry } from \"./SidebarEntry\";\n\nexport interface SidebarItem {\n key: string;\n label: ReactNode;\n icon?: ReactNode;\n badge?: ReactNode;\n disabled?: boolean;\n /**\n * Renders the entry as a link to this URL instead of a button.\n *\n * `onChange` still fires on click, so an app that tracks the active key keeps\n * working — the difference is that the entry now behaves like navigation:\n * middle-click, ctrl-click and \"copy link address\" do what the user expects,\n * and a screen reader announces a link rather than a button.\n *\n * A `disabled` entry ignores this and stays a `<button disabled>`: there is no\n * disabled state for an anchor, and dropping the `href` to fake one leaves a\n * link that announces itself as actionable and is not.\n */\n href?: string;\n}\n\n/**\n * A section heading. Every item after it belongs to the section, until the next\n * section or separator.\n */\nexport interface SidebarSection {\n type: \"section\";\n key: string;\n label: ReactNode;\n}\n\n/** A plain divider, for splitting the list without naming the parts. */\nexport interface SidebarSeparator {\n type: \"separator\";\n key: string;\n}\n\n/**\n * One entry of the navigation list.\n *\n * An entry with no `type` is an item, which is what keeps a plain\n * `SidebarItem[]` a valid `SidebarEntry[]`: adding sections to this component\n * needed no change at any existing call site.\n */\nexport type SidebarEntry = ({ type?: \"item\" } & SidebarItem) | SidebarSection | SidebarSeparator;\n\nexport interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n /** Top slot — typically the logo + brand. */\n header?: ReactNode;\n /** Navigation entries. An entry with no `type` is an item. */\n items: SidebarEntry[];\n /** Active item key. */\n value?: string;\n /** Fires when an item is clicked. Receives the item's `key`. */\n onChange?: (key: string) => void;\n /** Bottom slot — typically settings/profile/logout. */\n footer?: ReactNode;\n /** Collapsed mode — only icons visible. Default `false`. */\n collapsed?: boolean;\n /** Width when expanded, in pixels or any CSS length. Default `240px`. */\n width?: number | string;\n /** Width when collapsed, in pixels or any CSS length. Default `64px`. */\n collapsedWidth?: number | string;\n}\n\n/** A run of items, optionally under a section heading. */\ninterface ItemBlock {\n kind: \"items\";\n key: string;\n section?: SidebarSection;\n items: SidebarItem[];\n}\n\n/** A standalone divider between blocks. */\ninterface SeparatorBlock {\n kind: \"separator\";\n key: string;\n}\n\ntype Block = ItemBlock | SeparatorBlock;\n\n/**\n * Fold the flat entry list into the blocks that get rendered.\n *\n * A section opens a block that swallows every following item; a separator closes\n * whatever is open, so items after it are loose again. Items before the first\n * section are loose too — which is the whole existing behaviour, and why a list\n * with no sections comes out of here as one unnamed block.\n *\n * @param entries - The `items` prop, as given.\n * @returns Blocks in render order.\n */\nfunction toBlocks(entries: readonly SidebarEntry[]): Block[] {\n const blocks: Block[] = [];\n let open: ItemBlock | undefined;\n\n for (const entry of entries) {\n if (entry.type === \"separator\") {\n blocks.push({ kind: \"separator\", key: entry.key });\n open = undefined;\n continue;\n }\n if (entry.type === \"section\") {\n open = { kind: \"items\", key: entry.key, section: entry, items: [] };\n blocks.push(open);\n continue;\n }\n if (!open) {\n open = { kind: \"items\", key: `loose-${entry.key}`, items: [] };\n blocks.push(open);\n }\n open.items.push(entry);\n }\n return blocks;\n}\n\n/**\n * Desktop sidebar navigation. Pair with `<Show above=\"md\">` and a `Drawer`\n * for mobile.\n *\n * Sixteen screens in one flat list is a wall, so `items` also takes section\n * headings and separators. A section renders as a labelled `role=\"group\"`, which\n * is what tells a screen reader \"Monitoring, group, 3 items\" — the reason it is\n * not a styled `disabled` item, which would announce an unavailable button and\n * stay in the navigation order.\n *\n * @example\n * const [tab, setTab] = useState(\"home\");\n * <Show above=\"md\">\n * <Sidebar\n * header={<Brand />}\n * items={[{ key: \"home\", label: \"Home\", icon: <Home /> }]}\n * value={tab}\n * onChange={setTab}\n * />\n * </Show>\n *\n * @example\n * <Sidebar\n * items={[\n * { type: \"section\", key: \"monitoring\", label: \"Monitoring\" },\n * { key: \"overview\", label: \"Overview\", href: \"/overview\" },\n * { key: \"activity\", label: \"Activity\", href: \"/activity\" },\n * { type: \"section\", key: \"admin\", label: \"Administration\" },\n * { key: \"settings\", label: \"Settings\", href: \"/settings\" },\n * ]}\n * value={tab}\n * onChange={setTab}\n * />\n */\nexport function Sidebar({\n header,\n items,\n value,\n onChange,\n footer,\n collapsed = false,\n width = 240,\n collapsedWidth = 64,\n className,\n style,\n ...props\n}: SidebarProps) {\n const baseId = useId();\n const blocks = toBlocks(items);\n const finalWidth =\n typeof (collapsed ? collapsedWidth : width) === \"number\"\n ? `${collapsed ? collapsedWidth : width}px`\n : collapsed\n ? collapsedWidth\n : width;\n\n return (\n <aside\n className={cn(styles.sidebar, collapsed && styles.collapsed, className)}\n style={{ width: finalWidth, ...style }}\n {...props}\n >\n {header && <div className={styles.header}>{header}</div>}\n <nav className={styles.nav} aria-label=\"Navegação lateral\">\n {blocks.map((block) => {\n if (block.kind === \"separator\") {\n return <hr key={block.key} className={styles.separator} />;\n }\n if (!block.section) {\n return (\n <div key={block.key} className={styles.group}>\n {block.items.map((item) => (\n <SidebarEntry\n key={item.key}\n item={item}\n active={item.key === value}\n collapsed={collapsed}\n onSelect={onChange}\n />\n ))}\n </div>\n );\n }\n const labelId = `${baseId}-${block.key}`;\n return (\n <div\n key={block.key}\n role=\"group\"\n aria-labelledby={labelId}\n className={styles.group}\n >\n <div\n id={labelId}\n role=\"presentation\"\n className={cn(styles.section, collapsed && styles.sectionCollapsed)}\n >\n {block.section.label}\n </div>\n {block.items.map((item) => (\n <SidebarEntry\n key={item.key}\n item={item}\n active={item.key === value}\n collapsed={collapsed}\n onSelect={onChange}\n />\n ))}\n </div>\n );\n })}\n </nav>\n {footer && <div className={styles.footer}>{footer}</div>}\n </aside>\n );\n}\n"],"mappings":"8JAsGA,SAAS,EAAS,EAA2C,CACzD,IAAM,EAAkB,CAAC,EACrB,EAEJ,IAAK,IAAM,KAAS,EAAS,CACzB,GAAI,EAAM,OAAS,YAAa,CAC5B,EAAO,KAAK,CAAE,KAAM,YAAa,IAAK,EAAM,GAAI,CAAC,EACjD,EAAO,IAAA,GACP,QACJ,CACA,GAAI,EAAM,OAAS,UAAW,CAC1B,EAAO,CAAE,KAAM,QAAS,IAAK,EAAM,IAAK,QAAS,EAAO,MAAO,CAAC,CAAE,EAClE,EAAO,KAAK,CAAI,EAChB,QACJ,CACK,IACD,EAAO,CAAE,KAAM,QAAS,IAAK,SAAS,EAAM,MAAO,MAAO,CAAC,CAAE,EAC7D,EAAO,KAAK,CAAI,GAEpB,EAAK,MAAM,KAAK,CAAK,CACzB,CACA,OAAO,CACX,CAoCA,SAAgB,EAAQ,CACpB,SACA,QACA,QACA,WACA,SACA,YAAY,GACZ,QAAQ,IACR,iBAAiB,GACjB,YACA,QACA,GAAG,GACU,CACb,IAAM,GAAA,EAAS,EAAA,MAAA,CAAM,EACf,EAAS,EAAS,CAAK,EACvB,EACF,OAAQ,EAAY,EAAiB,IAAW,SAC1C,GAAG,EAAY,EAAiB,EAAM,IACtC,EACE,EACA,EAEZ,OACI,EAAA,EAAA,KAAA,CAAC,QAAD,CACI,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAa,EAAA,QAAO,UAAW,CAAS,EACtE,MAAO,CAAE,MAAO,EAAY,GAAG,CAAM,EACrC,GAAI,EAHR,SAAA,CAKK,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAS,SAAA,CAAY,CAAA,GACvD,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,IAAK,aAAW,oBAClC,SAAA,EAAO,IAAK,GAAU,CACnB,GAAI,EAAM,OAAS,YACf,OAAO,EAAA,EAAA,IAAA,CAAC,KAAD,CAAoB,UAAW,EAAA,QAAO,SAAY,EAAzC,EAAM,GAAmC,EAE7D,GAAI,CAAC,EAAM,QACP,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAqB,UAAW,EAAA,QAAO,MAClC,SAAA,EAAM,MAAM,IAAK,IACd,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CAEU,OACN,OAAQ,EAAK,MAAQ,EACV,YACX,SAAU,CACb,EALQ,EAAK,GAKb,CACJ,CACA,EAVK,EAAM,GAUX,EAGb,IAAM,EAAU,GAAG,EAAO,GAAG,EAAM,MACnC,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAEI,KAAK,QACL,kBAAiB,EACjB,UAAW,EAAA,QAAO,MAJtB,SAAA,EAMI,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,GAAI,EACJ,KAAK,eACL,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAa,EAAA,QAAO,gBAAgB,EAEjE,SAAA,EAAM,QAAQ,KACd,CAAA,EACJ,EAAM,MAAM,IAAK,IACd,EAAA,EAAA,IAAA,CAAC,EAAA,aAAD,CAEU,OACN,OAAQ,EAAK,MAAQ,EACV,YACX,SAAU,CACb,EALQ,EAAK,GAKb,CACJ,CACA,CArBI,EAAA,EAAM,GAqBV,CAEb,CAAC,CACA,CAAA,EACJ,IAAU,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,OAAS,SAAA,CAAY,CAAA,CACpD,GAEf"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
2
|
import t from "./Sidebar.module.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { SidebarEntry as n } from "./SidebarEntry.js";
|
|
4
|
+
import { useId as r } from "react";
|
|
5
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
6
|
//#region src/components/Sidebar/Sidebar.tsx
|
|
6
7
|
function o(e) {
|
|
7
8
|
let t = [], n;
|
|
@@ -31,38 +32,7 @@ function o(e) {
|
|
|
31
32
|
return t;
|
|
32
33
|
}
|
|
33
34
|
function s({ header: s, items: c, value: l, onChange: u, footer: d, collapsed: f = !1, width: p = 240, collapsedWidth: m = 64, className: h, style: g, ..._ }) {
|
|
34
|
-
let v =
|
|
35
|
-
let o = n.key === l, s = f && typeof n.label == "string" ? n.label : void 0, c = /* @__PURE__ */ a(r, { children: [
|
|
36
|
-
n.icon && /* @__PURE__ */ i("span", {
|
|
37
|
-
className: t.icon,
|
|
38
|
-
children: n.icon
|
|
39
|
-
}),
|
|
40
|
-
!f && /* @__PURE__ */ i("span", {
|
|
41
|
-
className: t.label,
|
|
42
|
-
children: n.label
|
|
43
|
-
}),
|
|
44
|
-
!f && n.badge !== void 0 && /* @__PURE__ */ i("span", {
|
|
45
|
-
className: t.badge,
|
|
46
|
-
children: n.badge
|
|
47
|
-
})
|
|
48
|
-
] });
|
|
49
|
-
return n.href && !n.disabled ? /* @__PURE__ */ i("a", {
|
|
50
|
-
href: n.href,
|
|
51
|
-
className: e(t.item, o && t.active),
|
|
52
|
-
"aria-current": o ? "page" : void 0,
|
|
53
|
-
onClick: () => u?.(n.key),
|
|
54
|
-
title: s,
|
|
55
|
-
children: c
|
|
56
|
-
}, n.key) : /* @__PURE__ */ i("button", {
|
|
57
|
-
type: "button",
|
|
58
|
-
className: e(t.item, o && t.active),
|
|
59
|
-
"aria-current": o ? "page" : void 0,
|
|
60
|
-
disabled: n.disabled,
|
|
61
|
-
onClick: () => u?.(n.key),
|
|
62
|
-
title: s,
|
|
63
|
-
children: c
|
|
64
|
-
}, n.key);
|
|
65
|
-
};
|
|
35
|
+
let v = r(), y = o(c), b = typeof (f ? m : p) == "number" ? `${f ? m : p}px` : f ? m : p;
|
|
66
36
|
return /* @__PURE__ */ a("aside", {
|
|
67
37
|
className: e(t.sidebar, f && t.collapsed, h),
|
|
68
38
|
style: {
|
|
@@ -78,24 +48,34 @@ function s({ header: s, items: c, value: l, onChange: u, footer: d, collapsed: f
|
|
|
78
48
|
/* @__PURE__ */ i("nav", {
|
|
79
49
|
className: t.nav,
|
|
80
50
|
"aria-label": "Navegação lateral",
|
|
81
|
-
children: y.map((
|
|
82
|
-
if (
|
|
83
|
-
if (!
|
|
51
|
+
children: y.map((r) => {
|
|
52
|
+
if (r.kind === "separator") return /* @__PURE__ */ i("hr", { className: t.separator }, r.key);
|
|
53
|
+
if (!r.section) return /* @__PURE__ */ i("div", {
|
|
84
54
|
className: t.group,
|
|
85
|
-
children:
|
|
86
|
-
|
|
87
|
-
|
|
55
|
+
children: r.items.map((e) => /* @__PURE__ */ i(n, {
|
|
56
|
+
item: e,
|
|
57
|
+
active: e.key === l,
|
|
58
|
+
collapsed: f,
|
|
59
|
+
onSelect: u
|
|
60
|
+
}, e.key))
|
|
61
|
+
}, r.key);
|
|
62
|
+
let o = `${v}-${r.key}`;
|
|
88
63
|
return /* @__PURE__ */ a("div", {
|
|
89
64
|
role: "group",
|
|
90
|
-
"aria-labelledby":
|
|
65
|
+
"aria-labelledby": o,
|
|
91
66
|
className: t.group,
|
|
92
67
|
children: [/* @__PURE__ */ i("div", {
|
|
93
|
-
id:
|
|
68
|
+
id: o,
|
|
94
69
|
role: "presentation",
|
|
95
70
|
className: e(t.section, f && t.sectionCollapsed),
|
|
96
|
-
children:
|
|
97
|
-
}),
|
|
98
|
-
|
|
71
|
+
children: r.section.label
|
|
72
|
+
}), r.items.map((e) => /* @__PURE__ */ i(n, {
|
|
73
|
+
item: e,
|
|
74
|
+
active: e.key === l,
|
|
75
|
+
collapsed: f,
|
|
76
|
+
onSelect: u
|
|
77
|
+
}, e.key))]
|
|
78
|
+
}, r.key);
|
|
99
79
|
})
|
|
100
80
|
}),
|
|
101
81
|
d && /* @__PURE__ */ i("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.js","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — header, items, footer are the three regions,\n * value/onChange the selection, and collapsed/width/collapsedWidth the two layout\n * modes it switches between.\n */\nimport { useId, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Sidebar.module.css\";\n\nexport interface SidebarItem {\n key: string;\n label: ReactNode;\n icon?: ReactNode;\n badge?: ReactNode;\n disabled?: boolean;\n /**\n * Renders the entry as a link to this URL instead of a button.\n *\n * `onChange` still fires on click, so an app that tracks the active key keeps\n * working — the difference is that the entry now behaves like navigation:\n * middle-click, ctrl-click and \"copy link address\" do what the user expects,\n * and a screen reader announces a link rather than a button.\n *\n * A `disabled` entry ignores this and stays a `<button disabled>`: there is no\n * disabled state for an anchor, and dropping the `href` to fake one leaves a\n * link that announces itself as actionable and is not.\n */\n href?: string;\n}\n\n/**\n * A section heading. Every item after it belongs to the section, until the next\n * section or separator.\n */\nexport interface SidebarSection {\n type: \"section\";\n key: string;\n label: ReactNode;\n}\n\n/** A plain divider, for splitting the list without naming the parts. */\nexport interface SidebarSeparator {\n type: \"separator\";\n key: string;\n}\n\n/**\n * One entry of the navigation list.\n *\n * An entry with no `type` is an item, which is what keeps a plain\n * `SidebarItem[]` a valid `SidebarEntry[]`: adding sections to this component\n * needed no change at any existing call site.\n */\nexport type SidebarEntry = ({ type?: \"item\" } & SidebarItem) | SidebarSection | SidebarSeparator;\n\nexport interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n /** Top slot — typically the logo + brand. */\n header?: ReactNode;\n /** Navigation entries. An entry with no `type` is an item. */\n items: SidebarEntry[];\n /** Active item key. */\n value?: string;\n /** Fires when an item is clicked. Receives the item's `key`. */\n onChange?: (key: string) => void;\n /** Bottom slot — typically settings/profile/logout. */\n footer?: ReactNode;\n /** Collapsed mode — only icons visible. Default `false`. */\n collapsed?: boolean;\n /** Width when expanded, in pixels or any CSS length. Default `240px`. */\n width?: number | string;\n /** Width when collapsed, in pixels or any CSS length. Default `64px`. */\n collapsedWidth?: number | string;\n}\n\n/** A run of items, optionally under a section heading. */\ninterface ItemBlock {\n kind: \"items\";\n key: string;\n section?: SidebarSection;\n items: SidebarItem[];\n}\n\n/** A standalone divider between blocks. */\ninterface SeparatorBlock {\n kind: \"separator\";\n key: string;\n}\n\ntype Block = ItemBlock | SeparatorBlock;\n\n/**\n * Fold the flat entry list into the blocks that get rendered.\n *\n * A section opens a block that swallows every following item; a separator closes\n * whatever is open, so items after it are loose again. Items before the first\n * section are loose too — which is the whole existing behaviour, and why a list\n * with no sections comes out of here as one unnamed block.\n *\n * @param entries - The `items` prop, as given.\n * @returns Blocks in render order.\n */\nfunction toBlocks(entries: readonly SidebarEntry[]): Block[] {\n const blocks: Block[] = [];\n let open: ItemBlock | undefined;\n\n for (const entry of entries) {\n if (entry.type === \"separator\") {\n blocks.push({ kind: \"separator\", key: entry.key });\n open = undefined;\n continue;\n }\n if (entry.type === \"section\") {\n open = { kind: \"items\", key: entry.key, section: entry, items: [] };\n blocks.push(open);\n continue;\n }\n if (!open) {\n open = { kind: \"items\", key: `loose-${entry.key}`, items: [] };\n blocks.push(open);\n }\n open.items.push(entry);\n }\n return blocks;\n}\n\n/**\n * Desktop sidebar navigation. Pair with `<Show above=\"md\">` and a `Drawer`\n * for mobile.\n *\n * Sixteen screens in one flat list is a wall, so `items` also takes section\n * headings and separators. A section renders as a labelled `role=\"group\"`, which\n * is what tells a screen reader \"Monitoring, group, 3 items\" — the reason it is\n * not a styled `disabled` item, which would announce an unavailable button and\n * stay in the navigation order.\n *\n * @example\n * const [tab, setTab] = useState(\"home\");\n * <Show above=\"md\">\n * <Sidebar\n * header={<Brand />}\n * items={[{ key: \"home\", label: \"Home\", icon: <Home /> }]}\n * value={tab}\n * onChange={setTab}\n * />\n * </Show>\n *\n * @example\n * <Sidebar\n * items={[\n * { type: \"section\", key: \"monitoring\", label: \"Monitoring\" },\n * { key: \"overview\", label: \"Overview\", href: \"/overview\" },\n * { key: \"activity\", label: \"Activity\", href: \"/activity\" },\n * { type: \"section\", key: \"admin\", label: \"Administration\" },\n * { key: \"settings\", label: \"Settings\", href: \"/settings\" },\n * ]}\n * value={tab}\n * onChange={setTab}\n * />\n */\nexport function Sidebar({\n header,\n items,\n value,\n onChange,\n footer,\n collapsed = false,\n width = 240,\n collapsedWidth = 64,\n className,\n style,\n ...props\n}: SidebarProps) {\n const baseId = useId();\n const blocks = toBlocks(items);\n const finalWidth =\n typeof (collapsed ? collapsedWidth : width) === \"number\"\n ? `${collapsed ? collapsedWidth : width}px`\n : collapsed\n ? collapsedWidth\n : width;\n\n /**\n * Render one navigation entry.\n *\n * @param item - The entry to render.\n * @returns The button, or the anchor when the entry carries an `href`.\n */\n const renderItem = (item: SidebarItem): ReactNode => {\n const active = item.key === value;\n const title = collapsed && typeof item.label === \"string\" ? item.label : undefined;\n const body = (\n <>\n {item.icon && <span className={styles.icon}>{item.icon}</span>}\n {!collapsed && <span className={styles.label}>{item.label}</span>}\n {!collapsed && item.badge !== undefined && (\n <span className={styles.badge}>{item.badge}</span>\n )}\n </>\n );\n\n if (item.href && !item.disabled) {\n return (\n <a\n key={item.key}\n href={item.href}\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n onClick={() => onChange?.(item.key)}\n title={title}\n >\n {body}\n </a>\n );\n }\n\n return (\n <button\n key={item.key}\n type=\"button\"\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n disabled={item.disabled}\n onClick={() => onChange?.(item.key)}\n title={title}\n >\n {body}\n </button>\n );\n };\n\n return (\n <aside\n className={cn(styles.sidebar, collapsed && styles.collapsed, className)}\n style={{ width: finalWidth, ...style }}\n {...props}\n >\n {header && <div className={styles.header}>{header}</div>}\n <nav className={styles.nav} aria-label=\"Navegação lateral\">\n {blocks.map((block) => {\n if (block.kind === \"separator\") {\n return <hr key={block.key} className={styles.separator} />;\n }\n if (!block.section) {\n return (\n <div key={block.key} className={styles.group}>\n {block.items.map(renderItem)}\n </div>\n );\n }\n const labelId = `${baseId}-${block.key}`;\n return (\n <div\n key={block.key}\n role=\"group\"\n aria-labelledby={labelId}\n className={styles.group}\n >\n <div\n id={labelId}\n role=\"presentation\"\n className={cn(styles.section, collapsed && styles.sectionCollapsed)}\n >\n {block.section.label}\n </div>\n {block.items.map(renderItem)}\n </div>\n );\n })}\n </nav>\n {footer && <div className={styles.footer}>{footer}</div>}\n </aside>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Sidebar.js","names":[],"sources":["../../../src/components/Sidebar/Sidebar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — header, items, footer are the three regions,\n * value/onChange the selection, and collapsed/width/collapsedWidth the two layout\n * modes it switches between.\n */\nimport { useId, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Sidebar.module.css\";\nimport { SidebarEntry } from \"./SidebarEntry\";\n\nexport interface SidebarItem {\n key: string;\n label: ReactNode;\n icon?: ReactNode;\n badge?: ReactNode;\n disabled?: boolean;\n /**\n * Renders the entry as a link to this URL instead of a button.\n *\n * `onChange` still fires on click, so an app that tracks the active key keeps\n * working — the difference is that the entry now behaves like navigation:\n * middle-click, ctrl-click and \"copy link address\" do what the user expects,\n * and a screen reader announces a link rather than a button.\n *\n * A `disabled` entry ignores this and stays a `<button disabled>`: there is no\n * disabled state for an anchor, and dropping the `href` to fake one leaves a\n * link that announces itself as actionable and is not.\n */\n href?: string;\n}\n\n/**\n * A section heading. Every item after it belongs to the section, until the next\n * section or separator.\n */\nexport interface SidebarSection {\n type: \"section\";\n key: string;\n label: ReactNode;\n}\n\n/** A plain divider, for splitting the list without naming the parts. */\nexport interface SidebarSeparator {\n type: \"separator\";\n key: string;\n}\n\n/**\n * One entry of the navigation list.\n *\n * An entry with no `type` is an item, which is what keeps a plain\n * `SidebarItem[]` a valid `SidebarEntry[]`: adding sections to this component\n * needed no change at any existing call site.\n */\nexport type SidebarEntry = ({ type?: \"item\" } & SidebarItem) | SidebarSection | SidebarSeparator;\n\nexport interface SidebarProps extends Omit<HTMLAttributes<HTMLElement>, \"onChange\"> {\n /** Top slot — typically the logo + brand. */\n header?: ReactNode;\n /** Navigation entries. An entry with no `type` is an item. */\n items: SidebarEntry[];\n /** Active item key. */\n value?: string;\n /** Fires when an item is clicked. Receives the item's `key`. */\n onChange?: (key: string) => void;\n /** Bottom slot — typically settings/profile/logout. */\n footer?: ReactNode;\n /** Collapsed mode — only icons visible. Default `false`. */\n collapsed?: boolean;\n /** Width when expanded, in pixels or any CSS length. Default `240px`. */\n width?: number | string;\n /** Width when collapsed, in pixels or any CSS length. Default `64px`. */\n collapsedWidth?: number | string;\n}\n\n/** A run of items, optionally under a section heading. */\ninterface ItemBlock {\n kind: \"items\";\n key: string;\n section?: SidebarSection;\n items: SidebarItem[];\n}\n\n/** A standalone divider between blocks. */\ninterface SeparatorBlock {\n kind: \"separator\";\n key: string;\n}\n\ntype Block = ItemBlock | SeparatorBlock;\n\n/**\n * Fold the flat entry list into the blocks that get rendered.\n *\n * A section opens a block that swallows every following item; a separator closes\n * whatever is open, so items after it are loose again. Items before the first\n * section are loose too — which is the whole existing behaviour, and why a list\n * with no sections comes out of here as one unnamed block.\n *\n * @param entries - The `items` prop, as given.\n * @returns Blocks in render order.\n */\nfunction toBlocks(entries: readonly SidebarEntry[]): Block[] {\n const blocks: Block[] = [];\n let open: ItemBlock | undefined;\n\n for (const entry of entries) {\n if (entry.type === \"separator\") {\n blocks.push({ kind: \"separator\", key: entry.key });\n open = undefined;\n continue;\n }\n if (entry.type === \"section\") {\n open = { kind: \"items\", key: entry.key, section: entry, items: [] };\n blocks.push(open);\n continue;\n }\n if (!open) {\n open = { kind: \"items\", key: `loose-${entry.key}`, items: [] };\n blocks.push(open);\n }\n open.items.push(entry);\n }\n return blocks;\n}\n\n/**\n * Desktop sidebar navigation. Pair with `<Show above=\"md\">` and a `Drawer`\n * for mobile.\n *\n * Sixteen screens in one flat list is a wall, so `items` also takes section\n * headings and separators. A section renders as a labelled `role=\"group\"`, which\n * is what tells a screen reader \"Monitoring, group, 3 items\" — the reason it is\n * not a styled `disabled` item, which would announce an unavailable button and\n * stay in the navigation order.\n *\n * @example\n * const [tab, setTab] = useState(\"home\");\n * <Show above=\"md\">\n * <Sidebar\n * header={<Brand />}\n * items={[{ key: \"home\", label: \"Home\", icon: <Home /> }]}\n * value={tab}\n * onChange={setTab}\n * />\n * </Show>\n *\n * @example\n * <Sidebar\n * items={[\n * { type: \"section\", key: \"monitoring\", label: \"Monitoring\" },\n * { key: \"overview\", label: \"Overview\", href: \"/overview\" },\n * { key: \"activity\", label: \"Activity\", href: \"/activity\" },\n * { type: \"section\", key: \"admin\", label: \"Administration\" },\n * { key: \"settings\", label: \"Settings\", href: \"/settings\" },\n * ]}\n * value={tab}\n * onChange={setTab}\n * />\n */\nexport function Sidebar({\n header,\n items,\n value,\n onChange,\n footer,\n collapsed = false,\n width = 240,\n collapsedWidth = 64,\n className,\n style,\n ...props\n}: SidebarProps) {\n const baseId = useId();\n const blocks = toBlocks(items);\n const finalWidth =\n typeof (collapsed ? collapsedWidth : width) === \"number\"\n ? `${collapsed ? collapsedWidth : width}px`\n : collapsed\n ? collapsedWidth\n : width;\n\n return (\n <aside\n className={cn(styles.sidebar, collapsed && styles.collapsed, className)}\n style={{ width: finalWidth, ...style }}\n {...props}\n >\n {header && <div className={styles.header}>{header}</div>}\n <nav className={styles.nav} aria-label=\"Navegação lateral\">\n {blocks.map((block) => {\n if (block.kind === \"separator\") {\n return <hr key={block.key} className={styles.separator} />;\n }\n if (!block.section) {\n return (\n <div key={block.key} className={styles.group}>\n {block.items.map((item) => (\n <SidebarEntry\n key={item.key}\n item={item}\n active={item.key === value}\n collapsed={collapsed}\n onSelect={onChange}\n />\n ))}\n </div>\n );\n }\n const labelId = `${baseId}-${block.key}`;\n return (\n <div\n key={block.key}\n role=\"group\"\n aria-labelledby={labelId}\n className={styles.group}\n >\n <div\n id={labelId}\n role=\"presentation\"\n className={cn(styles.section, collapsed && styles.sectionCollapsed)}\n >\n {block.section.label}\n </div>\n {block.items.map((item) => (\n <SidebarEntry\n key={item.key}\n item={item}\n active={item.key === value}\n collapsed={collapsed}\n onSelect={onChange}\n />\n ))}\n </div>\n );\n })}\n </nav>\n {footer && <div className={styles.footer}>{footer}</div>}\n </aside>\n );\n}\n"],"mappings":";;;;;;AAsGA,SAAS,EAAS,GAA2C;CACzD,IAAM,IAAkB,CAAC,GACrB;CAEJ,KAAK,IAAM,KAAS,GAAS;EACzB,IAAI,EAAM,SAAS,aAAa;GAE5B,AADA,EAAO,KAAK;IAAE,MAAM;IAAa,KAAK,EAAM;GAAI,CAAC,GACjD,IAAO,KAAA;GACP;EACJ;EACA,IAAI,EAAM,SAAS,WAAW;GAE1B,AADA,IAAO;IAAE,MAAM;IAAS,KAAK,EAAM;IAAK,SAAS;IAAO,OAAO,CAAC;GAAE,GAClE,EAAO,KAAK,CAAI;GAChB;EACJ;EAKA,AAJK,MACD,IAAO;GAAE,MAAM;GAAS,KAAK,SAAS,EAAM;GAAO,OAAO,CAAC;EAAE,GAC7D,EAAO,KAAK,CAAI,IAEpB,EAAK,MAAM,KAAK,CAAK;CACzB;CACA,OAAO;AACX;AAoCA,SAAgB,EAAQ,EACpB,WACA,UACA,UACA,aACA,WACA,eAAY,IACZ,WAAQ,KACR,oBAAiB,IACjB,cACA,UACA,GAAG,KACU;CACb,IAAM,IAAS,EAAM,GACf,IAAS,EAAS,CAAK,GACvB,IACF,QAAQ,IAAY,IAAiB,MAAW,WAC1C,GAAG,IAAY,IAAiB,EAAM,MACtC,IACE,IACA;CAEZ,OACI,kBAAC,SAAD;EACI,WAAW,EAAG,EAAO,SAAS,KAAa,EAAO,WAAW,CAAS;EACtE,OAAO;GAAE,OAAO;GAAY,GAAG;EAAM;EACrC,GAAI;EAHR,UAAA;GAKK,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;GACvD,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAK,cAAW;IAClC,UAAA,EAAO,KAAK,MAAU;KACnB,IAAI,EAAM,SAAS,aACf,OAAO,kBAAC,MAAD,EAAoB,WAAW,EAAO,UAAY,GAAzC,EAAM,GAAmC;KAE7D,IAAI,CAAC,EAAM,SACP,OACI,kBAAC,OAAD;MAAqB,WAAW,EAAO;MAClC,UAAA,EAAM,MAAM,KAAK,MACd,kBAAC,GAAD;OAEU;OACN,QAAQ,EAAK,QAAQ;OACV;OACX,UAAU;MACb,GALQ,EAAK,GAKb,CACJ;KACA,GAVK,EAAM,GAUX;KAGb,IAAM,IAAU,GAAG,EAAO,GAAG,EAAM;KACnC,OACI,kBAAC,OAAD;MAEI,MAAK;MACL,mBAAiB;MACjB,WAAW,EAAO;MAJtB,UAAA,CAMI,kBAAC,OAAD;OACI,IAAI;OACJ,MAAK;OACL,WAAW,EAAG,EAAO,SAAS,KAAa,EAAO,gBAAgB;OAEjE,UAAA,EAAM,QAAQ;MACd,CAAA,GACJ,EAAM,MAAM,KAAK,MACd,kBAAC,GAAD;OAEU;OACN,QAAQ,EAAK,QAAQ;OACV;OACX,UAAU;MACb,GALQ,EAAK,GAKb,CACJ,CACA;KArBI,GAAA,EAAM,GAqBV;IAEb,CAAC;GACA,CAAA;GACJ,KAAU,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAS,UAAA;GAAY,CAAA;EACpD;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.module.cjs","names":[],"sources":["../../../src/components/Sidebar/Sidebar.module.css"],"sourcesContent":[".sidebar {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--tempest-surface);\n border-right: 1px solid var(--tempest-border);\n font-family: var(--tempest-font-sans);\n transition: width var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n overflow: hidden;\n}\n\n.header {\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n min-height: 56px;\n display: flex;\n align-items: center;\n}\n\n.nav {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: var(--tempest-space-2);\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-3);\n}\n\n.collapsed .group + .group {\n margin-top: var(--tempest-space-2);\n padding-top: var(--tempest-space-2);\n border-top: 1px solid var(--tempest-border);\n}\n\n.section {\n padding: var(--tempest-space-2) var(--tempest-space-3) var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sectionCollapsed {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n.separator {\n flex: 0 0 auto;\n margin: var(--tempest-space-2) var(--tempest-space-1);\n border: 0;\n border-top: 1px solid var(--tempest-border);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: 0;\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n transition: background var(--tempest-duration-fast, 120ms);\n min-height: 36px;\n width: 100%;\n}\n\n.collapsed .item {\n justify-content: center;\n padding-left: 0;\n padding-right: 0;\n}\n\n.item
|
|
1
|
+
{"version":3,"file":"Sidebar.module.cjs","names":[],"sources":["../../../src/components/Sidebar/Sidebar.module.css"],"sourcesContent":[".sidebar {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--tempest-surface);\n border-right: 1px solid var(--tempest-border);\n font-family: var(--tempest-font-sans);\n transition: width var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n overflow: hidden;\n}\n\n.header {\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n min-height: 56px;\n display: flex;\n align-items: center;\n}\n\n.nav {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: var(--tempest-space-2);\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-3);\n}\n\n.collapsed .group + .group {\n margin-top: var(--tempest-space-2);\n padding-top: var(--tempest-space-2);\n border-top: 1px solid var(--tempest-border);\n}\n\n.section {\n padding: var(--tempest-space-2) var(--tempest-space-3) var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sectionCollapsed {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n.separator {\n flex: 0 0 auto;\n margin: var(--tempest-space-2) var(--tempest-space-1);\n border: 0;\n border-top: 1px solid var(--tempest-border);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: 0;\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n transition: background var(--tempest-duration-fast, 120ms);\n min-height: 36px;\n width: 100%;\n text-decoration: none;\n}\n\n.collapsed .item {\n justify-content: center;\n padding-left: 0;\n padding-right: 0;\n}\n\n.item:hover {\n background: var(--tempest-gray-100);\n}\n\n.item.active {\n background: var(--tempest-primary-100);\n color: var(--tempest-primary-700);\n font-weight: var(--tempest-weight-medium);\n}\n\n.item:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.icon {\n display: inline-flex;\n align-items: center;\n font-size: 18px;\n line-height: 1;\n flex: 0 0 auto;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.badge {\n flex: 0 0 auto;\n min-width: 18px;\n height: 18px;\n padding: 0 6px;\n border-radius: var(--tempest-radius-full);\n background: var(--tempest-gray-200);\n color: var(--tempest-text);\n font-size: 10px;\n line-height: 18px;\n text-align: center;\n}\n\n.footer {\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sidebar.module.js","names":[],"sources":["../../../src/components/Sidebar/Sidebar.module.css"],"sourcesContent":[".sidebar {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--tempest-surface);\n border-right: 1px solid var(--tempest-border);\n font-family: var(--tempest-font-sans);\n transition: width var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n overflow: hidden;\n}\n\n.header {\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n min-height: 56px;\n display: flex;\n align-items: center;\n}\n\n.nav {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: var(--tempest-space-2);\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-3);\n}\n\n.collapsed .group + .group {\n margin-top: var(--tempest-space-2);\n padding-top: var(--tempest-space-2);\n border-top: 1px solid var(--tempest-border);\n}\n\n.section {\n padding: var(--tempest-space-2) var(--tempest-space-3) var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sectionCollapsed {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n.separator {\n flex: 0 0 auto;\n margin: var(--tempest-space-2) var(--tempest-space-1);\n border: 0;\n border-top: 1px solid var(--tempest-border);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: 0;\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n transition: background var(--tempest-duration-fast, 120ms);\n min-height: 36px;\n width: 100%;\n}\n\n.collapsed .item {\n justify-content: center;\n padding-left: 0;\n padding-right: 0;\n}\n\n.item
|
|
1
|
+
{"version":3,"file":"Sidebar.module.js","names":[],"sources":["../../../src/components/Sidebar/Sidebar.module.css"],"sourcesContent":[".sidebar {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--tempest-surface);\n border-right: 1px solid var(--tempest-border);\n font-family: var(--tempest-font-sans);\n transition: width var(--tempest-duration-base, 200ms) var(--tempest-ease-out, ease-out);\n overflow: hidden;\n}\n\n.header {\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n min-height: 56px;\n display: flex;\n align-items: center;\n}\n\n.nav {\n flex: 1 1 auto;\n overflow-y: auto;\n padding: var(--tempest-space-2);\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.group + .group {\n margin-top: var(--tempest-space-3);\n}\n\n.collapsed .group + .group {\n margin-top: var(--tempest-space-2);\n padding-top: var(--tempest-space-2);\n border-top: 1px solid var(--tempest-border);\n}\n\n.section {\n padding: var(--tempest-space-2) var(--tempest-space-3) var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sectionCollapsed {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n}\n\n.separator {\n flex: 0 0 auto;\n margin: var(--tempest-space-2) var(--tempest-space-1);\n border: 0;\n border-top: 1px solid var(--tempest-border);\n}\n\n.item {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n background: none;\n border: 0;\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n font-size: var(--tempest-text-sm);\n text-align: left;\n cursor: pointer;\n transition: background var(--tempest-duration-fast, 120ms);\n min-height: 36px;\n width: 100%;\n text-decoration: none;\n}\n\n.collapsed .item {\n justify-content: center;\n padding-left: 0;\n padding-right: 0;\n}\n\n.item:hover {\n background: var(--tempest-gray-100);\n}\n\n.item.active {\n background: var(--tempest-primary-100);\n color: var(--tempest-primary-700);\n font-weight: var(--tempest-weight-medium);\n}\n\n.item:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.icon {\n display: inline-flex;\n align-items: center;\n font-size: 18px;\n line-height: 1;\n flex: 0 0 auto;\n}\n\n.label {\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.badge {\n flex: 0 0 auto;\n min-width: 18px;\n height: 18px;\n padding: 0 6px;\n border-radius: var(--tempest-radius-full);\n background: var(--tempest-gray-200);\n color: var(--tempest-text);\n font-size: 10px;\n line-height: 18px;\n text-align: center;\n}\n\n.footer {\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./Sidebar.module.cjs");let n=require("react/jsx-runtime");function r({item:r,active:i,collapsed:a,onSelect:o}){let s=a&&typeof r.label==`string`?r.label:void 0,c=(0,n.jsxs)(n.Fragment,{children:[r.icon&&(0,n.jsx)(`span`,{className:t.default.icon,children:r.icon}),!a&&(0,n.jsx)(`span`,{className:t.default.label,children:r.label}),!a&&r.badge!==void 0&&(0,n.jsx)(`span`,{className:t.default.badge,children:r.badge})]});return r.href&&!r.disabled?(0,n.jsx)(`a`,{href:r.href,className:e.cn(t.default.item,i&&t.default.active),"aria-current":i?`page`:void 0,onClick:()=>o?.(r.key),title:s,children:c}):(0,n.jsx)(`button`,{type:`button`,className:e.cn(t.default.item,i&&t.default.active),"aria-current":i?`page`:void 0,disabled:r.disabled,onClick:()=>o?.(r.key),title:s,children:c})}exports.SidebarEntry=r;
|
|
2
|
+
//# sourceMappingURL=SidebarEntry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SidebarEntry.cjs","names":[],"sources":["../../../src/components/Sidebar/SidebarEntry.tsx"],"sourcesContent":["import { cn } from \"@/utils/cn\";\n\nimport type { SidebarItem } from \"./Sidebar\";\nimport styles from \"./Sidebar.module.css\";\n\n/**\n * One navigation entry: an anchor when it carries an `href`, a button otherwise.\n *\n * A link that navigates has to be a link — middle-click, \"open in new tab\" and\n * the status bar preview all come from the element, not from the handler. A\n * disabled entry falls back to the button, because a disabled anchor does not\n * exist in HTML.\n */\nexport function SidebarEntry({\n item,\n active,\n collapsed,\n onSelect,\n}: {\n item: SidebarItem;\n active: boolean;\n collapsed: boolean;\n onSelect?: (key: string) => void;\n}) {\n const title = collapsed && typeof item.label === \"string\" ? item.label : undefined;\n const body = (\n <>\n {item.icon && <span className={styles.icon}>{item.icon}</span>}\n {!collapsed && <span className={styles.label}>{item.label}</span>}\n {!collapsed && item.badge !== undefined && (\n <span className={styles.badge}>{item.badge}</span>\n )}\n </>\n );\n\n if (item.href && !item.disabled) {\n return (\n <a\n href={item.href}\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n onClick={() => onSelect?.(item.key)}\n title={title}\n >\n {body}\n </a>\n );\n }\n\n return (\n <button\n type=\"button\"\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n disabled={item.disabled}\n onClick={() => onSelect?.(item.key)}\n title={title}\n >\n {body}\n </button>\n );\n}\n"],"mappings":"2GAaA,SAAgB,EAAa,CACzB,OACA,SACA,YACA,YAMD,CACC,IAAM,EAAQ,GAAa,OAAO,EAAK,OAAU,SAAW,EAAK,MAAQ,IAAA,GACnE,GACF,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,EAAK,OAAQ,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,KAAO,SAAA,EAAK,IAAW,CAAA,EAC5D,CAAC,IAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,EAAK,KAAY,CAAA,EAC/D,CAAC,GAAa,EAAK,QAAU,IAAA,KAC1B,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,EAAK,KAAY,CAAA,CAEvD,CAAA,CAAA,EAiBN,OAdI,EAAK,MAAQ,CAAC,EAAK,UAEf,EAAA,EAAA,IAAA,CAAC,IAAD,CACI,KAAM,EAAK,KACX,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAU,EAAA,QAAO,MAAM,EAClD,eAAc,EAAS,OAAS,IAAA,GAChC,YAAe,IAAW,EAAK,GAAG,EAC3B,QAEN,SAAA,CACF,CAAA,GAKP,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,GAAU,EAAA,QAAO,MAAM,EAClD,eAAc,EAAS,OAAS,IAAA,GAChC,SAAU,EAAK,SACf,YAAe,IAAW,EAAK,GAAG,EAC3B,QAEN,SAAA,CACG,CAAA,CAEhB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import t from "./Sidebar.module.js";
|
|
3
|
+
import { Fragment as n, jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/Sidebar/SidebarEntry.tsx
|
|
5
|
+
function a({ item: a, active: o, collapsed: s, onSelect: c }) {
|
|
6
|
+
let l = s && typeof a.label == "string" ? a.label : void 0, u = /* @__PURE__ */ i(n, { children: [
|
|
7
|
+
a.icon && /* @__PURE__ */ r("span", {
|
|
8
|
+
className: t.icon,
|
|
9
|
+
children: a.icon
|
|
10
|
+
}),
|
|
11
|
+
!s && /* @__PURE__ */ r("span", {
|
|
12
|
+
className: t.label,
|
|
13
|
+
children: a.label
|
|
14
|
+
}),
|
|
15
|
+
!s && a.badge !== void 0 && /* @__PURE__ */ r("span", {
|
|
16
|
+
className: t.badge,
|
|
17
|
+
children: a.badge
|
|
18
|
+
})
|
|
19
|
+
] });
|
|
20
|
+
return a.href && !a.disabled ? /* @__PURE__ */ r("a", {
|
|
21
|
+
href: a.href,
|
|
22
|
+
className: e(t.item, o && t.active),
|
|
23
|
+
"aria-current": o ? "page" : void 0,
|
|
24
|
+
onClick: () => c?.(a.key),
|
|
25
|
+
title: l,
|
|
26
|
+
children: u
|
|
27
|
+
}) : /* @__PURE__ */ r("button", {
|
|
28
|
+
type: "button",
|
|
29
|
+
className: e(t.item, o && t.active),
|
|
30
|
+
"aria-current": o ? "page" : void 0,
|
|
31
|
+
disabled: a.disabled,
|
|
32
|
+
onClick: () => c?.(a.key),
|
|
33
|
+
title: l,
|
|
34
|
+
children: u
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { a as SidebarEntry };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=SidebarEntry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SidebarEntry.js","names":[],"sources":["../../../src/components/Sidebar/SidebarEntry.tsx"],"sourcesContent":["import { cn } from \"@/utils/cn\";\n\nimport type { SidebarItem } from \"./Sidebar\";\nimport styles from \"./Sidebar.module.css\";\n\n/**\n * One navigation entry: an anchor when it carries an `href`, a button otherwise.\n *\n * A link that navigates has to be a link — middle-click, \"open in new tab\" and\n * the status bar preview all come from the element, not from the handler. A\n * disabled entry falls back to the button, because a disabled anchor does not\n * exist in HTML.\n */\nexport function SidebarEntry({\n item,\n active,\n collapsed,\n onSelect,\n}: {\n item: SidebarItem;\n active: boolean;\n collapsed: boolean;\n onSelect?: (key: string) => void;\n}) {\n const title = collapsed && typeof item.label === \"string\" ? item.label : undefined;\n const body = (\n <>\n {item.icon && <span className={styles.icon}>{item.icon}</span>}\n {!collapsed && <span className={styles.label}>{item.label}</span>}\n {!collapsed && item.badge !== undefined && (\n <span className={styles.badge}>{item.badge}</span>\n )}\n </>\n );\n\n if (item.href && !item.disabled) {\n return (\n <a\n href={item.href}\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n onClick={() => onSelect?.(item.key)}\n title={title}\n >\n {body}\n </a>\n );\n }\n\n return (\n <button\n type=\"button\"\n className={cn(styles.item, active && styles.active)}\n aria-current={active ? \"page\" : undefined}\n disabled={item.disabled}\n onClick={() => onSelect?.(item.key)}\n title={title}\n >\n {body}\n </button>\n );\n}\n"],"mappings":";;;;AAaA,SAAgB,EAAa,EACzB,SACA,WACA,cACA,eAMD;CACC,IAAM,IAAQ,KAAa,OAAO,EAAK,SAAU,WAAW,EAAK,QAAQ,KAAA,GACnE,IACF,kBAAA,GAAA,EAAA,UAAA;EACK,EAAK,QAAQ,kBAAC,QAAD;GAAM,WAAW,EAAO;GAAO,UAAA,EAAK;EAAW,CAAA;EAC5D,CAAC,KAAa,kBAAC,QAAD;GAAM,WAAW,EAAO;GAAQ,UAAA,EAAK;EAAY,CAAA;EAC/D,CAAC,KAAa,EAAK,UAAU,KAAA,KAC1B,kBAAC,QAAD;GAAM,WAAW,EAAO;GAAQ,UAAA,EAAK;EAAY,CAAA;CAEvD,EAAA,CAAA;CAiBN,OAdI,EAAK,QAAQ,CAAC,EAAK,WAEf,kBAAC,KAAD;EACI,MAAM,EAAK;EACX,WAAW,EAAG,EAAO,MAAM,KAAU,EAAO,MAAM;EAClD,gBAAc,IAAS,SAAS,KAAA;EAChC,eAAe,IAAW,EAAK,GAAG;EAC3B;EAEN,UAAA;CACF,CAAA,IAKP,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAG,EAAO,MAAM,KAAU,EAAO,MAAM;EAClD,gBAAc,IAAS,SAAS,KAAA;EAChC,UAAU,EAAK;EACf,eAAe,IAAW,EAAK,GAAG;EAC3B;EAEN,UAAA;CACG,CAAA;AAEhB"}
|
package/dist/http/api-client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./errors.cjs"),r=require("./retry.cjs");var
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./errors.cjs"),r=require("./retry.cjs"),i=require("./timeout.cjs");var a=new Set([`GET`,`HEAD`,`OPTIONS`]);function o(e,t){return!a.has(t)||!(e instanceof n.TempestApiError)?!1:n.isRetriableStatus(e.status)}function s(e){return e?e===!0?{}:e:null}var c=15e3,l=3e5;function u(e){return typeof FormData<`u`&&e instanceof FormData}function d(e){return Math.round(performance.now()-e)}async function f(e,t){let r;try{r=await e.clone().json()}catch{try{r=await e.text()}catch{r=null}}return new n.TempestApiError(n.buildApiError(e.status,r,e.headers,t))}function p(a){let p=a.fetcher??globalThis.fetch.bind(globalThis);function m(){let e=a.getToken?.();return e?{Authorization:`Bearer ${e}`}:{}}async function h(e,r,o){let{body:s,params:d,headers:f,signal:h,timeout:g,..._}=r,v=u(s),y=v?a.uploadTimeout:a.timeout,b=g===void 0?y===void 0?v?l:c:y:g,x={...v?{}:{"Content-Type":`application/json`},...o?{"X-Request-ID":o}:{},...a.headers,...m(),...f},S=i.withTimeout(h,b),C={..._,signal:S.signal,headers:x,credentials:a.withCredentials?`include`:_.credentials,body:s==null?void 0:v?s:JSON.stringify(s)},w=t.buildApiUrl(a.baseURL,e,{prefix:a.prefix,params:d});try{return await p(w,C)}catch(e){throw S.timedOut()?new n.TempestApiError({status:0,detail:`A requisição excedeu ${b}ms e foi abandonada.`}):e}finally{S.dispose()}}async function g(e,t,n,r){let i=a.logger;if(!i)return h(e,t,n);let o=performance.now();try{let a=await h(e,t,n),s={requestId:n,status:a.status,ms:d(o)},c=`${r} ${e} → ${a.status}`;return a.status>=400?i.warn(c,s):i.debug(c,s),a}catch(t){throw i.warn(`${r} ${e} → no response`,{requestId:n,ms:d(o),error:t}),t}}async function _(e,t){if(a.onUnauthorized){a.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await a.onUnauthorized(e)}catch(n){a.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function v(t,n){let r=a.requestId?a.requestId():e.randomId(),i=(n.method??`GET`).toUpperCase(),o=await g(t,n,r,i);if(o.status===401){if(a.refresh){try{await a.refresh(),o=await g(t,n,r,i)}catch{throw await _(o,r),await f(o,r)}o.status===401&&await _(o,r)}else await _(o,r)}if(!o.ok)throw await f(o,r);if(o.status!==204)return(o.headers.get(`content-type`)??``).includes(`application/json`)?await o.json():await o.text()}async function y(e,t={}){let n=s(a.retry);if(!n)return v(e,t);let i=(t.method??`GET`).toUpperCase();return r.retry(()=>v(e,t),{...n,shouldRetry:n.shouldRetry??(e=>o(e,i))})}async function b(e,t,n=`POST`,r){return y(e,{...r,method:n,body:t})}return{request:y,get:(e,t)=>y(e,{...t,method:`GET`}),post:(e,t)=>y(e,{...t,method:`POST`}),put:(e,t)=>y(e,{...t,method:`PUT`}),patch:(e,t)=>y(e,{...t,method:`PATCH`}),delete:(e,t)=>y(e,{...t,method:`DELETE`}),upload:b}}exports.createApiClient=p;
|
|
2
2
|
//# sourceMappingURL=api-client.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, ...rest } = options;\n const isForm = isFormData(body);\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const init: RequestInit = {\n ...rest,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n ): Promise<T> {\n return request<T>(path, { method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|