@notis_ai/cli 0.2.5 → 0.2.7
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/README.md +33 -9
- package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
- package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
- package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
- package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
- package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
- package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
- package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
- package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
- package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
- package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
- package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
- package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/notis.config.ts +0 -1
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
- package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
- package/dist/scaffolds/notis-journal/app/globals.css +37 -0
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
- package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-journal/components.json +20 -0
- package/dist/scaffolds/notis-journal/index.html +12 -0
- package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
- package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
- package/dist/scaffolds/notis-journal/package.json +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
- package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
- package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
- package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
- package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
- package/dist/scaffolds.json +22 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +43 -5
- package/skills/notis-apps/cli.md +22 -6
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/cli.js +8 -1
- package/src/command-specs/apps.js +307 -36
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +7 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +237 -44
- package/src/runtime/agent-browser.js +204 -21
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +21 -4
- package/src/runtime/app-dev-sessions.js +99 -1
- package/src/runtime/app-platform.js +197 -18
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/output.js +12 -1
- package/src/runtime/profiles.js +72 -16
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +24 -82
- package/template/.harness/index.html.tmpl +128 -6
- package/template/CHANGELOG.md +5 -0
- package/template/app/page.tsx +11 -41
- package/template/notis.config.ts +0 -1
- package/template/package-lock.json +4137 -0
- package/template/package.json +1 -0
- package/template/packages/sdk/package.json +4 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
- package/template/packages/sdk/src/config.ts +74 -0
- package/template/packages/sdk/src/documents.ts +229 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
- package/template/packages/sdk/src/index.ts +30 -1
- package/template/packages/sdk/src/runtime.ts +76 -17
- package/template/packages/sdk/src/styles.css +148 -0
- /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Markdown, useTopBarSearch } from '@notis/sdk';
|
|
5
|
+
import {
|
|
6
|
+
BookOpenTextIcon,
|
|
7
|
+
CaretDownIcon,
|
|
8
|
+
CaretUpIcon,
|
|
9
|
+
ChatCircleDotsIcon,
|
|
10
|
+
FlameIcon,
|
|
11
|
+
LightbulbIcon,
|
|
12
|
+
LightningIcon,
|
|
13
|
+
MoonStarsIcon,
|
|
14
|
+
NotebookIcon,
|
|
15
|
+
QuotesIcon,
|
|
16
|
+
RocketLaunchIcon,
|
|
17
|
+
SparkleIcon,
|
|
18
|
+
StarIcon,
|
|
19
|
+
SunHorizonIcon,
|
|
20
|
+
TargetIcon,
|
|
21
|
+
} from '@phosphor-icons/react';
|
|
22
|
+
|
|
23
|
+
import { Button } from '@/components/ui/button';
|
|
24
|
+
import { cn } from '@/lib/utils';
|
|
25
|
+
import {
|
|
26
|
+
computeStreak,
|
|
27
|
+
dayKey,
|
|
28
|
+
eveningComplete,
|
|
29
|
+
eveningStarted,
|
|
30
|
+
formatDayLong,
|
|
31
|
+
gratitudeCount,
|
|
32
|
+
monthLabel,
|
|
33
|
+
moodStep,
|
|
34
|
+
morningComplete,
|
|
35
|
+
morningStarted,
|
|
36
|
+
railDate,
|
|
37
|
+
relativeDay,
|
|
38
|
+
todayKey,
|
|
39
|
+
useJournalEntries,
|
|
40
|
+
type JournalEntry,
|
|
41
|
+
} from './journal-core';
|
|
42
|
+
import {
|
|
43
|
+
EmptyState,
|
|
44
|
+
LevelTicks,
|
|
45
|
+
LoadingState,
|
|
46
|
+
MoodDot,
|
|
47
|
+
MoodScale,
|
|
48
|
+
PromptBlock,
|
|
49
|
+
RitualChip,
|
|
50
|
+
RitualSection,
|
|
51
|
+
} from './journal-ui';
|
|
52
|
+
|
|
53
|
+
const MORNING_ACCENT = '#f59e0b';
|
|
54
|
+
const EVENING_ACCENT = '#6366f1';
|
|
55
|
+
|
|
56
|
+
export default function JournalPage() {
|
|
57
|
+
const { entries, loading, error } = useJournalEntries();
|
|
58
|
+
|
|
59
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
60
|
+
const [search, setSearch] = useState('');
|
|
61
|
+
|
|
62
|
+
const { setLoading: setSearchLoading } = useTopBarSearch({
|
|
63
|
+
value: search,
|
|
64
|
+
onChange: setSearch,
|
|
65
|
+
placeholder: 'Search your journal…',
|
|
66
|
+
});
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
setSearchLoading(loading);
|
|
69
|
+
}, [loading, setSearchLoading]);
|
|
70
|
+
|
|
71
|
+
const filtered = useMemo(() => {
|
|
72
|
+
const q = search.trim().toLowerCase();
|
|
73
|
+
if (!q) return entries;
|
|
74
|
+
return entries.filter((e) => entrySearchText(e).includes(q));
|
|
75
|
+
}, [entries, search]);
|
|
76
|
+
|
|
77
|
+
// Land on the most recent day.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!loading && filtered.length && !filtered.some((e) => e.id === selectedId)) {
|
|
80
|
+
setSelectedId(filtered[0].id);
|
|
81
|
+
}
|
|
82
|
+
}, [loading, filtered, selectedId]);
|
|
83
|
+
|
|
84
|
+
const selected = useMemo(
|
|
85
|
+
() => filtered.find((e) => e.id === selectedId) ?? null,
|
|
86
|
+
[filtered, selectedId],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const streak = useMemo(() => computeStreak(entries), [entries]);
|
|
90
|
+
const todayEntry = useMemo(
|
|
91
|
+
() => entries.find((e) => dayKey(e.date) === todayKey()) ?? null,
|
|
92
|
+
[entries],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const monthGroups = useMemo(() => {
|
|
96
|
+
const groups: Array<{ label: string; items: JournalEntry[] }> = [];
|
|
97
|
+
for (const entry of filtered) {
|
|
98
|
+
const label = monthLabel(entry.date ?? entry.createdAt);
|
|
99
|
+
const last = groups[groups.length - 1];
|
|
100
|
+
if (last && last.label === label) last.items.push(entry);
|
|
101
|
+
else groups.push({ label, items: [entry] });
|
|
102
|
+
}
|
|
103
|
+
return groups;
|
|
104
|
+
}, [filtered]);
|
|
105
|
+
|
|
106
|
+
const selectedIndex = useMemo(
|
|
107
|
+
() => filtered.findIndex((e) => e.id === selectedId),
|
|
108
|
+
[filtered, selectedId],
|
|
109
|
+
);
|
|
110
|
+
const newer = selectedIndex > 0 ? filtered[selectedIndex - 1] : null;
|
|
111
|
+
const older =
|
|
112
|
+
selectedIndex >= 0 && selectedIndex < filtered.length - 1
|
|
113
|
+
? filtered[selectedIndex + 1]
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
if (loading && !entries.length) {
|
|
117
|
+
return (
|
|
118
|
+
<div data-store-screenshot="journal" className="mx-auto w-full max-w-6xl px-5 py-6 sm:px-8">
|
|
119
|
+
<LoadingState label="Opening your journal…" />
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div data-store-screenshot="journal" className="mx-auto w-full max-w-6xl px-5 py-6 sm:px-8">
|
|
126
|
+
<header className="flex flex-wrap items-end justify-between gap-4">
|
|
127
|
+
<div>
|
|
128
|
+
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
129
|
+
<NotebookIcon size={14} weight="bold" />
|
|
130
|
+
5 Minutes Journal
|
|
131
|
+
</div>
|
|
132
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight">Your daily pages</h1>
|
|
133
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
134
|
+
Written with Notis, one morning and one evening at a time.
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
{streak > 0 ? (
|
|
139
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs font-medium">
|
|
140
|
+
<FlameIcon size={14} weight="fill" style={{ color: '#f59e0b' }} />
|
|
141
|
+
{streak}-day streak
|
|
142
|
+
</span>
|
|
143
|
+
) : null}
|
|
144
|
+
{todayEntry ? (
|
|
145
|
+
<>
|
|
146
|
+
<RitualChip
|
|
147
|
+
icon={SunHorizonIcon}
|
|
148
|
+
label="Morning"
|
|
149
|
+
accent={MORNING_ACCENT}
|
|
150
|
+
state={
|
|
151
|
+
morningComplete(todayEntry)
|
|
152
|
+
? 'complete'
|
|
153
|
+
: morningStarted(todayEntry)
|
|
154
|
+
? 'partial'
|
|
155
|
+
: 'missing'
|
|
156
|
+
}
|
|
157
|
+
/>
|
|
158
|
+
<RitualChip
|
|
159
|
+
icon={MoonStarsIcon}
|
|
160
|
+
label="Evening"
|
|
161
|
+
accent={EVENING_ACCENT}
|
|
162
|
+
state={
|
|
163
|
+
eveningComplete(todayEntry)
|
|
164
|
+
? 'complete'
|
|
165
|
+
: eveningStarted(todayEntry)
|
|
166
|
+
? 'partial'
|
|
167
|
+
: 'missing'
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
<span className="inline-flex items-center gap-1.5 rounded-full border border-dashed border-border px-3 py-1 text-xs text-muted-foreground">
|
|
173
|
+
<ChatCircleDotsIcon size={13} />
|
|
174
|
+
Today starts with your morning check-in
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</header>
|
|
179
|
+
|
|
180
|
+
{error ? (
|
|
181
|
+
<p className="mt-6 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</p>
|
|
182
|
+
) : null}
|
|
183
|
+
|
|
184
|
+
{entries.length === 0 ? (
|
|
185
|
+
<div className="mt-8">
|
|
186
|
+
<EmptyState
|
|
187
|
+
icon={BookOpenTextIcon}
|
|
188
|
+
title="Your journal is waiting for its first page"
|
|
189
|
+
description="Notis writes this journal with you — a short check-in in the morning, a gentle recap at night. Ask Notis to set up your Journal reminders to begin."
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
) : filtered.length === 0 ? (
|
|
193
|
+
<div className="mt-8">
|
|
194
|
+
<EmptyState
|
|
195
|
+
icon={NotebookIcon}
|
|
196
|
+
title="Nothing matches your search"
|
|
197
|
+
description="Try a different word — moods, gratitudes, highlights, and free entries are all searchable."
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
) : (
|
|
201
|
+
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-[248px_minmax(0,1fr)]">
|
|
202
|
+
{/* Timeline rail */}
|
|
203
|
+
<nav aria-label="Journal timeline" className="min-w-0 lg:max-h-[calc(100vh-220px)] lg:overflow-y-auto lg:pr-1">
|
|
204
|
+
<div className="space-y-5">
|
|
205
|
+
{monthGroups.map((group) => (
|
|
206
|
+
<div key={group.label}>
|
|
207
|
+
<p className="px-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
208
|
+
{group.label}
|
|
209
|
+
</p>
|
|
210
|
+
<div className="mt-1.5 space-y-0.5">
|
|
211
|
+
{group.items.map((entry) => (
|
|
212
|
+
<RailRow
|
|
213
|
+
key={entry.id}
|
|
214
|
+
entry={entry}
|
|
215
|
+
selected={entry.id === selectedId}
|
|
216
|
+
onSelect={() => setSelectedId(entry.id)}
|
|
217
|
+
/>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</nav>
|
|
224
|
+
|
|
225
|
+
{/* Day spread */}
|
|
226
|
+
{selected ? (
|
|
227
|
+
<DaySpread
|
|
228
|
+
entry={selected}
|
|
229
|
+
onNewer={newer ? () => setSelectedId(newer.id) : undefined}
|
|
230
|
+
onOlder={older ? () => setSelectedId(older.id) : undefined}
|
|
231
|
+
/>
|
|
232
|
+
) : null}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function RailRow({
|
|
240
|
+
entry,
|
|
241
|
+
selected,
|
|
242
|
+
onSelect,
|
|
243
|
+
}: {
|
|
244
|
+
entry: JournalEntry;
|
|
245
|
+
selected: boolean;
|
|
246
|
+
onSelect: () => void;
|
|
247
|
+
}) {
|
|
248
|
+
const { weekday, day } = railDate(entry.date ?? entry.createdAt);
|
|
249
|
+
const word = entry.dayMoodWord ?? entry.morningMoodWord;
|
|
250
|
+
return (
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={onSelect}
|
|
254
|
+
data-rail-row={dayKey(entry.date) ?? entry.id}
|
|
255
|
+
className={cn(
|
|
256
|
+
'flex w-full items-center gap-3 rounded-lg px-2 py-1.5 text-left transition-colors',
|
|
257
|
+
selected ? 'bg-muted' : 'hover:bg-muted/50',
|
|
258
|
+
)}
|
|
259
|
+
>
|
|
260
|
+
<span className="flex w-9 shrink-0 flex-col items-center rounded-md py-0.5">
|
|
261
|
+
<span className="text-[9px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
262
|
+
{weekday}
|
|
263
|
+
</span>
|
|
264
|
+
<span className="text-sm font-semibold tabular-nums leading-tight">{day}</span>
|
|
265
|
+
</span>
|
|
266
|
+
<span className="min-w-0 flex-1">
|
|
267
|
+
<span className={cn('block truncate text-xs capitalize', word ? 'font-medium' : 'text-muted-foreground')}>
|
|
268
|
+
{word ?? relativeDay(entry.date) ?? 'Entry'}
|
|
269
|
+
</span>
|
|
270
|
+
</span>
|
|
271
|
+
<span className="flex shrink-0 items-center gap-1" aria-hidden>
|
|
272
|
+
<MoodDot value={entry.morningMood} />
|
|
273
|
+
<MoodDot value={entry.dayMood} />
|
|
274
|
+
</span>
|
|
275
|
+
</button>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function DaySpread({
|
|
280
|
+
entry,
|
|
281
|
+
onNewer,
|
|
282
|
+
onOlder,
|
|
283
|
+
}: {
|
|
284
|
+
entry: JournalEntry;
|
|
285
|
+
onNewer?: () => void;
|
|
286
|
+
onOlder?: () => void;
|
|
287
|
+
}) {
|
|
288
|
+
const isToday = dayKey(entry.date) === todayKey();
|
|
289
|
+
const relative = relativeDay(entry.date);
|
|
290
|
+
const missingMorningHint = isToday
|
|
291
|
+
? 'Not captured yet — Notis will pick it up with you.'
|
|
292
|
+
: 'Not captured that day.';
|
|
293
|
+
const missingEveningHint = isToday
|
|
294
|
+
? 'Tonight’s check-in will ask about this.'
|
|
295
|
+
: 'Not captured that day.';
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<article className="min-w-0">
|
|
299
|
+
<header className="flex items-start justify-between gap-3">
|
|
300
|
+
<div>
|
|
301
|
+
<h2 className="text-xl font-semibold tracking-tight">
|
|
302
|
+
{formatDayLong(entry.date ?? entry.createdAt)}
|
|
303
|
+
</h2>
|
|
304
|
+
{relative ? (
|
|
305
|
+
<p className="mt-0.5 text-xs font-medium text-muted-foreground">{relative}</p>
|
|
306
|
+
) : null}
|
|
307
|
+
</div>
|
|
308
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
309
|
+
<Button variant="ghost" size="icon" onClick={onNewer} disabled={!onNewer} aria-label="Newer entry">
|
|
310
|
+
<CaretUpIcon size={16} weight="bold" />
|
|
311
|
+
</Button>
|
|
312
|
+
<Button variant="ghost" size="icon" onClick={onOlder} disabled={!onOlder} aria-label="Older entry">
|
|
313
|
+
<CaretDownIcon size={16} weight="bold" />
|
|
314
|
+
</Button>
|
|
315
|
+
</div>
|
|
316
|
+
</header>
|
|
317
|
+
|
|
318
|
+
<div className="mt-4 space-y-4">
|
|
319
|
+
<RitualSection
|
|
320
|
+
icon={SunHorizonIcon}
|
|
321
|
+
title="Morning"
|
|
322
|
+
accent={MORNING_ACCENT}
|
|
323
|
+
aside={
|
|
324
|
+
entry.morningMood == null && !morningStarted(entry) ? (
|
|
325
|
+
<span className="text-[11px] italic text-muted-foreground/80">{missingMorningHint}</span>
|
|
326
|
+
) : undefined
|
|
327
|
+
}
|
|
328
|
+
>
|
|
329
|
+
<PromptBlock prompt="Waking up, I felt" missingHint={missingMorningHint}>
|
|
330
|
+
{entry.morningMood != null || entry.morningMoodWord ? (
|
|
331
|
+
<MoodScale value={entry.morningMood} word={entry.morningMoodWord} />
|
|
332
|
+
) : null}
|
|
333
|
+
</PromptBlock>
|
|
334
|
+
|
|
335
|
+
{entry.morningFeeling ? (
|
|
336
|
+
<PromptBlock prompt="How I was feeling">
|
|
337
|
+
<p className="text-sm leading-relaxed">{entry.morningFeeling}</p>
|
|
338
|
+
</PromptBlock>
|
|
339
|
+
) : null}
|
|
340
|
+
|
|
341
|
+
<div className="space-y-2.5 rounded-xl bg-muted/40 px-4 py-3.5">
|
|
342
|
+
<LevelTicks label="Energy" value={entry.energy} color="#f59e0b" icon={LightningIcon} />
|
|
343
|
+
<LevelTicks label="Motivation" value={entry.motivation} color="#3b82f6" icon={RocketLaunchIcon} />
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<PromptBlock
|
|
347
|
+
prompt="Three things I’m grateful for"
|
|
348
|
+
missingHint={missingMorningHint}
|
|
349
|
+
>
|
|
350
|
+
{gratitudeCount(entry) > 0 ? (
|
|
351
|
+
<ol className="space-y-1.5">
|
|
352
|
+
{entry.gratitudes.map((text, index) =>
|
|
353
|
+
text ? (
|
|
354
|
+
<li key={index} className="flex items-start gap-2.5">
|
|
355
|
+
<SparkleIcon
|
|
356
|
+
size={14}
|
|
357
|
+
weight="fill"
|
|
358
|
+
className="mt-0.5 shrink-0"
|
|
359
|
+
style={{ color: MORNING_ACCENT }}
|
|
360
|
+
/>
|
|
361
|
+
<span className="text-sm leading-relaxed">{text}</span>
|
|
362
|
+
</li>
|
|
363
|
+
) : null,
|
|
364
|
+
)}
|
|
365
|
+
</ol>
|
|
366
|
+
) : null}
|
|
367
|
+
</PromptBlock>
|
|
368
|
+
|
|
369
|
+
<PromptBlock prompt="What will make today great" missingHint={missingMorningHint}>
|
|
370
|
+
{entry.intention ? (
|
|
371
|
+
<p className="flex items-start gap-2.5 text-sm leading-relaxed">
|
|
372
|
+
<TargetIcon size={14} weight="bold" className="mt-0.5 shrink-0 text-muted-foreground" />
|
|
373
|
+
<span>{entry.intention}</span>
|
|
374
|
+
</p>
|
|
375
|
+
) : null}
|
|
376
|
+
</PromptBlock>
|
|
377
|
+
|
|
378
|
+
{entry.affirmation ? (
|
|
379
|
+
<figure className="rounded-xl px-4 py-4 text-center" style={{ backgroundColor: `${MORNING_ACCENT}14` }}>
|
|
380
|
+
<QuotesIcon size={16} weight="fill" className="mx-auto" style={{ color: MORNING_ACCENT }} />
|
|
381
|
+
<blockquote className="mt-1.5 font-serif text-base italic leading-relaxed">
|
|
382
|
+
{entry.affirmation}
|
|
383
|
+
</blockquote>
|
|
384
|
+
<figcaption className="mt-1 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
|
385
|
+
Daily affirmation
|
|
386
|
+
</figcaption>
|
|
387
|
+
</figure>
|
|
388
|
+
) : (
|
|
389
|
+
<PromptBlock prompt="Daily affirmation" missingHint={missingMorningHint} />
|
|
390
|
+
)}
|
|
391
|
+
</RitualSection>
|
|
392
|
+
|
|
393
|
+
<RitualSection
|
|
394
|
+
icon={MoonStarsIcon}
|
|
395
|
+
title="Evening"
|
|
396
|
+
accent={EVENING_ACCENT}
|
|
397
|
+
aside={
|
|
398
|
+
!eveningStarted(entry) ? (
|
|
399
|
+
<span className="text-[11px] italic text-muted-foreground/80">{missingEveningHint}</span>
|
|
400
|
+
) : undefined
|
|
401
|
+
}
|
|
402
|
+
>
|
|
403
|
+
<PromptBlock prompt="The day felt" missingHint={missingEveningHint}>
|
|
404
|
+
{entry.dayMood != null || entry.dayMoodWord ? (
|
|
405
|
+
<MoodScale value={entry.dayMood} word={entry.dayMoodWord} />
|
|
406
|
+
) : null}
|
|
407
|
+
</PromptBlock>
|
|
408
|
+
|
|
409
|
+
<PromptBlock prompt="Highlight of the day" missingHint={missingEveningHint}>
|
|
410
|
+
{entry.highlight ? (
|
|
411
|
+
<p className="flex items-start gap-2.5 text-sm leading-relaxed">
|
|
412
|
+
<StarIcon size={14} weight="fill" className="mt-0.5 shrink-0" style={{ color: EVENING_ACCENT }} />
|
|
413
|
+
<span>{entry.highlight}</span>
|
|
414
|
+
</p>
|
|
415
|
+
) : null}
|
|
416
|
+
</PromptBlock>
|
|
417
|
+
|
|
418
|
+
<PromptBlock prompt="What today taught me" missingHint={missingEveningHint}>
|
|
419
|
+
{entry.lesson ? (
|
|
420
|
+
<p className="flex items-start gap-2.5 text-sm leading-relaxed">
|
|
421
|
+
<LightbulbIcon size={14} weight="bold" className="mt-0.5 shrink-0 text-muted-foreground" />
|
|
422
|
+
<span>{entry.lesson}</span>
|
|
423
|
+
</p>
|
|
424
|
+
) : null}
|
|
425
|
+
</PromptBlock>
|
|
426
|
+
</RitualSection>
|
|
427
|
+
|
|
428
|
+
<OwnWordsSection entry={entry} isToday={isToday} />
|
|
429
|
+
|
|
430
|
+
<p className="px-1 pb-2 text-center text-[11px] text-muted-foreground/70">
|
|
431
|
+
Want to add or fix something? Just tell Notis — “add to my journal for{' '}
|
|
432
|
+
{relative?.toLowerCase() ?? formatDayLong(entry.date)}…”
|
|
433
|
+
</p>
|
|
434
|
+
</div>
|
|
435
|
+
</article>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** The optional free-form reflection, rendered read-only like every app view. */
|
|
440
|
+
function OwnWordsSection({ entry, isToday }: { entry: JournalEntry; isToday: boolean }) {
|
|
441
|
+
return (
|
|
442
|
+
<RitualSection
|
|
443
|
+
icon={BookOpenTextIcon}
|
|
444
|
+
title="In my own words"
|
|
445
|
+
accent="#64748b"
|
|
446
|
+
aside={
|
|
447
|
+
<span className="text-[11px] italic text-muted-foreground/80">
|
|
448
|
+
Captured in conversation with Notis.
|
|
449
|
+
</span>
|
|
450
|
+
}
|
|
451
|
+
>
|
|
452
|
+
{entry.freeEntry ? (
|
|
453
|
+
<div className="prose prose-sm max-w-none text-sm leading-relaxed [&_p]:my-2">
|
|
454
|
+
<Markdown value={entry.freeEntry} size="sm" />
|
|
455
|
+
</div>
|
|
456
|
+
) : (
|
|
457
|
+
<p className="text-sm italic text-muted-foreground/70">
|
|
458
|
+
{isToday
|
|
459
|
+
? 'Nothing written yet — dictate a few lines to Notis tonight.'
|
|
460
|
+
: 'No free entry that day.'}
|
|
461
|
+
</p>
|
|
462
|
+
)}
|
|
463
|
+
</RitualSection>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function entrySearchText(entry: JournalEntry): string {
|
|
468
|
+
return [
|
|
469
|
+
entry.title,
|
|
470
|
+
entry.morningMoodWord,
|
|
471
|
+
entry.dayMoodWord,
|
|
472
|
+
entry.morningFeeling,
|
|
473
|
+
...entry.gratitudes,
|
|
474
|
+
entry.intention,
|
|
475
|
+
entry.affirmation,
|
|
476
|
+
entry.highlight,
|
|
477
|
+
entry.lesson,
|
|
478
|
+
entry.freeEntry,
|
|
479
|
+
moodStep(entry.morningMood)?.label,
|
|
480
|
+
moodStep(entry.dayMood)?.label,
|
|
481
|
+
]
|
|
482
|
+
.filter(Boolean)
|
|
483
|
+
.join(' ')
|
|
484
|
+
.toLowerCase();
|
|
485
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'border-transparent bg-primary/10 text-primary',
|
|
12
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
13
|
+
outline: 'text-foreground',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: 'default',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
23
|
+
|
|
24
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
25
|
+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
13
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
14
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
15
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
16
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
17
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
18
|
+
},
|
|
19
|
+
size: {
|
|
20
|
+
default: 'h-10 px-4 py-2',
|
|
21
|
+
sm: 'h-9 rounded-md px-3',
|
|
22
|
+
lg: 'h-11 rounded-md px-8',
|
|
23
|
+
icon: 'h-10 w-10',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
variant: 'default',
|
|
28
|
+
size: 'default',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export interface ButtonProps
|
|
34
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
35
|
+
VariantProps<typeof buttonVariants> {
|
|
36
|
+
asChild?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
40
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
41
|
+
const Comp = asChild ? Slot : 'button';
|
|
42
|
+
return (
|
|
43
|
+
<Comp
|
|
44
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
45
|
+
ref={ref}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
Button.displayName = 'Button';
|
|
52
|
+
|
|
53
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn('rounded-2xl border bg-card text-card-foreground shadow-sm', className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
Card.displayName = 'Card';
|
|
15
|
+
|
|
16
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
17
|
+
({ className, ...props }, ref) => (
|
|
18
|
+
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
CardHeader.displayName = 'CardHeader';
|
|
22
|
+
|
|
23
|
+
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
24
|
+
({ className, ...props }, ref) => (
|
|
25
|
+
<h3
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn('text-xl font-semibold leading-none tracking-tight', className)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
CardTitle.displayName = 'CardTitle';
|
|
33
|
+
|
|
34
|
+
const CardDescription = React.forwardRef<
|
|
35
|
+
HTMLParagraphElement,
|
|
36
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
37
|
+
>(({ className, ...props }, ref) => (
|
|
38
|
+
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
39
|
+
));
|
|
40
|
+
CardDescription.displayName = 'CardDescription';
|
|
41
|
+
|
|
42
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
43
|
+
({ className, ...props }, ref) => (
|
|
44
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
CardContent.displayName = 'CardContent';
|
|
48
|
+
|
|
49
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
50
|
+
({ className, ...props }, ref) => (
|
|
51
|
+
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
CardFooter.displayName = 'CardFooter';
|
|
55
|
+
|
|
56
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "tailwind.config.ts",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "zinc",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/hooks"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>AAA Journal — Dev Preview</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/dev-main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|