@notis_ai/cli 0.2.0-beta.156.1 → 0.2.0-beta.158.1

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 (69) hide show
  1. package/README.md +11 -45
  2. package/config/notis_app_design_rules.json +135 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +8893 -10612
  4. package/dist/base-skills/notis-apps/SKILL.md +25 -573
  5. package/dist/base-skills/notis-apps/references/architecture.md +147 -0
  6. package/dist/base-skills/notis-apps/references/design.md +154 -0
  7. package/dist/base-skills/notis-apps/references/release.md +93 -0
  8. package/dist/base-skills/notis-apps/references/sdk.md +60 -0
  9. package/dist/base-skills/notis-apps/references/troubleshooting.md +26 -0
  10. package/dist/base-skills/notis-cli/SKILL.md +19 -267
  11. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  12. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  13. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  14. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  15. package/dist/base-skills/notis-query/SKILL.md +13 -651
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +50 -0
  18. package/dist/base-skills/notis-query/references/query.md +543 -0
  19. package/dist/skill-sync/index.js +24 -7
  20. package/dist/skill-sync/index.js.map +4 -4
  21. package/dist/skill-sync-worker.mjs +2989 -0
  22. package/package.json +1 -2
  23. package/skills/notis-apps/cli.md +34 -95
  24. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +1 -1
  25. package/src/cli.js +4 -0
  26. package/src/command-specs/apps.js +322 -1560
  27. package/src/command-specs/diagnostics.js +37 -0
  28. package/src/command-specs/skills.js +23 -5
  29. package/src/runtime/agent-browser.js +169 -1
  30. package/src/runtime/app-boundary-validator.js +221 -0
  31. package/src/runtime/app-platform.js +359 -233
  32. package/src/runtime/app-test-server.js +292 -0
  33. package/src/runtime/profiles.js +5 -2
  34. package/src/runtime/skill-sync/cloud-client.ts +2 -1
  35. package/src/runtime/skill-sync/index.ts +24 -6
  36. package/src/runtime/skill-sync/types.ts +2 -0
  37. package/src/runtime/skill-sync-service.js +109 -0
  38. package/src/skill-sync-worker-entry.js +2 -0
  39. package/src/skill-sync-worker.js +50 -0
  40. package/template/app/page.tsx +45 -44
  41. package/template/components/page-heading.tsx +23 -0
  42. package/template/components/ui/badge.tsx +7 -4
  43. package/template/components/ui/card.tsx +24 -11
  44. package/template/components/ui/native-select.tsx +24 -0
  45. package/template/notis.config.ts +0 -1
  46. package/template/package.json +2 -2
  47. package/template/packages/sdk/package.json +1 -2
  48. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +55 -13
  49. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +3 -1
  50. package/template/packages/sdk/src/config.ts +0 -2
  51. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +138 -28
  52. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  53. package/template/packages/sdk/src/hooks/useMultiSelect.ts +2 -8
  54. package/template/packages/sdk/src/index.ts +3 -0
  55. package/template/packages/sdk/src/interactions/actions.ts +14 -1
  56. package/template/packages/sdk/src/interactions/shortcuts.tsx +79 -19
  57. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  58. package/template/packages/sdk/src/interactions.ts +5 -1
  59. package/template/packages/sdk/src/styles.css +28 -1
  60. package/src/runtime/app-dev-build-supervisor.js +0 -47
  61. package/src/runtime/app-dev-build.js +0 -41
  62. package/src/runtime/app-dev-consumers.js +0 -154
  63. package/src/runtime/app-dev-host-lock.js +0 -80
  64. package/src/runtime/app-dev-process-identity.js +0 -111
  65. package/src/runtime/app-dev-roots.js +0 -284
  66. package/src/runtime/app-dev-server.js +0 -1136
  67. package/src/runtime/app-dev-sessions.js +0 -185
  68. package/src/runtime/cli-mode.generated.js +0 -5
  69. package/src/runtime/cli-mode.js +0 -34
