Skip to content

Commit e3740f9

Browse files
committed
history: multi select
1 parent b83076b commit e3740f9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2247
-618
lines changed

special-pages/pages/history/app/Settings.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ export class Settings {
22
/**
33
* @param {object} params
44
* @param {{name: 'macos' | 'windows'}} [params.platform]
5-
* @param {number} [params.typingDebounce=500] how long to debounce typing in the search field
5+
* @param {number} [params.typingDebounce=100] how long to debounce typing in the search field - default: 100ms
6+
* @param {number} [params.urlDebounce=500] how long to debounce reflecting to the URL? - default: 500ms
67
*/
7-
constructor({ platform = { name: 'macos' }, typingDebounce = 100 }) {
8+
constructor({ platform = { name: 'macos' }, typingDebounce = 100, urlDebounce = 500 }) {
89
this.platform = platform;
910
this.typingDebounce = typingDebounce;
11+
this.urlDebounce = urlDebounce;
1012
}
1113

1214
withPlatformName(name) {
@@ -35,4 +37,19 @@ export class Settings {
3537
}
3638
return this;
3739
}
40+
41+
/**
42+
* @param {null|undefined|number|string} value
43+
*/
44+
withUrlDebounce(value) {
45+
if (!value) return this;
46+
const input = String(value).trim();
47+
if (input.match(/^\d+$/)) {
48+
return new Settings({
49+
...this,
50+
urlDebounce: parseInt(input, 10),
51+
});
52+
}
53+
return this;
54+
}
3855
}

special-pages/pages/history/app/components/App.jsx

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,64 @@
11
import { h } from 'preact';
2+
import cn from 'classnames';
23
import styles from './App.module.css';
34
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
45
import { Header } from './Header.js';
56
import { Results } from './Results.js';
6-
import { useRef } from 'preact/hooks';
7+
import { useEffect, useRef } from 'preact/hooks';
78
import { Sidebar } from './Sidebar.js';
8-
import { useGlobalState } from '../global-state/GlobalStateProvider.js';
9-
import { useSelected } from '../global-state/SelectionProvider.js';
10-
import { useGlobalHandlers } from '../global-state/HistoryServiceProvider.js';
9+
import { useData } from '../global-state/DataProvider.js';
10+
import { useResetSelectionsOnQueryChange, useRowInteractions, useSelected } from '../global-state/SelectionProvider.js';
11+
import {
12+
useAuxClickHandler,
13+
useButtonClickHandler,
14+
useContextMenuForEntries,
15+
useContextMenuForTitles,
16+
useGlobalHandlers,
17+
useLinkClickHandler,
18+
} from '../global-state/HistoryServiceProvider.js';
19+
import { useQueryEvents } from '../global-state/QueryProvider.js';
1120

1221
export function App() {
1322
const { isDarkMode } = useEnv();
14-
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
15-
const { ranges, term, results } = useGlobalState();
23+
const { ranges, results } = useData();
1624
const selected = useSelected();
1725

26+
/**
27+
* The following handlers are application-global in nature, so I want them
28+
* to be registered here for visibility
29+
*/
1830
useGlobalHandlers();
31+
useResetSelectionsOnQueryChange();
32+
useQueryEvents();
33+
useLinkClickHandler();
34+
useButtonClickHandler();
35+
useContextMenuForTitles();
36+
useContextMenuForEntries();
37+
useAuxClickHandler();
38+
39+
/**
40+
* onClick can be passed directly to the main container,
41+
* onKeyDown will be observed at the document level.
42+
* todo: can this be resolved if the `main` element is given focus/tab-index?
43+
*/
44+
const { onClick, onKeyDown } = useRowInteractions();
45+
46+
useEffect(() => {
47+
document.addEventListener('keydown', onKeyDown);
48+
return () => {
49+
document.removeEventListener('keydown', onKeyDown);
50+
};
51+
}, [onKeyDown]);
1952

2053
return (
2154
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
22-
<header class={styles.header}>
23-
<Header />
24-
</header>
2555
<aside class={styles.aside}>
2656
<Sidebar ranges={ranges} />
2757
</aside>
28-
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
58+
<header class={styles.header}>
59+
<Header />
60+
</header>
61+
<main class={cn(styles.main, styles.customScroller)} data-main-scroller onClick={onClick}>
2962
<Results results={results} selected={selected} />
3063
</main>
3164
</div>

special-pages/pages/history/app/components/App.module.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ body {
66
font-size: var(--body-font-size);
77
font-weight: var(--body-font-weight);
88
line-height: var(--body-line-height);
9+
background-color: var(--history-background-color);
910
}
1011

1112
.layout {
@@ -39,3 +40,20 @@ body {
3940
padding-right: 76px;
4041
padding-top: 24px;
4142
}
43+
44+
.customScroller {
45+
overflow-y: scroll;
46+
&::-webkit-scrollbar {
47+
width: 12px;
48+
}
49+
50+
&::-webkit-scrollbar-track {
51+
border-radius: 6px;
52+
}
53+
54+
&::-webkit-scrollbar-thumb {
55+
background: rgb(108, 108, 108);
56+
border: 4px solid var(--history-background-color);
57+
border-radius: 6px;
58+
}
59+
}

special-pages/pages/history/app/components/Empty.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ export function Empty() {
1010
const { t } = useTypedTranslation();
1111
return (
1212
<div class={cn(styles.emptyState, styles.emptyStateOffset)}>
13-
<img src="icons/clock.svg" width={128} height={96} alt="" class={styles.emptyStateImage} />
13+
<div class={styles.icons}>
14+
<img src="icons/backdrop.svg" width={128} height={96} alt="" />
15+
<img src="icons/clock.svg" width={60} height={60} alt="" class={styles.forground} />
16+
</div>
1417
<h2 class={styles.emptyTitle}>{t('empty_title')}</h2>
18+
<p class={styles.emptyText}>{t('empty_text')}</p>
1519
</div>
1620
);
1721
}

special-pages/pages/history/app/components/Header.js

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,84 @@ import { SearchForm } from './SearchForm.js';
55
import { Trash } from '../icons/Trash.js';
66
import { useTypedTranslation } from '../types.js';
77
import { useQueryContext } from '../global-state/QueryProvider.js';
8-
import { BTN_ACTION_DELETE_ALL } from '../constants.js';
8+
import { useData } from '../global-state/DataProvider.js';
9+
import { useSelected } from '../global-state/SelectionProvider.js';
10+
import { useHistoryServiceDispatch } from '../global-state/HistoryServiceProvider.js';
911

1012
/**
1113
*/
1214
export function Header() {
13-
const { t } = useTypedTranslation();
1415
const search = useQueryContext();
1516
const term = useComputed(() => search.value.term);
17+
const range = useComputed(() => search.value.range);
18+
const domain = useComputed(() => search.value.domain);
1619
return (
1720
<div class={styles.root}>
18-
<div class={styles.controls}>
19-
<button class={styles.largeButton} data-action={BTN_ACTION_DELETE_ALL}>
20-
<span>{t('delete_all')}</span>
21-
<Trash />
22-
</button>
23-
</div>
21+
<Controls term={term} range={range} />
2422
<div class={styles.search}>
25-
<SearchForm term={term} />
23+
<SearchForm term={term} domain={domain} />
2624
</div>
2725
</div>
2826
);
2927
}
28+
29+
/**
30+
* Renders the Controls component that displays a button for deletion functionality.
31+
*
32+
* @param {Object} props - Properties passed to the component.
33+
* @param {import("@preact/signals").Signal<string|null>} props.term
34+
* @param {import("@preact/signals").Signal<string|null>} props.range
35+
*/
36+
function Controls({ term, range }) {
37+
const { t } = useTypedTranslation();
38+
const { results } = useData();
39+
const selected = useSelected();
40+
const dispatch = useHistoryServiceDispatch();
41+
42+
/**
43+
* Aria labels + title text is derived from the current result set.
44+
*/
45+
const ariaDisabled = useComputed(() => results.value.items.length === 0);
46+
const title = useComputed(() => (results.value.items.length === 0 ? t('delete_none') : ''));
47+
48+
/**
49+
* The button text should alternate between 'delete' and 'delete all' depending on the
50+
* state of the current query. It should only read 'delete all' when the query is the default one
51+
* and there are no selections
52+
*/
53+
const buttonTxt = useComputed(() => {
54+
const hasSelections = selected.value.size > 0;
55+
if (hasSelections) return t('delete_some');
56+
return t('delete_all');
57+
});
58+
59+
/**
60+
* Which action should the delete button take?
61+
*
62+
* - if there are selections, they should be deleted
63+
* - if there's a range selected, that should be deleted
64+
* - or fallback to deleting all
65+
*/
66+
function onClick() {
67+
if (ariaDisabled.value === true) return;
68+
if (selected.value.size > 0) {
69+
return dispatch({ kind: 'delete-entries-by-index', value: [...selected.value] });
70+
}
71+
if (range.value !== null) {
72+
return dispatch({ kind: 'delete-range', value: range.value });
73+
}
74+
if (term.value !== null && term.value !== '') {
75+
return dispatch({ kind: 'delete-term', term: term.value });
76+
}
77+
dispatch({ kind: 'delete-all' });
78+
}
79+
80+
return (
81+
<div class={styles.controls}>
82+
<button class={styles.largeButton} onClick={onClick} aria-disabled={ariaDisabled} title={title}>
83+
<Trash />
84+
<span>{buttonTxt}</span>
85+
</button>
86+
</div>
87+
);
88+
}

special-pages/pages/history/app/components/Header.module.css

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,64 @@
4242
background-color: var(--color-white-at-12)
4343
}
4444
}
45+
46+
&[aria-disabled="true"] {
47+
opacity: .3;
48+
}
4549
}
4650

4751
.search {
4852
margin-left: auto;
4953
}
54+
.label {
55+
color: inherit;
56+
display: block;
57+
position: relative;
58+
}
59+
.searchIcon {
60+
position: absolute;
61+
display: block;
62+
width: 16px;
63+
height: 16px;
64+
top: 50%;
65+
left: 9px;
66+
transform: translateY(-50%);
67+
color: black;
68+
[data-theme="dark"] & {
69+
color: white;
70+
}
71+
}
5072
.searchInput {
5173
width: 238px;
5274
height: 28px;
5375
border-radius: 6px;
54-
border: 1px solid var(--history-surface-border-color);
55-
padding-left: 9px;
76+
border: 0.5px solid var(--history-surface-border-color);
77+
padding-left: 31px;
5678
padding-right: 9px;
5779
background: inherit;
5880
color: inherit;
81+
82+
[data-theme="dark"] & {
83+
background: var(--color-white-at-6);
84+
border-color: var(--color-white-at-9);
85+
}
86+
5987
&:focus {
6088
outline: none;
89+
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);
90+
}
91+
92+
&::-webkit-search-cancel-button {
93+
-webkit-appearance: none;
94+
height: 13px;
95+
width: 13px;
96+
background-image: url("../../public/icons/clear.svg");
97+
background-repeat: no-repeat;
98+
background-position: center center;
99+
cursor: pointer;
100+
}
101+
102+
[data-theme="dark"] &::-webkit-search-cancel-button {
103+
background-image: url("../../public/icons/clear-dark.svg");
61104
}
62105
}

0 commit comments

Comments
 (0)