@lessly/ui 3.0.0 → 4.1.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.
@@ -71,10 +71,12 @@ var buttonVariants = cva(
71
71
  // colour would reach the element beside the variant's and stylesheet order would pick.
72
72
  variant: {
73
73
  default: "bg-blue-400 text-text-on-brand focus-visible:ring-border-focus hover:bg-blue-350",
74
- /** The frame carries the danger at rest and the text stays neutral, because a hover-only
75
- * signal is no signal on a touch device. Hover adds the wash and the red label; keyboard
74
+ /** The wash carries the danger at rest, on the neutral frame every object button wears — so
75
+ * a pair still ranks by weight rather than by colour, and the signal survives a touch
76
+ * device that never hovers. The label rests neutral because a variant differs by frame and
77
+ * fill and never by text colour (#449). Hover reddens the frame and the label; keyboard
76
78
  * focus reddens the ring alone. */
77
- destructive: "border border-border-danger bg-transparent text-text-primary focus-visible:ring-border-danger hover:bg-bg-danger-subtle hover:text-text-danger",
79
+ destructive: "border border-border-default bg-bg-danger-subtle text-text-primary focus-visible:ring-border-danger hover:border-border-danger hover:text-text-danger",
78
80
  /** The borderless form of `destructive`, for an icon-only revoke repeated down a list — a row
79
81
  * of framed ✕ buttons would shout. */
80
82
  "destructive-ghost": "text-text-tertiary focus-visible:ring-border-focus hover:bg-bg-danger-subtle hover:text-text-danger",
@@ -83,7 +85,7 @@ var buttonVariants = cva(
83
85
  * `StepUpChallenge destructive` one. Use `destructive` for any trigger that sits on a page. */
84
86
  "destructive-solid": "bg-bg-danger text-text-on-danger focus-visible:ring-border-focus hover:bg-bg-danger-a90",
85
87
  outline: "border border-border-default bg-transparent text-text-primary focus-visible:ring-border-focus hover:bg-bg-secondary",
86
- secondary: "bg-bg-secondary text-text-primary focus-visible:ring-border-focus hover:bg-bg-secondary-a80",
88
+ secondary: "border border-border-default bg-bg-secondary text-text-primary focus-visible:ring-border-focus hover:bg-bg-secondary-a80",
87
89
  /** The frameless console tone, and the add affordance — `variant="ghost" size="xs"` with a
88
90
  * leading `+`. It rests at `text-text-primary` because a button's variant differs by frame
89
91
  * and fill, never by text colour; `destructive-ghost` keeps its quieter rest on purpose. */
@@ -290,7 +292,7 @@ var SheetContent = React3.forwardRef(({ side = "right", className, children, ...
290
292
  ...props,
291
293
  children: [
292
294
  children,
293
- /* @__PURE__ */ jsxs2(SheetPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm text-text-tertiary opacity-70 transition-opacity duration-fast hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-border-focus disabled:pointer-events-none", children: [
295
+ /* @__PURE__ */ jsxs2(SheetPrimitive.Close, { className: "absolute right-4 top-4 cursor-pointer rounded-sm text-text-tertiary opacity-70 transition-opacity duration-fast hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-border-focus disabled:pointer-events-none", children: [
294
296
  /* @__PURE__ */ jsx3(X, { className: "h-4 w-4" }),
295
297
  /* @__PURE__ */ jsx3("span", { className: "sr-only", children: "Close" })
296
298
  ] })
@@ -334,6 +336,112 @@ var SheetDescription = React3.forwardRef(({ className, ...props }, ref) => /* @_
334
336
  ));
335
337
  SheetDescription.displayName = SheetPrimitive.Description.displayName;
336
338
 
339
+ // src/components/scroll-area.tsx
340
+ import * as React4 from "react";
341
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
342
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
343
+ var ScrollArea = React4.forwardRef(({ className, children, viewportClassName, viewportProps, type = "scroll", scrollHideDelay = 600, ...props }, ref) => /* @__PURE__ */ jsxs3(
344
+ ScrollAreaPrimitive.Root,
345
+ {
346
+ ref,
347
+ type,
348
+ scrollHideDelay,
349
+ className: cn("group/scroll relative overflow-hidden", className),
350
+ ...props,
351
+ children: [
352
+ /* @__PURE__ */ jsx4(
353
+ ScrollAreaPrimitive.Viewport,
354
+ {
355
+ ...viewportProps,
356
+ className: cn("scroll-overlay-viewport h-full w-full rounded-[inherit]", viewportClassName),
357
+ children
358
+ }
359
+ ),
360
+ /* @__PURE__ */ jsx4(ScrollBar, {}),
361
+ /* @__PURE__ */ jsx4(ScrollAreaPrimitive.Corner, {})
362
+ ]
363
+ }
364
+ ));
365
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
366
+ var ScrollBar = React4.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ jsx4(
367
+ ScrollAreaPrimitive.ScrollAreaScrollbar,
368
+ {
369
+ ref,
370
+ orientation,
371
+ forceMount: true,
372
+ className: cn(
373
+ "motion-scroll-overlay flex touch-none select-none p-[2px] opacity-0",
374
+ "data-[state=visible]:opacity-100 group-hover/scroll:opacity-100 hover:opacity-100",
375
+ orientation === "vertical" && "h-full w-2",
376
+ orientation === "horizontal" && "h-2 flex-col",
377
+ className
378
+ ),
379
+ ...props,
380
+ children: /* @__PURE__ */ jsx4(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border-strong" })
381
+ }
382
+ ));
383
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
384
+
385
+ // src/components/entity-row.tsx
386
+ import * as React5 from "react";
387
+ import { Slot as Slot2 } from "@radix-ui/react-slot";
388
+ import { ChevronDown, ChevronRight } from "lucide-react";
389
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
390
+ var rowClass = "flex w-full items-center gap-3 px-4 py-2.5 text-left";
391
+ var plateClass = "relative isolate after:pointer-events-none after:absolute after:inset-x-1.5 after:inset-y-1 after:-z-10 after:rounded-lg hover:after:bg-[var(--hov-bg)] disabled:hover:after:bg-transparent focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-border-focus";
392
+ var stackClass = "flex-col items-stretch gap-1";
393
+ var caretMotion = {
394
+ transitionDuration: "var(--motion-duration-normal)",
395
+ transitionTimingFunction: "var(--motion-easing-standard)"
396
+ };
397
+ var EntityRow = React5.forwardRef(function EntityRow2({ mark, title, badge, sub, meta, summary, opens = "page", expanded, asChild = false, className, children, ...props }, ref) {
398
+ if (process.env.NODE_ENV !== "production" && opens === "disclosure" && expanded === void 0) {
399
+ console.warn(
400
+ '[EntityRow] `opens="disclosure"` needs `expanded`: it draws the caret AND sets `aria-expanded`, and without it the row renders a caret that never turns and states no state at all. Pass `expanded={open}`, or use `opens="page"` / `opens="modal"`.'
401
+ );
402
+ }
403
+ const line = /* @__PURE__ */ jsxs4(Fragment2, { children: [
404
+ mark,
405
+ /* @__PURE__ */ jsxs4("span", { className: "min-w-0 flex-1", children: [
406
+ /* @__PURE__ */ jsxs4("span", { className: "flex min-h-5 items-center gap-2", children: [
407
+ /* @__PURE__ */ jsx5("span", { className: cn("truncate text-sm text-text-primary", opens !== "none" && "font-medium"), children: title }),
408
+ badge != null && /* @__PURE__ */ jsx5("span", { className: "flex h-5 shrink-0 items-center", children: badge })
409
+ ] }),
410
+ sub != null && /* @__PURE__ */ jsx5("span", { className: "block truncate text-xs text-text-secondary", children: sub })
411
+ ] }),
412
+ meta != null && /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-sm text-text-secondary", children: meta }),
413
+ opens !== "none" && /* @__PURE__ */ jsxs4("span", { className: "flex size-4 shrink-0 items-center justify-center", children: [
414
+ opens === "page" && /* @__PURE__ */ jsx5(ChevronRight, { className: "size-4 text-text-tertiary", "aria-hidden": true }),
415
+ opens === "disclosure" && /* @__PURE__ */ jsx5(
416
+ ChevronDown,
417
+ {
418
+ className: cn(
419
+ "size-4 shrink-0 text-text-tertiary transition-transform",
420
+ expanded ? "rotate-180" : "rotate-0"
421
+ ),
422
+ style: caretMotion,
423
+ "aria-hidden": true
424
+ }
425
+ )
426
+ ] })
427
+ ] });
428
+ const content = summary == null ? line : /* @__PURE__ */ jsxs4(Fragment2, { children: [
429
+ /* @__PURE__ */ jsx5("span", { className: "flex w-full items-center gap-3", children: line }),
430
+ /* @__PURE__ */ jsx5("span", { className: "block w-full text-xs text-text-secondary", children: summary })
431
+ ] });
432
+ const classes = cn(rowClass, opens !== "none" && ["cursor-pointer", plateClass], summary != null && stackClass, className);
433
+ const stateProps = opens === "disclosure" ? { "aria-expanded": expanded } : void 0;
434
+ if (opens === "none") {
435
+ return /* @__PURE__ */ jsx5("div", { ...props, ref, className: classes, children: content });
436
+ }
437
+ if (asChild) {
438
+ const child = React5.Children.only(children);
439
+ return /* @__PURE__ */ jsx5(Slot2, { ...props, ref, className: classes, ...stateProps, children: React5.cloneElement(child, stateProps, content) });
440
+ }
441
+ return /* @__PURE__ */ jsx5("button", { ...props, ref, type: "button", className: classes, ...stateProps, children: content });
442
+ });
443
+ EntityRow.displayName = "EntityRow";
444
+
337
445
  // src/hooks/use-mobile.ts
338
446
  import { useEffect, useState as useState2 } from "react";
339
447
  var MD_BREAKPOINT = 768;
@@ -376,10 +484,11 @@ function useSidebar(options = {}) {
376
484
  }
377
485
 
378
486
  // src/components/nav-row.tsx
379
- import * as React4 from "react";
380
- import { Slot as Slot2 } from "@radix-ui/react-slot";
381
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
382
- var base = "flex w-full items-center text-left text-sm transition-colors duration-fast focus-visible:outline-none";
487
+ import * as React6 from "react";
488
+ import { Slot as Slot3 } from "@radix-ui/react-slot";
489
+ import { ChevronDown as ChevronDown2, ChevronRight as ChevronRight2 } from "lucide-react";
490
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
491
+ var base = "flex w-full cursor-pointer items-center text-left text-sm transition-colors duration-fast focus-visible:outline-none";
383
492
  var ring = "focus-visible:ring-2 focus-visible:ring-border-focus";
384
493
  var plate = "relative isolate after:pointer-events-none after:absolute after:inset-x-1.5 after:inset-y-1 after:-z-10 after:rounded-lg";
385
494
  var shape = {
@@ -390,23 +499,43 @@ var shape = {
390
499
  var state = {
391
500
  rail: {
392
501
  idle: "font-medium text-text-secondary hover:bg-[var(--hov-bg)] hover:text-text-primary",
393
- active: "bg-bg-sunken font-semibold text-text-primary"
502
+ active: "bg-[var(--current-bg)] font-semibold text-text-primary hover:bg-[var(--hov-current)]"
394
503
  },
395
504
  menu: {
396
505
  idle: "font-medium text-text-secondary hover:bg-[var(--menu-hov)]",
397
- active: "bg-bg-sunken font-semibold text-text-primary"
506
+ active: "ring-1 ring-inset ring-border-strong font-semibold text-text-primary hover:bg-[var(--menu-hov)]"
398
507
  },
399
508
  // A settings line reads as a label, not as navigation, so it idles at the body weight the rest of
400
509
  // the card is set in — and never marks itself current, because it isn't.
401
510
  card: {
402
- idle: "text-text-primary hover:after:bg-bg-elevated",
403
- active: "text-text-primary hover:after:bg-bg-elevated"
511
+ idle: "text-text-primary hover:after:bg-[var(--hov-bg)]",
512
+ active: "text-text-primary hover:after:bg-[var(--hov-bg)]"
404
513
  }
405
514
  };
406
- var NavRow = React4.forwardRef(function NavRow2({ icon, label, sub, active = false, variant, trailing, mutedTrailing = false, hideLabel = false, asChild = false, className, children, ...props }, ref) {
515
+ function Mark({ mark, open }) {
516
+ return /* @__PURE__ */ jsx6(
517
+ "span",
518
+ {
519
+ className: cn(
520
+ "flex size-5 items-center justify-center",
521
+ mark === "drill" ? "rounded-md transition-colors duration-fast group-hover/nav-row:bg-[color-mix(in_srgb,var(--text-primary)_10%,transparent)]" : "text-text-tertiary"
522
+ ),
523
+ children: mark === "drill" ? /* @__PURE__ */ jsx6(ChevronRight2, { className: "size-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx6(
524
+ ChevronDown2,
525
+ {
526
+ className: "size-4",
527
+ style: { ...caretMotion, transitionProperty: "transform", transform: open ? "rotate(180deg)" : "rotate(0deg)" },
528
+ "aria-hidden": "true"
529
+ }
530
+ )
531
+ }
532
+ );
533
+ }
534
+ var NavRow = React6.forwardRef(function NavRow2({ icon, label, sub, active = false, variant, trailing, mark, markOpen, mutedTrailing = false, hideLabel = false, asChild = false, className, children, ...props }, ref) {
407
535
  const card = variant === "card";
408
- const content = /* @__PURE__ */ jsxs3(Fragment2, { children: [
409
- Boolean(icon) && /* @__PURE__ */ jsx4(
536
+ const adornment = mark ? !hideLabel && /* @__PURE__ */ jsx6(Mark, { mark, open: markOpen }) : trailing;
537
+ const content = /* @__PURE__ */ jsxs5(Fragment3, { children: [
538
+ Boolean(icon) && /* @__PURE__ */ jsx6(
410
539
  "span",
411
540
  {
412
541
  className: cn(
@@ -417,31 +546,92 @@ var NavRow = React4.forwardRef(function NavRow2({ icon, label, sub, active = fal
417
546
  children: icon
418
547
  }
419
548
  ),
420
- !hideLabel && (card ? /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1", children: [
421
- /* @__PURE__ */ jsx4("span", { className: "block truncate", children: label }),
422
- Boolean(sub) && /* @__PURE__ */ jsx4("span", { className: "block truncate text-xs text-text-secondary", children: sub })
423
- ] }) : /* @__PURE__ */ jsx4("span", { className: "min-w-0 flex-1 truncate", children: label })),
424
- Boolean(trailing) && /* @__PURE__ */ jsx4(
549
+ !hideLabel && (card ? /* @__PURE__ */ jsxs5("span", { "data-slot": "nav-row-label", className: "min-w-0 flex-1", children: [
550
+ /* @__PURE__ */ jsx6("span", { className: "block truncate", children: label }),
551
+ Boolean(sub) && /* @__PURE__ */ jsx6("span", { className: "block truncate text-xs text-text-secondary", children: sub })
552
+ ] }) : /* @__PURE__ */ jsx6("span", { "data-slot": "nav-row-label", className: "min-w-0 flex-1 truncate", children: label })),
553
+ Boolean(adornment) && /* @__PURE__ */ jsx6(
425
554
  "span",
426
555
  {
556
+ "data-slot": "nav-row-trailing",
427
557
  className: cn(
428
558
  "flex shrink-0 items-center",
429
559
  mutedTrailing ? "text-text-tertiary" : card && "text-text-secondary"
430
560
  ),
431
- children: trailing
561
+ children: adornment
432
562
  }
433
563
  ),
434
- card && /* @__PURE__ */ jsx4("span", { className: "size-4 shrink-0", "aria-hidden": "true" })
564
+ card && /* @__PURE__ */ jsx6("span", { className: "size-4 shrink-0", "aria-hidden": "true" })
435
565
  ] });
436
- const classes = cn(base, shape[variant], state[variant][active ? "active" : "idle"], className);
566
+ const classes = cn(
567
+ base,
568
+ shape[variant],
569
+ state[variant][active ? "active" : "idle"],
570
+ // The drill mark's own plate lights on the row, so the row is what the plate hangs off.
571
+ mark === "drill" && "group/nav-row",
572
+ className
573
+ );
574
+ const expanded = mark === "disclosure" ? markOpen : void 0;
437
575
  if (asChild) {
438
- const child = React4.Children.only(children);
439
- return /* @__PURE__ */ jsx4(Slot2, { ref, className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: React4.cloneElement(child, void 0, content) });
576
+ const child = React6.Children.only(children);
577
+ return /* @__PURE__ */ jsx6(Slot3, { ref, className: classes, "aria-label": hideLabel ? label : void 0, "aria-expanded": expanded, ...props, children: React6.cloneElement(child, void 0, content) });
440
578
  }
441
- return /* @__PURE__ */ jsx4("button", { ref, type: "button", className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: content });
579
+ return /* @__PURE__ */ jsx6("button", { ref, type: "button", className: classes, "aria-label": hideLabel ? label : void 0, "aria-expanded": expanded, ...props, children: content });
442
580
  });
443
581
  NavRow.displayName = "NavRow";
444
582
 
583
+ // src/components/rail-toggle.tsx
584
+ import * as React7 from "react";
585
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
586
+ var RAIL_COLUMN_BOX = "flex size-10 shrink-0 items-center justify-center [&>*]:shrink-0";
587
+ var BAR_TRAVEL = 6;
588
+ function RailMark({ collapsed, className }) {
589
+ return /* @__PURE__ */ jsxs6(
590
+ "svg",
591
+ {
592
+ xmlns: "http://www.w3.org/2000/svg",
593
+ viewBox: "0 0 24 24",
594
+ fill: "none",
595
+ stroke: "currentColor",
596
+ strokeWidth: 2,
597
+ strokeLinecap: "round",
598
+ strokeLinejoin: "round",
599
+ className: cn("size-4", className),
600
+ "aria-hidden": "true",
601
+ children: [
602
+ /* @__PURE__ */ jsx7("rect", { "data-slot": "rail-mark-frame", x: "3", y: "3", width: "18", height: "18", rx: "2" }),
603
+ /* @__PURE__ */ jsx7(
604
+ "path",
605
+ {
606
+ "data-slot": "rail-mark-bar",
607
+ className: "motion-rail-collapse",
608
+ d: "M9 3v18",
609
+ style: { transform: `translateX(${collapsed ? BAR_TRAVEL : 0}px)` }
610
+ }
611
+ )
612
+ ]
613
+ }
614
+ );
615
+ }
616
+ var RailToggle = React7.forwardRef(function RailToggle2({ collapsed, className, "aria-label": ariaLabel, ...rest }, ref) {
617
+ return /* @__PURE__ */ jsx7(
618
+ "button",
619
+ {
620
+ ref,
621
+ type: "button",
622
+ "aria-label": ariaLabel ?? (collapsed ? "Expand sidebar" : "Collapse sidebar"),
623
+ "aria-expanded": !collapsed,
624
+ className: cn(
625
+ RAIL_COLUMN_BOX,
626
+ "cursor-pointer rounded-md text-text-secondary transition-colors duration-fast hover:bg-[var(--hov-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
627
+ className
628
+ ),
629
+ ...rest,
630
+ children: /* @__PURE__ */ jsx7(RailMark, { collapsed })
631
+ }
632
+ );
633
+ });
634
+
445
635
  export {
446
636
  cn,
447
637
  buttonVariants,
@@ -460,7 +650,12 @@ export {
460
650
  SheetFooter,
461
651
  SheetTitle,
462
652
  SheetDescription,
653
+ ScrollArea,
654
+ ScrollBar,
655
+ EntityRow,
463
656
  useIsMobile,
464
657
  useSidebar,
465
- NavRow
658
+ NavRow,
659
+ RAIL_COLUMN_BOX,
660
+ RailToggle
466
661
  };