@pastweb/components-core 2.0.0 → 2.2.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.
@@ -0,0 +1,188 @@
1
+ # Walktour
2
+
3
+ Walktour guides a user through an ordered set of targets. The core package owns
4
+ the browser-portable state machine: navigation, controlled or uncontrolled
5
+ visibility, target measurement, event handling, step options, geometry
6
+ calculation, and interaction decisions. A framework renderer renders
7
+ `WalktourState`.
8
+
9
+ ## When to use
10
+
11
+ Use Walktour for optional, short orientation flows that help people discover a
12
+ feature or a changed workflow. Keep each step focused on one action. Do not use
13
+ it for essential instructions, errors, or information a user must be able to
14
+ refer back to later; show that content in the interface instead.
15
+
16
+ ## Props
17
+
18
+ | PropName | Type | Required | Default | Description |
19
+ |---|---|---:|---:|---|
20
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves root and part classes in a renderer. |
21
+ | `steps` | `WalktourStep[]` | yes | - | Ordered guide steps. An empty list renders nothing. |
22
+ | `open` | `boolean` | no | `false` | Controlled visibility. Omit for mediator-managed visibility. |
23
+ | `initialStepIndex` | `number` | no | `0` | Initial zero-based step for `open()`. Out-of-range values are clamped. |
24
+ | `className` | `string` | no | `''` | Additional root classes. |
25
+ | `zIndex` | `number` | no | `1000` | Overlay stacking level used by web renderers. |
26
+ | `maskPadding` | `number` | no | `8` | Space around a target highlight in pixels. |
27
+ | `maskRadius` | `number` | no | `6` | Highlight corner radius in pixels. |
28
+ | `offset` | `number` | no | `12` | Gap between a target and the tour card in pixels. |
29
+ | `placement` | `WalktourPlacement` | no | `'bottom'` | Default card placement. |
30
+ | `disableMask` | `boolean` | no | - | Hides the dimmed mask and spotlight. |
31
+ | `disableMaskInteraction` | `boolean` | no | - | Prevents interaction with the highlighted target. |
32
+ | `closeOnMaskClick` | `boolean` | no | - | Lets an eligible mask activation close the tour. |
33
+ | `closeOnEscape` | `boolean` | no | `true` | Lets Escape close an eligible tour step. |
34
+ | `scrollIntoView` | `boolean` | no | `true` | Asks web renderers to scroll the current target into view. |
35
+ | `nextLabel` | `string` | no | `'Next'` | Default next control label. The last step uses `Finish`. |
36
+ | `previousLabel` | `string` | no | `'Back'` | Default previous control label. |
37
+ | `closeLabel` | `string` | no | `'Skip'` | Default close control label. |
38
+ | `onOpenChange` | `(open: boolean) => void` | no | - | Reports requested visibility changes. |
39
+ | `onStepChange` | `(index, step) => void` | no | - | Reports a completed step change. |
40
+ | `onComplete` | `() => void` | no | - | Runs before the final step closes. |
41
+ | `onClose` | `() => void` | no | - | Runs whenever the mediator requests closure. |
42
+
43
+ `WalktourPlacement` is `'top' | 'right' | 'bottom' | 'left' | 'center'`.
44
+ When a target cannot be resolved, the card uses the centred placement.
45
+
46
+ ## Core usage
47
+
48
+ Pass a framework-neutral element reference for every step that should highlight
49
+ an element. The reference must expose its current element through `value`.
50
+ Steps without `ref` intentionally render as centred, targetless steps.
51
+
52
+ ```xts
53
+ const saveButton = ref<HTMLElement | null>(null);
54
+
55
+ const steps: WalktourStep[] = [
56
+ {
57
+ ref: saveButton,
58
+ title: 'Save changes',
59
+ description: 'Select Save when you are ready to publish your updates.',
60
+ placement: 'top',
61
+ },
62
+ {
63
+ title: 'You are ready',
64
+ description: 'Your changes can now be reviewed.',
65
+ placement: 'center',
66
+ },
67
+ ];
68
+ ```
69
+
70
+ The core component is renderer-neutral. A framework package maps its own ref
71
+ type to this contract and is responsible only for rendering `WalktourState`.
72
+
73
+ ## Step options
74
+
75
+ | Property | Type | Description |
76
+ |---|---|---|
77
+ | `ref` | `{ value: HTMLElement \| null }` | Framework-neutral element reference. Omit it for a centred, targetless step. React adapters accept `RefObject<HTMLElement \| null>`. |
78
+ | `title` / `description` | renderer content | Content rendered by the framework adapter. `description` is required. |
79
+ | `placement`, `maskPadding`, `maskRadius`, `offset` | step override | Override the matching component default for this step. |
80
+ | `nextLabel`, `previousLabel`, `closeLabel` | `string` | Override a control label for this step. |
81
+ | `disableNext`, `disablePrevious`, `disableClose` | `boolean` | Removes the matching navigation action. |
82
+ | `nextOnTargetClick` | `boolean` | Advances through the mediator when the renderer reports a target activation. |
83
+
84
+ ## Renderer implementation
85
+
86
+ `walktourMediator` composes reactive state and effects. Stateless helpers in
87
+ `utils/` perform navigation, state updates, geometry calculation, target
88
+ resolution, and listener setup with explicit props, state, and mutable vars.
89
+ This keeps those helpers reusable and avoids creating internal action factories
90
+ for every Walktour instance.
91
+
92
+ The mediator resolves the active target reference, measures the target element,
93
+ subscribes to resize, scroll, keyboard, and target-click events, and cleans
94
+ them up when unmounted. A browser framework renderer creates the mediator,
95
+ connects its lifecycle methods, and renders the mediator state:
96
+
97
+ ```xts
98
+ import { Render, useBeforeUnmount, useMediator, useMounted, withDefaultProps } from '@pastweb/x';
99
+ import { DEFAULT_PROPS, walktourMediator } from '@pastweb/components-core/Walktour';
100
+
101
+ export function Walktour(props: WalktourProps) {
102
+ const p = withDefaultProps<WalktourDefaultProps>(props, DEFAULT_PROPS);
103
+ const m = useMediator<WalktourMediator>(walktourMediator, p);
104
+ const s = m.state;
105
+
106
+ useMounted(m.useMounted);
107
+ useBeforeUnmount(m.useBeforeUnmount);
108
+
109
+ if (!s.isOpen || !s.step) return null;
110
+
111
+ return (
112
+ <div class={p.cls('Walktour', p.className)} #style={{ zIndex: p.zIndex }} aria-live="polite">
113
+ <fragment #if={s.isMaskVisible && s.hole}>
114
+ <button class={p.cls('maskPiece')} type="button" tabindex={-1} aria-hidden="true"
115
+ #style={{ top: 0, left: 0, right: 0, height: s.hole.top }} @click={m.onMaskClick} />
116
+ <button class={p.cls('maskPiece')} type="button" tabindex={-1} aria-hidden="true"
117
+ #style={{ top: s.hole.bottom, left: 0, right: 0, bottom: 0 }} @click={m.onMaskClick} />
118
+ <button class={p.cls('maskPiece')} type="button" tabindex={-1} aria-hidden="true"
119
+ #style={{ top: s.hole.top, left: 0, width: s.hole.left, height: s.hole.bottom - s.hole.top }} @click={m.onMaskClick} />
120
+ <button class={p.cls('maskPiece')} type="button" tabindex={-1} aria-hidden="true"
121
+ #style={{ top: s.hole.top, left: s.hole.right, right: 0, height: s.hole.bottom - s.hole.top }} @click={m.onMaskClick} />
122
+ <div class={p.cls('spotlight')} #style={{
123
+ top: s.hole.top, left: s.hole.left, width: s.hole.right - s.hole.left,
124
+ height: s.hole.bottom - s.hole.top, borderRadius: s.maskRadius,
125
+ pointerEvents: s.isTargetInteractionDisabled ? 'auto' : 'none',
126
+ }} @click={s.isTargetInteractionDisabled ? m.onMaskClick : undefined} />
127
+ </fragment>
128
+ <button #if={s.isMaskVisible && !s.hole} class={p.cls('maskPiece')}
129
+ type="button" tabindex={-1} aria-hidden="true" #style={{ inset: 0 }} @click={m.onMaskClick} />
130
+ <section class={p.cls('card')} #style={s.cardPosition} role="dialog" aria-modal="true"
131
+ aria-label={typeof s.step.title === 'string' ? s.step.title : 'Guided tour'}>
132
+ <h2 #if={s.step.title} class={p.cls('title')}><Render content={s.step.title} /></h2>
133
+ <div class={p.cls('description')}><Render content={s.step.description} /></div>
134
+ <footer class={p.cls('footer')}>
135
+ <span class={p.cls('progress')}>{s.stepIndex + 1} / {p.steps.length}</span>
136
+ <button #if={s.canClose} class={p.cls('button')} type="button" @click={m.close}>{s.closeLabel}</button>
137
+ <button #if={s.canPrevious} class={p.cls('button')} type="button" @click={m.previous}>{s.previousLabel}</button>
138
+ <button #if={s.canNext} class={p.cls('button')} type="button" @click={m.next}>{s.nextLabel}</button>
139
+ </footer>
140
+ </section>
141
+ </div>
142
+ );
143
+ }
144
+ ```
145
+
146
+ The mediator exposes `open`, `close`, `next`, and `previous`, plus lifecycle
147
+ methods. It performs all clamping, callback invocation, per-step option
148
+ precedence, target measurement, scrolling, and close eligibility checks.
149
+
150
+ ## Mediator state
151
+
152
+ | Property | Description |
153
+ |---|---|
154
+ | `isOpen`, `stepIndex`, `step` | Visibility and current resolved step. |
155
+ | `targetRect`, `hole` | Target and viewport-clipped mask geometry. |
156
+ | `cardPosition` | Renderer-ready `top`, `left`, and `transform` card position. |
157
+ | `maskPadding`, `maskRadius` | Resolved visual values for the active step. |
158
+ | `isMaskVisible`, `isTargetInteractionDisabled` | Resolved mask interaction state. |
159
+ | `canClose`, `canPrevious`, `canNext` | Renderer-ready control availability. |
160
+ | `closeLabel`, `previousLabel`, `nextLabel` | Renderer-ready active-step labels. |
161
+
162
+ ## CSS Variables
163
+
164
+ | Variable | Part | Default | Description |
165
+ |---|---|---|---|
166
+ | `--pw_Walktour_mask_default_background-color` | mask | `rgba(0, 0, 0, 0.5)` | Dimmed overlay colour. |
167
+ | `--pw_Walktour_card_default_background-color` | card | `var(--pw_background-color)` | Card background. |
168
+ | `--pw_Walktour_card_default_color` | card | `var(--pw_text_color)` | Card text colour. |
169
+ | `--pw_Walktour_card_default_border-radius` | card | `var(--pw_border-radius)` | Card corner radius. |
170
+ | `--pw_Walktour_card_default_box-shadow` | card | elevation shadow | Card elevation. |
171
+ | `--pw_Walktour_step_default_transition-duration` | mask, spotlight, card | `320ms` | Geometry transition duration between steps. |
172
+
173
+ Web styles honour `prefers-reduced-motion` by disabling the step transition.
174
+
175
+ Portal integration is application-specific and belongs in a framework or app
176
+ wrapper, not in this core renderer contract.
177
+
178
+ ## Accessibility notes
179
+
180
+ - Web renderers expose the card as a labelled modal dialog and announce changes
181
+ through a polite live region.
182
+ - Escape is handled through `onKeyDown` only when `closeOnEscape` and
183
+ `canClose` are both true.
184
+ - Renderers should make mask controls non-focusable or otherwise ensure they do
185
+ not add unnecessary tab stops.
186
+ - A step without a target remains usable because the card is centred.
187
+ - Native renderers should provide an equivalent mediator implementation for
188
+ their native target, viewport, scrolling, and accessibility APIs.
@@ -0,0 +1,190 @@
1
+ # WindowController
2
+
3
+ WindowController presents either a modal dialog or a movable floating work
4
+ surface. Its rendering contract follows the WAI-ARIA Dialog pattern and targets
5
+ WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use a modal window when the user must complete or dismiss a task before
10
+ continuing. Use a floating window for a parallel work surface that does not
11
+ block the rest of the page.
12
+
13
+ ## Props
14
+
15
+ | PropName | Type | Required | Default | Description |
16
+ |---|---|---:|---:|---|
17
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
18
+ | `ariaLabel` | `string` | no | `"Window"` | Accessible dialog name. |
19
+ | `portal` | `Portal` | no | - | Portal configuration. |
20
+ | `portalElement` | `HTMLElement` | no | - | Portal container. |
21
+ | `entryId` | `string` | no | - | Portal entry ID. |
22
+ | `manager` | `WindowManager` | no | - | Application manager for shared state, visibility, focus, bounds, and taskbar behavior. |
23
+ | `getTaskBarButtonBounds` | `(id) => WindowBounds \| undefined` | no | - | Supplies the matching taskbar button bounds for minimize and restore animation. |
24
+ | `mode` | `'modal' \| 'floating'` | no | `'modal'` | Dialog modality and movement behavior. |
25
+ | `moveCursor` | `boolean` | no | `true` | Enables the move cursor in floating mode. |
26
+ | `resizable` | `boolean` | no | - | Enables resize handles and `is-resizable`. |
27
+ | `resizeCursor` | `boolean` | no | `true` | Enables resize cursors. |
28
+ | `width` | `number` | no | - | Initial width in pixels. |
29
+ | `height` | `number` | no | - | Initial height in pixels. |
30
+ | `animation` | `Animation` | no | `zoom` | Entry and exit animation. |
31
+ | `closeOnBackdropClick` | `boolean` | no | `true` | Dismisses a modal from the backdrop. |
32
+ | `closeOnEscape` | `boolean` | no | `true` | Dismisses on `Escape` unless the consumer prevents the event. |
33
+ | `onClose` | `() => void` | no | noop | Reports completed close. |
34
+ | `onKeyDown` | `(event) => void` | no | noop | Observes keyboard input. |
35
+
36
+ ## Window context
37
+
38
+ Projected window content can read `WindowContext` to control its own window.
39
+ `manager` in this context is the current `ManagedWindowState`, not the manager
40
+ instance. The application-owned `WindowManager` remains available to the
41
+ portal/controller through the `manager` prop.
42
+
43
+ | Member | Description |
44
+ |---|---|
45
+ | `close(options?)` | Requests closure through the portal lifecycle. |
46
+ | `bringToFront(activate?)` | Reveals and brings the window forward. Pass `false` while dragging or resizing to preserve its active position. |
47
+ | `hide()` / `show()` | Hides or reveals the window. Managed windows update the manager's reactive `isHidden` state; unmanaged windows use local controller state. |
48
+ | `minimize()` | Reveals and minimizes the managed window. Returns `false` without a manager. |
49
+ | `maximize()` | Toggles a managed window between the portal workspace bounds and its previous bounds. Returns `false` without a manager or workspace. |
50
+ | `isActive` | Indicates whether this window is active in its manager. |
51
+ | `manager` | Current immutable `ManagedWindowState`, including `status`, `bounds`, and `isHidden`. |
52
+
53
+ The manager is optional. Existing modal and floating windows continue to work
54
+ without one; manager-specific commands then return `false`.
55
+
56
+ Managed floating windows apply the current `manager.bounds` to their rendered
57
+ position and size. The portal host should therefore cover the intended
58
+ workspace (for example, an absolutely positioned full-layout anchor); a
59
+ zero-sized host causes windows to be measured from the top-left corner.
60
+
61
+ Modal renderers must contain focus, restore prior focus, expose `aria-modal`,
62
+ and include an obvious close control in the projected content. Floating
63
+ renderers must not begin a drag from interactive descendants; use
64
+ `data-window-no-drag` for custom interactive regions.
65
+
66
+ ## XTS rendering contract
67
+
68
+ ```xts
69
+ import {
70
+ setContext,
71
+ useBeforeUnmount,
72
+ useMediator,
73
+ useMounted,
74
+ useRef,
75
+ withDefaultProps,
76
+ } from '@pastweb/x';
77
+ import {
78
+ MODE,
79
+ WINDOW_CONTEXT_KEY,
80
+ windowControllerMediator,
81
+ } from '@pastweb/components-core/WindowController';
82
+ import { DEFAULT_PROPS } from './constants';
83
+
84
+ export function WindowController(props: WindowControllerProps) {
85
+ const p = withDefaultProps<WindowControllerDefaultProps>(props, DEFAULT_PROPS);
86
+ const m = useMediator<WindowControllerMediator>(windowControllerMediator, p);
87
+ const s = m.state;
88
+ const c = m.getContext();
89
+ const windowReference = useRef(null);
90
+
91
+ setContext(WINDOW_CONTEXT_KEY, c);
92
+ useMounted(() => {
93
+ if (windowReference.value) m.onMounted(windowReference.value);
94
+ });
95
+ useBeforeUnmount(m.useBeforeUnmount);
96
+
97
+ const className = p.cls(
98
+ p.mode === MODE.floating ? 'window' : 'modal',
99
+ {
100
+ [`${p.animation.name}-in`]: s.isOpen && !s.manager,
101
+ [`${p.animation.name}-out`]: !s.isOpen,
102
+ 'is-managed': !!s.manager,
103
+ 'is-hide': s.isHidden || s.manager?.isHidden,
104
+ 'is-maximised': s.manager?.status === 'maximized',
105
+ 'is-minimised': s.manager?.status === 'minimized',
106
+ 'is-resizable': p.resizable,
107
+ },
108
+ );
109
+
110
+ return p.mode === MODE.floating ? (
111
+ <div
112
+ #ref={windowReference}
113
+ class={className}
114
+ style={m.getAnimationStyle()}
115
+ role='dialog'
116
+ aria-label={p.ariaLabel}
117
+ tabIndex={-1}
118
+ @mousedown={() => c.bringToFront()}
119
+ @keydown={m.onKeyDown}
120
+ >
121
+ <slot />
122
+ </div>
123
+ ) : (
124
+ <div class={p.cls('WindowController', { 'is-hide': s.isHidden || s.manager?.isHidden })}>
125
+ <button
126
+ type='button'
127
+ class={p.cls('backdrop')}
128
+ tabIndex={-1}
129
+ aria-hidden='true'
130
+ @click={m.onBackdropClick}
131
+ />
132
+ <div
133
+ #ref={windowReference}
134
+ class={className}
135
+ style={m.getAnimationStyle()}
136
+ role='dialog'
137
+ aria-modal='true'
138
+ aria-label={p.ariaLabel}
139
+ tabIndex={-1}
140
+ @mousedown={() => c.bringToFront()}
141
+ @keydown={m.onKeyDown}
142
+ >
143
+ <slot />
144
+ </div>
145
+ </div>
146
+ );
147
+ }
148
+ ```
149
+
150
+ The controller publishes `WindowContext` before projected content renders, so a
151
+ Panel or custom child can call `close`, `hide`, `show`, `minimize`, `maximize`,
152
+ or `bringToFront` through the framework context hook.
153
+
154
+ ## Keyboard interaction
155
+
156
+ - Focus moves to the dialog when mounted.
157
+ - `Escape` closes when `closeOnEscape` is enabled.
158
+ - Modal windows keep `Tab` and `Shift+Tab` inside their focusable content.
159
+ - Focus returns to the previously focused element when unmounting.
160
+
161
+ ## Accessibility notes
162
+
163
+ - The web renderer exposes a named `dialog`. `aria-modal` is set only in modal
164
+ mode.
165
+ - The backdrop is a native `button` excluded from the tab order and
166
+ accessibility tree.
167
+ - Dragging ignores buttons, links, form controls, and `[data-window-no-drag]`.
168
+ - Shared web styles keep a visible `:focus-visible` indicator and disable
169
+ non-essential motion under `prefers-reduced-motion`.
170
+
171
+ ## Known limitations
172
+
173
+ - Nested windows require distinct labels so dialog announcements remain
174
+ understandable.
175
+ - A modal window still depends on its content to provide a visible close
176
+ control.
177
+ - Automated accessibility checks do not by themselves establish WCAG
178
+ conformance.
179
+
180
+ ## Native renderer mapping
181
+
182
+ NativeScript maps the window to an accessible modal or floating native
183
+ container, uses native focus containment and restoration, and maps back-button
184
+ dismissal to the same close intent.
185
+
186
+ ## Standards
187
+
188
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
189
+ - WAI-ARIA Dialog (Modal) pattern: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
190
+ - GOV.UK components: https://design-system.service.gov.uk/components/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pastweb/components-core",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "A components tools package.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,7 +15,9 @@
15
15
  "author": "Domenico Pasto",
16
16
  "funding": "https://github.com/sponsors/pastweb",
17
17
  "files": [
18
- "./dist"
18
+ "./dist",
19
+ "README.md",
20
+ "LICENSE"
19
21
  ],
20
22
  "main": "./dist/index.cjs",
21
23
  "module": "./dist/index.mjs",
@@ -1824,6 +1826,7 @@
1824
1826
  "require": "./dist/WindowController/windowController.mediator.cjs"
1825
1827
  },
1826
1828
  "./package.json": "./package.json",
1829
+ "./*/README.md": "./dist/*/README.md",
1827
1830
  "./*.scss": {
1828
1831
  "import": "./dist/*.scss",
1829
1832
  "require": "./dist/*.scss",