@even-toolkit/create-even-app 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/index.js +159 -0
  2. package/package.json +28 -0
  3. package/templates/chat/README.md +27 -0
  4. package/templates/chat/index.html +12 -0
  5. package/templates/chat/package.json +34 -0
  6. package/templates/chat/src/App.tsx +61 -0
  7. package/templates/chat/src/app.css +54 -0
  8. package/templates/chat/src/contexts/ChatContext.tsx +99 -0
  9. package/templates/chat/src/glass/AppGlasses.tsx +70 -0
  10. package/templates/chat/src/glass/screens/home.ts +24 -0
  11. package/templates/chat/src/glass/selectors.ts +9 -0
  12. package/templates/chat/src/glass/shared.ts +8 -0
  13. package/templates/chat/src/glass/splash.ts +25 -0
  14. package/templates/chat/src/main.tsx +13 -0
  15. package/templates/chat/src/screens/ChatScreen.tsx +69 -0
  16. package/templates/chat/src/screens/Settings.tsx +88 -0
  17. package/templates/chat/src/types.ts +13 -0
  18. package/templates/chat/src/vite-env.d.ts +1 -0
  19. package/templates/chat/template.json +7 -0
  20. package/templates/chat/tsconfig.json +20 -0
  21. package/templates/chat/tsconfig.node.json +13 -0
  22. package/templates/chat/vite.config.ts +12 -0
  23. package/templates/dashboard/README.md +17 -0
  24. package/templates/dashboard/index.html +12 -0
  25. package/templates/dashboard/package.json +34 -0
  26. package/templates/dashboard/src/App.tsx +27 -0
  27. package/templates/dashboard/src/app.css +54 -0
  28. package/templates/dashboard/src/glass/AppGlasses.tsx +53 -0
  29. package/templates/dashboard/src/glass/screens/home.ts +23 -0
  30. package/templates/dashboard/src/glass/selectors.ts +9 -0
  31. package/templates/dashboard/src/glass/shared.ts +8 -0
  32. package/templates/dashboard/src/glass/splash.ts +22 -0
  33. package/templates/dashboard/src/main.tsx +13 -0
  34. package/templates/dashboard/src/screens/ChartsScreen.tsx +99 -0
  35. package/templates/dashboard/src/screens/OverviewScreen.tsx +102 -0
  36. package/templates/dashboard/src/screens/SettingsScreen.tsx +60 -0
  37. package/templates/dashboard/src/vite-env.d.ts +1 -0
  38. package/templates/dashboard/template.json +7 -0
  39. package/templates/dashboard/tsconfig.json +20 -0
  40. package/templates/dashboard/tsconfig.node.json +13 -0
  41. package/templates/dashboard/vite.config.ts +12 -0
  42. package/templates/media/README.md +27 -0
  43. package/templates/media/index.html +12 -0
  44. package/templates/media/package.json +34 -0
  45. package/templates/media/src/App.tsx +24 -0
  46. package/templates/media/src/app.css +54 -0
  47. package/templates/media/src/contexts/MediaContext.tsx +108 -0
  48. package/templates/media/src/glass/AppGlasses.tsx +59 -0
  49. package/templates/media/src/glass/screens/home.ts +24 -0
  50. package/templates/media/src/glass/selectors.ts +9 -0
  51. package/templates/media/src/glass/shared.ts +8 -0
  52. package/templates/media/src/glass/splash.ts +25 -0
  53. package/templates/media/src/layouts/shell.tsx +39 -0
  54. package/templates/media/src/main.tsx +13 -0
  55. package/templates/media/src/screens/AudioScreen.tsx +78 -0
  56. package/templates/media/src/screens/GalleryScreen.tsx +98 -0
  57. package/templates/media/src/screens/Settings.tsx +86 -0
  58. package/templates/media/src/screens/UploadScreen.tsx +95 -0
  59. package/templates/media/src/types.ts +29 -0
  60. package/templates/media/src/vite-env.d.ts +1 -0
  61. package/templates/media/template.json +7 -0
  62. package/templates/media/tsconfig.json +20 -0
  63. package/templates/media/tsconfig.node.json +13 -0
  64. package/templates/media/vite.config.ts +12 -0
  65. package/templates/minimal/README.md +27 -0
  66. package/templates/minimal/index.html +12 -0
  67. package/templates/minimal/package.json +34 -0
  68. package/templates/minimal/src/App.tsx +50 -0
  69. package/templates/minimal/src/app.css +54 -0
  70. package/templates/minimal/src/glass/AppGlasses.tsx +54 -0
  71. package/templates/minimal/src/glass/screens/home.ts +24 -0
  72. package/templates/minimal/src/glass/selectors.ts +9 -0
  73. package/templates/minimal/src/glass/shared.ts +8 -0
  74. package/templates/minimal/src/glass/splash.ts +25 -0
  75. package/templates/minimal/src/main.tsx +13 -0
  76. package/templates/minimal/src/vite-env.d.ts +1 -0
  77. package/templates/minimal/template.json +7 -0
  78. package/templates/minimal/tsconfig.json +20 -0
  79. package/templates/minimal/tsconfig.node.json +13 -0
  80. package/templates/minimal/vite.config.ts +12 -0
  81. package/templates/notes/README.md +27 -0
  82. package/templates/notes/index.html +12 -0
  83. package/templates/notes/package.json +34 -0
  84. package/templates/notes/src/App.tsx +25 -0
  85. package/templates/notes/src/app.css +54 -0
  86. package/templates/notes/src/contexts/NotesContext.tsx +140 -0
  87. package/templates/notes/src/glass/AppGlasses.tsx +58 -0
  88. package/templates/notes/src/glass/screens/home.ts +24 -0
  89. package/templates/notes/src/glass/selectors.ts +9 -0
  90. package/templates/notes/src/glass/shared.ts +8 -0
  91. package/templates/notes/src/glass/splash.ts +24 -0
  92. package/templates/notes/src/layouts/shell.tsx +36 -0
  93. package/templates/notes/src/main.tsx +13 -0
  94. package/templates/notes/src/screens/NoteDetail.tsx +104 -0
  95. package/templates/notes/src/screens/NoteForm.tsx +84 -0
  96. package/templates/notes/src/screens/NoteList.tsx +108 -0
  97. package/templates/notes/src/screens/Settings.tsx +88 -0
  98. package/templates/notes/src/types.ts +14 -0
  99. package/templates/notes/src/vite-env.d.ts +1 -0
  100. package/templates/notes/template.json +7 -0
  101. package/templates/notes/tsconfig.json +20 -0
  102. package/templates/notes/tsconfig.node.json +13 -0
  103. package/templates/notes/vite.config.ts +12 -0
  104. package/templates/tracker/README.md +27 -0
  105. package/templates/tracker/index.html +12 -0
  106. package/templates/tracker/package.json +34 -0
  107. package/templates/tracker/src/App.tsx +24 -0
  108. package/templates/tracker/src/app.css +54 -0
  109. package/templates/tracker/src/contexts/TrackerContext.tsx +193 -0
  110. package/templates/tracker/src/glass/AppGlasses.tsx +64 -0
  111. package/templates/tracker/src/glass/screens/home.ts +24 -0
  112. package/templates/tracker/src/glass/selectors.ts +9 -0
  113. package/templates/tracker/src/glass/shared.ts +8 -0
  114. package/templates/tracker/src/glass/splash.ts +24 -0
  115. package/templates/tracker/src/layouts/shell.tsx +37 -0
  116. package/templates/tracker/src/main.tsx +13 -0
  117. package/templates/tracker/src/screens/HistoryScreen.tsx +106 -0
  118. package/templates/tracker/src/screens/NewEntryScreen.tsx +135 -0
  119. package/templates/tracker/src/screens/Settings.tsx +135 -0
  120. package/templates/tracker/src/screens/TodayScreen.tsx +147 -0
  121. package/templates/tracker/src/types.ts +34 -0
  122. package/templates/tracker/src/vite-env.d.ts +1 -0
  123. package/templates/tracker/template.json +7 -0
  124. package/templates/tracker/tsconfig.json +20 -0
  125. package/templates/tracker/tsconfig.node.json +13 -0
  126. package/templates/tracker/vite.config.ts +12 -0
