From: Eric Wertz Date: Mon, 9 Jun 2025 04:11:51 +0000 (-0400) Subject: Mobile styling updates X-Git-Url: https://ericdwertz.com/git/?a=commitdiff_plain;h=7000c65d3aae7290afcd72c85712f855e6019510;p=listv4.git Mobile styling updates --- diff --git a/backend/HTTPRequest.cpp b/backend/HTTPRequest.cpp index 4aa284b..3619be3 100644 --- a/backend/HTTPRequest.cpp +++ b/backend/HTTPRequest.cpp @@ -172,10 +172,12 @@ void CHTTPRequest::parsePath() _queryParams[param] = ""; } } + + _path = pathWithoutQuery; } // Parse path components - std::istringstream pathStream(pathWithoutQuery); + std::istringstream pathStream(_path); std::string component; while (std::getline(pathStream, component, '/')) { diff --git a/backend/StaticFileResponse.cpp b/backend/StaticFileResponse.cpp index 19e3f0e..1417e04 100644 --- a/backend/StaticFileResponse.cpp +++ b/backend/StaticFileResponse.cpp @@ -17,15 +17,8 @@ // CStaticFileResponse Implementation CStaticFileResponse::CStaticFileResponse(const CHTTPRequest& request, std::string path) : CHTTPResponse(request) { - // Remove query string from path for routing decisions - std::string clean_path = path; - size_t query_pos = clean_path.find('?'); - if (query_pos != std::string::npos) { - clean_path = clean_path.substr(0, query_pos); - } - // Index is easy enough to handle - if (clean_path == "/" ) + if (path == "/" ) { serveFileFromPath("index.html"); return; @@ -38,10 +31,6 @@ std::string CStaticFileResponse::sanitizePath(const std::string& requested_path) { // Remove query string if present std::string path = requested_path; - size_t query_pos = path.find('?'); - if (query_pos != std::string::npos) { - path = path.substr(0, query_pos); - } // Basic path normalization - remove duplicate slashes and handle relative paths std::string normalized = "/"; diff --git a/frontend/app/AppRouter.tsx b/frontend/app/AppRouter.tsx index 9a56c34..868e85b 100644 --- a/frontend/app/AppRouter.tsx +++ b/frontend/app/AppRouter.tsx @@ -7,7 +7,7 @@ import { AppContext } from './AppContext'; import NotFound from 'views/NotFound'; // Wrapper component to handle auth checks -const AuthRoute = ({ component: Component, auth_required }: { component: ComponentType, auth_required: boolean }) => { +const AuthRoute = ({ component: Component, auth_required, title }: { component: ComponentType, auth_required: boolean, title: string }) => { const { isLoading, loggedIn, checkAuth } = useContext(AppContext); const [, route] = useRouter(); @@ -29,6 +29,11 @@ const AuthRoute = ({ component: Component, auth_required }: { component: Compone } }, [isLoading, loggedIn, auth_required, route]); + // Set page title + useEffect(() => { + document.title = title; + }, [title]); + if (isLoading) { return null; } @@ -55,6 +60,7 @@ const AppRouter = () => { )} /> diff --git a/frontend/app_routes.tsx b/frontend/app_routes.tsx index 70380e0..d6da90b 100644 --- a/frontend/app_routes.tsx +++ b/frontend/app_routes.tsx @@ -14,21 +14,25 @@ export const AppRoutes = [ { path: '/', view: List, + title: 'List', auth_required: true, }, { path: '/list', view: List, + title: 'List', auth_required: true, }, { path: '/signup', view: SignUp, + title: 'Sign Up', auth_required: false, }, { path: '/login', view: Login, + title: 'Login', auth_required: false, }, ]; diff --git a/frontend/components/ListContext.tsx b/frontend/components/ListContext.tsx index 0c2659a..c26a02e 100644 --- a/frontend/components/ListContext.tsx +++ b/frontend/components/ListContext.tsx @@ -369,10 +369,14 @@ export const useListState = () => { const previousItem = findPreviousItem(prevItems, currentItem._uuid); if (!previousItem) return prevItems; - // Update previous item and mark current as deleted + // Get the children of the item being deleted + const childrenToMove = currentItem._children.filter(child => !child.__is_deleted); + + // Update previous item with remaining content and append deleted item's children let result = updateItemInTree(prevItems, previousItem._uuid, item => ({ ...item, content: item.content + remainingContent, + _children: reorderItems([...item._children, ...childrenToMove]), __is_dirty: true })); diff --git a/frontend/components/ListItem.tsx b/frontend/components/ListItem.tsx index af4f250..f9247ed 100644 --- a/frontend/components/ListItem.tsx +++ b/frontend/components/ListItem.tsx @@ -101,14 +101,26 @@ export const ListItem = forwardRef(({ item, onChange const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { - // Enter key - create new item - event.preventDefault(); - const target = event.target as HTMLDivElement; - const cursorPosition = getCursorPosition(target); - const fullContent = target.textContent || ''; - const beforeContent = fullContent.substring(0, cursorPosition); - const afterContent = fullContent.substring(cursorPosition); - createItem(item, beforeContent, afterContent); + if (event.shiftKey === true) { + // Shift+Enter - add newline (allow default behavior, then update content) + setTimeout(() => { + const target = event.target as HTMLDivElement; + const content = target.innerHTML; + if (item.content !== content) { + onChange({ ...item, content, __is_dirty: true }); + } + }, 0); + return; + } else { + // Enter key - create new item + event.preventDefault(); + const target = event.target as HTMLDivElement; + const cursorPosition = getCursorPosition(target); + const fullContent = target.textContent || ''; + const beforeContent = fullContent.substring(0, cursorPosition); + const afterContent = fullContent.substring(cursorPosition); + createItem(item, beforeContent, afterContent); + } } else if (event.key === 'Backspace') { const target = event.target as HTMLDivElement; const cursorPosition = getCursorPosition(target); diff --git a/frontend/components/Toolbar.tsx b/frontend/components/Toolbar.tsx index f1227c6..4a3e89f 100644 --- a/frontend/components/Toolbar.tsx +++ b/frontend/components/Toolbar.tsx @@ -1,5 +1,5 @@ import { h, JSX } from 'preact'; -import { useContext } from 'preact/hooks'; +import { useContext, useEffect, useRef } from 'preact/hooks'; import { AppContext } from 'app/AppContext'; import LoadingSpinner from './LoadingSpinner'; import { MenuIcon, CheckIcon, CircleIcon } from './Icons'; @@ -12,6 +12,7 @@ interface ToolbarProps { export const Toolbar = ({ buttons, onMenuClick }: ToolbarProps) => { const appContext = useContext(AppContext); + const toolbarRef = useRef(null); const handleMenuClick = () => { if (onMenuClick) { @@ -22,61 +23,97 @@ export const Toolbar = ({ buttons, onMenuClick }: ToolbarProps) => { } }; + useEffect(() => { + function resizeHandler() { + if( !toolbarRef.current) { + return; + } + + if( window.visualViewport?.width && window.visualViewport.width > 480) { + return; + } + + // viewport height + const viewportHeight = window.visualViewport?.height ?? 0; + const offsetTop = window.visualViewport?.offsetTop ?? 0; + const viewportScale = window.visualViewport?.scale ?? 1; + // math - removed scale transform to prevent event handling issues + toolbarRef.current.style.transform = `translate( 0px, ${viewportHeight - toolbarRef.current.offsetHeight + offsetTop}px)`; + } + + // run first time to initialize + resizeHandler(); + + // subscribe to events which affect scroll, or viewport position + window.visualViewport?.addEventListener('resize', resizeHandler); + window.visualViewport?.addEventListener('scroll', resizeHandler); + window?.addEventListener('touchmove', resizeHandler); + + // unsubscribe + return () => { + window.visualViewport?.removeEventListener('resize', resizeHandler); + window.visualViewport?.removeEventListener('scroll', resizeHandler); + window?.removeEventListener('touchmove', resizeHandler); + }; + }, []); + return ( -
-
- } - onClick={handleMenuClick} - title="Menu" - menuOptions={[ - { - label: 'List', - onClick: () => { - window.location.href = '/list'; +
+
+
+ } + onClick={handleMenuClick} + title="Menu" + menuOptions={[ + { + label: 'List', + onClick: () => { + window.location.href = '/list'; + } + }, + { + label: 'Chat', + onClick: () => { + window.location.href = '/chat'; + } + }, + { + label: 'Logout', + onClick: () => { + appContext.logout(); + } } - }, - { - label: 'Chat', - onClick: () => { - window.location.href = '/chat'; - } - }, - { - label: 'Logout', - onClick: () => { - appContext.logout(); - } - } - ]} - /> -
- {buttons.map((button, index) => ( -
- - {button.separatorAfter && ( -
- )} -
- ))} -
- -
- {appContext.saveStatus === 'saving' ? ( - - ) : appContext.saveStatus === 'saved' ? ( - - ) : appContext.saveStatus === 'queued' ? ( - - ) : ( - - )} + ]} + /> +
+ {buttons.map((button, index) => ( +
+ + {button.separatorAfter && ( +
+ )} +
+ ))} +
+ +
+ {appContext.saveStatus === 'saving' ? ( + + ) : appContext.saveStatus === 'saved' ? ( + + ) : appContext.saveStatus === 'queued' ? ( + + ) : ( + + )} +
); diff --git a/frontend/styles.css b/frontend/styles.css index af21cbd..62fbc71 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -3,6 +3,10 @@ /* Base Styles */ body { font-family: 'Lato', sans-serif; + padding-bottom: 3.25rem; /* Account for fixed bottom toolbar */ + margin: 0; + min-height: 100vh; + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ } h1 { @@ -10,49 +14,51 @@ h1 { } .loader { - width: 48px; - height: 48px; + width: 3rem; + height: 3rem; border-radius: 50%; position: relative; - animation: rotate 1s linear infinite - } + animation: rotate 1s linear infinite; +} + .loader-small { - width: 24px; - height: 24px; + width: 1.5rem; + height: 1.5rem; border-radius: 50%; position: relative; - animation: rotate 1s linear infinite - } - .loader::before, .loader-small::before { + animation: rotate 1s linear infinite; +} + +.loader::before, .loader-small::before { content: ""; box-sizing: border-box; position: absolute; - inset: 0px; + inset: 0; border-radius: 50%; - border: 5px solid #0074ff; - animation: prixClipFix 2s linear infinite ; - } - - @keyframes rotate { - 100% {transform: rotate(360deg)} - } - - @keyframes prixClipFix { - 0% {clip-path:polygon(50% 50%,0 0,0 0,0 0,0 0,0 0)} - 25% {clip-path:polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0)} - 50% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%)} - 75% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%)} - 100% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0)} - } + border: 0.3125rem solid #0074ff; + animation: prixClipFix 2s linear infinite; +} + +@keyframes rotate { + 100% { transform: rotate(360deg) } +} + +@keyframes prixClipFix { + 0% { clip-path: polygon(50% 50%,0 0,0 0,0 0,0 0,0 0) } + 25% { clip-path: polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0) } + 50% { clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%) } + 75% { clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%) } + 100% { clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0) } +} /* Form Styles */ form { - max-width: 400px; + max-width: 25rem; margin: 2rem auto; padding: 2rem; background: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + border-radius: 0.5rem; + box-shadow: 0 0.125rem 0.625rem rgba(0, 0, 0, 0.1); } form h1 { @@ -64,10 +70,10 @@ form h1 { form input { width: 100%; - padding: 12px; + padding: 0.75rem; margin-bottom: 1rem; border: 1px solid #ddd; - border-radius: 4px; + border-radius: 0.25rem; font-size: 1rem; transition: border-color 0.3s ease; } @@ -75,16 +81,16 @@ form input { form input:focus { outline: none; border-color: #0074ff; - box-shadow: 0 0 0 2px rgba(0, 116, 255, 0.1); + box-shadow: 0 0 0 0.125rem rgba(0, 116, 255, 0.1); } form button { width: 100%; - padding: 12px; + padding: 0.75rem; background: #0074ff; color: white; border: none; - border-radius: 4px; + border-radius: 0.25rem; font-size: 1rem; cursor: pointer; transition: background-color 0.3s ease; @@ -111,12 +117,12 @@ form button:active { animation: slideDown 0.3s ease forwards; z-index: 2000; border: 1px solid #000; - border-radius: 4px; + border-radius: 0.25rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem; - backdrop-filter: blur(10px); + backdrop-filter: blur(0.625rem); } .status-bar.info { @@ -166,21 +172,13 @@ form button:active { } @keyframes slideDown { - from { - transform: translateY(-100%); - } - to { - transform: translateY(0); - } + from { transform: translateY(-100%); } + to { transform: translateY(0); } } @keyframes slideUp { - from { - transform: translateY(0); - } - to { - transform: translateY(-100%); - } + from { transform: translateY(0); } + to { transform: translateY(-100%); } } /* List Item Styles */ @@ -211,7 +209,7 @@ li > div[contenteditable] { transition: all 0.2s ease; min-height: 1.2em; outline: none; - display: inline-block; + display: block; } li > div[contenteditable]:focus { @@ -226,7 +224,7 @@ li > div[contenteditable]:hover { /* Indentation levels using CSS custom properties */ li { - --indent-size: 24px; + --indent-size: 1.5rem; } .list-item-level-0 { @@ -257,47 +255,49 @@ li { font-style: italic; } +/* Toolbar Styles */ .toolbar { display: flex; justify-content: space-between; align-items: center; - padding: 8px 16px; + padding: 0.5rem 1rem; background: linear-gradient(135deg, rgba(248, 249, 250, 0.85) 0%, rgba(233, 236, 239, 0.85) 100%); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(0.5rem); + -webkit-backdrop-filter: blur(0.5rem); border-bottom: 1px solid #dee2e6; position: fixed; top: 0; left: 0; right: 0; z-index: 1000; - height: 52px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + height: 3.25rem; + box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.1); + -webkit-overflow-scrolling: touch; } -.toolbar-left { - display: flex; - align-items: center; - gap: 8px; +.toolbar-spacer { + display: block; + height: 3.75rem; } +.toolbar-left, .toolbar-right { display: flex; align-items: center; - gap: 8px; + gap: 0.5rem; } .toolbar-button-container { display: flex; align-items: center; - gap: 8px; + gap: 0.5rem; } .toolbar-separator { width: 1px; - height: 24px; + height: 1.5rem; background: #dee2e6; - margin: 0 8px; + margin: 0 0.5rem; } /* Toolbar Button Wrapper for Menu Functionality */ @@ -310,10 +310,10 @@ li { display: flex; align-items: center; justify-content: center; - width: 40px; - height: 40px; + width: 2.5rem; + height: 2.5rem; border: 1px solid transparent; - border-radius: 6px; + border-radius: 0.375rem; background: transparent; color: #495057; cursor: pointer; @@ -325,17 +325,17 @@ li { /* Menu indicator for buttons with menus */ .toolbar-button.has-menu { - padding-right: 18px; + padding-right: 1.125rem; width: auto; - min-width: 40px; + min-width: 2.5rem; } .menu-indicator { position: absolute; - right: 4px; + right: 0.25rem; top: 50%; transform: translateY(-50%); - font-size: 8px; + font-size: 0.5rem; line-height: 1; opacity: 0.7; pointer-events: none; @@ -384,22 +384,22 @@ li { position: absolute; top: 100%; left: 0; - min-width: 180px; + min-width: 11.25rem; background: white; border: 1px solid #dee2e6; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 0.375rem; + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15); z-index: 1001; - margin-top: 4px; - padding: 4px 0; - backdrop-filter: blur(10px); + margin-top: 0.25rem; + padding: 0.25rem 0; + backdrop-filter: blur(0.625rem); animation: menuFadeIn 0.15s ease-out; } @keyframes menuFadeIn { from { opacity: 0; - transform: translateY(-8px); + transform: translateY(-0.5rem); } to { opacity: 1; @@ -411,15 +411,15 @@ li { display: flex; align-items: center; width: 100%; - padding: 8px 12px; + padding: 0.5rem 0.75rem; border: none; background: transparent; color: #495057; - font-size: 14px; + font-size: 0.875rem; text-align: left; cursor: pointer; transition: all 0.2s ease; - gap: 8px; + gap: 0.5rem; } .toolbar-menu-item:hover:not(.disabled) { @@ -446,8 +446,8 @@ li { display: flex; align-items: center; justify-content: center; - width: 16px; - height: 16px; + width: 1rem; + height: 1rem; flex-shrink: 0; } @@ -456,4 +456,115 @@ li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} \ No newline at end of file +} + +/* Mobile and Responsive Styles */ +@media (max-width: 768px) { + .toolbar { + height: 2.75rem; + padding: 0.375rem 0.75rem; + } + + .toolbar-spacer { + height: 3.0rem; + } + + .toolbar-left, + .toolbar-right, + .toolbar-button-container { + gap: 0.375rem; + } + + .toolbar-separator { + height: 1.25rem; + margin: 0 0.375rem; + } + + .toolbar-button { + width: 2.25rem; + height: 2.25rem; + border-radius: 0.25rem; + } + + .toolbar-button.has-menu { + padding-right: 1rem; + min-width: 2.25rem; + } + + .menu-indicator { + right: 0.1875rem; + font-size: 0.4375rem; + } + + .toolbar-menu { + min-width: 10rem; + margin-top: 0.1875rem; + } + + .toolbar-menu-item { + padding: 0.375rem 0.625rem; + font-size: 0.8125rem; + gap: 0.375rem; + } + + .menu-item-icon { + width: 0.875rem; + height: 0.875rem; + } +} + +@media (max-width: 480px) { + .toolbar { + height: 2.5rem; + padding: 0.25rem 0.5rem; + /*top: auto; + bottom: 0;*/ + } + + .toolbar-spacer { + height: 0; + } + + .toolbar-left, + .toolbar-right, + .toolbar-button-container { + gap: 0.25rem; + } + + .toolbar-separator { + height: 1.125rem; + margin: 0 0.25rem; + } + + .toolbar-button { + width: 2rem; + height: 2rem; + } + + .toolbar-button.has-menu { + padding-right: 0.875rem; + min-width: 2rem; + } + + .menu-indicator { + right: 0.125rem; + font-size: 0.375rem; + } + + .toolbar-menu { + min-width: 8.75rem; + margin-top: 0.125rem; + padding: 0.125rem 0; + } + + .toolbar-menu-item { + padding: 0.3125rem 0.5rem; + font-size: 0.75rem; + gap: 0.25rem; + } + + .menu-item-icon { + width: 0.75rem; + height: 0.75rem; + } +} diff --git a/frontend/views/List.tsx b/frontend/views/List.tsx index 3bdc4bd..f06b3a8 100644 --- a/frontend/views/List.tsx +++ b/frontend/views/List.tsx @@ -55,7 +55,7 @@ const ListWithToolbar = () => { return (
-
+
    {listContext.items.filter(item => !item.__is_deleted).map(item => (