@alexandregme/zljs 1.18.1 → 1.19.1

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/CHANGELOG.md CHANGED
@@ -8,6 +8,45 @@ A change that needs action in an app has a **Migration** note. When updating, re
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.19.1] - 2026-11-03
12
+
13
+ ### Changed
14
+
15
+ - Collapsed `Sidebar` shows only the logout button in its footer. The avatar, name and email come back when it expands, and the footer and the collapse button keep their height in both states, at 64px and 56px
16
+ - The collapse button of `Sidebar` is named by `collapseLabel` while expanded, no longer `Collapse sidebar`, and shows only its icon when collapsed
17
+ - `Sidebar` links have fewer elements and look the same: the `Icon` is a direct child of the link, and the home link has no empty `span` when there is no `homeLabel`
18
+
19
+ #### Migration
20
+
21
+ Search the app's tests for `Collapse sidebar` and replace it with the value the app passes in `collapseLabel`. `Expand sidebar` stays.
22
+
23
+ ### Fixed
24
+
25
+ - Collapsed `Sidebar` can log out with a mouse. The button sat outside the visible area, and focusing it with the keyboard scrolled the sidebar sideways, out of view
26
+ - `Sidebar` links draw their own focus outline in the text color. They relied on the browser's default, which was 2.53:1 in Chromium
27
+ - The active home link of `Sidebar` has `aria-current="page"`, its link is styled like the others, and the avatar initials are hidden from screen readers
28
+ - A long email in `Sidebar` is cut with an ellipsis. It ran under the logout button
29
+ - The icon of the `Sidebar` collapse button lines up with the link icons, so it is centered when collapsed and doesn't move on toggle. It sat 4px to the right
30
+
31
+ ## [1.19.0] - 2026-11-02
32
+
33
+ ### Added
34
+
35
+ - `label` on `Spinner`: the name screen readers announce, `Loading` by default
36
+
37
+ ### Changed
38
+
39
+ - `Spinner` is a status with a name (`role="status"`) and has no color of its own. It was always white, and now inherits the color from around it
40
+ - `Loader` draws its spinner in the `primary` color, which holds 3:1 over the overlay on a white page. It was white, at 1.6:1. The `status` role moved from the overlay to the spinner, which takes the `label`
41
+
42
+ #### Migration
43
+
44
+ Search the app for `<Spinner`. On a dark surface, set a light text color around it, such as `text-white`. Remove a `role="status"` the app put around a `Spinner`, so there aren't two. `Loader` needs no change.
45
+
46
+ ### Fixed
47
+
48
+ - `Loader` covers the whole page. It had no position of its own, so it started where it was rendered, below anything before it
49
+
11
50
  ## [1.18.1] - 2026-11-02
12
51
 
13
52
  ### Removed