@@ -0,0 +1,193 @@
1
+ import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
2
+ import type { TrackerEntry, ActivityType, GoalTargets, DayRecord } from '../types'
3
+ import { DEFAULT_GOALS } from '../types'
4
+
5
+ interface TrackerContextValue {
6
+ entries: TrackerEntry[]
7
+ goals: GoalTargets
8
+ setGoals: (g: GoalTargets) => void
9
+ addEntry: (activity: ActivityType, value: number, note: string) => TrackerEntry
10
+ deleteEntry: (id: string) => void
11
+ getTodayEntries: () => TrackerEntry[]
12
+ getTodayTotal: (activity: ActivityType) => number
13
+ getDayRecords: () => DayRecord[]
14
+ showCompletionBadge: boolean
15
+ setShowCompletionBadge: (v: boolean) => void
16
+ }
17
+
18
+ const TrackerContext = createContext<TrackerContextValue | null>(null)
19
+
20
+ const ENTRIES_KEY = '{{APP_NAME}}-entries'
21
+ const GOALS_KEY = '{{APP_NAME}}-goals'
22
+ const SETTINGS_KEY = '{{APP_NAME}}-settings'
23
+
24
+ function generateId(): string {
25
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
26
+ }
27
+
28
+ function toDateKey(ts: number): string {
29
+ const d = new Date(ts)
30
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
31
+ }
32
+
33
+ function todayKey(): string {
34
+ return toDateKey(Date.now())
35
+ }
36
+
37
+ function generateSampleData(): TrackerEntry[] {
38
+ const now = Date.now()
39
+ const DAY = 86400000
40
+ const entries: TrackerEntry[] = []
41
+
42
+ for (let d = 6; d >= 0; d--) {
43
+ const dayBase = now - d * DAY
44
+ const dateStart = new Date(dayBase)
45
+ dateStart.setHours(8, 0, 0, 0)
46
+ const base = dateStart.getTime()
47
+
48
+ // Water entries
49
+ const waterCount = d === 0 ? 6 : Math.floor(Math.random() * 4) + 5
50
+ for (let i = 0; i < waterCount; i++) {
51
+ entries.push({
52
+ id: generateId() + `-w${d}-${i}`,
53
+ activity: 'Water',
54
+ value: 1,
55
+ note: '',
56
+ timestamp: base + i * 3600000,
57
+ })
58
+ }
59
+
60
+ // Steps entry
61
+ const steps = d === 0 ? 7500 : Math.floor(Math.random() * 5000) + 6000
62
+ entries.push({
63
+ id: generateId() + `-s${d}`,
64
+ activity: 'Steps',
65
+ value: steps,
66
+ note: d === 0 ? '' : 'Daily walk',
67
+ timestamp: base + 10 * 3600000,
68
+ })
69
+
70
+ // Focus entry
71
+ const focus = d === 0 ? 45 : Math.floor(Math.random() * 30) + 30
72
+ entries.push({
73
+ id: generateId() + `-f${d}`,
74
+ activity: 'Focus',
75
+ value: focus,
76
+ note: d === 0 ? '' : 'Deep work session',
77
+ timestamp: base + 14 * 3600000,
78
+ })
79
+ }
80
+
81
+ return entries
82
+ }
83
+
84
+ function loadEntries(): TrackerEntry[] {
85
+ try {
86
+ const raw = localStorage.getItem(ENTRIES_KEY)
87
+ if (raw) {
88
+ const parsed = JSON.parse(raw)
89
+ if (Array.isArray(parsed) && parsed.length > 0) return parsed
90
+ }
91
+ } catch { /* ignore */ }
92
+ return generateSampleData()
93
+ }
94
+
95
+ function loadGoals(): GoalTargets {
96
+ try {
97
+ const raw = localStorage.getItem(GOALS_KEY)
98
+ if (raw) return JSON.parse(raw)
99
+ } catch { /* ignore */ }
100
+ return DEFAULT_GOALS
101
+ }
102
+
103
+ function loadSettings(): { showCompletionBadge: boolean } {
104
+ try {
105
+ const raw = localStorage.getItem(SETTINGS_KEY)
106
+ if (raw) return JSON.parse(raw)
107
+ } catch { /* ignore */ }
108
+ return { showCompletionBadge: true }
109
+ }
110
+
111
+ export function TrackerProvider({ children }: { children: ReactNode }) {
112
+ const [entries, setEntries] = useState<TrackerEntry[]>(loadEntries)
113
+ const [goals, setGoals] = useState<GoalTargets>(loadGoals)
114
+ const [showCompletionBadge, setShowCompletionBadge] = useState(() => loadSettings().showCompletionBadge)
115
+
116
+ useEffect(() => {
117
+ localStorage.setItem(ENTRIES_KEY, JSON.stringify(entries))
118
+ }, [entries])
119
+
120
+ useEffect(() => {
121
+ localStorage.setItem(GOALS_KEY, JSON.stringify(goals))
122
+ }, [goals])
123
+
124
+ useEffect(() => {
125
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify({ showCompletionBadge }))
126
+ }, [showCompletionBadge])
127
+
128
+ const addEntry = useCallback((activity: ActivityType, value: number, note: string): TrackerEntry => {
129
+ const entry: TrackerEntry = {
130
+ id: generateId(),
131
+ activity,
132
+ value,
133
+ note,
134
+ timestamp: Date.now(),
135
+ }
136
+ setEntries((prev) => [entry, ...prev])
137
+ return entry
138
+ }, [])
139
+
140
+ const deleteEntry = useCallback((id: string) => {
141
+ setEntries((prev) => prev.filter((e) => e.id !== id))
142
+ }, [])
143
+
144
+ const getTodayEntries = useCallback(() => {
145
+ const key = todayKey()
146
+ return entries.filter((e) => toDateKey(e.timestamp) === key)
147
+ }, [entries])
148
+
149
+ const getTodayTotal = useCallback((activity: ActivityType) => {
150
+ const key = todayKey()
151
+ return entries
152
+ .filter((e) => toDateKey(e.timestamp) === key && e.activity === activity)
153
+ .reduce((sum, e) => sum + e.value, 0)
154
+ }, [entries])
155
+
156
+ const getDayRecords = useCallback((): DayRecord[] => {
157
+ const map = new Map<string, TrackerEntry[]>()
158
+ for (const entry of entries) {
159
+ const key = toDateKey(entry.timestamp)
160
+ const existing = map.get(key) ?? []
161
+ existing.push(entry)
162
+ map.set(key, existing)
163
+ }
164
+ return Array.from(map.entries())
165
+ .map(([date, dayEntries]) => ({ date, entries: dayEntries }))
166
+ .sort((a, b) => b.date.localeCompare(a.date))
167
+ }, [entries])
168
+
169
+ return (
170
+ <TrackerContext.Provider
171
+ value={{
172
+ entries,
173
+ goals,
174
+ setGoals,
175
+ addEntry,
176
+ deleteEntry,
177
+ getTodayEntries,
178
+ getTodayTotal,
179
+ getDayRecords,
180
+ showCompletionBadge,
181
+ setShowCompletionBadge,
182
+ }}
183
+ >
184
+ {children}
185
+ </TrackerContext.Provider>
186
+ )
187
+ }
188
+
189
+ export function useTracker() {
190
+ const ctx = useContext(TrackerContext)
191
+ if (!ctx) throw new Error('useTracker must be used within TrackerProvider')
192
+ return ctx
193
+ }
@@ -0,0 +1,64 @@
1
+ import { useCallback, useMemo, useRef } from 'react'
2
+ import { useNavigate, useLocation } from 'react-router'
3
+ import { useGlasses } from 'even-toolkit/useGlasses'
4
+ import { useFlashPhase } from 'even-toolkit/useFlashPhase'
5
+ import { createScreenMapper, getHomeTiles } from 'even-toolkit/glass-router'
6
+ import { appSplash } from './splash'
7
+ import { toDisplayData, onGlassAction, type AppSnapshot } from './selectors'
8
+ import type { AppActions } from './shared'
9
+ import { useTracker } from '../contexts/TrackerContext'
10
+
11
+ const deriveScreen = createScreenMapper([
12
+ { pattern: '/', screen: 'home' },
13
+ ], 'home')
14
+
15
+ const homeTiles = getHomeTiles(appSplash)
16
+
17
+ export function AppGlasses() {
18
+ const navigate = useNavigate()
19
+ const location = useLocation()
20
+ const { goals, getTodayTotal } = useTracker()
21
+ const flashPhase = useFlashPhase(deriveScreen(location.pathname) === 'home')
22
+
23
+ const snapshotRef = useMemo(() => ({
24
+ current: null as AppSnapshot | null,
25
+ }), [])
26
+
27
+ const waterTotal = getTodayTotal('Water')
28
+ const stepsTotal = getTodayTotal('Steps')
29
+ const focusTotal = getTodayTotal('Focus')
30
+
31
+ const snapshot: AppSnapshot = {
32
+ items: [
33
+ `Water: ${waterTotal}/${goals.water}`,
34
+ `Steps: ${stepsTotal.toLocaleString()}/${goals.steps.toLocaleString()}`,
35
+ `Focus: ${focusTotal}/${goals.focus} min`,
36
+ ],
37
+ flashPhase,
38
+ }
39
+ snapshotRef.current = snapshot
40
+
41
+ const getSnapshot = useCallback(() => snapshotRef.current!, [snapshotRef])
42
+
43
+ const ctxRef = useRef<AppActions>({ navigate })
44
+ ctxRef.current = { navigate }
45
+
46
+ const handleGlassAction = useCallback(
47
+ (action: Parameters<typeof onGlassAction>[0], nav: Parameters<typeof onGlassAction>[1], snap: AppSnapshot) =>
48
+ onGlassAction(action, nav, snap, ctxRef.current),
49
+ [],
50
+ )
51
+
52
+ useGlasses({
53
+ getSnapshot,
54
+ toDisplayData,
55
+ onGlassAction: handleGlassAction,
56
+ deriveScreen,
57
+ appName: '{{DISPLAY_NAME_UPPER}}',
58
+ splash: appSplash,
59
+ getPageMode: (screen) => screen === 'home' ? 'home' : 'text',
60
+ homeImageTiles: homeTiles,
61
+ })
62
+
63
+ return null
64
+ }
@@ -0,0 +1,24 @@
1
+ import type { GlassScreen } from 'even-toolkit/glass-screen-router'
2
+ import { buildScrollableList } from 'even-toolkit/glass-display-builders'
3
+ import { moveHighlight } from 'even-toolkit/glass-nav'
4
+ import type { AppSnapshot, AppActions } from '../shared'
5
+
6
+ export const homeScreen: GlassScreen<AppSnapshot, AppActions> = {
7
+ display(snapshot, nav) {
8
+ return {
9
+ lines: buildScrollableList({
10
+ items: snapshot.items,
11
+ highlightedIndex: nav.highlightedIndex,
12
+ maxVisible: 5,
13
+ formatter: (item) => item,
14
+ }),
15
+ }
16
+ },
17
+
18
+ action(action, nav, snapshot) {
19
+ if (action.type === 'HIGHLIGHT_MOVE') {
20
+ return { ...nav, highlightedIndex: moveHighlight(nav.highlightedIndex, action.direction, snapshot.items.length - 1) }
21
+ }
22
+ return nav
23
+ },
24
+ }
@@ -0,0 +1,9 @@
1
+ import { createGlassScreenRouter } from 'even-toolkit/glass-screen-router'
2
+ import type { AppSnapshot, AppActions } from './shared'
3
+ import { homeScreen } from './screens/home'
4
+
5
+ export type { AppSnapshot, AppActions }
6
+
7
+ export const { toDisplayData, onGlassAction } = createGlassScreenRouter<AppSnapshot, AppActions>({
8
+ 'home': homeScreen,
9
+ }, 'home')
@@ -0,0 +1,8 @@
1
+ export interface AppSnapshot {
2
+ items: string[]
3
+ flashPhase: boolean
4
+ }
5
+
6
+ export interface AppActions {
7
+ navigate: (path: string) => void
8
+ }
@@ -0,0 +1,24 @@
1
+ import { createSplash, TILE_PRESETS } from 'even-toolkit/splash'
2
+
3
+ export function renderSplash(ctx: CanvasRenderingContext2D, w: number, h: number) {
4
+ const fg = '#e0e0e0'
5
+ const cx = w / 2
6
+ const s = Math.min(w / 200, h / 200)
7
+
8
+ ctx.fillStyle = fg
9
+ ctx.font = `bold ${14 * s}px "Courier New", monospace`
10
+ ctx.textAlign = 'center'
11
+ ctx.fillText('{{DISPLAY_NAME_UPPER}}', cx, 50 * s)
12
+ ctx.textAlign = 'left'
13
+ }
14
+
15
+ export const appSplash = createSplash({
16
+ tiles: 1,
17
+ tileLayout: 'vertical',
18
+ tilePositions: TILE_PRESETS.topCenter1,
19
+ canvasSize: { w: 200, h: 200 },
20
+ minTimeMs: 0,
21
+ maxTimeMs: 0,
22
+ menuText: '',
23
+ render: renderSplash,
24
+ })
@@ -0,0 +1,37 @@
1
+ import { DrawerShell } from 'even-toolkit/web'
2
+ import type { SideDrawerItem } from 'even-toolkit/web'
3
+
4
+ const MENU_ITEMS: SideDrawerItem[] = [
5
+ { id: '/', label: 'Today', section: 'Tracker' },
6
+ { id: '/history', label: 'History', section: 'Tracker' },
7
+ ]
8
+
9
+ const BOTTOM_ITEMS: SideDrawerItem[] = [
10
+ { id: '/settings', label: 'Settings', section: 'App' },
11
+ ]
12
+
13
+ function getPageTitle(pathname: string): string {
14
+ if (pathname === '/') return '{{DISPLAY_NAME}}'
15
+ if (pathname === '/history') return 'History'
16
+ if (pathname === '/new') return 'New Entry'
17
+ if (pathname === '/settings') return 'Settings'
18
+ return '{{DISPLAY_NAME}}'
19
+ }
20
+
21
+ function deriveActiveId(pathname: string): string {
22
+ if (pathname === '/settings') return '/settings'
23
+ if (pathname === '/history') return '/history'
24
+ return '/'
25
+ }
26
+
27
+ export function Shell() {
28
+ return (
29
+ <DrawerShell
30
+ items={MENU_ITEMS}
31
+ bottomItems={BOTTOM_ITEMS}
32
+ title="{{DISPLAY_NAME}}"
33
+ getPageTitle={getPageTitle}
34
+ deriveActiveId={deriveActiveId}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,13 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router'
4
+ import { App } from './App'
5
+ import './app.css'
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </StrictMode>,
13
+ )
@@ -0,0 +1,106 @@
1
+ import { useMemo } from 'react'
2
+ import { Card, Calendar, Timeline, Badge, ScreenHeader } from 'even-toolkit/web'
3
+ import type { CalendarEvent } from 'even-toolkit/web'
4
+ import { useTracker } from '../contexts/TrackerContext'
5
+ import type { DayRecord } from '../types'
6
+
7
+ const ACTIVITY_COLORS: Record<string, string> = {
8
+ Water: 'var(--color-accent)',
9
+ Steps: 'var(--color-positive)',
10
+ Focus: 'var(--color-negative)',
11
+ }
12
+
13
+ function formatDayLabel(dateStr: string): string {
14
+ const d = new Date(dateStr + 'T12:00:00')
15
+ const today = new Date()
16
+ const yesterday = new Date()
17
+ yesterday.setDate(yesterday.getDate() - 1)
18
+
19
+ if (d.toDateString() === today.toDateString()) return 'Today'
20
+ if (d.toDateString() === yesterday.toDateString()) return 'Yesterday'
21
+ return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
22
+ }
23
+
24
+ function getDayCompletion(record: DayRecord, goals: { water: number; steps: number; focus: number }): number {
25
+ const waterTotal = record.entries.filter((e) => e.activity === 'Water').reduce((s, e) => s + e.value, 0)
26
+ const stepsTotal = record.entries.filter((e) => e.activity === 'Steps').reduce((s, e) => s + e.value, 0)
27
+ const focusTotal = record.entries.filter((e) => e.activity === 'Focus').reduce((s, e) => s + e.value, 0)
28
+
29
+ const waterPct = Math.min(waterTotal / goals.water, 1)
30
+ const stepsPct = Math.min(stepsTotal / goals.steps, 1)
31
+ const focusPct = Math.min(focusTotal / goals.focus, 1)
32
+
33
+ return Math.round(((waterPct + stepsPct + focusPct) / 3) * 100)
34
+ }
35
+
36
+ export function HistoryScreen() {
37
+ const { getDayRecords, goals, showCompletionBadge } = useTracker()
38
+ const dayRecords = getDayRecords()
39
+
40
+ // Calendar events from entries
41
+ const calendarEvents: CalendarEvent[] = useMemo(() => {
42
+ const events: CalendarEvent[] = []
43
+ for (const record of dayRecords) {
44
+ const activities = new Set(record.entries.map((e) => e.activity))
45
+ for (const activity of activities) {
46
+ const d = new Date(record.date + 'T12:00:00')
47
+ events.push({
48
+ id: `${record.date}-${activity}`,
49
+ title: activity,
50
+ start: d,
51
+ end: d,
52
+ color: ACTIVITY_COLORS[activity],
53
+ })
54
+ }
55
+ }
56
+ return events
57
+ }, [dayRecords])
58
+
59
+ // Recent 7 days for timeline
60
+ const recentDays = dayRecords.slice(0, 7)
61
+
62
+ const timelineEvents = useMemo(() => {
63
+ return recentDays.map((record) => {
64
+ const completion = getDayCompletion(record, goals)
65
+ const entryCount = record.entries.length
66
+ return {
67
+ id: record.date,
68
+ title: formatDayLabel(record.date),
69
+ subtitle: `${entryCount} ${entryCount === 1 ? 'entry' : 'entries'}`,
70
+ timestamp: `${completion}%`,
71
+ color: completion >= 80 ? 'var(--color-positive)' : completion >= 50 ? 'var(--color-accent)' : 'var(--color-negative)',
72
+ }
73
+ })
74
+ }, [recentDays, goals])
75
+
76
+ return (
77
+ <main className="px-3 pt-4 pb-8 space-y-3">
78
+ <ScreenHeader
79
+ title="History"
80
+ subtitle={`${dayRecords.length} days tracked`}
81
+ />
82
+
83
+ {/* Calendar */}
84
+ <Card className="p-4">
85
+ <Calendar events={calendarEvents} />
86
+ </Card>
87
+
88
+ {/* Recent Activity Timeline */}
89
+ <Card className="p-4 space-y-3">
90
+ <div className="flex items-center justify-between">
91
+ <p className="text-[15px] tracking-[-0.15px] text-text">Recent Activity</p>
92
+ {showCompletionBadge && recentDays.length > 0 && (
93
+ <Badge variant={getDayCompletion(recentDays[0], goals) >= 80 ? 'positive' : 'neutral'}>
94
+ {getDayCompletion(recentDays[0], goals)}% today
95
+ </Badge>
96
+ )}
97
+ </div>
98
+ {timelineEvents.length > 0 ? (
99
+ <Timeline events={timelineEvents} />
100
+ ) : (
101
+ <p className="text-[13px] tracking-[-0.13px] text-text-dim">No activity recorded yet.</p>
102
+ )}
103
+ </Card>
104
+ </main>
105
+ )
106
+ }
@@ -0,0 +1,135 @@
1
+ import { useState } from 'react'
2
+ import { useNavigate } from 'react-router'
3
+ import { Card, Button, StepIndicator, Select, Slider, Input, Textarea, useDrawerHeader } from 'even-toolkit/web'
4
+ import { useTracker } from '../contexts/TrackerContext'
5
+ import { ACTIVITIES, ACTIVITY_UNITS, type ActivityType } from '../types'
6
+
7
+ const ACTIVITY_OPTIONS = ACTIVITIES.map((a) => ({ value: a, label: a }))
8
+
9
+ const ACTIVITY_DEFAULTS: Record<ActivityType, { min: number; max: number; step: number; initial: number }> = {
10
+ Water: { min: 1, max: 12, step: 1, initial: 1 },
11
+ Steps: { min: 500, max: 20000, step: 500, initial: 5000 },
12
+ Focus: { min: 5, max: 120, step: 5, initial: 25 },
13
+ }
14
+
15
+ export function NewEntryScreen() {
16
+ const navigate = useNavigate()
17
+ const { addEntry } = useTracker()
18
+
19
+ const [step, setStep] = useState(1)
20
+ const [activity, setActivity] = useState<ActivityType>('Water')
21
+ const [value, setValue] = useState(ACTIVITY_DEFAULTS.Water.initial)
22
+ const [note, setNote] = useState('')
23
+
24
+ useDrawerHeader({ title: 'New Entry', backTo: '/' })
25
+
26
+ function handleActivityChange(a: string) {
27
+ const act = a as ActivityType
28
+ setActivity(act)
29
+ setValue(ACTIVITY_DEFAULTS[act].initial)
30
+ }
31
+
32
+ function handleSave() {
33
+ addEntry(activity, value, note.trim())
34
+ navigate('/')
35
+ }
36
+
37
+ const canNext = step < 3
38
+ const canPrev = step > 1
39
+
40
+ return (
41
+ <main className="px-3 pt-4 pb-8 space-y-3">
42
+ <StepIndicator
43
+ currentStep={step}
44
+ totalSteps={3}
45
+ onPrev={canPrev ? () => setStep((s) => s - 1) : undefined}
46
+ onNext={
47
+ step === 3
48
+ ? handleSave
49
+ : canNext
50
+ ? () => setStep((s) => s + 1)
51
+ : undefined
52
+ }
53
+ nextLabel={step === 3 ? 'Save' : undefined}
54
+ />
55
+
56
+ {/* Step 1: Select Activity */}
57
+ {step === 1 && (
58
+ <Card className="p-4 space-y-3">
59
+ <p className="text-[15px] tracking-[-0.15px] text-text">Select Activity</p>
60
+ <div className="space-y-1.5">
61
+ <label className="text-[11px] tracking-[-0.11px] text-text-dim block">Activity type</label>
62
+ <Select
63
+ options={ACTIVITY_OPTIONS}
64
+ value={activity}
65
+ onValueChange={handleActivityChange}
66
+ />
67
+ </div>
68
+ <p className="text-[13px] tracking-[-0.13px] text-text-dim">
69
+ Track your {activity.toLowerCase()} intake for today.
70
+ </p>
71
+ </Card>
72
+ )}
73
+
74
+ {/* Step 2: Set Value */}
75
+ {step === 2 && (
76
+ <Card className="p-4 space-y-3">
77
+ <p className="text-[15px] tracking-[-0.15px] text-text">Set Value</p>
78
+ <div className="space-y-1.5">
79
+ <label className="text-[11px] tracking-[-0.11px] text-text-dim block">
80
+ {activity} ({ACTIVITY_UNITS[activity]})
81
+ </label>
82
+ <Slider
83
+ value={value}
84
+ onChange={setValue}
85
+ min={ACTIVITY_DEFAULTS[activity].min}
86
+ max={ACTIVITY_DEFAULTS[activity].max}
87
+ step={ACTIVITY_DEFAULTS[activity].step}
88
+ />
89
+ </div>
90
+ <div className="space-y-1.5">
91
+ <label className="text-[11px] tracking-[-0.11px] text-text-dim block">Exact value</label>
92
+ <Input
93
+ type="number"
94
+ value={String(value)}
95
+ onChange={(e) => {
96
+ const v = Number(e.target.value)
97
+ if (!isNaN(v) && v >= 0) setValue(v)
98
+ }}
99
+ />
100
+ </div>
101
+ <p className="text-[13px] tracking-[-0.13px] text-text-dim">
102
+ {value} {ACTIVITY_UNITS[activity]}
103
+ </p>
104
+ </Card>
105
+ )}
106
+
107
+ {/* Step 3: Add Note */}
108
+ {step === 3 && (
109
+ <Card className="p-4 space-y-3">
110
+ <p className="text-[15px] tracking-[-0.15px] text-text">Add Note</p>
111
+ <div className="space-y-1.5">
112
+ <label className="text-[11px] tracking-[-0.11px] text-text-dim block">Note (optional)</label>
113
+ <Textarea
114
+ value={note}
115
+ onChange={(e) => setNote(e.target.value)}
116
+ placeholder="Add a note about this entry..."
117
+ rows={4}
118
+ />
119
+ </div>
120
+ <div className="rounded-[6px] bg-surface p-3 space-y-1">
121
+ <p className="text-[13px] tracking-[-0.13px] text-text">Summary</p>
122
+ <p className="text-[11px] tracking-[-0.11px] text-text-dim">
123
+ {activity}: {value} {ACTIVITY_UNITS[activity]}
124
+ </p>
125
+ {note.trim() && (
126
+ <p className="text-[11px] tracking-[-0.11px] text-text-dim">
127
+ Note: {note.trim()}
128
+ </p>
129
+ )}
130
+ </div>
131
+ </Card>
132
+ )}
133
+ </main>
134
+ )
135
+ }