@notis_ai/cli 0.2.4 → 0.2.6
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 +47 -5
- package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -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 +325 -543
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
- package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -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 +11 -0
- package/package.json +3 -3
- package/skills/notis-apps/SKILL.md +64 -6
- package/skills/notis-apps/cli.md +38 -2
- package/skills/notis-cli/SKILL.md +20 -2
- package/skills/notis-query/cli.md +1 -1
- package/src/command-specs/apps.js +529 -16
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/meta.js +8 -2
- package/src/command-specs/tools.js +50 -37
- package/src/runtime/agent-browser.js +273 -1
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-dev-server.js +113 -6
- package/src/runtime/app-dev-sessions.js +176 -12
- package/src/runtime/app-platform.js +200 -39
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/desktop-auth.js +93 -0
- package/src/runtime/profiles.js +37 -15
- package/src/runtime/store-screenshot.js +138 -0
- package/src/runtime/transport.js +21 -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/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/template/metadata/cover.png +0 -0
- package/template/packages/sdk/src/hooks/useDatabase.ts +0 -76
package/README.md
CHANGED
|
@@ -105,9 +105,9 @@ Examples:
|
|
|
105
105
|
|
|
106
106
|
### `npx --package @notis_ai/cli@latest -- notis apps verify [dir]`
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Validate every route and the production Store listing contract.
|
|
109
109
|
|
|
110
|
-
When to use: After notis apps
|
|
110
|
+
When to use: After notis apps screenshot, before deploy. Catches render-time crashes, missing runtime calls, and incomplete listing media that the build step cannot detect.
|
|
111
111
|
|
|
112
112
|
Options:
|
|
113
113
|
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
@@ -123,6 +123,28 @@ Examples:
|
|
|
123
123
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --mode live`
|
|
124
124
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --no-browser # start the harness, drive agent-browser yourself`
|
|
125
125
|
|
|
126
|
+
### `npx --package @notis_ai/cli@latest -- notis apps screenshot [dir]`
|
|
127
|
+
|
|
128
|
+
Capture configured listing route/scenario states via the headless harness.
|
|
129
|
+
|
|
130
|
+
When to use: Generate the 3–6 declared metadata/screenshot-N.png files for the App Store listing. Apps are icon-led (like Raycast) — there is no cover image, only these screenshots. Each screenshot may set a focus selector to remove empty canvas and a light or dark theme that also controls its Store frame. Run before notis apps verify / deploy / publish.
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
- `--routes <slugs>` — Comma-separated route slugs. Default: every configured screenshot state.
|
|
134
|
+
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
135
|
+
- `--width <px>` — Viewport width. Default: 2000.
|
|
136
|
+
- `--height <px>` — Viewport height. Default: 1250 (16:10).
|
|
137
|
+
- `--output-dir <dir>` — Where to write screenshot-N.png. Default: metadata/.
|
|
138
|
+
- `--mode <mode>` — stub | live. Default stub. Live renders against real data via the CLI JWT (requires a linked app), so screenshots show actual content instead of empty states.
|
|
139
|
+
- `--raw` — Write the unframed harness capture instead of the default Store presentation.
|
|
140
|
+
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
- `npx --package @notis_ai/cli@latest -- notis apps screenshot # honors notis.config.ts screenshot scenarios`
|
|
144
|
+
- `npx --package @notis_ai/cli@latest -- notis apps screenshot --routes home,history`
|
|
145
|
+
- `npx --package @notis_ai/cli@latest -- notis apps screenshot --mode live # populated screenshots from real data`
|
|
146
|
+
- `npx --package @notis_ai/cli@latest -- notis apps screenshot --raw # diagnostic capture without Store framing`
|
|
147
|
+
|
|
126
148
|
### `npx --package @notis_ai/cli@latest -- notis apps link <app-id> [dir]`
|
|
127
149
|
|
|
128
150
|
Link a local project to a remote Notis app.
|
|
@@ -164,6 +186,20 @@ Examples:
|
|
|
164
186
|
- `npx --package @notis_ai/cli@latest -- notis apps deploy --app-id abc123`
|
|
165
187
|
- `npx --package @notis_ai/cli@latest -- notis apps deploy --direct`
|
|
166
188
|
|
|
189
|
+
### `npx --package @notis_ai/cli@latest -- notis apps publish [dir]`
|
|
190
|
+
|
|
191
|
+
Submit the deployed app for Store review.
|
|
192
|
+
|
|
193
|
+
When to use: After the user explicitly confirms the App Details page and Store listing are ready. Requires the current local project to match the latest deployed version.
|
|
194
|
+
|
|
195
|
+
Options:
|
|
196
|
+
- `--app-id <id>` — Override linked app ID.
|
|
197
|
+
- `--confirm-ready` — Confirm the user approved the current App Details page for Store submission.
|
|
198
|
+
|
|
199
|
+
Examples:
|
|
200
|
+
- `npx --package @notis_ai/cli@latest -- notis apps publish --confirm-ready`
|
|
201
|
+
- `npx --package @notis_ai/cli@latest -- notis apps publish ./my-app --confirm-ready`
|
|
202
|
+
|
|
167
203
|
### `npx --package @notis_ai/cli@latest -- notis apps doctor [dir]`
|
|
168
204
|
|
|
169
205
|
Check project health and readiness.
|
|
@@ -245,13 +281,19 @@ Examples:
|
|
|
245
281
|
|
|
246
282
|
### `npx --package @notis_ai/cli@latest -- notis tools link <toolkit>`
|
|
247
283
|
|
|
248
|
-
|
|
284
|
+
Connect or reconnect an integration toolkit.
|
|
249
285
|
|
|
250
|
-
When to use: Use this when a tool requires authentication
|
|
286
|
+
When to use: Use this when a tool requires authentication or an active connection must be replaced.
|
|
287
|
+
|
|
288
|
+
Options:
|
|
289
|
+
- `--reconnect` — Replace the existing account instead of adding another connection.
|
|
290
|
+
- `--connection-id <id>` — Exact connection id to replace when multiple accounts exist.
|
|
291
|
+
- `--label <label>` — Account label for a new or replacement connection.
|
|
292
|
+
- `--credentials <json>` — Credential JSON object, @file path, or - for stdin. Prefer stdin so secrets do not enter shell history.
|
|
251
293
|
|
|
252
294
|
Examples:
|
|
253
295
|
- `npx --package @notis_ai/cli@latest -- notis tools link github`
|
|
254
|
-
- `npx --package @notis_ai/cli@latest -- notis tools link
|
|
296
|
+
- `npx --package @notis_ai/cli@latest -- notis tools link dataforseo --reconnect --credentials - < credentials.json`
|
|
255
297
|
|
|
256
298
|
|
|
257
299
|
## Meta Commands
|
|
Binary file
|
|
@@ -8,7 +8,6 @@ export default defineNotisApp({
|
|
|
8
8
|
author: { name: 'Notis' },
|
|
9
9
|
categories: ['Product & Engineering', 'Operations'],
|
|
10
10
|
tagline: 'Explore every database and schema in your Notis workspace.',
|
|
11
|
-
versionNotes: 'Initial scaffold release.',
|
|
12
11
|
databases: [],
|
|
13
12
|
routes: [
|
|
14
13
|
{
|
|
@@ -503,6 +503,7 @@ export function installMockRuntime(): NotisRuntime {
|
|
|
503
503
|
default: true,
|
|
504
504
|
collection: null,
|
|
505
505
|
},
|
|
506
|
+
databases: [],
|
|
506
507
|
context: {},
|
|
507
508
|
listTools: async () => [
|
|
508
509
|
{ name: 'LOCAL_NOTIS_DATABASE_LIST_DATABASES', inputSchema: { type: 'object', properties: {} } },
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 5 Minutes Journal Changelog
|
|
2
|
+
|
|
3
|
+
## [Package Details and History] - 2026-07-17
|
|
4
|
+
|
|
5
|
+
- Added complete package release history to App Details.
|
|
6
|
+
- Refreshed the light and dark Store screenshots for the latest release.
|
|
7
|
+
|
|
8
|
+
## [Publisher Attribution] - 2026-07-16
|
|
9
|
+
|
|
10
|
+
- Shows Florian (Flo) Pariset as the Store publisher.
|
|
11
|
+
|
|
12
|
+
## [Theme-Aware Store Screenshots] - 2026-07-16
|
|
13
|
+
|
|
14
|
+
- Added matching light and dark screenshots with the shared deep-blue background.
|
|
15
|
+
- Removed the screenshot shadow and white edge artifacts.
|
|
16
|
+
|
|
17
|
+
## [5 Minutes Journal Redesign] - 2026-07-16
|
|
18
|
+
|
|
19
|
+
- Renamed Journal to 5 Minutes Journal.
|
|
20
|
+
- Added focused morning and evening check-in views plus mood, energy, motivation, ritual-consistency, and gratitude statistics.
|
|
21
|
+
- Added guided onboarding for the morning and evening check-in routine.
|
|
22
|
+
|
|
23
|
+
## [Initial Journal] - 2026-07-14
|
|
24
|
+
|
|
25
|
+
- Added a browse-first Journal experience powered by entries created through conversations with Notis.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* The portal mounts the app inside [data-notis-app-root]; let it fill the width. */
|
|
6
|
+
[data-notis-app-root] {
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Dev-preview fallback tokens. Active only under [data-dev-preview], which the
|
|
12
|
+
* standalone dev entrypoint sets. The production bundle runs inside the portal
|
|
13
|
+
* shadow root where the host injects real theme tokens, so this rule is scoped
|
|
14
|
+
* narrowly enough to never collide with portal-provided values.
|
|
15
|
+
*/
|
|
16
|
+
[data-dev-preview] {
|
|
17
|
+
--background: 0 0% 100%;
|
|
18
|
+
--foreground: 222.2 84% 4.9%;
|
|
19
|
+
--card: 0 0% 100%;
|
|
20
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
21
|
+
--popover: 0 0% 100%;
|
|
22
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
23
|
+
--primary: 222.2 47.4% 11.2%;
|
|
24
|
+
--primary-foreground: 210 40% 98%;
|
|
25
|
+
--secondary: 210 40% 96.1%;
|
|
26
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
27
|
+
--muted: 210 40% 96.1%;
|
|
28
|
+
--muted-foreground: 0 0% 46.9%;
|
|
29
|
+
--accent: 210 40% 96.1%;
|
|
30
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
31
|
+
--destructive: 0 84.2% 60.2%;
|
|
32
|
+
--destructive-foreground: 210 40% 98%;
|
|
33
|
+
--border: 214.3 31.8% 91.4%;
|
|
34
|
+
--input: 214.3 31.8% 91.4%;
|
|
35
|
+
--ring: 222.2 84% 4.9%;
|
|
36
|
+
--radius: 0.5rem;
|
|
37
|
+
}
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useNotisNavigation } from '@notis/sdk';
|
|
5
|
+
import {
|
|
6
|
+
ChartLineUpIcon,
|
|
7
|
+
FlameIcon,
|
|
8
|
+
LightningIcon,
|
|
9
|
+
MoonStarsIcon,
|
|
10
|
+
NotebookIcon,
|
|
11
|
+
RocketLaunchIcon,
|
|
12
|
+
SparkleIcon,
|
|
13
|
+
SunHorizonIcon,
|
|
14
|
+
} from '@phosphor-icons/react';
|
|
15
|
+
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { cn } from '@/lib/utils';
|
|
18
|
+
import {
|
|
19
|
+
DAY_MOOD_COLOR,
|
|
20
|
+
ENERGY_COLOR,
|
|
21
|
+
MOOD_SCALE,
|
|
22
|
+
MORNING_MOOD_COLOR,
|
|
23
|
+
MOTIVATION_COLOR,
|
|
24
|
+
average,
|
|
25
|
+
computeStreak,
|
|
26
|
+
dayKey,
|
|
27
|
+
eveningComplete,
|
|
28
|
+
formatDay,
|
|
29
|
+
moodStep,
|
|
30
|
+
morningComplete,
|
|
31
|
+
recentGratitudes,
|
|
32
|
+
round1,
|
|
33
|
+
topMoodWords,
|
|
34
|
+
useJournalEntries,
|
|
35
|
+
type JournalEntry,
|
|
36
|
+
} from '../journal-core';
|
|
37
|
+
import { EmptyState, LoadingState, SectionCard, StatTile } from '../journal-ui';
|
|
38
|
+
|
|
39
|
+
export default function StatsPage() {
|
|
40
|
+
const { entries, loading, error } = useJournalEntries();
|
|
41
|
+
const navigation = useNotisNavigation();
|
|
42
|
+
|
|
43
|
+
const chrono = useMemo(() => [...entries].reverse(), [entries]); // oldest -> newest
|
|
44
|
+
const recent = useMemo(() => chrono.slice(-21), [chrono]); // last ~3 weeks for trends
|
|
45
|
+
|
|
46
|
+
const streak = useMemo(() => computeStreak(entries), [entries]);
|
|
47
|
+
const totals = useMemo(
|
|
48
|
+
() => ({
|
|
49
|
+
morningMood: average(entries.map((e) => e.morningMood)),
|
|
50
|
+
dayMood: average(entries.map((e) => e.dayMood)),
|
|
51
|
+
energy: average(entries.map((e) => e.energy)),
|
|
52
|
+
motivation: average(entries.map((e) => e.motivation)),
|
|
53
|
+
}),
|
|
54
|
+
[entries],
|
|
55
|
+
);
|
|
56
|
+
const lift = useMemo(() => moodLift(entries), [entries]);
|
|
57
|
+
const consistency = useMemo(() => ritualConsistency(entries, 30), [entries]);
|
|
58
|
+
const words = useMemo(() => topMoodWords(entries), [entries]);
|
|
59
|
+
const gratitudes = useMemo(() => recentGratitudes(entries, 12), [entries]);
|
|
60
|
+
|
|
61
|
+
if (loading && !entries.length) {
|
|
62
|
+
return (
|
|
63
|
+
<div data-store-screenshot="stats" className="mx-auto w-full max-w-6xl px-5 py-6 sm:px-8">
|
|
64
|
+
<LoadingState label="Reading back through your days…" />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div data-store-screenshot="stats" className="mx-auto w-full max-w-6xl px-5 py-6 sm:px-8">
|
|
71
|
+
<header className="flex flex-wrap items-end justify-between gap-4">
|
|
72
|
+
<div>
|
|
73
|
+
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
74
|
+
<ChartLineUpIcon size={14} weight="bold" />
|
|
75
|
+
5 Minutes Journal
|
|
76
|
+
</div>
|
|
77
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight">Stats</h1>
|
|
78
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
79
|
+
What {entries.length} {entries.length === 1 ? 'day' : 'days'} of checking in with
|
|
80
|
+
yourself add up to.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
<Button variant="outline" onClick={() => navigation.toRoute('/')} className="gap-1.5">
|
|
84
|
+
<NotebookIcon size={16} weight="bold" />
|
|
85
|
+
Back to journal
|
|
86
|
+
</Button>
|
|
87
|
+
</header>
|
|
88
|
+
|
|
89
|
+
{error ? (
|
|
90
|
+
<p className="mt-6 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive">{error}</p>
|
|
91
|
+
) : null}
|
|
92
|
+
|
|
93
|
+
{entries.length === 0 ? (
|
|
94
|
+
<div className="mt-8">
|
|
95
|
+
<EmptyState
|
|
96
|
+
icon={ChartLineUpIcon}
|
|
97
|
+
title="No patterns to show yet"
|
|
98
|
+
description="After a few mornings and evenings with Notis, your moods, energy, and gratitude will start telling a story here."
|
|
99
|
+
action={
|
|
100
|
+
<Button onClick={() => navigation.toRoute('/')} className="gap-1.5">
|
|
101
|
+
<NotebookIcon size={16} weight="bold" />
|
|
102
|
+
Open journal
|
|
103
|
+
</Button>
|
|
104
|
+
}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
<>
|
|
109
|
+
<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
|
110
|
+
<StatTile
|
|
111
|
+
label="Streak"
|
|
112
|
+
value={streak}
|
|
113
|
+
suffix={streak === 1 ? 'day' : 'days'}
|
|
114
|
+
icon={FlameIcon}
|
|
115
|
+
accent="#f59e0b"
|
|
116
|
+
/>
|
|
117
|
+
<StatTile label="Days journaled" value={entries.length} icon={NotebookIcon} />
|
|
118
|
+
<StatTile
|
|
119
|
+
label="Waking mood"
|
|
120
|
+
value={totals.morningMood ?? '—'}
|
|
121
|
+
suffix={totals.morningMood != null ? '/ 7' : undefined}
|
|
122
|
+
icon={SunHorizonIcon}
|
|
123
|
+
accent={MORNING_MOOD_COLOR}
|
|
124
|
+
hint={moodStep(totals.morningMood)?.label}
|
|
125
|
+
/>
|
|
126
|
+
<StatTile
|
|
127
|
+
label="Day mood"
|
|
128
|
+
value={totals.dayMood ?? '—'}
|
|
129
|
+
suffix={totals.dayMood != null ? '/ 7' : undefined}
|
|
130
|
+
icon={MoonStarsIcon}
|
|
131
|
+
accent={DAY_MOOD_COLOR}
|
|
132
|
+
hint={moodStep(totals.dayMood)?.label}
|
|
133
|
+
/>
|
|
134
|
+
<StatTile
|
|
135
|
+
label="Energy"
|
|
136
|
+
value={totals.energy ?? '—'}
|
|
137
|
+
suffix={totals.energy != null ? '/ 10' : undefined}
|
|
138
|
+
icon={LightningIcon}
|
|
139
|
+
accent={ENERGY_COLOR}
|
|
140
|
+
/>
|
|
141
|
+
<StatTile
|
|
142
|
+
label="Motivation"
|
|
143
|
+
value={totals.motivation ?? '—'}
|
|
144
|
+
suffix={totals.motivation != null ? '/ 10' : undefined}
|
|
145
|
+
icon={RocketLaunchIcon}
|
|
146
|
+
accent={MOTIVATION_COLOR}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="mt-6 grid grid-cols-1 gap-5 lg:grid-cols-2">
|
|
151
|
+
<SectionCard
|
|
152
|
+
title="Mood, morning to evening"
|
|
153
|
+
description="How you woke up next to how the day ended, on the pleasant scale"
|
|
154
|
+
icon={SunHorizonIcon}
|
|
155
|
+
className="lg:col-span-2"
|
|
156
|
+
>
|
|
157
|
+
<MoodTrend entries={recent} />
|
|
158
|
+
<div className="mt-3 flex items-center gap-4 text-[11px] text-muted-foreground">
|
|
159
|
+
<span className="inline-flex items-center gap-1.5">
|
|
160
|
+
<span className="h-2 w-2 rounded-sm bg-foreground/70" />
|
|
161
|
+
Waking mood
|
|
162
|
+
</span>
|
|
163
|
+
<span className="inline-flex items-center gap-1.5">
|
|
164
|
+
<span className="h-2 w-2 rounded-sm border-[1.5px] border-foreground/70 bg-foreground/20" />
|
|
165
|
+
Whole-day mood
|
|
166
|
+
</span>
|
|
167
|
+
<span>Bars take the color of the mood itself.</span>
|
|
168
|
+
{lift != null ? (
|
|
169
|
+
<span className="ml-auto">
|
|
170
|
+
{lift > 0
|
|
171
|
+
? `Your days tend to end ${lift} above where they start.`
|
|
172
|
+
: lift < 0
|
|
173
|
+
? `Your days tend to end ${Math.abs(lift)} below where they start.`
|
|
174
|
+
: 'Your days tend to end where they start.'}
|
|
175
|
+
</span>
|
|
176
|
+
) : (
|
|
177
|
+
<span className="ml-auto">Scale 1–7</span>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</SectionCard>
|
|
181
|
+
|
|
182
|
+
<SectionCard
|
|
183
|
+
title="Energy & motivation"
|
|
184
|
+
description="Morning levels day by day"
|
|
185
|
+
icon={LightningIcon}
|
|
186
|
+
className="lg:col-span-2"
|
|
187
|
+
>
|
|
188
|
+
<LevelTrend entries={recent} />
|
|
189
|
+
<div className="mt-3 flex items-center gap-4 text-[11px] text-muted-foreground">
|
|
190
|
+
<LegendSwatch color={ENERGY_COLOR} label="Energy" />
|
|
191
|
+
<LegendSwatch color={MOTIVATION_COLOR} label="Motivation" />
|
|
192
|
+
<span className="ml-auto">Scale 1–10</span>
|
|
193
|
+
</div>
|
|
194
|
+
</SectionCard>
|
|
195
|
+
|
|
196
|
+
<SectionCard
|
|
197
|
+
title="How your days feel"
|
|
198
|
+
description="Where the whole-day mood lands on the scale"
|
|
199
|
+
icon={MoonStarsIcon}
|
|
200
|
+
>
|
|
201
|
+
<MoodDistribution entries={entries} pick={(e) => e.dayMood} />
|
|
202
|
+
</SectionCard>
|
|
203
|
+
|
|
204
|
+
<SectionCard
|
|
205
|
+
title="How you wake up"
|
|
206
|
+
description="Where the waking mood lands on the scale"
|
|
207
|
+
icon={SunHorizonIcon}
|
|
208
|
+
>
|
|
209
|
+
<MoodDistribution entries={entries} pick={(e) => e.morningMood} />
|
|
210
|
+
</SectionCard>
|
|
211
|
+
|
|
212
|
+
<SectionCard
|
|
213
|
+
title="The ritual"
|
|
214
|
+
description="Check-ins completed over the last 30 days"
|
|
215
|
+
icon={FlameIcon}
|
|
216
|
+
>
|
|
217
|
+
<div className="space-y-4 pt-1">
|
|
218
|
+
<ConsistencyRow
|
|
219
|
+
icon={SunHorizonIcon}
|
|
220
|
+
label="Mornings"
|
|
221
|
+
fraction={consistency.morning}
|
|
222
|
+
color={MORNING_MOOD_COLOR}
|
|
223
|
+
/>
|
|
224
|
+
<ConsistencyRow
|
|
225
|
+
icon={MoonStarsIcon}
|
|
226
|
+
label="Evenings"
|
|
227
|
+
fraction={consistency.evening}
|
|
228
|
+
color={DAY_MOOD_COLOR}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</SectionCard>
|
|
232
|
+
|
|
233
|
+
<SectionCard
|
|
234
|
+
title="Words you reach for"
|
|
235
|
+
description="The adjectives you use most for your moods"
|
|
236
|
+
icon={ChartLineUpIcon}
|
|
237
|
+
>
|
|
238
|
+
{words.length ? (
|
|
239
|
+
<div className="flex flex-wrap gap-2">
|
|
240
|
+
{words.map(({ word, count }) => (
|
|
241
|
+
<span
|
|
242
|
+
key={word}
|
|
243
|
+
className="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs font-medium capitalize"
|
|
244
|
+
>
|
|
245
|
+
{word}
|
|
246
|
+
<span className="text-[10px] tabular-nums text-muted-foreground">{count}</span>
|
|
247
|
+
</span>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
) : (
|
|
251
|
+
<NoData label="No mood words yet" />
|
|
252
|
+
)}
|
|
253
|
+
</SectionCard>
|
|
254
|
+
|
|
255
|
+
<SectionCard
|
|
256
|
+
title="Gratitude wall"
|
|
257
|
+
description="The latest things you said thank you for"
|
|
258
|
+
icon={SparkleIcon}
|
|
259
|
+
className="lg:col-span-2"
|
|
260
|
+
>
|
|
261
|
+
{gratitudes.length ? (
|
|
262
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
263
|
+
{gratitudes.map((item, index) => (
|
|
264
|
+
<div
|
|
265
|
+
key={`${item.text}-${index}`}
|
|
266
|
+
className="rounded-xl border border-border/70 bg-muted/30 px-3.5 py-3"
|
|
267
|
+
>
|
|
268
|
+
<p className="text-sm leading-snug">{item.text}</p>
|
|
269
|
+
<p className="mt-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
270
|
+
{formatDay(item.date)}
|
|
271
|
+
</p>
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
) : (
|
|
276
|
+
<NoData label="Your gratitudes will collect here" />
|
|
277
|
+
)}
|
|
278
|
+
</SectionCard>
|
|
279
|
+
</div>
|
|
280
|
+
</>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Charts
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
function MoodTrend({ entries }: { entries: JournalEntry[] }) {
|
|
291
|
+
if (!entries.length) return <NoData />;
|
|
292
|
+
const summary = entries
|
|
293
|
+
.map(
|
|
294
|
+
(e) =>
|
|
295
|
+
`${formatDay(e.date)}: waking ${e.morningMood ?? 'not logged'}, day ${e.dayMood ?? 'not logged'}`,
|
|
296
|
+
)
|
|
297
|
+
.join('; ');
|
|
298
|
+
return (
|
|
299
|
+
<div
|
|
300
|
+
className="flex items-end gap-1.5 overflow-x-auto pb-1"
|
|
301
|
+
style={{ height: 168 }}
|
|
302
|
+
role="img"
|
|
303
|
+
aria-label={`Waking and whole-day mood by day, on a 1 to 7 scale. ${summary}`}
|
|
304
|
+
>
|
|
305
|
+
{entries.map((entry) => (
|
|
306
|
+
<div key={entry.id} className="flex min-w-[26px] flex-1 flex-col items-center gap-1.5">
|
|
307
|
+
<div className="flex h-[128px] w-full items-end justify-center gap-1">
|
|
308
|
+
<Bar
|
|
309
|
+
fraction={(entry.morningMood ?? 0) / 7}
|
|
310
|
+
color={moodStep(entry.morningMood)?.color ?? MORNING_MOOD_COLOR}
|
|
311
|
+
title={`Waking ${entry.morningMood ?? '—'}`}
|
|
312
|
+
/>
|
|
313
|
+
<Bar
|
|
314
|
+
fraction={(entry.dayMood ?? 0) / 7}
|
|
315
|
+
color={moodStep(entry.dayMood)?.color ?? DAY_MOOD_COLOR}
|
|
316
|
+
title={`Day ${entry.dayMood ?? '—'}`}
|
|
317
|
+
hollow
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
<span className="text-[9px] text-muted-foreground">{shortDay(entry.date)}</span>
|
|
321
|
+
</div>
|
|
322
|
+
))}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function LevelTrend({ entries }: { entries: JournalEntry[] }) {
|
|
328
|
+
if (!entries.length) return <NoData />;
|
|
329
|
+
const summary = entries
|
|
330
|
+
.map(
|
|
331
|
+
(e) =>
|
|
332
|
+
`${formatDay(e.date)}: energy ${e.energy ?? 'not logged'}, motivation ${e.motivation ?? 'not logged'}`,
|
|
333
|
+
)
|
|
334
|
+
.join('; ');
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
className="flex items-end gap-1.5 overflow-x-auto pb-1"
|
|
338
|
+
style={{ height: 168 }}
|
|
339
|
+
role="img"
|
|
340
|
+
aria-label={`Energy and motivation by day. ${summary}`}
|
|
341
|
+
>
|
|
342
|
+
{entries.map((entry) => (
|
|
343
|
+
<div key={entry.id} className="flex min-w-[26px] flex-1 flex-col items-center gap-1.5">
|
|
344
|
+
<div className="flex h-[128px] w-full items-end justify-center gap-1">
|
|
345
|
+
<Bar fraction={(entry.energy ?? 0) / 10} color={ENERGY_COLOR} title={`Energy ${entry.energy ?? '—'}`} />
|
|
346
|
+
<Bar
|
|
347
|
+
fraction={(entry.motivation ?? 0) / 10}
|
|
348
|
+
color={MOTIVATION_COLOR}
|
|
349
|
+
title={`Motivation ${entry.motivation ?? '—'}`}
|
|
350
|
+
/>
|
|
351
|
+
</div>
|
|
352
|
+
<span className="text-[9px] text-muted-foreground">{shortDay(entry.date)}</span>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function Bar({
|
|
360
|
+
fraction,
|
|
361
|
+
color,
|
|
362
|
+
title,
|
|
363
|
+
hollow,
|
|
364
|
+
}: {
|
|
365
|
+
fraction: number;
|
|
366
|
+
color: string;
|
|
367
|
+
title: string;
|
|
368
|
+
hollow?: boolean;
|
|
369
|
+
}) {
|
|
370
|
+
const pct = Math.max(0, Math.min(1, fraction)) * 100;
|
|
371
|
+
return (
|
|
372
|
+
<div className="flex h-full w-2.5 items-end" title={title}>
|
|
373
|
+
<div
|
|
374
|
+
className="w-full rounded-t-sm transition-all"
|
|
375
|
+
style={{
|
|
376
|
+
height: `${Math.max(pct, fraction > 0 ? 4 : 0)}%`,
|
|
377
|
+
backgroundColor: hollow ? `${color}55` : color,
|
|
378
|
+
boxShadow: hollow ? `inset 0 0 0 1.5px ${color}` : undefined,
|
|
379
|
+
}}
|
|
380
|
+
/>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function MoodDistribution({
|
|
386
|
+
entries,
|
|
387
|
+
pick,
|
|
388
|
+
}: {
|
|
389
|
+
entries: JournalEntry[];
|
|
390
|
+
pick: (entry: JournalEntry) => number | null;
|
|
391
|
+
}) {
|
|
392
|
+
const counts = new Map<number, number>();
|
|
393
|
+
let total = 0;
|
|
394
|
+
for (const entry of entries) {
|
|
395
|
+
const value = pick(entry);
|
|
396
|
+
const step = moodStep(value);
|
|
397
|
+
if (!step) continue;
|
|
398
|
+
counts.set(step.value, (counts.get(step.value) ?? 0) + 1);
|
|
399
|
+
total += 1;
|
|
400
|
+
}
|
|
401
|
+
if (!total) return <NoData />;
|
|
402
|
+
const max = Math.max(1, ...counts.values());
|
|
403
|
+
// Best mood first, mirroring how the scale reads left to right.
|
|
404
|
+
const rows = [...MOOD_SCALE].reverse();
|
|
405
|
+
return (
|
|
406
|
+
<div className="space-y-2.5">
|
|
407
|
+
{rows.map((step) => {
|
|
408
|
+
const count = counts.get(step.value) ?? 0;
|
|
409
|
+
const pct = Math.round((count / total) * 100);
|
|
410
|
+
return (
|
|
411
|
+
<div
|
|
412
|
+
key={step.value}
|
|
413
|
+
className="flex items-center gap-3"
|
|
414
|
+
aria-label={`${step.label}: ${count} days, ${pct} percent`}
|
|
415
|
+
>
|
|
416
|
+
<span className="w-32 shrink-0 truncate text-xs font-medium">{step.label}</span>
|
|
417
|
+
<div className="h-2.5 flex-1 overflow-hidden rounded-full bg-muted">
|
|
418
|
+
<div
|
|
419
|
+
className="h-full rounded-full transition-all"
|
|
420
|
+
style={{ width: `${(count / max) * 100}%`, backgroundColor: step.color }}
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
<span className="w-14 shrink-0 text-right text-[11px] tabular-nums text-muted-foreground">
|
|
424
|
+
{count} · {pct}%
|
|
425
|
+
</span>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
})}
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function ConsistencyRow({
|
|
434
|
+
icon: IconCmp,
|
|
435
|
+
label,
|
|
436
|
+
fraction,
|
|
437
|
+
color,
|
|
438
|
+
}: {
|
|
439
|
+
icon: typeof SunHorizonIcon;
|
|
440
|
+
label: string;
|
|
441
|
+
fraction: number;
|
|
442
|
+
color: string;
|
|
443
|
+
}) {
|
|
444
|
+
const pct = Math.round(Math.max(0, Math.min(1, fraction)) * 100);
|
|
445
|
+
return (
|
|
446
|
+
<div className="flex items-center gap-3">
|
|
447
|
+
<span className="flex w-24 shrink-0 items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
448
|
+
<IconCmp size={14} weight="bold" style={{ color }} />
|
|
449
|
+
{label}
|
|
450
|
+
</span>
|
|
451
|
+
<div className="h-2.5 flex-1 overflow-hidden rounded-full bg-muted">
|
|
452
|
+
<div
|
|
453
|
+
className="h-full rounded-full transition-all"
|
|
454
|
+
style={{ width: `${pct}%`, backgroundColor: color }}
|
|
455
|
+
/>
|
|
456
|
+
</div>
|
|
457
|
+
<span className="w-10 shrink-0 text-right text-xs font-semibold tabular-nums">{pct}%</span>
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function LegendSwatch({ color, label }: { color: string; label: string }) {
|
|
463
|
+
return (
|
|
464
|
+
<span className="inline-flex items-center gap-1.5">
|
|
465
|
+
<span className="h-2 w-2 rounded-sm" style={{ backgroundColor: color }} />
|
|
466
|
+
{label}
|
|
467
|
+
</span>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function NoData({ label = 'Not enough data yet' }: { label?: string }) {
|
|
472
|
+
return (
|
|
473
|
+
<div className={cn('flex items-center justify-center py-10 text-xs text-muted-foreground')}>
|
|
474
|
+
{label}
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
// Aggregation
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
|
|
483
|
+
/** Average change from waking mood to whole-day mood, when both were logged. */
|
|
484
|
+
function moodLift(entries: JournalEntry[]): number | null {
|
|
485
|
+
const deltas = entries
|
|
486
|
+
.filter((e) => e.morningMood != null && e.dayMood != null)
|
|
487
|
+
.map((e) => (e.dayMood as number) - (e.morningMood as number));
|
|
488
|
+
if (!deltas.length) return null;
|
|
489
|
+
return round1(deltas.reduce((sum, d) => sum + d, 0) / deltas.length);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Fraction of the last `windowDays` days with complete morning / evening rituals. */
|
|
493
|
+
function ritualConsistency(
|
|
494
|
+
entries: JournalEntry[],
|
|
495
|
+
windowDays: number,
|
|
496
|
+
): { morning: number; evening: number } {
|
|
497
|
+
const cutoff = new Date();
|
|
498
|
+
cutoff.setDate(cutoff.getDate() - windowDays);
|
|
499
|
+
const cutoffKey = dayKey(cutoff.toISOString()) ?? '';
|
|
500
|
+
const recent = entries.filter((e) => (dayKey(e.date) ?? '') >= cutoffKey);
|
|
501
|
+
if (!recent.length) return { morning: 0, evening: 0 };
|
|
502
|
+
return {
|
|
503
|
+
morning: Math.min(1, recent.filter(morningComplete).length / windowDays),
|
|
504
|
+
evening: Math.min(1, recent.filter(eveningComplete).length / windowDays),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function shortDay(value: string | null): string {
|
|
509
|
+
const key = dayKey(value);
|
|
510
|
+
if (!key) return '';
|
|
511
|
+
const [, m, d] = key.split('-');
|
|
512
|
+
return `${Number(m)}/${Number(d)}`;
|
|
513
|
+
}
|