@studiolxd/brand 49.24.0 → 49.24.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
@@ -7,6 +7,35 @@ El paquete sigue [semver](https://semver.org/lang/es/): **patch** para bug fixes
7
7
  regeneración de `dist`, **minor** para componentes/props/variantes/tokens nuevos, **major**
8
8
  para breaking changes.
9
9
 
10
+ ## [49.24.1] — 2026-10-04
11
+
12
+ > **Patch.** `AppShell` y `ChatShell` ya no rompen la hidratación por debajo del punto de ruptura de
13
+ > escritorio (encargo de Homenize: Next.js 16 con SSR, a 1000px de ancho, React 19 daba «Hydration
14
+ > failed because the server rendered HTML didn't match the client» en cada carga). Sin cambios de API.
15
+
16
+ ### El ancho se lee sin desajustar el HTML del servidor
17
+
18
+ - El servidor no conoce el ancho y pinta la versión de escritorio; el primer render del cliente leía
19
+ `matchMedia` en el estado inicial y, por debajo de `breakpoint.lg`, pintaba el cajón: otro
20
+ `aria-label`/`aria-expanded` en el botón de menú, otra clase, `data-state`, `inert` y `tabIndex` en
21
+ el `aside` de la `Sidebar`, y el asa de redimensión de menos.
22
+ - Nuevo hook interno `useMediaQuery` (`src/stories/constants/media-query.ts`) sobre
23
+ `useSyncExternalStore`, con una instantánea de servidor fija (`null`, «todavía no se sabe»): servidor
24
+ y render de hidratación pintan lo mismo, y React vuelve a pintar con el valor real nada más hidratar.
25
+ Un render solo de cliente lee el valor real desde el principio.
26
+ - `AppShell` y `ChatShell` publican la forma que han tomado en **`data-layout`** (`column`/`drawer`);
27
+ sin el atributo, aún no la saben. Por debajo de lg, su hoja esconde la columna mientras falte el
28
+ atributo —y el `AppShell` deja el glifo de menú en «menú»—, así que el HTML del servidor ya se ve
29
+ como el cajón cerrado: ni parpadeo ni salto de maquetación al hidratar.
30
+ - Una vez montado, todo igual: en escritorio la barra abierta y redimensionable; en móvil el cajón
31
+ cerrado, con `inert`, foco, `aria-expanded` y `aria-controls`.
32
+ - Revisados sin cambios: `SiteHeader` (el panel lo resuelve el CSS), `OrgSwitcher`, `Carousel` y
33
+ `ConversationThread` (solo leen `prefers-reduced-motion` dentro de efectos) y `Chart` (mide con
34
+ `ResizeObserver` tras montar). Las versiones nativas no hidratan y no tienen `AppShell`.
35
+ - Test nuevo (`AppShell.hydration.test.tsx`): `renderToString` + `hydrateRoot` del `AppShell` con
36
+ `AppHeader` y `Sidebar`, y del `ChatShell`, a 1000px y a 1280px, sin ningún `console.error` ni
37
+ error recuperable.
38
+
10
39
  ## [49.24.0] — 2026-10-04
11
40
 
12
41
  > **Minor.** Disparador propio y ancla sin disparador en `ColorPicker`, a petición de creator (la barra
@@ -0,0 +1,12 @@
1
+ import { useCallback as e, useSyncExternalStore as t } from "react";
2
+ //#region src/stories/constants/media-query.ts
3
+ var n = () => null;
4
+ function r(r) {
5
+ return t(e((e) => {
6
+ if (typeof window.matchMedia != "function") return () => {};
7
+ let t = window.matchMedia(r);
8
+ return t.addEventListener("change", e), () => t.removeEventListener("change", e);
9
+ }, [r]), () => typeof window.matchMedia == "function" ? window.matchMedia(r).matches : null, n);
10
+ }
11
+ //#endregion
12
+ export { r as t };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Lee una media query **sin romper la hidratación**.
3
+ *
4
+ * El servidor no conoce el ancho de la ventana. Si el primer render del
5
+ * cliente leyera `matchMedia` (en el `useState` inicial, por ejemplo), pintaría
6
+ * otro marcado que el servidor y React 19 tiraría la hidratación con «server
7
+ * rendered HTML didn't match the client». Con `useSyncExternalStore` el
8
+ * servidor y el render de hidratación leen la misma instantánea fija —`null`,
9
+ * «todavía no se sabe»— y React vuelve a pintar con el valor real nada más
10
+ * hidratar. Un render solo de cliente (sin SSR) lee el valor real desde el
11
+ * primer momento.
12
+ *
13
+ * Mientras valga `null`, el componente pinta su forma de partida y es su CSS,
14
+ * con la misma media query, quien la corrige a la vista: así no hay parpadeo
15
+ * entre el HTML del servidor y el render hidratado.
16
+ *
17
+ * Sin `matchMedia` en el entorno, `null` siempre.
18
+ */
19
+ export declare function useMediaQuery(query: string): boolean | null;
@@ -1 +1 @@
1
- .app-shell{--app-shell-sidebar-width:var(--sidebar-width);--app-shell-banner-height:0px;--app-shell-chrome-block-start:calc(var(--app-shell-banner-height) + var(--app-header-height) + env(safe-area-inset-top));flex-direction:column;block-size:100dvh;display:flex;overflow:clip}.app-shell__banner{flex-shrink:0}.app-shell__body{flex:1;min-block-size:0;display:flex;position:relative}.app-shell__content{min-inline-size:0;padding-inline:var(--section-padding-inline);padding-block:var(--section-padding-block-lg);flex:1;padding-block-end:max(var(--section-padding-block-lg), env(safe-area-inset-bottom));padding-inline-end:max(var(--section-padding-inline), env(safe-area-inset-right));overflow:auto}.app-shell__content.app-shell__content--flush{padding-block:0;padding-inline:0;padding-block-end:env(safe-area-inset-bottom);padding-inline-end:env(safe-area-inset-right)}.app-shell__backdrop{inset:0;background-color:var(--sidebar-backdrop-bg);opacity:var(--sidebar-backdrop-opacity);z-index:var(--sidebar-backdrop-z-index);position:fixed;inset-block-start:var(--app-shell-chrome-block-start)}.app-shell:has(.app-header__logo) .sidebar__header{display:none}
1
+ .app-shell{--app-shell-sidebar-width:var(--sidebar-width);--app-shell-banner-height:0px;--app-shell-chrome-block-start:calc(var(--app-shell-banner-height) + var(--app-header-height) + env(safe-area-inset-top));flex-direction:column;block-size:100dvh;display:flex;overflow:clip}.app-shell__banner{flex-shrink:0}.app-shell__body{flex:1;min-block-size:0;display:flex;position:relative}.app-shell__content{min-inline-size:0;padding-inline:var(--section-padding-inline);padding-block:var(--section-padding-block-lg);flex:1;padding-block-end:max(var(--section-padding-block-lg), env(safe-area-inset-bottom));padding-inline-end:max(var(--section-padding-inline), env(safe-area-inset-right));overflow:auto}.app-shell__content.app-shell__content--flush{padding-block:0;padding-inline:0;padding-block-end:env(safe-area-inset-bottom);padding-inline-end:env(safe-area-inset-right)}.app-shell__backdrop{inset:0;background-color:var(--sidebar-backdrop-bg);opacity:var(--sidebar-backdrop-opacity);z-index:var(--sidebar-backdrop-z-index);position:fixed;inset-block-start:var(--app-shell-chrome-block-start)}.app-shell:has(.app-header__logo) .sidebar__header{display:none}@media (width<=1023.98px){.app-shell:not([data-layout]) .sidebar{display:none}.app-shell:not([data-layout]) .app-header .menu-button .icon__line{opacity:1;transform:none}}
package/dist/app-shell.js CHANGED
@@ -5,94 +5,88 @@ import { SkipLink as t } from "./skip-link.js";
5
5
  import { t as n } from "./_shared/css-properties.js";
6
6
  import { TooltipProvider as r } from "./tooltip.js";
7
7
  import { n as i, t as a } from "./_shared/appshellcontext.js";
8
- import { useCallback as o, useEffect as s, useMemo as c, useState as l } from "react";
9
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
8
+ import { t as o } from "./_shared/media-query.js";
9
+ import { useCallback as s, useEffect as c, useMemo as l, useState as u } from "react";
10
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
10
11
  //#region src/stories/sections/AppShell/AppShell.tsx
11
- var f = "(min-width: 1024px)";
12
- function p() {
13
- let [e, t] = l(() => typeof window > "u" || typeof window.matchMedia != "function" ? !0 : window.matchMedia(f).matches);
14
- return s(() => {
15
- if (typeof window.matchMedia != "function") return;
16
- let e = window.matchMedia(f), n = () => t(e.matches);
17
- return n(), e.addEventListener("change", n), () => e.removeEventListener("change", n);
18
- }, []), e;
19
- }
20
- function m({ banner: i, header: f, sidebar: m, children: h, contentFlush: g = !1, defaultSidebar: _ = "open", sidebarState: v, onSidebarChange: y, defaultSidebarWidth: b, onSidebarWidthChange: x, skipLabel: S }) {
21
- let C = e("appShell"), w = p(), [T, E] = l(_), [D, O] = l(!1), [k, A] = l(b), j = w ? v ?? T : D ? "open" : "closed", M = o((e) => {
22
- w ? (E(e), y?.(e)) : O(e === "open");
23
- }, [w, y]), N = o(() => M(j === "open" ? "closed" : "open"), [M, j]), P = o(() => M("closed"), [M]), F = o((e) => {
24
- A(e), x?.(e);
25
- }, [x]);
26
- s(() => {
27
- if (w || !D) return;
12
+ var p = "(min-width: 1024px)";
13
+ function m({ banner: i, header: m, sidebar: h, children: g, contentFlush: _ = !1, defaultSidebar: v = "open", sidebarState: y, onSidebarChange: b, defaultSidebarWidth: x, onSidebarWidthChange: S, skipLabel: C }) {
14
+ let w = e("appShell"), T = o(p), E = T ?? !0, [D, O] = u(v), [k, A] = u(!1), [j, M] = u(x), N = E ? y ?? D : k ? "open" : "closed", P = s((e) => {
15
+ E ? (O(e), b?.(e)) : A(e === "open");
16
+ }, [E, b]), F = s(() => P(N === "open" ? "closed" : "open"), [P, N]), I = s(() => P("closed"), [P]), L = s((e) => {
17
+ M(e), S?.(e);
18
+ }, [S]);
19
+ c(() => {
20
+ if (E || !k) return;
28
21
  let e = (e) => {
29
- e.key === "Escape" && O(!1);
22
+ e.key === "Escape" && A(!1);
30
23
  };
31
24
  document.addEventListener("keydown", e);
32
25
  let t = document.body.style.overflow;
33
26
  return document.body.style.overflow = "hidden", () => {
34
27
  document.removeEventListener("keydown", e), document.body.style.overflow = t;
35
28
  };
36
- }, [w, D]);
37
- let I = c(() => ({
38
- sidebar: j,
39
- setSidebar: M,
40
- sidebarWidth: k ?? 0,
41
- setSidebarWidth: F,
42
- toggleSidebar: N,
43
- closeSidebar: P,
44
- isDesktop: w
29
+ }, [E, k]);
30
+ let R = l(() => ({
31
+ sidebar: N,
32
+ setSidebar: P,
33
+ sidebarWidth: j ?? 0,
34
+ setSidebarWidth: L,
35
+ toggleSidebar: F,
36
+ closeSidebar: I,
37
+ isDesktop: E
45
38
  }), [
46
- j,
47
- M,
48
- k,
49
- F,
50
39
  N,
51
40
  P,
52
- w
53
- ]), [L, R] = l(0), z = o((e) => {
41
+ j,
42
+ L,
43
+ F,
44
+ I,
45
+ E
46
+ ]), [z, B] = u(0), V = s((e) => {
54
47
  if (!e) return;
55
- let t = () => R(e.getBoundingClientRect().height);
48
+ let t = () => B(e.getBoundingClientRect().height);
56
49
  if (t(), typeof ResizeObserver > "u") return;
57
50
  let n = new ResizeObserver(t);
58
51
  return n.observe(e), () => {
59
- n.disconnect(), R(0);
52
+ n.disconnect(), B(0);
60
53
  };
61
- }, []), B = n({
62
- "--app-shell-sidebar-width": k ? `${k}px` : void 0,
63
- "--app-shell-banner-height": i ? `${L}px` : void 0
64
- }), V = !w && D;
65
- return /* @__PURE__ */ u(a.Provider, {
66
- value: I,
67
- children: /* @__PURE__ */ d(r, { children: [/* @__PURE__ */ u(t, {
54
+ }, []), H = n({
55
+ "--app-shell-sidebar-width": j ? `${j}px` : void 0,
56
+ "--app-shell-banner-height": i ? `${z}px` : void 0
57
+ }), U = !E && k;
58
+ return /* @__PURE__ */ d(a.Provider, {
59
+ value: R,
60
+ children: /* @__PURE__ */ f(r, { children: [/* @__PURE__ */ d(t, {
68
61
  href: "#main-content",
69
- children: C("skipToContent", S)
70
- }), /* @__PURE__ */ d("div", {
71
- ref: B,
62
+ children: w("skipToContent", C)
63
+ }), /* @__PURE__ */ f("div", {
64
+ ref: H,
72
65
  className: "app-shell",
73
- "data-sidebar": j,
66
+ "data-sidebar": N,
67
+ "data-layout": T === null ? void 0 : E ? "column" : "drawer",
74
68
  children: [
75
- i && /* @__PURE__ */ u("div", {
76
- ref: z,
69
+ i && /* @__PURE__ */ d("div", {
70
+ ref: V,
77
71
  className: "app-shell__banner",
78
72
  children: i
79
73
  }),
80
- f,
81
- /* @__PURE__ */ d("div", {
74
+ m,
75
+ /* @__PURE__ */ f("div", {
82
76
  className: "app-shell__body",
83
77
  children: [
84
- m,
85
- V && /* @__PURE__ */ u("div", {
78
+ h,
79
+ U && /* @__PURE__ */ d("div", {
86
80
  className: "app-shell__backdrop",
87
- onClick: P,
81
+ onClick: I,
88
82
  "aria-hidden": "true"
89
83
  }),
90
- /* @__PURE__ */ u("main", {
84
+ /* @__PURE__ */ d("main", {
91
85
  id: "main-content",
92
86
  tabIndex: -1,
93
- className: g ? "app-shell__content app-shell__content--flush" : "app-shell__content",
94
- inert: V || void 0,
95
- children: h
87
+ className: _ ? "app-shell__content app-shell__content--flush" : "app-shell__content",
88
+ inert: U || void 0,
89
+ children: g
96
90
  })
97
91
  ]
98
92
  })
@@ -1 +1 @@
1
- .chat-shell{background-color:var(--chat-shell-bg);flex-direction:column;block-size:100%;min-block-size:0;display:flex;position:relative}.chat-shell .chat-shell__drawer,.chat-shell .sheet__overlay{position:absolute}.chat-shell .chat-shell__drawer{--sheet-inline-size:100%;--sheet-padding-block:0;--sheet-padding-inline:0;--sheet-gap:0;overflow:visible}.chat-shell__drawer .sheet__body{gap:var(--chat-shell-drawer-gap);flex-direction:column;min-block-size:0;display:flex}.chat-shell__drawer-list{flex:1;min-block-size:0;overflow-y:auto}.chat-shell__list{inline-size:var(--chat-shell-list-width);flex-shrink:0;block-size:100%;overflow-y:auto}.chat-shell.chat-shell--with-list{gap:var(--chat-shell-gap);flex-direction:row}.chat-shell__main{flex-direction:column;flex:1;min-block-size:0;display:flex}.chat-shell__header{justify-content:space-between;align-items:center;gap:var(--chat-shell-header-gap);flex-shrink:0;display:flex}.chat-shell__list-trigger{flex-shrink:0;align-self:flex-start}.chat-shell__header-content{justify-content:space-between;align-items:center;gap:var(--chat-shell-header-gap);flex:1;min-inline-size:0;display:flex}.chat-shell__thread{--conversation-thread-padding-inline:var(--chat-shell-thread-padding-inline);min-block-size:0;padding-block:var(--chat-shell-thread-padding-block);flex-direction:column;flex:1;display:flex;overflow-y:auto}.chat-shell__composer{flex-shrink:0}
1
+ .chat-shell{background-color:var(--chat-shell-bg);flex-direction:column;block-size:100%;min-block-size:0;display:flex;position:relative}.chat-shell .chat-shell__drawer,.chat-shell .sheet__overlay{position:absolute}.chat-shell .chat-shell__drawer{--sheet-inline-size:100%;--sheet-padding-block:0;--sheet-padding-inline:0;--sheet-gap:0;overflow:visible}.chat-shell__drawer .sheet__body{gap:var(--chat-shell-drawer-gap);flex-direction:column;min-block-size:0;display:flex}.chat-shell__drawer-list{flex:1;min-block-size:0;overflow-y:auto}.chat-shell__list{inline-size:var(--chat-shell-list-width);flex-shrink:0;block-size:100%;overflow-y:auto}.chat-shell.chat-shell--with-list{gap:var(--chat-shell-gap);flex-direction:row}.chat-shell__main{flex-direction:column;flex:1;min-block-size:0;display:flex}.chat-shell__header{justify-content:space-between;align-items:center;gap:var(--chat-shell-header-gap);flex-shrink:0;display:flex}.chat-shell__list-trigger{flex-shrink:0;align-self:flex-start}.chat-shell__header-content{justify-content:space-between;align-items:center;gap:var(--chat-shell-header-gap);flex:1;min-inline-size:0;display:flex}.chat-shell__thread{--conversation-thread-padding-inline:var(--chat-shell-thread-padding-inline);min-block-size:0;padding-block:var(--chat-shell-thread-padding-block);flex-direction:column;flex:1;display:flex;overflow-y:auto}.chat-shell__composer{flex-shrink:0}@media (width<=1023.98px){.chat-shell:not([data-layout]) .chat-shell__list{display:none}.chat-shell.chat-shell--with-list:not([data-layout]){flex-direction:column}}
@@ -4,50 +4,47 @@ import { n as e } from "./_shared/brandmessagescontext.js";
4
4
  import { Icon as t } from "./icon.js";
5
5
  import { Button as n } from "./button.js";
6
6
  import { Sheet as r } from "./sheet.js";
7
- import { forwardRef as i, useCallback as a, useEffect as o, useState as s } from "react";
7
+ import { t as i } from "./_shared/media-query.js";
8
+ import { forwardRef as a, useCallback as o, useState as s } from "react";
8
9
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
9
10
  //#region src/stories/templates/ChatShell/ChatShell.tsx
10
- var u = "(min-width: 1024px)", d = i(function({ list: i, header: d, children: f, composer: p, listLabel: m, listTriggerLabel: h, listOpen: g, onListOpenChange: _, className: v, ...y }, b) {
11
- let x = e("chatShell"), [S, C] = s(() => typeof window > "u" ? !0 : window.matchMedia(u).matches), [w, T] = s(!1), [E, D] = s(null), O = a((e) => {
11
+ var u = "(min-width: 1024px)", d = a(function({ list: a, header: d, children: f, composer: p, listLabel: m, listTriggerLabel: h, listOpen: g, onListOpenChange: _, className: v, ...y }, b) {
12
+ let x = e("chatShell"), S = i(u), C = S ?? !0, [w, T] = s(!1), [E, D] = s(null), O = o((e) => {
12
13
  D(e), typeof b == "function" ? b(e) : b && (b.current = e);
13
- }, [b]);
14
- o(() => {
15
- let e = window.matchMedia(u), t = () => {
16
- C(e.matches), e.matches && T(!1);
17
- };
18
- return t(), e.addEventListener("change", t), () => e.removeEventListener("change", t);
19
- }, []);
20
- let k = a((e) => {
14
+ }, [b]), [k, A] = s(S);
15
+ S !== k && (A(S), S && T(!1));
16
+ let j = o((e) => {
21
17
  g === void 0 && T(e), _?.(e);
22
- }, [g, _]), A = !S && (g ?? w), j = i != null && S, M = i != null && !S;
18
+ }, [g, _]), M = !C && (g ?? w), N = a != null && C, P = a != null && !C;
23
19
  return /* @__PURE__ */ l("div", {
24
20
  ref: O,
25
21
  className: [
26
22
  "chat-shell",
27
- j ? "chat-shell--with-list" : "",
23
+ N ? "chat-shell--with-list" : "",
28
24
  v ?? ""
29
25
  ].filter(Boolean).join(" "),
26
+ "data-layout": S === null ? void 0 : C ? "column" : "drawer",
30
27
  ...y,
31
28
  children: [
32
- j && /* @__PURE__ */ c("aside", {
29
+ N && /* @__PURE__ */ c("aside", {
33
30
  className: "chat-shell__list",
34
31
  "aria-label": x("list", m),
35
- children: i
32
+ children: a
36
33
  }),
37
34
  /* @__PURE__ */ l("div", {
38
35
  className: "chat-shell__main",
39
36
  children: [
40
- (d || M) && /* @__PURE__ */ l("header", {
37
+ (d || P) && /* @__PURE__ */ l("header", {
41
38
  className: "chat-shell__header",
42
- children: [M && /* @__PURE__ */ c(n, {
39
+ children: [P && /* @__PURE__ */ c(n, {
43
40
  variant: "ghost",
44
41
  size: "sm",
45
42
  iconOnly: !0,
46
43
  "aria-label": x("listTrigger", h),
47
44
  "aria-haspopup": "dialog",
48
- "aria-expanded": A,
45
+ "aria-expanded": M,
49
46
  className: "chat-shell__list-trigger",
50
- onClick: () => k(!0),
47
+ onClick: () => j(!0),
51
48
  children: /* @__PURE__ */ c(t, {
52
49
  name: "layout-sidebar",
53
50
  size: "sm"
@@ -67,10 +64,10 @@ var u = "(min-width: 1024px)", d = i(function({ list: i, header: d, children: f,
67
64
  })
68
65
  ]
69
66
  }),
70
- M && /* @__PURE__ */ l(r, {
67
+ P && /* @__PURE__ */ l(r, {
71
68
  side: "left",
72
- open: A,
73
- onOpenChange: k,
69
+ open: M,
70
+ onOpenChange: j,
74
71
  title: x("list", m),
75
72
  titleHidden: !0,
76
73
  container: E ?? void 0,
@@ -85,7 +82,7 @@ var u = "(min-width: 1024px)", d = i(function({ list: i, header: d, children: f,
85
82
  "aria-label": x("listTrigger", h),
86
83
  "aria-expanded": !0,
87
84
  className: "chat-shell__list-trigger",
88
- onClick: () => k(!1),
85
+ onClick: () => j(!1),
89
86
  children: /* @__PURE__ */ c(t, {
90
87
  name: "layout-sidebar",
91
88
  size: "sm"
@@ -93,7 +90,7 @@ var u = "(min-width: 1024px)", d = i(function({ list: i, header: d, children: f,
93
90
  })
94
91
  }), /* @__PURE__ */ c("div", {
95
92
  className: "chat-shell__drawer-list",
96
- children: i
93
+ children: a
97
94
  })]
98
95
  })
99
96
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studiolxd/brand",
3
- "version": "49.24.0",
3
+ "version": "49.24.1",
4
4
  "description": "El sistema de diseño de Studio LXD: componentes React, tokens y documentación de la suite SLXD.",
5
5
  "repository": {
6
6
  "type": "git",