1+ import { useEffect , useRef , useState } from "react" ;
12import "./TopBar.css" ;
23
3- import { CATEGORY_LABELS , isHeader } from "../checklist" ;
4+ import { CATEGORY_LABELS } from "../checklist" ;
45import { Icon } from "../icons" ;
5- import { serializeChecklist } from "../schemas" ;
6+ import { nameResolverFor , serializeChecklist , serializePackage } from "../schemas" ;
67import { useSaveStatus , useSelectedChecklist } from "../state" ;
78
8- function sectionFor ( db : ReturnType < typeof useSelectedChecklist > [ "db" ] , checklistId : string ) : string | null {
9- if ( ! db ) return null ;
10- for ( const cat of Object . values ( db . categories ) ) {
11- let section : string | null = null ;
12- for ( const entry of cat ) {
13- if ( isHeader ( entry ) ) {
14- section = entry . name ;
15- } else if ( entry . id === checklistId ) {
16- return section ;
17- }
18- }
19- }
20- return null ;
9+ function downloadBlob ( text : string , filename : string ) {
10+ const blob = new Blob ( [ text ] , { type : "application/json" } ) ;
11+ const url = URL . createObjectURL ( blob ) ;
12+ const a = document . createElement ( "a" ) ;
13+ a . href = url ;
14+ a . download = filename ;
15+ a . click ( ) ;
16+ URL . revokeObjectURL ( url ) ;
2117}
2218
2319export function TopBar ( ) {
24- const { checklist, db } = useSelectedChecklist ( ) ;
20+ const { checklist, db, category , section } = useSelectedChecklist ( ) ;
2521 const status = useSaveStatus ( ) ;
22+ const [ exportOpen , setExportOpen ] = useState ( false ) ;
23+ const exportMenuRef = useRef < HTMLDivElement > ( null ) ;
24+
25+ useEffect ( ( ) => {
26+ if ( ! exportOpen ) return ;
27+ function onDown ( e : PointerEvent ) {
28+ if ( exportMenuRef . current && ! exportMenuRef . current . contains ( e . target as Node ) ) setExportOpen ( false ) ;
29+ }
30+ function onKey ( e : KeyboardEvent ) {
31+ if ( e . key === "Escape" ) setExportOpen ( false ) ;
32+ }
33+ window . addEventListener ( "pointerdown" , onDown ) ;
34+ window . addEventListener ( "keydown" , onKey ) ;
35+ return ( ) => {
36+ window . removeEventListener ( "pointerdown" , onDown ) ;
37+ window . removeEventListener ( "keydown" , onKey ) ;
38+ } ;
39+ } , [ exportOpen ] ) ;
2640
27- const section = checklist ? sectionFor ( db , checklist . id ) : null ;
41+ function handleExportPackage ( ) {
42+ if ( ! db ) return ;
43+ downloadBlob ( serializePackage ( db ) , `${ db . name . replace ( / \s + / g, "_" ) . toLowerCase ( ) } .json` ) ;
44+ }
2845
29- function handleExport ( ) {
30- if ( ! checklist ) return ;
31- const blob = new Blob ( [ serializeChecklist ( checklist ) ] , { type : "application/json" } ) ;
32- const url = URL . createObjectURL ( blob ) ;
33- const a = document . createElement ( "a" ) ;
34- a . href = url ;
35- a . download = `${ checklist . name . replace ( / \s + / g, "_" ) . toLowerCase ( ) } .json` ;
36- a . click ( ) ;
37- URL . revokeObjectURL ( url ) ;
46+ function handleExportChecklist ( ) {
47+ if ( ! checklist || ! db ) return ;
48+ downloadBlob (
49+ serializeChecklist ( checklist , nameResolverFor ( db ) ) ,
50+ `${ checklist . name . replace ( / \s + / g, "_" ) . toLowerCase ( ) } .json` ,
51+ ) ;
3852 }
3953
4054 return (
@@ -47,14 +61,14 @@ export function TopBar() {
4761 < span className = "sep" />
4862 < span className = "file mono" >
4963 < span > { db ? db . name : "—" } </ span >
50- { checklist && (
64+ { checklist && category && (
5165 < >
5266 < span className = "slash" > /</ span >
53- < span > { CATEGORY_LABELS [ checklist . category ] } </ span >
67+ < span > { CATEGORY_LABELS [ category ] } </ span >
5468 { section && (
5569 < >
5670 < span className = "slash" > /</ span >
57- < span > { section } </ span >
71+ < span > { section . name } </ span >
5872 </ >
5973 ) }
6074 < span className = "slash" > /</ span >
@@ -67,10 +81,33 @@ export function TopBar() {
6781 { status === "saving" ? "saving changes" : "changes automatically saved" }
6882 </ span >
6983 < div className = "spacer" />
70- < button className = "btn-export" onClick = { handleExport } disabled = { ! checklist } >
71- < Icon name = "download" size = { 11 } />
72- Export
73- </ button >
84+ < div className = { `export-ctl${ exportOpen ? " open" : "" } ` } ref = { exportMenuRef } >
85+ < button className = "btn-export" onClick = { ( ) => setExportOpen ( ( o ) => ! o ) } disabled = { ! db } >
86+ < Icon name = "download" size = { 11 } />
87+ Export
88+ </ button >
89+ { exportOpen && (
90+ < div className = "export-menu mono" >
91+ < button
92+ onClick = { ( ) => {
93+ setExportOpen ( false ) ;
94+ handleExportPackage ( ) ;
95+ } }
96+ >
97+ Export package…
98+ </ button >
99+ < button
100+ disabled = { ! checklist }
101+ onClick = { ( ) => {
102+ setExportOpen ( false ) ;
103+ handleExportChecklist ( ) ;
104+ } }
105+ >
106+ Export checklist…
107+ </ button >
108+ </ div >
109+ ) }
110+ </ div >
74111 </ header >
75112 ) ;
76113}
0 commit comments