@refraction-ui/react 0.11.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.js CHANGED
@@ -3427,6 +3427,8 @@ function Composer({
3427
3427
  toolbar = true,
3428
3428
  emoji = true,
3429
3429
  attachments = true,
3430
+ error,
3431
+ onRetry,
3430
3432
  onSubmit,
3431
3433
  onStop,
3432
3434
  onSlashCommand,
@@ -3567,148 +3569,154 @@ ${sel}
3567
3569
  submit();
3568
3570
  }
3569
3571
  }
3572
+ const iconBtn = "flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent hover:text-foreground";
3570
3573
  const toolbarBtn = (label, title, kind) => h(
3571
3574
  "button",
3572
3575
  {
3573
3576
  key: kind,
3574
3577
  type: "button",
3575
3578
  title,
3576
- className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground",
3579
+ className: cn(iconBtn, "text-xs font-medium"),
3577
3580
  onMouseDown: (e) => e.preventDefault(),
3578
3581
  // keep textarea selection
3579
3582
  onClick: () => format(kind)
3580
3583
  },
3581
3584
  label
3582
3585
  );
3583
- return h(
3586
+ const menu = menuOpen ? h(
3584
3587
  "div",
3585
- { className: "border-t border-border" },
3586
- // attachment chips
3587
- pending.length > 0 ? h(
3588
+ {
3589
+ className: "absolute bottom-full left-0 z-20 mb-2 w-72 overflow-hidden rounded-xl border border-border bg-popover shadow-lg",
3590
+ role: "listbox"
3591
+ },
3592
+ h(
3588
3593
  "div",
3589
- { className: "flex flex-wrap gap-2 px-3 pt-2" },
3590
- ...pending.map(
3591
- (a) => h(
3592
- "span",
3593
- { key: a.id, className: "inline-flex items-center gap-1 rounded bg-muted px-2 py-0.5 text-xs" },
3594
- a.name,
3595
- h(
3596
- "button",
3597
- {
3598
- type: "button",
3599
- className: "text-muted-foreground hover:text-destructive",
3600
- onClick: () => setPending((p) => p.filter((x) => x.id !== a.id))
3601
- },
3602
- "\u2715"
3603
- )
3604
- )
3594
+ { className: "border-b border-border px-3 py-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground" },
3595
+ trigger?.type === "/" ? "Commands" : trigger?.type === "@" ? "Mentions" : "Emoji"
3596
+ ),
3597
+ ...items.map(
3598
+ (it, i) => h(
3599
+ "button",
3600
+ {
3601
+ key: it.key,
3602
+ type: "button",
3603
+ role: "option",
3604
+ "aria-selected": i === active,
3605
+ className: cn(
3606
+ "flex w-full items-center gap-2 px-3 py-2 text-left text-sm",
3607
+ i === active ? "bg-accent" : "hover:bg-accent/50"
3608
+ ),
3609
+ onMouseEnter: () => setActive(i),
3610
+ onMouseDown: (e) => e.preventDefault(),
3611
+ onClick: () => selectItem(i)
3612
+ },
3613
+ it.icon ? h("span", { className: "w-4 text-center text-muted-foreground" }, it.icon) : null,
3614
+ h("span", { className: "flex-1 truncate" }, it.primary),
3615
+ it.secondary ? h("span", { className: "truncate text-xs text-muted-foreground" }, it.secondary) : null
3605
3616
  )
3606
- ) : null,
3607
- // toolbar
3608
- toolbar ? h(
3609
- "div",
3610
- { className: "flex items-center gap-0.5 px-2 pt-2" },
3611
- toolbarBtn("B", "Bold (\u2318B)", "bold"),
3612
- toolbarBtn("\u{1D456}", "Italic (\u2318I)", "italic"),
3613
- toolbarBtn("</>", "Code (\u2318E)", "code"),
3614
- toolbarBtn("\u{1F517}", "Link (\u2318K)", "link"),
3615
- toolbarBtn("\u275D", "Quote", "quote"),
3616
- toolbarBtn("\u2022", "Bulleted list", "ul"),
3617
- toolbarBtn("1.", "Numbered list", "ol")
3618
- ) : null,
3619
- // input row (relative for the popup menu)
3617
+ )
3618
+ ) : null;
3619
+ return h(
3620
+ "div",
3621
+ { className: "p-3" },
3620
3622
  h(
3621
3623
  "div",
3622
- { className: "relative flex items-end gap-2 p-3" },
3623
- menuOpen ? h(
3624
+ { className: "relative" },
3625
+ menu,
3626
+ // unified input card
3627
+ h(
3624
3628
  "div",
3625
3629
  {
3626
- className: "absolute bottom-full left-3 z-20 mb-1 w-72 overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
3627
- role: "listbox"
3630
+ className: "overflow-hidden rounded-2xl border border-border bg-background transition focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/40"
3628
3631
  },
3632
+ // error banner
3633
+ error ? h(
3634
+ "div",
3635
+ { className: "flex items-center gap-2 border-b border-border bg-destructive/5 px-3 py-2 text-xs text-destructive", role: "alert" },
3636
+ h("span", { className: "flex-1 truncate" }, error),
3637
+ onRetry ? h("button", { type: "button", className: "font-medium underline", onClick: () => onRetry() }, "Retry") : null
3638
+ ) : null,
3639
+ // attachment chips
3640
+ pending.length > 0 ? h(
3641
+ "div",
3642
+ { className: "flex flex-wrap gap-2 px-3 pt-3" },
3643
+ ...pending.map(
3644
+ (a) => h(
3645
+ "span",
3646
+ { key: a.id, className: "inline-flex items-center gap-1 rounded-md bg-muted px-2 py-0.5 text-xs" },
3647
+ a.name,
3648
+ h(
3649
+ "button",
3650
+ { type: "button", className: "text-muted-foreground hover:text-destructive", onClick: () => setPending((p) => p.filter((x) => x.id !== a.id)) },
3651
+ "\u2715"
3652
+ )
3653
+ )
3654
+ )
3655
+ ) : null,
3656
+ // textarea (borderless)
3657
+ h("textarea", {
3658
+ ref,
3659
+ 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",
3660
+ rows: 1,
3661
+ value,
3662
+ placeholder,
3663
+ autoFocus,
3664
+ "aria-label": "Message",
3665
+ onChange: (e) => syncFromTextarea(e.target),
3666
+ onClick: (e) => syncFromTextarea(e.currentTarget),
3667
+ onKeyUp: (e) => syncFromTextarea(e.currentTarget),
3668
+ onKeyDown,
3669
+ onBlur: () => setTimeout(() => setTrigger(null), 120)
3670
+ }),
3671
+ // bottom action bar
3629
3672
  h(
3630
3673
  "div",
3631
- { className: "border-b border-border px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground" },
3632
- trigger?.type === "/" ? "Commands" : trigger?.type === "@" ? "Mentions" : "Emoji"
3633
- ),
3634
- ...items.map(
3635
- (it, i) => h(
3674
+ { className: "flex items-center gap-0.5 px-2 pb-2" },
3675
+ attachments ? h(
3676
+ React11.Fragment,
3677
+ null,
3678
+ h("input", {
3679
+ ref: fileRef,
3680
+ type: "file",
3681
+ accept: "image/*",
3682
+ multiple: true,
3683
+ className: "hidden",
3684
+ onChange: (e) => {
3685
+ onFiles(e.target.files);
3686
+ e.target.value = "";
3687
+ }
3688
+ }),
3689
+ h("button", { type: "button", className: iconBtn, "aria-label": "Attach image or GIF", onClick: () => fileRef.current?.click() }, "\u{1F4CE}")
3690
+ ) : null,
3691
+ attachments && toolbar ? h("span", { className: "mx-1 h-5 w-px bg-border" }) : null,
3692
+ toolbar ? h(
3693
+ React11.Fragment,
3694
+ null,
3695
+ toolbarBtn("B", "Bold (\u2318B)", "bold"),
3696
+ toolbarBtn("\u{1D456}", "Italic (\u2318I)", "italic"),
3697
+ toolbarBtn("</>", "Code (\u2318E)", "code"),
3698
+ toolbarBtn("\u{1F517}", "Link (\u2318K)", "link"),
3699
+ toolbarBtn("\u275D", "Quote", "quote"),
3700
+ toolbarBtn("\u2022", "Bulleted list", "ul"),
3701
+ toolbarBtn("1.", "Numbered list", "ol")
3702
+ ) : null,
3703
+ h("div", { className: "flex-1" }),
3704
+ busy ? h(
3705
+ "button",
3706
+ { type: "button", "aria-label": "Stop", className: "flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", onClick: () => onStop?.() },
3707
+ "\u25A0"
3708
+ ) : h(
3636
3709
  "button",
3637
3710
  {
3638
- key: it.key,
3639
3711
  type: "button",
3640
- role: "option",
3641
- "aria-selected": i === active,
3642
- className: cn(
3643
- "flex w-full items-center gap-2 px-2 py-1.5 text-left text-sm",
3644
- i === active ? "bg-accent" : "hover:bg-accent/50"
3645
- ),
3646
- onMouseEnter: () => setActive(i),
3647
- onMouseDown: (e) => e.preventDefault(),
3648
- onClick: () => selectItem(i)
3712
+ "aria-label": "Send",
3713
+ 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",
3714
+ disabled: !value.trim() && pending.length === 0,
3715
+ onClick: submit
3649
3716
  },
3650
- it.icon ? h("span", { className: "w-4 text-center text-muted-foreground" }, it.icon) : null,
3651
- h("span", { className: "flex-1 truncate" }, it.primary),
3652
- it.secondary ? h("span", { className: "truncate text-xs text-muted-foreground" }, it.secondary) : null
3717
+ "\u2191"
3653
3718
  )
3654
3719
  )
3655
- ) : null,
3656
- attachments ? h(
3657
- React11.Fragment,
3658
- null,
3659
- h("input", {
3660
- ref: fileRef,
3661
- type: "file",
3662
- accept: "image/*",
3663
- multiple: true,
3664
- className: "hidden",
3665
- onChange: (e) => {
3666
- onFiles(e.target.files);
3667
- e.target.value = "";
3668
- }
3669
- }),
3670
- h(
3671
- "button",
3672
- {
3673
- type: "button",
3674
- className: "rounded-md border border-border px-2 py-2 text-sm hover:bg-accent",
3675
- "aria-label": "Attach image or GIF",
3676
- onClick: () => fileRef.current?.click()
3677
- },
3678
- "\u{1F4CE}"
3679
- )
3680
- ) : null,
3681
- h("textarea", {
3682
- ref,
3683
- 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",
3684
- rows: 1,
3685
- value,
3686
- placeholder,
3687
- autoFocus,
3688
- "aria-label": "Message",
3689
- onChange: (e) => syncFromTextarea(e.target),
3690
- onClick: (e) => syncFromTextarea(e.currentTarget),
3691
- onKeyUp: (e) => syncFromTextarea(e.currentTarget),
3692
- onKeyDown,
3693
- onBlur: () => setTimeout(() => setTrigger(null), 120)
3694
- }),
3695
- busy ? h(
3696
- "button",
3697
- {
3698
- type: "button",
3699
- className: "rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground",
3700
- onClick: () => onStop?.()
3701
- },
3702
- "Stop"
3703
- ) : h(
3704
- "button",
3705
- {
3706
- type: "button",
3707
- className: "rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50",
3708
- disabled: !value.trim() && pending.length === 0,
3709
- onClick: submit
3710
- },
3711
- "Send"
3712
3720
  )
3713
3721
  )
3714
3722
  );
@@ -4056,9 +4064,13 @@ function Chat({
4056
4064
  const timeline = selectMainTimeline(state.messages, state.threadingMode);
4057
4065
  const activeConv = state.conversations.find((c) => c.id === state.activeConversationId);
4058
4066
  const busy = state.status === "sending" || state.status === "streaming";
4067
+ const error = state.status === "error" ? state.error : null;
4068
+ const onRetry = () => void conversation.retryLast();
4059
4069
  const mainComposer = h2(Composer, {
4060
4070
  placeholder,
4061
4071
  busy,
4072
+ error,
4073
+ onRetry,
4062
4074
  slashCommands,
4063
4075
  mentions,
4064
4076
  onSlashCommand,
@@ -4069,6 +4081,8 @@ function Chat({
4069
4081
  const threadComposer = state.openThreadRootId ? h2(Composer, {
4070
4082
  placeholder: "Reply\u2026",
4071
4083
  busy,
4084
+ error,
4085
+ onRetry,
4072
4086
  slashCommands,
4073
4087
  mentions,
4074
4088
  onSlashCommand,
@@ -4372,14 +4386,56 @@ function useCookieConsent(config) {
4372
4386
  };
4373
4387
  }
4374
4388
  var h3 = React11.createElement;
4375
- var btnPrimary = "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90";
4376
- var btnGhost = "rounded-md border border-border px-3 py-1.5 text-sm hover:bg-accent";
4377
- var btnLink = "text-sm text-muted-foreground underline hover:text-foreground";
4389
+ var btnBase = "inline-flex items-center justify-center rounded-lg px-3.5 py-2 text-sm font-medium transition-colors";
4390
+ var btnPrimary = cn(btnBase, "bg-primary text-primary-foreground hover:opacity-90");
4391
+ var btnGhost = cn(btnBase, "border border-border hover:bg-accent");
4392
+ var btnLink = "text-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline";
4393
+ function Toggle({
4394
+ checked,
4395
+ disabled,
4396
+ onChange,
4397
+ label
4398
+ }) {
4399
+ if (disabled) {
4400
+ return h3(
4401
+ "span",
4402
+ { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground" },
4403
+ "Always on"
4404
+ );
4405
+ }
4406
+ return h3(
4407
+ "button",
4408
+ {
4409
+ type: "button",
4410
+ role: "switch",
4411
+ "aria-checked": checked,
4412
+ "aria-label": label,
4413
+ onClick: () => onChange(!checked),
4414
+ className: cn(
4415
+ "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
4416
+ checked ? "bg-primary" : "bg-muted"
4417
+ )
4418
+ },
4419
+ h3("span", {
4420
+ className: cn(
4421
+ "inline-block h-4 w-4 transform rounded-full bg-background shadow transition-transform",
4422
+ checked ? "translate-x-[1.125rem]" : "translate-x-0.5"
4423
+ )
4424
+ })
4425
+ );
4426
+ }
4427
+ function CookieIcon() {
4428
+ return h3(
4429
+ "div",
4430
+ { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-xl", "aria-hidden": true },
4431
+ "\u{1F36A}"
4432
+ );
4433
+ }
4378
4434
  function CookieConsent({
4379
4435
  consent,
4380
4436
  position = "bottom",
4381
4437
  title = "We use cookies",
4382
- description = "We use cookies to run the site, remember your preferences, and measure traffic. Choose which categories to allow.",
4438
+ description = "We use cookies to run the site, remember your preferences, and measure traffic. Choose which to allow.",
4383
4439
  policyUrl,
4384
4440
  policyLabel = "Cookie policy",
4385
4441
  className
@@ -4387,64 +4443,80 @@ function CookieConsent({
4387
4443
  const { state, acceptAll, rejectAll, savePreferences, setPreference } = consent;
4388
4444
  const [settings, setSettings] = React11.useState(false);
4389
4445
  if (!state.open) return null;
4390
- const wrapper = cn(
4391
- "fixed inset-x-0 z-50 p-4",
4392
- position === "bottom" ? "bottom-0" : "top-0",
4393
- className
4394
- );
4395
- const panel = "mx-auto max-w-3xl rounded-xl border border-border bg-background p-4 shadow-lg";
4396
- const header = h3(
4446
+ const wrapper = cn("fixed inset-x-0 z-50 p-4", position === "bottom" ? "bottom-0" : "top-0", className);
4447
+ const panel = "mx-auto max-w-2xl overflow-hidden rounded-2xl border border-border bg-background shadow-lg";
4448
+ const policy = policyUrl ? h3("a", { href: policyUrl, target: "_blank", rel: "noreferrer", className: cn(btnLink, "whitespace-nowrap") }, policyLabel) : null;
4449
+ const promptView = h3(
4397
4450
  "div",
4398
- null,
4399
- h3("h2", { className: "text-base font-semibold" }, title),
4400
- h3("p", { className: "mt-1 text-sm text-muted-foreground" }, description),
4401
- policyUrl ? h3("a", { href: policyUrl, target: "_blank", rel: "noreferrer", className: cn(btnLink, "mt-1 inline-block") }, policyLabel) : null
4402
- );
4403
- const promptActions = h3(
4404
- "div",
4405
- { className: "mt-3 flex flex-wrap items-center gap-2" },
4406
- h3("button", { type: "button", className: btnPrimary, onClick: () => acceptAll() }, "Accept all"),
4407
- h3("button", { type: "button", className: btnGhost, onClick: () => rejectAll() }, "Reject all"),
4408
- h3("button", { type: "button", className: cn(btnGhost, "ml-auto"), onClick: () => setSettings(true) }, "Manage preferences")
4451
+ { className: "flex flex-col gap-4 p-5 sm:flex-row sm:items-center" },
4452
+ h3(CookieIcon),
4453
+ h3(
4454
+ "div",
4455
+ { className: "min-w-0 flex-1" },
4456
+ h3("p", { className: "text-sm font-semibold" }, title),
4457
+ h3(
4458
+ "p",
4459
+ { className: "mt-0.5 text-sm leading-relaxed text-muted-foreground" },
4460
+ description,
4461
+ policy ? h3(React11.Fragment, null, " ", policy) : null
4462
+ )
4463
+ ),
4464
+ h3(
4465
+ "div",
4466
+ { className: "flex flex-wrap items-center gap-2 sm:shrink-0" },
4467
+ h3("button", { type: "button", className: btnLink, onClick: () => setSettings(true) }, "Customize"),
4468
+ h3("button", { type: "button", className: btnGhost, onClick: () => rejectAll() }, "Reject all"),
4469
+ h3("button", { type: "button", className: btnPrimary, onClick: () => acceptAll() }, "Accept all")
4470
+ )
4409
4471
  );
4410
4472
  const settingsView = h3(
4411
4473
  "div",
4412
- { className: "mt-3 space-y-2" },
4413
- ...state.categories.map(
4414
- (cat) => h3(
4415
- "label",
4416
- {
4417
- key: cat.id,
4418
- className: "flex items-start justify-between gap-3 rounded-md border border-border p-3"
4419
- },
4420
- h3(
4421
- "span",
4422
- { className: "min-w-0" },
4423
- h3("span", { className: "block text-sm font-medium" }, cat.label, cat.required ? " (required)" : ""),
4424
- cat.description ? h3("span", { className: "block text-xs text-muted-foreground" }, cat.description) : null
4425
- ),
4426
- h3("input", {
4427
- type: "checkbox",
4428
- className: "mt-0.5 h-4 w-4 accent-[hsl(var(--primary))]",
4429
- checked: !!state.preferences[cat.id],
4430
- disabled: cat.required,
4431
- "aria-label": cat.label,
4432
- onChange: (e) => setPreference(cat.id, e.target.checked)
4433
- })
4474
+ { className: "p-5" },
4475
+ h3(
4476
+ "div",
4477
+ { className: "flex items-center gap-3" },
4478
+ h3(CookieIcon),
4479
+ h3(
4480
+ "div",
4481
+ null,
4482
+ h3("p", { className: "text-sm font-semibold" }, "Cookie preferences"),
4483
+ h3("p", { className: "text-xs text-muted-foreground" }, "Toggle the categories you want to allow.")
4484
+ )
4485
+ ),
4486
+ h3(
4487
+ "div",
4488
+ { className: "mt-4 space-y-2" },
4489
+ ...state.categories.map(
4490
+ (cat) => h3(
4491
+ "div",
4492
+ { key: cat.id, className: "flex items-center justify-between gap-4 rounded-xl border border-border p-3" },
4493
+ h3(
4494
+ "div",
4495
+ { className: "min-w-0" },
4496
+ h3("p", { className: "text-sm font-medium" }, cat.label),
4497
+ cat.description ? h3("p", { className: "mt-0.5 text-xs text-muted-foreground" }, cat.description) : null
4498
+ ),
4499
+ h3(Toggle, {
4500
+ checked: !!state.preferences[cat.id],
4501
+ disabled: cat.required,
4502
+ label: cat.label,
4503
+ onChange: (v) => setPreference(cat.id, v)
4504
+ })
4505
+ )
4434
4506
  )
4435
4507
  ),
4436
4508
  h3(
4437
4509
  "div",
4438
- { className: "flex flex-wrap items-center gap-2 pt-1" },
4439
- h3("button", { type: "button", className: btnPrimary, onClick: () => savePreferences(state.preferences) }, "Save preferences"),
4440
- h3("button", { type: "button", className: btnGhost, onClick: () => acceptAll() }, "Accept all"),
4441
- h3("button", { type: "button", className: cn(btnLink, "ml-auto"), onClick: () => setSettings(false) }, "Back")
4510
+ { className: "mt-4 flex flex-wrap items-center gap-2" },
4511
+ h3("button", { type: "button", className: btnLink, onClick: () => setSettings(false) }, "\u2190 Back"),
4512
+ h3("button", { type: "button", className: cn(btnGhost, "sm:ml-auto"), onClick: () => acceptAll() }, "Accept all"),
4513
+ h3("button", { type: "button", className: btnPrimary, onClick: () => savePreferences(state.preferences) }, "Save preferences")
4442
4514
  )
4443
4515
  );
4444
4516
  return h3(
4445
4517
  "div",
4446
4518
  { className: wrapper, role: "dialog", "aria-label": "Cookie consent", "aria-modal": false },
4447
- h3("div", { className: panel }, header, settings ? settingsView : promptActions)
4519
+ h3("div", { className: panel }, settings ? settingsView : promptView)
4448
4520
  );
4449
4521
  }
4450
4522