@notis_ai/cli 0.2.0-beta.167.1 → 0.2.0-beta.175.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/dist/base-skills/notis-apps/references/design.md +14 -1
- package/dist/base-skills/notis-apps/references/sdk.md +1 -0
- package/package.json +1 -1
- package/template/packages/sdk/src/components/Dialog.tsx +46 -0
- package/template/packages/sdk/src/index.ts +2 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +10 -1
- package/template/packages/sdk/src/styles.css +21 -0
|
@@ -15,12 +15,25 @@
|
|
|
15
15
|
- Let Notis own navigation, folder trees, and search. Use `PageHeading`,
|
|
16
16
|
`NativeSelect`, `.list-row`, and `useTopBarSearch` instead of duplicating chrome.
|
|
17
17
|
- Prefer inline optimistic edits for simple changes, with rollback on failure.
|
|
18
|
-
Use
|
|
18
|
+
Use SDK `Dialog` for multi-field edits or destructive confirmation; keep its actions app-owned.
|
|
19
19
|
|
|
20
20
|
Build enforces the existing design rules and reports violations by file/line.
|
|
21
21
|
Use those diagnostics to fix specific problems; passing them does not establish
|
|
22
22
|
that the design is good. See [troubleshooting](troubleshooting.md) when needed.
|
|
23
23
|
|
|
24
|
+
### Controls and side panels
|
|
25
|
+
|
|
26
|
+
- **Keep controls with what they control.** Filters, counts, and Create belong
|
|
27
|
+
above the list; record actions belong inside the detail panel. Only controls
|
|
28
|
+
that affect both panes belong above both.
|
|
29
|
+
- **Align sibling panes.** Start the list and detail panel at the same height,
|
|
30
|
+
with consistent header spacing and control sizes.
|
|
31
|
+
- **Keep secondary actions quiet.** Put occasional destructive actions in an
|
|
32
|
+
overflow menu, retaining their confirmation step.
|
|
33
|
+
|
|
34
|
+
For example, a ticket view has two side-by-side panes: **Tickets / New ticket →
|
|
35
|
+
filters → list**, and **ticket ID / actions / close → details**.
|
|
36
|
+
|
|
24
37
|
### Look at the result
|
|
25
38
|
|
|
26
39
|
Run build and verification, then inspect screenshots of the affected view at a
|
|
@@ -23,6 +23,7 @@ already installs `ShortcutProvider`; app code should not add a second provider.
|
|
|
23
23
|
| `NotisSelectionBoundary` | `(NotisSelectionBoundaryProps) => ReactElement` | Attach structured, explicitly untrusted app/resource/selection context to selected content and copy operations |
|
|
24
24
|
| `SelectionCheckbox` / `SelectionMarquee` | components | Standard selection controls backed by `useCollectionInteractions` |
|
|
25
25
|
| `MultiSelectActionBar` | component | Standard bulk actions with pending/disabled state and shortcut support |
|
|
26
|
+
| `Dialog` | `{ open, onClose, title, description?, role?, children }` | Themed native modal with top-layer stacking, focus handling and background shortcut isolation. Use `role="alertdialog"` for destructive confirmation; put the safe action first. |
|
|
26
27
|
|
|
27
28
|
Import headless collection action types and helpers from
|
|
28
29
|
`@notis/sdk/interactions`. Keep an open detail view synchronized with
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useId, useRef, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface DialogProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
/** Request dismissal. Keep open unchanged to prevent dismissal while saving. */
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
title: ReactNode;
|
|
10
|
+
description?: ReactNode;
|
|
11
|
+
role?: 'dialog' | 'alertdialog';
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Native top-layer dialog; content and actions remain app-owned. */
|
|
16
|
+
export function Dialog({ open, onClose, title, description, role = 'dialog', children }: DialogProps) {
|
|
17
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
18
|
+
const titleId = useId();
|
|
19
|
+
const descriptionId = useId();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!open) return;
|
|
23
|
+
const dialog = ref.current!;
|
|
24
|
+
dialog.showModal();
|
|
25
|
+
return () => dialog.close();
|
|
26
|
+
}, [open]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<dialog
|
|
30
|
+
ref={ref}
|
|
31
|
+
className="notis-dialog"
|
|
32
|
+
role={role}
|
|
33
|
+
aria-labelledby={titleId}
|
|
34
|
+
aria-describedby={description == null ? undefined : descriptionId}
|
|
35
|
+
onCancel={(event) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
onClose();
|
|
38
|
+
}}
|
|
39
|
+
onKeyDown={(event) => event.stopPropagation()}
|
|
40
|
+
>
|
|
41
|
+
<h2 id={titleId} className="notis-dialog-title">{title}</h2>
|
|
42
|
+
{description != null && <p id={descriptionId} className="notis-dialog-description">{description}</p>}
|
|
43
|
+
{children}
|
|
44
|
+
</dialog>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -72,6 +72,8 @@ export {
|
|
|
72
72
|
} from './documents';
|
|
73
73
|
|
|
74
74
|
// Multi-select components
|
|
75
|
+
export { Dialog } from './components/Dialog';
|
|
76
|
+
export type { DialogProps } from './components/Dialog';
|
|
75
77
|
export { MultiSelectActionBar } from './components/MultiSelectActionBar';
|
|
76
78
|
export { MultiSelectCheckbox, MultiSelectCheckbox as SelectionCheckbox } from './components/MultiSelectCheckbox';
|
|
77
79
|
export { MultiSelectDragOverlay, MultiSelectDragOverlay as SelectionMarquee } from './components/MultiSelectDragOverlay';
|
|
@@ -215,6 +215,14 @@ function consumeShortcut(event: KeyboardEvent, definition: ShortcutDefinition):
|
|
|
215
215
|
void definition.onTrigger(event);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Native dialogs make the background inert, but document capture listeners still
|
|
219
|
+
// receive their keyboard events. Leave those events to the dialog and its controls.
|
|
220
|
+
function isDialogEvent(event: KeyboardEvent): boolean {
|
|
221
|
+
return event.composedPath().some((target) =>
|
|
222
|
+
target instanceof HTMLElement && target.matches('dialog.notis-dialog[open]'),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
218
226
|
export function ShortcutProvider({ children }: { children: ReactNode }) {
|
|
219
227
|
const parentRegistry = useContext(ShortcutContext);
|
|
220
228
|
if (parentRegistry) return <>{children}</>;
|
|
@@ -280,7 +288,7 @@ function registerFallbackShortcut(input: Omit<ShortcutRegistration, 'id' | 'orde
|
|
|
280
288
|
registrations,
|
|
281
289
|
nextId: 1,
|
|
282
290
|
dispatch: (event) => {
|
|
283
|
-
if (event.defaultPrevented) return;
|
|
291
|
+
if (event.defaultPrevented || isDialogEvent(event)) return;
|
|
284
292
|
const editable = isEditableShortcutEvent(event);
|
|
285
293
|
const token = chordToken(eventChord(event));
|
|
286
294
|
for (const registration of applicableRegistrations(registrations.values())) {
|
|
@@ -370,6 +378,7 @@ function ShortcutProviderRoot({ children }: { children: ReactNode }) {
|
|
|
370
378
|
|
|
371
379
|
useEffect(() => {
|
|
372
380
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
381
|
+
if (event.defaultPrevented || isDialogEvent(event)) return;
|
|
373
382
|
const editable = isEditableShortcutEvent(event);
|
|
374
383
|
if (!editable && !event.repeat && event.key === '?' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
375
384
|
event.preventDefault();
|
|
@@ -8,6 +8,27 @@
|
|
|
8
8
|
* The portal injects the live Notis theme variables at render time.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/* Top-layer surfaces must remain distinct from the app's flat content panels. */
|
|
12
|
+
dialog.notis-dialog {
|
|
13
|
+
position: fixed;
|
|
14
|
+
inset: 0;
|
|
15
|
+
margin: auto;
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
width: min(28rem, calc(100% - 2rem));
|
|
18
|
+
max-height: calc(100dvh - 2rem);
|
|
19
|
+
overflow: auto;
|
|
20
|
+
padding: 1.25rem;
|
|
21
|
+
border: 1px solid hsl(var(--border));
|
|
22
|
+
border-radius: 0.75rem;
|
|
23
|
+
background: hsl(var(--popover));
|
|
24
|
+
color: hsl(var(--popover-foreground));
|
|
25
|
+
box-shadow: 0 16px 48px rgb(0 0 0 / 0.2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dialog.notis-dialog::backdrop { background: rgb(0 0 0 / 0.45); }
|
|
29
|
+
.notis-dialog-title { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.5; }
|
|
30
|
+
.notis-dialog-description { margin: 0.5rem 0 0; color: hsl(var(--muted-foreground)); font-size: 0.875rem; line-height: 1.5; }
|
|
31
|
+
|
|
11
32
|
/* Framework-neutral styles: the app owns its Tailwind entry point. This file
|
|
12
33
|
* is also consumed in shadow roots and by apps on older Tailwind versions. */
|
|
13
34
|
@layer base {
|