Skip to content

Commit 6a1b766

Browse files
authored
Merge pull request #26 from nullabork/feature/newhomepage
cards
2 parents 43d37ed + 6e53bed commit 6a1b766

4 files changed

Lines changed: 141 additions & 40 deletions

File tree

changelog/v0.6.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@
1212
- Status bar simplified: removed Auth0 ID and email, kept logout and action buttons
1313
- AG Grid styled to match VS Code dark theme with custom colors, scrollbars, and context menus
1414
- Homepage query showcase replaced screenshot with teal-accented cheatsheet cards
15+
- Tags column uses hash-derived pastel colors with tiny tag icons, memoized per tag string
16+
- Middle-click table rows to open document in a new tab
17+
- Default columns for new users: Ticker, Form, Company, Tags, Description, Date Filed, Size
18+
- Table cell text vertically centered in rows

src/app/components/welcome-modal/changelog.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export const CHANGELOG: ChangelogEntry[] = [
2626
'Status bar simplified: removed Auth0 ID and email, kept logout and action buttons',
2727
'AG Grid styled to match VS Code dark theme with custom colors, scrollbars, and context menus',
2828
'Homepage query showcase replaced screenshot with teal-accented cheatsheet cards',
29+
'Tags column uses hash-derived pastel colors with tiny tag icons, memoized per tag string',
30+
'Middle-click table rows to open document in a new tab',
31+
'Default columns for new users: Ticker, Form, Company, Tags, Description, Date Filed, Size',
32+
'Table cell text vertically centered in rows',
2933
],
3034
},
3135
],

src/app/features/filings/components/filing-results-grid.component.ts

Lines changed: 127 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,45 @@ ModuleRegistry.registerModules([AllCommunityModule]);
3232

3333
const GRID_STATE_KEY = 'filing-grid-column-state';
3434

