@notis_ai/cli 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +33 -9
  2. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
  3. package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
  4. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
  5. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
  6. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
  7. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
  8. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
  9. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
  10. package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
  11. package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
  12. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
  13. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
  14. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
  15. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
  16. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  17. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  18. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  19. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
  20. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
  21. package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
  22. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
  23. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
  24. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  25. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  26. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
  27. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
  28. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
  29. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
  30. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  31. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
  32. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
  33. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  34. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
  35. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
  36. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
  37. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
  38. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
  39. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
  40. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
  41. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
  42. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
  43. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
  44. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
  45. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
  46. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
  47. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
  48. package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
  49. package/dist/scaffolds/notis-database/notis.config.ts +0 -1
  50. package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
  51. package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
  52. package/dist/scaffolds/notis-journal/app/globals.css +37 -0
  53. package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
  54. package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
  55. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
  56. package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
  57. package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
  58. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
  59. package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
  60. package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
  61. package/dist/scaffolds/notis-journal/components.json +20 -0
  62. package/dist/scaffolds/notis-journal/index.html +12 -0
  63. package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
  64. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  65. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  66. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  67. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  68. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  69. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  70. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
  71. package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
  72. package/dist/scaffolds/notis-journal/package.json +34 -0
  73. package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
  74. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  75. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
  76. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  77. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  78. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  79. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
  80. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
  81. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
  82. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  83. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
  84. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
  85. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  86. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
  87. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  88. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
  89. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
  90. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  91. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  92. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
  93. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
  94. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
  95. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
  96. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
  97. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
  98. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
  99. package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
  100. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
  101. package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
  102. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
  103. package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
  104. package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
  105. package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
  106. package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
  107. package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
  108. package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
  109. package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
  110. package/dist/scaffolds/notis-random/notis.config.ts +0 -1
  111. package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
  112. package/dist/scaffolds.json +22 -0
  113. package/package.json +3 -3
  114. package/skills/notis-apps/SKILL.md +43 -5
  115. package/skills/notis-apps/cli.md +22 -6
  116. package/skills/notis-cli/SKILL.md +20 -2
  117. package/skills/notis-query/cli.md +1 -1
  118. package/src/cli.js +8 -1
  119. package/src/command-specs/apps.js +307 -36
  120. package/src/command-specs/diagnostics.js +674 -0
  121. package/src/command-specs/helpers.js +4 -1
  122. package/src/command-specs/index.js +7 -1
  123. package/src/command-specs/meta.js +8 -2
  124. package/src/command-specs/smoke.js +386 -0
  125. package/src/command-specs/tools.js +237 -44
  126. package/src/runtime/agent-browser.js +204 -21
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-dev-server.js +21 -4
  129. package/src/runtime/app-dev-sessions.js +99 -1
  130. package/src/runtime/app-platform.js +197 -18
  131. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  132. package/src/runtime/desktop-auth.js +93 -0
  133. package/src/runtime/output.js +12 -1
  134. package/src/runtime/profiles.js +72 -16
  135. package/src/runtime/store-screenshot.js +138 -0
  136. package/src/runtime/transport.js +24 -82
  137. package/template/.harness/index.html.tmpl +128 -6
  138. package/template/CHANGELOG.md +5 -0
  139. package/template/app/page.tsx +11 -41
  140. package/template/notis.config.ts +0 -1
  141. package/template/package-lock.json +4137 -0
  142. package/template/package.json +1 -0
  143. package/template/packages/sdk/package.json +4 -0
  144. package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  145. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  146. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
  147. package/template/packages/sdk/src/config.ts +74 -0
  148. package/template/packages/sdk/src/documents.ts +229 -0
  149. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  150. package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
  151. package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
  152. package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
  153. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
  154. package/template/packages/sdk/src/index.ts +30 -1
  155. package/template/packages/sdk/src/runtime.ts +76 -17
  156. package/template/packages/sdk/src/styles.css +148 -0
  157. /package/{template → dist/scaffolds/notis-affiliate-prospects}/packages/sdk/src/hooks/useDatabase.ts +0 -0
