selftune 0.2.23 → 0.2.25

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 (219) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +93 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dhgv5BQO.js +15 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
  6. package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
  7. package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
  8. package/apps/local-dashboard/dist/index.html +5 -5
  9. package/cli/selftune/adapters/codex/install.ts +310 -78
  10. package/cli/selftune/adapters/opencode/install.ts +3 -4
  11. package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
  12. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  13. package/cli/selftune/auto-update.ts +200 -8
  14. package/cli/selftune/canonical-export.ts +55 -25
  15. package/cli/selftune/command-surface.ts +397 -0
  16. package/cli/selftune/contribute/contribute.ts +64 -13
  17. package/cli/selftune/contribution-config.ts +57 -3
  18. package/cli/selftune/contribution-preferences.ts +117 -0
  19. package/cli/selftune/contribution-signals.ts +8 -4
  20. package/cli/selftune/contribution-staging.ts +13 -2
  21. package/cli/selftune/contributions.ts +55 -121
  22. package/cli/selftune/creator-contributions.ts +29 -10
  23. package/cli/selftune/cron/setup.ts +7 -3
  24. package/cli/selftune/dashboard-contract.ts +73 -0
  25. package/cli/selftune/dashboard-server.ts +168 -17
  26. package/cli/selftune/dashboard.ts +350 -17
  27. package/cli/selftune/eval/baseline.ts +21 -5
  28. package/cli/selftune/eval/execution-eval.ts +170 -0
  29. package/cli/selftune/eval/family-overlap.ts +2 -2
  30. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  31. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  32. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  33. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  34. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  35. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  36. package/cli/selftune/evolution/engines/replay-engine.ts +79 -57
  37. package/cli/selftune/evolution/evolve-body.ts +100 -39
  38. package/cli/selftune/evolution/evolve.ts +244 -52
  39. package/cli/selftune/evolution/rollback.ts +0 -1
  40. package/cli/selftune/evolution/validate-body.ts +68 -42
  41. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  42. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  43. package/cli/selftune/evolution/validate-routing.ts +43 -41
  44. package/cli/selftune/evolution/validation-contract.ts +91 -0
  45. package/cli/selftune/grading/auto-grade.ts +11 -7
  46. package/cli/selftune/grading/grade-session.ts +10 -16
  47. package/cli/selftune/index.ts +35 -10
  48. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  49. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  50. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  51. package/cli/selftune/ingestors/pi-ingest.ts +3 -2
  52. package/cli/selftune/init.ts +27 -3
  53. package/cli/selftune/localdb/direct-write.ts +35 -1
  54. package/cli/selftune/localdb/queries/cron.ts +34 -0
  55. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  56. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  57. package/cli/selftune/localdb/queries/execution.ts +133 -0
  58. package/cli/selftune/localdb/queries/json.ts +18 -0
  59. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  60. package/cli/selftune/localdb/queries/raw.ts +95 -0
  61. package/cli/selftune/localdb/queries/staging.ts +270 -0
  62. package/cli/selftune/localdb/queries/trust.ts +392 -0
  63. package/cli/selftune/localdb/queries.ts +60 -2288
  64. package/cli/selftune/localdb/schema.ts +21 -0
  65. package/cli/selftune/monitoring/watch.ts +96 -29
  66. package/cli/selftune/normalization.ts +3 -0
  67. package/cli/selftune/observability.ts +4 -2
  68. package/cli/selftune/orchestrate/cli.ts +161 -0
  69. package/cli/selftune/orchestrate/execute.ts +295 -0
  70. package/cli/selftune/orchestrate/finalize.ts +157 -0
  71. package/cli/selftune/orchestrate/locks.ts +40 -0
  72. package/cli/selftune/orchestrate/plan.ts +131 -0
  73. package/cli/selftune/orchestrate/post-run.ts +59 -0
  74. package/cli/selftune/orchestrate/prepare.ts +334 -0
  75. package/cli/selftune/orchestrate/report.ts +182 -0
  76. package/cli/selftune/orchestrate/runtime.ts +120 -0
  77. package/cli/selftune/orchestrate/signals.ts +48 -0
  78. package/cli/selftune/orchestrate.ts +150 -1173
  79. package/cli/selftune/repair/skill-usage.ts +5 -2
  80. package/cli/selftune/routes/overview.ts +5 -2
  81. package/cli/selftune/routes/skill-report.ts +15 -2
  82. package/cli/selftune/schedule.ts +5 -5
  83. package/cli/selftune/status.ts +39 -2
  84. package/cli/selftune/testing-readiness.ts +597 -0
  85. package/cli/selftune/types.ts +44 -4
  86. package/cli/selftune/uninstall.ts +2 -1
  87. package/cli/selftune/utils/canonical-log.ts +1 -9
  88. package/cli/selftune/utils/cli-error.ts +9 -0
  89. package/cli/selftune/utils/llm-call.ts +126 -6
  90. package/cli/selftune/utils/skill-discovery.ts +2 -0
  91. package/cli/selftune/workflows/proposals.ts +184 -0
  92. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  93. package/cli/selftune/workflows/workflows.ts +100 -26
  94. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  95. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  96. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  97. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  98. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +41 -1
  99. package/node_modules/@selftune/telemetry-contract/src/types.ts +103 -2
  100. package/package.json +25 -9
  101. package/packages/dashboard-core/AGENTS.md +18 -0
  102. package/packages/dashboard-core/README.md +30 -0
  103. package/packages/dashboard-core/index.ts +3 -0
  104. package/packages/dashboard-core/package.json +39 -0
  105. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  106. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  107. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  108. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  109. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  110. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  111. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  112. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  113. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  114. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  115. package/packages/dashboard-core/src/gates/index.ts +3 -0
  116. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  117. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  118. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  119. package/packages/dashboard-core/src/host/index.ts +3 -0
  120. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  121. package/packages/dashboard-core/src/models/index.ts +4 -0
  122. package/packages/dashboard-core/src/models/overview.ts +98 -0
  123. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  124. package/packages/dashboard-core/src/models/skills.ts +34 -0
  125. package/packages/dashboard-core/src/routes/index.ts +2 -0
  126. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  127. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  128. package/packages/dashboard-core/src/routes/types.ts +39 -0
  129. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  130. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  131. package/packages/dashboard-core/src/screens/index.ts +37 -0
  132. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  133. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  134. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  135. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  136. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  137. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  138. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  139. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  140. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  141. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  142. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  143. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  144. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  145. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  146. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  147. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  148. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  149. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  150. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  151. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  152. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  153. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  154. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  155. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  156. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  157. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  158. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  159. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  160. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  161. package/packages/telemetry-contract/src/schemas.ts +41 -1
  162. package/packages/telemetry-contract/src/types.ts +103 -2
  163. package/packages/ui/src/components/EvidenceViewer.tsx +80 -25
  164. package/packages/ui/src/components/OverviewPanels.tsx +67 -26
  165. package/packages/ui/src/primitives/tabs.tsx +7 -6
  166. package/packages/ui/src/types.ts +10 -0
  167. package/skill/SKILL.md +130 -332
  168. package/skill/agents/diagnosis-analyst.md +3 -3
  169. package/skill/agents/evolution-reviewer.md +3 -3
  170. package/skill/agents/integration-guide.md +3 -3
  171. package/skill/agents/pattern-analyst.md +2 -2
  172. package/skill/references/cli-quick-reference.md +89 -0
  173. package/skill/references/creator-playbook.md +131 -0
  174. package/skill/references/examples.md +48 -0
  175. package/skill/references/troubleshooting.md +47 -0
  176. package/skill/references/version-history.md +1 -1
  177. package/skill/selftune.contribute.json +11 -0
  178. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  179. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  180. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  181. package/skill/workflows/CreateTestDeploy.md +170 -0
  182. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  183. package/skill/{Workflows → workflows}/Cron.md +1 -1
  184. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  185. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  186. package/skill/{Workflows → workflows}/Evals.md +67 -2
  187. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  188. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  189. package/skill/{Workflows → workflows}/Grade.md +1 -1
  190. package/skill/{Workflows → workflows}/Initialize.md +8 -4
  191. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  192. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  193. package/skill/workflows/SignalsDashboard.md +87 -0
  194. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  195. package/skill/{Workflows → workflows}/Watch.md +42 -2
  196. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  197. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +0 -1
  198. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +0 -59
  199. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  200. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +0 -12
  201. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  202. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  203. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  204. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  205. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  206. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  207. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  208. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  209. /package/skill/{Workflows → workflows}/Ingest.md +0 -0
  210. /package/skill/{Workflows → workflows}/PlatformHooks.md +0 -0
  211. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  212. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  213. /package/skill/{Workflows → workflows}/Registry.md +0 -0
  214. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  215. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  216. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  217. /package/skill/{Workflows → workflows}/Sync.md +0 -0
  218. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  219. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ import { ArrowLeftIcon, BellIcon, BoltIcon, MenuIcon, SearchIcon, UserIcon } from "lucide-react";
