Skip to content

Commit de6480d

Browse files
committed
refactor(app): organize into components and pages structure
- Extract PageLayout, ThemeSelector, PluginDebugPanel to components/ - Extract Shell, SettingsPage to pages/ - Export common components via components/index.ts for plugin reuse - Reduce App.tsx from 456 to 208 lines - Improve code organization and maintainability
1 parent deed6b3 commit de6480d

File tree

7 files changed

+344
-251
lines changed

7 files changed

+344
-251
lines changed

examples/app/src/App.tsx

Lines changed: 3 additions & 251 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ import {
77
SidebarSlotProvider,
88
DashboardSlotProvider,
99
SettingsSlotProvider,
10-
useAppLayout,
11-
AppHeader,
12-
AppSidebar,
13-
AppDashboard,
1410
} from 'example-sdk';
15-
import { AppPluginProvider, useAppPluginMeta, PluginEntrypoints } from 'example-sdk/react';
11+
import { AppPluginProvider, PluginEntrypoints } from 'example-sdk/react';
1612
import { createMockAppContext } from './mock-context.js';
1713
import { LocalStoragePluginProvider } from './local-storage-adapter.js';
14+
import { Shell } from './pages/Shell.js';
15+
import { SettingsPage } from './pages/SettingsPage.js';
1816
import type { PluginHost } from '@react-pkl/core';
1917
import type { AppContext, PluginRoute } from 'example-sdk';
2018

