design-folio 0.1.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.
- package/README.md +109 -0
- package/package.json +45 -0
- package/src/config/defaults.js +19 -0
- package/src/config/load-config.js +20 -0
- package/src/hooks/useHashRoute.js +93 -0
- package/src/hooks/usePanZoom.js +156 -0
- package/src/hooks/useTheme.js +21 -0
- package/src/icons.jsx +261 -0
- package/src/index.js +1 -0
- package/src/plugin/vite-plugin.js +57 -0
- package/src/styles/folio.css +3 -0
- package/src/viewer/ChangelogTab.jsx +38 -0
- package/src/viewer/DesignNotes.jsx +18 -0
- package/src/viewer/DocumentsTab.jsx +76 -0
- package/src/viewer/FolioViewer.jsx +140 -0
- package/src/viewer/GalleryView.jsx +103 -0
- package/src/viewer/Header.jsx +30 -0
- package/src/viewer/PrototypeTab.jsx +20 -0
- package/src/viewer/ScreenFrame.jsx +36 -0
- package/src/viewer/ScreensTab.jsx +158 -0
- package/src/viewer/ScreensToolbar.jsx +134 -0
- package/src/viewer/Sidebar.jsx +106 -0
- package/src/viewer/SimulatedWindow.jsx +47 -0
- package/src/viewer/StatePicker.jsx +83 -0
- package/src/viewer/TabNav.jsx +40 -0
- package/src/viewer/ThemeToggle.jsx +23 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { LayersIcon } from '../icons.jsx';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Groups an array of state objects by their `group` field.
|
|
5
|
+
* Preserves the order groups are first encountered.
|
|
6
|
+
* States without a `group` field are placed under 'Ungrouped'.
|
|
7
|
+
*
|
|
8
|
+
* @param {Array} states
|
|
9
|
+
* @returns {Array<{ group: string, states: Array }>}
|
|
10
|
+
*/
|
|
11
|
+
export function groupStates(states) {
|
|
12
|
+
const order = [];
|
|
13
|
+
const map = {};
|
|
14
|
+
|
|
15
|
+
for (const s of states) {
|
|
16
|
+
const group = s.group || 'Ungrouped';
|
|
17
|
+
if (!map[group]) {
|
|
18
|
+
map[group] = [];
|
|
19
|
+
order.push(group);
|
|
20
|
+
}
|
|
21
|
+
map[group].push(s);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return order.map((group) => ({ group, states: map[group] }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* StatePicker renders a sidebar listing all prototype states grouped by their
|
|
29
|
+
* `group` field. Clicking an item calls `onSelect(key)`.
|
|
30
|
+
*
|
|
31
|
+
* Props:
|
|
32
|
+
* states – array of { key, name, group?, state }
|
|
33
|
+
* activeKey – key of the currently active state
|
|
34
|
+
* onSelect – callback(key) when a state is clicked
|
|
35
|
+
*/
|
|
36
|
+
export function StatePicker({ states, activeKey, onSelect }) {
|
|
37
|
+
const groups = groupStates(states);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<aside aria-label="State picker" className="flex flex-col w-56 shrink-0 h-full overflow-y-auto bg-white dark:bg-neutral-900 border-r border-neutral-200 dark:border-neutral-700">
|
|
41
|
+
{/* Header */}
|
|
42
|
+
<div className="flex items-center gap-2 px-3 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
|
43
|
+
<LayersIcon className="w-4 h-4 text-neutral-500 dark:text-neutral-400" />
|
|
44
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
|
45
|
+
States
|
|
46
|
+
</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Groups */}
|
|
50
|
+
<div className="flex flex-col gap-1 py-2">
|
|
51
|
+
{groups.map(({ group, states: groupedStates }) => (
|
|
52
|
+
<section key={group}>
|
|
53
|
+
<h3 className="px-3 py-1 text-xs font-semibold uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
|
|
54
|
+
{group}
|
|
55
|
+
</h3>
|
|
56
|
+
<ul>
|
|
57
|
+
{groupedStates.map((s) => {
|
|
58
|
+
const isActive = s.key === activeKey;
|
|
59
|
+
return (
|
|
60
|
+
<li
|
|
61
|
+
key={s.key}
|
|
62
|
+
aria-current={isActive ? 'true' : undefined}
|
|
63
|
+
onClick={() => onSelect(s.key)}
|
|
64
|
+
className={[
|
|
65
|
+
'mx-1 px-2 py-1.5 rounded text-sm cursor-pointer select-none',
|
|
66
|
+
isActive
|
|
67
|
+
? 'bg-black dark:bg-white text-white dark:text-black'
|
|
68
|
+
: 'text-neutral-800 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
|
69
|
+
].join(' ')}
|
|
70
|
+
>
|
|
71
|
+
{s.name}
|
|
72
|
+
</li>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</ul>
|
|
76
|
+
</section>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</aside>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default StatePicker;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PlayIcon, GridIcon, ClockIcon, DocumentIcon } from '../icons.jsx';
|
|
2
|
+
|
|
3
|
+
const TABS = [
|
|
4
|
+
{ id: 'prototype', label: 'Prototype', Icon: PlayIcon },
|
|
5
|
+
{ id: 'screens', label: 'Screens', Icon: GridIcon },
|
|
6
|
+
{ id: 'changelog', label: 'Changelog', Icon: ClockIcon },
|
|
7
|
+
{ id: 'documents', label: 'Documents', Icon: DocumentIcon },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const activeClasses =
|
|
11
|
+
'font-medium text-black dark:text-white border-b-2 border-black dark:border-white';
|
|
12
|
+
const inactiveClasses =
|
|
13
|
+
'font-medium text-neutral-500 dark:text-neutral-400 border-b-2 border-transparent hover:text-black dark:hover:text-white hover:border-neutral-300 dark:hover:border-neutral-500';
|
|
14
|
+
const baseClasses = 'transition-colors cursor-pointer';
|
|
15
|
+
|
|
16
|
+
export default function TabNav({ activeTab, onTabChange }) {
|
|
17
|
+
return (
|
|
18
|
+
<nav aria-label="Viewer tabs">
|
|
19
|
+
<ul className="flex bg-white dark:bg-neutral-900 border-b border-neutral-300 dark:border-neutral-700 list-none m-0 p-0" role="tablist">
|
|
20
|
+
{TABS.map(({ id, label, Icon }) => {
|
|
21
|
+
const isActive = activeTab === id;
|
|
22
|
+
return (
|
|
23
|
+
<li
|
|
24
|
+
key={id}
|
|
25
|
+
role="tab"
|
|
26
|
+
aria-selected={isActive}
|
|
27
|
+
aria-label={label}
|
|
28
|
+
tabIndex={isActive ? 0 : -1}
|
|
29
|
+
className={`flex items-center gap-1.5 px-5 py-2.5 text-sm ${baseClasses} ${isActive ? activeClasses : inactiveClasses}`}
|
|
30
|
+
onClick={() => onTabChange(id)}
|
|
31
|
+
>
|
|
32
|
+
<Icon className="w-4 h-4" />
|
|
33
|
+
<span>{label}</span>
|
|
34
|
+
</li>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
</ul>
|
|
38
|
+
</nav>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SunIcon, MoonIcon } from '../icons.jsx';
|
|
2
|
+
|
|
3
|
+
export default function ThemeToggle({ isDark, onToggle }) {
|
|
4
|
+
return (
|
|
5
|
+
<button
|
|
6
|
+
className="flex items-center gap-1.5 px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded-full hover:border-neutral-400 dark:hover:border-neutral-500 transition-colors cursor-pointer"
|
|
7
|
+
aria-label="Toggle theme"
|
|
8
|
+
role="switch"
|
|
9
|
+
aria-checked={isDark}
|
|
10
|
+
onClick={onToggle}
|
|
11
|
+
>
|
|
12
|
+
<SunIcon className={`w-4 h-4 ${isDark ? 'text-neutral-400' : 'text-black'}`} />
|
|
13
|
+
<span className="w-9 h-5 bg-neutral-300 dark:bg-neutral-600 rounded-full relative block">
|
|
14
|
+
<span
|
|
15
|
+
className={`w-4 h-4 bg-white rounded-full absolute top-0.5 shadow-sm block transition-all ${
|
|
16
|
+
isDark ? 'right-0.5' : 'left-0.5'
|
|
17
|
+
}`}
|
|
18
|
+
/>
|
|
19
|
+
</span>
|
|
20
|
+
<MoonIcon className={`w-4 h-4 ${isDark ? 'text-white' : 'text-neutral-400'}`} />
|
|
21
|
+
</button>
|
|
22
|
+
);
|
|
23
|
+
}
|