Skip to content

Commit

Permalink
history: multi select
Browse files Browse the repository at this point in the history
  • Loading branch information
shakyShane committed Feb 25, 2025
1 parent b83076b commit e3740f9
Show file tree
Hide file tree
Showing 48 changed files with 2,247 additions and 618 deletions.
21 changes: 19 additions & 2 deletions special-pages/pages/history/app/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ export class Settings {
/**
* @param {object} params
* @param {{name: 'macos' | 'windows'}} [params.platform]
* @param {number} [params.typingDebounce=500] how long to debounce typing in the search field
* @param {number} [params.typingDebounce=100] how long to debounce typing in the search field - default: 100ms
* @param {number} [params.urlDebounce=500] how long to debounce reflecting to the URL? - default: 500ms
*/
constructor({ platform = { name: 'macos' }, typingDebounce = 100 }) {
constructor({ platform = { name: 'macos' }, typingDebounce = 100, urlDebounce = 500 }) {
this.platform = platform;
this.typingDebounce = typingDebounce;
this.urlDebounce = urlDebounce;
}

withPlatformName(name) {
Expand Down Expand Up @@ -35,4 +37,19 @@ export class Settings {
}
return this;
}

/**
* @param {null|undefined|number|string} value
*/
withUrlDebounce(value) {
if (!value) return this;
const input = String(value).trim();
if (input.match(/^\d+$/)) {
return new Settings({
...this,
urlDebounce: parseInt(input, 10),
});
}
return this;
}
}
53 changes: 43 additions & 10 deletions special-pages/pages/history/app/components/App.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,64 @@
import { h } from 'preact';
import cn from 'classnames';
import styles from './App.module.css';
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
import { Header } from './Header.js';
import { Results } from './Results.js';
import { useRef } from 'preact/hooks';
import { useEffect, useRef } from 'preact/hooks';
import { Sidebar } from './Sidebar.js';
import { useGlobalState } from '../global-state/GlobalStateProvider.js';
import { useSelected } from '../global-state/SelectionProvider.js';
import { useGlobalHandlers } from '../global-state/HistoryServiceProvider.js';
import { useData } from '../global-state/DataProvider.js';
import { useResetSelectionsOnQueryChange, useRowInteractions, useSelected } from '../global-state/SelectionProvider.js';
import {
useAuxClickHandler,
useButtonClickHandler,
useContextMenuForEntries,
useContextMenuForTitles,
useGlobalHandlers,
useLinkClickHandler,
} from '../global-state/HistoryServiceProvider.js';
import { useQueryEvents } from '../global-state/QueryProvider.js';

export function App() {
const { isDarkMode } = useEnv();
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
const { ranges, term, results } = useGlobalState();
const { ranges, results } = useData();
const selected = useSelected();

/**
* The following handlers are application-global in nature, so I want them
* to be registered here for visibility
*/
useGlobalHandlers();
useResetSelectionsOnQueryChange();
useQueryEvents();
useLinkClickHandler();
useButtonClickHandler();
useContextMenuForTitles();
useContextMenuForEntries();
useAuxClickHandler();

/**
* onClick can be passed directly to the main container,
* onKeyDown will be observed at the document level.
* todo: can this be resolved if the `main` element is given focus/tab-index?
*/
const { onClick, onKeyDown } = useRowInteractions();

useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('keydown', onKeyDown);
};
}, [onKeyDown]);