@@ -207,249 +205,3 @@ function NotFoundPage() {
207205
</div>
208206
);
209207
}
210-
211-
// ---------------------------------------------------------------------------
212-
// PageLayout – shared layout wrapper for all pages
213-
// ---------------------------------------------------------------------------
214-
215-
function PageLayout({
216-
host,
217-
pluginRoutes,
218-
currentPath,
219-
children
220-
}: {
221-
host: PluginHost<AppContext>;
222-
pluginRoutes: Map<string, PluginRoute>;
223-
currentPath: string;
224-
children: React.ReactNode;
225-
}) {
226-
const layout = useAppLayout();
227-
228-
return (
229-
<div style={{ fontFamily: 'system-ui, sans-serif', padding: 24 }}>
230-
{/* Toolbar - themeable layout slot */}
231-
<AppHeader toolbar={layout.toolbar} />
232-
233-
<div style={{ display: 'flex', gap: 24 }}>
234-
{/* Sidebar - themeable layout slot */}
235-
<AppSidebar pluginRoutes={pluginRoutes} sidebarItems={layout.sidebar} Link={Link} />
236-
237-
{/* Main content */}
238-
<main style={{ flex: 1 }}>
239-
{children}
240-
</main>
241-
</div>
242-
</div>
243-
);
244-
}
245-
246-
// ---------------------------------------------------------------------------
247-
// Shell – home page
248-
// ---------------------------------------------------------------------------
249-
250-
function Shell({ host, pluginRoutes }: { host: PluginHost<AppContext>; pluginRoutes: Map<string, PluginRoute> }) {
251-
const layout = useAppLayout();
252-
253-
return (
254-
<PageLayout host={host} pluginRoutes={pluginRoutes} currentPath="/">
255-
<h2 style={{ marginTop: 0, color: 'var(--text-primary, inherit)' }}>Dashboard</h2>
256-
257-
{/* Dashboard - themeable layout slot */}
258-
<AppDashboard dashboardItems={layout.dashboard} />
259-
260-
<PluginDebugPanel host={host} />
261-
</PageLayout>
262-
);
263-
}
264-
265-
// ---------------------------------------------------------------------------
266-
// Debug panel – shows registered plugins and lets you toggle them
267-
// ---------------------------------------------------------------------------
268-
269-
function PluginDebugPanel({
270-
host,
271-
}: {
272-
host: PluginHost<AppContext>;
273-
}) {
274-
const meta = useAppPluginMeta();
275-
const [, forceUpdate] = useState(0);
276-
const manager = host.getManager();
277-
278-
return (
279-
<section
280-
style={{
281-
marginTop: 32,
282-
padding: 16,
283-
border: '1px dashed #cbd5e1',
284-
borderRadius: 8,
285-
}}
286-
>
287-
<h3 style={{ marginTop: 0, fontSize: 14, color: '#475569' }}>
288-
Registered plugins ({meta.length})
289-
</h3>
290-
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 8 }}>
291-
{meta.map((m) => (
292-
<li key={m.id} style={{ display: 'flex', gap: 12, alignItems: 'center', fontSize: 13 }}>
293-
<span style={{ fontWeight: 500 }}>{m.name}</span>
294-
<span style={{ color: '#94a3b8' }}>v{m.version}</span>
295-
<button
296-
style={{ marginLeft: 'auto', fontSize: 12, cursor: 'pointer' }}
297-
onClick={async () => {
298-
const entry = manager.getAll().find((e: import('@react-pkl/core').PluginEntry<AppContext>) => e.module.meta.id === m.id);
299-
if (!entry) return;
300-
if (entry.status === 'enabled') {
301-
await manager.disable(m.id);
302-
} else {
303-
await manager.enable(m.id);
304-
}
305-
forceUpdate((v) => v + 1);
306-
}}
307-
>
308-
{manager.getAll().find((e: import('@react-pkl/core').PluginEntry<AppContext>) => e.module.meta.id === m.id)?.status === 'enabled'
309-
? 'Disable'
310-
: 'Enable'}
311-
</button>
312-
</li>
313-
))}
314-
</ul>
315-
</section>
316-
);
317-
}
318-
319-
// ---------------------------------------------------------------------------
320-
// ThemeSelector – dropdown to select active theme plugin
321-
// ---------------------------------------------------------------------------
322-
323-
function ThemeSelector({ host }: { host: PluginHost<AppContext> }) {
324-
const manager = host.getManager();
325-
const registry = host.getRegistry();
326-
const [currentTheme, setCurrentTheme] = useState(() => host.getThemePlugin());
327-
328-
// Subscribe to host changes (theme changes trigger re-render)
329-
useEffect(() => {
330-
const unsubscribe = host.subscribe(() => {
331-
setCurrentTheme(host.getThemePlugin());
332-
});
333-
return unsubscribe;
334-
}, [host]);
335-
336-
// Get all plugins (enabled + static) that have onThemeEnable (theme plugins)
337-
// Static plugins don't need to be "enabled" to be used as themes
338-
const themePlugins = registry
339-
.getAll()
340-
.map(entry => entry.module)
341-
.filter(plugin => typeof plugin.onThemeEnable === 'function');
342-
343-
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
344-
const selectedId = e.target.value;
345-
346-
// Save theme preference to localStorage
347-
localStorage.setItem('react-pkl:active-theme', selectedId);
348-
349-
if (selectedId === 'default') {
350-
host.setThemePlugin(null);
351-
} else {
352-
const plugin = themePlugins.find(p => p.meta.id === selectedId);
353-
if (plugin) {
354-
host.setThemePlugin(plugin);
355-
}
356-
}
357-
};
358-
359-
return (
360-
<section
361-
style={{
362-
padding: 16,
363-
background: 'var(--card-bg, #f8fafc)',
364-
borderRadius: 8,
365-
border: '1px solid var(--border-color, #e2e8f0)',
366-
}}
367-
>
368-
<h3 style={{ marginTop: 0, fontSize: 16, color: 'var(--text-primary, inherit)' }}>
369-
🎨 Theme
370-
</h3>
371-
<p style={{ color: 'var(--text-secondary, #64748b)', fontSize: 14, marginBottom: 12 }}>
372-
Select a theme to customize the appearance of the application
373-
</p>
374-
375-
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
376-
<label
377-
htmlFor="theme-select"
378-
style={{
379-
fontSize: 14,
380-
fontWeight: 500,
381-
color: 'var(--text-primary, inherit)',
382-
}}
383-
>
384-
Active Theme:
385-
</label>
386-
<select
387-
id="theme-select"
388-
value={currentTheme?.meta.id || 'default'}
389-
onChange={handleThemeChange}
390-
style={{
391-
padding: '8px 12px',
392-
borderRadius: 6,
393-
border: '1px solid var(--border-color, #cbd5e1)',
394-
background: 'var(--bg-primary, white)',
395-
color: 'var(--text-primary, inherit)',
396-
fontSize: 14,
397-
cursor: 'pointer',
398-
minWidth: 200,
399-
}}
400-
>
401-
<option value="default">Default (Light)</option>
402-
{themePlugins.map(plugin => (
403-
<option key={plugin.meta.id} value={plugin.meta.id}>
404-
{plugin.meta.name}
405-
</option>
406-
))}
407-
</select>
408-
</div>
409-
410-
{themePlugins.length === 0 && (
411-
<p style={{ color: 'var(--text-muted, #94a3b8)', fontSize: 13, marginTop: 12, marginBottom: 0 }}>
412-
No theme plugins available. Enable a theme plugin to customize the appearance.
413-
</p>
414-
)}
415-
416-
{currentTheme && (
417-
<p style={{ color: 'var(--text-secondary, #64748b)', fontSize: 13, marginTop: 12, marginBottom: 0 }}>
418-
<strong>{currentTheme.meta.name}</strong> - {currentTheme.meta.description}
419-
</p>
420-
)}
421-
</section>
422-
);
423-
}
424-
425-
// ---------------------------------------------------------------------------
426-
// ---------------------------------------------------------------------------
427-
// SettingsPage – accessible via /settings route
428-
// ---------------------------------------------------------------------------
429-
430-
function SettingsPage({ host, pluginRoutes }: { host: PluginHost<AppContext>; pluginRoutes: Map<string, PluginRoute> }) {
431-
const layout = useAppLayout();
432-
433-
return (
434-
<PageLayout host={host} pluginRoutes={pluginRoutes} currentPath="/settings">
435-
<h2 style={{ marginTop: 0, color: 'var(--text-primary, inherit)' }}>Application Settings</h2>
436-
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
437-
{/* Theme Selection */}
438-
<ThemeSelector host={host} />
439-
440-
<section style={{ padding: 16, background: 'var(--card-bg, #f8fafc)', borderRadius: 8 }}>
441-
<h3 style={{ marginTop: 0, fontSize: 16, color: 'var(--text-primary, inherit)' }}>General</h3>
442-
<p style={{ color: 'var(--text-secondary, #64748b)', fontSize: 14 }}>Basic application settings</p>
443-
</section>
444-
445-
{/* Plugin settings sections */}
446-
{layout.settings.length === 0 ? (
447-
<p style={{ color: 'var(--text-muted, #94a3b8)', fontSize: 14 }}>No plugin settings available.</p>
448-
) : (
449-
layout.settings
450-
)}
451-
</div>
452-
</PageLayout>
453-
);
454-
}
455-
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { PluginHost } from '@react-pkl/core';
2+
import type { AppContext, PluginRoute } from 'example-sdk';
3+
import { useAppLayout, AppHeader, AppSidebar } from 'example-sdk';
4+
import { Link } from 'react-router-dom';
5+
6+
/**
7+
* PageLayout - Shared layout wrapper for all pages
8+
*
9+
* Provides consistent header and sidebar layout with themeable components.
10+
* Used by both the home page (Shell) and settings page.
11+
*/
12+
export function PageLayout({
13+
host,
14+
pluginRoutes,
15+
currentPath,
16+
children
17+
}: {
18+
host: PluginHost<AppContext>;
19+
pluginRoutes: Map<string, PluginRoute>;
20+
currentPath: string;
21+
children: React.ReactNode;
22+
}) {
23+
const layout = useAppLayout();
24+
25+
return (
26+
<div style={{ fontFamily: 'system-ui, sans-serif', padding: 24 }}>
27+
{/* Toolbar - themeable layout slot */}
28+
<AppHeader toolbar={layout.toolbar} />
29+
30+
<div style={{ display: 'flex', gap: 24 }}>
31+
{/* Sidebar - themeable layout slot */}
32+
<AppSidebar pluginRoutes={pluginRoutes} sidebarItems={layout.sidebar} Link={Link} />
33+
34+
{/* Main content */}
35+
<main style={{ flex: 1 }}>
36+
{children}
37+
</main>
38+
</div>
39+
</div>
40+
);
41+
}

0 commit comments

Comments
 (0)