@@ -2,8 +2,11 @@
2
2
 
3
3
  import { getDocumentPreview, useDocuments, useNotis, Skeleton, ViewSkeleton } from '@notis/sdk';
4
4
  import { Badge } from '@/components/ui/badge';
5
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { PageHeading } from '@/components/page-heading';
6
6
 
7
+ // Reference page for the flat Notis design bar: a plain page header, bare
8
+ // figures, and a hairline-free list. No bordered boxes, no dividers, no
9
+ // palette colors, no loading text. `notis apps build` enforces these rules.
7
10
  export default function HomePage() {
8
11
  const { app, ready } = useNotis();
9
12
  // Documents come back normalized: plain property values, camelCase fields,
@@ -11,51 +14,49 @@ export default function HomePage() {
11
14
  const { documents, loading, hasData, error, refetch } = useDocuments('items', { pageSize: 25 });
12
15
 
13
16
  return (
14
- <main className="notis-app-shell space-y-6">
15
- <Card>
16
- <CardHeader className="space-y-3">
17
- <Badge variant="secondary" className="w-fit">Installed app</Badge>
18
- <div className="space-y-2">
19
- <CardTitle>{ready ? app?.name : <Skeleton style={{ width: 160 }} />}</CardTitle>
20
- <CardDescription>
21
- {ready ? app?.description : <Skeleton style={{ width: 240 }} />}
22
- </CardDescription>
23
- </div>
24
- </CardHeader>
25
- </Card>
17
+ <main className="notis-app-shell space-y-8">
18
+ <PageHeading
19
+ title={ready ? app?.name ?? 'Items' : <Skeleton style={{ width: 160, height: 28 }} />}
20
+ description={ready ? app?.description ?? undefined : <Skeleton style={{ width: 240 }} />}
21
+ />
22
+
23
+ <section className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4">
24
+ <div className="min-w-0">
25
+ <p className="text-xs font-medium text-muted-foreground">Items</p>
26
+ <p className="mt-1 text-2xl font-semibold tabular-nums">
27
+ {hasData ? documents.length : <Skeleton style={{ width: 48, height: 28 }} />}
28
+ </p>
29
+ </div>
30
+ </section>
26
31
 
27
- <Card>
28
- <CardHeader>
29
- <CardTitle>Items</CardTitle>
30
- <CardDescription>Use shadcn surfaces and portal tokens so the app feels native inside Notis.</CardDescription>
31
- </CardHeader>
32
- <CardContent>
33
- {error && <p role="alert" className="mb-3 text-sm text-destructive">{error.message} <button onClick={refetch}>Retry</button></p>}
34
- {loading ? (
35
- <ViewSkeleton variant="table" rows={4} />
36
- ) : !hasData ? null : documents.length === 0 ? (
37
- <div className="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
38
- No items yet. Create your first item.
39
- </div>
40
- ) : (
41
- <div className="space-y-3">
42
- {documents.map((doc) => (
43
- <div key={doc.id} className="rounded-xl border border-border bg-background px-4 py-3">
44
- <div className="flex items-center justify-between gap-3">
45
- <p className="font-medium">{doc.title || 'Untitled'}</p>
46
- {typeof doc.properties.status === 'string' ? (
47
- <Badge variant="outline">{doc.properties.status}</Badge>
48
- ) : null}
49
- </div>
50
- <p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
51
- {getDocumentPreview(doc)}
52
- </p>
32
+ <section className="space-y-3">
33
+ <h2 className="text-base font-semibold">Recent items</h2>
34
+ {error ? (
35
+ <p role="alert" className="rounded-xl bg-destructive/10 px-4 py-3 text-sm text-destructive">
36
+ {error.message}{' '}
37
+ <button type="button" className="font-medium underline" onClick={refetch}>Retry</button>
38
+ </p>
39
+ ) : null}
40
+ {loading ? (
41
+ <ViewSkeleton variant="table" rows={4} />
42
+ ) : !hasData ? null : documents.length === 0 ? (
43
+ <p className="px-4 py-10 text-center text-sm text-muted-foreground">No items yet. Create your first item.</p>
44
+ ) : (
45
+ <div className="space-y-2 lg:space-y-0">
46
+ {documents.map((doc) => (
47
+ <div key={doc.id} className="list-row flex items-center justify-between gap-3">
48
+ <div className="min-w-0">
49
+ <p className="truncate text-sm font-medium">{doc.title || 'Untitled'}</p>
50
+ <p className="mt-0.5 truncate text-xs text-muted-foreground">{getDocumentPreview(doc)}</p>
53
51
  </div>
54
- ))}
55
- </div>
56
- )}
57
- </CardContent>
58
- </Card>
52
+ {typeof doc.properties.status === 'string' ? (
53
+ <Badge variant="secondary" className="shrink-0">{doc.properties.status}</Badge>
54
+ ) : null}
55
+ </div>
56
+ ))}
57
+ </div>
58
+ )}
59
+ </section>
59
60
  </main>
60
61
  );
61
62
  }
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ // Mirrors the portal's PortalPageHeader: plain title, one-line description,
4
+ // actions on the right. No eyebrow, no rule underneath.
5
+ export function PageHeading({
6
+ title,
7
+ description,
8
+ actions,
9
+ }: {
10
+ title: ReactNode;
11
+ description?: ReactNode;
12
+ actions?: ReactNode;
13
+ }) {
14
+ return (
15
+ <header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
16
+ <div className="min-w-0 flex-1">
17
+ <h1 className="text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
18
+ {description ? <p className="mt-1.5 text-sm text-muted-foreground">{description}</p> : null}
19
+ </div>
20
+ {actions ? <div className="flex shrink-0 flex-wrap items-center gap-2 sm:justify-end">{actions}</div> : null}
21
+ </header>
22
+ );
23
+ }
@@ -4,13 +4,16 @@ import { cva, type VariantProps } from 'class-variance-authority';
4
4
  import { cn } from '@/lib/utils';
5
5
 
6
6
  const badgeVariants = cva(
7
- 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7
+ 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors',
8
8
  {
9
9
  variants: {
10
10
  variant: {
11
- default: 'border-transparent bg-primary/10 text-primary',
12
- secondary: 'border-transparent bg-secondary text-secondary-foreground',
13
- outline: 'text-foreground',
11
+ default: 'bg-primary/10 text-primary',
12
+ secondary: 'bg-foreground/[0.07] text-foreground',
13
+ destructive: 'bg-destructive/10 text-destructive',
14
+ // `outline` is kept for API compatibility and renders as plain text: the
15
+ // design bar has no outlined badges.
16
+ outline: 'bg-transparent text-foreground',
14
17
  },
15
18
  },
16
19
  defaultVariants: {
@@ -2,20 +2,33 @@ import * as React from 'react';
2
2
 
3
3
  import { cn } from '@/lib/utils';
4
4
 
5
+ // Surfaces are flat: a tinted panel on the page, and a plain background panel
6
+ // when nested inside another Card. No borders, no dividers, no shadows.
7
+ const CardDepthContext = React.createContext(0);
8
+
5
9
  const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
- ({ className, ...props }, ref) => (
7
- <div
8
- ref={ref}
9
- className={cn('rounded-2xl border bg-card text-card-foreground shadow-sm', className)}
10
- {...props}
11
- />
12
- ),
10
+ ({ className, ...props }, ref) => {
11
+ const depth = React.useContext(CardDepthContext);
12
+ return (
13
+ <CardDepthContext.Provider value={depth + 1}>
14
+ <div
15
+ ref={ref}
16
+ className={cn(
17
+ 'min-w-0 rounded-2xl text-card-foreground',
18
+ depth === 0 ? 'bg-muted' : 'bg-background',
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ </CardDepthContext.Provider>
24
+ );
25
+ },
13
26
  );
14
27
  Card.displayName = 'Card';
15
28
 
16
29
  const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
30
  ({ className, ...props }, ref) => (
18
- <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
31
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-5', className)} {...props} />
19
32
  ),
20
33
  );
21
34
  CardHeader.displayName = 'CardHeader';
@@ -24,7 +37,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
24
37
  ({ className, ...props }, ref) => (
25
38
  <h3
26
39
  ref={ref}
27
- className={cn('text-xl font-semibold leading-none tracking-tight', className)}
40
+ className={cn('text-base font-semibold leading-none tracking-tight', className)}
28
41
  {...props}
29
42
  />
30
43
  ),
@@ -41,14 +54,14 @@ CardDescription.displayName = 'CardDescription';
41
54
 
42
55
  const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
43
56
  ({ className, ...props }, ref) => (
44
- <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
57
+ <div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
45
58
  ),
46
59
  );
47
60
  CardContent.displayName = 'CardContent';
48
61
 
49
62
  const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
50
63
  ({ className, ...props }, ref) => (
51
- <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
64
+ <div ref={ref} className={cn('flex items-center p-5 pt-0', className)} {...props} />
52
65
  ),
53
66
  );
54
67
  CardFooter.displayName = 'CardFooter';
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import { CaretDown } from '@phosphor-icons/react';
3
+
4
+ import { cn } from '@/lib/utils';
5
+
6
+ // Borderless native select: a tinted pill with a caret. Keeps the platform
7
+ // dropdown (no portal needed inside the app surface) and the portal focus ring.
8
+ const NativeSelect = React.forwardRef<HTMLSelectElement, React.SelectHTMLAttributes<HTMLSelectElement>>(
9
+ ({ className, children, ...props }, ref) => (
10
+ <span className={cn('relative inline-flex', className)}>
11
+ <select
12
+ ref={ref}
13
+ className="h-9 w-full appearance-none rounded-md bg-muted pl-3 pr-8 text-sm text-foreground outline-none transition-colors hover:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
14
+ {...props}
15
+ >
16
+ {children}
17
+ </select>
18
+ <CaretDown className="pointer-events-none absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
19
+ </span>
20
+ ),
21
+ );
22
+ NativeSelect.displayName = 'NativeSelect';
23
+
24
+ export { NativeSelect };
@@ -2,7 +2,6 @@ import { defineNotisApp } from '@notis/sdk/config';
2
2
 
3
3
  export default defineNotisApp({
4
4
  name: 'my-notis-app',
5
- devSlug: 'my-notis-app',
6
5
  title: 'My Notis App',
7
6
  description: 'A focused Notis app with one route and one database-backed workflow.',
8
7
  icon: 'phosphor:squares-four',
@@ -5,8 +5,8 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "scripts": {
8
- "dev": "vite",
9
- "build": "vite build"
8
+ "dev": "vite --configLoader runner",
9
+ "build": "vite build --configLoader runner"
10
10
  },
11
11
  "dependencies": {
12
12
  "@notis/sdk": "file:./packages/sdk",
@@ -12,8 +12,7 @@
12
12
  "./styles.css": "./src/styles.css"
13
13
  },
14
14
  "files": [
15
- "src",
16
- "template"
15
+ "src"
17
16
  ],
18
17
  "scripts": {
19
18
  "type-check": "tsc --noEmit"
@@ -9,11 +9,19 @@ import React, {
9
9
  type ReactElement,
10
10
  } from 'react';
11
11
  import type { ResolvedCollectionAction } from '../interactions/actions';
12
- import { shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
12
+ import { activateShortcutCollection, shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
13
+ import { isInteractionElementVisible } from '../interactions/visibility';
13
14
  import type { ShortcutScope } from '../interactions/shortcuts';
14
15
 
15
16
  export type MultiSelectAction = ResolvedCollectionAction;
16
17
 
18
+ /** The Portal owns global launcher positioning; app code only reports its own bar. */
19
+ export const BULK_ACTION_LAYOUT_EVENT = 'notis:bulk-actions-layout';
20
+ export interface BulkActionLayoutDetail {
21
+ bar: HTMLElement;
22
+ active: boolean;
23
+ }
24
+
17
25
  export interface MultiSelectActionBarProps {
18
26
  selectedCount: number;
19
27
  actions: MultiSelectAction[];
@@ -27,6 +35,9 @@ export interface MultiSelectActionBarProps {
27
35
  shortcutsEnabled?: boolean;
28
36
  /** Override the default collection shortcut scope. */
29
37
  shortcutScope?: ShortcutScope;
38
+ collectionOwnerId?: string;
39
+ isAvailable?: () => boolean;
40
+ onClearSelection?: () => void;
30
41
  }
31
42
 
32
43
  const containerBaseStyle: CSSProperties = {
@@ -107,7 +118,7 @@ const iconSlotStyle: CSSProperties = {
107
118
  display: 'inline-flex',
108
119
  alignItems: 'center',
109
120
  justifyContent: 'center',
110
- opacity: 0.8,
121
+ color: 'inherit',
111
122
  };
112
123
 
113
124
  export function MultiSelectActionBar({
@@ -118,35 +129,55 @@ export function MultiSelectActionBar({
118
129
  style,
119
130
  shortcutsEnabled = true,
120
131
  shortcutScope = 'collection',
132
+ collectionOwnerId,
133
+ isAvailable,
134
+ onClearSelection,
121
135
  }: MultiSelectActionBarProps): ReactElement | null {
122
136
  const barRef = useRef<HTMLDivElement>(null);
123
137
  const visible = selectedCount > 0;
124
138
  useEffect(() => {
125
139
  const bar = barRef.current;
126
140
  if (!visible || !bar) return;
127
- // Also reaches a Portal launcher when an app renders inside a shadow root.
128
- const root = document.documentElement;
129
- const property = '--notis-bulk-actions-height';
130
- const previous = root.style.getPropertyValue(property);
131
- const update = () => root.style.setProperty(property, `${bar.getBoundingClientRect().height + 12}px`);
141
+ // Notify the owning document, including from a shadow root or after unmount.
142
+ // Only the Portal host may measure this bar and change global layout styles.
143
+ const report = (active: boolean) => {
144
+ const owner = bar.ownerDocument;
145
+ const LayoutEvent = owner.defaultView?.CustomEvent;
146
+ if (LayoutEvent) {
147
+ owner.dispatchEvent(new LayoutEvent<BulkActionLayoutDetail>(BULK_ACTION_LAYOUT_EVENT, {
148
+ detail: { bar, active },
149
+ }));
150
+ }
151
+ };
152
+ const update = () => report(isAvailable?.() !== false && isInteractionElementVisible(bar));
132
153
  update();
133
154
  const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
134
155
  observer?.observe(bar);
156
+ const VisibilityObserver = bar.ownerDocument.defaultView?.MutationObserver;
157
+ const visibilityObserver = VisibilityObserver ? new VisibilityObserver(update) : null;
158
+ let element: Element | null = bar;
159
+ while (element) {
160
+ visibilityObserver?.observe(element, { attributes: true, attributeFilter: element === bar.ownerDocument.documentElement ? ['hidden', 'inert', 'aria-hidden', 'class'] : ['hidden', 'inert', 'aria-hidden', 'style', 'class'] });
161
+ const root: Node = element.getRootNode();
162
+ element = element.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
163
+ }
135
164
  return () => {
136
165
  observer?.disconnect();
137
- if (previous) root.style.setProperty(property, previous);
138
- else root.style.removeProperty(property);
166
+ visibilityObserver?.disconnect();
167
+ report(false);
139
168
  };
140
- }, [visible]);
169
+ }, [isAvailable, visible]);
141
170
 
142
171
  const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
143
172
  return actions.flatMap((action): ShortcutDefinition[] => {
144
- if (!action.shortcut || action.disabled || action.pending) return [];
173
+ if (!action.shortcut) return [];
145
174
  return [{
146
175
  id: `collection.action.${action.id}`,
147
176
  keys: action.shortcut,
148
177
  label: action.label,
149
- onTrigger: () => action.onRun(),
178
+ allowRepeat: Boolean(action.disabled || action.pending),
179
+ // Keep advertised keys owned while disabled/pending; never fall through to another action.
180
+ onTrigger: () => { if (!action.disabled && !action.pending) action.onRun(); },
150
181
  }];
151
182
  });
152
183
  }, [actions]);
@@ -154,6 +185,8 @@ export function MultiSelectActionBar({
154
185
  enabled: shortcutsEnabled && selectedCount > 0,
155
186
  scope: shortcutScope,
156
187
  priority: 25,
188
+ collectionOwnerId,
189
+ isAvailable: () => isAvailable?.() !== false && isInteractionElementVisible(barRef.current),
157
190
  });
158
191
 
159
192
  if (selectedCount === 0) return null;
@@ -173,6 +206,14 @@ export function MultiSelectActionBar({
173
206
  <div
174
207
  ref={barRef}
175
208
  data-notis-bulk-actions
209
+ onFocusCapture={() => { if (collectionOwnerId) activateShortcutCollection(collectionOwnerId); }}
210
+ onMouseDownCapture={() => { if (collectionOwnerId) activateShortcutCollection(collectionOwnerId); }}
211
+ onKeyDown={(event) => {
212
+ if (event.key !== 'Escape' || !shortcutsEnabled || !onClearSelection || isAvailable?.() === false) return;
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ onClearSelection();
216
+ }}
176
217
  role="toolbar"
177
218
  aria-label={`Bulk actions for ${selectedCount} selected ${countWord}`}
178
219
  className={className}
@@ -186,10 +227,11 @@ export function MultiSelectActionBar({
186
227
  [data-notis-bulk-divider] { display: none; }
187
228
  [data-notis-bulk-action-list] { width: 100%; }
188
229
  [data-notis-bulk-actions] button { min-height: 48px; font-size: 16px !important; }
230
+ }
231
+ @media (hover: none) and (pointer: coarse) {
189
232
  [data-notis-bulk-actions] kbd { display: none !important; }
190
233
  [data-notis-bulk-action-icon][data-has-shortcut] { display: inline-flex !important; }
191
234
  }
192
- @media (hover: none) { [data-notis-bulk-actions] kbd { display: none !important; } }
193
235
  `}</style>
194
236
  <span data-notis-bulk-count style={countStyle}>{countLabel}</span>
195
237
  {actions.length > 0 ? <span data-notis-bulk-divider aria-hidden style={dividerStyle} /> : null}
@@ -23,7 +23,9 @@ const baseStyle: CSSProperties = {
23
23
  width: '16px',
24
24
  height: '16px',
25
25
  borderRadius: '4px',
26
- border: '1px solid hsl(var(--border))',
26
+ borderWidth: '1px',
27
+ borderStyle: 'solid',
28
+ borderColor: 'hsl(var(--border))',
27
29
  background: 'transparent',
28
30
  color: 'hsl(var(--primary-foreground))',
29
31
  cursor: 'pointer',
@@ -198,8 +198,6 @@ export interface NotisAppToolBinding {
198
198
  export interface NotisAppConfig {
199
199
  /** URL-safe app slug. Existing apps may still use a display name here. */
200
200
  name: string;
201
- /** Stable local-development slug. Defaults to a slug derived from `name`. */
202
- devSlug?: string;
203
201
  /** Human display title, Raycast-style. Falls back to `name`. */
204
202
  title?: string;
205
203
  description?: string;