35+
function hash32(str: string): number {
36+
let h = 2166136261 >>> 0;
37+
for (let i = 0; i < str.length; i++) {
38+
h ^= str.charCodeAt(i);
39+
h = Math.imul(h, 16777619) >>> 0;
40+
}
41+
return h;
42+
}
43+
44+
function mixHash(a: number, b: number): number {
45+
let h = a ^ b;
46+
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b) >>> 0;
47+
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b) >>> 0;
48+
return h ^ (h >>> 16);
49+
}
50+
51+
const tagColorCache = new Map<string, string>();
52+
53+
function tagColor(str: string): string {
54+
const key = str.trim().toLowerCase();
55+
let color = tagColorCache.get(key);
56+
if (color) return color;
57+
58+
const h = mixHash(hash32(key), hash32('14'));
59+
const hue = (h % 360 + 360) % 360;
60+
const sat = 68 + ((h >> 8) % 22);
61+
const lig = 74 + ((h >> 16) % 10);
62+
const s = sat / 100, l = lig / 100;
63+
const a = s * Math.min(l, 1 - l);
64+
const f = (n: number) => {
65+
const k = (n + hue / 30) % 12;
66+
const c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
67+
return Math.round(255 * c).toString(16).padStart(2, '0');
68+
};
69+
color = `#${f(0)}${f(8)}${f(4)}`;
70+
tagColorCache.set(key, color);
71+
return color;
72+
}
73+
3574
interface ColumnInfo {
3675
colId: string;
3776
headerName: string;
@@ -388,6 +427,22 @@ export class FilingResultsGridComponent implements OnInit {
388427
columnList = signal<ColumnInfo[]>([]);
389428
headerContextMenu = signal<{ x: number; y: number; colId: string; headerName: string } | null>(null);
390429

430+
@HostListener('mousedown', ['$event'])
431+
onMouseDown(event: MouseEvent): void {
432+
if (event.button !== 1) return; // only middle click
433+
const row = (event.target as HTMLElement).closest('.ag-row');
434+
if (!row) return;
435+
// Prevent browser auto-scroll and AG Grid's middle-click handling
436+
event.preventDefault();
437+
const rowIndex = row.getAttribute('row-index');
438+
if (rowIndex == null) return;
439+
const rowNode = this.gridApi?.getDisplayedRowAtIndex(Number(rowIndex));
440+
const filing = rowNode?.data as Filing | undefined;
441+
if (filing?.accessionNumber) {
442+
window.open(`/filings/document/${filing.accessionNumber}/1`, '_blank');
443+
}
444+
}
445+
391446
@HostListener('contextmenu', ['$event'])
392447
onHostContextMenu(event: MouseEvent): void {
393448
const headerCell = (event.target as HTMLElement).closest('.ag-header-cell');
@@ -437,6 +492,7 @@ export class FilingResultsGridComponent implements OnInit {
437492

438493
/** Base column definitions — order/widths/visibility may be overridden by saved state */
439494
private readonly baseColumnDefs: ColDef<Filing>[] = [
495+
// --- Default visible columns (in order) ---
440496
{
441497
field: 'ticker',
442498
headerName: 'Ticker',
@@ -450,23 +506,56 @@ export class FilingResultsGridComponent implements OnInit {
450506
return span;
451507
},
452508
},
509+
{
510+
field: 'formType',
511+
headerName: 'Form',
512+
width: 90,
513+
cellStyle: { color: '#61afef', fontWeight: '500' },
514+
},
453515
{
454516
field: 'companyConformedName',
455517
headerName: 'Company',
456518
width: 220,
457519
flex: 1,
458520
},
459521
{
460-
field: 'formType',
461-
headerName: 'Form Type',
462-
width: 110,
522+
field: 'tags',
523+
headerName: 'Tags',
524+
width: 180,
525+
sortable: false,
526+
filter: false,
463527
cellRenderer: (params: ICellRendererParams) => {
464-
if (!params.value) return '';
465-
const span = document.createElement('span');
466-
span.textContent = params.value;
467-
span.style.cssText =
468-
'display:inline-block;padding:1px 6px;font-size:10px;font-weight:500;background:#0e639c;color:white;border-radius:2px;';
469-
return span;
528+
if (!params.value?.length) return '';
529+
const container = document.createElement('div');
530+
container.style.cssText = 'display:flex;align-items:center;gap:6px;flex-wrap:nowrap;overflow:hidden;';
531+
for (const tag of params.value) {
532+
const color = tagColor(tag);
533+
const item = document.createElement('span');
534+
item.style.cssText = `display:inline-flex;align-items:center;gap:3px;white-space:nowrap;color:${color};font-size:12px;`;
535+
// Tiny tag icon (SVG)
536+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
537+
svg.setAttribute('width', '10');
538+
svg.setAttribute('height', '10');
539+
svg.setAttribute('viewBox', '0 0 24 24');
540+
svg.setAttribute('fill', 'none');
541+
svg.setAttribute('stroke', color);
542+
svg.setAttribute('stroke-width', '2.5');
543+
svg.setAttribute('stroke-linecap', 'round');
544+
svg.setAttribute('stroke-linejoin', 'round');
545+
svg.style.cssText = 'flex-shrink:0;';
546+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
547+
path.setAttribute('d', 'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z');
548+
svg.appendChild(path);
549+
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
550+
circle.setAttribute('cx', '7.5');
551+
circle.setAttribute('cy', '7.5');
552+
circle.setAttribute('r', '1');
553+
svg.appendChild(circle);
554+
item.appendChild(svg);
555+
item.appendChild(document.createTextNode(tag));
556+
container.appendChild(item);
557+
}
558+
return container;
470559
},
471560
},
472561
{
@@ -482,128 +571,126 @@ export class FilingResultsGridComponent implements OnInit {
482571
valueFormatter: (params: ValueFormatterParams) =>
483572
params.value ? new Date(params.value).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '',
484573
},
574+
{
575+
field: 'size',
576+
headerName: 'Size',
577+
width: 100,
578+
valueFormatter: (params: ValueFormatterParams) => {
579+
if (!params.value) return '';
580+
const kb = params.value / 1024;
581+
if (kb < 1024) return `${kb.toFixed(0)} KB`;
582+
return `${(kb / 1024).toFixed(1)} MB`;
583+
},
584+
},
585+
// --- Hidden by default ---
485586
{
486587
field: 'datePublished',
487588
headerName: 'Date Published',
488589
width: 130,
590+
hide: true,
489591
valueFormatter: (params: ValueFormatterParams) =>
490592
params.value ? new Date(params.value).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : '',
491593
},
492-
{
493-
field: 'tags',
494-
headerName: 'Tags',
495-
width: 180,
496-
sortable: false,
497-
filter: false,
498-
cellRenderer: (params: ICellRendererParams) => {
499-
if (!params.value?.length) return '';
500-
const container = document.createElement('div');
501-
container.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:nowrap;overflow:hidden;';
502-
for (const tag of params.value) {
503-
const span = document.createElement('span');
504-
span.textContent = tag;
505-
span.style.cssText =
506-
'display:inline-block;padding:1px 6px;font-size:10px;font-weight:500;background:#333333;color:#a0a0a0;border-radius:3px;white-space:nowrap;';
507-
container.appendChild(span);
508-
}
509-
return container;
510-
},
511-
},
512594
{
513595
field: 'accessionNumber',
514596
headerName: 'Accession #',
515597
width: 200,
598+
hide: true,
516599
cellStyle: { fontFamily: "'SF Mono', 'Fira Code', monospace", fontSize: '12px' },
517600
},
518601
{
519602
field: 'centralIndexKey',
520603
headerName: 'CIK',
521604
width: 100,
605+
hide: true,
522606
},
523607
{
524608
field: 'relationship',
525609
headerName: 'Relationship',
526610
width: 130,
527-
},
528-
{
529-
field: 'size',
530-
headerName: 'Size',
531-
width: 100,
532-
valueFormatter: (params: ValueFormatterParams) => {
533-
if (!params.value) return '';
534-
const kb = params.value / 1024;
535-
if (kb < 1024) return `${kb.toFixed(0)} KB`;
536-
return `${(kb / 1024).toFixed(1)} MB`;
537-
},
611+
hide: true,
538612
},
539613
{
540614
field: 'documentCount',
541615
headerName: 'Docs',
542616
width: 80,
617+
hide: true,
543618
},
544619
{
545620
field: 'fileNumber',
546621
headerName: 'File #',
547622
width: 120,
623+
hide: true,
548624
},
549625
{
550626
field: 'filmNumber',
551627
headerName: 'Film #',
552628
width: 120,
629+
hide: true,
553630
},
554631
{
555632
field: 'isAmendment',
556633
headerName: 'Amendment',
557634
width: 110,
635+
hide: true,
558636
valueFormatter: (params: ValueFormatterParams) => (params.value ? 'Yes' : 'No'),
559637
},
560638
{
561639
field: 'isAmended',
562640
headerName: 'Amended',
563641
width: 100,
642+
hide: true,
564643
valueFormatter: (params: ValueFormatterParams) => (params.value ? 'Yes' : 'No'),
565644
},
566645
{
567646
field: 'amendedAccessionNumber',
568647
headerName: 'Amended Accession #',
569648
width: 200,
649+
hide: true,
570650
},
571651
{
572652
field: 'amendmentAccessionNumber',
573653
headerName: 'Amendment Accession #',
574654
width: 200,
655+
hide: true,
575656
},
576657
{
577658
field: 'sponsorCIK',
578659
headerName: 'Sponsor CIK',
579660
width: 120,
661+
hide: true,
580662
},
581663
{
582664
field: 'fileName',
583665
headerName: 'File Name',
584666
width: 180,
667+
hide: true,
585668
},
586669
{
587670
field: 'snowflakeId',
588671
headerName: 'Snowflake ID',
589672
width: 160,
673+
hide: true,
590674
},
591675
{
592676
field: 'noDocument',
593677
headerName: 'No Document',
594678
width: 120,
679+
hide: true,
595680
valueFormatter: (params: ValueFormatterParams) => (params.value ? 'Yes' : 'No'),
596681
},
597682
{
598683
field: 'isEmpty',
599684
headerName: 'Empty',
600685
width: 90,
686+
hide: true,
601687
valueFormatter: (params: ValueFormatterParams) => (params.value ? 'Yes' : 'No'),
602688
},
603689
{
604690
field: 'id',
605691
headerName: 'ID',
606692
width: 80,
693+
hide: true,
607694
},
608695
];
609696

src/styles.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ body {
176176
cursor: pointer;
177177
}
178178

179+
/* AG Grid cell vertical centering */
180+
.ag-cell {
181+
display: flex !important;
182+
align-items: center !important;
183+
}
184+
179185
/* AG Grid header text uppercase */
180186
.ag-header-cell-text {
181187
text-transform: uppercase;

0 commit comments

Comments
 (0)