From b2cd31c5d2f83c97dc07b7ac5ca2601bfd62a919 Mon Sep 17 00:00:00 2001 From: Eric Wertz Date: Wed, 4 Jun 2025 19:25:45 -0400 Subject: [PATCH] Getting everything in order for initial deployment --- backend/Database.h | 47 +++++- backend/ListController.cpp | 230 +++++++++++++--------------- backend/ListController.h | 2 +- frontend/app/AppContext.tsx | 6 + frontend/components/Icons.tsx | 16 +- frontend/components/ListContext.tsx | 8 +- frontend/components/ListItem.tsx | 4 +- frontend/components/Toolbar.tsx | 62 +++++--- frontend/styles.css | 166 +++++++++++++++++--- frontend/views/List.tsx | 5 +- 10 files changed, 365 insertions(+), 181 deletions(-) diff --git a/backend/Database.h b/backend/Database.h index 08a8ae6..c93b583 100644 --- a/backend/Database.h +++ b/backend/Database.h @@ -42,18 +42,28 @@ class CDatabase int lastInsertRowid(); - /*template + template std::string encodeJsonObject(Args&... args) { static_assert(sizeof...(Args) % 2 == 0, "encodeJsonObject requires an even number of arguments"); + std::ostringstream select_str; + std::ostringstream params_str; + + build_encode_sql_object(select_str, params_str, 0, args...); + std::ostringstream sql; - sql << "SELECT json_object("; - build_encode_sql_object(sql, 0, args...); - sql << ") FROM (SELECT ? as json_request)"; + sql << "SELECT json_object(" << select_str.str() << ") FROM (SELECT " << params_str.str() << " as json_request)"; - CDBQuery encode_query = query(sql.str(), json); - encode_query.extract_json_values(args...); - }*/ + CDBQuery encode_query = query(sql.str()); + bind_encode_params(encode_query, 0, args...); + + std::string result; + if( encode_query.fetch_row(result) == SQLITE_ROW ) + { + return result; + } + return "{}"; + } private: sqlite3* _db = nullptr; std::string _db_path; @@ -70,4 +80,27 @@ class CDatabase } void build_extract_sql_select([[maybe_unused]] std::ostringstream& sql, [[maybe_unused]] int i) {}; + + template + void build_encode_sql_object(std::ostringstream& select_str, std::ostringstream& params_str, int i, std::string json_key, [[maybe_unused]] T val, Args&... args) + { + select_str << json_key << ", " << val; + params_str << "? as " << json_key; + if( sizeof...(args) > 0 ) + { + select_str << ", "; + } + build_encode_sql_object(select_str, params_str, i + 1, args...); + } + + void build_encode_sql_object([[maybe_unused]] std::ostringstream& select_str, [[maybe_unused]] std::ostringstream& params_str, [[maybe_unused]] int i) {}; + + template + void bind_encode_params(CDBQuery& query, int i, [[maybe_unused]] std::string json_key, T val, Args&... args) + { + query.bind(i, val); + bind_encode_params(query, i + 1, args...); + } + + void bind_encode_params([[maybe_unused]] CDBQuery& query, [[maybe_unused]] int i) {}; }; \ No newline at end of file diff --git a/backend/ListController.cpp b/backend/ListController.cpp index e68a0da..c2bef17 100644 --- a/backend/ListController.cpp +++ b/backend/ListController.cpp @@ -36,44 +36,120 @@ const ACTIONMAP* CListController::getActions() { return actions; } -// DEPRECATED: This function is kept for backward compatibility but is no longer used -// by the optimized fetch() method. It has O(N) database queries which is inefficient. -std::string CListController::getItemJson(CDatabase* db, const int rowid) { - //Get child items for uuid - const std::string sql = "SELECT child_id FROM itemslink WHERE parent_id = ? AND type = 'listitem_hierarchy'"; - CDBQuery query = db->query(sql, rowid); - - std::string child_array_json = ""; +// Recursive function to build complete item JSON with all nested children +std::string CListController::buildItemWithChildren(CDatabase* db, int rowid) { + // Get the item's basic data and JSON + const std::string item_sql = R"( + SELECT i.uuid, i.created_at, i.updated_at, i.type, i.json + FROM items i + WHERE i.rowid = ? + AND i.type = ? + AND IFNULL(json_extract(i.json, '$.completed_at'), '9999-01-01') > datetime('now', '-12 hours') + )"; + + CDBQuery item_query = db->query(item_sql, rowid, ITEM_TYPE); + std::string uuid, created_at, updated_at, type, json_data; + + if (item_query.fetch_row(uuid, created_at, updated_at, type, json_data) != SQLITE_ROW) { + return ""; // Item filtered out or doesn't exist + } + + // Get child items + const std::string children_sql = R"( + SELECT i.rowid, COALESCE(json_extract(i.json, '$.order'), 0) as item_order + FROM items i + JOIN itemslink il ON i.rowid = il.child_id + WHERE il.parent_id = ? + AND il.type = ? + AND i.type = ? + AND IFNULL(json_extract(i.json, '$.completed_at'), '9999-01-01') > datetime('now', '-12 hours') + ORDER BY item_order ASC + )"; + + CDBQuery children_query = db->query(children_sql, rowid, LINK_TYPE_HIERARCHY, ITEM_TYPE); + + // Build children array efficiently + std::vector children_json; int child_rowid; - while( query.fetch_row( child_rowid ) == SQLITE_ROW ) { - std::string child_json = getItemJson(db, child_rowid); - if( child_array_json.empty() ) { - child_array_json = "[" + child_json; - } else { - child_array_json += "," + child_json; + int child_order; + + while (children_query.fetch_row(child_rowid, child_order) == SQLITE_ROW) { + std::string child_json = buildItemWithChildren(db, child_rowid); + if (!child_json.empty()) { + children_json.push_back(child_json); } } - if( child_array_json.empty() ) { - child_array_json = "[]"; - } else { - child_array_json += "]"; + + // Build final JSON array + std::ostringstream child_builder; + child_builder << "["; + for (size_t i = 0; i < children_json.size(); ++i) { + if (i > 0) child_builder << ","; + child_builder << children_json[i]; + } + child_builder << "]"; + + // Build the final JSON efficiently + std::ostringstream json_builder; + json_builder << "{"; + json_builder << "\"_uuid\":\"" << uuid << "\","; + json_builder << "\"_created_at\":\"" << created_at << "\","; + json_builder << "\"_updated_at\":\"" << updated_at << "\","; + json_builder << "\"_type\":\"" << type << "\","; + json_builder << "\"_children\":" << child_builder.str(); + + // Merge with item's custom JSON data + if (!json_data.empty() && json_data != "{}") { + // Add comma and append the item's JSON fields + json_builder << ","; + + // Extract fields from json_data (skip the opening '{' and closing '}') + std::string item_fields = json_data.substr(1, json_data.length() - 2); + json_builder << item_fields; } + + json_builder << "}"; + return json_builder.str(); +} - const std::string obj_sql = R"(SELECT json_patch( json_object( '_uuid', i.uuid, - '_created_at', i.created_at, - '_updated_at', i.updated_at, - '_type', i.type, - '_children', json(?) - ), json(i.json) ) +CHTTPResponse* CListController::fetch(CHTTPRequest& request) { + CDatabase* db = &request.getUser().getDatabase(); + + // Get root items (items with no parent) + const std::string root_sql = R"( + SELECT i.rowid, COALESCE(json_extract(i.json, '$.order'), 0) as item_order FROM items i - WHERE i.rowid = ? - ORDER BY i.created_at ASC)"; - CDBQuery obj_query = db->query(obj_sql, child_array_json, rowid); - std::string result_json_string; - if( obj_query.fetch_row( result_json_string ) == SQLITE_ROW ) { - return result_json_string; + LEFT JOIN itemslink il ON i.rowid = il.child_id AND il.type = ? + WHERE i.type = ? + AND il.parent_id IS NULL + AND IFNULL(json_extract(i.json, '$.completed_at'), '9999-01-01') > datetime('now', '-12 hours') + ORDER BY item_order ASC + )"; + + CDBQuery root_query = db->query(root_sql, LINK_TYPE_HIERARCHY, ITEM_TYPE); + + // Build result array efficiently + std::vector root_items; + int root_rowid; + int root_order; + + while (root_query.fetch_row(root_rowid, root_order) == SQLITE_ROW) { + std::string item_json = buildItemWithChildren(db, root_rowid); + if (!item_json.empty()) { + root_items.push_back(item_json); + } + } + + // Build final JSON array + std::ostringstream result_builder; + result_builder << "["; + for (size_t i = 0; i < root_items.size(); ++i) { + if (i > 0) result_builder << ","; + result_builder << root_items[i]; } - return "{}"; + result_builder << "]"; + + return new CJSONResponse(request, "success", "List fetched successfully", result_builder.str()); } CHTTPResponse* CListController::save(CHTTPRequest& request) { @@ -241,96 +317,4 @@ CHTTPResponse* CListController::save(CHTTPRequest& request) { try { db->query("ROLLBACK").execute(); } catch (...) {} throw; } -} - -// Alternative ultra-fast implementation using single recursive SQL query -// Uncomment this to use instead of the above method -CHTTPResponse* CListController::fetch(CHTTPRequest& request) { - CDatabase* db = &request.getUser().getDatabase(); - - // Single query approach using recursive CTE and JSON aggregation - const std::string sql = R"( - WITH RECURSIVE item_tree AS ( - -- Anchor: root items (no parent) - SELECT - i.rowid, i.uuid, i.created_at, i.updated_at, i.type, i.json, - 0 as level, - COALESCE(json_extract(i.json, '$.order'), 0) as item_order, - printf('%010d', COALESCE(json_extract(i.json, '$.order'), 0)) as sort_path, - json_patch( - json_object( - '_uuid', i.uuid, - '_created_at', i.created_at, - '_updated_at', i.updated_at, - '_type', i.type, - '_children', json('[]') - ), - COALESCE(json(i.json), json('{}')) - ) as item_json - FROM items i - LEFT JOIN itemslink il ON i.rowid = il.child_id AND il.type = 'listitem_hierarchy' - WHERE i.type = 'listitem' AND il.parent_id IS NULL - AND IFNULL(json_extract(i.json, '$.completed_at'), '9999-01-01') > datetime('now', '-12 hours') - - UNION ALL - - -- Recursive: children - SELECT - child.rowid, child.uuid, child.created_at, child.updated_at, - child.type, child.json, - parent.level + 1, - COALESCE(json_extract(child.json, '$.order'), 0) as item_order, - parent.sort_path || '.' || printf('%010d', COALESCE(json_extract(child.json, '$.order'), 0)) as sort_path, - json_patch( - json_object( - '_uuid', child.uuid, - '_created_at', child.created_at, - '_updated_at', child.updated_at, - '_type', child.type, - '_children', json('[]') - ), - COALESCE(json(child.json), json('{}')) - ) as item_json - FROM items child - JOIN itemslink il ON child.rowid = il.child_id AND il.type = 'listitem_hierarchy' - JOIN item_tree parent ON il.parent_id = parent.rowid - WHERE child.type = 'listitem' - AND IFNULL(json_extract(child.json, '$.completed_at'), '9999-01-01') > datetime('now', '-12 hours') - ), - -- Build hierarchy by updating parent _children arrays - hierarchy AS ( - SELECT - t1.rowid, - t1.level, - t1.created_at, - t1.item_order, - t1.sort_path, - json_set( - t1.item_json, - '$._children', - COALESCE( - (SELECT json_group_array(json(t2.item_json)) - FROM item_tree t2 - JOIN itemslink il ON t2.rowid = il.child_id - WHERE il.parent_id = t1.rowid - AND il.type = 'listitem_hierarchy' - ORDER BY t2.item_order ASC), - json('[]') - ) - ) as final_json - FROM item_tree t1 - ) - SELECT json_group_array(json(final_json)) as result - FROM hierarchy - WHERE level = 0 - ORDER BY sort_path ASC - )"; - - CDBQuery query = db->query(sql); - std::string result_json; - if (query.fetch_row(result_json) == SQLITE_ROW && !result_json.empty()) { - return new CJSONResponse(request, "success", "List fetched successfully", result_json); - } - - return new CJSONResponse(request, "success", "List fetched successfully", "[]"); } \ No newline at end of file diff --git a/backend/ListController.h b/backend/ListController.h index bf329e9..f553b8d 100644 --- a/backend/ListController.h +++ b/backend/ListController.h @@ -18,7 +18,7 @@ private: CHTTPResponse* save(CHTTPRequest& request); CHTTPResponse* fetch(CHTTPRequest& request); - std::string getItemJson(CDatabase* db, const int rowid); + std::string buildItemWithChildren(CDatabase* db, int rowid); const std::string ITEM_TYPE = "listitem"; const std::string LINK_TYPE_HIERARCHY = "listitem_hierarchy"; diff --git a/frontend/app/AppContext.tsx b/frontend/app/AppContext.tsx index 5b56b15..5acbb61 100644 --- a/frontend/app/AppContext.tsx +++ b/frontend/app/AppContext.tsx @@ -16,6 +16,8 @@ interface User { interface AppContextType { isLoading: boolean; setIsLoading: (loading: boolean) => void; + saveStatus: 'idle' | 'queued' | 'saving' | 'saved' | 'error'; + setSaveStatus: (status: 'idle' | 'queued' | 'saving' | 'saved' | 'error') => void; notification: AppNotification; setNotification: (notification: AppNotification) => void; loggedIn: boolean; @@ -39,6 +41,10 @@ export class AppContextProvider extends Component<{ children: any }> { setIsLoading: (loading: boolean) => { this.setState({ isLoading: loading }); }, + saveStatus: 'idle', + setSaveStatus: (status: 'idle' | 'queued' | 'saving' | 'saved' | 'error') => { + this.setState({ saveStatus: status }); + }, notification: { visible: false, message: '', diff --git a/frontend/components/Icons.tsx b/frontend/components/Icons.tsx index 7c0a295..67d0d35 100644 --- a/frontend/components/Icons.tsx +++ b/frontend/components/Icons.tsx @@ -205,4 +205,18 @@ export const MoveDownIcon = ({ size = 24, color = 'currentColor', className = '' > -); \ No newline at end of file +); + +export const CircleIcon = ({ size = 24, color = 'currentColor', className = '' }: IconProps) => ( + + + +); \ No newline at end of file diff --git a/frontend/components/ListContext.tsx b/frontend/components/ListContext.tsx index acbfba8..0c2659a 100644 --- a/frontend/components/ListContext.tsx +++ b/frontend/components/ListContext.tsx @@ -62,7 +62,7 @@ export const useListState = () => { const [focusedItemUuid, setFocusedItemUuid] = useState(null); const [pendingFocusRestore, setPendingFocusRestore] = useState<{ uuid: string, cursorPosition: number } | null>(null); const [saveTimeout, setSaveTimeout] = useState(null); - const { fetchData } = useContext(AppContext); + const { fetchData, setSaveStatus } = useContext(AppContext); // Core tree utilities const findItemInTree = (items: ItemProps[], uuid: string): ItemProps | null => { @@ -180,7 +180,7 @@ export const useListState = () => { if (response?.data?.length > 0) { const itemsWithOrders = ensureItemOrders(response.data); setItems(itemsWithOrders); - setFocusedItemUuid(itemsWithOrders[0]._uuid); + //setFocusedItemUuid(itemsWithOrders[0]._uuid); } else { // Create default empty item const defaultItem: ItemProps = { @@ -245,6 +245,7 @@ export const useListState = () => { const flatItems = flattenItemsForSave(items); if (flatItems.length === 0) return; + setSaveStatus('saving'); const response = await fetch('/list/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -256,11 +257,14 @@ export const useListState = () => { const savedUuids = new Set(flatItems.map(item => item.uuid)); setItems(prevItems => clearDirtyFlags(prevItems, savedUuids)); + setSaveStatus('saved'); } catch (error) { console.error('Failed to save list:', error); + setSaveStatus('error'); } }, 1000); + setSaveStatus('queued'); setSaveTimeout(newTimeout); }; diff --git a/frontend/components/ListItem.tsx b/frontend/components/ListItem.tsx index 46852a2..af4f250 100644 --- a/frontend/components/ListItem.tsx +++ b/frontend/components/ListItem.tsx @@ -3,6 +3,8 @@ import { forwardRef } from 'preact/compat'; import { useRef, useEffect, useImperativeHandle, useState } from 'preact/hooks'; import { getCursorPosition, setCursorPosition } from '../util/util'; +const ITEM_LEVEL_COUNT = 5; + export interface ItemProps { _uuid: string; _type: string; @@ -179,7 +181,7 @@ export const ListItem = forwardRef(({ item, onChange onFocus={handleFocus} onKeyDown={handleKeyDown} data-uuid={item._uuid} - className={`list-item-level-${Math.min(item.level, 9)} ${item.important ? 'list-item-important' : ''} ${item.completed_at ? 'list-item-completed' : ''}`} + className={`list-item-level-${item.level % ITEM_LEVEL_COUNT} ${item.important ? 'list-item-important' : ''} ${item.completed_at ? 'list-item-completed' : ''}`} dangerouslySetInnerHTML={{ __html: item.content }}/> {item._children.filter(child => !child.__is_deleted).length > 0 && (
    diff --git a/frontend/components/Toolbar.tsx b/frontend/components/Toolbar.tsx index 6151803..f1227c6 100644 --- a/frontend/components/Toolbar.tsx +++ b/frontend/components/Toolbar.tsx @@ -2,19 +2,11 @@ import { h, JSX } from 'preact'; import { useContext } from 'preact/hooks'; import { AppContext } from 'app/AppContext'; import LoadingSpinner from './LoadingSpinner'; -import { MenuIcon, CheckIcon } from './Icons'; - -export interface ToolBarButton { - icon: JSX.Element; - onClick: () => void; - title: string; - disabled?: boolean; - active?: boolean; - separatorAfter?: boolean; -} +import { MenuIcon, CheckIcon, CircleIcon } from './Icons'; +import ToolbarButton, { ToolbarButtonProps } from './ToolbarButton'; interface ToolbarProps { - buttons: ToolBarButton[]; + buttons: ToolbarButtonProps[]; onMenuClick?: () => void; } @@ -33,16 +25,41 @@ export const Toolbar = ({ buttons, onMenuClick }: ToolbarProps) => { return (
    + } + onClick={handleMenuClick} + title="Menu" + menuOptions={[ + { + label: 'List', + onClick: () => { + window.location.href = '/list'; + } + }, + { + label: 'Chat', + onClick: () => { + window.location.href = '/chat'; + } + }, + { + label: 'Logout', + onClick: () => { + appContext.logout(); + } + } + ]} + /> +
    {buttons.map((button, index) => (
    - + active={button.active} + /> {button.separatorAfter && (
    )} @@ -51,18 +68,15 @@ export const Toolbar = ({ buttons, onMenuClick }: ToolbarProps) => {
    - {appContext.isLoading ? ( + {appContext.saveStatus === 'saving' ? ( + ) : appContext.saveStatus === 'saved' ? ( + + ) : appContext.saveStatus === 'queued' ? ( + ) : ( )} -
    ); diff --git a/frontend/styles.css b/frontend/styles.css index 9346a02..af21cbd 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1,6 +1,8 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&display=swap'); + /* Base Styles */ body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-family: 'Lato', sans-serif; } h1 { @@ -107,7 +109,7 @@ form button:active { text-align: center; transform: translateY(-100%); animation: slideDown 0.3s ease forwards; - z-index: 1000; + z-index: 2000; border: 1px solid #000; border-radius: 4px; display: flex; @@ -192,7 +194,9 @@ li { } .list-item-important { + font-family: 'Merriweather', serif; font-weight: bold; + font-size: 1.5rem; } .list-item-completed { @@ -200,9 +204,9 @@ li { } li > div[contenteditable] { - padding: 8px 12px; - margin: 2px 0; - border-radius: 4px; + margin: 0.25rem 0; + padding: 0.125rem; + border-radius: 0.5rem; border: 1px solid transparent; transition: all 0.2s ease; min-height: 1.2em; @@ -213,7 +217,7 @@ li > div[contenteditable] { li > div[contenteditable]:focus { border-color: #0074ff; background-color: rgba(0, 116, 255, 0.05); - box-shadow: 0 0 0 2px rgba(0, 116, 255, 0.1); + box-shadow: 0.125rem 0.125rem 0.25rem rgba(0, 116, 255, 0.1); } li > div[contenteditable]:hover { @@ -225,28 +229,42 @@ li { --indent-size: 24px; } -li > div[contenteditable] { - margin-left: calc(var(--level, 0) * var(--indent-size)); +.list-item-level-0 { + font-weight: normal; + text-decoration: underline; } -/* Alternative approach using classes for different levels */ -.list-item-level-0 { margin-left: 0; } -.list-item-level-1 { margin-left: 24px; } -.list-item-level-2 { margin-left: 48px; } -.list-item-level-3 { margin-left: 72px; } -.list-item-level-4 { margin-left: 96px; } -.list-item-level-5 { margin-left: 120px; } -.list-item-level-6 { margin-left: 144px; } -.list-item-level-7 { margin-left: 168px; } -.list-item-level-8 { margin-left: 192px; } -.list-item-level-9 { margin-left: 216px; } +.list-item-level-0.list-item-completed { + text-decoration: line-through underline; +} + +.list-item-level-2 { + font-weight: normal; +} + +.list-item-level-3 { + font-weight: normal; + font-style: italic; +} + +.list-item-level-4 { + font-weight: bold; + font-style: underline; +} + +.list-item-level-5 { + font-weight: bold; + font-style: italic; +} .toolbar { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + 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); border-bottom: 1px solid #dee2e6; position: fixed; top: 0; @@ -282,6 +300,12 @@ li > div[contenteditable] { margin: 0 8px; } +/* Toolbar Button Wrapper for Menu Functionality */ +.toolbar-button-wrapper { + position: relative; + display: inline-block; +} + .toolbar-button { display: flex; align-items: center; @@ -296,6 +320,25 @@ li > div[contenteditable] { transition: all 0.2s ease; padding: 0; margin: 0; + position: relative; +} + +/* Menu indicator for buttons with menus */ +.toolbar-button.has-menu { + padding-right: 18px; + width: auto; + min-width: 40px; +} + +.menu-indicator { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 8px; + line-height: 1; + opacity: 0.7; + pointer-events: none; } .toolbar-button:hover:not(.disabled) { @@ -320,6 +363,10 @@ li > div[contenteditable] { color: white; } +.toolbar-button.active .menu-indicator { + opacity: 1; +} + .toolbar-button.disabled { opacity: 0.4; cursor: not-allowed; @@ -330,4 +377,83 @@ li > div[contenteditable] { background: transparent; border-color: transparent; color: #6c757d; +} + +/* Toolbar Menu Styles */ +.toolbar-menu { + position: absolute; + top: 100%; + left: 0; + min-width: 180px; + background: white; + border: 1px solid #dee2e6; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 1001; + margin-top: 4px; + padding: 4px 0; + backdrop-filter: blur(10px); + animation: menuFadeIn 0.15s ease-out; +} + +@keyframes menuFadeIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.toolbar-menu-item { + display: flex; + align-items: center; + width: 100%; + padding: 8px 12px; + border: none; + background: transparent; + color: #495057; + font-size: 14px; + text-align: left; + cursor: pointer; + transition: all 0.2s ease; + gap: 8px; +} + +.toolbar-menu-item:hover:not(.disabled) { + background: rgba(0, 116, 255, 0.1); + color: #0074ff; +} + +.toolbar-menu-item:active:not(.disabled) { + background: rgba(0, 116, 255, 0.15); +} + +.toolbar-menu-item.disabled { + opacity: 0.4; + cursor: not-allowed; + color: #6c757d; +} + +.toolbar-menu-item.disabled:hover { + background: transparent; + color: #6c757d; +} + +.menu-item-icon { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.menu-item-label { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } \ No newline at end of file diff --git a/frontend/views/List.tsx b/frontend/views/List.tsx index b7704d2..3bdc4bd 100644 --- a/frontend/views/List.tsx +++ b/frontend/views/List.tsx @@ -2,13 +2,14 @@ import { h } from 'preact'; import { useContext } from 'preact/hooks'; import { ListContextProvider, useListState } from 'components/ListContext'; import { ListItem } from 'components/ListItem'; -import { Toolbar, ToolBarButton } from 'components/Toolbar'; +import { Toolbar } from 'components/Toolbar'; +import ToolbarButton, { ToolbarButtonProps } from 'components/ToolbarButton'; import { IndentIcon, OutdentIcon, ImportantIcon, CheckIcon, MoveUpIcon, MoveDownIcon } from 'components/Icons'; const ListWithToolbar = () => { const listContext = useContext(ListContextProvider); - const buttons: ToolBarButton[] = [ + const buttons: ToolbarButtonProps[] = [ { icon: , onClick: listContext.moveUpItem, -- 2.49.0