@internetarchive/collection-browser 4.3.0 → 4.3.1-alpha-webdev8257.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/app-root.d.ts +11 -0
  4. package/dist/src/app-root.js +107 -0
  5. package/dist/src/app-root.js.map +1 -1
  6. package/dist/src/collection-browser.d.ts +8 -4
  7. package/dist/src/collection-browser.js +19 -20
  8. package/dist/src/collection-browser.js.map +1 -1
  9. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  10. package/dist/src/manage/manage-bar.d.ts +7 -2
  11. package/dist/src/manage/manage-bar.js +25 -3
  12. package/dist/src/manage/manage-bar.js.map +1 -1
  13. package/dist/src/styles/tile-action-styles.d.ts +14 -0
  14. package/dist/src/styles/tile-action-styles.js +52 -0
  15. package/dist/src/styles/tile-action-styles.js.map +1 -0
  16. package/dist/src/tiles/base-tile-component.d.ts +17 -1
  17. package/dist/src/tiles/base-tile-component.js +48 -1
  18. package/dist/src/tiles/base-tile-component.js.map +1 -1
  19. package/dist/src/tiles/grid/item-tile.js +1 -0
  20. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  21. package/dist/src/tiles/list/tile-list-compact-header.js +66 -46
  22. package/dist/src/tiles/list/tile-list-compact-header.js.map +1 -1
  23. package/dist/src/tiles/list/tile-list-compact.d.ts +1 -1
  24. package/dist/src/tiles/list/tile-list-compact.js +132 -100
  25. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  26. package/dist/src/tiles/list/tile-list.d.ts +1 -1
  27. package/dist/src/tiles/list/tile-list.js +316 -298
  28. package/dist/src/tiles/list/tile-list.js.map +1 -1
  29. package/dist/src/tiles/models.d.ts +14 -0
  30. package/dist/src/tiles/models.js.map +1 -1
  31. package/dist/src/tiles/tile-dispatcher.d.ts +14 -0
  32. package/dist/src/tiles/tile-dispatcher.js +107 -4
  33. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  34. package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
  35. package/dist/test/data-source/collection-browser-data-source.test.js +2 -2
  36. package/dist/test/data-source/collection-browser-data-source.test.js.map +1 -1
  37. package/dist/test/manage/manage-bar.test.d.ts +1 -0
  38. package/dist/test/manage/manage-bar.test.js +123 -1
  39. package/dist/test/manage/manage-bar.test.js.map +1 -1
  40. package/dist/test/tiles/list/tile-list-compact-header.test.js +12 -12
  41. package/dist/test/tiles/list/tile-list-compact-header.test.js.map +1 -1
  42. package/dist/test/tiles/list/tile-list.test.js +134 -134
  43. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  44. package/index.ts +1 -0
  45. package/package.json +1 -1
  46. package/src/app-root.ts +115 -0
  47. package/src/collection-browser.ts +16 -30
  48. package/src/data-source/collection-browser-data-source.ts +1465 -1465
  49. package/src/manage/manage-bar.ts +33 -4
  50. package/src/styles/tile-action-styles.ts +52 -0
  51. package/src/tiles/base-tile-component.ts +57 -1
  52. package/src/tiles/grid/item-tile.ts +1 -0
  53. package/src/tiles/list/tile-list-compact-header.ts +106 -86
  54. package/src/tiles/list/tile-list-compact.ts +273 -239
  55. package/src/tiles/list/tile-list.ts +718 -700
  56. package/src/tiles/models.ts +16 -0
  57. package/src/tiles/tile-dispatcher.ts +114 -4
  58. package/src/tiles/tile-display-value-provider.ts +134 -134
  59. package/test/data-source/collection-browser-data-source.test.ts +193 -193
  60. package/test/manage/manage-bar.test.ts +192 -2
  61. package/test/tiles/list/tile-list-compact-header.test.ts +43 -43
  62. package/test/tiles/list/tile-list.test.ts +576 -576
  63. package/.claude/settings.local.json +0 -11
@@ -6,3 +6,19 @@
6
6
  * - `minimal`: Show neither tile stats nor the text snippets.
7
7
  */
8
8
  export type LayoutType = 'default' | 'stats-only' | 'snippets-only' | 'minimal';
