@sesionario/blocks 1.39.0 → 1.40.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/index.cjs CHANGED
@@ -23,6 +23,7 @@ var RPNInput = require('react-phone-number-input');
23
23
  var flags = require('react-phone-number-input/flags');
24
24
  var ScrollAreaPrimitive = require('@radix-ui/react-scroll-area');
25
25
  var messageScroller = require('@shadcn/react/message-scroller');
26
+ var radixUi = require('radix-ui');
26
27
 
27
28
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
28
29
 
@@ -633,7 +634,7 @@ function useEditableSection({
633
634
  },
634
635
  resolvedLabels,
635
636
  contentStyle: stableHeight && minHeightPx != null ? { minHeight: minHeightPx } : void 0,
636
- actionsClassName: "flex min-h-9 min-w-[11rem] flex-wrap items-center justify-end gap-2"
637
+ actionsClassName: "flex min-h-9 w-full flex-wrap items-center gap-2 *:flex-1 sm:w-auto sm:min-w-[11rem] sm:justify-end sm:*:flex-none"
637
638
  };
638
639
  }
639
640
  function EditableSection({
@@ -4731,6 +4732,7 @@ function useSearchHotkey(options = {}) {
4731
4732
  }
4732
4733
 
4733
4734
  // src/lib/scroll-restoration/store.ts
4735
+ var programmaticDepth = 0;
4734
4736
  var MAX_MARKS = 30;
4735
4737
  var MAX_SNAPSHOT_ROUTES = 3;
4736
4738
  var PENDING_TRAVERSAL_TTL_MS = 1e3;
@@ -4822,6 +4824,9 @@ function consumePendingTraversal(routeKey) {
4822
4824
  clearPendingTraversalTimer();
4823
4825
  return true;
4824
4826
  }
4827
+ function isProgrammaticScroll() {
4828
+ return programmaticDepth > 0;
4829
+ }
4825
4830
  function getScrollTop(container) {
4826
4831
  if (!container) {
4827
4832
  return window.scrollY || document.documentElement.scrollTop || 0;
@@ -4829,11 +4834,37 @@ function getScrollTop(container) {
4829
4834
  return container.scrollTop;
4830
4835
  }
4831
4836
  function setScrollTop(container, top) {
4832
- if (!container) {
4833
- window.scrollTo(0, top);
4834
- return;
4837
+ programmaticDepth += 1;
4838
+ try {
4839
+ if (!container) {
4840
+ window.scrollTo({ top, left: 0, behavior: "instant" });
4841
+ return;
4842
+ }
4843
+ container.scrollTop = top;
4844
+ } finally {
4845
+ queueMicrotask(() => {
4846
+ programmaticDepth = Math.max(0, programmaticDepth - 1);
4847
+ });
4835
4848
  }
4836
- container.scrollTop = top;
4849
+ }
4850
+ function isOverflowScrollport(element) {
4851
+ const overflow = getComputedStyle(element).overflowY;
4852
+ return overflow === "auto" || overflow === "scroll" || overflow === "overlay";
4853
+ }
4854
+ function resolveScrollPort(container) {
4855
+ if (!container) return "window";
4856
+ const windowTop = getScrollTop(null);
4857
+ const nestedTop = container.scrollTop;
4858
+ if (nestedTop > windowTop) return "container";
4859
+ if (windowTop > nestedTop) return "window";
4860
+ return isOverflowScrollport(container) ? "container" : "window";
4861
+ }
4862
+ function resetScrollports(container) {
4863
+ setScrollTop(null, 0);
4864
+ if (container) setScrollTop(container, 0);
4865
+ }
4866
+ function portNode(container, port) {
4867
+ return port === "container" ? container : null;
4837
4868
  }
4838
4869
  function scrollportTop(container) {
4839
4870
  return container ? container.getBoundingClientRect().top : 0;
@@ -4875,11 +4906,14 @@ function resolveAnchor(container, attribute) {
4875
4906
  return { id: bestId, top: bestTop };
4876
4907
  }
4877
4908
  function captureMark(container, attribute) {
4878
- const anchor = resolveAnchor(container, attribute);
4909
+ const port = resolveScrollPort(container);
4910
+ const node = portNode(container, port);
4911
+ const anchor = resolveAnchor(node, attribute);
4879
4912
  return {
4880
- top: getScrollTop(container),
4913
+ top: getScrollTop(node),
4881
4914
  anchorId: anchor?.id ?? null,
4882
- anchorTop: anchor?.top ?? 0
4915
+ anchorTop: anchor?.top ?? 0,
4916
+ port
4883
4917
  };
4884
4918
  }
4885
4919
  function applyMark(container, mark, attribute) {
@@ -4899,6 +4933,22 @@ function applyMark(container, mark, attribute) {
4899
4933
  setScrollTop(container, mark.top);
4900
4934
  return false;
4901
4935
  }
4936
+ function restoreScrollports(container, mark, attribute) {
4937
+ const port = mark.port ?? "window";
4938
+ switch (port) {
4939
+ case "container":
4940
+ setScrollTop(null, 0);
4941
+ if (!container) return false;
4942
+ return applyMark(container, mark, attribute);
4943
+ case "window":
4944
+ if (container) setScrollTop(container, 0);
4945
+ return applyMark(null, mark, attribute);
4946
+ default: {
4947
+ const exhaustive = port;
4948
+ return exhaustive;
4949
+ }
4950
+ }
4951
+ }
4902
4952
  function getScrollHeight(container) {
4903
4953
  if (!container) {
4904
4954
  return Math.max(
@@ -4914,8 +4964,37 @@ var ScrollRestorationContext = React2.createContext(null);
4914
4964
  function useScrollRestorationContext() {
4915
4965
  return React2.useContext(ScrollRestorationContext);
4916
4966
  }
4917
- function readContainer(containerRef) {
4918
- return containerRef?.current ?? null;
4967
+ function readContainer(containerRef, registered) {
4968
+ return registered ?? containerRef?.current ?? null;
4969
+ }
4970
+ var USER_INTENT_EVENTS = [
4971
+ "wheel",
4972
+ "touchstart",
4973
+ "keydown",
4974
+ "pointerdown"
4975
+ ];
4976
+ function listenForUserIntent(onIntent) {
4977
+ for (const type of USER_INTENT_EVENTS) {
4978
+ window.addEventListener(type, onIntent, { passive: true, once: true });
4979
+ }
4980
+ return () => {
4981
+ for (const type of USER_INTENT_EVENTS) {
4982
+ window.removeEventListener(type, onIntent);
4983
+ }
4984
+ };
4985
+ }
4986
+ function useRegisterScrollContainer(containerRef) {
4987
+ const ctx = useScrollRestorationContext();
4988
+ const setContainerRef = React2.useRef(ctx?.setScrollContainer);
4989
+ setContainerRef.current = ctx?.setScrollContainer;
4990
+ React2.useLayoutEffect(() => {
4991
+ const setContainer = setContainerRef.current;
4992
+ if (!setContainer) return;
4993
+ setContainer(containerRef.current);
4994
+ return () => {
4995
+ setContainer(null);
4996
+ };
4997
+ }, [containerRef]);
4919
4998
  }
4920
4999
  function ScrollRestorationProvider({
4921
5000
  routeKey,
@@ -4932,13 +5011,23 @@ function ScrollRestorationProvider({
4932
5011
  };
4933
5012
  }
4934
5013
  const isTraversal = traversalForRouteRef.current.isTraversal;
5014
+ const registeredContainerRef = React2.useRef(null);
5015
+ const [scrollContainerNode, setScrollContainerNode] = React2.useState(null);
5016
+ const setScrollContainer = React2.useCallback((node) => {
5017
+ registeredContainerRef.current = node;
5018
+ setScrollContainerNode((current) => current === node ? current : node);
5019
+ }, []);
5020
+ const readActiveContainer = React2.useCallback(() => {
5021
+ return readContainer(containerRef, registeredContainerRef.current);
5022
+ }, [containerRef]);
4935
5023
  const value = React2.useMemo(
4936
5024
  () => ({
4937
5025
  routeKey,
4938
5026
  isTraversal,
4939
- anchorAttribute
5027
+ anchorAttribute,
5028
+ setScrollContainer
4940
5029
  }),
4941
- [routeKey, isTraversal, anchorAttribute]
5030
+ [routeKey, isTraversal, anchorAttribute, setScrollContainer]
4942
5031
  );
4943
5032
  React2.useEffect(() => {
4944
5033
  if (typeof window === "undefined" || !("scrollRestoration" in history)) {
@@ -4953,34 +5042,50 @@ function ScrollRestorationProvider({
4953
5042
  const prevRouteKeyRef = React2.useRef(routeKey);
4954
5043
  const anchorAttributeRef = React2.useRef(anchorAttribute);
4955
5044
  anchorAttributeRef.current = anchorAttribute;
5045
+ React2.useLayoutEffect(() => {
5046
+ const nodes = [document.documentElement];
5047
+ const container = readActiveContainer();
5048
+ if (container) nodes.push(container);
5049
+ const previous = nodes.map((node) => node.style.overflowAnchor);
5050
+ for (const node of nodes) node.style.overflowAnchor = "none";
5051
+ return () => {
5052
+ nodes.forEach((node, index) => {
5053
+ node.style.overflowAnchor = previous[index] ?? "";
5054
+ });
5055
+ };
5056
+ }, [readActiveContainer, scrollContainerNode]);
4956
5057
  React2.useEffect(() => {
4957
- const container = readContainer(containerRef);
4958
- const target = container ?? window;
5058
+ const container = readActiveContainer();
4959
5059
  let rafId = 0;
4960
5060
  const onScroll = () => {
5061
+ if (isProgrammaticScroll()) return;
4961
5062
  if (rafId) return;
4962
5063
  rafId = requestAnimationFrame(() => {
4963
5064
  rafId = 0;
4964
5065
  setScrollMark(
4965
5066
  routeKey,
4966
- captureMark(readContainer(containerRef), anchorAttribute)
5067
+ captureMark(readActiveContainer(), anchorAttribute)
4967
5068
  );
4968
5069
  });
4969
5070
  };
4970
- target.addEventListener("scroll", onScroll, { passive: true });
5071
+ window.addEventListener("scroll", onScroll, { passive: true });
5072
+ container?.addEventListener("scroll", onScroll, { passive: true });
4971
5073
  return () => {
4972
- target.removeEventListener("scroll", onScroll);
5074
+ window.removeEventListener("scroll", onScroll);
5075
+ container?.removeEventListener("scroll", onScroll);
4973
5076
  if (rafId) cancelAnimationFrame(rafId);
4974
5077
  };
4975
- }, [routeKey, containerRef, anchorAttribute]);
5078
+ }, [routeKey, anchorAttribute, readActiveContainer, scrollContainerNode]);
4976
5079
  React2.useLayoutEffect(() => {
4977
- const container = readContainer(containerRef);
5080
+ const container = readActiveContainer();
4978
5081
  const prevKey = prevRouteKeyRef.current;
4979
5082
  if (prevKey !== routeKey) {
4980
- setScrollMark(
4981
- prevKey,
4982
- captureMark(container, anchorAttributeRef.current)
4983
- );
5083
+ const fresh = captureMark(container, anchorAttributeRef.current);
5084
+ const existing = getScrollMark(prevKey);
5085
+ const paneUnmounted = existing?.port === "container" && fresh.port === "window" && fresh.top === 0 && container === null;
5086
+ if (!paneUnmounted) {
5087
+ setScrollMark(prevKey, fresh);
5088
+ }
4984
5089
  prevRouteKeyRef.current = routeKey;
4985
5090
  }
4986
5091
  const hasHash = typeof window !== "undefined" && window.location.hash.length > 1;
@@ -4988,51 +5093,58 @@ function ScrollRestorationProvider({
4988
5093
  consumePendingTraversal(routeKey);
4989
5094
  const mark = getScrollMark(routeKey);
4990
5095
  if (!mark) return void 0;
4991
- applyMark(container, mark, anchorAttribute);
4992
- return startStabilizationLoop(container, mark, anchorAttribute);
5096
+ restoreScrollports(container, mark, anchorAttribute);
5097
+ return startStabilizationLoop(
5098
+ () => readActiveContainer(),
5099
+ mark,
5100
+ anchorAttribute
5101
+ );
4993
5102
  }
4994
5103
  clearRoute(routeKey);
4995
- if (!hasHash) {
4996
- setScrollTop(container, 0);
4997
- }
4998
- return void 0;
4999
- }, [routeKey, isTraversal, containerRef, anchorAttribute]);
5104
+ if (hasHash) return void 0;
5105
+ return startFreshReset(readActiveContainer);
5106
+ }, [routeKey, isTraversal, anchorAttribute, readActiveContainer]);
5000
5107
  return /* @__PURE__ */ jsxRuntime.jsx(ScrollRestorationContext.Provider, { value, children });
5001
5108
  }
5002
- function startStabilizationLoop(container, mark, attribute) {
5109
+ function startFreshReset(readContainer2) {
5110
+ let cancelled = false;
5111
+ const apply = () => {
5112
+ if (cancelled) return;
5113
+ resetScrollports(readContainer2());
5114
+ };
5115
+ apply();
5116
+ let rafId = requestAnimationFrame(() => {
5117
+ rafId = 0;
5118
+ apply();
5119
+ });
5120
+ const stopIntent = listenForUserIntent(() => {
5121
+ cancelled = true;
5122
+ if (rafId) cancelAnimationFrame(rafId);
5123
+ rafId = 0;
5124
+ });
5125
+ return () => {
5126
+ cancelled = true;
5127
+ if (rafId) cancelAnimationFrame(rafId);
5128
+ stopIntent();
5129
+ };
5130
+ }
5131
+ function startStabilizationLoop(readContainer2, mark, attribute) {
5132
+ const portNode2 = () => (mark.port ?? "window") === "container" ? readContainer2() : null;
5003
5133
  const startedAt = performance.now();
5004
- let lastHeight = getScrollHeight(container);
5134
+ let lastHeight = getScrollHeight(portNode2());
5005
5135
  let rafId = 0;
5006
5136
  let aborted = false;
5007
5137
  const abort = () => {
5008
5138
  aborted = true;
5009
5139
  };
5010
- const onUserIntent = () => {
5011
- abort();
5012
- };
5013
- window.addEventListener("wheel", onUserIntent, {
5014
- passive: true,
5015
- once: true
5016
- });
5017
- window.addEventListener("touchstart", onUserIntent, {
5018
- passive: true,
5019
- once: true
5020
- });
5021
- window.addEventListener("keydown", onUserIntent, {
5022
- passive: true,
5023
- once: true
5024
- });
5025
- window.addEventListener("pointerdown", onUserIntent, {
5026
- passive: true,
5027
- once: true
5028
- });
5140
+ const stopIntent = listenForUserIntent(abort);
5029
5141
  const tick = () => {
5030
5142
  if (aborted) return;
5031
5143
  if (performance.now() - startedAt > STABILIZE_MS) return;
5032
- const height = getScrollHeight(container);
5144
+ const height = getScrollHeight(portNode2());
5033
5145
  if (height !== lastHeight) {
5034
5146
  lastHeight = height;
5035
- applyMark(container, mark, attribute);
5147
+ restoreScrollports(readContainer2(), mark, attribute);
5036
5148
  }
5037
5149
  rafId = requestAnimationFrame(tick);
5038
5150
  };
@@ -5040,10 +5152,7 @@ function startStabilizationLoop(container, mark, attribute) {
5040
5152
  return () => {
5041
5153
  abort();
5042
5154
  if (rafId) cancelAnimationFrame(rafId);
5043
- window.removeEventListener("wheel", onUserIntent);
5044
- window.removeEventListener("touchstart", onUserIntent);
5045
- window.removeEventListener("keydown", onUserIntent);
5046
- window.removeEventListener("pointerdown", onUserIntent);
5155
+ stopIntent();
5047
5156
  };
5048
5157
  }
5049
5158
 
@@ -8113,6 +8222,651 @@ function useRealtimeChannel({
8113
8222
  };
8114
8223
  }, [enabled, reconnectDelayMs]);
8115
8224
  }
8225
+ var badgeVariants = classVarianceAuthority.cva(
8226
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
8227
+ {
8228
+ variants: {
8229
+ variant: {
8230
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
8231
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
8232
+ destructive: "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
8233
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
8234
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
8235
+ link: "text-primary underline-offset-4 [a&]:hover:underline"
8236
+ }
8237
+ },
8238
+ defaultVariants: {
8239
+ variant: "default"
8240
+ }
8241
+ }
8242
+ );
8243
+ function Badge({
8244
+ className,
8245
+ variant = "default",
8246
+ asChild = false,
8247
+ ...props
8248
+ }) {
8249
+ const Comp = asChild ? radixUi.Slot.Root : "span";
8250
+ return /* @__PURE__ */ jsxRuntime.jsx(
8251
+ Comp,
8252
+ {
8253
+ "data-slot": "badge",
8254
+ "data-variant": variant,
8255
+ className: cn(badgeVariants({ variant }), className),
8256
+ ...props
8257
+ }
8258
+ );
8259
+ }
8260
+ var INVITE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;
8261
+ var INVITE_STATUS_CLASS = {
8262
+ pending: "border-amber-500/40 bg-amber-500/15 font-medium text-amber-800 dark:text-amber-300",
8263
+ claimed: "border-emerald-500/40 bg-emerald-500/15 font-medium text-emerald-800 dark:text-emerald-300",
8264
+ failed: "border-destructive/40 bg-destructive/15 font-medium text-destructive",
8265
+ cancelled: "text-muted-foreground"
8266
+ };
8267
+ var INVITE_STATUS_LABELS = {
8268
+ es: {
8269
+ pending: "Pendiente",
8270
+ claimed: "Registrado",
8271
+ failed: "Con error",
8272
+ cancelled: "Cancelada"
8273
+ },
8274
+ en: {
8275
+ pending: "Pending",
8276
+ claimed: "Registered",
8277
+ failed: "Error",
8278
+ cancelled: "Cancelled"
8279
+ }
8280
+ };
8281
+ function inviteStatusLabel(status, language) {
8282
+ const labels = language.startsWith("en") ? INVITE_STATUS_LABELS.en : INVITE_STATUS_LABELS.es;
8283
+ switch (status) {
8284
+ case "pending":
8285
+ case "claimed":
8286
+ case "failed":
8287
+ case "cancelled":
8288
+ return labels[status];
8289
+ default:
8290
+ return status;
8291
+ }
8292
+ }
8293
+ function resendWaitMessage(waitSeconds) {
8294
+ const total = Math.max(0, Math.ceil(waitSeconds));
8295
+ if (total < 60) {
8296
+ const secondLabel2 = total === 1 ? "segundo" : "segundos";
8297
+ return `Espera ${total} ${secondLabel2} antes de reenviar`;
8298
+ }
8299
+ const minutes = Math.floor(total / 60);
8300
+ const seconds = total % 60;
8301
+ const minuteLabel = minutes === 1 ? "minuto" : "minutos";
8302
+ if (seconds === 0)
8303
+ return `Espera ${minutes} ${minuteLabel} antes de reenviar`;
8304
+ const secondLabel = seconds === 1 ? "segundo" : "segundos";
8305
+ return `Espera ${minutes} ${minuteLabel} y ${seconds} ${secondLabel} antes de reenviar`;
8306
+ }
8307
+ function isInviteEmail(value) {
8308
+ const email = value.trim().toLowerCase();
8309
+ return email.length > 0 && email.length <= 320 && INVITE_EMAIL_RE.test(email);
8310
+ }
8311
+ function mergeInviteEmails(current, raw) {
8312
+ const pills = [...current];
8313
+ const seen = new Set(pills);
8314
+ const invalid = [];
8315
+ for (const part of raw.split(/[\n,;]+/)) {
8316
+ const token = part.trim();
8317
+ if (!token) continue;
8318
+ const email = token.toLowerCase();
8319
+ if (!isInviteEmail(email)) {
8320
+ invalid.push(token);
8321
+ continue;
8322
+ }
8323
+ if (seen.has(email)) continue;
8324
+ seen.add(email);
8325
+ pills.push(email);
8326
+ }
8327
+ return { pills, invalid };
8328
+ }
8329
+ async function readJson(res) {
8330
+ return await res.json();
8331
+ }
8332
+ function MembershipInvitePanel({
8333
+ basePath,
8334
+ query,
8335
+ onQueryChange,
8336
+ onNotify,
8337
+ onConfigure
8338
+ }) {
8339
+ const language = useLanguageContext()?.language ?? "es";
8340
+ const [configured, setConfigured] = React2.useState(null);
8341
+ const [types, setTypes] = React2.useState([]);
8342
+ const [tab, setTab] = React2.useState("emails");
8343
+ const [emailPills, setEmailPills] = React2.useState([]);
8344
+ const [emailDraft, setEmailDraft] = React2.useState("");
8345
+ const [csvText, setCsvText] = React2.useState("");
8346
+ const [typeId, setTypeId] = React2.useState("");
8347
+ const [paidUp, setPaidUp] = React2.useState(null);
8348
+ const [unpaidPolicy, setUnpaidPolicy] = React2.useState("lapsed_with_charge");
8349
+ const [graceDays, setGraceDays] = React2.useState("30");
8350
+ const [sendInvites, setSendInvites] = React2.useState(true);
8351
+ const [preview, setPreview] = React2.useState(null);
8352
+ const [busy, setBusy] = React2.useState(false);
8353
+ const [items, setItems] = React2.useState([]);
8354
+ const [nextCursor, setNextCursor] = React2.useState(null);
8355
+ const [listLoading, setListLoading] = React2.useState(true);
8356
+ const [draftQ, setDraftQ] = React2.useState(query.q);
8357
+ const [cancelId, setCancelId] = React2.useState(null);
8358
+ const [refresh, setRefresh] = React2.useState(0);
8359
+ React2.useEffect(() => {
8360
+ let cancelled = false;
8361
+ void (async () => {
8362
+ const res = await fetch(`${basePath}/membership-invites/options`, {
8363
+ credentials: "include"
8364
+ });
8365
+ const body = await readJson(res);
8366
+ if (cancelled) return;
8367
+ const result = body.result;
8368
+ setConfigured(Boolean(result?.configured));
8369
+ const nextTypes = result?.types ?? [];
8370
+ setTypes(nextTypes);
8371
+ setTypeId((current) => current || nextTypes[0]?.id || "");
8372
+ })();
8373
+ return () => {
8374
+ cancelled = true;
8375
+ };
8376
+ }, [basePath]);
8377
+ React2.useEffect(() => {
8378
+ const handle = setTimeout(() => {
8379
+ if (draftQ !== query.q)
8380
+ onQueryChange({ q: draftQ, status: query.status });
8381
+ }, 300);
8382
+ return () => clearTimeout(handle);
8383
+ }, [draftQ, onQueryChange, query.q, query.status]);
8384
+ React2.useEffect(() => {
8385
+ let cancelled = false;
8386
+ setListLoading(true);
8387
+ const params = new URLSearchParams();
8388
+ if (query.q) params.set("q", query.q);
8389
+ if (query.status) params.set("status", query.status);
8390
+ params.set("limit", "20");
8391
+ void (async () => {
8392
+ const res = await fetch(
8393
+ `${basePath}/membership-invites?${params.toString()}`,
8394
+ { credentials: "include" }
8395
+ );
8396
+ const body = await readJson(res);
8397
+ if (cancelled) return;
8398
+ const result = body.result;
8399
+ setItems(result?.items ?? []);
8400
+ setNextCursor(result?.nextCursor ?? null);
8401
+ setListLoading(false);
8402
+ })();
8403
+ return () => {
8404
+ cancelled = true;
8405
+ };
8406
+ }, [basePath, query.q, query.status, refresh]);
8407
+ if (configured === null)
8408
+ return /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "Cargando invitaciones\u2026" });
8409
+ if (!configured) {
8410
+ return /* @__PURE__ */ jsxRuntime.jsx(
8411
+ EmptyState,
8412
+ {
8413
+ title: "Configura la membres\xEDa primero",
8414
+ description: "El colegio necesita un modo de suscripci\xF3n y al menos un tipo activo.",
8415
+ action: onConfigure ? /* @__PURE__ */ jsxRuntime.jsx(Button, { type: "button", onClick: onConfigure, children: "Ir a configuraci\xF3n" }) : void 0
8416
+ }
8417
+ );
8418
+ }
8419
+ function applyEmailDraft() {
8420
+ const next = mergeInviteEmails(emailPills, emailDraft);
8421
+ setEmailPills(next.pills);
8422
+ setEmailDraft(next.invalid.join("\n"));
8423
+ if (next.invalid.length > 0) onNotify?.("Correo no v\xE1lido", "error");
8424
+ return next;
8425
+ }
8426
+ function emailSourceText() {
8427
+ if (tab !== "emails") return "";
8428
+ const next = applyEmailDraft();
8429
+ return next.invalid.length > 0 ? null : next.pills.join("\n");
8430
+ }
8431
+ const payload = (emailText) => ({
8432
+ source: tab === "emails" ? { kind: "emails", text: emailText } : { kind: "csv", text: csvText },
8433
+ defaults: { defaultTypeId: typeId, defaultPaidUp: paidUp === true },
8434
+ options: {
8435
+ unpaidPolicy,
8436
+ unpaidGraceDays: unpaidPolicy === "active_with_charge" ? Number(graceDays) || 30 : void 0,
8437
+ sendInvites
8438
+ }
8439
+ });
8440
+ async function runPreview() {
8441
+ if (paidUp === null) {
8442
+ onNotify?.("Elige si est\xE1n al corriente", "error");
8443
+ return;
8444
+ }
8445
+ const emailText = emailSourceText();
8446
+ if (emailText === null) return;
8447
+ setBusy(true);
8448
+ try {
8449
+ const res = await fetch(`${basePath}/membership-invites/preview`, {
8450
+ method: "POST",
8451
+ credentials: "include",
8452
+ headers: { "content-type": "application/json" },
8453
+ body: JSON.stringify(payload(emailText))
8454
+ });
8455
+ const body = await readJson(res);
8456
+ if (!res.ok || !body.success) {
8457
+ onNotify?.(body.error ?? "No se pudo revisar", "error");
8458
+ return;
8459
+ }
8460
+ const result = body.result;
8461
+ setPreview(result.rows ?? []);
8462
+ } finally {
8463
+ setBusy(false);
8464
+ }
8465
+ }
8466
+ async function runCommit() {
8467
+ const emailText = emailSourceText();
8468
+ if (emailText === null) return;
8469
+ setBusy(true);
8470
+ try {
8471
+ const res = await fetch(`${basePath}/membership-invites`, {
8472
+ method: "POST",
8473
+ credentials: "include",
8474
+ headers: { "content-type": "application/json" },
8475
+ body: JSON.stringify(payload(emailText))
8476
+ });
8477
+ const body = await readJson(res);
8478
+ if (!res.ok || !body.success) {
8479
+ onNotify?.(body.error ?? "No se pudo guardar", "error");
8480
+ return;
8481
+ }
8482
+ onNotify?.("Invitaciones guardadas", "success");
8483
+ setPreview(null);
8484
+ setEmailPills([]);
8485
+ setEmailDraft("");
8486
+ setCsvText("");
8487
+ setRefresh((value) => value + 1);
8488
+ } finally {
8489
+ setBusy(false);
8490
+ }
8491
+ }
8492
+ async function downloadTemplate() {
8493
+ const res = await fetch(`${basePath}/membership-invites/template`, {
8494
+ credentials: "include"
8495
+ });
8496
+ const blob = await res.blob();
8497
+ const url = URL.createObjectURL(blob);
8498
+ const anchor = document.createElement("a");
8499
+ anchor.href = url;
8500
+ anchor.download = "membership-invites.csv";
8501
+ anchor.click();
8502
+ URL.revokeObjectURL(url);
8503
+ }
8504
+ async function loadMore() {
8505
+ if (!nextCursor) return;
8506
+ const params = new URLSearchParams();
8507
+ if (query.q) params.set("q", query.q);
8508
+ if (query.status) params.set("status", query.status);
8509
+ params.set("limit", "20");
8510
+ params.set("cursor", nextCursor);
8511
+ const res = await fetch(
8512
+ `${basePath}/membership-invites?${params.toString()}`,
8513
+ { credentials: "include" }
8514
+ );
8515
+ const body = await readJson(res);
8516
+ const result = body.result;
8517
+ setItems((current) => [...current, ...result?.items ?? []]);
8518
+ setNextCursor(result?.nextCursor ?? null);
8519
+ }
8520
+ async function sendOne(id) {
8521
+ const res = await fetch(`${basePath}/membership-invites/${id}/send`, {
8522
+ method: "POST",
8523
+ credentials: "include"
8524
+ });
8525
+ const body = await readJson(res);
8526
+ if (!res.ok) {
8527
+ onNotify?.(
8528
+ body.error === "throttled" ? typeof body.waitSeconds === "number" ? resendWaitMessage(body.waitSeconds) : "Espera unos minutos antes de reenviar" : body.error ?? "No se pudo enviar",
8529
+ "error"
8530
+ );
8531
+ return;
8532
+ }
8533
+ onNotify?.("Invitaci\xF3n enviada", "success");
8534
+ }
8535
+ async function confirmCancel() {
8536
+ if (!cancelId) return;
8537
+ const id = cancelId;
8538
+ setCancelId(null);
8539
+ const res = await fetch(`${basePath}/membership-invites/${id}/cancel`, {
8540
+ method: "POST",
8541
+ credentials: "include"
8542
+ });
8543
+ if (!res.ok) {
8544
+ onNotify?.("No se pudo cancelar", "error");
8545
+ return;
8546
+ }
8547
+ setItems(
8548
+ (current) => current.map(
8549
+ (row) => row.id === id ? { ...row, status: "cancelled" } : row
8550
+ )
8551
+ );
8552
+ onNotify?.("Invitaci\xF3n cancelada", "success");
8553
+ }
8554
+ const invalidPreview = preview?.some((row) => row.outcome === "invalid");
8555
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-8", children: [
8556
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
8557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", role: "tablist", children: [
8558
+ /* @__PURE__ */ jsxRuntime.jsx(
8559
+ Button,
8560
+ {
8561
+ type: "button",
8562
+ variant: tab === "emails" ? "default" : "outline",
8563
+ onClick: () => setTab("emails"),
8564
+ children: "Correos"
8565
+ }
8566
+ ),
8567
+ /* @__PURE__ */ jsxRuntime.jsx(
8568
+ Button,
8569
+ {
8570
+ type: "button",
8571
+ variant: tab === "csv" ? "default" : "outline",
8572
+ onClick: () => setTab("csv"),
8573
+ children: "CSV"
8574
+ }
8575
+ )
8576
+ ] }),
8577
+ tab === "emails" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-input bg-background focus-within:border-ring focus-within:ring-ring/50 flex min-h-28 w-full flex-wrap items-center gap-1.5 rounded-md border px-2 py-2 focus-within:ring-[3px]", children: [
8578
+ emailPills.map((email) => /* @__PURE__ */ jsxRuntime.jsxs(Badge, { variant: "secondary", children: [
8579
+ email,
8580
+ /* @__PURE__ */ jsxRuntime.jsx(
8581
+ Button,
8582
+ {
8583
+ type: "button",
8584
+ variant: "ghost",
8585
+ size: "icon-sm",
8586
+ className: "size-4",
8587
+ "aria-label": `Quitar ${email}`,
8588
+ onClick: () => setEmailPills(
8589
+ (current) => current.filter((item) => item !== email)
8590
+ ),
8591
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, {})
8592
+ }
8593
+ )
8594
+ ] }, email)),
8595
+ /* @__PURE__ */ jsxRuntime.jsx(
8596
+ Input,
8597
+ {
8598
+ "aria-label": "Correos",
8599
+ className: "h-8 min-w-40 flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:border-transparent focus-visible:ring-0 dark:bg-transparent",
8600
+ placeholder: emailPills.length === 0 ? "Escribe un correo y pulsa Enter" : "",
8601
+ value: emailDraft,
8602
+ onChange: (event) => setEmailDraft(event.target.value),
8603
+ onKeyDown: (event) => {
8604
+ if (event.key === "Enter" || event.key === ",") {
8605
+ event.preventDefault();
8606
+ applyEmailDraft();
8607
+ return;
8608
+ }
8609
+ if (event.key === "Backspace" && emailDraft.length === 0 && emailPills.length > 0) {
8610
+ event.preventDefault();
8611
+ setEmailPills((current) => current.slice(0, -1));
8612
+ }
8613
+ },
8614
+ onPaste: (event) => {
8615
+ const text = event.clipboardData.getData("text");
8616
+ if (!/[\n,;]/.test(text) && !isInviteEmail(text)) return;
8617
+ event.preventDefault();
8618
+ const next = mergeInviteEmails(
8619
+ emailPills,
8620
+ `${emailDraft}${text}`
8621
+ );
8622
+ setEmailPills(next.pills);
8623
+ setEmailDraft(next.invalid.join("\n"));
8624
+ if (next.invalid.length > 0)
8625
+ onNotify?.("Correo no v\xE1lido", "error");
8626
+ }
8627
+ }
8628
+ )
8629
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
8630
+ /* @__PURE__ */ jsxRuntime.jsx(
8631
+ Button,
8632
+ {
8633
+ type: "button",
8634
+ variant: "outline",
8635
+ onClick: () => void downloadTemplate(),
8636
+ children: "Descargar plantilla"
8637
+ }
8638
+ ),
8639
+ /* @__PURE__ */ jsxRuntime.jsx(
8640
+ Input,
8641
+ {
8642
+ "aria-label": "Archivo CSV",
8643
+ type: "file",
8644
+ accept: ".csv,text/csv",
8645
+ onChange: (event) => {
8646
+ const file = event.target.files?.[0];
8647
+ if (!file) return;
8648
+ void file.text().then(setCsvText);
8649
+ }
8650
+ }
8651
+ )
8652
+ ] }),
8653
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1 text-sm", children: [
8654
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Tipo de membres\xEDa" }),
8655
+ /* @__PURE__ */ jsxRuntime.jsx(
8656
+ "select",
8657
+ {
8658
+ "aria-label": "Tipo de membres\xEDa",
8659
+ className: "border-input bg-background h-9 w-full rounded-md border px-2",
8660
+ value: typeId,
8661
+ onChange: (event) => setTypeId(event.target.value),
8662
+ children: types.map((type) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: type.id, children: type.name }, type.id))
8663
+ }
8664
+ )
8665
+ ] }),
8666
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1 text-sm", children: [
8667
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xBFAl corriente?" }),
8668
+ /* @__PURE__ */ jsxRuntime.jsxs(
8669
+ "select",
8670
+ {
8671
+ "aria-label": "Al corriente",
8672
+ className: "border-input bg-background h-9 w-full rounded-md border px-2",
8673
+ value: paidUp === null ? "" : paidUp ? "yes" : "no",
8674
+ onChange: (event) => {
8675
+ if (event.target.value === "yes") setPaidUp(true);
8676
+ else if (event.target.value === "no") setPaidUp(false);
8677
+ else setPaidUp(null);
8678
+ },
8679
+ children: [
8680
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Elige una opci\xF3n" }),
8681
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "yes", children: "S\xED, al corriente" }),
8682
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "no", children: "No" })
8683
+ ]
8684
+ }
8685
+ )
8686
+ ] }),
8687
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1 text-sm", children: [
8688
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Si no est\xE1n al corriente" }),
8689
+ /* @__PURE__ */ jsxRuntime.jsxs(
8690
+ "select",
8691
+ {
8692
+ "aria-label": "Pol\xEDtica de adeudo",
8693
+ className: "border-input bg-background h-9 w-full rounded-md border px-2",
8694
+ value: unpaidPolicy,
8695
+ onChange: (event) => setUnpaidPolicy(event.target.value),
8696
+ children: [
8697
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "lapsed_with_charge", children: "Socio reconocido, sin acceso hasta que pague" }),
8698
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active_with_charge", children: "Con acceso por unos d\xEDas y un cargo abierto" }),
8699
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active_no_charge", children: "Activo sin cargo (no se registra el adeudo)" })
8700
+ ]
8701
+ }
8702
+ )
8703
+ ] }),
8704
+ unpaidPolicy === "active_with_charge" ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block space-y-1 text-sm", children: [
8705
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "D\xEDas de acceso antes de vencer" }),
8706
+ /* @__PURE__ */ jsxRuntime.jsx(
8707
+ Input,
8708
+ {
8709
+ "aria-label": "D\xEDas de gracia",
8710
+ inputMode: "numeric",
8711
+ value: graceDays,
8712
+ onChange: (event) => setGraceDays(event.target.value)
8713
+ }
8714
+ )
8715
+ ] }) : null,
8716
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
8717
+ /* @__PURE__ */ jsxRuntime.jsx(
8718
+ "input",
8719
+ {
8720
+ type: "checkbox",
8721
+ checked: sendInvites,
8722
+ onChange: (event) => setSendInvites(event.target.checked)
8723
+ }
8724
+ ),
8725
+ "Enviar invitaciones al guardar"
8726
+ ] }),
8727
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
8728
+ /* @__PURE__ */ jsxRuntime.jsx(
8729
+ Button,
8730
+ {
8731
+ type: "button",
8732
+ variant: "outline",
8733
+ disabled: busy,
8734
+ onClick: () => void runPreview(),
8735
+ children: "Revisar"
8736
+ }
8737
+ ),
8738
+ /* @__PURE__ */ jsxRuntime.jsx(
8739
+ Button,
8740
+ {
8741
+ type: "button",
8742
+ disabled: busy || !preview || invalidPreview,
8743
+ onClick: () => void runCommit(),
8744
+ children: "Guardar"
8745
+ }
8746
+ )
8747
+ ] }),
8748
+ preview ? /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full text-sm", children: [
8749
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
8750
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "text-left", children: "Correo" }),
8751
+ /* @__PURE__ */ jsxRuntime.jsx("th", { className: "text-left", children: "Resultado" })
8752
+ ] }) }),
8753
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: preview.map((row) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
8754
+ /* @__PURE__ */ jsxRuntime.jsx("td", { children: row.email }),
8755
+ /* @__PURE__ */ jsxRuntime.jsxs("td", { children: [
8756
+ row.outcome,
8757
+ row.errors.length > 0 ? ` (${row.errors.join(", ")})` : ""
8758
+ ] })
8759
+ ] }, `${row.line}-${row.email}`)) })
8760
+ ] }) : null
8761
+ ] }),
8762
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
8763
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-medium", children: "Invitaciones" }),
8764
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
8765
+ /* @__PURE__ */ jsxRuntime.jsx(
8766
+ Input,
8767
+ {
8768
+ "aria-label": "Buscar invitaciones",
8769
+ className: "min-w-0 flex-1",
8770
+ value: draftQ,
8771
+ onChange: (event) => setDraftQ(event.target.value),
8772
+ placeholder: "Correo o nombre"
8773
+ }
8774
+ ),
8775
+ /* @__PURE__ */ jsxRuntime.jsxs(
8776
+ "select",
8777
+ {
8778
+ "aria-label": "Estado",
8779
+ className: "border-input bg-background h-9 shrink-0 rounded-md border px-2",
8780
+ value: query.status,
8781
+ onChange: (event) => onQueryChange({ q: query.q, status: event.target.value }),
8782
+ children: [
8783
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "Todos" }),
8784
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "pending", children: "Pendientes" }),
8785
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "claimed", children: "Registrados" }),
8786
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "failed", children: "Con error" }),
8787
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "cancelled", children: "Canceladas" })
8788
+ ]
8789
+ }
8790
+ )
8791
+ ] }),
8792
+ listLoading ? /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: "Cargando lista\u2026" }) : null,
8793
+ !listLoading && items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
8794
+ EmptyState,
8795
+ {
8796
+ title: "No hay invitaciones",
8797
+ description: "Agrega correos o un CSV."
8798
+ }
8799
+ ) : null,
8800
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "divide-y", children: items.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(
8801
+ "li",
8802
+ {
8803
+ className: "flex flex-col gap-2 py-3 sm:flex-row sm:items-center sm:justify-between",
8804
+ children: [
8805
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
8806
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "break-all", children: row.email }),
8807
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs", children: [
8808
+ row.fullName ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: row.fullName }) : null,
8809
+ /* @__PURE__ */ jsxRuntime.jsx(
8810
+ Badge,
8811
+ {
8812
+ variant: "outline",
8813
+ className: INVITE_STATUS_CLASS[row.status],
8814
+ children: inviteStatusLabel(row.status, language)
8815
+ }
8816
+ ),
8817
+ row.failureReason ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: row.failureReason }) : null
8818
+ ] })
8819
+ ] }),
8820
+ row.status === "pending" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
8821
+ /* @__PURE__ */ jsxRuntime.jsx(
8822
+ Button,
8823
+ {
8824
+ type: "button",
8825
+ variant: "outline",
8826
+ onClick: () => void sendOne(row.id),
8827
+ children: "Enviar invitaci\xF3n"
8828
+ }
8829
+ ),
8830
+ /* @__PURE__ */ jsxRuntime.jsx(
8831
+ Button,
8832
+ {
8833
+ type: "button",
8834
+ variant: "ghost",
8835
+ onClick: () => setCancelId(row.id),
8836
+ children: "Cancelar"
8837
+ }
8838
+ )
8839
+ ] }) : null
8840
+ ]
8841
+ },
8842
+ row.id
8843
+ )) }),
8844
+ nextCursor ? /* @__PURE__ */ jsxRuntime.jsx(
8845
+ Button,
8846
+ {
8847
+ type: "button",
8848
+ variant: "outline",
8849
+ onClick: () => void loadMore(),
8850
+ children: "Cargar m\xE1s"
8851
+ }
8852
+ ) : null
8853
+ ] }),
8854
+ /* @__PURE__ */ jsxRuntime.jsx(
8855
+ ConfirmDestructiveDialog,
8856
+ {
8857
+ open: cancelId !== null,
8858
+ onOpenChange: (open) => {
8859
+ if (!open) setCancelId(null);
8860
+ },
8861
+ title: "Cancelar invitaci\xF3n",
8862
+ description: "Esta persona ya no quedar\xE1 registrada como socia al crear su cuenta.",
8863
+ confirmLabel: "Cancelar invitaci\xF3n",
8864
+ cancelLabel: "Volver",
8865
+ onConfirm: () => void confirmCancel()
8866
+ }
8867
+ )
8868
+ ] });
8869
+ }
8116
8870
 
