@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.
- package/README.md +11 -45
- package/config/notis_app_design_rules.json +135 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +8893 -10612
- package/dist/base-skills/notis-apps/SKILL.md +25 -573
- package/dist/base-skills/notis-apps/references/architecture.md +147 -0
- package/dist/base-skills/notis-apps/references/design.md +154 -0
- package/dist/base-skills/notis-apps/references/release.md +93 -0
- package/dist/base-skills/notis-apps/references/sdk.md +60 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +26 -0
- package/dist/base-skills/notis-cli/SKILL.md +19 -267
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +13 -651
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/dist/skill-sync/index.js +24 -7
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +2989 -0
- package/package.json +1 -2
- package/skills/notis-apps/cli.md +34 -95
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +1 -1
- package/src/cli.js +4 -0
- package/src/command-specs/apps.js +322 -1560
- package/src/command-specs/diagnostics.js +37 -0
- package/src/command-specs/skills.js +23 -5
- package/src/runtime/agent-browser.js +169 -1
- package/src/runtime/app-boundary-validator.js +221 -0
- package/src/runtime/app-platform.js +359 -233
- package/src/runtime/app-test-server.js +292 -0
- package/src/runtime/profiles.js +5 -2
- package/src/runtime/skill-sync/cloud-client.ts +2 -1
- package/src/runtime/skill-sync/index.ts +24 -6
- package/src/runtime/skill-sync/types.ts +2 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/template/app/page.tsx +45 -44
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +0 -1
- package/template/package.json +2 -2
- package/template/packages/sdk/package.json +1 -2
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +55 -13
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +3 -1
- package/template/packages/sdk/src/config.ts +0 -2
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +138 -28
- package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +2 -8
- package/template/packages/sdk/src/index.ts +3 -0
- package/template/packages/sdk/src/interactions/actions.ts +14 -1
- package/template/packages/sdk/src/interactions/shortcuts.tsx +79 -19
- package/template/packages/sdk/src/interactions/visibility.ts +13 -0
- package/template/packages/sdk/src/interactions.ts +5 -1
- package/template/packages/sdk/src/styles.css +28 -1
- package/src/runtime/app-dev-build-supervisor.js +0 -47
- package/src/runtime/app-dev-build.js +0 -41
- package/src/runtime/app-dev-consumers.js +0 -154
- package/src/runtime/app-dev-host-lock.js +0 -80
- package/src/runtime/app-dev-process-identity.js +0 -111
- package/src/runtime/app-dev-roots.js +0 -284
- package/src/runtime/app-dev-server.js +0 -1136
- package/src/runtime/app-dev-sessions.js +0 -185
- package/src/runtime/cli-mode.generated.js +0 -5
- package/src/runtime/cli-mode.js +0 -34
package/template/app/page.tsx
CHANGED
|
@@ -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 {
|
|
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-
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
<div
|
|
44
|
-
<
|
|
45
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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: '
|
|
12
|
-
secondary: '
|
|
13
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 };
|
package/template/notis.config.ts
CHANGED
|
@@ -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',
|
package/template/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|