@lessly/ui 3.0.0 → 4.0.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,66 @@ var SheetDescription = React3.forwardRef(({ className, ...props }, ref) => /* @_
334
336
  ));
335
337
  SheetDescription.displayName = SheetPrimitive.Description.displayName;
336
338
 
339
+ // src/components/entity-row.tsx
340
+ import * as React4 from "react";
341
+ import { Slot as Slot2 } from "@radix-ui/react-slot";
342
+ import { ChevronDown, ChevronRight } from "lucide-react";
343
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
344
+ var rowClass = "flex w-full items-center gap-3 px-4 py-2.5 text-left";
345
+ 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";
346
+ var stackClass = "flex-col items-stretch gap-1";
347
+ var caretMotion = {
348
+ transitionDuration: "var(--motion-duration-normal)",
349
+ transitionTimingFunction: "var(--motion-easing-standard)"
350
+ };
351
+ var EntityRow = React4.forwardRef(function EntityRow2({ mark, title, badge, sub, meta, summary, opens = "page", expanded, asChild = false, className, children, ...props }, ref) {
352
+ if (process.env.NODE_ENV !== "production" && opens === "disclosure" && expanded === void 0) {
353
+ console.warn(
354
+ '[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"`.'
355
+ );
356
+ }
357
+ const line = /* @__PURE__ */ jsxs3(Fragment2, { children: [
358
+ mark,
359
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1", children: [
360
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-h-5 items-center gap-2", children: [
361
+ /* @__PURE__ */ jsx4("span", { className: cn("truncate text-sm text-text-primary", opens !== "none" && "font-medium"), children: title }),
362
+ badge != null && /* @__PURE__ */ jsx4("span", { className: "flex h-5 shrink-0 items-center", children: badge })
363
+ ] }),
364
+ sub != null && /* @__PURE__ */ jsx4("span", { className: "block truncate text-xs text-text-secondary", children: sub })
365
+ ] }),
366
+ meta != null && /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-sm text-text-secondary", children: meta }),
367
+ opens !== "none" && /* @__PURE__ */ jsxs3("span", { className: "flex size-4 shrink-0 items-center justify-center", children: [
368
+ opens === "page" && /* @__PURE__ */ jsx4(ChevronRight, { className: "size-4 text-text-tertiary", "aria-hidden": true }),
369
+ opens === "disclosure" && /* @__PURE__ */ jsx4(
370
+ ChevronDown,
371
+ {
372
+ className: cn(
373
+ "size-4 shrink-0 text-text-tertiary transition-transform",
374
+ expanded ? "rotate-180" : "rotate-0"
375
+ ),
376
+ style: caretMotion,
377
+ "aria-hidden": true
378
+ }
379
+ )
380
+ ] })
381
+ ] });
382
+ const content = summary == null ? line : /* @__PURE__ */ jsxs3(Fragment2, { children: [
383
+ /* @__PURE__ */ jsx4("span", { className: "flex w-full items-center gap-3", children: line }),
384
+ /* @__PURE__ */ jsx4("span", { className: "block w-full text-xs text-text-secondary", children: summary })
385
+ ] });
386
+ const classes = cn(rowClass, opens !== "none" && ["cursor-pointer", plateClass], summary != null && stackClass, className);
387
+ const stateProps = opens === "disclosure" ? { "aria-expanded": expanded } : void 0;
388
+ if (opens === "none") {
389
+ return /* @__PURE__ */ jsx4("div", { ...props, ref, className: classes, children: content });
390
+ }
391
+ if (asChild) {
392
+ const child = React4.Children.only(children);
393
+ return /* @__PURE__ */ jsx4(Slot2, { ...props, ref, className: classes, ...stateProps, children: React4.cloneElement(child, stateProps, content) });
394
+ }
395
+ return /* @__PURE__ */ jsx4("button", { ...props, ref, type: "button", className: classes, ...stateProps, children: content });
396
+ });
397
+ EntityRow.displayName = "EntityRow";
398
+
337
399
  // src/hooks/use-mobile.ts
338
400
  import { useEffect, useState as useState2 } from "react";
339
401
  var MD_BREAKPOINT = 768;
@@ -376,10 +438,11 @@ function useSidebar(options = {}) {
376
438
  }
377
439
 
378
440
  // 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";