8117
8871
  Object.defineProperty(exports, "useMessageScroller", {
8118
8872
  enumerable: true,
@@ -8166,6 +8920,7 @@ exports.MarkerContent = MarkerContent;
8166
8920
  exports.MarkerIcon = MarkerIcon;
8167
8921
  exports.MediaCarousel = MediaCarousel;
8168
8922
  exports.MediaViewerDialog = MediaViewerDialog;
8923
+ exports.MembershipInvitePanel = MembershipInvitePanel;
8169
8924
  exports.Message = Message;
8170
8925
  exports.MessageAvatar = MessageAvatar;
8171
8926
  exports.MessageComposer = MessageComposer;
@@ -8220,6 +8975,7 @@ exports.useEditableSection = useEditableSection;
8220
8975
  exports.useLanguageContext = useLanguageContext;
8221
8976
  exports.useMediaQuery = useMediaQuery2;
8222
8977
  exports.useRealtimeChannel = useRealtimeChannel;
8978
+ exports.useRegisterScrollContainer = useRegisterScrollContainer;
8223
8979
  exports.useRestorableState = useRestorableState;
8224
8980
  exports.useScrollFadeXEdges = useScrollFadeXEdges;
8225
8981
  exports.useScrollRestorationContext = useScrollRestorationContext;