tempest-react-sdk 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/bin/lib/doctor/doctor.e2e.test.mjs +201 -0
  3. package/bin/tempest.mjs +103 -29
  4. package/dist/components/ImageCropper/ImageCropper.cjs +2 -0
  5. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -0
  6. package/dist/components/ImageCropper/ImageCropper.js +240 -0
  7. package/dist/components/ImageCropper/ImageCropper.js.map +1 -0
  8. package/dist/components/ImageCropper/ImageCropper.module.cjs +2 -0
  9. package/dist/components/ImageCropper/ImageCropper.module.cjs.map +1 -0
  10. package/dist/components/ImageCropper/ImageCropper.module.js +15 -0
  11. package/dist/components/ImageCropper/ImageCropper.module.js.map +1 -0
  12. package/dist/components/ImageCropper/crop-geometry.cjs +2 -0
  13. package/dist/components/ImageCropper/crop-geometry.cjs.map +1 -0
  14. package/dist/components/ImageCropper/crop-geometry.js +52 -0
  15. package/dist/components/ImageCropper/crop-geometry.js.map +1 -0
  16. package/dist/components/Scheduler/Scheduler.cjs +2 -0
  17. package/dist/components/Scheduler/Scheduler.cjs.map +1 -0
  18. package/dist/components/Scheduler/Scheduler.js +134 -0
  19. package/dist/components/Scheduler/Scheduler.js.map +1 -0
  20. package/dist/components/Scheduler/Scheduler.module.cjs +2 -0
  21. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -0
  22. package/dist/components/Scheduler/Scheduler.module.js +26 -0
  23. package/dist/components/Scheduler/Scheduler.module.js.map +1 -0
  24. package/dist/components/Scheduler/scheduler-layout.cjs +2 -0
  25. package/dist/components/Scheduler/scheduler-layout.cjs.map +1 -0
  26. package/dist/components/Scheduler/scheduler-layout.js +97 -0
  27. package/dist/components/Scheduler/scheduler-layout.js.map +1 -0
  28. package/dist/styles.css +1 -1
  29. package/dist/tempest-react-sdk.cjs +1 -1
  30. package/dist/tempest-react-sdk.d.ts +162 -0
  31. package/dist/tempest-react-sdk.js +151 -149
  32. package/package.json +1 -1
  33. package/template/package.json +1 -1