@@ -263,7 +302,9 @@ Replace the shade with the class of the `color` being tested, for example `/(^|\
263
302
 
264
303
  The message of `Error` is `text-zl-danger`. `none` and `ghost` didn't change.
265
304
 
266
- [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.18.1...HEAD
305
+ [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.19.1...HEAD
306
+ [1.19.1]: https://github.com/alexandregme/zljs/compare/v1.19.0...v1.19.1
307
+ [1.19.0]: https://github.com/alexandregme/zljs/compare/v1.18.1...v1.19.0
267
308
  [1.18.1]: https://github.com/alexandregme/zljs/compare/v1.18.0...v1.18.1
268
309
  [1.18.0]: https://github.com/alexandregme/zljs/compare/v1.17.0...v1.18.0
269
310
  [1.17.0]: https://github.com/alexandregme/zljs/compare/v1.16.0...v1.17.0
@@ -352,17 +352,30 @@ var spinnerIcons = {
352
352
  // core/spinner/spinner.styles.ts
353
353
  import { tv as tv4 } from "tailwind-variants";
354
354
  var spinner = tv4({
355
- base: "inline-flex animate-spin text-white"
355
+ base: "inline-flex animate-spin"
356
356
  });
357
357
 
358
358
  // core/spinner/spinner.tsx
359
359
  import { jsx as jsx6 } from "react/jsx-runtime";
360
- var Spinner = ({ variant = "circle", size = "md" }) => /* @__PURE__ */ jsx6("span", { "data-testid": "spinner", className: spinner(), children: /* @__PURE__ */ jsx6(Icon, { name: spinnerIcons[variant], size }) });
360
+ var Spinner = ({
361
+ variant = "circle",
362
+ size = "md",
363
+ label = "Loading"
364
+ }) => /* @__PURE__ */ jsx6(
365
+ "span",
366
+ {
367
+ "data-testid": "spinner",
368
+ role: "status",
369
+ "aria-label": label,
370
+ className: spinner(),
371
+ children: /* @__PURE__ */ jsx6(Icon, { name: spinnerIcons[variant], size })
372
+ }
373
+ );
361
374
 
362
375
  // core/loader/loader.styles.ts
363
376
  import { tv as tv5 } from "tailwind-variants";
364
- var overlay = tv5({
365
- base: "flex fixed justify-center items-center w-full h-full bg-black/20 z-50"
377
+ var loader = tv5({
378
+ base: "flex fixed inset-0 justify-center items-center bg-black/20 text-zl-primary z-50"
366
379
  });
367
380
 
368
381
  // core/loader/loader.tsx
@@ -370,7 +383,7 @@ import { jsx as jsx7 } from "react/jsx-runtime";
370
383
  var Loader = ({ label = "Loading" }) => {
371
384
  const fetching = useAppStore((s) => s.fetching);
372
385
  if (!fetching) return null;
373
- return /* @__PURE__ */ jsx7("div", { role: "status", "aria-label": label, className: overlay(), children: /* @__PURE__ */ jsx7(Spinner, { size: "lg" }) });
386
+ return /* @__PURE__ */ jsx7("div", { className: loader(), children: /* @__PURE__ */ jsx7(Spinner, { size: "lg", label }) });
374
387
  };
375
388
 
376
389
  export {
package/dist/index.d.ts CHANGED
@@ -431,9 +431,10 @@ type SpinnerVariant = "circle" | "dots" | "refresh";
431
431
  interface SpinnerProps {
432
432
  variant?: SpinnerVariant;
433
433
  size?: Size;
434
+ label?: string;
434
435
  }
435
436
 
436
- declare const Spinner: ({ variant, size }: SpinnerProps) => react.JSX.Element;
437
+ declare const Spinner: ({ variant, size, label, }: SpinnerProps) => react.JSX.Element;
437
438
 
438
439
  interface LoaderProps {
439
440
  label?: string;
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  usePageStore,
13
13
  useRouterStore,
14
14
  useToastStore
15
- } from "./chunk-KPSFYQYV.js";
15
+ } from "./chunk-O6UAZGBC.js";
16
16
  import {
17
17
  Error,
18
18
  FormInput,
@@ -829,7 +829,7 @@ import Link from "next/link.js";
829
829
  // core/sidebar/sidebar.styles.ts
830
830
  import { tv as tv10 } from "tailwind-variants";
831
831
  var sidebar = tv10({
832
- base: "h-screen bg-neutral-800 text-neutral-300 flex flex-col overflow-hidden transition-all duration-200",
832
+ base: "h-screen bg-neutral-800 text-neutral-300 flex flex-col overflow-hidden transition-all duration-200 divide-y divide-neutral-600/50",
833
833
  variants: {
834
834
  collapsed: {
835
835
  true: "w-14",
@@ -841,62 +841,52 @@ var sidebar = tv10({
841
841
  }
842
842
  });
843
843
  var nav = tv10({
844
- base: "flex-1 min-h-0 overflow-y-auto overflow-x-hidden"
844
+ base: "flex-1 min-h-0 overflow-y-auto overflow-x-hidden divide-y divide-neutral-600/50"
845
845
  });
846
846
  var header2 = tv10({
847
- base: "shrink-0 border-b border-neutral-600/50 grid p-2"
847
+ base: "shrink-0 p-2"
848
848
  });
849
849
  var section = tv10({
850
- base: "border-b border-neutral-600/50 last:border-b-0 grid gap-1 p-2"
850
+ base: "grid gap-1 p-2"
851
851
  });
852
852
  var item = tv10({
853
- base: "flex w-full items-center rounded-md text-sm whitespace-nowrap transition-colors cursor-pointer p-1",
853
+ // [&>*+*] is the label: Text takes no className, and the icon comes first, so a link with no label has nothing to hide.
854
+ base: "flex items-center rounded-md transition-colors h-10 px-3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current [&>*+*]:truncate [&>*+*]:transition-all [&>*+*]:duration-200",
854
855
  variants: {
855
856
  active: {
856
857
  true: "bg-neutral-700 text-white",
857
858
  false: "text-neutral-400 hover:bg-neutral-700/50 hover:text-white"
859
+ },
860
+ collapsed: {
861
+ true: "[&>*+*]:w-0 [&>*+*]:opacity-0",
862
+ false: "[&>*+*]:ml-2 [&>*+*]:opacity-100"
858
863
  }
859
864
  },
860
865
  defaultVariants: {
861
- active: false
866
+ active: false,
867
+ collapsed: false
862
868
  }
863
869
  });
864
- var itemIcon = tv10({
865
- base: "p-2"
866
- });
867
- var itemLabel = tv10({
868
- base: "truncate transition-all duration-200",
870
+ var footer = tv10({
871
+ base: "shrink-0 h-16 p-3 flex items-center gap-3",
869
872
  variants: {
870
873
  collapsed: {
871
- true: "opacity-0 w-0",
872
- false: "opacity-100"
874
+ true: "justify-center"
873
875
  }
874
- },
875
- defaultVariants: {
876
- collapsed: false
877
876
  }
878
877
  });
879
- var footer = tv10({
880
- base: "shrink-0 border-t border-neutral-600/50 p-3 flex items-center gap-3 whitespace-nowrap"
881
- });
882
878
  var avatar = tv10({
883
- base: "shrink-0 w-8 h-8 rounded-full bg-neutral-600 text-white text-xs font-medium flex items-center justify-center"
879
+ base: "shrink-0 size-8 rounded-full bg-neutral-600 text-white text-xs font-medium flex items-center justify-center"
884
880
  });
885
881
  var userInfo = tv10({
886
- base: "flex flex-col min-w-0"
887
- });
888
- var userName = tv10({
889
- base: "text-sm text-white truncate"
890
- });
891
- var userEmail = tv10({
892
- base: "text-xs text-neutral-400"
882
+ base: "flex flex-col min-w-0 text-white [&>*]:truncate [&>*:last-child]:text-xs [&>*:last-child]:text-neutral-400"
893
883
  });
894
884
  var toggleButton = tv10({
895
- base: "shrink-0 border-t border-neutral-600/50 grid p-2 [&>button]:justify-start [&>button]:overflow-hidden [&>button]:whitespace-nowrap"
885
+ base: "shrink-0 h-14 grid p-2 [&>button]:justify-start [&>button]:px-3 [&>button]:overflow-hidden [&>button]:whitespace-nowrap"
896
886
  });
897
887
 
898
888
  // core/sidebar/sidebar.tsx
899
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
889
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
900
890
  var getInitials = (name) => {
901
891
  const parts = name.trim().split(/\s+/);
902
892
  if (parts.length >= 2) {
@@ -917,39 +907,42 @@ var Sidebar = ({
917
907
  const [collapsed, setCollapsed] = useState3(defaultCollapsed);
918
908
  const initials = getInitials(user.name);
919
909
  return /* @__PURE__ */ jsxs12("aside", { className: sidebar({ collapsed }), children: [
920
- homeHref && /* @__PURE__ */ jsx14(
910
+ homeHref && /* @__PURE__ */ jsx14("div", { className: header2(), children: /* @__PURE__ */ jsxs12(
921
911
  Link,
922
912
  {
923
913
  href: homeHref,
924
- className: header2(),
914
+ className: item({ active: homeHref === activeHref, collapsed }),
925
915
  "aria-label": homeLabel || "Home",
926
- children: /* @__PURE__ */ jsxs12("span", { className: item({ active: homeHref === activeHref }), children: [
927
- /* @__PURE__ */ jsx14("span", { className: itemIcon(), children: /* @__PURE__ */ jsx14(Icon, { name: "Home" }) }),
928
- /* @__PURE__ */ jsx14("span", { className: itemLabel({ collapsed }), children: homeLabel })
929
- ] })
916
+ ...homeHref === activeHref && { "aria-current": "page" },
917
+ children: [
918
+ /* @__PURE__ */ jsx14(Icon, { name: "Home" }),
919
+ homeLabel && /* @__PURE__ */ jsx14(Text, { as: "span", variant: "caption", children: homeLabel })
920
+ ]
930
921
  }
931
- ),
922
+ ) }),
932
923
  /* @__PURE__ */ jsx14("nav", { "aria-label": "Main navigation", className: nav(), children: sections.map((sectionData, i) => /* @__PURE__ */ jsx14("div", { className: section(), children: sectionData.items.map((sidebarItem) => {
933
924
  const isActive = sidebarItem.href === activeHref;
934
925
  return /* @__PURE__ */ jsxs12(
935
926
  Link,
936
927
  {
937
928
  href: sidebarItem.href,
938
- className: item({ active: isActive }),
929
+ className: item({ active: isActive, collapsed }),
939
930
  ...isActive && { "aria-current": "page" },
940
931
  children: [
941
- /* @__PURE__ */ jsx14("span", { className: itemIcon(), children: /* @__PURE__ */ jsx14(Icon, { name: sidebarItem.icon }) }),
942
- /* @__PURE__ */ jsx14("span", { className: itemLabel({ collapsed }), children: sidebarItem.label })
932
+ /* @__PURE__ */ jsx14(Icon, { name: sidebarItem.icon }),
933
+ /* @__PURE__ */ jsx14(Text, { as: "span", variant: "caption", children: sidebarItem.label })
943
934
  ]
944
935
  },
945
936
  sidebarItem.href
946
937
  );
947
938
  }) }, i)) }),
948
- /* @__PURE__ */ jsxs12("div", { className: footer(), children: [
949
- /* @__PURE__ */ jsx14("div", { className: avatar(), children: initials }),
950
- /* @__PURE__ */ jsxs12("div", { className: userInfo(), children: [
951
- /* @__PURE__ */ jsx14("span", { className: userName(), children: user.name }),
952
- /* @__PURE__ */ jsx14("span", { className: userEmail(), children: user.email })
939
+ /* @__PURE__ */ jsxs12("div", { className: footer({ collapsed }), children: [
940
+ !collapsed && /* @__PURE__ */ jsxs12(Fragment3, { children: [
941
+ /* @__PURE__ */ jsx14("div", { "aria-hidden": "true", className: avatar(), children: initials }),
942
+ /* @__PURE__ */ jsxs12("div", { className: userInfo(), children: [
943
+ /* @__PURE__ */ jsx14(Text, { as: "span", variant: "caption", children: user.name }),
944
+ /* @__PURE__ */ jsx14(Text, { as: "span", variant: "caption", children: user.email })
945
+ ] })
953
946
  ] }),
954
947
  /* @__PURE__ */ jsx14(
955
948
  Button,
@@ -964,11 +957,12 @@ var Sidebar = ({
964
957
  /* @__PURE__ */ jsx14("div", { className: toggleButton(), children: /* @__PURE__ */ jsx14(
965
958
  Button,
966
959
  {
960
+ variant: "default",
967
961
  color: "ghost",
968
962
  icon: collapsed ? "ChevronRight" : "ChevronLeft",
969
- "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
963
+ "aria-label": collapsed ? "Expand sidebar" : void 0,
970
964
  onClick: () => setCollapsed(!collapsed),
971
- children: collapseLabel
965
+ children: collapsed ? void 0 : collapseLabel
972
966
  }
973
967
  ) })
974
968
  ] });
@@ -7,7 +7,7 @@ import {
7
7
  Toaster,
8
8
  usePageStore,
9
9
  useRouterStore
10
- } from "../chunk-KPSFYQYV.js";
10
+ } from "../chunk-O6UAZGBC.js";
11
11
 
12
12
  // core/next/root-layout/root-layout.styles.ts
13
13
  import { tv } from "tailwind-variants";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexandregme/zljs",
3
- "version": "1.18.1",
3
+ "version": "1.19.1",
4
4
  "description": "Opinionated, accessible React component library for enterprise apps (Next.js, Tailwind CSS v4).",
5
5
  "keywords": [
6
6
  "react",