@preset.nz/app-kit 0.1.0 → 0.1.2

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 CHANGED
@@ -51,11 +51,12 @@ tauri::Builder::default()
51
51
 
52
52
  - **`Command`**: id, menu label, optional toolbar label, Tauri accelerator, `MenuName` (App, File, Edit, View, Window, Help), `section` (items in a section sit together), item or toggle. Undo and Redo are built in; Settings is opt-in.
53
53
  - **File menu**: New, Open…, Open Recent ▸, Close, Save, Save As… and Revert to Saved… are built in, run against the app's `Document` behind the close guard. Revert is enabled only for an unsaved document with a file, and asks first, because reloading starts an empty history. Open Recent keeps ten documents, newest first, in `recent-documents.json` in the app's data directory; a file that has gone says so and leaves the list. `note_recent(app, path)` adds a document opened some other way (from the Finder, or by restore), and `recent_documents(app)[0]` is the last document. `.documents_folder(|app| …)` names where the Open and Save panels start for an Untitled document (`~/preset-nz/<App>/…`); the app supplies the lookup, so app-kit doesn't depend on app-folders.
54
+ - **Relaunch**: `.plugin(preset_app_kit::window_state())` on the builder brings each window back at its size and position (`tauri-plugin-window-state`, not visibility). `on_run_event` reopens the last document at launch, the first entry of Open Recent, and opens a document the Finder hands over (double-click, `open some.x`, the Dock), which wins over the restored one. A last document that has gone, or won't load, becomes a note, not a dialog: register `app_kit_document_note` and show what `useDocumentNotes` hands you. For an app with documents, app-kit owns Finder opens, so don't add a second handler.
54
55
  - **Menu config**: `.menu_config(include_str!("../menu.toml"))` switches the built-in items on and off: `[file] new, open, open_recent, close, save, save_as, revert` and `[app] settings`. A missing key takes the default (on); an unknown key is an error, so add a test that calls `MenuConfig::parse` on your file. Labels, accelerators and positions stay the standard's. Design: guidance `projects/app-kit/design/menu-config.md`.
55
56
  - **Apps without documents**: `install_history::<H: History>` instead of `install::<H: Document>`, for a library such as Strata. The config must turn New, Open, Open Recent, Save, Save As and Revert off; with Close off too, Window gains Close Window on Cmd+W. No close guard, no window-title handling, and it doesn't register `tauri-plugin-dialog`, so the app keeps its own.
56
57
  - **`History`**: `undo_label`, `redo_label`, `undo_labels` (oldest first), `redo_labels` (next first), `undo`, `redo`. Implement it on the type you manage as Tauri state. The reads may lock, so call `refresh_history(app)` after a change and never while holding that lock.
57
58
  - **`edit_titles`**: the pure rule behind Edit > Undo: "Undo Set Opacity" when there is a step, plain and disabled when there is none, plain and enabled while a text field has focus.
58
- - **Events**: `command` (id, to the main window), `text-undo` (to the focused window), `app-kit://history` (a `HistoryState`).
59
+ - **Events**: `command` (id, to the main window), `text-undo` (to the focused window), `app-kit://history` (a `HistoryState`), `app-kit://document` (a `DocumentState`), `app-kit://note` (a string).
59
60
  - Commands are plain Tauri commands, not a plugin, so no capability entries are needed.
60
61
 
61
62
  ## TypeScript
@@ -70,6 +71,7 @@ const { commands, run, shortcut } = useCommands(bindings) // bindings: Record<id
70
71
  - **`useCommands(bindings)`**: loads the table from Rust, binds `run`, icon and gating by id, follows the history, routes text-field undo and keeps the menu's enabled and checked state in step. `edit.undo` and `edit.redo` need only an icon. `bindCommands`, `useCommandTable`, `useMenuSync`, `toItems` are the parts.
71
72
  - **`CommandToolbar`**: ux-kit's `ToolbarItems` fed from the commands.
72
73
  - **`@preset.nz/app-kit/core`**: everything except `CommandToolbar` and `toItems`, with no ux-kit import, for apps that use only the menu half. ux-kit is an optional peer.