@@ -0,0 +1,65 @@
1
+ import { defineNotisApp } from '@notis/sdk/config';
2
+
3
+ export default defineNotisApp({
4
+ name: 'affiliate-prospects',
5
+ title: 'Affiliate Prospects',
6
+ description:
7
+ 'A campaign control center for sourcing, qualifying, contacting, and activating high-fit Notis affiliates.',
8
+ icon: 'phosphor:users-three',
9
+ accent: 'violet',
10
+ author: { name: 'Florian (Flo) Pariset' },
11
+ categories: ['Sales & Marketing', 'Operations'],
12
+ tagline: 'Turn affiliate recruiting into a measurable, repeatable pipeline.',
13
+ screenshots: [
14
+ {
15
+ path: 'metadata/screenshot-1.png',
16
+ alt: 'Affiliate campaign dashboard with pipeline, priority queue, and segment performance.',
17
+ route: 'dashboard',
18
+ scenario: 'affiliate-dashboard',
19
+ focus: '[data-store-screenshot="dashboard"]',
20
+ theme: 'light',
21
+ },
22
+ {
23
+ path: 'metadata/screenshot-2.png',
24
+ alt: 'Affiliate prospect list with status, segment, fit score, source, and next action.',
25
+ route: 'prospects',
26
+ scenario: 'affiliate-prospects',
27
+ focus: '[data-store-screenshot="prospects"]',
28
+ theme: 'light',
29
+ },
30
+ {
31
+ path: 'metadata/screenshot-3.png',
32
+ alt: 'Affiliate segment scorecards showing campaign progress and sourcing playbooks.',
33
+ route: 'segments',
34
+ scenario: 'affiliate-segments',
35
+ focus: '[data-store-screenshot="segments"]',
36
+ theme: 'dark',
37
+ },
38
+ ],
39
+
40
+ databases: ['affiliate_prospects_1', 'affiliate_outreach_events_1'],
41
+
42
+ routes: [
43
+ {
44
+ path: '/',
45
+ slug: 'dashboard',
46
+ name: 'Dashboard',
47
+ icon: 'phosphor:squares-four',
48
+ default: true,
49
+ },
50
+ {
51
+ path: '/prospects',
52
+ slug: 'prospects',
53
+ name: 'Prospects',
54
+ icon: 'phosphor:users',
55
+ },
56
+ {
57
+ path: '/segments',
58
+ slug: 'segments',
59
+ name: 'Segments',
60
+ icon: 'phosphor:circles-three-plus',
61
+ },
62
+ ],
63
+
64
+ tools: ['LOCAL_NOTIS_DATABASE_QUERY'],
65
+ });
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "affiliate-prospects",
3
+ "version": "0.1.0",
4
+ "notisAppVersion": "0.1.0",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "dev": "vite",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@notis/sdk": "file:./packages/sdk",
14
+ "@phosphor-icons/react": "^2.1.10",
15
+ "clsx": "^2.1.1",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "tailwind-merge": "^2.6.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "@types/react": "^19.0.0",
23
+ "@types/react-dom": "^19.0.0",
24
+ "@vitejs/plugin-react": "^4.3.0",
25
+ "autoprefixer": "^10.4.0",
26
+ "postcss": "^8.4.0",
27
+ "tailwindcss": "^3.4.0",
28
+ "tailwindcss-animate": "^1.0.7",
29
+ "typescript": "^5.5.0",
30
+ "vite": "^6.0.0"
31
+ }
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@notis/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./config": "./src/config.ts",
9
+ "./vite": "./src/vite.ts",
10
+ "./ui": "./src/ui.ts",
11
+ "./styles.css": "./src/styles.css"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "template"
16
+ ],
17
+ "scripts": {
18
+ "type-check": "tsc --noEmit"
19
+ },
20
+ "peerDependencies": {
21
+ "react": ">=18.0.0",
22
+ "react-dom": ">=18.0.0",
23
+ "vite": ">=5.0.0",
24
+ "@vitejs/plugin-react": ">=4.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "@types/react": "^19.0.0",
29
+ "@types/react-dom": "^19.0.0",
30
+ "typescript": "^5.0.0"
31
+ }
32
+ }
@@ -0,0 +1,273 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type CSSProperties,
9
+ type ReactElement,
10
+ type ReactNode,
11
+ } from 'react';
12
+
13
+ export interface MultiSelectAction {
14
+ id: string;
15
+ label: string;
16
+ icon: ReactNode;
17
+ /** Single-key (e.g. "E", "#") or "Mod+K" shortcut. Bound while the bar is mounted with count > 0. */
18
+ shortcut?: string;
19
+ onRun: () => void | Promise<void>;
20
+ /** Hint for future destructive styling. Currently unused visually. */
21
+ destructive?: boolean;
22
+ }
23
+
24
+ export interface MultiSelectActionBarProps {
25
+ selectedCount: number;
26
+ actions: MultiSelectAction[];
27
+ /** Override the "{count} selected" label units. Default: "selected" with no item word. */
28
+ itemLabel?: { singular: string; plural: string };
29
+ /** Optional extra class on the outer container (composed alongside the inline styles). */
30
+ className?: string;
31
+ /** Optional override for the bar's positioning style. Defaults to bottom-center floating. */
32
+ style?: CSSProperties;
33
+ }
34
+
35
+ interface ParsedShortcut {
36
+ key: string;
37
+ needsMod: boolean;
38
+ needsShift: boolean;
39
+ }
40
+
41
+ function parseShortcut(raw: string): ParsedShortcut | null {
42
+ const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
43
+ if (parts.length === 0) return null;
44
+ let needsMod = false;
45
+ let needsShift = false;
46
+ let key = '';
47
+ for (const part of parts) {
48
+ const lower = part.toLowerCase();
49
+ if (lower === 'mod' || lower === 'cmd' || lower === 'ctrl' || lower === 'meta') {
50
+ needsMod = true;
51
+ } else if (lower === 'shift') {
52
+ needsShift = true;
53
+ } else {
54
+ key = lower;
55
+ }
56
+ }
57
+ if (!key) return null;
58
+ return { key, needsMod, needsShift };
59
+ }
60
+
61
+ function shortcutMatches(shortcut: ParsedShortcut, event: KeyboardEvent): boolean {
62
+ const eventKey = event.key.toLowerCase();
63
+ // Special-case "#" so Shift+3 matches without the consumer specifying Shift.
64
+ if (shortcut.key === '#') {
65
+ if (eventKey === '#') return true;
66
+ if (event.shiftKey && eventKey === '3') return true;
67
+ return false;
68
+ }
69
+ if (eventKey !== shortcut.key) return false;
70
+ if (shortcut.needsMod !== Boolean(event.metaKey || event.ctrlKey)) return false;
71
+ if (shortcut.needsShift !== event.shiftKey) return false;
72
+ if (event.altKey) return false;
73
+ return true;
74
+ }
75
+
76
+ function isEditableTarget(target: EventTarget | null): boolean {
77
+ if (!(target instanceof HTMLElement)) return false;
78
+ if (target.isContentEditable) return true;
79
+ const tag = target.tagName;
80
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
81
+ }
82
+
83
+ function shortcutDisplay(raw: string): string {
84
+ const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
85
+ return parts
86
+ .map((part) => {
87
+ const lower = part.toLowerCase();
88
+ if (lower === 'mod' || lower === 'cmd' || lower === 'meta') return '⌘';
89
+ if (lower === 'ctrl') return '⌃';
90
+ if (lower === 'shift') return '⇧';
91
+ if (lower === 'alt' || lower === 'option') return '⌥';
92
+ return part.length === 1 ? part.toUpperCase() : part;
93
+ })
94
+ .join('');
95
+ }
96
+
97
+ const containerBaseStyle: CSSProperties = {
98
+ position: 'fixed',
99
+ bottom: '1rem',
100
+ left: '50%',
101
+ transform: 'translateX(-50%)',
102
+ zIndex: 60,
103
+ display: 'flex',
104
+ alignItems: 'center',
105
+ gap: '0.25rem',
106
+ padding: '0.375rem 0.5rem',
107
+ borderRadius: '0.5rem',
108
+ background: 'color-mix(in srgb, hsl(var(--foreground)) 95%, transparent)',
109
+ color: 'hsl(var(--background))',
110
+ boxShadow: '0 10px 25px -10px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.08)',
111
+ backdropFilter: 'blur(6px)',
112
+ WebkitBackdropFilter: 'blur(6px)',
113
+ pointerEvents: 'auto',
114
+ fontSize: '13px',
115
+ lineHeight: 1.2,
116
+ };
117
+
118
+ const countStyle: CSSProperties = {
119
+ padding: '0 0.5rem',
120
+ fontSize: '12px',
121
+ fontWeight: 500,
122
+ fontVariantNumeric: 'tabular-nums',
123
+ color: 'color-mix(in srgb, hsl(var(--background)) 70%, transparent)',
124
+ };
125
+
126
+ const dividerStyle: CSSProperties = {
127
+ width: '1px',
128
+ height: '1rem',
129
+ margin: '0 0.125rem',
130
+ background: 'color-mix(in srgb, hsl(var(--background)) 20%, transparent)',
131
+ };
132
+
133
+ const baseButtonStyle: CSSProperties = {
134
+ display: 'inline-flex',
135
+ alignItems: 'center',
136
+ gap: '0.375rem',
137
+ padding: '0.25rem 0.5rem',
138
+ border: 0,
139
+ background: 'transparent',
140
+ color: 'color-mix(in srgb, hsl(var(--background)) 90%, transparent)',
141
+ borderRadius: '0.375rem',
142
+ cursor: 'pointer',
143
+ fontSize: '13px',
144
+ fontFamily: 'inherit',
145
+ transition: 'background-color 120ms ease, color 120ms ease',
146
+ };
147
+
148
+ const keycapStyle: CSSProperties = {
149
+ display: 'inline-flex',
150
+ alignItems: 'center',
151
+ justifyContent: 'center',
152
+ minWidth: '20px',
153
+ height: '20px',
154
+ padding: '0 4px',
155
+ borderRadius: '4px',
156
+ border: '1px solid color-mix(in srgb, hsl(var(--background)) 20%, transparent)',
157
+ background: 'color-mix(in srgb, hsl(var(--background)) 15%, transparent)',
158
+ color: 'inherit',
159
+ fontSize: '11px',
160
+ fontWeight: 500,
161
+ lineHeight: 1,
162
+ fontFamily: 'inherit',
163
+ };
164
+
165
+ const iconSlotStyle: CSSProperties = {
166
+ display: 'inline-flex',
167
+ alignItems: 'center',
168
+ justifyContent: 'center',
169
+ opacity: 0.8,
170
+ };
171
+
172
+ export function MultiSelectActionBar({
173
+ selectedCount,
174
+ actions,
175
+ itemLabel,
176
+ className,
177
+ style,
178
+ }: MultiSelectActionBarProps): ReactElement | null {
179
+ const actionsRef = useRef(actions);
180
+ actionsRef.current = actions;
181
+
182
+ const parsedShortcuts = useMemo(() => {
183
+ return actions
184
+ .map((action) => {
185
+ if (!action.shortcut) return null;
186
+ const parsed = parseShortcut(action.shortcut);
187
+ return parsed ? { action, parsed } : null;
188
+ })
189
+ .filter((entry): entry is { action: MultiSelectAction; parsed: ParsedShortcut } => entry !== null);
190
+ }, [actions]);
191
+
192
+ useEffect(() => {
193
+ if (selectedCount === 0 || parsedShortcuts.length === 0) return;
194
+
195
+ const handleKeyDown = (event: KeyboardEvent) => {
196
+ if (isEditableTarget(event.target)) return;
197
+ for (const { action, parsed } of parsedShortcuts) {
198
+ if (shortcutMatches(parsed, event)) {
199
+ event.preventDefault();
200
+ event.stopPropagation();
201
+ if (typeof event.stopImmediatePropagation === 'function') {
202
+ event.stopImmediatePropagation();
203
+ }
204
+ void action.onRun();
205
+ return;
206
+ }
207
+ }
208
+ };
209
+
210
+ document.addEventListener('keydown', handleKeyDown, true);
211
+ return () => {
212
+ document.removeEventListener('keydown', handleKeyDown, true);
213
+ };
214
+ }, [parsedShortcuts, selectedCount]);
215
+
216
+ if (selectedCount === 0) return null;
217
+
218
+ const countWord = itemLabel
219
+ ? selectedCount === 1
220
+ ? itemLabel.singular
221
+ : itemLabel.plural
222
+ : 'selected';
223
+ const countLabel = itemLabel
224
+ ? `${selectedCount} ${countWord} selected`
225
+ : `${selectedCount} selected`;
226
+
227
+ const composedStyle: CSSProperties = { ...containerBaseStyle, ...(style || {}) };
228
+
229
+ return (
230
+ <div
231
+ role="toolbar"
232
+ aria-label={`Bulk actions for ${selectedCount} selected ${countWord}`}
233
+ className={className}
234
+ style={composedStyle}
235
+ >
236
+ <span style={countStyle}>{countLabel}</span>
237
+ {actions.length > 0 ? <span aria-hidden style={dividerStyle} /> : null}
238
+ {actions.map((action) => (
239
+ <ActionButton key={action.id} action={action} />
240
+ ))}
241
+ </div>
242
+ );
243
+ }
244
+
245
+ function ActionButton({ action }: { action: MultiSelectAction }) {
246
+ const [hover, setHover] = useState(false);
247
+ const display = action.shortcut ? shortcutDisplay(action.shortcut) : null;
248
+ const buttonStyle: CSSProperties = {
249
+ ...baseButtonStyle,
250
+ background: hover
251
+ ? 'color-mix(in srgb, hsl(var(--background)) 12%, transparent)'
252
+ : 'transparent',
253
+ color: hover
254
+ ? 'hsl(var(--background))'
255
+ : baseButtonStyle.color,
256
+ };
257
+
258
+ return (
259
+ <button
260
+ type="button"
261
+ onClick={() => void action.onRun()}
262
+ onMouseEnter={() => setHover(true)}
263
+ onMouseLeave={() => setHover(false)}
264
+ onFocus={() => setHover(true)}
265
+ onBlur={() => setHover(false)}
266
+ style={buttonStyle}
267
+ >
268
+ <span aria-hidden style={iconSlotStyle}>{action.icon}</span>
269
+ {display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
270
+ <span>{action.label}</span>
271
+ </button>
272
+ );
273
+ }
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
4
+
5
+ export interface MultiSelectCheckboxProps {
6
+ isSelected: boolean;
7
+ onClick: (event: ReactMouseEvent) => void;
8
+ /** When true, the checkbox is always visible. Default false (hover/focus reveal via the parent's :hover state). */
9
+ alwaysVisible?: boolean;
10
+ /** Optional aria-label override. Defaults to "Select item" / "Deselect item". */
11
+ ariaLabel?: string;
12
+ className?: string;
13
+ /** Optional inline-style override merged with the defaults. */
14
+ style?: CSSProperties;
15
+ }
16
+
17
+ const baseStyle: CSSProperties = {
18
+ flexShrink: 0,
19
+ display: 'inline-flex',
20
+ alignItems: 'center',
21
+ justifyContent: 'center',
22
+ width: '16px',
23
+ height: '16px',
24
+ borderRadius: '4px',
25
+ border: '1px solid hsl(var(--border))',
26
+ background: 'transparent',
27
+ color: 'hsl(var(--primary-foreground))',
28
+ cursor: 'pointer',
29
+ padding: 0,
30
+ transition: 'background-color 120ms ease, border-color 120ms ease, opacity 150ms ease',
31
+ };
32
+
33
+ const selectedStyle: CSSProperties = {
34
+ background: 'hsl(var(--primary))',
35
+ borderColor: 'hsl(var(--primary))',
36
+ };
37
+
38
+ const checkPath = 'M3.5 7.5l2.5 2.5 6.5-6.5';
39
+
40
+ export function MultiSelectCheckbox({
41
+ isSelected,
42
+ onClick,
43
+ alwaysVisible = false,
44
+ ariaLabel,
45
+ className,
46
+ style,
47
+ }: MultiSelectCheckboxProps): ReactElement {
48
+ const merged: CSSProperties = {
49
+ ...baseStyle,
50
+ ...(isSelected ? selectedStyle : null),
51
+ ...(alwaysVisible || isSelected ? { opacity: 1 } : null),
52
+ ...(style || null),
53
+ };
54
+
55
+ // When alwaysVisible is false and the row isn't selected, defer the
56
+ // hover-reveal to the parent — consumers wrap the checkbox in a `.group`
57
+ // hover scope and pass `data-notis-hover-reveal` styles via className.
58
+ // Out of the box, we render the checkbox at full opacity if selected,
59
+ // otherwise inherit the parent's reveal state.
60
+ return (
61
+ <button
62
+ type="button"
63
+ role="checkbox"
64
+ aria-checked={isSelected}
65
+ aria-label={ariaLabel ?? (isSelected ? 'Deselect item' : 'Select item')}
66
+ onClick={onClick}
67
+ onMouseDown={(e) => e.stopPropagation()}
68
+ data-notis-multiselect-checkbox=""
69
+ data-selected={isSelected ? 'true' : 'false'}
70
+ className={className}
71
+ style={merged}
72
+ >
73
+ {isSelected ? (
74
+ <svg
75
+ width="10"
76
+ height="10"
77
+ viewBox="0 0 16 16"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ strokeWidth={3}
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ aria-hidden
84
+ focusable="false"
85
+ >
86
+ <path d={checkPath} />
87
+ </svg>
88
+ ) : null}
89
+ </button>
90
+ );
91
+ }
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { type CSSProperties, type ReactElement } from 'react';
4
+ import type { DragRect } from '../hooks/useMultiSelect';
5
+
6
+ export interface MultiSelectDragOverlayProps {
7
+ rect: DragRect | null;
8
+ className?: string;
9
+ style?: CSSProperties;
10
+ }
11
+
12
+ const overlayBaseStyle: CSSProperties = {
13
+ position: 'fixed',
14
+ borderRadius: '3px',
15
+ border: '1px solid rgba(35, 131, 226, 0.3)',
16
+ background: 'rgba(35, 131, 226, 0.08)',
17
+ pointerEvents: 'none',
18
+ zIndex: 50,
19
+ };
20
+
21
+ export function MultiSelectDragOverlay({
22
+ rect,
23
+ className,
24
+ style,
25
+ }: MultiSelectDragOverlayProps): ReactElement | null {
26
+ if (!rect) return null;
27
+ if (rect.width === 0 && rect.height === 0) return null;
28
+
29
+ const merged: CSSProperties = {
30
+ ...overlayBaseStyle,
31
+ left: rect.left,
32
+ top: rect.top,
33
+ width: rect.width,
34
+ height: rect.height,
35
+ ...(style || null),
36
+ };
37
+
38
+ return <div aria-hidden className={className} style={merged} />;
39
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Configuration utilities for notis.config.ts.
3
+ *
4
+ * Usage:
5
+ * ```ts
6
+ * // notis.config.ts
7
+ * import { defineNotisApp } from '@notis/sdk/config';
8
+ *
9
+ * export default defineNotisApp({
10
+ * name: 'My App',
11
+ * description: 'Does things',
12
+ * icon: 'phosphor:squares-four',
13
+ * databases: ['tasks'],
14
+ * routes: [
15
+ * {
16
+ * path: '/',
17
+ * slug: 'notes',
18
+ * name: 'Notes',
19
+ * default: true,
20
+ * collection: {
21
+ * database: 'notes',
22
+ * titleProperty: 'Name',
23
+ * parentProperty: 'Parent',
24
+ * sidebar: {
25
+ * mode: 'tree',
26
+ * allowCreate: true,
27
+ * },
28
+ * },
29
+ * },
30
+ * ],
31
+ * tools: [...],
32
+ * });
33
+ * ```
34
+ */
35
+
36
+ export interface NotisRouteConfig {
37
+ path: string;
38
+ slug: string;
39
+ name: string;
40
+ icon?: string;
41
+ parentSlug?: string | null;
42
+ default?: boolean;
43
+ exportName?: string;
44
+ collection?: {
45
+ database: string;
46
+ titleProperty: string;
47
+ parentProperty?: string | null;
48
+ sidebar?: {
49
+ mode: 'flat-list' | 'tree';
50
+ allowCreate: boolean;
51
+ };
52
+ };
53
+ }
54
+
55
+ export interface NotisAppConfig {
56
+ name: string;
57
+ title?: string;
58
+ description?: string;
59
+ icon?: string;
60
+ accent?: 'blue' | 'violet' | 'emerald' | 'amber' | 'rose' | 'sky' | 'fuchsia' | 'teal';
61
+ author?: {
62
+ name: string;
63
+ handle?: string;
64
+ url?: string;
65
+ };
66
+ categories?: Array<
67
+ 'Productivity' | 'Sales & Marketing' | 'Operations' | 'Product & Engineering' | 'Personal'
68
+ >;
69
+ tagline?: string;
70
+ screenshots?: Array<{
71
+ path: string;
72
+ alt: string;
73
+ route?: string;
74
+ scenario?: string;
75
+ focus?: string;
76
+ theme?: 'light' | 'dark';
77
+ }>;
78
+ databases?: string[];
79
+ routes?: NotisRouteConfig[];
80
+ tools?: string[];
81
+ }
82
+
83
+ /**
84
+ * Identity function that provides type checking and autocomplete for the
85
+ * Notis app configuration. The returned object is read at build time by
86
+ * `notis apps build` to generate the manifest.
87
+ */
88
+ export function defineNotisApp(config: NotisAppConfig): NotisAppConfig {
89
+ return config;
90
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+
6
+ interface UseBackendResult {
7
+ /**
8
+ * Make an authenticated request to the Notis backend.
9
+ * The path should be an absolute backend path (e.g. `/portal_composio/actions`).
10
+ */
11
+ request: (path: string, options?: {
12
+ method?: string;
13
+ headers?: Record<string, string>;
14
+ body?: unknown;
15
+ }) => Promise<unknown>;
16
+ }
17
+
18
+ /**
19
+ * Raw backend request proxy. Use this for custom server endpoints or
20
+ * integration APIs not covered by the typed hooks.
21
+ *
22
+ * ```tsx
23
+ * const { request } = useBackend();
24
+ * const data = await request('/portal_composio/actions', { method: 'POST', body: { ... } });
25
+ * ```
26
+ */
27
+ export function useBackend(): UseBackendResult {
28
+ const runtime = useNotisRuntime();
29
+
30
+ const request = useCallback(
31
+ async (path: string, options?: { method?: string; headers?: Record<string, string>; body?: unknown }) => {
32
+ if (!runtime) {
33
+ throw new Error('Notis runtime not available. Ensure NotisProvider is mounted.');
34
+ }
35
+ return runtime.request(path, options);
36
+ },
37
+ [runtime],
38
+ );
39
+
40
+ return { request };
41
+ }