4
+ import { useDeferredValue, useRef, useState } from "react";
5
+
6
+ import type { DashboardUser } from "../host/index";
7
+ import { getUserInitials, matchesSearchItem } from "./utils";
8
+ import type { DashboardHeaderMeta, DashboardLinkRenderer, DashboardSearchItem } from "./types";
9
+
10
+ interface DashboardHeaderProps {
11
+ renderLink: DashboardLinkRenderer;
12
+ headerMeta: DashboardHeaderMeta;
13
+ searchItems: DashboardSearchItem[];
14
+ headerUser?: DashboardUser;
15
+ onToggleSidebar(): void;
16
+ }
17
+
18
+ export function DashboardHeader({
19
+ renderLink,
20
+ headerMeta,
21
+ searchItems,
22
+ headerUser,
23
+ onToggleSidebar,
24
+ }: DashboardHeaderProps) {
25
+ const [query, setQuery] = useState("");
26
+ const [open, setOpen] = useState(false);
27
+ const suppressBlurRef = useRef(false);
28
+ const deferredQuery = useDeferredValue(query);
29
+
30
+ const filteredItems = searchItems
31
+ .filter((item) => matchesSearchItem(item, deferredQuery))
32
+ .slice(0, deferredQuery.trim() ? 12 : 8);
33
+
34
+ const groups = new Map<string, DashboardSearchItem[]>();
35
+ for (const item of filteredItems) {
36
+ const existing = groups.get(item.group) ?? [];
37
+ existing.push(item);
38
+ groups.set(item.group, existing);
39
+ }
40
+
41
+ return (
42
+ <header className="sticky top-0 z-40 border-b border-border/10 bg-background/80 backdrop-blur-xl">
43
+ <div className="flex h-16 items-center justify-between gap-4 px-4 lg:px-8">
44
+ <div className="flex min-w-0 flex-1 items-center gap-3 lg:gap-4">
45
+ <button
46
+ type="button"
47
+ className="rounded-lg bg-card p-2 text-foreground lg:hidden"
48
+ onClick={onToggleSidebar}
49
+ aria-label="Toggle sidebar"
50
+ >
51
+ <MenuIcon className="size-5" />
52
+ </button>
53
+
54
+ {headerMeta.backHref && headerMeta.backLabel
55
+ ? renderLink({
56
+ href: headerMeta.backHref,
57
+ className:
58
+ "inline-flex items-center gap-1 font-headline text-[10px] uppercase tracking-[0.2em] text-slate-500 transition-colors hover:text-primary",
59
+ children: (
60
+ <>
61
+ <ArrowLeftIcon className="size-3" />
62
+ {headerMeta.backLabel}
63
+ </>
64
+ ),
65
+ })
66
+ : null}
67
+
68
+ <div className="hidden xl:flex items-center gap-3 rounded-full border border-border/15 bg-card/60 px-3 py-1.5 text-sm shadow-[0_10px_40px_rgba(0,0,0,0.12)]">
69
+ {headerMeta.icon ? (
70
+ <span className="shrink-0 text-primary">{headerMeta.icon}</span>
71
+ ) : null}
72
+ <div className="flex min-w-0 items-center gap-2">
73
+ <span className="font-headline text-[10px] uppercase tracking-[0.18em] text-slate-500">
74
+ {headerMeta.badge ?? "View"}
75
+ </span>
76
+ <span className="truncate font-medium text-foreground">{headerMeta.title}</span>
77
+ </div>
78
+ </div>
79
+
80
+ <div className="relative w-full max-w-xl">
81
+ <SearchIcon className="pointer-events-none absolute left-3 top-1/2 z-10 size-4 -translate-y-1/2 text-slate-400" />
82
+ <input
83
+ value={query}
84
+ onChange={(event) => setQuery(event.target.value)}
85
+ onFocus={() => setOpen(true)}
86
+ onBlur={() => {
87
+ if (suppressBlurRef.current) {
88
+ suppressBlurRef.current = false;
89
+ return;
90
+ }
91
+ setOpen(false);
92
+ }}
93
+ placeholder="Search skills or pages..."
94
+ className="h-9 w-full rounded-full border border-border/10 bg-input/50 pl-10 pr-4 text-sm text-foreground outline-none transition focus:border-primary/30 focus:ring-1 focus:ring-primary/40 placeholder:text-slate-500"
95
+ />
96
+
97
+ {open ? (
98
+ <div className="absolute left-0 right-0 top-full z-50 mt-2 max-h-80 overflow-y-auto rounded-2xl border border-border/15 bg-card shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-xl">
99
+ {filteredItems.length === 0 ? (
100
+ <div className="px-4 py-5 text-sm text-muted-foreground">No results found.</div>
101
+ ) : (
102
+ Array.from(groups.entries()).map(([group, items]) => (
103
+ <div key={group} className="border-b border-border/10 last:border-b-0">
104
+ <div className="px-4 pt-3 text-[10px] uppercase tracking-[0.18em] text-slate-500">
105
+ {group}
106
+ </div>
107
+ <div className="p-2">
108
+ {items.map((item) => (
109
+ <button
110
+ key={item.id}
111
+ type="button"
112
+ onMouseDown={(event) => {
113
+ event.preventDefault();
114
+ suppressBlurRef.current = true;
115
+ }}
116
+ onClick={() => {
117
+ item.onSelect();
118
+ setOpen(false);
119
+ setQuery("");
120
+ }}
121
+ className="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-left text-sm transition-colors hover:bg-muted/40"
122
+ >
123
+ {item.leading ? (
124
+ <span className="shrink-0">{item.leading}</span>
125
+ ) : (
126
+ <span className="shrink-0 text-slate-400">
127
+ {item.meta ? "•" : ""}
128
+ </span>
129
+ )}
130
+ <div className="min-w-0 flex-1">
131
+ <div className="truncate text-foreground">{item.label}</div>
132
+ {item.meta ? (
133
+ <div className="truncate text-xs text-muted-foreground">
134
+ {item.meta}
135
+ </div>
136
+ ) : null}
137
+ </div>
138
+ {item.trailing ? (
139
+ <span className="shrink-0 text-xs text-muted-foreground">
140
+ {item.trailing}
141
+ </span>
142
+ ) : null}
143
+ </button>
144
+ ))}
145
+ </div>
146
+ </div>
147
+ ))
148
+ )}
149
+ </div>
150
+ ) : null}
151
+ </div>
152
+ </div>
153
+
154
+ <div className="hidden items-center gap-4 sm:flex lg:gap-6">
155
+ <div className="flex items-center gap-4 text-slate-400">
156
+ <span className="relative" aria-hidden="true">
157
+ <BellIcon className="size-4" />
158
+ <span className="absolute -right-0.5 -top-0.5 size-2 rounded-full border-2 border-background bg-primary shadow-[0_0_6px_color-mix(in_srgb,var(--primary)_50%,transparent)]" />
159
+ </span>
160
+ <BoltIcon className="size-4" aria-hidden="true" />
161
+ </div>
162
+
163
+ {headerUser ? (
164
+ <>
165
+ <div className="h-8 w-px bg-border/20" />
166
+ <div className="flex items-center gap-3">
167
+ <div className="hidden text-right md:block">
168
+ <div className="font-headline text-[10px] uppercase tracking-widest text-slate-400">
169
+ {headerUser.name}
170
+ </div>
171
+ <div className="text-[10px] uppercase tracking-widest text-primary">
172
+ {headerUser.subtitle ?? headerUser.email ?? "Active"}
173
+ </div>
174
+ </div>
175
+ {headerUser.image ? (
176
+ // eslint-disable-next-line @next/next/no-img-element
177
+ <img
178
+ src={headerUser.image}
179
+ alt={headerUser.name}
180
+ className="size-8 rounded-full"
181
+ />
182
+ ) : (
183
+ <div className="flex size-8 items-center justify-center rounded-full border border-primary/20 bg-card text-primary">
184
+ {headerUser.name ? (
185
+ <span className="text-xs font-medium">
186
+ {getUserInitials(headerUser.name)}
187
+ </span>
188
+ ) : (
189
+ <UserIcon className="size-4" />
190
+ )}
191
+ </div>
192
+ )}
193
+ </div>
194
+ </>
195
+ ) : null}
196
+ </div>
197
+ </div>
198
+ </header>
199
+ );
200
+ }
@@ -0,0 +1,219 @@
1
+ "use client";
2
+
3
+ import { ChevronDownIcon, LockIcon, LogOutIcon } from "lucide-react";
4
+ import { useState } from "react";
5
+
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@selftune/ui/primitives";
7
+
8
+ import type { DashboardUser } from "../host/index";
9
+ import { cn, getUserInitials } from "./utils";
10
+ import type {
11
+ DashboardBrand,
12
+ DashboardChromeAction,
13
+ DashboardLinkRenderer,
14
+ DashboardNavItem,
15
+ } from "./types";
16
+
17
+ interface DashboardSidebarProps {
18
+ brand: DashboardBrand;
19
+ navItems: DashboardNavItem[];
20
+ renderLink: DashboardLinkRenderer;
21
+ sidebarAction?: DashboardChromeAction;
22
+ sidebarUser?: DashboardUser;
23
+ onSignOut?(): Promise<void> | void;
24
+ mobileOpen: boolean;
25
+ onMobileOpenChange(open: boolean): void;
26
+ }
27
+
28
+ export function DashboardSidebar({
29
+ brand,
30
+ navItems,
31
+ renderLink,
32
+ sidebarAction,
33
+ sidebarUser,
34
+ onSignOut,
35
+ mobileOpen,
36
+ onMobileOpenChange,
37
+ }: DashboardSidebarProps) {
38
+ const [userMenuOpen, setUserMenuOpen] = useState(false);
39
+
40
+ return (
41
+ <>
42
+ {mobileOpen ? (
43
+ <button
44
+ type="button"
45
+ className="fixed inset-0 z-40 bg-black/50 lg:hidden"
46
+ onClick={() => onMobileOpenChange(false)}
47
+ aria-label="Close sidebar overlay"
48
+ />
49
+ ) : null}
50
+
51
+ <aside
52
+ className={cn(
53
+ "fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-sidebar-border bg-sidebar",
54
+ "transition-transform duration-200 lg:translate-x-0",
55
+ mobileOpen ? "translate-x-0" : "-translate-x-full",
56
+ )}
57
+ >
58
+ <div className="px-4 pb-8 pt-6">
59
+ {renderLink({
60
+ href: brand.href,
61
+ className: "flex items-center gap-3",
62
+ onClick: () => onMobileOpenChange(false),
63
+ children: (
64
+ <>
65
+ <div
66
+ className="size-8 shrink-0 bg-primary shadow-[0_0_12px_rgba(79,242,255,0.3)]"
67
+ role="img"
68
+ aria-label={brand.name}
69
+ style={{
70
+ WebkitMaskImage: "url(/logo.svg)",
71
+ WebkitMaskSize: "contain",
72
+ WebkitMaskRepeat: "no-repeat",
73
+ WebkitMaskPosition: "center",
74
+ maskImage: "url(/logo.svg)",
75
+ maskSize: "contain",
76
+ maskRepeat: "no-repeat",
77
+ maskPosition: "center",
78
+ }}
79
+ />
80
+ <div className="flex flex-col">
81
+ <div className="flex items-center gap-2">
82
+ <span className="font-headline text-2xl font-bold tracking-tighter text-primary text-glow">
83
+ {brand.name}
84
+ </span>
85
+ {brand.badge ? (
86
+ <span className="rounded bg-primary/10 px-1.5 py-0.5 text-xs font-medium text-primary">
87
+ {brand.badge}
88
+ </span>
89
+ ) : null}
90
+ </div>
91
+ {brand.caption ? (
92
+ <span className="font-headline text-[10px] uppercase tracking-[0.2em] text-slate-500">
93
+ {brand.caption}
94
+ </span>
95
+ ) : null}
96
+ </div>
97
+ </>
98
+ ),
99
+ })}
100
+ </div>
101
+
102
+ <nav className="flex-1 space-y-1 px-2">
103
+ {navItems.map((item) => (
104
+ <Tooltip key={item.href}>
105
+ <TooltipTrigger
106
+ render={renderLink({
107
+ href: item.href,
108
+ onClick: () => onMobileOpenChange(false),
109
+ className: cn(
110
+ "flex items-center gap-3 rounded-lg px-4 py-2.5 font-headline text-sm tracking-tight transition-all duration-200",
111
+ item.isActive
112
+ ? "bg-card font-bold text-primary shadow-[inset_0_0_0_1px_rgba(79,242,255,0.08)]"
113
+ : "text-slate-400 hover:bg-muted/50 hover:text-slate-200",
114
+ ),
115
+ children: (
116
+ <>
117
+ {item.icon}
118
+ <span className="flex min-w-0 items-center gap-2">
119
+ <span>{item.label}</span>
120
+ {item.isLocked ? <LockIcon className="size-3.5 opacity-70" /> : null}
121
+ </span>
122
+ </>
123
+ ),
124
+ })}
125
+ />
126
+ <TooltipContent side="right">{item.tooltip}</TooltipContent>
127
+ </Tooltip>
128
+ ))}
129
+ </nav>
130
+
131
+ <div className="px-4 pb-4">
132
+ {sidebarAction ? (
133
+ <Tooltip>
134
+ <TooltipTrigger
135
+ render={
136
+ <button
137
+ type="button"
138
+ disabled={sidebarAction.disabled}
139
+ aria-disabled={sidebarAction.disabled}
140
+ tabIndex={sidebarAction.disabled ? -1 : 0}
141
+ title={sidebarAction.tooltip}
142
+ onClick={sidebarAction.onClick}
143
+ className={cn(
144
+ "flex w-full items-center justify-center gap-2 rounded-xl border py-2.5 font-headline text-xs uppercase tracking-wider transition-colors",
145
+ sidebarAction.disabled
146
+ ? "cursor-not-allowed border-primary/15 bg-gradient-to-r from-primary/10 to-primary/5 text-primary/50 opacity-70"
147
+ : "border-primary/30 bg-gradient-to-r from-primary/12 to-primary/6 text-primary hover:border-primary/50 hover:bg-primary/10",
148
+ )}
149
+ >
150
+ {sidebarAction.icon}
151
+ <span>{sidebarAction.label}</span>
152
+ </button>
153
+ }
154
+ />
155
+ <TooltipContent side="right">{sidebarAction.tooltip}</TooltipContent>
156
+ </Tooltip>
157
+ ) : null}
158
+
159
+ {brand.footerLabel ? (
160
+ <div className="mt-3 flex items-center gap-2 px-4 py-1.5 font-headline text-[10px] uppercase tracking-widest text-slate-600">
161
+ <span className="size-1.5 animate-pulse rounded-full bg-primary shadow-[0_0_8px_rgba(79,242,255,0.4)]" />
162
+ <span>{brand.footerLabel}</span>
163
+ </div>
164
+ ) : null}
165
+ </div>
166
+
167
+ {sidebarUser ? (
168
+ <div className="border-t border-sidebar-border p-3">
169
+ <div className="relative">
170
+ <button
171
+ type="button"
172
+ onClick={() => setUserMenuOpen((open) => !open)}
173
+ className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-sidebar-accent/50"
174
+ >
175
+ {sidebarUser.image ? (
176
+ // eslint-disable-next-line @next/next/no-img-element
177
+ <img
178
+ src={sidebarUser.image}
179
+ alt={sidebarUser.name}
180
+ className="size-9 rounded-full"
181
+ />
182
+ ) : (
183
+ <div className="flex size-9 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground">
184
+ {getUserInitials(sidebarUser.name)}
185
+ </div>
186
+ )}
187
+ <span className="min-w-0 flex-1 text-left">
188
+ <span className="block truncate font-medium text-sidebar-foreground">
189
+ {sidebarUser.name}
190
+ </span>
191
+ <span className="block truncate text-xs text-slate-500">
192
+ {sidebarUser.subtitle ?? sidebarUser.email ?? "Signed in"}
193
+ </span>
194
+ </span>
195
+ {onSignOut ? <ChevronDownIcon className="size-4" /> : null}
196
+ </button>
197
+
198
+ {userMenuOpen && onSignOut ? (
199
+ <div className="absolute bottom-full left-0 mb-2 w-full rounded-lg border border-border bg-popover py-1 shadow-lg">
200
+ <button
201
+ type="button"
202
+ onClick={async () => {
203
+ setUserMenuOpen(false);
204
+ await onSignOut();
205
+ }}
206
+ className="flex w-full items-center gap-2 px-4 py-2 text-sm text-muted-foreground hover:bg-sidebar-accent/50"
207
+ >
208
+ <LogOutIcon className="size-4" />
209
+ <span>Sign out</span>
210
+ </button>
211
+ </div>
212
+ ) : null}
213
+ </div>
214
+ </div>
215
+ ) : null}
216
+ </aside>
217
+ </>
218
+ );
219
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { cn } from "./utils";
4
+ import type { RuntimeBadgeProps } from "./types";
5
+
6
+ export function RuntimeBadge({
7
+ href,
8
+ label,
9
+ detail,
10
+ tone = "healthy",
11
+ renderLink,
12
+ }: RuntimeBadgeProps) {
13
+ const toneClassName =
14
+ tone === "warning"
15
+ ? "text-amber-400 ring-amber-400/20 hover:bg-amber-400/8"
16
+ : tone === "critical"
17
+ ? "text-destructive ring-destructive/20 hover:bg-destructive/8"
18
+ : "text-primary ring-primary/20 hover:bg-primary/8";
19
+
20
+ const dotClassName =
21
+ tone === "warning"
22
+ ? "bg-amber-400"
23
+ : tone === "critical"
24
+ ? "bg-destructive"
25
+ : "animate-pulse bg-primary shadow-[0_0_8px_color-mix(in_srgb,var(--primary)_60%,transparent)]";
26
+
27
+ return (
28
+ <footer className="pointer-events-none fixed bottom-4 right-4 z-20">
29
+ {renderLink({
30
+ href,
31
+ className: cn(
32
+ "glass-panel pointer-events-auto flex items-center gap-2 rounded-full border border-foreground/5 px-3 py-2 font-headline text-[10px] uppercase tracking-[0.18em] text-slate-300 shadow-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
33
+ toneClassName,
34
+ ),
35
+ children: (
36
+ <>
37
+ <span className={cn("size-1.5 rounded-full", dotClassName)} />
38
+ <span>{label}</span>
39
+ <span className="text-foreground/25">/</span>
40
+ <span className="text-slate-400">{detail}</span>
41
+ </>
42
+ ),
43
+ })}
44
+ </footer>
45
+ );
46
+ }
@@ -0,0 +1,14 @@
1
+ export { DashboardChrome } from "./DashboardChrome";
2
+ export { RuntimeBadge } from "./RuntimeBadge";
3
+ export type {
4
+ DashboardBrand,
5
+ DashboardChromeAction,
6
+ DashboardChromeProps,
7
+ DashboardHeaderMeta,
8
+ DashboardLinkRenderer,
9
+ DashboardLinkRenderProps,
10
+ DashboardNavItem,
11
+ DashboardSearchItem,
12
+ RuntimeBadgeProps,
13
+ RuntimeBadgeTone,
14
+ } from "./types";
@@ -0,0 +1,81 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+
3
+ import type { DashboardUser } from "../host/index";
4
+
5
+ export interface DashboardLinkRenderProps {
6
+ href: string;
7
+ className?: string;
8
+ children: ReactNode;
9
+ onClick?: () => void;
10
+ }
11
+
12
+ export type DashboardLinkRenderer = (props: DashboardLinkRenderProps) => ReactElement;
13
+
14
+ export interface DashboardNavItem {
15
+ href: string;
16
+ label: string;
17
+ tooltip: string;
18
+ icon: ReactNode;
19
+ isActive: boolean;
20
+ isLocked?: boolean;
21
+ }
22
+
23
+ export interface DashboardSearchItem {
24
+ id: string;
25
+ group: string;
26
+ label: string;
27
+ meta?: string | null;
28
+ keywords?: readonly string[];
29
+ leading?: ReactNode;
30
+ trailing?: ReactNode;
31
+ onSelect(): void;
32
+ }
33
+
34
+ export interface DashboardHeaderMeta {
35
+ title: string;
36
+ icon?: ReactNode;
37
+ badge?: string;
38
+ backHref?: string | null;
39
+ backLabel?: string | null;
40
+ }
41
+
42
+ export interface DashboardBrand {
43
+ href: string;
44
+ name: string;
45
+ caption?: string;
46
+ badge?: string;
47
+ footerLabel?: string;
48
+ }
49
+
50
+ export interface DashboardChromeAction {
51
+ label: string;
52
+ tooltip: string;
53
+ icon?: ReactNode;
54
+ disabled?: boolean;
55
+ onClick?(): void;
56
+ }
57
+
58
+ export interface DashboardChromeProps {
59
+ brand: DashboardBrand;
60
+ navItems: DashboardNavItem[];
61
+ renderLink: DashboardLinkRenderer;
62
+ headerMeta: DashboardHeaderMeta;
63
+ searchItems?: DashboardSearchItem[];
64
+ headerUser?: DashboardUser;
65
+ sidebarUser?: DashboardUser;
66
+ sidebarAction?: DashboardChromeAction;
67
+ onSignOut?(): Promise<void> | void;
68
+ overlay?: ReactNode;
69
+ contentClassName?: string | null;
70
+ children: ReactNode;
71
+ }
72
+
73
+ export type RuntimeBadgeTone = "healthy" | "warning" | "critical";
74
+
75
+ export interface RuntimeBadgeProps {
76
+ href: string;
77
+ label: string;
78
+ detail: string;
79
+ tone?: RuntimeBadgeTone;
80
+ renderLink: DashboardLinkRenderer;
81
+ }
@@ -0,0 +1,23 @@
1
+ import type { DashboardSearchItem } from "./types";
2
+
3
+ export function cn(...values: Array<string | false | null | undefined>): string {
4
+ return values.filter(Boolean).join(" ");
5
+ }
6
+
7
+ export function getUserInitials(name: string): string {
8
+ const parts = name.trim().split(/\s+/).slice(0, 2);
9
+ return parts.map((part) => part.charAt(0).toUpperCase()).join("") || "?";
10
+ }
11
+
12
+ function normalize(value: string): string {
13
+ return value.trim().toLowerCase();
14
+ }
15
+
16
+ export function matchesSearchItem(item: DashboardSearchItem, query: string): boolean {
17
+ const needle = normalize(query);
18
+ if (!needle) return true;
19
+
20
+ const haystack = [item.label, item.meta ?? "", ...(item.keywords ?? [])].map(normalize).join(" ");
21
+
22
+ return haystack.includes(needle);
23
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ interface FeatureGateProps {
4
+ enabled: boolean;
5
+ fallback?: ReactNode;
6
+ children: ReactNode;
7
+ }
8
+
9
+ export function FeatureGate({ enabled, fallback = null, children }: FeatureGateProps) {
10
+ return enabled ? <>{children}</> : <>{fallback}</>;
11
+ }
@@ -0,0 +1,29 @@
1
+ import { UpgradeCard } from "./UpgradeCard";
2
+
3
+ interface LockedRouteProps {
4
+ eyebrow: string;
5
+ title: string;
6
+ description: string;
7
+ highlights?: readonly string[];
8
+ primaryAction: {
9
+ href: string;
10
+ label: string;
11
+ };
12
+ secondaryAction?: {
13
+ href: string;
14
+ label: string;
15
+ };
16
+ note?: string;
17
+ }
18
+
19
+ export function LockedRoute(props: LockedRouteProps) {
20
+ return (
21
+ <div className="@container/main flex flex-1 flex-col py-6">
22
+ <div className="grid grid-cols-12 gap-6 px-4 lg:px-6">
23
+ <div className="col-span-12">
24
+ <UpgradeCard {...props} />
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }