@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.
@@ -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 a dialog for multi-field edits or destructive confirmation.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notis_ai/cli",
3
- "version": "0.2.0-beta.167.1",
3
+ "version": "0.2.0-beta.175.1",
4
4
  "description": "Agent-first Notis CLI for apps and generic tool execution",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 {