@@ -0,0 +1,240 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { clampOffset as t, computeCropRect as n, coverScale as r, outputSize as i } from "./crop-geometry.js";
3
+ import a from "./ImageCropper.module.js";
4
+ import { useCallback as o, useEffect as s, useId as c, useImperativeHandle as l, useMemo as u, useRef as d, useState as f } from "react";
5
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
6
+ //#region src/components/ImageCropper/ImageCropper.tsx
7
+ var h = 12, g = .15;
8
+ function _({ src: _, aspect: v = 1, maxZoom: y = 4, maxSize: b, outputType: x = "image/png", outputQuality: S = .92, shape: C = "rect", onCropChange: w, label: T = "Área de recorte", className: E, ref: D, ...O }) {
9
+ let k = `${c()}-hint`, A = d(null), j = d(null), M = d(null), [N, P] = f(null), [F, I] = f(null), [L, R] = f({
10
+ width: 0,
11
+ height: 0
12
+ }), [z, B] = f(1), [V, H] = f({
13
+ x: 0,
14
+ y: 0
15
+ });
16
+ s(() => {
17
+ if (typeof _ == "string") {
18
+ P(_);
19
+ return;
20
+ }
21
+ let e = URL.createObjectURL(_);
22
+ return P(e), () => URL.revokeObjectURL(e);
23
+ }, [_]), s(() => {
24
+ I(null), B(1), H({
25
+ x: 0,
26
+ y: 0
27
+ });
28
+ }, [N]), s(() => {
29
+ let e = A.current;
30
+ if (!e) return;
31
+ let t = () => R({
32
+ width: e.clientWidth,
33
+ height: e.clientHeight
34
+ });
35
+ if (t(), typeof ResizeObserver > "u") return;
36
+ let n = new ResizeObserver(t);
37
+ return n.observe(e), () => n.disconnect();
38
+ }, []);
39
+ let U = F ? r(F, L) * z : 0, W = u(() => F ? {
40
+ width: F.width * U,
41
+ height: F.height * U
42
+ } : {
43
+ width: 0,
44
+ height: 0
45
+ }, [F, U]), G = o((e) => {
46
+ H((n) => {
47
+ let r = t(e, W, L);
48
+ return r.x === n.x && r.y === n.y ? n : (w?.({
49
+ zoom: z,
50
+ offset: r
51
+ }), r);
52
+ });
53
+ }, [
54
+ W,
55
+ L,
56
+ w,
57
+ z
58
+ ]), K = o((e) => {
59
+ let n = Math.min(y, Math.max(1, e));
60
+ if (B(n), !F) return;
61
+ let i = r(F, L) * n, a = {
62
+ width: F.width * i,
63
+ height: F.height * i
64
+ };
65
+ H((e) => {
66
+ let r = t(e, a, L);
67
+ return w?.({
68
+ zoom: n,
69
+ offset: r
70
+ }), r;
71
+ });
72
+ }, [
73
+ L,
74
+ y,
75
+ F,
76
+ w
77
+ ]), q = o(() => {
78
+ B(1), H({
79
+ x: 0,
80
+ y: 0
81
+ }), w?.({
82
+ zoom: 1,
83
+ offset: {
84
+ x: 0,
85
+ y: 0
86
+ }
87
+ });
88
+ }, [w]), J = o(async () => {
89
+ let e = j.current;
90
+ if (!e || !F) return null;
91
+ let t = n({
92
+ image: F,
93
+ frame: L,
94
+ zoom: z,
95
+ offset: V
96
+ });
97
+ if (t.sWidth <= 0 || t.sHeight <= 0) return null;
98
+ let r = i(t, b), a = document.createElement("canvas");
99
+ a.width = r.width, a.height = r.height;
100
+ let o = a.getContext("2d");
101
+ return o ? (o.drawImage(e, t.sx, t.sy, t.sWidth, t.sHeight, 0, 0, r.width, r.height), new Promise((e) => {
102
+ a.toBlob((t) => e(t), x, S);
103
+ })) : null;
104
+ }, [
105
+ L,
106
+ b,
107
+ F,
108
+ V,
109
+ S,
110
+ x,
111
+ z
112
+ ]);
113
+ l(D, () => ({
114
+ crop: J,
115
+ reset: q
116
+ }), [J, q]);
117
+ let Y = (e) => {
118
+ F && (M.current = {
119
+ pointerId: e.pointerId,
120
+ startX: e.clientX,
121
+ startY: e.clientY,
122
+ from: V
123
+ }, e.currentTarget.setPointerCapture?.(e.pointerId));
124
+ }, X = (e) => {
125
+ let t = M.current;
126
+ !t || t.pointerId !== e.pointerId || G({
127
+ x: t.from.x + (e.clientX - t.startX),
128
+ y: t.from.y + (e.clientY - t.startY)
129
+ });
130
+ }, Z = (e) => {
131
+ M.current?.pointerId === e.pointerId && (M.current = null);
132
+ }, Q = (e) => {
133
+ let t = e.shiftKey ? h * 4 : h, n = {
134
+ ArrowLeft: {
135
+ x: -t,
136
+ y: 0
137
+ },
138
+ ArrowRight: {
139
+ x: t,
140
+ y: 0
141
+ },
142
+ ArrowUp: {
143
+ x: 0,
144
+ y: -t
145
+ },
146
+ ArrowDown: {
147
+ x: 0,
148
+ y: t
149
+ }
150
+ }[e.key];
151
+ if (n) {
152
+ e.preventDefault(), G({
153
+ x: V.x + n.x,
154
+ y: V.y + n.y
155
+ });
156
+ return;
157
+ }
158
+ e.key === "+" || e.key === "=" ? (e.preventDefault(), K(z + g)) : e.key === "-" || e.key === "_" ? (e.preventDefault(), K(z - g)) : e.key === "0" && (e.preventDefault(), q());
159
+ };
160
+ return /* @__PURE__ */ m("div", {
161
+ className: e(a.wrapper, E),
162
+ ...O,
163
+ children: [
164
+ /* @__PURE__ */ p("div", {
165
+ ref: A,
166
+ className: e(a.frame, C === "circle" && a.circle),
167
+ style: { aspectRatio: String(v) },
168
+ role: "group",
169
+ "aria-label": T,
170
+ "aria-describedby": k,
171
+ tabIndex: 0,
172
+ onPointerDown: Y,
173
+ onPointerMove: X,
174
+ onPointerUp: Z,
175
+ onPointerCancel: Z,
176
+ onKeyDown: Q,
177
+ onWheel: (e) => {
178
+ e.preventDefault(), K(z + (e.deltaY < 0 ? g : -.15));
179
+ },
180
+ children: N && /* @__PURE__ */ p("img", {
181
+ ref: j,
182
+ src: N,
183
+ alt: "",
184
+ draggable: !1,
185
+ className: a.image,
186
+ style: {
187
+ width: W.width || void 0,
188
+ height: W.height || void 0,
189
+ transform: `translate(${V.x}px, ${V.y}px)`
190
+ },
191
+ onLoad: (e) => {
192
+ let t = e.currentTarget;
193
+ t.naturalWidth > 0 && t.naturalHeight > 0 && I({
194
+ width: t.naturalWidth,
195
+ height: t.naturalHeight
196
+ });
197
+ }
198
+ })
199
+ }),
200
+ /* @__PURE__ */ m("div", {
201
+ className: a.controls,
202
+ children: [/* @__PURE__ */ p("input", {
203
+ type: "range",
204
+ className: a.zoom,
205
+ min: 1,
206
+ max: y,
207
+ step: .01,
208
+ value: z,
209
+ onChange: (e) => K(Number(e.target.value)),
210
+ "aria-label": "Zoom",
211
+ disabled: !F
212
+ }), /* @__PURE__ */ p("button", {
213
+ type: "button",
214
+ className: a.reset,
215
+ onClick: q,
216
+ disabled: !F,
217
+ children: "Centralizar"
218
+ })]
219
+ }),
220
+ /* @__PURE__ */ m("p", {
221
+ id: k,
222
+ className: a.hint,
223
+ children: [
224
+ "Arraste para reposicionar. Setas movem, ",
225
+ /* @__PURE__ */ p("kbd", { children: "+" }),
226
+ " e ",
227
+ /* @__PURE__ */ p("kbd", { children: "−" }),
228
+ " dão zoom,",
229
+ " ",
230
+ /* @__PURE__ */ p("kbd", { children: "0" }),
231
+ " centraliza."
232
+ ]
233
+ })
234
+ ]
235
+ });
236
+ }
237
+ //#endregion
238
+ export { _ as ImageCropper };
239
+
240
+ //# sourceMappingURL=ImageCropper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageCropper.js","names":[],"sources":["../../../src/components/ImageCropper/ImageCropper.tsx"],"sourcesContent":["import {\n type HTMLAttributes,\n type KeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n useCallback,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n clampOffset,\n computeCropRect,\n coverScale,\n type Offset,\n outputSize,\n type Size,\n} from \"./crop-geometry\";\nimport styles from \"./ImageCropper.module.css\";\n\n/** Imperative handle for exporting the current crop. */\nexport interface ImageCropperHandle {\n /**\n * Render the current crop and resolve with it.\n *\n * Resolves `null` when the image has not loaded yet or the browser refuses to\n * encode — never throws, so a submit handler does not need a try/catch.\n */\n crop: () => Promise<Blob | null>;\n /** Recentre at zoom 1. */\n reset: () => void;\n}\n\nexport interface ImageCropperProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** The image to crop: a `File`/`Blob` from an input, or a URL. */\n src: File | Blob | string;\n /** Crop aspect ratio as `width / height`. Default `1` (square). */\n aspect?: number;\n /** Maximum zoom over the cover scale. Default `4`. */\n maxZoom?: number;\n /**\n * Cap on the longest edge of the exported image, in px.\n *\n * Without it the export keeps the source resolution, which is right for a\n * document scan and wasteful for an avatar.\n */\n maxSize?: number;\n /** Output MIME type. Default `\"image/png\"`. */\n outputType?: string;\n /** Output quality for lossy types, `0`–`1`. Default `0.92`. */\n outputQuality?: number;\n /** Overlay shape. `\"circle\"` for an avatar, `\"rect\"` for a document. Default `\"rect\"`. */\n shape?: \"rect\" | \"circle\";\n /** Called whenever the crop changes, e.g. to enable a submit button. */\n onCropChange?: (state: { zoom: number; offset: Offset }) => void;\n /** Accessible name for the crop area. */\n label?: string;\n ref?: React.Ref<ImageCropperHandle>;\n}\n\n/** Pixels moved per arrow-key press. */\nconst KEY_STEP = 12;\n\n/** Zoom added or removed per `+`/`-` press, and per wheel notch. */\nconst ZOOM_STEP = 0.15;\n\n/**\n * Crop an image to a fixed aspect ratio.\n *\n * The frame stays put and the image pans and zooms behind it — the model an avatar\n * or document-photo flow wants, where the output shape is decided by the app and\n * the user only chooses what lands inside it. (A free-form draggable rectangle is a\n * different component; this one cannot produce an off-ratio crop by construction.)\n *\n * The export reads the *natural* pixels through a canvas, so a 4000 px photo is not\n * silently downsampled to whatever the on-screen preview measured. The image is\n * also always clamped to cover the frame, so an export can never contain the empty\n * bands you get from panning past an edge.\n *\n * Works by pointer, wheel and keyboard: arrows pan, `+`/`-` zoom, `0` resets.\n *\n * @example\n * const cropper = useRef<ImageCropperHandle>(null);\n *\n * <ImageCropper ref={cropper} src={file} aspect={1} shape=\"circle\" maxSize={512} />\n * <Button onClick={async () => upload(await cropper.current?.crop())}>Salvar</Button>\n */\nexport function ImageCropper({\n src,\n aspect = 1,\n maxZoom = 4,\n maxSize,\n outputType = \"image/png\",\n outputQuality = 0.92,\n shape = \"rect\",\n onCropChange,\n label = \"Área de recorte\",\n className,\n ref,\n ...rest\n}: ImageCropperProps) {\n /**\n * Id for the shortcut hint.\n *\n * Generated rather than derived from `label`: a label with spaces would produce\n * an id with spaces, and `aria-describedby` splits on whitespace — so the\n * description would silently never associate with the frame.\n */\n const hintId = `${useId()}-hint`;\n\n const frameRef = useRef<HTMLDivElement>(null);\n const imageRef = useRef<HTMLImageElement | null>(null);\n const dragRef = useRef<{\n pointerId: number;\n startX: number;\n startY: number;\n from: Offset;\n } | null>(null);\n\n const [url, setUrl] = useState<string | null>(null);\n const [natural, setNatural] = useState<Size | null>(null);\n const [frame, setFrame] = useState<Size>({ width: 0, height: 0 });\n const [zoom, setZoom] = useState(1);\n const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });\n\n /**\n * Resolve `src` to a displayable URL.\n *\n * A `File`/`Blob` needs `createObjectURL`, and the URL must be revoked when the\n * source changes or the component unmounts — otherwise every re-pick of a photo\n * leaks the previous one for the lifetime of the document.\n */\n useEffect(() => {\n if (typeof src === \"string\") {\n setUrl(src);\n return;\n }\n const objectUrl = URL.createObjectURL(src);\n setUrl(objectUrl);\n return () => URL.revokeObjectURL(objectUrl);\n }, [src]);\n\n // A new source invalidates the previous framing.\n useEffect(() => {\n setNatural(null);\n setZoom(1);\n setOffset({ x: 0, y: 0 });\n }, [url]);\n\n /** Track the frame's rendered size — the crop math is all relative to it. */\n useEffect(() => {\n const element = frameRef.current;\n if (!element) return;\n const measure = () =>\n setFrame({ width: element.clientWidth, height: element.clientHeight });\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n const scale = natural ? coverScale(natural, frame) * zoom : 0;\n\n /**\n * On-screen image size.\n *\n * Memoized because the pan callback depends on it: a fresh object every render\n * would rebuild that callback every render, and the pointer handlers close over\n * it during a drag.\n */\n const displayed = useMemo<Size>(\n () =>\n natural\n ? { width: natural.width * scale, height: natural.height * scale }\n : { width: 0, height: 0 },\n [natural, scale],\n );\n\n /** Apply a pan, clamped so the frame stays covered. */\n const pan = useCallback(\n (next: Offset) => {\n setOffset((current) => {\n const clamped = clampOffset(next, displayed, frame);\n if (clamped.x === current.x && clamped.y === current.y) return current;\n onCropChange?.({ zoom, offset: clamped });\n return clamped;\n });\n },\n [displayed, frame, onCropChange, zoom],\n );\n\n /**\n * Apply a zoom, then re-clamp the offset.\n *\n * Re-clamping is not optional: zooming *out* shrinks the image, so an offset\n * that was legal a moment ago can now expose background.\n */\n const applyZoom = useCallback(\n (next: number) => {\n const clampedZoom = Math.min(maxZoom, Math.max(1, next));\n setZoom(clampedZoom);\n if (!natural) return;\n const nextScale = coverScale(natural, frame) * clampedZoom;\n const nextDisplayed = {\n width: natural.width * nextScale,\n height: natural.height * nextScale,\n };\n setOffset((current) => {\n const clamped = clampOffset(current, nextDisplayed, frame);\n onCropChange?.({ zoom: clampedZoom, offset: clamped });\n return clamped;\n });\n },\n [frame, maxZoom, natural, onCropChange],\n );\n\n const reset = useCallback(() => {\n setZoom(1);\n setOffset({ x: 0, y: 0 });\n onCropChange?.({ zoom: 1, offset: { x: 0, y: 0 } });\n }, [onCropChange]);\n\n /**\n * Draw the current crop and encode it.\n *\n * Returns `null` rather than throwing on the paths a caller cannot do anything\n * about: no image yet, no 2D context, or an encoder that declined.\n */\n const crop = useCallback(async (): Promise<Blob | null> => {\n const image = imageRef.current;\n if (!image || !natural) return null;\n\n const rect = computeCropRect({ image: natural, frame, zoom, offset });\n if (rect.sWidth <= 0 || rect.sHeight <= 0) return null;\n\n const out = outputSize(rect, maxSize);\n const canvas = document.createElement(\"canvas\");\n canvas.width = out.width;\n canvas.height = out.height;\n const context = canvas.getContext(\"2d\");\n if (!context) return null;\n\n context.drawImage(\n image,\n rect.sx,\n rect.sy,\n rect.sWidth,\n rect.sHeight,\n 0,\n 0,\n out.width,\n out.height,\n );\n\n return new Promise((resolve) => {\n canvas.toBlob((blob) => resolve(blob), outputType, outputQuality);\n });\n }, [frame, maxSize, natural, offset, outputQuality, outputType, zoom]);\n\n useImperativeHandle(ref, () => ({ crop, reset }), [crop, reset]);\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>): void => {\n if (!natural) return;\n dragRef.current = {\n pointerId: event.pointerId,\n startX: event.clientX,\n startY: event.clientY,\n from: offset,\n };\n event.currentTarget.setPointerCapture?.(event.pointerId);\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>): void => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== event.pointerId) return;\n pan({\n x: drag.from.x + (event.clientX - drag.startX),\n y: drag.from.y + (event.clientY - drag.startY),\n });\n };\n\n const endDrag = (event: ReactPointerEvent<HTMLDivElement>): void => {\n if (dragRef.current?.pointerId !== event.pointerId) return;\n dragRef.current = null;\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n const step = event.shiftKey ? KEY_STEP * 4 : KEY_STEP;\n const moves: Record<string, Offset> = {\n ArrowLeft: { x: -step, y: 0 },\n ArrowRight: { x: step, y: 0 },\n ArrowUp: { x: 0, y: -step },\n ArrowDown: { x: 0, y: step },\n };\n const move = moves[event.key];\n if (move) {\n event.preventDefault();\n pan({ x: offset.x + move.x, y: offset.y + move.y });\n return;\n }\n if (event.key === \"+\" || event.key === \"=\") {\n event.preventDefault();\n applyZoom(zoom + ZOOM_STEP);\n } else if (event.key === \"-\" || event.key === \"_\") {\n event.preventDefault();\n applyZoom(zoom - ZOOM_STEP);\n } else if (event.key === \"0\") {\n event.preventDefault();\n reset();\n }\n };\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n ref={frameRef}\n className={cn(styles.frame, shape === \"circle\" && styles.circle)}\n style={{ aspectRatio: String(aspect) }}\n role=\"group\"\n aria-label={label}\n aria-describedby={hintId}\n tabIndex={0}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n onKeyDown={onKeyDown}\n onWheel={(event) => {\n event.preventDefault();\n applyZoom(zoom + (event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP));\n }}\n >\n {url && (\n <img\n ref={imageRef}\n src={url}\n alt=\"\"\n draggable={false}\n className={styles.image}\n style={{\n width: displayed.width || undefined,\n height: displayed.height || undefined,\n transform: `translate(${offset.x}px, ${offset.y}px)`,\n }}\n onLoad={(event) => {\n const element = event.currentTarget;\n // A decode can succeed and still report no intrinsic size —\n // an SVG without a viewBox is the common case. Accepting\n // that would enable the controls over an image the crop\n // maths can do nothing with.\n if (element.naturalWidth > 0 && element.naturalHeight > 0) {\n setNatural({\n width: element.naturalWidth,\n height: element.naturalHeight,\n });\n }\n }}\n />\n )}\n </div>\n\n <div className={styles.controls}>\n <input\n type=\"range\"\n className={styles.zoom}\n min={1}\n max={maxZoom}\n step={0.01}\n value={zoom}\n onChange={(event) => applyZoom(Number(event.target.value))}\n aria-label=\"Zoom\"\n disabled={!natural}\n />\n <button type=\"button\" className={styles.reset} onClick={reset} disabled={!natural}>\n Centralizar\n </button>\n </div>\n\n <p id={hintId} className={styles.hint}>\n Arraste para reposicionar. Setas movem, <kbd>+</kbd> e <kbd>−</kbd> dão zoom,{\" \"}\n <kbd>0</kbd> centraliza.\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ var e=`tempest_wrapper_l83On`,t=`tempest_frame_AFLKa`,n=`tempest_circle_qI5b8`,r=`tempest_image_8RAkb`,i=`tempest_controls_Tbg70`,a=`tempest_zoom_hMOSD`,o=`tempest_reset_TK3vP`,s=`tempest_hint_rIrZ8`,c={wrapper:e,frame:t,circle:n,image:r,controls:i,zoom:a,reset:o,hint:s};exports.circle=n,exports.controls=i,exports.default=c,exports.frame=t,exports.hint=s,exports.image=r,exports.reset=o,exports.wrapper=e,exports.zoom=a;
2
+ //# sourceMappingURL=ImageCropper.module.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageCropper.module.cjs","names":[],"sources":["../../../src/components/ImageCropper/ImageCropper.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3);\n max-width: 100%;\n font-family: var(--tempest-font-sans);\n}\n\n.frame {\n position: relative;\n width: 100%;\n overflow: hidden;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n /*\n * `grab` rather than `move`: nothing is being moved to a destination, the\n * image is being dragged under a fixed frame.\n */\n cursor: grab;\n /*\n * The frame owns dragging on touch, so the browser must not also scroll the\n * page from the same gesture.\n */\n touch-action: none;\n user-select: none;\n}\n\n.frame:active {\n cursor: grabbing;\n}\n\n.frame:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: 2px;\n}\n\n.circle {\n border-radius: var(--tempest-radius-full);\n}\n\n.image {\n position: absolute;\n top: 50%;\n left: 50%;\n max-width: none;\n pointer-events: none;\n /*\n * Centring lives in `translate` and the pan in the inline `transform`, which\n * are separate CSS properties applied in that order. Keeping them apart means\n * a pan is just `transform: translate(x, y)` — it never has to re-derive the\n * `-50% -50%` centring, and the two cannot overwrite each other.\n */\n translate: -50% -50%;\n}\n\n.controls {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.zoom {\n flex: 1;\n min-width: 0;\n accent-color: var(--tempest-primary);\n}\n\n.zoom:disabled {\n opacity: 0.5;\n}\n\n.reset {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .reset:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.reset:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: 2px;\n}\n\n.reset:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.hint {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.hint kbd {\n padding: 0 var(--tempest-space-1);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface);\n font-family: var(--tempest-font-mono, monospace);\n font-size: inherit;\n}\n"],"mappings":""}
@@ -0,0 +1,15 @@
1
+ //#region src/components/ImageCropper/ImageCropper.module.css
2
+ var e = "tempest_wrapper_l83On", t = "tempest_frame_AFLKa", n = "tempest_circle_qI5b8", r = "tempest_image_8RAkb", i = "tempest_controls_Tbg70", a = "tempest_zoom_hMOSD", o = "tempest_reset_TK3vP", s = "tempest_hint_rIrZ8", c = {
3
+ wrapper: e,
4
+ frame: t,
5
+ circle: n,
6
+ image: r,
7
+ controls: i,
8
+ zoom: a,
9
+ reset: o,
10
+ hint: s
11
+ };
12
+ //#endregion
13
+ export { n as circle, i as controls, c as default, t as frame, s as hint, r as image, o as reset, e as wrapper, a as zoom };
14
+
15
+ //# sourceMappingURL=ImageCropper.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImageCropper.module.js","names":[],"sources":["../../../src/components/ImageCropper/ImageCropper.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-3);\n max-width: 100%;\n font-family: var(--tempest-font-sans);\n}\n\n.frame {\n position: relative;\n width: 100%;\n overflow: hidden;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n /*\n * `grab` rather than `move`: nothing is being moved to a destination, the\n * image is being dragged under a fixed frame.\n */\n cursor: grab;\n /*\n * The frame owns dragging on touch, so the browser must not also scroll the\n * page from the same gesture.\n */\n touch-action: none;\n user-select: none;\n}\n\n.frame:active {\n cursor: grabbing;\n}\n\n.frame:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: 2px;\n}\n\n.circle {\n border-radius: var(--tempest-radius-full);\n}\n\n.image {\n position: absolute;\n top: 50%;\n left: 50%;\n max-width: none;\n pointer-events: none;\n /*\n * Centring lives in `translate` and the pan in the inline `transform`, which\n * are separate CSS properties applied in that order. Keeping them apart means\n * a pan is just `transform: translate(x, y)` — it never has to re-derive the\n * `-50% -50%` centring, and the two cannot overwrite each other.\n */\n translate: -50% -50%;\n}\n\n.controls {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.zoom {\n flex: 1;\n min-width: 0;\n accent-color: var(--tempest-primary);\n}\n\n.zoom:disabled {\n opacity: 0.5;\n}\n\n.reset {\n padding: var(--tempest-space-1) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-sm);\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .reset:hover:not(:disabled) {\n background-color: var(--tempest-surface);\n }\n}\n\n.reset:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: 2px;\n}\n\n.reset:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.hint {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n}\n\n.hint kbd {\n padding: 0 var(--tempest-space-1);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface);\n font-family: var(--tempest-font-mono, monospace);\n font-size: inherit;\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ function e(e,t){return e.width<=0||e.height<=0||t.width<=0||t.height<=0?0:Math.max(t.width/e.width,t.height/e.height)}function t(e,t){return{x:Math.max(0,(e.width-t.width)/2),y:Math.max(0,(e.height-t.height)/2)}}function n(e,n,i){let a=t(n,i);return{x:r(Math.min(a.x,Math.max(-a.x,e.x))),y:r(Math.min(a.y,Math.max(-a.y,e.y)))}}function r(e){return e===0?0:e}function i({image:t,frame:n,zoom:r,offset:i}){let a=e(t,n)*r;if(a<=0)return{sx:0,sy:0,sWidth:0,sHeight:0};let o=n.width/a,s=n.height/a,c=t.width/2-i.x/a,l=t.height/2-i.y/a;return{sx:Math.max(0,Math.min(t.width-o,c-o/2)),sy:Math.max(0,Math.min(t.height-s,l-s/2)),sWidth:o,sHeight:s}}function a(e,t){let n=e.sWidth,r=e.sHeight;if(!t||t<=0||n<=t&&r<=t)return{width:Math.max(1,Math.round(n)),height:Math.max(1,Math.round(r))};let i=t/Math.max(n,r);return{width:Math.max(1,Math.round(n*i)),height:Math.max(1,Math.round(r*i))}}exports.clampOffset=n,exports.computeCropRect=i,exports.coverScale=e,exports.maxOffset=t,exports.outputSize=a;
2
+ //# sourceMappingURL=crop-geometry.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop-geometry.cjs","names":[],"sources":["../../../src/components/ImageCropper/crop-geometry.ts"],"sourcesContent":["/** A width/height pair in pixels. */\nexport interface Size {\n width: number;\n height: number;\n}\n\n/** A pan offset in frame pixels, measured from the centered position. */\nexport interface Offset {\n x: number;\n y: number;\n}\n\n/** The source rectangle to read out of the natural-size image. */\nexport interface CropRect {\n sx: number;\n sy: number;\n sWidth: number;\n sHeight: number;\n}\n\n/**\n * Scale that makes `image` exactly cover `frame` with no empty space.\n *\n * `max` rather than `min`: a crop frame must never show background, so the\n * constraining axis is the one that would leave a gap.\n *\n * @param image - Natural image size.\n * @param frame - Crop frame size.\n * @returns The cover scale, or `0` when either dimension is unusable.\n */\nexport function coverScale(image: Size, frame: Size): number {\n if (image.width <= 0 || image.height <= 0 || frame.width <= 0 || frame.height <= 0) return 0;\n return Math.max(frame.width / image.width, frame.height / image.height);\n}\n\n/**\n * The largest pan offset that keeps the image covering the frame.\n *\n * Zero on an axis means the image is exactly as wide (or tall) as the frame\n * there, so panning along it would expose background.\n *\n * @param displayed - On-screen image size, after scale and zoom.\n * @param frame - Crop frame size.\n * @returns Maximum absolute offset per axis.\n */\nexport function maxOffset(displayed: Size, frame: Size): Offset {\n return {\n x: Math.max(0, (displayed.width - frame.width) / 2),\n y: Math.max(0, (displayed.height - frame.height) / 2),\n };\n}\n\n/**\n * Clamp a pan offset so the frame stays fully covered.\n *\n * This is what stops the single most common defect in a cropper: dragging or\n * zooming until the frame shows empty space, which then bakes transparent or\n * black bands into the exported image.\n *\n * @param offset - Desired offset.\n * @param displayed - On-screen image size, after scale and zoom.\n * @param frame - Crop frame size.\n * @returns The offset, clamped per axis.\n */\nexport function clampOffset(offset: Offset, displayed: Size, frame: Size): Offset {\n const max = maxOffset(displayed, frame);\n return {\n x: normalizeZero(Math.min(max.x, Math.max(-max.x, offset.x))),\n y: normalizeZero(Math.min(max.y, Math.max(-max.y, offset.y))),\n };\n}\n\n/**\n * Turn `-0` into `0`.\n *\n * Clamping a negative offset against a zero maximum yields `-0`, which compares\n * equal to `0` under `===` but not under `Object.is`. Left alone it leaks into\n * state comparisons and into the `translate()` string, so it is normalized once\n * here rather than guarded at every call site.\n */\nfunction normalizeZero(value: number): number {\n return value === 0 ? 0 : value;\n}\n\n/**\n * Map the crop frame back onto the natural-size image.\n *\n * The frame is what the user sees; the export has to read the corresponding\n * region of the *original* pixels, so every on-screen quantity is divided back\n * out by the effective scale. Working in natural pixels — instead of exporting\n * whatever the preview happens to be sized at — is what keeps a 4000 px photo\n * from being downsampled to the width of a 320 px preview.\n *\n * @param params.image - Natural image size.\n * @param params.frame - Crop frame size.\n * @param params.zoom - Zoom multiplier over the cover scale (`1` = cover).\n * @param params.offset - Pan offset in frame pixels.\n * @returns The source rectangle, clamped to the image bounds.\n */\nexport function computeCropRect({\n image,\n frame,\n zoom,\n offset,\n}: {\n image: Size;\n frame: Size;\n zoom: number;\n offset: Offset;\n}): CropRect {\n const scale = coverScale(image, frame) * zoom;\n if (scale <= 0) return { sx: 0, sy: 0, sWidth: 0, sHeight: 0 };\n\n const sWidth = frame.width / scale;\n const sHeight = frame.height / scale;\n\n // A positive offset moves the image right/down on screen, which means the\n // visible region moves left/up within the source — hence the subtraction.\n const centerX = image.width / 2 - offset.x / scale;\n const centerY = image.height / 2 - offset.y / scale;\n\n const sx = Math.max(0, Math.min(image.width - sWidth, centerX - sWidth / 2));\n const sy = Math.max(0, Math.min(image.height - sHeight, centerY - sHeight / 2));\n\n return { sx, sy, sWidth, sHeight };\n}\n\n/**\n * Output size for a crop, honoring an optional cap on the long edge.\n *\n * Exporting at the frame's own size would tie file size to whatever the preview\n * happened to measure. Exporting at full source resolution is right by default,\n * but a 12 MP phone photo cropped for a 96 px avatar is megabytes of waste — so\n * `maxSize` caps the long edge while preserving the aspect ratio.\n *\n * @param crop - The source rectangle being exported.\n * @param maxSize - Cap on the longest output edge, if any.\n * @returns Integer output dimensions, at least 1 px per axis.\n */\nexport function outputSize(crop: CropRect, maxSize?: number): Size {\n const width = crop.sWidth;\n const height = crop.sHeight;\n if (!maxSize || maxSize <= 0 || (width <= maxSize && height <= maxSize)) {\n return { width: Math.max(1, Math.round(width)), height: Math.max(1, Math.round(height)) };\n }\n const ratio = maxSize / Math.max(width, height);\n return {\n width: Math.max(1, Math.round(width * ratio)),\n height: Math.max(1, Math.round(height * ratio)),\n };\n}\n"],"mappings":"AA8BA,SAAgB,EAAW,EAAa,EAAqB,CAEzD,OADI,EAAM,OAAS,GAAK,EAAM,QAAU,GAAK,EAAM,OAAS,GAAK,EAAM,QAAU,EAAU,EACpF,KAAK,IAAI,EAAM,MAAQ,EAAM,MAAO,EAAM,OAAS,EAAM,MAAM,CAC1E,CAYA,SAAgB,EAAU,EAAiB,EAAqB,CAC5D,MAAO,CACH,EAAG,KAAK,IAAI,GAAI,EAAU,MAAQ,EAAM,OAAS,CAAC,EAClD,EAAG,KAAK,IAAI,GAAI,EAAU,OAAS,EAAM,QAAU,CAAC,CACxD,CACJ,CAcA,SAAgB,EAAY,EAAgB,EAAiB,EAAqB,CAC9E,IAAM,EAAM,EAAU,EAAW,CAAK,EACtC,MAAO,CACH,EAAG,EAAc,KAAK,IAAI,EAAI,EAAG,KAAK,IAAI,CAAC,EAAI,EAAG,EAAO,CAAC,CAAC,CAAC,EAC5D,EAAG,EAAc,KAAK,IAAI,EAAI,EAAG,KAAK,IAAI,CAAC,EAAI,EAAG,EAAO,CAAC,CAAC,CAAC,CAChE,CACJ,CAUA,SAAS,EAAc,EAAuB,CAC1C,OAAO,IAAU,EAAI,EAAI,CAC7B,CAiBA,SAAgB,EAAgB,CAC5B,QACA,QACA,OACA,UAMS,CACT,IAAM,EAAQ,EAAW,EAAO,CAAK,EAAI,EACzC,GAAI,GAAS,EAAG,MAAO,CAAE,GAAI,EAAG,GAAI,EAAG,OAAQ,EAAG,QAAS,CAAE,EAE7D,IAAM,EAAS,EAAM,MAAQ,EACvB,EAAU,EAAM,OAAS,EAIzB,EAAU,EAAM,MAAQ,EAAI,EAAO,EAAI,EACvC,EAAU,EAAM,OAAS,EAAI,EAAO,EAAI,EAK9C,MAAO,CAAE,GAHE,KAAK,IAAI,EAAG,KAAK,IAAI,EAAM,MAAQ,EAAQ,EAAU,EAAS,CAAC,CAGjE,EAAI,GAFF,KAAK,IAAI,EAAG,KAAK,IAAI,EAAM,OAAS,EAAS,EAAU,EAAU,CAAC,CAEhE,EAAI,SAAQ,SAAQ,CACrC,CAcA,SAAgB,EAAW,EAAgB,EAAwB,CAC/D,IAAM,EAAQ,EAAK,OACb,EAAS,EAAK,QACpB,GAAI,CAAC,GAAW,GAAW,GAAM,GAAS,GAAW,GAAU,EAC3D,MAAO,CAAE,MAAO,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EAAG,OAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,CAAE,EAE5F,IAAM,EAAQ,EAAU,KAAK,IAAI,EAAO,CAAM,EAC9C,MAAO,CACH,MAAO,KAAK,IAAI,EAAG,KAAK,MAAM,EAAQ,CAAK,CAAC,EAC5C,OAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,EAAS,CAAK,CAAC,CAClD,CACJ"}
@@ -0,0 +1,52 @@
1
+ //#region src/components/ImageCropper/crop-geometry.ts
2
+ function e(e, t) {
3
+ return e.width <= 0 || e.height <= 0 || t.width <= 0 || t.height <= 0 ? 0 : Math.max(t.width / e.width, t.height / e.height);
4
+ }
5
+ function t(e, t) {
6
+ return {
7
+ x: Math.max(0, (e.width - t.width) / 2),
8
+ y: Math.max(0, (e.height - t.height) / 2)
9
+ };
10
+ }
11
+ function n(e, n, i) {
12
+ let a = t(n, i);
13
+ return {
14
+ x: r(Math.min(a.x, Math.max(-a.x, e.x))),
15
+ y: r(Math.min(a.y, Math.max(-a.y, e.y)))
16
+ };
17
+ }
18
+ function r(e) {
19
+ return e === 0 ? 0 : e;
20
+ }
21
+ function i({ image: t, frame: n, zoom: r, offset: i }) {
22
+ let a = e(t, n) * r;
23
+ if (a <= 0) return {
24
+ sx: 0,
25
+ sy: 0,
26
+ sWidth: 0,
27
+ sHeight: 0
28
+ };
29
+ let o = n.width / a, s = n.height / a, c = t.width / 2 - i.x / a, l = t.height / 2 - i.y / a;
30
+ return {
31
+ sx: Math.max(0, Math.min(t.width - o, c - o / 2)),
32
+ sy: Math.max(0, Math.min(t.height - s, l - s / 2)),
33
+ sWidth: o,
34
+ sHeight: s
35
+ };
36
+ }
37
+ function a(e, t) {
38
+ let n = e.sWidth, r = e.sHeight;
39
+ if (!t || t <= 0 || n <= t && r <= t) return {
40
+ width: Math.max(1, Math.round(n)),
41
+ height: Math.max(1, Math.round(r))
42
+ };
43
+ let i = t / Math.max(n, r);
44
+ return {
45
+ width: Math.max(1, Math.round(n * i)),
46
+ height: Math.max(1, Math.round(r * i))
47
+ };
48
+ }
49
+ //#endregion
50
+ export { n as clampOffset, i as computeCropRect, e as coverScale, t as maxOffset, a as outputSize };
51
+
52
+ //# sourceMappingURL=crop-geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop-geometry.js","names":[],"sources":["../../../src/components/ImageCropper/crop-geometry.ts"],"sourcesContent":["/** A width/height pair in pixels. */\nexport interface Size {\n width: number;\n height: number;\n}\n\n/** A pan offset in frame pixels, measured from the centered position. */\nexport interface Offset {\n x: number;\n y: number;\n}\n\n/** The source rectangle to read out of the natural-size image. */\nexport interface CropRect {\n sx: number;\n sy: number;\n sWidth: number;\n sHeight: number;\n}\n\n/**\n * Scale that makes `image` exactly cover `frame` with no empty space.\n *\n * `max` rather than `min`: a crop frame must never show background, so the\n * constraining axis is the one that would leave a gap.\n *\n * @param image - Natural image size.\n * @param frame - Crop frame size.\n * @returns The cover scale, or `0` when either dimension is unusable.\n */\nexport function coverScale(image: Size, frame: Size): number {\n if (image.width <= 0 || image.height <= 0 || frame.width <= 0 || frame.height <= 0) return 0;\n return Math.max(frame.width / image.width, frame.height / image.height);\n}\n\n/**\n * The largest pan offset that keeps the image covering the frame.\n *\n * Zero on an axis means the image is exactly as wide (or tall) as the frame\n * there, so panning along it would expose background.\n *\n * @param displayed - On-screen image size, after scale and zoom.\n * @param frame - Crop frame size.\n * @returns Maximum absolute offset per axis.\n */\nexport function maxOffset(displayed: Size, frame: Size): Offset {\n return {\n x: Math.max(0, (displayed.width - frame.width) / 2),\n y: Math.max(0, (displayed.height - frame.height) / 2),\n };\n}\n\n/**\n * Clamp a pan offset so the frame stays fully covered.\n *\n * This is what stops the single most common defect in a cropper: dragging or\n * zooming until the frame shows empty space, which then bakes transparent or\n * black bands into the exported image.\n *\n * @param offset - Desired offset.\n * @param displayed - On-screen image size, after scale and zoom.\n * @param frame - Crop frame size.\n * @returns The offset, clamped per axis.\n */\nexport function clampOffset(offset: Offset, displayed: Size, frame: Size): Offset {\n const max = maxOffset(displayed, frame);\n return {\n x: normalizeZero(Math.min(max.x, Math.max(-max.x, offset.x))),\n y: normalizeZero(Math.min(max.y, Math.max(-max.y, offset.y))),\n };\n}\n\n/**\n * Turn `-0` into `0`.\n *\n * Clamping a negative offset against a zero maximum yields `-0`, which compares\n * equal to `0` under `===` but not under `Object.is`. Left alone it leaks into\n * state comparisons and into the `translate()` string, so it is normalized once\n * here rather than guarded at every call site.\n */\nfunction normalizeZero(value: number): number {\n return value === 0 ? 0 : value;\n}\n\n/**\n * Map the crop frame back onto the natural-size image.\n *\n * The frame is what the user sees; the export has to read the corresponding\n * region of the *original* pixels, so every on-screen quantity is divided back\n * out by the effective scale. Working in natural pixels — instead of exporting\n * whatever the preview happens to be sized at — is what keeps a 4000 px photo\n * from being downsampled to the width of a 320 px preview.\n *\n * @param params.image - Natural image size.\n * @param params.frame - Crop frame size.\n * @param params.zoom - Zoom multiplier over the cover scale (`1` = cover).\n * @param params.offset - Pan offset in frame pixels.\n * @returns The source rectangle, clamped to the image bounds.\n */\nexport function computeCropRect({\n image,\n frame,\n zoom,\n offset,\n}: {\n image: Size;\n frame: Size;\n zoom: number;\n offset: Offset;\n}): CropRect {\n const scale = coverScale(image, frame) * zoom;\n if (scale <= 0) return { sx: 0, sy: 0, sWidth: 0, sHeight: 0 };\n\n const sWidth = frame.width / scale;\n const sHeight = frame.height / scale;\n\n // A positive offset moves the image right/down on screen, which means the\n // visible region moves left/up within the source — hence the subtraction.\n const centerX = image.width / 2 - offset.x / scale;\n const centerY = image.height / 2 - offset.y / scale;\n\n const sx = Math.max(0, Math.min(image.width - sWidth, centerX - sWidth / 2));\n const sy = Math.max(0, Math.min(image.height - sHeight, centerY - sHeight / 2));\n\n return { sx, sy, sWidth, sHeight };\n}\n\n/**\n * Output size for a crop, honoring an optional cap on the long edge.\n *\n * Exporting at the frame's own size would tie file size to whatever the preview\n * happened to measure. Exporting at full source resolution is right by default,\n * but a 12 MP phone photo cropped for a 96 px avatar is megabytes of waste — so\n * `maxSize` caps the long edge while preserving the aspect ratio.\n *\n * @param crop - The source rectangle being exported.\n * @param maxSize - Cap on the longest output edge, if any.\n * @returns Integer output dimensions, at least 1 px per axis.\n */\nexport function outputSize(crop: CropRect, maxSize?: number): Size {\n const width = crop.sWidth;\n const height = crop.sHeight;\n if (!maxSize || maxSize <= 0 || (width <= maxSize && height <= maxSize)) {\n return { width: Math.max(1, Math.round(width)), height: Math.max(1, Math.round(height)) };\n }\n const ratio = maxSize / Math.max(width, height);\n return {\n width: Math.max(1, Math.round(width * ratio)),\n height: Math.max(1, Math.round(height * ratio)),\n };\n}\n"],"mappings":";AA8BA,SAAgB,EAAW,GAAa,GAAqB;CAEzD,OADI,EAAM,SAAS,KAAK,EAAM,UAAU,KAAK,EAAM,SAAS,KAAK,EAAM,UAAU,IAAU,IACpF,KAAK,IAAI,EAAM,QAAQ,EAAM,OAAO,EAAM,SAAS,EAAM,MAAM;AAC1E;AAYA,SAAgB,EAAU,GAAiB,GAAqB;CAC5D,OAAO;EACH,GAAG,KAAK,IAAI,IAAI,EAAU,QAAQ,EAAM,SAAS,CAAC;EAClD,GAAG,KAAK,IAAI,IAAI,EAAU,SAAS,EAAM,UAAU,CAAC;CACxD;AACJ;AAcA,SAAgB,EAAY,GAAgB,GAAiB,GAAqB;CAC9E,IAAM,IAAM,EAAU,GAAW,CAAK;CACtC,OAAO;EACH,GAAG,EAAc,KAAK,IAAI,EAAI,GAAG,KAAK,IAAI,CAAC,EAAI,GAAG,EAAO,CAAC,CAAC,CAAC;EAC5D,GAAG,EAAc,KAAK,IAAI,EAAI,GAAG,KAAK,IAAI,CAAC,EAAI,GAAG,EAAO,CAAC,CAAC,CAAC;CAChE;AACJ;AAUA,SAAS,EAAc,GAAuB;CAC1C,OAAO,MAAU,IAAI,IAAI;AAC7B;AAiBA,SAAgB,EAAgB,EAC5B,UACA,UACA,SACA,aAMS;CACT,IAAM,IAAQ,EAAW,GAAO,CAAK,IAAI;CACzC,IAAI,KAAS,GAAG,OAAO;EAAE,IAAI;EAAG,IAAI;EAAG,QAAQ;EAAG,SAAS;CAAE;CAE7D,IAAM,IAAS,EAAM,QAAQ,GACvB,IAAU,EAAM,SAAS,GAIzB,IAAU,EAAM,QAAQ,IAAI,EAAO,IAAI,GACvC,IAAU,EAAM,SAAS,IAAI,EAAO,IAAI;CAK9C,OAAO;EAAE,IAHE,KAAK,IAAI,GAAG,KAAK,IAAI,EAAM,QAAQ,GAAQ,IAAU,IAAS,CAAC,CAGjE;EAAI,IAFF,KAAK,IAAI,GAAG,KAAK,IAAI,EAAM,SAAS,GAAS,IAAU,IAAU,CAAC,CAEhE;EAAI;EAAQ;CAAQ;AACrC;AAcA,SAAgB,EAAW,GAAgB,GAAwB;CAC/D,IAAM,IAAQ,EAAK,QACb,IAAS,EAAK;CACpB,IAAI,CAAC,KAAW,KAAW,KAAM,KAAS,KAAW,KAAU,GAC3D,OAAO;EAAE,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,CAAK,CAAC;EAAG,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,CAAM,CAAC;CAAE;CAE5F,IAAM,IAAQ,IAAU,KAAK,IAAI,GAAO,CAAM;CAC9C,OAAO;EACH,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,IAAQ,CAAK,CAAC;EAC5C,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,IAAS,CAAK,CAAC;CAClD;AACJ"}
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("./scheduler-layout.cjs"),n=require("./Scheduler.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=6e4;function o({events:o,anchor:s,days:c=7,startHour:l=8,endHour:u=20,snapMinutes:d=30,onEventClick:f,onSlotClick:p,renderEvent:m,locale:h=`pt-BR`,showCurrentTime:g=!0,now:_,className:v,...y}){let[b,x]=(0,r.useState)(()=>_??new Date);(0,r.useEffect)(()=>{if(_||!g)return;let e=setInterval(()=>x(new Date),a);return()=>clearInterval(e)},[_,g]);let S=_??b,C=(0,r.useMemo)(()=>({startMinute:l*60,endMinute:u*60}),[l,u]),w=(s??S).toDateString(),T=(0,r.useMemo)(()=>t.dayRange(new Date(w),c),[w,c]),E=(0,r.useMemo)(()=>t.layoutEvents({events:o,days:T,window:C}),[o,T,C]),D=(0,r.useMemo)(()=>t.layoutAllDay({events:o,days:T}),[o,T]),O=(0,r.useMemo)(()=>t.hourMarks(C),[C]),k=new Intl.DateTimeFormat(h,{weekday:`short`,day:`numeric`}),A=new Intl.DateTimeFormat(h,{hour:`2-digit`,minute:`2-digit`}),j=(e,t)=>{let n=t.currentTarget.getBoundingClientRect(),r=n.height>0?(t.clientY-n.top)/n.height:0,i=C.startMinute+r*(C.endMinute-C.startMinute),a=Math.round(i/d)*d,o=Math.max(C.startMinute,Math.min(C.endMinute,a)),s=new Date(e);return s.setHours(0,o,0,0),s},M=g?t.fractionOfWindow(S,C):null,N=T.findIndex(e=>t.isSameDay(e,S));return(0,i.jsxs)(`div`,{className:e.cn(n.default.wrapper,v),...y,children:[(0,i.jsxs)(`div`,{className:n.default.head,style:{"--tempest-scheduler-days":c},children:[(0,i.jsx)(`span`,{className:n.default.gutterHead}),T.map((t,r)=>(0,i.jsx)(`span`,{className:e.cn(n.default.dayHead,r===N&&n.default.today),children:k.format(t)},t.toISOString()))]}),D.length>0&&(0,i.jsxs)(`div`,{className:n.default.allDayLane,style:{"--tempest-scheduler-days":c},children:[(0,i.jsx)(`span`,{className:n.default.gutterHead,children:`Dia inteiro`}),(0,i.jsx)(`div`,{className:n.default.allDayTrack,children:D.map(({event:e,dayIndex:t,span:r})=>(0,i.jsx)(`button`,{type:`button`,className:n.default.allDayEvent,style:{gridColumn:`${t+1} / span ${r}`},onClick:()=>f?.(e),disabled:!f,children:e.title},e.id))})]}),(0,i.jsxs)(`div`,{className:n.default.body,style:{"--tempest-scheduler-days":c},tabIndex:0,role:`group`,"aria-label":`Grade de horários`,children:[(0,i.jsx)(`div`,{className:n.default.gutter,children:O.map(e=>(0,i.jsx)(`span`,{className:n.default.hourLabel,style:{top:`${(e-C.startMinute)/(C.endMinute-C.startMinute)*100}%`},children:A.format(new Date(2026,0,1,Math.floor(e/60),e%60))},e))}),(0,i.jsxs)(`div`,{className:n.default.grid,"aria-label":`Agenda`,children:[T.map((t,r)=>(0,i.jsx)(`div`,{className:e.cn(n.default.dayColumn,r===N&&n.default.todayColumn),role:`group`,"aria-label":k.format(t),onClick:p?e=>{e.target===e.currentTarget&&p(j(t,e))}:void 0,children:O.map(e=>(0,i.jsx)(`span`,{className:n.default.hourLine,style:{top:`${(e-C.startMinute)/(C.endMinute-C.startMinute)*100}%`}},e))},t.toISOString())),E.map(e=>(0,i.jsx)(`button`,{type:`button`,className:n.default.event,style:{gridColumn:e.dayIndex+1,top:`${e.top*100}%`,height:`${e.height*100}%`,left:`${e.column/e.columns*100}%`,width:`${1/e.columns*100}%`},onClick:()=>f?.(e.event),disabled:!f,title:`${e.event.title} — ${A.format(e.event.start)}`,children:m?m(e.event):(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(`span`,{className:n.default.eventTime,children:A.format(e.event.start)}),(0,i.jsx)(`span`,{className:n.default.eventTitle,children:e.event.title})]})},`${e.event.id}-${e.dayIndex}`)),M!==null&&N>=0&&(0,i.jsx)(`span`,{className:n.default.nowLine,style:{top:`${M*100}%`},"aria-hidden":!0,"data-testid":`scheduler-now`})]})]})]})}exports.Scheduler=o;
2
+ //# sourceMappingURL=Scheduler.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Scheduler.cjs","names":[],"sources":["../../../src/components/Scheduler/Scheduler.tsx"],"sourcesContent":["import { type HTMLAttributes, type ReactNode, useEffect, useMemo, useState } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport {\n dayRange,\n type DayWindow,\n fractionOfWindow,\n hourMarks,\n isSameDay,\n layoutAllDay,\n layoutEvents,\n type SchedulerEvent,\n} from \"./scheduler-layout\";\nimport styles from \"./Scheduler.module.css\";\n\nexport interface SchedulerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Events to place. Instants are read in the browser's local time. */\n events: readonly SchedulerEvent[];\n /** Any day within the range to show. Default: today. */\n anchor?: Date;\n /** How many consecutive days to render. `1` is a day view, `7` a week. Default `7`. */\n days?: number;\n /** First visible hour, `0`–`23`. Default `8`. */\n startHour?: number;\n /** Last visible hour, `1`–`24`. Default `20`. */\n endHour?: number;\n /** Minutes a click on empty space snaps to. Default `30`. */\n snapMinutes?: number;\n /** Called when an event is activated by click, `Enter` or `Space`. */\n onEventClick?: (event: SchedulerEvent) => void;\n /** Called with the snapped instant when empty space is clicked. */\n onSlotClick?: (start: Date) => void;\n /** Render an event's contents. Defaults to its title and start time. */\n renderEvent?: (event: SchedulerEvent) => ReactNode;\n /** Locale for the day and hour labels. Default `\"pt-BR\"`. */\n locale?: string;\n /** Draw the current-time line. Default `true`. */\n showCurrentTime?: boolean;\n /** Fixed \"now\" for the indicator. Default: the real clock, ticking each minute. */\n now?: Date;\n}\n\n/** Minutes between ticks of the current-time indicator. */\nconst TICK_MS = 60_000;\n\n/**\n * An agenda: events placed on a time grid across consecutive days.\n *\n * `Calendar` is a date *picker* — it answers \"which day?\". This answers \"what is on\n * those days, and when\", which needs a different structure entirely: a vertical time\n * axis, events sized by duration, and overlapping events sitting side by side.\n *\n * That last part is the one worth naming. Overlapping events are grouped into\n * clusters of mutual overlap and every event in a cluster shares one column count,\n * so widths line up; a column is reused the moment it frees, so `9–10`, `9–10`,\n * `10–11` takes two columns and not three. The layout is pure and lives in\n * `scheduler-layout.ts`.\n *\n * Times are local. An event crossing midnight is split into both day columns, and\n * the day range is built by incrementing the calendar day, so a DST boundary does\n * not duplicate or skip a date.\n *\n * @example\n * <Scheduler\n * events={bookings}\n * days={7}\n * startHour={7}\n * endHour={21}\n * onEventClick={(e) => open(e.id)}\n * onSlotClick={(start) => createAt(start)}\n * />\n */\nexport function Scheduler({\n events,\n anchor,\n days: dayCount = 7,\n startHour = 8,\n endHour = 20,\n snapMinutes = 30,\n onEventClick,\n onSlotClick,\n renderEvent,\n locale = \"pt-BR\",\n showCurrentTime = true,\n now,\n className,\n ...rest\n}: SchedulerProps) {\n const [clock, setClock] = useState<Date>(() => now ?? new Date());\n\n /**\n * Keep the current-time line moving.\n *\n * Skipped entirely when `now` is supplied: that is the hook tests and demos use\n * to be deterministic, and a timer would fight it.\n */\n useEffect(() => {\n if (now || !showCurrentTime) return;\n const id = setInterval(() => setClock(new Date()), TICK_MS);\n return () => clearInterval(id);\n }, [now, showCurrentTime]);\n\n const reference = now ?? clock;\n\n const window = useMemo<DayWindow>(\n () => ({ startMinute: startHour * 60, endMinute: endHour * 60 }),\n [startHour, endHour],\n );\n\n /**\n * The calendar day the range starts from, as a stable string.\n *\n * `reference` ticks every minute when the current-time line is live. Depending on\n * the Date itself would re-slice the day range — and therefore relayout every\n * event — once a minute; depending on the day only recomputes at midnight.\n */\n const anchorDay = (anchor ?? reference).toDateString();\n const dayList = useMemo(() => dayRange(new Date(anchorDay), dayCount), [anchorDay, dayCount]);\n\n const placed = useMemo(\n () => layoutEvents({ events, days: dayList, window }),\n [events, dayList, window],\n );\n const allDay = useMemo(() => layoutAllDay({ events, days: dayList }), [events, dayList]);\n const marks = useMemo(() => hourMarks(window), [window]);\n\n const dayLabel = new Intl.DateTimeFormat(locale, { weekday: \"short\", day: \"numeric\" });\n const timeLabel = new Intl.DateTimeFormat(locale, { hour: \"2-digit\", minute: \"2-digit\" });\n\n /** Turn a click's vertical position within a day column into a snapped instant. */\n const slotFromClick = (day: Date, event: React.MouseEvent<HTMLDivElement>): Date => {\n const rect = event.currentTarget.getBoundingClientRect();\n const fraction = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0;\n const raw = window.startMinute + fraction * (window.endMinute - window.startMinute);\n const snapped = Math.round(raw / snapMinutes) * snapMinutes;\n const clamped = Math.max(window.startMinute, Math.min(window.endMinute, snapped));\n const result = new Date(day);\n result.setHours(0, clamped, 0, 0);\n return result;\n };\n\n const currentFraction = showCurrentTime ? fractionOfWindow(reference, window) : null;\n const todayIndex = dayList.findIndex((day) => isSameDay(day, reference));\n\n return (\n <div className={cn(styles.wrapper, className)} {...rest}>\n <div\n className={styles.head}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead} />\n {dayList.map((day, index) => (\n <span\n key={day.toISOString()}\n className={cn(styles.dayHead, index === todayIndex && styles.today)}\n >\n {dayLabel.format(day)}\n </span>\n ))}\n </div>\n\n {allDay.length > 0 && (\n <div\n className={styles.allDayLane}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n >\n <span className={styles.gutterHead}>Dia inteiro</span>\n <div className={styles.allDayTrack}>\n {allDay.map(({ event, dayIndex, span }) => (\n <button\n key={event.id}\n type=\"button\"\n className={styles.allDayEvent}\n style={{ gridColumn: `${dayIndex + 1} / span ${span}` }}\n onClick={() => onEventClick?.(event)}\n disabled={!onEventClick}\n >\n {event.title}\n </button>\n ))}\n </div>\n </div>\n )}\n\n {/*\n * Focusable because it scrolls vertically: a scroll region that cannot be\n * focused is unreachable by keyboard, which is what `axe`'s\n * `scrollable-region-focusable` rule is about. It needs a name too, or the\n * new tab stop would announce nothing — but `group`, not `region`: a named\n * `region` is a landmark, and two schedulers on one page would then be two\n * identically-named landmarks (`landmark-unique`).\n */}\n <div\n className={styles.body}\n style={{ [\"--tempest-scheduler-days\" as string]: dayCount }}\n tabIndex={0}\n role=\"group\"\n aria-label=\"Grade de horários\"\n >\n <div className={styles.gutter}>\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLabel}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n >\n {timeLabel.format(\n new Date(2026, 0, 1, Math.floor(minute / 60), minute % 60),\n )}\n </span>\n ))}\n </div>\n\n {/*\n * Not `role=\"grid\"`: that requires `row` children, and the events are\n * siblings of the day columns inside one CSS grid — a `row` wrapper\n * would stop the columns being grid items and collapse the layout.\n * A labelled group per day is the honest structure anyway: a screen\n * reader tabs the event buttons and the group name supplies the day.\n */}\n <div className={styles.grid} aria-label=\"Agenda\">\n {dayList.map((day, index) => (\n <div\n key={day.toISOString()}\n className={cn(\n styles.dayColumn,\n index === todayIndex && styles.todayColumn,\n )}\n role=\"group\"\n aria-label={dayLabel.format(day)}\n onClick={\n onSlotClick\n ? (event) => {\n // Only empty space creates: a click that\n // landed on an event is that event's.\n if (event.target !== event.currentTarget) return;\n onSlotClick(slotFromClick(day, event));\n }\n : undefined\n }\n >\n {marks.map((minute) => (\n <span\n key={minute}\n className={styles.hourLine}\n style={{\n top: `${((minute - window.startMinute) / (window.endMinute - window.startMinute)) * 100}%`,\n }}\n />\n ))}\n </div>\n ))}\n\n {placed.map((item) => (\n <button\n key={`${item.event.id}-${item.dayIndex}`}\n type=\"button\"\n className={styles.event}\n style={{\n gridColumn: item.dayIndex + 1,\n top: `${item.top * 100}%`,\n height: `${item.height * 100}%`,\n left: `${(item.column / item.columns) * 100}%`,\n width: `${(1 / item.columns) * 100}%`,\n }}\n onClick={() => onEventClick?.(item.event)}\n disabled={!onEventClick}\n title={`${item.event.title} — ${timeLabel.format(item.event.start)}`}\n >\n {renderEvent ? (\n renderEvent(item.event)\n ) : (\n <>\n <span className={styles.eventTime}>\n {timeLabel.format(item.event.start)}\n </span>\n <span className={styles.eventTitle}>{item.event.title}</span>\n </>\n )}\n </button>\n ))}\n\n {currentFraction !== null && todayIndex >= 0 && (\n <span\n className={styles.nowLine}\n style={{ top: `${currentFraction * 100}%` }}\n aria-hidden\n data-testid=\"scheduler-now\"\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport type { SchedulerEvent };\n"],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { dayRange as t, fractionOfWindow as n, hourMarks as r, isSameDay as i, layoutAllDay as a, layoutEvents as o } from "./scheduler-layout.js";
3
+ import s from "./Scheduler.module.js";
4
+ import { useEffect as c, useMemo as l, useState as u } from "react";
5
+ import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
6
+ //#region src/components/Scheduler/Scheduler.tsx
7
+ var m = 6e4;
8
+ function h({ events: h, anchor: g, days: _ = 7, startHour: v = 8, endHour: y = 20, snapMinutes: b = 30, onEventClick: x, onSlotClick: S, renderEvent: C, locale: w = "pt-BR", showCurrentTime: T = !0, now: E, className: D, ...O }) {
9
+ let [k, A] = u(() => E ?? /* @__PURE__ */ new Date());
10
+ c(() => {
11
+ if (E || !T) return;
12
+ let e = setInterval(() => A(/* @__PURE__ */ new Date()), m);
13
+ return () => clearInterval(e);
14
+ }, [E, T]);
15
+ let j = E ?? k, M = l(() => ({
16
+ startMinute: v * 60,
17
+ endMinute: y * 60
18
+ }), [v, y]), N = (g ?? j).toDateString(), P = l(() => t(new Date(N), _), [N, _]), F = l(() => o({
19
+ events: h,
20
+ days: P,
21
+ window: M
22
+ }), [
23
+ h,
24
+ P,
25
+ M
26
+ ]), I = l(() => a({
27
+ events: h,
28
+ days: P
29
+ }), [h, P]), L = l(() => r(M), [M]), R = new Intl.DateTimeFormat(w, {
30
+ weekday: "short",
31
+ day: "numeric"
32
+ }), z = new Intl.DateTimeFormat(w, {
33
+ hour: "2-digit",
34
+ minute: "2-digit"
35
+ }), B = (e, t) => {
36
+ let n = t.currentTarget.getBoundingClientRect(), r = n.height > 0 ? (t.clientY - n.top) / n.height : 0, i = M.startMinute + r * (M.endMinute - M.startMinute), a = Math.round(i / b) * b, o = Math.max(M.startMinute, Math.min(M.endMinute, a)), s = new Date(e);
37
+ return s.setHours(0, o, 0, 0), s;
38
+ }, V = T ? n(j, M) : null, H = P.findIndex((e) => i(e, j));
39
+ return /* @__PURE__ */ p("div", {
40
+ className: e(s.wrapper, D),
41
+ ...O,
42
+ children: [
43
+ /* @__PURE__ */ p("div", {
44
+ className: s.head,
45
+ style: { "--tempest-scheduler-days": _ },
46
+ children: [/* @__PURE__ */ f("span", { className: s.gutterHead }), P.map((t, n) => /* @__PURE__ */ f("span", {
47
+ className: e(s.dayHead, n === H && s.today),
48
+ children: R.format(t)
49
+ }, t.toISOString()))]
50
+ }),
51
+ I.length > 0 && /* @__PURE__ */ p("div", {
52
+ className: s.allDayLane,
53
+ style: { "--tempest-scheduler-days": _ },
54
+ children: [/* @__PURE__ */ f("span", {
55
+ className: s.gutterHead,
56
+ children: "Dia inteiro"
57
+ }), /* @__PURE__ */ f("div", {
58
+ className: s.allDayTrack,
59
+ children: I.map(({ event: e, dayIndex: t, span: n }) => /* @__PURE__ */ f("button", {
60
+ type: "button",
61
+ className: s.allDayEvent,
62
+ style: { gridColumn: `${t + 1} / span ${n}` },
63
+ onClick: () => x?.(e),
64
+ disabled: !x,
65
+ children: e.title
66
+ }, e.id))
67
+ })]
68
+ }),
69
+ /* @__PURE__ */ p("div", {
70
+ className: s.body,
71
+ style: { "--tempest-scheduler-days": _ },
72
+ tabIndex: 0,
73
+ role: "group",
74
+ "aria-label": "Grade de horários",
75
+ children: [/* @__PURE__ */ f("div", {
76
+ className: s.gutter,
77
+ children: L.map((e) => /* @__PURE__ */ f("span", {
78
+ className: s.hourLabel,
79
+ style: { top: `${(e - M.startMinute) / (M.endMinute - M.startMinute) * 100}%` },
80
+ children: z.format(new Date(2026, 0, 1, Math.floor(e / 60), e % 60))
81
+ }, e))
82
+ }), /* @__PURE__ */ p("div", {
83
+ className: s.grid,
84
+ "aria-label": "Agenda",
85
+ children: [
86
+ P.map((t, n) => /* @__PURE__ */ f("div", {
87
+ className: e(s.dayColumn, n === H && s.todayColumn),
88
+ role: "group",
89
+ "aria-label": R.format(t),
90
+ onClick: S ? (e) => {
91
+ e.target === e.currentTarget && S(B(t, e));
92
+ } : void 0,
93
+ children: L.map((e) => /* @__PURE__ */ f("span", {
94
+ className: s.hourLine,
95
+ style: { top: `${(e - M.startMinute) / (M.endMinute - M.startMinute) * 100}%` }
96
+ }, e))
97
+ }, t.toISOString())),
98
+ F.map((e) => /* @__PURE__ */ f("button", {
99
+ type: "button",
100
+ className: s.event,
101
+ style: {
102
+ gridColumn: e.dayIndex + 1,
103
+ top: `${e.top * 100}%`,
104
+ height: `${e.height * 100}%`,
105
+ left: `${e.column / e.columns * 100}%`,
106
+ width: `${1 / e.columns * 100}%`
107
+ },
108
+ onClick: () => x?.(e.event),
109
+ disabled: !x,
110
+ title: `${e.event.title} — ${z.format(e.event.start)}`,
111
+ children: C ? C(e.event) : /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ f("span", {
112
+ className: s.eventTime,
113
+ children: z.format(e.event.start)
114
+ }), /* @__PURE__ */ f("span", {
115
+ className: s.eventTitle,
116
+ children: e.event.title
117
+ })] })
118
+ }, `${e.event.id}-${e.dayIndex}`)),
119
+ V !== null && H >= 0 && /* @__PURE__ */ f("span", {
120
+ className: s.nowLine,
121
+ style: { top: `${V * 100}%` },
122
+ "aria-hidden": !0,
123
+ "data-testid": "scheduler-now"
124
+ })
125
+ ]
126
+ })]
127
+ })
128
+ ]
129
+ });
130
+ }
131
+ //#endregion
132
+ export { h as Scheduler };
133
+
134
+ //# sourceMappingURL=Scheduler.js.map