@3sln/nook 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +615 -2
- package/package.json +52 -4
- package/src/bl/commands.js +27 -0
- package/src/bl/menu.js +311 -0
- package/src/capture.js +129 -0
- package/src/index.js +2136 -0
- package/src/providers/attention.js +99 -0
- package/src/providers/onboarding.js +35 -0
- package/src/ui/bounds.js +159 -0
- package/src/ui/commandPalette.js +236 -0
- package/src/ui/enumPicker.js +307 -0
- package/src/ui/feedback.js +565 -0
- package/src/ui/icons.js +46 -0
- package/src/ui/localization.js +95 -0
- package/src/ui/motion.js +49 -0
- package/src/ui/styles.js +1876 -0
- package/src/ui/swipeDial.js +169 -0
package/README.md
CHANGED
|
@@ -1,3 +1,616 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nook
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A quiet, recognizable corner for contextual actions, help, settings, and feedback. Native custom elements, persistent DOM, and an ngin-owned navigation projection. The Nook mark is fixed across integrations; colors, geometry, typography, controls, and surfaces are customizable.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @3sln/nook
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Import `@3sln/nook` once in your browser entry point, then add one `<nook-dock>` to your application. The package ships native ES modules and registers its custom elements on import. It requires a browser DOM; import it on the client in server-rendered apps. `@3sln/ngin` is installed as a dependency and remains external, so your application's bundler or import map can share one copy.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<nook-dock corner="bottom-right">
|
|
15
|
+
<nook-action id="share" label="Share page"></nook-action>
|
|
16
|
+
</nook-dock>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import "@3sln/nook";
|
|
21
|
+
|
|
22
|
+
const dock = document.querySelector("nook-dock");
|
|
23
|
+
dock.addEventListener("nook-action", ({ detail }) => {
|
|
24
|
+
// Handle the selected action in your app; see the event reference below.
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Feedback is disabled until its submit hook is wired. Commands and Settings appear only when their definitions are present; More appears only when entries overflow. Configuration belongs to each dock, through attributes, properties, and inherited CSS custom properties; there is no global configuration registry.
|
|
29
|
+
|
|
30
|
+
## Try the demo
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
git clone https://github.com/3sln/nook.git
|
|
34
|
+
cd nook
|
|
35
|
+
bun install --frozen-lockfile
|
|
36
|
+
bun run dev
|
|
37
|
+
# http://localhost:4178
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use Bun 1.3.14 and Node.js 24 for development. The demo serves the installed `@3sln/ngin` dependency; no sibling checkout or build step is needed. The demo's feedback handler stores reports only in memory and does not send them to a server.
|
|
41
|
+
|
|
42
|
+
## Embed
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import "@3sln/nook";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<nook-dock corner="bottom-right">
|
|
50
|
+
<nook-action id="share" label="Share page" keybind="Alt+S">
|
|
51
|
+
<template slot="icon">
|
|
52
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
53
|
+
<use href="#nook-icon-link"></use>
|
|
54
|
+
</svg>
|
|
55
|
+
</template>
|
|
56
|
+
</nook-action>
|
|
57
|
+
|
|
58
|
+
<nook-menu label="Resources">
|
|
59
|
+
<nook-panel label="Page guide">
|
|
60
|
+
<template slot="content"
|
|
61
|
+
><p>Your context-specific documentation.</p></template
|
|
62
|
+
>
|
|
63
|
+
</nook-panel>
|
|
64
|
+
</nook-menu>
|
|
65
|
+
|
|
66
|
+
<nook-setting id="dark" label="Dark mode" type="toggle" value="false">
|
|
67
|
+
<template slot="description">A softer space after hours</template>
|
|
68
|
+
</nook-setting>
|
|
69
|
+
<nook-setting
|
|
70
|
+
label="Volume"
|
|
71
|
+
type="range"
|
|
72
|
+
min="0"
|
|
73
|
+
max="100"
|
|
74
|
+
step="1"
|
|
75
|
+
value="70"
|
|
76
|
+
></nook-setting>
|
|
77
|
+
<nook-setting label="Color" type="select" value="iris">
|
|
78
|
+
<option value="iris">Iris</option>
|
|
79
|
+
<option value="sage">Sage</option>
|
|
80
|
+
</nook-setting>
|
|
81
|
+
</nook-dock>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Each dock owns only its descendants; elements from nested docks are excluded. Use one dock per app for predictable document-level shortcuts. Declarative `nook-action`, `nook-menu`, `nook-panel`, and `nook-setting` elements are hidden by their own shadow styles. Optionally include `nook-action,nook-menu,nook-panel,nook-setting,nook-command,nook-arg { display:none }` in the host stylesheet to hide definitions before JavaScript loads.
|
|
85
|
+
|
|
86
|
+
- `nook-dock`: the corner handle and composition root.
|
|
87
|
+
- `nook-action`: emits intent for the embedding app to handle.
|
|
88
|
+
- `nook-menu`: a nested group of actions, panels, or menus.
|
|
89
|
+
- `nook-panel`: an overlay surface for arbitrary application UI.
|
|
90
|
+
- `nook-command`: a searchable command with optional declarative arguments.
|
|
91
|
+
- `nook-arg`: a command argument definition.
|
|
92
|
+
- `nook-setting`: an inline control. `type="toggle"`, `range`, `enum` (`select` is a compatibility alias), or native input types such as `color`, `number`, and `text`. `min`, `max`, `step`, and `value` reflect to the control. Toggle values use the strings `true` and `false`; the JS `value` property returns a boolean for toggles and a string for other controls.
|
|
93
|
+
|
|
94
|
+
Top-level actions, panels, and menus fill the root automatically. When they exceed the available row width, Nook reserves the last slot for **More** and moves lower-priority entries into an overflow menu. The default root limit is five items, including More; `--nook-root-max-items` can change that limit. Viewport width and the configured item size/gap can reduce it further. When everything fits, More disappears.
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<nook-action label="Save" priority="100" group="Workspace"></nook-action>
|
|
98
|
+
<nook-action label="Share" priority="50" group="Sharing"></nook-action>
|
|
99
|
+
<nook-menu label="Export" priority="10" group="Workspace">
|
|
100
|
+
<nook-action label="PDF"></nook-action>
|
|
101
|
+
<nook-action label="Markdown"></nook-action>
|
|
102
|
+
</nook-menu>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Higher numeric `priority` values win root slots. Equal priorities keep DOM order. `item.priority` and `item.group` reflect their attributes and update live. Groups organize overflow into labeled sections; they do not create extra navigation levels. Groups appear in order of their highest-priority member, and entries retain priority order within each group. Ungrouped entries appear under Other when named groups are present. There are no duplicate root entries in More.
|
|
106
|
+
|
|
107
|
+
For compatibility, `root`/`.root = true` makes a nested definition eligible for the root and supplies priority 100 when no explicit priority is set. It is a preference, not a guarantee against overflow. Top-level definitions need no `root` attribute. Root settings remain inline controls; other settings populate the automatic Settings launcher. Individual commands remain in the Commands palette.
|
|
108
|
+
|
|
109
|
+
Automatic Feedback, Settings, and Commands launchers participate in allocation too. Their default priorities are 30, 20, and 10. Override them per dock:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
dock.priorities = { feedback: 100, settings: 50, commands: 25 };
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Reassign the object to apply changes. Their shortcuts remain available when their launcher moves into More. Priority reflects app-provided configuration; Nook does not yet provide an end-user reorder interface or persist personal ordering. Embedders can store preferences and apply them through these properties.
|
|
116
|
+
|
|
117
|
+
`label` and `description` accept attributes or slotted templates. `slot="icon"` accepts a template or an element. Labels are accessible names; desktop tooltips include supported keybindings. Overflow entries show their labels inline. Set `--nook-label-display: block` to show text beneath root action icons. Set `hidden` or `disabled` to withdraw a definition and its descendants.
|
|
118
|
+
|
|
119
|
+
## Live DOM and persistence
|
|
120
|
+
|
|
121
|
+
Add, remove, reorder, hide, or change labels, icons, descriptions, bindings, values, and select options on the source elements. A batched MutationObserver reconciles them in the next observer turn, whether the menu is open or closed. Existing item buttons, panel inputs, and setting controls retain identity. Removing an active panel/menu closes navigation safely. Removed items release their rendered views and overflow button projections.
|
|
122
|
+
|
|
123
|
+
All surfaces are constructed before opening; navigation changes visibility, focus, and inertness. Opening and closing does not rebuild their contents. Structural changes reconcile only the affected definitions. Resizing and changes to priority/group rebalance root and overflow. Panels and setting controls retain their mounted state. An emptied open overflow returns to the root while an open child panel keeps its state.
|
|
124
|
+
|
|
125
|
+
Panel `slot="content"` templates and custom control templates are instantiated once when their source item is registered. Subsequent edits to an inert template's content do not patch the mounted UI: update the mounted live UI, or remove/reinsert the definition to instantiate it again. This is intentional: silently replacing a panel on an unrelated menu mutation would discard an unfinished form. Event listeners attached to an inert template's nodes are not cloned; listen for bubbling events or use custom elements inside templates.
|
|
126
|
+
|
|
127
|
+
For already-live panel content, omit the template. Unslotted nodes move once into the mounted surface, preserving listeners and element state. Application styles for moved/cloned content must live inside its custom elements or an included `<style>` in the panel content. Store app data outside Nook; a remove/reinsert or route teardown creates a fresh panel.
|
|
128
|
+
|
|
129
|
+
## Settings and rich enums
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<nook-setting
|
|
133
|
+
id="dark"
|
|
134
|
+
type="toggle"
|
|
135
|
+
label="Dark mode"
|
|
136
|
+
description="A softer space after hours"
|
|
137
|
+
value="false"
|
|
138
|
+
></nook-setting>
|
|
139
|
+
<nook-setting
|
|
140
|
+
id="scale"
|
|
141
|
+
type="range"
|
|
142
|
+
label="Text size"
|
|
143
|
+
min="12"
|
|
144
|
+
max="20"
|
|
145
|
+
step="1"
|
|
146
|
+
value="14"
|
|
147
|
+
></nook-setting>
|
|
148
|
+
<nook-setting id="accent" type="enum" label="Accent" value="iris">
|
|
149
|
+
<nook-option value="iris" label="Iris" description="Soft violet">
|
|
150
|
+
<template slot="icon">
|
|
151
|
+
<span
|
|
152
|
+
style="display:block;width:20px;height:20px;border-radius:50%;background:#7560e8"
|
|
153
|
+
></span>
|
|
154
|
+
</template>
|
|
155
|
+
</nook-option>
|
|
156
|
+
<nook-option
|
|
157
|
+
value="sage"
|
|
158
|
+
label="Sage"
|
|
159
|
+
description="Quiet green"
|
|
160
|
+
></nook-option>
|
|
161
|
+
</nook-setting>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Enums use a custom trigger and a listbox in Nook's own view system. Arrow spacing and padding are controlled by CSS; no native select arrow is used. The picker opens as a contained desktop view or fullscreen mobile view and inherits expanded ancestors. Selecting returns to the previous view and focuses the trigger. Arrows, Home/End, single-letter typeahead, Enter, and Space work; Escape closes Nook, and Backspace goes back. Disabled choices cannot be selected.
|
|
165
|
+
|
|
166
|
+
`nook-option` supports `value`, `label`, `description`, `disabled`, and `hidden`, plus templates for `icon`, `label`, `description`, or a full `content` layout. Rich option content is presentational (icons, swatches, text, and badges), without nested interactive controls. Use unique values. Plain `<option>` definitions and `type="select"` remain compatible and use the same custom picker. A missing initial value selects the first enabled option; an explicitly requested value that no longer exists is retained as invalid and displays “Choose…” until corrected. Initial defaults do not emit a change event.
|
|
167
|
+
|
|
168
|
+
The same picker works for `nook-arg type="enum"`/`select`, including required command arguments and native form validation through a hidden value proxy. Changes to options, labels, descriptions, disabled states, and the selected `value` reconcile live. Existing option buttons and values survive unrelated changes. Changing the setting/control type explicitly recreates that control.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const setting = document.querySelector("#accent");
|
|
172
|
+
setting.value = "sage";
|
|
173
|
+
dock.addEventListener("nook-change", ({ detail: { source, value } }) => {
|
|
174
|
+
// Translate this input into your application's setting Action.
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Native `color`, `number`, and `text` inputs and custom `<template slot="control">` remain available. Add `root` to put a setting directly in the root, or leave it off to include it in quick settings.
|
|
179
|
+
|
|
180
|
+
Set `settings-href` on the dock to show an **All settings** link below the quick settings controls:
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<nook-dock settings-href="/settings">
|
|
184
|
+
<nook-setting label="Dark mode" type="toggle" value="false"></nook-setting>
|
|
185
|
+
</nook-dock>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The equivalent property is `dock.settingsHref = "/settings"`. Set it to `null` or an empty string to remove the link. Changes project live without replacing controls or their drafts. A link alone makes Settings available, even without inline settings. It is a normal same-tab anchor with standard browser navigation and modified-click behavior. Its label uses Bab's `All settings` message. Style it with `::part(settings-footer)` and `::part(settings-link)`, `--nook-settings-footer-gap`, and `--nook-settings-link-color`.
|
|
189
|
+
|
|
190
|
+
Picker parts: `enum-control`, `enum-trigger`, `enum-current`, `enum-chevron`, `enum-value`, `enum-options`, `enum-option`, `enum-option-content`, `enum-option-icon`, `enum-option-text`, `enum-option-label`, `enum-option-description`, `enum-selected-label`, `enum-check`, `enum-error`, and `enum-empty`. See the [ARIA listbox interaction pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
|
|
191
|
+
|
|
192
|
+
The command palette keeps its search field at the anchored edge: top for top corners and bottom for bottom corners. Results scroll independently, including in fullscreen mobile views. Style the results viewport with `::part(command-results)`.
|
|
193
|
+
|
|
194
|
+
## Optional feedback UI
|
|
195
|
+
|
|
196
|
+
The built-in feedback launcher and shortcut are disabled until `dock.feedback.submit` is a function. Wire capabilities explicitly:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { createTabCaptureSession } from "@3sln/nook/capture";
|
|
200
|
+
const captureSession = createTabCaptureSession();
|
|
201
|
+
|
|
202
|
+
dock.feedback = {
|
|
203
|
+
submit: async (report, { signal }) => {
|
|
204
|
+
// Deliver report through your app. Resolve on success; throw on failure.
|
|
205
|
+
// report = { type, message, attachments: [{ kind, file }], context }
|
|
206
|
+
// file is a File/Blob or the descriptor returned by an attachment hook.
|
|
207
|
+
},
|
|
208
|
+
attachFiles: async (files, { signal }) => files,
|
|
209
|
+
captureScreenshot: navigator.mediaDevices?.getDisplayMedia
|
|
210
|
+
? (options) => captureSession.capture(options)
|
|
211
|
+
: undefined,
|
|
212
|
+
// root: document.querySelector('#app'),
|
|
213
|
+
// accept: 'image/*,.txt,.log',
|
|
214
|
+
// maxFiles: 5,
|
|
215
|
+
// maxFileBytes: 10 * 1024 * 1024,
|
|
216
|
+
// types: [{ value: 'bug', label: 'Bug report' }, ...],
|
|
217
|
+
// successMessage: 'Your report was received.',
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Submit is required; attachment and capture controls appear only when their respective hooks are functions. Reassign `dock.feedback` to update capabilities live. `dock.feedback = false` or `feedback-disabled` disables feedback entirely, including custom feedback definitions. `dock.feedback = null` unwires the built-in UI while allowing a supplied custom panel. A custom `<nook-panel feedback root label="Feedback">` takes precedence over the built-in form. Its app-owned UI and `nook-submit` handling work as before.
|
|
222
|
+
|
|
223
|
+
The built-in form includes Bug report, UI / usability, Feature request, and Other categories, a required message, removable attachment previews, loading states, an error message that retains the draft, and a success state. Duplicate submissions are prevented. Disabling feedback or disconnecting Nook aborts outstanding operations, clears attachments, revokes preview URLs, and restores the app if capture was in progress. The demo supplies explicit local hooks and says that nothing is sent to a server.
|
|
224
|
+
|
|
225
|
+
`attachFiles(files, { signal })` may prepare/upload files and return an array of files or app-owned descriptors; returning nothing stages the original files. Submission receives these as `{ kind: 'file' | 'screenshot', file }`. The defaults allow five attachments of up to 10 MiB each. Native file `accept` is a picker hint; your hooks/backend own delivery, validation, authorization, and any upload cleanup.
|
|
226
|
+
|
|
227
|
+
`captureScreenshot({ root, exclude, signal })` is an optional app capability. “Capture app view” enters a session with a compact Capture / snapshot-count / Done toolbar, leaving the app navigable. Snapshot previews expand only when the count is selected; routine capture status is announced without occupying screen space. Users can take multiple snapshots, see their count, preview them, and remove individual snapshots before returning to their feedback draft. Done aborts the session signal. Nook hides only its own UI during each capture; wrapped app content stays visible. The hook receives the app root and Nook UI nodes to exclude. DOM renderers should honor both. Returned File/Blob values or app descriptors are staged for final submission, never automatically sent.
|
|
228
|
+
|
|
229
|
+
`createTabCaptureSession()` reuses one browser sharing stream across snapshots and stops its tracks when Done aborts the signal; call `stop()` for explicit cleanup. The compatibility `captureTabScreenshot(options)` helper takes one shot and immediately stops sharing. Browser permission and a user gesture are required to begin sharing. The browser owns the surface chooser, so the selected surface cannot be guaranteed. These helpers are not silent DOM screenshot services. Apps may supply a DOM-to-image hook instead, subject to that renderer's font, image, shadow-DOM, and cross-origin limitations.
|
|
230
|
+
|
|
231
|
+
Capture-session styling parts include `capture-session`, `capture-snapshot`, `capture-done`, `capture-review`, `capture-review-icon`, `capture-count`, `capture-status`, `capture-gallery`, `capture-preview-image`, `capture-thumbnails`, `capture-thumbnail`, `capture-preview`, and `capture-remove`.
|
|
232
|
+
|
|
233
|
+
Feedback parts include `feedback-form`, `feedback-intro`, `feedback-types`, `feedback-type`, `feedback-label`, `feedback-message`, `feedback-tools`, `feedback-file-input`, `feedback-attach`, `feedback-capture`, `feedback-attachments`, `feedback-attachment`, `feedback-preview`, `feedback-attachment-name`, `feedback-remove`, `feedback-privacy`, `feedback-status`, `feedback-send`, `feedback-success`, `feedback-another`, and `feedback-done`.
|
|
234
|
+
|
|
235
|
+
## First use
|
|
236
|
+
|
|
237
|
+
Before its first opening, Nook gives a brief attention burst: about three soft pulses over six seconds. It then stops. If unopened, it retries after a random 45–75 minutes. The cooldown is stored under `@3sln/nook:attention:v1`, so reloads do not restart the burst. Hidden tabs defer attention until visible; opening Nook stops reminders. The scheduler releases its timer/listeners when the last dock disconnects. The first open shows a short device-specific introduction, then “Got it” continues to the requested menu or panel. Escape may dismiss it immediately. Reduced-motion preferences disable the pulse. The keyboard-specific number tip appears only when layout data is available.
|
|
238
|
+
|
|
239
|
+
Showing the introduction records `seen` under `@3sln/nook:tutorial:v1` in localStorage. This is shared across Nook instances on the same origin. Future visits open directly into the requested view. If storage is denied or throws, a provider-owned, document-scoped in-memory fallback prevents repeated introductions during the current page session. No network requests or account identification are involved.
|
|
240
|
+
|
|
241
|
+
To replay the first-use experience and its initial attention burst while developing, remove both `@3sln/nook:tutorial:v1` and `@3sln/nook:attention:v1` from localStorage and reload. `dock.open("welcome")` can replay just the tutorial without resetting stored first-use state. The introduction is marked as seen when shown, rather than requiring completion.
|
|
242
|
+
|
|
243
|
+
### Customizing the tutorial
|
|
244
|
+
|
|
245
|
+
Customize each dock's headings and button labels through its `tutorial` property:
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
dock.tutorial = {
|
|
249
|
+
title: "Meet your workspace shortcuts",
|
|
250
|
+
continueLabel: "Let's go",
|
|
251
|
+
};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Reassign this property to update labels live; `null` restores the defaults. Titles also update the dialogs' accessible names. Tutorial customization leaves first-use tracking and device detection intact.
|
|
255
|
+
|
|
256
|
+
Supply direct-child templates for custom content. Desktop and touch wrappers remain mounted, and Nook switches them automatically as input capabilities change:
|
|
257
|
+
|
|
258
|
+
```html
|
|
259
|
+
<nook-dock>
|
|
260
|
+
<template slot="tutorial-desktop">
|
|
261
|
+
<p>Your workspace extras live here.</p>
|
|
262
|
+
<p>Use arrows to browse and Enter to choose. Escape closes Nook.</p>
|
|
263
|
+
</template>
|
|
264
|
+
<template slot="tutorial-touch">
|
|
265
|
+
<p>Tap the corner for workspace extras.</p>
|
|
266
|
+
<p>Swipe sideways for all actions or vertically for settings.</p>
|
|
267
|
+
</template>
|
|
268
|
+
<template slot="tutorial-drag">Hold, then drag to choose a corner.</template>
|
|
269
|
+
</nook-dock>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Slot `tutorial` replaces the entire welcome body and takes precedence over individual device regions. The Continue button remains Nook-owned. With a full replacement, the embedder owns any device-specific content inside it. The drag-region template remains hidden when Nook cannot move.
|
|
273
|
+
|
|
274
|
+
The library does not include a persistent “How Nook works” entry, help surface, or corner-picker menu. The demo's guide is an ordinary app-authored `nook-panel`; embedders can supply their own help in the same way. Position can be changed through dragging or the dock's `position` property.
|
|
275
|
+
|
|
276
|
+
Templates are cloned once on insertion/replacement. Unrelated mutations and navigation preserve mounted custom content and form state. Replace a template element to apply new content; removing it restores the built-in region. Listeners on inert template nodes are not cloned; use custom elements or bubbling events. Custom content renders in the dock's shadow root, so include a `<style>` in the template or use the exposed parts.
|
|
277
|
+
|
|
278
|
+
Style `::part(tutorial)`, `::part(tutorial-desktop)`, `::part(tutorial-touch)`, `::part(tutorial-drag)`, and `::part(tutorial-continue)`. The welcome surface also exposes the shared `heading`, `header`, `body`, and `surface` parts. They inherit Nook's color, typography, spacing, and animation tokens.
|
|
279
|
+
|
|
280
|
+
## SVG sprite
|
|
281
|
+
|
|
282
|
+
Nook includes its own consistent line icons in a single SVG symbol sprite per dock's shadow root. Built-in launchers, navigation, and the demo use `<use>` references; More uses a three-dot icon, without the Mac command glyph. The Nook mark retains its fixed geometry.
|
|
283
|
+
|
|
284
|
+
Inside an icon template, reference the built-in sprite:
|
|
285
|
+
|
|
286
|
+
```html
|
|
287
|
+
<template slot="icon">
|
|
288
|
+
<svg
|
|
289
|
+
viewBox="0 0 24 24"
|
|
290
|
+
fill="none"
|
|
291
|
+
stroke="currentColor"
|
|
292
|
+
stroke-width="1.7"
|
|
293
|
+
stroke-linecap="round"
|
|
294
|
+
stroke-linejoin="round"
|
|
295
|
+
aria-hidden="true"
|
|
296
|
+
>
|
|
297
|
+
<use href="#nook-icon-folder"></use>
|
|
298
|
+
</svg>
|
|
299
|
+
</template>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Symbol names: `down`, `check`, `nook`, `menu`, `settings`, `commands`, `feedback`, `heart`, `guide`, `folder`, `download`, `link`, `info`, `edit`, `plus`, `people`, `file`, `code`, `archive`, `table`, `text`, `more`, `back`, and `close`.
|
|
303
|
+
|
|
304
|
+
`@3sln/nook/icons` exports `sprite` and `icon(name)` for programmatic use. `icon(name)` returns a lightweight SVG reference; unknown names fall back to `file`. References must be in the same shadow tree as their sprite. Outside Nook, mount the exported sprite in the relevant document/tree. Colors inherit `currentColor`; sizes can be customized through parts.
|
|
305
|
+
|
|
306
|
+
## Commands
|
|
307
|
+
|
|
308
|
+
`nook-command` is a hidden declarative definition with required nonempty `name` and `description` attributes. Valid, available commands automatically add a Commands launcher to root/overflow allocation. Individual commands do not appear as ordinary menu items. Removing the last valid command withdraws the launcher and closes an open palette. Missing required metadata, `hidden`, and `disabled` withdraw a command until corrected. Definitions are discovered inside the dock, including nested contexts.
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<nook-command
|
|
312
|
+
id="create-note"
|
|
313
|
+
name="Create note"
|
|
314
|
+
description="Create a note with a title and priority"
|
|
315
|
+
keybind="Ctrl+Shift+N"
|
|
316
|
+
>
|
|
317
|
+
<nook-arg
|
|
318
|
+
name="title"
|
|
319
|
+
label="Title"
|
|
320
|
+
placeholder="A new idea"
|
|
321
|
+
required
|
|
322
|
+
></nook-arg>
|
|
323
|
+
<nook-arg name="priority" label="Priority" type="select" value="normal">
|
|
324
|
+
<option value="normal">Normal</option>
|
|
325
|
+
<option value="high">High</option>
|
|
326
|
+
</nook-arg>
|
|
327
|
+
</nook-command>
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Ctrl+Shift+P and Cmd+Shift+P open the search palette. Set `commands-keybind` on the dock to change that binding, or set it to an empty string to disable it. `dock.open('commands')` also opens the surface. Search matches all words across names and descriptions. Arrow Up/Down chooses a result; Enter opens/runs it. Tab can focus result buttons normally. The palette uses a desktop card and the fullscreen mobile presentation.
|
|
331
|
+
|
|
332
|
+
A command's optional `keybind` activates it directly, opening its argument form if needed. Supported bindings appear alongside results on desktop and update live when changed. They are hidden on touch devices. Modifier aliases `Ctrl`/`Control`, `Alt`/`Opt`/`Option`, and `Meta`/`Cmd`/`Command` are accepted without detecting the user's operating system.
|
|
333
|
+
|
|
334
|
+
Arguments use `nook-arg`, with a required `name` and optional `label`. `type` accepts native input types, plus `toggle` and `enum`/`select`; the default is `text`. Attributes include `required`, `value` (initial/default value), `placeholder`, `min`, `max`, `step`, `pattern`, `disabled`, and `hidden`. Supply option elements for selects. Use unique argument names. Forms use native validation. Values are strings, booleans for toggles/checkboxes, and numbers (or `null` for an empty optional field) for number/range inputs. Field values survive navigation and unrelated DOM changes; changing an argument's type recreates that field. No-argument commands run immediately.
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
dock.addEventListener("nook-command", (event) => {
|
|
338
|
+
const { source, id, name, args, context } = event.detail;
|
|
339
|
+
// Dispatch your app's Action. Nook does not execute application behavior.
|
|
340
|
+
// Prevent default to retain the current view, for example while handling errors.
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Keyboard interpretation uses reported `KeyboardEvent.key` values and, for Alt/Option printable-key bindings and numbered selection, `navigator.keyboard.getLayoutMap()`. There are no US key-position tables, user-agent checks, inferred OS layouts, or embedder layout configuration. A layout-dependent binding is disabled and its hint hidden unless browser-provided mapping resolves its key. If layout data is absent, denied, incomplete, or later becomes unavailable, those bindings and their number badges are disabled; the UI remains clickable and logical-key shortcuts such as Ctrl/Cmd+Shift+P remain available. Available mappings are refreshed on focus, Alt/Option modifier presses, and browser layout-change events. Dependent bindings are suspended while a refresh is pending, and stale requests cannot overwrite newer results. Browser/OS-reserved combinations can still take precedence.
|
|
345
|
+
|
|
346
|
+
Search results, bindings, command availability, and argument definitions update live. Existing result buttons and form fields are retained. Adding a command does not erase the current query or draft arguments; opening the palette anew starts a fresh search. Newly added argument fields are pre-rendered before navigation.
|
|
347
|
+
|
|
348
|
+
Additional parts: `command-search`, `command-list`, `command-empty`, `command-result`, `command-name`, `command-description`, `command-keybind`, `command-status`, `command-form`, `command-fields`, `command-field`, `command-arg-label`, `command-arg-control`, `command-run`, `number`, `tutorial`, and `tutorial-continue`. Set `--nook-command-width` (default `480px`) to customize the desktop palette width.
|
|
349
|
+
|
|
350
|
+
## Compact and expanded views
|
|
351
|
+
|
|
352
|
+
Small menus float without a surrounding card on desktop and mobile. Root views stay compact within their configured row capacity; submenu views through four; settings stay compact through three controls. The root shows only its items. Floating actions share one restrained frosted surface for contrast over app content.
|
|
353
|
+
|
|
354
|
+
Larger views use a contained card on desktop. On touch/coarse-pointer or hoverless devices they fill the viewport over a blurred, tinted backdrop, with safe-area padding, a close/back header, and scrollable content. Adding/removing definitions updates presentation live without remounting their UI. Arbitrary panels and the first-use tutorial default to expanded presentation because their content size is not implied by an item count.
|
|
355
|
+
|
|
356
|
+
Once navigation enters an expanded view, descendants retain expanded presentation until Back returns above that ancestor. On mobile, nested submenu trees appear as labeled sections within the fullscreen view, including deeper nested groups. Leaf panels still open their own view and return through the existing navigation path. Group launch buttons are reconciled ahead of time; actual panels and controls retain one mounted instance. Group labels, source order, and additions/removals update live. Definitions marked `root` are excluded from their original submenu. Desktop keeps its normal submenu navigation within the retained card layout.
|
|
357
|
+
|
|
358
|
+
The close button appears only in fullscreen mobile views. Compact and desktop views close through the corner handle, clicking outside, or Escape. The fullscreen close button also resets navigation. Presentation is inherited from expanded ancestors even if a child requests compact presentation; leaving that branch restores its normal layout.
|
|
359
|
+
|
|
360
|
+
Embedders can override the default with `presentation="compact"` or `presentation="expanded"` on a `nook-menu`/`nook-panel`, or `root-presentation`, `settings-presentation`, `overflow-presentation` (or legacy `full-presentation`), `welcome-presentation` on the dock. Removing the override restores automatic selection. Use explicit presentation for custom content with unusual dimensions.
|
|
361
|
+
|
|
362
|
+
Customize floating control backgrounds with `--nook-floating-background`; customize the viewport layer with `--nook-backdrop` and `--nook-backdrop-blur` (defaults to the shared 24px material blur). Group spacing uses `--nook-group-gap` (default `16px`).
|
|
363
|
+
|
|
364
|
+
## Events and application behavior
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
const dock = document.querySelector("nook-dock");
|
|
368
|
+
dock.context = { route: location.pathname, documentId: "current-document" };
|
|
369
|
+
|
|
370
|
+
dock.addEventListener("nook-action", (event) => {
|
|
371
|
+
const { source, id, context } = event.detail;
|
|
372
|
+
// Translate intent into your application's Actions here.
|
|
373
|
+
// event.preventDefault() keeps Nook open for this action.
|
|
374
|
+
});
|
|
375
|
+
dock.addEventListener("nook-change", (event) => {
|
|
376
|
+
const { source, value } = event.detail;
|
|
377
|
+
if (source.id === "dark")
|
|
378
|
+
document.documentElement.classList.toggle("dark", value);
|
|
379
|
+
});
|
|
380
|
+
dock.addEventListener("nook-submit", (event) => {
|
|
381
|
+
const { source, formData } = event.detail;
|
|
382
|
+
// Dispatch your feedback action with formData and app-owned context.
|
|
383
|
+
});
|
|
384
|
+
dock.addEventListener("nook-corner-change", (event) => {
|
|
385
|
+
// Optional: persist event.detail.corner in your app's preferences.
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
All these events bubble and cross shadow boundaries. `nook-input` reports continuous control changes; `nook-change` reports committed changes. `nook-submit` intercepts native form submission inside panels and supplies `FormData`. The generic panel/action layer makes no network requests. Optional feedback hooks and the separate native capture helper are described above; feedback delivery remains app-owned. The demo records feedback locally and says so in its UI. The app owns consent, context capture, storage, and delivery.
|
|
390
|
+
|
|
391
|
+
For a custom setting, supply `<template slot="control">…</template>`. Its native bubbling `change` event is translated to `nook-change` with the source setting and target's value. Complex controls may emit their own composed custom events for richer payloads. The native `value` property synchronization described above applies to the supplied native controls.
|
|
392
|
+
|
|
393
|
+
## Navigation
|
|
394
|
+
|
|
395
|
+
Tap/click opens the root menu, with action icons. The device-specific introduction is shown on first use. Touch/coarse-pointer or hoverless devices hide keyboard hints and show swipe instructions instead. These capability queries update live when browser emulation or input capabilities change. Clicking outside closes it. Hold for 450ms, then drag. The live handle stays anchored and dims while a non-interactive preview shows the destination corner; release commits that corner. Cancellation restores the original placement. The mark rotates to match each corner. The handle has a 48px touch target and a 32px visible corner with the same recognizable quarter-circle shape.
|
|
396
|
+
|
|
397
|
+
On touch devices (and pointer-based testing), swipe horizontally or diagonally for All actions, and vertically for settings. Swipes have a 32px distance threshold and broad directional bands. Empty surfaces do not activate from gestures or shortcuts. Gestures start only on the handle, leaving application scrolling alone. More is the overflow launcher in the root menu; swipes show the complete All actions list.
|
|
398
|
+
|
|
399
|
+
| Desktop action | Default | Dock attribute |
|
|
400
|
+
| --------------- | ------------- | ------------------ |
|
|
401
|
+
| Main/root menu | `Shift+?` | `root-keybind` |
|
|
402
|
+
| Settings | `Alt+Shift+S` | `settings-keybind` |
|
|
403
|
+
| More / overflow | `Alt+Shift+Q` | `overflow-keybind` |
|
|
404
|
+
| Feedback | `Alt+Shift+F` | `feedback-keybind` |
|
|
405
|
+
|
|
406
|
+
`quick-keybind` remains a compatibility alias when `overflow-keybind` is absent. Set an empty `overflow-keybind` to disable the default.
|
|
407
|
+
|
|
408
|
+
Designate the feedback action/panel with `feedback`. The first available designated item wins. Any action, panel, or submenu, including a root item, may also declare `keybind` or set `.keybind`. Bindings use `Ctrl`, `Alt`, `Meta`, `Shift` plus `KeyboardEvent.key`, for example `Meta+Shift+K`. Modifiers must match exactly. Set an empty dock binding attribute to disable that default. Dock defaults take precedence over item bindings; otherwise the first available item in DOM order wins. Browser/OS-reserved bindings may not reach the page.
|
|
409
|
+
|
|
410
|
+
Shortcuts ignore editable elements, composition, repeated presses, and already-prevented events. Inside a menu, arrows cycle focus, Home/End jump, Enter/Space activate buttons, and Alt/Option+1–9 chooses the corresponding displayed action. Hold Alt/Option to reveal assigned numbers alongside item labels in readable inline rows; release it or leave the window to hide them. Plain number keys never activate items. Tab cycles within the current surface. Escape closes the entire menu and clears navigation; reopening starts at the root. Backspace (the Mac Delete key) goes back one level. Backspace/Delete retains normal editing behavior in text fields. Native editing keys retain their behavior inside controls. Focus returns to the element that opened the menu. Reduced-motion preferences disable animation.
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
dock.open(); // root
|
|
414
|
+
dock.open("settings");
|
|
415
|
+
dock.open("overflow"); // grouped overflow (More)
|
|
416
|
+
// Legacy "quick" and "full" view names open the same surface.
|
|
417
|
+
dock.open(document.querySelector("#feedback")); // a panel/menu element
|
|
418
|
+
dock.close();
|
|
419
|
+
dock.setAttribute("corner", "top-left");
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Anchor positions
|
|
423
|
+
|
|
424
|
+
Nook supports eight positions: `top-left`, `top-center`, `top-right`, `right-center`, `bottom-right`, `bottom-center`, `bottom-left`, and `left-center`.
|
|
425
|
+
|
|
426
|
+
```html
|
|
427
|
+
<nook-dock position="right-center" disabled-positions="top-left top-center">
|
|
428
|
+
<!-- app definitions -->
|
|
429
|
+
</nook-dock>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
dock.position = "bottom-center";
|
|
434
|
+
dock.disabledPositions = ["top-center", "left-center"];
|
|
435
|
+
dock.addEventListener("nook-position-change", ({ detail }) => {
|
|
436
|
+
// Persist detail.preferredPosition in your application's preferences, if desired.
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The default position policy hides `top-center` and `bottom-center` on touch/coarse-pointer or hoverless screens narrower than 480px in portrait orientation. They become available in landscape. Explicit `disabled-positions` constraints always take precedence. Set `position-policy="all"` to keep all otherwise-allowed positions eligible at every viewport size.
|
|
441
|
+
|
|
442
|
+
`dock.availablePositions` returns the current eligible positions. Nook remembers `dock.preferredPosition` when a viewport change temporarily requires a different actual `dock.position`, then restores the preference when it becomes eligible. A deliberate position change, including reselecting the current fallback, replaces that preference. `nook-position-change` reports `position`, `preferredPosition`, and `reason` (`preference` or `availability`); persist the preferred value when saving user placement. Height-only resizes while a text field is focused preserve the existing anchor policy, so keyboard-related resizing does not turn portrait into landscape.
|
|
443
|
+
|
|
444
|
+
Corners retain the small quarter-circle handle. Edge centers use a shallow semicircle with the same mark oriented toward the app. Menus open inward from the chosen edge, and fullscreen mobile views stay fixed to the viewport. For side-center command palettes, a bounded height keeps the search field stable as results change; `--nook-command-height` customizes that desktop height.
|
|
445
|
+
|
|
446
|
+
`corner` and `disabled-corners` / `.disabledCorners` remain compatible aliases. Both disabled attributes are combined, so reserved corners stay unavailable when edge centers are enabled. `position` wins over `corner` for initial placement; both attributes reflect subsequent changes. Legacy `nook-corner-change` still fires alongside `nook-position-change`.
|
|
447
|
+
|
|
448
|
+
During dragging, every allowed corner/center is outlined and the closest one is highlighted. The actual handle stays anchored until release. Changing constraints updates the preview immediately. If no other position is available, dragging and its hints are disabled. Blocking the current position moves Nook to the nearest allowed anchor; blocking every position retains the current anchor and disables dragging. Unknown constraint names are ignored. Swipes, opening, and keyboard navigation remain available.
|
|
449
|
+
|
|
450
|
+
## Styling
|
|
451
|
+
|
|
452
|
+
Defaults adapt to light and dark schemes through native `color-scheme` and `light-dark()` colors. Set `nook-dock { color-scheme: dark; }` or `light` to choose explicitly; leave the default `light dark` to follow the system. Native controls, accent text contrast, floating controls, and backdrops follow the same palette. Explicit custom properties remain authoritative; use `light-dark(lightValue, darkValue)` when an override should also adapt.
|
|
453
|
+
|
|
454
|
+
Compact views use one acrylic-style frosted toolbar: a neutral translucent tint, 24px blur, mild saturation, a crisp rounded edge, a fine inset highlight, and a single elevation shadow. Buttons and setting rows sit directly on the shared material; there are no feathered masks or persistent button bubbles. The material becomes opaque when backdrop filtering is unsupported or reduced transparency is requested.
|
|
455
|
+
|
|
456
|
+
Fullscreen views share the same tint, blur, saturation, and edge highlight, extending the material to the viewport. `--nook-material-background` and `--nook-material-blur` control both variants together; per-variant overrides remain available. Both variants use the same opaque fallback for reduced transparency or unsupported backdrop filtering.
|
|
457
|
+
|
|
458
|
+
Customize it with `--nook-halo-background`, `--nook-halo-blur` (24px), `--nook-halo-inset` (-8px), `--nook-material-saturation` (125%), `--nook-material-edge`, `--nook-material-highlight`, `--nook-radius`, and `--nook-shadow`. Tap highlights are disabled; keyboard focus on the handle follows its quarter-circle outline.
|
|
459
|
+
|
|
460
|
+
References: [Microsoft Acrylic guidance for transient menus and flyouts](https://learn.microsoft.com/en-us/windows/apps/design/style/acrylic) and [Josh W. Comeau's frosted-glass examples](https://www.joshwcomeau.com/css/backdrop-filter/). This is a CSS interpretation of the material, not the native OS implementation.
|
|
461
|
+
|
|
462
|
+
```css
|
|
463
|
+
nook-dock {
|
|
464
|
+
--nook-accent: #7560e8;
|
|
465
|
+
--nook-surface: #fff;
|
|
466
|
+
--nook-text: #232237;
|
|
467
|
+
--nook-muted: #77768a;
|
|
468
|
+
--nook-border: #e9e7ef;
|
|
469
|
+
--nook-radius: 16px;
|
|
470
|
+
--nook-shadow: 0 8px 32px #241a4918, 0 2px 8px #241a490a;
|
|
471
|
+
--nook-duration: 180ms;
|
|
472
|
+
--nook-font: system-ui, sans-serif;
|
|
473
|
+
--nook-width: 304px;
|
|
474
|
+
--nook-overflow-width: 360px;
|
|
475
|
+
--nook-root-max-items: 5;
|
|
476
|
+
--nook-z: 10000;
|
|
477
|
+
--nook-label-display: none;
|
|
478
|
+
--nook-padding: 16px;
|
|
479
|
+
--nook-gap: 8px;
|
|
480
|
+
--nook-item-size: 48px;
|
|
481
|
+
--nook-surface-offset: 56px;
|
|
482
|
+
--nook-edge-gap: 12px;
|
|
483
|
+
}
|
|
484
|
+
nook-dock::part(surface) {
|
|
485
|
+
border-width: 2px;
|
|
486
|
+
}
|
|
487
|
+
nook-dock::part(item) {
|
|
488
|
+
border-radius: 12px;
|
|
489
|
+
}
|
|
490
|
+
nook-dock::part(control) {
|
|
491
|
+
accent-color: teal;
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Exposed parts: `handle-tooltip`, `handle-shortcut`, `drag-hint`, `landing-zone`, `landing-outline`, `drag-preview`, `preview-handle`, `preview-mark`, `anchor`, `handle`, `mark`, `announcement`, `surface`, `header`, `back`, `heading`, `close`, `body`, `items`, `item`, `icon`, `item-label`, `tooltip`, `tooltip-text`, `setting`, `setting-label`, `description`, `control`, `content`, `groups`, `group`, `group-heading`, and `group-body`. The handle's quarter-circle background is a pseudo-element; customize its color with `--nook-accent` or the handle itself with `::part(handle)`. Inner geometry can be overridden by styling parts. Nook's inline SVG mark remains constant.
|
|
496
|
+
|
|
497
|
+
The surface is fixed-position and uses a configurable z-index. Mount the dock near the document root, outside transformed/clipping ancestors. This version does not use the browser top layer; a host modal in the top layer can cover it.
|
|
498
|
+
|
|
499
|
+
Overflow exposes `overflow-groups`, `overflow-group`, `overflow-group-heading`, and `overflow-group-body` parts alongside the standard item parts. `--nook-overflow-width` controls its desktop width; `--nook-keyboard-width` controls the temporary labeled width of icon menus while Alt/Option is held. More and the command palette keep their own widths.
|
|
500
|
+
|
|
501
|
+
Typography uses a shared hierarchy: 16px semibold headings, 14px primary text and command names, 13px control labels, and 12px descriptions/captions. Headings have a tighter line height; supporting text uses more breathing room and starts at the same inline edge. Fine print stays at 12px, and touch text inputs keep their 16px minimum. Customize `--nook-font`, `--nook-text-xs`, `--nook-text-sm`, `--nook-text-md`, `--nook-text-lg`, `--nook-line-height`, `--nook-heading-weight`, and `--nook-label-weight` to change the hierarchy consistently. `--nook-invite-cycle` (2s) and `--nook-invite-delay` (200ms) style the attention burst; the six-second budget and randomized cooldown are owned by the onboarding provider.
|
|
502
|
+
|
|
503
|
+
After wrapping and switching long-label menus to rows, extreme metadata is visually clamped: headings to four lines (two in short viewports), item/setting labels and descriptions to three, command names and filenames to two, and group headings to two. Full text stays in the DOM and existing accessible names/descriptions. Input values and app-owned panel content are never shortened; their controls or body scroll instead.
|
|
504
|
+
|
|
505
|
+
Settings and feedback use wider form layouts by default: 420px and 520px respectively, constrained to the visible app bounds. Override them with `--nook-settings-width` and `--nook-feedback-width`; an explicit shared `--nook-width` still supplies their fallback. Expanded touch views continue to fill the available viewport. Tooltip padding belongs to `::part(tooltip)`; its separately clamped text is exposed as `::part(tooltip-text)`, so long labels retain space below the last visible line.
|
|
506
|
+
|
|
507
|
+
Customize these limits with `--nook-heading-lines`, `--nook-label-lines`, `--nook-description-lines`, `--nook-command-name-lines`, `--nook-group-lines`, `--nook-filename-lines`, and `--nook-tooltip-lines`. Rich option content has a `--nook-option-content-max-height` budget (12rem by default). `setting-name`, `feedback-success-heading`, and `feedback-success-message` are separately styleable parts.
|
|
508
|
+
|
|
509
|
+
## Motion
|
|
510
|
+
|
|
511
|
+
Menus open with a small spring overshoot. Fullscreen views keep the backdrop stable and give their content a gentler entrance. Closing is faster than opening. Corner changes settle in place around the destination corner rather than moving an exposed arc across the screen; preview targets ease in without following the pointer. Hover, press, selection, and toggle feedback use short transitions.
|
|
512
|
+
|
|
513
|
+
Timing tokens are `--nook-pop-duration` (260ms), `--nook-settle-duration` (240ms), `--nook-exit-duration` (120ms), and `--nook-duration` (180ms for general feedback). `prefers-reduced-motion` suppresses CSS and scripted effects; active settling effects are canceled if the preference changes. Disconnecting Nook cancels outstanding effects, and a new effect replaces the previous effect on the same element. No navigation or application event waits for an animation to finish.
|
|
514
|
+
|
|
515
|
+
## Stack boundaries
|
|
516
|
+
|
|
517
|
+
`src/bl/menu.js` contains the pure navigation/gesture/keybinding rules and `MenuState`, an ngin Query with declared `intents` and `onboarding` dependencies. `nook-dock` is the composition root: it provides an injected EventTarget, subscribes to the Query, and owns browser lifecycle cleanup. Disconnect unsubscribes, aborts input listeners, disconnects the DOM observer, cancels hold timers, and disposes the QueryStore/container. There is no second observable state store or app action registry.
|
|
518
|
+
|
|
519
|
+
The UI uses native DOM primitives because its contract is persistent custom-element instances, rather than an application VDOM tree. App behavior is represented only by native bubbling events; consumers can wire those into ngin Actions or any other app composition. No engine handle is passed to panel content.
|
|
520
|
+
|
|
521
|
+
## Verification
|
|
522
|
+
|
|
523
|
+
`bun run test` runs Node/Happy DOM and pure-rule tests covering persistent drafts/control identity, live addition/removal, stable relabeling, root reassignment, active-panel removal, setting synchronization, separate shortcuts, editable-field guards, navigation, directional classification, exact key modifiers, source ordering, nested groups, and zero child-node churn during navigation. Node runs the DOM harness because this Happy DOM version's MutationObserver scheduling differs under Bun.
|
|
524
|
+
|
|
525
|
+
`bun run test:browser` adds desktop/mobile Playwright coverage for rendering, keyboard navigation, live mutations, persistent forms, swipe routing, and long-press corner movement. Browser tests require an installed Chromium and permission to bind a local port. CI runs both browser projects on Ubuntu and retains the HTML report, stress screenshots, and failure traces for seven days. Browser automation does not replace real-device checks for touch gestures or browser permission dialogs.
|
|
526
|
+
|
|
527
|
+
### Browser stress lab
|
|
528
|
+
|
|
529
|
+
With the dev server running, open [the stress lab](http://localhost:4178/nook/demo/stress.html). It provides 80 actions with long group names, 40 settings, 60 rich choices per enum, 35 commands, long unbroken identifiers, multilingual text, large-type controls, nested menus, and feedback with long filenames. Switch scenarios, resize the viewport, and drag between available anchors.
|
|
530
|
+
|
|
531
|
+
```sh
|
|
532
|
+
npm run test:browser -- stress.spec.js
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
The browser stress suite checks horizontal overflow, panel bounds, last-control reachability, header/close visibility while scrolling, enum selection, live control identity, command-search stability, large text at 280px width, and portrait/landscape preference restoration. It attaches screenshots of the scenarios to the Playwright HTML report. Height-only viewport resizing with a focused text field exercises the keyboard-resize guard; it does not simulate every native on-screen keyboard behavior. Real-device validation remains necessary.
|
|
536
|
+
|
|
537
|
+
Long-label submenus use rows instead of narrow icon tiles. Large views scroll their body while retaining the header, and very long headings have their own bounded area. Descriptions, identifiers, and group headings wrap rather than widening the panel. Small-screen text/range/enum settings place the control below its label; toggles stay alongside their labels.
|
|
538
|
+
|
|
539
|
+
## CI and releases
|
|
540
|
+
|
|
541
|
+
GitHub Actions runs unit/DOM tests, npm package-content checks, and desktop/mobile Chromium tests on pull requests and pushes to `main`. `CI / test` is the check to require in branch protection. The browser suite uses the installed dependency from the lockfile. `npm run check:package` verifies every exported entry point is packaged and excludes demo, test, and workflow files.
|
|
542
|
+
|
|
543
|
+
A push to `main` that changes the `package.json` version runs the same checks, publishes `@3sln/nook`, then creates a GitHub release and `v<version>` tag at the tested commit. Other package metadata changes do not publish. Stable versions use npm's `latest` tag; prereleases use `next` and create a GitHub prerelease. The initial main push also qualifies. Release jobs run serially. Release notes are generated by GitHub, and the published tarball is attached to the release.
|
|
544
|
+
|
|
545
|
+
To release:
|
|
546
|
+
|
|
547
|
+
```sh
|
|
548
|
+
npm version patch --no-git-tag-version
|
|
549
|
+
# Or use minor, major, or a specific prerelease version.
|
|
550
|
+
# Commit package.json with your changes and merge/push to main.
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
No build output or manual tag push is required. If publishing succeeds but GitHub release creation fails, rerun the failed workflow. An already-published version is accepted only when its tarball checksum matches the current package. The workflow rejects conflicting tags or different bytes under an existing npm version. The Release workflow also supports a manual run from `main` to retry the current version; it never replaces a published version. A changed package with an existing version requires a new version bump.
|
|
554
|
+
|
|
555
|
+
### One-time trusted publisher setup
|
|
556
|
+
|
|
557
|
+
These files target the public GitHub repository **`3sln/nook`**. If the repository name differs, update `repository`, `homepage`, and `bugs` in `package.json` and the clone URL above before publishing. This checkout's automation files must live at the root of that repository.
|
|
558
|
+
|
|
559
|
+
In the npm settings for `@3sln/nook`, configure a GitHub Actions trusted publisher:
|
|
560
|
+
|
|
561
|
+
| Field | Value |
|
|
562
|
+
| ----------------- | --------------------------- |
|
|
563
|
+
| Organization/user | `3sln` |
|
|
564
|
+
| Repository | `nook` |
|
|
565
|
+
| Workflow filename | `release.yml` |
|
|
566
|
+
| Environment | Leave empty |
|
|
567
|
+
| Allowed actions | Enable direct `npm publish` |
|
|
568
|
+
|
|
569
|
+
The workflow uses a GitHub-hosted runner, Node.js 24 with an OIDC-capable npm CLI, and `id-token: write`. Publishing uses short-lived OIDC credentials and provenance; no `NPM_TOKEN` secret is needed. The job's GitHub token creates tags and releases. Keep the repository public for provenance. Details: [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
570
|
+
|
|
571
|
+
If npm requires the package to exist before its settings are available, bootstrap the initial package with an authenticated local publish, then configure its trusted publisher and rerun Release. An unchanged bootstrap tarball is recognized rather than republished. Ensure the workflow file is already present on GitHub when configuring the publisher.
|
|
572
|
+
|
|
573
|
+
## License
|
|
574
|
+
|
|
575
|
+
MIT. See [LICENSE](./LICENSE).
|
|
576
|
+
|
|
577
|
+
### App boundaries and elevation
|
|
578
|
+
|
|
579
|
+
A dock containing only Nook definitions anchors to the viewport. To anchor within the app area below a header or above a bottom bar, wrap the live app in the dock:
|
|
580
|
+
|
|
581
|
+
```html
|
|
582
|
+
<header>App bar</header>
|
|
583
|
+
<nook-dock style="--nook-handle-z: 20; --nook-overlay-z: 1000">
|
|
584
|
+
<main><!-- Existing application, rendered through a slot --></main>
|
|
585
|
+
<nook-action label="Share"></nook-action>
|
|
586
|
+
</nook-dock>
|
|
587
|
+
<footer>Bottom bar</footer>
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
The dock's visible rectangle becomes the anchor boundary and updates on resize and scrolling. Nook follows the Visual Viewport when the on-screen keyboard opens, resizes, or pans the page, keeping bottom anchors above it and constraining menus to the remaining visible app area. Browsers without Visual Viewport support use window resize events. Give the wrapper its intended size using your app layout. App nodes are not cloned. `boundary="viewport"` opts out; `boundary="self"` explicitly uses the dock rectangle. `--nook-inset-top`, `--nook-inset-right`, `--nook-inset-bottom`, and `--nook-inset-left` provide additional clearance. `--nook-z` sets the default elevation; `--nook-handle-z`, `--nook-overlay-z`, and `--nook-drag-z` independently override the handle, menus, and drag previews. Normal CSS stacking-context constraints still apply.
|
|
591
|
+
|
|
592
|
+
### Localization with Bab
|
|
593
|
+
|
|
594
|
+
Each dock uses `@3sln/bab` with the `nook` scope. Supply a catalogue and locale, or pass your existing Bab translator as `dock.localization`:
|
|
595
|
+
|
|
596
|
+
```js
|
|
597
|
+
dock.localization = {
|
|
598
|
+
locale: "es",
|
|
599
|
+
catalogue: {
|
|
600
|
+
nook: {
|
|
601
|
+
Settings: "Ajustes",
|
|
602
|
+
More: "Más",
|
|
603
|
+
"All actions": "Todas las acciones",
|
|
604
|
+
"Search commands": "Buscar comandos",
|
|
605
|
+
"{#} snapshots": "{#} capturas",
|
|
606
|
+
"{#} snapshot": "{#} captura",
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Without an explicit locale, Nook follows its `lang` attribute, then the document language. Untranslated messages fall back to their English source text. Reassign `localization` to change language or catalogue live; existing controls and drafts are preserved. Built-in text, accessible names, authored labels/descriptions, command search text, and snapshot counts use the translator. App-owned template content remains app-owned. Set `dir` on the dock or an ancestor for right-to-left presentation.
|
|
613
|
+
|
|
614
|
+
### Held swipe dial
|
|
615
|
+
|
|
616
|
+
Fast horizontal and diagonal swipes open **All actions**, including items already visible in the root menu; vertical swipes open settings. Hold a swipe outside Nook briefly to reveal a dial with Settings and All actions, then release directly on a choice. Releasing anywhere else cancels. Its opacity follows distance from Nook. A clear inner gap separates options from the handle; moving back inside that area cancels selection. Long-pressing before moving continues to drag the handle. Dial parts are `swipe-dial`, `swipe-dial-option`, `swipe-dial-icon`, and `swipe-dial-label`. `dock.open("all")` opens the complete list programmatically.
|