jamdesk 1.1.208 → 1.1.210
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/dist/__tests__/unit/deps-sync.test.js +11 -10
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/dist/__tests__/unit/deps.test.js +30 -0
- package/dist/__tests__/unit/deps.test.js.map +1 -1
- package/dist/__tests__/unit/vendored-sync.test.js +2 -2
- package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
- package/dist/lib/deps.d.ts +2 -1
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +20 -2
- package/dist/lib/deps.js.map +1 -1
- package/package.json +1 -1
- package/vendored/app/layout.tsx +3 -2
- package/vendored/components/navigation/Header.tsx +0 -7
- package/vendored/components/navigation/Sidebar.tsx +14 -0
- package/vendored/components/navigation/ThemePreviewChips.tsx +121 -0
- package/vendored/components/theme/ThemeToggle.tsx +8 -1
- package/vendored/lib/theme-preview-client.ts +72 -0
- package/vendored/lib/theme-preview.ts +41 -31
- package/vendored/themes/jam/variables.css +9 -0
- package/vendored/themes/nebula/variables.css +19 -14
- package/vendored/themes/pulsar/variables.css +11 -2
- package/vendored/workspace-package-lock.json +29 -7
- package/vendored/components/navigation/ThemePreviewPicker.tsx +0 -197
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
5
|
-
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
6
|
-
import { useThemePreview } from '@/lib/theme-preview-context';
|
|
7
|
-
import { THEME_PREVIEW_COOKIE } from '@/lib/theme-preview';
|
|
8
|
-
import { getAllThemes, type ThemeName } from '@/themes';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Jamdesk-only affordance: re-skins jamdesk.com/docs with any built-in theme so
|
|
12
|
-
* prospects can see all four on a real docs site.
|
|
13
|
-
*
|
|
14
|
-
* The gate is server-side (lib/theme-preview.ts decides whether the provider is
|
|
15
|
-
* enabled); this component ALSO self-guards on `enabled` so a mistake in
|
|
16
|
-
* Header's conditional can never leak it onto a customer site.
|
|
17
|
-
*
|
|
18
|
-
* Strings are English-only by design. jamdesk-docs has fr/es locales, but the
|
|
19
|
-
* only prose here is "Reset to default" and "Default" — everything else is a
|
|
20
|
-
* proper noun — and adding entries to lib/ui-strings.ts for a sales affordance
|
|
21
|
-
* isn't worth the churn across every locale.
|
|
22
|
-
*/
|
|
23
|
-
export function ThemePreviewPicker() {
|
|
24
|
-
const { enabled, activeTheme, defaultTheme } = useThemePreview();
|
|
25
|
-
const linkPrefix = useLinkPrefix();
|
|
26
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
27
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
28
|
-
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
29
|
-
const listRef = useRef<HTMLUListElement>(null);
|
|
30
|
-
|
|
31
|
-
// Every close path but a theme selection runs through here. A click-outside
|
|
32
|
-
// would otherwise unmount the focused option and drop focus to <body>, so
|
|
33
|
-
// the visitor's next Tab restarts at the top of the document. Synchronous on
|
|
34
|
-
// purpose: this runs during the mousedown dispatch, so a click that lands on
|
|
35
|
-
// something focusable still ends up there — the browser's own focus default
|
|
36
|
-
// wins afterwards, which is what it should do.
|
|
37
|
-
function closeAndRestoreFocus() {
|
|
38
|
-
const hadFocus = !!containerRef.current?.contains(document.activeElement);
|
|
39
|
-
setIsOpen(false);
|
|
40
|
-
if (hadFocus) buttonRef.current?.focus();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
useOnClickOutside(containerRef, closeAndRestoreFocus, isOpen);
|
|
44
|
-
|
|
45
|
-
// Move focus onto the active option when the menu opens. Without this the
|
|
46
|
-
// trigger keeps focus and Tab is the only way forward — and an earlier cut of
|
|
47
|
-
// this component closed the menu on Tab (copied from LanguageSelector, where
|
|
48
|
-
// that is safe only because ArrowUp/Down have already moved focus into the
|
|
49
|
-
// list), which left the widget keyboard-inoperable: WCAG 2.1.1.
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!isOpen) return;
|
|
52
|
-
const options = listRef.current?.querySelectorAll<HTMLElement>('[role="option"]');
|
|
53
|
-
if (!options?.length) return;
|
|
54
|
-
const active = Array.from(options).find((o) => o.getAttribute('aria-selected') === 'true');
|
|
55
|
-
(active ?? options[0]).focus();
|
|
56
|
-
}, [isOpen]);
|
|
57
|
-
|
|
58
|
-
if (!enabled) return null;
|
|
59
|
-
|
|
60
|
-
// Escape only. Arrow-key roving focus is deliberately NOT copied from
|
|
61
|
-
// LanguageSelector: its options are a customer-configurable list, whereas
|
|
62
|
-
// this is a fixed four-row Jamdesk-only affordance whose rows are ordinary
|
|
63
|
-
// buttons that Tab already walks. Escape must stay — it is the way back out
|
|
64
|
-
// of a menu focus has just been moved into.
|
|
65
|
-
function handleKeyDown(e: React.KeyboardEvent) {
|
|
66
|
-
if (e.key === 'Escape') closeAndRestoreFocus();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Tabbing past the last row leaves the widget; close behind it. This is what
|
|
70
|
-
// LanguageSelector's Tab branch achieved, without eating the Tab that is this
|
|
71
|
-
// component's only way INTO the option list.
|
|
72
|
-
function handleFocusOut(e: React.FocusEvent) {
|
|
73
|
-
// A null relatedTarget means focus landed on <body>, NOT that focus left
|
|
74
|
-
// the widget — and since the menu always opens with focus inside itself,
|
|
75
|
-
// that blur comes from inside. Safari and Firefox-on-macOS produce it on
|
|
76
|
-
// every mousedown over a <button>; Chrome produces it for a mousedown on
|
|
77
|
-
// this panel's own padding or its divider band. Closing there unmounts the
|
|
78
|
-
// row before its click can land, so no theme is ever applied. Do not
|
|
79
|
-
// "simplify" this branch away: genuine outside-pointer closes are
|
|
80
|
-
// useOnClickOutside's job, and Escape and Tab both supply a real
|
|
81
|
-
// relatedTarget. The only thing given up is close-on-window-blur.
|
|
82
|
-
if (!(e.relatedTarget instanceof Element)) return;
|
|
83
|
-
if (!e.currentTarget.contains(e.relatedTarget)) setIsOpen(false);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// `null` clears the cookie. Path mirrors the docs prefix so the cookie is
|
|
87
|
-
// scoped to /docs on jamdesk.com and to / on jamdesk-docs.jamdesk.app.
|
|
88
|
-
function applyTheme(name: ThemeName | null) {
|
|
89
|
-
const path = linkPrefix || '/';
|
|
90
|
-
// SESSION cookie on the set path — no Max-Age, so the preview dies with the
|
|
91
|
-
// browser. The picker is desktop-only (`hidden lg:block`), so a persisted
|
|
92
|
-
// preview would follow a visitor to mobile with no affordance to reset it.
|
|
93
|
-
// The CLEAR path is the opposite and must stay explicit: `Max-Age=0` is
|
|
94
|
-
// what expires an existing cookie, and omitting it there would leave the
|
|
95
|
-
// preview in place.
|
|
96
|
-
const expiry = name === null ? '; Max-Age=0' : '';
|
|
97
|
-
// `Secure` means the browser silently DROPS this cookie over plain http, so
|
|
98
|
-
// the picker looks inert when QA'd on http://localhost — use https or a
|
|
99
|
-
// deployed host.
|
|
100
|
-
document.cookie =
|
|
101
|
-
`${THEME_PREVIEW_COOKIE}=${name ?? ''}; Path=${path}${expiry}; SameSite=Lax; Secure`;
|
|
102
|
-
// Close first: the reload is not instant, and an open menu left showing the
|
|
103
|
-
// previous selection reads as a dead click.
|
|
104
|
-
setIsOpen(false);
|
|
105
|
-
// Full reload — the theme is applied by the server render (CSS variables,
|
|
106
|
-
// font class and layout variant all key off config.theme), so there is no
|
|
107
|
-
// client-side state to update.
|
|
108
|
-
window.location.reload();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const allThemes = getAllThemes();
|
|
112
|
-
const activeDisplayName = allThemes.find((t) => t.name === activeTheme)?.displayName ?? '';
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div
|
|
116
|
-
ref={containerRef}
|
|
117
|
-
className="relative hidden lg:block"
|
|
118
|
-
onKeyDown={handleKeyDown}
|
|
119
|
-
onBlur={handleFocusOut}
|
|
120
|
-
>
|
|
121
|
-
<button
|
|
122
|
-
ref={buttonRef}
|
|
123
|
-
onClick={() => {
|
|
124
|
-
setIsOpen(!isOpen);
|
|
125
|
-
// Closing unmounts the focused option. Safari does not focus a
|
|
126
|
-
// button on mousedown, so without this focus would land on <body>.
|
|
127
|
-
// Mirrors Prompt.tsx's trigger.
|
|
128
|
-
if (isOpen) buttonRef.current?.focus();
|
|
129
|
-
}}
|
|
130
|
-
aria-expanded={isOpen}
|
|
131
|
-
aria-haspopup="listbox"
|
|
132
|
-
aria-label={`Preview a theme: ${activeDisplayName}`}
|
|
133
|
-
className="flex items-center gap-2 rounded-lg px-2 py-1.5 cursor-pointer transition-colors text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)]"
|
|
134
|
-
>
|
|
135
|
-
<i
|
|
136
|
-
className="fa-solid fa-palette text-[12px] text-[var(--color-text-tertiary)]"
|
|
137
|
-
aria-hidden="true"
|
|
138
|
-
/>
|
|
139
|
-
<i
|
|
140
|
-
className={`fa-solid fa-chevron-down text-[10px] text-[var(--color-text-tertiary)] transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
141
|
-
aria-hidden="true"
|
|
142
|
-
/>
|
|
143
|
-
</button>
|
|
144
|
-
|
|
145
|
-
{isOpen && (
|
|
146
|
-
<div
|
|
147
|
-
className="absolute right-0 top-full mt-1 z-50 min-w-[190px] py-1 bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-lg"
|
|
148
|
-
style={{ boxShadow: 'var(--shadow-lg)' }}
|
|
149
|
-
>
|
|
150
|
-
<ul ref={listRef} role="listbox" aria-label="Preview a theme">
|
|
151
|
-
{allThemes.map((theme) => {
|
|
152
|
-
const isActive = theme.name === activeTheme;
|
|
153
|
-
return (
|
|
154
|
-
<li key={theme.name}>
|
|
155
|
-
<button
|
|
156
|
-
role="option"
|
|
157
|
-
aria-selected={isActive}
|
|
158
|
-
onClick={() => applyTheme(theme.name as ThemeName)}
|
|
159
|
-
className="w-full flex items-center justify-between gap-3 px-3 py-2 text-left text-sm cursor-pointer text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)]"
|
|
160
|
-
>
|
|
161
|
-
<span>{theme.displayName}</span>
|
|
162
|
-
<span className="flex items-center gap-2">
|
|
163
|
-
{theme.name === defaultTheme && (
|
|
164
|
-
<span className="text-xs text-[var(--color-text-tertiary)]">Default</span>
|
|
165
|
-
)}
|
|
166
|
-
{isActive && (
|
|
167
|
-
<i
|
|
168
|
-
className="fa-solid fa-check text-[11px] text-[var(--color-accent)]"
|
|
169
|
-
aria-hidden="true"
|
|
170
|
-
/>
|
|
171
|
-
)}
|
|
172
|
-
</span>
|
|
173
|
-
</button>
|
|
174
|
-
</li>
|
|
175
|
-
);
|
|
176
|
-
})}
|
|
177
|
-
</ul>
|
|
178
|
-
|
|
179
|
-
{/* Outside the <ul>: a `role="listbox"` may only own `option` and
|
|
180
|
-
`group` children, and a reset action is neither. Nesting it as a
|
|
181
|
-
bare <li><button> would put a `listitem` inside a listbox, which
|
|
182
|
-
screen readers drop or mis-announce. LanguageSelector — the
|
|
183
|
-
pattern this component mirrors — has no extra row, so there is no
|
|
184
|
-
existing precedent to copy here. */}
|
|
185
|
-
<div className="mt-1 pt-1 border-t border-[var(--color-border)]">
|
|
186
|
-
<button
|
|
187
|
-
onClick={() => applyTheme(null)}
|
|
188
|
-
className="w-full px-3 py-2 text-left text-sm cursor-pointer text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-tertiary)]"
|
|
189
|
-
>
|
|
190
|
-
Reset to default
|
|
191
|
-
</button>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
)}
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|