@skipleague/design 0.7.0 → 0.8.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.
@@ -0,0 +1,33 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ /** A single destination in the tablet {@link IconRail}. */
3
+ export interface IconRailItem {
4
+ /** Stable key. */
5
+ key: string;
6
+ label: string;
7
+ /** ~21px line icon (stroke 2, currentColor) — inherits the item's color. */
8
+ icon: ReactNode;
9
+ /** Current route — highlighted brand. Derive from your router. */
10
+ active?: boolean;
11
+ /** Renders an `<a href>`; omit for a `<button>` (pure onClick). */
12
+ href?: string;
13
+ onClick?: () => void;
14
+ }
15
+ export interface IconRailProps {
16
+ items: IconRailItem[];
17
+ /** Rail width in px (default 78 — the shell's tablet nav column). */
18
+ width?: number;
19
+ /** Route item links via your own router (e.g. React Router's `<Link>`). */
20
+ renderLink?: (args: {
21
+ href: string;
22
+ style: CSSProperties;
23
+ "aria-current"?: "page";
24
+ children: ReactNode;
25
+ }) => ReactNode;
26
+ }
27
+ /**
28
+ * The tablet-width vertical navigation rail (design_handoff_responsive_shell) —
29
+ * icon-over-label stacks in a 78px column. Sits in the `rail` slot of
30
+ * {@link ResponsiveShell} between the phone bottom bar and the desktop
31
+ * {@link SidebarNav}. Nav only; identity/account live in the top bar.
32
+ */
33
+ export declare function IconRail({ items, width, renderLink }: IconRailProps): import("react").JSX.Element;
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The tablet-width vertical navigation rail (design_handoff_responsive_shell) —
4
+ * icon-over-label stacks in a 78px column. Sits in the `rail` slot of
5
+ * {@link ResponsiveShell} between the phone bottom bar and the desktop
6
+ * {@link SidebarNav}. Nav only; identity/account live in the top bar.
7
+ */
8
+ export function IconRail({ items, width = 78, renderLink }) {
9
+ return (_jsx("nav", { style: {
10
+ width,
11
+ height: "100%",
12
+ display: "flex",
13
+ flexDirection: "column",
14
+ alignItems: "stretch",
15
+ gap: 4,
16
+ padding: "14px 10px",
17
+ background: "#ffffff",
18
+ fontFamily: "var(--skl-font-sans)",
19
+ }, children: items.map((item) => (_jsx(Item, { item: item, renderLink: renderLink }, item.key))) }));
20
+ }
21
+ function Item({ item, renderLink, }) {
22
+ const { label, icon, active, href, onClick } = item;
23
+ const style = {
24
+ display: "flex",
25
+ flexDirection: "column",
26
+ justifyContent: "center",
27
+ alignItems: "center",
28
+ gap: 5,
29
+ padding: "11px 6px",
30
+ borderRadius: 12,
31
+ textDecoration: "none",
32
+ cursor: "pointer",
33
+ color: active ? "var(--skl-color-brand)" : "#94a3b8",
34
+ background: active ? "var(--skl-color-current-bg)" : "transparent",
35
+ };
36
+ const inner = (_jsxs(_Fragment, { children: [icon, _jsx("span", { style: { fontSize: 9.5, fontWeight: 600, lineHeight: 1 }, children: label })] }));
37
+ if (href) {
38
+ if (renderLink) {
39
+ return renderLink({ href, style, "aria-current": active ? "page" : undefined, children: inner });
40
+ }
41
+ return (_jsx("a", { href: href, style: style, "aria-current": active ? "page" : undefined, onClick: onClick, children: inner }));
42
+ }
43
+ return (_jsx("button", { type: "button", onClick: onClick, "aria-current": active ? "page" : undefined, style: { ...style, border: "none", width: "100%" }, children: inner }));
44
+ }
@@ -57,6 +57,10 @@ export function ProfileMenu({ user, currentSlug, apps = SKIPLEAGUE_APPS, enabled
57
57
  display: "inline-flex",
58
58
  alignItems: "center",
59
59
  justifyContent: "center",
60
+ // Explicit padding:0 — a host app's global `button { padding }` would
61
+ // otherwise collapse this fixed 38×38 box's content area (border-box)
62
+ // and shrink the icon to a dot.
63
+ padding: 0,
60
64
  width: 38,
61
65
  height: 38,
62
66
  borderRadius: "var(--skl-radius-control)",
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Class for an in-detail control that should only show while the detail panel is
4
+ * a phone/tablet overlay (e.g. its back button) — hidden once the panel is
5
+ * docked on desktop. Put it on the app's detail back button:
6
+ * `<button className={RESPONSIVE_SHELL_DETAIL_BACK_CLASS} onClick={onClose}>`.
7
+ */
8
+ export declare const RESPONSIVE_SHELL_DETAIL_BACK_CLASS = "skl-shell-detail-back";
9
+ export interface ResponsiveShellProps {
10
+ /** Persistent top bar — spans the full width at every breakpoint (e.g. TopBar/DesktopActionBar). */
11
+ topBar: ReactNode;
12
+ /** The center content column (scrolls). */
13
+ main: ReactNode;
14
+ /** Phone nav (`< 720px`) — e.g. the app's BottomNav. Hidden at wider widths. */
15
+ bottomNav?: ReactNode;
16
+ /** Tablet nav (`720–1079px`) — e.g. {@link IconRail}. Hidden otherwise. */
17
+ rail?: ReactNode;
18
+ /** Desktop nav (`≥ 1080px`) — e.g. SidebarNav (pass width 236 to fill the column). */
19
+ sidebar?: ReactNode;
20
+ /** Master-detail panel: docked right column on desktop; full-screen overlay on phone/tablet. */
21
+ detail?: ReactNode;
22
+ /** Phone/tablet overlay visibility. Ignored on desktop, where detail is always docked. */
23
+ detailOpen?: boolean;
24
+ /** Optional id for scoping/testing. */
25
+ id?: string;
26
+ }
27
+ /**
28
+ * The SkipLeague adaptive app shell (design_handoff_responsive_shell).
29
+ *
30
+ * ONE layout that reflows across three breakpoints — no separate mobile/desktop
31
+ * product. The top bar is constant; the nav and detail regions rearrange:
32
+ *
33
+ * Phone (<720): top bar / content / bottom tab bar; detail = full overlay
34
+ * Tablet (720–1079): top bar / [icon rail | content]; detail = full overlay
35
+ * Desktop (≥1080): top bar / [sidebar | content | docked detail]
36
+ *
37
+ * The detail is the SAME element everywhere (list-detail / master-detail): a
38
+ * drill-in overlay on phone/tablet, a permanently docked right column on desktop.
39
+ * Pass the app's own nav components into `bottomNav` / `rail` / `sidebar` — the
40
+ * shell shows the right one per breakpoint via CSS (no JS width state needed).
41
+ *
42
+ * Give the shell a fixed-height parent (e.g. `height: 100dvh`); it fills it and
43
+ * scrolls internally.
44
+ */
45
+ export declare function ResponsiveShell({ topBar, main, bottomNav, rail, sidebar, detail, detailOpen, id, }: ResponsiveShellProps): import("react").JSX.Element;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Class for an in-detail control that should only show while the detail panel is
4
+ * a phone/tablet overlay (e.g. its back button) — hidden once the panel is
5
+ * docked on desktop. Put it on the app's detail back button:
6
+ * `<button className={RESPONSIVE_SHELL_DETAIL_BACK_CLASS} onClick={onClose}>`.
7
+ */
8
+ export const RESPONSIVE_SHELL_DETAIL_BACK_CLASS = "skl-shell-detail-back";
9
+ // Breakpoints: phone < 720 ≤ tablet < 1080 ≤ desktop. One list-detail layout that
10
+ // reflows — the desktop right panel IS the mobile drill-in, given a permanent column.
11
+ const CSS = `
12
+ .skl-shell {
13
+ height: 100%;
14
+ position: relative;
15
+ overflow: hidden;
16
+ display: flex;
17
+ flex-direction: column;
18
+ background: var(--skl-color-app-bg, #f8fafc);
19
+ font-family: var(--skl-font-sans);
20
+ }
21
+ .skl-shell__top { flex: 0 0 auto; }
22
+ .skl-shell__main { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
23
+ .skl-shell__bottom { flex: 0 0 auto; }
24
+ .skl-shell__rail, .skl-shell__sidebar { display: none; }
25
+ .skl-shell__detail {
26
+ position: absolute; inset: 0; z-index: 40; background: #fff;
27
+ display: none; flex-direction: column;
28
+ }
29
+ .skl-shell__detail.is-open { display: flex; animation: sklShellFade .2s ease-out; }
30
+ .${RESPONSIVE_SHELL_DETAIL_BACK_CLASS} { display: inline-flex; }
31
+ @keyframes sklShellFade { from { opacity: 0; } to { opacity: 1; } }
32
+
33
+ @media (min-width: 720px) {
34
+ .skl-shell {
35
+ display: grid;
36
+ grid-template-columns: 78px 1fr;
37
+ grid-template-rows: auto minmax(0, 1fr);
38
+ grid-template-areas: "top top" "nav main";
39
+ }
40
+ .skl-shell__top { grid-area: top; }
41
+ .skl-shell__main { grid-area: main; height: 100%; }
42
+ .skl-shell__rail { grid-area: nav; display: flex; border-right: 1px solid var(--skl-color-border); }
43
+ .skl-shell__sidebar { grid-area: nav; }
44
+ .skl-shell__bottom { display: none; }
45
+ }
46
+
47
+ @media (min-width: 1080px) {
48
+ .skl-shell {
49
+ grid-template-columns: 236px 1fr 340px;
50
+ grid-template-areas: "top top top" "nav main detail";
51
+ }
52
+ .skl-shell__rail { display: none; }
53
+ .skl-shell__sidebar { grid-area: nav; display: block; border-right: 1px solid var(--skl-color-border); }
54
+ .skl-shell__detail {
55
+ position: relative; inset: auto; z-index: auto; grid-area: detail;
56
+ display: flex; border-left: 1px solid var(--skl-color-border); overflow-y: auto;
57
+ }
58
+ .${RESPONSIVE_SHELL_DETAIL_BACK_CLASS} { display: none; }
59
+ }
60
+ `;
61
+ /**
62
+ * The SkipLeague adaptive app shell (design_handoff_responsive_shell).
63
+ *
64
+ * ONE layout that reflows across three breakpoints — no separate mobile/desktop
65
+ * product. The top bar is constant; the nav and detail regions rearrange:
66
+ *
67
+ * Phone (<720): top bar / content / bottom tab bar; detail = full overlay
68
+ * Tablet (720–1079): top bar / [icon rail | content]; detail = full overlay
69
+ * Desktop (≥1080): top bar / [sidebar | content | docked detail]
70
+ *
71
+ * The detail is the SAME element everywhere (list-detail / master-detail): a
72
+ * drill-in overlay on phone/tablet, a permanently docked right column on desktop.
73
+ * Pass the app's own nav components into `bottomNav` / `rail` / `sidebar` — the
74
+ * shell shows the right one per breakpoint via CSS (no JS width state needed).
75
+ *
76
+ * Give the shell a fixed-height parent (e.g. `height: 100dvh`); it fills it and
77
+ * scrolls internally.
78
+ */
79
+ export function ResponsiveShell({ topBar, main, bottomNav, rail, sidebar, detail, detailOpen = false, id, }) {
80
+ return (_jsxs("div", { className: "skl-shell", id: id, children: [_jsx("style", { children: CSS }), _jsx("div", { className: "skl-shell__top", children: topBar }), sidebar && _jsx("div", { className: "skl-shell__sidebar", children: sidebar }), rail && _jsx("div", { className: "skl-shell__rail", children: rail }), _jsx("div", { className: "skl-shell__main", children: main }), detail && _jsx("div", { className: `skl-shell__detail${detailOpen ? " is-open" : ""}`, children: detail }), bottomNav && _jsx("div", { className: "skl-shell__bottom", children: bottomNav })] }));
81
+ }
package/dist/TopBar.js CHANGED
@@ -82,6 +82,9 @@ export function TopBarIconButton({ tone = "dark", compact = false, style, childr
82
82
  justifyContent: "center",
83
83
  cursor: "pointer",
84
84
  flex: "0 0 auto",
85
+ // Explicit padding:0 so a host app's global `button { padding }` can't
86
+ // collapse this fixed-size box (border-box) and shrink the icon.
87
+ padding: 0,
85
88
  background: palette.background,
86
89
  border: `1px solid ${palette.border}`,
87
90
  color: palette.color,
package/dist/index.d.ts CHANGED
@@ -7,6 +7,10 @@ export { TopBar, TopBarIconButton, DesktopActionBar } from "./TopBar.js";
7
7
  export type { TopBarTone } from "./TopBar.js";
8
8
  export { SidebarNav } from "./SidebarNav.js";
9
9
  export type { SidebarNavProps, SidebarNavSection, SidebarNavItem, SidebarNavLinkArgs } from "./SidebarNav.js";
10
+ export { IconRail } from "./IconRail.js";
11
+ export type { IconRailProps, IconRailItem } from "./IconRail.js";
12
+ export { ResponsiveShell, RESPONSIVE_SHELL_DETAIL_BACK_CLASS } from "./ResponsiveShell.js";
13
+ export type { ResponsiveShellProps } from "./ResponsiveShell.js";
10
14
  export { BottomNav } from "./BottomNav.js";
11
15
  export type { BottomNavProps, BottomNavTab, BottomNavAction } from "./BottomNav.js";
12
16
  export { ShareMenu } from "./ShareMenu.js";
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export { AppBadge } from "./AppBadge.js";
3
3
  export { AppLogo, APP_GLYPHS, appGlyphForSlug } from "./AppLogo.js";
4
4
  export { TopBar, TopBarIconButton, DesktopActionBar } from "./TopBar.js";
5
5
  export { SidebarNav } from "./SidebarNav.js";
6
+ export { IconRail } from "./IconRail.js";
7
+ export { ResponsiveShell, RESPONSIVE_SHELL_DETAIL_BACK_CLASS } from "./ResponsiveShell.js";
6
8
  export { BottomNav } from "./BottomNav.js";
7
9
  export { ShareMenu } from "./ShareMenu.js";
8
10
  export { Button } from "./Button.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skipleague/design",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "SkipUI — the SkipLeague design system: shared tokens and React components for every SkipLeague app.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,96 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+
3
+ /** A single destination in the tablet {@link IconRail}. */
4
+ export interface IconRailItem {
5
+ /** Stable key. */
6
+ key: string;
7
+ label: string;
8
+ /** ~21px line icon (stroke 2, currentColor) — inherits the item's color. */
9
+ icon: ReactNode;
10
+ /** Current route — highlighted brand. Derive from your router. */
11
+ active?: boolean;
12
+ /** Renders an `<a href>`; omit for a `<button>` (pure onClick). */
13
+ href?: string;
14
+ onClick?: () => void;
15
+ }
16
+
17
+ export interface IconRailProps {
18
+ items: IconRailItem[];
19
+ /** Rail width in px (default 78 — the shell's tablet nav column). */
20
+ width?: number;
21
+ /** Route item links via your own router (e.g. React Router's `<Link>`). */
22
+ renderLink?: (args: { href: string; style: CSSProperties; "aria-current"?: "page"; children: ReactNode }) => ReactNode;
23
+ }
24
+
25
+ /**
26
+ * The tablet-width vertical navigation rail (design_handoff_responsive_shell) —
27
+ * icon-over-label stacks in a 78px column. Sits in the `rail` slot of
28
+ * {@link ResponsiveShell} between the phone bottom bar and the desktop
29
+ * {@link SidebarNav}. Nav only; identity/account live in the top bar.
30
+ */
31
+ export function IconRail({ items, width = 78, renderLink }: IconRailProps) {
32
+ return (
33
+ <nav
34
+ style={{
35
+ width,
36
+ height: "100%",
37
+ display: "flex",
38
+ flexDirection: "column",
39
+ alignItems: "stretch",
40
+ gap: 4,
41
+ padding: "14px 10px",
42
+ background: "#ffffff",
43
+ fontFamily: "var(--skl-font-sans)",
44
+ }}
45
+ >
46
+ {items.map((item) => (
47
+ <Item key={item.key} item={item} renderLink={renderLink} />
48
+ ))}
49
+ </nav>
50
+ );
51
+ }
52
+
53
+ function Item({
54
+ item,
55
+ renderLink,
56
+ }: {
57
+ item: IconRailItem;
58
+ renderLink?: IconRailProps["renderLink"];
59
+ }) {
60
+ const { label, icon, active, href, onClick } = item;
61
+ const style: CSSProperties = {
62
+ display: "flex",
63
+ flexDirection: "column",
64
+ justifyContent: "center",
65
+ alignItems: "center",
66
+ gap: 5,
67
+ padding: "11px 6px",
68
+ borderRadius: 12,
69
+ textDecoration: "none",
70
+ cursor: "pointer",
71
+ color: active ? "var(--skl-color-brand)" : "#94a3b8",
72
+ background: active ? "var(--skl-color-current-bg)" : "transparent",
73
+ };
74
+ const inner = (
75
+ <>
76
+ {icon}
77
+ <span style={{ fontSize: 9.5, fontWeight: 600, lineHeight: 1 }}>{label}</span>
78
+ </>
79
+ );
80
+
81
+ if (href) {
82
+ if (renderLink) {
83
+ return renderLink({ href, style, "aria-current": active ? "page" : undefined, children: inner });
84
+ }
85
+ return (
86
+ <a href={href} style={style} aria-current={active ? "page" : undefined} onClick={onClick}>
87
+ {inner}
88
+ </a>
89
+ );
90
+ }
91
+ return (
92
+ <button type="button" onClick={onClick} aria-current={active ? "page" : undefined} style={{ ...style, border: "none", width: "100%" }}>
93
+ {inner}
94
+ </button>
95
+ );
96
+ }
@@ -161,6 +161,10 @@ export function ProfileMenu({
161
161
  display: "inline-flex",
162
162
  alignItems: "center",
163
163
  justifyContent: "center",
164
+ // Explicit padding:0 — a host app's global `button { padding }` would
165
+ // otherwise collapse this fixed 38×38 box's content area (border-box)
166
+ // and shrink the icon to a dot.
167
+ padding: 0,
164
168
  width: 38,
165
169
  height: 38,
166
170
  borderRadius: "var(--skl-radius-control)",
@@ -0,0 +1,122 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Class for an in-detail control that should only show while the detail panel is
5
+ * a phone/tablet overlay (e.g. its back button) — hidden once the panel is
6
+ * docked on desktop. Put it on the app's detail back button:
7
+ * `<button className={RESPONSIVE_SHELL_DETAIL_BACK_CLASS} onClick={onClose}>`.
8
+ */
9
+ export const RESPONSIVE_SHELL_DETAIL_BACK_CLASS = "skl-shell-detail-back";
10
+
11
+ export interface ResponsiveShellProps {
12
+ /** Persistent top bar — spans the full width at every breakpoint (e.g. TopBar/DesktopActionBar). */
13
+ topBar: ReactNode;
14
+ /** The center content column (scrolls). */
15
+ main: ReactNode;
16
+ /** Phone nav (`< 720px`) — e.g. the app's BottomNav. Hidden at wider widths. */
17
+ bottomNav?: ReactNode;
18
+ /** Tablet nav (`720–1079px`) — e.g. {@link IconRail}. Hidden otherwise. */
19
+ rail?: ReactNode;
20
+ /** Desktop nav (`≥ 1080px`) — e.g. SidebarNav (pass width 236 to fill the column). */
21
+ sidebar?: ReactNode;
22
+ /** Master-detail panel: docked right column on desktop; full-screen overlay on phone/tablet. */
23
+ detail?: ReactNode;
24
+ /** Phone/tablet overlay visibility. Ignored on desktop, where detail is always docked. */
25
+ detailOpen?: boolean;
26
+ /** Optional id for scoping/testing. */
27
+ id?: string;
28
+ }
29
+
30
+ // Breakpoints: phone < 720 ≤ tablet < 1080 ≤ desktop. One list-detail layout that
31
+ // reflows — the desktop right panel IS the mobile drill-in, given a permanent column.
32
+ const CSS = `
33
+ .skl-shell {
34
+ height: 100%;
35
+ position: relative;
36
+ overflow: hidden;
37
+ display: flex;
38
+ flex-direction: column;
39
+ background: var(--skl-color-app-bg, #f8fafc);
40
+ font-family: var(--skl-font-sans);
41
+ }
42
+ .skl-shell__top { flex: 0 0 auto; }
43
+ .skl-shell__main { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
44
+ .skl-shell__bottom { flex: 0 0 auto; }
45
+ .skl-shell__rail, .skl-shell__sidebar { display: none; }
46
+ .skl-shell__detail {
47
+ position: absolute; inset: 0; z-index: 40; background: #fff;
48
+ display: none; flex-direction: column;
49
+ }
50
+ .skl-shell__detail.is-open { display: flex; animation: sklShellFade .2s ease-out; }
51
+ .${RESPONSIVE_SHELL_DETAIL_BACK_CLASS} { display: inline-flex; }
52
+ @keyframes sklShellFade { from { opacity: 0; } to { opacity: 1; } }
53
+
54
+ @media (min-width: 720px) {
55
+ .skl-shell {
56
+ display: grid;
57
+ grid-template-columns: 78px 1fr;
58
+ grid-template-rows: auto minmax(0, 1fr);
59
+ grid-template-areas: "top top" "nav main";
60
+ }
61
+ .skl-shell__top { grid-area: top; }
62
+ .skl-shell__main { grid-area: main; height: 100%; }
63
+ .skl-shell__rail { grid-area: nav; display: flex; border-right: 1px solid var(--skl-color-border); }
64
+ .skl-shell__sidebar { grid-area: nav; }
65
+ .skl-shell__bottom { display: none; }
66
+ }
67
+
68
+ @media (min-width: 1080px) {
69
+ .skl-shell {
70
+ grid-template-columns: 236px 1fr 340px;
71
+ grid-template-areas: "top top top" "nav main detail";
72
+ }
73
+ .skl-shell__rail { display: none; }
74
+ .skl-shell__sidebar { grid-area: nav; display: block; border-right: 1px solid var(--skl-color-border); }
75
+ .skl-shell__detail {
76
+ position: relative; inset: auto; z-index: auto; grid-area: detail;
77
+ display: flex; border-left: 1px solid var(--skl-color-border); overflow-y: auto;
78
+ }
79
+ .${RESPONSIVE_SHELL_DETAIL_BACK_CLASS} { display: none; }
80
+ }
81
+ `;
82
+
83
+ /**
84
+ * The SkipLeague adaptive app shell (design_handoff_responsive_shell).
85
+ *
86
+ * ONE layout that reflows across three breakpoints — no separate mobile/desktop
87
+ * product. The top bar is constant; the nav and detail regions rearrange:
88
+ *
89
+ * Phone (<720): top bar / content / bottom tab bar; detail = full overlay
90
+ * Tablet (720–1079): top bar / [icon rail | content]; detail = full overlay
91
+ * Desktop (≥1080): top bar / [sidebar | content | docked detail]
92
+ *
93
+ * The detail is the SAME element everywhere (list-detail / master-detail): a
94
+ * drill-in overlay on phone/tablet, a permanently docked right column on desktop.
95
+ * Pass the app's own nav components into `bottomNav` / `rail` / `sidebar` — the
96
+ * shell shows the right one per breakpoint via CSS (no JS width state needed).
97
+ *
98
+ * Give the shell a fixed-height parent (e.g. `height: 100dvh`); it fills it and
99
+ * scrolls internally.
100
+ */
101
+ export function ResponsiveShell({
102
+ topBar,
103
+ main,
104
+ bottomNav,
105
+ rail,
106
+ sidebar,
107
+ detail,
108
+ detailOpen = false,
109
+ id,
110
+ }: ResponsiveShellProps) {
111
+ return (
112
+ <div className="skl-shell" id={id}>
113
+ <style>{CSS}</style>
114
+ <div className="skl-shell__top">{topBar}</div>
115
+ {sidebar && <div className="skl-shell__sidebar">{sidebar}</div>}
116
+ {rail && <div className="skl-shell__rail">{rail}</div>}
117
+ <div className="skl-shell__main">{main}</div>
118
+ {detail && <div className={`skl-shell__detail${detailOpen ? " is-open" : ""}`}>{detail}</div>}
119
+ {bottomNav && <div className="skl-shell__bottom">{bottomNav}</div>}
120
+ </div>
121
+ );
122
+ }
package/src/TopBar.tsx CHANGED
@@ -168,6 +168,9 @@ export function TopBarIconButton({
168
168
  justifyContent: "center",
169
169
  cursor: "pointer",
170
170
  flex: "0 0 auto",
171
+ // Explicit padding:0 so a host app's global `button { padding }` can't
172
+ // collapse this fixed-size box (border-box) and shrink the icon.
173
+ padding: 0,
171
174
  background: palette.background,
172
175
  border: `1px solid ${palette.border}`,
173
176
  color: palette.color,
package/src/index.ts CHANGED
@@ -7,6 +7,10 @@ export { TopBar, TopBarIconButton, DesktopActionBar } from "./TopBar.js";
7
7
  export type { TopBarTone } from "./TopBar.js";
8
8
  export { SidebarNav } from "./SidebarNav.js";
9
9
  export type { SidebarNavProps, SidebarNavSection, SidebarNavItem, SidebarNavLinkArgs } from "./SidebarNav.js";
10
+ export { IconRail } from "./IconRail.js";
11
+ export type { IconRailProps, IconRailItem } from "./IconRail.js";
12
+ export { ResponsiveShell, RESPONSIVE_SHELL_DETAIL_BACK_CLASS } from "./ResponsiveShell.js";
13
+ export type { ResponsiveShellProps } from "./ResponsiveShell.js";
10
14
  export { BottomNav } from "./BottomNav.js";
11
15
  export type { BottomNavProps, BottomNavTab, BottomNavAction } from "./BottomNav.js";
12
16
  export { ShareMenu } from "./ShareMenu.js";