return (
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
<header class={styles.header}>
<Header />
</header>
<aside class={styles.aside}>
<Sidebar ranges={ranges} />
</aside>
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
<header class={styles.header}>
<Header />
</header>
<main class={cn(styles.main, styles.customScroller)} data-main-scroller onClick={onClick}>
<Results results={results} selected={selected} />
</main>
</div>
Expand Down
18 changes: 18 additions & 0 deletions special-pages/pages/history/app/components/App.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ body {
font-size: var(--body-font-size);
font-weight: var(--body-font-weight);
line-height: var(--body-line-height);
background-color: var(--history-background-color);
}

.layout {
Expand Down Expand Up @@ -39,3 +40,20 @@ body {
padding-right: 76px;
padding-top: 24px;
}

.customScroller {
overflow-y: scroll;
&::-webkit-scrollbar {
width: 12px;
}

&::-webkit-scrollbar-track {
border-radius: 6px;
}

&::-webkit-scrollbar-thumb {
background: rgb(108, 108, 108);
border: 4px solid var(--history-background-color);
border-radius: 6px;
}
}
6 changes: 5 additions & 1 deletion special-pages/pages/history/app/components/Empty.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ export function Empty() {
const { t } = useTypedTranslation();
return (
<div class={cn(styles.emptyState, styles.emptyStateOffset)}>
<img src="icons/clock.svg" width={128} height={96} alt="" class={styles.emptyStateImage} />
<div class={styles.icons}>
<img src="icons/backdrop.svg" width={128} height={96} alt="" />
<img src="icons/clock.svg" width={60} height={60} alt="" class={styles.forground} />
</div>
<h2 class={styles.emptyTitle}>{t('empty_title')}</h2>
<p class={styles.emptyText}>{t('empty_text')}</p>
</div>
);
}
77 changes: 68 additions & 9 deletions special-pages/pages/history/app/components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,84 @@ import { SearchForm } from './SearchForm.js';
import { Trash } from '../icons/Trash.js';
import { useTypedTranslation } from '../types.js';
import { useQueryContext } from '../global-state/QueryProvider.js';
import { BTN_ACTION_DELETE_ALL } from '../constants.js';
import { useData } from '../global-state/DataProvider.js';
import { useSelected } from '../global-state/SelectionProvider.js';
import { useHistoryServiceDispatch } from '../global-state/HistoryServiceProvider.js';

/**
*/
export function Header() {
const { t } = useTypedTranslation();
const search = useQueryContext();
const term = useComputed(() => search.value.term);
const range = useComputed(() => search.value.range);
const domain = useComputed(() => search.value.domain);
return (
<div class={styles.root}>
<div class={styles.controls}>
<button class={styles.largeButton} data-action={BTN_ACTION_DELETE_ALL}>
<span>{t('delete_all')}</span>
<Trash />
</button>
</div>
<Controls term={term} range={range} />
<div class={styles.search}>
<SearchForm term={term} />
<SearchForm term={term} domain={domain} />
</div>
</div>
);
}

/**
* Renders the Controls component that displays a button for deletion functionality.
*
* @param {Object} props - Properties passed to the component.
* @param {import("@preact/signals").Signal<string|null>} props.term
* @param {import("@preact/signals").Signal<string|null>} props.range
*/
function Controls({ term, range }) {
const { t } = useTypedTranslation();
const { results } = useData();
const selected = useSelected();
const dispatch = useHistoryServiceDispatch();

/**
* Aria labels + title text is derived from the current result set.
*/
const ariaDisabled = useComputed(() => results.value.items.length === 0);
const title = useComputed(() => (results.value.items.length === 0 ? t('delete_none') : ''));

/**
* The button text should alternate between 'delete' and 'delete all' depending on the
* state of the current query. It should only read 'delete all' when the query is the default one
* and there are no selections
*/
const buttonTxt = useComputed(() => {
const hasSelections = selected.value.size > 0;
if (hasSelections) return t('delete_some');
return t('delete_all');
});

/**
* Which action should the delete button take?
*
* - if there are selections, they should be deleted
* - if there's a range selected, that should be deleted
* - or fallback to deleting all
*/
function onClick() {
if (ariaDisabled.value === true) return;
if (selected.value.size > 0) {
return dispatch({ kind: 'delete-entries-by-index', value: [...selected.value] });
}
if (range.value !== null) {
return dispatch({ kind: 'delete-range', value: range.value });
}
if (term.value !== null && term.value !== '') {
return dispatch({ kind: 'delete-term', term: term.value });
}
dispatch({ kind: 'delete-all' });
}

return (
<div class={styles.controls}>
<button class={styles.largeButton} onClick={onClick} aria-disabled={ariaDisabled} title={title}>
<Trash />
<span>{buttonTxt}</span>
</button>
</div>
);
}
47 changes: 45 additions & 2 deletions special-pages/pages/history/app/components/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,64 @@
background-color: var(--color-white-at-12)
}
}

&[aria-disabled="true"] {
opacity: .3;
}
}

.search {
margin-left: auto;
}
.label {
color: inherit;
display: block;
position: relative;
}
.searchIcon {
position: absolute;
display: block;
width: 16px;
height: 16px;
top: 50%;
left: 9px;
transform: translateY(-50%);
color: black;
[data-theme="dark"] & {
color: white;
}
}
.searchInput {
width: 238px;
height: 28px;
border-radius: 6px;
border: 1px solid var(--history-surface-border-color);
padding-left: 9px;
border: 0.5px solid var(--history-surface-border-color);
padding-left: 31px;
padding-right: 9px;
background: inherit;
color: inherit;

[data-theme="dark"] & {
background: var(--color-white-at-6);
border-color: var(--color-white-at-9);
}

&:focus {
outline: none;
box-shadow: 0px 0px 0px 2.5px rgba(87, 151, 237, 0.64), 0px 0px 0px 1px rgba(87, 151, 237, 0.64) inset, 0px 0.5px 0px -0.5px rgba(0, 0, 0, 0.10), 0px 1px 0px -0.5px rgba(0, 0, 0, 0.10);
}

&::-webkit-search-cancel-button {
-webkit-appearance: none;
height: 13px;
width: 13px;
background-image: url("../../public/icons/clear.svg");
background-repeat: no-repeat;
background-position: center center;
cursor: pointer;
}

[data-theme="dark"] &::-webkit-search-cancel-button {
background-image: url("../../public/icons/clear-dark.svg");
}
}
Loading

0 comments on commit e3740f9

Please sign in to comment.