@refraction-ui/react 0.10.0 → 0.12.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
@@ -32660,6 +32660,7 @@ function createConversation(config = {}) {
32660
32660
  const messagesByConv = /* @__PURE__ */ new Map();
32661
32661
  let activeConversationId = config.activeConversationId ?? null;
32662
32662
  let openThreadRootId = null;
32663
+ let replyTarget = null;
32663
32664
  let status = "idle";
32664
32665
  let error = null;
32665
32666
  let abortController = null;
@@ -32681,6 +32682,7 @@ function createConversation(config = {}) {
32681
32682
  activeConversationId,
32682
32683
  messages: activeConversationId ? messagesByConv.get(activeConversationId) ?? [] : [],
32683
32684
  openThreadRootId,
32685
+ replyTarget,
32684
32686
  threadingMode,
32685
32687
  status,
32686
32688
  error
@@ -32777,6 +32779,7 @@ function createConversation(config = {}) {
32777
32779
  newConversation(opts) {
32778
32780
  const conversation = createConversationInternal(opts ?? {});
32779
32781
  openThreadRootId = null;
32782
+ replyTarget = null;
32780
32783
  emit2();
32781
32784
  return conversation;
32782
32785
  },
@@ -32784,6 +32787,7 @@ function createConversation(config = {}) {
32784
32787
  if (!conversations.has(conversationId) || activeConversationId === conversationId) return;
32785
32788
  activeConversationId = conversationId;
32786
32789
  openThreadRootId = null;
32790
+ replyTarget = null;
32787
32791
  emit2();
32788
32792
  },
32789
32793
  deleteConversation(conversationId) {
@@ -32793,6 +32797,7 @@ function createConversation(config = {}) {
32793
32797
  if (activeConversationId === conversationId) {
32794
32798
  activeConversationId = orderedConversations()[0]?.id ?? null;
32795
32799
  openThreadRootId = null;
32800
+ replyTarget = null;
32796
32801
  }
32797
32802
  emit2();
32798
32803
  },
@@ -32830,6 +32835,7 @@ function createConversation(config = {}) {
32830
32835
  const next = list.filter((m2) => !removeIds.has(m2.id));
32831
32836
  messagesByConv.set(activeConversationId, next);
32832
32837
  if (openThreadRootId && removeIds.has(openThreadRootId)) openThreadRootId = null;
32838
+ if (replyTarget && removeIds.has(replyTarget)) replyTarget = openThreadRootId;
32833
32839
  emit2();
32834
32840
  },
32835
32841
  react(messageId, emoji) {
@@ -32860,6 +32866,7 @@ function createConversation(config = {}) {
32860
32866
  const conversationId = ensureActiveConversation(opts);
32861
32867
  const list = messagesByConv.get(conversationId);
32862
32868
  const parentId = opts?.replyTo ? rootIdOf(list, opts.replyTo) : void 0;
32869
+ const replyToId = opts?.replyTo;
32863
32870
  const isFirstRoot = !parentId && selectRoots(list).length === 0;
32864
32871
  const userMsg = {
32865
32872
  id: generateId("rfr-msg"),
@@ -32870,6 +32877,7 @@ function createConversation(config = {}) {
32870
32877
  timestamp: /* @__PURE__ */ new Date(),
32871
32878
  status: "sent",
32872
32879
  parentId,
32880
+ replyToId,
32873
32881
  attachments: opts?.attachments,
32874
32882
  metadata: opts?.metadata
32875
32883
  };
@@ -32926,11 +32934,20 @@ function createConversation(config = {}) {
32926
32934
  },
32927
32935
  openThread(rootId) {
32928
32936
  openThreadRootId = rootId;
32937
+ replyTarget = rootId;
32938
+ emit2();
32939
+ },
32940
+ replyTo(messageId) {
32941
+ if (!activeConversationId) return;
32942
+ const list = messagesByConv.get(activeConversationId);
32943
+ openThreadRootId = rootIdOf(list, messageId);
32944
+ replyTarget = messageId;
32929
32945
  emit2();
32930
32946
  },
32931
32947
  closeThread() {
32932
- if (openThreadRootId === null) return;
32948
+ if (openThreadRootId === null && replyTarget === null) return;
32933
32949
  openThreadRootId = null;
32950
+ replyTarget = null;
32934
32951
  emit2();
32935
32952
  },
32936
32953
  setThreadingMode(mode) {
@@ -33274,6 +33291,7 @@ function useConversation(config) {
33274
33291
  retryLast: api.retryLast,
33275
33292
  stop: api.stop,
33276
33293
  openThread: api.openThread,
33294
+ replyTo: api.replyTo,
33277
33295
  closeThread: api.closeThread,
33278
33296
  setThreadingMode: api.setThreadingMode
33279
33297
  };
@@ -33338,6 +33356,8 @@ function Composer({
33338
33356
  toolbar = true,
33339
33357
  emoji = true,
33340
33358
  attachments = true,
33359
+ error,
33360
+ onRetry,
33341
33361
  onSubmit,
33342
33362
  onStop,
33343
33363
  onSlashCommand,
@@ -33478,148 +33498,154 @@ ${sel}
33478
33498
  submit();
33479
33499
  }
33480
33500
  }
33501
+ const iconBtn = "flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent hover:text-foreground";
33481
33502
  const toolbarBtn = (label, title, kind) => h(
33482
33503
  "button",
33483
33504
  {
33484
33505
  key: kind,
33485
33506
  type: "button",
33486
33507
  title,
33487
- className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground",
33508
+ className: cn(iconBtn, "text-xs font-medium"),
33488
33509
  onMouseDown: (e2) => e2.preventDefault(),
33489
33510
  // keep textarea selection
33490
33511
  onClick: () => format(kind)
33491
33512
  },
33492
33513
  label
33493
33514
  );
33494
- return h(
33515
+ const menu = menuOpen ? h(
33495
33516
  "div",
33496
- { className: "border-t border-border" },
33497
- // attachment chips
33498
- pending.length > 0 ? h(
33517
+ {
33518
+ className: "absolute bottom-full left-0 z-20 mb-2 w-72 overflow-hidden rounded-xl border border-border bg-popover shadow-lg",
33519
+ role: "listbox"
33520
+ },
33521
+ h(
33499
33522
  "div",
33500
- { className: "flex flex-wrap gap-2 px-3 pt-2" },
33501
- ...pending.map(
33502
- (a2) => h(
33503
- "span",
33504
- { key: a2.id, className: "inline-flex items-center gap-1 rounded bg-muted px-2 py-0.5 text-xs" },
33505
- a2.name,
33506
- h(
33507
- "button",
33508
- {
33509
- type: "button",
33510
- className: "text-muted-foreground hover:text-destructive",
33511
- onClick: () => setPending((p2) => p2.filter((x2) => x2.id !== a2.id))
33512
- },
33513
- "\u2715"
33514
- )
33515
- )
33523
+ { className: "border-b border-border px-3 py-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground" },
33524
+ trigger?.type === "/" ? "Commands" : trigger?.type === "@" ? "Mentions" : "Emoji"
33525
+ ),
33526
+ ...items.map(
33527
+ (it2, i2) => h(
33528
+ "button",
33529
+ {
33530
+ key: it2.key,
33531
+ type: "button",
33532
+ role: "option",
33533
+ "aria-selected": i2 === active,
33534
+ className: cn(
33535
+ "flex w-full items-center gap-2 px-3 py-2 text-left text-sm",
33536
+ i2 === active ? "bg-accent" : "hover:bg-accent/50"
33537
+ ),
33538
+ onMouseEnter: () => setActive(i2),
33539
+ onMouseDown: (e2) => e2.preventDefault(),
33540
+ onClick: () => selectItem(i2)
33541
+ },
33542
+ it2.icon ? h("span", { className: "w-4 text-center text-muted-foreground" }, it2.icon) : null,
33543
+ h("span", { className: "flex-1 truncate" }, it2.primary),
33544
+ it2.secondary ? h("span", { className: "truncate text-xs text-muted-foreground" }, it2.secondary) : null
33516
33545
  )
33517
- ) : null,
33518
- // toolbar
33519
- toolbar ? h(
33520
- "div",
33521
- { className: "flex items-center gap-0.5 px-2 pt-2" },
33522
- toolbarBtn("B", "Bold (\u2318B)", "bold"),
33523
- toolbarBtn("\u{1D456}", "Italic (\u2318I)", "italic"),
33524
- toolbarBtn("</>", "Code (\u2318E)", "code"),
33525
- toolbarBtn("\u{1F517}", "Link (\u2318K)", "link"),
33526
- toolbarBtn("\u275D", "Quote", "quote"),
33527
- toolbarBtn("\u2022", "Bulleted list", "ul"),
33528
- toolbarBtn("1.", "Numbered list", "ol")
33529
- ) : null,
33530
- // input row (relative for the popup menu)
33546
+ )
33547
+ ) : null;
33548
+ return h(
33549
+ "div",
33550
+ { className: "p-3" },
33531
33551
  h(
33532
33552
  "div",
33533
- { className: "relative flex items-end gap-2 p-3" },
33534
- menuOpen ? h(
33553
+ { className: "relative" },
33554
+ menu,
33555
+ // unified input card
33556
+ h(
33535
33557
  "div",
33536
33558
  {
33537
- className: "absolute bottom-full left-3 z-20 mb-1 w-72 overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
33538
- role: "listbox"
33559
+ className: "overflow-hidden rounded-2xl border border-border bg-background transition focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/40"
33539
33560
  },
33561
+ // error banner
33562
+ error ? h(
33563
+ "div",
33564
+ { className: "flex items-center gap-2 border-b border-border bg-destructive/5 px-3 py-2 text-xs text-destructive", role: "alert" },
33565
+ h("span", { className: "flex-1 truncate" }, error),
33566
+ onRetry ? h("button", { type: "button", className: "font-medium underline", onClick: () => onRetry() }, "Retry") : null
33567
+ ) : null,
33568
+ // attachment chips
33569
+ pending.length > 0 ? h(
33570
+ "div",
33571
+ { className: "flex flex-wrap gap-2 px-3 pt-3" },
33572
+ ...pending.map(
33573
+ (a2) => h(
33574
+ "span",
33575
+ { key: a2.id, className: "inline-flex items-center gap-1 rounded-md bg-muted px-2 py-0.5 text-xs" },
33576
+ a2.name,
33577
+ h(
33578
+ "button",
33579
+ { type: "button", className: "text-muted-foreground hover:text-destructive", onClick: () => setPending((p2) => p2.filter((x2) => x2.id !== a2.id)) },
33580
+ "\u2715"
33581
+ )
33582
+ )
33583
+ )
33584
+ ) : null,
33585
+ // textarea (borderless)
33586
+ h("textarea", {
33587
+ ref,
33588
+ className: "block max-h-40 w-full resize-none bg-transparent px-3.5 py-3 text-sm placeholder:text-muted-foreground focus:outline-none",
33589
+ rows: 1,
33590
+ value,
33591
+ placeholder,
33592
+ autoFocus,
33593
+ "aria-label": "Message",
33594
+ onChange: (e2) => syncFromTextarea(e2.target),
33595
+ onClick: (e2) => syncFromTextarea(e2.currentTarget),
33596
+ onKeyUp: (e2) => syncFromTextarea(e2.currentTarget),
33597
+ onKeyDown,
33598
+ onBlur: () => setTimeout(() => setTrigger(null), 120)
33599
+ }),
33600
+ // bottom action bar
33540
33601
  h(
33541
33602
  "div",
33542
- { className: "border-b border-border px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground" },
33543
- trigger?.type === "/" ? "Commands" : trigger?.type === "@" ? "Mentions" : "Emoji"
33544
- ),
33545
- ...items.map(
33546
- (it2, i2) => h(
33603
+ { className: "flex items-center gap-0.5 px-2 pb-2" },
33604
+ attachments ? h(
33605
+ React11__namespace.Fragment,
33606
+ null,
33607
+ h("input", {
33608
+ ref: fileRef,
33609
+ type: "file",
33610
+ accept: "image/*",
33611
+ multiple: true,
33612
+ className: "hidden",
33613
+ onChange: (e2) => {
33614
+ onFiles(e2.target.files);
33615
+ e2.target.value = "";
33616
+ }
33617
+ }),
33618
+ h("button", { type: "button", className: iconBtn, "aria-label": "Attach image or GIF", onClick: () => fileRef.current?.click() }, "\u{1F4CE}")
33619
+ ) : null,
33620
+ attachments && toolbar ? h("span", { className: "mx-1 h-5 w-px bg-border" }) : null,
33621
+ toolbar ? h(
33622
+ React11__namespace.Fragment,
33623
+ null,
33624
+ toolbarBtn("B", "Bold (\u2318B)", "bold"),
33625
+ toolbarBtn("\u{1D456}", "Italic (\u2318I)", "italic"),
33626
+ toolbarBtn("</>", "Code (\u2318E)", "code"),
33627
+ toolbarBtn("\u{1F517}", "Link (\u2318K)", "link"),
33628
+ toolbarBtn("\u275D", "Quote", "quote"),
33629
+ toolbarBtn("\u2022", "Bulleted list", "ul"),
33630
+ toolbarBtn("1.", "Numbered list", "ol")
33631
+ ) : null,
33632
+ h("div", { className: "flex-1" }),
33633
+ busy ? h(
33634
+ "button",
33635
+ { type: "button", "aria-label": "Stop", className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", onClick: () => onStop?.() },
33636
+ "\u25A0"
33637
+ ) : h(
33547
33638
  "button",
33548
33639
  {
33549
- key: it2.key,
33550
33640
  type: "button",
33551
- role: "option",
33552
- "aria-selected": i2 === active,
33553
- className: cn(
33554
- "flex w-full items-center gap-2 px-2 py-1.5 text-left text-sm",
33555
- i2 === active ? "bg-accent" : "hover:bg-accent/50"
33556
- ),
33557
- onMouseEnter: () => setActive(i2),
33558
- onMouseDown: (e2) => e2.preventDefault(),
33559
- onClick: () => selectItem(i2)
33641
+ "aria-label": "Send",
33642
+ className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-base font-semibold text-primary-foreground transition disabled:opacity-40",
33643
+ disabled: !value.trim() && pending.length === 0,
33644
+ onClick: submit
33560
33645
  },
33561
- it2.icon ? h("span", { className: "w-4 text-center text-muted-foreground" }, it2.icon) : null,
33562
- h("span", { className: "flex-1 truncate" }, it2.primary),
33563
- it2.secondary ? h("span", { className: "truncate text-xs text-muted-foreground" }, it2.secondary) : null
33646
+ "\u2191"
33564
33647
  )
33565
33648
  )
33566
- ) : null,
33567
- attachments ? h(
33568
- React11__namespace.Fragment,
33569
- null,
33570
- h("input", {
33571
- ref: fileRef,
33572
- type: "file",
33573
- accept: "image/*",
33574
- multiple: true,
33575
- className: "hidden",
33576
- onChange: (e2) => {
33577
- onFiles(e2.target.files);
33578
- e2.target.value = "";
33579
- }
33580
- }),
33581
- h(
33582
- "button",
33583
- {
33584
- type: "button",
33585
- className: "rounded-md border border-border px-2 py-2 text-sm hover:bg-accent",
33586
- "aria-label": "Attach image or GIF",
33587
- onClick: () => fileRef.current?.click()
33588
- },
33589
- "\u{1F4CE}"
33590
- )
33591
- ) : null,
33592
- h("textarea", {
33593
- ref,
33594
- className: "max-h-40 flex-1 resize-none rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary",
33595
- rows: 1,
33596
- value,
33597
- placeholder,
33598
- autoFocus,
33599
- "aria-label": "Message",
33600
- onChange: (e2) => syncFromTextarea(e2.target),
33601
- onClick: (e2) => syncFromTextarea(e2.currentTarget),
33602
- onKeyUp: (e2) => syncFromTextarea(e2.currentTarget),
33603
- onKeyDown,
33604
- onBlur: () => setTimeout(() => setTrigger(null), 120)
33605
- }),
33606
- busy ? h(
33607
- "button",
33608
- {
33609
- type: "button",
33610
- className: "rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground",
33611
- onClick: () => onStop?.()
33612
- },
33613
- "Stop"
33614
- ) : h(
33615
- "button",
33616
- {
33617
- type: "button",
33618
- className: "rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50",
33619
- disabled: !value.trim() && pending.length === 0,
33620
- onClick: submit
33621
- },
33622
- "Send"
33623
33649
  )
33624
33650
  )
33625
33651
  );
@@ -33736,7 +33762,7 @@ function HoverActions({
33736
33762
  onToggleEmojis,
33737
33763
  align
33738
33764
  }) {
33739
- const { state, openThread, deleteMessage } = conversation;
33765
+ const { replyTo, deleteMessage } = conversation;
33740
33766
  return h2(
33741
33767
  "div",
33742
33768
  {
@@ -33745,7 +33771,8 @@ function HoverActions({
33745
33771
  align === "end" && "justify-end"
33746
33772
  )
33747
33773
  },
33748
- h2("button", { type: "button", className: "hover:text-foreground", onClick: () => openThread(rootIdOf(state.messages, message.id)) }, "Reply"),
33774
+ // Reply targets this specific message but groups under the originating root.
33775
+ h2("button", { type: "button", className: "hover:text-foreground", onClick: () => replyTo(message.id) }, "Reply"),
33749
33776
  h2("button", { type: "button", className: "hover:text-foreground", onClick: onToggleEmojis }, "React"),
33750
33777
  isOwn ? h2("button", { type: "button", className: "hover:text-foreground", onClick: onEdit }, "Edit") : null,
33751
33778
  isOwn ? h2("button", { type: "button", className: "hover:text-destructive", onClick: () => deleteMessage(message.id) }, "Delete") : null
@@ -33807,7 +33834,7 @@ function MessageRow({
33807
33834
  const inner = h2(
33808
33835
  React11__namespace.Fragment,
33809
33836
  null,
33810
- quotedParent ? h2(QuotedParent, { parent: quotedParent, onClick: () => openThread(quotedParent.id) }) : null,
33837
+ quotedParent ? h2(QuotedParent, { parent: quotedParent, onClick: () => openThread(rootIdOf(state.messages, quotedParent.id)) }) : null,
33811
33838
  editing ? h2(EditField, { message, conversation, onDone: () => setEditing(false) }) : isUser ? h2("div", { className: "inline-block rounded-2xl rounded-br-sm bg-primary/10 px-3 py-2 text-left" }, h2(MessageBody, { message })) : h2(MessageBody, { message }),
33812
33839
  h2(Reactions, { message, onReact: (e2) => react(message.id, e2), align }),
33813
33840
  showThreadAffordance && replyCount > 0 ? h2(
@@ -33907,13 +33934,14 @@ function ThreadPanel({ conversation, currentUserId, composer }) {
33907
33934
  const rootId = state.openThreadRootId;
33908
33935
  if (!rootId) return null;
33909
33936
  const messages = selectThreadMessages(state.messages, rootId);
33937
+ const target = state.replyTarget && state.replyTarget !== rootId ? findMessage(state.messages, state.replyTarget) : void 0;
33910
33938
  return h2(
33911
33939
  "aside",
33912
33940
  { className: "flex w-80 flex-col border-l border-border", "aria-label": "Thread" },
33913
33941
  h2(
33914
33942
  "div",
33915
33943
  { className: "flex items-center justify-between border-b border-border px-3 py-2" },
33916
- h2("span", { className: "text-sm font-semibold" }, "Thread"),
33944
+ h2("span", { className: "text-sm font-semibold" }, `Thread \xB7 ${messages.length - 1} ${messages.length - 1 === 1 ? "reply" : "replies"}`),
33917
33945
  h2("button", { type: "button", className: "text-muted-foreground hover:text-foreground", "aria-label": "Close thread", onClick: () => conversation.closeThread() }, "\u2715")
33918
33946
  ),
33919
33947
  h2(
@@ -33921,6 +33949,12 @@ function ThreadPanel({ conversation, currentUserId, composer }) {
33921
33949
  { className: "flex-1 overflow-y-auto p-1" },
33922
33950
  ...messages.map((m2) => h2(MessageRow, { key: m2.id, message: m2, conversation, currentUserId, showThreadAffordance: false }))
33923
33951
  ),
33952
+ target ? h2(
33953
+ "div",
33954
+ { className: "flex items-center justify-between gap-2 border-t border-border bg-muted/40 px-3 py-1 text-xs text-muted-foreground" },
33955
+ h2("span", { className: "truncate" }, `\u21B3 Replying to ${target.author.name}`),
33956
+ h2("button", { type: "button", className: "hover:text-foreground", onClick: () => conversation.openThread(rootId) }, "Reply to thread instead")
33957
+ ) : null,
33924
33958
  composer
33925
33959
  );
33926
33960
  }
@@ -33959,9 +33993,13 @@ function Chat({
33959
33993
  const timeline = selectMainTimeline(state.messages, state.threadingMode);
33960
33994
  const activeConv = state.conversations.find((c2) => c2.id === state.activeConversationId);
33961
33995
  const busy = state.status === "sending" || state.status === "streaming";
33996
+ const error = state.status === "error" ? state.error : null;
33997
+ const onRetry = () => void conversation.retryLast();
33962
33998
  const mainComposer = h2(Composer, {
33963
33999
  placeholder,
33964
34000
  busy,
34001
+ error,
34002
+ onRetry,
33965
34003
  slashCommands,
33966
34004
  mentions,
33967
34005
  onSlashCommand,
@@ -33972,11 +34010,13 @@ function Chat({
33972
34010
  const threadComposer = state.openThreadRootId ? h2(Composer, {
33973
34011
  placeholder: "Reply\u2026",
33974
34012
  busy,
34013
+ error,
34014
+ onRetry,
33975
34015
  slashCommands,
33976
34016
  mentions,
33977
34017
  onSlashCommand,
33978
34018
  toolbar: composerToolbar,
33979
- onSubmit: (content, atts) => void sendMessage(content, { replyTo: state.openThreadRootId, attachments: atts }),
34019
+ onSubmit: (content, atts) => void sendMessage(content, { replyTo: state.replyTarget ?? state.openThreadRootId, attachments: atts }),
33980
34020
  onStop: () => conversation.stop()
33981
34021
  }) : null;
33982
34022
  const body = timeline.length === 0 ? h2("div", { className: "flex flex-1 items-center justify-center p-6 text-sm text-muted-foreground" }, emptyState ?? "No messages yet. Say hello \u{1F44B}") : h2(
@@ -33988,8 +34028,10 @@ function Chat({
33988
34028
  message: m2,
33989
34029
  conversation,
33990
34030
  currentUserId,
33991
- showThreadAffordance: state.threadingMode === "panel",
33992
- quotedParent: state.threadingMode === "inline" && m2.parentId ? findMessage(state.messages, m2.parentId) : void 0
34031
+ // Show the "N replies" count on originating messages in BOTH modes.
34032
+ showThreadAffordance: true,
34033
+ // Inline: quote the specific message replied to (falls back to the root).
34034
+ quotedParent: state.threadingMode === "inline" && m2.parentId ? findMessage(state.messages, m2.replyToId ?? m2.parentId) : void 0
33993
34035
  })
33994
34036
  )
33995
34037
  );
@@ -34273,14 +34315,56 @@ function useCookieConsent(config) {
34273
34315
  };
34274
34316
  }
34275
34317
  var h3 = React11__namespace.createElement;
34276
- var btnPrimary = "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90";
34277
- var btnGhost = "rounded-md border border-border px-3 py-1.5 text-sm hover:bg-accent";
34278
- var btnLink = "text-sm text-muted-foreground underline hover:text-foreground";
34318
+ var btnBase = "inline-flex items-center justify-center rounded-lg px-3.5 py-2 text-sm font-medium transition-colors";
34319
+ var btnPrimary = cn(btnBase, "bg-primary text-primary-foreground hover:opacity-90");
34320
+ var btnGhost = cn(btnBase, "border border-border hover:bg-accent");
34321
+ var btnLink = "text-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline";
34322
+ function Toggle({
34323
+ checked,
34324
+ disabled,
34325
+ onChange,
34326
+ label
34327
+ }) {
34328
+ if (disabled) {
34329
+ return h3(
34330
+ "span",
34331
+ { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground" },
34332
+ "Always on"
34333
+ );
34334
+ }
34335
+ return h3(
34336
+ "button",
34337
+ {
34338
+ type: "button",
34339
+ role: "switch",
34340
+ "aria-checked": checked,
34341
+ "aria-label": label,
34342
+ onClick: () => onChange(!checked),
34343
+ className: cn(
34344
+ "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
34345
+ checked ? "bg-primary" : "bg-muted"
34346
+ )
34347
+ },
34348
+ h3("span", {
34349
+ className: cn(
34350
+ "inline-block h-4 w-4 transform rounded-full bg-background shadow transition-transform",
34351
+ checked ? "translate-x-[1.125rem]" : "translate-x-0.5"
34352
+ )
34353
+ })
34354
+ );
34355
+ }
34356
+ function CookieIcon() {
34357
+ return h3(
34358
+ "div",
34359
+ { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-xl", "aria-hidden": true },
34360
+ "\u{1F36A}"
34361
+ );
34362
+ }
34279
34363
  function CookieConsent({
34280
34364
  consent,
34281
34365
  position = "bottom",
34282
34366
  title = "We use cookies",
34283
- description = "We use cookies to run the site, remember your preferences, and measure traffic. Choose which categories to allow.",
34367
+ description = "We use cookies to run the site, remember your preferences, and measure traffic. Choose which to allow.",
34284
34368
  policyUrl,
34285
34369
  policyLabel = "Cookie policy",
34286
34370
  className
@@ -34288,64 +34372,80 @@ function CookieConsent({
34288
34372
  const { state, acceptAll, rejectAll, savePreferences, setPreference } = consent;
34289
34373
  const [settings, setSettings] = React11__namespace.useState(false);
34290
34374
  if (!state.open) return null;
34291
- const wrapper = cn(
34292
- "fixed inset-x-0 z-50 p-4",
34293
- position === "bottom" ? "bottom-0" : "top-0",
34294
- className
34295
- );
34296
- const panel = "mx-auto max-w-3xl rounded-xl border border-border bg-background p-4 shadow-lg";
34297
- const header = h3(
34298
- "div",
34299
- null,
34300
- h3("h2", { className: "text-base font-semibold" }, title),
34301
- h3("p", { className: "mt-1 text-sm text-muted-foreground" }, description),
34302
- policyUrl ? h3("a", { href: policyUrl, target: "_blank", rel: "noreferrer", className: cn(btnLink, "mt-1 inline-block") }, policyLabel) : null
34303
- );
34304
- const promptActions = h3(
34375
+ const wrapper = cn("fixed inset-x-0 z-50 p-4", position === "bottom" ? "bottom-0" : "top-0", className);
34376
+ const panel = "mx-auto max-w-2xl overflow-hidden rounded-2xl border border-border bg-background shadow-lg";
34377
+ const policy = policyUrl ? h3("a", { href: policyUrl, target: "_blank", rel: "noreferrer", className: cn(btnLink, "whitespace-nowrap") }, policyLabel) : null;
34378
+ const promptView = h3(
34305
34379
  "div",
34306
- { className: "mt-3 flex flex-wrap items-center gap-2" },
34307
- h3("button", { type: "button", className: btnPrimary, onClick: () => acceptAll() }, "Accept all"),
34308
- h3("button", { type: "button", className: btnGhost, onClick: () => rejectAll() }, "Reject all"),
34309
- h3("button", { type: "button", className: cn(btnGhost, "ml-auto"), onClick: () => setSettings(true) }, "Manage preferences")
34380
+ { className: "flex flex-col gap-4 p-5 sm:flex-row sm:items-center" },
34381
+ h3(CookieIcon),
34382
+ h3(
34383
+ "div",
34384
+ { className: "min-w-0 flex-1" },
34385
+ h3("p", { className: "text-sm font-semibold" }, title),
34386
+ h3(
34387
+ "p",
34388
+ { className: "mt-0.5 text-sm leading-relaxed text-muted-foreground" },
34389
+ description,
34390
+ policy ? h3(React11__namespace.Fragment, null, " ", policy) : null
34391
+ )
34392
+ ),
34393
+ h3(
34394
+ "div",
34395
+ { className: "flex flex-wrap items-center gap-2 sm:shrink-0" },
34396
+ h3("button", { type: "button", className: btnLink, onClick: () => setSettings(true) }, "Customize"),
34397
+ h3("button", { type: "button", className: btnGhost, onClick: () => rejectAll() }, "Reject all"),
34398
+ h3("button", { type: "button", className: btnPrimary, onClick: () => acceptAll() }, "Accept all")
34399
+ )
34310
34400
  );
34311
34401
  const settingsView = h3(
34312
34402
  "div",
34313
- { className: "mt-3 space-y-2" },
34314
- ...state.categories.map(
34315
- (cat) => h3(
34316
- "label",
34317
- {
34318
- key: cat.id,
34319
- className: "flex items-start justify-between gap-3 rounded-md border border-border p-3"
34320
- },
34321
- h3(
34322
- "span",
34323
- { className: "min-w-0" },
34324
- h3("span", { className: "block text-sm font-medium" }, cat.label, cat.required ? " (required)" : ""),
34325
- cat.description ? h3("span", { className: "block text-xs text-muted-foreground" }, cat.description) : null
34326
- ),
34327
- h3("input", {
34328
- type: "checkbox",
34329
- className: "mt-0.5 h-4 w-4 accent-[hsl(var(--primary))]",
34330
- checked: !!state.preferences[cat.id],
34331
- disabled: cat.required,
34332
- "aria-label": cat.label,
34333
- onChange: (e2) => setPreference(cat.id, e2.target.checked)
34334
- })
34403
+ { className: "p-5" },
34404
+ h3(
34405
+ "div",
34406
+ { className: "flex items-center gap-3" },
34407
+ h3(CookieIcon),
34408
+ h3(
34409
+ "div",
34410
+ null,
34411
+ h3("p", { className: "text-sm font-semibold" }, "Cookie preferences"),
34412
+ h3("p", { className: "text-xs text-muted-foreground" }, "Toggle the categories you want to allow.")
34413
+ )
34414
+ ),
34415
+ h3(
34416
+ "div",
34417
+ { className: "mt-4 space-y-2" },
34418
+ ...state.categories.map(
34419
+ (cat) => h3(
34420
+ "div",
34421
+ { key: cat.id, className: "flex items-center justify-between gap-4 rounded-xl border border-border p-3" },
34422
+ h3(
34423
+ "div",
34424
+ { className: "min-w-0" },
34425
+ h3("p", { className: "text-sm font-medium" }, cat.label),
34426
+ cat.description ? h3("p", { className: "mt-0.5 text-xs text-muted-foreground" }, cat.description) : null
34427
+ ),
34428
+ h3(Toggle, {
34429
+ checked: !!state.preferences[cat.id],
34430
+ disabled: cat.required,
34431
+ label: cat.label,
34432
+ onChange: (v2) => setPreference(cat.id, v2)
34433
+ })
34434
+ )
34335
34435
  )
34336
34436
  ),
34337
34437
  h3(
34338
34438
  "div",
34339
- { className: "flex flex-wrap items-center gap-2 pt-1" },
34340
- h3("button", { type: "button", className: btnPrimary, onClick: () => savePreferences(state.preferences) }, "Save preferences"),
34341
- h3("button", { type: "button", className: btnGhost, onClick: () => acceptAll() }, "Accept all"),
34342
- h3("button", { type: "button", className: cn(btnLink, "ml-auto"), onClick: () => setSettings(false) }, "Back")
34439
+ { className: "mt-4 flex flex-wrap items-center gap-2" },
34440
+ h3("button", { type: "button", className: btnLink, onClick: () => setSettings(false) }, "\u2190 Back"),
34441
+ h3("button", { type: "button", className: cn(btnGhost, "sm:ml-auto"), onClick: () => acceptAll() }, "Accept all"),
34442
+ h3("button", { type: "button", className: btnPrimary, onClick: () => savePreferences(state.preferences) }, "Save preferences")
34343
34443
  )
34344
34444
  );
34345
34445
  return h3(
34346
34446
  "div",
34347
34447
  { className: wrapper, role: "dialog", "aria-label": "Cookie consent", "aria-modal": false },
34348
- h3("div", { className: panel }, header, settings ? settingsView : promptActions)
34448
+ h3("div", { className: panel }, settings ? settingsView : promptView)
34349
34449
  );
34350
34450
  }
34351
34451