@xenide-io/the-old-ui-theme 0.5.2 → 0.5.3

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.
@@ -1,8 +1,8 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { type ReactNode, type PointerEvent } from 'react';
3
+ import { type ReactNode, type PointerEvent } from "react";
4
4
 
5
- import { cn } from '../lib/cn';
5
+ import { cn } from "../lib/cn";
6
6
 
7
7
  export interface SuiteAppLayoutProps {
8
8
  sidebar: ReactNode;
@@ -20,6 +20,8 @@ export interface SuiteAppLayoutProps {
20
20
  lockMainScroll?: boolean;
21
21
  /** Drag this to resize the sidebar. */
22
22
  onStartResize?: (e: PointerEvent<HTMLDivElement>) => void;
23
+ /** Keyboard equivalent for the resize handle. */
24
+ onResizeBy?: (delta: number) => void;
23
25
  /** Double-click to reset sidebar width. */
24
26
  onResetWidth?: () => void;
25
27
  className?: string;
@@ -39,18 +41,22 @@ export function SuiteAppLayout({
39
41
  collapsed = false,
40
42
  lockMainScroll = false,
41
43
  onStartResize,
44
+ onResizeBy,
42
45
  onResetWidth,
43
46
  className,
44
47
  }: SuiteAppLayoutProps) {
45
48
  return (
46
49
  <div
47
50
  data-test="app-shell"
48
- className={cn('flex min-h-dvh lg:h-screen lg:overflow-hidden', className)}
51
+ className={cn(
52
+ "suite-app-layout flex min-h-dvh lg:h-screen lg:overflow-hidden",
53
+ className,
54
+ )}
49
55
  >
50
56
  <aside
51
- className="relative hidden shrink-0 flex-col overflow-hidden border-r border-ph-border transition-[width] duration-200 ease-out lg:flex"
57
+ className="relative hidden shrink-0 flex-col overflow-visible border-r border-ph-border ease-out lg:flex"
52
58
  style={{ width: `${sidebarWidth}px` }}
53
- data-collapsed={collapsed ? 'true' : 'false'}
59
+ data-collapsed={collapsed ? "true" : "false"}
54
60
  >
55
61
  {sidebar}
56
62
  {onStartResize ? (
@@ -58,9 +64,23 @@ export function SuiteAppLayout({
58
64
  role="separator"
59
65
  aria-orientation="vertical"
60
66
  aria-label="Resize sidebar"
67
+ data-test="sidebar-resize-handle"
68
+ tabIndex={onResizeBy ? 0 : undefined}
61
69
  onPointerDown={onStartResize}
62
70
  onDoubleClick={onResetWidth}
63
- className="absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
71
+ onKeyDown={(event) => {
72
+ if (event.key === "Home") {
73
+ event.preventDefault();
74
+ onResetWidth?.();
75
+ } else if (event.key === "ArrowLeft") {
76
+ event.preventDefault();
77
+ onResizeBy?.(-16);
78
+ } else if (event.key === "ArrowRight") {
79
+ event.preventDefault();
80
+ onResizeBy?.(16);
81
+ }
82
+ }}
83
+ className="absolute inset-y-0 -right-1 z-10 w-2 cursor-col-resize touch-none transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
64
84
  />
65
85
  ) : null}
66
86
  </aside>
@@ -72,10 +92,11 @@ export function SuiteAppLayout({
72
92
  <main
73
93
  id="main-content"
74
94
  tabIndex={-1}
75
- data-lock-scroll={lockMainScroll ? 'true' : undefined}
95
+ data-lock-scroll={lockMainScroll ? "true" : undefined}
76
96
  className={cn(
77
- 'flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overscroll-contain bg-ph-canvas focus:outline-none',
78
- lockMainScroll ? 'overflow-hidden' : 'overflow-y-auto',
97
+ "flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overscroll-contain bg-ph-canvas focus:outline-none",
98
+ "suite-app-layout__main",
99
+ lockMainScroll ? "overflow-hidden" : "overflow-y-auto",
79
100
  )}
80
101
  >
81
102
  {children}
@@ -1,11 +1,11 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import Link from 'next/link';
4
- import { type ReactNode } from 'react';
3
+ import Link from "next/link";
4
+ import { type ReactNode } from "react";
5
5
 
6
- import { cn } from '../lib/cn';
7
- import { Tooltip } from '../../components/ui/Tooltip';
8
- import type { SuiteNavIcon } from './suite-bottom-nav';
6
+ import { cn } from "../lib/cn";
7
+ import { Tooltip } from "../../components/ui/Tooltip";
8
+ import type { SuiteNavIcon } from "./suite-bottom-nav";
9
9
 
10
10
  type CollapsedNode = ReactNode | ((collapsed: boolean) => ReactNode);
11
11
 
@@ -38,8 +38,11 @@ export interface SuiteSidebarProps {
38
38
  surface?: boolean;
39
39
  }
40
40
 
41
- function renderNode(node: CollapsedNode | undefined, collapsed: boolean): ReactNode {
42
- if (typeof node === 'function') return node(collapsed);
41
+ function renderNode(
42
+ node: CollapsedNode | undefined,
43
+ collapsed: boolean,
44
+ ): ReactNode {
45
+ if (typeof node === "function") return node(collapsed);
43
46
  return node;
44
47
  }
45
48
 
@@ -66,42 +69,41 @@ export function SuiteSidebar({
66
69
  return (
67
70
  <div
68
71
  className={cn(
69
- 'flex h-full w-full min-w-0 flex-col text-ph-ink',
70
- surface ? 'bg-ph-surface' : 'bg-ph-canvas',
72
+ "flex h-full w-full min-w-0 flex-col text-ph-ink",
73
+ surface ? "bg-ph-surface" : "bg-ph-canvas",
71
74
  className,
72
75
  )}
73
76
  >
74
77
  {/* Header */}
75
78
  <div
76
79
  className={cn(
77
- 'flex shrink-0 border-b border-ph-border',
78
- surface ? 'bg-ph-surface' : 'bg-ph-canvas',
80
+ "flex shrink-0 border-b border-ph-border",
81
+ surface ? "bg-ph-surface" : "bg-ph-canvas",
79
82
  collapsed
80
- ? 'flex-col items-center gap-1 px-1 py-2'
81
- : 'h-14 items-center justify-between gap-2 px-3',
83
+ ? "flex-col items-center gap-1 px-1 py-2"
84
+ : "h-14 items-center justify-between gap-2 px-3",
82
85
  )}
83
86
  >
84
- <div className={cn('min-w-0', collapsed ? 'shrink-0' : 'flex-1')}>
87
+ <div className={cn("min-w-0", collapsed ? "shrink-0" : "flex-1")}>
85
88
  {renderNode(appSwitcher, collapsed)}
86
89
  </div>
87
90
  {collapsed ? null : (
88
- <div className="shrink-0">{renderNode(notificationBell, collapsed)}</div>
91
+ <div className="shrink-0">
92
+ {renderNode(notificationBell, collapsed)}
93
+ </div>
89
94
  )}
90
95
  </div>
91
96
 
92
97
  {/* Nav body */}
93
98
  <div
94
99
  className={cn(
95
- 'flex min-h-0 flex-1 flex-col overflow-y-auto',
96
- surface ? 'bg-ph-surface' : 'bg-ph-canvas',
97
- collapsed ? 'px-1.5 py-2' : 'p-2',
100
+ "flex min-h-0 flex-1 flex-col overflow-y-auto",
101
+ surface ? "bg-ph-surface" : "bg-ph-canvas",
102
+ collapsed ? "px-1.5 py-2" : "p-2",
98
103
  )}
99
104
  >
100
- <nav
101
- aria-label="Pages"
102
- className="shrink-0 space-y-0.5"
103
- >
104
- <div className={cn('mb-3', collapsed && 'flex justify-center')}>
105
+ <nav aria-label="Pages" className="shrink-0 space-y-0.5">
106
+ <div className={cn("mb-3", collapsed && "flex justify-center")}>
105
107
  {renderNode(contextSwitcher, collapsed)}
106
108
  </div>
107
109
 
@@ -114,21 +116,21 @@ export function SuiteSidebar({
114
116
  key={item.label}
115
117
  href={item.href}
116
118
  data-test="nav-link"
117
- aria-current={active ? 'page' : undefined}
119
+ aria-current={active ? "page" : undefined}
118
120
  onClick={item.onClick}
119
121
  className={cn(
120
- 'group relative flex items-center gap-2.5 rounded-[var(--ph-radius-app)] text-sm font-medium transition-colors',
122
+ "group relative flex items-center gap-2.5 rounded-[var(--ph-radius-app)] text-sm font-medium transition-colors",
121
123
  active
122
- ? 'bg-ph-muted'
123
- : 'text-ph-subtle hover:bg-ph-muted hover:text-ph-ink',
124
+ ? "bg-ph-muted"
125
+ : "text-ph-subtle hover:bg-ph-muted hover:text-ph-ink",
124
126
  collapsed
125
- ? 'mx-auto size-10 shrink-0 justify-center gap-0 px-0 py-0'
126
- : 'w-full px-2.5 py-2',
127
+ ? "mx-auto size-10 shrink-0 justify-center gap-0 px-0 py-0"
128
+ : "w-full px-2.5 py-2",
127
129
  )}
128
130
  >
129
131
  <span
130
132
  className={cn(
131
- 'relative flex h-4 w-4 shrink-0 items-center justify-center',
133
+ "relative flex h-4 w-4 shrink-0 items-center justify-center",
132
134
  item.iconClassName,
133
135
  )}
134
136
  aria-hidden
@@ -140,15 +142,19 @@ export function SuiteSidebar({
140
142
  </span>
141
143
  <span
142
144
  className={cn(
143
- 'relative truncate',
144
- active ? 'text-ph-ink' : 'text-ph-subtle group-hover:text-ph-ink',
145
- collapsed && 'sr-only',
145
+ "relative truncate",
146
+ active
147
+ ? "text-ph-ink"
148
+ : "text-ph-subtle group-hover:text-ph-ink",
149
+ collapsed && "sr-only",
146
150
  )}
147
151
  >
148
152
  {item.label}
149
153
  </span>
150
154
  {!collapsed && item.badge ? (
151
- <span className="relative ml-auto shrink-0">{item.badge}</span>
155
+ <span className="relative ml-auto shrink-0">
156
+ {item.badge}
157
+ </span>
152
158
  ) : null}
153
159
  </Link>
154
160
  );
@@ -176,20 +182,22 @@ export function SuiteSidebar({
176
182
  {/* Footer */}
177
183
  <div
178
184
  className={cn(
179
- 'shrink-0 border-t border-ph-border',
180
- surface ? 'bg-ph-surface' : 'bg-ph-canvas',
181
- collapsed ? 'p-1.5' : 'p-3',
185
+ "shrink-0 border-t border-ph-border",
186
+ surface ? "bg-ph-surface" : "bg-ph-canvas",
187
+ collapsed ? "p-1.5" : "p-3",
182
188
  )}
183
189
  >
184
190
  <div
185
191
  className={cn(
186
- 'flex items-center gap-2',
187
- collapsed && 'justify-center',
192
+ "flex items-center gap-2",
193
+ collapsed ? "flex-col justify-center" : "min-w-0",
188
194
  )}
189
195
  >
190
196
  {renderNode(userMenu, collapsed)}
191
- {!collapsed && footerExtras ? (
192
- <div className="ml-auto shrink-0">{renderNode(footerExtras, collapsed)}</div>
197
+ {footerExtras ? (
198
+ <div className={cn("shrink-0", collapsed ? "mt-2" : "ml-auto")}>
199
+ {renderNode(footerExtras, collapsed)}
200
+ </div>
193
201
  ) : null}
194
202
  </div>
195
203
  </div>
@@ -1,11 +1,11 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import Image from 'next/image';
4
- import { useRouter } from 'next/navigation';
5
- import { LogOut, Settings } from 'iconoir-react';
3
+ import Image from "next/image";
4
+ import { useRouter } from "next/navigation";
5
+ import { LogOut, Settings } from "iconoir-react";
6
6
 
7
- import { cn } from '../lib/cn';
8
- import { DropdownMenu, DropdownItem } from '../../components/ui/DropdownMenu';
7
+ import { cn } from "../lib/cn";
8
+ import { DropdownMenu, DropdownItem } from "../../components/ui/DropdownMenu";
9
9
 
10
10
  export interface SuiteUserMenuProps {
11
11
  name?: string | null;
@@ -16,26 +16,30 @@ export interface SuiteUserMenuProps {
16
16
  onSignOut: () => void;
17
17
  /** Fallback letter(s) when there is no avatar image. */
18
18
  fallbackInitials?: string;
19
+ /** Show the old desktop sidebar sign-out action beside the avatar. */
20
+ showSignOutAction?: boolean;
19
21
  dataTest?: string;
20
22
  className?: string;
21
23
  }
22
24
 
23
- function computeInitials(name: string | null | undefined, email: string | null | undefined): string {
24
- const source = (name || email || '?').trim();
25
+ function computeInitials(
26
+ name: string | null | undefined,
27
+ email: string | null | undefined,
28
+ ): string {
29
+ const source = (name || email || "?").trim();
25
30
  return source
26
31
  .split(/[\s@]+/)
27
32
  .filter(Boolean)
28
33
  .slice(0, 2)
29
34
  .map((part) => part[0])
30
- .join('')
35
+ .join("")
31
36
  .toUpperCase();
32
37
  }
33
38
 
34
39
  /**
35
- * Suite account context menu — one tap on the avatar opens a small menu
36
- * with profile settings and sign out. Used in the mobile app bar of every
37
- * app so account actions behave identically across the suite (no dedicated
38
- * logout icon cluttering the mobile chrome).
40
+ * Suite account context menu — one tap on the avatar opens a small menu with
41
+ * profile settings and sign out. Desktop sidebars can also expose the legacy
42
+ * adjacent sign-out action without adding it to mobile chrome.
39
43
  */
40
44
  export function SuiteUserMenu({
41
45
  name,
@@ -44,32 +48,33 @@ export function SuiteUserMenu({
44
48
  settingsHref,
45
49
  onSignOut,
46
50
  fallbackInitials,
47
- dataTest = 'suite-user-menu',
51
+ showSignOutAction = false,
52
+ dataTest = "suite-user-menu",
48
53
  className,
49
54
  }: SuiteUserMenuProps) {
50
55
  const router = useRouter();
51
56
  const initials = fallbackInitials ?? computeInitials(name, email);
52
57
 
53
58
  const avatar = (
54
- <span className="inline-flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:bg-ph-muted">
59
+ <span className="inline-flex h-11 w-11 items-center justify-center rounded-full ring-2 ring-ph-border ring-offset-1 ring-offset-ph-canvas transition-colors hover:bg-ph-muted">
55
60
  {image ? (
56
61
  <Image
57
62
  src={image}
58
63
  alt=""
59
- width={32}
60
- height={32}
61
- className="h-8 w-8 rounded-full object-cover"
64
+ width={40}
65
+ height={40}
66
+ className="h-10 w-10 rounded-full object-cover"
62
67
  unoptimized
63
68
  />
64
69
  ) : (
65
- <span className="flex h-8 w-8 items-center justify-center rounded-full bg-ph-muted text-sm font-semibold text-ph-brand">
70
+ <span className="flex h-10 w-10 items-center justify-center rounded-full bg-ph-muted text-sm font-semibold text-ph-brand">
66
71
  {initials}
67
72
  </span>
68
73
  )}
69
74
  </span>
70
75
  );
71
76
 
72
- return (
77
+ const accountMenu = (
73
78
  <DropdownMenu
74
79
  trigger={avatar}
75
80
  aria-label="Account menu"
@@ -78,12 +83,12 @@ export function SuiteUserMenu({
78
83
  sideOffset={6}
79
84
  collisionPadding={16}
80
85
  modal={false}
81
- className={className}
86
+ className={showSignOutAction ? undefined : className}
82
87
  data-test={dataTest}
83
88
  >
84
89
  <div className="px-2 py-1.5">
85
90
  <p className="truncate text-sm font-medium text-ph-ink">
86
- {name || email || 'Account'}
91
+ {name || email || "Account"}
87
92
  </p>
88
93
  {email ? (
89
94
  <p className="truncate text-xs text-ph-mutedtext">{email}</p>
@@ -97,13 +102,30 @@ export function SuiteUserMenu({
97
102
  <Settings className="h-4 w-4 text-ph-mutedtext" aria-hidden />
98
103
  Profile settings
99
104
  </DropdownItem>
100
- <DropdownItem
101
- data-test={`${dataTest}-sign-out`}
102
- onClick={onSignOut}
103
- >
105
+ <DropdownItem data-test={`${dataTest}-sign-out`} onClick={onSignOut}>
104
106
  <LogOut className="h-4 w-4 text-ph-mutedtext" aria-hidden />
105
107
  Sign out
106
108
  </DropdownItem>
107
109
  </DropdownMenu>
108
110
  );
111
+
112
+ if (!showSignOutAction) {
113
+ return accountMenu;
114
+ }
115
+
116
+ return (
117
+ <div className={cn("flex min-w-0 items-center gap-2", className)}>
118
+ {accountMenu}
119
+ <button
120
+ type="button"
121
+ data-test={`${dataTest}-visible-sign-out`}
122
+ title="Sign out"
123
+ aria-label="Sign out"
124
+ className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ph-focus-ring"
125
+ onClick={onSignOut}
126
+ >
127
+ <LogOut className="h-4 w-4" aria-hidden />
128
+ </button>
129
+ </div>
130
+ );
109
131
  }
@@ -3,7 +3,7 @@
3
3
  import type { ReactNode } from "react";
4
4
 
5
5
  /**
6
- * Today page content wrapper with soft atmosphere.
6
+ * Today page content wrapper with a soft themed visual treatment.
7
7
  * Does not own scrolling — `#main-content` is the scrollport so sticky headers work.
8
8
  * Glow/clouds are absolute and do not create a nested scrollport.
9
9
  */
@@ -1,4 +1,4 @@
1
- import type { SuiteIconName } from './glyphs';
1
+ import type { SuiteIconName } from "./glyphs";
2
2
 
3
3
  /**
4
4
  * Per-app brand colours for the suite icon system — the single place hexes
@@ -8,12 +8,7 @@ import type { SuiteIconName } from './glyphs';
8
8
  */
9
9
 
10
10
  export type SuiteAccentSlug =
11
- | 'turtletime'
12
- | 'tides'
13
- | 'kraken'
14
- | 'shellstack'
15
- | 'crew'
16
- | 'nakama';
11
+ "turtletime" | "tides" | "kraken" | "shellstack" | "crew" | "nakama";
17
12
 
18
13
  export interface SuiteAppAccent {
19
14
  /** Display name (default aria label for app marks). */
@@ -30,55 +25,55 @@ export interface SuiteAppAccent {
30
25
 
31
26
  export const APP_ACCENTS: Record<SuiteAccentSlug, SuiteAppAccent> = {
32
27
  turtletime: {
33
- label: 'TurtleTime',
34
- accent: '#3f8f57',
35
- tile: '#173521',
36
- onTile: '#f7ead0',
37
- onTileAccent: '#f3c24e',
28
+ label: "TurtleTime",
29
+ accent: "#3f8f57",
30
+ tile: "#173521",
31
+ onTile: "#f7ead0",
32
+ onTileAccent: "#f3c24e",
38
33
  },
39
34
  tides: {
40
- label: 'Tides',
41
- accent: '#a63bb5',
42
- tile: '#8f2d96',
43
- onTile: '#ffffff',
44
- onTileAccent: '#bfe3ff',
35
+ label: "Tides",
36
+ accent: "#a63bb5",
37
+ tile: "#8f2d96",
38
+ onTile: "#ffffff",
39
+ onTileAccent: "#bfe3ff",
45
40
  },
46
41
  kraken: {
47
- label: 'Kraken',
48
- accent: '#737373',
49
- tile: '#191919',
50
- onTile: '#ffffff',
51
- onTileAccent: '#d4d4d4',
42
+ label: "Kraken",
43
+ accent: "#737373",
44
+ tile: "#191919",
45
+ onTile: "#ffffff",
46
+ onTileAccent: "#d4d4d4",
52
47
  },
53
48
  shellstack: {
54
- label: 'ShellStack',
55
- accent: '#5e5893',
56
- tile: '#5e5893',
57
- onTile: '#ffffff',
58
- onTileAccent: '#c4a7e7',
49
+ label: "ShellStack",
50
+ accent: "#5e5893",
51
+ tile: "#5e5893",
52
+ onTile: "#ffffff",
53
+ onTileAccent: "#c4a7e7",
59
54
  },
60
55
  crew: {
61
- label: 'Crew',
62
- accent: '#0891b2',
63
- tile: '#155e75',
64
- onTile: '#ffffff',
65
- onTileAccent: '#a5f3fc',
56
+ label: "Crew",
57
+ accent: "#0891b2",
58
+ tile: "#155e75",
59
+ onTile: "#ffffff",
60
+ onTileAccent: "#a5f3fc",
66
61
  },
67
62
  nakama: {
68
- label: 'Nakama',
69
- accent: '#0891b2',
70
- tile: '#155e75',
71
- onTile: '#ffffff',
72
- onTileAccent: '#a5f3fc',
63
+ label: "Nakama",
64
+ accent: "#b8eb44",
65
+ tile: "#17101f",
66
+ onTile: "#fff2e2",
67
+ onTileAccent: "#f54b87",
73
68
  },
74
69
  };
75
70
 
76
71
  /** Which glyph represents each app. */
77
72
  export const APP_GLYPHS: Record<SuiteAccentSlug, SuiteIconName> = {
78
- turtletime: 'timer-shell',
79
- tides: 'kanban-wave',
80
- kraken: 'squid-doc',
81
- shellstack: 'stack-hex',
82
- crew: 'crew-bot',
83
- nakama: 'crew-bot',
73
+ turtletime: "timer-shell",
74
+ tides: "kanban-wave",
75
+ kraken: "squid-doc",
76
+ shellstack: "stack-hex",
77
+ crew: "crew-bot",
78
+ nakama: "nakama-fist",
84
79
  };
@@ -1,14 +1,21 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import type { SuiteGlyphElement } from './glyphs';
3
+ import type { SuiteGlyphElement } from "./glyphs";
4
4
 
5
5
  function GlyphElement({ element }: { element: SuiteGlyphElement }) {
6
- const fill = element.filled ? 'currentColor' : 'none';
7
- const stroke = element.filled ? 'none' : undefined;
6
+ const fill = element.filled ? "currentColor" : "none";
7
+ const stroke = element.filled ? "none" : undefined;
8
8
  switch (element.kind) {
9
- case 'path':
10
- return <path d={element.d} fill={fill} stroke={stroke} />;
11
- case 'circle':
9
+ case "path":
10
+ return (
11
+ <path
12
+ d={element.d}
13
+ fill={fill}
14
+ stroke={stroke}
15
+ transform={element.transform}
16
+ />
17
+ );
18
+ case "circle":
12
19
  return (
13
20
  <circle
14
21
  cx={element.cx}
@@ -18,7 +25,7 @@ function GlyphElement({ element }: { element: SuiteGlyphElement }) {
18
25
  stroke={stroke}
19
26
  />
20
27
  );
21
- case 'rect':
28
+ case "rect":
22
29
  return (
23
30
  <rect
24
31
  x={element.x}
@@ -55,7 +62,7 @@ export function GlyphParts({
55
62
  {accented.length > 0 ? (
56
63
  <g
57
64
  className={accentClassName}
58
- style={{ color: 'var(--suite-icon-accent, currentColor)' }}
65
+ style={{ color: "var(--suite-icon-accent, currentColor)" }}
59
66
  >
60
67
  {accented.map((el, i) => (
61
68
  <GlyphElement key={i} element={el} />