9
+
10
+ /**
11
+ * Describes an action button to render at the bottom of a tile.
12
+ * Styling is controlled via CSS custom properties on the host:
13
+ * - `--tileActionColor` (default: #333)
14
+ * - `--tileActionBg` (default: #fff)
15
+ * - `--tileActionHoverBg` (default: #f0f0f0)
16
+ * - `--tileActionSeparatorColor` (default: #ddd)
17
+ */
18
+ export interface TileAction {
19
+ /** Unique identifier for this action */
20
+ id: string;
21
+
22
+ /** Label text displayed on the button */
23
+ label: string;
24
+ }
@@ -1,5 +1,6 @@
1
1
  import { css, html, nothing, PropertyValues } from 'lit';
2
2
  import { customElement, property, query } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
3
4
  import { ifDefined } from 'lit/directives/if-defined.js';
4
5
  import { msg } from '@lit/localize';
5
6
  import type {
@@ -26,6 +27,7 @@ import {
26
27
  HoverPaneProviderInterface,
27
28
  } from './hover/hover-pane-controller';
28
29
  import { srOnlyStyle } from '../styles/sr-only';
30
+ import { tileActionStyles } from '../styles/tile-action-styles';
29
31
 
30
32
  @customElement('tile-dispatcher')
31
33
  export class TileDispatcher
@@ -37,6 +39,7 @@ export class TileDispatcher
37
39
  /*
38
40
  * Reactive properties inherited from BaseTileComponent:
39
41
  * - model?: TileModel;
42
+ * - tileActions: TileAction[] = [];
40
43
  * - currentWidth?: number;
41
44
  * - currentHeight?: number;
42
45
  * - baseNavigationUrl?: string;
@@ -104,14 +107,20 @@ export class TileDispatcher
104
107
 
105
108
  render() {
106
109
  const isGridMode = this.tileDisplayMode === 'grid';
110
+ const hasTileActions = isGridMode && this.showGridTileActions;
107
111
  const hoverPaneTemplate =
108
112
  this.hoverPaneController?.getTemplate() ?? nothing;
113
+ const containerClasses = classMap({
114
+ hoverable: isGridMode,
115
+ 'has-tile-actions': hasTileActions,
116
+ });
109
117
  return html`
110
- <div id="container" class=${isGridMode ? 'hoverable' : ''}>
118
+ <div id="container" class=${containerClasses}>
111
119
  ${this.tileDisplayMode === 'list-header'
112
120
  ? this.headerTemplate
113
121
  : this.tileTemplate}
114
- ${this.manageCheckTemplate} ${hoverPaneTemplate}
122
+ ${this.gridTileActionsTemplate} ${this.manageCheckTemplate}
123
+ ${hoverPaneTemplate}
115
124
  </div>
116
125
  `;
117
126
  }
@@ -134,6 +143,7 @@ export class TileDispatcher
134
143
  .currentWidth=${currentWidth}
135
144
  .sortParam=${sortParam ?? defaultSortParam}
136
145
  .mobileBreakpoint=${mobileBreakpoint}
146
+ .tileActions=${this.tileActions}
137
147
  >
138
148
  </tile-list-compact-header>
139
149
  `;
@@ -306,6 +316,52 @@ export class TileDispatcher
306
316
  });
307
317
  }
308
318
 
319
+ /** Whether tile action buttons should be rendered in grid mode */
320
+ private get showGridTileActions(): boolean {
321
+ return (
322
+ this.tileActions.length > 0 &&
323
+ !this.isManageView &&
324
+ this.tileDisplayMode === 'grid'
325
+ );
326
+ }
327
+
328
+ /**
329
+ * Template for the grid-mode action buttons. Rendered alongside the inner
330
+ * tile link inside the dispatcher's shadow root so the action buttons can
331
+ * suppress the hover pane on hover.
332
+ */
333
+ private get gridTileActionsTemplate() {
334
+ if (!this.showGridTileActions) return nothing;
335
+
336
+ return html`
337
+ <div
338
+ class="tile-actions grid-tile-actions"
339
+ @mouseenter=${this.handleGridActionsMouseEnter}
340
+ @mousemove=${(e: Event) => e.stopPropagation()}
341
+ >
342
+ ${this.tileActions.map(
343
+ action => html`
344
+ <button
345
+ class="tile-action-btn"
346
+ @click=${(e: Event) => this.handleTileActionClick(e, action)}
347
+ >
348
+ ${action.label}
349
+ </button>
350
+ `,
351
+ )}
352
+ </div>
353
+ `;
354
+ }
355
+
356
+ /**
357
+ * When the mouse enters the grid-mode tile actions area, dispatch a
358
+ * synthetic mouseleave on the host to cancel the hover pane's show timer
359
+ * and hide any visible pane.
360
+ */
361
+ private handleGridActionsMouseEnter = (): void => {
362
+ this.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));
363
+ };
364
+
309
365
  private get tile() {
310
366
  const {
311
367
  model,
@@ -402,6 +458,7 @@ export class TileDispatcher
402
458
  .baseImageUrl=${this.baseImageUrl}
403
459
  .loggedIn=${this.loggedIn}
404
460
  .suppressBlurring=${this.suppressBlurring}
461
+ .tileActions=${this.isManageView ? [] : this.tileActions}
405
462
  ?useLocalTime=${this.useLocalTime}
406
463
  >
407
464
  </tile-list-compact>`;
@@ -420,6 +477,7 @@ export class TileDispatcher
420
477
  .baseImageUrl=${this.baseImageUrl}
421
478
  .loggedIn=${this.loggedIn}
422
479
  .suppressBlurring=${this.suppressBlurring}
480
+ .tileActions=${this.isManageView ? [] : this.tileActions}
423
481
  ?useLocalTime=${this.useLocalTime}
424
482
  >
425
483
  </tile-list>`;
@@ -431,6 +489,7 @@ export class TileDispatcher
431
489
  static get styles() {
432
490
  return [
433
491
  srOnlyStyle,
492
+ tileActionStyles,
434
493
  css`
435
494
  :host {
436
495
  display: block;
@@ -466,8 +525,45 @@ export class TileDispatcher
466
525
  border-radius: 4px;
467
526
  }
468
527
 
469
- #container.hoverable a:focus,
470
- #container.hoverable a:hover {
528
+ /*
529
+ * When tile actions are present, the container takes on the role of
530
+ * the tile's visual card so the tile content and action row appear
531
+ * as a single unified element. The inner tile's own shadow/radius
532
+ * are disabled via CSS variable overrides to avoid visual
533
+ * duplication, and the action row sits as a footer inside the same
534
+ * card.
535
+ */
536
+ #container.has-tile-actions {
537
+ display: flex;
538
+ flex-direction: column;
539
+ overflow: hidden;
540
+ box-shadow: var(--tileShadow, 1px 1px 2px 0);
541
+ --tileBoxShadow: none;
542
+ --tileCornerRadius: 0;
543
+ }
544
+
545
+ #container.has-tile-actions .tile-link {
546
+ flex: 1;
547
+ min-height: 0;
548
+ overflow: hidden;
549
+ border-radius: 0;
550
+ }
551
+
552
+ /* Normal hover shadow lives on the inner anchor for plain tiles */
553
+ #container.hoverable:not(.has-tile-actions) a:focus,
554
+ #container.hoverable:not(.has-tile-actions) a:hover {
555
+ box-shadow: var(
556
+ --tileHoverBoxShadow,
557
+ 0 0 6px 2px rgba(8, 8, 32, 0.8)
558
+ );
559
+ transition: box-shadow 0.1s ease;
560
+ }
561
+
562
+ /*
563
+ * When the container owns the card visuals, the hover shadow needs
564
+ * to move up to the container so it wraps the action row too.
565
+ */
566
+ #container.hoverable.has-tile-actions:hover {
471
567
  box-shadow: var(
472
568
  --tileHoverBoxShadow,
473
569
  0 0 6px 2px rgba(8, 8, 32, 0.8)
@@ -521,6 +617,20 @@ export class TileDispatcher
521
617
  left: -9999px;
522
618
  z-index: 2;
523
619
  }
620
+
621
+ /*
622
+ * Grid-mode action row sits flush against the bottom of the card —
623
+ * the buttons' own borders form the visible bottom edge. The outer
624
+ * buttons get rounded bottom corners to match the container so the
625
+ * red border traces cleanly around the card's bottom corners.
626
+ */
627
+ .grid-tile-actions .tile-action-btn:first-child {
628
+ border-bottom-left-radius: 4px;
629
+ }
630
+
631
+ .grid-tile-actions .tile-action-btn:last-child {
632
+ border-bottom-right-radius: 4px;
633
+ }
524
634
  `,
525
635
  ];
526
636
  }
@@ -1,134 +1,134 @@
1
- import { TemplateResult, html, nothing } from 'lit';
2
- import { msg, str } from '@lit/localize';
3
- import type { SortParam } from '@internetarchive/search-service';
4
- import type { TileModel } from '../models';
5
- import { formatDate } from '../utils/format-date';
6
-
7
- /**
8
- * A class encapsulating shared logic for converting model values into display values
9
- * across different types of tiles.
10
- */
11
- export class TileDisplayValueProvider {
12
- private model?: TileModel;
13
-
14
- private baseNavigationUrl?: string;
15
-
16
- private collectionPagePath?: string;
17
-
18
- private sortParam?: SortParam;
19
-
20
- private creatorFilter?: string;
21
-
22
- constructor(
23
- options: {
24
- model?: TileModel;
25
- baseNavigationUrl?: string;
26
- collectionPagePath?: string;
27
- sortParam?: SortParam;
28
- creatorFilter?: string;
29
- } = {},
30
- ) {
31
- this.model = options.model;
32
- this.baseNavigationUrl = options.baseNavigationUrl;
33
- this.collectionPagePath = options.collectionPagePath ?? '/details/';
34
- this.sortParam = options.sortParam;
35
- this.creatorFilter = options.creatorFilter;
36
- }
37
-
38
- /**
39
- * Examines the creator(s) for the given tile model, returning
40
- * the first creator whose name matches the provided filter
41
- * (or simply the first creator overall if no filter is provided).
42
- */
43
- get firstCreatorMatchingFilter(): string | undefined {
44
- let matchingCreator;
45
-
46
- // If we're filtering by creator initial and have multiple creators, we want
47
- // to surface the first creator who matches the filter.
48
- if (this.creatorFilter && this.model?.creators.length) {
49
- const firstLetter = this.creatorFilter; // This is just to satisfy tsc
50
- matchingCreator = this.model.creators.find(creator =>
51
- // Decompose combining characters first, so that e.g., filtering on E matches É too.
52
- // Then remove anything that isn't strictly alphabetic, since our filters currently
53
- // only handle A-Z. The first such letter (if one exists) is what needs to match.
54
- creator
55
- .normalize('NFD')
56
- .replace(/[^A-Z]+/gi, '')
57
- .toUpperCase()
58
- .startsWith(firstLetter),
59
- );
60
- }
61
-
62
- return matchingCreator ?? this.model?.creator;
63
- }
64
-
65
- /**
66
- * The label indicating what year an account item was created.
67
- * E.g., "Archivist since 2015"
68
- */
69
- get accountLabel(): string {
70
- return this.model?.dateAdded
71
- ? msg(str`Archivist since ${this.model.dateAdded.getFullYear()}`)
72
- : '';
73
- }
74
-
75
- /**
76
- * The readable label for the current sort if it is a type of date sort,
77
- * or the empty string otherwise.
78
- */
79
- get dateLabel(): string {
80
- switch (this.sortParam?.field) {
81
- case 'publicdate':
82
- return msg('Archived');
83
- case 'reviewdate':
84
- return msg('Reviewed');
85
- case 'addeddate':
86
- return msg('Added');
87
- case 'date':
88
- return msg('Published');
89
- default:
90
- return '';
91
- }
92
- }
93
-
94
- /**
95
- * The readable label for the current views column, based on whether
96
- * weekly or all-time views are being shown.
97
- */
98
- get viewsLabel(): string {
99
- return this.sortParam?.field === 'week'
100
- ? msg('Weekly views')
101
- : msg('All-time views');
102
- }
103
-
104
- /**
105
- * Produces a URL pointing at the item page for the given identifier,
106
- * using the current base URL and the correct path based on whether the
107
- * item is specified to be a collection (default false).
108
- */
109
- itemPageUrl(
110
- identifier?: string,
111
- isCollection = false,
112
- ): string | typeof nothing {
113
- if (!identifier || this.baseNavigationUrl == null) return nothing;
114
- const basePath = isCollection ? this.collectionPagePath : '/details/';
115
- return `${this.baseNavigationUrl}${basePath}${identifier}`;
116
- }
117
-
118
- /**
119
- * Produces a template for a link to a single web capture of the given URL and date
120
- */
121
- webArchivesCaptureLink(url: string, date: Date): TemplateResult {
122
- // Convert the date into the format used to identify wayback captures (e.g., '20150102124550')
123
- const captureDateStr = date
124
- .toISOString()
125
- .replace(/[TZ:-]/g, '')
126
- .replace(/\..*/, '');
127
- const captureHref = `https://web.archive.org/web/${captureDateStr}/${encodeURIComponent(
128
- url,
129
- )}`;
130
- const captureText = formatDate(date, 'long');
131
-
132
- return html` <a href=${captureHref}> ${captureText} </a> `;
133
- }
134
- }
1
+ import { TemplateResult, html, nothing } from 'lit';
2
+ import { msg, str } from '@lit/localize';
3
+ import type { SortParam } from '@internetarchive/search-service';
4
+ import type { TileModel } from '../models';
5
+ import { formatDate } from '../utils/format-date';
6
+
7
+ /**
8
+ * A class encapsulating shared logic for converting model values into display values
9
+ * across different types of tiles.
10
+ */
11
+ export class TileDisplayValueProvider {
12
+ private model?: TileModel;
13
+
14
+ private baseNavigationUrl?: string;
15
+
16
+ private collectionPagePath?: string;
17
+
18
+ private sortParam?: SortParam;
19
+
20
+ private creatorFilter?: string;
21
+
22
+ constructor(
23
+ options: {
24
+ model?: TileModel;
25
+ baseNavigationUrl?: string;
26
+ collectionPagePath?: string;
27
+ sortParam?: SortParam;
28
+ creatorFilter?: string;
29
+ } = {},
30
+ ) {
31
+ this.model = options.model;
32
+ this.baseNavigationUrl = options.baseNavigationUrl;
33
+ this.collectionPagePath = options.collectionPagePath ?? '/details/';
34
+ this.sortParam = options.sortParam;
35
+ this.creatorFilter = options.creatorFilter;
36
+ }
37
+
38
+ /**
39
+ * Examines the creator(s) for the given tile model, returning
40
+ * the first creator whose name matches the provided filter
41
+ * (or simply the first creator overall if no filter is provided).
42
+ */
43
+ get firstCreatorMatchingFilter(): string | undefined {
44
+ let matchingCreator;
45
+
46
+ // If we're filtering by creator initial and have multiple creators, we want
47
+ // to surface the first creator who matches the filter.
48
+ if (this.creatorFilter && this.model?.creators.length) {
49
+ const firstLetter = this.creatorFilter; // This is just to satisfy tsc
50
+ matchingCreator = this.model.creators.find(creator =>
51
+ // Decompose combining characters first, so that e.g., filtering on E matches É too.
52
+ // Then remove anything that isn't strictly alphabetic, since our filters currently
53
+ // only handle A-Z. The first such letter (if one exists) is what needs to match.
54
+ creator
55
+ .normalize('NFD')
56
+ .replace(/[^A-Z]+/gi, '')
57
+ .toUpperCase()
58
+ .startsWith(firstLetter),
59
+ );
60
+ }
61
+
62
+ return matchingCreator ?? this.model?.creator;
63
+ }
64
+
65
+ /**
66
+ * The label indicating what year an account item was created.
67
+ * E.g., "Archivist since 2015"
68
+ */
69
+ get accountLabel(): string {
70
+ return this.model?.dateAdded
71
+ ? msg(str`Archivist since ${this.model.dateAdded.getFullYear()}`)
72
+ : '';
73
+ }
74
+
75
+ /**
76
+ * The readable label for the current sort if it is a type of date sort,
77
+ * or the empty string otherwise.
78
+ */
79
+ get dateLabel(): string {
80
+ switch (this.sortParam?.field) {
81
+ case 'publicdate':
82
+ return msg('Archived');
83
+ case 'reviewdate':
84
+ return msg('Reviewed');
85
+ case 'addeddate':
86
+ return msg('Added');
87
+ case 'date':
88
+ return msg('Published');
89
+ default:
90
+ return '';
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The readable label for the current views column, based on whether
96
+ * weekly or all-time views are being shown.
97
+ */
98
+ get viewsLabel(): string {
99
+ return this.sortParam?.field === 'week'
100
+ ? msg('Weekly views')
101
+ : msg('All-time views');
102
+ }
103
+
104
+ /**
105
+ * Produces a URL pointing at the item page for the given identifier,
106
+ * using the current base URL and the correct path based on whether the
107
+ * item is specified to be a collection (default false).
108
+ */
109
+ itemPageUrl(
110
+ identifier?: string,
111
+ isCollection = false,
112
+ ): string | typeof nothing {
113
+ if (!identifier || this.baseNavigationUrl == null) return nothing;
114
+ const basePath = isCollection ? this.collectionPagePath : '/details/';
115
+ return `${this.baseNavigationUrl}${basePath}${identifier}`;
116
+ }
117
+
118
+ /**
119
+ * Produces a template for a link to a single web capture of the given URL and date
120
+ */
121
+ webArchivesCaptureLink(url: string, date: Date): TemplateResult {
122
+ // Convert the date into the format used to identify wayback captures (e.g., '20150102124550')
123
+ const captureDateStr = date
124
+ .toISOString()
125
+ .replace(/[TZ:-]/g, '')
126
+ .replace(/\..*/, '');
127
+ const captureHref = `https://web.archive.org/web/${captureDateStr}/${encodeURIComponent(
128
+ url,
129
+ )}`;
130
+ const captureText = formatDate(date, 'long');
131
+
132
+ return html` <a href=${captureHref}> ${captureText} </a> `;
133
+ }
134
+ }