441
+ import * as React5 from "react";
442
+ import { Slot as Slot3 } from "@radix-ui/react-slot";
443
+ import { ChevronDown as ChevronDown2, ChevronRight as ChevronRight2 } from "lucide-react";
444
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
445
+ var base = "flex w-full cursor-pointer items-center text-left text-sm transition-colors duration-fast focus-visible:outline-none";
383
446
  var ring = "focus-visible:ring-2 focus-visible:ring-border-focus";
384
447
  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
448
  var shape = {
@@ -390,23 +453,43 @@ var shape = {
390
453
  var state = {
391
454
  rail: {
392
455
  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"
456
+ active: "bg-[var(--current-bg)] font-semibold text-text-primary hover:bg-[var(--hov-current)]"
394
457
  },
395
458
  menu: {
396
459
  idle: "font-medium text-text-secondary hover:bg-[var(--menu-hov)]",
397
- active: "bg-bg-sunken font-semibold text-text-primary"
460
+ active: "ring-1 ring-inset ring-border-strong font-semibold text-text-primary hover:bg-[var(--menu-hov)]"
398
461
  },
399
462
  // A settings line reads as a label, not as navigation, so it idles at the body weight the rest of
400
463
  // the card is set in — and never marks itself current, because it isn't.
401
464
  card: {
402
- idle: "text-text-primary hover:after:bg-bg-elevated",
403
- active: "text-text-primary hover:after:bg-bg-elevated"
465
+ idle: "text-text-primary hover:after:bg-[var(--hov-bg)]",
466
+ active: "text-text-primary hover:after:bg-[var(--hov-bg)]"
404
467
  }
405
468
  };
406
- var NavRow = React4.forwardRef(function NavRow2({ icon, label, sub, active = false, variant, trailing, mutedTrailing = false, hideLabel = false, asChild = false, className, children, ...props }, ref) {
469
+ function Mark({ mark, open }) {
470
+ return /* @__PURE__ */ jsx5(
471
+ "span",
472
+ {
473
+ className: cn(
474
+ "flex size-5 items-center justify-center",
475
+ 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"
476
+ ),
477
+ children: mark === "drill" ? /* @__PURE__ */ jsx5(ChevronRight2, { className: "size-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx5(
478
+ ChevronDown2,
479
+ {
480
+ className: "size-4",
481
+ style: { ...caretMotion, transitionProperty: "transform", transform: open ? "rotate(180deg)" : "rotate(0deg)" },
482
+ "aria-hidden": "true"
483
+ }
484
+ )
485
+ }
486
+ );
487
+ }
488
+ var NavRow = React5.forwardRef(function NavRow2({ icon, label, sub, active = false, variant, trailing, mark, markOpen, mutedTrailing = false, hideLabel = false, asChild = false, className, children, ...props }, ref) {
407
489
  const card = variant === "card";
408
- const content = /* @__PURE__ */ jsxs3(Fragment2, { children: [
409
- Boolean(icon) && /* @__PURE__ */ jsx4(
490
+ const adornment = mark ? !hideLabel && /* @__PURE__ */ jsx5(Mark, { mark, open: markOpen }) : trailing;
491
+ const content = /* @__PURE__ */ jsxs4(Fragment3, { children: [
492
+ Boolean(icon) && /* @__PURE__ */ jsx5(
410
493
  "span",
411
494
  {
412
495
  className: cn(
@@ -417,31 +500,92 @@ var NavRow = React4.forwardRef(function NavRow2({ icon, label, sub, active = fal
417
500
  children: icon
418
501
  }
419
502
  ),
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(
503
+ !hideLabel && (card ? /* @__PURE__ */ jsxs4("span", { "data-slot": "nav-row-label", className: "min-w-0 flex-1", children: [
504
+ /* @__PURE__ */ jsx5("span", { className: "block truncate", children: label }),
505
+ Boolean(sub) && /* @__PURE__ */ jsx5("span", { className: "block truncate text-xs text-text-secondary", children: sub })
506
+ ] }) : /* @__PURE__ */ jsx5("span", { "data-slot": "nav-row-label", className: "min-w-0 flex-1 truncate", children: label })),
507
+ Boolean(adornment) && /* @__PURE__ */ jsx5(
425
508
  "span",
426
509
  {
510
+ "data-slot": "nav-row-trailing",
427
511
  className: cn(
428
512
  "flex shrink-0 items-center",
429
513
  mutedTrailing ? "text-text-tertiary" : card && "text-text-secondary"
430
514
  ),
431
- children: trailing
515
+ children: adornment
432
516
  }
433
517
  ),
434
- card && /* @__PURE__ */ jsx4("span", { className: "size-4 shrink-0", "aria-hidden": "true" })
518
+ card && /* @__PURE__ */ jsx5("span", { className: "size-4 shrink-0", "aria-hidden": "true" })
435
519
  ] });
436
- const classes = cn(base, shape[variant], state[variant][active ? "active" : "idle"], className);
520
+ const classes = cn(
521
+ base,
522
+ shape[variant],
523
+ state[variant][active ? "active" : "idle"],
524
+ // The drill mark's own plate lights on the row, so the row is what the plate hangs off.
525
+ mark === "drill" && "group/nav-row",
526
+ className
527
+ );
528
+ const expanded = mark === "disclosure" ? markOpen : void 0;
437
529
  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) });
530
+ const child = React5.Children.only(children);
531
+ return /* @__PURE__ */ jsx5(Slot3, { ref, className: classes, "aria-label": hideLabel ? label : void 0, "aria-expanded": expanded, ...props, children: React5.cloneElement(child, void 0, content) });
440
532
  }
441
- return /* @__PURE__ */ jsx4("button", { ref, type: "button", className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: content });
533
+ return /* @__PURE__ */ jsx5("button", { ref, type: "button", className: classes, "aria-label": hideLabel ? label : void 0, "aria-expanded": expanded, ...props, children: content });
442
534
  });
443
535
  NavRow.displayName = "NavRow";
444
536
 
537
+ // src/components/rail-toggle.tsx
538
+ import * as React6 from "react";
539
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
540
+ var RAIL_COLUMN_BOX = "flex size-10 shrink-0 items-center justify-center [&>*]:shrink-0";
541
+ var BAR_TRAVEL = 6;
542
+ function RailMark({ collapsed, className }) {
543
+ return /* @__PURE__ */ jsxs5(
544
+ "svg",
545
+ {
546
+ xmlns: "http://www.w3.org/2000/svg",
547
+ viewBox: "0 0 24 24",
548
+ fill: "none",
549
+ stroke: "currentColor",
550
+ strokeWidth: 2,
551
+ strokeLinecap: "round",
552
+ strokeLinejoin: "round",
553
+ className: cn("size-4", className),
554
+ "aria-hidden": "true",
555
+ children: [
556
+ /* @__PURE__ */ jsx6("rect", { "data-slot": "rail-mark-frame", x: "3", y: "3", width: "18", height: "18", rx: "2" }),
557
+ /* @__PURE__ */ jsx6(
558
+ "path",
559
+ {
560
+ "data-slot": "rail-mark-bar",
561
+ className: "motion-rail-collapse",
562
+ d: "M9 3v18",
563
+ style: { transform: `translateX(${collapsed ? BAR_TRAVEL : 0}px)` }
564
+ }
565
+ )
566
+ ]
567
+ }
568
+ );
569
+ }
570
+ var RailToggle = React6.forwardRef(function RailToggle2({ collapsed, className, "aria-label": ariaLabel, ...rest }, ref) {
571
+ return /* @__PURE__ */ jsx6(
572
+ "button",
573
+ {
574
+ ref,
575
+ type: "button",
576
+ "aria-label": ariaLabel ?? (collapsed ? "Expand sidebar" : "Collapse sidebar"),
577
+ "aria-expanded": !collapsed,
578
+ className: cn(
579
+ RAIL_COLUMN_BOX,
580
+ "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",
581
+ className
582
+ ),
583
+ ...rest,
584
+ children: /* @__PURE__ */ jsx6(RailMark, { collapsed })
585
+ }
586
+ );
587
+ });
588
+
445
589
  export {
446
590
  cn,
447
591
  buttonVariants,
@@ -460,7 +604,10 @@ export {
460
604
  SheetFooter,
461
605
  SheetTitle,
462
606
  SheetDescription,
607
+ EntityRow,
463
608
  useIsMobile,
464
609
  useSidebar,
465
- NavRow
610
+ NavRow,
611
+ RAIL_COLUMN_BOX,
612
+ RailToggle
466
613
  };