22import logging
33from pathlib import Path
44from typing import List , Optional , Tuple
5- from urllib .parse import quote , unquote
5+ from urllib .parse import unquote
66
77from nicegui import ui
88from starlette .requests import Request
1616logger .setLevel (logging .INFO )
1717
1818
19- _REPO_ROOT = Path (__file__ ).resolve ().parent .parent .parent . parent
19+ _REPO_ROOT = Path (__file__ ).resolve ().parent .parent .parent
2020LICENSE_ROOT = _REPO_ROOT / "License&Copyright"
2121# Relative to LICENSE_ROOT — default document when ``?doc=`` is missing/invalid.
2222DEFAULT_LICENSE_REL = "LICENSE"
@@ -114,7 +114,7 @@ def render_one_file(
114114 )
115115 ui .code (raw ).classes (
116116 "w-full max-w-none text-sm whitespace-pre-wrap break-words "
117- "block p-4 bg-zinc -50 rounded-lg border border-zinc-300 "
117+ "block p-4 bg-slate -50 rounded-xl border border-slate-200 shadow-inner "
118118 )
119119
120120
@@ -124,92 +124,132 @@ def render_license_documents_section(
124124 static_url : str = "/license-copyright" ,
125125 page_path : str = "/about" ,
126126) -> None :
127- """License & Copyright picker and viewer; uses ``?doc=`` on ``page_path``."""
128- doc = request .query_params .get ("doc" )
127+ """License & Copyright picker and viewer; uses dynamic, inline, closable, and scrollable rendering."""
129128 root = LICENSE_ROOT
130129 files = list_text_docs (root )
131130
132131 ui .element ("div" ).props ('id="license-copyright"' ).classes ("scroll-mt-24" )
133132 with ui .card ().classes (
134- "w-full max-w-3xl p-6 bg-white border border-zinc-300 rounded-xl shadow-sm "
133+ "w-full max-w-3xl p-6 bg-white border border-slate-200 rounded-2xl shadow-md border-t-4 border-t-[#881c1c] flex flex-col gap-4 "
135134 ):
136- ui .label ("License & Copyright" ).classes (
137- "text-xl font-semibold text-[#505759] mb-2"
138- )
135+ ui .label ("License & Copyright" ).classes ("text-xl font-semibold text-slate-800" )
139136 ui .label (
140- "RescueBox LICENSE, COPYRIGHT, and NOTICE, see bundled third-party notices when you choose Third party."
141- ).classes ("text-sm text-zinc-600 mb-4" )
142-
143- if not root .is_dir ():
144- ui .label (f"Folder not found: { root } " ).classes ("text-red-600" )
145- return
146- if not files :
147- ui .label ("No license documents found in that folder." ).classes ("text-zinc-600" )
148- return
149-
150- primary_entries , third_party_files = _primary_and_third_party_paths (files )
151-
152- rel = unquote (doc ) if doc else ""
153- if rel not in files :
154- if DEFAULT_LICENSE_REL in files :
155- rel = DEFAULT_LICENSE_REL
156- elif primary_entries :
157- rel = primary_entries [0 ][1 ]
158- else :
159- rel = files [0 ]
160-
161- base = page_path .rstrip ("/" ) or "/about"
162-
163- def _navigate_to_doc (new_rel : str ) -> None :
164- if new_rel in files :
165- ui .navigate .to (f"{ base } ?doc={ quote (new_rel , safe = '' )} " )
137+ "Select a document below to view RescueBox LICENSE, COPYRIGHT, NOTICE, or bundled third-party notices."
138+ ).classes ("text-sm text-zinc-600" )
166139
167- # Main picker: primary docs + optional "Third party".
168- # NiceGUI dict options are {value: label} (keys are selected values; values are shown in the UI).
169- main_options : dict [str , str ] = {path : label for label , path in primary_entries }
170- if third_party_files :
171- main_options [_THIRD_PARTY_SENTINEL ] = "Third party"
172-
173- if rel in third_party_files :
174- main_value = _THIRD_PARTY_SENTINEL
175- else :
176- main_value = next (
177- (path for label , path in primary_entries if path == rel ),
178- primary_entries [0 ][1 ] if primary_entries else rel ,
179- )
180-
181- def _on_main_pick (e ) -> None :
182- v = e .value
183- if not isinstance (v , str ):
140+ if not root .is_dir ():
141+ ui .label (f"Folder not found: { root } " ).classes ("text-red-600" )
184142 return
185- if v == _THIRD_PARTY_SENTINEL and third_party_files :
186- target = rel if rel in third_party_files else third_party_files [0 ]
187- _navigate_to_doc (target )
188- elif v != _THIRD_PARTY_SENTINEL :
189- _navigate_to_doc (v )
190-
191- ui .select (
192- options = main_options ,
193- value = main_value ,
194- label = "Document" ,
195- on_change = _on_main_pick ,
196- ).classes ("w-full max-w-2xl" )
197-
198- if third_party_files :
199- with ui .column ().classes ("w-full max-w-2xl mt-2" ) as third_wrap :
200- third_wrap .visible = rel in third_party_files
201-
202- def _on_third_pick (e ) -> None :
203- v = e .value
204- if isinstance (v , str ) and v in third_party_files :
205- _navigate_to_doc (v )
206-
207- ui .select (
143+ if not files :
144+ ui .label ("No license documents found in that folder." ).classes (
145+ "text-zinc-600"
146+ )
147+ return
148+
149+ primary_entries , third_party_files = _primary_and_third_party_paths (files )
150+
151+ # Main options for the select dropdown
152+ main_options : dict [str , str ] = {path : label for label , path in primary_entries }
153+ if third_party_files :
154+ main_options [_THIRD_PARTY_SENTINEL ] = "Third party"
155+
156+ # Dropdowns row
157+ with ui .row ().classes ("w-full gap-4 items-center flex-wrap sm:flex-nowrap" ):
158+ # Primary document selector
159+ main_select = ui .select (
160+ options = main_options ,
161+ label = "Document" ,
162+ value = None ,
163+ on_change = lambda e : _on_main_change (e ),
164+ ).classes ("flex-1 min-w-[200px]" )
165+
166+ # Third-party document selector (hidden by default)
167+ third_select = ui .select (
208168 options = third_party_files ,
209- value = rel if rel in third_party_files else third_party_files [0 ],
210169 label = "Third-party document" ,
211- on_change = _on_third_pick ,
212- ).classes ("w-full" )
170+ value = None ,
171+ on_change = lambda e : _on_third_change (e ),
172+ ).classes ("flex-1 min-w-[200px]" )
173+ third_select .visible = False
174+
175+ # Closable & Scrollable Viewer Container (hidden by default)
176+ with ui .card ().classes (
177+ "w-full p-4 bg-slate-50 border border-slate-200 rounded-xl shadow-sm flex flex-col gap-3"
178+ ) as viewer_card :
179+ viewer_card .visible = False
180+
181+ # Viewer Header
182+ with ui .row ().classes (
183+ "w-full justify-between items-center border-b pb-2 border-slate-200"
184+ ):
185+ with ui .row ().classes ("items-center gap-2" ):
186+ ui .icon ("article" , size = "sm" ).classes ("text-[#881c1c]" )
187+ viewer_title = ui .label ("" ).classes (
188+ "text-sm font-bold text-slate-700 font-mono"
189+ )
190+
191+ # Close button
192+ ui .button (
193+ "Close" ,
194+ icon = "close" ,
195+ color = None ,
196+ on_click = lambda : _close_viewer (),
197+ ).props ("flat dense no-caps" ).classes (
198+ "text-slate-600 hover:text-slate-800 hover:bg-slate-100 px-3 py-1 "
199+ "rounded-lg border border-slate-200 transition-colors text-sm font-medium"
200+ )
201+
202+ # Scrollable body
203+ viewer_body = ui .column ().classes (
204+ "w-full max-h-[350px] overflow-y-auto pr-2"
205+ )
213206
214- body = ui .column ().classes ("w-full min-w-0 mt-6" )
215- render_one_file (body , root , rel , static_url = static_url )
207+ # Helper to render document content
208+ def _show_document (rel_path : str ):
209+ viewer_body .clear ()
210+ viewer_title .text = rel_path
211+ render_one_file (viewer_body , root , rel_path , static_url = static_url )
212+ viewer_card .visible = True
213+
214+ # Close viewer action
215+ def _close_viewer ():
216+ viewer_card .visible = False
217+ main_select .value = None
218+ third_select .value = None
219+ third_select .visible = False
220+
221+ # On primary selection change
222+ def _on_main_change (e ):
223+ val = e .value
224+ if not val :
225+ return
226+ if val == _THIRD_PARTY_SENTINEL :
227+ third_select .visible = True
228+ # Automatically select and show the first third party file
229+ first_third = third_party_files [0 ] if third_party_files else None
230+ if first_third :
231+ third_select .value = first_third
232+ _show_document (first_third )
233+ else :
234+ third_select .visible = False
235+ third_select .value = None
236+ _show_document (val )
237+
238+ # On third-party selection change
239+ def _on_third_change (e ):
240+ val = e .value
241+ if val and val in third_party_files :
242+ _show_document (val )
243+
244+ # Initial load from query param if present
245+ doc = request .query_params .get ("doc" )
246+ if doc :
247+ rel = unquote (doc )
248+ if rel in files :
249+ if rel in third_party_files :
250+ main_select .value = _THIRD_PARTY_SENTINEL
251+ third_select .value = rel
252+ third_select .visible = True
253+ else :
254+ main_select .value = rel
255+ _show_document (rel )
0 commit comments