1- import { useFileSystemAccess } from '@siberiacancode/reactuse' ;
1+ import type { MouseEvent } from 'react' ;
2+
3+ import {
4+ useBoolean ,
5+ useClickOutside ,
6+ useField ,
7+ useFileSystemAccess
8+ } from '@siberiacancode/reactuse' ;
9+ import { FileTextIcon , ReplaceIcon , XIcon } from 'lucide-react' ;
10+ import { useState } from 'react' ;
211
312const Demo = ( ) => {
413 const fileSystemAccess = useFileSystemAccess ( {
@@ -11,60 +20,163 @@ const Demo = () => {
1120 ]
1221 } ) ;
1322
23+ const findField = useField ( '' ) ;
24+ const replaceField = useField ( '' ) ;
25+ const [ findOpen , toggleFindOpen ] = useBoolean ( false ) ;
26+ const [ content , setContent ] = useState ( '' ) ;
27+
28+ const findPanelRef = useClickOutside < HTMLDivElement > ( ( ) => {
29+ toggleFindOpen ( false ) ;
30+ findField . reset ( ) ;
31+ replaceField . reset ( ) ;
32+ } ) ;
33+
1434 if ( ! fileSystemAccess . supported )
1535 return (
1636 < p >
17- Api not supported, make sure to check for compatibility with different browsers when using
37+ API not supported, make sure to check for compatibility with different browsers when using
1838 this{ ' ' }
1939 < a
2040 href = 'https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API'
2141 rel = 'noreferrer'
2242 target = '_blank'
2343 >
24- api
44+ API
2545 </ a >
2646 </ p >
2747 ) ;
2848
29- return (
30- < div className = 'flex max-w-lg flex-col gap-3' >
31- < textarea
32- className = 'box-border min-h-[12rem] w-full resize-y rounded-lg border border-zinc-200 bg-[var(--vp-c-bg-soft)] p-2 font-mono text-sm text-zinc-900 dark:border-zinc-600 dark:text-zinc-100'
33- placeholder = 'Enter your text here'
34- rows = { 4 }
35- value = { fileSystemAccess . data ?? '' }
36- onChange = { ( event ) => fileSystemAccess . set ( event . target . value ) }
37- />
38-
39- < div className = 'flex flex-wrap gap-2' >
40- < button type = 'button' onClick = { ( ) => void fileSystemAccess . open ( ) } >
41- Open
42- </ button >
43- < button type = 'button' onClick = { ( ) => void fileSystemAccess . save ( ) } >
44- Save
45- </ button >
46- < button type = 'button' onClick = { ( ) => void fileSystemAccess . update ( ) } >
47- Reload
48- </ button >
49- </ div >
49+ const find = findField . watch ( ) ;
50+ const replace = replaceField . watch ( ) ;
51+ const matches = ( fileSystemAccess . data ?? '' ) . split ( find ) . length - 1 || 0 ;
52+ const dirty = ! ! fileSystemAccess . file && content !== fileSystemAccess . data ;
5053
51- { fileSystemAccess . file && (
52- < div className = 'flex flex-col gap-2' >
53- < div >
54- name: < code > { fileSystemAccess . name } </ code >
55- </ div >
56- < div >
57- type: < code > { fileSystemAccess . type } </ code >
58- </ div >
59- < div >
60- size: < code > { fileSystemAccess . size } </ code >
61- </ div >
62- < div >
63- lastModified: < code > { fileSystemAccess . lastModified } </ code >
54+ const onSave = async ( event : MouseEvent < HTMLButtonElement > ) => {
55+ event . preventDefault ( ) ;
56+ await fileSystemAccess . save ( ) ;
57+ setContent ( fileSystemAccess . data ?? '' ) ;
58+ } ;
59+
60+ const onOpen = async ( event : MouseEvent < HTMLButtonElement > ) => {
61+ event . preventDefault ( ) ;
62+ const data = await fileSystemAccess . open ( ) ;
63+ setContent ( data ) ;
64+ } ;
65+
66+ const onReplaceAll = ( event : MouseEvent < HTMLButtonElement > ) => {
67+ event . preventDefault ( ) ;
68+ if ( ! find || ! matches ) return ;
69+ fileSystemAccess . set ( ( fileSystemAccess . data ?? '' ) . split ( find ) . join ( replace ) ) ;
70+ findField . setValue ( '' ) ;
71+ replaceField . setValue ( '' ) ;
72+ } ;
73+
74+ return (
75+ < section className = 'demo-ui flex w-full max-w-2xl flex-col p-4' >
76+ < div className = 'border-border bg-card relative flex h-[280px] flex-col overflow-hidden rounded-xl border shadow-sm' >
77+ { ! fileSystemAccess . file && (
78+ < div className = 'flex size-full flex-col items-center justify-center gap-3 p-6' >
79+ < div className = 'bg-muted text-muted-foreground flex size-12 items-center justify-center rounded-full' >
80+ < FileTextIcon className = 'size-6' />
81+ </ div >
82+ < div className = 'flex flex-col items-center gap-1 text-center' >
83+ < p className = 'text-foreground text-sm font-medium' > No file opened</ p >
84+ < p className = 'text-muted-foreground text-xs' > Open a .txt file to start editing</ p >
85+ </ div >
86+ < button data-size = 'sm' type = 'button' onClick = { onOpen } >
87+ Open file
88+ </ button >
6489 </ div >
65- </ div >
66- ) }
67- </ div >
90+ ) }
91+
92+ { fileSystemAccess . file && (
93+ < >
94+ < div className = 'border-border bg-muted/40 flex shrink-0 items-center gap-2 border-b px-3 py-2' >
95+ < div className = 'bg-card flex size-6 shrink-0 items-center justify-center' >
96+ < FileTextIcon className = 'text-muted-foreground size-3' />
97+ </ div >
98+ < span className = 'text-foreground min-w-0 flex-1 truncate text-xs font-medium' >
99+ { fileSystemAccess . name }
100+ </ span >
101+
102+ < div className = 'flex items-center gap-1' >
103+ < button
104+ aria-label = 'Find and replace'
105+ data-size = 'icon-sm'
106+ data-variant = 'ghost'
107+ type = 'button'
108+ onClick = { ( ) => toggleFindOpen ( ) }
109+ >
110+ < ReplaceIcon className = 'size-3.5' />
111+ </ button >
112+ < button data-size = 'sm' disabled = { ! dirty } type = 'button' onClick = { onSave } >
113+ Save
114+ </ button >
115+ </ div >
116+ </ div >
117+
118+ < textarea
119+ className = 'no-scrollbar text-foreground flex-1 resize-none rounded-none! border-none! bg-transparent p-3 font-mono text-xs shadow-none! ring-0! outline-none!'
120+ value = { fileSystemAccess . data }
121+ onChange = { ( event ) => fileSystemAccess . set ( event . target . value ) }
122+ />
123+
124+ { findOpen && (
125+ < div
126+ ref = { findPanelRef }
127+ className = 'border-border bg-card absolute top-12 right-3 z-20 flex w-[240px] flex-col gap-3 rounded-xl border p-3 shadow-lg'
128+ >
129+ < div className = 'flex items-center justify-between' >
130+ < span className = 'text-foreground text-[11px] font-medium' > Find and replace</ span >
131+ < button
132+ aria-label = 'Close'
133+ data-size = 'icon-xs'
134+ data-variant = 'ghost'
135+ type = 'button'
136+ onClick = { ( ) => toggleFindOpen ( ) }
137+ >
138+ < XIcon className = 'size-3' />
139+ </ button >
140+ </ div >
141+
142+ < div className = 'flex flex-col gap-2' >
143+ < input
144+ autoFocus
145+ className = 'border-border bg-background text-foreground rounded-md border px-2.5 py-1.5 text-[11px] outline-none'
146+ placeholder = 'Find'
147+ { ...findField . register ( ) }
148+ />
149+ < div className = 'relative' >
150+ < input
151+ className = 'border-border bg-background text-foreground w-full rounded-md border px-2.5 py-1.5 pr-8 text-[11px] outline-none'
152+ placeholder = 'Replace with'
153+ { ...replaceField . register ( ) }
154+ />
155+ < button
156+ aria-label = 'Replace all'
157+ className = 'absolute top-1/2 right-1 -translate-y-1/2'
158+ data-size = 'icon-xs'
159+ data-variant = 'ghost'
160+ disabled = { ! find || ! matches }
161+ type = 'button'
162+ onClick = { onReplaceAll }
163+ >
164+ < ReplaceIcon className = 'size-3' />
165+ </ button >
166+ </ div >
167+ </ div >
168+
169+ { ! ! find && (
170+ < span className = 'text-muted-foreground font-mono text-[10px] tabular-nums' >
171+ { matches } { matches === 1 ? 'match' : 'matches' }
172+ </ span >
173+ ) }
174+ </ div >
175+ ) }
176+ </ >
177+ ) }
178+ </ div >
179+ </ section >
68180 ) ;
69181} ;
70182
0 commit comments