grok-digest 0.1.1 → 0.3.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.
- package/BOT.md +181 -0
- package/LICENSE-THIRD-PARTY +24 -0
- package/README.md +9 -7
- package/dist/assets/app-icon.svg +9 -0
- package/dist/assets/favicon.svg +1 -1
- package/dist/assets/icon-180.png +0 -0
- package/dist/assets/icon-192.png +0 -0
- package/dist/assets/icon-512.png +0 -0
- package/dist/components/BookmarkCard.astro +131 -43
- package/dist/components/{BookmarkFilters.d.ts → BookmarkControls.d.ts} +1 -1
- package/dist/components/BookmarkControls.js +252 -0
- package/dist/components/ui/card.d.ts +1 -4
- package/dist/components/ui/card.js +1 -10
- package/dist/index.js +14 -1
- package/dist/layouts/Layout.astro +7 -1
- package/dist/lib/bookmarks.d.ts +54 -0
- package/dist/lib/bookmarks.js +12 -3
- package/dist/lib/copy.d.ts +2 -0
- package/dist/lib/copy.js +23 -0
- package/dist/lib/feed.js +8 -3
- package/dist/lib/filter.d.ts +11 -6
- package/dist/lib/filter.js +13 -2
- package/dist/lib/load-bookmarks.d.ts +8 -0
- package/dist/lib/review.d.ts +18 -0
- package/dist/lib/review.js +57 -0
- package/dist/lib/search.d.ts +10 -3
- package/dist/lib/search.js +22 -4
- package/dist/pages/index.astro +13 -5
- package/dist/routes/icon-[size].png.d.ts +10 -0
- package/dist/routes/icon-[size].png.js +10 -0
- package/dist/routes/manifest.webmanifest.d.ts +1 -0
- package/dist/routes/manifest.webmanifest.js +14 -0
- package/dist/styles/global.css +57 -3
- package/dist/styles/shadcn-tailwind.css +529 -0
- package/package.json +5 -4
- package/dist/components/BookmarkFilters.js +0 -32
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const REVIEW_SIZE = 5;
|
|
2
|
+
export type ReviewPick = {
|
|
3
|
+
id: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
};
|
|
6
|
+
type Saved = {
|
|
7
|
+
id: string;
|
|
8
|
+
savedAt: Date;
|
|
9
|
+
shelfLife: "news" | "evergreen";
|
|
10
|
+
};
|
|
11
|
+
export declare function localDay(date: Date, timeZone?: string): string;
|
|
12
|
+
export declare function dailyReview({ bookmarks, done, today, timeZone, }: {
|
|
13
|
+
bookmarks: readonly Saved[];
|
|
14
|
+
done: ReadonlySet<string>;
|
|
15
|
+
today: string;
|
|
16
|
+
timeZone?: string;
|
|
17
|
+
}): ReviewPick[];
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const REVIEW_SIZE = 5;
|
|
2
|
+
const REVIEW_AGES = new Map([
|
|
3
|
+
[1, "Saved yesterday"],
|
|
4
|
+
[7, "Saved a week ago"],
|
|
5
|
+
[30, "Saved a month ago"],
|
|
6
|
+
[90, "Saved 3 months ago"],
|
|
7
|
+
]);
|
|
8
|
+
const NEWS_AGES = new Set([1, 7]);
|
|
9
|
+
const BACKLOG_REASON = "From your backlog";
|
|
10
|
+
const DAY_MS = 86_400_000;
|
|
11
|
+
export function localDay(date, timeZone) {
|
|
12
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
|
|
13
|
+
}
|
|
14
|
+
function daysBetween(from, to) {
|
|
15
|
+
return Math.round((Date.parse(to) - Date.parse(from)) / DAY_MS);
|
|
16
|
+
}
|
|
17
|
+
function anniversaryReason(saved, today) {
|
|
18
|
+
const years = Number(today.slice(0, 4)) - Number(saved.slice(0, 4));
|
|
19
|
+
if (years < 1 || saved.slice(5) !== today.slice(5))
|
|
20
|
+
return undefined;
|
|
21
|
+
return years === 1 ? "Saved a year ago today" : `Saved ${years} years ago today`;
|
|
22
|
+
}
|
|
23
|
+
function hash(text) {
|
|
24
|
+
let value = 0x811c9dc5;
|
|
25
|
+
for (const char of text) {
|
|
26
|
+
value ^= char.codePointAt(0) ?? 0;
|
|
27
|
+
value = Math.imul(value, 0x01000193) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
export function dailyReview({ bookmarks, done, today, timeZone, }) {
|
|
32
|
+
const candidates = bookmarks
|
|
33
|
+
.filter(({ id }) => !done.has(id))
|
|
34
|
+
.map(({ id, savedAt, shelfLife }) => {
|
|
35
|
+
const saved = localDay(savedAt, timeZone);
|
|
36
|
+
const age = daysBetween(saved, today);
|
|
37
|
+
const news = shelfLife === "news";
|
|
38
|
+
const reason = news
|
|
39
|
+
? NEWS_AGES.has(age)
|
|
40
|
+
? REVIEW_AGES.get(age)
|
|
41
|
+
: undefined
|
|
42
|
+
: (REVIEW_AGES.get(age) ?? anniversaryReason(saved, today));
|
|
43
|
+
return { id, age, news, reason };
|
|
44
|
+
})
|
|
45
|
+
.filter(({ age }) => age >= 1);
|
|
46
|
+
const due = candidates
|
|
47
|
+
.flatMap(({ id, age, reason }) => (reason === undefined ? [] : [{ id, age, reason }]))
|
|
48
|
+
.toSorted((a, b) => b.age - a.age)
|
|
49
|
+
.slice(0, REVIEW_SIZE)
|
|
50
|
+
.map(({ id, reason }) => ({ id, reason }));
|
|
51
|
+
const backlog = candidates
|
|
52
|
+
.filter(({ news, reason }) => !news && reason === undefined)
|
|
53
|
+
.toSorted((a, b) => hash(`${today}:${a.id}`) - hash(`${today}:${b.id}`))
|
|
54
|
+
.slice(0, REVIEW_SIZE - due.length)
|
|
55
|
+
.map(({ id }) => ({ id, reason: BACKLOG_REASON }));
|
|
56
|
+
return [...due, ...backlog];
|
|
57
|
+
}
|
package/dist/lib/search.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { Bookmark } from "./bookmarks.ts";
|
|
2
|
+
import type { Filter } from "./filter.ts";
|
|
2
3
|
import { type Topic } from "./topics.ts";
|
|
3
4
|
export type SearchEntry = {
|
|
4
5
|
id: string;
|
|
5
6
|
topic: Topic;
|
|
7
|
+
savedAt: Date;
|
|
8
|
+
shelfLife: Bookmark["shelfLife"];
|
|
6
9
|
text: string;
|
|
7
10
|
};
|
|
8
11
|
export declare function toSearchEntry(bookmark: Bookmark): SearchEntry;
|
|
9
|
-
export
|
|
12
|
+
export type Triage = {
|
|
13
|
+
done: ReadonlySet<string>;
|
|
14
|
+
review: ReadonlySet<string>;
|
|
15
|
+
};
|
|
16
|
+
export declare function matchingIds({ entries, filter: { query, topic, view }, triage, }: {
|
|
10
17
|
entries: readonly SearchEntry[];
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
filter: Filter;
|
|
19
|
+
triage: Triage;
|
|
13
20
|
}): Set<string>;
|
package/dist/lib/search.js
CHANGED
|
@@ -3,17 +3,35 @@ function normalize(text) {
|
|
|
3
3
|
return text.normalize("NFKC").toLowerCase();
|
|
4
4
|
}
|
|
5
5
|
export function toSearchEntry(bookmark) {
|
|
6
|
-
const { id, topic, author, summary, text, keyPoints } = bookmark;
|
|
6
|
+
const { id, topic, savedAt, shelfLife, author, tldr, summary, text, keyPoints, action, links } = bookmark;
|
|
7
7
|
return {
|
|
8
8
|
id,
|
|
9
9
|
topic,
|
|
10
|
-
|
|
10
|
+
savedAt,
|
|
11
|
+
shelfLife,
|
|
12
|
+
text: normalize([
|
|
13
|
+
author.name,
|
|
14
|
+
`@${author.handle}`,
|
|
15
|
+
TOPIC_LABELS[topic],
|
|
16
|
+
tldr,
|
|
17
|
+
summary,
|
|
18
|
+
text,
|
|
19
|
+
...keyPoints,
|
|
20
|
+
action,
|
|
21
|
+
...links.map(({ title }) => title),
|
|
22
|
+
].join("\n")),
|
|
11
23
|
};
|
|
12
24
|
}
|
|
13
|
-
|
|
25
|
+
const IN_VIEW = {
|
|
26
|
+
today: (id, { review }) => review.has(id),
|
|
27
|
+
inbox: (id, { done }) => !done.has(id),
|
|
28
|
+
done: (id, { done }) => done.has(id),
|
|
29
|
+
all: () => true,
|
|
30
|
+
};
|
|
31
|
+
export function matchingIds({ entries, filter: { query, topic, view }, triage, }) {
|
|
14
32
|
const terms = normalize(query).split(/\s+/).filter(Boolean);
|
|
15
33
|
return new Set(entries
|
|
16
34
|
.filter((entry) => topic === null || entry.topic === topic)
|
|
17
|
-
.filter((entry) => terms.every((term) => entry.text.includes(term)))
|
|
35
|
+
.filter((entry) => terms.length === 0 ? IN_VIEW[view](entry.id, triage) : terms.every((term) => entry.text.includes(term)))
|
|
18
36
|
.map((entry) => entry.id));
|
|
19
37
|
}
|
package/dist/pages/index.astro
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import BookmarkCard from "../components/BookmarkCard.astro";
|
|
3
|
-
import {
|
|
3
|
+
import { BookmarkControls } from "../components/BookmarkControls";
|
|
4
4
|
import { ThemeToggle } from "../components/ThemeToggle";
|
|
5
5
|
import Layout from "../layouts/Layout.astro";
|
|
6
6
|
import { bookmarks } from "../lib/load-bookmarks";
|
|
@@ -22,11 +22,19 @@ const designFeedHref = `${base}rss/design.xml`;
|
|
|
22
22
|
<ThemeToggle client:load />
|
|
23
23
|
</header>
|
|
24
24
|
|
|
25
|
+
<noscript>
|
|
26
|
+
<style is:inline>
|
|
27
|
+
[data-bookmark-list] {
|
|
28
|
+
visibility: visible !important;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
31
|
+
</noscript>
|
|
32
|
+
|
|
25
33
|
<main class="flex flex-1 flex-col gap-6 pb-16">
|
|
26
34
|
{bookmarks.length > 0 ? (
|
|
27
35
|
<>
|
|
28
|
-
<
|
|
29
|
-
<ul class="flex flex-col gap-4 sm:gap-5">
|
|
36
|
+
<BookmarkControls client:only="react" entries={entries} />
|
|
37
|
+
<ul data-bookmark-list class="flex flex-col gap-4 not-data-ready:invisible sm:gap-5">
|
|
30
38
|
{bookmarks.map((bookmark) => (
|
|
31
39
|
<BookmarkCard bookmark={bookmark} />
|
|
32
40
|
))}
|
|
@@ -39,11 +47,11 @@ const designFeedHref = `${base}rss/design.xml`;
|
|
|
39
47
|
|
|
40
48
|
<footer class="flex flex-col gap-2 border-t py-8 text-sm text-muted-foreground">
|
|
41
49
|
<p>
|
|
42
|
-
<a class="underline decoration-border underline-offset-4 hover:decoration-
|
|
50
|
+
<a class="underline decoration-border underline-offset-4 hover:decoration-brand" href={feedHref}>
|
|
43
51
|
Follow new bookmarks
|
|
44
52
|
</a>{" "}
|
|
45
53
|
in a feed reader. Each topic has its own feed, such as{" "}
|
|
46
|
-
<a class="underline decoration-border underline-offset-4 hover:decoration-
|
|
54
|
+
<a class="underline decoration-border underline-offset-4 hover:decoration-brand" href={designFeedHref}>
|
|
47
55
|
Design
|
|
48
56
|
</a>
|
|
49
57
|
.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import icon180 from "../assets/icon-180.png?inline";
|
|
2
|
+
import icon192 from "../assets/icon-192.png?inline";
|
|
3
|
+
import icon512 from "../assets/icon-512.png?inline";
|
|
4
|
+
const ICONS = { "180": icon180, "192": icon192, "512": icon512 };
|
|
5
|
+
export const getStaticPaths = () => Object.keys(ICONS).map((size) => ({ params: { size } }));
|
|
6
|
+
export const GET = ({ params }) => {
|
|
7
|
+
const dataUrl = ICONS[params.size ?? ""] ?? "";
|
|
8
|
+
const bytes = Buffer.from(dataUrl.slice(dataUrl.indexOf(",") + 1), "base64");
|
|
9
|
+
return new Response(bytes, { headers: { "Content-Type": "image/png" } });
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const GET: () => Response;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const manifest = {
|
|
2
|
+
name: "Grok Digest",
|
|
3
|
+
short_name: "Digest",
|
|
4
|
+
start_url: "./",
|
|
5
|
+
scope: "./",
|
|
6
|
+
display: "standalone",
|
|
7
|
+
background_color: "#f9f6f1",
|
|
8
|
+
theme_color: "#f9f6f1",
|
|
9
|
+
icons: [
|
|
10
|
+
{ src: "icon-192.png", sizes: "192x192", type: "image/png", purpose: "any maskable" },
|
|
11
|
+
{ src: "icon-512.png", sizes: "512x512", type: "image/png", purpose: "any maskable" },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
export const GET = () => new Response(JSON.stringify(manifest), { headers: { "Content-Type": "application/manifest+json" } });
|
package/dist/styles/global.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
@import "tw-animate-css";
|
|
3
|
-
@import "shadcn
|
|
3
|
+
@import "./shadcn-tailwind.css";
|
|
4
4
|
@import "@fontsource-variable/geist";
|
|
5
5
|
@import "@fontsource-variable/source-serif-4";
|
|
6
6
|
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
--color-card: var(--card);
|
|
31
31
|
--color-foreground: var(--foreground);
|
|
32
32
|
--color-background: var(--background);
|
|
33
|
+
--color-brand: var(--brand);
|
|
34
|
+
--color-brand-ink: var(--brand-ink);
|
|
35
|
+
--color-brand-soft: var(--brand-soft);
|
|
36
|
+
--color-done: var(--done);
|
|
37
|
+
--color-done-foreground: var(--done-foreground);
|
|
38
|
+
--color-topic: var(--topic);
|
|
39
|
+
--color-topic-soft: var(--topic-soft);
|
|
40
|
+
--color-topic-foreground: var(--topic-foreground);
|
|
33
41
|
--radius-sm: calc(var(--radius) * 0.6);
|
|
34
42
|
--radius-md: calc(var(--radius) * 0.8);
|
|
35
43
|
--radius-lg: var(--radius);
|
|
@@ -57,8 +65,13 @@
|
|
|
57
65
|
--destructive: oklch(0.577 0.245 27.325);
|
|
58
66
|
--border: oklch(0.9 0.01 80);
|
|
59
67
|
--input: oklch(0.88 0.01 80);
|
|
60
|
-
--ring: oklch(0.
|
|
68
|
+
--ring: oklch(0.76 0.15 70);
|
|
61
69
|
--radius: 0.625rem;
|
|
70
|
+
--brand: oklch(0.76 0.15 70);
|
|
71
|
+
--brand-ink: oklch(0.5 0.11 60);
|
|
72
|
+
--brand-soft: oklch(0.955 0.04 85);
|
|
73
|
+
--done: oklch(0.52 0.12 155);
|
|
74
|
+
--done-foreground: oklch(0.985 0.004 85);
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
.dark {
|
|
@@ -79,7 +92,48 @@
|
|
|
79
92
|
--destructive: oklch(0.704 0.191 22.216);
|
|
80
93
|
--border: oklch(1 0 0 / 9%);
|
|
81
94
|
--input: oklch(1 0 0 / 14%);
|
|
82
|
-
--ring: oklch(0.
|
|
95
|
+
--ring: oklch(0.78 0.14 72);
|
|
96
|
+
--brand: oklch(0.78 0.14 72);
|
|
97
|
+
--brand-ink: oklch(0.82 0.12 78);
|
|
98
|
+
--brand-soft: oklch(0.28 0.04 72);
|
|
99
|
+
--done: oklch(0.76 0.13 155);
|
|
100
|
+
--done-foreground: oklch(0.2 0.008 60);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[data-topic] {
|
|
104
|
+
--topic-chroma: 1;
|
|
105
|
+
--topic: oklch(0.5 calc(0.11 * var(--topic-chroma)) var(--topic-hue));
|
|
106
|
+
--topic-soft: oklch(0.95 calc(0.025 * var(--topic-chroma)) var(--topic-hue));
|
|
107
|
+
--topic-foreground: oklch(0.985 0.004 85);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dark [data-topic] {
|
|
111
|
+
--topic: oklch(0.8 calc(0.1 * var(--topic-chroma)) var(--topic-hue));
|
|
112
|
+
--topic-soft: oklch(0.3 calc(0.045 * var(--topic-chroma)) var(--topic-hue));
|
|
113
|
+
--topic-foreground: oklch(0.2 0.008 60);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[data-topic="ai-agents"] {
|
|
117
|
+
--topic-hue: 295;
|
|
118
|
+
}
|
|
119
|
+
[data-topic="models-releases"] {
|
|
120
|
+
--topic-hue: 250;
|
|
121
|
+
}
|
|
122
|
+
[data-topic="dev-tools"] {
|
|
123
|
+
--topic-hue: 195;
|
|
124
|
+
}
|
|
125
|
+
[data-topic="startups-business"] {
|
|
126
|
+
--topic-hue: 120;
|
|
127
|
+
}
|
|
128
|
+
[data-topic="japan-tech"] {
|
|
129
|
+
--topic-hue: 20;
|
|
130
|
+
}
|
|
131
|
+
[data-topic="design"] {
|
|
132
|
+
--topic-hue: 340;
|
|
133
|
+
}
|
|
134
|
+
[data-topic="other"] {
|
|
135
|
+
--topic-hue: 70;
|
|
136
|
+
--topic-chroma: 0.15;
|
|
83
137
|
}
|
|
84
138
|
|
|
85
139
|
@layer base {
|