74
+ - **`useDocument`, `useDocumentNotes(onNote)`**: the live `DocumentState`, and notes about the document (a last document that wouldn't reopen), each delivered once. Show a note as a snackbar.
73
75
  - **`useHistory`, `undo`, `redo`**: the live `HistoryState`, and the same undo the menu takes. The Inspector's History list reads `undoLabels` and `redoLabels`.
74
76
  - **`useTextUndo`, `useTextFocus`, `isTextField`, `blurField`**: Cmd+Z undoes typing while a text field has focus, the document otherwise.
75
77
  - **`createSelection<T>(none)`**: single-select store with `get`, `use`, `select`, `clear`. **`createMultiSelection<T>(key)`**: the opt-in multi-select. **`createStore`, `useStore`**: the small store under both. Wrap them in your own narrow setters; there is no `setSelection`, and selection is not an undo step.
@@ -94,4 +96,4 @@ In Vite, add `@preset.nz/app-kit` (with `react`, `react-dom`, `@tauri-apps/api`,
94
96
 
95
97
  ## Development
96
98
 
97
- `just check` runs `cargo fmt --check`, clippy, `cargo test`, `tsc` and eslint. MIT.
99
+ `just check` runs `cargo fmt --check`, clippy, `cargo test`, `tsc` and Biome. MIT.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preset.nz/app-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The behaviour of a preset.nz desktop app: command table, menu sync, Undo and Redo titles, text-field undo routing and a selection store, over the preset-app-kit Rust crate.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,19 +22,15 @@
22
22
  "react": "^19"
23
23
  },
24
24
  "devDependencies": {
25
- "@eslint/js": "^9.39.4",
25
+ "@biomejs/biome": "^2.5.15",
26
26
  "@preset.nz/ux-kit": "^0.1.0",
27
27
  "@tauri-apps/api": "^2.11.1",
28
28
  "@types/react": "^19.2.17",
29
29
  "@types/react-dom": "^19.2.3",
30
- "eslint": "^9.39.4",
31
- "eslint-plugin-react-hooks": "^7.1.1",
32
- "eslint-plugin-react-refresh": "^0.5.3",
33
- "globals": "^16.5.0",
30
+ "lefthook": "^2.1.17",
34
31
  "react": "^19.2.7",
35
32
  "react-dom": "^19.2.7",
36
- "typescript": "~5.9.3",
37
- "typescript-eslint": "^8.61.1"
33
+ "typescript": "~5.9.3"
38
34
  },
39
35
  "peerDependenciesMeta": {
40
36
  "@preset.nz/ux-kit": {
@@ -1,10 +1,13 @@
1
- import type { ComponentProps } from "react"
2
1
  import { ToolbarItems } from "@preset.nz/ux-kit"
2
+ import type { ComponentProps } from "react"
3
3
 
4
4
  import type { Command } from "./commands"
5
- import { toItems, type ToolbarLayout } from "./toolbar"
5
+ import { type ToolbarLayout, toItems } from "./toolbar"
6
6
 
7
- type Props = Omit<ComponentProps<typeof ToolbarItems>, "groups" | "leading" | "trailing" | "onCommand"> & {
7
+ type Props = Omit<
8
+ ComponentProps<typeof ToolbarItems>,
9
+ "groups" | "leading" | "trailing" | "onCommand"
10
+ > & {
8
11
  commands: Command[]
9
12
  layout: ToolbarLayout
10
13
  /** From `useCommands`: runs the command the menu would. */
@@ -1,7 +1,7 @@
1
- import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"
2
1
  import { invoke } from "@tauri-apps/api/core"
2
+ import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"
3
3
 
4
- import { redo, undo, useHistory, type HistoryState } from "./history"
4
+ import { type HistoryState, redo, undo, useHistory } from "./history"
5
5
  import { useTextUndo } from "./text"
6
6
  import { useMenuSync } from "./useMenuSync"
7
7
 
@@ -133,6 +133,9 @@ export function useCommands(bindings: Record<string, Binding>) {
133
133
  [commands],
134
134
  )
135
135
  /** A command's display shortcut, for anything that shows one (a panel header's tooltip). */
136
- const shortcut = useCallback((id: string) => table?.find((s) => s.id === id)?.shortcut ?? undefined, [table])
136
+ const shortcut = useCallback(
137
+ (id: string) => table?.find((s) => s.id === id)?.shortcut ?? undefined,
138
+ [table],
139
+ )
137
140
  return { commands, run, shortcut, history }
138
141
  }
package/src-ts/core.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  // The menu half of app-kit: commands, history, text-field undo, selection and stores, with
2
2
  // no dependency on ux-kit. `index.ts` adds the toolbar bridge on top.
3
3
  export {
4
- bindCommands,
5
- useCommands,
6
- useCommandTable,
7
4
  type Binding,
5
+ bindCommands,
8
6
  type Command,
9
7
  type CommandSpec,
8
+ useCommands,
9
+ useCommandTable,
10
10
  } from "./commands"
11
- export { useDocument, type DocumentState } from "./document"
12
- export { redo, undo, useHistory, type HistoryState } from "./history"
11
+ export { type DocumentState, useDocument, useDocumentNotes } from "./document"
12
+ export { type HistoryState, redo, undo, useHistory } from "./history"
13
13
  export { nativeContextMenu } from "./nativeContextMenu"
14
14
  export {
15
15
  createMultiSelection,
@@ -18,6 +18,6 @@ export {
18
18
  type MultiSelection,
19
19
  type Selection,
20
20
  } from "./selection"
21
- export { createStore, useStore, type Store } from "./store"
21
+ export { createStore, type Store, useStore } from "./store"
22
22
  export { blurField, isTextField, useTextFocus, useTextUndo } from "./text"
23
23
  export { useMenuSync } from "./useMenuSync"
@@ -1,6 +1,6 @@
1
- import { useEffect, useState } from "react"
2
1
  import { invoke } from "@tauri-apps/api/core"
3
2
  import { listen } from "@tauri-apps/api/event"
3
+ import { useEffect, useRef, useState } from "react"
4
4
 
5
5
  /** The document as Rust reports it (`DocumentState`): from `app_kit_document`, pushed as `app-kit://document`. */
6
6
  export interface DocumentState {
@@ -29,3 +29,32 @@ export function useDocument(): DocumentState | null {
29
29
  }, [])
30
30
  return state
31
31
  }
32
+
33
+ /**
34
+ * Notes about the document for the app to show, such as a last document that wouldn't reopen
35
+ * at launch. Each arrives once: the ones held from before the webview listened, then any later
36
+ * `app-kit://note`. Show them as the app shows a passing message (a snackbar), never a dialog.
37
+ */
38
+ export function useDocumentNotes(onNote: (message: string) => void): void {
39
+ const handler = useRef(onNote)
40
+ useEffect(() => {
41
+ handler.current = onNote
42
+ }, [onNote])
43
+ useEffect(() => {
44
+ let live = true
45
+ const un = listen<string>("app-kit://note", (e) => handler.current(e.payload))
46
+ // Ask only once listening, so nothing falls between the held notes and the event. And only
47
+ // while still mounted: taking them is once-only, so an unmounted effect (StrictMode mounts
48
+ // twice in dev) must leave them for the next.
49
+ un.then(() => (live ? invoke<string[]>("app_kit_document_note") : [])).then(
50
+ (held) => {
51
+ if (live) for (const m of held) handler.current(m)
52
+ },
53
+ () => {},
54
+ )
55
+ return () => {
56
+ live = false
57
+ un.then((f) => f())
58
+ }
59
+ }, [])
60
+ }
package/src-ts/history.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { useEffect, useState } from "react"
2
1
  import { invoke } from "@tauri-apps/api/core"
3
2
  import { listen } from "@tauri-apps/api/event"
3
+ import { useEffect, useState } from "react"
4
4
 
5
5
  import { blurField } from "./text"
6
6
 
package/src-ts/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Everything: the core plus the ux-kit toolbar bridge. Apps without ux-kit import
2
2
  // "@preset.nz/app-kit/core" instead.
3
- export * from "./core"
3
+
4
4
  export { CommandToolbar } from "./CommandToolbar"
5
- export { toItems, type ToolbarLayout } from "./toolbar"
5
+ export * from "./core"
6
+ export { type ToolbarLayout, toItems } from "./toolbar"
package/src-ts/store.ts CHANGED
@@ -15,7 +15,7 @@ export function createStore<T>(initial: T): Store<T> {
15
15
  set(next) {
16
16
  if (Object.is(next, state)) return
17
17
  state = next
18
- listeners.forEach((l) => l())
18
+ for (const l of listeners) l()
19
19
  },
20
20
  subscribe(l) {
21
21
  listeners.add(l)
package/src-ts/text.ts CHANGED
@@ -1,13 +1,23 @@
1
- import { useEffect, useState } from "react"
2
1
  import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
2
+ import { useEffect, useState } from "react"
3
3
 
4
- const TEXT_INPUT_TYPES = new Set(["", "text", "search", "url", "email", "tel", "password", "number"])
4
+ const TEXT_INPUT_TYPES = new Set([
5
+ "",
6
+ "text",
7
+ "search",
8
+ "url",
9
+ "email",
10
+ "tel",
11
+ "password",
12
+ "number",
13
+ ])
5
14
 
6
15
  /** True for elements that have their own typing undo: text inputs, textareas, contenteditable. */
7
16
  export function isTextField(el: EventTarget | Element | null): boolean {
8
17
  if (!(el instanceof HTMLElement)) return false
9
18
  if (el instanceof HTMLTextAreaElement) return !el.readOnly && !el.disabled
10
- if (el instanceof HTMLInputElement) return TEXT_INPUT_TYPES.has(el.type) && !el.readOnly && !el.disabled
19
+ if (el instanceof HTMLInputElement)
20
+ return TEXT_INPUT_TYPES.has(el.type) && !el.readOnly && !el.disabled
11
21
  return el.isContentEditable
12
22
  }
13
23
 
@@ -1,6 +1,6 @@
1
- import { useEffect, useRef } from "react"
2
1
  import { invoke } from "@tauri-apps/api/core"
3
2
  import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
3
+ import { useEffect, useRef } from "react"
4
4
 
5
5
  import type { Command } from "./commands"
6
6
 
@@ -31,12 +31,11 @@ export function useMenuSync(commands: Command[], textFocus: boolean) {
31
31
  }, [])
32
32
 
33
33
  const key = JSON.stringify(commands.map((c) => [c.id, c.enabled, c.pressed])) + textFocus
34
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `key` stands for the commands' menu-relevant state (and textFocus); `latest` holds the commands themselves.
34
35
  useEffect(() => {
35
36
  const states = latest.current
36
37
  .filter((c) => !RUST_GATED_IDS.has(c.id))
37
38
  .map((c) => ({ id: c.id, enabled: c.enabled, checked: c.pressed }))
38
39
  invoke("app_kit_menu_state", { states, textFocus }).catch(() => {})
39
- // `key` stands for the commands' menu-relevant state; `latest` holds the commands.
40
- // eslint-disable-next-line react-hooks/exhaustive-deps
41
40
  }, [key])
42
41
  }